From bf7746980fb7505d6f046a0fc0f1d0461a541689 Mon Sep 17 00:00:00 2001 From: Wyatt Lowery Date: Fri, 23 Oct 2026 06:06:51 -0500 Subject: [PATCH 01/14] experimental changes --- .changeset/drawing-pages.md | 18 + benchmark/README.md | 6 +- ...dia-geforce-rtx-2080-super-chrome-156.json | 1050 +++++++------- benchmark/baselines/work.json | 1234 ++++++++++++----- benchmark/judge.ts | 2 +- benchmark/page/scenarios.ts | 63 +- benchmark/page/stage.ts | 2 +- crates/latkit-js/src/lib.rs | 12 + crates/latkit/Cargo.toml | 2 + crates/latkit/shaders/compute/bounds.wesl | 17 +- crates/latkit/shaders/compute/candidates.wesl | 13 +- crates/latkit/shaders/compute/evaluate.wesl | 79 +- crates/latkit/shaders/compute/freeze.wesl | 4 +- crates/latkit/shaders/compute/labels.wesl | 35 +- crates/latkit/shaders/compute/lists.wesl | 46 +- crates/latkit/shaders/compute/probe.wesl | 59 +- crates/latkit/shaders/draw/glyphs.wesl | 19 +- crates/latkit/shaders/draw/points.wesl | 8 +- crates/latkit/shaders/draw/strokes.wesl | 7 +- crates/latkit/shaders/draw/triangles.wesl | 10 +- crates/latkit/shaders/lib/drawn.wesl | 37 + crates/latkit/shaders/lib/focus.wesl | 5 +- crates/latkit/shaders/lib/label.wesl | 16 +- crates/latkit/shaders/lib/path.wesl | 57 +- crates/latkit/shaders/lib/points.wesl | 10 + crates/latkit/shaders/lib/read.wesl | 45 +- crates/latkit/shaders/lib/trace.wesl | 21 +- crates/latkit/shaders/lib/triangles.wesl | 21 +- crates/latkit/shaders/lib/types.wesl | 45 +- crates/latkit/src/data/bits.rs | 8 - crates/latkit/src/data/column.rs | 100 +- crates/latkit/src/data/dataset.rs | 14 - crates/latkit/src/data/ids.rs | 42 +- crates/latkit/src/data/mod.rs | 1 - crates/latkit/src/data/name.rs | 4 + crates/latkit/src/data/samples.rs | 17 - crates/latkit/src/data/table.rs | 61 +- crates/latkit/src/data/values.rs | 28 +- crates/latkit/src/derive/lanes.rs | 20 +- crates/latkit/src/derive/mod.rs | 35 +- crates/latkit/src/derive/order.rs | 223 +++ crates/latkit/src/derive/pyramid.rs | 526 ++++--- crates/latkit/src/derive/shapes.rs | 470 ++++++- crates/latkit/src/derive/sight.rs | 27 +- crates/latkit/src/derive/tiled.rs | 1174 +++++----------- crates/latkit/src/derive/tiles/vector.rs | 4 +- crates/latkit/src/expr/compile.rs | 5 +- crates/latkit/src/gpu/arena.rs | 44 +- crates/latkit/src/gpu/dictionary.rs | 8 +- crates/latkit/src/gpu/mod.rs | 84 +- crates/latkit/src/gpu/packed.rs | 256 ++++ crates/latkit/src/gpu/pipelines.rs | 7 +- crates/latkit/src/gpu/shaders.rs | 10 +- crates/latkit/src/gpu/work.rs | 3 + crates/latkit/src/host/browser.rs | 55 +- crates/latkit/src/host/job.rs | 32 +- crates/latkit/src/host/mod.rs | 2 + crates/latkit/src/host/queue.rs | 67 + crates/latkit/src/sheet/mod.rs | 4 +- crates/latkit/src/view/camera.rs | 30 +- crates/latkit/src/view/drawing.rs | 835 +++++++++++ crates/latkit/src/view/event.rs | 6 - crates/latkit/src/view/fit.rs | 6 +- crates/latkit/src/view/focus.rs | 77 +- crates/latkit/src/view/frame.rs | 307 ++-- crates/latkit/src/view/gestures.rs | 3 +- crates/latkit/src/view/labels.rs | 34 +- crates/latkit/src/view/lists.rs | 49 +- crates/latkit/src/view/marks.rs | 217 ++- crates/latkit/src/view/mod.rs | 57 +- crates/latkit/src/view/passes.rs | 153 +- crates/latkit/src/view/plan.rs | 31 +- crates/latkit/src/view/probe.rs | 210 ++- crates/latkit/src/view/program.rs | 29 +- crates/latkit/src/view/query.rs | 108 +- crates/latkit/src/view/resources.rs | 5 - crates/latkit/src/view/rig.rs | 7 +- crates/latkit/src/view/rows.rs | 8 +- crates/latkit/src/view/sync.rs | 846 ++++++++--- crates/latkit/src/view/transition.rs | 140 +- crates/latkit/src/view/uniforms.rs | 4 + crates/latkit/src/view/variants.rs | 4 + crates/latkit/src/web.rs | 12 +- crates/latkit/tests/gestures.rs | 22 + crates/latkit/tests/picks.rs | 22 + crates/latkit/tests/scale.rs | 374 ++++- docs/_static/performance.csv | 28 +- docs/guide.md | 15 +- docs/performance.md | 8 +- docs/reference/api.md | 4 + docs/reference/types.md | 11 +- npm/examples/src/examples/networks/index.ts | 2 +- npm/schema.json | 28 +- npm/src/outputs.ts | 11 +- npm/src/runtime.ts | 10 + 95 files changed, 6951 insertions(+), 3036 deletions(-) create mode 100644 .changeset/drawing-pages.md create mode 100644 crates/latkit/shaders/lib/drawn.wesl create mode 100644 crates/latkit/src/derive/order.rs create mode 100644 crates/latkit/src/gpu/packed.rs create mode 100644 crates/latkit/src/host/queue.rs create mode 100644 crates/latkit/src/view/drawing.rs diff --git a/.changeset/drawing-pages.md b/.changeset/drawing-pages.md new file mode 100644 index 00000000..8cfbfefa --- /dev/null +++ b/.changeset/drawing-pages.md @@ -0,0 +1,18 @@ +--- +'latkit': minor +--- + +A table too large to draw whole draws pages of its order, which keep their place on the GPU as the camera moves: coming back where the camera was packs, uploads, and evaluates nothing, and what the view derives follows the data, never the camera. + +- Added: `Gpu.resident` and `Gpu.uploading` (`Gpu::resident()` and `Gpu::uploading()`): bytes of data on the GPU, and waiting to upload, for every view of it. +- Added: `Work.packed`, the rows packed into pages of the order a table drawn in part draws in: each page once, the first time a view draws it, and again only where a write changed its rows. +- Removed: `Stats.uploading`, `Stats.resident`, and `Stats.budget`; read them from the GPU (`gpu.uploading`, `gpu.resident`, `gpu.budget`). +- Changed: a table drawn in part holds whole pages of its order, each in a place of its own, and a page it no longer draws stays held while the plan leaves room, so drawing it again costs nothing. +- Changed: appending to a table drawn through tiles no longer thins it, and costs a page: rows appended draw after the rows its tiles were made from, which are made again once rows appended since are an eighth as many, or a row before them changes. +- Changed: lanes, the stars of nets, joins, and fitted domains read the data, not the rows drawn: they no longer change as the camera moves, and a fitted domain spans every row of its table. Areas drawn in part are triangulated a page at a time, the first time the page draws. +- Changed: a vertex shows from as far out as the most important edge ending at it, so an edge drawn always shows with its ends; until tiles land, the first look at a table draws the vertices its edges sampled end at first. +- Changed: a hit names the row and the place the frame it answers drew, however the data, the scene, or the camera changed before it landed. +- Changed: a GPU's views draw one frame together: `Budget.uploadPerFrame` bounds what all of them upload in it, and jobs go to a worker only as one frees, those nothing waits for any more dropped first. +- Changed: a GPU decides its antialiasing once, from the screen it serves, so a browser zoom compiles nothing. +- Changed: zooming at the limit a fit sets keeps the center where it was. +- Fixed: on a flat map, a table drawn through tiles draws alike at every latitude, where a band of tiles drew finer than the rest; vector tiles split alike too. diff --git a/benchmark/README.md b/benchmark/README.md index 30e9801f..d3c75832 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -18,9 +18,9 @@ A run fails on: (`late`), or resident data over its budget; - a phase growing with size faster than its order; - more work than `baselines/work.json`: exactly for what the data sets (`uploaded`, `evaluated`, - `jobs`, `pipelines`, `late`, `resident`) in a phase that acts once, and within 2× for what moves - with the frame a job or a fetch lands in (`written`, `readbacks`, `texts`, every count of a phase - acting each frame, and all of a scenario fed by fetches); + `packed`, `jobs`, `pipelines`, `late`, `resident`) in a phase that acts once, and within 2× for + what moves with the frame a job or a fetch lands in (`written`, `readbacks`, `texts`, every count + of a phase acting each frame, and all of a scenario fed by fetches); - in a full sweep, on the machine `baselines/.json` was taken on, a phase over 1.25× slower, and for a phase acting once, later by over a timer tick (16 ms). diff --git a/benchmark/baselines/nvidia-turing-nvidia-geforce-rtx-2080-super-chrome-156.json b/benchmark/baselines/nvidia-turing-nvidia-geforce-rtx-2080-super-chrome-156.json index e1c98524..0e5af80d 100644 --- a/benchmark/baselines/nvidia-turing-nvidia-geforce-rtx-2080-super-chrome-156.json +++ b/benchmark/baselines/nvidia-turing-nvidia-geforce-rtx-2080-super-chrome-156.json @@ -4,535 +4,615 @@ "start": { "10000": { "init": { - "time": 280, + "time": 255.5, "gpu": 0 }, "data": { - "time": 20.799999952316284, + "time": 19.399999976158142, "gpu": 0 }, "load": { - "time": 353.30000001192093, - "gpu": 0.284448 + "time": 454.39999997615814, + "gpu": 0 } } }, "points": { "1000": { "init": { - "time": 142.4000000357628, + "time": 109.30000001192093, "gpu": 0 }, "data": { - "time": 14.100000023841858, + "time": 13.899999976158142, "gpu": 0 }, "load": { - "time": 350, - "gpu": 0.119456 + "time": 438.69999998807907, + "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.073792 + "gpu": 0.073728 }, "zoom": { "time": 0.099999994, "gpu": 0.067584 }, + "revisit": { + "time": 3.600000023841858, + "gpu": 0.069632 + }, "restyle": { - "time": 5.5, - "gpu": 0.070016 + "time": 4.900000035762787, + "gpu": 0.071328 }, "one row": { "time": 0.099999994, - "gpu": 0.085568 + "gpu": 0.085664 }, "every row": { - "time": 3.900000035762787, - "gpu": 0.085664 + "time": 3.600000023841858, + "gpu": 0.08576 }, "first pick": { - "time": 9, - "gpu": 0.085824 + "time": 8.400000035762787, + "gpu": 0.085536 }, "pick": { - "time": 8.300000011920929, - "gpu": 0.085728 + "time": 7.099999964237213, + "gpu": 0.085664 }, "hover": { - "time": 13.699999988079071, - "gpu": 0.085856 + "time": 10.5, + "gpu": 0.08576 }, "select": { - "time": 7.400000035762787, - "gpu": 0.085824 + "time": 6, + "gpu": 0.085504 + }, + "append": { + "time": 0.099999994, + "gpu": 0.114688 } }, "10000": { "init": { - "time": 177.70000004768372, + "time": 118.5, "gpu": 0 }, "data": { - "time": 17, + "time": 14.100000023841858, "gpu": 0 }, "load": { - "time": 354.0999999642372, - "gpu": 0.280032 + "time": 448.69999998807907, + "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.145408 + "gpu": 0.145792 }, "zoom": { "time": 0.099999994, - "gpu": 0.085152 + "gpu": 0.088064 + }, + "revisit": { + "time": 3, + "gpu": 0.10752 }, "restyle": { - "time": 9.400000035762787, - "gpu": 0.106496 + "time": 4.800000011920929, + "gpu": 0.1144 }, "one row": { - "time": 0.19999999, - "gpu": 0.168576 + "time": 0.099999994, + "gpu": 0.168928 }, "every row": { - "time": 4.800000011920929, - "gpu": 0.168448 + "time": 4.5, + "gpu": 0.169088 }, "first pick": { - "time": 13.199999988079071, - "gpu": 0.168448 + "time": 8.899999976158142, + "gpu": 0.168736 }, "pick": { - "time": 8.900000035762787, - "gpu": 0.167968 + "time": 6.599999964237213, + "gpu": 0.168544 }, "hover": { - "time": 15.599999964237213, - "gpu": 0.167904 + "time": 11, + "gpu": 0.16848 }, "select": { - "time": 10, - "gpu": 0.167904 + "time": 8.300000011920929, + "gpu": 0.168576 + }, + "append": { + "time": 0.39999998, + "gpu": 0.200288 } }, "100000": { "init": { - "time": 149.5, + "time": 118.60000002384186, "gpu": 0 }, "data": { - "time": 15.900000035762787, + "time": 16.099999964237213, "gpu": 0 }, "load": { - "time": 363.5, - "gpu": 2.904192 + "time": 440.69999998807907, + "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.859616 + "gpu": 0.872448 }, "zoom": { "time": 0.099999994, - "gpu": 0.274432 + "gpu": 0.27968 + }, + "revisit": { + "time": 3, + "gpu": 0.484864 }, "restyle": { - "time": 6.5, - "gpu": 0.479232 + "time": 4.899999976158142, + "gpu": 0.551456 }, "one row": { "time": 0.19999999, - "gpu": 0.988192 + "gpu": 0.999936 }, "every row": { - "time": 8.400000035762787, - "gpu": 0.98784 + "time": 8.100000023841858, + "gpu": 0.999968 }, "first pick": { - "time": 10, - "gpu": 0.98784 + "time": 10.100000023841858, + "gpu": 0.999968 }, "pick": { - "time": 8.800000011920929, - "gpu": 0.987296 + "time": 6.600000023841858, + "gpu": 0.999456 }, "hover": { - "time": 13.599999964237213, - "gpu": 0.987136 + "time": 14.199999988079071, + "gpu": 0.999136 }, "select": { - "time": 11.200000047683716, - "gpu": 0.987136 + "time": 7.899999976158142, + "gpu": 0.998944 + }, + "append": { + "time": 0.7, + "gpu": 1.05792 } }, "1000000": { "init": { - "time": 168.5, + "time": 112.80000001192093, "gpu": 0 }, "data": { - "time": 30, + "time": 31.599999964237213, "gpu": 0 }, "load": { - "time": 664, - "gpu": 1.245824 + "time": 482.60000002384186, + "gpu": 0.657568 }, "pan": { "time": 0.099999994, - "gpu": 0.183392 + "gpu": 0.185344 }, "zoom": { - "time": 0.19999999, - "gpu": 0.497664 + "time": 0.099999994, + "gpu": 0.479232 + }, + "revisit": { + "time": 3.600000023841858, + "gpu": 0.38944 }, "restyle": { - "time": 5.800000011920929, - "gpu": 0.415648 + "time": 6.200000047683716, + "gpu": 0.377792 }, "one row": { - "time": 0.19999999, - "gpu": 0.194976 + "time": 0.099999994, + "gpu": 0.196768 }, "every row": { - "time": 28.399999976158142, - "gpu": 0.195392 + "time": 36.19999998807907, + "gpu": 0.19664 }, "first pick": { - "time": 10.799999952316284, - "gpu": 0.195392 + "time": 9.699999988079071, + "gpu": 0.19664 }, "pick": { - "time": 7.800000011920929, - "gpu": 0.195776 + "time": 7.399999976158142, + "gpu": 0.196768 }, "hover": { - "time": 15.399999976158142, - "gpu": 0.196448 + "time": 13.699999988079071, + "gpu": 0.197088 }, "select": { - "time": 11.300000011920929, - "gpu": 0.196608 + "time": 8.899999976158142, + "gpu": 0.197792 + }, + "append": { + "time": 0.19999999, + "gpu": 0.243968 } }, "2000000": { "init": { - "time": 166.30000001192093, + "time": 108.59999996423721, "gpu": 0 }, "data": { - "time": 87.20000004768372, + "time": 44.90000003576279, "gpu": 0 }, "load": { - "time": 1000.8999999761581, - "gpu": 1.283104 + "time": 746.8999999761581, + "gpu": 1.290688 }, "pan": { "time": 0.099999994, - "gpu": 0.183488 + "gpu": 0.18432 }, "zoom": { "time": 0.19999999, - "gpu": 0.503168 + "gpu": 0.490496 + }, + "revisit": { + "time": 3.699999988079071, + "gpu": 0.397312 }, "restyle": { - "time": 7.5, - "gpu": 0.414432 + "time": 7.100000023841858, + "gpu": 0.376832 }, "one row": { - "time": 0.099999994, - "gpu": 0.19504 + "time": 0.19999999, + "gpu": 0.196608 }, "every row": { - "time": 50.30000001192093, - "gpu": 0.194848 + "time": 56.40000003576279, + "gpu": 0.196608 }, "first pick": { - "time": 12.900000035762787, - "gpu": 0.194944 + "time": 10, + "gpu": 0.196608 }, "pick": { - "time": 9.300000011920929, - "gpu": 0.195232 + "time": 6.599999964237213, + "gpu": 0.196608 }, "hover": { - "time": 14.5, - "gpu": 0.196 + "time": 13.600000023841858, + "gpu": 0.196864 }, "select": { - "time": 11.199999988079071, - "gpu": 0.196064 + "time": 8.5, + "gpu": 0.19696 + }, + "append": { + "time": 0.5, + "gpu": 0.243712 } } }, "grid": { "1000": { "init": { - "time": 142, + "time": 128.10000002384186, "gpu": 0 }, "data": { - "time": 14.699999988079071, + "time": 14.199999988079071, "gpu": 0 }, "load": { - "time": 526.4000000357628, - "gpu": 0.194464 + "time": 510, + "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.121408 + "gpu": 0.122528 }, "zoom": { "time": 0.099999994, - "gpu": 0.12304 + "gpu": 0.124256 + }, + "revisit": { + "time": 3.300000011920929, + "gpu": 0.123456 }, "one row": { "time": 0.099999994, - "gpu": 0.131072 + "gpu": 0.131104 }, "first pick": { - "time": 8.399999976158142, - "gpu": 0.131232 + "time": 9, + "gpu": 0.13136 }, "pick": { - "time": 7.100000023841858, - "gpu": 0.131264 + "time": 5.699999988079071, + "gpu": 0.13136 }, "hover": { - "time": 12, - "gpu": 0.131264 + "time": 11.400000035762787, + "gpu": 0.13136 + }, + "append": { + "time": 0.19999999, + "gpu": 0.208384 } }, "10000": { "init": { - "time": 163.69999998807907, + "time": 133.9000000357628, "gpu": 0 }, "data": { - "time": 16, + "time": 16.100000023841858, "gpu": 0 }, "load": { - "time": 586.6999999880791, - "gpu": 0.256 + "time": 549.8000000119209, + "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.17808 + "gpu": 0.178176 }, "zoom": { "time": 0.099999994, - "gpu": 0.173632 + "gpu": 0.17408 + }, + "revisit": { + "time": 3.100000023841858, + "gpu": 0.176064 }, "one row": { - "time": 0.19999999, - "gpu": 0.187584 + "time": 0.099999994, + "gpu": 0.187456 }, "first pick": { - "time": 10.5, - "gpu": 0.187712 + "time": 8.300000011920929, + "gpu": 0.187488 }, "pick": { - "time": 9.800000011920929, - "gpu": 0.187712 + "time": 6.199999988079071, + "gpu": 0.187808 }, "hover": { - "time": 12.700000047683716, - "gpu": 0.187584 + "time": 11.399999976158142, + "gpu": 0.187328 + }, + "append": { + "time": 0.3, + "gpu": 0.280128 } }, "100000": { "init": { - "time": 158.10000002384186, + "time": 126.69999998807907, "gpu": 0 }, "data": { - "time": 20.19999998807907, + "time": 19.700000047683716, "gpu": 0 }, "load": { - "time": 541.6000000238419, - "gpu": 0.94208 + "time": 514.3000000119209, + "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.6024 + "gpu": 0.601696 }, "zoom": { "time": 0.099999994, "gpu": 0.524288 }, + "revisit": { + "time": 3.400000035762787, + "gpu": 0.556224 + }, "one row": { "time": 0.19999999, - "gpu": 0.636288 + "gpu": 0.634496 }, "first pick": { - "time": 14.300000011920929, - "gpu": 0.636224 + "time": 12.600000023841858, + "gpu": 0.633824 }, "pick": { - "time": 6.699999988079071, - "gpu": 0.636896 + "time": 6.199999988079071, + "gpu": 0.633824 }, "hover": { - "time": 17.69999998807907, - "gpu": 0.638496 + "time": 14.5, + "gpu": 0.63424 + }, + "append": { + "time": 0.7, + "gpu": 0.89696 } }, "1000000": { "init": { - "time": 161.30000001192093, + "time": 131.39999997615814, "gpu": 0 }, "data": { - "time": 63.799999952316284, + "time": 61.200000047683716, "gpu": 0 }, "load": { - "time": 1243.300000011921, - "gpu": 0.734304 + "time": 997.5, + "gpu": 0.778208 }, "pan": { "time": 0.19999999, - "gpu": 0.581312 + "gpu": 0.610304 }, "zoom": { - "time": 0.19999999, - "gpu": 1.359872 + "time": 0.3, + "gpu": 1.240192 + }, + "revisit": { + "time": 7.600000023841858, + "gpu": 1.046208 }, "one row": { "time": 0.3, - "gpu": 0.597408 + "gpu": 0.63632 }, "first pick": { - "time": 14, - "gpu": 0.612096 + "time": 10, + "gpu": 0.63648 }, "pick": { - "time": 7.099999964237213, - "gpu": 0.59712 + "time": 5.800000011920929, + "gpu": 0.63648 }, "hover": { - "time": 17.099999964237213, - "gpu": 0.614208 + "time": 11, + "gpu": 0.639264 + }, + "append": { + "time": 0.5, + "gpu": 0.820704 } }, "2000000": { "init": { - "time": 165, + "time": 149.70000004768372, "gpu": 0 }, "data": { - "time": 108.89999997615814, + "time": 105, "gpu": 0 }, "load": { - "time": 1460.3999999761581, - "gpu": 0.755744 + "time": 1634.8999999761581, + "gpu": 0.797664 }, "pan": { - "time": 0.099999994, - "gpu": 0.590656 + "time": 0.19999999, + "gpu": 0.62384 }, "zoom": { - "time": 0.19999999, - "gpu": 1.616288 + "time": 0.3, + "gpu": 1.315808 + }, + "revisit": { + "time": 6.900000035762787, + "gpu": 1.083584 }, "one row": { - "time": 0.19999999, - "gpu": 0.599648 + "time": 0.3, + "gpu": 0.641984 }, "first pick": { - "time": 16.799999952316284, - "gpu": 0.604096 + "time": 11.900000035762787, + "gpu": 0.88976 }, "pick": { - "time": 6.699999988079071, - "gpu": 0.604096 + "time": 6.399999976158142, + "gpu": 0.89088 }, "hover": { - "time": 16.100000023841858, - "gpu": 0.610304 + "time": 13.699999988079071, + "gpu": 0.896128 + }, + "append": { + "time": 0.39999998, + "gpu": 1.178016 } } }, "labels": { "10000": { "init": { - "time": 176.0999999642372, + "time": 143.9000000357628, "gpu": 0 }, "data": { - "time": 22, + "time": 18, "gpu": 0 }, "load": { - "time": 568.1999999880791, - "gpu": 0.321536 + "time": 505.9000000357628, + "gpu": 0.296192 }, "pan": { "time": 0.7, - "gpu": 0.32224 + "gpu": 0.33792 }, "zoom": { - "time": 1.2, - "gpu": 0.286304 + "time": 1.1, + "gpu": 0.30176 } }, "100000": { "init": { - "time": 177.60000002384186, + "time": 145.0999999642372, "gpu": 0 }, "data": { - "time": 67.09999996423721, + "time": 63.30000001192093, "gpu": 0 }, "load": { - "time": 621.0999999642372, - "gpu": 1.501248 + "time": 651.1999999880791, + "gpu": 1.483008 }, "pan": { - "time": 0.79999995, - "gpu": 1.465056 + "time": 1, + "gpu": 1.448224 }, "zoom": { - "time": 1.5, - "gpu": 0.689888 + "time": 1.5999999, + "gpu": 0.719584 } }, "1000000": { "init": { - "time": 164.30000001192093, + "time": 131.10000002384186, "gpu": 0 }, "data": { - "time": 510.19999998807907, + "time": 514.4000000357628, "gpu": 0 }, "load": { - "time": 780.6000000238419, - "gpu": 0.681568 + "time": 784, + "gpu": 0.687072 }, "pan": { - "time": 1, - "gpu": 0.429344 + "time": 1.2, + "gpu": 0.446272 }, "zoom": { "time": 1.7, - "gpu": 1.60848 + "gpu": 1.067584 } } }, "lanes": { "100": { "init": { - "time": 161.10000002384186, + "time": 151.60000002384186, "gpu": 0 }, "data": { @@ -540,686 +620,686 @@ "gpu": 0 }, "load": { - "time": 535.3999999761581, - "gpu": 0.144448 + "time": 509.5, + "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.080704 + "gpu": 0.081184 }, "first pick": { - "time": 10.400000035762787, - "gpu": 0.080768 + "time": 10.800000011920929, + "gpu": 0.08128 }, "pick": { - "time": 5.699999988079071, - "gpu": 0.080928 + "time": 9.399999976158142, + "gpu": 0.08128 } }, "1000": { "init": { - "time": 157, + "time": 134.5999999642372, "gpu": 0 }, "data": { - "time": 13.799999952316284, + "time": 14.899999976158142, "gpu": 0 }, "load": { - "time": 532.8000000119209, - "gpu": 0.18368 + "time": 569.1000000238419, + "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.16784 + "gpu": 0.16976 }, "first pick": { - "time": 10.699999988079071, - "gpu": 0.166304 + "time": 10.5, + "gpu": 0.168608 }, "pick": { - "time": 6.5, - "gpu": 0.167648 + "time": 6.399999976158142, + "gpu": 0.169376 } }, "10000": { "init": { - "time": 150.4000000357628, + "time": 125.60000002384186, "gpu": 0 }, "data": { - "time": 14.799999952316284, + "time": 14.899999976158142, "gpu": 0 }, "load": { - "time": 534.7999999523163, - "gpu": 0.388384 + "time": 517.4000000357628, + "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 2.306144 + "gpu": 1.91296 }, "first pick": { - "time": 23.5, - "gpu": 2.270912 + "time": 15.900000035762787, + "gpu": 1.880064 }, "pick": { - "time": 15.5, - "gpu": 2.29008 + "time": 13.399999976158142, + "gpu": 1.924768 } }, "100000": { "init": { - "time": 190.30000001192093, + "time": 124.19999998807907, "gpu": 0 }, "data": { - "time": 20.19999998807907, + "time": 16.099999964237213, "gpu": 0 }, "load": { - "time": 816.3000000119209, - "gpu": 18.844288 + "time": 496.5, + "gpu": 0 }, "pan": { - "time": 0.19999999, - "gpu": 19.761152 + "time": 0.099999994, + "gpu": 20.007263 }, "first pick": { - "time": 76.10000002384186, - "gpu": 19.828447 + "time": 78.59999996423721, + "gpu": 19.996544 }, "pick": { - "time": 70.80000001192093, - "gpu": 19.762688 + "time": 72.59999996423721, + "gpu": 20.10176 } } }, "areas": { "100": { "init": { - "time": 187.4000000357628, + "time": 135.9000000357628, "gpu": 0 }, "data": { - "time": 16.100000023841858, + "time": 15.300000011920929, "gpu": 0 }, "load": { - "time": 493.9000000357628, - "gpu": 0.32768 + "time": 464.39999997615814, + "gpu": 0 }, "pan": { - "time": 0.19999999, - "gpu": 0.141408 + "time": 0.099999994, + "gpu": 0.14144 }, "zoom": { "time": 0.099999994, - "gpu": 0.139264 + "gpu": 0.139392 }, "one row": { - "time": 0.19999999, - "gpu": 0.296896 + "time": 0.099999994, + "gpu": 0.296448 }, "first pick": { - "time": 8.5, - "gpu": 0.296736 + "time": 9.300000011920929, + "gpu": 0.296672 }, "pick": { - "time": 8.800000011920929, - "gpu": 0.296736 + "time": 7.699999988079071, + "gpu": 0.296544 } }, "1000": { "init": { - "time": 148.0999999642372, + "time": 124.69999998807907, "gpu": 0 }, "data": { - "time": 18.19999998807907, + "time": 17.400000035762787, "gpu": 0 }, "load": { - "time": 396.0999999642372, - "gpu": 0.13952 + "time": 450.9000000357628, + "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.219424 + "gpu": 0.219808 }, "zoom": { "time": 0.099999994, - "gpu": 0.217728 + "gpu": 0.218048 }, "one row": { - "time": 0.19999999, - "gpu": 0.359136 + "time": 0.099999994, + "gpu": 0.3584 }, "first pick": { - "time": 8.300000011920929, - "gpu": 0.358944 + "time": 7, + "gpu": 0.3584 }, "pick": { "time": 7.300000011920929, - "gpu": 0.35872 + "gpu": 0.3584 } }, "10000": { "init": { - "time": 159.79999995231628, + "time": 123.70000004768372, "gpu": 0 }, "data": { - "time": 29.899999976158142, + "time": 31, "gpu": 0 }, "load": { - "time": 474, - "gpu": 0.1344 + "time": 511.5, + "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.604192 + "gpu": 0.604384 }, "zoom": { "time": 0.099999994, - "gpu": 0.602112 + "gpu": 0.602752 }, "one row": { "time": 0.099999994, - "gpu": 0.728768 + "gpu": 0.728096 }, "first pick": { - "time": 7.399999976158142, - "gpu": 0.72816 + "time": 8.899999976158142, + "gpu": 0.727712 }, "pick": { - "time": 6.899999976158142, - "gpu": 0.728128 + "time": 6.800000011920929, + "gpu": 0.727648 } }, "30000": { "init": { - "time": 150.19999998807907, + "time": 128.5999999642372, "gpu": 0 }, "data": { - "time": 59, + "time": 56.5, "gpu": 0 }, "load": { - "time": 779.6000000238419, - "gpu": 0.096448 + "time": 704.5, + "gpu": 0.096544 }, "pan": { "time": 0.099999994, - "gpu": 1.21536 + "gpu": 1.216128 }, "zoom": { "time": 0.099999994, - "gpu": 1.213888 + "gpu": 1.213248 }, "one row": { "time": 0.099999994, - "gpu": 1.741728 + "gpu": 1.652288 }, "first pick": { - "time": 7.100000023841858, - "gpu": 1.61152 + "time": 6.800000011920929, + "gpu": 1.645248 }, "pick": { - "time": 6.200000047683716, - "gpu": 1.6112 + "time": 6.099999964237213, + "gpu": 1.643008 } } }, "tiles": { "16": { "init": { - "time": 143.39999997615814, + "time": 134.60000002384186, "gpu": 0 }, "data": { - "time": 11.100000023841858, + "time": 12.5, "gpu": 0 }, "load": { - "time": 459.5, - "gpu": 0.121216 + "time": 581.1000000238419, + "gpu": 0 }, "pan": { - "time": 0.19999999, - "gpu": 0.1232 + "time": 0.3, + "gpu": 0.122976 }, "zoom": { - "time": 0.19999999, - "gpu": 0.118432 + "time": 0.3, + "gpu": 0.11824 } }, "256": { "init": { - "time": 144.5, + "time": 129.30000001192093, "gpu": 0 }, "data": { - "time": 11.800000011920929, + "time": 11.899999976158142, "gpu": 0 }, "load": { - "time": 441.4000000357628, - "gpu": 0.1216 + "time": 583.3999999761581, + "gpu": 0 }, "pan": { - "time": 0.099999994, - "gpu": 0.13312 + "time": 0.19999999, + "gpu": 0.132576 }, "zoom": { - "time": 0.19999999, - "gpu": 0.12288 + "time": 0.3, + "gpu": 0.123392 } }, "4096": { "init": { - "time": 150.5999999642372, + "time": 123.19999998807907, "gpu": 0 }, "data": { - "time": 11.100000023841858, + "time": 12.100000023841858, "gpu": 0 }, "load": { - "time": 512.3000000119209, - "gpu": 0.122688 + "time": 549.8000000119209, + "gpu": 0 }, "pan": { - "time": 0.19999999, - "gpu": 0.171744 + "time": 0.3, + "gpu": 0.17152 }, "zoom": { - "time": 2.8, - "gpu": 0.155648 + "time": 0.3, + "gpu": 0.135168 } } }, "diagram": { "100": { "init": { - "time": 150.30000001192093, + "time": 143.29999995231628, "gpu": 0 }, "data": { - "time": 15.899999976158142, + "time": 14.199999988079071, "gpu": 0 }, "load": { - "time": 498.9000000357628, - "gpu": 1.456416 + "time": 497.19999998807907, + "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.083968 + "gpu": 0.08192 }, "edit": { "time": 0.39999998, - "gpu": 0.122656 + "gpu": 0.1208 }, "arrange": { - "time": 12, - "gpu": 0.12288 + "time": 12.400000035762787, + "gpu": 0.121248 } }, "1000": { "init": { - "time": 155.39999997615814, + "time": 139.0999999642372, "gpu": 0 }, "data": { - "time": 15.5, + "time": 15.699999988079071, "gpu": 0 }, "load": { - "time": 503.4000000357628, - "gpu": 1.454784 + "time": 506.19999998807907, + "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.096448 + "gpu": 0.092576 }, "edit": { "time": 0.6, - "gpu": 0.142688 + "gpu": 0.137376 }, "arrange": { - "time": 16.19999998807907, - "gpu": 0.143424 + "time": 14.900000035762787, + "gpu": 0.138208 } }, "10000": { "init": { - "time": 147.10000002384186, + "time": 141.30000001192093, "gpu": 0 }, "data": { - "time": 20.80000001192093, + "time": 20.5, "gpu": 0 }, "load": { - "time": 581.3999999761581, - "gpu": 1.478176 + "time": 593.6999999880791, + "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.216224 + "gpu": 0.192032 }, "edit": { - "time": 3.4, - "gpu": 0.297088 + "time": 2.8999999, + "gpu": 0.264256 }, "arrange": { - "time": 58.299999952316284, - "gpu": 0.297824 + "time": 56.40000003576279, + "gpu": 0.264608 } } }, "traces": { "10": { "init": { - "time": 148.5, + "time": 130.69999998807907, "gpu": 0 }, "data": { - "time": 17.30000001192093, + "time": 17.19999998807907, "gpu": 0 }, "load": { - "time": 432.2999999523163, - "gpu": 1.408352 + "time": 572.7999999523163, + "gpu": 0 }, "stream": { "time": 0.099999994, - "gpu": 0.097632 + "gpu": 0.096256 }, "scrub": { "time": 0.099999994, - "gpu": 0.077824 + "gpu": 0.075776 }, "narrow": { "time": 0.099999994, - "gpu": 0.090528 + "gpu": 0.088064 }, "hover": { - "time": 11.699999988079071, - "gpu": 0.08 + "time": 9.800000011920929, + "gpu": 0.079776 } }, "100": { "init": { - "time": 147.30000001192093, + "time": 130.10000002384186, "gpu": 0 }, "data": { - "time": 18.899999976158142, + "time": 19.100000023841858, "gpu": 0 }, "load": { - "time": 462.89999997615814, - "gpu": 1.466368 + "time": 518.5, + "gpu": 0 }, "stream": { "time": 0.099999994, - "gpu": 0.337184 + "gpu": 0.34176 }, "scrub": { "time": 0.099999994, - "gpu": 0.279744 + "gpu": 0.28304 }, "narrow": { "time": 0.099999994, - "gpu": 0.24784 + "gpu": 0.256 }, "hover": { - "time": 12.5, - "gpu": 0.136704 + "time": 13.5, + "gpu": 0.135168 } }, "1000": { "init": { - "time": 177.39999997615814, + "time": 135.10000002384186, "gpu": 0 }, "data": { - "time": 37.69999998807907, + "time": 36.89999997615814, "gpu": 0 }, "load": { - "time": 460, - "gpu": 1.582336 + "time": 544.8999999761581, + "gpu": 0 }, "stream": { "time": 0.099999994, - "gpu": 2.24256 + "gpu": 2.28896 }, "scrub": { "time": 0.099999994, - "gpu": 1.92512 + "gpu": 1.968192 }, "narrow": { "time": 0.099999994, - "gpu": 1.506816 + "gpu": 1.529856 }, "hover": { - "time": 12.799999952316284, - "gpu": 0.538624 + "time": 13, + "gpu": 0.548384 } }, "10000": { "init": { - "time": 187.70000004768372, + "time": 139.60000002384186, "gpu": 0 }, "data": { - "time": 233.0999999642372, + "time": 240.69999998807907, "gpu": 0 }, "load": { - "time": 547.7999999523163, - "gpu": 3.033696 + "time": 561, + "gpu": 0 }, "stream": { "time": 0.3, - "gpu": 23.631264 + "gpu": 23.959265 }, "scrub": { "time": 0.19999999, - "gpu": 20.68048 + "gpu": 20.939264 }, "narrow": { "time": 0.19999999, - "gpu": 15.118592 + "gpu": 15.726592 }, "hover": { - "time": 19.600000023841858, - "gpu": 5.16896 + "time": 22.100000023841858, + "gpu": 5.122048 } } }, "history": { "1000": { "init": { - "time": 147.5999999642372, + "time": 126.19999998807907, "gpu": 0 }, "data": { - "time": 16.399999976158142, + "time": 18.400000035762787, "gpu": 0 }, "load": { - "time": 437, - "gpu": 1.41152 + "time": 530.1000000238419, + "gpu": 0 }, "scrub": { "time": 0.099999994, - "gpu": 0.058112 + "gpu": 0.057472 }, "narrow": { - "time": 0.099999994, - "gpu": 0.074688 + "time": 0.19999999, + "gpu": 0.074752 } }, "10000": { "init": { - "time": 156.19999998807907, + "time": 124.89999997615814, "gpu": 0 }, "data": { - "time": 22.899999976158142, + "time": 23.100000023841858, "gpu": 0 }, "load": { - "time": 435.7000000476837, - "gpu": 1.40288 + "time": 524, + "gpu": 0 }, "scrub": { "time": 0.099999994, - "gpu": 0.086016 + "gpu": 0.086304 }, "narrow": { "time": 0.19999999, - "gpu": 0.092768 + "gpu": 0.094048 } }, "100000": { "init": { - "time": 161.80000001192093, + "time": 133.30000001192093, "gpu": 0 }, "data": { - "time": 60.80000001192093, + "time": 62.30000001192093, "gpu": 0 }, "load": { - "time": 442.89999997615814, - "gpu": 1.410528 + "time": 558.8999999761581, + "gpu": 0 }, "scrub": { - "time": 0.099999994, - "gpu": 0.286688 + "time": 0.19999999, + "gpu": 0.293664 }, "narrow": { - "time": 0.19999999, - "gpu": 0.215744 + "time": 0.099999994, + "gpu": 0.220736 } }, "1000000": { "init": { - "time": 159.89999997615814, + "time": 131.19999998807907, "gpu": 0 }, "data": { - "time": 394.4000000357628, + "time": 390.80000001192093, "gpu": 0 }, "load": { - "time": 440.89999997615814, - "gpu": 1.403584 + "time": 534.5, + "gpu": 0 }, "scrub": { "time": 0.099999994, - "gpu": 0.405504 + "gpu": 0.417024 }, "narrow": { - "time": 0.19999999, - "gpu": 0.288896 + "time": 0.099999994, + "gpu": 0.296512 } } }, "transition": { "1000": { "init": { - "time": 149.29999995231628, + "time": 124.60000002384186, "gpu": 0 }, "data": { - "time": 14.800000011920929, + "time": 13.199999988079071, "gpu": 0 }, "load": { - "time": 313.89999997615814, - "gpu": 0.124672 + "time": 446.7999999523163, + "gpu": 0 }, "move": { "time": 0.099999994, - "gpu": 0.083392 + "gpu": 0.083968 } }, "10000": { "init": { - "time": 147.19999998807907, + "time": 130.69999998807907, "gpu": 0 }, "data": { - "time": 14.599999964237213, + "time": 14.5, "gpu": 0 }, "load": { - "time": 315, - "gpu": 0.279616 + "time": 448.89999997615814, + "gpu": 0 }, "move": { "time": 0.099999994, - "gpu": 0.172128 + "gpu": 0.17408 } }, "100000": { "init": { - "time": 161.29999995231628, + "time": 141, "gpu": 0 }, "data": { - "time": 16.69999998807907, + "time": 15.899999976158142, "gpu": 0 }, "load": { - "time": 334.89999997615814, - "gpu": 1.939008 + "time": 436, + "gpu": 0 }, "move": { "time": 0.099999994, - "gpu": 1.233024 + "gpu": 1.267712 } }, "1000000": { "init": { - "time": 148.4000000357628, + "time": 136.30000001192093, "gpu": 0 }, "data": { - "time": 31.19999998807907, + "time": 31.69999998807907, "gpu": 0 }, "load": { - "time": 816.6999999880791, - "gpu": 0.776192 + "time": 687.0999999642372, + "gpu": 0.790816 }, "move": { "time": 0.099999994, - "gpu": 0.223104 + "gpu": 0.226944 } } }, "views": { "1": { "init": { - "time": 159.5, + "time": 131.60000002384186, "gpu": 0 }, "data": { - "time": 23.600000023841858, + "time": 19.5, "gpu": 0 }, "load": { - "time": 544.7999999523163, - "gpu": 1.428128 + "time": 506.2999999523163, + "gpu": 0 }, "pan": { "time": 0.099999994, @@ -1228,216 +1308,216 @@ }, "2": { "init": { - "time": 155.70000004768372, + "time": 135.80000001192093, "gpu": 0 }, "data": { - "time": 21.80000001192093, + "time": 20.5, "gpu": 0 }, "load": { - "time": 559.7999999523163, - "gpu": 2.794816 + "time": 536.8000000119209, + "gpu": 0 }, "pan": { - "time": 0.099999994, - "gpu": 1.4248319999999999 + "time": 0.199999988, + "gpu": 1.502304 } }, "4": { "init": { - "time": 157.5, + "time": 121, "gpu": 0 }, "data": { - "time": 19.899999976158142, + "time": 19.700000047683716, "gpu": 0 }, "load": { - "time": 562.7999999523163, - "gpu": 5.52096 + "time": 522.3999999761581, + "gpu": 0 }, "pan": { - "time": 0.299999982, - "gpu": 2.4928320000000004 + "time": 0.199999988, + "gpu": 2.491552 } }, "8": { "init": { - "time": 167, + "time": 127.09999996423721, "gpu": 0 }, "data": { - "time": 24.900000035762787, + "time": 19.5, "gpu": 0 }, "load": { - "time": 679.1000000238419, - "gpu": 7.603104 + "time": 555.1999999880791, + "gpu": 0 }, "pan": { - "time": 0.49999997199999996, - "gpu": 2.757472 + "time": 0.299999984, + "gpu": 2.767808 } } }, "budget": { "100000": { "init": { - "time": 162.69999998807907, + "time": 131.5, "gpu": 0 }, "data": { - "time": 22, + "time": 20.30000001192093, "gpu": 0 }, "load": { - "time": 779.6000000238419, - "gpu": 1.01232 + "time": 523.8999999761581, + "gpu": 0 }, "pan": { "time": 0.19999999, - "gpu": 0.460448 + "gpu": 0.237632 }, "zoom": { "time": 0.19999999, - "gpu": 0.18672 + "gpu": 0.123616 } }, "1000000": { "init": { - "time": 178.69999998807907, + "time": 132.69999998807907, "gpu": 0 }, "data": { - "time": 63.69999998807907, + "time": 63.10000002384186, "gpu": 0 }, "load": { - "time": 1287.199999988079, - "gpu": 1.517728 + "time": 979.4000000357628, + "gpu": 1.110016 }, "pan": { "time": 0.19999999, - "gpu": 0.590176 + "gpu": 0.249856 }, "zoom": { "time": 0.19999999, - "gpu": 0.22128 + "gpu": 0.126976 } }, "2000000": { "init": { - "time": 164.29999995231628, + "time": 135.5, "gpu": 0 }, "data": { - "time": 123.60000002384186, + "time": 114.30000001192093, "gpu": 0 }, "load": { - "time": 1516.3999999761581, - "gpu": 1.424768 + "time": 1573.1000000238419, + "gpu": 1.368416 }, "pan": { "time": 0.19999999, - "gpu": 0.592096 + "gpu": 0.248288 }, "zoom": { - "time": 0.099999994, - "gpu": 0.221536 + "time": 0.19999999, + "gpu": 0.126976 } } }, "shots": { "518400": { "init": { - "time": 137.79999995231628, + "time": 116, "gpu": 0 }, "data": { - "time": 21.30000001192093, + "time": 20.69999998807907, "gpu": 0 }, "load": { - "time": 567.3000000119209, - "gpu": 2.792736 + "time": 542, + "gpu": 0 }, "image": { - "time": 30.69999998807907, - "gpu": 2.314368 + "time": 26.600000023841858, + "gpu": 2.5392 }, "png": { - "time": 10.5, - "gpu": 2.314368 + "time": 10, + "gpu": 2.3034879999999998 }, "video": { - "time": 247.80000001192093, - "gpu": 2.094336 + "time": 213.4000000357628, + "gpu": 1.238176 }, "held": { - "time": 141.10000002384186, - "gpu": 2.127872 + "time": 154.60000002384186, + "gpu": 1.278848 } }, "2073600": { "init": { - "time": 135.39999997615814, + "time": 119.30000001192093, "gpu": 0 }, "data": { - "time": 20.399999976158142, + "time": 21.30000001192093, "gpu": 0 }, "load": { - "time": 545.8000000119209, - "gpu": 3.086304 + "time": 534.1999999880791, + "gpu": 0 }, "image": { - "time": 46.80000001192093, - "gpu": 2.3137600000000003 + "time": 40.60000002384186, + "gpu": 2.54304 }, "png": { - "time": 24.200000047683716, - "gpu": 2.313088 + "time": 25.19999998807907, + "gpu": 2.303232 }, "video": { - "time": 286.60000002384186, - "gpu": 2.1913600000000004 + "time": 353.69999998807907, + "gpu": 1.282848 }, "held": { - "time": 148.0999999642372, - "gpu": 2.199136 + "time": 167.0999999642372, + "gpu": 1.33088 } }, "8294400": { "init": { - "time": 139.5, + "time": 142.9000000357628, "gpu": 0 }, "data": { - "time": 20.399999976158142, + "time": 23.700000047683716, "gpu": 0 }, "load": { - "time": 543, - "gpu": 2.79392 + "time": 651.6999999880791, + "gpu": 0 }, "image": { - "time": 87, - "gpu": 2.385632 + "time": 71, + "gpu": 0 }, "png": { - "time": 78.60000002384186, - "gpu": 2.385632 + "time": 82.80000001192093, + "gpu": 2.303776 }, "video": { - "time": 646.6999999880791, - "gpu": 2.6850240000000003 + "time": 705, + "gpu": 1.50528 }, "held": { - "time": 191, - "gpu": 2.7155199999999997 + "time": 203.9000000357628, + "gpu": 1.527072 } } } diff --git a/benchmark/baselines/work.json b/benchmark/baselines/work.json index d4a0b5d7..2da4afa0 100644 --- a/benchmark/baselines/work.json +++ b/benchmark/baselines/work.json @@ -5,6 +5,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -16,6 +17,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -25,12 +27,13 @@ }, "load": { "uploaded": 200000, - "written": 4004, + "written": 5228, "evaluated": 10000, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 9, + "pipelines": 27, "late": 0, "resident": 327680 } @@ -42,6 +45,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -53,6 +57,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -62,12 +67,13 @@ }, "load": { "uploaded": 20000, - "written": 4004, + "written": 5220, "evaluated": 1000, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 9, + "pipelines": 27, "late": 0, "resident": 20480 }, @@ -75,6 +81,7 @@ "uploaded": 0, "written": 48640, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -86,6 +93,19 @@ "uploaded": 0, "written": 48640, "evaluated": 0, + "packed": 0, + "readbacks": 0, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 20480 + }, + "revisit": { + "uploaded": 0, + "written": 1216, + "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -97,6 +117,7 @@ "uploaded": 0, "written": 2788, "evaluated": 1000, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -106,8 +127,9 @@ }, "one row": { "uploaded": 160000, - "written": 64160, + "written": 48960, "evaluated": 40000, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -117,8 +139,9 @@ }, "every row": { "uploaded": 4000, - "written": 1604, + "written": 1224, "evaluated": 1000, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -130,6 +153,7 @@ "uploaded": 0, "written": 2440, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -141,6 +165,7 @@ "uploaded": 0, "written": 2440, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -152,6 +177,7 @@ "uploaded": 0, "written": 3752, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, @@ -163,12 +189,25 @@ "uploaded": 0, "written": 3284, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, "resident": 188416 + }, + "append": { + "uploaded": 816400, + "written": 72576, + "evaluated": 40820, + "packed": 0, + "readbacks": 40, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 1335296 } }, "10000": { @@ -176,6 +215,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -187,6 +227,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -196,12 +237,13 @@ }, "load": { "uploaded": 200000, - "written": 4004, + "written": 5228, "evaluated": 10000, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 9, + "pipelines": 27, "late": 0, "resident": 327680 }, @@ -209,6 +251,7 @@ "uploaded": 0, "written": 48640, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -220,6 +263,19 @@ "uploaded": 0, "written": 48640, "evaluated": 0, + "packed": 0, + "readbacks": 0, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 327680 + }, + "revisit": { + "uploaded": 0, + "written": 1216, + "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -229,8 +285,9 @@ }, "restyle": { "uploaded": 0, - "written": 2788, + "written": 2796, "evaluated": 10000, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -240,8 +297,9 @@ }, "one row": { "uploaded": 1600000, - "written": 64160, + "written": 49280, "evaluated": 400000, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -251,8 +309,9 @@ }, "every row": { "uploaded": 40000, - "written": 1604, + "written": 1232, "evaluated": 10000, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -264,6 +323,7 @@ "uploaded": 0, "written": 2440, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -275,6 +335,7 @@ "uploaded": 0, "written": 2440, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -286,6 +347,7 @@ "uploaded": 0, "written": 3752, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, @@ -297,12 +359,25 @@ "uploaded": 0, "written": 3284, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, "resident": 3014656 + }, + "append": { + "uploaded": 8016400, + "written": 72896, + "evaluated": 400820, + "packed": 0, + "readbacks": 40, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 16121856 } }, "100000": { @@ -310,6 +385,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -321,6 +397,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -330,12 +407,13 @@ }, "load": { "uploaded": 2000000, - "written": 4032, + "written": 6556, "evaluated": 100000, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 9, + "pipelines": 27, "late": 0, "resident": 2621440 }, @@ -343,6 +421,7 @@ "uploaded": 0, "written": 48640, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -354,6 +433,19 @@ "uploaded": 0, "written": 48640, "evaluated": 0, + "packed": 0, + "readbacks": 0, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 2621440 + }, + "revisit": { + "uploaded": 0, + "written": 1216, + "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -363,8 +455,9 @@ }, "restyle": { "uploaded": 0, - "written": 2816, + "written": 2908, "evaluated": 100000, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -374,8 +467,9 @@ }, "one row": { "uploaded": 10485760, - "written": 64960, + "written": 51360, "evaluated": 2621440, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -385,8 +479,9 @@ }, "every row": { "uploaded": 400000, - "written": 1628, + "written": 1324, "evaluated": 100000, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -398,6 +493,7 @@ "uploaded": 0, "written": 2440, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -409,6 +505,7 @@ "uploaded": 0, "written": 2440, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -420,6 +517,7 @@ "uploaded": 0, "written": 3752, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, @@ -431,12 +529,25 @@ "uploaded": 0, "written": 3284, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, "resident": 13631488 + }, + "append": { + "uploaded": 27587600, + "written": 74656, + "evaluated": 1379380, + "packed": 0, + "readbacks": 40, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 66060288 } }, "1000000": { @@ -444,6 +555,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -455,6 +567,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -463,20 +576,22 @@ "resident": 0 }, "load": { - "uploaded": 1567520, - "written": 5608, - "evaluated": 78376, + "uploaded": 1638400, + "written": 7376, + "evaluated": 81920, + "packed": 409600, "readbacks": 1, "jobs": 1, "texts": 0, - "pipelines": 9, + "pipelines": 27, "late": 0, "resident": 1638400 }, "pan": { "uploaded": 0, - "written": 48640, + "written": 48752, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -485,92 +600,124 @@ "resident": 1638400 }, "zoom": { - "uploaded": 13309320, - "written": 51652, - "evaluated": 665466, + "uploaded": 3932160, + "written": 62376, + "evaluated": 196608, + "packed": 983040, + "readbacks": 0, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 5570560 + }, + "revisit": { + "uploaded": 0, + "written": 1228, + "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 15319040 + "resident": 5570560 }, "restyle": { "uploaded": 0, - "written": 2788, - "evaluated": 15876, + "written": 2908, + "evaluated": 16384, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 15319040 + "resident": 5570560 }, "one row": { "uploaded": 0, - "written": 64000, + "written": 48640, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 15319040 + "resident": 5570560 }, "every row": { - "uploaded": 63504, - "written": 1604, - "evaluated": 15876, + "uploaded": 65536, + "written": 1248, + "evaluated": 16384, + "packed": 16384, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 15384576 + "resident": 5636096 }, "first pick": { "uploaded": 0, "written": 2440, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 15384576 + "resident": 5636096 }, "pick": { "uploaded": 0, "written": 2440, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 15384576 + "resident": 5636096 }, "hover": { "uploaded": 0, "written": 3752, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 15384576 + "resident": 5636096 }, "select": { - "uploaded": 319480, - "written": 3672, - "evaluated": 15974, + "uploaded": 81920, + "written": 3776, + "evaluated": 4096, + "packed": 490, "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 15712256 + "resident": 5718016 + }, + "append": { + "uploaded": 3358720, + "written": 73164, + "evaluated": 167936, + "packed": 119785, + "readbacks": 40, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 9076736 } }, "2000000": { @@ -578,6 +725,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -589,6 +737,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -597,20 +746,22 @@ "resident": 0 }, "load": { - "uploaded": 1607860, - "written": 5608, - "evaluated": 80393, + "uploaded": 1638400, + "written": 6160, + "evaluated": 81920, + "packed": 409600, "readbacks": 1, "jobs": 1, "texts": 0, - "pipelines": 9, + "pipelines": 27, "late": 0, "resident": 1638400 }, "pan": { "uploaded": 0, - "written": 48640, + "written": 48752, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -619,92 +770,124 @@ "resident": 1638400 }, "zoom": { - "uploaded": 13591040, - "written": 51652, - "evaluated": 679552, + "uploaded": 4014080, + "written": 62404, + "evaluated": 200704, + "packed": 1003520, + "readbacks": 0, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 5652480 + }, + "revisit": { + "uploaded": 0, + "written": 1228, + "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 15564800 + "resident": 5652480 }, "restyle": { "uploaded": 0, - "written": 2788, - "evaluated": 15876, + "written": 2908, + "evaluated": 16384, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 15564800 + "resident": 5652480 }, "one row": { "uploaded": 0, - "written": 64000, + "written": 48640, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 15564800 + "resident": 5652480 }, "every row": { - "uploaded": 63504, - "written": 1604, - "evaluated": 15876, + "uploaded": 65536, + "written": 1248, + "evaluated": 16384, + "packed": 16384, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 15630336 + "resident": 5718016 }, "first pick": { "uploaded": 0, "written": 2440, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 15630336 + "resident": 5718016 }, "pick": { "uploaded": 0, "written": 2440, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 15630336 + "resident": 5718016 }, "hover": { "uploaded": 0, "written": 3752, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 15630336 + "resident": 5718016 }, "select": { - "uploaded": 319500, - "written": 3672, - "evaluated": 15975, + "uploaded": 81920, + "written": 3776, + "evaluated": 4096, + "packed": 495, "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 15958016 + "resident": 5799936 + }, + "append": { + "uploaded": 3276800, + "written": 72672, + "evaluated": 163840, + "packed": 234500, + "readbacks": 40, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 9076736 } } }, @@ -714,6 +897,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -725,6 +909,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -734,12 +919,13 @@ }, "load": { "uploaded": 36352, - "written": 5080, + "written": 8036, "evaluated": 3008, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, "resident": 36864 }, @@ -747,6 +933,7 @@ "uploaded": 0, "written": 58880, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -758,6 +945,19 @@ "uploaded": 0, "written": 58880, "evaluated": 0, + "packed": 0, + "readbacks": 0, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 36864 + }, + "revisit": { + "uploaded": 0, + "written": 1472, + "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -767,8 +967,9 @@ }, "one row": { "uploaded": 163840, - "written": 74400, + "written": 59200, "evaluated": 40960, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -780,6 +981,7 @@ "uploaded": 0, "written": 2960, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -791,6 +993,7 @@ "uploaded": 0, "written": 2960, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -802,12 +1005,25 @@ "uploaded": 0, "written": 4536, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, "resident": 200704 + }, + "append": { + "uploaded": 835600, + "written": 69312, + "evaluated": 41780, + "packed": 0, + "readbacks": 80, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 1839104 } }, "10000": { @@ -815,6 +1031,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -826,6 +1043,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -835,12 +1053,13 @@ }, "load": { "uploaded": 358400, - "written": 5080, + "written": 9532, "evaluated": 29800, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, "resident": 589824 }, @@ -848,6 +1067,7 @@ "uploaded": 0, "written": 58880, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -859,6 +1079,19 @@ "uploaded": 0, "written": 58880, "evaluated": 0, + "packed": 0, + "readbacks": 0, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 589824 + }, + "revisit": { + "uploaded": 0, + "written": 1472, + "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -868,8 +1101,9 @@ }, "one row": { "uploaded": 1600000, - "written": 74400, + "written": 59520, "evaluated": 400000, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -881,6 +1115,7 @@ "uploaded": 0, "written": 2960, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -892,6 +1127,7 @@ "uploaded": 0, "written": 2960, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -903,12 +1139,25 @@ "uploaded": 0, "written": 4520, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, "resident": 3211264 + }, + "append": { + "uploaded": 8016400, + "written": 69312, + "evaluated": 400820, + "packed": 0, + "readbacks": 80, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 16318464 } }, "100000": { @@ -916,6 +1165,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -927,6 +1177,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -936,12 +1187,13 @@ }, "load": { "uploaded": 3589760, - "written": 5152, + "written": 9848, "evaluated": 298936, + "packed": 0, "readbacks": 2, "jobs": 1, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, "resident": 4227072 }, @@ -949,6 +1201,7 @@ "uploaded": 0, "written": 58880, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -960,6 +1213,19 @@ "uploaded": 0, "written": 58880, "evaluated": 0, + "packed": 0, + "readbacks": 0, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 4227072 + }, + "revisit": { + "uploaded": 0, + "written": 1472, + "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -969,8 +1235,9 @@ }, "one row": { "uploaded": 10485760, - "written": 75200, + "written": 61600, "evaluated": 2621440, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -982,6 +1249,7 @@ "uploaded": 0, "written": 2960, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -993,6 +1261,7 @@ "uploaded": 0, "written": 2960, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -1004,12 +1273,25 @@ "uploaded": 0, "written": 4520, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, "resident": 14712832 + }, + "append": { + "uploaded": 27472400, + "written": 71072, + "evaluated": 1373620, + "packed": 0, + "readbacks": 80, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 67141632 } }, "1000000": { @@ -1017,6 +1299,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1028,6 +1311,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1036,81 +1320,112 @@ "resident": 0 }, "load": { - "uploaded": 16751128, - "written": 37956, - "evaluated": 983822, - "readbacks": 6, + "uploaded": 7913472, + "written": 18120, + "evaluated": 466944, + "packed": 1978368, + "readbacks": 3, "jobs": 2, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, - "resident": 18153472 + "resident": 7913472 }, "pan": { - "uploaded": 14622728, - "written": 77120, - "evaluated": 864442, - "readbacks": 6, + "uploaded": 0, + "written": 62576, + "evaluated": 0, + "packed": 0, + "readbacks": 4, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 33619968 + "resident": 7913472 }, "zoom": { - "uploaded": 31139892, - "written": 63588, - "evaluated": 1990386, - "readbacks": 5, - "jobs": 4, + "uploaded": 16285696, + "written": 88428, + "evaluated": 1040384, + "packed": 4071424, + "readbacks": 14, + "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 65830912 + "resident": 24199168 + }, + "revisit": { + "uploaded": 0, + "written": 3760, + "evaluated": 0, + "packed": 0, + "readbacks": 1, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 24199168 }, "one row": { - "uploaded": 4775696, - "written": 75916, - "evaluated": 1193924, + "uploaded": 655360, + "written": 59200, + "evaluated": 163840, + "packed": 163840, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 70615040 + "resident": 24854528 }, "first pick": { "uploaded": 0, "written": 2960, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 70615040 + "resident": 24854528 }, "pick": { "uploaded": 0, "written": 2960, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 70615040 + "resident": 24854528 }, "hover": { "uploaded": 0, "written": 4520, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 70615040 + "resident": 24854528 + }, + "append": { + "uploaded": 3276800, + "written": 69272, + "evaluated": 163840, + "packed": 119300, + "readbacks": 80, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 28131328 } }, "2000000": { @@ -1118,6 +1433,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1129,6 +1445,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1137,81 +1454,112 @@ "resident": 0 }, "load": { - "uploaded": 17197752, - "written": 37956, - "evaluated": 1008054, - "readbacks": 6, + "uploaded": 8241152, + "written": 17980, + "evaluated": 483328, + "packed": 2060288, + "readbacks": 3, "jobs": 2, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, - "resident": 18481152 + "resident": 8241152 }, "pan": { - "uploaded": 14841448, - "written": 77120, - "evaluated": 875378, - "readbacks": 6, + "uploaded": 0, + "written": 62576, + "evaluated": 0, + "packed": 0, + "readbacks": 4, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 35258368 + "resident": 8241152 }, "zoom": { - "uploaded": 39083136, - "written": 64504, - "evaluated": 2388039, - "readbacks": 5, - "jobs": 4, + "uploaded": 23937024, + "written": 94040, + "evaluated": 1425408, + "packed": 5984256, + "readbacks": 14, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 32178176 + }, + "revisit": { + "uploaded": 0, + "written": 3760, + "evaluated": 0, + "packed": 0, + "readbacks": 1, + "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 76023040 + "resident": 32178176 }, "one row": { - "uploaded": 2883584, - "written": 75884, - "evaluated": 720896, + "uploaded": 589824, + "written": 59168, + "evaluated": 147456, + "packed": 147456, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 78906624 + "resident": 32768000 }, "first pick": { "uploaded": 0, "written": 2960, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 78906624 + "resident": 32768000 }, "pick": { "uploaded": 0, "written": 2960, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 78906624 + "resident": 32768000 }, "hover": { "uploaded": 0, "written": 4520, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 78906624 + "resident": 32768000 + }, + "append": { + "uploaded": 3276800, + "written": 70792, + "evaluated": 163840, + "packed": 113700, + "readbacks": 80, + "jobs": 0, + "texts": 0, + "pipelines": 0, + "late": 0, + "resident": 36044800 } } }, @@ -1221,6 +1569,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1232,6 +1581,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1241,33 +1591,36 @@ }, "load": { "uploaded": 240000, - "written": 722428, + "written": 838672, "evaluated": 10000, - "readbacks": 8, + "packed": 0, + "readbacks": 10, "jobs": 0, - "texts": 207, - "pipelines": 20, + "texts": 214, + "pipelines": 34, "late": 0, "resident": 393216 }, "pan": { "uploaded": 0, - "written": 2131648, + "written": 3187496, "evaluated": 0, - "readbacks": 40, + "packed": 0, + "readbacks": 43, "jobs": 0, - "texts": 586, + "texts": 771, "pipelines": 0, "late": 0, "resident": 393216 }, "zoom": { "uploaded": 0, - "written": 4825740, + "written": 5470912, "evaluated": 0, - "readbacks": 41, + "packed": 0, + "readbacks": 43, "jobs": 0, - "texts": 1297, + "texts": 1296, "pipelines": 0, "late": 0, "resident": 393216 @@ -1278,6 +1631,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1289,6 +1643,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1298,33 +1653,36 @@ }, "load": { "uploaded": 2400000, - "written": 1487188, + "written": 2473380, "evaluated": 100000, - "readbacks": 8, + "packed": 0, + "readbacks": 30, "jobs": 0, - "texts": 216, - "pipelines": 20, + "texts": 385, + "pipelines": 34, "late": 0, "resident": 3145728 }, "pan": { "uploaded": 0, - "written": 2831152, + "written": 5912000, "evaluated": 0, - "readbacks": 40, + "packed": 0, + "readbacks": 63, "jobs": 0, - "texts": 739, + "texts": 1292, "pipelines": 0, "late": 0, "resident": 3145728 }, "zoom": { "uploaded": 0, - "written": 6473928, + "written": 8491440, "evaluated": 0, - "readbacks": 41, + "packed": 0, + "readbacks": 55, "jobs": 0, - "texts": 1641, + "texts": 1881, "pipelines": 0, "late": 0, "resident": 3145728 @@ -1335,6 +1693,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1346,6 +1705,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1354,37 +1714,40 @@ "resident": 0 }, "load": { - "uploaded": 1881024, - "written": 2977136, - "evaluated": 78376, - "readbacks": 18, + "uploaded": 1966080, + "written": 4312152, + "evaluated": 81920, + "packed": 491520, + "readbacks": 37, "jobs": 1, - "texts": 392, - "pipelines": 20, + "texts": 569, + "pipelines": 34, "late": 0, "resident": 1966080 }, "pan": { "uploaded": 0, - "written": 4540644, + "written": 6503124, "evaluated": 0, - "readbacks": 40, + "packed": 0, + "readbacks": 43, "jobs": 0, - "texts": 867, + "texts": 1103, "pipelines": 0, "late": 0, "resident": 1966080 }, "zoom": { - "uploaded": 15971184, - "written": 11933216, - "evaluated": 927610, - "readbacks": 45, + "uploaded": 4718592, + "written": 12752104, + "evaluated": 196608, + "packed": 1179648, + "readbacks": 49, "jobs": 0, - "texts": 1659, + "texts": 1607, "pipelines": 0, "late": 0, - "resident": 18382848 + "resident": 6684672 } } }, @@ -1394,6 +1757,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1405,6 +1769,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1414,12 +1779,13 @@ }, "load": { "uploaded": 1456, - "written": 5496, + "written": 8452, "evaluated": 116, + "packed": 0, "readbacks": 3, "jobs": 0, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, "resident": 2560 }, @@ -1427,6 +1793,7 @@ "uploaded": 0, "written": 58880, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1438,6 +1805,7 @@ "uploaded": 0, "written": 2960, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -1449,6 +1817,7 @@ "uploaded": 0, "written": 2960, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -1462,6 +1831,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1473,6 +1843,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1482,12 +1853,13 @@ }, "load": { "uploaded": 12256, - "written": 5496, + "written": 8452, "evaluated": 1016, + "packed": 0, "readbacks": 3, "jobs": 0, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, "resident": 13312 }, @@ -1495,6 +1867,7 @@ "uploaded": 0, "written": 58880, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1506,6 +1879,7 @@ "uploaded": 0, "written": 2960, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -1517,6 +1891,7 @@ "uploaded": 0, "written": 2960, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -1530,6 +1905,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1541,6 +1917,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1550,12 +1927,13 @@ }, "load": { "uploaded": 120256, - "written": 5496, + "written": 8460, "evaluated": 10016, + "packed": 0, "readbacks": 3, "jobs": 0, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, "resident": 197632 }, @@ -1563,6 +1941,7 @@ "uploaded": 0, "written": 58880, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1574,6 +1953,7 @@ "uploaded": 0, "written": 4448, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, @@ -1585,6 +1965,7 @@ "uploaded": 0, "written": 4448, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, @@ -1598,6 +1979,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1609,6 +1991,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1618,12 +2001,13 @@ }, "load": { "uploaded": 1200256, - "written": 7348, + "written": 8864, "evaluated": 100016, + "packed": 0, "readbacks": 3, "jobs": 1, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, "resident": 1573888 }, @@ -1631,6 +2015,7 @@ "uploaded": 0, "written": 58880, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1642,6 +2027,7 @@ "uploaded": 0, "written": 4448, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, @@ -1653,6 +2039,7 @@ "uploaded": 0, "written": 4448, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, @@ -1668,6 +2055,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1679,6 +2067,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1688,12 +2077,13 @@ }, "load": { "uploaded": 1340368, - "written": 3852, + "written": 5072, "evaluated": 100, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 14, + "pipelines": 29, "late": 0, "resident": 1344512 }, @@ -1701,6 +2091,7 @@ "uploaded": 0, "written": 48640, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1712,6 +2103,7 @@ "uploaded": 0, "written": 48640, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1721,8 +2113,9 @@ }, "one row": { "uploaded": 16000, - "written": 59680, + "written": 50880, "evaluated": 4000, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1734,6 +2127,7 @@ "uploaded": 0, "written": 2432, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -1745,6 +2139,7 @@ "uploaded": 0, "written": 2432, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -1758,6 +2153,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1769,6 +2165,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1778,12 +2175,13 @@ }, "load": { "uploaded": 4172168, - "written": 5348, + "written": 7720, "evaluated": 1000, + "packed": 0, "readbacks": 1, "jobs": 1, "texts": 0, - "pipelines": 14, + "pipelines": 29, "late": 0, "resident": 4206592 }, @@ -1791,6 +2189,7 @@ "uploaded": 0, "written": 48640, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1802,6 +2201,7 @@ "uploaded": 0, "written": 48640, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1811,8 +2211,9 @@ }, "one row": { "uploaded": 160000, - "written": 61280, + "written": 50880, "evaluated": 40000, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1824,6 +2225,7 @@ "uploaded": 0, "written": 2432, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -1835,6 +2237,7 @@ "uploaded": 0, "written": 2432, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -1848,6 +2251,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1859,6 +2263,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1868,12 +2273,13 @@ }, "load": { "uploaded": 18104720, - "written": 5740, + "written": 11840, "evaluated": 10000, + "packed": 0, "readbacks": 1, "jobs": 1, "texts": 0, - "pipelines": 14, + "pipelines": 29, "late": 0, "resident": 18284544 }, @@ -1881,6 +2287,7 @@ "uploaded": 0, "written": 48640, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1892,6 +2299,7 @@ "uploaded": 0, "written": 48640, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1901,8 +2309,9 @@ }, "one row": { "uploaded": 1600000, - "written": 69760, + "written": 51200, "evaluated": 400000, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1914,6 +2323,7 @@ "uploaded": 0, "written": 2432, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -1925,6 +2335,7 @@ "uploaded": 0, "written": 2432, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -1938,6 +2349,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1949,6 +2361,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1958,12 +2371,13 @@ }, "load": { "uploaded": 40370040, - "written": 60880, + "written": 40248, "evaluated": 30000, + "packed": 0, "readbacks": 1, "jobs": 1, "texts": 0, - "pipelines": 14, + "pipelines": 29, "late": 0, "resident": 40501248 }, @@ -1971,6 +2385,7 @@ "uploaded": 0, "written": 48640, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1982,6 +2397,7 @@ "uploaded": 0, "written": 48640, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -1991,8 +2407,9 @@ }, "one row": { "uploaded": 4800000, - "written": 83200, + "written": 52000, "evaluated": 1200000, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2004,6 +2421,7 @@ "uploaded": 0, "written": 2432, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -2015,6 +2433,7 @@ "uploaded": 0, "written": 2432, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -2030,6 +2449,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2041,6 +2461,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2049,37 +2470,40 @@ "resident": 0 }, "load": { - "uploaded": 336000, - "written": 5260, - "evaluated": 80, + "uploaded": 272064, + "written": 5068, + "evaluated": 64, + "packed": 0, "readbacks": 0, "jobs": 4, "texts": 0, - "pipelines": 14, + "pipelines": 29, "late": 0, - "resident": 344576 + "resident": 278784 }, "pan": { "uploaded": 2160192, - "written": 50232, + "written": 49848, "evaluated": 512, + "packed": 0, "readbacks": 0, "jobs": 8, "texts": 0, "pipelines": 0, "late": 0, - "resident": 2771968 + "resident": 2706176 }, "zoom": { - "uploaded": 2119824, - "written": 51004, - "evaluated": 1088, + "uploaded": 2017824, + "written": 50208, + "evaluated": 1040, + "packed": 0, "readbacks": 0, "jobs": 23, "texts": 0, "pipelines": 0, "late": 0, - "resident": 5546752 + "resident": 5284096 } }, "256": { @@ -2087,6 +2511,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2098,6 +2523,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2106,37 +2532,40 @@ "resident": 0 }, "load": { - "uploaded": 788640, - "written": 5264, - "evaluated": 1280, + "uploaded": 635904, + "written": 5072, + "evaluated": 1024, + "packed": 0, "readbacks": 0, "jobs": 4, "texts": 0, - "pipelines": 14, + "pipelines": 29, "late": 0, - "resident": 922624 + "resident": 659456 }, "pan": { "uploaded": 5062272, "written": 50272, "evaluated": 8192, + "packed": 0, "readbacks": 0, "jobs": 8, "texts": 0, "pipelines": 0, "late": 0, - "resident": 6333440 + "resident": 6070272 }, "zoom": { - "uploaded": 5673696, - "written": 51048, + "uploaded": 5709024, + "written": 50736, "evaluated": 17152, + "packed": 0, "readbacks": 0, "jobs": 22, "texts": 0, "pipelines": 0, "late": 0, - "resident": 13184000 + "resident": 12826624 } }, "4096": { @@ -2144,6 +2573,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2155,6 +2585,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2163,37 +2594,40 @@ "resident": 0 }, "load": { - "uploaded": 3344464, - "written": 6704, - "evaluated": 20480, + "uploaded": 2703712, + "written": 7704, + "evaluated": 16384, + "packed": 0, "readbacks": 0, - "jobs": 6, + "jobs": 5, "texts": 0, - "pipelines": 14, + "pipelines": 29, "late": 0, - "resident": 3424256 + "resident": 2752512 }, "pan": { - "uploaded": 21165184, - "written": 52928, + "uploaded": 19818144, + "written": 51780, "evaluated": 172032, + "packed": 0, "readbacks": 0, "jobs": 12, "texts": 0, "pipelines": 0, "late": 0, - "resident": 25509888 + "resident": 23396352 }, "zoom": { - "uploaded": 20321088, - "written": 55384, - "evaluated": 225280, + "uploaded": 17787104, + "written": 55320, + "evaluated": 208896, + "packed": 0, "readbacks": 0, - "jobs": 19, + "jobs": 18, "texts": 0, "pipelines": 0, "late": 0, - "resident": 46465024 + "resident": 43253760 } } }, @@ -2203,6 +2637,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2214,6 +2649,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2223,12 +2659,13 @@ }, "load": { "uploaded": 24240, - "written": 8108, + "written": 11508, "evaluated": 300, + "packed": 0, "readbacks": 1, "jobs": 2, "texts": 0, - "pipelines": 23, + "pipelines": 29, "late": 0, "resident": 33792 }, @@ -2236,6 +2673,7 @@ "uploaded": 0, "written": 60352, "evaluated": 0, + "packed": 0, "readbacks": 39, "jobs": 0, "texts": 0, @@ -2245,8 +2683,9 @@ }, "edit": { "uploaded": 371960, - "written": 105216, + "written": 65376, "evaluated": 4000, + "packed": 0, "readbacks": 40, "jobs": 0, "texts": 0, @@ -2256,8 +2695,9 @@ }, "arrange": { "uploaded": 19000, - "written": 5836, + "written": 4576, "evaluated": 100, + "packed": 0, "readbacks": 1, "jobs": 2, "texts": 0, @@ -2271,6 +2711,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2282,6 +2723,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2291,12 +2733,13 @@ }, "load": { "uploaded": 242960, - "written": 15308, + "written": 21652, "evaluated": 3000, + "packed": 0, "readbacks": 1, "jobs": 2, "texts": 0, - "pipelines": 23, + "pipelines": 29, "late": 0, "resident": 335872 }, @@ -2304,6 +2747,7 @@ "uploaded": 0, "written": 60352, "evaluated": 0, + "packed": 0, "readbacks": 39, "jobs": 0, "texts": 0, @@ -2313,8 +2757,9 @@ }, "edit": { "uploaded": 3463800, - "written": 112384, + "written": 72544, "evaluated": 40000, + "packed": 0, "readbacks": 40, "jobs": 0, "texts": 0, @@ -2324,8 +2769,9 @@ }, "arrange": { "uploaded": 178144, - "written": 5836, + "written": 4576, "evaluated": 1000, + "packed": 0, "readbacks": 1, "jobs": 2, "texts": 0, @@ -2339,6 +2785,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2350,6 +2797,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2359,12 +2807,13 @@ }, "load": { "uploaded": 2429800, - "written": 88800, + "written": 93932, "evaluated": 30000, + "packed": 0, "readbacks": 1, "jobs": 2, "texts": 0, - "pipelines": 23, + "pipelines": 29, "late": 0, "resident": 3014656 }, @@ -2372,6 +2821,7 @@ "uploaded": 0, "written": 60352, "evaluated": 0, + "packed": 0, "readbacks": 39, "jobs": 0, "texts": 0, @@ -2381,8 +2831,9 @@ }, "edit": { "uploaded": 34375160, - "written": 104512, + "written": 64992, "evaluated": 400000, + "packed": 0, "readbacks": 40, "jobs": 0, "texts": 0, @@ -2391,9 +2842,10 @@ "resident": 42336256 }, "arrange": { - "uploaded": 1710032, - "written": 5852, + "uploaded": 1711520, + "written": 4600, "evaluated": 10000, + "packed": 0, "readbacks": 1, "jobs": 2, "texts": 0, @@ -2409,6 +2861,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2420,6 +2873,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2429,19 +2883,21 @@ }, "load": { "uploaded": 49152, - "written": 44096, + "written": 41216, "evaluated": 10, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 22, - "pipelines": 15, + "pipelines": 30, "late": 0, "resident": 524288 }, "stream": { "uploaded": 48, - "written": 194816, + "written": 181696, "evaluated": 0, + "packed": 0, "readbacks": 40, "jobs": 0, "texts": 0, @@ -2453,6 +2909,7 @@ "uploaded": 0, "written": 179136, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2462,8 +2919,9 @@ }, "narrow": { "uploaded": 1872, - "written": 210040, + "written": 197596, "evaluated": 0, + "packed": 0, "readbacks": 40, "jobs": 0, "texts": 19, @@ -2475,6 +2933,7 @@ "uploaded": 0, "written": 11816, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, @@ -2488,6 +2947,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2499,6 +2959,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2508,19 +2969,21 @@ }, "load": { "uploaded": 417792, - "written": 44108, + "written": 41232, "evaluated": 100, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 22, - "pipelines": 15, + "pipelines": 30, "late": 0, "resident": 786432 }, "stream": { "uploaded": 408, - "written": 195296, + "written": 181696, "evaluated": 0, + "packed": 0, "readbacks": 40, "jobs": 0, "texts": 0, @@ -2532,6 +2995,7 @@ "uploaded": 0, "written": 179136, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2541,8 +3005,9 @@ }, "narrow": { "uploaded": 15912, - "written": 210364, + "written": 197880, "evaluated": 0, + "packed": 0, "readbacks": 40, "jobs": 0, "texts": 19, @@ -2554,6 +3019,7 @@ "uploaded": 0, "written": 11816, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, @@ -2567,6 +3033,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2578,6 +3045,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2587,19 +3055,21 @@ }, "load": { "uploaded": 4104192, - "written": 44468, + "written": 41440, "evaluated": 1000, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 22, - "pipelines": 15, + "pipelines": 30, "late": 0, "resident": 4456448 }, "stream": { "uploaded": 4008, - "written": 202016, + "written": 181696, "evaluated": 0, + "packed": 0, "readbacks": 40, "jobs": 0, "texts": 0, @@ -2611,6 +3081,7 @@ "uploaded": 0, "written": 179136, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2620,8 +3091,9 @@ }, "narrow": { "uploaded": 156312, - "written": 214324, + "written": 203272, "evaluated": 0, + "packed": 0, "readbacks": 40, "jobs": 0, "texts": 19, @@ -2633,6 +3105,7 @@ "uploaded": 0, "written": 11816, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, @@ -2646,6 +3119,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2657,6 +3131,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2666,19 +3141,21 @@ }, "load": { "uploaded": 40968192, - "written": 51748, + "written": 48924, "evaluated": 10000, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 22, - "pipelines": 15, + "pipelines": 30, "late": 0, "resident": 45088768 }, "stream": { "uploaded": 40008, - "written": 275048, + "written": 195388, "evaluated": 0, + "packed": 0, "readbacks": 40, "jobs": 0, "texts": 0, @@ -2690,6 +3167,7 @@ "uploaded": 0, "written": 179136, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2699,8 +3177,9 @@ }, "narrow": { "uploaded": 1560312, - "written": 255868, + "written": 252508, "evaluated": 0, + "packed": 0, "readbacks": 40, "jobs": 0, "texts": 19, @@ -2712,6 +3191,7 @@ "uploaded": 0, "written": 11816, "evaluated": 0, + "packed": 0, "readbacks": 2, "jobs": 0, "texts": 0, @@ -2727,6 +3207,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2738,6 +3219,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2747,12 +3229,13 @@ }, "load": { "uploaded": 12000, - "written": 53080, + "written": 50088, "evaluated": 1, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 26, - "pipelines": 15, + "pipelines": 30, "late": 0, "resident": 524288 }, @@ -2760,6 +3243,7 @@ "uploaded": 0, "written": 179200, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2769,8 +3253,9 @@ }, "narrow": { "uploaded": 0, - "written": 211216, + "written": 198724, "evaluated": 0, + "packed": 0, "readbacks": 39, "jobs": 0, "texts": 19, @@ -2784,6 +3269,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2795,6 +3281,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2803,31 +3290,34 @@ "resident": 0 }, "load": { - "uploaded": 140000, - "written": 55252, + "uploaded": 20000, + "written": 52552, "evaluated": 1, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 26, - "pipelines": 15, + "pipelines": 30, "late": 0, - "resident": 1310720 + "resident": 786432 }, "scrub": { "uploaded": 0, "written": 179200, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 1310720 + "resident": 786432 }, "narrow": { - "uploaded": 0, - "written": 209620, + "uploaded": 120000, + "written": 198548, "evaluated": 0, + "packed": 0, "readbacks": 39, "jobs": 0, "texts": 16, @@ -2841,6 +3331,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2852,6 +3343,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2860,37 +3352,40 @@ "resident": 0 }, "load": { - "uploaded": 749280, - "written": 57492, + "uploaded": 224992, + "written": 59516, "evaluated": 1, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 26, - "pipelines": 15, + "pipelines": 30, "late": 0, - "resident": 2097152 + "resident": 1572864 }, "scrub": { "uploaded": 0, "written": 179200, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 2097152 + "resident": 1572864 }, "narrow": { "uploaded": 0, - "written": 218196, + "written": 205780, "evaluated": 0, + "packed": 0, "readbacks": 39, "jobs": 0, "texts": 16, "pipelines": 0, "late": 0, - "resident": 2097152 + "resident": 1572864 } }, "1000000": { @@ -2898,6 +3393,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2909,6 +3405,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2917,37 +3414,40 @@ "resident": 0 }, "load": { - "uploaded": 805536, - "written": 59652, + "uploaded": 281248, + "written": 61856, "evaluated": 1, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 26, - "pipelines": 15, + "pipelines": 30, "late": 0, - "resident": 2097152 + "resident": 1572864 }, "scrub": { "uploaded": 0, "written": 179200, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 2097152 + "resident": 1572864 }, "narrow": { "uploaded": 0, - "written": 224116, + "written": 212348, "evaluated": 0, + "packed": 0, "readbacks": 39, "jobs": 0, "texts": 16, "pipelines": 0, "late": 0, - "resident": 2097152 + "resident": 1572864 } } }, @@ -2957,6 +3457,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2968,6 +3469,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -2977,19 +3479,21 @@ }, "load": { "uploaded": 20000, - "written": 4004, + "written": 5220, "evaluated": 1000, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 9, + "pipelines": 27, "late": 0, "resident": 20480 }, "move": { "uploaded": 20000, - "written": 74568, + "written": 75420, "evaluated": 2000, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3003,6 +3507,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3014,6 +3519,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3023,19 +3529,21 @@ }, "load": { "uploaded": 200000, - "written": 4004, + "written": 5228, "evaluated": 10000, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 9, + "pipelines": 27, "late": 0, "resident": 327680 }, "move": { "uploaded": 200000, - "written": 74568, + "written": 75436, "evaluated": 20000, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3049,6 +3557,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3060,6 +3569,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3069,19 +3579,21 @@ }, "load": { "uploaded": 2000000, - "written": 4032, + "written": 6556, "evaluated": 100000, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 9, + "pipelines": 27, "late": 0, "resident": 2621440 }, "move": { "uploaded": 2000000, - "written": 74596, + "written": 74416, "evaluated": 200000, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3095,6 +3607,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3106,6 +3619,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3114,26 +3628,28 @@ "resident": 0 }, "load": { - "uploaded": 1567520, - "written": 388296, - "evaluated": 155802, + "uploaded": 1638400, + "written": 391984, + "evaluated": 98304, + "packed": 409600, "readbacks": 1, "jobs": 1, "texts": 0, - "pipelines": 9, + "pipelines": 27, "late": 0, "resident": 1638400 }, "move": { - "uploaded": 952560, - "written": 298688, - "evaluated": 71468, + "uploaded": 655360, + "written": 227616, + "evaluated": 49152, + "packed": 163840, "readbacks": 0, "jobs": 1, "texts": 0, "pipelines": 0, "late": 0, - "resident": 2621440 + "resident": 2293760 } } }, @@ -3143,6 +3659,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3154,6 +3671,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3163,12 +3681,13 @@ }, "load": { "uploaded": 3589760, - "written": 5104, + "written": 8328, "evaluated": 298936, + "packed": 0, "readbacks": 1, "jobs": 1, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, "resident": 4227072 }, @@ -3176,6 +3695,7 @@ "uploaded": 0, "written": 58880, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3189,6 +3709,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3200,6 +3721,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3209,12 +3731,13 @@ }, "load": { "uploaded": 3589760, - "written": 9184, + "written": 18576, "evaluated": 597872, + "packed": 0, "readbacks": 2, "jobs": 2, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, "resident": 4227072 }, @@ -3222,6 +3745,7 @@ "uploaded": 0, "written": 117760, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3235,6 +3759,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3246,6 +3771,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3255,12 +3781,13 @@ }, "load": { "uploaded": 3589760, - "written": 17344, + "written": 30240, "evaluated": 1195744, + "packed": 0, "readbacks": 4, "jobs": 4, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, "resident": 4227072 }, @@ -3268,6 +3795,7 @@ "uploaded": 0, "written": 235520, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3281,6 +3809,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3292,6 +3821,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3301,12 +3831,13 @@ }, "load": { "uploaded": 3589760, - "written": 33664, + "written": 59456, "evaluated": 2391488, + "packed": 0, "readbacks": 8, "jobs": 8, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, "resident": 4227072 }, @@ -3314,6 +3845,7 @@ "uploaded": 0, "written": 471040, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3329,6 +3861,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3340,6 +3873,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3348,37 +3882,40 @@ "resident": 0 }, "load": { - "uploaded": 4764460, - "written": 12468, - "evaluated": 289253, + "uploaded": 3915776, + "written": 10340, + "evaluated": 249856, + "packed": 978944, "readbacks": 1, "jobs": 2, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, - "resident": 5636096 + "resident": 3915776 }, "pan": { - "uploaded": 6457376, - "written": 72224, - "evaluated": 416008, + "uploaded": 0, + "written": 61024, + "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 14417920 + "resident": 3915776 }, "zoom": { "uploaded": 0, - "written": 58880, + "written": 62388, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 14417920 + "resident": 3915776 } }, "1000000": { @@ -3386,6 +3923,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3397,6 +3935,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3405,37 +3944,40 @@ "resident": 0 }, "load": { - "uploaded": 9351656, - "written": 22148, - "evaluated": 557254, + "uploaded": 5341184, + "written": 13052, + "evaluated": 323584, + "packed": 1335296, "readbacks": 1, "jobs": 2, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, - "resident": 10092544 + "resident": 5341184 }, "pan": { - "uploaded": 8253296, - "written": 72320, - "evaluated": 505804, + "uploaded": 0, + "written": 61024, + "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 16646144 + "resident": 5341184 }, "zoom": { "uploaded": 0, - "written": 58880, + "written": 62388, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 16646144 + "resident": 5341184 } }, "2000000": { @@ -3443,6 +3985,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3454,6 +3997,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3462,37 +4006,40 @@ "resident": 0 }, "load": { - "uploaded": 9480488, - "written": 22148, - "evaluated": 564688, + "uploaded": 5914624, + "written": 14876, + "evaluated": 352256, + "packed": 1478656, "readbacks": 1, "jobs": 2, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, - "resident": 10420224 + "resident": 5914624 }, "pan": { - "uploaded": 8270536, - "written": 72320, - "evaluated": 506666, + "uploaded": 0, + "written": 61024, + "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 16187392 + "resident": 5914624 }, "zoom": { "uploaded": 0, - "written": 58880, + "written": 62388, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 16187392 + "resident": 5914624 } } }, @@ -3502,6 +4049,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3513,6 +4061,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3522,12 +4071,13 @@ }, "load": { "uploaded": 3589760, - "written": 9184, + "written": 15632, "evaluated": 597872, + "packed": 0, "readbacks": 2, "jobs": 2, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, "resident": 4227072 }, @@ -3535,10 +4085,11 @@ "uploaded": 0, "written": 8928, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 6, + "pipelines": 1, "late": 0, "resident": 4227072 }, @@ -3546,6 +4097,7 @@ "uploaded": 0, "written": 2944, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3557,6 +4109,7 @@ "uploaded": 0, "written": 97248, "evaluated": 0, + "packed": 0, "readbacks": 30, "jobs": 0, "texts": 0, @@ -3568,6 +4121,7 @@ "uploaded": 0, "written": 5984, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -3581,6 +4135,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3592,6 +4147,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3601,12 +4157,13 @@ }, "load": { "uploaded": 3589760, - "written": 9184, + "written": 18576, "evaluated": 597872, + "packed": 0, "readbacks": 2, "jobs": 2, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, "resident": 4227072 }, @@ -3614,10 +4171,11 @@ "uploaded": 0, "written": 8928, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 6, + "pipelines": 1, "late": 0, "resident": 4227072 }, @@ -3625,6 +4183,7 @@ "uploaded": 0, "written": 2944, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3636,6 +4195,7 @@ "uploaded": 0, "written": 97248, "evaluated": 0, + "packed": 0, "readbacks": 30, "jobs": 0, "texts": 0, @@ -3647,6 +4207,7 @@ "uploaded": 0, "written": 5984, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, @@ -3660,6 +4221,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3671,6 +4233,7 @@ "uploaded": 0, "written": 0, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3680,23 +4243,25 @@ }, "load": { "uploaded": 3589760, - "written": 9184, + "written": 15632, "evaluated": 597872, + "packed": 0, "readbacks": 2, "jobs": 2, "texts": 0, - "pipelines": 18, + "pipelines": 29, "late": 0, "resident": 4227072 }, "image": { "uploaded": 0, - "written": 11872, + "written": 8928, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 6, + "pipelines": 1, "late": 0, "resident": 4227072 }, @@ -3704,6 +4269,7 @@ "uploaded": 0, "written": 2944, "evaluated": 0, + "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, @@ -3715,6 +4281,7 @@ "uploaded": 0, "written": 97248, "evaluated": 0, + "packed": 0, "readbacks": 30, "jobs": 0, "texts": 0, @@ -3726,6 +4293,7 @@ "uploaded": 0, "written": 5984, "evaluated": 0, + "packed": 0, "readbacks": 1, "jobs": 0, "texts": 0, diff --git a/benchmark/judge.ts b/benchmark/judge.ts index b504c6ec..486de674 100644 --- a/benchmark/judge.ts +++ b/benchmark/judge.ts @@ -50,7 +50,7 @@ const FLOOR = { frame: 0.5, action: TICK, bytes: 64 * 1024 }; /** How much slower than the baseline a median may be. */ const SLOWER = 1.25; /** Work the data and the frames drawn alone set, which repeats exactly once a phase settles. */ -const EXACT = new Set(['uploaded', 'evaluated', 'jobs', 'pipelines', 'late', 'resident']); +const EXACT = new Set(['uploaded', 'evaluated', 'packed', 'jobs', 'pipelines', 'late', 'resident']); /** How much more of the rest a phase may do than the baseline: the frame a job or a fetch lands in * moves label placements and tiles by up to about 1.7×, and a per-frame blowup by far more. */ const STEADY = 2; diff --git a/benchmark/page/scenarios.ts b/benchmark/page/scenarios.ts index 8443d8e8..27a7fa8c 100644 --- a/benchmark/page/scenarios.ts +++ b/benchmark/page/scenarios.ts @@ -2,7 +2,8 @@ * Every scenario: what it sweeps, the scene and data at each size, and what it does. Each phase * declares how its cost may grow with size, so the sweep checks the README's rules: CPU per frame * stays flat in the rows, a one-row change costs the same at any size, restyling moves no data, - * views share pages, and loading grows no faster than the data. `init`, `data`, and `load` come + * views share pages, coming back where the camera was costs nothing, appending never thins what + * draws, and loading grows no faster than the data. `init`, `data`, and `load` come * first in every scenario, and idle frames are counted after the last. */ import { field, type Budget, type Camera, type ColormapName, type Scene, type Space } from 'latkit'; @@ -156,6 +157,22 @@ const zoom = (): Phase => ({ }, }); +/** Zooms two levels in, then back where it was: measured, the way back, which draws the pages it held + * and so packs, uploads, and evaluates nothing. */ +const revisit = (): Phase => ({ + name: 'revisit', + order: { time: 0, bytes: 0 }, + run: async (s) => { + const start = s.view.camera as MapCamera; + await s.once(() => { + for (const v of s.views) v.camera = { ...start, zoom: start.zoom + 2 }; + }); + return s.once(() => { + for (const v of s.views) v.camera = start; + }); + }, +}); + /** Recolors through `scene`'s colormap: the GPU evaluates again; no data moves. */ const restyle = (scene: Scene): Phase => ({ name: 'restyle', @@ -171,6 +188,30 @@ const one = (data: string, table: string, name: string): Phase => ({ s.steady(FRAMES, (i) => s.apply(data, (p) => p.set(table, [i], { [name]: [(i % 10) / 10] }))), }); +/** Appends a row to `table` every frame, as a feed of new rows does: what draws never thins. */ +const append = ( + data: string, + table: string, + row: (i: number) => Record, +): Phase => ({ + name: 'append', + order: { time: 0 }, + run: async (s) => { + const n = s.count(data, table); + const drawn = () => s.views.map((v) => v.stats.drawn); + const start = drawn(); + let thinned = ''; + const cost = await s.steady(FRAMES, (i) => { + const now = drawn(); + if (!thinned && now.some((d, v) => d < start[v]!)) + thinned = `${now.join(', ')} drawn, ${start.join(', ')} before`; + s.apply(data, (p) => p.set(table, [n + i], row(i))); + }); + if (thinned) throw new Error(`appending thinned what draws: ${thinned}`); + return cost; + }, +}); + /** Changes every row's value at once. */ const every = (data: string, table: string, name: string): Phase => ({ name: 'every row', @@ -364,12 +405,18 @@ export const scenarios: Record = { phases: [ pan(), zoom(), + revisit(), restyle(dots('plane', 'magma')), one('d', 'Point', 'v'), every('d', 'Point', 'v'), ...pick(), hover(), select('d', 'Point'), + append('d', 'Point', (i) => ({ + x: [((i * 37) % 100) / 100], + y: [((i * 53) % 100) / 100], + v: [0.5], + })), ], }, grid: { @@ -380,7 +427,19 @@ export const scenarios: Record = { scene: grid({ earth: { projection: 'globe' } }), data: { g: make.lattice(n) }, }), - phases: [pan(), zoom(), one('g', 'Bus', 'load'), ...pick(), hover()], + phases: [ + pan(), + zoom(), + revisit(), + one('g', 'Bus', 'load'), + ...pick(), + hover(), + append('g', 'Bus', (i) => ({ + lon: [-170 + (340 * ((i * 37) % 100)) / 100], + lat: [-60 + (120 * ((i * 53) % 100)) / 100], + load: [0.5], + })), + ], }, labels: { about: 'every vertex labeled: placement, collisions, and the texts set', diff --git a/benchmark/page/stage.ts b/benchmark/page/stage.ts index 6b9f264e..f11891c3 100644 --- a/benchmark/page/stage.ts +++ b/benchmark/page/stage.ts @@ -247,7 +247,7 @@ export class Stage { cpu95: quantile(drawn.cpu, 0.95), gpu: quantile(drawn.gpu, 0.5), work, - resident: this.views[0]?.stats.resident ?? 0, + resident: this.gpu.resident, steady, }; } diff --git a/crates/latkit-js/src/lib.rs b/crates/latkit-js/src/lib.rs index 7fb47678..38b0cae1 100644 --- a/crates/latkit-js/src/lib.rs +++ b/crates/latkit-js/src/lib.rs @@ -111,6 +111,18 @@ impl Gpu { to_json(&self.0.work()) } + /// Bytes on the GPU every view of it draws from. + #[wasm_bindgen(getter)] + pub fn resident(&self) -> f64 { + self.0.resident() as f64 + } + + /// Bytes waiting to upload, for every view of it. + #[wasm_bindgen(getter)] + pub fn uploading(&self) -> f64 { + self.0.uploading() as f64 + } + /// Draws one frame now at `now`, milliseconds as an animation frame's timestamp; from the first, /// no animation frames are asked for. pub fn step(&self, now: f64) { diff --git a/crates/latkit/Cargo.toml b/crates/latkit/Cargo.toml index 0a4858c9..5c9b7e65 100644 --- a/crates/latkit/Cargo.toml +++ b/crates/latkit/Cargo.toml @@ -70,6 +70,7 @@ web-sys = { workspace = true, optional = true, features = [ "KeyboardEvent", "MediaQueryList", "MouseEvent", + "Navigator", "OffscreenCanvas", "OffscreenCanvasRenderingContext2d", "PointerEvent", @@ -81,6 +82,7 @@ web-sys = { workspace = true, optional = true, features = [ "WheelEvent", "Window", "WorkerGlobalScope", + "WorkerNavigator", ] } [build-dependencies] diff --git a/crates/latkit/shaders/compute/bounds.wesl b/crates/latkit/shaders/compute/bounds.wesl index afe01bbf..861d3346 100644 --- a/crates/latkit/shaders/compute/bounds.wesl +++ b/crates/latkit/shaders/compute/bounds.wesl @@ -10,7 +10,8 @@ import package::lib::reduce::ordered; import package::lib::state::{row_at, vertex_at}; import package::lib::points::{placed, vertex_half}; import package::lib::path::{PATH, segment, segment_ends, stroke_style, stroke_instances}; -import package::lib::triangles::{triangle_corner, triangle_row, triangle_instances}; +import package::lib::triangles::{triangle_corner, triangle_instances}; +import package::lib::drawn::held_row; @group(2) @binding(2) var extents: array>; @@ -31,10 +32,14 @@ fn counted(row: u32) -> bool { @compute @workgroup_size(256) fn points(@builtin(global_invocation_id) id: vec3u) { let i = item(id); - if i >= mark.rows || !counted(i) { + if i >= mark.rows { return; } - let v = vertex_at(row_at(i)); + let row = held_row(i); + if !counted(row) { + return; + } + let v = vertex_at(row_at(row)); if (v.flags & 1u) == 0u || !placed(v) { return; } @@ -55,7 +60,7 @@ fn strokes(@builtin(global_invocation_id) id: vec3u) { return; } let s = segment(i); - if s.row >= mark.rows || !counted(s.row) || !stroke_style(s.row).shown { + if s.item >= mark.rows || !counted(s.row) || !stroke_style(s.row).shown { return; } let ends = segment_ends(s); @@ -72,8 +77,8 @@ fn areas(@builtin(global_invocation_id) id: vec3u) { if PATH != PATH_GEOMETRY || i >= triangle_instances() { return; } - let row = triangle_row(i); - if row >= mark.rows || !counted(row) { + let s = segment(i); + if s.item >= mark.rows || !counted(s.row) { return; } for (var k = 0u; k < 3u; k++) { diff --git a/crates/latkit/shaders/compute/candidates.wesl b/crates/latkit/shaders/compute/candidates.wesl index 6ddc0ce1..94870f37 100644 --- a/crates/latkit/shaders/compute/candidates.wesl +++ b/crates/latkit/shaders/compute/candidates.wesl @@ -14,26 +14,31 @@ import package::lib::state::extra_at; import package::lib::read::missing_bits; import package::lib::label::{anchor, length_px, legibility, text_at, fits, in_view}; import package::lib::placement::{word, put, candidate, want, seen}; +import package::lib::drawn::drawn_row; @compute @workgroup_size(256) fn candidates(@builtin(global_invocation_id) id: vec3u) { - let row = item(id); - if row >= mark.rows { + let i = item(id); + if i >= mark.rows { return; } + let drawn = drawn_row(i); + let row = drawn.row; let at = candidate(row); let before = word(at + CANDIDATE_STATE); let sticky = (before & PLACED_LABEL) != 0u; let code_bits = state[extra_at(row, mark.label + LABEL_CODE)]; var next = select(0u, STICKY, sticky); - if missing_bits(code_bits) || (state[mark.state + row * mark.stride + mark.stride - 1u] & VISIBLE) == 0u { + let flags = state[mark.state + row * mark.stride + mark.stride - 1u]; + // A row whose tile leaves keeps no label. + if missing_bits(code_bits) || (flags & VISIBLE) == 0u || drawn.leaving { put(at + CANDIDATE_STATE, next); return; } let code = u32(bitcast(code_bits)); let uv0 = word(mark.runs + 2u * code); let uv1 = word(mark.runs + 2u * code + 1u); - let a = anchor(row); + let a = anchor(i); let size = length_px(mark.label_size); let diagram = frame.space == SPACE_DIAGRAM; let estimated = (uv0 & TEXT_ESTIMATE) != 0u; diff --git a/crates/latkit/shaders/compute/evaluate.wesl b/crates/latkit/shaders/compute/evaluate.wesl index 56726384..37d3d326 100644 --- a/crates/latkit/shaders/compute/evaluate.wesl +++ b/crates/latkit/shaders/compute/evaluate.wesl @@ -6,11 +6,12 @@ // Evaluating knows nothing of transitions: while a mark eases, the view binds its settled mirror as // the state, and each frame `ease` writes what every row shows from its frozen and settled state by // the mark's progress: numbers and colors mix, positions move by their offset, a row whose visibility -// flipped fades, rows past `mark.rows` left and fade out, and the rest show as evaluated. No channel -// runs to ease. +// flipped fades, rows past the live ones left and fade out, and the rest show as evaluated. No +// channel runs to ease. Every row here is a held row. import package::lib::types::{ - NONE, MISSING, MISSING_COLOR, STACK, VISIBLE, PLACED, FADE_IN, FADE_OUT, ENTERING, BLOCK_ROWS, item, + NONE, MISSING, MISSING_COLOR, STACK, VISIBLE, PLACED, FADE_IN, FADE_OUT, ENTERING, BLOCK_ROWS, HELD_BITS, + HELD_MASK, item, BLEND_KEEP, BLEND_NUMBER, BLEND_COLOR, BLEND_POSITION, STEP_REDUCE, VERTEX_POSITION, VERTEX_ELEVATION, VERTEX_FLAGS, VERTEX_HALF, VERTEX_RANGE, HALF_UNITS, OWN_HEIGHT, SIZED, MARK_VERTICES, OP_CONST, OP_MISSING, OP_FIELD, OP_SELECTED, OP_HOVERED, OP_POINTER, OP_ELAPSED, OP_THEME, OP_ZOOM, @@ -50,6 +51,45 @@ fn row_of(id: vec3u) -> u32 { return blocks[mark.block + id.y] * BLOCK_ROWS + id.x; } +// Rows of the mark that are live: those past them left and fade out. +fn live() -> u32 { + return mark.held - mark.exits; +} + +// The held row of drawing row `row` of a table whose held pages' slots the program lists at `slots` +// (how many pages, then each one's slot; then how many rows it keeps whose pages it does not hold, +// then each one's place and held row, ascending by place), NONE where it holds the row nowhere; a +// table drawn whole, with no such list, holds every row as itself. +fn held_of(slots: u32, row: u32) -> u32 { + if slots == NONE || row == NONE { + return row; + } + let pages = program[slots]; + let page = row >> HELD_BITS; + if page < pages { + let slot = program[slots + 1u + page]; + if slot != NONE { + return (slot << HELD_BITS) | (row & HELD_MASK); + } + } + let kept = slots + 1u + pages; + let count = program[kept]; + var lo = 0u; + var hi = count; + while lo < hi { + let mid = (lo + hi) / 2u; + if program[kept + 1u + 2u * mid] < row { + lo = mid + 1u; + } else { + hi = mid; + } + } + if lo < count && program[kept + 1u + 2u * lo] == row { + return program[kept + 2u + 2u * lo]; + } + return NONE; +} + fn constant(i: u32) -> u32 { return program[mark.constants + i]; } @@ -312,10 +352,14 @@ fn run(row: u32, start: u32, end: u32) { position(row, a, column_value(b, row), column_value(c, row)); } case OP_STORE_END: { - // Without a column, the end is the row itself, as a pole's. - let end_row = select(bits_value(b, row), row, b == NONE); - let first = program[mark.ends + c]; - store(row, a, select(first + end_row, NONE, end_row == NONE || first == NONE)); + // An end is a row of the table its reference names, held where the vertex mark drawing + // that table holds it; without a column, the row itself, as a pole's. + let first = program[mark.ends + 2u * c]; + var end = row; + if b != NONE { + end = held_of(program[mark.ends + 2u * c + 1u], bits_value(b, row)); + } + store(row, a, select(first + end, NONE, end == NONE || first == NONE)); } case OP_REDUCE: { sp--; @@ -345,16 +389,11 @@ fn fade_exit(row: u32, flags: u32) { @compute @workgroup_size(256) fn evaluate(@builtin(global_invocation_id) id: vec3u) { - // Reductions run over every row; channels over the listed blocks, those the mask names. One - // loop runs either, since a backend compiles each call of the machine whole. + // Reductions run over every live block, channels over the blocks listed and only those the mask + // names. One loop runs either, since a backend compiles each call of the machine whole. let reducing = step == STEP_REDUCE; - var row: u32; - if reducing { - row = item(id); - } else { - row = row_of(id); - } - if row >= mark.rows { + let row = row_of(id); + if row >= live() { return; } let first = select(0u, mark.channels, reducing); @@ -382,8 +421,8 @@ fn evaluate(@builtin(global_invocation_id) id: vec3u) { fn ease(@builtin(global_invocation_id) id: vec3u) { let row = item(id); let flags = mark.stride - 1u; - if row >= mark.rows { - if row < mark.rows + mark.exits { + if row >= live() { + if row < mark.held { fade_exit(row, flags); } return; @@ -413,7 +452,7 @@ fn ease(@builtin(global_invocation_id) id: vec3u) { fn travel(@builtin(global_invocation_id) id: vec3u) { let row = item(id); let words = travel_words(); - if row >= mark.rows + mark.exits || words.x == NONE { + if row >= mark.held || words.x == NONE { return; } let at = word_at(row, words.x); @@ -431,7 +470,7 @@ fn travel(@builtin(global_invocation_id) id: vec3u) { fn carry(@builtin(global_invocation_id) id: vec3u) { let row = item(id); let travel = travel_words().x; - if row >= mark.rows + mark.exits || travel == NONE || !easing() { + if row >= mark.held || travel == NONE || !easing() { return; } state[word_at(row, travel)] = was(row, travel); diff --git a/crates/latkit/shaders/compute/freeze.wesl b/crates/latkit/shaders/compute/freeze.wesl index a5438e0c..9d72d892 100644 --- a/crates/latkit/shaders/compute/freeze.wesl +++ b/crates/latkit/shaders/compute/freeze.wesl @@ -1,6 +1,6 @@ // Freezing: what each row of a mark showed when its transition began, gathered into the frozen // buffer at the row's new place. The remap words list where each new row was (NONE for a row that -// entered), then the rows that left, which fade out past `mark.rows`. Slot words of a mark whose +// entered), then the rows that left, which fade out past the live ones. Slot words of a mark whose // ends point at vertices follow them to their new slots. import package::lib::types::{NONE, ENTERING, item}; @@ -14,7 +14,7 @@ import package::lib::bind::{mark, state}; @compute @workgroup_size(256) fn gather(@builtin(global_invocation_id) id: vec3u) { let i = item(id); - if i >= mark.rows + mark.exits { + if i >= mark.held { return; } let old_state = remap[mark.remap]; diff --git a/crates/latkit/shaders/compute/labels.wesl b/crates/latkit/shaders/compute/labels.wesl index b3979adb..45c414e3 100644 --- a/crates/latkit/shaders/compute/labels.wesl +++ b/crates/latkit/shaders/compute/labels.wesl @@ -19,6 +19,7 @@ import package::lib::bind::{frame, mark, state}; import package::lib::state::extra_at; import package::lib::read::missing_bits; import package::lib::placement::{labels, word, put, candidate, want, seen}; +import package::lib::drawn::held_row; // Buckets of a digit of the select capping labels. const SELECT_BUCKETS: u32 = 2048u; @@ -38,6 +39,12 @@ fn digit(d: u32) -> vec2u { } } +// The held row of the drawn row invocation `id` covers, or NONE past the mark's live rows. +fn labeled(id: vec3u) -> u32 { + let i = item(id); + return select(NONE, held_row(i), i < mark.rows); +} + fn wanting(row: u32) -> bool { return (word(candidate(row) + CANDIDATE_STATE) & WANTS) != 0u; } @@ -60,9 +67,9 @@ fn select_start(@builtin(global_invocation_id) id: vec3u) { @compute @workgroup_size(256) fn select_count(@builtin(global_invocation_id) id: vec3u) { - let row = item(id); + let row = labeled(id); let d = word(select_word(2u)); - if row >= mark.rows || mark.label_max == NONE || d >= 3u || !wanting(row) { + if row == NONE || mark.label_max == NONE || d >= 3u || !wanting(row) { return; } let key = word(candidate(row) + CANDIDATE_KEY); @@ -108,8 +115,8 @@ fn select_find() { @compute @workgroup_size(256) fn select_cut(@builtin(global_invocation_id) id: vec3u) { - let row = item(id); - if row >= mark.rows || mark.label_max == NONE || !wanting(row) || word(select_word(2u)) == 4u { + let row = labeled(id); + if row == NONE || mark.label_max == NONE || !wanting(row) || word(select_word(2u)) == 4u { return; } let at = candidate(row) + CANDIDATE_STATE; @@ -157,8 +164,8 @@ fn reset_cells(@builtin(global_invocation_id) id: vec3u) { @compute @workgroup_size(256) fn claim(@builtin(global_invocation_id) id: vec3u) { - let row = item(id); - if row >= mark.rows || !undecided(row) { + let row = labeled(id); + if row == NONE || !undecided(row) { return; } let at = candidate(row); @@ -177,8 +184,8 @@ fn claim(@builtin(global_invocation_id) id: vec3u) { @compute @workgroup_size(256) fn decide(@builtin(global_invocation_id) id: vec3u) { - let row = item(id); - if row >= mark.rows || !undecided(row) { + let row = labeled(id); + if row == NONE || !undecided(row) { return; } let at = candidate(row); @@ -196,8 +203,8 @@ fn decide(@builtin(global_invocation_id) id: vec3u) { @compute @workgroup_size(256) fn occupy(@builtin(global_invocation_id) id: vec3u) { - let row = item(id); - if row >= mark.rows || (word(candidate(row) + CANDIDATE_STATE) & PLACED_LABEL) == 0u { + let row = labeled(id); + if row == NONE || (word(candidate(row) + CANDIDATE_STATE) & PLACED_LABEL) == 0u { return; } let c = cells_of(row); @@ -210,8 +217,8 @@ fn occupy(@builtin(global_invocation_id) id: vec3u) { @compute @workgroup_size(256) fn ask(@builtin(global_invocation_id) id: vec3u) { - let row = item(id); - if row >= mark.rows { + let row = labeled(id); + if row == NONE { return; } let s = word(candidate(row) + CANDIDATE_STATE); @@ -228,8 +235,8 @@ fn ask(@builtin(global_invocation_id) id: vec3u) { @compute @workgroup_size(256) fn fade(@builtin(global_invocation_id) id: vec3u) { - let row = item(id); - if row >= mark.rows { + let row = labeled(id); + if row == NONE { return; } let at = candidate(row); diff --git a/crates/latkit/shaders/compute/lists.wesl b/crates/latkit/shaders/compute/lists.wesl index 92f857f7..993dda18 100644 --- a/crates/latkit/shaders/compute/lists.wesl +++ b/crates/latkit/shaders/compute/lists.wesl @@ -4,13 +4,14 @@ // entries the list keeps. Each count is held to its share of a u32, so the total never wraps. // Reduce-then-scan needs no forward progress between workgroups, so it is safe on every device. -import package::lib::bind::mark; +import package::lib::bind::{mark, program}; import package::lib::types::{ LIST_ITEMS, LIST_COUNTS, LIST_SUMS, LIST_ENTRIES, LIST_DRAW, LIST_DISPATCH, LIST_KEPT, PATH_TRACE, TRIANGLES, - BLOCK_ROWS, item, + DISPATCH_ROWS, RUN_WORDS, FADE_IN, FADE_OUT, DRAWN_ENTERING, DRAWN_LEAVING, item, }; import package::lib::lists::{lists, list_start}; import package::lib::path::{row_segments, path_point, path_length, PATH}; +import package::lib::drawn::held_row; var tile: array; var shared_items: u32; @@ -35,8 +36,8 @@ fn scan_blocks(@builtin(local_invocation_id) l: vec3u, @builtin(workgroup_id) g: let h = header(); let n = lists[h + LIST_ITEMS]; let counts = lists[h + LIST_COUNTS]; - // Past one dimension's workgroups, blocks of BLOCK_ROWS items step along the second. - let block = g.y * (BLOCK_ROWS / 256u) + g.x; + // Past one dimension's workgroups, runs of DISPATCH_ROWS items step along the second. + let block = g.y * (DISPATCH_ROWS / 256u) + g.x; let i = block * 256u + l.x; // Whatever the counts, the starts climb, so every item's writes end. let share = 0xffffffffu / max(n, 1u); @@ -92,14 +93,40 @@ fn add_sums(@builtin(global_invocation_id) id: vec3u) { } } -// A mark's segments, numbered by its list: each row counts the segments of its path, then writes -// its row at each of their places, with how far along its path each starts, so a segment's instance +// Each drawn row's word, from the runs the program lists: its held row, in the last run starting at +// or before it, and whether that run's tile fades in or out. What every pass drawing or answering for +// the mark reads in one word, written when the runs change. +@compute @workgroup_size(256) +fn expand_runs(@builtin(global_invocation_id) id: vec3u) { + let i = item(id); + if i >= mark.rows + mark.exits { + return; + } + var lo = 0u; + var hi = max(program[mark.row_runs], 1u); + while hi - lo > 1u { + let mid = (lo + hi) / 2u; + if program[mark.row_runs + 1u + mid * RUN_WORDS] <= i { + lo = mid; + } else { + hi = mid; + } + } + let run = mark.row_runs + 1u + lo * RUN_WORDS; + let fade = program[run + 2u]; + let entering = select(0u, DRAWN_ENTERING, (fade & FADE_IN) != 0u); + let leaving = select(0u, DRAWN_LEAVING, (fade & FADE_OUT) != 0u); + lists[mark.drawn + i] = (program[run + 1u] + (i - program[run])) | entering | leaving; +} + +// A mark's segments, numbered by its list: each drawn row counts the segments of its path, then +// writes its number at each of their places, with how far along its path each starts, so a segment's instance // finds its row, its index in it, and where dashes and comets stand at its start. @compute @workgroup_size(256) fn count_segments(@builtin(global_invocation_id) id: vec3u) { let i = item(id); if i < lists[mark.list + LIST_ITEMS] { - lists[lists[mark.list + LIST_COUNTS] + i] = row_segments(i); + lists[lists[mark.list + LIST_COUNTS] + i] = row_segments(held_row(i)); } } @@ -115,12 +142,13 @@ fn write_segments(@builtin(global_invocation_id) id: vec3u) { // An area's triangles have no distance along. let measured = mark.primitive != TRIANGLES && PATH != PATH_TRACE; var along = 0.0; - var last = path_point(i, 0u); + let row = held_row(i); + var last = path_point(row, end - at, 0u); for (var k = at; k < end; k++) { lists[entries + 2u * k] = i; lists[entries + 2u * k + 1u] = bitcast(along); if measured { - let next = path_point(i, k - at + 1u); + let next = path_point(row, end - at, k - at + 1u); along += select(0.0, path_length(last, next), last.shown && next.shown); last = next; } diff --git a/crates/latkit/shaders/compute/probe.wesl b/crates/latkit/shaders/compute/probe.wesl index cd978946..61c11768 100644 --- a/crates/latkit/shaders/compute/probe.wesl +++ b/crates/latkit/shaders/compute/probe.wesl @@ -11,7 +11,9 @@ // // Queries write past the probe's words, each mark from the word its `query` names: `hits` appends every // instance within reach of the query's point while the mark has room; `boxed` sets a bit for every row -// with an instance in the query's box, hidden or not. `locate` writes where one row lies on screen. +// with an instance in the query's box, hidden or not. `locate` writes where one drawn row lies on +// screen. Every row an answer names is a held row, which the view reads back against what the frame +// drew. import package::lib::types::{ NONE, FADE_OUT, POINTS, STROKES, TRIANGLES, MARK_EDGES, PATH_GEOMETRY, PROBE_KEY, PROBE_INSTANCE, PROBE_DEPTH, @@ -22,10 +24,13 @@ import package::lib::bind::{frame, eye, mark, state}; import package::lib::state::row_at; import package::lib::project::{to_screen, clip_of}; import package::lib::points::{point, port_at}; -import package::lib::path::{PATH, Segment, segment, segment_screen, stroke_style, stroke_instances, path_count}; -import package::lib::triangles::{triangle_corner, triangle_row, triangle_instances, area_anchor}; +import package::lib::path::{ + PATH, Segment, segment, path_segment, segment_screen, stroke_style, stroke_instances, path_count, +}; +import package::lib::triangles::{triangle_corner, triangle_item, triangle_instances, area_anchor}; import package::lib::marker::outline_distance; import package::lib::mark::PRIMITIVE; +import package::lib::drawn::{drawn_row, held_row}; // Which step a dispatch takes. struct Step { @@ -53,11 +58,13 @@ fn unreached() -> Reach { // An instance as the frame drew it, on screen, depths as z: a point's center and, for a port, the // port's center as `b`; a segment's ends; a triangle's corners, with whether each is shown, ahead of -// the eye, and in front of it. `part` is a port's number from 1, or a segment's index along its path. +// the eye, and in front of it. `item` is the drawn row it belongs to and `row` that row's held row; +// `part` is a port's number from 1, or a segment's index along its path. struct Seen { + item: u32, row: u32, part: u32, - // The row is the mark's and not fading out. + // The row is one of the mark's live rows, and neither it nor its tile is fading out. kept: bool, // A point is shown; a stroke's style is shown and its segment on screen. shown: bool, @@ -83,19 +90,21 @@ fn nearest_on(p: vec2f, a: vec3f, b: vec3f) -> vec3f { return mix(a, b, t); } -// Whether a row is fading out: it no longer answers the pointer. -fn leaving(row: u32) -> bool { - return (state[row_at(row) + mark.stride - 1u] & FADE_OUT) != 0u; +// Whether drawn row `item`, held as `row`, answers the pointer: one of the mark's live rows, neither +// it nor its tile, which is `leaving` or not, fading out. +fn answers(item: u32, row: u32, leaving: bool) -> bool { + return item < mark.rows && !leaving && (state[row_at(row) + mark.stride - 1u] & FADE_OUT) == 0u; } // A point's marker, or past the rows one of their ports. fn seen_point(instance: u32) -> Seen { var s: Seen; let port = select(0u, instance - mark.rows, instance >= mark.rows); - let row = select(instance, port / max(mark.ports, 1u), instance >= mark.rows); - let pt = point(row); - s.row = row; - s.kept = row < mark.rows && !leaving(row); + s.item = select(instance, port / max(mark.ports, 1u), instance >= mark.rows); + let d = drawn_row(s.item); + s.row = d.row; + let pt = point(s.row); + s.kept = answers(s.item, s.row, d.leaving); s.shown = pt.shown; s.a = screen(pt.clip); s.front = vec3(pt.clip.w > 0.0); @@ -110,18 +119,19 @@ fn seen_point(instance: u32) -> Seen { return s; } -// A segment of a stroke: the instance's, or, locating, the middle one of row `instance`'s path. +// A segment of a stroke: the instance's, or, locating, the middle one of drawn row `instance`'s path. fn seen_segment(instance: u32, locating: bool) -> Seen { var s: Seen; - var g = Segment(instance, 0u, 0.0); + var g: Segment; if locating { - g.index = path_count(instance) / 2u; + g = path_segment(instance, path_count(instance) / 2u, 0.0); } else { g = segment(instance); } + s.item = g.item; s.row = g.row; s.part = g.index; - s.kept = g.row < mark.rows && !leaving(g.row); + s.kept = answers(g.item, g.row, g.leaving); if !s.kept && !locating { return s; } @@ -135,12 +145,13 @@ fn seen_segment(instance: u32, locating: bool) -> Seen { return s; } -// A triangle: a face, or one of an area's. +// A triangle: a face, or one of an area's; locating, drawn row `instance`'s face. fn seen_triangle(instance: u32, locating: bool) -> Seen { var s: Seen; - let row = select(triangle_row(instance), instance, locating); - s.row = row; - s.kept = row < mark.rows && !leaving(row); + s.item = select(triangle_item(instance), instance, locating); + let d = drawn_row(s.item); + s.row = d.row; + s.kept = answers(s.item, s.row, d.leaving); if !s.kept && !locating { return s; } @@ -160,7 +171,7 @@ fn seen_triangle(instance: u32, locating: bool) -> Seen { return s; } -// Instance `instance` of the mark's primitive on screen; locating, row `instance`'s place. +// Instance `instance` of the mark's primitive on screen; locating, drawn row `instance`'s place. fn seen(instance: u32, locating: bool) -> Seen { switch PRIMITIVE { case POINTS: { return seen_point(instance); } @@ -187,7 +198,7 @@ fn reach(s: Seen, p: vec2f) -> Reach { // The nearer end names the sample a trace's hit lies at; for an edge, which of its ends. var end = select(s.part, s.part + 1u, length(q.xy - s.b.xy) < length(q.xy - s.a.xy)); if mark.kind == MARK_EDGES { - end = u32(2u * end >= path_count(s.row)); + end = u32(2u * end >= path_count(s.item)); } return Reach(max(length(p - q.xy) - 0.5 * s.half.x, 0.0), q.z, q.xy, s.row, end); } @@ -246,7 +257,7 @@ fn boxed(s: Seen) -> bool { } } -// Where an area's row `row` lies on screen, and 1 as z where it shows: where its label hangs. +// Where an area's held row `row` lies on screen, and 1 as z where it shows: where its label hangs. fn anchored(row: u32) -> vec3f { let anchor = area_anchor(row); let clip = clip_of(anchor.world); @@ -346,7 +357,7 @@ fn answer(@builtin(global_invocation_id) id: vec3u) { } // An area lies where its label hangs. if PRIMITIVE == TRIANGLES && PATH == PATH_GEOMETRY { - locate(anchored(i)); + locate(anchored(held_row(i))); return; } } else if s == STEP_DESCRIBE { diff --git a/crates/latkit/shaders/draw/glyphs.wesl b/crates/latkit/shaders/draw/glyphs.wesl index 704748e2..9c07cc22 100644 --- a/crates/latkit/shaders/draw/glyphs.wesl +++ b/crates/latkit/shaders/draw/glyphs.wesl @@ -10,11 +10,12 @@ import package::lib::types::{ import package::lib::bind::{frame, eye, mark, state, program, ramp}; import package::lib::state::extra_at; import package::lib::read::missing_bits; -import package::lib::points::{point, port_at, port_inward}; +import package::lib::points::{shown_point, port_at, port_inward}; import package::lib::marker::theme_port_size; import package::lib::label::{anchor, length_px, legibility, text_at, fits, Text}; import package::lib::project::culled; import package::lib::color::output; +import package::lib::drawn::drawn_row; @group(2) @binding(8) var atlas: texture_2d; @group(2) @binding(13) var labels: array; @@ -52,10 +53,12 @@ fn unshown() -> Varyings { @if(!ports) @vertex -fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) row: u32) -> Varyings { - if row >= mark.rows { +fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: u32) -> Varyings { + if instance >= mark.rows { return unshown(); } + let drawn = drawn_row(instance); + let row = drawn.row; let code_bits = state[extra_at(row, mark.label + LABEL_CODE)]; if missing_bits(code_bits) { return unshown(); @@ -67,7 +70,7 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) row: u32) if (uv0 == 0u && uv1 == 0u) || (uv0 & TEXT_ESTIMATE) != 0u { return unshown(); } - let a = anchor(row); + let a = anchor(instance); let size = length_px(mark.label_size); let t = text_at(uv0, uv1, size, a.at, a.align); // A diagram's labels show while they fit and read; elsewhere as placement decided. @@ -75,6 +78,8 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) row: u32) if frame.space == SPACE_DIAGRAM { alpha = f32(a.shown && fits(t.hi - t.lo, a.room)) * legibility(size); } + // A label fades with its row's tile. + alpha *= drawn.presence; if !(alpha > 0.0) { return unshown(); } @@ -88,9 +93,9 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) row: u32) @vertex fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: u32) -> Varyings { let n = max(mark.ports, 1u); - let row = instance / n; + let item = instance / n; let k = instance % n; - if row >= mark.rows { + if item >= mark.rows { return unshown(); } let info = mark.port_info + k * PORT_WORDS; @@ -99,7 +104,7 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: if uv0 == 0u && uv1 == 0u { return unshown(); } - let p = point(row); + let p = shown_point(item); let port = port_at(p, k); let code = program[info + PORT_CODE]; let inward = port_inward(code); diff --git a/crates/latkit/shaders/draw/points.wesl b/crates/latkit/shaders/draw/points.wesl index 760966c2..e4598b53 100644 --- a/crates/latkit/shaders/draw/points.wesl +++ b/crates/latkit/shaders/draw/points.wesl @@ -10,7 +10,7 @@ import package::lib::types::{STATUS, MARKER_ICON, MARKER_SHAPE, MARKER_BLOCK, GR import package::lib::bind::{frame, eye, mark, state, ramp, program, focused}; import package::lib::state::extra_at; import package::lib::project::{to_screen, to_clip, nearer, culled}; -import package::lib::points::{Point, point, reach, port_at}; +import package::lib::points::{Point, shown_point, reach, port_at}; import package::lib::marker::{ MarkerColor, MarkerFragment, builtin_marker, container, border, dropped, outline_distance, box_distance, shape_distance, filled, over, status_ring, glow, premultiplied, theme_text, theme_corner, theme_lift, @@ -48,20 +48,20 @@ struct Drawn { @if(!ports && !focused) fn drawn(instance: u32) -> Drawn { - let p = point(instance); + let p = shown_point(instance); return Drawn(p, to_screen(p.clip), p.half, reach(p, false), mark.shape, p.depth); } @if(focused) fn drawn(instance: u32) -> Drawn { - let p = point(focused[mark.focused + instance]); + let p = shown_point(focused[mark.focused + instance]); return Drawn(p, to_screen(p.clip), p.half, reach(p, true), mark.shape, p.depth); } @if(ports) fn drawn(instance: u32) -> Drawn { let n = max(mark.ports, 1u); - let p = point(instance / n); + let p = shown_point(instance / n); let port = port_at(p, instance % n); let half = vec2f(port.half); return Drawn(p, port.center, half, half + 1.5 * frame.unit + fringe(), port.shape, f32(port.wired)); diff --git a/crates/latkit/shaders/draw/strokes.wesl b/crates/latkit/shaders/draw/strokes.wesl index a49d1253..5e38469e 100644 --- a/crates/latkit/shaders/draw/strokes.wesl +++ b/crates/latkit/shaders/draw/strokes.wesl @@ -48,11 +48,14 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: var out: Varyings; let s = segment(instance); out.row = s.row; - if LIT_DRAW && (s.row >= mark.rows || s.index >= path_count(s.row)) { + if LIT_DRAW && (s.item >= mark.rows || s.index >= path_count(s.item)) { out.clip = culled(); return out; } - let style = stroke_style(s.row); + // A stroke fades and grows with its row's tile. + var style = stroke_style(s.row); + style.color.a *= s.presence; + style.width *= s.growth; let on = segment_screen(s); let ends = on.ends; if !style.shown || !on.shown { diff --git a/crates/latkit/shaders/draw/triangles.wesl b/crates/latkit/shaders/draw/triangles.wesl index ce734c92..37d84c66 100644 --- a/crates/latkit/shaders/draw/triangles.wesl +++ b/crates/latkit/shaders/draw/triangles.wesl @@ -26,11 +26,11 @@ struct Varyings { } @vertex -fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) row: u32) -> Varyings { +fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: u32) -> Varyings { var out: Varyings; - out.row = row; - let c = triangle_corner(row, corner); - if !triangle_shown(row) { + let c = triangle_corner(instance, corner); + out.row = c.row; + if !triangle_shown(instance) { out.clip = culled(); return out; } @@ -39,7 +39,7 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) row: u32) let s = c.slot * 3u; out.normal = vec3f(f32(normals[s]), f32(normals[s + 1u]), f32(normals[s + 2u])); out.color = c.color; - out.focus = focus_of(row); + out.focus = focus_of(c.row); return out; } diff --git a/crates/latkit/shaders/lib/drawn.wesl b/crates/latkit/shaders/lib/drawn.wesl new file mode 100644 index 00000000..cf3940c2 --- /dev/null +++ b/crates/latkit/shaders/lib/drawn.wesl @@ -0,0 +1,37 @@ +// Drawn rows: which held row each row a mark draws is, and how present it is while tiles come and go. +// A table drawn whole draws each of its rows as the held row of its number. For one drawn in part, +// the program lists the runs it draws, and an expansion of them (`compute/lists.wesl`) leaves each +// drawn row's word in the lists at `mark.drawn`: its held row, and whether its tile fades in or out. + +import package::lib::types::{NONE, DRAWN_ROW, DRAWN_ENTERING, DRAWN_LEAVING}; +import package::lib::bind::mark; +import package::lib::lists::lists; + +struct DrawnRow { + row: u32, + // How much of it shows, and how large it grows, as its tile fades in or out; 1 when steady. + presence: f32, + growth: f32, + // Whether its tile is leaving: it no longer answers the pointer. + leaving: bool, +} + +// Drawn row `i` of the current mark. +fn drawn_row(i: u32) -> DrawnRow { + if mark.drawn == NONE { + return DrawnRow(i, 1.0, 1.0, false); + } + let word = lists[mark.drawn + i]; + let entering = (word & DRAWN_ENTERING) != 0u; + let leaving = (word & DRAWN_LEAVING) != 0u; + let presence = select(select(1.0, mark.reveal, entering), 1.0 - mark.reveal, leaving); + return DrawnRow(word & DRAWN_ROW, presence, select(1.0, mark.reveal, entering), leaving); +} + +// The held row drawn row `i` of the current mark is. +fn held_row(i: u32) -> u32 { + if mark.drawn == NONE { + return i; + } + return lists[mark.drawn + i] & DRAWN_ROW; +} diff --git a/crates/latkit/shaders/lib/focus.wesl b/crates/latkit/shaders/lib/focus.wesl index a01b61ae..a7c3a9ab 100644 --- a/crates/latkit/shaders/lib/focus.wesl +++ b/crates/latkit/shaders/lib/focus.wesl @@ -1,10 +1,11 @@ -// Selection and hover: one word of bits per row; and how present a row is while its mark eases. +// Selection and hover: one word of bits per held row; and how present a row is while its mark eases. import package::lib::types::{SELECTED, HOVERED, FADE_IN, FADE_OUT}; import package::lib::bind::{frame, focus, mark}; +// The focus bits of held row `row`; none for a row that left. fn focus_of(row: u32) -> u32 { - if row >= mark.rows { + if row >= mark.held - mark.exits { return 0u; } return focus[mark.focus + row]; diff --git a/crates/latkit/shaders/lib/label.wesl b/crates/latkit/shaders/lib/label.wesl index 5ef4edcc..034f63e9 100644 --- a/crates/latkit/shaders/lib/label.wesl +++ b/crates/latkit/shaders/lib/label.wesl @@ -14,9 +14,10 @@ import package::lib::project::{to_screen, clip_of}; import package::lib::points::point; import package::lib::path::{PATH, Segment, segment_screen, path_count}; import package::lib::triangles::{triangle_corner, area_anchor}; +import package::lib::drawn::held_row; -// Where row `row`'s label hangs, CSS pixels, how it aligns to that point (0 left or top, 0.5 -// centered), the room it must fit in (none when zero), and whether its row shows. +// Where a row's label hangs, CSS pixels, how it aligns to that point (0 left or top, 0.5 centered), +// the room it must fit in (none when zero), and whether its row shows. struct Anchor { at: vec2f, align: vec2f, @@ -29,8 +30,10 @@ fn length_px(size: f32) -> f32 { return size * frame.unit; } -fn anchor(row: u32) -> Anchor { +// Where drawn row `item`'s label hangs. +fn anchor(item: u32) -> Anchor { var a: Anchor; + let row = held_row(item); switch PRIMITIVE { case POINTS: { let p = point(row); @@ -54,10 +57,11 @@ fn anchor(row: u32) -> Anchor { } } case STROKES: { - let count = path_count(row); + let count = path_count(item); // A trace labels its last sample; other paths their middle. let last = PATH == PATH_TRACE; - let on = segment_screen(Segment(row, select(count / 2u, max(count, 1u) - 1u, last), 0.0)); + let index = select(count / 2u, max(count, 1u) - 1u, last); + let on = segment_screen(Segment(row, item, index, 0.0, 1.0, 1.0, false)); a.shown = count > 0u && on.shown; if last { a.at = on.b + vec2f(6.0, 0.0); @@ -80,7 +84,7 @@ fn anchor(row: u32) -> Anchor { var sum = vec2f(0.0); a.shown = true; for (var k = 0u; k < 3u; k++) { - let c = triangle_corner(row, k); + let c = triangle_corner(item, k); let clip = clip_of(c.world); a.shown = a.shown && c.shown && c.ahead && clip.w > 0.0; sum += to_screen(clip); diff --git a/crates/latkit/shaders/lib/path.wesl b/crates/latkit/shaders/lib/path.wesl index e9d59f00..8996808e 100644 --- a/crates/latkit/shaders/lib/path.wesl +++ b/crates/latkit/shaders/lib/path.wesl @@ -18,17 +18,30 @@ import package::lib::focus::{faded, growth}; import package::lib::mark::px; import package::lib::points::vertex_half; import package::lib::lists::{lists, list_entry, list_start, list_total}; +import package::lib::drawn::drawn_row; // The path a pipeline's strokes read: one of the `PATH_` codes, fixed when the pipeline is made. override PATH: u32 = PATH_ENDS; // One segment of a row's path, and how far along the path it starts, in length units: position -// units, degrees on the earth. A list keeps each segment's distance along; a row of one segment -// starts at 0. +// units, degrees on the earth. `row` is the held row, whose state and columns it reads; `item` the +// drawn row, which the mark's list numbers. A list keeps each segment's distance along; a row of one +// segment starts at 0. As its row's tile comes and goes, it shows `presence` of itself, grown by +// `growth`, and while it leaves answers no pointer. struct Segment { row: u32, + item: u32, index: u32, along: f32, + presence: f32, + growth: f32, + leaving: bool, +} + +// Segment `index` of drawn row `item`'s path, starting `along` it: the row looked up once. +fn path_segment(item: u32, index: u32, along: f32) -> Segment { + let d = drawn_row(item); + return Segment(d.row, item, index, along, d.presence, d.growth, d.leaving); } // A point a path runs through: where it stands (position units as high and low parts, elevation), @@ -78,11 +91,11 @@ struct OnScreen { fn segment(instance: u32) -> Segment { if mark.per_row != NONE { let per = max(mark.per_row, 1u); - return Segment(instance / per, instance % per, 0.0); + return path_segment(instance / per, instance % per, 0.0); } let entry = list_entry(mark.list, 2u * instance); - let row = lists[entry]; - return Segment(row, instance - list_start(mark.list, row), bitcast(lists[entry + 1u])); + let item = lists[entry]; + return path_segment(item, instance - list_start(mark.list, item), bitcast(lists[entry + 1u])); } // In the focus draw, piece `instance % mark.pieces` of the lit row `instance / mark.pieces`, as far @@ -90,13 +103,13 @@ fn segment(instance: u32) -> Segment { @if(focused) fn segment(instance: u32) -> Segment { let pieces = max(mark.pieces, 1u); - let row = focused[mark.focused + instance / pieces]; + let item = focused[mark.focused + instance / pieces]; let index = instance % pieces; var along = 0.0; - if mark.per_row == NONE && row < mark.rows && index < path_count(row) { - along = bitcast(lists[list_entry(mark.list, 2u * (list_start(mark.list, row) + index)) + 1u]); + if mark.per_row == NONE && item < mark.rows && index < path_count(item) { + along = bitcast(lists[list_entry(mark.list, 2u * (list_start(mark.list, item) + index)) + 1u]); } - return Segment(row, index, along); + return path_segment(item, index, along); } // Length units between two points of a path: what a segment adds to the distance along it. @@ -117,12 +130,12 @@ fn stroke_instances() -> u32 { return list_total(mark.list); } -// Segments in row `row`'s path, as drawn. -fn path_count(row: u32) -> u32 { +// Segments in drawn row `item`'s path, as drawn. +fn path_count(item: u32) -> u32 { if mark.per_row != NONE { return mark.per_row; } - return list_start(mark.list, row + 1u) - list_start(mark.list, row); + return list_start(mark.list, item + 1u) - list_start(mark.list, item); } // Segments a geodesic from `a` to `b` takes: one per few degrees of arc. @@ -133,9 +146,9 @@ fn arcs(a: vec2f, b: vec2f) -> u32 { return clamp(u32(ceil(degrees(d) / GEODESIC_STEP)), 1u, MAX_PIECES); } -// Points the geometry column gives row `row`: an edge's bends, or a whole path. +// Points the geometry column gives held row `row`: an edge's bends, or a whole path. fn geometry_count(row: u32) -> u32 { - if row >= mark.rows { + if row >= mark.held - mark.exits { return 0u; } return points_of(mark.geometry, row).count; @@ -264,13 +277,13 @@ fn between(a: Vertex, b: Vertex, t: f32, bow: f32) -> PathPoint { // it runs between its ends in as many pieces as its list kept for it, each lane bowing in world units // so the camera never moves it. An end reads only its own vertex. Each kind of point is made in one // place, so shaders compile small. -fn end_point(row: u32, i: u32) -> PathPoint { +fn end_point(row: u32, count: u32, i: u32) -> PathPoint { let e = edge_at(row_at(row)); if e.a == NONE || e.b == NONE || e.a == e.b { return hidden(); } let bends = points_of(mark.geometry, row); - let last = select(bends.count + 1u, path_count(row), bends.count == 0u); + let last = select(bends.count + 1u, count, bends.count == 0u); if i == 0u || i >= last { return vertex_point(select(e.b, e.a, i == 0u)); } @@ -379,10 +392,10 @@ fn pole_point(row: u32, i: u32) -> PathPoint { return p; } -// Point `i` of row `row`'s path, whatever it runs through, placed in the world. -fn path_point(row: u32, i: u32) -> PathPoint { +// Point `i` of held row `row`'s path of `count` segments, whatever it runs through, placed in the world. +fn path_point(row: u32, count: u32, i: u32) -> PathPoint { switch PATH { - case PATH_ENDS: { return end_point(row, i); } + case PATH_ENDS: { return end_point(row, count, i); } case PATH_TRACE: { return placed_point(trace_point(row, i)); } case PATH_GEOMETRY: { return placed_point(geometry_point(row, i)); } case PATH_POLES: { return pole_point(row, i); } @@ -395,7 +408,7 @@ fn path_point(row: u32, i: u32) -> PathPoint { fn segment_ends(s: Segment) -> array { var ends: array; for (var e = 0u; e < 2u; e++) { - ends[e] = path_point(s.row, s.index + e); + ends[e] = path_point(s.row, path_count(s.item), s.index + e); } return ends; } @@ -407,12 +420,12 @@ fn ends_part(s: Segment) -> bool { let next = s.index + 2u; return next >= points.count || missing_bits(point_bits(points, next).x); } - return s.index + 1u == path_count(s.row); + return s.index + 1u == path_count(s.item); } // Lanes from the middle that row `row` runs in among edges joining the same two vertices. fn lane(row: u32) -> f32 { - if mark.lane == NONE || row >= mark.rows { + if mark.lane == NONE || row >= mark.held - mark.exits { return 0.0; } let bits = column_value(mark.lane, row).x; diff --git a/crates/latkit/shaders/lib/points.wesl b/crates/latkit/shaders/lib/points.wesl index e051ec03..7cb27591 100644 --- a/crates/latkit/shaders/lib/points.wesl +++ b/crates/latkit/shaders/lib/points.wesl @@ -12,6 +12,7 @@ import package::lib::focus::{focus_of, faded, growth}; import package::lib::read::bits_value; import package::lib::mark::{px, fringe, within}; import package::lib::marker::{theme_port_size, theme_lift}; +import package::lib::drawn::drawn_row; struct Point { clip: vec4f, @@ -60,6 +61,15 @@ fn point(row: u32) -> Point { return p; } +// The marker of drawn row `item`, faded and grown as its tile comes and goes. +fn shown_point(item: u32) -> Point { + let d = drawn_row(item); + var p = point(d.row); + p.color.a *= d.presence; + p.half *= d.growth; + return p; +} + // Half the marker of the vertex in any slot, CSS pixels: its flags say whether its size is in // position units and whether it has its own height. fn vertex_half(v: Vertex) -> vec2f { diff --git a/crates/latkit/shaders/lib/read.wesl b/crates/latkit/shaders/lib/read.wesl index 3216ca6f..f5bf548d 100644 --- a/crates/latkit/shaders/lib/read.wesl +++ b/crates/latkit/shaders/lib/read.wesl @@ -1,10 +1,11 @@ -// Reading columns: page tables in `program` point at blocks of `pages`. Values stay `u32` bits until -// arithmetic needs them, so a missing value is never an `f32` constant (WGSL cannot represent NaN -// constants). +// Reading columns: page tables in `program` point at blocks of `pages`. A column's pages are its +// data's, or for a mark drawing a table in part the pages of it the mark holds, a slot each; its +// descriptor says how many rows a page holds. Values stay `u32` bits until arithmetic needs them, so +// a missing value is never an `f32` constant (WGSL cannot represent NaN constants). import package::lib::types::{ NONE, MISSING, COLUMN_WORDS, COLUMN_FLAGS, COLUMN_ROWS, COLUMN_TABLE, COLUMN_LOW, COLUMN_JOIN, COLUMN_T, - SAMPLED, STEP, PAGE_BITS, PAGE_MASK, POINT_BITS, POINT_MASK, + COLUMN_BITS, SAMPLED, STEP, POINT_BITS, POINT_MASK, PATH_PAGE_WORDS, }; import package::lib::bind::{pages, program, mark}; @@ -33,10 +34,17 @@ fn page_word(block: u32, offset: u32, fallback: u32) -> u32 { return pages[block + offset]; } +// The page of column `column` row `row` lies in, and where in it. +fn page_of(column: u32, row: u32) -> vec2u { + let bits = column_word(column, COLUMN_BITS); + return vec2u(row >> bits, row & ((1u << bits) - 1u)); +} + // A static column's value as (high, low) bits: the low part carries what f32 drops of an f64. fn static_value(column: u32, row: u32) -> vec2u { - let page = row >> PAGE_BITS; - let local = row & PAGE_MASK; + let at = page_of(column, row); + let page = at.x; + let local = at.y; let table = column_word(column, COLUMN_TABLE); let hi = page_word(program[table + page], local, MISSING); let low = column_word(column, COLUMN_LOW); @@ -46,11 +54,12 @@ fn static_value(column: u32, row: u32) -> vec2u { return vec2u(hi, page_word(program[low + page], local, 0u)); } -// A sampled column's value at the shown coordinate, as bits. Each row block has a cursor of six +// A sampled column's value at the shown coordinate, as bits. Each page of rows has a cursor of six // words: block and word of this frame, block and word of the next frame, and both frames' row counts. fn sample_value(column: u32, row: u32) -> u32 { - let cursor = column_word(column, COLUMN_TABLE) + (row >> PAGE_BITS) * 6u; - let local = row & PAGE_MASK; + let at = page_of(column, row); + let cursor = column_word(column, COLUMN_TABLE) + at.x * 6u; + let local = at.y; if local >= program[cursor + 4u] { return MISSING; } @@ -73,7 +82,8 @@ fn bits_value(column: u32, row: u32) -> u32 { return NONE; } let table = column_word(column, COLUMN_TABLE); - return page_word(program[table + (row >> PAGE_BITS)], row & PAGE_MASK, NONE); + let at = page_of(column, row); + return page_word(program[table + at.x], at.y, NONE); } // The row a column reads for `row`: through its join when it reads another table. @@ -105,24 +115,23 @@ struct Points { count: u32, } -// Row `row`'s points of geometry column `column`. The column's table holds, per page, the block of -// each row's first point, where the page's blocks of points are listed in the table, and how many -// points the page has; a block holds `1 << POINT_BITS` points as x and y bits. +// Row `row`'s points of geometry column `column`. A block holds `1 << POINT_BITS` points as x and y +// bits. fn points_of(column: u32, row: u32) -> Points { if column == NONE || row >= column_word(column, COLUMN_ROWS) { return Points(0u, 0u, 0u); } - let rows = column_word(column, COLUMN_ROWS); let table = column_word(column, COLUMN_TABLE); - let page = table + 3u * (row >> PAGE_BITS); + let at = page_of(column, row); + let page = table + PATH_PAGE_WORDS * at.x; let firsts = program[page]; - if firsts == NONE { + if firsts == NONE || at.y >= program[page + 3u] { return Points(0u, 0u, 0u); } - let local = row & PAGE_MASK; + let local = at.y; let first = pages[firsts + local]; var end = program[page + 2u]; - if local < PAGE_MASK && row + 1u < rows { + if local + 1u < program[page + 3u] { end = pages[firsts + local + 1u]; } return Points(table + program[page + 1u], first, end - first); diff --git a/crates/latkit/shaders/lib/trace.wesl b/crates/latkit/shaders/lib/trace.wesl index 90b2de8b..348773b1 100644 --- a/crates/latkit/shaders/lib/trace.wesl +++ b/crates/latkit/shaders/lib/trace.wesl @@ -1,19 +1,19 @@ // Traces: what a line draws of a sampled field across the axis: runs of entries left to right, each -// of one level, frames or buckets. The traced column's table holds a header (how many runs, row -// blocks, and levels); per run its level's place among the levels, its first entry, how many, and the +// of one level, frames or buckets. The traced column's table holds a header (how many runs, pages of +// rows, and levels); per run its level's place among the levels, its first entry, how many, and the // point it starts at; per level the level, how many axis pages it has, where they are listed, and -// where its row blocks' lists start; per level and row block how many pages of frames or least values +// where its pages' lists start; per level and page of rows how many pages of frames or least values // it has and where they are listed, then of greatest values; then those lists: axis pages as (offset, // first entry), pages of values as (offset, first entry, words per entry). Offsets within the table // are relative to it. A line runs through each frame, and through each bucket's least and greatest // value where it draws; between runs that do not meet, a point is left out, breaking the line. -import package::lib::types::{NONE, MISSING, COLUMN_TABLE, PAGE_BITS, PAGE_MASK}; +import package::lib::types::{NONE, MISSING, COLUMN_TABLE}; import package::lib::bind::{pages, program, mark}; -import package::lib::read::column_word; +import package::lib::read::{column_word, page_of}; const HEADER: u32 = 4u; -const RUN_WORDS: u32 = 4u; +const TRACE_RUN_WORDS: u32 = 4u; const LEVEL_WORDS: u32 = 4u; fn trace_table() -> u32 { @@ -47,14 +47,14 @@ struct TraceRun { // The run point `k` lies in: the last starting at or before it. fn trace_run(k: u32) -> TraceRun { let t = trace_table(); - let r = last_at_most(t + HEADER, max(program[t], 1u), RUN_WORDS, 3u, k); + let r = last_at_most(t + HEADER, max(program[t], 1u), TRACE_RUN_WORDS, 3u, k); return TraceRun(program[r], program[r + 1u], program[r + 2u], program[r + 3u]); } // Where level `slot`'s words start. fn trace_level_at(slot: u32) -> u32 { let t = trace_table(); - return t + HEADER + RUN_WORDS * program[t] + LEVEL_WORDS * slot; + return t + HEADER + TRACE_RUN_WORDS * program[t] + LEVEL_WORDS * slot; } // The level a run of `slot` draws. @@ -75,7 +75,8 @@ fn trace_axis(slot: u32, entry: u32) -> vec2f { // (`side` 0) or greatest (1). fn trace_entry(row: u32, slot: u32, entry: u32, side: u32) -> u32 { let t = trace_table(); - let block = row >> PAGE_BITS; + let at = page_of(mark.trace, row); + let block = at.x; if block >= program[t + 1u] { return MISSING; } @@ -85,7 +86,7 @@ fn trace_entry(row: u32, slot: u32, entry: u32, side: u32) -> u32 { return MISSING; } let e = last_at_most(t + program[lists + 1u], count, 3u, 1u, entry); - let local = row & PAGE_MASK; + let local = at.y; let per = program[e + 2u]; if program[e] == NONE || local >= per || entry < program[e + 1u] { return MISSING; diff --git a/crates/latkit/shaders/lib/triangles.wesl b/crates/latkit/shaders/lib/triangles.wesl index 5d6c474c..1512ebdf 100644 --- a/crates/latkit/shaders/lib/triangles.wesl +++ b/crates/latkit/shaders/lib/triangles.wesl @@ -10,6 +10,7 @@ import package::lib::project::{world, clip_of, shows}; import package::lib::focus::faded; import package::lib::read::{points_of, point_bits, missing_bits}; import package::lib::path::{PATH, Segment, segment, stroke_instances}; +import package::lib::drawn::drawn_row; struct Corner { world: vec3f, @@ -32,12 +33,18 @@ fn triangle_instances() -> u32 { return mark.rows; } -// Corner `corner` of triangle `instance`. +// Corner `corner` of triangle `instance`, faded as its tile comes and goes. fn triangle_corner(instance: u32, corner: u32) -> Corner { if PATH == PATH_GEOMETRY { - return area_point(segment(instance), 1u + corner); + let s = segment(instance); + var c = area_point(s, 1u + corner); + c.color.a *= s.presence; + return c; } - return face_corner(instance, corner); + let d = drawn_row(instance); + var c = face_corner(d.row, corner); + c.color.a *= d.presence; + return c; } // Whether triangle `instance` draws: every corner placed, and one in front of the globe's horizon; @@ -54,10 +61,10 @@ fn triangle_shown(instance: u32) -> bool { return ahead; } -// The row triangle `instance` belongs to. -fn triangle_row(instance: u32) -> u32 { +// The drawn row triangle `instance` belongs to. +fn triangle_item(instance: u32) -> u32 { if PATH == PATH_GEOMETRY { - return segment(instance).row; + return segment(instance).item; } return instance; } @@ -111,7 +118,7 @@ fn area_point(t: Segment, offset: u32) -> Corner { // Where area row `row`'s label hangs: inside its largest polygon, farthest from its edges. fn area_anchor(row: u32) -> Corner { - return area_point(Segment(row, 0u, 0.0), 0u); + return area_point(Segment(row, NONE, 0u, 0.0, 1.0, 1.0, false), 0u); } fn corner_clip(c: Corner) -> vec4f { diff --git a/crates/latkit/shaders/lib/types.wesl b/crates/latkit/shaders/lib/types.wesl index ad8ea89f..1ed16cee 100644 --- a/crates/latkit/shaders/lib/types.wesl +++ b/crates/latkit/shaders/lib/types.wesl @@ -65,6 +65,9 @@ const COLUMN_TABLE: u32 = 2u; const COLUMN_LOW: u32 = 3u; const COLUMN_JOIN: u32 = 4u; const COLUMN_T: u32 = 5u; +// Rows per page of the column's table, as a shift: a page of the data, or a page a table drawn in +// part holds. +const COLUMN_BITS: u32 = 6u; // Column flags. const SAMPLED: u32 = 1u; const STEP: u32 = 2u; @@ -73,14 +76,29 @@ const BITS: u32 = 4u; // Rows per data page, as a shift. const PAGE_BITS: u32 = 16u; const PAGE_MASK: u32 = 65535u; -// Rows a block holds: a page of every column. A dispatch over more items than one dimension holds -// steps a block at a time in its second, 256 workgroups of 256 wide. -const BLOCK_ROWS: u32 = 65536u; - -// The item invocation `id` of a dispatch over items covers: blocks of BLOCK_ROWS along the second +// Rows per page a table drawn in part holds, as a shift: a mark's rows of it lie a page at a time in +// slots, in the order its pyramid draws them. +const HELD_BITS: u32 = 12u; +const HELD_MASK: u32 = 4095u; +// Rows a block holds: what a mark evaluates at a time, a held page. +const BLOCK_ROWS: u32 = 4096u; +// Items a dispatch over more items than one dimension holds steps through at a time in its second +// dimension, 256 workgroups of 256 wide. +const DISPATCH_ROWS: u32 = 65536u; + +// Words of a run of drawn rows in a mark's list of them: where it starts among the drawn rows, the +// held row it starts at, and whether its tile fades in or out (FADE_IN, FADE_OUT). +const RUN_WORDS: u32 = 3u; +// A drawn row's word, as the expansion of the runs writes it: its held row, and whether its tile +// fades in or out. +const DRAWN_ROW: u32 = 0x3fffffffu; +const DRAWN_ENTERING: u32 = 0x40000000u; +const DRAWN_LEAVING: u32 = 0x80000000u; + +// The item invocation `id` of a dispatch over items covers: runs of DISPATCH_ROWS along the second // dimension, when the first holds too few. fn item(id: vec3u) -> u32 { - return id.y * BLOCK_ROWS + id.x; + return id.y * DISPATCH_ROWS + id.x; } // How a word of a row eases while its mark does: kept as it is (no channel stores it), shown as @@ -94,6 +112,9 @@ const BLEND_POSITION: u32 = 4u; // Points per block of a geometry's points, as a shift: a block of x and y words is a chunk. const POINT_BITS: u32 = 15u; const POINT_MASK: u32 = 32767u; +// Words of a geometry column's table per page: the block of each row's first point, where the page's +// blocks of points are listed in the table, how many points the page has, and how many rows. +const PATH_PAGE_WORDS: u32 = 4u; // Vertex state words. const VERTEX_WORDS: u32 = 12u; @@ -486,9 +507,19 @@ struct Mark { index: u32, kind: u32, primitive: u32, - // Rows drawn and evaluated, and rows past them that left and fade out. + // Rows drawn, and rows past them that left and fade out. rows: u32, exits: u32, + // Rows the mark holds evaluated, leaving rows included: of a table drawn whole its rows, of one + // drawn in part its held pages' rows. Every per-row word is a held row's. + held: u32, + // Where the program lists which held rows the drawn rows are, a run at a time, and where the + // lists hold each drawn row's word, expanded from them; NONE where every drawn row is the held + // row of its number, as a table drawn whole draws. + row_runs: u32, + drawn: u32, + // How far the tiles coming and going have faded, from 0 to 1. + reveal: f32, // Each row's kind words: where row 0's start and how many; then its extra words. state: u32, stride: u32, diff --git a/crates/latkit/src/data/bits.rs b/crates/latkit/src/data/bits.rs index e75ce1e3..97cb30b8 100644 --- a/crates/latkit/src/data/bits.rs +++ b/crates/latkit/src/data/bits.rs @@ -40,14 +40,6 @@ impl Bits { Bits { words: words.into(), offset: 0, len } } - /// How many bits are set. - pub(crate) fn count(&self) -> usize { - if self.offset != 0 { - return (0..self.len).filter(|&i| self.get(i)).count(); - } - self.to_words().iter().map(|w| w.count_ones() as usize).sum() - } - pub(crate) fn all(&self) -> bool { if self.offset != 0 { return (0..self.len).all(|i| self.get(i)); diff --git a/crates/latkit/src/data/column.rs b/crates/latkit/src/data/column.rs index d8aefba9..98b2c15c 100644 --- a/crates/latkit/src/data/column.rs +++ b/crates/latkit/src/data/column.rs @@ -116,6 +116,11 @@ impl Column { &self.pages } + /// Whether `other` holds the same pages: the same values, without comparing one. + pub(crate) fn shares(&self, other: &Column) -> bool { + self.len == other.len && self.pages.iter().zip(other.pages.iter()).all(|(a, b)| a.id == b.id) + } + pub(crate) fn like(&self) -> &Values { &self.like } @@ -124,11 +129,25 @@ impl Column { self.pages.get(row >> PAGE_BITS)?.values.get(row & PAGE_MASK) } + /// Where row `row` lies: its page's values and its place among them; `None` past the column's end. + pub(crate) fn at(&self, row: usize) -> Option<(&Values, usize)> { + let values = &self.pages.get(row >> PAGE_BITS)?.values; + let i = row & PAGE_MASK; + (i < values.len()).then_some((values, i)) + } + + /// The values of rows `rows`, in that order, as the GPU reads them: missing past the column's end, + /// as rows a derived table standing in for one not yet derived lacks are. + pub(crate) fn pick(&self, rows: &[u32]) -> Values { + let from: Vec> = rows.iter().map(|&r| Some(r as usize).filter(|&r| r < self.len)).collect(); + let sources: Vec<&Values> = std::iter::once(&self.like).chain(self.pages.iter().map(|p| &p.values)).collect(); + Values::assemble(&sources, Picks::Writes { from: &from, kept: 0 }).expect("one storage") + } + /// The column with `len` rows (at least the current count) and `writes` applied. /// - /// `writes` are `(row, row of source)` sorted by row with no row twice; a [`HOLE`](super::values::HOLE) - /// for the row of source writes a missing value. Pages that no write touches and whose length does - /// not change keep their ids; new rows not written are missing. + /// `writes` are `(row, row of source)` sorted by row with no row twice. Pages that no write + /// touches and whose length does not change keep their ids; new rows not written are missing. pub(crate) fn write(&self, len: usize, writes: &[(usize, usize)], source: &Column) -> Result { debug_assert!(len >= self.len); debug_assert!(writes.windows(2).all(|w| w[0].0 < w[1].0)); @@ -190,6 +209,37 @@ impl Column { Some(rows) } + /// Whether its first `rows` rows hold what they held in `before`, which it was written from a few + /// writes back at most: written since only past them, as appending writes. Writes a page no + /// longer remembers count as written. + pub(crate) fn kept_below(&self, before: &Column, rows: usize) -> bool { + (0..rows.div_ceil(PAGE_ROWS)).all(|p| { + let (Some(now), Some(then)) = (self.pages.get(p), before.pages.get(p)) else { return false }; + let start = p * PAGE_ROWS; + now.id == then.id + || now.rows_since(then.id).is_some_and(|written| written.iter().all(|&r| start + r as usize >= rows)) + }) + } + + /// The rows written since `before`, or added, in order: of each page written over `before`'s, + /// the rows it remembers, else every row of it. Rows `before` held past this column's end are not + /// rows of it. + pub(crate) fn written_since(&self, before: &Column) -> Vec { + let mut rows = Vec::new(); + for (p, now) in self.pages.iter().enumerate() { + let start = p * PAGE_ROWS; + match before.pages.get(p) { + Some(then) if then.id == now.id => {} + Some(then) => match now.rows_since(then.id) { + Some(written) => rows.extend(written.into_iter().map(|r| start + r as usize)), + None => rows.extend(start..start + now.values.len()), + }, + None => rows.extend(start..start + now.values.len()), + } + } + rows + } + /// Every value in row order, as one run: a single page's without copying, else the pages copied /// one after another. pub(crate) fn values(&self) -> Values { @@ -234,8 +284,8 @@ impl Column { rows } - /// The column of rows `rows`, in that order, missing for a [`HOLE`](super::values::HOLE). Leading - /// pages whose rows are unchanged keep their ids. + /// The column of rows `rows`, in that order. Leading pages whose rows are unchanged keep their + /// ids. pub(crate) fn gather(&self, rows: &[usize]) -> Column { let kept = rows.iter().enumerate().take_while(|&(i, &r)| i == r).count(); let kept_pages = (kept / PAGE_ROWS).min(rows.len() / PAGE_ROWS); @@ -269,19 +319,6 @@ mod tests { assert_eq!(written.get(PAGE_ROWS + 4), Some(Value::Number((PAGE_ROWS + 4) as f64))); } - #[test] - fn holes_write_and_gather_missing() { - let column = Column::new(&numbers(4)); - let source = Column::new(&Values::from(vec![7.0_f64])); - let written = column.write(5, &[(1, super::super::values::HOLE), (4, 0)], &source).expect("numbers"); - assert_eq!( - (written.get(0), written.get(1), written.get(4)), - (Some(Value::Number(0.0)), None, Some(Value::Number(7.0))) - ); - let gathered = column.gather(&[3, super::super::values::HOLE, 0]); - assert_eq!((gathered.get(0), gathered.get(1)), (Some(Value::Number(3.0)), None)); - } - #[test] fn growth_pads_with_missing() { let column = Column::new(&numbers(3)); @@ -329,6 +366,33 @@ mod tests { assert_eq!(far.rows_since(&near), Some(vec![7])); } + #[test] + fn rows_written_or_added_since_are_found_page_by_page() { + let column = Column::new(&numbers(PAGE_ROWS + 10)); + let source = Column::new(&Values::from(vec![-1.0_f64])); + let written = column.write(PAGE_ROWS + 12, &[(3, 0)], &source).expect("numbers"); + assert_eq!( + written.written_since(&column), + [3, PAGE_ROWS + 10, PAGE_ROWS + 11], + "the row written, and the rows added past the end" + ); + let fresh = Column::new(&numbers(PAGE_ROWS + 10)); + assert_eq!(fresh.written_since(&column).len(), PAGE_ROWS + 10, "a page not written over is every row"); + assert_eq!(column.written_since(&column), Vec::::new(), "nothing written since itself"); + } + + #[test] + fn rows_appended_keep_the_rows_before_them() { + let column = Column::new(&numbers(PAGE_ROWS + 10)); + let source = Column::new(&Values::from(vec![-1.0_f64])); + let appended = column.write(PAGE_ROWS + 12, &[(PAGE_ROWS + 11, 0)], &source).expect("numbers"); + assert!(appended.kept_below(&column, PAGE_ROWS + 10), "rows appended past them"); + let written = appended.write(appended.len(), &[(PAGE_ROWS + 9, 0)], &source).expect("numbers"); + assert!(written.kept_below(&appended, PAGE_ROWS + 9), "a row written past those asked about"); + assert!(!written.kept_below(&appended, PAGE_ROWS + 10), "and before them"); + assert!(!Column::new(&numbers(PAGE_ROWS + 10)).kept_below(&column, 1), "another column's rows are not"); + } + #[test] fn rows_written_the_value_they_held_have_not_changed() { let column = Column::new(&numbers(PAGE_ROWS + 10)); diff --git a/crates/latkit/src/data/dataset.rs b/crates/latkit/src/data/dataset.rs index 11cda757..a530ab54 100644 --- a/crates/latkit/src/data/dataset.rs +++ b/crates/latkit/src/data/dataset.rs @@ -229,20 +229,6 @@ impl Data { self.0 = Arc::new(next); true } - - /// A version of `self` holding `tables` instead, its name, axis, schema, and origin kept: what a - /// view draws of a dataset it draws in part, eased from one version to the next like `self`'s. - pub(crate) fn drawn(&self, mut tables: Vec) -> Data { - crate::order::by_name(&mut tables, |t| t.name.as_str()); - Data(Arc::new(Inner { - name: self.0.name.clone(), - axis: self.0.axis.clone(), - tables, - version: Version::next(), - origin: self.0.origin, - schema: self.0.schema, - })) - } } impl PartialEq for Data { diff --git a/crates/latkit/src/data/ids.rs b/crates/latkit/src/data/ids.rs index b1796042..d02462ea 100644 --- a/crates/latkit/src/data/ids.rs +++ b/crates/latkit/src/data/ids.rs @@ -170,10 +170,11 @@ impl RowIds { } } + /// The id of row `row`, `None` past the rows. pub(crate) fn key(&self, row: usize) -> Option> { match self { - RowIds::Count(_) => Some(IdRef::Number(row as u64)), - RowIds::Listed(c) => key(&c.pages()[row >> PAGE_BITS].values, row & PAGE_MASK), + RowIds::Count(n) => (row < *n as usize).then_some(IdRef::Number(row as u64)), + RowIds::Listed(c) => c.at(row).and_then(|(values, i)| key(values, i)), } } @@ -202,24 +203,6 @@ impl RowIds { Ok(RowIds::Listed(column.write(len, &writes, &Column::new(&source))?)) } - /// The ids with `len` rows (at least the current count) and `writes` applied, `(row, row of - /// source)` by row, a hole where the row of source is a [`HOLE`](super::values::HOLE). Pages no - /// write touches keep their ids. - pub(crate) fn write(&self, len: usize, writes: &[(usize, usize)], source: &RowIds) -> Result { - // The ids written, gathered first, so a count's ids are made only for those. - let from: Vec = writes.iter().map(|&(_, s)| s).collect(); - let written = match source { - RowIds::Count(_) => { - let ids: Vec> = - from.iter().map(|&s| (s != super::values::HOLE).then_some(s as u64)).collect(); - Column::new(&Values::from(ids)) - } - RowIds::Listed(column) => column.gather(&from), - }; - let at: Vec<(usize, usize)> = writes.iter().enumerate().map(|(k, &(row, _))| (row, k)).collect(); - Ok(RowIds::Listed(self.listed().write(len, &at, &written)?)) - } - /// The ids of `rows`, in that order. pub(crate) fn gather(&self, rows: &[usize]) -> RowIds { if rows.iter().enumerate().all(|(i, &r)| i == r) { @@ -252,8 +235,7 @@ impl RowIds { .collect() } - /// The first row without an id: a hole, which only the rows a view draws of a table drawn through - /// tiles have. + /// The first row without an id, which no table may have. pub(crate) fn hole(&self) -> Option { let RowIds::Listed(column) = self else { return None }; let mut start = 0; @@ -268,12 +250,6 @@ impl RowIds { None } - /// How many rows are holes, without an id. - pub(crate) fn holes(&self) -> usize { - let RowIds::Listed(column) = self else { return 0 }; - column.pages().iter().filter_map(|p| p.values.valid.as_ref().map(|v| p.values.len() - v.count())).sum() - } - /// The id → row index, past holes; an error names a repeated id. pub(crate) fn index(&self) -> Result { let RowIds::Listed(_) = self else { return Ok(Index::Identity(self.len() as u32)) }; @@ -404,14 +380,4 @@ mod tests { assert_eq!(ids.id(0), Some(Id::from("c"))); assert_eq!(RowIds::Count(4).gather(&[0, 2]).id(1), Some(Id::Number(2))); } - - #[test] - fn holes_have_no_id_and_index_past() { - let ids = RowIds::new(&Ids::from(vec!["a", "b", "c"])).gather(&[2, super::super::values::HOLE, 0]); - assert_eq!((ids.id(1), ids.hole()), (None, Some(1))); - let index = ids.index().expect("unique past the hole"); - assert_eq!(index.get(&ids, IdRef::Text("a")), Some(2)); - let numbered = RowIds::Count(3).gather(&[1, super::super::values::HOLE]); - assert_eq!(numbered.index().expect("unique").get(&numbered, IdRef::Number(1)), Some(0)); - } } diff --git a/crates/latkit/src/data/mod.rs b/crates/latkit/src/data/mod.rs index 42c2cee0..7736b514 100644 --- a/crates/latkit/src/data/mod.rs +++ b/crates/latkit/src/data/mod.rs @@ -48,7 +48,6 @@ pub(crate) use { patch::Op, samples::{AxisPage, FRAME_PAGE_WORDS, FramePage, MAX_LEVEL, Run, Samples, bucket_frames, detail, sides}, table::{Inverse, Reference, UNWIRED}, - values::HOLE, }; #[cfg(test)] diff --git a/crates/latkit/src/data/name.rs b/crates/latkit/src/data/name.rs index 961d1a13..56ab247c 100644 --- a/crates/latkit/src/data/name.rs +++ b/crates/latkit/src/data/name.rs @@ -411,6 +411,10 @@ impl Keyed { self.entries.iter().map(|(key, value)| (key, value)) } + pub(crate) fn iter_mut(&mut self) -> impl Iterator { + self.entries.iter_mut().map(|(key, value)| (&*key, value)) + } + pub(crate) fn keys(&self) -> impl Iterator { self.entries.iter().map(|(key, _)| key) } diff --git a/crates/latkit/src/data/samples.rs b/crates/latkit/src/data/samples.rs index 4a108930..731ea251 100644 --- a/crates/latkit/src/data/samples.rs +++ b/crates/latkit/src/data/samples.rs @@ -1085,21 +1085,6 @@ impl Samples { self.gather_runs(rows, &self.runs(), self.start..self.end) } - /// The samples of rows `rows`, in that order, over the frames of `frames` held: what a table drawn - /// in part shows of them about one coordinate. - pub(crate) fn gather_frames(&self, rows: &[usize], frames: Range) -> Samples { - let start = frames.start.max(self.start); - let frames = start..frames.end.min(self.end).max(start); - let runs: Vec = self - .runs() - .into_iter() - .filter(|r| r.level == 0) - .map(|r| Run { level: 0, entries: r.entries.start.max(frames.start)..r.entries.end.min(frames.end) }) - .filter(|r| !r.entries.is_empty()) - .collect(); - self.gather_runs(rows, &runs, frames) - } - /// The samples of rows `rows` over `runs`, reading frames `frames`. fn gather_runs(&self, rows: &[usize], runs: &[Run], frames: Range) -> Samples { let mut gathered = Samples::from_frame(frames.start); @@ -1250,8 +1235,6 @@ mod tests { let gathered = samples.gather(&[1]); assert_eq!(gathered.value(0, 0), Some(2.0)); assert_eq!(gathered.value(0, 1), Some(4.0)); - let one = samples.gather_frames(&[0], 1..2); - assert_eq!((one.start(), one.end(), one.value(0, 1), one.value(0, 0)), (1, 2, Some(3.0), None)); } /// Level 1 buckets 0..4 (frames 0..32) and the frames of bucket 1, of two rows. diff --git a/crates/latkit/src/data/table.rs b/crates/latkit/src/data/table.rs index 6a67f000..90b41880 100644 --- a/crates/latkit/src/data/table.rs +++ b/crates/latkit/src/data/table.rs @@ -1,7 +1,7 @@ use std::sync::{Arc, OnceLock}; use super::bits::Bits; -use super::column::{Column, Page}; +use super::column::Column; use super::ids::{Ids, Index, RowIds}; use super::name::{Id, IdRef, Name, PageId, Version}; use super::samples::Samples; @@ -131,6 +131,23 @@ impl Reference { } } + /// The row row `row` names, [`UNWIRED`] where it names none or lies past the rows: read where its + /// page stores it, as [`targets`](Reference::targets) reads every row. + pub(crate) fn target_row(&self, row: usize) -> u32 { + let Some((values, i)) = self.rows.at(row) else { return UNWIRED }; + match (&values.array, &values.valid) { + (Array::U32(rows), None) => rows[i], + (Array::U32(rows), Some(valid)) => { + if valid.get(i) { + rows[i] + } else { + UNWIRED + } + } + _ => self.target(row).unwrap_or(UNWIRED), + } + } + /// Every row's target row, [`UNWIRED`] where it names none: read page by page, not value by value. pub(crate) fn targets(&self) -> Vec { let mut out = Vec::with_capacity(self.rows.len()); @@ -175,48 +192,6 @@ impl Reference { }) } - /// This reference for ids `ids`, naming rows `targets` of a target table of `order` and - /// `structure`, [`UNWIRED`] where none: resolved already, as a table drawn in part holds it. - /// `targets` holds a row for each id. A page of `kept`, as it was resolved before, whose ids and - /// targets are those of the same page now, keeps its id, so it does not upload again. - pub(crate) fn resolved_to( - &self, - ids: Column, - targets: &[u32], - order: Version, - structure: Version, - kept: Option<&Reference>, - ) -> Reference { - let mut pages = Vec::with_capacity(ids.pages().len()); - let mut resolved = Vec::with_capacity(ids.pages().len()); - let mut start = 0; - for (p, page) in ids.pages().iter().enumerate() { - let rows = &targets[start..start + page.values.len()]; - start += rows.len(); - let dangling = (0..rows.len()).any(|i| rows[i] == UNWIRED && page.values.is_valid(i)); - let same = - kept.and_then(|k| Some((k.rows.pages().get(p)?, k.ids.pages().get(p)?))).filter(|(old, old_ids)| { - old_ids.id == page.id && matches!(&old.values.array, Array::U32(old) if old[..] == *rows) - }); - if let Some((old, _)) = same { - pages.push(old.clone()); - } else { - let valid = Bits::from_fn(rows.len(), |i| rows[i] != UNWIRED); - let values = Values { array: Array::U32(rows.to_vec().into()), valid: (!valid.all()).then_some(valid) }; - pages.push(Page::new(values)); - } - resolved.push(Resolved { ids: page.id, order, structure, dangling }); - } - Reference { - to: self.to.clone(), - ids, - rows: Column::of_pages(pages, &Values::from(Vec::::new())), - resolved: resolved.into(), - version: Version::next(), - inverse: Arc::default(), - } - } - /// Whether `rows` must be resolved again against a target with these versions. pub(crate) fn stale(&self, order: Version, structure: Version) -> bool { self.resolved.len() != self.ids.pages().len() diff --git a/crates/latkit/src/data/values.rs b/crates/latkit/src/data/values.rs index f7b8b737..6aed9b11 100644 --- a/crates/latkit/src/data/values.rs +++ b/crates/latkit/src/data/values.rs @@ -135,19 +135,15 @@ impl schemars::JsonSchema for Value { } } -/// A row picked that names none: the value made of it is missing, as a hole among the rows drawn -/// of a table drawn through tiles is. -pub(crate) const HOLE: usize = usize::MAX; - /// Where each value an assembly makes comes from, among its sources: the first, then pages of /// [`PAGE_ROWS`](super::column::PAGE_ROWS) rows each. A concrete type rather than a closure, so /// [`Values::assemble`] is compiled once. #[derive(Clone, Copy)] pub(crate) enum Picks<'a> { - /// Value `i` is row `rows[i]` of the pages, missing for a [`HOLE`]. + /// Value `i` is row `rows[i]` of the pages. Rows(&'a [usize]), - /// Value `i` is row `from[i]` of the pages where it is written, missing for a [`HOLE`], else row - /// `i` of the first source while `i < kept`, else missing. + /// Value `i` is row `from[i]` of the pages where it is written, else row `i` of the first source + /// while `i < kept`, else missing. Writes { from: &'a [Option], kept: usize }, } @@ -162,7 +158,7 @@ impl Picks<'_> { /// The source and row of value `i`, or `None` where it is missing. #[inline] fn get(self, i: usize) -> Option<(usize, usize)> { - let paged = |row: usize| (row != HOLE).then_some((1 + (row >> PAGE_BITS), row & PAGE_MASK)); + let paged = |row: usize| Some((1 + (row >> PAGE_BITS), row & PAGE_MASK)); match self { Picks::Rows(rows) => paged(rows[i]), Picks::Writes { from, kept } => match from[i] { @@ -302,22 +298,6 @@ impl Values { } } - /// The values as `u32`s, when they are stored so and none is missing. - pub(crate) fn u32s(&self) -> Option<&[u32]> { - match (&self.array, &self.valid) { - (Array::U32(v), None) => Some(v), - _ => None, - } - } - - /// The values as `u64`s, when they are stored so and none is missing. - pub(crate) fn u64s(&self) -> Option<&[u64]> { - match (&self.array, &self.valid) { - (Array::U64(v), None) => Some(v), - _ => None, - } - } - pub(crate) fn text(&self) -> Option<&Text> { match &self.array { Array::Text(t) => Some(t), diff --git a/crates/latkit/src/derive/lanes.rs b/crates/latkit/src/derive/lanes.rs index c7452936..fddafae4 100644 --- a/crates/latkit/src/derive/lanes.rs +++ b/crates/latkit/src/derive/lanes.rs @@ -9,7 +9,7 @@ use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use super::{Derive, Derived, Input, Jobs, Tables, beside, kept, stamp_of}; use crate::Mark; use crate::Pending; -use crate::data::{Field, Ids, Keyed, Name, Patch, Reference, Table, UNWIRED, Values}; +use crate::data::{Field, Keyed, Name, Reference, Table, UNWIRED, Values}; use crate::host::Job; /// Microseconds a row takes to find its lane, by estimate. @@ -100,17 +100,9 @@ fn reference<'t>(table: &'t Table, name: &str) -> Option<&'t Reference> { } } -/// The lanes a lanes job answered: its table's field `lane`, or none. +/// The lanes a lanes job answered, a little-endian `f32` each; none where it answered nothing. fn answered(bytes: &[u8]) -> Option> { - let patch = Patch::from_bytes(bytes).ok()?; - patch.ops.into_iter().find_map(|op| match op { - crate::data::Op::Set(table) => { - let Some(Field::Values(column, _)) = table.field_named("lane") else { return None }; - let pages = column.pages().iter(); - Some(pages.flat_map(|page| (0..page.values.len()).map(|i| page.values.number(i) as f32)).collect()) - } - _ => None, - }) + (!bytes.is_empty()).then(|| bytes.as_chunks::<4>().0.iter().map(|&w| f32::from_le_bytes(w)).collect()) } /// Each row's lane among the rows joining the same two vertices, from the rows' two ends @@ -150,12 +142,6 @@ pub(crate) fn lanes(from: &[u32], to: &[u32]) -> Option> { ) } -/// Lanes as a job's answer: a table of every row's lane, or one without rows when there are none. -pub(crate) fn answer(from: &[u32], to: &[u32]) -> Patch { - let lanes = lanes(from, to).unwrap_or_default(); - Patch::new().set(Table::new("lanes", Ids::count(lanes.len() as u32)).field("lane", lanes)) -} - /// Each of `rows` rows' lane among the rows joining the same two ends, which `ends` names: the /// sheet's straight wires spread by it. `None` when no two share their ends. pub(crate) fn spread(rows: usize, ends: impl Fn(usize) -> Option<[usize; 2]>) -> Option> { diff --git a/crates/latkit/src/derive/mod.rs b/crates/latkit/src/derive/mod.rs index 9d5a19ef..3dcfaaf8 100644 --- a/crates/latkit/src/derive/mod.rs +++ b/crates/latkit/src/derive/mod.rs @@ -13,16 +13,17 @@ pub(crate) mod graph; mod lanes; +pub(crate) mod order; pub(crate) mod places; pub(crate) mod pyramid; -mod shapes; +pub(crate) mod shapes; pub(crate) mod sight; mod stars; pub(crate) mod tiled; pub(crate) mod tiles; mod tiling; -pub(crate) use lanes::{answer as lanes_answer, spread}; +pub(crate) use lanes::{lanes, spread}; use std::hash::{Hash, Hasher}; use std::sync::{Arc, Mutex}; @@ -127,9 +128,9 @@ impl Derived { } } -/// Microseconds of work, by estimate, the derivations do on one frame between them; what does not fit -/// goes to the host as jobs. -const FRAME_WORK: u32 = 4000; +/// Microseconds of work, by estimate, the derivations of a GPU's views do on one frame between them; +/// what does not fit goes to the host as jobs. +pub(crate) const FRAME_WORK: u32 = 4000; /// The frame's budget of inline work and the host's pool, which every derivation shares. Each job /// and fetch wakes the view when it lands. @@ -144,13 +145,15 @@ pub(crate) struct Jobs<'a> { } impl<'a> Jobs<'a> { + /// The jobs of a frame with `left` microseconds of work on it still to spend. pub(crate) fn new( host: Option<&'a dyn Host>, waker: &'a Waker, counters: &'a Counters, errors: &'a Arc>>, + left: u32, ) -> Jobs<'a> { - Jobs { host, waker, counters, errors, left: FRAME_WORK } + Jobs { errors, host, waker, counters, left } } /// Whether work of `cost` microseconds fits what is left of the frame's budget, spending it if so. @@ -195,8 +198,8 @@ impl<'a> Jobs<'a> { } } -/// What every derivation reads: the scene and its data, the host that measures text, and whether -/// rows are being dragged. Jobs go through the frame's [`Jobs`]. +/// What every derivation reads: the scene and its data, the host that measures text, whether rows +/// are being dragged, and what each table drawn in part draws. Jobs go through the frame's [`Jobs`]. pub(crate) struct Input<'a> { pub(crate) scene: &'a Scene, /// Bumps whenever the scene changes. @@ -205,6 +208,16 @@ pub(crate) struct Input<'a> { pub(crate) host: Option<&'a dyn Host>, /// Whether rows are being dragged: what they move derives on the frame, however much. pub(crate) dragging: bool, + /// Each table drawn in part, by dataset and table: what row-local derivations work on alone. + pub(crate) paged: Option<&'a Keyed<(Name, Name), Paged>>, +} + +/// What a table drawn in part draws this frame: the order its rows draw in, the pages of it drawn, +/// and the places of the rows kept whose pages are not, each ascending. +pub(crate) struct Paged { + pub(crate) order: order::Order, + pub(crate) pages: Vec, + pub(crate) kept: Vec, } impl Input<'_> { @@ -414,6 +427,12 @@ impl Derivations { .find_map(|tables| tables.get(&key)); [first, self.shapes.tables().get(&key)] } + + /// The shapes of table `table` of dataset `data` drawn in part, a page of its order at a time, + /// by the field each is written as. + pub(crate) fn paged(&self, data: &Name, table: &Name) -> Vec<(Name, &shapes::PagedShapes)> { + self.shapes.paged(data, table) + } } /// A table beside `source`, row for row, holding `fields`. diff --git a/crates/latkit/src/derive/order.rs b/crates/latkit/src/derive/order.rs new file mode 100644 index 00000000..27139e8f --- /dev/null +++ b/crates/latkit/src/derive/order.rs @@ -0,0 +1,223 @@ +//! Orders: how a table drawn in part draws its rows. Every per-row input a mark reads of such a table, +//! and every per-row word it keeps, lies in pages of its order, so the camera choosing tiles chooses +//! ranges of pages and nothing is gathered as it moves. Until its pyramid lands a table draws an even +//! sample of its rows first, after the rows the edges a sample draws end at, which land with those +//! edges as in a pyramid; a pyramid serves while the rows it was made from hold, the rows appended +//! since drawing after them in their own order. + +use std::sync::Arc; + +use super::pyramid::Pyramid; +use super::stamp_of; +use crate::gpu::shaders as wgsl; + +/// Rows a page of an order holds: a held page. +pub(crate) const PAGE: u32 = wgsl::BLOCK_ROWS; + +#[derive(Clone, Debug)] +pub(crate) enum Order { + /// The rows `lifted` lists first; then every `step`th of the rest of a table's `rows` rows, then + /// the rest, each in their own order. + Sampled { rows: u32, step: u32, lifted: Lifted }, + /// A pyramid's order of a table's first rows, then the rest of its `rows` rows, appended since, + /// in their own order. + Pyramid { pyramid: Arc, rows: u32 }, +} + +/// The rows a sample draws before its own, as a pyramid lands a vertex with the coarsest edge ending +/// at it: those the edges a sample draws end at. Ascending, with where each draws in the sample +/// without them, ascending too. +#[derive(Clone, Debug, Default)] +pub(crate) struct Lifted { + rows: Arc<[u32]>, + places: Arc<[u32]>, + stamp: u64, +} + +impl Lifted { + /// `rows` of a table of `len` rows sampled every `step`th. + pub(crate) fn new(mut rows: Vec, len: u32, step: u32) -> Lifted { + rows.retain(|&r| r < len); + rows.sort_unstable(); + rows.dedup(); + let mut places: Vec = rows.iter().map(|&r| sample_rank(r, len, step)).collect(); + places.sort_unstable(); + let stamp = stamp_of(&rows); + Lifted { rows: rows.into(), places: places.into(), stamp } + } + + fn len(&self) -> u32 { + self.rows.len() as u32 + } +} + +/// Where row `r` draws in an even sample of `rows` rows every `step`th: the sample first, then the +/// rest, each in their own order. +fn sample_rank(r: u32, rows: u32, step: u32) -> u32 { + if r.is_multiple_of(step) { r / step } else { rows.div_ceil(step) + r - r / step - 1 } +} + +/// The row drawn at `d` in an even sample of `rows` rows every `step`th. +fn sample_source(d: u32, rows: u32, step: u32) -> u32 { + let first = rows.div_ceil(step); + if d < first || step <= 1 { + return d * step; + } + // Each step's rows but its first, in their own order. + let k = d - first; + (k / (step - 1)) * step + k % (step - 1) + 1 +} + +/// The `e`th place, from 0, that `places`, ascending, does not list. +fn unlisted(places: &[u32], e: u32) -> u32 { + let (mut lo, mut hi) = (e, e + places.len() as u32); + while lo < hi { + let mid = lo + (hi - lo) / 2; + // Places up to `mid` it does not list. + let free = mid + 1 - places.partition_point(|&p| p <= mid) as u32; + if free > e { + hi = mid; + } else { + lo = mid + 1; + } + } + lo +} + +impl Order { + /// Rows it orders: every row of its table. + pub(crate) fn len(&self) -> u32 { + match self { + Order::Sampled { rows, .. } | Order::Pyramid { rows, .. } => *rows, + } + } + + /// Pages of it. + pub(crate) fn pages(&self) -> u32 { + self.len().div_ceil(PAGE) + } + + /// The rows a sample draws first: those lifted, then its own. + pub(crate) fn sampled(&self) -> u32 { + match self { + Order::Sampled { rows, step, lifted } => { + let first = rows.div_ceil(*step); + lifted.len() + first - lifted.places.partition_point(|&p| p < first) as u32 + } + Order::Pyramid { .. } => 0, + } + } + + /// The row drawn at `d`. + pub(crate) fn source(&self, d: u32) -> u32 { + match self { + Order::Sampled { rows, step, lifted } => match lifted.rows.get(d as usize) { + Some(&r) => r, + None => sample_source(unlisted(&lifted.places, d - lifted.len()), *rows, *step), + }, + Order::Pyramid { pyramid, .. } => pyramid.order.get(d as usize).copied().unwrap_or(d), + } + } + + /// Where row `r` draws. + pub(crate) fn rank(&self, r: u32) -> u32 { + match self { + Order::Sampled { rows, step, lifted } => { + if let Ok(i) = lifted.rows.binary_search(&r) { + return i as u32; + } + let place = sample_rank(r, *rows, *step); + lifted.len() + place - lifted.places.partition_point(|&p| p < place) as u32 + } + Order::Pyramid { pyramid, .. } => pyramid.rank.get(r as usize).copied().unwrap_or(r), + } + } + + /// The rows page `page` holds, in order. + pub(crate) fn page(&self, page: u32) -> Vec { + let (start, end) = (page * PAGE, ((page + 1) * PAGE).min(self.len())); + let Order::Sampled { rows, step, lifted } = self else { return (start..end).map(|d| self.source(d)).collect() }; + // The rows lifted it holds, then the sample's in turn, stepping past the places lifted. + let m = lifted.len(); + let mut out: Vec = + lifted.rows.get(start.min(m) as usize..end.min(m) as usize).unwrap_or_default().to_vec(); + if end > m { + let places = &lifted.places; + let mut place = unlisted(places, start.max(m) - m); + let mut next = places.partition_point(|&p| p <= place); + for _ in start.max(m)..end { + out.push(sample_source(place, *rows, *step)); + place += 1; + while places.get(next) == Some(&place) { + (place, next) = (place + 1, next + 1); + } + } + } + out + } + + /// What pages packed in it are kept by: one pyramid's whatever rows were appended since, a + /// sample's by its rows and those it lifts. + pub(crate) fn id(&self) -> u64 { + match self { + Order::Sampled { rows, step, lifted } => stamp_of(("sampled", rows, step, lifted.stamp)), + Order::Pyramid { pyramid, .. } => stamp_of(pyramid.made), + } + } +} + +impl PartialEq for Order { + fn eq(&self, other: &Order) -> bool { + self.id() == other.id() && self.len() == other.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample(rows: u32, step: u32) -> Order { + Order::Sampled { rows, step, lifted: Lifted::default() } + } + + #[test] + fn samples_draw_every_step_first_then_the_rest() { + let order = sample(10, 3); + let drawn: Vec = (0..10).map(|d| order.source(d)).collect(); + assert_eq!(drawn, [0, 3, 6, 9, 1, 2, 4, 5, 7, 8]); + for (d, &r) in drawn.iter().enumerate() { + assert_eq!(order.rank(r), d as u32, "row {r} draws where its rank says"); + } + assert_eq!(order.sampled(), 4); + assert_eq!(sample(4, 1).page(0), [0, 1, 2, 3], "a step of one is the rows' own order"); + } + + #[test] + fn rows_lifted_draw_before_the_sample_and_leave_it() { + // Rows 4 and 6 lifted: 6 was a sample's row, 4 was not. + let order = Order::Sampled { rows: 10, step: 3, lifted: Lifted::new(vec![6, 4, 6, 12], 10, 3) }; + let drawn: Vec = (0..10).map(|d| order.source(d)).collect(); + assert_eq!(drawn, [4, 6, 0, 3, 9, 1, 2, 5, 7, 8]); + for (d, &r) in drawn.iter().enumerate() { + assert_eq!(order.rank(r), d as u32, "row {r} draws where its rank says"); + } + assert_eq!(order.sampled(), 5, "the rows lifted, then the sample's own"); + assert_ne!(order.id(), sample(10, 3).id(), "pages packed in it are its own"); + // A page steps through the sample past the places lifted, as each row's source finds them. + let rows = 3 * PAGE + 17; + let lifted = Lifted::new((0..rows).filter(|r| r % 5 == 1 || r % 7 == 0).collect(), rows, 4); + let order = Order::Sampled { rows, step: 4, lifted }; + for page in 0..order.pages() { + let one: Vec = (page * PAGE..((page + 1) * PAGE).min(rows)).map(|d| order.source(d)).collect(); + assert_eq!(order.page(page), one, "page {page}"); + } + } + + #[test] + fn rows_appended_after_a_pyramid_draw_after_it_in_their_own_order() { + let pyramid = Arc::new(Pyramid::new(vec![2, 0, 1], Vec::new())); + let order = Order::Pyramid { pyramid, rows: 5 }; + assert_eq!(order.page(0), [2, 0, 1, 3, 4]); + assert_eq!((order.rank(2), order.rank(4)), (0, 4)); + } +} diff --git a/crates/latkit/src/derive/pyramid.rs b/crates/latkit/src/derive/pyramid.rs index 34b20b7c..42c47cfd 100644 --- a/crates/latkit/src/derive/pyramid.rs +++ b/crates/latkit/src/derive/pyramid.rs @@ -1,23 +1,28 @@ //! Pyramids: a table's rows grouped into tiles by the coarsest level each shows at, worked out in a -//! job. Each level splits the tiles above it four ways, and each tile holds a grid of cells, a row to -//! a cell, so no tile holds more rows than its grid has cells. Rows land by priority: each at the -//! coarsest level where its cell is free, so what matters more shows from farther out, and rows -//! crowding one cell wait for a finer level, where they spread over cells of their own. A row never -//! lands finer than its size allows, so its tile grown by half its own size holds it whole. An edge -//! lands by its own priority, in the cell of its middle, and edges joining the same two vertices land -//! together; what draws an edge draws its ends with it. A table drawn whole by priority is one tile, -//! its rows in priority order. +//! job, a dataset's tables in one. Each level splits the tiles above it four ways, and each tile +//! holds a grid of cells, a row to a cell. Rows land by priority: each at the coarsest level where its +//! cell is free, so what matters more shows from farther out, and rows crowding one cell wait for a +//! finer level, where they spread over cells of their own. A row never lands finer than its size +//! allows, so its tile grown by half its own size holds it whole. An edge lands by its own priority, +//! in the cell of its middle, and edges joining the same two vertices land together; a vertex lands +//! no finer than the coarsest edge ending at it, so the ends of every edge drawn show from as far out +//! as it does. A table drawn whole by priority is one tile, its rows in priority order. +//! +//! A pyramid is an order: rows by tile, coarsest level first, each tile's rows together, and the +//! place of each row in it. The camera chooses tiles, each a range of the order. use std::ops::Range; use std::sync::Arc; use rustc_hash::{FxHashMap, FxHashSet}; +use super::order::PAGE; use super::sight::{Domain, Region, Sight, TILE_PX, meets}; -use crate::data::{Field, Ids, Op, Patch, Table}; +use crate::data::{UNWIRED, Version}; use crate::tiles::TileKey; -/// Cells across a tile's grid, as a shift: a tile holds at most `4^GRID_BITS` rows of its level. +/// Cells across a tile's grid, as a shift: a tile holds at most `4^GRID_BITS` rows of its level, but +/// for the ends of edges, which land with them. pub(crate) const GRID_BITS: u32 = 7; /// The finest level. pub(crate) const MAX_LEVEL: u8 = 24; @@ -25,6 +30,11 @@ pub(crate) const MAX_LEVEL: u8 = 24; const NEVER: u8 = u8::MAX; /// Tiles a camera may ask a pyramid for at most: past it, tiles stop splitting. const MAX_TILES: usize = 1024; +/// Cells across a tile's grid of sub-cells, as a shift: the parts of a tile the camera may ask for +/// alone, each a range of the order, so what draws of a large tile is what the camera sees of it. +const SUB_BITS: u32 = 3; +/// Rows past which a tile is asked for a sub-cell at a time. +const SUB_ROWS: usize = 256; /// What a pyramid job places: tables of rows in a square of their space. #[derive(Clone, Debug)] @@ -89,17 +99,25 @@ fn anchored(places: &Places, domain: &Domain) -> (Vec<[u32; 2]>, Vec) { } } -/// The pyramid of each table a job answered for, by its place among the tables asked, with where -/// each row lies across the square and how far it reaches. -pub(crate) fn answer(leveling: &Leveling) -> Patch { +/// Where one table's rows lie across the square, how far each reaches, the coarsest level each may +/// land at, and for an edge table each row's pair of ends. +struct Anchored { + anchors: Vec<[u32; 2]>, + extents: Vec, + floor: Vec, + pairs: Option>, +} + +/// The pyramid of each table a job answered for, by its place among the tables asked. +pub(crate) fn answer(leveling: &Leveling) -> Vec<(u32, Pyramid)> { let wraps = leveling.domain == Domain::Earth; - let mut anchors: Vec> = Vec::with_capacity(leveling.tables.len()); - let mut patch = Patch::new(); - for (i, table) in leveling.tables.iter().enumerate() { + let tables = &leveling.tables; + let mut placed: Vec = Vec::with_capacity(tables.len()); + for table in tables { let n = table.rows as usize; - let (own_anchors, extents, floor, pairs) = if let Some((target, ends)) = &table.ends { - let Some(at) = anchors.get(*target as usize) else { return Patch::new() }; - let (own, extents) = by_ends(ends, at, wraps, n); + let anchoring = if let Some((target, ends)) = &table.ends { + let Some(at) = placed.get(*target as usize).map(|a| &a.anchors) else { return Vec::new() }; + let (anchors, extents) = by_ends(ends, at, wraps, n); // A row with an end naming no row draws nothing: it lands only at the finest level. let floor = (0..n) .map(|r| { @@ -108,45 +126,64 @@ pub(crate) fn answer(leveling: &Leveling) -> Patch { }) .collect(); let pairs = (ends.len() == 2).then(|| pairs(&ends[0], &ends[1])); - (own, extents, floor, pairs) + Anchored { anchors, extents, floor, pairs } } else { - let (own, extents) = anchored(&table.places, &leveling.domain); - (own, extents, vec![0; n], None) + let (anchors, extents) = anchored(&table.places, &leveling.domain); + Anchored { anchors, extents, floor: vec![0; n], pairs: None } }; - let leveled = table.leveled && own_anchors.len() == n; - let sorted = if leveled { along(&own_anchors) } else { Vec::new() }; - let ranks = ranks(&table.priority, n); - let level = if leveled { place(&sorted, &extents, &ranks, &floor, pairs.as_deref()) } else { vec![0; n] }; - if table.answered { - let (order, tiles) = if leveled { - tiled(&sorted, &own_anchors, &extents, &ranks, &level) - } else { - let mut order: Vec = (0..table.rows).collect(); - crate::order::by_key(&mut order, |&r| ranks[r as usize]); - (order, vec![(pack(TileKey { z: 0, x: 0, y: 0 }), table.rows, u32::MAX)]) - }; - let (keys, (ends, reaches)): (Vec, (Vec, Vec)) = - tiles.into_iter().map(|(k, e, r)| (k, (e, r))).unzip(); - patch = patch - .set( - Table::new(format!("tiles{i}"), Ids::count(keys.len() as u32)) - .field("key", keys) - .field("end", ends) - .field("reach", reaches), - ) - .set(Table::new(format!("order{i}"), Ids::count(order.len() as u32)).field("row", order)); - if leveled { - let (u, v): (Vec, Vec) = own_anchors.iter().map(|a| (a[0], a[1])).unzip(); - let mut placed = Table::new(format!("anchors{i}"), Ids::count(n as u32)).field("u", u).field("v", v); - if !extents.is_empty() { - placed = placed.field("extent", extents.clone()); + placed.push(anchoring); + } + let leveled = |i: usize| tables[i].leveled && placed[i].anchors.len() == tables[i].rows as usize; + let mut sorted: Vec> = Vec::with_capacity(tables.len()); + let mut ranked: Vec> = Vec::with_capacity(tables.len()); + for (i, table) in tables.iter().enumerate() { + sorted.push(if leveled(i) { along(&placed[i].anchors) } else { Vec::new() }); + ranked.push(ranks(&table.priority, table.rows as usize)); + } + let level_of = |i: usize, ceiling: &[u8]| -> Vec { + if !leveled(i) { + return vec![0; tables[i].rows as usize]; + } + let a = &placed[i]; + place(&sorted[i], &a.extents, &ranked[i], &a.floor, a.pairs.as_deref(), ceiling) + }; + // Edges land first: a vertex lands no finer than the coarsest edge ending at it. + let mut levels: Vec>> = vec![None; tables.len()]; + let mut ceilings: Vec> = vec![Vec::new(); tables.len()]; + for (i, table) in tables.iter().enumerate() { + let Some((target, ends)) = &table.ends else { continue }; + let level = level_of(i, &[]); + let target = *target as usize; + if leveled(i) && leveled(target) { + let ceiling = &mut ceilings[target]; + ceiling.resize(tables[target].rows as usize, MAX_LEVEL); + for end in ends { + for (r, &t) in end.iter().enumerate() { + if let Some(c) = ceiling.get_mut(t as usize).filter(|_| t != UNWIRED) { + *c = (*c).min(level[r]); + } } - patch = patch.set(placed); } } - anchors.push(own_anchors); + levels[i] = Some(level); } - patch + let mut out = Vec::new(); + for (i, table) in tables.iter().enumerate() { + let level = levels[i].take().unwrap_or_else(|| level_of(i, &ceilings[i])); + if !table.answered { + continue; + } + let (order, tiles) = if leveled(i) { + tiled(&sorted[i], &placed[i].anchors, &placed[i].extents, &ranked[i], &level) + } else { + let mut order: Vec = (0..table.rows).collect(); + crate::order::by_key(&mut order, |&r| ranked[i][r as usize]); + let whole = Tile { key: TileKey { z: 0, x: 0, y: 0 }, rows: 0..table.rows, reach: 1.0, cells: Vec::new() }; + (order, vec![whole]) + }; + out.push((i as u32, Pyramid::new(order, tiles))); + } + out } /// Where each of `n` rows placed by `ends` lies, among rows anchored at `at`: the middle of the box @@ -277,20 +314,6 @@ fn tile_of(anchor: [u32; 2], z: u8) -> TileKey { TileKey { z, x: at(anchor[0]), y: at(anchor[1]) } } -/// `cover` grown to hold tile `key`: a box from the west and the north, fixed point. -fn covering(cover: [u64; 4], key: TileKey) -> [u64; 4] { - let shift = 32 - u32::from(key.z); - let (x, y) = (u64::from(key.x) << shift, u64::from(key.y) << shift); - let size = 1_u64 << shift; - [cover[0].min(x), cover[1].min(y), cover[2].max(x + size), cover[3].max(y + size)] -} - -/// Whether a row anchored at `anchor`, reaching `extent` from it, reaches into `cover`. -pub(crate) fn reaches(cover: &[u64; 4], anchor: [u32; 2], extent: u32) -> bool { - let (u, v, e) = (u64::from(anchor[0]), u64::from(anchor[1]), u64::from(extent)); - u + e >= cover[0] && u.saturating_sub(e) < cover[2] && v + e >= cover[1] && v.saturating_sub(e) < cover[3] -} - /// A tile as a job's answer numbers it: in 53 bits, so a number holds it exactly. pub(crate) fn pack(key: TileKey) -> u64 { u64::from(key.z) << 48 | u64::from(key.x) << 24 | u64::from(key.y) @@ -302,15 +325,29 @@ fn unpack(key: u64) -> TileKey { /// The level each row lands at: rows in each cell of each level, coarsest first, give way by rank; a /// row lands at the first level where it outranks every row left in its cell, no coarser than -/// `floor`, at the finest its size allows when it has not landed by then. +/// `floor`, and no finer than its size allows or `ceiling` says, where it lands when it has not by +/// then. /// /// Rows along the curve are taken in runs, each the rows left in one cell. While a run stays in one /// cell level after level, those levels take its highest rows in rank order at once; where it /// spreads over several cells, each cell's rows go on as a run of their own. So a crowd sharing a /// cell for many levels costs its size once, not once a level. -fn place(sorted: &[(u64, u32)], extents: &[u32], ranks: &[u64], floor: &[u8], pairs: Option<&[u64]>) -> Vec { +fn place( + sorted: &[(u64, u32)], + extents: &[u32], + ranks: &[u64], + floor: &[u8], + pairs: Option<&[u64]>, + ceiling: &[u8], +) -> Vec { let n = sorted.len(); - let fits: Vec = (0..n).map(|r| extents.get(r).map_or(MAX_LEVEL, |&e| fit(e))).collect(); + let fits: Vec = (0..n) + .map(|r| { + let own = extents.get(r).map_or(MAX_LEVEL, |&e| fit(e)); + own.min(ceiling.get(r).copied().unwrap_or(MAX_LEVEL)) + }) + .collect(); + let forcing = !extents.is_empty() || !ceiling.is_empty(); let mates = pairs.map(|pairs| mates(sorted, pairs)).unwrap_or_default(); let mut level = vec![NEVER; n]; let mut work: Vec<(u64, u32)> = sorted.to_vec(); @@ -375,8 +412,8 @@ fn place(sorted: &[(u64, u32)], extents: &[u32], ranks: &[u64], floor: &[u8], pa } scratch = heap.into_vec(); } - // Rows no finer level holds land where their size says. - if !extents.is_empty() { + // Rows no finer level may hold land where their size or their edges say. + if forcing { forced.clear(); forced.extend(work[a..b].iter().map(|&(_, r)| r).filter(|&r| fits[r as usize] <= deepest)); for &r in &forced { @@ -406,16 +443,16 @@ fn place(sorted: &[(u64, u32)], extents: &[u32], ranks: &[u64], floor: &[u8], pa level } -/// Rows by tile, coarsest level first, each tile's rows in priority order, highest last; and each -/// tile packed, where its rows end, and how far they reach past it, fixed point. `sorted` holds the -/// rows along the curve. +/// Rows by tile, coarsest level first, each tile's rows in priority order, highest last, and a large +/// tile's a sub-cell at a time, each in priority order; and the tiles. `sorted` holds the rows along +/// the curve. fn tiled( sorted: &[(u64, u32)], anchors: &[[u32; 2]], extents: &[u32], ranks: &[u64], level: &[u8], -) -> (Vec, Vec<(u64, u32, u32)>) { +) -> (Vec, Vec) { let n = anchors.len(); // Rows by level, along the curve within each, so each tile's rows are a run. let mut starts = vec![0_usize; usize::from(MAX_LEVEL) + 2]; @@ -446,103 +483,98 @@ fn tiled( j += 1; } let start = order.len(); - order.extend_from_slice(&rows[i..j]); + let members = &rows[i..j]; // By priority alone: rows alike draw in either order. - crate::order::by_key(&mut order[start..], |&r| ranks[r as usize] >> 32); - tiles.push((pack(key), order.len() as u32, reach)); + let by_priority = |order: &mut Vec, from: usize| { + crate::order::by_key(&mut order[from..], |&r| ranks[r as usize] >> 32); + }; + let mut cells = Vec::new(); + if members.len() > SUB_ROWS { + // Along the curve, each sub-cell's rows are a run. + let shift = 2 * (32 - u32::from(z) - SUB_BITS); + let mask = (1_u64 << (2 * SUB_BITS)) - 1; + let mut k = 0; + for cell in 0..=mask { + let from = order.len(); + while k < members.len() && (morton(anchors[members[k] as usize]) >> shift) & mask == cell { + order.push(members[k]); + k += 1; + } + by_priority(&mut order, from); + cells.push(order.len() as u32); + } + } else { + order.extend_from_slice(members); + by_priority(&mut order, start); + } + let reach = f64::from(reach) / 4_294_967_296.0; + tiles.push(Tile { key, rows: start as u32..order.len() as u32, reach, cells }); i = j; } } (order, tiles) } -/// One tile of a pyramid: where its rows lie in the pyramid's order, and how far they reach past it, -/// across the square. +/// One tile of a pyramid: where its rows lie in the pyramid's order, how far they reach past it +/// across the square, and for a large tile where each sub-cell's rows end, in the curve's order of +/// the sub-cells. #[derive(Clone, Debug, PartialEq)] pub(crate) struct Tile { pub(crate) key: TileKey, pub(crate) rows: Range, pub(crate) reach: f64, + pub(crate) cells: Vec, } -/// The tiles the camera asks of a pyramid, each with whether it is one of the finest asked, and the -/// part of the square, from the west and the north, the finest cover: a coarser tile draws only its -/// rows reaching into it. -#[derive(Clone, Debug, Default, PartialEq, Hash)] -pub(crate) struct Wanted { - pub(crate) tiles: Vec<(u32, bool)>, - pub(crate) cover: Option<[u64; 4]>, +/// The pages of the order `ranges` hold past page `last`, and the last page they reach. The order lays +/// its levels out coarsest first, so a level's pages lie past those of the levels above it, but for +/// one they may share. +fn pages_past(ranges: &[(u8, Range)], last: Option) -> (u32, Option) { + let mut spans: Vec<(u32, u32)> = + ranges.iter().filter(|(_, r)| r.start < r.end).map(|(_, r)| (r.start / PAGE, (r.end - 1) / PAGE)).collect(); + spans.sort_unstable(); + let (mut pages, mut reached) = (0, last); + for (first, end) in spans { + let from = reached.map_or(first, |l| first.max(l + 1)); + pages += (end + 1).saturating_sub(from); + reached = Some(reached.map_or(end, |l| l.max(end))); + } + (pages, reached) } -/// A table's rows grouped into tiles: a job's answer, read. -#[derive(Debug, Default)] +/// Sub-cell `cell` of tile `key`, numbered along the curve. +fn sub_key(key: TileKey, cell: usize) -> TileKey { + let (mut x, mut y) = (0, 0); + for b in 0..SUB_BITS { + x |= ((cell >> (2 * b)) as u32 & 1) << b; + y |= ((cell >> (2 * b + 1)) as u32 & 1) << b; + } + TileKey { z: key.z + SUB_BITS as u8, x: key.x << SUB_BITS | x, y: key.y << SUB_BITS | y } +} + +/// A table's rows grouped into tiles: its order, rows by tile, coarsest level first, each tile's rows +/// in priority order, highest last; each row's place in it; and its tiles. +#[derive(Debug)] pub(crate) struct Pyramid { - /// Rows by tile, coarsest level first, each tile's rows in priority order, highest last. pub(crate) order: Vec, + pub(crate) rank: Vec, pub(crate) tiles: Vec, - /// Where each row lies across the square, fixed point, and half its size alike: none for a - /// table in one tile, and no sizes for points. - pub(crate) anchors: Vec<[u32; 2]>, - pub(crate) extents: Vec, + /// What tells it from every other pyramid: what pages packed in its order are kept by. + pub(crate) made: Version, index: FxHashMap, /// Every tile with tiles below it. above: FxHashSet, } impl Pyramid { - /// The pyramids a job answered, by the place of the table among those asked. - pub(crate) fn read(bytes: &[u8]) -> FxHashMap { - Patch::from_bytes(bytes).map(Pyramid::answered).unwrap_or_default() - } - - /// The pyramids `patch` answers, by the place of the table among those asked. - pub(crate) fn answered(patch: Patch) -> FxHashMap { - let mut tiles: FxHashMap> = FxHashMap::default(); - let mut orders: FxHashMap> = FxHashMap::default(); - let mut placed: FxHashMap, Vec)> = FxHashMap::default(); - for op in patch.ops { - let Op::Set(table) = op else { continue }; - let number = |prefix: &str| table.name.strip_prefix(prefix).and_then(|i| i.parse::().ok()); - let column = |name: &str| match table.field_named(name) { - Some(Field::Values(column, _)) => Some(column.clone()), - _ => None, - }; - // Words read as they are stored, page by page. - let words = |name: &str| -> Vec { - column(name).map_or_else(Vec::new, |c| { - c.pages().iter().flat_map(|p| p.values.u32s().unwrap_or(&[]).iter().copied()).collect() - }) - }; - if let Some(i) = number("tiles") { - let keys: Vec = column("key").map_or_else(Vec::new, |c| { - c.pages().iter().flat_map(|p| p.values.u64s().unwrap_or(&[]).iter().copied()).collect() - }); - let (ends, reaches) = (words("end"), words("reach")); - let mut start = 0; - let read = keys.iter().zip(&ends).zip(&reaches).map(|((&key, &end), &reach)| { - let rows = start..end; - start = end; - Tile { key: unpack(key), rows, reach: f64::from(reach) / 4_294_967_296.0 } - }); - tiles.insert(i, read.collect()); - } else if let Some(i) = number("order") { - orders.insert(i, words("row")); - } else if let Some(i) = number("anchors") { - let anchors = words("u").into_iter().zip(words("v")).map(|(u, v)| [u, v]).collect(); - placed.insert(i, (anchors, words("extent"))); + /// The pyramid of `order` and `tiles`: each row's place in the order worked out from it. + pub(crate) fn new(order: Vec, tiles: Vec) -> Pyramid { + let mut rank = vec![UNWIRED; order.len()]; + for (d, &r) in order.iter().enumerate() { + if let Some(slot) = rank.get_mut(r as usize) { + *slot = d as u32; } } - tiles - .into_iter() - .filter_map(|(i, tiles)| { - let mut pyramid = Pyramid::new(orders.remove(&i)?, tiles); - (pyramid.anchors, pyramid.extents) = placed.remove(&i).unwrap_or_default(); - Some((i, pyramid)) - }) - .collect() - } - - pub(crate) fn new(order: Vec, tiles: Vec) -> Pyramid { let index = tiles.iter().enumerate().map(|(i, t)| (t.key, i as u32)).collect(); let mut above = FxHashSet::default(); for tile in &tiles { @@ -552,81 +584,129 @@ impl Pyramid { } } } - Pyramid { order, tiles, anchors: Vec::new(), extents: Vec::new(), index, above } + Pyramid { order, rank, tiles, made: Version::next(), index, above } + } + + /// Rows it orders. + pub(crate) fn rows(&self) -> u32 { + self.order.len() as u32 } - /// The tiles the camera asks for, coarsest first: at each level, every tile its rows reach into - /// what the camera shows from, splitting while a tile draws larger than its size. A level whose - /// rows would pass `rows` goes, coarser ones drawing whole in its place. - pub(crate) fn wanted(&self, sight: &Sight<'_>, domain: &Domain, rows: u32) -> Wanted { + /// The ranges of the order the camera asks for, coarsest first, each with its level: at each + /// level, every tile its rows reach into what the camera shows from, a large one only where its + /// sub-cells do, splitting while a tile draws larger than its size. A drawing holds whole pages of + /// the order, so a level whose pages would hold more than `rows` rows goes, coarser ones drawing + /// whole in its place. + pub(crate) fn wanted(&self, sight: &Sight<'_>, domain: &Domain, rows: u32) -> Vec<(u8, Range)> { if self.tiles.len() == 1 && self.tiles[0].key.z == 0 { - return Wanted { tiles: vec![(0, true)], cover: None }; + return vec![(0, self.tiles[0].rows.clone())]; } let region: Region = sight.region(); let focus = sight.focus(); let limit = TILE_PX * 1.25; - let mut out: Vec<(u32, bool)> = Vec::new(); - let mut cover = [u64::MAX, u64::MAX, 0, 0]; - let mut total = 0_u32; + let mut out: Vec<(u8, Range)> = Vec::new(); + let (fit, mut held, mut last) = (rows.div_ceil(PAGE), 0_u32, None); let mut level = vec![TileKey { z: 0, x: 0, y: 0 }]; - // The tiles of the level above that split, and where among those found each is. - let mut split: Vec<(TileKey, Option)> = Vec::new(); while !level.is_empty() { let mut found = Vec::new(); - let mut leaves = Vec::new(); - let mut splitting = Vec::new(); - let mut added = 0_u32; let mut next = Vec::new(); for key in level { let grow = 0.5 / f64::from(1_u32 << key.z); if !meets(®ion, domain.bounds(key, grow)) { continue; } - let splits = self.above.contains(&key) - && next.len() < MAX_TILES - && sight.measure(&focus, domain.bounds(key, 0.0)).0 > limit; - let mut at = None; if let Some(&t) = self.index.get(&key) { let tile = &self.tiles[t as usize]; if meets(®ion, domain.bounds(key, tile.reach)) { - at = Some(out.len() + found.len()); - found.push((t, !splits)); - added = added.saturating_add(tile.rows.len() as u32); + let first = found.len(); + if tile.cells.is_empty() { + found.push((key.z, tile.rows.clone())); + } + // A sub-cell's rows lie within it, each reaching no farther than the tile's. + let grow = tile.reach + 0.5 / f64::from(1_u32 << (u32::from(key.z) + SUB_BITS)); + let mut start = tile.rows.start; + for (cell, &end) in tile.cells.iter().enumerate() { + if end > start && meets(®ion, domain.bounds(sub_key(key, cell), grow)) { + let ours = found.len() > first; + match found.last_mut() { + Some((_, range)) if ours && range.end == start => range.end = end, + _ => found.push((key.z, start..end)), + } + } + start = end; + } } } + let splits = + self.above.contains(&key) && next.len() < MAX_TILES && sight.measure(&focus, domain, key).0 > limit; if splits { next.extend(key.children()); - splitting.push((key, at)); - } else { - leaves.push(key); } } - if total.saturating_add(added) > rows && !out.is_empty() { - // The tiles that split draw whole instead. - for (key, at) in split { - cover = covering(cover, key); - if let Some(at) = at { - out[at].1 = true; - } - } + // A level whose pages would pass what fits goes: the coarser ones draw in its place. + let (pages, reached) = pages_past(&found, last); + if held + pages > fit && !out.is_empty() { break; } - total = total.saturating_add(added); + (held, last) = (held + pages, reached); out.extend(found); - for key in leaves { - cover = covering(cover, key); - } - split = splitting; level = next; } - Wanted { tiles: out, cover: (cover[0] <= cover[2]).then_some(cover) } + out + } + + /// The pyramids a job answered, by the place of the table among those asked, as + /// [`encode`] wrote them. + pub(crate) fn read(bytes: &[u8]) -> Vec<(u32, Pyramid)> { + let words: Vec = bytes.as_chunks::<4>().0.iter().map(|&w| u32::from_le_bytes(w)).collect(); + let mut words = words.as_slice(); + let mut take = |n: usize| -> Option<&[u32]> { + let (head, rest) = words.split_at_checked(n)?; + words = rest; + Some(head) + }; + let mut out = Vec::new(); + 'tables: while let Some(&[table, n]) = take(2) { + let n = n as usize; + let (Some(order), Some(&[count])) = (take(n).map(<[u32]>::to_vec), take(1)) else { break }; + let mut tiles = Vec::with_capacity(count as usize); + let mut start = 0; + for _ in 0..count { + let Some(&[lo, hi, end, reach, cells]) = take(5) else { break 'tables }; + let Some(cells) = take(cells as usize) else { break 'tables }; + let key = unpack(u64::from(lo) | u64::from(hi) << 32); + let reach = f64::from(reach) / 4_294_967_296.0; + tiles.push(Tile { key, rows: start..end, reach, cells: cells.to_vec() }); + start = end; + } + out.push((table, Pyramid::new(order, tiles))); + } + out + } +} + +/// `pyramids` as a job answers them: for each, the place of its table among those asked, how many +/// rows it orders, its order, how many tiles, and each tile's key, where its rows end, how far they +/// reach, and how many sub-cells it has and where each one's rows end, little-endian words. +pub(crate) fn encode(pyramids: &[(u32, Pyramid)]) -> Vec { + let mut words: Vec = Vec::new(); + for (table, pyramid) in pyramids { + words.extend([*table, pyramid.rows()]); + words.extend_from_slice(&pyramid.order); + words.push(pyramid.tiles.len() as u32); + for tile in &pyramid.tiles { + let key = pack(tile.key); + let reach = (tile.reach * 4_294_967_296.0).min(f64::from(u32::MAX)) as u32; + words.extend([key as u32, (key >> 32) as u32, tile.rows.end, reach, tile.cells.len() as u32]); + words.extend_from_slice(&tile.cells); + } } + words.iter().flat_map(|w| w.to_le_bytes()).collect() } #[cfg(test)] mod tests { use super::*; - use crate::data::UNWIRED; /// Fixed point across the square. fn at(u: f64, v: f64) -> [u32; 2] { @@ -639,16 +719,22 @@ mod tests { (domain, Places::Points(points.iter().map(|&[u, v]| [u as f32, (1.0 - v) as f32]).collect())) } + /// The level row `row` of `pyramid` lands at. + fn level_of(pyramid: &Pyramid, row: u32) -> u8 { + let at = pyramid.rank[row as usize]; + pyramid.tiles.iter().find(|t| t.rows.contains(&at)).expect("a tile").key.z + } + #[test] fn crowded_rows_wait_for_finer_levels_by_priority() { // Two rows in one cell: the higher lands at the root, the lower a level finer. let anchors = [at(0.3, 0.3), at(0.3, 0.3), at(0.8, 0.8)]; - let level = place(&along(&anchors), &[], &ranks(&[1.0, 2.0, 0.0], 3), &[0, 0, 0], None); + let level = place(&along(&anchors), &[], &ranks(&[1.0, 2.0, 0.0], 3), &[0, 0, 0], None, &[]); assert_eq!(level[1], 0, "the higher priority lands at the root"); assert_eq!(level[2], 0, "a row alone in its cell lands at the root"); assert_eq!(level[0], 1, "the lower lands once the higher has left its cell"); let alike = [at(0.3, 0.3); 4]; - let level = place(&along(&alike), &[], &ranks(&[], 4), &[0; 4], None); + let level = place(&along(&alike), &[], &ranks(&[], 4), &[0; 4], None, &[]); let mut sorted = level.clone(); sorted.sort_unstable(); assert_eq!(sorted, vec![0, 1, 2, 3], "a row a level, equals in an order of their own"); @@ -661,22 +747,31 @@ mod tests { (0..n).map(|i| at(f64::from(i % 250) / 250.0, f64::from(i / 250) / 200.0)).collect(); let sorted = along(&anchors); let ranks = ranks(&[], n as usize); - let level = place(&sorted, &[], &ranks, &vec![0; n as usize], None); + let level = place(&sorted, &[], &ranks, &vec![0; n as usize], None, &[]); let (order, tiles) = tiled(&sorted, &anchors, &[], &ranks, &level); assert_eq!(order.len(), n as usize); - let mut start = 0; - for &(_, end, _) in &tiles { - assert!(end - start <= 1 << (2 * GRID_BITS), "a tile holds at most its grid's cells"); - start = end; + for tile in &tiles { + assert!(tile.rows.len() <= 1 << (2 * GRID_BITS), "a tile holds at most its grid's cells"); } assert!(tiles.len() > 1); + // A large tile's rows lie a sub-cell at a time, each sub-cell's within it. + let large = tiles.iter().find(|t| !t.cells.is_empty()).expect("a large tile"); + let mut start = large.rows.start; + for (cell, &end) in large.cells.iter().enumerate() { + let sub = sub_key(large.key, cell); + for &r in &order[start as usize..end as usize] { + assert_eq!(tile_of(anchors[r as usize], sub.z), sub, "row {r} lies in sub-cell {cell}"); + } + start = end; + } + assert_eq!(start, large.rows.end); } #[test] fn large_rows_stay_coarse_and_edges_land_by_their_own_priority() { let anchors = [at(0.1, 0.1), at(0.9, 0.9)]; let extents = [1 << 30, 0]; - let level = place(&along(&anchors), &extents, &ranks(&[0.0, 0.0], 2), &[0, 0], None); + let level = place(&along(&anchors), &extents, &ranks(&[0.0, 0.0], 2), &[0, 0], None, &[]); assert!(level[0] <= 1, "a row reaching a quarter of the square stays coarse"); // Two vertices crowd one cell; the edges between them land by their own priority, parallel // ones together, and one naming no row only at the finest level. @@ -701,15 +796,44 @@ mod tests { }, ], }; - let pyramids = Pyramid::read(&answer(&leveling).to_bytes()); + let pyramids: FxHashMap = answer(&leveling).into_iter().collect(); let edges = &pyramids[&1]; - let level_of = |p: &Pyramid, row: u32| { - let at = p.order.iter().position(|&r| r == row).unwrap() as u32; - p.tiles.iter().find(|t| t.rows.contains(&at)).unwrap().key.z - }; - assert_eq!(level_of(edges, 0), 0, "an edge alone in its cell shows from the root, its ends carried"); + assert_eq!(level_of(edges, 0), 0, "an edge alone in its cell shows from the root"); assert_eq!(level_of(edges, 1), 0, "with the edge joining the same vertices"); assert_eq!(level_of(edges, 2), MAX_LEVEL, "an edge naming no row waits for the finest level"); + // Its ends show with it: vertices 1 and 2 land at the root though they share a cell. + let vertices = &pyramids[&0]; + assert_eq!((level_of(vertices, 1), level_of(vertices, 2)), (0, 0), "ends land with their edges"); + } + + #[test] + fn answers_read_back_as_written() { + let (domain, places) = square(&[[0.1, 0.1], [0.9, 0.9], [0.5, 0.5]]); + let leveling = Leveling { + domain, + tables: vec![Placing { rows: 3, leveled: true, answered: true, places, ..Placing::default() }], + }; + let answered = answer(&leveling); + let read = Pyramid::read(&encode(&answered)); + assert_eq!(read.len(), 1); + let (table, pyramid) = &read[0]; + assert_eq!(*table, 0); + assert_eq!(pyramid.order, answered[0].1.order); + assert_eq!(pyramid.tiles, answered[0].1.tiles); + for (d, &r) in pyramid.order.iter().enumerate() { + assert_eq!(pyramid.rank[r as usize], d as u32, "each row's place in the order"); + } + } + + #[test] + fn levels_count_the_pages_they_hold() { + // Two sub-cells in one page, one in the next, and a level below reaching the page where the + // one above ended. + let above = [(0, 10..20), (0, 50..60), (0, PAGE + 3..PAGE + 9)]; + let (pages, last) = pages_past(&above, None); + assert_eq!((pages, last), (2, Some(1))); + let below = [(1, PAGE + 20..3 * PAGE + 1)]; + assert_eq!(pages_past(&below, last), (2, Some(3)), "pages 2 and 3; page 1 is held already"); } #[test] @@ -725,8 +849,8 @@ mod tests { domain: Domain::Earth, tables: vec![Placing { rows: 3, answered: true, priority: [5.0, 1.0, 3.0].into(), ..Placing::default() }], }; - let pyramids = Pyramid::read(&answer(&leveling).to_bytes()); - assert_eq!(pyramids[&0].order, vec![1, 2, 0], "highest last, drawn on top"); - assert_eq!(pyramids[&0].tiles.len(), 1); + let pyramids = answer(&leveling); + assert_eq!(pyramids[0].1.order, vec![1, 2, 0], "highest last, drawn on top"); + assert_eq!(pyramids[0].1.tiles.len(), 1); } } diff --git a/crates/latkit/src/derive/shapes.rs b/crates/latkit/src/derive/shapes.rs index 657e7c99..1c43fe6a 100644 --- a/crates/latkit/src/derive/shapes.rs +++ b/crates/latkit/src/derive/shapes.rs @@ -1,16 +1,18 @@ -//! Shapes: what geometry fields become to draw, written beside the tables marks draw them from. Every -//! area's triangles, the point its label hangs from first; and on the earth every line's path, -//! unwrapped across the antimeridian and bent with the globe. Shapes are made page by page from a -//! field's pages and kept while a page holds; a page written again keeps the shapes of its rows -//! whose geometry did not change, so a tile landing shapes only its own rows. Triangles that do not -//! fit the frame's budget are made in a job, their rows drawing nothing until it lands; bending a -//! path is cheap, and done at once. +//! Shapes: what geometry fields become to draw. Every area's triangles, the point its label hangs from +//! first; and on the earth every line's path, unwrapped across the antimeridian and bent with the +//! globe. A table drawn whole is shaped a page of its field at a time, written beside it, and a page +//! written again keeps the shapes of its rows whose geometry did not change, so a tile landing shapes +//! only its own rows. A table drawn in part is shaped a page of its order at a time, the first time +//! the page is drawn: a row whose geometry did not change keeps its shape however its pages come and +//! go. Triangles that do not fit the frame's budget are made in a job, their rows drawing nothing +//! until it lands; bending a path is cheap, and done at once. -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; -use super::{Derive, Input, Jobs, Tables, answers, beside, stamp_of}; +use super::order::{Order, PAGE}; +use super::{Derive, Input, Jobs, Paged, Tables, answers, beside, stamp_of}; use crate::data::{ - Column, Field, GeometryBuilder, Id, Keyed, Name, PAGE_ROWS, Page, PageId, Table, Value, Values, Version, + Column, Field, Geometry, GeometryBuilder, Id, Keyed, Name, PAGE_ROWS, Page, PageId, Table, Value, Values, Version, }; use crate::host::{Job, Shaping}; use crate::scene::areas::triangles_of; @@ -23,6 +25,9 @@ const TRIANGULATE: f64 = 0.5; /// Rows by id, each with the digest of the geometry it was shaped from. type Digests = FxHashMap; +/// Rows to shape: each row, its id, and the digest of its geometry. +type Missing = Vec<(usize, Id, u64)>; + /// A field to shape: its dataset, table, and field, and how. #[derive(Clone, PartialEq, Eq, Hash, Debug)] struct Want { @@ -42,8 +47,55 @@ impl Want { } } -/// One field's shapes: the last whole set made, the derived field written, shapes made but not yet -/// written, and a job under way. +/// The shapes of a table drawn in part, a page of its order at a time: each page the shapes of the +/// rows of one page of the order, in its order, with an id of its own; and the shapes of the rows kept +/// whose pages are not drawn, a row at a time, by their place in the order. +pub(crate) struct PagedShapes { + /// The order whose pages these are. + pub(crate) order: u64, + /// Bumps whenever a page or a row kept is shaped. + pub(crate) version: Version, + pub(crate) pages: FxHashMap, + pub(crate) kept: FxHashMap, + /// How deep the shapes' geometry is. + pub(crate) depth: u8, +} + +/// One page of an order, shaped. +pub(crate) struct ShapedPage { + pub(crate) id: PageId, + pub(crate) shapes: Values, + /// Whether every row holds its shape: none waits for a job. + whole: bool, +} + +/// What a table drawn in part was shaped from: the order, the table and the field its pages were made +/// from, and the pages made. +struct Paging { + order: Order, + source: Table, + column: Column, + shapes: PagedShapes, +} + +impl Paging { + /// Where a page made whole holds the shape of the row with `id`, when its geometry's digest was + /// `digest`: the page's geometry and the row's place in it. + fn held(&self, id: &Id, digest: u64) -> Option<(&Geometry, usize)> { + let row = self.source.row(id.as_ref())?; + let (values, i) = self.column.at(row).filter(|&(values, i)| values.is_valid(i))?; + if row >= self.order.len() as usize || values.geometry()?.row_hash(i) != digest { + return None; + } + let d = self.order.rank(row as u32); + let page = self.shapes.pages.get(&(d / PAGE)).filter(|page| page.whole)?; + Some((page.shapes.geometry()?, (d % PAGE) as usize)) + } +} + +/// One field's shapes: drawn whole, the last whole set made, the derived field written, and the shapes +/// made but not yet written; drawn in part, its pages, and those of the order drawn before; and a job +/// under way. #[derive(Default)] struct Shaped { /// The source table, its field, and the derived column the shapes were last wholly made from. @@ -53,6 +105,10 @@ struct Shaped { stamp: u64, /// Shapes made since, by row id: the digest of the geometry each was made from, and the shape. landed: FxHashMap, + /// Drawn in part, its pages; and those of the order drawn before, whose rows alike pages made + /// afresh take their shapes from. + paging: Option, + before: Option, /// A job under way, with the digest of each row it shapes. working: Option<(Pending>, Digests)>, /// Bumps whenever a job's answer lands. @@ -72,10 +128,11 @@ impl Derive for Shapes { let mut changed = false; for want in &wants { let Some(source) = input.data(&want.data).and_then(|d| d.table(&want.table)) else { continue }; + let paged = input.paged.and_then(|paged| paged.get(&(want.data.clone(), want.table.clone()))); let shaped = self.shaped.entry(want.clone()).or_default(); - changed |= shaped.update(want, source, jobs); + changed |= shaped.update(want, source, paged, jobs); } - // Each drawn table's derived table holds every field shaped from it. + // Each table drawn whole has a derived table holding every field shaped from it. let mut fields: Keyed<(Name, Name), Vec<(Name, Field)>> = Keyed::default(); for want in &wants { if let Some(field) = self.shaped.get(want).and_then(|s| s.field.clone()) { @@ -108,6 +165,17 @@ impl Derive for Shapes { } } +impl Shapes { + /// The shapes of table `table` of dataset `data` drawn in part, by the field each is written as. + pub(crate) fn paged(&self, data: &Name, table: &Name) -> Vec<(Name, &PagedShapes)> { + self.shaped + .iter() + .filter(|(want, _)| want.data == *data && want.table == *table) + .filter_map(|(want, shaped)| Some((want.derived(), &shaped.paging.as_ref()?.shapes))) + .collect() + } +} + /// The fields the scene's marks shape: every area's polygons, and on the earth every line's paths. fn wants(input: &Input<'_>) -> Vec { let earth = matches!(input.scene.space, Space::Earth(_)); @@ -128,8 +196,9 @@ fn wants(input: &Input<'_>) -> Vec { } impl Shaped { - /// Brings the shapes up to `source`; whether the derived field changed. - fn update(&mut self, want: &Want, source: &Table, jobs: &mut Jobs<'_>) -> bool { + /// Brings the shapes up to `source`, drawn in part as `paged` says or else whole; whether they + /// changed. + fn update(&mut self, want: &Want, source: &Table, paged: Option<&Paged>, jobs: &mut Jobs<'_>) -> bool { if let Some((pending, asked)) = &self.working && let Some(answer) = pending.take() { @@ -141,18 +210,44 @@ impl Shaped { self.working = None; self.landings += 1; } - let Some(Field::Values(column, version)) = source.field_named(&want.field) else { - let had = self.field.take().is_some(); - self.stamp = 0; - return had; - }; - let depth = column.kind().depth(); + let Some(Field::Values(column, version)) = source.field_named(&want.field) else { return self.forget() }; let ((Some(depth @ (2 | 3)), Shaping::Triangles { .. }) | (Some(depth @ 1..), Shaping::Path)) = - (depth, want.shaping) + (column.kind().depth(), want.shaping) else { - return self.field.take().is_some(); + return self.forget(); }; - let stamp = stamp_of((source.structure, *version, self.landings, want.shaping)); + if let Some(paged) = paged { + // Should it draw whole again, its field is written afresh. + let had = self.field.take().is_some(); + self.stamp = 0; + return self.update_paged(want, source, column, depth, paged, jobs) || had; + } + // Drawn whole again: rows alike take their shapes from the pages drawn in part. + let had = self.paging.is_some(); + if had { + self.before = self.paging.take(); + } + self.update_whole(want, source, column, *version, depth, jobs) || had + } + + /// Forgets what it shaped, its field no longer shapes; whether it had shaped anything. + fn forget(&mut self) -> bool { + let had = self.field.is_some() || self.paging.is_some(); + (self.field, self.paging, self.before, self.stamp) = (None, None, None, 0); + had + } + + /// Brings the shapes of a table drawn whole up to `source`; whether the derived field changed. + fn update_whole( + &mut self, + want: &Want, + source: &Table, + column: &Column, + version: Version, + depth: u8, + jobs: &mut Jobs<'_>, + ) -> bool { + let stamp = stamp_of((source.structure, version, self.landings, want.shaping)); if stamp == self.stamp { return false; } @@ -164,23 +259,13 @@ impl Shaped { self.field = Some(Field::Values(derived.clone(), Version::next())); self.made = Some((source.clone(), column.clone(), derived)); self.landed.clear(); + self.before = None; return true; } - let points: usize = missing.iter().map(|&(row, ..)| points_of(column, row)).sum(); - if jobs.fits((points as f64 * TRIANGULATE).ceil() as u64) { - for (row, id, digest) in missing { - let Some(value) = column.get(row) else { continue }; - let shape = want.shaping.shape(&value).unwrap_or(value); - self.landed.insert(id, (digest, shape)); - } + if self.shape_now(want, column, &missing, jobs) { continue; } - if self.working.is_none() { - let rows: Vec = missing.iter().map(|&(row, ..)| row).collect(); - let job = Job::shapes(want.shaping, rows_of(source, column, &rows)); - let asked = missing.into_iter().map(|(_, id, digest)| (id, digest)).collect(); - self.working = Some((jobs.spawn(job), asked)); - } + self.ask(want, source, column, missing, jobs); // Until the job lands, its rows draw nothing and the rest draw as they were. self.field = Some(Field::Values(derived, Version::next())); return true; @@ -188,11 +273,131 @@ impl Shaped { false } + /// Brings the shapes of a table drawn in part up to `source`, making each page `paged` draws the + /// first time it is drawn, and again once rows it waited on land; whether a page was made. + fn update_paged( + &mut self, + want: &Want, + source: &Table, + column: &Column, + depth: u8, + paged: &Paged, + jobs: &mut Jobs<'_>, + ) -> bool { + let order = &paged.order; + // Another order: its pages are made afresh, rows alike taking their shapes from the last one's. + if self.paging.as_ref().is_none_or(|p| p.shapes.order != order.id()) { + let shapes = PagedShapes { + order: order.id(), + version: Version::next(), + pages: FxHashMap::default(), + kept: FxHashMap::default(), + depth: want.shaping.depth(depth), + }; + let fresh = Paging { order: order.clone(), source: source.clone(), column: column.clone(), shapes }; + self.before = self.paging.replace(fresh).or(self.before.take()); + } + let paging = self.paging.as_mut().expect("paged"); + paging.order = order.clone(); + // The pages rows written since draw in are made again, and those rows kept shaped again. + if !paging.column.shares(column) { + let written = column.written_since(&paging.column); + for &row in written.iter().filter(|&&row| row < order.len() as usize) { + paging.shapes.pages.remove(&(order.rank(row as u32) / PAGE)); + } + if !paging.shapes.kept.is_empty() { + let written: FxHashSet = written.into_iter().map(|row| row as u32).collect(); + paging.shapes.kept.retain(|&d, _| !written.contains(&order.source(d))); + } + (paging.source, paging.column) = (source.clone(), column.clone()); + } + paging.shapes.kept.retain(|d, _| paged.kept.binary_search(d).is_ok()); + // Pages drawn that were not made, or that wait on rows no job is shaping now; and rows kept + // not shaped yet, while no job is. + let running = self.working.is_some(); + let made = &paging.shapes.pages; + let make: Vec = + paged.pages.iter().copied().filter(|p| made.get(p).is_none_or(|page| !page.whole && !running)).collect(); + let kept_made = &paging.shapes.kept; + let keep: Vec = if running { + Vec::new() + } else { + paged.kept.iter().copied().filter(|d| !kept_made.contains_key(d)).collect() + }; + if make.is_empty() && keep.is_empty() { + return false; + } + // Rows shaped on the frame land at once; a second pass makes their pages. + let mut made: Vec<(u32, Values, bool)> = Vec::with_capacity(make.len()); + let mut kept: Vec<(u32, Values)> = Vec::with_capacity(keep.len()); + let mut missing: Missing = Vec::new(); + for _ in 0..2 { + made.clear(); + kept.clear(); + missing.clear(); + for &page in &make { + let (shapes, rows) = self.make_page(want, order, page, source, column, depth); + made.push((page, shapes, rows.is_empty())); + missing.extend(rows); + } + for &d in &keep { + let (shape, rows) = self.make_rows(want, &[order.source(d)], source, column, depth); + if rows.is_empty() { + kept.push((d, shape)); + } + missing.extend(rows); + } + if missing.is_empty() || !self.shape_now(want, column, &missing, jobs) { + break; + } + } + if !missing.is_empty() { + self.ask(want, source, column, missing, jobs); + } + let paging = self.paging.as_mut().expect("paged"); + for (page, shapes, whole) in made { + paging.shapes.pages.insert(page, ShapedPage { id: PageId::next(), shapes, whole }); + } + paging.shapes.kept.extend(kept); + paging.shapes.version = Version::next(); + // Every page drawn whole and no job under way: what landed is in its pages, and the order + // before has served the pages made afresh. + if self.working.is_none() && paging.shapes.pages.values().all(|page| page.whole) { + self.landed.clear(); + self.before = None; + } + true + } + + /// Shapes `missing` on the frame, if they fit what is left of its budget; whether they did. + fn shape_now(&mut self, want: &Want, column: &Column, missing: &Missing, jobs: &mut Jobs<'_>) -> bool { + let points: usize = missing.iter().map(|&(row, ..)| points_of(column, row)).sum(); + if !jobs.fits((points as f64 * TRIANGULATE).ceil() as u64) { + return false; + } + for (row, id, digest) in missing { + let Some(value) = column.get(*row) else { continue }; + let shape = want.shaping.shape(&value).unwrap_or(value); + self.landed.insert(id.clone(), (*digest, shape)); + } + true + } + + /// Asks a job to shape `missing`, unless one is under way: the rows it leaves wait for the next. + fn ask(&mut self, want: &Want, source: &Table, column: &Column, missing: Missing, jobs: &mut Jobs<'_>) { + if self.working.is_some() { + return; + } + let rows: Vec = missing.iter().map(|&(row, ..)| row).collect(); + let job = Job::shapes(want.shaping, rows_of(source, column, &rows)); + let asked = missing.into_iter().map(|(_, id, digest)| (id, digest)).collect(); + self.working = Some((jobs.spawn(job), asked)); + } + /// The derived column for `column` of `source`, page by page: a page whose source page is the /// one shaped last keeps its shapes; others are assembled row by row from shapes landed, or kept - /// from the last set where a row's geometry did not change. Rows with neither are missing: each - /// row, its id, and the digest of its geometry. - fn build(&self, want: &Want, source: &Table, column: &Column, depth: u8) -> (Column, Vec<(usize, Id, u64)>) { + /// from the last set where a row's geometry did not change. Rows with neither are missing. + fn build(&self, want: &Want, source: &Table, column: &Column, depth: u8) -> (Column, Missing) { let made_pages: FxHashMap = self .made .iter() @@ -218,32 +423,9 @@ impl Shaped { let mut shapes = GeometryBuilder::new(depth); for local in 0..page.values.len() { let row = p * PAGE_ROWS + local; - let Some(id) = source.ids.id(row) else { - shapes.push_empty(); - continue; - }; - if !page.values.is_valid(local) { - shapes.push_empty(); - continue; - } - let digest = geometry.row_hash(local); - if let Some((d, shape)) = self.landed.get(&id) - && *d == digest - { - shapes.push_value(shape); - } else if let Some((page, at)) = self.kept(&id, digest) { - shapes.push_from(page, at); - } else if want.shaping == Shaping::Path { - // Bending is cheap, so done here; rows that need none keep their geometry. - match page.values.get(local).and_then(|v| want.shaping.shape(&v)) { - Some(shape) => { - shapes.push_value(&shape); - } - None => shapes.push_from(geometry, local), - } - } else { - missing.push((row, id, digest)); - shapes.push_empty(); + match source.ids.id(row).filter(|_| page.values.is_valid(local)) { + Some(id) => self.shape(want, &mut shapes, (&page.values, local), row, id, &mut missing), + None => shapes.push_empty(), } } pages.push(Page::new(Values::of_geometry(shapes.finish()))); @@ -252,9 +434,81 @@ impl Shaped { (Column::of_pages(pages, &like), missing) } + /// Page `page` of `order` shaped: the shapes of its rows, in its order, as [`shape`](Shaped::shape) + /// finds them; and the rows left to shape. + fn make_page( + &self, + want: &Want, + order: &Order, + page: u32, + source: &Table, + column: &Column, + depth: u8, + ) -> (Values, Missing) { + self.make_rows(want, &order.page(page), source, column, depth) + } + + /// Rows `rows` of `source` shaped, in that order, as [`shape`](Shaped::shape) finds them; and the + /// rows left to shape. + fn make_rows(&self, want: &Want, rows: &[u32], source: &Table, column: &Column, depth: u8) -> (Values, Missing) { + let mut shapes = GeometryBuilder::new(want.shaping.depth(depth)); + let mut missing = Vec::new(); + for row in rows.iter().map(|&r| r as usize) { + let found = column.at(row).filter(|&(values, i)| values.is_valid(i)); + match (found, source.ids.id(row)) { + (Some(at), Some(id)) => self.shape(want, &mut shapes, at, row, id, &mut missing), + _ => shapes.push_empty(), + } + } + (Values::of_geometry(shapes.finish()), missing) + } + + /// Writes the shape of row `row`, with `id`, whose geometry is value `at.1` of `at.0`: one landed + /// or made before while its geometry is the same, bent at once for a path, or else empty and the + /// row left to shape in `missing`. + fn shape( + &self, + want: &Want, + shapes: &mut GeometryBuilder, + (values, i): (&Values, usize), + row: usize, + id: Id, + missing: &mut Missing, + ) { + let Some(geometry) = values.geometry() else { + shapes.push_empty(); + return; + }; + let digest = geometry.row_hash(i); + if let Some((d, shape)) = self.landed.get(&id) + && *d == digest + { + shapes.push_value(shape); + } else if let Some((page, at)) = self.kept(&id, digest) { + shapes.push_from(page, at); + } else if want.shaping == Shaping::Path { + // Bending is cheap, so done here; rows that need none keep their geometry. + match values.get(i).and_then(|v| want.shaping.shape(&v)) { + Some(shape) => { + shapes.push_value(&shape); + } + None => shapes.push_from(geometry, i), + } + } else { + missing.push((row, id, digest)); + shapes.push_empty(); + } + } + + /// Where a shape made before holds the row with `id`, when its geometry's digest was `digest`: in + /// the last whole set, or in a page made whole of the order drawn in part before. + fn kept(&self, id: &Id, digest: u64) -> Option<(&Geometry, usize)> { + self.whole(id, digest).or_else(|| self.before.as_ref()?.held(id, digest)) + } + /// Where the last whole set holds the shape of the row with `id`, when its geometry's digest was /// `digest`: the derived page's geometry and the row's place in it. - fn kept(&self, id: &Id, digest: u64) -> Option<(&crate::data::Geometry, usize)> { + fn whole(&self, id: &Id, digest: u64) -> Option<(&Geometry, usize)> { let (table, field, derived) = self.made.as_ref()?; let row = table.row(id.as_ref())?; let (page, at) = (row / PAGE_ROWS, row % PAGE_ROWS); @@ -275,8 +529,7 @@ fn bends(coords: &[[f64; 2]]) -> bool { /// How many points row `row` of geometry column `column` holds. fn points_of(column: &Column, row: usize) -> usize { - let Some(page) = column.pages().get(row / PAGE_ROWS) else { return 0 }; - page.values.geometry().map_or(0, |g| g.row_coords(row % PAGE_ROWS).len()) + column.at(row).and_then(|(values, i)| values.geometry().map(|g| g.row_coords(i).len())).unwrap_or(0) } /// Rows `rows` of `source`, by id, with their geometry from `column` as field `geometry`: what a @@ -286,11 +539,82 @@ fn rows_of(source: &Table, column: &Column, rows: &[usize]) -> Table { let depth = column.kind().depth().expect("a geometry column"); let mut geometry = GeometryBuilder::new(depth); for &row in rows { - let page = &column.pages()[row / PAGE_ROWS]; - match page.values.geometry() { - Some(g) if page.values.is_valid(row % PAGE_ROWS) => geometry.push_from(g, row % PAGE_ROWS), + match column.at(row).filter(|&(values, i)| values.is_valid(i)) { + Some((values, i)) if let Some(g) = values.geometry() => geometry.push_from(g, i), _ => geometry.push_empty(), } } Table::new(source.name.clone(), ids).field("geometry", Values::of_geometry(geometry.finish())) } + +#[cfg(test)] +mod tests { + use std::sync::{Arc, Mutex}; + use std::task::Waker; + + use super::*; + use crate::derive::pyramid::Pyramid; + use crate::gpu::work::Counters; + use crate::{Areas, Data, Ids, Plane, Scene}; + + type Ring = Vec<[f64; 2]>; + + /// A square a unit across at `x`. + fn square(x: f64) -> Vec { + vec![vec![[x, 0.0], [x + 1.0, 0.0], [x + 1.0, 1.0], [x, 1.0], [x, 0.0]]] + } + + /// The pages made of the shapes of `shapes`' one table drawn in part, and their ids. + fn made(shapes: &Shapes) -> Vec<(u32, PageId)> { + let paged = shapes.paged(&"map".into(), &"Zone".into()); + let mut pages: Vec<(u32, PageId)> = paged[0].1.pages.iter().map(|(&page, made)| (page, made.id)).collect(); + pages.sort_unstable_by_key(|&(page, _)| page); + pages + } + + #[test] + fn a_table_drawn_in_part_is_shaped_a_page_drawn_at_a_time() { + let n = 3 * PAGE as usize; + let mut data = Data::new("map"); + let squares: Vec> = (0..n).map(|i| square(i as f64)).collect(); + data.apply(Table::new("Zone", Ids::count(n as u32)).field("geometry", squares)).expect("valid"); + let scene = Scene::new(Plane::default()).draw(Areas::of("map", "Zone")); + // The order draws the rows backward. + let pyramid = Arc::new(Pyramid::new((0..n as u32).rev().collect(), Vec::new())); + let order = Order::Pyramid { pyramid, rows: n as u32 }; + let (counters, errors) = (Counters::default(), Arc::new(Mutex::new(Vec::new()))); + let mut shapes = Shapes::default(); + let update = |shapes: &mut Shapes, data: &Data, pages: Vec| { + let mut paged = Keyed::default(); + paged.insert(("map".into(), "Zone".into()), Paged { order: order.clone(), pages, kept: Vec::new() }); + let input = Input { + scene: &scene, + scene_version: 1, + data: std::slice::from_ref(data), + host: None, + dragging: false, + paged: Some(&paged), + }; + shapes.update(&input, &mut Jobs::new(None, Waker::noop(), &counters, &errors, u32::MAX)); + }; + update(&mut shapes, &data, vec![1]); + let first = made(&shapes); + assert_eq!(first.iter().map(|&(page, _)| page).collect::>(), [1], "only the page drawn is shaped"); + let paged = shapes.paged(&"map".into(), &"Zone".into()); + let page = &paged[0].1.pages[&1]; + assert_eq!(page.shapes.len(), PAGE as usize); + let triangles = page.shapes.geometry().expect("triangles").row_coords(0).len(); + assert!(triangles >= 3, "its rows' triangles, in the order's order: {triangles} points"); + // Drawing another page shapes it alone; the first keeps its shapes. + update(&mut shapes, &data, vec![1, 2]); + let second = made(&shapes); + assert_eq!((second.len(), second[0]), (2, first[0])); + // A write to a row drawn in page 2 shapes that page again, and only it. + let row = u64::from(order.source(2 * PAGE + 5)); + data.apply(Table::new("Zone", vec![row]).field("geometry", vec![square(-9.0)])).expect("valid"); + update(&mut shapes, &data, vec![1, 2]); + let third = made(&shapes); + assert_eq!(third[0], second[0], "page 1 holds no row written"); + assert_ne!(third[1].1, second[1].1, "page 2 is shaped again"); + } +} diff --git a/crates/latkit/src/derive/sight.rs b/crates/latkit/src/derive/sight.rs index 8d199c3a..4ed7a231 100644 --- a/crates/latkit/src/derive/sight.rs +++ b/crates/latkit/src/derive/sight.rs @@ -205,18 +205,23 @@ impl Sight<'_> { boxes(lon - half, lon + half, south, north) } - /// How large a tile of bounds `b` draws where it lies nearest each focus, and how far from the - /// view's middle that lands. - pub(crate) fn measure(&self, focus: &[[f64; 2]], b: [f64; 4]) -> (f64, f64) { - let [w, s, e, n] = b; - let (dx, dy) = ((e - w) / 64.0, (n - s) / 64.0); + /// How large tile `key` of `domain`'s square draws where it lies nearest each focus, and how far + /// from the view's middle that lands. It steps across the tile in the square, where the tiles of a + /// level are alike, so on a flat map they all measure the same wherever they lie. + pub(crate) fn measure(&self, focus: &[[f64; 2]], domain: &Domain, key: TileKey) -> (f64, f64) { + let side = 1.0 / f64::from(1_u32 << key.z); + let (west, north) = (f64::from(key.x) * side, f64::from(key.y) * side); + let step = side / 64.0; let (mut size, mut distance) = (0.0_f64, f64::INFINITY); - for &[fx, fy] in focus { - let near = if self.earth { fx + 360.0 * ((w.midpoint(e) - fx) / 360.0).round() } else { fx }; - let (x, y) = (near.clamp(w, e - dx), fy.clamp(s, n - dy)); - let Some(p) = self.project([x, y]) else { continue }; - let reach = |q: Option<[f64; 2]>| q.map_or(0.0, |q| 64.0 * (q[0] - p[0]).hypot(q[1] - p[1])); - size = size.max(reach(self.project([x + dx, y]))).max(reach(self.project([x, y + dy]))); + for &f in focus { + let [fx, fy] = domain.unit(f); + // The earth's square wraps: the focus a turn either way, whichever lies nearer the tile. + let fx = if *domain == Domain::Earth { fx + (west + side / 2.0 - fx).round() } else { fx }; + let at = [fx.clamp(west, west + side - step), fy.clamp(north, north + side - step)]; + let Some(p) = self.project(domain.position(at)) else { continue }; + let reach = + |q: [f64; 2]| self.project(domain.position(q)).map_or(0.0, |q| 64.0 * (q[0] - p[0]).hypot(q[1] - p[1])); + size = size.max(reach([at[0] + step, at[1]])).max(reach([at[0], at[1] + step])); distance = distance.min((p[0] - self.size[0] / 2.0).hypot(p[1] - self.size[1] / 2.0)); } (size, distance) diff --git a/crates/latkit/src/derive/tiled.rs b/crates/latkit/src/derive/tiled.rs index e2071077..e06db797 100644 --- a/crates/latkit/src/derive/tiled.rs +++ b/crates/latkit/src/derive/tiled.rs @@ -1,31 +1,26 @@ //! Tables drawn in part. A table with a priority draws its rows in priority order, the highest on //! top; one too large to draw whole draws the rows of the tiles the camera asks for, or where nothing -//! places its rows, those that matter most. What a view draws of such a dataset is a dataset of its -//! own, of the same name, ids, and origin: each such table's rows drawn in drawing order, with the -//! rows the edges drawn end at carried along and the rows pinned (selected, or framed by a fit) -//! kept; other tables as they are, their references naming the rows drawn. Marks draw it like any -//! other dataset, so picks, selection, hover, transitions, lanes, and labels follow it. -//! -//! Each table's pyramid is worked out from where its rows lie and their priority: on the frame while -//! that fits its budget, else in a job of its own, so a dataset's tables are placed side by side and -//! each draws once its own lands; until then a table draws an even sample of its rows, as many as a -//! first look needs. A pyramid serves while the rows it was placed from hold. The rows drawn are laid -//! out again when the tiles asked for, the rows pinned, or the rows a table holds change; when only -//! values change, every row stays where it is drawn and each field writes only the rows whose values -//! changed, so a patch of one row costs one row. Only the fields marks read are gathered. - +//! places its rows, those that matter most. Each draws in an [`Order`]: its pyramid's, worked out from +//! where its rows lie and their priority, on the frame while that fits its budget, else in a job, a +//! dataset's tables in one; until it lands, an even sample of its rows first, as many as a first look +//! needs, after the rows the edges sampled end at. A table and the tables its edges end at draw from +//! their pyramids together, or from their samples. A pyramid serves while the rows it was made from +//! hold; rows appended since draw after them, so appending never thins what draws, until they are an +//! eighth as many and it is made again. What the camera asks for of a table is ranges of its order: +//! nothing is gathered as it moves. + +use std::ops::Range; use std::sync::Arc; -use rustc_hash::FxHashSet; - -use super::pyramid::{Leveling, Places, Placing, Pyramid, Wanted, answer, reaches}; +use super::order::{Lifted, Order, PAGE}; +use super::pyramid::{Leveling, Places, Placing, Pyramid, answer}; use super::sight::{Domain, Sight}; use super::{Jobs, stamp_of}; -use crate::data::{Data, Field, Keyed, Name, Named, RowIds, Table, UNWIRED, Version}; +use crate::Pending; +use crate::data::{Column, Data, Field, Keyed, Name, Named, Reference, Table, UNWIRED, Version}; use crate::expr::cpu::Rows; use crate::expr::{Expr, Node}; use crate::host::Job; -use crate::{Pending, Row}; /// Microseconds placing a row takes, by estimate. const PLACE_COST: f64 = 1.0; @@ -63,12 +58,6 @@ pub(crate) struct Planned { /// Each row's priority, as the first mark giving one has it. pub(crate) priority: Option, pub(crate) place: Place, - /// The fields marks read of it, which its rows drawn hold with every reference field; `None` - /// where they hold every field. - pub(crate) reads: Option>, - /// The sampled fields marks trace across the axis, which its rows drawn hold whole: every frame, - /// at every level, where other sampled fields hold only the frames about the coordinate shown. - pub(crate) traced: FxHashSet, } impl Planned { @@ -88,65 +77,51 @@ pub(crate) struct Ask<'a> { /// What the camera sees, on a map or a plane. pub(crate) sight: Option<&'a Sight<'a>>, pub(crate) earth: bool, - /// Rows drawn whatever the tiles: those selected, and those a fit frames. - pub(crate) pinned: &'a [Row], - /// The axis coordinate shown, which sampled fields drawn in part are gathered about. - pub(crate) at: f64, } -/// What each dataset drawn in part draws. +/// What a view draws of a table drawn in part: its order, and the ranges of it the camera asks for, +/// finest tiles first, so what matters more draws on top. +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct Chosen { + pub(crate) order: Order, + pub(crate) ranges: Vec>, +} + +/// What each table drawn in part draws, by dataset and table. +pub(crate) type Choices = Keyed<(Name, Name), Chosen>; + +/// The pyramids of each dataset drawn in part. #[derive(Default)] pub(crate) struct Tiled { sets: Vec, } impl Tiled { - /// Brings what each dataset drawn in part draws up to `ask`: the datasets drawn that changed, and - /// the names of those no longer drawn in part, which draw as they are. - pub(crate) fn update(&mut self, ask: &Ask<'_>, jobs: &mut Jobs<'_>) -> (Vec, Vec) { + /// Brings each dataset's pyramids up to `ask`, and chooses what each table drawn in part draws. + pub(crate) fn update(&mut self, ask: &Ask<'_>, jobs: &mut Jobs<'_>) -> Choices { let mut wanted: Vec<&Name> = Vec::new(); for ((data, _), planned) in ask.plans { if !planned.as_is() && !wanted.contains(&data) { wanted.push(data); } } - let mut gone = Vec::new(); - self.sets.retain(|set| { - let keep = wanted.contains(&&set.name) && ask.sources.iter().any(|d| *d.name() == set.name); - if !keep { - gone.push(set.name.clone()); - } - keep - }); - let mut changed = Vec::new(); + self.sets.retain(|set| wanted.contains(&&set.name) && ask.sources.iter().any(|d| *d.name() == set.name)); + let mut choices = Choices::default(); for name in wanted { let Some(source) = ask.sources.iter().find(|d| d.name() == name) else { continue }; let i = self.sets.iter().position(|s| s.name == *name).unwrap_or_else(|| { self.sets.push(Set::new(name.clone())); self.sets.len() - 1 }); - if let Some(drawn) = self.sets[i].update(source, ask, jobs) { - changed.push(drawn); - } + self.sets[i].update(source, ask, jobs, &mut choices); } - (changed, gone) + choices } - /// Whether every table drawn in part draws from its pyramid, its rows in drawing order: none draws - /// its first sample while its pyramid is worked out, nor waits to be laid out again. + /// Whether every table drawn in part draws from its pyramid: none draws its first sample while + /// its pyramid is worked out. pub(crate) fn complete(&self) -> bool { - self.sets.iter().all(|s| s.working.is_empty() && !s.deferred && !s.sampling && s.settled()) - } - - /// Whether a dataset waits for a frame: to ask for its pyramids, or to lay its rows out again in - /// drawing order once the camera rests. - pub(crate) fn due(&self) -> bool { - self.sets.iter().any(|s| s.deferred || !s.settled()) - } - - /// Whether dataset `name` draws in part. - pub(crate) fn draws(&self, name: &str) -> bool { - self.sets.iter().any(|s| *s.name == *name) + self.sets.iter().all(|s| s.working.is_none() && !s.sampling) } /// The bounds of every row of table `table` of dataset `data` drawn through tiles, west, south, @@ -157,73 +132,81 @@ impl Tiled { } } -/// A table's pyramid: the rows it was placed from (the table's structure and order), the square it -/// tiles, the bounds of its rows in position units, and the version that tells it from any other. +/// A table's pyramid: the order of the table's rows it was made from, the square it tiles, and the +/// bounds of its rows in position units. struct Placed { - made: Version, - rows: (Version, Version), + order: Version, domain: Domain, - pyramid: Pyramid, + pyramid: Arc, bounds: Option<[f64; 4]>, } -/// A pyramid job under way: the table it places, where the answer holds its pyramid, and what the -/// pyramid is kept with once it lands. +impl Placed { + /// Whether it serves `table`: the rows it was made from hold, rows appended since aside. + fn serves(&self, table: &Table) -> bool { + self.order == table.order && table.len() >= self.pyramid.rows() as usize + } +} + +/// A dataset's pyramid job under way: the tables it answers, by their place among those asked, each +/// with the order of its rows asked from, and the square and bounds its pyramid is kept with. struct Working { - table: Name, pending: Pending>, - answered: u32, - placed: Placed, + tables: Vec<(u32, Name, Version, Option<[f64; 4]>)>, + domain: Domain, } -/// One table's rows drawn last time and where each lies, the versions of the table drawn while they -/// hold, and its fields as gathered with what each was gathered from. -#[derive(Default)] -struct Gathered { - /// Each row drawn: its source row, or a [`HOLE`]. - rows: Option>, - slots: Slots, - /// The rows written since the ids and fields kept were gathered, `(row, source row or HOLE)` by - /// row; `None` where every row was laid out afresh, and they gather again. - writes: Option>, - versions: Option<(Version, Version)>, - ids: Option, - fields: Named, - /// Each reference field's ids as gathered and resolved, as of a version of it, and of that version - /// and the versions the rows it names hold where they are drawn; and every row's target by it, as - /// of a version of it. - references: Named<(u64, u64, Field)>, - targets: Named<(Version, Arc<[u32]>)>, +/// What a table's pyramid was asked for from: its rows and their order then, how it is placed, and +/// the columns placing it, as last seen; and whether a row of those it had was written since. +struct Read { + rows: usize, + order: Version, + inputs: u64, + columns: Vec<(Name, Column)>, + written: bool, } -/// A field as gathered: a stamp of what it was gathered from, that field of the table, and the field -/// drawn. -struct Kept { - stamp: u64, - source: Field, - drawn: Field, +impl Read { + fn new(table: &Table, planned: &Planned, earth: bool) -> Read { + let columns = fields_read(planned) + .into_iter() + .filter_map(|name| column_of(table, &name).map(|column| (name.clone(), column.clone()))) + .collect(); + Read { rows: table.len(), order: table.order, inputs: inputs_of(planned, earth), columns, written: false } + } + + /// Sees `table` anew: whether a row it had was written since, or its rows reordered or removed. + /// Once one was, it stays so. + fn see(&mut self, table: &Table) { + if self.written { + return; + } + let rows = self.rows; + self.written = table.order != self.order + || table.len() < rows + || self.columns.iter_mut().any(|(name, then)| match column_of(table, name) { + Some(now) if now.kept_below(then, rows) => { + *then = now.clone(); + false + } + _ => true, + }); + } } /// One dataset drawn in part. struct Set { name: Name, - /// The stamp of everything its pyramids are placed from: every table placed, and the square. + /// The stamp of everything its pyramids are placed from, as last seen. stamp: u64, /// Each table's pyramid, as last placed. - placed: Named>, - working: Vec, - /// Whether asking for its pyramids waits for a frame that gathers nothing. - deferred: bool, - /// What decides which rows draw and where, and what they hold, as last gathered. - laid: u64, - held: u64, - /// Updates since its rows were laid out. - quiet: u32, - tables: Named, - /// Each table drawn in part as last laid out: its rows drawn, and the versions it holds while they - /// do. - rows: Named>, - versions: Named<(Version, Version)>, + placed: Named, + working: Option, + /// What the pyramids last asked for were asked from, by table. + read: Named, + /// Each table's rows that the samples of the edges ending at it end at, and what they were worked + /// out from. + lifted: Named<(u64, Lifted)>, /// Whether a table draws an even sample of its rows until its pyramid lands. sampling: bool, } @@ -234,672 +217,233 @@ impl Set { name, stamp: 0, placed: Named::default(), - working: Vec::new(), - deferred: false, - laid: 0, - held: 0, - quiet: 0, - tables: Named::default(), - rows: Named::default(), - versions: Named::default(), + working: None, + read: Named::default(), + lifted: Named::default(), sampling: false, } } - /// Whether every table's rows lie in drawing order: none waits to be laid out again. - fn settled(&self) -> bool { - self.tables.values().all(|g| g.slots.ordered || g.slots.rows.is_empty()) - } - - /// Brings the pyramids and the rows drawn up to `ask`; the dataset drawn, when it changed. - fn update(&mut self, source: &Data, ask: &Ask<'_>, jobs: &mut Jobs<'_>) -> Option { + /// Brings the pyramids up to `ask`, and chooses what each table drawn in part draws. + fn update(&mut self, source: &Data, ask: &Ask<'_>, jobs: &mut Jobs<'_>, choices: &mut Choices) { + let landed = self.land(); let placing = placed(source, ask); - // Pyramids that landed serve from now on. - let mut i = 0; - while i < self.working.len() { - let Some(landed) = self.working[i].pending.take() else { - i += 1; - continue; - }; - let working = self.working.swap_remove(i); - if let Ok(bytes) = landed - && let Some(pyramid) = Pyramid::read(&bytes).remove(&working.answered) - { - self.placed - .insert(working.table, Arc::new(Placed { pyramid, made: Version::next(), ..working.placed })); - } - } let stamp = stamp(source, ask, &placing); - let due = !placing.is_empty() && stamp != self.stamp; - // Pyramids small enough are placed at once, before the rows drawn are gathered from them; - // asking jobs for larger ones waits for a frame that gathers nothing, so no frame does both. - let cost: f64 = placing - .iter() - .map(|(t, p)| { - t.len() as f64 * if matches!(p.draw, Draw::Tiles { .. }) { PLACE_COST } else { PLACE_COST / 20.0 } - }) - .sum(); - let at_once = cost <= jobs.left() as f64; - if due && at_once { - self.level(&placing, ask, stamp, jobs); - } - let drawn = self.gather(source, ask); - self.deferred = due && !at_once && drawn.is_some(); - if due && !at_once && drawn.is_none() { - self.level(&placing, ask, stamp, jobs); - } - drawn - } - - /// Asks for the pyramids of `placing`, from what `stamp` stamps: at once where one fits the - /// frame, else in a job of its own. - fn level(&mut self, placing: &[(&Table, &Planned)], ask: &Ask<'_>, stamp: u64, jobs: &mut Jobs<'_>) { - { + if !placing.is_empty() && (stamp != self.stamp || landed) { self.stamp = stamp; - // What was asked before no longer serves once it lands. - self.working.clear(); - let (leveling, bounds) = leveling(ask, placing); - let domain = leveling.domain; - for (i, table) in leveling.tables.iter().enumerate().filter(|(_, t)| t.answered) { - let name = placing[i].0.name.clone(); - // A table placed by its ends goes with the table they name, which only anchors it. - let mut alone = Leveling { domain, tables: Vec::with_capacity(2) }; - let mut table = table.clone(); - if let Some((target, _)) = table.ends.as_mut() { - let at = &leveling.tables[*target as usize]; - alone.tables.push(Placing { rows: at.rows, places: at.places.clone(), ..Placing::default() }); - *target = 0; - } - let answered = alone.tables.len() as u32; - let rows = (placing[i].0.structure, placing[i].0.order); - alone.tables.push(table); - // Placing a row takes about a microsecond; ordering one by priority alone, far less. - let per_row = if alone.tables[answered as usize].leveled { PLACE_COST } else { PLACE_COST / 20.0 }; - let cost = per_row * alone.tables.iter().map(|t| f64::from(t.rows)).sum::(); - let made = Version::next(); - let placed = - Placed { made, rows, domain, pyramid: Pyramid::default(), bounds: bounds.get(&name).copied() }; - if jobs.fits(cost as u64) { - let pyramid = Pyramid::answered(answer(&alone)).remove(&answered).unwrap_or_default(); - self.placed.insert(name, Arc::new(Placed { pyramid, ..placed })); - } else { - let job = Job::pyramid(&alone); - let pending = jobs.spawn(job); - self.working.push(Working { table: name, pending, answered, placed }); - } - } - } - } - - /// Table `name`'s pyramid while it serves: placed from the rows the table holds. - fn pyramid(&self, source: &Data, name: &Name) -> Option> { - let table = source.table(name)?; - self.placed.get(name).filter(|p| p.rows == (table.structure, table.order)).cloned() - } - - /// The dataset drawn, gathered again when what it is gathered from changed. - fn gather(&mut self, source: &Data, ask: &Ask<'_>) -> Option { - // Each table's tiles drawn, or how many of its rows, by what it draws. - let mut chosen: Vec<(Name, Choice)> = Vec::new(); - let mut serving: Vec<(Name, Arc)> = Vec::new(); - for table in source.tables() { - let Some(planned) = ask.plans.get(&(source.name().clone(), table.name.clone())) else { continue }; - if planned.as_is() { - continue; - } - let placed = self.pyramid(source, &table.name); - if let Some(placed) = &placed { - serving.push((table.name.clone(), placed.clone())); - } - let pyramid = placed.as_deref().map(|placed| (&placed.pyramid, &placed.domain)); - let choice = match (pyramid, planned.draw) { - (None, Draw::Whole) => Choice::AsIs, - (None, Draw::Tiles { rows } | Draw::First { rows }) => { - Choice::Sample((table.len() as u32).div_ceil(rows.clamp(1, SAMPLE)).max(1)) - } - (Some(_), Draw::Whole) => Choice::Tiles(Wanted { tiles: vec![(0, true)], cover: None }), - (Some((pyramid, _)), Draw::First { rows }) => Choice::Last(rows.min(pyramid.order.len() as u32)), - (Some((pyramid, domain)), Draw::Tiles { rows }) => { - Choice::Tiles(ask.sight.map(|sight| pyramid.wanted(sight, domain, rows)).unwrap_or_default()) + for (table, _) in &placing { + if let Some(read) = self.read.get_mut(&table.name) { + read.see(table); } - }; - chosen.push((table.name.clone(), choice)); - } - self.sampling = chosen.iter().any(|(_, choice)| matches!(choice, Choice::Sample(_))); - let pinned: Vec<&Row> = ask.pinned.iter().filter(|r| r.data == self.name).collect(); - let frames: Vec> = sampled_frames(source, ask); - let pyramids: Vec = serving.iter().map(|(_, p)| p.made).collect(); - // Where rows draw, apart from what they hold: a patch writing values lays nothing out again. - let laid = stamp_of((pyramids, &chosen, &pinned, rows_of(source))); - let held = stamp_of((source.0.version, frames, reads_of(source, ask))); - if laid == self.laid { - // Once the camera rests, rows laid out of drawing order are laid out again in it. - self.quiet = self.quiet.saturating_add(1); - if held == self.held && (self.settled() || self.quiet < SETTLE) { - return None; } - } else { - self.quiet = 0; - } - let afresh = self.quiet >= SETTLE && !self.settled(); - let relaid = laid != self.laid || afresh; - (self.laid, self.held) = (laid, held); - if !relaid { - return Some(self.rewrite(source, ask)); - } - let serving: Named> = serving.into_iter().collect(); - Some(self.build(source, ask, &serving, &chosen, &pinned, afresh)) - } - - /// The dataset drawn when only what its rows hold changed: every row stays where it is drawn, and - /// each field writes only the rows whose values changed. - fn rewrite(&mut self, source: &Data, ask: &Ask<'_>) -> Data { - let (rows, versions) = (std::mem::take(&mut self.rows), std::mem::take(&mut self.versions)); - for name in rows.keys() { - if let Some(gathered) = self.tables.get_mut(name) { - gathered.writes = Some(Vec::new()); + if !self.holds(&placing, ask.earth) { + self.level(&placing, ask, jobs); } } - let tables: Vec
= - source.tables().map(|table| self.drawn(source, table, ask, &rows, &versions)).collect(); - (self.rows, self.versions) = (rows, versions); - source.drawn(tables) - } - - /// The dataset drawn: every table's rows drawn, pinned rows kept, the ends of edges drawn carried - /// along, each table's rows laid out, afresh where `afresh` says so, and gathered. - fn build( - &mut self, - source: &Data, - ask: &Ask<'_>, - serving: &Named>, - chosen: &[(Name, Choice)], - pinned: &[&Row], - afresh: bool, - ) -> Data { - let mut wanted: Named> = chosen - .iter() - .filter_map(|(name, choice)| { - let len = source.table(name).map_or(0, Table::len) as u32; - Some((name.clone(), choice.rows(serving.get(name).map(Arc::as_ref), len)?)) + self.sampling = false; + let drawn: Vec<(&Table, &Planned)> = source + .tables() + .filter_map(|table| { + let planned = ask.plans.get(&(source.name().clone(), table.name.clone()))?; + (!planned.as_is()).then_some((table, planned)) }) .collect(); - // Rows pinned, and the rows the edges drawn end at, draw whatever their tiles. - let mut drawn: Named = wanted + // Each draws from its pyramid while that serves it, and while the pyramids of the table its + // edges end at and of the edges ending at it do: edges land with their ends. + let ends: Vec> = drawn .iter() - .map(|(name, list)| (name.clone(), Bitset::of(list, source.table(name).map_or(0, Table::len)))) + .map(|(table, planned)| { + let to = ends_of(table, planned).filter(|to| **to != table.name)?; + drawn.iter().position(|(t, _)| t.name == *to) + }) .collect(); - for row in pinned { - let (Some(list), Some(table)) = (wanted.get_mut(&row.table), source.table(&row.table)) else { continue }; - if let Some(r) = table.row(row.id.as_ref()) - && drawn.get_mut(&row.table).is_some_and(|set| set.insert(r as u32)) - { - list.push(r as u32); - } - } - let mut carried: Vec<(Name, Vec)> = Vec::new(); - for table in source.tables() { - // Only the ends of edges and faces drawn are carried. - let Some(Place::Ends(ends)) = ask.plans.get(&(source.name().clone(), table.name.clone())).map(|p| &p.place) - else { - continue; - }; - for (name, field) in &table.fields { - let Field::Reference(reference) = field else { continue }; - if !wanted.contains_key(&reference.to) || !ends.contains(name) { - continue; - } - let targets = self.targets(&table.name, name, field); - let ends: Vec = match wanted.get(&table.name) { - Some(list) => list.iter().map(|&r| targets[r as usize]).collect(), - None => targets.to_vec(), - }; - carried.push((reference.to.clone(), ends)); - } - } - for (to, ends) in carried { - let (Some(list), Some(set)) = (wanted.get_mut(&to), drawn.get_mut(&to)) else { continue }; - for t in ends { - if t != UNWIRED && set.insert(t) { - list.push(t); - } - } - } - // Each table drawn in part: its rows laid out, and the versions it holds while they do. - let mut rows: Named> = Named::default(); - let mut versions: Named<(Version, Version)> = Named::default(); - for (name, list) in &wanted { - let gathered = self.tables.entry(name.clone()).or_default(); - let len = source.table(name).map_or(0, Table::len); - let writes = gathered.slots.lay(list, len, afresh); - let laid: Arc<[u32]> = match (&writes, &gathered.rows) { - (Some(writes), Some(before)) if writes.is_empty() => before.clone(), - _ => gathered.slots.rows.as_slice().into(), - }; - // Rows written in place keep the order the table's rows are drawn in, as every row still - // drawn keeps its place; rows laid out afresh draw in another. - let held = match (&writes, gathered.versions) { - (Some(writes), Some(held)) if writes.is_empty() => held, - (Some(_), Some((order, _))) => (order, Version::next()), - _ => { - let v = Version::next(); - (v, v) - } - }; - gathered.versions = Some(held); - versions.insert(name.clone(), held); - if writes.is_none() { - gathered.ids = None; - gathered.fields.clear(); - gathered.references.clear(); - } - gathered.writes = writes.map(|w| { - w.iter().map(|&(at, r)| (at as usize, if r == HOLE { crate::data::HOLE } else { r as usize })).collect() - }); - gathered.rows = Some(laid.clone()); - rows.insert(name.clone(), laid); - } - let mut tables = Vec::with_capacity(source.tables().count()); - for table in source.tables() { - tables.push(self.drawn(source, table, ask, &rows, &versions)); - } - (self.rows, self.versions) = (rows, versions); - source.drawn(tables) - } - - /// Every row's target by reference field `name` of table `table`, `field`, as of its version. - fn targets(&mut self, table: &Name, name: &Name, field: &Field) -> Arc<[u32]> { - let Field::Reference(reference) = field else { return Arc::from([]) }; - let gathered = self.tables.entry(table.clone()).or_default(); - if let Some((version, targets)) = gathered.targets.get(name) - && *version == reference.version + let mut served: Vec = + drawn.iter().map(|(table, _)| self.placed.get(&table.name).is_some_and(|p| p.serves(table))).collect(); + while let Some((i, j)) = + ends.iter().enumerate().find_map(|(i, end)| end.filter(|&j| served[i] != served[j]).map(|j| (i, j))) { - return targets.clone(); + (served[i], served[j]) = (false, false); } - let targets: Arc<[u32]> = reference.targets().into(); - gathered.targets.insert(name.clone(), (reference.version, targets.clone())); - targets - } - - /// Table `table` as it is, its references naming the rows drawn of the tables drawn in part, each - /// holding `versions` while they do: resolved again only when the reference or where the rows it - /// names are drawn changed. - fn remapped(&mut self, table: &Table, versions: &Named<(Version, Version)>) -> Table { - let mut out = table.clone(); - for (name, field) in &table.fields { - let Field::Reference(reference) = field else { continue }; - let Some(&(order, structure)) = versions.get(&reference.to) else { continue }; - let whole = stamp_of((reference.version, order, structure)); - let kept = self.tables.get(&table.name).and_then(|g| g.references.get(name)); - if let Some((_, _, field)) = kept.filter(|(_, w, _)| *w == whole) { - out.insert(name.clone(), field.clone()); - continue; - } - let targets = self.targets(&table.name, name, field); - let map = &self.tables[&reference.to].slots.at; - let mapped: Vec = targets.iter().map(|&t| remap(map, t)).collect(); - let kept = self.tables.get(&table.name).and_then(|g| g.references.get(name)).map(|(_, _, f)| f.clone()); - let kept = kept.as_ref().and_then(|f| if let Field::Reference(r) = f { Some(r) } else { None }); - let remapped = reference.resolved_to(reference.ids.clone(), &mapped, order, structure, kept); - let field = Field::Reference(remapped); - let gathered = self.tables.entry(table.name.clone()).or_default(); - gathered.references.insert(name.clone(), (stamp_of(reference.version), whole, field.clone())); - out.insert(name.clone(), field); - } - out - } - - /// Table `table` as drawn: as it is, its references naming the rows drawn; or its rows drawn - /// in part, with the fields marks read. Rows written in place write the ids and fields kept, - /// touching only their pages, and so do rows whose values changed; rows laid out afresh gather - /// them again. What nothing changed under is kept as it is. - fn drawn( - &mut self, - source: &Data, - table: &Table, - ask: &Ask<'_>, - rows: &Named>, - versions: &Named<(Version, Version)>, - ) -> Table { - // A table no mark draws stays as it is: only joins read it, by id. - let planned = ask.plans.contains_key(&(source.name().clone(), table.name.clone())); - let remaps = table.fields.iter().any(|(_, f)| matches!(f, Field::Reference(r) if rows.contains_key(&r.to))); - let Some(list) = rows.get(&table.name) else { - return if remaps && planned { self.remapped(table, versions) } else { table.clone() }; - }; - let plan = ask.plans.get(&(source.name().clone(), table.name.clone())); - let reads = plan.and_then(|p| p.reads.as_ref()); - let traced = |name: &Name| plan.is_some_and(|p| p.traced.contains(name)); - // The source row of each row drawn, picked only where something gathers afresh. - let picks = std::cell::OnceCell::new(); - let picked = || { - picks.get_or_init(|| { - list.iter().map(|&r| if r == HOLE { crate::data::HOLE } else { r as usize }).collect::>() + let steps: Vec = drawn + .iter() + .map(|(table, planned)| match planned.draw { + Draw::Tiles { rows } | Draw::First { rows } => { + (table.len() as u32).div_ceil(rows.clamp(1, SAMPLE)).max(1) + } + Draw::Whole => 1, }) - }; - let (order, structure) = versions[&table.name]; - let gathered = self.tables.get_mut(&table.name).expect("made with its rows"); - let writes = gathered.writes.take(); - let len = list.len(); - let ids = match (&gathered.ids, writes.as_deref()) { - (Some(ids), Some([])) => ids.clone(), - (Some(ids), Some(writes)) => ids.write(len, writes, &table.ids).expect("ids of one kind"), - _ => table.ids.gather(picked()), - }; - gathered.ids = Some(ids.clone()); - let mut out = - Table { name: table.name.clone(), ids, index: Arc::default(), fields: Vec::new(), structure, order }; - // Fields not read now miss the rows written: gathered again once read. - if writes.as_ref().is_some_and(|w| !w.is_empty()) { - gathered.fields.retain(|name, _| reads.is_none_or(|r| r.contains(name))); - } - let frames = |samples: &crate::data::Samples| samples.frame_at(ask.at); - for (name, field) in &table.fields { - let read = reads.is_none_or(|r| r.contains(name)); - let gathered = self.tables.get_mut(&table.name).expect("made with its rows"); - let kept = gathered.fields.get(name); - let (stamp, made) = match field { - Field::Values(column, version) if read => { - let stamp = stamp_of(version); - let made = match (kept, writes.as_deref()) { - (Some(kept), Some([])) if kept.stamp == stamp => kept.drawn.clone(), - // The rows laid out in place, and the rows whose values changed since the field - // kept was gathered, wherever they are drawn; a field now of another kind - // gathers afresh. - ( - Some(Kept { stamp: was, source: Field::Values(before, _), drawn: Field::Values(drawn, _) }), - Some(laid), - ) if before.kind() == column.kind() => { - // What the pages remember was written, else what differs: a row written - // the value it held is written again, cheaper than comparing every row. - let changed = if *was == stamp { - Vec::new() - } else { - column.rows_since(before).unwrap_or_else(|| column.differing(before)) - }; - let at = &gathered.slots.at; - let mut written: Vec<(usize, usize)> = changed - .into_iter() - .filter_map(|r| at.get(r).filter(|&&a| a != UNWIRED).map(|&a| (a as usize, r))) - .collect(); - written.extend_from_slice(laid); - crate::order::by_key(&mut written, |w| w.0 as u64); - written.dedup_by_key(|w| w.0); - Field::Values(drawn.write(len, &written, column).expect("one storage"), Version::next()) - } - _ => Field::Values(column.gather(picked()), Version::next()), + .collect(); + let mut lifted: Named<(u64, Lifted)> = Named::default(); + for (i, (table, planned)) in drawn.iter().enumerate() { + let key = (source.name().clone(), table.name.clone()); + let len = table.len() as u32; + let placed = self.placed.get(&table.name).filter(|_| served[i]); + let chosen = match (placed, planned.draw) { + (None, Draw::Whole) => continue, + (None, Draw::Tiles { .. } | Draw::First { .. }) => { + self.sampling = true; + // The rows the edges sampled end at draw first, as they land with them in a pyramid. + let edges: Vec = (0..drawn.len()).filter(|&e| ends[e] == Some(i)).collect(); + let rows = if edges.is_empty() { + Lifted::default() + } else { + let was = self.lifted.get(&table.name); + let made = lift(&drawn, &edges, &steps, i, was); + let rows = made.1.clone(); + lifted.insert(table.name.clone(), made); + rows }; - (stamp, made) + let order = Order::Sampled { rows: len, step: steps[i], lifted: rows }; + let sampled = order.sampled(); + Chosen { order, ranges: std::iter::once(0..sampled).collect() } } - Field::Sampled(samples, version) if read => { - // A trace draws every frame; else only the frames about the coordinate shown. - let frame = if traced(name) { None } else { frames(samples) }; - let stamp = stamp_of((version, frame, traced(name))); - let made = match (kept, writes.as_deref()) { - (Some(kept), Some([])) if kept.stamp == stamp => kept.drawn.clone(), - _ if traced(name) => Field::Sampled(samples.gather(picked()), Version::next()), - _ => { - let span = frame.map_or(0..0, |f| f..f + 2); - Field::Sampled(samples.gather_frames(picked(), span), Version::next()) + (Some(placed), draw) => { + let pyramid = &placed.pyramid; + let made = pyramid.rows(); + let ranges = match draw { + Draw::Whole => std::iter::once(0..len).collect(), + // The rows that matter most draw last; rows appended since draw too. + Draw::First { rows } => std::iter::once(made - rows.min(made)..len).collect(), + Draw::Tiles { rows } => { + let mut wanted = + ask.sight.map(|s| pyramid.wanted(s, &placed.domain, rows)).unwrap_or_default(); + // Finest first, so what matters more draws on top. + crate::order::by_key(&mut wanted, |(z, range)| { + !crate::order::pair(u32::from(*z), !range.start) + }); + let mut ranges: Vec> = wanted.into_iter().map(|(_, range)| range).collect(); + if len > made { + ranges.push(made..len); + } + ranges } }; - (stamp, made) - } - Field::Reference(reference) => { - // Where the rows it names are drawn: as laid out where drawn in part, else as they are. - let held = versions.get(&reference.to).copied(); - let named = held.unwrap_or_else(|| { - source.table(&reference.to).map_or((order, structure), |t| (t.order, t.structure)) - }); - let resolved = - self.reference(&table.name, name, field, list, writes.as_deref(), (held.is_some(), named)); - out.insert(name.clone(), resolved); - continue; + Chosen { order: Order::Pyramid { pyramid: pyramid.clone(), rows: len }, ranges } } - _ => continue, }; - let gathered = self.tables.get_mut(&table.name).expect("made with its rows"); - gathered.fields.insert(name.clone(), Kept { stamp, source: field.clone(), drawn: made.clone() }); - out.insert(name.clone(), made); - } - out - } - - /// Reference field `name` of table `table`, `field`, for the rows `list` drawn, resolved to where - /// the rows it names are drawn: `named` says whether those are drawn in part, and the versions they - /// hold. Kept as it is where no row was written here, the reference is the same, and the rows it - /// names hold; else its ids are written where rows were written in place. - fn reference( - &mut self, - table: &Name, - name: &Name, - field: &Field, - list: &[u32], - writes: Option<&[(usize, usize)]>, - (in_part, (order, structure)): (bool, (Version, Version)), - ) -> Field { - let Field::Reference(reference) = field else { return field.clone() }; - let stamp = stamp_of(reference.version); - let whole = stamp_of((reference.version, order, structure)); - let gathered = self.tables.entry(table.clone()).or_default(); - if let Some((_, _, field)) = gathered.references.get(name).filter(|(_, w, _)| *w == whole) - && writes.is_some_and(<[_]>::is_empty) - { - return field.clone(); - } - let targets = self.targets(table, name, field); - let gathered = self.tables.entry(table.clone()).or_default(); - let kept = gathered.references.get(name).filter(|(s, _, _)| *s == stamp).map(|(_, _, f)| f.clone()); - let kept = kept.as_ref().and_then(|f| if let Field::Reference(r) = f { Some(r) } else { None }); - let len = list.len(); - let ids = match (kept, writes) { - (Some(kept), Some(writes)) => kept.ids.write(len, writes, &reference.ids).expect("ids of one kind"), - (Some(kept), None) if kept.ids.len() == len => kept.ids.clone(), - _ => { - let picked: Vec = - list.iter().map(|&r| if r == HOLE { crate::data::HOLE } else { r as usize }).collect(); - reference.ids.gather(&picked) - } - }; - let target = self.tables.get(&reference.to).filter(|_| in_part); - let mapped: Vec = list - .iter() - .map(|&r| match (r, target) { - (HOLE, _) => UNWIRED, - (r, Some(target)) => remap(&target.slots.at, targets[r as usize]), - (r, None) => targets[r as usize], + choices.insert(key, chosen); + } + self.lifted = lifted; + } + + /// Whether what was last asked for still serves `placing`: the same tables, placed alike, none of + /// whose rows were written since but those appended; while no job is under way, each table's + /// pyramid serving it, the rows appended since fewer than an eighth of those it was made from, a + /// page at least. A job under way keeps going while only rows were appended since it was asked, + /// so appending never starves the pyramids; once it lands, it serves until the rows appended + /// since are that many. + fn holds(&self, placing: &[(&Table, &Planned)], earth: bool) -> bool { + placing.len() == self.read.len() + && placing.iter().all(|(table, planned)| { + let Some(read) = self.read.get(&table.name) else { return false }; + if read.written || read.inputs != inputs_of(planned, earth) { + return false; + } + let served = planned.as_is() || self.placed.get(&table.name).is_some_and(|p| p.serves(table)); + let appended = table.len().saturating_sub(read.rows); + self.working.is_some() || (served && appended < (read.rows / 8).max(PAGE as usize)) }) - .collect(); - let resolved = Field::Reference(reference.resolved_to(ids, &mapped, order, structure, kept)); - let gathered = self.tables.entry(table.clone()).or_default(); - gathered.references.insert(name.clone(), (stamp, whole, resolved.clone())); - resolved } -} - -/// Rows, a bit each: which rows of a table are drawn. -struct Bitset(Vec); -impl Bitset { - /// The rows of `list`, of a table of `len` rows. - fn of<'a>(list: impl IntoIterator, len: usize) -> Bitset { - let mut set = Bitset(vec![0; len.div_ceil(64)]); - for &r in list { - set.insert(r); + /// Takes the pyramids a job answered, once it landed: whether it placed any. One that failed is + /// asked again only once what it was asked from changes. + fn land(&mut self) -> bool { + let Some(working) = &self.working else { return false }; + let Some(landed) = working.pending.take() else { return false }; + let working = self.working.take().expect("under way"); + let Ok(bytes) = landed else { return false }; + for (index, pyramid) in Pyramid::read(&bytes) { + if let Some((_, name, order, bounds)) = working.tables.iter().find(|t| t.0 == index) { + let placed = + Placed { order: *order, domain: working.domain, pyramid: Arc::new(pyramid), bounds: *bounds }; + self.placed.insert(name.clone(), placed); + } } - set + true } - /// Whether row `r` is there. - fn contains(&self, r: u32) -> bool { - self.0.get(r as usize / 64).is_some_and(|word| word >> (r % 64) & 1 == 1) - } - - /// Adds row `r`; whether it was not there before. - fn insert(&mut self, r: u32) -> bool { - let Some(word) = self.0.get_mut(r as usize / 64) else { return false }; - let bit = 1 << (r % 64); - let new = *word & bit == 0; - *word |= bit; - new - } -} - -/// What a table drawn in part draws of its pyramid. -#[derive(Clone, Debug, PartialEq, Hash)] -enum Choice { - /// Every row as it is, until its pyramid lands. - AsIs, - /// Every row a step apart, until its pyramid lands. - Sample(u32), - /// The rows of these tiles. - Tiles(Wanted), - /// The last rows of its one tile: those that matter most. - Last(u32), -} - -impl Choice { - /// The rows it draws of a table of `len` rows, by `placed`, its pyramid, in drawing order: finest - /// tiles first, so what matters more draws on top. `None` for a table drawn as it is. - fn rows(&self, placed: Option<&Placed>, len: u32) -> Option> { - Some(match (self, placed) { - (Choice::AsIs, _) => return None, - (Choice::Tiles(wanted), Some(placed)) => { - let pyramid = &placed.pyramid; - let mut tiles = wanted.tiles.clone(); - crate::order::by_key(&mut tiles, |&(t, _)| { - !crate::order::pair(u32::from(pyramid.tiles[t as usize].key.z), u32::MAX - t) - }); - let mut list = Vec::new(); - for (t, finest) in tiles { - let range = pyramid.tiles[t as usize].rows.clone(); - let rows = &pyramid.order[range.start as usize..range.end as usize]; - // A coarser tile draws only its rows reaching where the finest cover. - match wanted.cover.filter(|_| !finest && !pyramid.anchors.is_empty()) { - Some(cover) => list.extend(rows.iter().copied().filter(|&r| { - let extent = pyramid.extents.get(r as usize).copied().unwrap_or(0); - reaches(&cover, pyramid.anchors[r as usize], extent) - })), - None => list.extend_from_slice(rows), - } + /// Asks for the pyramids of `placing`: at once where they fit the frame, else in one job for the + /// dataset. + fn level(&mut self, placing: &[(&Table, &Planned)], ask: &Ask<'_>, jobs: &mut Jobs<'_>) { + // What was asked before no longer serves once it lands. + self.working = None; + self.read = + placing.iter().map(|(table, planned)| (table.name.clone(), Read::new(table, planned, ask.earth))).collect(); + let (leveling, bounds) = leveling(ask, placing); + // Placing a row takes about a microsecond; ordering one by priority alone, far less. + let cost: f64 = leveling + .tables + .iter() + .map(|t| f64::from(t.rows) * if t.leveled { PLACE_COST } else { PLACE_COST / 20.0 }) + .sum(); + let tables: Vec<(u32, Name, Version, Option<[f64; 4]>)> = placing + .iter() + .enumerate() + .filter(|(i, _)| leveling.tables[*i].answered) + .map(|(i, (table, _))| (i as u32, table.name.clone(), table.order, bounds.get(&table.name).copied())) + .collect(); + let domain = leveling.domain; + if jobs.fits(cost as u64) { + for (index, pyramid) in answer(&leveling) { + if let Some((_, name, order, bounds)) = tables.iter().find(|t| t.0 == index) { + let placed = Placed { order: *order, domain, pyramid: Arc::new(pyramid), bounds: *bounds }; + self.placed.insert(name.clone(), placed); } - list } - (Choice::Last(n), Some(placed)) => { - let order = &placed.pyramid.order; - order[order.len() - *n as usize..].to_vec() - } - (Choice::Sample(step), _) => (0..len).step_by(*step as usize).collect(), - (_, None) => Vec::new(), - }) + return; + } + let pending = jobs.spawn(Job::pyramid(&leveling)); + self.working = Some(Working { pending, tables, domain }); } } -/// A row drawn that no source row fills: a hole a row no longer drawn left. -const HOLE: u32 = u32::MAX; -/// Updates without change after which rows laid out of drawing order are laid out again in order. -const SETTLE: u32 = 8; - -/// Where a table drawn in part draws each row: a row still drawn keeps its place while the camera -/// moves, so only arriving rows are gathered and evaluated. A row no longer drawn leaves a hole, which -/// a row arriving later takes, the first hole first, else a row past the last. The rows are laid out -/// afresh, in drawing order, when holes pass a quarter of them, and once the camera rests while they -/// lie out of that order. -#[derive(Default)] -struct Slots { - /// Each row drawn: its source row, or a [`HOLE`]. - rows: Vec, - /// Where each source row is drawn, `UNWIRED` where it is not. - at: Vec, - /// Holes, by row. - holes: Vec, - /// Whether the rows lie in drawing order with no hole, as laying out afresh lays them. - ordered: bool, -} - -impl Slots { - /// Lays out `wanted`, rows of a table of `len` rows, in drawing order: each still drawn keeps its - /// place, each no longer drawn leaves a hole, and each arriving takes the first hole, else a row - /// past the last. Every row is laid out afresh where `afresh` says so or holes would pass a quarter - /// of the rows. The rows written, `(row, source row or HOLE)` by row; `None` where every row was - /// laid out afresh. - fn lay(&mut self, wanted: &[u32], len: usize, afresh: bool) -> Option> { - self.at.resize(len, UNWIRED); - if afresh || self.rows.is_empty() { - return self.afresh(wanted); - } - let keep = Bitset::of(wanted, len); - let mut writes: Vec<(u32, u32)> = Vec::new(); - let mut freed: Vec = Vec::new(); - for (i, slot) in self.rows.iter_mut().enumerate() { - if *slot != HOLE && !keep.contains(*slot) { - if let Some(at) = self.at.get_mut(*slot as usize) { - *at = UNWIRED; +/// The rows of table `drawn[at]` that the samples of the edge tables `edges` among `drawn`, each +/// every `steps`th row, end at, lifted in its own sample: `was` where nothing it was worked out from +/// changed, else worked out again; and what they were worked out from. +fn lift( + drawn: &[(&Table, &Planned)], + edges: &[usize], + steps: &[u32], + at: usize, + was: Option<&(u64, Lifted)>, +) -> (u64, Lifted) { + let target = drawn[at].0; + let references = |e: usize| -> Vec<&Reference> { + let (table, planned) = drawn[e]; + let Place::Ends(fields) = &planned.place else { return Vec::new() }; + fields + .iter() + .filter_map(|f| match table.field_named(f) { + Some(Field::Reference(r)) if r.to == target.name => Some(r), + _ => None, + }) + .collect() + }; + let from = stamp_of(( + target.len(), + steps[at], + edges + .iter() + .map(|&e| (drawn[e].0.len(), steps[e], references(e).iter().map(|r| r.version).collect::>())) + .collect::>(), + )); + if let Some(was) = was.filter(|(had, _)| *had == from) { + return was.clone(); + } + let mut rows: Vec = Vec::new(); + for &e in edges { + let references = references(e); + let len = drawn[e].0.len() as u32; + for k in 0..len.div_ceil(steps[e]) { + for reference in &references { + let t = reference.target_row((k * steps[e]) as usize); + if t != UNWIRED { + rows.push(t); } - *slot = HOLE; - freed.push(i as u32); - writes.push((i as u32, HOLE)); - } - } - if !freed.is_empty() { - self.holes.extend(freed); - self.holes.sort_unstable(); - } - let mut taken = 0; - for &source in wanted { - if self.at.get(source as usize).is_none_or(|&at| at != UNWIRED) { - continue; } - let row = if let Some(&hole) = self.holes.get(taken) { - taken += 1; - hole - } else { - self.rows.push(HOLE); - self.rows.len() as u32 - 1 - }; - self.rows[row as usize] = source; - self.at[source as usize] = row; - writes.push((row, source)); - } - self.holes.drain(..taken); - if 4 * self.holes.len() > self.rows.len() { - return self.afresh(wanted); } - // A row left and taken again writes what takes it. - crate::order::by_key(&mut writes, |w| u64::from(w.0)); - writes.dedup_by(|later, earlier| { - let same = later.0 == earlier.0; - if same { - earlier.1 = later.1; - } - same - }); - self.ordered = self.holes.is_empty() && self.rows == wanted; - Some(writes) } - - /// Lays every row out afresh, in drawing order, with no hole. - fn afresh(&mut self, wanted: &[u32]) -> Option> { - for &old in self.rows.iter().filter(|&&r| r != HOLE) { - if let Some(at) = self.at.get_mut(old as usize) { - *at = UNWIRED; - } - } - for (row, &source) in wanted.iter().enumerate() { - if let Some(at) = self.at.get_mut(source as usize) { - *at = row as u32; - } - } - self.rows = wanted.to_vec(); - self.holes.clear(); - self.ordered = true; - None - } -} - -/// The row `t` is drawn at, by `map`. -fn remap(map: &[u32], t: u32) -> u32 { - map.get(t as usize).copied().unwrap_or(UNWIRED) + (from, Lifted::new(rows, target.len() as u32, steps[at])) } /// The tables of `source` a pyramid places, each with its plan: every table drawn in part, and every @@ -922,7 +466,7 @@ fn placed<'a>(source: &'a Data, ask: &'a Ask<'_>) -> Vec<(&'a Table, &'a Planned } /// The table the ends of `table` name, as `planned` places it. -fn ends_of<'t>(table: &'t Table, planned: &Planned) -> Option<&'t Name> { +pub(crate) fn ends_of<'t>(table: &'t Table, planned: &Planned) -> Option<&'t Name> { let Place::Ends(fields) = &planned.place else { return None }; match table.field_named(fields.first()?)? { Field::Reference(reference) => Some(&reference.to), @@ -932,24 +476,48 @@ fn ends_of<'t>(table: &'t Table, planned: &Planned) -> Option<&'t Name> { /// The stamp of everything a dataset's pyramids are placed from. fn stamp(source: &Data, ask: &Ask<'_>, placed: &[(&Table, &Planned)]) -> u64 { - let rows: Vec<_> = placed.iter().map(|(t, _)| (t.name.clone(), t.structure, t.order)).collect(); - let mut inputs = Vec::new(); - for (table, planned) in placed { - let mut fields = Vec::new(); - if let Some(priority) = &planned.priority { - fields_of(priority, &mut fields); - } - match &planned.place { - Place::Points(xy) => xy.iter().for_each(|e| fields_of(e, &mut fields)), - Place::Geometry(field) => fields.push(field.clone()), - Place::Ends(ends) => fields.extend(ends.iter().cloned()), - Place::Nowhere => {} - } - let versions: Vec<_> = fields.iter().map(|f| table.field_named(f).map(Field::version)).collect(); - let tiles = matches!(planned.draw, Draw::Tiles { .. }); - inputs.push((format!("{:?}{:?}", planned.priority, planned.place), versions, tiles, planned.as_is())); + let tables: Vec<_> = placed + .iter() + .map(|(table, planned)| { + let versions: Vec<_> = + fields_read(planned).iter().map(|f| table.field_named(f).map(Field::version)).collect(); + (inputs_of(planned, ask.earth), table.structure, table.order, versions) + }) + .collect(); + stamp_of((source.name(), tables)) +} + +/// What a pyramid placing `planned`'s rows is made for besides its rows: how they are placed and +/// ordered, whether they draw through tiles or as they are, and on the earth or not. +fn inputs_of(planned: &Planned, earth: bool) -> u64 { + let tiles = matches!(planned.draw, Draw::Tiles { .. }); + stamp_of((format!("{:?}{:?}", planned.priority, planned.place), tiles, planned.as_is(), earth)) +} + +/// The fields of its own table a pyramid places `planned`'s rows by: their priority, and where they +/// lie. +fn fields_read(planned: &Planned) -> Vec { + let mut fields = Vec::new(); + if let Some(priority) = &planned.priority { + fields_of(priority, &mut fields); + } + match &planned.place { + Place::Points(xy) => xy.iter().for_each(|e| fields_of(e, &mut fields)), + Place::Geometry(field) => fields.push(field.clone()), + Place::Ends(ends) => fields.extend(ends.iter().cloned()), + Place::Nowhere => {} + } + fields +} + +/// The column of field `name` of `table` a pyramid places its rows by: its values, or the rows a +/// reference names. +fn column_of<'t>(table: &'t Table, name: &Name) -> Option<&'t Column> { + match table.field_named(name)? { + Field::Values(column, _) => Some(column), + Field::Reference(reference) => Some(&reference.ids), + Field::Sampled(..) => None, } - stamp_of((source.name(), rows, inputs, ask.earth)) } /// The fields `expr` reads of its mark's own table. @@ -971,53 +539,6 @@ fn fields_of(expr: &Expr, out: &mut Vec) { } } -/// The frame shown of each sampled field marks read, and do not trace, of a table drawn in part. -fn sampled_frames(source: &Data, ask: &Ask<'_>) -> Vec> { - let mut frames = Vec::new(); - for table in source.tables() { - let Some(planned) = ask.plans.get(&(source.name().clone(), table.name.clone())) else { continue }; - if planned.as_is() { - continue; - } - for (name, field) in &table.fields { - if let Field::Sampled(samples, _) = field - && planned.reads.as_ref().is_none_or(|r| r.contains(name)) - && !planned.traced.contains(name) - { - frames.push(samples.frame_at(ask.at)); - } - } - } - frames -} - -/// What decides which of `source`'s rows draw and where, beside the tiles and the rows pinned: each -/// table's rows and their order, and the rows its references name. -fn rows_of(source: &Data) -> Vec<(Version, Version, Vec)> { - source - .tables() - .map(|table| { - let references = table.fields.iter().filter(|(_, f)| matches!(f, Field::Reference(_))); - (table.structure, table.order, references.map(|(_, f)| f.version()).collect()) - }) - .collect() -} - -/// The fields read of each table drawn in part, in a stable order; `None` for every field. -fn reads_of(source: &Data, ask: &Ask<'_>) -> Vec>> { - source - .tables() - .filter_map(|t| ask.plans.get(&(source.name().clone(), t.name.clone()))) - .map(|p| { - p.reads.as_ref().map(|reads| { - let mut reads: Vec = reads.iter().cloned().collect(); - reads.sort(); - reads - }) - }) - .collect() -} - /// The box every placed row of `places` lies in, west, south, east, north; inverted when none is. fn bounds_of(places: &Places) -> [f64; 4] { let mut b = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY]; @@ -1114,30 +635,35 @@ fn leveling(ask: &Ask<'_>, placed: &[(&Table, &Planned)]) -> (Leveling, Named<[f #[cfg(test)] mod tests { use super::*; + use crate::{Ids, Patch}; #[test] - fn rows_still_drawn_keep_their_places_and_arriving_ones_fill_holes() { - let mut slots = Slots::default(); - let first: Vec = (0..20).collect(); - assert_eq!(slots.lay(&first, 100, false), None, "laid out afresh"); - assert!(slots.ordered); - // Rows 4..8 go: their places are holes, every other row stays. - let kept: Vec = (0..4).chain(8..20).collect(); - let writes = slots.lay(&kept, 100, false).expect("in place"); - assert_eq!(writes, (4..8).map(|r| (r, HOLE)).collect::>()); - assert_eq!((slots.rows[3], slots.rows[8], slots.at[9]), (3, 8, 9)); - assert!(!slots.ordered, "holes lie among the rows"); - // Rows 50..55 arrive: four take the holes, the fifth goes past the last row. - let more: Vec = kept.iter().copied().chain(50..55).collect(); - let writes = slots.lay(&more, 100, false).expect("in place"); - assert_eq!(writes, [(4, 50), (5, 51), (6, 52), (7, 53), (20, 54)]); - assert_eq!(slots.lay(&more, 100, false), Some(vec![]), "nothing changed, nothing written"); - // A row leaving and another arriving in one go: the place left is taken at once. - let swapped: Vec = more.iter().copied().filter(|&r| r != 10).chain([60]).collect(); - assert_eq!(slots.lay(&swapped, 100, false), Some(vec![(10, 60)])); - // Holes past a quarter of the rows lay every row out afresh, in order. - assert_eq!(slots.lay(&[60, 61], 100, false), None); - assert_eq!((slots.rows.as_slice(), slots.ordered, slots.at[10]), (&[60, 61][..], true, UNWIRED)); - assert_eq!(slots.lay(&[60, 61], 100, true), None, "and so does asking for it"); + fn a_sample_lifts_the_rows_its_edges_sampled_end_at() { + // A thousand vertices, and a thousand edges between vertices far apart in the table. + let n = 1000_u64; + let mut data = Data::new("g"); + data.apply( + Patch::new().set(Table::new("V", Ids::count(n as u32)).field("x", vec![0.0; n as usize])).set( + Table::new("E", Ids::count(n as u32)) + .reference("a", "V", (0..n).map(|e| (e * 7) % n).collect::>()) + .reference("b", "V", (0..n).map(|e| (e * 13 + 1) % n).collect::>()), + ), + ) + .expect("valid"); + let (vertices, edges) = (data.table("V").expect("set"), data.table("E").expect("set")); + let tiles = |place| Planned { draw: Draw::Tiles { rows: 100 }, priority: None, place }; + let (at, ends) = (tiles(Place::Nowhere), tiles(Place::Ends(vec!["a".into(), "b".into()]))); + let drawn = [(vertices, &at), (edges, &ends)]; + let (from, lifted) = lift(&drawn, &[1], &[10, 10], 0, None); + let order = Order::Sampled { rows: n as u32, step: 10, lifted: lifted.clone() }; + // Every edge the edges' sample draws ends at a row the vertices' sample draws. + for e in (0..n).step_by(10) { + for t in [(e * 7) % n, (e * 13 + 1) % n] { + assert!(order.rank(t as u32) < order.sampled(), "edge {e} ends at vertex {t}, drawn first"); + } + } + assert!(order.sampled() < n as u32, "and the rest wait for the pyramid"); + let again = lift(&drawn, &[1], &[10, 10], 0, Some(&(from, lifted))); + assert_eq!(again.0, from, "worked out once while its edges hold"); } } diff --git a/crates/latkit/src/derive/tiles/vector.rs b/crates/latkit/src/derive/tiles/vector.rs index 9c72d024..1b7869ce 100644 --- a/crates/latkit/src/derive/tiles/vector.rs +++ b/crates/latkit/src/derive/tiles/vector.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use rustc_hash::{FxHashMap, FxHashSet}; -use super::super::sight::{Sight, TILE_PX, meets}; +use super::super::sight::{Domain, Sight, TILE_PX, meets}; use super::super::tiling::{Decoding, Requested, Source, Tiling}; use super::super::{Input, Jobs, stamp_of}; use crate::data::{Data, Field, Id, Name, Op, Patch, Table, Values}; @@ -37,7 +37,7 @@ fn wanted(sight: &Sight<'_>, zooms: [u8; 2], bounds: [f64; 4]) -> Vec { if b[0] > bounds[2] || b[2] < bounds[0] || b[1] > bounds[3] || b[3] < bounds[1] || !meets(®ion, b) { continue; } - let (size, distance) = sight.measure(&focus, b); + let (size, distance) = sight.measure(&focus, &Domain::Earth, key); let room = out.len() + next.len() + (queue.len() - k) * 4 < MAX_TILES; if key.z < zooms[1] && (key.z < zooms[0] || size > limit) && room { next.extend(key.children()); diff --git a/crates/latkit/src/expr/compile.rs b/crates/latkit/src/expr/compile.rs index 73ded0f3..81e90235 100644 --- a/crates/latkit/src/expr/compile.rs +++ b/crates/latkit/src/expr/compile.rs @@ -113,8 +113,10 @@ pub(crate) struct Program { /// Op ranges of every channel, then of every reduction. pub(crate) ranges: Vec<[u32; 2]>, pub(crate) channels: Vec, - /// What each fitted domain's input reads. + /// What each fitted domain's input reads, and the input: a table drawn in part fits a domain + /// reading only its data over all its rows on the CPU. pub(crate) domains: Vec, + pub(crate) fitted: Vec, pub(crate) columns: Vec, pub(crate) texts: Vec, /// Colormap ops, whose layer the view writes. @@ -637,6 +639,7 @@ impl<'a> Compiler<'a> { } let index = self.program.domains.len() as u32; self.program.domains.push(Reads::default()); + self.program.fitted.push(input.clone()); self.program.constants.extend([1, 0, 0, index]); // Compile the input again as a reduction; `finish` places it after the channels. let saved = (std::mem::take(&mut self.program.code), self.depth, self.deps, self.columns_read); diff --git a/crates/latkit/src/gpu/arena.rs b/crates/latkit/src/gpu/arena.rs index 09ba3206..8f4ff181 100644 --- a/crates/latkit/src/gpu/arena.rs +++ b/crates/latkit/src/gpu/arena.rs @@ -6,8 +6,8 @@ //! A page is identified by its id, the words of it a program reads, and its piece. Page tables pin //! the pages they reference, so a page in use is never evicted; unpinned pages stay cached, the least //! recently used going first when a page needs their room. Uploads stream within a per-frame byte -//! budget. A page that finds no room waits until pins let room go: views plan what they draw to fit -//! the pool, so none waits long. +//! budget. A page that finds no room waits aside until pins let room go, while the pages after it go +//! on: views plan what they draw to fit the pool, so none waits long. use std::collections::hash_map::Entry; use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque}; @@ -231,11 +231,11 @@ pub(crate) struct Arena { unpinned: BTreeMap, tick: u64, uploads: VecDeque, + /// Pages that found no room, waiting for a pin to let some go. + stuck: Vec, contents: HashMap, /// Bytes of allocated blocks. resident: u64, - /// Whether the page at the front of the queue found no room, the last time it tried. - starved: bool, } impl Arena { @@ -254,9 +254,9 @@ impl Arena { unpinned: BTreeMap::new(), tick: 0, uploads: VecDeque::new(), + stuck: Vec::new(), contents: HashMap::default(), resident: 0, - starved: false, } } @@ -286,6 +286,11 @@ impl Arena { self.blocks.get(&key).filter(|b| b.offset != NONE && b.written >= need).map(|b| b.offset) } + /// Whether `key` waits to upload. + pub(crate) fn queued(&self, key: PageKey) -> bool { + self.contents.contains_key(&key) + } + /// Keeps `key` resident while pinned; pins nest. pub(crate) fn pin(&mut self, key: PageKey) { let block = self.blocks.entry(key).or_insert(Block { offset: NONE, class: 0, written: 0, pins: 0, tick: 0 }); @@ -309,13 +314,15 @@ impl Arena { self.tick += 1; block.tick = self.tick; self.unpinned.insert(self.tick, key); - // Room let go may be what a waiting page needs. - self.starved = false; + // Room let go may be what a page without room needs: it tries again first. + for key in self.stuck.drain(..).rev() { + self.uploads.push_front(key); + } } /// Whether uploads are waiting that can make progress. pub(crate) fn uploading(&self) -> bool { - !self.uploads.is_empty() && !self.starved + !self.uploads.is_empty() } /// Bytes waiting to upload. @@ -328,19 +335,17 @@ impl Arena { self.resident } - /// Bytes the pool may grow to. - pub(crate) fn capacity(&self) -> u64 { - u64::from(self.limit) * CHUNK_BYTES - } - /// Uploads queued pages, at least one and at most `budget` bytes; returns the bytes uploaded. /// - /// A page that finds no room stays queued at the front, and later pages wait behind it, so the - /// pages of one table upload in order; it tries again once a pin lets room go. + /// A page that finds no room waits aside, and the pages after it go on; it tries again once a + /// pin lets room go. pub(crate) fn pump(&mut self, device: &wgpu::Device, queue: &Queue, budget: u64) -> u64 { let mut bytes = 0; - while !self.starved - && (bytes == 0 || bytes < budget) + // Views drawn earlier in the frame spent it. + if budget == 0 { + return 0; + } + while (bytes == 0 || bytes < budget) && let Some(key) = self.uploads.pop_front() { let Some(content) = self.contents.remove(&key) else { continue }; @@ -351,9 +356,8 @@ impl Arena { _ => { let Some(offset) = self.place(device, queue, key, content.class(len)) else { self.contents.insert(key, content); - self.uploads.push_front(key); - self.starved = true; - break; + self.stuck.push(key); + continue; }; (offset, 0) } diff --git a/crates/latkit/src/gpu/dictionary.rs b/crates/latkit/src/gpu/dictionary.rs index b452fac1..e769997b 100644 --- a/crates/latkit/src/gpu/dictionary.rs +++ b/crates/latkit/src/gpu/dictionary.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use rustc_hash::FxBuildHasher; use crate::Name; -use crate::data::{Keyed, Page, PageId, Values}; +use crate::data::{Column, Keyed, Page, PageId, Values}; use crate::gpu::shaders::MISSING; /// Pages a dictionary keeps the codes of: past them, it forgets them all. @@ -72,6 +72,12 @@ impl Dictionary { codes } + /// The codes of `column`'s rows `rows`, in that order, their texts added: what a page packed in a + /// table's order holds. Each row costs a lookup, however its pages were coded. + pub(crate) fn rows(&mut self, column: &Column, rows: &[u32]) -> Vec { + rows.iter().map(|&r| column.at(r as usize).map_or(MISSING, |(values, i)| self.code(values, i))).collect() + } + /// The code of value `i` of `values` as an `f32` word, its text added when it is new. fn code(&mut self, values: &Values, i: usize) -> u32 { let Some(text) = values.text().filter(|_| values.is_valid(i)) else { return MISSING }; diff --git a/crates/latkit/src/gpu/mod.rs b/crates/latkit/src/gpu/mod.rs index 5e40934c..f699b189 100644 --- a/crates/latkit/src/gpu/mod.rs +++ b/crates/latkit/src/gpu/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod atlas; pub(crate) mod colormaps; pub(crate) mod dictionary; pub(crate) mod icons; +pub(crate) mod packed; pub(crate) mod pipelines; pub(crate) mod readback; pub(crate) mod shaders; @@ -19,6 +20,7 @@ use atlas::Atlas; use colormaps::Colormaps; use dictionary::Dictionaries; use icons::Icons; +use packed::Packed; use pipelines::Pipelines; use readback::Readbacks; use work::{Counters, Queue, Work}; @@ -70,14 +72,26 @@ pub(crate) struct Shared { pub(crate) lost: Arc, /// Errors the device reported outside any scope, for views to tell. pub(crate) errors: Arc>>, + /// Samples per pixel every frame drawn with this GPU takes: decided once, from the screen it + /// serves, so a pixel ratio changing later (a browser's zoom, a window moved to another screen) + /// never makes a scene's pipelines again. + samples: std::sync::OnceLock, inner: Mutex, } +/// Samples per pixel at `pixel_ratio`: shader-drawn marks antialias themselves, so dense screens skip +/// multisampling, which only triangles' edges need. +fn samples_at(pixel_ratio: f32) -> u32 { + if pixel_ratio >= 1.75 { 1 } else { 4 } +} + pub(crate) struct Inner { pub(crate) arena: Arena, pub(crate) pipelines: Pipelines, pub(crate) colormaps: Colormaps, pub(crate) dictionaries: Dictionaries, + /// Pages of tables drawn in part, packed in their orders, which every view drawing them shares. + pub(crate) packed: Packed, pub(crate) atlas: Atlas, pub(crate) icons: Icons, pub(crate) readbacks: Readbacks, @@ -85,6 +99,30 @@ pub(crate) struct Inner { pub(crate) budget: Budget, /// What each view plans within: its share of the budget. pub(crate) ledger: Ledger, + /// What the frame its views draw together may still do. + pub(crate) frame: Allowance, +} + +/// What one frame of a GPU's views may still do between them: bytes to upload, and microseconds of +/// work done on the frame rather than in jobs. A frame ends when a view that drew in it draws again, +/// so views drawn together (a page's canvases, a shot's panels) share one, and a view drawn alone, +/// even at a held instant, has one each time. +#[derive(Default)] +pub(crate) struct Allowance { + drawn: Vec, + pub(crate) upload: u64, + pub(crate) work: u32, +} + +impl Inner { + /// Begins view `view`'s part of a frame: a new frame when it already drew in this one. + pub(crate) fn begin_frame(&mut self, view: u64) { + if self.frame.drawn.is_empty() || self.frame.drawn.contains(&view) { + self.frame = + Allowance { drawn: Vec::new(), upload: self.budget.upload_per_frame, work: crate::derive::FRAME_WORK }; + } + self.frame.drawn.push(view); + } } /// The share of the budget the views of a GPU plan what they draw within, between them; the rest holds @@ -208,8 +246,20 @@ impl Gpu { #[cfg(all(feature = "web", target_arch = "wasm32"))] pub(crate) fn warm(&self) { let pixel_ratio = web_sys::window().map_or(1.0, |w| w.device_pixel_ratio() as f32); - let samples = crate::view::samples(pixel_ratio); - self.0.lock().pipelines.warm(&self.0.device, || crate::view::common(FORMAT, samples)); + let samples = self.samples_for(pixel_ratio); + let waker = crate::web::waker(self); + self.0.lock().pipelines.warm(&self.0.device, &waker, || crate::view::common(FORMAT, samples)); + } + + /// Samples per pixel its frames take, once decided. + pub(crate) fn samples(&self) -> Option { + self.0.samples.get().copied() + } + + /// Samples per pixel its frames take: decided by the first frame's `pixel_ratio` where no screen + /// decided it when the GPU was made, and kept from then on. + pub(crate) fn samples_for(&self, pixel_ratio: f32) -> u32 { + *self.0.samples.get_or_init(|| samples_at(pixel_ratio)) } /// Draws with an application-owned device and queue without replacing their callbacks. @@ -261,15 +311,33 @@ impl Gpu { pipelines: Pipelines::new(&device, counters.clone(), gpu.clone()), colormaps: Colormaps::new(&device), dictionaries: Dictionaries::default(), + packed: Packed::new(counters.clone()), atlas: Atlas::new(&device, counters.clone()), icons: Icons::new(&device), readbacks: Readbacks::counted(counters.clone()), host: None, budget, ledger: Ledger::default(), + frame: Allowance::default(), }; - Shared { instance, adapter, device, queue, counters, sampler, lost, errors, inner: Mutex::new(inner) } + Shared { + instance, + adapter, + device, + queue, + counters, + sampler, + lost, + errors, + samples: std::sync::OnceLock::new(), + inner: Mutex::new(inner), + } }); + // In a page, the screen decides at once: every view and canvas of the GPU draws for it. + #[cfg(all(feature = "web", target_arch = "wasm32"))] + if let Some(window) = web_sys::window() { + let _ = shared.samples.set(samples_at(window.device_pixel_ratio() as f32)); + } Gpu(shared) } @@ -312,6 +380,16 @@ impl Gpu { self.0.lock().budget } + /// Bytes on the GPU every view of it draws from: data pages and what each view keeps per row. + pub fn resident(&self) -> u64 { + self.0.lock().arena.resident() + } + + /// Bytes waiting to upload, for every view of it. + pub fn uploading(&self) -> u64 { + self.0.lock().arena.pending() + } + /// Gives views the platform's services: setting text, and doing jobs off the frame. pub fn set_host(&self, host: impl Host) { self.0.lock().host = Some(Arc::new(host)); diff --git a/crates/latkit/src/gpu/packed.rs b/crates/latkit/src/gpu/packed.rs new file mode 100644 index 00000000..e48150c9 --- /dev/null +++ b/crates/latkit/src/gpu/packed.rs @@ -0,0 +1,256 @@ +//! Pages packed in an order: a column of a table drawn in part, a page of its order at a time, the +//! first time a view draws that page, as the words its marks read. Each is kept by what it was +//! packed from, so views drawing a table alike share its pages, a page the camera comes back to is +//! the one it left, and a write repacks only the pages its rows draw in. A table drawn in its own +//! order reads its data's pages as they are, but a reference to a table drawn in another order, whose +//! pages are packed through that order's ranks. + +use std::sync::Arc; + +use rustc_hash::FxHashMap; + +use crate::data::{Column, Name, PAGE_ROWS, PageId, Reference, Samples, Table, UNWIRED, Version}; +use crate::derive::order::{Order, PAGE}; +use crate::expr::compile::Words; +use crate::gpu::arena::page_words; +use crate::gpu::shaders::{MISSING, NONE}; +use crate::gpu::work::{Count, Counters}; + +/// Columns of pages kept at most: past them, the half used least recently is forgotten. +const KEPT: usize = 4096; + +/// What one column of pages packed in an order is: the order, the dataset and table, the field and +/// how it is read, and what else its words read (a reference's target order, the frame of samples +/// shown, the rows a join reads). +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub(crate) struct Packing { + pub(crate) order: u64, + pub(crate) origin: Version, + pub(crate) table: Name, + pub(crate) field: Name, + pub(crate) words: Words, + pub(crate) low: bool, + pub(crate) reads: u64, +} + +/// A column's pages: what they were packed from, each page's id once packed, the samples a traced +/// page was gathered as, and when they were last used. +struct Pages { + source: Option, + ids: Vec>, + samples: Vec>, + used: u64, +} + +/// Every column of pages packed in an order, on the GPU's views' behalf, and what counts the rows +/// packed. +pub(crate) struct Packed { + counters: Arc, + columns: FxHashMap, + /// Pages of a table's own order whose reference rows are packed through the ranks of the order + /// their table draws in, by the page and that order. + ranked: FxHashMap<(PageId, u64), (PageId, u64)>, + uses: u64, +} + +impl Packed { + pub(crate) fn new(counters: Arc) -> Packed { + Packed { counters, columns: FxHashMap::default(), ranked: FxHashMap::default(), uses: 0 } + } + + /// Counts `rows` rows packed. + pub(crate) fn count(&self, rows: usize) { + self.counters.add(Count::Packed, rows as u64); + } + + /// What counts the rows packed, for packing that may happen later. + pub(crate) fn counters(&self) -> Arc { + self.counters.clone() + } + + /// The pages of `packing`, packed from `source` as it is now, in `order`: kept while no row they + /// hold was written since. Without a source, kept while `packing` is the same. + fn pages(&mut self, packing: &Packing, order: &Order, source: Option<&Column>) -> &mut Pages { + self.uses += 1; + if !self.columns.contains_key(packing) && self.columns.len() >= KEPT { + forget(&mut self.columns, |p| p.used); + } + let pages = self.columns.entry(packing.clone()).or_insert_with(|| Pages { + source: source.cloned(), + ids: Vec::new(), + samples: Vec::new(), + used: 0, + }); + pages.used = self.uses; + if let Some(source) = source + && !pages.source.as_ref().is_some_and(|s| s.shares(source)) + { + if let Some(before) = &pages.source { + for row in source.written_since(before) { + let page = (order.rank(row as u32) / PAGE) as usize; + if let Some(id) = pages.ids.get_mut(page) { + *id = None; + } + if let Some(samples) = pages.samples.get_mut(page) { + *samples = None; + } + } + } else { + pages.ids.clear(); + pages.samples.clear(); + } + pages.source = Some(source.clone()); + } + pages + } + + /// The id of page `page` of `packing`, packed from `source` in `order`. + pub(crate) fn page(&mut self, packing: &Packing, order: &Order, source: Option<&Column>, page: u32) -> PageId { + let pages = self.pages(packing, order, source); + let k = page as usize; + if pages.ids.len() <= k { + pages.ids.resize(k + 1, None); + } + *pages.ids[k].get_or_insert_with(PageId::next) + } + + /// Samples `samples`' rows `rows` hold, page `page` of `packing`, gathered once. + pub(crate) fn samples( + &mut self, + packing: &Packing, + order: &Order, + page: u32, + rows: &[u32], + samples: &Samples, + ) -> Samples { + let counters = self.counters.clone(); + let pages = self.pages(packing, order, None); + let k = page as usize; + if pages.samples.len() <= k { + pages.samples.resize(k + 1, None); + } + pages.samples[k] + .get_or_insert_with(|| { + let rows: Vec = rows.iter().map(|&r| r as usize).collect(); + counters.add(Count::Packed, rows.len() as u64); + samples.gather(&rows) + }) + .clone() + } + + /// The id of reference page `page` of a table drawn in its own order, packed through the ranks of + /// the order `order` its target draws in. + pub(crate) fn ranked(&mut self, page: PageId, order: u64) -> PageId { + self.uses += 1; + if self.ranked.len() >= KEPT && !self.ranked.contains_key(&(page, order)) { + forget(&mut self.ranked, |&(_, used)| used); + } + let uses = self.uses; + let entry = self.ranked.entry((page, order)).or_insert_with(|| (PageId::next(), 0)); + entry.1 = uses; + entry.0 + } +} + +/// Drops the half of `kept` used least recently, by when `used` says each was. +fn forget(kept: &mut FxHashMap, used: impl Fn(&V) -> u64) { + let mut uses: Vec = kept.values().map(&used).collect(); + let half = uses.len() / 2; + let (_, &mut cut, _) = uses.select_nth_unstable(half); + kept.retain(|_, v| used(v) > cut); +} + +/// The words `column`'s rows `rows` are read as, a page of them, missing past the rows. +pub(crate) fn values(column: &Column, rows: &[u32], words: Words, low: bool) -> Vec { + let mut out = page_words(&column.pick(rows), words, low); + out.resize(PAGE as usize, if words == Words::Bits { NONE } else { MISSING }); + out +} + +/// Words of the rows `reference` names for rows `rows`, a page of them: where each draws in `rank`, +/// the order its table draws in, or as it is in a table drawn whole; `NONE` where a row names none. +pub(crate) fn targets(reference: &Reference, rows: &[u32], rank: Option<&Order>) -> Vec { + let mut out: Vec = rows + .iter() + .map(|&r| match reference.target_row(r as usize) { + UNWIRED => NONE, + t => rank.map_or(t, |order| order.rank(t)), + }) + .collect(); + out.resize(PAGE as usize, NONE); + out +} + +/// The words of reference page `page` of a table drawn in its own order: the rows it names, as +/// `rank`, the order their table draws in, places them. +pub(crate) fn ranked_page(reference: &Reference, page: usize, rank: &Order) -> Vec { + let start = page * PAGE_ROWS; + let len = reference.rows.pages().get(page).map_or(0, |p| p.values.len()); + (start..start + len) + .map(|r| match reference.target_row(r) { + UNWIRED => NONE, + t => rank.rank(t), + }) + .collect() +} + +/// Values of `samples`' rows `rows` at frame `frame`, a page of them, missing past the rows. +pub(crate) fn frame(samples: &Samples, rows: &[u32], frame: Option) -> Vec { + let mut out: Vec = rows + .iter() + .map(|&r| frame.and_then(|f| samples.value(r as usize, f)).map_or(MISSING, |v| (v as f32).to_bits())) + .collect(); + out.resize(PAGE as usize, MISSING); + out +} + +/// The rows of `other` with the ids of `table`'s rows `rows`, a page of them; `NONE` where none. +pub(crate) fn joined(table: &Table, rows: &[u32], other: &Table) -> Vec { + let mut out: Vec = rows + .iter() + .map(|&r| table.ids.key(r as usize).and_then(|key| other.row(key)).map_or(NONE, |o| o as u32)) + .collect(); + out.resize(PAGE as usize, NONE); + out +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + use crate::data::Values; + use crate::derive::pyramid::Pyramid; + + fn packing() -> Packing { + Packing { + order: 1, + origin: Version::next(), + table: "T".into(), + field: "v".into(), + words: Words::Values, + low: false, + reads: 0, + } + } + + #[test] + fn a_write_repacks_only_the_pages_its_rows_draw_in() { + let n = 3 * PAGE as usize; + let column = Column::new(&Values::from((0..n).map(|i| i as f64).collect::>())); + // The order draws the rows backward: the last page of rows is its first page. + let order = Order::Pyramid { + pyramid: Arc::new(Pyramid::new((0..n as u32).rev().collect(), Vec::new())), + rows: n as u32, + }; + let mut packed = Packed::new(Arc::default()); + let packing = packing(); + let ids: Vec = (0..3).map(|p| packed.page(&packing, &order, Some(&column), p)).collect(); + let written = column.write(n, &[(5, 0)], &Column::new(&Values::from(vec![-1.0_f64]))).expect("numbers"); + let after: Vec = (0..3).map(|p| packed.page(&packing, &order, Some(&written), p)).collect(); + assert_eq!((after[0], after[1]), (ids[0], ids[1]), "pages not drawing row 5 are kept"); + assert_ne!(after[2], ids[2], "row 5 draws in the last page, which packs again"); + let words = values(&written, &order.page(2), Words::Values, false); + assert_eq!(f32::from_bits(words[PAGE as usize - 1 - 5]), -1.0, "row 5 packed where it draws"); + } +} diff --git a/crates/latkit/src/gpu/pipelines.rs b/crates/latkit/src/gpu/pipelines.rs index 9b52238b..eaf96a7a 100644 --- a/crates/latkit/src/gpu/pipelines.rs +++ b/crates/latkit/src/gpu/pipelines.rs @@ -369,11 +369,12 @@ impl Pipelines { } /// Queues what `needs` lists, what views to come draw with, once per GPU: it compiles once the - /// first scene's frames have all they draw with, so it never holds them back. + /// first scene's frames have all they draw with, so it never holds them back. `waker` wakes as + /// each lands, so what waits for the GPU's work to land sees it. #[cfg(all(feature = "web", target_arch = "wasm32"))] - pub(crate) fn warm(&mut self, device: &wgpu::Device, needs: impl FnOnce() -> Vec) { + pub(crate) fn warm(&mut self, device: &wgpu::Device, waker: &Waker, needs: impl FnOnce() -> Vec) { if !std::mem::replace(&mut self.warmed, true) { - self.queue(device, &needs(), Urgency::Warm, Waker::noop()); + self.queue(device, &needs(), Urgency::Warm, waker); } } diff --git a/crates/latkit/src/gpu/shaders.rs b/crates/latkit/src/gpu/shaders.rs index 58495b6a..8f10a77b 100644 --- a/crates/latkit/src/gpu/shaders.rs +++ b/crates/latkit/src/gpu/shaders.rs @@ -114,5 +114,11 @@ impl Binding { include!(concat!(env!("OUT_DIR"), "/shaders.rs")); -// A block of rows the GPU evaluates is a page of every column. -const _: () = assert!(BLOCK_ROWS as usize == crate::data::PAGE_ROWS && 1 << PAGE_BITS == BLOCK_ROWS); +// A block of rows the GPU evaluates is a held page; a page of the data holds a whole number of them. +const _: () = assert!( + 1 << HELD_BITS == BLOCK_ROWS + && HELD_MASK + 1 == BLOCK_ROWS + && 1 << PAGE_BITS as usize == crate::data::PAGE_ROWS + && crate::data::PAGE_ROWS.is_multiple_of(BLOCK_ROWS as usize) + && DISPATCH_ROWS as usize == crate::data::PAGE_ROWS +); diff --git a/crates/latkit/src/gpu/work.rs b/crates/latkit/src/gpu/work.rs index e14cee58..73df64b0 100644 --- a/crates/latkit/src/gpu/work.rs +++ b/crates/latkit/src/gpu/work.rs @@ -59,6 +59,9 @@ work! { written: Written, /// Rows whose channels the GPU evaluated. evaluated: Evaluated, + /// Rows packed into pages of the order a table drawn in part draws in: each page once, the first + /// time a view draws it, and again only where a write changed its rows. + packed: Packed, /// Reads back from the GPU: probes, queries, fits, label placements, segment counts, pictures; /// not the frame timer's, which only some devices have. readbacks: Readbacks, diff --git a/crates/latkit/src/host/browser.rs b/crates/latkit/src/host/browser.rs index d0c04fcd..008abc01 100644 --- a/crates/latkit/src/host/browser.rs +++ b/crates/latkit/src/host/browser.rs @@ -3,6 +3,7 @@ use std::cell::RefCell; use wasm_bindgen::{JsCast, JsValue}; use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d}; +use super::queue::Queue; use super::{Encoder, Unit}; use super::{Host, Job, Raster, Request, TextRun}; use crate::Encoding; @@ -17,9 +18,48 @@ pub struct Browser; thread_local! { static CANVAS: RefCell> = const { RefCell::new(None) }; static JOBS: RefCell> = const { RefCell::new(None) }; + static QUEUE: RefCell = RefCell::new(Queue::default()); static ENCODERS: RefCell> = const { RefCell::new(None) }; } +/// Jobs the page's workers do at once: as many as its pool keeps, one fewer than its cores, at most +/// four. +fn workers() -> usize { + let cores = web_sys::window() + .map(|window| window.navigator().hardware_concurrency()) + .or_else(|| { + js_sys::global() + .dyn_into::() + .ok() + .map(|scope| scope.navigator().hardware_concurrency()) + }) + .unwrap_or(2.0) as usize; + cores.saturating_sub(1).clamp(1, 4) +} + +/// Hands waiting jobs to the page's workers while they have room, skipping those nothing waits for. +fn pump(run: &js_sys::Function) { + loop { + let next = QUEUE.with(|queue| queue.borrow_mut().next(workers())); + let Some((job, resolver)) = next else { return }; + let bytes = js_sys::Uint8Array::from(&job.into_bytes()[..]); + let promise = run.call1(&JsValue::NULL, &bytes).map(js_sys::Promise::from); + let run = run.clone(); + wasm_bindgen_futures::spawn_local(async move { + let answer = match promise { + Ok(promise) => wasm_bindgen_futures::JsFuture::from(promise) + .await + .map(|bytes| js_sys::Uint8Array::new(&bytes).to_vec()) + .map_err(|error| Error::Unsupported(format!("a job failed: {error:?}").into())), + Err(error) => Err(Error::Unsupported(format!("jobs: {error:?}").into())), + }; + resolver.resolve(answer); + QUEUE.with(|queue| queue.borrow_mut().done()); + pump(&run); + }); + } +} + impl Browser { /// Has views' jobs done by `run`, which takes a job's bytes as a `Uint8Array` and returns a /// promise of its answer's: a pool of workers, each holding the page's compiled module. @@ -175,20 +215,9 @@ impl Host for Browser { fn spawn(&self, job: Job) -> Pending> { let Some(run) = JOBS.with(|jobs| jobs.borrow().clone()) else { return Pending::ready(Ok(job.run())) }; - let bytes = js_sys::Uint8Array::from(&job.into_bytes()[..]); - let promise = match run.call1(&JsValue::NULL, &bytes).map(js_sys::Promise::from) { - Ok(promise) => promise, - Err(error) => return Pending::ready(Err(Error::Unsupported(format!("jobs: {error:?}").into()))), - }; let (pending, resolver) = Pending::new(); - wasm_bindgen_futures::spawn_local(async move { - let answer = wasm_bindgen_futures::JsFuture::from(promise).await; - resolver.resolve( - answer - .map(|bytes| js_sys::Uint8Array::new(&bytes).to_vec()) - .map_err(|error| Error::Unsupported(format!("a job failed: {error:?}").into())), - ); - }); + QUEUE.with(|queue| queue.borrow_mut().push(job, resolver)); + pump(&run); pending } diff --git a/crates/latkit/src/host/job.rs b/crates/latkit/src/host/job.rs index e34c7fb4..21e3c512 100644 --- a/crates/latkit/src/host/job.rs +++ b/crates/latkit/src/host/job.rs @@ -2,8 +2,9 @@ //! into triangles, grouping a large table's rows into tiles, writing a picture as a file), handed to the //! host to do off the frame. A job is bytes in and bytes out, so a thread, or a worker holding its own //! copy of latkit, can do it with nothing shared. A view's job answers with a [`Patch`]: the rows it -//! worked out, by id, with their places, the paths their wires run along, or their shapes; an image -//! job answers with the file. +//! worked out, by id, with their places, the paths their wires run along, or their shapes; or in its +//! own words, lanes as numbers and pyramids as their orders and tiles. An image job answers with the +//! file. use std::sync::Arc; @@ -142,21 +143,27 @@ impl Job { self.0 } - /// Does the job, answering with a patch's bytes. Bytes that are no job answer nothing. + /// Does the job, answering in the words its derivation reads: rows by id as a patch's bytes, + /// lanes as numbers, a pyramid as its order and tiles, an image as its file. Bytes that are no + /// job answer nothing. pub fn run(self) -> Vec { let mut input = Reader::new(&self.0); let kind = input.u32(); if kind == Some(Kind::Image as u32) { return run_image(&mut input).unwrap_or_default(); } + if kind == Some(Kind::Lanes as u32) { + return run_lanes(&mut input).unwrap_or_default(); + } + if kind == Some(Kind::Pyramid as u32) { + return run_pyramid(&mut input).unwrap_or_default(); + } let answer = match kind { Some(k) if k == Kind::Layered as u32 || k == Kind::Stress as u32 => run_layout(k, &mut input), Some(k) if k == Kind::Routes as u32 => run_routes(&mut input), Some(k) if k == Kind::Shapes as u32 => run_shapes(&mut input), Some(k) if k == Kind::Tile as u32 => run_tile(&mut input), Some(k) if k == Kind::GeoJson as u32 => run_geojson(&mut input), - Some(k) if k == Kind::Lanes as u32 => run_lanes(&mut input), - Some(k) if k == Kind::Pyramid as u32 => run_pyramid(&mut input), _ => None, }; answer.map(|patch| patch.to_bytes()).unwrap_or_default() @@ -260,7 +267,7 @@ impl Job { } /// Finding every edge row's lane among the rows joining the same two vertices, from each row's two - /// ends; it answers a table of every row's lane as field `lane`, without rows when none share. + /// ends; it answers every row's lane as a little-endian `f32`, nothing when none share. pub(crate) fn lanes(from: &[u32], to: &[u32]) -> Job { let mut out = Writer::new(Kind::Lanes); out.u32(from.len() as u32); @@ -269,8 +276,8 @@ impl Job { Job(out.0) } - /// Grouping `leveling`'s tables into tiles; it answers each table's pyramid, as - /// [`Pyramid::read`](crate::derive::pyramid::Pyramid::read) reads it. + /// Grouping `leveling`'s tables into tiles, a dataset's at once; it answers each table's pyramid, + /// as [`Pyramid::read`](crate::derive::pyramid::Pyramid::read) reads it. pub(crate) fn pyramid(leveling: &Leveling) -> Job { let mut out = Writer::new(Kind::Pyramid); match leveling.domain { @@ -492,7 +499,7 @@ fn run_geojson(input: &mut Reader<'_>) -> Option { crate::geo::geojson(name, input.rest()).ok().map(|data| data.to_patch()) } -fn run_pyramid(input: &mut Reader<'_>) -> Option { +fn run_pyramid(input: &mut Reader<'_>) -> Option> { let domain = match [input.f64()?, input.f64()?, input.f64()?, input.f64()?] { [0.0, ..] => Domain::Earth, [_, x, y, size] => Domain::Plane { min: [x, y], size }, @@ -519,13 +526,14 @@ fn run_pyramid(input: &mut Reader<'_>) -> Option { }; tables.push(Placing { rows, leveled: has(0), answered: has(1), places, priority, ends }); } - Some(crate::derive::pyramid::answer(&Leveling { domain, tables })) + Some(crate::derive::pyramid::encode(&crate::derive::pyramid::answer(&Leveling { domain, tables }))) } -fn run_lanes(input: &mut Reader<'_>) -> Option { +fn run_lanes(input: &mut Reader<'_>) -> Option> { let rows = input.u32()? as usize; let (from, to) = (input.words(rows)?, input.words(rows)?); - Some(crate::derive::lanes_answer(&from, &to)) + let lanes = crate::derive::lanes(&from, &to)?; + Some(lanes.iter().flat_map(|lane| lane.to_le_bytes()).collect()) } /// Little-endian words, floats, and keys, written. diff --git a/crates/latkit/src/host/mod.rs b/crates/latkit/src/host/mod.rs index 80422815..5e2d4ba7 100644 --- a/crates/latkit/src/host/mod.rs +++ b/crates/latkit/src/host/mod.rs @@ -11,6 +11,8 @@ mod browser; mod job; #[cfg(all(feature = "native", not(target_arch = "wasm32")))] mod native; +#[cfg(any(test, all(feature = "web", target_arch = "wasm32")))] +mod queue; use std::sync::Arc; diff --git a/crates/latkit/src/host/queue.rs b/crates/latkit/src/host/queue.rs new file mode 100644 index 00000000..13e9742f --- /dev/null +++ b/crates/latkit/src/host/queue.rs @@ -0,0 +1,67 @@ +//! Jobs waiting for a worker, in the order asked. A job goes to a worker only as one frees, so one +//! whose answer nothing waits for any more, as a pyramid superseded by newer data, is dropped before +//! it costs a worker anything. + +use std::collections::VecDeque; + +use super::Job; +use crate::Resolver; + +#[derive(Default)] +pub(crate) struct Queue { + waiting: VecDeque<(Job, Resolver>)>, + running: usize, +} + +impl Queue { + /// Queues `job`, whose answer `resolver` gives. + pub(crate) fn push(&mut self, job: Job, resolver: Resolver>) { + self.waiting.push_back((job, resolver)); + } + + /// The next job for a worker while fewer than `workers` run, dropping those nothing waits for. + pub(crate) fn next(&mut self, workers: usize) -> Option<(Job, Resolver>)> { + while let Some((job, resolver)) = self.waiting.pop_front() { + if resolver.is_cancelled() { + continue; + } + if self.running >= workers { + self.waiting.push_front((job, resolver)); + return None; + } + self.running += 1; + return Some((job, resolver)); + } + None + } + + /// A worker finished its job. + pub(crate) fn done(&mut self) { + self.running = self.running.saturating_sub(1); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Pending; + + #[test] + fn a_job_dropped_before_a_worker_frees_never_reaches_one() { + let mut queue = Queue::default(); + let (kept, first) = Pending::>::new(); + let (dropped, second) = Pending::>::new(); + let (later, third) = Pending::>::new(); + queue.push(Job::from_bytes(vec![1]), first); + queue.push(Job::from_bytes(vec![2]), second); + queue.push(Job::from_bytes(vec![3]), third); + let (job, _) = queue.next(1).expect("a worker is free"); + assert_eq!(job.into_bytes(), [1]); + assert!(queue.next(1).is_none(), "the one worker is busy"); + drop(dropped); + queue.done(); + let (job, _) = queue.next(1).expect("the worker freed"); + assert_eq!(job.into_bytes(), [3], "the job nothing waits for never reached it"); + drop((kept, later)); + } +} diff --git a/crates/latkit/src/sheet/mod.rs b/crates/latkit/src/sheet/mod.rs index 6468075d..4c6ad348 100644 --- a/crates/latkit/src/sheet/mod.rs +++ b/crates/latkit/src/sheet/mod.rs @@ -927,13 +927,15 @@ mod tests { data: std::slice::from_ref(&data), host: None, dragging: true, + paged: None, }; if let Some(drawn) = &edits.drawn { redrawn += usize::from(edited(drawn, &input, &marks(&input).0).is_some()); } whole.drawn = None; for sheet in [&mut edits, &mut whole] { - sheet.update(&input, &mut Jobs::new(None, Waker::noop(), &counters, &errors)); + sheet + .update(&input, &mut Jobs::new(None, Waker::noop(), &counters, &errors, crate::derive::FRAME_WORK)); } if let Some(difference) = differ(&edits.tables, &whole.tables) { panic!("step {step}: {difference}"); diff --git a/crates/latkit/src/view/camera.rs b/crates/latkit/src/view/camera.rs index d01fe6eb..dbb9684a 100644 --- a/crates/latkit/src/view/camera.rs +++ b/crates/latkit/src/view/camera.rs @@ -86,6 +86,11 @@ impl Bounds { zoom: [self.zoom[0].min(other.zoom[0]), self.zoom[1].max(other.zoom[1])], } } + + /// The zooms the camera keeps within: a few levels out past what was framed, eight in. + pub(crate) fn zooms(&self) -> [f64; 2] { + [self.zoom[0] - 3.0, self.zoom[1] + 8.0] + } } impl Camera { @@ -199,8 +204,10 @@ impl Camera { Camera::Map { center: view.shifted(center, d, d.z), zoom, pitch: to_pitch, bearing: to_bearing } } - /// Zoomed in by `levels`, out when negative, toward `anchor`, which keeps its place on screen. - pub(crate) fn zoomed(&self, view: &Projected, anchor: [f64; 3], levels: f64) -> Camera { + /// Zoomed in by `levels`, out when negative, toward `anchor`, which keeps its place on screen. A + /// map's zoom stays within `limits` (what a fit framed lets it reach) before the anchor moves the + /// center, so a zoom held at its limit moves nothing. + pub(crate) fn zoomed(&self, view: &Projected, anchor: [f64; 3], levels: f64, limits: Option<[f64; 2]>) -> Camera { match *self { Camera::Plot { x, y } => { let scale = (-levels).exp2(); @@ -210,7 +217,13 @@ impl Camera { Camera::Plot { x: about(x, anchor[0]), y: y.map(|y| about(y, anchor[1])) } } Camera::Map { center, zoom, pitch, bearing } => { - let to = (zoom + levels).clamp(-MAX_ZOOM, MAX_ZOOM); + let [least, most] = limits.unwrap_or([-MAX_ZOOM, MAX_ZOOM]); + let (least, most) = (least.max(-MAX_ZOOM), most.min(MAX_ZOOM)); + // A zoom already past a limit is not pulled back toward it by a gesture going further. + let to = (zoom + levels).clamp(least.min(zoom), most.max(zoom)); + if to == zoom { + return *self; + } if view.globe >= 0.5 { let zoomed = Camera::Map { center, zoom: to, pitch, bearing }; return match view.screen(view.world(anchor)) { @@ -235,7 +248,7 @@ impl Camera { let across = |k: usize| center[k].clamp(bounds.min[k] - span, bounds.max[k] + span); Camera::Map { center: [across(0), across(1), center[2].clamp(bounds.min[2], bounds.max[2])], - zoom: zoom.clamp(bounds.zoom[0] - 3.0, bounds.zoom[1] + 8.0), + zoom: zoom.clamp(bounds.zooms()[0], bounds.zooms()[1]), pitch: pitch.clamp(0.0, MAX_PITCH), bearing, } @@ -344,6 +357,7 @@ pub(crate) fn latitude(mercator_y: f64) -> f64 { /// /// World coordinates are what the shaders project: position units from the camera's center across a /// plane or plot, mercator radians or the globe on the earth, with elevation up. +#[derive(Clone)] pub(crate) struct Projected { pub(crate) clip_from_world: DMat4, pub(crate) world_from_clip: DMat4, @@ -681,8 +695,8 @@ mod tests { }; check(camera.panned(&view, anchor, [120.0, -50.0]), [at[0] + 120.0, at[1] - 50.0]); check(camera.turned(&view, anchor, [35.0, 12.0]), at); - check(camera.zoomed(&view, anchor, 1.5), at); - check(camera.zoomed(&view, anchor, -2.0), at); + check(camera.zoomed(&view, anchor, 1.5, None), at); + check(camera.zoomed(&view, anchor, -2.0, None), at); } #[test] @@ -723,8 +737,8 @@ mod tests { assert!((s[0] - want[0]).abs() < 0.5 && (s[1] - want[1]).abs() < 0.5, "{s:?} for {want:?}"); }; check(camera.panned(&view, anchor, [-90.0, 40.0]), [at[0] - 90.0, at[1] + 40.0]); - check(camera.zoomed(&view, anchor, 1.25), at); - check(camera.zoomed(&view, anchor, -0.75), at); + check(camera.zoomed(&view, anchor, 1.25, None), at); + check(camera.zoomed(&view, anchor, -0.75, None), at); check(camera.turned(&view, anchor, [25.0, 10.0]), at); } diff --git a/crates/latkit/src/view/drawing.rs b/crates/latkit/src/view/drawing.rs new file mode 100644 index 00000000..2b5bb2d6 --- /dev/null +++ b/crates/latkit/src/view/drawing.rs @@ -0,0 +1,835 @@ +//! Drawings: what a view draws of each table drawn in part. The marks drawing a table hold pages of +//! its order, each in a slot of rows of their own, and draw runs of those held rows: the tiles the +//! camera asks for, finest first; the rows kept whatever their tiles, selected or framed by a fit; +//! and while tiles fade, those leaving. A page keeps its slot while it is held, so its rows keep their +//! words as the camera moves; a page no longer drawn stays held while the plan leaves room, so +//! drawing it again costs nothing, and a page held again costs only its words evaluated again. A page +//! holding vertices that the edges drawn end at is held with them, so an edge always finds its ends. +//! Rows kept whose pages it does not hold are held a row at a time, in pages of kept rows: each keeps +//! its place there while kept, so the ends of edges that name it hold. + +use std::ops::Range; +use std::sync::Arc; + +use rustc_hash::FxHashSet; + +use super::View; +use crate::data::{Field, Name, Reference, UNWIRED, Version}; +use crate::derive::order::{Order, PAGE}; +use crate::derive::stamp_of; +use crate::derive::tiled::{Chosen, Draw, Place}; +use crate::gpu::shaders as wgsl; + +/// What marks a slot holding a page of kept rows, beside the page's number. +const KEPT: u32 = 1 << 31; + +/// What a slot holds. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum Holds { + Free, + /// Page `k` of the order. + Page(u32), + /// Page `j` of the rows kept. + Kept(u32), +} + +impl Holds { + fn of(slot: u32) -> Holds { + match slot { + wgsl::NONE => Holds::Free, + slot if slot & KEPT != 0 => Holds::Kept(slot & !KEPT), + page => Holds::Page(page), + } + } +} + +/// A run of held rows a mark draws: where it starts among the rows drawn, the held row it starts at, +/// how many, and whether its tile fades in or out (`FADE_IN`, `FADE_OUT`). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) struct Run { + pub(crate) item: u32, + pub(crate) held: u32, + pub(crate) len: u32, + pub(crate) fade: u32, +} + +/// What a view draws of one table drawn in part. +#[derive(Debug)] +pub(crate) struct Drawing { + pub(crate) order: Order, + /// What each slot holds: a page of the order, a page of the rows kept (`KEPT` and its number), or + /// nothing (`NONE`). + slots: Arc<[u32]>, + /// Each page's slot, `NONE` where it is not held: what the ends of edges find their vertices by. + pub(crate) pages: Arc<[u32]>, + /// The rows kept whose pages it does not hold, by their place among the rows kept: each one's + /// place in the order, `NONE` where a place is free. + kept: Arc<[u32]>, + /// The rows kept by their place in the order, ascending, each with its held row: what the ends of + /// edges find kept vertices by. + pub(crate) kept_rows: Arc<[[u32; 2]]>, + pub(crate) runs: Vec, + /// Runs by the held row they start at, for finding where a held row draws. + by_held: Vec, + /// The ranges of the order drawn, steadily or fading in, and those fading out. + ranges: Vec>, + leaving: Vec>, + /// Whether tiles fade in or out: until they have, it settles to a drawing without fades. + pub(crate) fades: bool, + /// Rows drawn, and of them those fading out. + pub(crate) items: u32, + pub(crate) leaving_items: u32, + /// When each slot's page was last asked for, by the layout that asked: pages no longer asked for + /// stay held while there is room, those asked for latest first. + asked: Arc<[u64]>, + /// How many layouts led to it. + lays: u64, + /// Pages it holds that it no longer draws, and what their words were made from: the scene and + /// the table's data. + pub(crate) idle: u32, + pub(crate) content: u64, + /// What it was laid out from: the order, what it draws, the rows kept, and the pages ends hold. + pub(crate) laid: u64, + /// What tells it from every other drawing: what lists of its rows are kept by. + pub(crate) made: Version, + /// What tells how it holds rows: see [`Drawing::layout`]. + layout: u64, +} + +/// Rows drawn whatever their tiles, as places in the order, ascending, and what tells them from other +/// such rows without reading them. +#[derive(Clone, Default, Debug)] +pub(crate) struct Pinned { + pub(crate) places: Arc<[u32]>, + stamp: u64, +} + +/// What a drawing is laid out from. +pub(crate) struct Lay<'a> { + pub(crate) chosen: &'a Chosen, + /// Rows drawn whatever their tiles: those selected or framed by a fit, and those the edges kept end + /// at. + pub(crate) pinned: &'a Pinned, + /// Pages held for the ends of edges drawn, ascending. + pub(crate) ends: &'a [u32], + /// Pages it may hold in all: those it no longer draws stay held while there is room. + pub(crate) room: u32, + /// What the words of the rows it holds are made from: the scene and the table's data. Pages no + /// longer drawn stay held only while it is as it was, so none is made again unless drawn. + pub(crate) content: u64, +} + +impl Lay<'_> { + /// The stamp of what it lays out. + pub(crate) fn stamp(&self) -> u64 { + let chosen = self.chosen; + stamp_of((chosen.order.id(), chosen.order.len(), &chosen.ranges, self.pinned.stamp, self.ends, self.room)) + } +} + +impl Drawing { + /// What draws of `lay`: pages `before` holds kept in their slots, those it no longer draws while + /// `lay` leaves room, the rows kept whose pages it does not hold in pages of their own, each in + /// its place there, and with `fades` the tiles arriving since fading in and those gone fading out. + /// A drawing of another order keeps nothing: its rows move, and a transition moves them. + pub(crate) fn lay(before: Option<&Drawing>, lay: &Lay<'_>, fades: bool) -> Drawing { + let order = lay.chosen.order.clone(); + let before = before.filter(|b| b.order.id() == order.id()); + let ranges = lay.chosen.ranges.clone(); + // What the drawing before settles to: its ranges drawn, those it was fading out gone. + let was: &[Range] = before.map_or(&[], |b| &b.ranges); + let has = |list: &[Range], r: &Range| list.iter().any(|x| x == r); + let fading = fades && before.is_some(); + let entering: Vec> = + if fading { ranges.iter().filter(|r| !has(was, r)).cloned().collect() } else { Vec::new() }; + let leaving: Vec> = + if fading { was.iter().filter(|r| !has(&ranges, r)).cloned().collect() } else { Vec::new() }; + // Pages held: those drawn, those fading out, and those ends lie in; while there is room and + // their words hold, those held before and no longer asked for; and the rows kept whose pages + // it does not hold, in pages of their own. + let count = order.pages() as usize; + let asked_now = asked(count, ranges.iter().chain(&leaving), lay.ends); + let mut held = asked_now.clone(); + let idle_kept = + before.filter(|b| b.content == lay.content).map_or(0, |b| linger(b, &asked_now, lay.room, &mut held)); + let kept = keep(before, &lay.pinned.places, &held, &order); + let kept_pages = kept.len().div_ceil(PAGE as usize); + let (slots, pages, kept_slots) = place(before, &held, kept_pages); + let mut kept_rows: Vec<[u32; 2]> = kept + .iter() + .enumerate() + .filter(|&(_, &d)| d != wgsl::NONE) + .map(|(i, &d)| [d, kept_slots[i / PAGE as usize] * PAGE + (i as u32) % PAGE]) + .collect(); + kept_rows.sort_unstable(); + let lays = before.map_or(1, |b| b.lays + 1); + let asked: Arc<[u64]> = slots + .iter() + .enumerate() + .map(|(s, &slot)| match Holds::of(slot) { + Holds::Free => 0, + Holds::Page(page) if !asked_now[page as usize] => { + // An idle page kept its slot, and when it was asked for. + before.and_then(|b| b.asked.get(s)).copied().unwrap_or(lays) + } + _ => lays, + }) + .collect(); + let mut drawing = Drawing { + order, + slots: slots.into(), + pages: pages.into(), + kept: kept.into(), + kept_rows: kept_rows.into(), + runs: Vec::new(), + by_held: Vec::new(), + ranges, + leaving, + fades: false, + items: 0, + leaving_items: 0, + asked, + lays, + idle: idle_kept, + content: lay.content, + laid: lay.stamp(), + made: Version::next(), + layout: 0, + }; + drawing.layout = stamp_of((drawing.order.id(), drawing.order.len(), &*drawing.slots, &*drawing.kept)); + drawing.run(&entering, &lay.pinned.places); + drawing.fades = !entering.is_empty() || !drawing.leaving.is_empty(); + drawing + } + + /// The drawing this one settles to once its tiles have faded: those arriving drawn steadily, + /// those leaving gone, their pages let go. + pub(crate) fn settled(&self, lay: &Lay<'_>) -> Drawing { + Drawing::lay(Some(self), lay, false) + } + + /// Lays out its runs: the tiles fading out first, under the rest; then what it draws, finest + /// first, `entering` fading in; then the rows kept that no tile draws. + fn run(&mut self, entering: &[Range], pinned: &[u32]) { + let covered = |list: &[Range], row: u32| list.iter().any(|r| r.contains(&row)); + let kept: Vec = pinned + .iter() + .filter(|&&row| !covered(&self.ranges, row) && row < self.order.len()) + .filter_map(|&row| self.held_of(row)) + .collect(); + let mut runs: Vec = Vec::new(); + let mut item = 0; + let pages = self.pages.clone(); + let mut push = |held: u32, len: u32, fade: u32, runs: &mut Vec| { + match runs.last_mut() { + Some(last) if last.held + last.len == held && last.fade == fade => last.len += len, + _ => runs.push(Run { item, held, len, fade }), + } + item += len; + }; + let mut span = |range: Range, fade: u32, runs: &mut Vec| { + let mut d = range.start; + while d < range.end { + let page = d / PAGE; + let end = range.end.min((page + 1) * PAGE); + push(pages[page as usize] * PAGE + d % PAGE, end - d, fade, runs); + d = end; + } + }; + let mut leaving_items = 0; + for range in &self.leaving { + // A row kept leaves no gap: it draws steadily, with the rows kept. + let mut start = range.start; + for &row in pinned.iter().filter(|&&row| range.contains(&row)) { + span(start..row, wgsl::FADE_OUT, &mut runs); + start = row + 1; + } + span(start..range.end, wgsl::FADE_OUT, &mut runs); + leaving_items += range.len() as u32 - pinned.iter().filter(|&&row| range.contains(&row)).count() as u32; + } + for range in &self.ranges { + let fade = if entering.contains(range) { wgsl::FADE_IN } else { 0 }; + span(range.clone(), fade, &mut runs); + } + for held in kept { + push(held, 1, 0, &mut runs); + } + let mut by_held: Vec = (0..runs.len() as u32).collect(); + crate::order::by_key(&mut by_held, |&r| u64::from(runs[r as usize].held)); + self.runs = runs; + self.by_held = by_held; + self.items = item; + self.leaving_items = leaving_items; + } + + /// Held rows: a page's rows in each slot. + pub(crate) fn live(&self) -> u32 { + self.slots.len() as u32 * PAGE + } + + /// How many slots it has, free ones among them. + pub(crate) fn slot_count(&self) -> usize { + self.slots.len() + } + + /// What slot `s` holds. + pub(crate) fn holds(&self, s: usize) -> Holds { + Holds::of(self.slots.get(s).copied().unwrap_or(wgsl::NONE)) + } + + /// The places in the order of the rows slot `s` holds, `NONE` where a place holds none. + pub(crate) fn places(&self, s: usize) -> Vec { + match self.holds(s) { + Holds::Free => Vec::new(), + Holds::Page(k) => (k * PAGE..((k + 1) * PAGE).min(self.order.len())).collect(), + Holds::Kept(j) => { + let start = (j * PAGE) as usize; + self.kept[start..self.kept.len().min(start + PAGE as usize)].to_vec() + } + } + } + + /// The rows of its table slot `s` holds, in their places: what a page of its columns packs from; + /// `NONE` where a place holds no row. + pub(crate) fn rows(&self, s: usize) -> Vec { + match self.holds(s) { + Holds::Free => Vec::new(), + Holds::Page(k) => self.order.page(k), + Holds::Kept(_) => { + let source = |d: u32| if d == wgsl::NONE { wgsl::NONE } else { self.order.source(d) }; + self.places(s).into_iter().map(source).collect() + } + } + } + + /// The held row of place `d` in the order, if it holds it: in its page, or among the rows kept. + pub(crate) fn held_of(&self, d: u32) -> Option { + let slot = *self.pages.get((d / PAGE) as usize)?; + if slot != wgsl::NONE { + return Some(slot * PAGE + d % PAGE); + } + let i = self.kept_rows.partition_point(|&[place, _]| place < d); + self.kept_rows.get(i).filter(|&&[place, _]| place == d).map(|&[_, held]| held) + } + + /// The held row of row `r` of its table, if it holds it. + pub(crate) fn held_row(&self, r: u32) -> Option { + (r < self.order.len()).then(|| self.held_of(self.order.rank(r))).flatten() + } + + /// The row of its table held row `h` holds. + pub(crate) fn source(&self, h: u32) -> Option { + let d = match self.holds((h / PAGE) as usize) { + Holds::Free => return None, + Holds::Page(page) => page.checked_mul(PAGE)? + h % PAGE, + Holds::Kept(j) => *self.kept.get((j * PAGE + h % PAGE) as usize)?, + }; + (d != wgsl::NONE && d < self.order.len()).then(|| self.order.source(d)) + } + + /// Where held row `h` draws among the rows drawn, if it does. + pub(crate) fn item_of(&self, h: u32) -> Option { + let i = self.by_held.partition_point(|&r| self.runs[r as usize].held <= h).checked_sub(1)?; + let run = self.runs[self.by_held[i] as usize]; + (h < run.held + run.len).then_some(run.item + h - run.held) + } + + /// The pages of its order it holds. + pub(crate) fn held_pages(&self) -> impl Iterator + '_ { + (0..self.slots.len()).filter_map(|s| match self.holds(s) { + Holds::Page(page) => Some(page), + _ => None, + }) + } + + /// The rows of its table it keeps whose pages it does not hold. + pub(crate) fn kept(&self) -> impl Iterator + '_ { + self.kept_rows.iter().map(|&[place, _]| self.order.source(place)) + } + + /// What tells how it holds rows: its order, what each slot holds, and where it keeps rows. A + /// column's pages follow it. + pub(crate) fn layout(&self) -> u64 { + self.layout + } + + /// The slots holding rows kept. + pub(crate) fn kept_slots(&self) -> Vec { + (0..self.slots.len()).filter(|&s| matches!(self.holds(s), Holds::Kept(_))).map(|s| s as u32).collect() + } + + /// Whether slot `s` holds the same rows in `other` as in it. + pub(crate) fn holds_alike(&self, other: &Drawing, s: usize) -> bool { + self.slots.get(s) == other.slots.get(s) + && (!matches!(self.holds(s), Holds::Kept(_)) || self.places(s) == other.places(s)) + } +} + +/// The pages of `count` asked for: those `ranges` draw, and `ends`. +fn asked<'r>(count: usize, ranges: impl Iterator>, ends: &[u32]) -> Vec { + let mut asked = vec![false; count]; + for range in ranges.filter(|r| r.start < r.end) { + for page in range.start / PAGE..=(range.end - 1) / PAGE { + if let Some(a) = asked.get_mut(page as usize) { + *a = true; + } + } + } + for &page in ends { + if let Some(a) = asked.get_mut(page as usize) { + *a = true; + } + } + asked +} + +/// Holds the pages `before` held that are no longer `asked` for while `room` leaves room, those +/// asked for latest first: how many it holds. +fn linger(before: &Drawing, asked: &[bool], room: u32, held: &mut [bool]) -> u32 { + let wanted = asked.iter().filter(|&&a| a).count() as u32; + let mut idle: Vec<(u64, u32)> = (0..before.slots.len()) + .filter_map(|s| match before.holds(s) { + Holds::Page(page) if !asked.get(page as usize).copied().unwrap_or(true) => Some((before.asked[s], page)), + _ => None, + }) + .collect(); + idle.sort_unstable_by(|a, b| b.cmp(a)); + let kept = idle.iter().take(room.saturating_sub(wanted) as usize); + kept.clone().for_each(|&(_, page)| held[page as usize] = true); + kept.count() as u32 +} + +/// The rows of `pinned`, places in `order`, whose pages are not `held`, by their places among the rows +/// kept, `NONE` where a place is free: each keeps its place in `before`, and the rest take the first +/// free ones. +fn keep(before: Option<&Drawing>, pinned: &[u32], held: &[bool], order: &Order) -> Vec { + let out: Vec = pinned.iter().copied().filter(|&d| d < order.len() && !held[(d / PAGE) as usize]).collect(); + let wanted: FxHashSet = out.iter().copied().collect(); + let mut kept: Vec = before.map_or_else(Vec::new, |b| b.kept.to_vec()); + let mut placed: FxHashSet = FxHashSet::default(); + for d in &mut kept { + if *d != wgsl::NONE && wanted.contains(d) { + placed.insert(*d); + } else { + *d = wgsl::NONE; + } + } + let mut free = (0..kept.len()).filter(|&i| kept[i] == wgsl::NONE).collect::>().into_iter(); + for &d in out.iter().filter(|d| !placed.contains(d)) { + match free.next() { + Some(i) => kept[i] = d, + None => kept.push(d), + } + } + while kept.last() == Some(&wgsl::NONE) { + kept.pop(); + } + kept +} + +/// Slots for the pages `held` and `kept_pages` pages of rows kept, each keeping the slot it had in +/// `before`, the rest taking the first free ones: the slots, each page's slot, and each page of rows +/// kept's slot. +fn place(before: Option<&Drawing>, held: &[bool], kept_pages: usize) -> (Vec, Vec, Vec) { + let mut slots: Vec = before.map_or_else(Vec::new, |b| b.slots.to_vec()); + let mut pages = vec![wgsl::NONE; held.len()]; + let mut kept_slots = vec![wgsl::NONE; kept_pages]; + for (s, slot) in slots.iter_mut().enumerate() { + match Holds::of(*slot) { + Holds::Page(k) if held.get(k as usize).copied().unwrap_or(false) => pages[k as usize] = s as u32, + Holds::Kept(j) if (j as usize) < kept_pages => kept_slots[j as usize] = s as u32, + _ => *slot = wgsl::NONE, + } + } + let mut free = (0..slots.len()).filter(|&s| slots[s] == wgsl::NONE).collect::>().into_iter(); + let mut take = |slots: &mut Vec, what: u32| -> u32 { + let slot = free.next().unwrap_or_else(|| { + slots.push(wgsl::NONE); + slots.len() - 1 + }); + slots[slot] = what; + slot as u32 + }; + for page in 0..held.len() { + if held[page] && pages[page] == wgsl::NONE { + pages[page] = take(&mut slots, page as u32); + } + } + for (j, slot) in kept_slots.iter_mut().enumerate() { + if *slot == wgsl::NONE { + *slot = take(&mut slots, KEPT | j as u32); + } + } + while slots.last() == Some(&wgsl::NONE) { + slots.pop(); + } + (slots, pages, kept_slots) +} + +/// Of each page of an edge table's order, the pages of its ends' table's order that the rows its +/// edges end at lie in: worked out the first time it is held, kept while neither order nor the ends +/// change. Rows appended to either table move no row already placed, so a page is worked out again +/// only once rows were appended to it. A sample lifts the rows its edges end at in the sample of +/// their table, so they lie in the pages it draws first. +#[derive(Default)] +pub(crate) struct Ends { + stamp: u64, + /// Each page's rows when it was worked out, and the pages its ends lie in. + pages: Vec)>>, +} + +impl Ends { + /// The pages of `target`, the order the ends' table draws in, that edge page `page` of `order` + /// ends in, by `ends`, the edges' reference fields. + fn of(&mut self, order: &Order, target: &Order, ends: &[&Reference], page: u32) -> Arc<[u32]> { + let stamp = stamp_of((order.id(), target.id(), ends.iter().map(|r| r.version).collect::>())); + if stamp != self.stamp { + self.stamp = stamp; + self.pages.clear(); + } + if self.pages.len() <= page as usize { + self.pages.resize(page as usize + 1, None); + } + let rows = order.len().saturating_sub(page * PAGE).min(PAGE); + match &self.pages[page as usize] { + Some((had, found)) if *had == rows => found.clone(), + _ => { + let mut found: Vec = Vec::new(); + for r in order.page(page) { + for reference in ends { + let t = reference.target_row(r as usize); + if t != UNWIRED && t < target.len() { + found.push(target.rank(t) / PAGE); + } + } + } + found.sort_unstable(); + found.dedup(); + let found: Arc<[u32]> = found.into(); + self.pages[page as usize] = Some((rows, found.clone())); + found + } + } + } +} + +/// What the edges drawn ask of the table they end at: the pages of its order their ends lie in, and +/// the rows edges kept end at, kept whatever their pages. +#[derive(Default)] +struct Ending { + pages: Vec, + kept: Vec, +} + +/// What draws of a table drawn in part: the drawing drawn, the one replacing it once every page it +/// needs is on the GPU, when the one drawn was first drawn on the view's clock, its ends' pages, and +/// the rows it draws whatever their tiles. +#[derive(Default)] +pub(crate) struct Drawn { + pub(crate) current: Option>, + pub(crate) next: Option>, + pub(crate) since: f64, + ends: Ends, + pinned: Pinned, +} + +impl Drawn { + /// The drawing marks sync to: the one replacing what draws, else what draws. + pub(crate) fn latest(&self) -> Option<&Arc> { + self.next.as_ref().or(self.current.as_ref()) + } + + /// The rows drawn whatever their tiles: `own`, those selected or framed, and those `ending` keeps; + /// gathered again only where those changed. + fn pin(&mut self, own: Vec, ending: &Ending) -> Pinned { + let stamp = stamp_of((&own, &ending.kept)); + if stamp != self.pinned.stamp { + let mut places = own; + places.extend_from_slice(&ending.kept); + places.sort_unstable(); + places.dedup(); + self.pinned = Pinned { places: places.into(), stamp }; + } + self.pinned.clone() + } +} + +impl View { + /// Lays out what draws of each table drawn in part, from what the camera chose: a drawing to + /// replace what draws where what it is laid out from changed, or where what draws has faded in. + /// A table no longer drawn in part draws as it is. + pub(super) fn lay_out_drawings(&mut self) { + let choices = std::mem::take(&mut self.choices); + self.drawings.retain(|key, _| choices.contains_key(key)); + let fades = !self.ease().is_zero(); + let ease = self.ease().as_secs_f64(); + // Edges first: the pages their ends lie in are held with them, and the rows edges kept end at + // kept with them. + let mut keys: Vec<&(Name, Name)> = choices.keys().collect(); + crate::order::by_key(&mut keys, |key| { + u64::from(!matches!(self.planning.plans().get(*key).map(|p| &p.place), Some(Place::Ends(_)))) + }); + let mut ends: crate::data::Keyed<(Name, Name), Ending> = crate::data::Keyed::default(); + for key in keys { + let chosen = &choices[key]; + let ending = ends.remove(key).unwrap_or_default(); + let own = self.pinned(key, &chosen.order); + let (content, room) = (self.content(key), self.room(key)); + let drawn = self.drawings.entry(key.clone()).or_default(); + let pinned = drawn.pin(own, &ending); + let lay = Lay { chosen, pinned: &pinned, ends: &ending.pages, room, content }; + let stamp = lay.stamp(); + let current = drawn.current.clone(); + let latest = drawn.next.as_ref().or(current.as_ref()); + // Pages no longer drawn are let go once their words would be made again. + let stale = latest.is_some_and(|d| d.idle > 0 && d.content != content); + if latest.map(|d| d.laid) != Some(stamp) || stale { + drawn.next = Some(Arc::new(Drawing::lay(current.as_deref(), &lay, fades))); + } else if drawn.next.is_none() + && let Some(current) = current.filter(|c| c.fades && self.clock.done(drawn.since, ease)) + { + drawn.next = Some(Arc::new(current.settled(&lay))); + } + // The pages the edges this drawing holds end in, held with their vertices. + let Some(place) = self.planning.plans().get(key).map(|p| &p.place) else { continue }; + let Place::Ends(fields) = place else { continue }; + let Some(table) = self.sources.iter().find(|d| *d.name() == key.0).and_then(|d| d.table(&key.1)) else { + continue; + }; + let references: Vec<&Reference> = fields + .iter() + .filter_map(|f| match table.field_named(f) { + Some(Field::Reference(r)) => Some(r), + _ => None, + }) + .collect(); + let Some(to) = references.first().map(|r| r.to.clone()) else { continue }; + let target_key = (key.0.clone(), to); + let Some(target) = choices.get(&target_key) else { continue }; + let drawn = self.drawings.get_mut(key).expect("laid out"); + let drawing = drawn.latest().expect("laid out").clone(); + let mut kept: Vec = Vec::new(); + for row in drawing.kept() { + for reference in &references { + let t = reference.target_row(row as usize); + if t != UNWIRED && t < target.order.len() { + kept.push(target.order.rank(t)); + } + } + } + let mut pages: Vec = Vec::new(); + for page in drawing.held_pages() { + pages.extend(drawn.ends.of(&drawing.order, &target.order, &references, page).iter()); + } + pages.sort_unstable(); + pages.dedup(); + ends.insert(target_key, Ending { pages, kept }); + } + self.choices = choices; + } + + /// How far the tiles of each mark's table drawn in part have faded in or out this frame. + pub(super) fn reveal(&mut self) { + let ease = self.ease().as_secs_f64(); + for shown in &mut self.marks { + let key = (shown.mark.style().data.clone(), shown.mark.style().table.clone()); + shown.reveal = match self.drawings.get(&key) { + Some(drawn) if drawn.current.as_ref().is_some_and(|c| c.fades) => { + self.clock.eased(drawn.since, ease) as f32 + } + _ => 1.0, + }; + } + } + + /// What each table of lines or areas drawn in part draws this frame, which their shapes follow: + /// the pages of its order the camera chose, and those holding the rows drawn whatever their tiles. + pub(super) fn paged(&self) -> crate::data::Keyed<(Name, Name), crate::derive::Paged> { + let shaped = |(data, table): &(Name, Name)| { + self.scene.marks.iter().any(|mark| { + let style = mark.style(); + matches!(mark, crate::Mark::Lines(_) | crate::Mark::Areas(_)) + && style.data == *data + && style.table == *table + }) + }; + let mut paged = crate::data::Keyed::default(); + for (key, chosen) in self.choices.iter().filter(|(key, _)| shaped(key)) { + let ranges = chosen.ranges.iter().filter(|r| r.start < r.end); + let mut pages: Vec = ranges.flat_map(|r| r.start / PAGE..=(r.end - 1) / PAGE).collect(); + pages.sort_unstable(); + pages.dedup(); + let pinned = self.pinned(key, &chosen.order); + let kept: Vec = pinned.into_iter().filter(|&d| pages.binary_search(&(d / PAGE)).is_err()).collect(); + paged.insert(key.clone(), crate::derive::Paged { order: chosen.order.clone(), pages, kept }); + } + paged + } + + /// What the words of the rows of `key`'s table are made from: the scene, and the table's data. + fn content(&self, key: &(Name, Name)) -> u64 { + let table = self.source(&key.0).and_then(|d| d.table(&key.1)); + let data = table.map(|t| (t.structure, t.fields.iter().map(|(_, f)| f.version()).collect::>())); + stamp_of((self.scene_version, data)) + } + + /// Pages the drawing of `key`'s table may hold in all: for a table drawn through tiles, as many as + /// the rows the plan gives it fill; else none past those it draws. + fn room(&self, key: &(Name, Name)) -> u32 { + match self.planning.plans().get(key).map(|p| p.draw) { + Some(Draw::Tiles { rows }) => rows.div_ceil(PAGE), + _ => 0, + } + } + + /// The places in `order` of the rows of `key`'s table drawn whatever their tiles: those selected, + /// and those a fit frames, ascending. + fn pinned(&self, key: &(Name, Name), order: &Order) -> Vec { + let Some(table) = self.source(&key.0).and_then(|d| d.table(&key.1)) else { return Vec::new() }; + let mut rows: Vec = self + .focus + .selection + .iter() + .chain(&self.focus.fitting) + .filter(|row| row.data == key.0 && row.table == key.1) + .filter_map(|row| table.row(row.id.as_ref())) + .map(|r| order.rank(r as u32)) + .collect(); + rows.sort_unstable(); + rows.dedup(); + rows + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::derive::pyramid::Pyramid; + + fn order(rows: u32) -> Order { + Order::Pyramid { pyramid: Arc::new(Pyramid::new((0..rows).collect(), Vec::new())), rows } + } + + fn chosen(order: &Order, ranges: &[Range]) -> Chosen { + Chosen { order: order.clone(), ranges: ranges.to_vec() } + } + + /// Rows drawn whatever their tiles: `places`. + fn pinned(places: &[u32]) -> Pinned { + Pinned { places: places.into(), stamp: stamp_of(places) } + } + + /// What draws one range of `order`. + fn one(order: &Order, range: Range) -> Chosen { + Chosen { order: order.clone(), ranges: vec![range] } + } + + #[test] + fn pages_keep_their_slots_as_tiles_come_and_go() { + let order = order(8 * PAGE); + let first = Drawing::lay( + None, + &Lay { + chosen: &chosen(&order, &[0..PAGE, 3 * PAGE..4 * PAGE]), + pinned: &Pinned::default(), + ends: &[], + room: 0, + content: 0, + }, + false, + ); + assert_eq!(&*first.slots, [0, 3]); + assert_eq!(first.items, 2 * PAGE); + // Page 0 leaves, page 5 arrives: it takes the slot page 0 left, and page 3 stays where it was. + let next = Drawing::lay( + Some(&first), + &Lay { + chosen: &chosen(&order, &[3 * PAGE..4 * PAGE, 5 * PAGE..6 * PAGE]), + pinned: &Pinned::default(), + ends: &[], + room: 0, + content: 0, + }, + false, + ); + assert_eq!(&*next.slots, [5, 3]); + assert_eq!(next.held_of(3 * PAGE + 7), Some(PAGE + 7)); + assert_eq!(next.source(7), Some(5 * PAGE + 7)); + assert_eq!(next.item_of(PAGE + 7), Some(7), "page 3's rows draw first"); + } + + #[test] + fn pages_no_longer_drawn_stay_held_while_there_is_room() { + let order = order(8 * PAGE); + let lay = |ranges: &[Range], room: u32, before: Option<&Drawing>| { + Drawing::lay( + before, + &Lay { chosen: &chosen(&order, ranges), pinned: &Pinned::default(), ends: &[], room, content: 0 }, + false, + ) + }; + let first = lay(&[0..PAGE, PAGE..2 * PAGE], 3, None); + assert_eq!(&*first.slots, [0, 1]); + // Page 1 is no longer drawn and page 4 is: with room for three pages, page 1 stays held. + let next = lay(&[0..PAGE, 4 * PAGE..5 * PAGE], 3, Some(&first)); + assert_eq!((&*next.slots, next.items), (&[0, 1, 4][..], 2 * PAGE), "page 1 held, not drawn"); + // Drawn again, it draws from its slot: no slot takes a page of other rows. + let back = lay(&[0..PAGE, PAGE..2 * PAGE], 3, Some(&next)); + assert_eq!(&*back.slots, [0, 1, 4]); + assert_eq!(crate::view::transition::bound(Some(&next), &back), Vec::::new()); + // With room for two, of the pages no longer drawn the one asked for latest stays. + let tight = lay(std::slice::from_ref(&(6 * PAGE..7 * PAGE)), 2, Some(&back)); + assert_eq!(&*tight.slots, [6, 1]); + // Their words made from other data or another scene, pages no longer drawn are let go. + let ranges = [0..PAGE, 4 * PAGE..5 * PAGE]; + let changed = + Lay { chosen: &chosen(&order, &ranges), pinned: &Pinned::default(), ends: &[], room: 3, content: 1 }; + let changed = Drawing::lay(Some(&back), &changed, false); + assert_eq!((&*changed.slots, changed.idle), (&[0, wgsl::NONE, 4][..], 0)); + } + + #[test] + fn tiles_fade_in_and_out_then_settle() { + let order = order(4 * PAGE); + let first = Drawing::lay( + None, + &Lay { chosen: &one(&order, 0..10), pinned: &Pinned::default(), ends: &[], room: 0, content: 0 }, + true, + ); + assert!(!first.fades, "a first drawing has nothing to fade from"); + let lay = Lay { chosen: &one(&order, PAGE..PAGE + 10), pinned: &pinned(&[3]), ends: &[], room: 0, content: 0 }; + let next = Drawing::lay(Some(&first), &lay, true); + assert!(next.fades); + let fades: Vec<(u32, u32)> = next.runs.iter().map(|r| (r.len, r.fade)).collect(); + // Rows 0..10 fade out but row 3, kept, which draws steadily after the tile arriving. + assert_eq!(fades, [(3, wgsl::FADE_OUT), (6, wgsl::FADE_OUT), (10, wgsl::FADE_IN), (1, 0)]); + assert_eq!(next.leaving_items, 9); + let settled = next.settled(&lay); + assert!(!settled.fades); + assert_eq!(settled.runs.iter().map(|r| r.fade).collect::>(), [0, 0]); + assert_eq!(settled.items, 11); + } + + #[test] + fn rows_kept_lie_in_pages_of_their_own_and_ends_hold_their_pages() { + let order = order(4 * PAGE); + let lay = + Lay { chosen: &one(&order, 0..10), pinned: &pinned(&[2 * PAGE + 1]), ends: &[3], room: 0, content: 0 }; + let drawing = Drawing::lay(None, &lay, false); + assert_eq!(&*drawing.slots, [0, 3, KEPT], "the page drawn, the ends', and a page of the row kept"); + assert_eq!(drawing.items, 11, "ends' pages are held, not drawn"); + let held = drawing.held_of(2 * PAGE + 1).expect("kept"); + assert_eq!((held, drawing.source(held)), (2 * PAGE, Some(2 * PAGE + 1))); + assert_eq!(drawing.held_pages().collect::>(), [0, 3]); + // Another row kept takes the next place among them; the first keeps its own. + let more = Lay { pinned: &pinned(&[PAGE + 7, 2 * PAGE + 1]), ..lay }; + let next = Drawing::lay(Some(&drawing), &more, false); + assert_eq!((next.held_of(2 * PAGE + 1), next.held_of(PAGE + 7)), (Some(2 * PAGE), Some(2 * PAGE + 1))); + assert_eq!(crate::view::transition::bound(Some(&drawing), &next), [2], "only the page of rows kept"); + // A row kept whose page comes to be drawn draws from its page. + let drawn = Lay { chosen: &chosen(&order, &[0..10, 2 * PAGE..2 * PAGE + 10]), ..more }; + let paged = Drawing::lay(Some(&next), &drawn, false); + assert_eq!(paged.held_of(2 * PAGE + 1), Some(paged.pages[2] * PAGE + 1)); + assert_eq!(paged.held_of(PAGE + 7), Some(2 * PAGE + 1), "the other kept where it was"); + } +} diff --git a/crates/latkit/src/view/event.rs b/crates/latkit/src/view/event.rs index f3598372..4c3e0df7 100644 --- a/crates/latkit/src/view/event.rs +++ b/crates/latkit/src/view/event.rs @@ -342,12 +342,6 @@ pub struct Stats { pub tiles: u64, /// Tiles being fetched or decoded, of every tile source. pub tiles_loading: u64, - /// Bytes of data waiting to upload. - pub uploading: u64, - /// Bytes of data pages on the GPU, for every view of its `Gpu`. - pub resident: u64, - /// Bytes the GPU's [`Budget`](crate::Budget) lets data take. - pub budget: u64, /// The work under way the last frame stood in for; nothing when it drew everything as the data, /// scene, and camera say: see [`View::complete`](crate::View::complete). pub waiting: Waiting, diff --git a/crates/latkit/src/view/fit.rs b/crates/latkit/src/view/fit.rs index 12ebfe54..ee1c1f24 100644 --- a/crates/latkit/src/view/fit.rs +++ b/crates/latkit/src/view/fit.rs @@ -105,8 +105,8 @@ impl View { // Everything a table drawn through tiles holds frames by its rows' bounds, whatever is drawn. A // mark with no rows has nothing to frame: it holds a fit only while no mark has rows. let compiled = || self.marks.iter().filter(|m| m.compiled.is_some()); - let drawn = |m: &Shown| (m.drawable() && !m.tables.waiting()) || (all && self.tiled_bounds(m).is_some()); - let empty = |m: &Shown| m.tables.synced() && m.rows + m.exits == 0; + let drawn = |m: &Shown| (m.drawable() && !m.tables.pending()) || (all && self.tiled_bounds(m).is_some()); + let empty = |m: &Shown| m.tables.synced() && m.held() == 0; let ready = compiled().any(&drawn) && compiled().all(|m| drawn(m) || empty(m)) && !self.derived.sheet.placing() @@ -262,7 +262,7 @@ impl View { let spans = self.marks.iter().filter_map(|shown| { let crate::Mark::Lines(lines) = &shown.mark else { return None }; let style = shown.mark.style(); - let table = self.drawn(&style.data)?.table(&style.table)?; + let table = self.source(&style.data)?.table(&style.table)?; match table.field_named(&lines.traced())? { crate::data::Field::Sampled(samples, _) => samples.span(), _ => None, diff --git a/crates/latkit/src/view/focus.rs b/crates/latkit/src/view/focus.rs index 425c02a5..3425eb25 100644 --- a/crates/latkit/src/view/focus.rs +++ b/crates/latkit/src/view/focus.rs @@ -1,8 +1,8 @@ -//! Selection, hover, and the rows a fit measures, as focus words: one per row of every mark, written -//! only where they change. The selection keeps only rows a pickable mark shows. The ends of a selected -//! or hovered edge are lit too. Each mark's lit rows are listed for its focus draw, which draws them -//! again over everything, glowing. Hover is state either way, which expressions read; whether it -//! glows is the controls' to say. +//! Selection, hover, and the rows a fit measures, as focus words: one per held row of every mark, +//! written only where they change. The selection keeps only rows a pickable mark shows. The ends of a +//! selected or hovered edge are lit too. Each mark's lit rows are listed, as the rows it draws, for its +//! focus draw, which draws them again over everything, glowing. Hover is state either way, which +//! expressions read; whether it glows is the controls' to say. use rustc_hash::FxHashMap; @@ -90,10 +90,10 @@ impl View { // Channels reading focus evaluate the blocks of the rows whose words changed; every row // after the words were laid out again. for shown in &mut self.marks { - let (focus, rows) = (shown.region.focus, shown.rows + shown.exits); + let (focus, held) = (shown.region.focus, shown.held()); let blocks: Vec = changes .iter() - .filter(|&&(w, _)| w >= focus && w - focus < rows) + .filter(|&&(w, _)| w >= focus && w - focus < held) .map(|&(w, _)| (w - focus) / wgsl::BLOCK_ROWS) .collect(); shown.dirty_rows(shown.channels_reading(Deps::FOCUS), (!fresh).then_some(&blocks[..])); @@ -104,9 +104,9 @@ impl View { self.focus.written = words; } - /// Sets `bit` in the focus words of `rows` in every mark showing them (with `picked`, every + /// Sets `bit` in the focus words of `rows` in every mark holding them (with `picked`, every /// pickable one), and with `ends` lights the ends of each edge among them; returns the rows some - /// mark shows. A row its data holds stays shown while a table drawn in part does not draw it. + /// mark shows. A row its data holds stays shown while a table drawn in part holds no page of it. fn focus_rows( &self, rows: &[Row], @@ -117,12 +117,8 @@ impl View { ) -> Vec { let mut kept = Vec::new(); for row in rows { - let held = self.data(&row.data).and_then(|d| d.table(&row.table)).and_then(|t| t.row(row.id.as_ref())); - if held.is_none() { - continue; - } - let table = self.drawn(&row.data).and_then(|d| d.table(&row.table)); - let index = table.and_then(|t| t.row(row.id.as_ref())); + let Some(table) = self.data(&row.data).and_then(|d| d.table(&row.table)) else { continue }; + let Some(index) = table.row(row.id.as_ref()) else { continue }; let mut shows = false; for shown in &self.marks { let style = shown.mark.style(); @@ -130,9 +126,8 @@ impl View { continue; } shows = true; - let (Some(table), Some(index)) = (table, index) else { continue }; - if (index as u32) < shown.rows { - *words.entry(shown.region.focus + index as u32).or_default() |= bit; + if let Some(held) = shown.held_row(index as u32) { + *words.entry(shown.region.focus + held).or_default() |= bit; } let crate::Mark::Edges(edges) = &shown.mark else { continue }; let Some((from, to)) = edges.ends.as_ref().filter(|_| ends) else { continue }; @@ -148,7 +143,7 @@ impl View { kept } - /// Lights the row `reference` names for row `index` in every vertex mark drawing its table. + /// Lights the row `reference` names for row `index` in every vertex mark holding its table's. fn light_end(&self, data: &crate::Name, reference: &Reference, index: usize, words: &mut FxHashMap) { let Some(target) = reference.target(index) else { return }; for shown in &self.marks { @@ -156,37 +151,40 @@ impl View { if shown.kind() == crate::scene::mark::MarkKind::Vertices && style.data == *data && style.table == reference.to - && target < shown.rows + && let Some(held) = shown.held_row(target) { - *words.entry(shown.region.focus + target).or_default() |= wgsl::END; + *words.entry(shown.region.focus + held).or_default() |= wgsl::END; } } } - /// Lists every mark's rows with a bit of `glowing` set for its focus draw, with the instances - /// each takes. + /// Lists every mark's drawn rows with a bit of `glowing` set for its focus draw, with the + /// instances each takes. fn lay_out_lit(&mut self, words: &FxHashMap, glowing: u32) { let mut all = Vec::new(); for i in 0..self.marks.len() { - let (focus, rows) = (self.marks[i].region.focus, self.marks[i].rows); - let mut lit: Vec = words + let shown = &self.marks[i]; + let (focus, live) = (shown.region.focus, shown.live()); + let mut lit: Vec<(u32, u32)> = words .iter() - .filter(|&(&w, &bits)| w >= focus && w - focus < rows && bits & glowing != 0) - .map(|(&w, _)| w - focus) + .filter(|&(&w, &bits)| w >= focus && w - focus < live && bits & glowing != 0) + .filter_map(|(&w, _)| Some((shown.item_of(w - focus)?, w - focus))) .collect(); lit.sort_unstable(); - let pieces = if lit.is_empty() { 0 } else { self.pieces(i, &lit) }; + let held: Vec = lit.iter().map(|&(_, h)| h).collect(); + let pieces = if lit.is_empty() { 0 } else { self.pieces(i, &held) }; let rows = if pieces == 0 { 0 } else { lit.len() as u32 }; self.marks[i].lit = Lit { at: all.len() as u32, rows, pieces }; if rows > 0 { - all.extend(lit); + all.extend(lit.into_iter().map(|(item, _)| item)); } } self.focus.lit = Some(all); } - /// Instances mark `i`'s focus draw takes for each of its `lit` rows: a point one; a path as many - /// as the longest of theirs has pieces, past which a row's own draw nothing; none for faces. + /// Instances mark `i`'s focus draw takes for each of its lit held rows `lit`: a point one; a path + /// as many as the longest of theirs has pieces, past which a row's own draw nothing; none for + /// faces. fn pieces(&self, i: usize, lit: &[u32]) -> u32 { let shown = &self.marks[i]; let Some(compiled) = &shown.compiled else { return 0 }; @@ -200,23 +198,24 @@ impl View { } } - /// The most points any of mark `i`'s `lit` rows has in its geometry column: its path, its bends, - /// or its triangles. + /// The most points any of mark `i`'s held rows `lit` has in its geometry column: its path, its + /// bends, or its triangles. fn longest(&self, i: usize, lit: &[u32]) -> u32 { let shown = &self.marks[i]; let Some(compiled) = &shown.compiled else { return 0 }; let Some(column) = compiled.program.columns.get(compiled.drawing.geometry as usize) else { return 0 }; let style = shown.mark.style(); - let Some(data) = self.drawn(&style.data) else { return 0 }; + let Some(data) = self.source(&style.data) else { return 0 }; let Some(table) = data.table(&style.table) else { return 0 }; - let source = Source { data, table, derived: self.derived.beside(&style.data, &style.table) }; - let Some((_, table)) = table_of(column, &source, &self.data) else { return 0 }; + let source = Source { data, table, derived: self.derived.beside(&style.data, &style.table), paged: Vec::new() }; + let Some((_, table)) = table_of(column, &source, &self.sources) else { return 0 }; let Some(Field::Values(values, _)) = table.field_named(&column.name) else { return 0 }; let pages = values.pages(); lit.iter() - .filter_map(|&row| { - let page = pages.get(row as usize / PAGE_ROWS)?; - Some(page.values.geometry()?.row_coords(row as usize % PAGE_ROWS).len() as u32) + .filter_map(|&held| { + let row = shown.source_row(held)? as usize; + let page = pages.get(row / PAGE_ROWS)?; + Some(page.values.geometry()?.row_coords(row % PAGE_ROWS).len() as u32) }) .max() .unwrap_or(0) diff --git a/crates/latkit/src/view/frame.rs b/crates/latkit/src/view/frame.rs index 173b1279..71c96b4b 100644 --- a/crates/latkit/src/view/frame.rs +++ b/crates/latkit/src/view/frame.rs @@ -5,17 +5,20 @@ use web_time::Instant; -use super::marks::Shown; -use super::passes::Drawn; +use super::drawing::Drawn; +use super::marks::{End, Holding, Shown}; use super::resources::{EyeSlot, MARK_STRIDE, Resources}; -use super::sync::{Source, Synced, Window, table_of}; -use super::transition::{remap, replaced, rewritten}; +use super::sync::{Inputs, Rows, Source, Synced, Window, table_of}; +use super::transition::{bound, holds_alike, in_place, moved}; use super::{Camera, Event, Frame, View, timer, uniforms}; +use crate::data::Keyed; +use crate::derive::order::Order; +use crate::derive::tiled::Place; use crate::expr::compile::Deps; use crate::gpu::Inner; use crate::gpu::shaders as wgsl; use crate::scene::mark::MarkKind; -use crate::{Error, derive}; +use crate::{Error, Name, derive}; /// The most seconds comets travel in one frame: after a pause they carry on, not leap. const TRAVEL: f64 = 0.1; @@ -65,6 +68,7 @@ impl View { self.settle(); self.advance(dt); let mut inner = gpu.0.lock(); + inner.begin_frame(self.id); inner.atlas.tick(); self.settle_labels(&mut inner); self.derive(&mut inner); @@ -112,7 +116,7 @@ impl View { self.measuring_started(readback, measure.and_then(|m| m.fit)); } self.count_found(encoder, &inner); - let mut drawn = Drawn::default(); + let mut drawn = super::passes::Drawn::default(); if let Some(light) = &light { self.cast(encoder, &mut inner, &mut drawn, light); } else { @@ -132,7 +136,7 @@ impl View { timer.end(encoder, device); } for shown in &mut self.marks { - shown.held = shown.rows + shown.exits; + shown.before = shown.held(); shown.moved = None; shown.carry = false; } @@ -229,9 +233,9 @@ impl View { } /// Derives what marks read beside their data, from the data and the scene alone, handing the - /// host what the frame cannot afford. First the tiles the camera and the plot window ask for: - /// the datasets tiles make, then what each dataset drawn in part draws, which the rest derive - /// from. + /// host what the frame cannot afford. First the tiles the camera and the plot window ask for, + /// the datasets tiles make; then the orders tables drawn in part draw in, and the ranges of them + /// the camera asks for; then every other derivation, from the data. fn derive(&mut self, inner: &mut Inner) { let host = inner.host.clone(); let projected = self.projected(); @@ -241,7 +245,7 @@ impl View { // One budget for the frame: the tiles, what is drawn in part, then every other derivation. let (waker, counters) = (self.waker.clone(), self.gpu.0.counters.clone()); let errors = self.gpu.0.errors.clone(); - let mut jobs = derive::Jobs::new(host.as_deref(), &waker, &counters, &errors); + let mut jobs = derive::Jobs::new(host.as_deref(), &waker, &counters, &errors, inner.frame.work); let mut tiles = std::mem::take(&mut self.derived.tiles); let reading = self.reading(); let asked = derive::tiles::Asked { sight: earth.then_some(&sight), reading: &reading }; @@ -253,48 +257,23 @@ impl View { let budget = inner.budget.bytes; self.update_plan(&mut inner.ledger, budget); self.trace_columns = self.planning.trace_columns(); - let pinned: Vec = self.focus.selection.iter().chain(&self.focus.fitting).cloned().collect(); let ask = derive::tiled::Ask { sources: &self.sources, plans: self.planning.plans(), sight: placing.then_some(&sight), earth, - pinned: &pinned, - at: self.at, }; let mut tiled = std::mem::take(&mut self.derived.tiled); - let (changed, _) = tiled.update(&ask, &mut jobs); + self.choices = tiled.update(&ask, &mut jobs); self.derived.tiled = tiled; - self.follow_sources(&changed); + let paged = self.paged(); let mut derived = std::mem::take(&mut self.derived); - if derived.update(&self.derive_input(host.as_deref()), &mut jobs) { + let input = derive::Input { paged: Some(&paged), ..self.derive_input(host.as_deref()) }; + if derived.update(&input, &mut jobs) { self.changed = true; } self.derived = derived; - } - - /// Makes what marks draw follow the sources: each dataset as it is, or as drawn in part, which - /// `changed` holds the new versions of. - fn follow_sources(&mut self, changed: &[crate::Data]) { - let mut next = Vec::with_capacity(self.sources.len()); - for source in &self.sources { - let name = source.name(); - let drawn = match changed.iter().find(|d| d.name() == name).cloned() { - Some(drawn) => drawn, - None if self.derived.tiled.draws(name) => match self.drawn(name) { - Some(drawn) => drawn.clone(), - None => continue, - }, - None => source.clone(), - }; - next.push(drawn); - } - if next != self.data { - self.data = next; - // Rows may have moved under the focus. - self.focus.dirty = true; - self.fitting.plot_due = true; - } + inner.frame.work = jobs.left() as u32; } /// What derivations read this frame. @@ -302,9 +281,10 @@ impl View { derive::Input { scene: &self.scene, scene_version: self.scene_version, - data: &self.data, + data: &self.sources, host, dragging: self.dragging(), + paged: None, } } @@ -314,7 +294,7 @@ impl View { fn lay_out_lists(&mut self, answers: bool) -> Option<([f32; 4], bool)> { self.lay_out_segments(); let device = &self.gpu.0.device; - self.resources.as_mut().expect("created").fit_lists(device, u64::from(self.segment_words)); + self.resources.as_mut().expect("created").fit_lists(device, u64::from(self.list_words)); if answers { self.next_query() } else { None } } @@ -328,6 +308,7 @@ impl View { ) { let gpu = self.gpu.clone(); let (device, queue) = (&gpu.0.device, &gpu.0.queue); + self.reveal(); let marks = self.mark_uniforms(); let resources = self.resources.as_mut().expect("created"); queue.write_buffer(&resources.frame, 0, bytemuck::bytes_of(frame)); @@ -347,16 +328,16 @@ impl View { } } - /// Syncs every mark's columns and streams the pages they need within the frame's budget. + /// Lays out what each table drawn in part draws, syncs every mark's columns to it, streams the + /// pages they need within what the GPU's frame has left to upload, and swaps in each dataset + /// whose pages have all landed, those that just did included. fn stream(&mut self, inner: &mut Inner) { let gpu = self.gpu.clone(); - let waiting = self.sync(inner); - let budget = inner.budget; - inner.arena.pump(&gpu.0.device, &gpu.0.queue, budget.upload_per_frame); - if waiting { - // Pages that just uploaded can show this frame. - self.sync(inner); - } + self.lay_out_drawings(); + self.sync(inner); + let uploaded = inner.arena.pump(&gpu.0.device, &gpu.0.queue, inner.frame.upload); + inner.frame.upload = inner.frame.upload.saturating_sub(uploaded); + self.publish(inner); } /// Every mark's uniform, at its dynamic offset. @@ -406,74 +387,156 @@ impl View { derive::tiles::Reading { across, columns, at: self.at, fields } } - /// Syncs every mark's columns; changed columns dirty the channels reading them, and changed - /// static values or rows start a transition. True when a mark waits for uploads. - fn sync(&mut self, inner: &mut Inner) -> bool { + /// Syncs every mark's columns to its data, and for a table drawn in part to the drawing replacing + /// what draws: tables whose inputs changed are rebuilt once, and wait for their pages. + fn sync(&mut self, inner: &mut Inner) { let at = self.at; let (across, columns) = self.trace_window(); let window = Window { across, columns, steps: 0 }; - // A drag moves rows at once. - let easing = !self.ease().is_zero() && !self.dragging(); - let mut waiting = false; + // The order each table drawn in part draws in, by dataset. + let mut orders: Keyed> = Keyed::default(); + for ((data, table), drawn) in &self.drawings { + if let Some(drawing) = drawn.latest() { + orders.entry(data.clone()).or_default().push((table.clone(), drawing.order.clone())); + } + } for shown in &mut self.marks { let Some(compiled) = &shown.compiled else { continue }; - let program = compiled.program.clone(); - let geometry = compiled.drawing.geometry; - let window = Window { steps: compiled.drawing.flags & (wgsl::STEP_BEFORE | wgsl::STEP_AFTER), ..window }; let style = shown.mark.style(); - let Some(data) = self.data.iter().find(|d| *d.name() == style.data) else { continue }; + let Some(data) = self.sources.iter().find(|d| *d.name() == style.data) else { continue }; let Some(table) = data.table(&style.table) else { continue }; let derived = self.derived.beside(&style.data, &style.table); - let source = Source { data, table, derived }; - let synced = shown.tables.sync(&program, &source, &self.data, at, window, geometry, inner); - waiting |= matches!(synced, Synced::Waiting); - let Synced::Changed { columns, blocks } = synced else { continue }; - let moved = shown.drawn.as_ref().and_then(|(origin, old)| { - if *origin == data.origin() { remap(old, table) } else { Some(replaced(old, table)) } - }); - // A path morphs from the one its row ran through only while every row keeps its place; - // rows that moved, as tiles' do when finer ones replace them, fade instead. - let kept = moved.as_ref().is_none_or(|(sources, exits)| { - exits.is_empty() && sources.iter().enumerate().all(|(r, &s)| s == r as u32 || s == wgsl::NONE) - }); - // Rows that came, went, or moved evaluate every channel, constants included: where every row - // kept its place, as a table drawn through tiles keeps those of the tiles still drawn, over - // the row blocks rewritten; else over every row. - let in_place = moved - .as_ref() - .is_some_and(|(sources, _)| sources.iter().enumerate().all(|(r, &s)| s == r as u32 || s == wgsl::NONE)); - if moved.is_some() || shown.rows != table.len() as u32 { - match shown.drawn.as_ref().filter(|_| in_place).and_then(|(_, old)| rewritten(old, table)) { - Some(blocks) => shown.rows_rewritten(&blocks), - None => shown.all_dirty(), - } + let source = Source { data, table, derived, paged: self.derived.paged(&style.data, &style.table) }; + let drawing = self.drawings.get(&(style.data.clone(), style.table.clone())).and_then(Drawn::latest); + let rows = drawing.map_or(Rows::Own, |drawing| Rows::Held(drawing)); + let window = Window { steps: compiled.drawing.flags & (wgsl::STEP_BEFORE | wgsl::STEP_AFTER), ..window }; + let orders = orders.get(&style.data).map_or(&[][..], Vec::as_slice); + let inputs = Inputs { all: &self.sources, at, window, orders }; + shown.tables.sync(&compiled.program, &source, rows, &inputs, inner); + } + } + + /// Swaps in what each dataset draws once all of it is ready: every mark's rebuilt tables, and + /// the drawing of each of its tables drawn in part, together, so edges never end at another + /// drawing's vertices and a data version appears whole. + fn publish(&mut self, inner: &mut Inner) { + let mut datasets: Vec = + self.marks.iter().filter(|m| m.compiled.is_some()).map(|m| m.mark.style().data.clone()).collect(); + datasets.sort(); + datasets.dedup(); + let now = self.clock.now(); + for name in datasets { + let marks: Vec = (0..self.marks.len()) + .filter(|&i| self.marks[i].compiled.is_some() && self.marks[i].mark.style().data == name) + .collect(); + if marks.iter().any(|&i| self.marks[i].tables.waiting(inner) == Synced::Waiting) { + continue; } - // Without an ease, a flowing mark whose rows moved still begins, a transition of no length, - // so its comets follow the rows. - let carries = shown.mark.flows() && moved.is_some(); - if (easing || carries) && shown.held > 0 && (moved.is_some() || shown.static_columns(columns)) { - shown.begin(moved); - } else if moved.is_some() { - shown.exits = 0; + for (key, drawn) in self.drawings.iter_mut() { + if key.0 == name + && let Some(next) = drawn.next.take() + { + if next.fades { + drawn.since = now; + } + drawn.current = Some(next); + } } - if !kept || (shown.begin.is_none() && shown.transition.is_none()) { - shown.tables.thaw(inner); + for i in marks { + self.took(i, inner); } - shown.rows = table.len() as u32; - shown.drawn = Some((data.origin(), table.clone())); - shown.columns_changed(columns, blocks.as_deref()); - if shown.kind() == MarkKind::Lines { - self.fitting.plot_due = true; + } + } + + /// Takes what mark `i` draws now its dataset is published: its rebuilt tables, its table's + /// drawing, and the rows they hold. Changed columns dirty the channels reading them; rows that + /// came, went, or moved evaluate every channel, and with changed static values begin a + /// transition. + fn took(&mut self, i: usize, inner: &mut Inner) { + // A drag moves rows at once. + let easing = !self.ease().is_zero() && !self.dragging(); + let shown = &mut self.marks[i]; + let Some(compiled) = &shown.compiled else { return }; + let (program, geometry) = (compiled.program.clone(), compiled.drawing.geometry); + let key = (shown.mark.style().data.clone(), shown.mark.style().table.clone()); + let Some(data) = self.sources.iter().find(|d| *d.name() == key.0) else { return }; + let Some(table) = data.table(&key.1) else { return }; + let drawing = self.drawings.get(&key).and_then(|d| d.current.clone()); + let published = shown.tables.publish(&program, geometry, inner); + let holding = Holding { origin: data.origin(), table: table.clone(), drawing: drawing.clone() }; + if published.is_none() && shown.holding.as_ref().is_some_and(|h| holds_alike(h, &holding)) { + return; + } + let before = shown.holding.take(); + let alike = before.as_ref().is_some_and(|old| holds_alike(old, &holding)); + let moved = before.as_ref().and_then(|old| moved(old, &holding)); + // A path morphs from the one its row ran through only while every row keeps its place; + // rows that moved, as rows do between orders, fade instead. + let kept = moved.as_ref().is_none_or(|(sources, exits)| exits.is_empty() && in_place(sources)); + // Slots given pages of other rows: their rows evaluate every channel, and their labels start + // over. + let bound = match &drawing { + Some(new) => bound(before.as_ref().and_then(|h| h.drawing.as_deref()).filter(|_| moved.is_none()), new), + None => Vec::new(), + }; + match &before { + // Rows that came, went, or moved evaluate every channel, constants included: where every + // row kept its place, over the row blocks rewritten; else over every row. + Some(old) if old.drawing.is_none() && drawing.is_none() => { + if moved.is_some() || shown.rows != table.len() as u32 { + let rewritten = old.table.len() <= table.len() && moved.as_ref().is_none_or(|(s, _)| in_place(s)); + match rewritten.then(|| super::transition::rewritten(&old.table, table)).flatten() { + Some(pages) => shown.rows_rewritten(&super::marks::Blocks::of_pages(&pages)), + None => shown.all_dirty(), + } + } } + Some(_) if moved.is_none() => shown.slots_bound(&bound), + _ => shown.all_dirty(), + } + shown.rows = drawing.as_ref().map_or(table.len() as u32, |d| d.items); + shown.holding = Some(holding); + // Without an ease, a flowing mark whose rows moved still begins, a transition of no length, + // so its comets follow the rows. + let carries = shown.mark.flows() && moved.is_some(); + let eased = published.as_ref().is_some_and(|p| shown.static_columns(p.data)); + if (easing || carries) && shown.before > 0 && (moved.is_some() || eased) { + shown.begin(moved); + } else if moved.is_some() { + shown.exits = 0; + } else if shown.transition.is_some() && !bound.is_empty() { + // Rows of slots bound while the mark eases enter; the rest ease on from what they show. + shown.restart(&bound); + } + if !kept || (shown.begin.is_none() && shown.transition.is_none()) { + shown.tables.thaw(inner); + } + // Where rows draw changed: what follows it, shadows, normals, labels, and segment lists, follows. + // A column changed alone evaluates the channels reading it, which moves them as it must. + if !alike { + shown.version += 1; + shown.place_version += 1; + } + shown.bound = bound; + shown.packed = false; + if let Some(published) = &published { + shown.columns_changed(published.data, published.blocks.as_deref()); + let source = Source { data, table, derived: self.derived.beside(&key.0, &key.1), paged: Vec::new() }; for text in &program.texts { let column = &program.columns[text.column as usize]; - let code = table_of(column, &source, &self.data).map_or(wgsl::NONE, |(d, t)| { + let code = table_of(column, &source, &self.sources).map_or(wgsl::NONE, |(d, t)| { inner.dictionaries.field(d, &t.name, &column.name).code_of(&text.text) }); shown.constants[text.constant as usize] = code; } } - waiting + if shown.kind() == MarkKind::Lines { + self.fitting.plot_due = true; + } + // Rows may have moved under the focus. + if !alike { + self.focus.dirty = true; + } } /// Where each mark's fitted domains start; returns how many there are. @@ -490,30 +553,56 @@ impl View { } /// Points every edge and face mark's ends at the vertex marks drawing their tables. + /// The ends of rows evaluate again where their vertices moved: another vertex mark or region, + /// another order, or for rows that hold no page their ends lie in, other slots. Edges drawn in + /// part hold the pages their ends lie in, whose slots never move under them. fn attach_ends(&mut self) { for i in 0..self.marks.len() { let ends = ends(&self.marks, i); - if ends != self.marks[i].ends { - self.marks[i].ends = ends; - self.marks[i].packed = false; + let was = &self.marks[i].ends; + if ends == *was { + continue; + } + let style = self.marks[i].mark.style(); + let place = self.planning.plans().get(&(style.data.clone(), style.table.clone())).map(|p| &p.place); + let closed = self.marks[i].drawing().is_some() && matches!(place, Some(Place::Ends(_))); + let moved = ends.len() != was.len() + || ends.iter().zip(was).any(|(a, b)| { + a.first != b.first || a.order != b.order || (!closed && (a.pages != b.pages || a.kept != b.kept)) + }); + // Edges kept find their ends among the rows kept, which move as those change: they alone + // evaluate again. + let kept = closed && ends.iter().zip(was).any(|(a, b)| a.kept != b.kept); + self.marks[i].ends = ends; + self.marks[i].packed = false; + if moved { self.marks[i].all_dirty(); + } else if kept { + let slots = self.marks[i].drawing().map(|d| d.kept_slots()).unwrap_or_default(); + self.marks[i].slots_bound(&slots); } } } } -/// The graph slot of vertex 0 of the vertex mark each of mark `i`'s ends attaches to, or `NONE`. An -/// end attaches to the unnamed vertex mark drawing its table, else to the first one. -fn ends(marks: &[Shown], i: usize) -> Vec { +/// Where each of mark `i`'s ends finds its vertices: the vertex mark it attaches to, its vertex 0's +/// graph slot, and where it holds the pages of the order it draws its table in, if in part. An end +/// attaches to the unnamed vertex mark drawing its table, else to the first one. +fn ends(marks: &[Shown], i: usize) -> Vec { let Some(compiled) = &marks[i].compiled else { return Vec::new() }; let data = &marks[i].mark.style().data; compiled .program .ends .iter() - .map(|table| { - super::graph::end_mark(marks, data, table) - .map_or(wgsl::NONE, |m| marks[m].region.state / wgsl::VERTEX_WORDS) + .map(|table| match super::graph::end_mark(marks, data, table) { + Some(m) => End { + first: marks[m].region.state / wgsl::VERTEX_WORDS, + order: marks[m].drawing().map_or(0, |d| d.order.id()), + pages: marks[m].drawing().map(|d| d.pages.clone()), + kept: marks[m].drawing().map(|d| d.kept_rows.clone()), + }, + None => End { first: wgsl::NONE, order: 0, pages: None, kept: None }, }) .collect() } diff --git a/crates/latkit/src/view/gestures.rs b/crates/latkit/src/view/gestures.rs index 3a20016f..44454fe1 100644 --- a/crates/latkit/src/view/gestures.rs +++ b/crates/latkit/src/view/gestures.rs @@ -320,7 +320,8 @@ impl View { let delta = [f64::from(to[0] - from[0]), f64::from(to[1] - from[1])]; let camera = self.camera.panned(&self.projected(), anchor, delta); let view = self.projected_with(&camera); - let camera = camera.zoomed(&view, anchor, f64::from(spread(after) / spread(before)).log2()); + let levels = f64::from(spread(after) / spread(before)).log2(); + let camera = camera.zoomed(&view, anchor, levels, self.zoom_limits()); self.steer(camera); } _ => {} diff --git a/crates/latkit/src/view/labels.rs b/crates/latkit/src/view/labels.rs index 3a4a529c..2caccc64 100644 --- a/crates/latkit/src/view/labels.rs +++ b/crates/latkit/src/view/labels.rs @@ -105,9 +105,9 @@ impl View { let labeling = compiled.drawing.label.as_ref()?; let column = &compiled.program.columns[labeling.column as usize]; let style = shown.mark.style(); - let data = self.drawn(&style.data)?; - let source = Source { data, table: data.table(&style.table)?, derived: [None; 2] }; - let (d, t) = table_of(column, &source, &self.data)?; + let data = self.source(&style.data)?; + let source = Source { data, table: data.table(&style.table)?, derived: [None; 2], paged: Vec::new() }; + let (d, t) = table_of(column, &source, &self.sources)?; let text = inner.dictionaries.field(d, &t.name, &column.name).text(code)?.clone(); Some(atlas::run(text, labeling.font.clone(), labeling.size, labeling.max_width)) } @@ -119,10 +119,10 @@ impl View { let Some(labeling) = &compiled.drawing.label else { return 0 }; let column = &compiled.program.columns[labeling.column as usize]; let style = shown.mark.style(); - let Some(data) = self.drawn(&style.data) else { return 0 }; + let Some(data) = self.source(&style.data) else { return 0 }; let Some(table) = data.table(&style.table) else { return 0 }; - let source = Source { data, table, derived: [None; 2] }; - let Some((d, t)) = table_of(column, &source, &self.data) else { return 0 }; + let source = Source { data, table, derived: [None; 2], paged: Vec::new() }; + let Some((d, t)) = table_of(column, &source, &self.sources) else { return 0 }; inner.dictionaries.field(d, &t.name, &column.name).len() as u32 } @@ -295,10 +295,10 @@ impl View { let Some(labeling) = &compiled.drawing.label else { break 'texts None }; let column = &compiled.program.columns[labeling.column as usize]; let style = shown.mark.style(); - let Some(data) = self.drawn(&style.data) else { break 'texts None }; + let Some(data) = self.source(&style.data) else { break 'texts None }; let Some(table) = data.table(&style.table) else { break 'texts None }; - let source = Source { data, table, derived: [None; 2] }; - let Some((d, t)) = table_of(column, &source, &self.data) else { break 'texts None }; + let source = Source { data, table, derived: [None; 2], paged: Vec::new() }; + let Some((d, t)) = table_of(column, &source, &self.sources) else { break 'texts None }; Some((inner.dictionaries.field(d, &t.name, &column.name), labeling)) }; let mut words = Vec::with_capacity(2 * codes.len()); @@ -348,9 +348,25 @@ impl View { frame.label_fade = if self.moves() { (dt / FADE).min(1.0) as f32 } else { 1.0 }; } + /// Starts over the labels of rows whose slots took pages of other rows: their candidates hold + /// nothing placed before, and fade in from nothing. + fn clear_bound(&mut self, encoder: &mut wgpu::CommandEncoder) { + let Some(resources) = &self.resources else { return }; + let words = wgsl::BLOCK_ROWS * wgsl::CANDIDATE_WORDS; + for (i, shown) in self.marks.iter_mut().enumerate() { + let bound = std::mem::take(&mut shown.bound); + let Some(slots) = self.labels.slots(i) else { continue }; + for slot in bound { + let at = u64::from(slots.candidates + slot * words) * 4; + encoder.clear_buffer(resources.labels(), at, Some(u64::from(words) * 4)); + } + } + } + /// Places labels when what they would show changed, and fades them; reads back the texts /// placement asked for. Labels wait for what places them to compile. pub(super) fn place_labels(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner, eye: &wgsl::Eye) { + self.clear_bound(encoder); if !self.texts() { return; } diff --git a/crates/latkit/src/view/lists.rs b/crates/latkit/src/view/lists.rs index 15c5612a..cdb780df 100644 --- a/crates/latkit/src/view/lists.rs +++ b/crates/latkit/src/view/lists.rs @@ -1,6 +1,8 @@ //! Lists on the GPU: each item counts its entries, a scan turns the counts into where each item's //! entries start, and each item writes its entries there. The total lands in the list's header as -//! indirect draw and dispatch arguments, so nothing waits on a readback to know it. +//! indirect draw and dispatch arguments, so nothing waits on a readback to know it. After the +//! segment lists lie the words of the rows each mark drawing a table in part draws: which held row +//! each is, expanded from its runs when they change, so every pass reads a drawn row in one word. use super::View; use super::marks::Shown; @@ -26,6 +28,15 @@ pub(crate) struct Segments { /// Words a segment list keeps per segment: its row, and its distance along. const SEGMENT_WORDS: u32 = 2; +/// Where the words of the rows a mark drawing a table in part draws lie in the lists buffer, +/// `[start, room]` in words, and the stamp of what they were last expanded from: the drawing, where +/// they lie, and the buffer. +#[derive(Default)] +pub(crate) struct DrawnRows { + pub(crate) at: Option<[u32; 2]>, + expanded: u64, +} + /// Where a list lies in the lists buffer, in words. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) struct List { @@ -106,7 +117,38 @@ impl View { at += List::front_words(list.kept, SEGMENT_WORDS) + List::back_words(items); shown.segments.list = Some(list); } - self.segment_words = at; + // Then a word for each row a mark drawing a table in part draws, those that left included. + for shown in &mut self.marks { + if shown.drawing().is_none() { + shown.drawn_rows.at = None; + continue; + } + let items = shown.rows + shown.exits; + let region = match shown.drawn_rows.at { + Some([start, room]) if start == at && room >= items => [start, room], + _ => [at, items + items / 4], + }; + at += region[1]; + shown.drawn_rows.at = Some(region); + } + self.list_words = at; + } + + /// The marks whose drawn rows' words are written again this frame: where their drawing, where + /// the words lie, or the lists buffer changed. + pub(super) fn drawn_rows_due(&mut self) -> Vec { + let generation = self.resources.as_ref().map_or(0, super::resources::Resources::lists_generation); + let mut due = Vec::new(); + for (i, shown) in self.marks.iter_mut().enumerate() { + let Some(at) = shown.drawn_rows.at.filter(|_| shown.drawable()) else { continue }; + let drawn = shown.drawing().map(|d| d.made); + let stamp = stamp_of((at, shown.rows, shown.exits, drawn, generation)); + if stamp != shown.drawn_rows.expanded { + shown.drawn_rows.expanded = stamp; + due.push(i); + } + } + due } /// The marks whose segment lists build again this frame: their list, the lists buffer, or their @@ -121,7 +163,8 @@ impl View { let Some(list) = shown.segments.list.filter(|_| shown.drawable()) else { continue }; let arcs = shown.geodesic.then(|| (&vertices, shown.places_drawn())); let tables = shown.tables.generation(); - let stamp = stamp_of((list.header, list.items, list.kept, tables, generation, arcs)); + let drawn = shown.drawing().map(|d| d.made); + let stamp = stamp_of((list.header, list.items, list.kept, tables, generation, arcs, drawn)); if stamp != shown.segments.built { shown.segments.built = stamp; due.push(i); diff --git a/crates/latkit/src/view/marks.rs b/crates/latkit/src/view/marks.rs index 96546e8e..d3482677 100644 --- a/crates/latkit/src/view/marks.rs +++ b/crates/latkit/src/view/marks.rs @@ -2,10 +2,11 @@ use std::sync::Arc; +use super::drawing::Drawing; use super::rows::Region; use super::sync::Tables; use super::transition::{Begin, Transition}; -use crate::data::Table; +use crate::data::{Table, Version}; use crate::expr::compile::{Deps, Reads}; use crate::gpu::shaders as wgsl; use crate::gpu::{Inner, atlas}; @@ -24,27 +25,81 @@ pub(crate) struct Offsets { pub(crate) palette: u32, pub(crate) port_info: u32, pub(crate) blends: u32, + /// The runs of held rows it draws, `NONE` where it draws every row as itself. + pub(crate) runs: u32, } impl Default for Offsets { fn default() -> Offsets { - Offsets { columns: 0, ranges: 0, code: 0, constants: 0, ends: 0, palette: 0, port_info: wgsl::NONE, blends: 0 } + Offsets { + columns: 0, + ranges: 0, + code: 0, + constants: 0, + ends: 0, + palette: 0, + port_info: wgsl::NONE, + blends: 0, + runs: wgsl::NONE, + } + } +} + +/// What a mark's state holds: rows of a version of its table, as a drawing lays them out or as the +/// table orders them, and the origin of its dataset, whose rows are followed by id when they move. +#[derive(Clone)] +pub(crate) struct Holding { + pub(crate) origin: Version, + pub(crate) table: Table, + pub(crate) drawing: Option>, +} + +/// Where the rows an end of a mark's rows names are held: the graph slot of vertex 0 of the vertex +/// mark drawing their table, and where it holds each page of the order it draws that table in, and +/// each row it keeps whose page it does not hold, by its place in the order. +#[derive(Clone, PartialEq, Debug)] +pub(crate) struct End { + pub(crate) first: u32, + pub(crate) order: u64, + pub(crate) pages: Option>, + pub(crate) kept: Option>, +} + +impl End { + /// Words it takes in a mark's program: two, then where pages and rows kept are held. + fn words(&self) -> usize { + 2 + self.pages.as_ref().map_or(0, |p| 2 + p.len() + 2 * self.kept.as_ref().map_or(0, |k| k.len())) } } +/// The end before end `c` of `ends` that holds its rows where it does, whose words it shares: as the +/// two ends of an edge between vertices of one mark do. +fn shared(ends: &[End], c: usize) -> Option { + let end = &ends[c]; + end.pages.as_ref()?; + ends[..c].iter().position(|e| e == end) +} + pub(crate) struct Shown { pub(crate) mark: Mark, pub(crate) key: Key, pub(crate) compiled: Option, pub(crate) tables: Tables, - /// Rows of the mark's table when last synced, and rows past them that left and fade out. + /// Rows the mark draws, as of the tables last published, and rows past them that left and fade + /// out. pub(crate) rows: u32, pub(crate) exits: u32, /// Rows the region held when the frame began: what moves and freezes. - pub(crate) held: u32, - /// The table whose rows the mark's state holds, for following rows by id when they move, and the - /// origin of its dataset: rows of another dataset are not followed but replaced. - pub(crate) drawn: Option<(crate::data::Version, Table)>, + pub(crate) before: u32, + /// What its state holds, as the tables last published lay it out. + pub(crate) holding: Option, + /// How far the tiles of the table it draws in part have faded in or out, from 0 to 1. + pub(crate) reveal: f32, + /// Slots of held rows given a page of other rows this frame, whose labels start over. + pub(crate) bound: Vec, + /// Each fitted domain fitted over every row of a table drawn in part: the stamp of what it read, + /// and its words. + pub(crate) fitted: Vec<(u64, [u32; 2])>, /// Where its rows sit in every per-row buffer, and where they sat before this frame moved them. pub(crate) region: Region, pub(crate) moved: Option, @@ -63,15 +118,16 @@ pub(crate) struct Shown { /// The program's code and constants with colormap layers and text codes written in. pub(crate) code: Vec<[u32; 4]>, pub(crate) constants: Vec, - /// Graph slot of vertex 0 of each table the program's ends name. - pub(crate) ends: Vec, + /// Where the rows each end of its rows names are held, by end. + pub(crate) ends: Vec, /// A vertex mark's ports, `PORT_WORDS` each, their labels' places in the atlas written in. pub(crate) ports: Vec, /// The mark's region of the program buffer, `[start, capacity]` in words, and where its parts - /// are; `packed` is false until the region holds the current words. + /// are; `packed` is false until the region holds the current words, and `written` what it holds. pub(crate) words: Option<[u32; 2]>, pub(crate) offsets: Offsets, pub(crate) packed: bool, + pub(crate) written: Vec, /// A transition waiting to freeze, the one running, and where its remap words start; and whether /// this frame's froze rows where they moved, so their comets' travel follows them. pub(crate) begin: Option, @@ -82,6 +138,8 @@ pub(crate) struct Shown { pub(crate) query: u32, /// The list numbering a stroke mark's segments when rows have different numbers of them. pub(crate) segments: super::lists::Segments, + /// Where the words of the rows it draws of a table drawn in part lie. + pub(crate) drawn_rows: super::lists::DrawnRows, /// Whether an edge mark follows great circles. pub(crate) geodesic: bool, /// Bumps whenever the mark's state is written, so what derives from it knows to follow; and @@ -105,8 +163,11 @@ impl Shown { tables: Tables::default(), rows: 0, exits: 0, - held: 0, - drawn: None, + before: 0, + holding: None, + reveal: 1.0, + bound: Vec::new(), + fitted: Vec::new(), region: Region::EMPTY, moved: None, domains: 0, @@ -123,12 +184,14 @@ impl Shown { words: None, offsets: Offsets::default(), packed: false, + written: Vec::new(), begin: None, transition: None, remap: 0, carry: false, query: wgsl::NONE, segments: super::lists::Segments::default(), + drawn_rows: super::lists::DrawnRows::default(), geodesic: false, version: 0, place_version: 0, @@ -141,9 +204,71 @@ impl Shown { self.key.kind } - /// Whether the mark has a program, rows, and columns synced at least once. + /// Rows the mark holds evaluated whose data holds them: every per-row word is a held row's. A + /// table drawn as it is holds its rows; one drawn in part a page of rows in each slot. + pub(crate) fn live(&self) -> u32 { + self.drawing().map_or(self.rows, |d| d.live()) + } + + /// The drawing of the table it draws in part, as last published. + pub(crate) fn drawing(&self) -> Option<&Arc> { + self.holding.as_ref().and_then(|h| h.drawing.as_ref()) + } + + /// The row of its table held row `h` holds. + pub(crate) fn source_row(&self, h: u32) -> Option { + match self.drawing() { + Some(drawing) => drawing.source(h), + None => (h < self.rows).then_some(h), + } + } + + /// The held row of row `r` of its table, if it holds it. + pub(crate) fn held_row(&self, r: u32) -> Option { + match self.drawing() { + Some(drawing) => drawing.held_row(r), + None => (r < self.rows).then_some(r), + } + } + + /// Where held row `h` draws among its rows, if it does. + pub(crate) fn item_of(&self, h: u32) -> Option { + match self.drawing() { + Some(drawing) => drawing.item_of(h), + None => (h < self.rows).then_some(h), + } + } + + /// Rows it draws that are not fading out with their tiles. + pub(crate) fn steady_rows(&self) -> u32 { + self.drawing().map_or(self.rows, |d| d.items - d.leaving_items) + } + + /// The runs of held rows it draws, as `lib/drawn.wesl` reads them: how many, then each one's + /// first row drawn, first held row, and fade; the rows that left last, steady. None where it + /// draws every row as itself. + fn runs(&self) -> Vec { + let Some(drawing) = self.drawing() else { return Vec::new() }; + let exits = (self.exits > 0).then_some([drawing.items, drawing.live(), 0]); + let count = drawing.runs.len() + usize::from(exits.is_some()); + let mut words = Vec::with_capacity(1 + count * wgsl::RUN_WORDS as usize); + words.push(count as u32); + for run in &drawing.runs { + words.extend([run.item, run.held, run.fade]); + } + words.extend(exits.into_iter().flatten()); + words + } + + /// Rows the mark holds evaluated, those that left and fade out included. + pub(crate) fn held(&self) -> u32 { + self.live() + self.exits + } + + /// Whether the mark has a program, rows it holds, and columns synced at least once: a mark holding + /// only the rows the ends of edges drawn name evaluates them, and draws none. pub(crate) fn drawable(&self) -> bool { - self.compiled.is_some() && self.rows + self.exits > 0 && self.tables.synced() + self.compiled.is_some() && self.held() > 0 && self.tables.synced() } /// Kind words per row. @@ -281,9 +406,9 @@ impl Shown { } /// Channels `channels` evaluate again on the next frame, over the row blocks `blocks` names, or - /// over every row for `None`. + /// over every row for `None`; naming no block, none does. pub(crate) fn dirty_rows(&mut self, channels: u32, blocks: Option<&[u32]>) { - if channels != 0 { + if channels != 0 && blocks.is_none_or(|b| !b.is_empty()) { self.dirty |= channels; self.blocks.add(blocks); } @@ -309,6 +434,13 @@ impl Shown { if !compiled.program.domains.is_empty() { return self.all_dirty(); } + self.slots_bound(blocks); + } + + /// Slots `blocks` of held rows given pages of rows held before or never: those blocks evaluate + /// every channel, constants included. Their rows' data is as it was, so domains fitted to it hold. + pub(crate) fn slots_bound(&mut self, blocks: &[u32]) { + let Some(compiled) = &self.compiled else { return }; let n = compiled.program.channels.len(); self.dirty_rows(if n >= 32 { u32::MAX } else { (1 << n) - 1 }, Some(blocks)); } @@ -372,9 +504,12 @@ impl Shown { + 2 * compiled.program.ranges.len() + 4 * self.code.len() + self.constants.len() - + self.ends.len() + + (0..self.ends.len()) + .map(|c| if shared(&self.ends, c).is_some() { 2 } else { self.ends[c].words() }) + .sum::() + compiled.drawing.markers.palette.len() - + self.ports.len()) as u32 + + self.ports.len() + + self.drawing().map_or(0, |d| 1 + (d.runs.len() + 1) * wgsl::RUN_WORDS as usize)) as u32 + self.stride() + self.extras() } @@ -427,19 +562,51 @@ impl Shown { let constants = at(out); out.extend(&self.constants); let ends = at(out); - out.extend(&self.ends); + // Two words an end, then each end's pages: how many, then each one's slot; then the rows it + // keeps: how many, then each one's place and held row. Ends holding their rows alike share + // those words. + let mut tables: Vec = Vec::with_capacity(self.ends.len()); + let mut table = ends + 2 * self.ends.len() as u32; + for (c, end) in self.ends.iter().enumerate() { + out.push(end.first); + let at = match (&end.pages, shared(&self.ends, c)) { + (None, _) => wgsl::NONE, + (Some(_), Some(earlier)) => tables[earlier], + (Some(_), None) => { + let at = table; + table += (end.words() - 2) as u32; + at + } + }; + tables.push(at); + out.push(at); + } + for (c, end) in self.ends.iter().enumerate() { + let Some(pages) = &end.pages else { continue }; + if shared(&self.ends, c).is_some() { + continue; + } + out.push(pages.len() as u32); + out.extend(pages.iter()); + let kept = end.kept.as_deref().unwrap_or_default(); + out.push(kept.len() as u32); + out.extend(kept.iter().flatten()); + } let palette = at(out); out.extend(&compiled.drawing.markers.palette); let port_info = if self.ports.is_empty() { wgsl::NONE } else { at(out) }; out.extend(&self.ports); let blends = at(out); out.extend(self.blends()); - self.offsets = Offsets { columns, ranges, code, constants, ends, palette, port_info, blends }; + let runs = if self.drawing().is_some() { at(out) } else { wgsl::NONE }; + out.extend(self.runs()); + self.offsets = Offsets { columns, ranges, code, constants, ends, palette, port_info, blends, runs }; } } /// The row blocks of a mark whose rows evaluate next: every block, or those listed, the blocks of the -/// pages that changed. A block is the `BLOCK_ROWS` rows a page of every column holds. +/// pages that changed. A block is the `BLOCK_ROWS` held rows of a held page; a page of the data holds +/// several. #[derive(Clone, PartialEq, Eq, Debug)] pub(crate) enum Blocks { All, @@ -452,7 +619,15 @@ impl Default for Blocks { } } +/// Blocks a page of the data holds. +const PAGE_BLOCKS: u32 = (crate::data::PAGE_ROWS / wgsl::BLOCK_ROWS as usize) as u32; + impl Blocks { + /// The blocks of the data pages `pages`. + pub(crate) fn of_pages(pages: &[u32]) -> Vec { + pages.iter().flat_map(|&p| p * PAGE_BLOCKS..(p + 1) * PAGE_BLOCKS).collect() + } + /// Adds the blocks `blocks` names, or every block for `None`. fn add(&mut self, blocks: Option<&[u32]>) { match (&mut *self, blocks) { diff --git a/crates/latkit/src/view/mod.rs b/crates/latkit/src/view/mod.rs index 5a629eb6..c3f3b8a7 100644 --- a/crates/latkit/src/view/mod.rs +++ b/crates/latkit/src/view/mod.rs @@ -3,6 +3,7 @@ mod axes; mod camera; mod clock; +mod drawing; mod event; mod fit; mod focus; @@ -39,9 +40,9 @@ pub(crate) use camera::Projected; pub use event::{Cursor, End, Event, Fit, Hit, Phase, Sample, Stats, Trigger, Waiting}; pub use input::{Button, Device, Input, Key, Modifiers}; pub(crate) use resources::{DEPTH, SHADE}; -pub(crate) use wake::Signal; #[cfg(all(feature = "web", target_arch = "wasm32"))] -pub(crate) use {resources::samples, variants::common}; +pub(crate) use variants::common; +pub(crate) use wake::Signal; use camera::Area; use clock::Clock; @@ -103,8 +104,9 @@ pub struct View { scene_version: u64, /// The datasets given, and those the view fetches as the camera asks, by name. sources: Vec, - /// What marks draw of each: the dataset as it is, or as drawn in part. - data: Vec, + /// What the camera chose of each table drawn in part, this frame, and what draws of each. + choices: crate::derive::tiled::Choices, + drawings: crate::data::Keyed<(Name, Name), drawing::Drawn>, /// What the view derives from the data and the scene: tables beside the tables it draws, row for /// row, which marks read like data. derived: Derivations, @@ -158,7 +160,7 @@ pub struct View { probing: Probing, queries: query::Queries, /// Words the marks' segment lists take at the front of the lists buffer. - segment_words: u32, + list_words: u32, labels: labels::Labels, gestures: Gestures, /// The pointer over the view: where, with what keys, and with what; `None` once it left. @@ -189,7 +191,8 @@ impl View { scene: Scene::new(scene.space.clone()), scene_version: 0, sources: Vec::new(), - data: Vec::new(), + choices: crate::derive::tiled::Choices::default(), + drawings: crate::data::Keyed::default(), derived: Derivations::default(), marks: Vec::new(), camera, @@ -221,7 +224,7 @@ impl View { focus: Focus::default(), probing: Probing::default(), queries: query::Queries::default(), - segment_words: 0, + list_words: 0, labels: labels::Labels::default(), gestures: Gestures::default(), pointer: None, @@ -319,7 +322,11 @@ impl View { self.compiling = None; self.answering = None; self.texting = None; - self.ask(&mut gpu.0.lock(), (crate::gpu::FORMAT, resources::samples(self.pixel_ratio)), true); + // Where the GPU's frames have their samples per pixel already, what the scene draws with + // starts compiling now; else its first frame asks. + if let Some(samples) = gpu.samples() { + self.ask(&mut gpu.0.lock(), (crate::gpu::FORMAT, samples), true); + } self.focus.dirty = true; self.gestures.keys.stale(); self.invalidate(); @@ -375,11 +382,6 @@ impl View { self.sources.iter().find(|d| d.name() == name) } - /// What marks draw of the dataset named `name`: it as it is, or as drawn in part. - fn drawn(&self, name: &str) -> Option<&Data> { - self.data.iter().find(|d| d.name() == name) - } - /// Fetches `tiles` as the camera asks for them, kept as the dataset they name, which the view /// owns: marks draw it like any other, [`data`](View::data) reads it, and /// [`remove_data`](View::remove_data) stops it. Tiles of the same name replace these. @@ -392,7 +394,7 @@ impl View { pub fn remove_data(&mut self, name: &str) { self.derived.tiles.remove(name); self.sources.retain(|d| d.name() != name); - self.data.retain(|d| d.name() != name); + self.drawings.retain(|key, _| key.0 != name); let gpu = self.gpu.clone(); let mut inner = gpu.0.lock(); for shown in &mut self.marks { @@ -404,7 +406,7 @@ impl View { shown.compiled = None; shown.rows = 0; shown.exits = 0; - shown.drawn = None; + shown.holding = None; } } drop(inner); @@ -584,25 +586,16 @@ impl View { /// What the view did. pub fn stats(&self) -> Stats { - let inner = self.gpu.0.lock(); let rows = |m: &Shown| { let style = m.mark.style(); self.source(&style.data).and_then(|d| d.table(&style.table)).map_or(0, |t| t.len() as u64) }; let [tiles, tiles_loading] = self.derived.tiles.counts(); - // A table drawn through tiles leaves holes where tiles drawn before were: they draw nothing. - let drawn = |m: &Shown| match &m.drawn { - Some((_, table)) => (table.len() - table.ids.holes()) as u64, - None => u64::from(m.rows), - }; Stats { rows: self.marks.iter().map(rows).sum(), - drawn: self.marks.iter().map(drawn).sum(), + drawn: self.marks.iter().map(|m| u64::from(m.steady_rows())).sum(), tiles, tiles_loading, - uploading: inner.arena.pending(), - resident: inner.arena.resident(), - budget: inner.budget.bytes.min(inner.arena.capacity()), waiting: self.waiting, ..self.stats } @@ -669,8 +662,9 @@ impl View { /// [`ready`](View::ready). pub(crate) fn ready_for(&mut self, format: wgpu::TextureFormat, pixel_ratio: f32, input: bool) -> bool { let gpu = self.gpu.clone(); + let samples = gpu.samples_for(pixel_ratio); let mut inner = gpu.0.lock(); - let draws = self.pipelines_ready(&mut inner, format, resources::samples(pixel_ratio), input); + let draws = self.pipelines_ready(&mut inner, format, samples, input); let answers = self.answers_ready(&inner); let texts = self.texts_ready(&inner); let landed = inner.pipelines.landed(); @@ -723,7 +717,7 @@ impl View { || (self.answers() && (self.probing.due() || self.queries.due())) || self.answering.is_some_and(|seen| landed() != seen) || self.texting.is_some_and(|seen| landed() != seen) - || self.derived.tiled.due() + || self.fading() || self.labels.fading(self.clock.now()) // A mark evaluates once its columns synced: until then, the pages they wait for wake it. || self.marks.iter().any(|m| (m.drawable() && (m.dirty != 0 || m.reduce)) || m.begin.is_some() || m.transition.is_some()) @@ -753,7 +747,6 @@ impl View { || self.regions_dirty || self.fitting.due() || (input && self.answers() && (self.probing.due() || self.queries.due())) - || self.derived.tiled.due() || self.marks.iter().any(|m| m.begin.is_some()) } @@ -776,7 +769,7 @@ impl View { || self.compiling.is_some() || self.answering.is_some() || self.texting.is_some(), - data: !self.marks.iter().all(given) || marks().any(|m| !m.tables.synced() || m.tables.waiting()), + data: !self.marks.iter().all(given) || marks().any(|m| !m.tables.synced() || m.tables.pending()), derived: !self.derived.complete(), labels: !self.labels.complete(), fit: self.fitting.due() || self.fitting.reading(), @@ -789,10 +782,16 @@ impl View { /// and is not counted. pub(crate) fn easing(&self) -> bool { self.rig.easing() + || self.fading() || self.labels.fading(self.clock.now()) || self.marks.iter().any(|m| m.begin.is_some() || m.transition.is_some()) } + /// Whether tiles of a table drawn in part fade in or out. + fn fading(&self) -> bool { + self.drawings.values().any(|d| d.current.as_ref().is_some_and(|c| c.fades)) + } + fn invalidate(&mut self) { self.regions_dirty = true; self.regions.relayout = true; diff --git a/crates/latkit/src/view/passes.rs b/crates/latkit/src/view/passes.rs index 72a02fba..98619bf3 100644 --- a/crates/latkit/src/view/passes.rs +++ b/crates/latkit/src/view/passes.rs @@ -5,9 +5,13 @@ //! overlay; a probe, when one waits, reads what the frame drew through the same geometry. use super::resources::{EyeSlot, MARK_STRIDE}; +use super::sync::{Source, table_of}; use super::{Frame, View}; use crate::Space; +use crate::data::Field; use crate::derive::stamp_of; +use crate::expr::compile::{Deps, Reads}; +use crate::expr::cpu::Rows; use crate::gpu::Inner; use crate::gpu::pipelines::Variant; use crate::gpu::readback::Readback; @@ -185,20 +189,23 @@ struct Work { /// Marks whose comets carry their travel over to rows a transition moved, and that travel. carried: Vec, travelled: Vec, + /// Marks whose drawn rows' words are expanded from their runs again. + expanded: Vec, } /// Dispatches a workgroup for every `WORKGROUP` of `items`, if there are any; past one dimension's -/// workgroups, blocks of `BLOCK_ROWS` items along the second, as `item` in `lib/types.wesl` reads them. +/// workgroups, runs of `DISPATCH_ROWS` items along the second, as `item` in `lib/types.wesl` reads +/// them. pub(super) fn dispatch(pass: &mut wgpu::ComputePass<'_>, items: u32) { let groups = items.div_ceil(WORKGROUP); if groups > MAX_GROUPS { - pass.dispatch_workgroups(wgsl::BLOCK_ROWS / WORKGROUP, items.div_ceil(wgsl::BLOCK_ROWS), 1); + pass.dispatch_workgroups(wgsl::DISPATCH_ROWS / WORKGROUP, items.div_ceil(wgsl::DISPATCH_ROWS), 1); } else if groups > 0 { pass.dispatch_workgroups(groups, 1, 1); } } -/// Dispatches over `blocks` listed row blocks of a mark of `rows` rows: a block's rows wide. +/// Dispatches over `blocks` listed row blocks of a mark of `rows` held rows: a block's rows wide. fn dispatch_blocks(pass: &mut wgpu::ComputePass<'_>, rows: u32, blocks: u32) { let wide = rows.min(wgsl::BLOCK_ROWS).div_ceil(WORKGROUP); if wide > 0 && blocks > 0 { @@ -211,7 +218,41 @@ pub(super) fn offset(index: usize) -> u32 { (index as u64 * MARK_STRIDE) as u32 } +/// `input`'s least and greatest value over `rows`, as `lib/reduce.wesl` keeps them: the complement +/// of the least's ordered bits, then the greatest's; nothing where no row has one. +fn fit(rows: &Rows<'_>, input: &crate::Expr) -> [u32; 2] { + let ordered = |x: f32| { + let b = x.to_bits(); + if b & 0x8000_0000 != 0 { !b } else { b | 0x8000_0000 } + }; + let (mut least, mut most) = (f32::INFINITY, f32::NEG_INFINITY); + for x in rows.numbers(input, |v| v as f32) { + if x.is_finite() { + (least, most) = (least.min(x), most.max(x)); + } + } + if least > most { [0, 0] } else { [!ordered(least), ordered(most)] } +} + impl super::marks::Shown { + /// Whether its fitted domains fit over every row of the table it draws in part, on the CPU: it + /// draws a table in part, and they read only its own data, nothing the view shows or derives. + pub(crate) fn fits_data(&self) -> bool { + let Some(compiled) = &self.compiled else { return false }; + let program = &compiled.program; + let viewed = Deps::TIME | Deps::POINTER | Deps::FOCUS | Deps::CAMERA | Deps::DOMAIN; + let derived = |reads: &Reads| { + program + .columns + .iter() + .enumerate() + .any(|(c, column)| column.derived && reads.columns & Reads::column(c) != 0) + }; + self.drawing().is_some() + && !program.domains.is_empty() + && program.domains.iter().all(|reads| !reads.deps.intersects(viewed) && !derived(reads)) + } + /// Segments of each row's path, for strokes whose rows all have as many. pub(crate) fn segments_per_row(&self) -> u32 { if self.compiled.is_none() { @@ -296,12 +337,15 @@ impl View { let into = settling.filter(|_| shown.transition.is_some()).unwrap_or(state); pass.set_pipeline(&inner.pipelines.compute_pipeline(device, Variant::channels(shown.reads()))); pass.set_bind_group(1, group, &[offset(i)]); - if shown.reduce && shown.rows > 0 { + let rows = shown.live(); + // A domain changing evaluates every block, so the reductions run over the blocks listed; + // a table drawn in part fits domains reading only its data on the CPU, over every row. + if shown.reduce && rows > 0 && !shown.fits_data() { + debug_assert_eq!(shown.blocks, super::marks::Blocks::All, "a domain changing changes every row"); pass.set_bind_group(2, into, &[step(wgsl::STEP_REDUCE)]); - dispatch(pass, shown.rows); + dispatch_blocks(pass, rows, shown.block_count); } pass.set_bind_group(2, into, &[step(wgsl::STEP_CHANNELS)]); - let rows = shown.rows; dispatch_blocks(pass, rows, shown.block_count); let blocks = shown.blocks.of(rows); let evaluated = blocks.iter().map(|&b| u64::from((rows - b * wgsl::BLOCK_ROWS).min(wgsl::BLOCK_ROWS))); @@ -325,16 +369,29 @@ impl View { ) -> Option { let gpu = self.gpu.clone(); let device = &gpu.0.device; - let Work { evaluated: work, eased, segments, faces, measured, carried, travelled } = self.work(measure, travel); - if [&work, &eased, &segments, &faces, &measured, &carried, &travelled].iter().all(|marks| marks.is_empty()) { + let Work { evaluated: work, eased, segments, faces, measured, carried, travelled, expanded } = + self.work(measure, travel); + let all = [&work, &eased, &segments, &faces, &measured, &carried, &travelled, &expanded]; + if all.iter().all(|marks| marks.is_empty()) { return None; } + // Domains a table drawn in part fits over all its rows, on the CPU; the GPU fits the rest. + let mut fitted: Vec<(usize, Vec)> = Vec::new(); + for &i in &work { + if self.marks[i].reduce && self.marks[i].fits_data() { + fitted.push((i, self.data_domains(i))); + } + } let resources = self.resources.as_mut().expect("created before passes"); for &i in &work { let shown = &self.marks[i]; - if shown.reduce { - let bytes = u64::from(shown.reductions()) * 8; - encoder.clear_buffer(resources.domains(), u64::from(shown.domains) * 8, Some(bytes)); + if !shown.reduce { + continue; + } + let at = u64::from(shown.domains) * 8; + match fitted.iter().find(|(m, _)| *m == i) { + Some((_, words)) => gpu.0.queue.write_buffer(resources.domains(), at, bytemuck::cast_slice(words)), + None => encoder.clear_buffer(resources.domains(), at, Some(u64::from(shown.reductions()) * 8)), } } let extent_bytes = self.marks.len() as u64 * u64::from(wgsl::EXTENT_WORDS) * 4; @@ -353,7 +410,8 @@ impl View { .flatten(); let normals_group = (!faces.is_empty()).then(|| resources.pass(device, inner, Shader::Normals)); let bounds_group = (!measured.is_empty()).then(|| resources.pass(device, inner, Shader::Bounds)); - let lists_group = (!segments.is_empty()).then(|| resources.pass(device, inner, Shader::Lists)); + let lists_group = + (!segments.is_empty() || !expanded.is_empty()).then(|| resources.pass(device, inner, Shader::Lists)); for &i in &segments { let list = self.marks[i].segments.list.expect("listed"); gpu.0.queue.write_buffer( @@ -368,6 +426,11 @@ impl View { timestamp_writes: None, }); pass.set_bind_group(0, &groups.view, &[EyeSlot::Main.offset()]); + // Each drawn row's word first: every pass after reads it. + if let Some(group) = &lists_group { + pass.set_bind_group(2, group, &[]); + self.expand_runs(&mut pass, inner, &groups.mark, &expanded); + } pass.set_bind_group(2, &evaluate_group, &[super::resources::step(wgsl::STEP_CHANNELS)]); self.every_row(&mut pass, inner, &groups.mark, "carry", &carried); self.evaluate(&mut pass, inner, &groups.mark, (&evaluate_group, settling_group.as_ref()), &work); @@ -414,7 +477,7 @@ impl View { fn work(&mut self, measure: bool, travel: bool) -> Work { for shown in &mut self.marks { // A mark without rows has nothing to evaluate. - if shown.rows + shown.exits == 0 && shown.tables.synced() { + if shown.held() == 0 && shown.tables.synced() { shown.dirty = 0; shown.reduce = false; } @@ -454,7 +517,8 @@ impl View { let flowing = |i: &usize| self.marks[*i].drawable() && self.marks[*i].mark.flows(); let carried = (0..self.marks.len()).filter(|i| self.marks[*i].carry && flowing(i)).collect(); let travelled = (0..self.marks.len()).filter(|i| travel && flowing(i)).collect(); - Work { evaluated: work, eased, segments, faces, measured, carried, travelled } + let expanded = self.drawn_rows_due(); + Work { evaluated: work, eased, segments, faces, measured, carried, travelled, expanded } } /// Lists the row blocks each mark evaluates this frame where its uniform finds them: every block of @@ -463,7 +527,7 @@ impl View { let mut words = Vec::new(); for shown in &mut self.marks { let evaluates = shown.drawable() && (shown.dirty != 0 || shown.reduce); - let blocks = if evaluates { shown.blocks.of(shown.rows) } else { Vec::new() }; + let blocks = if evaluates { shown.blocks.of(shown.live()) } else { Vec::new() }; shown.block = words.len() as u32; shown.block_count = blocks.len() as u32; words.extend(blocks); @@ -473,7 +537,26 @@ impl View { resources.write_blocks(&gpu.0.device, &gpu.0.queue, &words); } - /// Runs the expression machine's entry point `entry` over every row and exit of each of `marks`. + /// Writes each drawn row's word of each of `marks`, those that left included, from its runs. + fn expand_runs( + &self, + pass: &mut wgpu::ComputePass<'_>, + inner: &mut Inner, + mark: &wgpu::BindGroup, + marks: &[usize], + ) { + if marks.is_empty() { + return; + } + pass.set_pipeline(&inner.pipelines.compute(&self.gpu.0.device, Shader::Lists, "expand_runs")); + for &i in marks { + pass.set_bind_group(1, mark, &[offset(i)]); + dispatch(pass, self.marks[i].rows + self.marks[i].exits); + } + } + + /// Runs the expression machine's entry point `entry` over every held row of each of `marks`, those + /// that left included. fn every_row( &self, pass: &mut wgpu::ComputePass<'_>, @@ -488,7 +571,7 @@ impl View { pass.set_pipeline(&inner.pipelines.compute(&self.gpu.0.device, Shader::Evaluate, entry)); for &i in marks { pass.set_bind_group(1, mark, &[offset(i)]); - dispatch(pass, self.marks[i].rows + self.marks[i].exits); + dispatch(pass, self.marks[i].held()); } } @@ -515,6 +598,40 @@ impl View { } } + /// The words of mark `i`'s fitted domains as `lib/reduce.wesl` keeps them, each fitted over every + /// row of the table it draws in part, so a domain stays put however little of the table the + /// camera shows. Each is kept by what it read: a camera move fits none again. + fn data_domains(&mut self, i: usize) -> Vec { + let shown = &self.marks[i]; + let Some(compiled) = &shown.compiled else { return Vec::new() }; + let program = &compiled.program; + let style = shown.mark.style(); + let table = self.source(&style.data).and_then(|d| Some((d, d.table(&style.table)?))); + let Some((data, table)) = table else { return vec![0; 2 * program.domains.len()] }; + let source = Source { data, table, derived: self.derived.beside(&style.data, &style.table), paged: Vec::new() }; + let rows = Rows::new(table, &self.sources).at(self.at); + let mut fitted = Vec::with_capacity(program.domains.len()); + for (d, (reads, input)) in program.domains.iter().zip(&program.fitted).enumerate() { + let versions: Vec> = (0..program.columns.len()) + .filter(|&c| reads.columns & Reads::column(c) != 0) + .map(|c| { + let column = &program.columns[c]; + table_of(column, &source, &self.sources)?.1.field_named(&column.name).map(Field::version) + }) + .collect(); + let sampled = reads.deps.intersects(Deps::SAMPLES).then_some(self.at.to_bits()); + let stamp = stamp_of((versions, table.structure, sampled)); + let words = match shown.fitted.get(d) { + Some(&(kept, words)) if kept == stamp => words, + _ => fit(&rows, input), + }; + fitted.push((stamp, words)); + } + let words = fitted.iter().flat_map(|(_, w)| *w).collect(); + self.marks[i].fitted = fitted; + words + } + /// Whether face normals must be summed again: a face mark or a vertex mark wrote its state since /// they were, or the globe turned under them. fn normals_due(&mut self) -> bool { @@ -562,7 +679,7 @@ impl View { if self.signal.transparent() { [0.0; 4] } else { self.scene.theme.background.to_array().map(f64::from) }; let plot = matches!(self.scene.space, Space::Plot(_)); let scissor = scissor(self.area(), frame); - let samples = super::resources::samples(frame.pixel_ratio); + let samples = gpu.samples_for(frame.pixel_ratio); let resources = self.resources.as_mut().expect("created before passes"); let (color, depth) = resources.targets(device, frame.size, frame.format, samples); let groups = resources.groups(&gpu.0, inner); diff --git a/crates/latkit/src/view/plan.rs b/crates/latkit/src/view/plan.rs index 79b7c88d..f08da0fc 100644 --- a/crates/latkit/src/view/plan.rs +++ b/crates/latkit/src/view/plan.rs @@ -10,7 +10,7 @@ use std::hash::{Hash, Hasher}; -use rustc_hash::{FxHashMap, FxHashSet, FxHasher}; +use rustc_hash::{FxHashMap, FxHasher}; use super::View; use super::marks::Shown; @@ -179,7 +179,8 @@ impl View { let left = (share - whole).max(share / 4.0); let each = left / (tiled.len() + usize::from(traced > 0.0)).max(1) as f64; for key in &tiled { - let per_row = costs[key].per_row.max(1.0) + costs[key].geometry / costs[key].rows.max(1) as f64; + // A row drawn in part takes a word more: which held row it is. + let per_row = costs[key].per_row.max(1.0) + 4.0 + costs[key].geometry / costs[key].rows.max(1) as f64; let rows = (each / per_row).min(f64::from(u32::MAX)) as u32; plans.get_mut(key).expect("listed").draw = Draw::Tiles { rows: rows.max(LEAST) }; } @@ -195,7 +196,6 @@ impl View { let mut plans = Plans::default(); let mut costs: Keyed<(Name, Name), Cost> = Keyed::default(); let mut shared: FxHashMap = FxHashMap::default(); - let mut joined: Vec<((Name, Name), Name)> = Vec::new(); for shown in &self.marks { let style = shown.mark.style(); let Some(table) = self.source(&style.data).and_then(|d| d.table(&style.table)) else { continue }; @@ -204,8 +204,6 @@ impl View { draw: Draw::Whole, priority: None, place: Place::Nowhere, - reads: (!matches!(space, Space::Diagram(_))).then(FxHashSet::default), - traced: FxHashSet::default(), }); if planned.priority.is_none() { planned.priority.clone_from(&style.priority); @@ -213,24 +211,6 @@ impl View { if placing && planned.place == Place::Nowhere { planned.place = place_of(&shown.mark, table); } - if let Some(compiled) = &shown.compiled { - let traces = compiled.program.columns.iter().filter(|c| c.trace && c.of.is_none() && !c.derived); - planned.traced.extend(traces.map(|c| c.name.clone())); - } - if let (Some(reads), Some(compiled)) = (&mut planned.reads, &shown.compiled) { - for column in &compiled.program.columns { - match &column.of { - None if !column.derived => { - reads.insert(column.name.clone()); - } - Some(of) => joined.push((of.clone(), column.name.clone())), - None => {} - } - } - if let Some(field) = geometry_of(&shown.mark) { - reads.insert(field); - } - } let cost = costs.entry(key).or_insert(Cost { rows: table.len(), per_row: 0.0, geometry: 0.0, traced: 0.0 }); cost.per_row += row_bytes(shown); cost.geometry += geometry_bytes(shown, table); @@ -241,11 +221,6 @@ impl View { *once = bytes; } } - for (key, field) in joined { - if let Some(reads) = plans.get_mut(&key).and_then(|p| p.reads.as_mut()) { - reads.insert(field); - } - } Costs { whole: plans, tables: costs, shared } } diff --git a/crates/latkit/src/view/probe.rs b/crates/latkit/src/view/probe.rs index c0daf779..27ef4236 100644 --- a/crates/latkit/src/view/probe.rs +++ b/crates/latkit/src/view/probe.rs @@ -1,14 +1,20 @@ //! Probes: what lies under a point, answered by a compute pass over what the frame drew and read back //! a frame or two later: the nearest row, and the nearest depth drawn. One probe is in flight at a -//! time; a newer point waits for it. +//! time; a newer point waits for it. An answer decodes against what its frame drew, whatever changed +//! before it landed: the rows its marks held, and the camera it drew through. +use std::sync::Arc; + +use super::Projected; use super::event::{Hit, Sample}; use super::gestures::Gesture; -use super::marks::Shown; +use super::marks::{Holding, Shown}; +use super::sync::Plan; use super::{Device, View}; use crate::gpu::readback::Readback; use crate::gpu::shaders as wgsl; -use crate::{Camera, Row}; +use crate::scene::mark::MarkKind; +use crate::{Camera, Name, Row}; /// A hit, with the depth it lies at for dragging, and for an edge which end it is nearer: 0 its /// first, 1 its second. @@ -27,11 +33,85 @@ pub(crate) struct Answer { pub(crate) under: f32, } +/// What a frame drew, as an answer about it reads it: each mark's rows as it held them, and the camera +/// the frame drew through. +#[derive(Clone)] +pub(crate) struct Drew { + marks: Arc<[Held]>, + projected: Projected, +} + +/// What a mark held as its frame drew it: its dataset, table, and name, whether its rows are vertices +/// and selection picks them, the rows its state held and how, its ports, and for a line tracing a +/// field, the field and what its trace drew. +struct Held { + data: Name, + table: Name, + name: Option, + vertices: bool, + selects: bool, + holding: Option, + ports: Vec, + traced: Option<(Name, Plan)>, +} + +impl Drew { + /// The row mark `i` held as held row `held`, and its place in its table. + pub(super) fn row(&self, i: usize, held: u32) -> Option<(Row, usize)> { + let mark = self.marks.get(i)?; + let holding = mark.holding.as_ref()?; + let r = match &holding.drawing { + Some(drawing) => drawing.source(held)?, + None => (held < holding.table.len() as u32).then_some(held)?, + }; + let id = holding.table.ids.id(r as usize)?; + Some((Row { data: mark.data.clone(), table: mark.table.clone(), id }, r as usize)) + } + + /// The name of mark `i`. + pub(super) fn name(&self, i: usize) -> Option { + self.marks.get(i)?.name.clone() + } + + /// Whether a box's selection picks mark `i`'s rows. + pub(super) fn selects(&self, i: usize) -> bool { + self.marks.get(i).is_some_and(|m| m.selects) + } + + /// The port of a vertex mark `i` its probe reached: its number from 1, or 0 for the marker. + pub(super) fn port(&self, i: usize, number: u32) -> Option { + let mark = self.marks.get(i)?; + if !mark.vertices || number == 0 { + return None; + } + mark.ports.get(number as usize - 1).cloned() + } + + /// The sample point `point` of row `row`'s traced path stands for, on mark `i` tracing a field. + pub(super) fn sample(&self, i: usize, row: usize, point: u32) -> Option { + let mark = self.marks.get(i)?; + let (field, plan) = mark.traced.as_ref()?; + let (level, index, side) = plan.entry(point)?; + let table = &mark.holding.as_ref()?.table; + let crate::data::Field::Sampled(samples, _) = table.field_named(field)? else { return None }; + // A bucket's points stand for its middle frame, its least or greatest value. + let span = crate::data::bucket_frames(level); + let frame = index * span + span / 2; + let value = samples.entry(row, level, side, index)?; + Some(Sample { frame, at: samples.drawn_at(level, index)?, value }) + } + + /// The position `point` on screen at `depth` stands for, as the frame's camera saw it. + pub(super) fn position(&self, point: [f32; 2], depth: f64) -> [f64; 3] { + self.projected.position(self.projected.unproject(point.map(f64::from), depth)) + } +} + #[derive(Default)] pub(crate) struct Probing { /// The next point to probe. pub(crate) wanted: Option<[f32; 2]>, - reading: Option<([f32; 2], Readback)>, + reading: Option<([f32; 2], Readback, Drew)>, /// The latest answer and the point it was asked for. pub(crate) latest: Option<([f32; 2], Answer)>, } @@ -84,77 +164,46 @@ impl View { } pub(super) fn probing(&mut self, point: [f32; 2], readback: Readback) { - self.probing.reading = Some((point, readback)); + self.probing.reading = Some((point, readback, self.drew())); + } + + /// What this frame drew, as an answer about it reads it. + pub(super) fn drew(&self) -> Drew { + let marks: Vec = self + .marks + .iter() + .map(|shown| { + let style = shown.mark.style(); + let ports = shown.compiled.as_ref().map(|c| c.drawing.ports.iter().map(|(name, ..)| name.clone())); + let traced = match &shown.mark { + crate::Mark::Lines(lines) => shown.tables.plan().map(|plan| (lines.traced(), plan.clone())), + _ => None, + }; + Held { + data: style.data.clone(), + table: style.table.clone(), + name: style.name.clone(), + vertices: shown.kind() == MarkKind::Vertices, + selects: style.interaction.select, + holding: shown.holding.clone(), + ports: ports.map(Iterator::collect).unwrap_or_default(), + traced, + } + }) + .collect(); + Drew { marks: marks.into(), projected: self.projected() } } /// Hands a landed probe's answer to hover and gestures. pub(super) fn settle_probe(&mut self) { - let Some((point, readback)) = &self.probing.reading else { return }; + let Some((point, readback, _)) = &self.probing.reading else { return }; let Some(bytes) = readback.take() else { return }; let point = *point; - self.probing.reading = None; - let answer = bytes.map_or_else(|_| Answer::default(), |bytes| self.answer(bytemuck::cast_slice(&bytes))); + let (_, _, drew) = self.probing.reading.take().expect("reading"); + let answer = bytes.map_or_else(|_| Answer::default(), |bytes| answer(bytemuck::cast_slice(&bytes), &drew)); self.landed(point, answer); } - /// What the probe's words say. - fn answer(&self, words: &[u32]) -> Answer { - let word = |i: u32| words.get(i as usize).copied().unwrap_or(0); - Answer { hit: self.hit(&word), under: f32::from_bits(word(wgsl::PROBE_UNDER)) } - } - - fn hit(&self, word: &impl Fn(u32) -> u32) -> Option { - let key = word(wgsl::PROBE_KEY); - if key == wgsl::NONE || word(wgsl::PROBE_INSTANCE) == 0 { - return None; - } - let row = word(wgsl::PROBE_ROW); - let mark = 255 - (key & 255) as usize; - let shown = self.marks.get(mark)?; - let style = shown.mark.style(); - let id = self.drawn(&style.data)?.table(&style.table)?.ids.id(row as usize)?; - let depth = f32::from_bits(word(wgsl::PROBE_DEPTH)); - let point = [f32::from_bits(word(wgsl::PROBE_X)), f32::from_bits(word(wgsl::PROBE_Y))]; - let view = self.projected(); - let position = view.position(view.unproject(point.map(f64::from), f64::from(depth))); - let hit = Hit { - row: Row { data: style.data.clone(), table: style.table.clone(), id }, - mark: style.name.clone(), - index: mark, - point, - position, - distance: (key >> 8) as f32 / 64.0, - port: self.port(mark, word(wgsl::PROBE_SEGMENT)), - sample: self.sample(mark, row, word(wgsl::PROBE_SEGMENT)), - }; - Some(Probed { hit, depth, end: word(wgsl::PROBE_SEGMENT) }) - } - - /// The port a vertex mark's probe reached: its number from 1, or 0 for the marker. - pub(super) fn port(&self, mark: usize, number: u32) -> Option { - let shown = self.marks.get(mark)?; - if shown.kind() != crate::scene::mark::MarkKind::Vertices || number == 0 { - return None; - } - let compiled = shown.compiled.as_ref()?; - compiled.drawing.ports.get(number as usize - 1).map(|(name, ..)| name.clone()) - } - - /// The sample point `point` of a traced row's path stands for, on a mark tracing a field. - pub(super) fn sample(&self, mark: usize, row: u32, point: u32) -> Option { - let shown = self.marks.get(mark)?; - let crate::Mark::Lines(lines) = &shown.mark else { return None }; - let (level, index, side) = shown.tables.plan()?.entry(point)?; - let style = shown.mark.style(); - let table = self.drawn(&style.data)?.table(&style.table)?; - let crate::data::Field::Sampled(samples, _) = table.field_named(&lines.traced())? else { return None }; - // A bucket's points stand for its middle frame, its least or greatest value. - let span = crate::data::bucket_frames(level); - let frame = index * span + span / 2; - let value = samples.entry(row as usize, level, side, index)?; - Some(Sample { frame, at: samples.drawn_at(level, index)?, value }) - } - /// The point gestures move about under `point`: what the frame drew there, else the level the /// camera looks at, else a point as far away as the camera's center. pub(super) fn anchor(&self, point: [f32; 2], answer: Option<&Answer>) -> [f64; 3] { @@ -178,6 +227,35 @@ impl View { } } +/// What a probe's words say of what its frame drew. +fn answer(words: &[u32], drew: &Drew) -> Answer { + let word = |i: u32| words.get(i as usize).copied().unwrap_or(0); + Answer { hit: hit(&word, drew), under: f32::from_bits(word(wgsl::PROBE_UNDER)) } +} + +fn hit(word: &impl Fn(u32) -> u32, drew: &Drew) -> Option { + let key = word(wgsl::PROBE_KEY); + if key == wgsl::NONE || word(wgsl::PROBE_INSTANCE) == 0 { + return None; + } + let mark = 255 - (key & 255) as usize; + // The probe names a held row: the row of its table it held. + let (row, at) = drew.row(mark, word(wgsl::PROBE_ROW))?; + let depth = f32::from_bits(word(wgsl::PROBE_DEPTH)); + let point = [f32::from_bits(word(wgsl::PROBE_X)), f32::from_bits(word(wgsl::PROBE_Y))]; + let hit = Hit { + row, + mark: drew.name(mark), + index: mark, + point, + position: drew.position(point, f64::from(depth)), + distance: (key >> 8) as f32 / 64.0, + port: drew.port(mark, word(wgsl::PROBE_SEGMENT)), + sample: drew.sample(mark, at, word(wgsl::PROBE_SEGMENT)), + }; + Some(Probed { hit, depth, end: word(wgsl::PROBE_SEGMENT) }) +} + impl super::marks::Shown { /// Whether probes reach this mark's rows. pub(crate) fn probed(&self) -> bool { diff --git a/crates/latkit/src/view/program.rs b/crates/latkit/src/view/program.rs index 982c1e12..33e46495 100644 --- a/crates/latkit/src/view/program.rs +++ b/crates/latkit/src/view/program.rs @@ -1,6 +1,7 @@ //! The program buffer: each mark's descriptors, tables, ranges, code, constants, ends, and ports in a -//! region of its own, rewritten alone when only that mark changed. A region that outgrows its room -//! moves to the end of the buffer; when the end passes the buffer, every region is laid out again. +//! region of its own, rewritten alone when only that mark changed, and then only where its words +//! changed. A region that outgrows its room moves to the end of the buffer; when the end passes the +//! buffer, every region is laid out again. use super::{Event, View}; use crate::gpu::Inner; @@ -12,11 +13,27 @@ pub(crate) struct Regions { pub(crate) relayout: bool, } +/// Words alike between two that changed, past which they are written apart. +const GAP: usize = 16; + /// The room a region of `len` words gets, so it can grow a little in place. fn room(len: u32) -> u32 { len + len / 4 + 16 } +/// The spans of `words` that differ from `was`, what the same place held, as `[start, end)`: those +/// closer than `GAP` words written as one. +fn changed(was: &[u32], words: &[u32]) -> Vec<[usize; 2]> { + let mut spans: Vec<[usize; 2]> = Vec::new(); + for i in (0..words.len()).filter(|&i| was.get(i) != Some(&words[i])) { + match spans.last_mut() { + Some(span) if i - span[1] < GAP => span[1] = i + 1, + _ => spans.push([i, i + 1]), + } + } + spans +} + impl View { /// Writes every mark whose words changed into its region of the program buffer. pub(super) fn pack(&mut self, inner: &mut Inner) { @@ -47,6 +64,7 @@ impl View { _ if self.regions.end + room(len) <= capacity => { let start = self.regions.end; shown.words = Some([start, room(len)]); + shown.written.clear(); self.regions.end += room(len); start } @@ -57,7 +75,11 @@ impl View { }; let mut words = Vec::with_capacity(len as usize); shown.pack(start, &mut words); - gpu.0.queue.write_buffer(resources.program(), u64::from(start) * 4, bytemuck::cast_slice(&words)); + for [from, to] in changed(&shown.written, &words) { + let at = u64::from(start) * 4 + from as u64 * 4; + gpu.0.queue.write_buffer(resources.program(), at, bytemuck::cast_slice(&words[from..to])); + } + shown.written = words; shown.packed = true; } } @@ -69,6 +91,7 @@ impl View { let start = words.len() as u32; let len = shown.packed_len(); shown.pack(start, &mut words); + shown.written = words[start as usize..].to_vec(); words.resize((start + room(len)) as usize, 0); shown.words = Some([start, room(len)]); shown.packed = true; diff --git a/crates/latkit/src/view/query.rs b/crates/latkit/src/view/query.rs index b18b88c0..4219fa5b 100644 --- a/crates/latkit/src/view/query.rs +++ b/crates/latkit/src/view/query.rs @@ -1,6 +1,6 @@ //! Queries: every row within reach of a point, every row in a box, and where a row lies on screen, //! answered on the GPU by each mark's probe program through the geometry the frame drew, and read back -//! a frame or two later. What a query finds of each mark follows the probe's words: the hits it keeps, +//! a frame or two later, then decoded against what that frame drew. What a query finds of each mark follows the probe's words: the hits it keeps, //! after how many it found and how many it keeps, or a bit per row. Hits land in any order; they are //! told in one, and a mark that found more than it kept asks again with room for them all. One query //! runs at a time; the rest wait their turn. @@ -11,6 +11,7 @@ use rustc_hash::FxHashSet; use super::View; use super::event::{Hit, Trigger}; +use super::probe::Drew; use super::resources::{EyeSlot, step}; use crate::gpu::Inner; use crate::gpu::readback::Readback; @@ -82,11 +83,12 @@ struct Running { words: u32, } -/// A query whose answer the GPU is copying back. +/// A query whose answer the GPU is copying back, and what its frame drew. struct Reading { ask: Ask, found: Vec, readback: Readback, + drew: Drew, } #[derive(Default)] @@ -134,21 +136,15 @@ impl View { self.wake(); } - /// The index of `row` in mark `i`'s table. + /// Where mark `i` draws `row` among the rows it draws, if it does. pub(super) fn row_index(&self, i: usize, row: &Row) -> Option { - let style = self.marks.get(i)?.mark.style(); + let shown = self.marks.get(i)?; + let style = shown.mark.style(); if style.data != row.data || style.table != row.table { return None; } - let index = self.drawn(&style.data)?.table(&style.table)?.row(row.id.as_ref())?; - (index < self.marks[i].rows as usize).then_some(index as u32) - } - - /// The row at `index` of mark `i`'s table. - pub(super) fn row_at(&self, i: usize, index: u32) -> Option { - let style = self.marks.get(i)?.mark.style(); - let id = self.drawn(&style.data)?.table(&style.table)?.ids.id(index as usize)?; - Some(Row { data: style.data.clone(), table: style.table.clone(), id }) + let index = self.source(&style.data)?.table(&style.table)?.row(row.id.as_ref())?; + shown.item_of(shown.held_row(index as u32)?).filter(|&item| item < shown.rows) } /// Takes the next query for this frame when none runs, makes room for what it finds past the @@ -176,7 +172,7 @@ impl View { let mut at = wgsl::PROBE_WORDS; let mut found = Vec::with_capacity(marks.len()); for i in marks { - let (room, taken) = ask.room(self.marks[i].live_instances(), self.marks[i].rows); + let (room, taken) = ask.room(self.marks[i].live_instances(), self.marks[i].live()); self.marks[i].query = at; found.push(Found { mark: i, at, room }); at += taken; @@ -225,14 +221,14 @@ impl View { _ => (start, u64::from(words.max(1)) * 4), }; let readback = Readback::new(&inner.readbacks, device, encoder, &probe, at, bytes, &self.waker); - self.queries.reading = Some(Reading { ask, found, readback }); + self.queries.reading = Some(Reading { ask, found, readback, drew: self.drew() }); } /// Answers a query whose readback landed. pub(super) fn settle_query(&mut self) { let Some(reading) = &self.queries.reading else { return }; let Some(bytes) = reading.readback.take() else { return }; - let Reading { ask, found, .. } = self.queries.reading.take().expect("reading"); + let Reading { ask, found, drew, .. } = self.queries.reading.take().expect("reading"); let words: Vec = match bytes { Ok(bytes) => bytemuck::cast_slice(&bytes).to_vec(), Err(error) => { @@ -250,19 +246,19 @@ impl View { let most = found.iter().map(|f| words_at(&words, f.at, 1).first().copied().unwrap_or(0)).max(); match most.filter(|&most| most > room) { Some(room) => self.queries.waiting.push_front(Ask::Hits { point, radius, room, reply }), - None => reply.resolve(Ok(self.hits(&found, &words))), + None => reply.resolve(Ok(hits(&found, &words, &drew))), } } Ask::Boxed { trigger, .. } => { let mut rows = Vec::new(); let mut seen = FxHashSet::default(); - for f in found.iter().filter(|f| self.marks[f.mark].mark.style().interaction.select) { + for f in found.iter().filter(|f| drew.selects(f.mark)) { for (k, &bits) in words_at(&words, f.at, f.room.div_ceil(32)).iter().enumerate() { let mut bits = bits; while bits != 0 { let index = k as u32 * 32 + bits.trailing_zeros(); bits &= bits - 1; - if let Some(row) = self.row_at(f.mark, index) + if let Some((row, _)) = drew.row(f.mark, index) && seen.insert(row.clone()) { rows.push(row); @@ -280,46 +276,46 @@ impl View { } self.wake(); } +} - fn hits(&self, found: &[Found], words: &[u32]) -> Vec { - let view = self.projected(); - // Nearest first, the mark drawn last first among equals, and a port before its marker; then - // by row and where it lies, so the order never depends on which hit landed first. Each row - // answers once, where it is nearest. - let mut kept: Vec<(usize, &[u32; wgsl::HIT_WORDS as usize])> = found - .iter() - .flat_map(|f| { - let count = words_at(words, f.at, 1).first().map_or(0, |&n| n.min(f.room)); - let entries = words_at(words, f.at + 2, count * wgsl::HIT_WORDS); - entries.as_chunks().0.iter().map(move |entry| (f.mark, entry)) - }) - .collect(); - // The last keys first, as each sort keeps the order of ties. - let key = |high: u32, low: u32| u128::from(crate::order::pair(high, low)); - crate::order::by_wide_key(&mut kept, |&(_, entry)| key(entry[3], entry[4]) << 32 | u128::from(entry[5])); - crate::order::by_wide_key(&mut kept, |&(i, entry)| key(entry[1], !(i as u32)) << 64 | key(!entry[2], entry[0])); - let mut seen = FxHashSet::default(); - let mut hits = Vec::new(); - for (i, entry) in kept { - if !seen.insert((i, entry[0])) { - continue; - } - let Some(row) = self.row_at(i, entry[0]) else { continue }; - let point = [f32::from_bits(entry[3]), f32::from_bits(entry[4])]; - let depth = f64::from(f32::from_bits(entry[5])); - hits.push(Hit { - row, - mark: self.marks[i].mark.style().name.clone(), - index: i, - point, - position: view.position(view.unproject(point.map(f64::from), depth)), - distance: entry[1] as f32 / 64.0, - port: self.port(i, entry[2]), - sample: self.sample(i, entry[0], entry[2]), - }); +/// The hits `words` hold of `found`, as `drew`, what their frame drew, names them. +fn hits(found: &[Found], words: &[u32], drew: &Drew) -> Vec { + // Nearest first, the mark drawn last first among equals, and a port before its marker; then + // by row and where it lies, so the order never depends on which hit landed first. Each row + // answers once, where it is nearest. + let mut kept: Vec<(usize, &[u32; wgsl::HIT_WORDS as usize])> = found + .iter() + .flat_map(|f| { + let count = words_at(words, f.at, 1).first().map_or(0, |&n| n.min(f.room)); + let entries = words_at(words, f.at + 2, count * wgsl::HIT_WORDS); + entries.as_chunks().0.iter().map(move |entry| (f.mark, entry)) + }) + .collect(); + // The last keys first, as each sort keeps the order of ties. + let key = |high: u32, low: u32| u128::from(crate::order::pair(high, low)); + crate::order::by_wide_key(&mut kept, |&(_, entry)| key(entry[3], entry[4]) << 32 | u128::from(entry[5])); + crate::order::by_wide_key(&mut kept, |&(i, entry)| key(entry[1], !(i as u32)) << 64 | key(!entry[2], entry[0])); + let mut seen = FxHashSet::default(); + let mut hits = Vec::new(); + for (i, entry) in kept { + if !seen.insert((i, entry[0])) { + continue; } - hits + let Some((row, at)) = drew.row(i, entry[0]) else { continue }; + let point = [f32::from_bits(entry[3]), f32::from_bits(entry[4])]; + let depth = f64::from(f32::from_bits(entry[5])); + hits.push(Hit { + row, + mark: drew.name(i), + index: i, + point, + position: drew.position(point, depth), + distance: entry[1] as f32 / 64.0, + port: drew.port(i, entry[2]), + sample: drew.sample(i, at, entry[2]), + }); } + hits } /// `len` words from word `at` of the probe's, in a query's answer read from past the probe's words. diff --git a/crates/latkit/src/view/resources.rs b/crates/latkit/src/view/resources.rs index 0628e226..d01c3a54 100644 --- a/crates/latkit/src/view/resources.rs +++ b/crates/latkit/src/view/resources.rs @@ -57,11 +57,6 @@ fn steps(device: &wgpu::Device) -> wgpu::Buffer { }) } -/// Samples per pixel: shader-drawn marks antialias themselves, so dense screens skip multisampling. -pub(crate) fn samples(pixel_ratio: f32) -> u32 { - if pixel_ratio >= 1.75 { 1 } else { 4 } -} - /// A storage buffer that grows by half again when it runs out of room. struct Grown { buffer: wgpu::Buffer, diff --git a/crates/latkit/src/view/rig.rs b/crates/latkit/src/view/rig.rs index 199965bf..934b9640 100644 --- a/crates/latkit/src/view/rig.rs +++ b/crates/latkit/src/view/rig.rs @@ -74,6 +74,11 @@ impl Rig { } impl View { + /// The zooms gestures and the rig keep the camera within: what the last fit framed allows. + pub(super) fn zoom_limits(&self) -> Option<[f64; 2]> { + self.rig.bounds.map(|bounds| bounds.zooms()) + } + /// Moves the camera by a gesture or the rig, kept within reach of what was fitted. pub(super) fn steer(&mut self, camera: Camera) { let camera = match &self.rig.bounds { @@ -108,7 +113,7 @@ impl View { let view = self.projected(); let mut camera = self.camera.panned(&view, anchor, pan); camera = camera.turned(&self.projected_with(&camera), anchor, turn); - camera = camera.zoomed(&self.projected_with(&camera), anchor, zoom); + camera = camera.zoomed(&self.projected_with(&camera), anchor, zoom, self.zoom_limits()); let settle = |v: f64, least: f64| if v.abs() < least { 0.0 } else { v }; self.rig.pan = [settle(self.rig.pan[0] - pan[0], 0.05), settle(self.rig.pan[1] - pan[1], 0.05)]; self.rig.turn = [settle(self.rig.turn[0] - turn[0], 0.01), settle(self.rig.turn[1] - turn[1], 0.01)]; diff --git a/crates/latkit/src/view/rows.rs b/crates/latkit/src/view/rows.rs index a66ac260..af4ec825 100644 --- a/crates/latkit/src/view/rows.rs +++ b/crates/latkit/src/view/rows.rs @@ -45,7 +45,7 @@ impl View { let shape = |m: &super::marks::Shown| (m.stride(), m.extras()); let fits = self.marks.iter().all(|m| { let r = &m.region; - m.rows + m.exits <= r.capacity && (r.stride, r.extras) == shape(m) + m.held() <= r.capacity && (r.stride, r.extras) == shape(m) }); if fits && !self.regions_dirty { return false; @@ -60,7 +60,7 @@ impl View { for &i in &order { let m = &self.marks[i]; let (stride, extras) = shape(m); - let need = m.rows + m.exits; + let need = m.held(); let capacity = if need <= m.region.capacity && (m.region.stride, m.region.extras) == (stride, extras) { m.region.capacity } else { @@ -81,7 +81,7 @@ impl View { let old = resources.replace_rows(&device, sizes); for (shown, new) in self.marks.iter_mut().zip(regions) { let was = shown.region; - let kept = was.capacity.min(new.capacity).min(shown.held); + let kept = was.capacity.min(new.capacity).min(shown.before); let copy = |encoder: &mut wgpu::CommandEncoder, from: &wgpu::Buffer, to: &wgpu::Buffer, @@ -100,7 +100,7 @@ impl View { } // A running transition's frozen and settled rows move with its rows. if shown.transition.is_some() && (was.stride, was.extras) == (new.stride, new.extras) { - let rows = was.capacity.min(new.capacity).min(shown.rows + shown.exits); + let rows = was.capacity.min(new.capacity).min(shown.held()); for (from, to) in [(&old.frozen, resources.frozen()), (&old.settled, resources.settled())] { if let (Some(from), Some(to)) = (from, to) { copy(encoder, from, to, was.state, new.state, rows * new.stride); diff --git a/crates/latkit/src/view/sync.rs b/crates/latkit/src/view/sync.rs index dc86ddf0..d82178fd 100644 --- a/crates/latkit/src/view/sync.rs +++ b/crates/latkit/src/view/sync.rs @@ -1,27 +1,41 @@ //! A mark's columns on the GPU: for every column its program reads, a descriptor and a page table of -//! arena offsets, or for sampled columns a cursor per row block at the shown coordinate. +//! arena offsets, or for sampled columns a cursor per page of rows at the shown coordinate. +//! +//! A table drawn as it is pages its rows as its data does, and its columns read the data's pages. A +//! table drawn in part pages its rows as its marks hold them, a page of its order in each slot, and +//! its columns read pages packed in that order, kept on the GPU by what they were packed from. A +//! reference names the rows of its table where they draw. //! //! A column a line traces holds what is in view instead: runs of entries, frames while there are at //! most two per pixel column, else the buckets of the level of detail that fit, and where those are //! not held, the coarser ones standing in or the finer ones there are. Each level it draws lists its -//! axis pages and, per row block, its pages of values. +//! axis pages and, per page of rows, its pages of values. //! -//! Only columns whose inputs changed are rebuilt: moving the shown coordinate rebuilds sampled -//! columns' cursors and nothing else. A rebuilt column that lists pages says which row blocks hold -//! pages that changed, so only their rows evaluate again. A mark keeps drawing its current tables until every page of -//! its rebuilt ones is resident, then swaps them in together, so a data version appears whole. -//! Tables pin the pages they reference. While a mark eases, the table its geometry column had when -//! the transition began stays, packed after the rest, so paths morph from it. +//! Only columns whose inputs changed are rebuilt, once each: moving the shown coordinate rebuilds +//! sampled columns' cursors and nothing else, and a table drawn in part rebuilds only where the pages +//! its marks hold changed. Rebuilt tables wait, pinning their pages, until every page is resident; +//! the view then swaps them in with every other mark of their dataset, so a data version, and what a +//! camera move draws, appear whole. A rebuilt column that lists pages says which row blocks hold pages +//! that changed, so only their rows evaluate again. While a mark eases, the table its geometry column +//! had when the transition began stays, packed after the rest, so paths morph from it. use std::ops::Range; use std::sync::Arc; -use crate::data::{Column, Field, FramePage, PAGE_ROWS, PageId, Run, Samples, Version, detail}; +use super::drawing::{Drawing, Holds}; +use crate::data::{ + Column, Field, FramePage, GeometryBuilder, PAGE_ROWS, PageId, Run, Samples, Values, Version, detail, +}; use crate::derive::Beside; +use crate::derive::order::{Order, PAGE}; +use crate::derive::shapes::PagedShapes; +use crate::derive::stamp_of; use crate::expr::compile::{ColumnUse, Program, Reads, Words}; use crate::gpu::Inner; use crate::gpu::arena::{Content, PIECE_POINTS, PageKey, path_pieces, path_points}; +use crate::gpu::packed::{self, Packing}; use crate::gpu::shaders as wgsl; +use crate::gpu::work::Count; use crate::{Data, Name, Table}; const WORDS: usize = wgsl::COLUMN_WORDS as usize; @@ -30,7 +44,8 @@ const WORDS: usize = wgsl::COLUMN_WORDS as usize; #[derive(Clone, PartialEq, Debug)] enum Stamp { Missing, - Field(Version), + /// A field, and the order the table a reference of it names draws in. + Field(Version, u64), Sampled(Version, Option, u32), Trace(Version, Plan), Join(Version, Version), @@ -74,20 +89,57 @@ impl Plan { } } +/// How a mark's columns of its own table page its rows: a page of the data at a time, or a page of +/// the order its drawing holds in each slot. +#[derive(Clone, Copy)] +pub(crate) enum Rows<'a> { + Own, + Held(&'a Drawing), +} + +impl Rows<'_> { + /// The stamp of how it pages rows: none for the data's own pages. + fn stamp(self) -> u64 { + match self { + Rows::Own => 0, + Rows::Held(drawing) => drawing.layout(), + } + } +} + /// One column on the GPU: its descriptor, whose table offsets are relative to `words`, its page table -/// or cursors, and the pages it pins. +/// or cursors, the pages it pins, and the words still waiting for a page. struct ColumnTable { stamp: Stamp, + /// How it pages its rows: 0 for the data's pages, else the stamp of the pages held. + held: u64, descriptor: [u32; WORDS], words: Vec, pins: Vec, + /// Words waiting for a page's offset: the word, the page, and the words it must hold. + missing: Vec<(usize, PageKey, u32)>, /// What a traced column draws. plan: Option, /// At most how many points a geometry column's paths take. points: u32, - /// The page of each row block the table lists, for a column whose rows read their own block's - /// page; `None` for one whose every row changes when it does (cursors, traces, joins). - pages: Option>, + /// The page of each page of rows the table lists, for a column whose rows read their own page; + /// `None` for one whose every row changes when it does (cursors, traces, joins). + pages: Option>>, +} + +impl ColumnTable { + /// Takes the offsets of the pages it waits for that landed; whether none waits now. + fn resolve(&mut self, gpu: &Inner) -> bool { + let words = &mut self.words; + self.missing.retain(|&(at, key, need)| match gpu.arena.offset(key, need) { + Some(offset) => { + words[at] = offset; + false + } + None => true, + }); + self.missing.is_empty() + } } /// Rows of the mark's table joined by id to rows of another table. @@ -97,38 +149,78 @@ struct Join { pages: Vec<(PageId, Arc<[u32]>)>, } +/// Tables rebuilt and waiting for their pages, by index into the current ones; and whether every +/// table was rebuilt, which replaces them all. +struct Pending { + tables: Vec<(usize, ColumnTable)>, + reshape: bool, +} + /// What a mark's program reads, column by column: the program's columns, then one per join. #[derive(Default)] pub(crate) struct Tables { current: Vec, - /// Rebuilt tables waiting for their pages, by index into `current`. - pending: Vec<(usize, ColumnTable)>, + pending: Option, /// The geometry column's table before it was last rebuilt, kept while the mark eases from it. frozen: Option, joins: Vec, synced: bool, /// The rows of the table last synced, which a program reading no columns follows alone. - structure: Option, + structure: Option, /// Bumps whenever a table is swapped in. generation: u64, } -/// What a sync did. +/// What a sync found. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) enum Synced { - /// Nothing changed. + /// Nothing waits to replace the current tables. Same, - /// Pages are uploading; the current tables still draw. + /// Rebuilt tables wait for pages; the current ones still draw. Waiting, - /// New tables: bit `i` of `columns` for each program column that changed, and the row blocks - /// whose pages changed; `None` when every row did. - Changed { columns: u64, blocks: Option> }, + /// Rebuilt tables have every page, ready to replace the current ones. + Ready, } -/// Where a mark's own rows come from, and the tables the view derives beside them. +/// What publishing rebuilt tables changed: bit `i` of `columns` for each program column whose table +/// changed, of `data` for each whose data did, not only where its rows are held; and the row blocks +/// whose pages changed, `None` when every row did. +pub(crate) struct Published { + pub(crate) columns: u64, + pub(crate) data: u64, + pub(crate) blocks: Option>, +} + +/// Where a mark's own rows come from, the tables the view derives beside them, and drawn in part, the +/// shapes it makes a page of their order at a time, by the field each is written as. pub(crate) struct Source<'a> { pub(crate) data: &'a Data, pub(crate) table: &'a Table, pub(crate) derived: Beside<'a>, + pub(crate) paged: Vec<(Name, &'a PagedShapes)>, +} + +impl<'a> Source<'a> { + /// The shapes written as field `name`, made a page of the order drawn at a time. + fn paged(&self, name: &Name) -> Option<&'a PagedShapes> { + self.paged.iter().find(|(field, _)| field == name).map(|&(_, shapes)| shapes) + } +} + +/// What a sync reads besides the mark's own rows: every dataset, the coordinate shown, the window a +/// trace spans, and the order each table of its dataset drawn in part draws in. +pub(crate) struct Inputs<'a> { + pub(crate) all: &'a [Data], + pub(crate) at: f64, + pub(crate) window: Window, + pub(crate) orders: &'a [(Name, Order)], +} + +impl Inputs<'_> { + /// The order table `table` of the mark's dataset draws in, when drawn in part. + fn order_of(&self, table: &Name) -> Option<&Order> { + self.orders.iter().find(|(name, _)| name == table).map(|(_, order)| order) + } } /// One column a program reads: a field, or the rows a join maps to. @@ -138,18 +230,14 @@ enum Use<'a> { } impl Tables { - /// Brings the tables up to date with `source` and every dataset in `all` at coordinate `at`; - /// traced columns hold the frames across `window`, or every frame without one. When column - /// `geometry` is rebuilt, its table before stays frozen until [`thaw`](Tables::thaw). - #[allow(clippy::too_many_arguments, reason = "one sync of a mark's tables")] + /// Brings the tables up to `source` paged as `rows`, with what `inputs` reads: tables whose + /// inputs changed are rebuilt once, and wait until every page they read is resident. pub(crate) fn sync( &mut self, program: &Program, source: &Source<'_>, - all: &[Data], - at: f64, - window: Window, - geometry: u32, + rows: Rows<'_>, + inputs: &Inputs<'_>, gpu: &mut Inner, ) -> Synced { let joins = joins(program); @@ -164,74 +252,118 @@ impl Tables { || self.current.len() != uses.len() || (uses.is_empty() && self.structure != Some(source.table.structure)); self.structure = Some(source.table.structure); - let mut rebuilt = Vec::new(); - let mut ready = true; + let held = rows.stamp(); + let pending = self.pending.take(); + let mut rebuilt: Vec<(usize, ColumnTable)> = Vec::new(); for (i, use_) in uses.iter().enumerate() { - let stamp = match use_ { - Use::Field { column, .. } => stamp(column, source, all, at, window), - Use::Join(of) => match joined(of, all) { - Some(other) => Stamp::Join(source.table.structure, other.structure), - None => Stamp::Missing, + let (stamp, paged) = match use_ { + Use::Field { column, .. } => { + (stamp(column, source, inputs), if column.of.is_none() { held } else { 0 }) + } + Use::Join(of) => match joined(of, inputs.all) { + Some(other) => (Stamp::Join(source.table.structure, other.structure), held), + None => (Stamp::Missing, held), }, }; - if !reshape && self.current[i].stamp == stamp { + // What replaces table `i` once ready, else what draws. + let waiting = + pending.as_ref().filter(|p| !p.reshape || reshape).and_then(|p| p.tables.iter().find(|(j, _)| *j == i)); + let latest = waiting.map(|(_, t)| t).or(self.current.get(i)); + if !reshape && latest.is_some_and(|t| t.stamp == stamp && t.held == paged) { continue; } - let mut builder = Builder { - words: Vec::new(), - pins: Vec::new(), - ready: true, - plan: None, - points: 0, - pages: Some(Vec::new()), - gpu: &mut *gpu, - }; + let mut builder = Builder::new(gpu); let descriptor = match use_ { Use::Field { column, join } => { - let mut descriptor = builder.column(column, source, all, at, window); + let rows = if column.of.is_none() { rows } else { Rows::Own }; + let mut descriptor = builder.column(column, source, rows, inputs); descriptor[wgsl::COLUMN_JOIN as usize] = *join; descriptor } Use::Join(of) => { - let join = self.join(of, source, all); - let descriptor = builder.join(&join, source.table.len()); + let join = self.join(of, source, inputs.all); + let descriptor = builder.join(&join, of, source, rows, inputs.all); self.remember(join); descriptor } }; - let Builder { words, pins, ready: column_ready, plan, points, pages, gpu: _ } = builder; - for &key in &pins { - gpu.arena.pin(key); + rebuilt.push((i, builder.finish(stamp, paged, descriptor))); + } + // Tables rebuilt again replace those waiting, whose pins go; a reshape replaces them all. + let mut tables = match pending { + Some(p) if p.reshape == reshape || !reshape => p.tables, + Some(p) => { + p.tables.into_iter().for_each(|(_, t)| unpin(&t.pins, gpu)); + Vec::new() } - ready &= column_ready; - rebuilt.push((i, ColumnTable { stamp, descriptor, words, pins, plan, points, pages })); + None => Vec::new(), + }; + if rebuilt.is_empty() && !reshape { + if !tables.is_empty() { + self.pending = Some(Pending { tables, reshape: false }); + } + return self.waiting(gpu); } - for (_, table) in std::mem::take(&mut self.pending) { - unpin(&table.pins, gpu); + for (i, table) in rebuilt { + match tables.iter().position(|(j, _)| *j == i) { + Some(at) => { + let (_, old) = std::mem::replace(&mut tables[at], (i, table)); + unpin(&old.pins, gpu); + } + None => tables.push((i, table)), + } } - if rebuilt.is_empty() && !reshape { - return Synced::Same; + self.pending = Some(Pending { tables, reshape }); + self.waiting(gpu) + } + + /// Whether rebuilt tables wait, and for pages: taking the offsets of those that landed. + pub(crate) fn waiting(&mut self, gpu: &Inner) -> Synced { + let Some(pending) = &mut self.pending else { return Synced::Same }; + let mut ready = true; + for (_, table) in &mut pending.tables { + ready &= table.resolve(gpu); } - if !ready { - self.pending = rebuilt; - return Synced::Waiting; + if ready { Synced::Ready } else { Synced::Waiting } + } + + /// Swaps in the tables rebuilt, once every page they read is resident: the current ones' pins + /// go, but the geometry column's, `geometry`, which stays frozen while the mark eases from it. + pub(crate) fn publish(&mut self, program: &Program, geometry: u32, gpu: &mut Inner) -> Option { + if self.waiting(gpu) != Synced::Ready { + return None; } + let Pending { tables: rebuilt, reshape } = self.pending.take().expect("ready"); self.generation += 1; - let mut blocks = Some(Vec::new()); - let changed = if reshape { + let joins = joins(program); + let published = if reshape { self.thaw(gpu); for table in std::mem::take(&mut self.current) { unpin(&table.pins, gpu); } - self.current = rebuilt.into_iter().map(|(_, table)| table).collect(); - blocks = None; - u64::MAX + let mut tables: Vec> = Vec::new(); + for (i, table) in rebuilt { + if tables.len() <= i { + tables.resize_with(i + 1, || None); + } + tables[i] = Some(table); + } + self.current = tables.into_iter().map(|t| t.expect("every table rebuilt")).collect(); + Published { columns: u64::MAX, data: u64::MAX, blocks: None } } else { - let mut changed = 0; + let mut published = Published { columns: 0, data: 0, blocks: Some(Vec::new()) }; for (i, table) in rebuilt { - match (blocks.as_mut(), changed_blocks(&self.current[i], &table)) { + match (published.blocks.as_mut(), changed_blocks(&self.current[i], &table)) { (Some(blocks), Some(more)) => blocks.extend(more), - _ => blocks = None, + _ => published.blocks = None, + } + let bits = match program.columns.get(i) { + Some(_) => Reads::column(i), + None => columns_of(program, joins[i - program.columns.len()]), + }; + published.columns |= bits; + if table.stamp != self.current[i].stamp { + published.data |= bits; } let old = std::mem::replace(&mut self.current[i], table); if i as u32 == geometry { @@ -240,15 +372,11 @@ impl Tables { } else { unpin(&old.pins, gpu); } - changed |= match &uses[i] { - Use::Field { .. } => Reads::column(i), - Use::Join(of) => columns_of(program, of), - }; } - changed + published }; self.synced = true; - Synced::Changed { columns: changed, blocks } + Some(published) } /// Drops the frozen geometry table: the mark no longer eases from it. @@ -293,9 +421,9 @@ impl Tables { self.plan().map_or(0, |plan| plan.points) } - /// Whether rebuilt tables wait for pages to upload. - pub(crate) fn waiting(&self) -> bool { - !self.pending.is_empty() + /// Whether rebuilt tables wait to replace the current ones. + pub(crate) fn pending(&self) -> bool { + self.pending.is_some() } /// Words the descriptors and tables take in the program buffer. @@ -329,7 +457,8 @@ impl Tables { /// Releases every pin. pub(crate) fn release(&mut self, gpu: &mut Inner) { self.thaw(gpu); - for table in self.current.drain(..).chain(self.pending.drain(..).map(|(_, t)| t)) { + let pending = self.pending.take().map(|p| p.tables).unwrap_or_default(); + for table in self.current.drain(..).chain(pending.into_iter().map(|(_, t)| t)) { unpin(&table.pins, gpu); } self.synced = false; @@ -361,11 +490,17 @@ impl Tables { } } -/// The row blocks whose pages differ between table `before` and table `now`; `None` when either reads -/// no block's page alone, so every row changed. +/// The row blocks whose pages differ between table `before` and table `now`, a page's every block; +/// `None` when either reads no page alone, or they page their rows apart, so every row changed. fn changed_blocks(before: &ColumnTable, now: &ColumnTable) -> Option> { let (Some(old), Some(new)) = (&before.pages, &now.pages) else { return None }; - Some((0..old.len().max(new.len())).filter(|&p| old.get(p) != new.get(p)).map(|p| p as u32).collect()) + let bits = now.descriptor[wgsl::COLUMN_BITS as usize]; + if before.descriptor[wgsl::COLUMN_BITS as usize] != bits { + return None; + } + let blocks = 1 << (bits - wgsl::HELD_BITS); + let pages = (0..old.len().max(new.len())).filter(|&p| old.get(p) != new.get(p)).map(|p| p as u32); + Some(pages.flat_map(|p| p * blocks..(p + 1) * blocks).collect()) } fn unpin(pins: &[PageKey], gpu: &mut Inner) { @@ -441,17 +576,24 @@ fn plan(samples: &Samples, window: Window) -> Plan { Plan { level, runs, points: point, steps: window.steps } } -fn stamp(column: &ColumnUse, source: &Source<'_>, all: &[Data], at: f64, window: Window) -> Stamp { - let Some((_, table)) = table_of(column, source, all) else { return Stamp::Missing }; +fn stamp(column: &ColumnUse, source: &Source<'_>, inputs: &Inputs<'_>) -> Stamp { + if let Some(shapes) = source.paged(&column.name).filter(|_| column.derived) { + return Stamp::Field(shapes.version, shapes.order); + } + let Some((_, table)) = table_of(column, source, inputs.all) else { return Stamp::Missing }; match table.field_named(&column.name) { None => Stamp::Missing, - Some(Field::Sampled(samples, version)) if column.trace => Stamp::Trace(*version, plan(samples, window)), + Some(Field::Sampled(samples, version)) if column.trace => Stamp::Trace(*version, plan(samples, inputs.window)), Some(Field::Sampled(samples, version)) => { - let frame = samples.frame_at(at); - let t = if column.step { 0.0 } else { frame.map_or(0.0, |f| samples.t(f, at)) }; + let frame = samples.frame_at(inputs.at); + let t = if column.step { 0.0 } else { frame.map_or(0.0, |f| samples.t(f, inputs.at)) }; Stamp::Sampled(*version, frame, t.to_bits()) } - Some(field) => Stamp::Field(field.version()), + Some(Field::Reference(reference)) => { + let to = if column.of.is_none() { inputs.order_of(&reference.to).map_or(0, Order::id) } else { 0 }; + Stamp::Field(reference.version, to) + } + Some(field) => Stamp::Field(field.version(), 0), } } @@ -459,49 +601,117 @@ fn stamp(column: &ColumnUse, source: &Source<'_>, all: &[Data], at: f64, window: struct Builder<'g> { words: Vec, pins: Vec, - ready: bool, + missing: Vec<(usize, PageKey, u32)>, plan: Option, points: u32, - /// The page of each row block, while every row reads its own block's. - pages: Option>, + /// The page of each page of rows, while every row reads its own page's. + pages: Option>>, gpu: &'g mut Inner, } -impl Builder<'_> { - fn column( - &mut self, - column: &ColumnUse, - source: &Source<'_>, - all: &[Data], - at: f64, - window: Window, - ) -> [u32; WORDS] { +impl<'g> Builder<'g> { + fn new(gpu: &'g mut Inner) -> Builder<'g> { + Builder { + words: Vec::new(), + pins: Vec::new(), + missing: Vec::new(), + plan: None, + points: 0, + pages: Some(Vec::new()), + gpu, + } + } + + fn finish(self, stamp: Stamp, held: u64, descriptor: [u32; WORDS]) -> ColumnTable { + let Builder { words, pins, missing, plan, points, pages, gpu: _ } = self; + ColumnTable { stamp, held, descriptor, words, pins, missing, plan, points, pages } + } + + /// Writes at word `at` where page `key` lies once it holds `need` words, pinning it; until it + /// does, `NONE`, and the table waits for it. `content` makes what it uploads. + fn place(&mut self, at: usize, key: PageKey, need: u32, content: impl FnOnce() -> Content) { + let offset = self.gpu.arena.get(key, need, content); + self.gpu.arena.pin(key); + self.pins.push(key); + self.words[at] = offset.unwrap_or(wgsl::NONE); + if offset.is_none() { + self.missing.push((at, key, need)); + } + } + + /// Appends where page `key` lies, as [`place`](Builder::place) writes it. + fn push(&mut self, key: PageKey, need: u32, content: impl FnOnce() -> Content) { + self.words.push(wgsl::NONE); + let at = self.words.len() - 1; + self.place(at, key, need, content); + } + + /// Lists page `page` as the one a page of rows reads. + fn list(&mut self, page: Option) { + if let Some(pages) = &mut self.pages { + pages.push(page); + } + } + + fn column(&mut self, column: &ColumnUse, source: &Source<'_>, rows: Rows<'_>, inputs: &Inputs<'_>) -> [u32; WORDS] { let mut d = [0; WORDS]; d[wgsl::COLUMN_LOW as usize] = wgsl::NONE; + d[wgsl::COLUMN_BITS as usize] = wgsl::PAGE_BITS; if column.words == Words::Bits { d[wgsl::COLUMN_FLAGS as usize] = wgsl::BITS; } - let Some((data, table)) = table_of(column, source, all) else { + if let Rows::Held(drawing) = rows + && let Some(shapes) = source.paged(&column.name).filter(|_| column.derived) + { + d[wgsl::COLUMN_BITS as usize] = wgsl::HELD_BITS; + d[wgsl::COLUMN_ROWS as usize] = drawing.live(); + let packing = Packing { + order: drawing.order.id(), + origin: source.data.origin(), + table: source.table.name.clone(), + field: column.name.clone(), + words: Words::Path, + low: false, + reads: 0, + }; + self.shaped(shapes, drawing, &packing); + return d; + } + let Some((data, table)) = table_of(column, source, inputs.all) else { self.pages = None; return d; }; d[wgsl::COLUMN_ROWS as usize] = table.len() as u32; - match table.field_named(&column.name) { + let field = table.field_named(&column.name); + if let Rows::Held(drawing) = rows { + d[wgsl::COLUMN_BITS as usize] = wgsl::HELD_BITS; + d[wgsl::COLUMN_ROWS as usize] = drawing.live(); + let packing = Packing { + order: drawing.order.id(), + origin: source.data.origin(), + table: table.name.clone(), + field: column.name.clone(), + words: column.words, + low: false, + reads: 0, + }; + self.held(column, data, field, drawing, packing, inputs, &mut d); + return d; + } + match field { Some(Field::Values(values, _)) if column.words == Words::Codes => { let field = (data, &table.name, &column.name); - self.listed(values); for page in values.pages() { + self.list(Some(page.id)); let key = PageKey::of(page.id, Words::Codes, false); // Coded once, whether or not the page is on the GPU, so the dictionary holds its texts. let codes = self.gpu.dictionaries.field(field.0, field.1, field.2).codes(page); - let offset = - self.gpu.arena.get(key, page.values.len() as u32, || Content::Computed(codes.to_vec())); - self.push(key, offset); + self.push(key, page.values.len() as u32, || Content::Computed(codes.to_vec())); } } Some(Field::Values(values, _)) if column.words == Words::Path => self.path(values), Some(Field::Values(values, _)) => { - self.listed(values); + values.pages().iter().for_each(|page| self.list(Some(page.id))); self.pages(values, column.words, false); if column.low { d[wgsl::COLUMN_LOW as usize] = self.words.len() as u32; @@ -509,19 +719,32 @@ impl Builder<'_> { } } Some(Field::Reference(reference)) => { - self.listed(&reference.rows); - self.pages(&reference.rows, Words::Bits, false); + reference.rows.pages().iter().for_each(|page| self.list(Some(page.id))); + match inputs.order_of(&reference.to).filter(|_| column.of.is_none()) { + // The rows it names where their table draws them. + Some(order) => { + for (p, page) in reference.rows.pages().iter().enumerate() { + let id = self.gpu.packed.ranked(page.id, order.id()); + let key = PageKey::of(id, Words::Bits, false); + let need = page.values.len() as u32; + self.push(key, need, || Content::Computed(packed::ranked_page(reference, p, order))); + } + } + None => self.pages(&reference.rows, Words::Bits, false), + } } Some(Field::Sampled(samples, _)) if column.trace => { self.pages = None; d[wgsl::COLUMN_FLAGS as usize] |= wgsl::SAMPLED; - self.trace(samples, plan(samples, window), table.len()); + let blocks: Vec<(Samples, usize)> = + (0..table.len().div_ceil(PAGE_ROWS)).map(|b| (samples.clone(), b)).collect(); + self.trace(samples, &blocks, plan(samples, inputs.window)); } Some(Field::Sampled(samples, _)) => { self.pages = None; d[wgsl::COLUMN_FLAGS as usize] |= wgsl::SAMPLED | if column.step { wgsl::STEP } else { 0 }; - let frame = samples.frame_at(at); - let t = if column.step { 0.0 } else { frame.map_or(0.0, |f| samples.t(f, at)) }; + let frame = samples.frame_at(inputs.at); + let t = if column.step { 0.0 } else { frame.map_or(0.0, |f| samples.t(f, inputs.at)) }; d[wgsl::COLUMN_T as usize] = t.to_bits(); self.cursors(samples, frame, table.len()); } @@ -530,78 +753,301 @@ impl Builder<'_> { d } - /// A join's table: a bits column of the mark's rows. - fn join(&mut self, join: &Join, rows: usize) -> [u32; WORDS] { + /// A column of a table drawn in part: a page packed in its order for each page its drawing holds, + /// as `packing` keeps them. + #[allow(clippy::too_many_arguments, reason = "one column's pages in its order")] + fn held( + &mut self, + column: &ColumnUse, + data: &Name, + field: Option<&Field>, + drawing: &Drawing, + packing: Packing, + inputs: &Inputs<'_>, + d: &mut [u32; WORDS], + ) { + let order = &drawing.order; + let slots = drawing.slot_count(); + match field { + Some(Field::Values(values, _)) if column.words == Words::Codes => { + for s in 0..slots { + self.held_page(drawing, s, &packing, Some(values), |gpu, rows| { + let mut codes = gpu.dictionaries.field(data, &packing.table, &packing.field).rows(values, rows); + codes.resize(PAGE as usize, wgsl::MISSING); + codes + }); + } + } + Some(Field::Values(values, _)) if column.words == Words::Path => self.held_path(values, drawing, &packing), + Some(Field::Values(values, _)) => { + let words = column.words; + for s in 0..slots { + self.held_page(drawing, s, &packing, Some(values), |_, rows| { + packed::values(values, rows, words, false) + }); + } + if column.low { + d[wgsl::COLUMN_LOW as usize] = self.words.len() as u32; + let low = Packing { low: true, ..packing }; + let listed = self.pages.take(); + for s in 0..slots { + self.held_page(drawing, s, &low, Some(values), |_, rows| { + packed::values(values, rows, words, true) + }); + } + self.pages = listed; + } + } + Some(Field::Reference(reference)) => { + let target = inputs.order_of(&reference.to); + let packing = Packing { reads: target.map_or(0, Order::id), ..packing }; + for s in 0..slots { + self.held_page(drawing, s, &packing, Some(&reference.rows), |_, rows| { + packed::targets(reference, rows, target) + }); + } + } + Some(Field::Sampled(samples, version)) if column.trace => { + self.pages = None; + d[wgsl::COLUMN_FLAGS as usize] |= wgsl::SAMPLED; + let packing = Packing { reads: stamp_of(version), ..packing }; + let blocks: Vec<(Samples, usize)> = (0..slots) + .map(|s| match slot_key(&packing, drawing, None, s) { + None => (Samples::default(), 0), + Some((packing, page, _)) => { + (self.gpu.packed.samples(&packing, order, page, &drawing.rows(s), samples), 0) + } + }) + .collect(); + self.trace(samples, &blocks, plan(samples, inputs.window)); + } + Some(Field::Sampled(samples, version)) => { + self.pages = None; + d[wgsl::COLUMN_FLAGS as usize] |= wgsl::SAMPLED | if column.step { wgsl::STEP } else { 0 }; + let frame = samples.frame_at(inputs.at); + let t = if column.step { 0.0 } else { frame.map_or(0.0, |f| samples.t(f, inputs.at)) }; + d[wgsl::COLUMN_T as usize] = t.to_bits(); + // Six words a page: this frame's block and word, the next frame's, and their rows. + for s in 0..slots { + let at = self.words.len(); + self.words.extend([wgsl::NONE, 0, wgsl::NONE, 0, 0, 0]); + let Some((base, page, _)) = slot_key(&packing, drawing, None, s) else { continue }; + for (k, f) in [frame, frame.map(|f| f + 1)].into_iter().enumerate() { + let Some(f) = f.filter(|&f| samples.held(0, f)) else { continue }; + let packing = Packing { reads: stamp_of((version, f)), ..base.clone() }; + let id = self.gpu.packed.page(&packing, order, None, page); + let key = PageKey::of(id, Words::Values, false); + let rows = drawing.rows(s); + let counters = self.gpu.packed.counters(); + let content = || { + counters.add(Count::Packed, rows.len() as u64); + Content::Computed(packed::frame(samples, &rows, Some(f))) + }; + self.place(at + 2 * k, key, PAGE, content); + self.words[at + 4 + k] = PAGE; + } + } + } + None => self.pages = None, + } + } + + /// Appends the page of `packing` that slot `s` of `drawing` holds, packed from `source`, made by + /// `pack` from its rows the first time; or nothing for a free slot. + fn held_page( + &mut self, + drawing: &Drawing, + s: usize, + packing: &Packing, + source: Option<&Column>, + pack: impl FnOnce(&mut Inner, &[u32]) -> Vec, + ) { + let Some((packing, page, source)) = slot_key(packing, drawing, source, s) else { + self.words.push(wgsl::NONE); + self.list(None); + return; + }; + let id = self.gpu.packed.page(&packing, &drawing.order, source, page); + self.list(Some(id)); + let key = PageKey::of(id, packing.words, packing.low); + let held = self.gpu.arena.offset(key, PAGE).is_some() || self.gpu.arena.queued(key); + let words = if held { + Vec::new() + } else { + let rows = drawing.rows(s); + self.gpu.packed.count(rows.len()); + pack(self.gpu, &rows) + }; + self.push(key, PAGE, || Content::Computed(words)); + } + + /// A join's table: a bits column of the mark's rows, the rows of the other table with their ids. + fn join( + &mut self, + join: &Join, + of: &(Name, Name), + source: &Source<'_>, + rows: Rows<'_>, + all: &[Data], + ) -> [u32; WORDS] { self.pages = None; let mut d = [0; WORDS]; d[wgsl::COLUMN_FLAGS as usize] = wgsl::BITS; - d[wgsl::COLUMN_ROWS as usize] = rows as u32; + d[wgsl::COLUMN_ROWS as usize] = source.table.len() as u32; d[wgsl::COLUMN_LOW as usize] = wgsl::NONE; d[wgsl::COLUMN_JOIN as usize] = wgsl::NONE; + d[wgsl::COLUMN_BITS as usize] = wgsl::PAGE_BITS; + if let Rows::Held(drawing) = rows { + d[wgsl::COLUMN_ROWS as usize] = drawing.live(); + d[wgsl::COLUMN_BITS as usize] = wgsl::HELD_BITS; + let Some(other) = joined(of, all) else { return d }; + let packing = Packing { + order: drawing.order.id(), + origin: source.data.origin(), + table: source.table.name.clone(), + field: format!("{}/{}", of.0, of.1).into(), + words: Words::Bits, + low: false, + reads: stamp_of((source.table.structure, other.structure, other.order)), + }; + for s in 0..drawing.slot_count() { + self.held_page(drawing, s, &packing, None, |_, rows| packed::joined(source.table, rows, other)); + } + return d; + } for (id, words) in &join.pages { let key = PageKey::of(*id, Words::Bits, false); - let offset = self.gpu.arena.get(key, words.len() as u32, || Content::Computed(words.to_vec())); - self.push(key, offset); + self.push(key, words.len() as u32, || Content::Computed(words.to_vec())); } d } - /// Lists 's pages as the row blocks the table reads. - fn listed(&mut self, column: &Column) { - if let Some(pages) = &mut self.pages { - pages.extend(column.pages().iter().map(|page| page.id)); - } - } - fn pages(&mut self, column: &Column, words: Words, low: bool) { for page in column.pages() { let key = PageKey::of(page.id, words, low); let values = page.values.clone(); - let offset = self.gpu.arena.get(key, values.len() as u32, || Content::Values { values, words, low }); - self.push(key, offset); + self.push(key, values.len() as u32, || Content::Values { values, words, low }); } } /// A geometry column's table, as `lib/read.wesl` reads it: per page, the block of each row's - /// first point, where the page's blocks of points are listed in the table, and how many points - /// it has; then those lists. A page's points take as many blocks of a chunk as they need. + /// first point, where the page's blocks of points are listed in the table, how many points it + /// has, and how many rows; then those lists. A page's points take as many blocks of a chunk as + /// they need. fn path(&mut self, column: &Column) { - self.listed(column); let pages = column.pages(); - self.words.resize(3 * pages.len(), 0); + self.words.resize(wgsl::PATH_PAGE_WORDS as usize * pages.len(), 0); for (p, page) in pages.iter().enumerate() { + self.list(Some(page.id)); let points = path_points(&page.values); - self.points += points as u32; - let mut made: Option>> = None; - let mut blocks = Vec::with_capacity(1 + points.div_ceil(PIECE_POINTS)); - for piece in 0..=points.div_ceil(PIECE_POINTS) { - let key = PageKey { page: page.id, words: Words::Path, low: false, piece: piece as u32 }; - let offset = self.gpu.arena.get(key, 1, || { - let pieces = made.get_or_insert_with(|| path_pieces(&page.values)); - Content::Computed(std::mem::take(&mut pieces[piece])) - }); - self.pins.push(key); - self.ready &= offset.is_some(); - blocks.push(offset.unwrap_or(wgsl::NONE)); - } - let listed = self.words.len() as u32; - self.words[3 * p..3 * p + 3].copy_from_slice(&[blocks[0], listed, points as u32]); - self.words.extend(&blocks[1..]); + self.path_page(p, page.id, points, page.values.len() as u32, || path_pieces(&page.values)); + } + } + + /// The geometry column of a table drawn in part: a page of its points per page its drawing holds, + /// as `packing` keeps them. + fn held_path(&mut self, column: &Column, drawing: &Drawing, packing: &Packing) { + let per = wgsl::PATH_PAGE_WORDS as usize; + self.words.resize(per * drawing.slot_count(), 0); + for s in 0..drawing.slot_count() { + let Some((packing, page, source)) = slot_key(packing, drawing, Some(column), s) else { + self.words[per * s..per * (s + 1)].copy_from_slice(&[wgsl::NONE, 0, 0, 0]); + self.list(None); + continue; + }; + let id = self.gpu.packed.page(&packing, &drawing.order, source, page); + self.list(Some(id)); + let rows = drawing.rows(s); + let points = rows_points(column, &rows); + let counters = self.gpu.packed.counters(); + let pieces = || { + counters.add(Count::Packed, rows.len() as u64); + path_pieces(&column.pick(&rows)) + }; + self.path_page(s, id, points, rows.len() as u32, pieces); } } - /// What a line draws, laid out as `lib/trace.wesl` reads it: a header (how many runs, row blocks, - /// and levels, and the points in all); per run its level's place, first entry, entries, and first - /// point; per level the level, how many axis pages and where they are listed, and where its row - /// blocks' lists start; per level and row block how many pages of frames or least values and where - /// they are listed, then of greatest values; then those lists, axis pages as (offset, first entry) - /// and pages of values as (offset, first entry, words per entry). Offsets within the table are - /// relative to it. - fn trace(&mut self, samples: &Samples, plan: Plan, rows: usize) { - let blocks = rows.div_ceil(PAGE_ROWS); + /// The shapes of a table drawn in part, made a page of its order at a time: those of each page its + /// drawing holds, and of its rows kept, a row at a time, in pages `packing` keeps by what they + /// hold. A page not shaped yet draws nothing until it is. + fn shaped(&mut self, shapes: &PagedShapes, drawing: &Drawing, packing: &Packing) { + let per = wgsl::PATH_PAGE_WORDS as usize; + self.words.resize(per * drawing.slot_count(), 0); + let alike = shapes.order == drawing.order.id(); + for s in 0..drawing.slot_count() { + let page: Option<(PageId, std::borrow::Cow<'_, Values>)> = match drawing.holds(s) { + Holds::Page(page) if alike => { + shapes.pages.get(&page).map(|p| (p.id, std::borrow::Cow::Borrowed(&p.shapes))) + } + Holds::Kept(_) if alike => { + let places = drawing.places(s); + let mut kept = GeometryBuilder::new(shapes.depth); + for d in &places { + match shapes.kept.get(d).and_then(Values::geometry) { + Some(shape) => kept.push_from(shape, 0), + None => kept.push_empty(), + } + } + let packing = + Packing { order: stamp_of((packing.order, places, shapes.version)), ..packing.clone() }; + let id = self.gpu.packed.page(&packing, &drawing.order, None, 0); + Some((id, std::borrow::Cow::Owned(Values::of_geometry(kept.finish())))) + } + _ => None, + }; + let Some((id, shaped)) = page else { + self.words[per * s..per * (s + 1)].copy_from_slice(&[wgsl::NONE, 0, 0, 0]); + self.list(None); + continue; + }; + self.list(Some(id)); + let rows = shaped.len() as u32; + let counters = self.gpu.packed.counters(); + let points = path_points(&shaped); + let pieces = || { + counters.add(Count::Packed, u64::from(rows)); + path_pieces(&shaped) + }; + self.path_page(s, id, points, rows, pieces); + } + } + + /// Page `p` of a geometry column's table, `id`, of `points` points and `rows` rows: its first + /// points, then its points a chunk at a time, which `pieces` makes where one is not on the GPU. + fn path_page(&mut self, p: usize, id: PageId, points: usize, rows: u32, pieces: impl FnOnce() -> Vec>) { + let per = wgsl::PATH_PAGE_WORDS as usize; + self.points += points as u32; + let count = points.div_ceil(PIECE_POINTS) + 1; + let keys: Vec = + (0..count).map(|piece| PageKey { page: id, words: Words::Path, low: false, piece: piece as u32 }).collect(); + let arena = &self.gpu.arena; + let made = keys.iter().all(|&key| arena.offset(key, 1).is_some() || arena.queued(key)); + let mut pieces = if made { Vec::new() } else { pieces() }; + let listed = self.words.len(); + self.words.resize(listed + count - 1, wgsl::NONE); + for (piece, key) in keys.into_iter().enumerate() { + let at = if piece == 0 { per * p } else { listed + piece - 1 }; + let words = pieces.get_mut(piece).map(std::mem::take).unwrap_or_default(); + self.place(at, key, 1, || Content::Computed(words)); + } + self.words[per * p + 1] = listed as u32; + self.words[per * p + 2] = points as u32; + self.words[per * p + 3] = rows; + } + + /// What a line draws, laid out as `lib/trace.wesl` reads it: a header (how many runs, pages of + /// rows, and levels, and the points in all); per run its level's place, first entry, entries, + /// and first point; per level the level, how many axis pages and where they are listed, and + /// where its pages' lists start; per level and page of rows how many pages of frames or least + /// values and where they are listed, then of greatest values; then those lists, axis pages as + /// (offset, first entry) and pages of values as (offset, first entry, words per entry). Offsets + /// within the table are relative to it. `blocks` are the samples each page of rows reads, and its + /// block among theirs. + fn trace(&mut self, axis: &Samples, blocks: &[(Samples, usize)], plan: Plan) { // Each run has its own local origin. Even sparse runs billions of frames apart use // compact GPU offsets, while CPU plans retain their absolute 64-bit indexes. - self.words.extend([plan.runs.len() as u32, blocks as u32, plan.runs.len() as u32, plan.points]); + self.words.extend([plan.runs.len() as u32, blocks.len() as u32, plan.runs.len() as u32, plan.points]); let origins: Vec = plan .runs .iter() @@ -614,25 +1060,23 @@ impl Builder<'_> { self.words.resize(at + 4 * plan.runs.len(), 0); for (i, (run, _)) in plan.runs.iter().enumerate() { let origin = origins[i]; - let axis = samples.axis_pages(run.level, run.entries.clone()); + let axis_pages = axis.axis_pages(run.level, run.entries.clone()); let listed_at = self.words.len() as u32; - self.words[at + 4 * i..at + 4 * i + 3].copy_from_slice(&[run.level, axis.len() as u32, listed_at]); - for (page, held) in &axis { + self.words[at + 4 * i..at + 4 * i + 3].copy_from_slice(&[run.level, axis_pages.len() as u32, listed_at]); + for (page, held) in &axis_pages { let key = PageKey::of(page.id, Words::Values, false); - let offset = - self.gpu.arena.get(key, ((held.end - page.first) * 2) as u32, || Content::Axis(page.clone())); - self.pins.push(key); - self.ready &= offset.is_some(); - self.words.extend([offset.unwrap_or(wgsl::NONE), (page.first - origin) as u32]); + let need = ((held.end - page.first) * 2) as u32; + self.push(key, need, || Content::Axis(page.clone())); + self.words.push((page.first - origin) as u32); } let lists = self.words.len(); self.words[at + 4 * i + 3] = lists as u32; - self.words.resize(lists + 4 * blocks, 0); - for block in 0..blocks { + self.words.resize(lists + 4 * blocks.len(), 0); + for (b, (samples, block)) in blocks.iter().enumerate() { for side in 0..crate::data::sides(run.level) { - let pages = samples.pages(block, run.level, side, run.entries.clone()); - self.words[lists + 4 * block + 2 * side] = pages.len() as u32; - self.words[lists + 4 * block + 2 * side + 1] = self.words.len() as u32; + let pages = samples.pages(*block, run.level, side, run.entries.clone()); + self.words[lists + 4 * b + 2 * side] = pages.len() as u32; + self.words[lists + 4 * b + 2 * side + 1] = self.words.len() as u32; self.sample_pages(&pages, origin); } } @@ -645,35 +1089,59 @@ impl Builder<'_> { for (page, held) in pages { let key = PageKey::of(page.id, Words::Values, false); let need = ((held.end - page.first) * u64::from(page.per)) as u32; - let offset = self.gpu.arena.get(key, need, || Content::Frames(page.clone())); - self.pins.push(key); - self.ready &= offset.is_some(); - self.words.extend([offset.unwrap_or(wgsl::NONE), (page.first - origin) as u32, page.per]); + self.push(key, need, || Content::Frames(page.clone())); + self.words.extend([(page.first - origin) as u32, page.per]); } } - /// Six words per row block: this frame's block and word, the next frame's, and their row counts. + /// Six words per page of rows: this frame's block and word, the next frame's, and their row + /// counts. fn cursors(&mut self, samples: &Samples, frame: Option, rows: usize) { for block in 0..rows.div_ceil(PAGE_ROWS) { - let mut cursor = [wgsl::NONE, 0, wgsl::NONE, 0, 0, 0]; + let at = self.words.len(); + self.words.extend([wgsl::NONE, 0, wgsl::NONE, 0, 0, 0]); for (i, f) in [frame, frame.map(|f| f + 1)].into_iter().enumerate() { let Some((page, base)) = f.and_then(|f| samples.cursor(block, f)) else { continue }; let key = PageKey::of(page.id, Words::Values, false); let rows_in_page = page.per; - let offset = self.gpu.arena.get(key, base + rows_in_page, || Content::Frames(page.clone())); - self.pins.push(key); - self.ready &= offset.is_some(); - cursor[2 * i] = offset.unwrap_or(wgsl::NONE); - cursor[2 * i + 1] = base; - cursor[4 + i] = rows_in_page; + self.place(at + 2 * i, key, base + rows_in_page, || Content::Frames(page.clone())); + self.words[at + 2 * i + 1] = base; + self.words[at + 4 + i] = rows_in_page; } - self.words.extend(cursor); } } +} - fn push(&mut self, key: PageKey, offset: Option) { - self.pins.push(key); - self.ready &= offset.is_some(); - self.words.push(offset.unwrap_or(wgsl::NONE)); +/// How the registry keeps the page slot `s` of `drawing` holds of a column packed as `packing` from +/// `source`, and the source it follows: a page of the order by its number, kept while no row it holds +/// is written; a page of rows kept by the rows it holds and the pages of `source` they lie in, so a +/// write to any of those packs it again. `None` for a free slot. +fn slot_key<'c>( + packing: &Packing, + drawing: &Drawing, + source: Option<&'c Column>, + s: usize, +) -> Option<(Packing, u32, Option<&'c Column>)> { + match drawing.holds(s) { + Holds::Free => None, + Holds::Page(page) => Some((packing.clone(), page, source)), + Holds::Kept(_) => { + let rows = drawing.rows(s); + let mut pages: Vec = source.map_or_else(Vec::new, |column| { + rows.iter().filter_map(|&r| column.pages().get(r as usize / PAGE_ROWS).map(|page| page.id)).collect() + }); + pages.sort_unstable(); + pages.dedup(); + Some((Packing { order: stamp_of((packing.order, rows, pages)), ..packing.clone() }, 0, None)) + } } } + +/// Points rows `rows` of geometry column `column` take as shaders read them. +fn rows_points(column: &Column, rows: &[u32]) -> usize { + rows.iter() + .filter_map(|&r| column.at(r as usize)) + .filter(|(values, i)| values.is_valid(*i)) + .map(|(values, i)| values.geometry().map_or(0, |g| g.row_points(i))) + .sum() +} diff --git a/crates/latkit/src/view/transition.rs b/crates/latkit/src/view/transition.rs index 1e6df114..157a1b6f 100644 --- a/crates/latkit/src/view/transition.rs +++ b/crates/latkit/src/view/transition.rs @@ -15,10 +15,14 @@ //! Without an ease, a flowing mark whose rows moved still begins a transition, of no length: it //! shows the new state at once, and its comets carry their travel over to the rows' new places. +use std::sync::Arc; + use super::View; use super::clock::Clock; -use super::marks::Shown; +use super::drawing::{Drawing, Holds}; +use super::marks::{Holding, Shown}; use crate::data::{PAGE_ROWS, RowIds, Table}; +use crate::derive::order::PAGE; use crate::gpu::Inner; use crate::gpu::shaders::{self as wgsl, Shader}; use crate::scene::mark::MarkKind; @@ -55,9 +59,8 @@ pub(crate) fn remap(old: &Table, new: &Table) -> Option<(Vec, Vec)> { return None; } if old.order == new.order && new.len() >= old.len() { - // Rows kept in place, those of pages rewritten aside: there a row whose id changed enters, as - // one a table drawn through tiles writes where a tile drawn before was, and the row it - // replaces leaves. + // Rows kept in place, those of pages rewritten aside: there a row whose id changed enters, and + // the row it replaces leaves. let mut sources: Vec = (0..new.len()).map(|r| if r < old.len() { r as u32 } else { wgsl::NONE }).collect(); let mut exits = Vec::new(); for block in rewritten(old, new).unwrap_or_default() { @@ -111,26 +114,117 @@ pub(crate) fn rewritten(old: &Table, new: &Table) -> Option> { } } -/// The rows of `new` replacing every row of `old`, as when another dataset of the same name replaces -/// the one drawn: each enters, and each old row leaves, whatever its id. -pub(crate) fn replaced(old: &Table, new: &Table) -> (Vec, Vec) { - (vec![wgsl::NONE; new.len()], (0..old.len() as u32).collect()) +/// How rows moved from what `old` held to what `new` holds: each new held row's old held row +/// (`NONE` if it entered), and the old rows drawn that left. `None` when every row kept its place, as +/// a table drawn in part keeps its rows' places while its order serves, whatever the camera draws. +pub(crate) fn moved(old: &Holding, new: &Holding) -> Option<(Vec, Vec)> { + if old.origin != new.origin { + // Another dataset of the same name: every row enters, and every row drawn leaves. + return Some((vec![wgsl::NONE; held(new) as usize], drawn(old))); + } + match (&old.drawing, &new.drawing) { + (None, None) => remap(&old.table, &new.table), + (Some(a), Some(b)) if a.order.id() == b.order.id() && old.table.order == new.table.order => None, + _ => Some(follow(old, new)), + } +} + +/// Rows held by the table's own order or a drawing's. +fn held(holding: &Holding) -> u32 { + holding.drawing.as_ref().map_or(holding.table.len() as u32, |d| d.live()) +} + +/// The held rows `holding` draws, but those fading out with their tiles. +fn drawn(holding: &Holding) -> Vec { + match &holding.drawing { + None => (0..holding.table.len() as u32).collect(), + Some(drawing) => drawing + .runs + .iter() + .filter(|run| run.fade != wgsl::FADE_OUT) + .flat_map(|run| run.held..run.held + run.len) + .collect(), + } +} + +/// How rows moved between holdings of different orders: each row followed to where `old` held it, +/// by id where rows came or went. +fn follow(old: &Holding, new: &Holding) -> (Vec, Vec) { + let same = old.table.order == new.table.order; + let source = |h: u32| match &new.drawing { + Some(drawing) => drawing.source(h), + None => (h < new.table.len() as u32).then_some(h), + }; + let held_of = |r: usize| match &old.drawing { + Some(drawing) => drawing.held_row(r as u32), + None => (r < old.table.len()).then_some(r as u32), + }; + let mut kept = vec![false; held(old) as usize]; + let sources: Vec = (0..held(new)) + .map(|h| { + let Some(r) = source(h).map(|r| r as usize) else { return wgsl::NONE }; + let was = if same { + (r < old.table.len()).then_some(r) + } else { + new.table.ids.key(r).and_then(|key| old.table.row(key)) + }; + let Some(at) = was.and_then(held_of) else { return wgsl::NONE }; + if let Some(k) = kept.get_mut(at as usize) { + *k = true; + } + at + }) + .collect(); + let exits = drawn(old).into_iter().filter(|&h| !kept.get(h as usize).copied().unwrap_or(false)).collect(); + (sources, exits) +} + +/// Whether every new row is where it was, or entered. +pub(crate) fn in_place(sources: &[u32]) -> bool { + sources.iter().enumerate().all(|(r, &s)| s == r as u32 || s == wgsl::NONE) +} + +/// The slots of `new` holding rows `old` did not hold there: every slot holding a page without one +/// before, those whose page took rows appended since, and those of rows kept that changed. +pub(crate) fn bound(old: Option<&Drawing>, new: &Drawing) -> Vec { + let grown = old.map_or(u32::MAX, |old| old.order.len() / PAGE); + let appended = old.is_some_and(|o| o.order.len() != new.order.len()); + (0..new.slot_count()) + .filter(|&s| match new.holds(s) { + Holds::Free => false, + held => { + let alike = old.is_some_and(|o| o.holds_alike(new, s)); + !alike || matches!(held, Holds::Page(page) if appended && page >= grown) + } + }) + .map(|s| s as u32) + .collect() +} + +/// Whether two holdings hold the same rows the same way: the same table's rows, laid out alike. +pub(crate) fn holds_alike(a: &Holding, b: &Holding) -> bool { + let same_drawing = match (&a.drawing, &b.drawing) { + (None, None) => true, + (Some(x), Some(y)) => Arc::ptr_eq(x, y), + _ => false, + }; + a.origin == b.origin && a.table.structure == b.table.structure && a.table.order == b.table.order && same_drawing } impl Shown { /// Takes over the rows `before` holds, as a changed description of the same mark. pub(crate) fn inherit(&mut self, before: &Shown) { self.region = before.region; - self.held = before.held; + self.before = before.before; self.rows = before.rows; - self.drawn.clone_from(&before.drawn); + self.holding.clone_from(&before.holding); self.version = before.version + 1; self.place_version = before.place_version + 1; } /// Starts easing from what the mark shows: its rows moved as `moved` says, if they did. pub(crate) fn begin(&mut self, moved: Option<(Vec, Vec)>) { - if self.held == 0 { + if self.before == 0 { return; } let fades = moved.as_ref().is_some_and(|(sources, _)| sources.iter().all(|&s| s == wgsl::NONE)); @@ -145,6 +239,20 @@ impl Shown { self.begin = Some(Begin { sources, fades }); self.all_dirty(); } + + /// Starts the running transition over from what the mark shows, as slots `bound` of held rows + /// take pages of rows that enter: the rest ease on toward where they settle, which their channels + /// evaluated already, and rows that left go on fading out past the live ones. + pub(crate) fn restart(&mut self, bound: &[u32]) { + let mut sources: Vec = (0..self.live()).collect(); + for &slot in bound { + let rows = slot * PAGE..((slot + 1) * PAGE).min(sources.len() as u32); + sources[rows.start as usize..rows.end as usize].fill(wgsl::NONE); + } + // The rows that left lay past the live rows when the frame began. + sources.extend(self.before.saturating_sub(self.exits)..self.before); + self.begin = Some(Begin { sources: Some(sources), fades: false }); + } } impl View { @@ -223,7 +331,7 @@ impl View { match begin.sources { None => { let source = if shown.moved.is_some() { resources.retired_state() } else { None }; - let rows = shown.held.min(shown.rows); + let rows = shown.before.min(shown.live()); resources.copy_frozen(encoder, source, was, now, rows); } Some(sources) => { @@ -258,7 +366,7 @@ impl View { for &i in &gathers { let shown = &self.marks[i]; pass.set_bind_group(1, &groups.mark, &[super::passes::offset(i)]); - super::passes::dispatch(&mut pass, shown.rows + shown.exits); + super::passes::dispatch(&mut pass, shown.held()); } } @@ -274,20 +382,20 @@ impl View { } let vertices = self.marks.iter().filter(|m| m.kind() == MarkKind::Vertices); let old_slot = |m: &Shown| m.moved.unwrap_or(m.region).state / wgsl::VERTEX_WORDS; - let size = vertices.clone().map(|m| old_slot(m) + m.held).max().unwrap_or(0); + let size = vertices.clone().map(|m| old_slot(m) + m.before).max().unwrap_or(0); let mut table = vec![wgsl::NONE; size as usize]; for m in vertices { let (from, to) = (old_slot(m), m.region.state / wgsl::VERTEX_WORDS); match m.begin.as_ref().and_then(|b| b.sources.as_ref()) { Some(sources) => { for (new_row, &old_row) in sources.iter().enumerate() { - if old_row != wgsl::NONE && old_row < m.held { + if old_row != wgsl::NONE && old_row < m.before { table[(from + old_row) as usize] = to + new_row as u32; } } } None => { - for row in 0..m.held.min(m.rows) { + for row in 0..m.before.min(m.live()) { table[(from + row) as usize] = to + row; } } diff --git a/crates/latkit/src/view/uniforms.rs b/crates/latkit/src/view/uniforms.rs index 6b15e7f4..2aba01b8 100644 --- a/crates/latkit/src/view/uniforms.rs +++ b/crates/latkit/src/view/uniforms.rs @@ -207,6 +207,10 @@ pub(crate) fn mark( }; mark.rows = shown.rows; mark.exits = shown.exits; + mark.held = shown.held(); + mark.row_runs = shown.offsets.runs; + mark.drawn = shown.drawn_rows.at.map_or(wgsl::NONE, |[start, _]| start); + mark.reveal = shown.reveal; mark.state = region.state; mark.stride = shown.stride(); mark.extra = region.extra; diff --git a/crates/latkit/src/view/variants.rs b/crates/latkit/src/view/variants.rs index 1fc777e4..4d129e6e 100644 --- a/crates/latkit/src/view/variants.rs +++ b/crates/latkit/src/view/variants.rs @@ -180,6 +180,10 @@ fn stages(scene: &Scene, format: wgpu::TextureFormat, samples: u32) -> Stages { if any(|m| matches!(m, Mark::Edges(_) | Mark::Lines(_) | Mark::Areas(_))) { stages.draw.extend(["scan_blocks", "scan_sums", "add_sums"].map(|e| compute(Shader::Lists, e))); } + // Any table may come to draw in part, as its data grows past the budget or orders it by priority. + if !scene.marks.is_empty() { + stages.draw.push(compute(Shader::Lists, "expand_runs")); + } for mark in &scene.marks { mark_stages(mark, scene, format, samples, &mut stages); } diff --git a/crates/latkit/src/web.rs b/crates/latkit/src/web.rs index 5a5eac15..003436c3 100644 --- a/crates/latkit/src/web.rs +++ b/crates/latkit/src/web.rs @@ -210,12 +210,18 @@ fn release_idle(frames: &Rc>) { FRAMES.with(|all| all.borrow_mut().retain(|f| !Rc::ptr_eq(f, frames))); } -/// Asks for an animation frame of the `Gpu` whose shared state lies at `gpu`: what a view's waker does -/// when work it waited on lands, always on the page's one thread. +/// Asks for an animation frame of the `Gpu` whose shared state lies at `gpu`, waking what waits for +/// its work to land: what a view's waker does when work it waited on lands, always on the page's one +/// thread. struct Wakes { gpu: usize, } +/// What wakes `gpu`'s frames, and what waits for its work to land ([`landed`]), as work lands. +pub(crate) fn waker(gpu: &Gpu) -> std::task::Waker { + std::task::Waker::from(Arc::new(Wakes { gpu: Arc::as_ptr(&gpu.0) as usize })) +} + impl std::task::Wake for Wakes { fn wake(self: Arc) { self.wake_by_ref(); @@ -326,7 +332,7 @@ impl PageView { pub fn new(mut view: View) -> PageView { let gpu = view.gpu().clone(); // The view sleeps while it waits; what it waits on asks for the next frame as it lands. - view.set_waker(std::task::Waker::from(Arc::new(Wakes { gpu: Arc::as_ptr(&gpu.0) as usize }))); + view.set_waker(waker(&gpu)); if gpu.0.lock().host.is_none() { gpu.set_host(crate::host::Browser); } diff --git a/crates/latkit/tests/gestures.rs b/crates/latkit/tests/gestures.rs index f0d9423d..73e211de 100644 --- a/crates/latkit/tests/gestures.rs +++ b/crates/latkit/tests/gestures.rs @@ -417,6 +417,28 @@ fn the_wheel_zooms_as_the_controls_say() { assert!(!view.input(&wheel(true), common::now()), "nothing zooms"); } +#[test] +fn zooming_at_the_fits_limit_moves_nothing() { + let Some(gpu) = gpu() else { return }; + let mut view = view(&gpu, Controls { motion: Motion::Reduced, ..Controls::default() }); + view.fit(latkit::Fit::All); + frame(&gpu, &mut view); + let Camera::Map { zoom: framed, .. } = *view.camera() else { panic!("a diagram's camera") }; + // Just short of the deepest zoom what was framed allows, the wheel turns toward a corner. + let near = Camera::Map { center: [5.0, -3.0, 0.0], zoom: framed + 7.95, pitch: 0.0, bearing: 0.0 }; + view.set_camera(near); + let wheel = Input::Wheel { point: [C + 30.0, C - 20.0], delta: [0.0, -400.0], modifiers: Modifiers::default() }; + view.input(&wheel, common::now()); + step(&gpu, &mut view); + let at_limit = *view.camera(); + assert!((zoom(&view) - (framed + 8.0)).abs() < 1e-9, "the zoom stops at its limit: {}", zoom(&view)); + for _ in 0..3 { + view.input(&wheel, common::now()); + step(&gpu, &mut view); + assert_eq!(*view.camera(), at_limit, "past the limit the camera holds still, center and all"); + } +} + fn view_off(gpu: &latkit::Gpu) -> View { view(gpu, Controls { zoom: Zoom::Off, ..Controls::default() }) } diff --git a/crates/latkit/tests/picks.rs b/crates/latkit/tests/picks.rs index 7215c5e9..d60275f1 100644 --- a/crates/latkit/tests/picks.rs +++ b/crates/latkit/tests/picks.rs @@ -76,6 +76,28 @@ fn picks_list_every_row_in_reach_nearest_first() { assert_eq!(errors(&mut view), Vec::new()); } +#[test] +fn a_hit_names_the_row_and_place_its_frame_drew() { + let Some(gpu) = gpu() else { return }; + let mut view = view(&gpu, Controls::default()); + let c = SIZE as f32 / 2.0; + let pending = view.pick([c, c], 6.0); + // The frame answering draws node 1 at the middle; before its answer lands, the nodes are given + // in another order and the camera moves away. + step(&gpu, &mut view); + let mut data = graph(); + let reordered = + Table::new("Node", vec![3_u64, 2, 1]).field("x", vec![-40.0, 10.0, 0.0]).field("y", vec![30.0, 0.0, 0.0]); + data.apply(Patch::new().replace(reordered)).expect("valid"); + view.set_data(data); + view.set_camera(Camera::Map { center: [500.0, 0.0, 0.0], zoom: 0.0, pitch: 0.0, bearing: 0.0 }); + let hits = answer(&gpu, &mut view, pending); + let first = hits.first().expect("a hit"); + assert_eq!(first.row, latkit::Row::new("graph", "Node", 1_u64), "{hits:?}"); + assert!(first.position[0].abs() < 1.0, "where its frame drew it: {:?}", first.position); + assert_eq!(errors(&mut view), Vec::new()); +} + #[test] fn every_hit_of_geodesic_edges_lies_in_reach() { let Some(gpu) = gpu() else { return }; diff --git a/crates/latkit/tests/scale.rs b/crates/latkit/tests/scale.rs index 3085fa33..1d3b6265 100644 --- a/crates/latkit/tests/scale.rs +++ b/crates/latkit/tests/scale.rs @@ -8,8 +8,8 @@ mod common; use common::{SIZE, at, camera, close, errors, frame, gpu, plain_theme, plane}; use latkit::{ - Axis, Budget, Camera, Color, Data, Edges, Fit, Frame, Ids, Lines, Patch, Plot, Row, Scene, Table, Vertices, View, - field, when, + Areas, Axis, Budget, Camera, Color, Data, Edges, Fit, Frame, Ids, Lines, Patch, Plot, Row, Scene, Table, Vertices, + View, field, when, }; /// A grid of `columns` by `rows` vertices a unit apart, from the origin, and an edge from each to @@ -120,24 +120,24 @@ fn panning_a_dense_table_draws_every_row_in_view_as_tiles_come_and_go() { view.set_data(lattice(columns, rows)); let center = SIZE / 2; let camera = |x: f64| Camera::Map { center: [x, 250.0, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }; - let mut in_place = 0; + let mut whole = 0; for step in 0..6 { view.set_camera(camera(100.0 + 60.0 * f64::from(step))); - // While the rows still drawn stay where they were, those arriving in holes and past the last - // row, every row in view draws. + // Every frame that waits on no page draws every row in view: what a camera move draws shows + // whole, the edges with their ends. for _ in 0..4 { let pixels = common::step(&gpu, &mut view); let waiting = view.stats().waiting; - if step > 0 && !waiting.data && !waiting.marks && !view.complete() { + if step > 0 && !waiting.data && !waiting.marks { let pixel = |dx: u32, dy: u32| at(&pixels, center + dx, center + dy); - assert!(close(pixel(0, 0), [255, 255, 255, 255]), "step {step}, in place: {:?}", pixel(0, 0)); - assert!(close(pixel(4, 0), [0, 255, 0, 255]), "step {step}, in place: {:?}", pixel(4, 0)); - in_place += 1; + assert!(close(pixel(0, 0), [255, 255, 255, 255]), "step {step}, as it lands: {:?}", pixel(0, 0)); + assert!(close(pixel(4, 0), [0, 255, 0, 255]), "step {step}, as it lands: {:?}", pixel(4, 0)); + whole += 1; } } let pixels = frame(&gpu, &mut view); assert_eq!(errors(&mut view), []); - assert!(view.complete(), "every row laid out in drawing order once the camera rests"); + assert!(view.complete(), "every row in view draws once the camera rests"); let stats = view.stats(); assert!(stats.drawn > 0 && stats.drawn < stats.rows / 4, "step {step}: {} of {}", stats.drawn, stats.rows); let pixel = |dx: u32, dy: u32| at(&pixels, center + dx, center + dy); @@ -145,8 +145,16 @@ fn panning_a_dense_table_draws_every_row_in_view_as_tiles_come_and_go() { assert!(close(pixel(4, 4), [0, 0, 0, 255]), "step {step}: none between rows: {:?}", pixel(4, 4)); assert!(close(pixel(4, 0), [0, 255, 0, 255]), "step {step}: an edge between two: {:?}", pixel(4, 0)); } - assert!(in_place > 0, "rows were written in place"); - // A row picked by id is found among the rows drawn, holes and all. + assert!(whole > 0, "frames drew as their pages landed"); + // Back where the camera began: its pages are held still, so nothing packs, uploads, or evaluates + // again. + let before = gpu.work(); + view.set_camera(camera(100.0)); + let pixels = frame(&gpu, &mut view); + let work = gpu.work() - before; + assert_eq!((work.packed, work.uploaded, work.evaluated), (0, 0, 0), "the way back costs nothing"); + assert!(close(at(&pixels, center, center), [255, 255, 255, 255]), "{:?}", at(&pixels, center, center)); + // A row picked by id is found among the rows drawn. let selected = Row::new("grid", "Bus", (250 * columns + 400) as u64); view.select([selected.clone().into()]); view.set_camera(camera(380.0)); @@ -155,6 +163,342 @@ fn panning_a_dense_table_draws_every_row_in_view_as_tiles_come_and_go() { assert_eq!(errors(&mut view), []); } +#[test] +fn appending_to_a_table_drawn_through_tiles_never_thins_it() { + let Some(gpu) = gpu() else { return }; + let (columns, rows) = (600, 500); + let mut data = lattice(columns, rows); + let scene = Scene::new(plane()) + .theme(plain_theme()) + .ease(std::time::Duration::ZERO) + .draw(Vertices::of("grid", "Bus").position("x", "y").radius(2.0).color(Color::rgb(1.0, 1.0, 1.0))); + let mut view = View::new(&gpu, scene).expect("valid scene"); + view.set_camera(Camera::Map { center: [300.0, 250.0, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }); + view.set_data(data.clone()); + frame(&gpu, &mut view); + assert!(view.complete(), "its pyramid serves"); + let drawn = view.stats().drawn; + let center = SIZE / 2; + let before = gpu.work(); + for added in 0..4_u32 { + // A row a step, far from the view: until a pyramid holds it, it draws after the rest. + let id = (columns * rows) as u64 + u64::from(added); + let row = Table::new("Bus", vec![id]).field("x", vec![-50.0]).field("y", vec![f64::from(added)]); + data.apply(row).expect("valid"); + view.set_data(data.clone()); + let pixels = common::step(&gpu, &mut view); + assert_eq!(errors(&mut view), []); + let pixel = |dx: u32| at(&pixels, center + dx, center); + assert!(close(pixel(0), [255, 255, 255, 255]), "append {added}: a row at the middle: {:?}", pixel(0)); + assert!(close(pixel(8), [255, 255, 255, 255]), "append {added}: and its neighbor: {:?}", pixel(8)); + assert!(view.stats().drawn >= drawn, "append {added}: {} drawn, {drawn} before", view.stats().drawn); + } + // The pyramid serves the rows appended as they come, and only the page they land in evaluates. + let work = gpu.work() - before; + assert_eq!(work.jobs, 0, "rows appended level nothing until they are many"); + assert!(work.evaluated <= 4 << 12, "a page each: {}", work.evaluated); +} + +#[test] +fn a_change_to_one_rows_color_moves_no_row() { + let Some(gpu) = gpu() else { return }; + // A grid on the globe, drawn in part: its lines are great circles, whose pieces follow where the + // buses lie. + let (columns, rows) = (600, 500); + let n = columns * rows; + let links: Vec = (0..n).filter(|i| i % columns + 1 < columns).collect(); + let mut data = Data::new("grid"); + data.apply( + Patch::new() + .set( + Table::new("Bus", Ids::count(n as u32)) + .field("lon", (0..n).map(|i| -170.0 + 340.0 * (i % columns) as f64 / 599.0).collect::>()) + .field("lat", (0..n).map(|i| -75.0 + 150.0 * (i / columns) as f64 / 499.0).collect::>()) + .field("v", vec![0.0; n]), + ) + .set( + Table::new("Line", Ids::count(links.len() as u32)) + .reference("from", "Bus", links.iter().map(|&i| i as u64).collect::>()) + .reference("to", "Bus", links.iter().map(|&i| i as u64 + 1).collect::>()), + ), + ) + .expect("valid"); + let scene = Scene::new(latkit::Earth::globe()) + .theme(plain_theme()) + .ease(std::time::Duration::ZERO) + .draw(Edges::of("grid", "Line").ends("from", "to")) + .draw( + Vertices::of("grid", "Bus") + .position("lon", "lat") + .color(field("v").colormap(latkit::Colormap::GRAYS).domain(0.0..=1.0)), + ); + let mut view = View::new(&gpu, scene).expect("valid scene"); + view.set_camera(Camera::Map { center: [10.0, 10.0, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }); + view.set_data(data.clone()); + frame(&gpu, &mut view); + assert_eq!(errors(&mut view), []); + assert!(view.stats().drawn < view.stats().rows / 4, "the grid draws in part"); + // One bus far off view, one in it: neither moves a line's pieces, so nothing is read back. + for id in [0, 250 * columns + 300] { + let before = gpu.work(); + data.apply(Table::new("Bus", vec![id as u64]).field("v", vec![1.0])).expect("valid"); + view.set_data(data.clone()); + frame(&gpu, &mut view); + let work = gpu.work() - before; + assert_eq!(work.readbacks, 0, "bus {id}: no line counts its pieces again"); + assert!(work.evaluated <= 1 << 12, "bus {id}: a page at most evaluates: {}", work.evaluated); + } +} + +#[test] +fn a_frame_waiting_for_pages_packs_nothing_again() { + let Some(gpu) = gpu() else { return }; + let budget = gpu.budget(); + let scene = Scene::new(plane()).theme(plain_theme()).ease(std::time::Duration::ZERO).draw( + Vertices::of("grid", "Bus").position("x", "y").radius(2.0).color(field("x").colormap(latkit::Colormap::GRAYS)), + ); + let mut view = View::new(&gpu, scene).expect("valid scene"); + view.set_camera(Camera::Map { center: [300.0, 250.0, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }); + view.set_data(lattice(600, 500)); + frame(&gpu, &mut view); + assert_eq!(errors(&mut view), []); + // Pages trickle up a few at a time: what the camera moves to waits for them. + gpu.set_budget(Budget { upload_per_frame: 16 << 10, ..budget }); + view.set_camera(Camera::Map { center: [80.0, 60.0, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }); + let before = gpu.work(); + common::step(&gpu, &mut view); + let packed = (gpu.work() - before).packed; + assert!(packed > 0 && view.stats().waiting.data, "the pages it moved to pack, and wait to upload"); + let mut waited = 0; + while view.stats().waiting.data && waited < common::SETTLE { + common::step(&gpu, &mut view); + waited += 1; + } + assert!(waited > 1, "it waited for pages over frames"); + assert_eq!((gpu.work() - before).packed, packed, "frames waiting packed nothing again"); + gpu.set_budget(budget); +} + +#[test] +fn a_fitted_domain_holds_however_little_of_its_table_draws() { + let Some(gpu) = gpu() else { return }; + // A lattice whose rows hold v = x / 600, and one more far in a corner holding 10, the lowest + // priority, so it lands among the finest rows, which no page drawn about the middle holds. + let (columns, rows) = (600_usize, 500_usize); + let n = columns * rows; + let mut x: Vec = (0..n).map(|i| (i % columns) as f64).collect(); + let mut y: Vec = (0..n).map(|i| (i / columns) as f64).collect(); + let mut v: Vec = x.iter().map(|x| x / 600.0).collect(); + x.push(599.5); + y.push(499.5); + v.push(10.0); + let p: Vec = v.iter().map(|v| -v).collect(); + let mut data = Data::new("grid"); + data.apply(Table::new("Bus", Ids::count(n as u32 + 1)).field("x", x).field("y", y).field("v", v).field("p", p)) + .expect("valid"); + // A blue ground, so a gray row tells from no row. + let theme = latkit::Theme { background: Color::rgb(0.0, 0.0, 1.0), ..plain_theme() }; + let scene = Scene::new(plane()).theme(theme).ease(std::time::Duration::ZERO).draw( + Vertices::of("grid", "Bus") + .position("x", "y") + .radius(3.0) + .priority(field("p")) + .color(field("v").colormap(latkit::Colormap::GRAYS)), + ); + let mut view = View::new(&gpu, scene).expect("valid scene"); + view.set_camera(Camera::Map { center: [300.0, 250.0, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }); + view.set_data(data); + let pixels = frame(&gpu, &mut view); + assert_eq!(errors(&mut view), []); + assert!(view.stats().drawn < view.stats().rows / 4, "the table draws in part"); + // Fitted over every row, 0 to 10: the middle row's half draws near black, not the gray it would + // over the rows held about the view. + let center = at(&pixels, SIZE / 2, SIZE / 2); + let gray = center[0].abs_diff(center[2]) <= 3 && center[1].abs_diff(center[2]) <= 3; + assert!(gray && center[0] < 90, "the middle row draws {center:?}"); +} + +#[test] +fn a_join_into_a_table_drawn_in_part_reads_every_row() { + let Some(gpu) = gpu() else { return }; + let (columns, rows) = (600, 500); + let n = columns * rows; + let mut data = lattice(columns, rows); + // Bus 7 lies far from the view; a probe in the middle reads its value by id. + let v: Vec = (0..n).map(|i| if i == 7 { 1.0 } else { 0.0 }).collect(); + data.apply( + Patch::new() + .set(Table::new("Bus", Ids::count(n as u32)).field("v", v)) + .set(Table::new("Probe", vec![7_u64]).field("x", vec![300.5]).field("y", vec![250.5])), + ) + .expect("valid"); + let gray = Color::rgb(0.2, 0.2, 0.2); + let scene = Scene::new(plane()) + .theme(plain_theme()) + .draw(Vertices::of("grid", "Bus").position("x", "y").radius(1.0).color(gray)) + .draw( + Vertices::of("grid", "Probe") + .position("x", "y") + .radius(4.0) + .color(field("v").of("grid", "Bus").colormap(latkit::Colormap::GRAYS).domain(0.0..=1.0)), + ); + let mut view = View::new(&gpu, scene).expect("valid scene"); + view.set_camera(Camera::Map { center: [300.0, 250.0, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }); + view.set_data(data); + let pixels = frame(&gpu, &mut view); + assert_eq!(errors(&mut view), []); + assert!(view.stats().drawn < view.stats().rows / 4, "the buses draw in part"); + let probe = view.point([300.5, 250.5, 0.0]).expect("in view"); + let pixel = at(&pixels, probe[0] as u32, probe[1] as u32); + assert!(close(pixel, [255, 255, 255, 255]), "bus 7's value, though no tile in view holds it: {pixel:?}"); +} + +#[test] +fn pages_held_while_rows_ease_enter_as_the_rest_ease_on() { + let Some(gpu) = gpu() else { return }; + let camera = |zoom: f64| Camera::Map { center: [300.0, 250.0, 0.0], zoom, pitch: 0.0, bearing: 0.0 }; + let view = |zoom: f64| { + let scene = Scene::new(plane()) + .theme(plain_theme()) + .draw(Vertices::of("grid", "Bus").position("x", "y").radius(2.0).color(Color::rgb(1.0, 1.0, 1.0))); + let mut view = View::new(&gpu, scene).expect("valid scene"); + view.set_camera(camera(zoom)); + view.set_data(lattice(600, 500)); + view + }; + let mut pulled = view(1.0); + let want = frame(&gpu, &mut pulled); + // A sample draws first; then the pyramid lands, and its rows ease in from it... + let mut easing = view(3.0); + common::step(&gpu, &mut easing); + common::step(&gpu, &mut easing); + // ...as the camera pulls back, holding more pages than were held before. + easing.set_camera(camera(1.0)); + let pixels = frame(&gpu, &mut easing); + assert_eq!(errors(&mut easing), []); + let differ = pixels.iter().zip(&want).filter(|(a, b)| !close(**a, **b)).count(); + assert_eq!(differ, 0, "settles to what the camera pulled back at once draws"); +} + +#[test] +fn areas_drawn_in_part_fill_the_pages_drawn_as_the_camera_moves() { + let Some(gpu) = gpu() else { return }; + // A lattice of cells a unit apart, each a square half a unit across: too many to draw whole. + let (columns, rows) = (600_usize, 500_usize); + let cells: Vec>> = (0..columns * rows) + .map(|i| { + let (x, y) = ((i % columns) as f64, (i / columns) as f64); + vec![vec![[x, y], [x + 0.5, y], [x + 0.5, y + 0.5], [x, y + 0.5], [x, y]]] + }) + .collect(); + let mut data = Data::new("map"); + data.apply(Table::new("Cell", Ids::count((columns * rows) as u32)).field("geometry", cells)).expect("valid"); + let scene = Scene::new(plane()) + .theme(plain_theme()) + .ease(std::time::Duration::ZERO) + .draw(Areas::of("map", "Cell").color(Color::rgb(1.0, 0.0, 0.0))); + let mut view = View::new(&gpu, scene).expect("valid scene"); + let camera = |x: f64, y: f64| Camera::Map { center: [x, y, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }; + view.set_data(data); + for (x, y) in [(300.25, 250.25), (80.25, 60.25)] { + view.set_camera(camera(x, y)); + let pixels = frame(&gpu, &mut view); + assert_eq!(errors(&mut view), []); + let stats = view.stats(); + assert!(stats.drawn < stats.rows / 4, "the cells draw in part: {} of {}", stats.drawn, stats.rows); + // The cell at the middle fills, and the gap beside it does not. + let center = SIZE / 2; + assert!(close(at(&pixels, center, center), [255, 0, 0, 255]), "at {x}, {y}: {:?}", at(&pixels, center, center)); + assert!( + close(at(&pixels, center + 4, center), [0, 0, 0, 255]), + "at {x}, {y}: {:?}", + at(&pixels, center + 4, center) + ); + } +} + +#[test] +fn rows_kept_off_screen_hold_pages_of_their_own() { + let Some(gpu) = gpu() else { return }; + let scene = Scene::new(plane()) + .theme(plain_theme()) + .ease(std::time::Duration::ZERO) + .draw(Vertices::of("grid", "Bus").position("x", "y").radius(2.0).color(Color::rgb(1.0, 1.0, 1.0))); + let mut view = View::new(&gpu, scene).expect("valid scene"); + view.set_camera(Camera::Map { center: [300.0, 250.0, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }); + view.set_data(lattice(600, 500)); + frame(&gpu, &mut view); + let (drawn, before) = (view.stats().drawn, gpu.work()); + // A hundred rows along the grid's foot, far from the view, each in another page of the order. + let ids: Vec = (0..100_u64).map(|i| latkit::Id::from(600 * (i % 50) + i * 6)).collect(); + view.select([latkit::Rows { data: "grid".into(), table: "Bus".into(), ids }]); + frame(&gpu, &mut view); + assert_eq!(errors(&mut view), []); + assert_eq!(view.stats().drawn, drawn + 100, "every row kept draws"); + let packed = (gpu.work() - before).packed; + assert!(packed < 4096, "rows kept pack together, not a page of the order each: {packed} rows packed"); +} + +#[test] +fn an_edge_kept_draws_between_its_ends() { + let Some(gpu) = gpu() else { return }; + let budget = gpu.budget(); + // So little room that only the coarsest edges draw. + gpu.set_budget(Budget { bytes: 1 << 20, ..budget }); + let scene = Scene::new(plane()) + .theme(plain_theme()) + .ease(std::time::Duration::ZERO) + .draw(Edges::of("grid", "Line").ends("from", "to").width(2.0).color(Color::rgb(0.0, 1.0, 0.0))) + .draw(Vertices::of("grid", "Bus").position("x", "y").radius(1.0).color(Color::rgb(1.0, 1.0, 1.0))); + let mut view = View::new(&gpu, scene).expect("valid scene"); + view.set_camera(Camera::Map { center: [300.5, 250.0, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }); + view.set_data(lattice(600, 500)); + let center = SIZE / 2; + let green = |p: [u8; 4]| p[1] > 150 && p[0] < 100 && p[2] < 100; + let before = frame(&gpu, &mut view); + assert!(!green(at(&before, center, center)), "the edge is not drawn: {:?}", at(&before, center, center)); + // Line 150050 runs from bus 150300 to bus 150301, (300, 250) to (301, 250): kept, with its ends. + view.select([Row::new("grid", "Line", 150_050_u64).into()]); + let after = frame(&gpu, &mut view); + gpu.set_budget(budget); + assert_eq!(errors(&mut view), []); + assert!(green(at(&after, center, center)), "kept, it draws between its ends: {:?}", at(&after, center, center)); +} + +#[test] +fn lanes_standing_in_for_a_table_drawn_in_part_read_missing_past_their_rows() { + let Some(gpu) = gpu() else { return }; + // Two buses joined twice: their lanes derive on the frame. + let mut pair = Data::new("grid"); + pair.apply( + Patch::new().set(Table::new("Bus", Ids::count(2)).field("x", vec![0.0, 1.0]).field("y", vec![0.0, 0.0])).set( + Table::new("Line", Ids::count(2)).reference("from", "Bus", vec![0_u64, 0]).reference( + "to", + "Bus", + vec![1_u64, 1], + ), + ), + ) + .expect("valid"); + let scene = Scene::new(plane()) + .theme(plain_theme()) + .ease(std::time::Duration::ZERO) + .draw(Edges::of("grid", "Line").ends("from", "to").width(2.0).color(Color::rgb(0.0, 1.0, 0.0))) + .draw(Vertices::of("grid", "Bus").position("x", "y").radius(2.0).color(Color::rgb(1.0, 1.0, 1.0))); + let mut view = View::new(&gpu, scene).expect("valid scene"); + view.set_camera(Camera::Map { center: [300.0, 250.0, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }); + view.set_data(pair); + frame(&gpu, &mut view); + // A grid too large to draw whole replaces it: its lanes take a job, and until it lands the pair's + // stand in, holding none of its rows past the first two. + view.set_data(lattice(600, 500)); + let pixels = frame(&gpu, &mut view); + assert_eq!(errors(&mut view), []); + assert!(view.stats().drawn < view.stats().rows / 4, "the grid draws in part"); + let pixel = at(&pixels, SIZE / 2 + 4, SIZE / 2); + assert!(close(pixel, [0, 255, 0, 255]), "an edge between two buses: {pixel:?}"); +} + #[test] fn a_change_to_one_row_drawn_in_part_writes_that_row_alone() { let Some(gpu) = gpu() else { return }; @@ -281,7 +625,7 @@ fn a_tiny_budget_never_fails() { frame(&gpu, &mut view); assert!(errors(&mut view).is_empty(), "nothing fails"); let stats = view.stats(); - assert!(stats.resident <= 8 << 20, "{} bytes resident", stats.resident); + assert!(gpu.resident() <= 8 << 20, "{} bytes resident", gpu.resident()); assert!(stats.drawn > 0 && stats.drawn < u64::from(n), "{} of {n} rows draw", stats.drawn); let Camera::Map { center, .. } = *view.camera() else { panic!("a map camera") }; assert!((center[0] - 249.5).abs() < 5.0 && (center[1] - 199.5).abs() < 5.0, "a fit frames every row: {center:?}"); @@ -310,8 +654,8 @@ fn traces_too_large_for_the_budget_thin_in_time() { assert_eq!(errors(&mut view), []); let stats = view.stats(); assert_eq!(stats.drawn, rows as u64, "every row traces"); - assert_eq!(stats.uploading, 0, "nothing waits for room"); - assert!(stats.resident <= 8 << 20, "{} bytes resident", stats.resident); + assert_eq!(gpu.uploading(), 0, "nothing waits for room"); + assert!(gpu.resident() <= 8 << 20, "{} bytes resident", gpu.resident()); for x in [SIZE / 4, SIZE / 2, SIZE * 3 / 4] { assert!(close(at(&pixels, x, SIZE / 2), [255, 0, 0, 255]), "at {x}: {:?}", at(&pixels, x, SIZE / 2)); } diff --git a/docs/_static/performance.csv b/docs/_static/performance.csv index 517421c8..07195f74 100644 --- a/docs/_static/performance.csv +++ b/docs/_static/performance.csv @@ -1,15 +1,15 @@ Scenario,Largest,Open (ms),Frame (ms),GPU (ms),Pick (ms) -"A first visit: latkit, the GPU, and pipelines made without a shader cache","10,000 rows",353,,, -"Vertices on a plane, colored through a colormap","2,000,000 rows","1,001",0.2,0.5,14.5 -"A lattice grid on the globe: buses, and the geodesic lines between them","2,000,000 buses","1,460",0.2,1.6,16.1 -"Every vertex labeled: placement, collisions, and the texts set","1,000,000 rows",781,1.7,1.6, -Edges by the thousand between the same sixteen pairs of vertices,"100,000 edges",816,0.2,19.8,70.8 -"Polygons of 33 points on the mercator map, triangulated in jobs","30,000 polygons",780,0.1,1.2,6.2 -"Vector tiles fetched as the camera asks for them, from a local server","4,096 squares a tile",512,2.8,0.2, -"Blocks with ports in trees of 64, laid out in layers, wires routed around them","10,000 blocks",581,0.1,0.2, -"A plot of traces 1,024 frames long, fed a frame every animation frame","10,000 traces",548,0.3,23.6,19.6 -One trace with a long history: levels of detail,"1,000,000 frames",441,0.2,0.4, -"Every row moving to a new place at once, easing over a second","1,000,000 rows",817,0.1,0.2, -"Views of one GPU drawing one dataset: pages shared, not copied",8 views,679,0.5,2.8, -"A 16 MiB budget: what draws fits, through tiles, and nothing fails","2,000,000 buses","1,516",0.2,0.6, -"Pictures and video of two views of a 100,000-bus grid","8,294,400 pixels",543,,, +"A first visit: latkit, the GPU, and pipelines made without a shader cache","10,000 rows",454,,, +"Vertices on a plane, colored through a colormap","2,000,000 rows",747,0.2,0.5,13.6 +"A lattice grid on the globe: buses, and the geodesic lines between them","2,000,000 buses","1,635",0.3,1.3,13.7 +"Every vertex labeled: placement, collisions, and the texts set","1,000,000 rows",784,1.7,1.1, +Edges by the thousand between the same sixteen pairs of vertices,"100,000 edges",497,0.1,20.0,72.6 +"Polygons of 33 points on the mercator map, triangulated in jobs","30,000 polygons",705,0.1,1.2,6.1 +"Vector tiles fetched as the camera asks for them, from a local server","4,096 squares a tile",550,0.3,0.2, +"Blocks with ports in trees of 64, laid out in layers, wires routed around them","10,000 blocks",594,0.1,0.2, +"A plot of traces 1,024 frames long, fed a frame every animation frame","10,000 traces",561,0.3,24.0,22.1 +One trace with a long history: levels of detail,"1,000,000 frames",535,0.1,0.4, +"Every row moving to a new place at once, easing over a second","1,000,000 rows",687,0.1,0.2, +"Views of one GPU drawing one dataset: pages shared, not copied",8 views,555,0.3,2.8, +"A 16 MiB budget: what draws fits, through tiles, and nothing fails","2,000,000 buses","1,573",0.2,0.2, +"Pictures and video of two views of a 100,000-bus grid","8,294,400 pixels",652,,, diff --git a/docs/guide.md b/docs/guide.md index 33cbddee..0d51d704 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -328,12 +328,15 @@ view.setData(grid); ### Stats and memory -`view.stats` tells rows drawn, draw calls, tiles, bytes resident and uploading, and CPU and GPU -milliseconds a frame. `gpu.work` counts exactly what the GPU did: bytes uploaded and written, rows -evaluated, readbacks, jobs, texts, and pipelines. `gpu.budget = { bytes, uploadPerFrame }` sets how -much memory data takes. The GPU's views share it: a view gets what it asks while an even split -covers it, and the rest split what is left; pages several views draw count once. A table too large -for its view's share draws through the tiles in view. +`view.stats` tells rows drawn, draw calls, tiles, and CPU and GPU milliseconds a frame; +`gpu.resident` and `gpu.uploading` the bytes of data on the GPU and waiting to upload, for every +view of it. `gpu.work` counts exactly what the GPU did: bytes uploaded and written, rows evaluated +and packed, readbacks, jobs, texts, and pipelines. `gpu.budget = { bytes, uploadPerFrame }` sets how +much memory data takes, and how much of it a frame uploads. The GPU's views share it: a view gets +what it asks while an even split covers it, and the rest split what is left; pages several views +draw count once. A table too large for its view's share draws through the tiles in view, a page of +its order at a time; a page it drew stays on the GPU while its share leaves room, so the camera +coming back draws it again for nothing. A frame does the work of what changed: a patch evaluates only the pages it wrote, unless a fitted domain or a join reads them; a transition evaluates once and blends each frame; and rows drawn diff --git a/docs/performance.md b/docs/performance.md index aa12a854..5445ce74 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -29,4 +29,10 @@ The browser gate opens every example page cold and moves the pointer over it. It their text as compile workers free up, and the rest a few at a time. - A pipeline compiles only what its mark reads and is: the primitive it draws, its joins, sampled fields, and view state. -- A table too large to draw whole draws the tiles the camera asks for, within the GPU's budget. +- A table too large to draw whole draws the tiles the camera asks for, within the GPU's budget, a + page of its order at a time. A page keeps its place on the GPU while the camera moves, and stays + there while the budget leaves room once the camera moves on, so coming back costs nothing. +- What the view derives from the data, as lanes, triangles, and fitted domains, follows the data, + never the camera: a page's triangles are made the first time it draws. +- A write costs the pages holding the rows it changed, and appending the page the rows land in: + tiles are made again only once the rows appended since are an eighth as many as they hold. diff --git a/docs/reference/api.md b/docs/reference/api.md index fddd44e9..d01fe7b6 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -584,6 +584,10 @@ class Gpu { set budget(budget: Budget); /** What this GPU did since it was created, counted exactly; see {@link Work}. */ get work(): Work; + /** Bytes on the GPU every view of it draws from: data, and what each view keeps per row. */ + get resident(): number; + /** Bytes waiting to upload, for every view of this GPU. */ + get uploading(): number; /** * Draws one frame now at `now`, milliseconds on the clock animation frames give: the frame * listeners, then every canvas that needs a frame. From the first step no animation frames are diff --git a/docs/reference/types.md b/docs/reference/types.md index fa5d47bc..fc507a68 100644 --- a/docs/reference/types.md +++ b/docs/reference/types.md @@ -1731,12 +1731,6 @@ interface Stats { tiles: number; /** Tiles being fetched or decoded, of every tile source. */ tilesLoading: number; - /** Bytes of data waiting to upload. */ - uploading: number; - /** Bytes of data pages on the GPU, for every view of its `Gpu`. */ - resident: number; - /** Bytes the GPU's `Budget` lets data take. */ - budget: number; /** * The work under way the last frame stood in for; nothing when it drew everything as the data, * scene, and camera say: see `View::complete`. @@ -1776,6 +1770,11 @@ interface Work { written: number; /** Rows whose channels the GPU evaluated. */ evaluated: number; + /** + * Rows packed into pages of the order a table drawn in part draws in: each page once, the first + * time a view draws it, and again only where a write changed its rows. + */ + packed: number; /** * Reads back from the GPU: probes, queries, fits, label placements, segment counts, pictures; not * the frame timer's, which only some devices have. diff --git a/npm/examples/src/examples/networks/index.ts b/npm/examples/src/examples/networks/index.ts index 9ff27218..7950371d 100644 --- a/npm/examples/src/examples/networks/index.ts +++ b/npm/examples/src/examples/networks/index.ts @@ -393,7 +393,7 @@ export default async function page(stage: Stage): Promise { meter.note(() => `${thousands(view.stats.drawn)} of ${thousands(view.stats.rows)} rows drawn`); meter.note( () => - `${(view.stats.resident / 1e6).toFixed(0)} of ${(view.stats.budget / 1e6).toFixed(0)} MB on the GPU`, + `${(stage.gpu.resident / 1e6).toFixed(0)} of ${(stage.gpu.budget.bytes / 1e6).toFixed(0)} MB on the GPU`, ); function showLegend(): void { diff --git a/npm/schema.json b/npm/schema.json index 3bd58006..2679454e 100644 --- a/npm/schema.json +++ b/npm/schema.json @@ -4355,24 +4355,6 @@ "minimum": 0, "description": "Tiles being fetched or decoded, of every tile source." }, - "uploading": { - "type": "integer", - "format": "uint64", - "minimum": 0, - "description": "Bytes of data waiting to upload." - }, - "resident": { - "type": "integer", - "format": "uint64", - "minimum": 0, - "description": "Bytes of data pages on the GPU, for every view of its `Gpu`." - }, - "budget": { - "type": "integer", - "format": "uint64", - "minimum": 0, - "description": "Bytes the GPU's [`Budget`](crate::Budget) lets data take." - }, "waiting": { "$ref": "#/outputs/Waiting", "description": "The work under way the last frame stood in for; nothing when it drew everything as the data,\nscene, and camera say: see [`View::complete`](crate::View::complete)." @@ -4396,9 +4378,6 @@ "instances", "tiles", "tilesLoading", - "uploading", - "resident", - "budget", "waiting", "cpuMs", "gpuMs" @@ -4426,6 +4405,12 @@ "minimum": 0, "description": "Rows whose channels the GPU evaluated." }, + "packed": { + "type": "integer", + "format": "uint64", + "minimum": 0, + "description": "Rows packed into pages of the order a table drawn in part draws in: each page once, the first\ntime a view draws it, and again only where a write changed its rows." + }, "readbacks": { "type": "integer", "format": "uint64", @@ -4461,6 +4446,7 @@ "uploaded", "written", "evaluated", + "packed", "readbacks", "jobs", "texts", diff --git a/npm/src/outputs.ts b/npm/src/outputs.ts index 9741df92..d6e4f221 100644 --- a/npm/src/outputs.ts +++ b/npm/src/outputs.ts @@ -122,12 +122,6 @@ export interface Stats { tiles: number; /** Tiles being fetched or decoded, of every tile source. */ tilesLoading: number; - /** Bytes of data waiting to upload. */ - uploading: number; - /** Bytes of data pages on the GPU, for every view of its `Gpu`. */ - resident: number; - /** Bytes the GPU's `Budget` lets data take. */ - budget: number; /** * The work under way the last frame stood in for; nothing when it drew everything as the data, * scene, and camera say: see `View::complete`. @@ -278,6 +272,11 @@ export interface Work { written: number; /** Rows whose channels the GPU evaluated. */ evaluated: number; + /** + * Rows packed into pages of the order a table drawn in part draws in: each page once, the first + * time a view draws it, and again only where a write changed its rows. + */ + packed: number; /** * Reads back from the GPU: probes, queries, fits, label placements, segment counts, pictures; not * the frame timer's, which only some devices have. diff --git a/npm/src/runtime.ts b/npm/src/runtime.ts index ed53198a..4290e71f 100644 --- a/npm/src/runtime.ts +++ b/npm/src/runtime.ts @@ -319,6 +319,16 @@ export class Gpu { return JSON.parse(this.inner.work) as Work; } + /** Bytes on the GPU every view of it draws from: data, and what each view keeps per row. */ + get resident(): number { + return this.inner.resident; + } + + /** Bytes waiting to upload, for every view of this GPU. */ + get uploading(): number { + return this.inner.uploading; + } + /** * Draws one frame now at `now`, milliseconds on the clock animation frames give: the frame * listeners, then every canvas that needs a frame. From the first step no animation frames are From 60bf09d08b97ba63590dea4a6dcea59fddb9a6bd Mon Sep 17 00:00:00 2001 From: Wyatt Lowery Date: Sat, 24 Oct 2026 08:43:58 -0500 Subject: [PATCH 02/14] performance enhancments --- benchmark/judge.ts | 4 +- benchmark/page/stage.ts | 2 + crates/latkit-js/src/lib.rs | 31 +- crates/latkit-js/src/shot.rs | 17 +- crates/latkit/build/shaders.rs | 10 +- crates/latkit/shaders/compute/bounds.wesl | 2 +- crates/latkit/shaders/compute/candidates.wesl | 30 +- crates/latkit/shaders/compute/evaluate.wesl | 136 ++--- crates/latkit/shaders/compute/freeze.wesl | 67 ++- crates/latkit/shaders/compute/labels.wesl | 58 +- crates/latkit/shaders/compute/normals.wesl | 25 +- crates/latkit/shaders/compute/probe.wesl | 4 +- crates/latkit/shaders/draw/glyphs.wesl | 14 +- crates/latkit/shaders/draw/points.wesl | 4 +- crates/latkit/shaders/draw/triangles.wesl | 12 +- crates/latkit/shaders/lib/bind.wesl | 36 +- crates/latkit/shaders/lib/focus.wesl | 8 +- crates/latkit/shaders/lib/path.wesl | 101 +++- crates/latkit/shaders/lib/placement.wesl | 23 +- crates/latkit/shaders/lib/points.wesl | 4 +- crates/latkit/shaders/lib/read.wesl | 31 +- crates/latkit/shaders/lib/state.wesl | 81 ++- crates/latkit/shaders/lib/trace.wesl | 8 +- crates/latkit/shaders/lib/triangles.wesl | 12 +- crates/latkit/shaders/lib/types.wesl | 72 ++- crates/latkit/src/data/bytes.rs | 27 +- crates/latkit/src/data/column.rs | 5 +- crates/latkit/src/data/geometry.rs | 61 +- crates/latkit/src/data/ids.rs | 94 +++- crates/latkit/src/data/mod.rs | 1 + crates/latkit/src/data/patch.rs | 9 +- crates/latkit/src/data/values.rs | 62 +- crates/latkit/src/derive/lanes.rs | 70 ++- crates/latkit/src/derive/mod.rs | 4 +- crates/latkit/src/derive/order.rs | 12 +- crates/latkit/src/derive/pyramid.rs | 15 +- crates/latkit/src/derive/shapes.rs | 528 +++++++++++++----- crates/latkit/src/derive/tiled.rs | 5 +- crates/latkit/src/derive/tiles/mod.rs | 10 + crates/latkit/src/derive/tiles/vector.rs | 61 +- crates/latkit/src/expr/compile.rs | 8 +- crates/latkit/src/geo/tiles.rs | 41 +- crates/latkit/src/gpu/arena.rs | 331 +++++++---- crates/latkit/src/gpu/atlas.rs | 83 ++- crates/latkit/src/gpu/mod.rs | 6 +- crates/latkit/src/gpu/packed.rs | 21 +- crates/latkit/src/gpu/shaders.rs | 11 +- crates/latkit/src/host/browser.rs | 7 +- crates/latkit/src/host/job.rs | 129 +++-- crates/latkit/src/host/mod.rs | 2 +- crates/latkit/src/scene/edges.rs | 2 +- crates/latkit/src/scene/theme.rs | 3 +- crates/latkit/src/view/drawing.rs | 106 ++-- crates/latkit/src/view/event.rs | 4 +- crates/latkit/src/view/fit.rs | 3 +- crates/latkit/src/view/focus.rs | 104 ++-- crates/latkit/src/view/frame.rs | 246 ++++++-- crates/latkit/src/view/labels.rs | 110 ++-- crates/latkit/src/view/lists.rs | 16 +- crates/latkit/src/view/marks.rs | 191 +++++-- crates/latkit/src/view/mod.rs | 54 +- crates/latkit/src/view/passes.rs | 31 +- crates/latkit/src/view/plan.rs | 144 +++-- crates/latkit/src/view/program.rs | 6 +- crates/latkit/src/view/resources.rs | 220 +------- crates/latkit/src/view/rows.rs | 117 ---- crates/latkit/src/view/sync.rs | 322 ++++++----- crates/latkit/src/view/transition.rs | 202 ++++--- crates/latkit/src/view/uniforms.rs | 30 +- crates/latkit/src/view/words.rs | 145 +++++ crates/latkit/tests/labels.rs | 5 + crates/latkit/tests/scale.rs | 2 +- npm/examples/src/examples/soft-body/index.ts | 2 +- npm/schema.json | 4 +- npm/src/inputs.ts | 3 +- npm/src/jobs.ts | 43 +- npm/src/outputs.ts | 4 +- npm/src/runtime.ts | 26 +- npm/test/jobs.test.mjs | 25 +- 79 files changed, 2831 insertions(+), 1734 deletions(-) delete mode 100644 crates/latkit/src/view/rows.rs create mode 100644 crates/latkit/src/view/words.rs diff --git a/benchmark/judge.ts b/benchmark/judge.ts index 486de674..36df439a 100644 --- a/benchmark/judge.ts +++ b/benchmark/judge.ts @@ -41,8 +41,8 @@ const SLACK = 0.2; /** Milliseconds a phase acting once may land later than its baseline by: a timer tick, which is * 15.6 ms on Windows, so readbacks answer one tick or the next. */ const TICK = 16; -/** Bytes a changed row uploads at least: its page, 65,536 rows of four bytes. */ -const PAGE = 65536 * 4; +/** Bytes a changed row uploads at least: its page, 4,096 rows of four bytes. */ +const PAGE = 4096 * 4; /** Below these a cost counts as these: too small to fit an exponent to, or to call slower. A frame's * CPU time is read on a page's clock, which steps by 0.1 ms, so it counts from five steps; a phase * acting once lands on a tick. */ diff --git a/benchmark/page/stage.ts b/benchmark/page/stage.ts index f11891c3..c6858dba 100644 --- a/benchmark/page/stage.ts +++ b/benchmark/page/stage.ts @@ -89,6 +89,8 @@ export class Stage { constructor(latkit: Latkit, gpu: Gpu, canvases: readonly HTMLCanvasElement[], size: number) { [this.latkit, this.gpu, this.canvases, this.size] = [latkit, gpu, canvases, size]; + // Stepped from the start, so no animation frame draws while views load either. + gpu.step(this.#now); } get view(): View { diff --git a/crates/latkit-js/src/lib.rs b/crates/latkit-js/src/lib.rs index 38b0cae1..bcc3c8f5 100644 --- a/crates/latkit-js/src/lib.rs +++ b/crates/latkit-js/src/lib.rs @@ -14,6 +14,7 @@ mod convert; mod error; mod shot; +use latkit::host::Host as _; use wasm_bindgen::JsCast; use wasm_bindgen::prelude::*; @@ -173,13 +174,24 @@ impl Data { Data(latkit::Data::new(name)) } - /// The dataset latkit bytes hold, renamed `name` when given. - #[allow(clippy::needless_pass_by_value, reason = "wasm-bindgen hands optional texts over owned")] - pub fn read(bytes: &[u8], name: Option) -> Result { - if !bytes.starts_with(b"LATK") { - return Err(latkit::Error::invalid("not latkit bytes").at("bytes").into()); + /// A promise of the dataset `bytes` hold: latkit's bytes, renamed `name` when given, or GeoJSON, + /// read in a worker as dataset `name`. + pub fn read(bytes: &[u8], name: Option) -> js_sys::Promise { + if bytes.starts_with(b"LATK") { + let data = latkit::Data::read(bytes, name.as_deref()); + return promise(async move { data.map(Data) }); } - Ok(Data(latkit::Data::read(bytes, name.as_deref())?)) + let Some(name) = name else { + return promise(async { Err::(latkit::Error::invalid("GeoJSON names its dataset").at("name")) }); + }; + let job = latkit::host::Job::geojson(&name, bytes); + promise(async move { + let data = latkit::host::Browser.spawn(job).await?; + if data.is_empty() { + return Err(latkit::Error::invalid(format!("{name}: the bytes are neither latkit's nor GeoJSON"))); + } + Ok(Data(latkit::Data::read(&data, Some(&name))?)) + }) } /// A promise of the dataset its JSON document builds, its slices fetched relative to `base`. @@ -480,13 +492,6 @@ pub fn job(bytes: &[u8]) -> Vec { latkit::host::Job::from_bytes(bytes.to_vec()).run() } -/// The job reading GeoJSON `bytes`, gzip-compressed or not, as dataset `name`, for a worker to do: it -/// answers the bytes of the patch building the dataset, or nothing when the bytes are no GeoJSON. -#[wasm_bindgen(js_name = geojsonJob)] -pub fn geojson_job(name: &str, bytes: &[u8]) -> Vec { - latkit::host::Job::geojson(name, bytes).into_bytes() -} - /// The compiled module, for workers to instantiate. #[wasm_bindgen] pub fn module() -> JsValue { diff --git a/crates/latkit-js/src/shot.rs b/crates/latkit-js/src/shot.rs index 46801ea5..7b0e8184 100644 --- a/crates/latkit-js/src/shot.rs +++ b/crates/latkit-js/src/shot.rs @@ -8,6 +8,7 @@ use std::io::Write; use std::rc::Rc; use futures_util::future::{AbortHandle, Abortable}; +use latkit::host::Host as _; use wasm_bindgen::prelude::*; use crate::Error; @@ -35,10 +36,9 @@ impl Image { self.0.rgba.to_vec() } - /// The job writing the image as a `png`, `jpeg` at `quality` from 1 to 100, or `webp` file, for a - /// worker to do. - #[wasm_bindgen(js_name = encodeJob)] - pub fn encode_job(&self, format: &str, quality: Option) -> Result, Error> { + /// A promise of the image written in a worker as a `png`, `jpeg` at `quality` from 1 to 100, or + /// `webp` file. + pub fn encode(&self, format: &str, quality: Option) -> Result { let quality = match quality { None => 90, Some(q) if (1.0..=100.0).contains(&q) => q.round() as u8, @@ -50,7 +50,14 @@ impl Image { "webp" => latkit::ImageFormat::WebP, other => return Err(latkit::Error::invalid(format!("no image format {other:?}")).at("format").into()), }; - Ok(latkit::host::Job::encode(&self.0, format).into_bytes()) + let job = latkit::host::Job::encode(&self.0, format); + Ok(crate::promise(async move { + let file = latkit::host::Browser.spawn(job).await?; + if file.is_empty() { + return Err(latkit::Error::invalid(format!("the image could not be written as {format}"))); + } + Ok(js_sys::Uint8Array::from(&file[..])) + })) } } diff --git a/crates/latkit/build/shaders.rs b/crates/latkit/build/shaders.rs index 48d74a5b..88ee5e5b 100644 --- a/crates/latkit/build/shaders.rs +++ b/crates/latkit/build/shaders.rs @@ -31,9 +31,9 @@ struct Entry { const ENTRIES: &[Entry] = &[ Entry { module: "compute::evaluate", variants: &[("main", &["writes"])] }, - Entry { module: "compute::freeze", variants: &[("main", &[])] }, + Entry { module: "compute::freeze", variants: &[("main", &["writes"])] }, Entry { module: "compute::bounds", variants: &[("main", &[])] }, - Entry { module: "compute::normals", variants: &[("main", &[])] }, + Entry { module: "compute::normals", variants: &[("main", &["atomic"])] }, Entry { module: "compute::probe", variants: &[("main", &[]), ("multisampled", &["multisampled"])] }, Entry { module: "compute::lists", variants: &[("main", &["lists"])] }, Entry { module: "draw::backdrop", variants: &[("main", &[])] }, @@ -54,14 +54,14 @@ const ENTRIES: &[Entry] = &[ Entry { module: "draw::areas", variants: &[("main", &[]), ("focus", &["focused"])] }, Entry { module: "draw::shade", variants: &[("main", &[])] }, Entry { module: "draw::glyphs", variants: &[("main", &[]), ("ports", &["ports"])] }, - Entry { module: "compute::candidates", variants: &[("main", &[])] }, - Entry { module: "compute::labels", variants: &[("main", &[])] }, + Entry { module: "compute::candidates", variants: &[("main", &["writes"])] }, + Entry { module: "compute::labels", variants: &[("main", &["writes"])] }, Entry { module: "draw::overlay", variants: &[("main", &[])] }, Entry { module: "compute::compose", variants: &[("main", &[])] }, Entry { module: "compute::fields", variants: &[("main", &[])] }, ]; -const FEATURES: &[&str] = &["writes", "casts", "custom", "multisampled", "lists", "ports", "focused"]; +const FEATURES: &[&str] = &["writes", "atomic", "casts", "custom", "multisampled", "lists", "ports", "focused"]; /// What a custom marker supplies, for validating its variant. const CUSTOM_MARKER: &str = "fn marker(f: MarkerFragment) -> MarkerColor { return MarkerColor(f.color, 0.0); }"; diff --git a/crates/latkit/shaders/compute/bounds.wesl b/crates/latkit/shaders/compute/bounds.wesl index 861d3346..e3629b7d 100644 --- a/crates/latkit/shaders/compute/bounds.wesl +++ b/crates/latkit/shaders/compute/bounds.wesl @@ -4,7 +4,7 @@ // rows, only rows marked FIT count. import package::lib::types::{NONE, FIT, PATH_ENDS, PATH_GEOMETRY, EXTENT_WORDS, VERTEX_POSITION, HALF_UNITS, item}; -import package::lib::bind::{frame, eye, mark, state}; +import package::lib::bind::{frame, eye, mark}; import package::lib::focus::focus_of; import package::lib::reduce::ordered; import package::lib::state::{row_at, vertex_at}; diff --git a/crates/latkit/shaders/compute/candidates.wesl b/crates/latkit/shaders/compute/candidates.wesl index 94870f37..5be99521 100644 --- a/crates/latkit/shaders/compute/candidates.wesl +++ b/crates/latkit/shaders/compute/candidates.wesl @@ -1,5 +1,5 @@ // The first step of label placement: each labeled row's label measured where its anchor puts it, by -// the size of its text, or an estimate of it while the text is not set, and keyed by its priority, +// the size of its text, as set or as measured while it is not, and keyed by its priority, // then whether it showed before, then its id: a label that arrives later still displaces lesser // ones, and among equals those shown keep their place. `compute/labels.wesl` places them. Made for // one primitive, it holds only that one's anchors. A diagram's labels scale with its sheet, so they @@ -7,13 +7,13 @@ import package::lib::types::{ VISIBLE, LABEL_CODE, LABEL_PRIORITY, CANDIDATE_BOX, CANDIDATE_KEY, CANDIDATE_STATE, WANTS, PLACED_LABEL, - STICKY, ESTIMATED, TEXT_ESTIMATE, SPACE_DIAGRAM, item, + STICKY, UNSET, TEXT_UNSET, SPACE_DIAGRAM, item, }; -import package::lib::bind::{frame, eye, mark, state}; -import package::lib::state::extra_at; +import package::lib::bind::{frame, eye, mark, word}; +import package::lib::state::{row_at, extra_at}; import package::lib::read::missing_bits; import package::lib::label::{anchor, length_px, legibility, text_at, fits, in_view}; -import package::lib::placement::{word, put, candidate, want, seen}; +import package::lib::placement::{label_word, put, candidate, want, seen}; import package::lib::drawn::drawn_row; @compute @workgroup_size(256) @@ -27,22 +27,22 @@ fn candidates(@builtin(global_invocation_id) id: vec3u) { let at = candidate(row); let before = word(at + CANDIDATE_STATE); let sticky = (before & PLACED_LABEL) != 0u; - let code_bits = state[extra_at(row, mark.label + LABEL_CODE)]; + let code_bits = word(extra_at(row, mark.label + LABEL_CODE)); var next = select(0u, STICKY, sticky); - let flags = state[mark.state + row * mark.stride + mark.stride - 1u]; + let flags = word(row_at(row) + mark.stride - 1u); // A row whose tile leaves keeps no label. if missing_bits(code_bits) || (flags & VISIBLE) == 0u || drawn.leaving { put(at + CANDIDATE_STATE, next); return; } let code = u32(bitcast(code_bits)); - let uv0 = word(mark.runs + 2u * code); - let uv1 = word(mark.runs + 2u * code + 1u); + let uv0 = label_word(mark.runs + 2u * code); + let uv1 = label_word(mark.runs + 2u * code + 1u); let a = anchor(i); let size = length_px(mark.label_size); let diagram = frame.space == SPACE_DIAGRAM; - let estimated = (uv0 & TEXT_ESTIMATE) != 0u; - if (uv0 == 0u && uv1 == 0u) || (estimated && diagram) { + let unset = (uv0 & TEXT_UNSET) != 0u; + if (uv0 == 0u && uv1 == 0u) || (unset && diagram) { // The atlas lacks this text, and the label cannot wait for placement to ask for it: ask now, // if it could show, legible and near the view. let near = 0.25 * eye.size; @@ -52,10 +52,10 @@ fn candidates(@builtin(global_invocation_id) id: vec3u) { put(at + CANDIDATE_STATE, next); return; } - if estimated { - next |= ESTIMATED; + if unset { + next |= UNSET; } - let t = text_at(uv0 & ~TEXT_ESTIMATE, uv1, size, a.at, a.align); + let t = text_at(uv0 & ~TEXT_UNSET, uv1, size, a.at, a.align); if a.shown && in_view(t.lo, t.hi) { // A diagram shows every label in view that fits; elsewhere `ask` notes those placed. if diagram { @@ -69,7 +69,7 @@ fn candidates(@builtin(global_invocation_id) id: vec3u) { put(at + CANDIDATE_BOX + 1u, bitcast(t.lo.y)); put(at + CANDIDATE_BOX + 2u, bitcast(t.hi.x)); put(at + CANDIDATE_BOX + 3u, bitcast(t.hi.y)); - let level = u32(clamp(bitcast(state[extra_at(row, mark.label + LABEL_PRIORITY)]), 0.0, 255.0)); + let level = u32(clamp(bitcast(word(extra_at(row, mark.label + LABEL_PRIORITY))), 0.0, 255.0)); let id_bits = (mark.label_id + row + 1u) & 0x7fffffu; put(at + CANDIDATE_KEY, ((255u - level) << 24u) | select(0x800000u, 0u, sticky) | id_bits); put(at + CANDIDATE_STATE, next); diff --git a/crates/latkit/shaders/compute/evaluate.wesl b/crates/latkit/shaders/compute/evaluate.wesl index 37d3d326..b8ca2172 100644 --- a/crates/latkit/shaders/compute/evaluate.wesl +++ b/crates/latkit/shaders/compute/evaluate.wesl @@ -1,17 +1,17 @@ -// The expression machine: every row of a mark runs the mark's channels, which store into what the -// view binds as its state. All rows run the same instructions, so control flow stays uniform across a -// workgroup. A dispatch covers the row blocks the mark lists, only those whose rows changed when only -// those did. +// The expression machine: every row of a mark runs the mark's channels, which store into the row's +// state words. All rows run the same instructions, so control flow stays uniform across a workgroup. +// A dispatch covers the pages of rows the mark lists, only those whose rows changed when only those +// did. // -// Evaluating knows nothing of transitions: while a mark eases, the view binds its settled mirror as -// the state, and each frame `ease` writes what every row shows from its frozen and settled state by -// the mark's progress: numbers and colors mix, positions move by their offset, a row whose visibility +// Evaluating knows nothing of transitions: while a mark eases, its channels store into its settled +// words, and each frame `ease` writes what every row shows from its frozen and settled words by the +// mark's progress: numbers and colors mix, positions move by their offset, a row whose visibility // flipped fades, rows past the live ones left and fade out, and the rest show as evaluated. No -// channel runs to ease. Every row here is a held row. +// channel runs to ease. Every row here is a held row, its words in pages its mark's page tables list. import package::lib::types::{ - NONE, MISSING, MISSING_COLOR, STACK, VISIBLE, PLACED, FADE_IN, FADE_OUT, ENTERING, BLOCK_ROWS, HELD_BITS, - HELD_MASK, item, + NONE, MISSING, MISSING_COLOR, STACK, VISIBLE, PLACED, FADE_IN, FADE_OUT, ENTERING, PAGE_ROWS, PAGE_BITS, + PAGE_MASK, END_WORDS, END_STATE, END_SLOTS, item, BLEND_KEEP, BLEND_NUMBER, BLEND_COLOR, BLEND_POSITION, STEP_REDUCE, VERTEX_POSITION, VERTEX_ELEVATION, VERTEX_FLAGS, VERTEX_HALF, VERTEX_RANGE, HALF_UNITS, OWN_HEIGHT, SIZED, MARK_VERTICES, OP_CONST, OP_MISSING, OP_FIELD, OP_SELECTED, OP_HOVERED, OP_POINTER, OP_ELAPSED, OP_THEME, OP_ZOOM, @@ -21,21 +21,16 @@ import package::lib::types::{ OP_SELECT, OP_SCALE, OP_COLORMAP, OP_STORE, OP_STORE_COLOR, OP_STORE_FLAG, OP_STORE_POSITION, OP_POSITION, OP_STORE_END, OP_STORE_CODE, OP_REDUCE, }; -import package::lib::bind::{frame, program, mark, state}; -import package::lib::state::{travel_words, pack_range}; +import package::lib::bind::{frame, program, mark, pages}; +import package::lib::state::{travel_words, pack_range, record_at}; import package::lib::read::{column_value, bits_value, missing, missing_bits}; import package::lib::focus::{is_selected, is_hovered}; import package::lib::color::{scale, colormap}; import package::lib::project::{project, to_screen}; import package::lib::reduce::{ordered, unordered}; -// Each row's state as drawn when the mark's transition began. -@group(2) @binding(1) var frozen: array; // Per fitted domain: the order-preserving bits of its least and greatest value. @group(2) @binding(4) var domains: array>; -// Each row's state as its channels evaluated it, where it settles once the mark's transition ends: -// the state's mirror, which `ease` reads. -@group(2) @binding(14) var settled: array; // Each mark's row blocks a dispatch covers: at `mark.block`, `mark.blocks` of them. @group(2) @binding(16) var blocks: array; // What a dispatch of `evaluate` runs: the mark's channels over its listed blocks, or over every row @@ -46,9 +41,9 @@ import package::lib::reduce::{ordered, unordered}; // made for marks that do compile it. override VIEWED: bool = true; -// The row invocation `id` evaluates: row `id.x` of the mark's `id.y`th listed block. +// The row invocation `id` evaluates: row `id.x` of the mark's `id.y`th listed page. fn row_of(id: vec3u) -> u32 { - return blocks[mark.block + id.y] * BLOCK_ROWS + id.x; + return blocks[mark.block + id.y] * PAGE_ROWS + id.x; } // Rows of the mark that are live: those past them left and fade out. @@ -65,11 +60,11 @@ fn held_of(slots: u32, row: u32) -> u32 { return row; } let pages = program[slots]; - let page = row >> HELD_BITS; + let page = row >> PAGE_BITS; if page < pages { let slot = program[slots + 1u + page]; if slot != NONE { - return (slot << HELD_BITS) | (row & HELD_MASK); + return (slot << PAGE_BITS) | (row & PAGE_MASK); } } let kept = slots + 1u + pages; @@ -115,32 +110,43 @@ fn theme_word(slot: u32) -> u32 { return frame.theme[slot / 4u][slot % 4u]; } -// Where word `word` of `row` lives: a kind word, or past the kind words an extra word. -fn word_at(row: u32, word: u32) -> u32 { - if word < mark.stride { - return mark.state + row * mark.stride + word; - } - return mark.extra + row * mark.extras + (word - mark.stride); +// Where word `w` of `row` lies among the pages page table `table` lists: a kind word, or past the +// kind words an extra word. +fn word_in(table: u32, row: u32, w: u32) -> u32 { + return record_at(table, mark.stride + mark.extras, row) + w; } fn easing() -> bool { return mark.frozen != NONE; } -fn load(row: u32, word: u32) -> u32 { - return state[word_at(row, word)]; +// Where channels store word `w` of `row`: while the mark eases, among the words it settles to, which +// `ease` reads. +fn word_at(row: u32, w: u32) -> u32 { + return word_in(select(mark.state, mark.settled, easing()), row, w); } -fn store(row: u32, word: u32, value: u32) { - state[word_at(row, word)] = value; +fn load(row: u32, w: u32) -> u32 { + return pages[word_at(row, w)]; } -// Word `word` of `row` as it was drawn when the transition began. -fn was(row: u32, word: u32) -> u32 { - if word < mark.stride { - return frozen[mark.frozen + row * mark.stride + word]; - } - return frozen[mark.frozen_extra + row * mark.extras + (word - mark.stride)]; +fn store(row: u32, w: u32, value: u32) { + pages[word_at(row, w)] = value; +} + +// Word `w` of `row` as it was drawn when the transition began. +fn was(row: u32, w: u32) -> u32 { + return pages[word_in(mark.frozen, row, w)]; +} + +// Word `w` of `row` as its channels evaluated it, where it settles. +fn settled(row: u32, w: u32) -> u32 { + return pages[word_in(mark.settled, row, w)]; +} + +// Where word `w` of `row` shows. +fn shown_at(row: u32, w: u32) -> u32 { + return word_in(mark.state, row, w); } // Whether `row` has nothing to ease from: it entered with this change. @@ -152,20 +158,20 @@ fn number(x: u32) -> f32 { return bitcast(x); } -fn word(x: f32) -> u32 { +fn bits(x: f32) -> u32 { return bitcast(x); } fn truth(x: bool) -> u32 { - return word(f32(x)); + return bits(f32(x)); } // CSS pixels from a vertex to the pointer. fn pointer_distance(row: u32) -> f32 { let at = word_at(row, VERTEX_POSITION); - let hi = vec2f(bitcast(state[at]), bitcast(state[at + 1u])); - let lo = vec2f(bitcast(state[at + 2u]), bitcast(state[at + 3u])); - let z = bitcast(state[word_at(row, VERTEX_ELEVATION)]); + let hi = vec2f(bitcast(pages[at]), bitcast(pages[at + 1u])); + let lo = vec2f(bitcast(pages[at + 2u]), bitcast(pages[at + 3u])); + let z = bitcast(pages[word_at(row, VERTEX_ELEVATION)]); return distance(to_screen(project(hi, lo, z)), frame.pointer); } @@ -178,8 +184,8 @@ fn viewed(code: u32, row: u32) -> u32 { switch code { case OP_SELECTED: { return truth(is_selected(row)); } case OP_HOVERED: { return truth(is_hovered(row)); } - case OP_POINTER: { return word(pointer_distance(row)); } - default: { return word(frame.zoom); } + case OP_POINTER: { return bits(pointer_distance(row)); } + default: { return bits(frame.zoom); } } } @@ -235,7 +241,7 @@ fn ease_number(row: u32, w: u32, now: u32, flags: u32) -> u32 { if missing_bits(start) { return now; } - return word(mix(number(start), number(now), mark.motion)); + return bits(mix(number(start), number(now), mark.motion)); } fn ease_color(row: u32, w: u32, now: u32, flags: u32) -> u32 { @@ -251,15 +257,15 @@ fn ease_position(row: u32, w: u32, now: u32, flags: u32) -> u32 { if !easing() || entering(row, flags) || (was(row, flags) & PLACED) == 0u { return now; } - let now_lo = number(settled[word_at(row, w + 2u)]); + let now_lo = number(settled(row, w + 2u)); let offset = (number(was(row, w)) - number(now)) + (number(was(row, w + 2u)) - now_lo); - return word(number(now) + offset * (1.0 - mark.motion)); + return bits(number(now) + offset * (1.0 - mark.motion)); } // The flags `row` shows while its mark eases: as evaluated, but a row whose visibility flipped stays // drawn and fades, in where it became visible or entered, out where it stopped being. fn faded(row: u32, flags: u32) -> u32 { - let now = settled[word_at(row, flags)] & ~(FADE_IN | FADE_OUT); + let now = settled(row, flags) & ~(FADE_IN | FADE_OUT); let fresh = entering(row, flags); let before = fresh || (was(row, flags) & VISIBLE) != 0u; let on = (now & VISIBLE) != 0u; @@ -307,15 +313,15 @@ fn run(row: u32, start: u32, end: u32) { case OP_MISSING: { stack[sp] = MISSING; sp++; } case OP_FIELD: { stack[sp] = column_value(a, row).x; sp++; } case OP_SELECTED, OP_HOVERED, OP_POINTER, OP_ZOOM: { stack[sp] = viewed(code, row); sp++; } - case OP_ELAPSED: { stack[sp] = word(frame.elapsed); sp++; } + case OP_ELAPSED: { stack[sp] = bits(frame.elapsed); sp++; } case OP_THEME: { stack[sp] = theme_word(a); sp++; } case OP_NEG, OP_NOT, OP_ABS, OP_SQRT, OP_SIN, OP_COS, OP_FLOOR, OP_LOG, OP_EXP: { - stack[sp - 1u] = word(unary(code, number(stack[sp - 1u]))); + stack[sp - 1u] = bits(unary(code, number(stack[sp - 1u]))); } case OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_MIN, OP_MAX, OP_MOD, OP_POW, OP_LT, OP_LE, OP_GT, OP_GE, OP_EQ, OP_NE, OP_AND, OP_OR: { sp--; - stack[sp - 1u] = word(binary(code, number(stack[sp - 1u]), number(stack[sp]))); + stack[sp - 1u] = bits(binary(code, number(stack[sp - 1u]), number(stack[sp]))); } case OP_SELECT: { sp -= 2u; @@ -323,7 +329,7 @@ fn run(row: u32, start: u32, end: u32) { } case OP_SCALE: { let range = vec2f(constant_float(b), constant_float(b + 1u)); - stack[sp - 1u] = word(scale(number(stack[sp - 1u]), domain(a), range)); + stack[sp - 1u] = bits(scale(number(stack[sp - 1u]), domain(a), range)); } case OP_COLORMAP: { stack[sp - 1u] = colormap(a & 0xffffu, a >> 16u, number(stack[sp - 1u]), domain(b)); @@ -354,12 +360,12 @@ fn run(row: u32, start: u32, end: u32) { case OP_STORE_END: { // An end is a row of the table its reference names, held where the vertex mark drawing // that table holds it; without a column, the row itself, as a pole's. - let first = program[mark.ends + 2u * c]; + let e = mark.ends + END_WORDS * c; var end = row; if b != NONE { - end = held_of(program[mark.ends + 2u * c + 1u], bits_value(b, row)); + end = held_of(program[e + END_SLOTS], bits_value(b, row)); } - store(row, a, select(first + end, NONE, end == NONE || first == NONE)); + store(row, a, select(end, NONE, end == NONE || program[e + END_STATE] == NONE)); } case OP_REDUCE: { sp--; @@ -381,10 +387,10 @@ fn fade_exit(row: u32, flags: u32) { let travel = travel_words().x; for (var w = 0u; w < mark.stride; w++) { if w != travel { - state[word_at(row, w)] = was(row, w); + pages[shown_at(row, w)] = was(row, w); } } - state[word_at(row, flags)] = (was(row, flags) & ~FADE_IN) | FADE_OUT; + pages[shown_at(row, flags)] = (was(row, flags) & ~FADE_IN) | FADE_OUT; } @compute @workgroup_size(256) @@ -432,7 +438,7 @@ fn ease(@builtin(global_invocation_id) id: vec3u) { if blend == BLEND_KEEP { continue; } - let now = settled[word_at(row, w)]; + let now = settled(row, w); var shown = now; switch blend { case BLEND_NUMBER: { shown = ease_number(row, w, now, flags); } @@ -440,9 +446,9 @@ fn ease(@builtin(global_invocation_id) id: vec3u) { case BLEND_POSITION: { shown = ease_position(row, w, now, flags); } default: {} } - state[word_at(row, w)] = shown; + pages[shown_at(row, w)] = shown; } - state[word_at(row, flags)] = faded(row, flags); + pages[shown_at(row, flags)] = faded(row, flags); } // Comets travel on by each stroke row's flow over the frame's step, so a flow that changes carries @@ -455,13 +461,13 @@ fn travel(@builtin(global_invocation_id) id: vec3u) { if row >= mark.held || words.x == NONE { return; } - let at = word_at(row, words.x); - let flow = number(state[word_at(row, words.y)]); + let at = shown_at(row, words.x); + let flow = number(pages[shown_at(row, words.y)]); let spacing = max(frame.flow_spacing, 1e-6); - var moved = number(state[at]) + select(flow, 0.0, missing(flow)) * frame.flow_step; + var moved = number(pages[at]) + select(flow, 0.0, missing(flow)) * frame.flow_step; moved -= spacing * floor(moved / spacing); // Words a row never held read as no travel. - state[at] = word(select(0.0, moved, moved >= 0.0 && moved <= spacing)); + pages[at] = bits(select(0.0, moved, moved >= 0.0 && moved <= spacing)); } // Where a transition moved rows, their comets carry on from where they were: each row takes the @@ -473,5 +479,5 @@ fn carry(@builtin(global_invocation_id) id: vec3u) { if row >= mark.held || travel == NONE || !easing() { return; } - state[word_at(row, travel)] = was(row, travel); + pages[shown_at(row, travel)] = was(row, travel); } diff --git a/crates/latkit/shaders/compute/freeze.wesl b/crates/latkit/shaders/compute/freeze.wesl index 9d72d892..9686f031 100644 --- a/crates/latkit/shaders/compute/freeze.wesl +++ b/crates/latkit/shaders/compute/freeze.wesl @@ -1,45 +1,54 @@ -// Freezing: what each row of a mark showed when its transition began, gathered into the frozen -// buffer at the row's new place. The remap words list where each new row was (NONE for a row that -// entered), then the rows that left, which fade out past the live ones. Slot words of a mark whose -// ends point at vertices follow them to their new slots. +// Freezing: what each row of a mark showed when its transition began, gathered into its frozen words +// at the row's new place. The remap words at `mark.remap` say where the words it showed lie (the page +// table of its words before the change, copied into them), how many rows it showed, and where each new +// row was, NONE for a row that entered, then the rows that left, which fade out past the live ones; +// without that list every row is where it was. They say too, for each kind word naming a vertex, where +// the vertices of the vertex mark it names moved, so the ends of rows easing out follow them. -import package::lib::types::{NONE, ENTERING, item}; -import package::lib::bind::{mark, state}; +import package::lib::types::{NONE, ENTERING, PAGE_BITS, PAGE_MASK, item}; +import package::lib::bind::{mark, pages}; +import package::lib::state::record_at; -@group(2) @binding(1) var frozen: array; -// At `mark.remap`: the old kind and extra offsets, a mask of slot words, where the slot table -// starts or NONE, then a source row per new row and exit. @group(2) @binding(12) var remap: array; +// The remap header's words: where the page table of the words shown starts, how many rows showed, +// where the row each new row was starts (NONE when every row is where it was), and where each kind +// word's vertex moves start (NONE when no vertex moved). +const REMAP_TABLE: u32 = 0u; +const REMAP_SHOWN: u32 = 1u; +const REMAP_SOURCES: u32 = 2u; +const REMAP_ENDS: u32 = 3u; + @compute @workgroup_size(256) fn gather(@builtin(global_invocation_id) id: vec3u) { let i = item(id); if i >= mark.held { return; } - let old_state = remap[mark.remap]; - let old_extra = remap[mark.remap + 1u]; - let slots = remap[mark.remap + 2u]; - let table = remap[mark.remap + 3u]; - let source = remap[mark.remap + 4u + i]; + let header = mark.remap; + let sources = remap[header + REMAP_SOURCES]; + var source = select(NONE, i, i < remap[header + REMAP_SHOWN]); + if sources != NONE { + source = remap[sources + i]; + } + let words = mark.stride + mark.extras; + let to = record_at(mark.frozen, words, i); let flags = mark.stride - 1u; - for (var w = 0u; w < mark.stride; w++) { - var word = 0u; - if source == NONE { - word = select(0u, ENTERING, w == flags); - } else { - word = state[old_state + source * mark.stride + w]; - if ((slots >> w) & 1u) != 0u && word != NONE && table != NONE { - word = remap[table + word]; - } + if source == NONE { + for (var w = 0u; w < words; w++) { + pages[to + w] = select(0u, ENTERING, w == flags); } - frozen[mark.frozen + i * mark.stride + w] = word; + return; } - for (var w = 0u; w < mark.extras; w++) { - var word = 0u; - if source != NONE { - word = state[old_extra + source * mark.extras + w]; + let shown = remap[remap[header + REMAP_TABLE] + (source >> PAGE_BITS)] + (source & PAGE_MASK) * words; + let ends = remap[header + REMAP_ENDS]; + for (var w = 0u; w < words; w++) { + var word = pages[shown + w]; + // Each vertex's moves: how many rows it held, then where each went, NONE where it left. + let moves = select(NONE, remap[ends + min(w, mark.stride - 1u)], ends != NONE && w < mark.stride); + if moves != NONE && word != NONE { + word = select(NONE, remap[moves + 1u + word], word < remap[moves]); } - frozen[mark.frozen_extra + i * mark.extras + w] = word; + pages[to + w] = word; } } diff --git a/crates/latkit/shaders/compute/labels.wesl b/crates/latkit/shaders/compute/labels.wesl index 45c414e3..30ed4064 100644 --- a/crates/latkit/shaders/compute/labels.wesl +++ b/crates/latkit/shaders/compute/labels.wesl @@ -13,12 +13,12 @@ import package::lib::types::{ NONE, LABEL_CODE, CANDIDATE_BOX, CANDIDATE_KEY, CANDIDATE_STATE, CANDIDATE_ALPHA, WANTS, PLACED_LABEL, DROPPED, - ESTIMATED, OCCUPIED, CELL, item, + UNSET, OCCUPIED, CELL, item, }; -import package::lib::bind::{frame, mark, state}; +import package::lib::bind::{frame, mark, word}; import package::lib::state::extra_at; import package::lib::read::missing_bits; -import package::lib::placement::{labels, word, put, candidate, want, seen}; +import package::lib::placement::{labels, label_word, put_label, put, candidate, want, seen}; import package::lib::drawn::held_row; // Buckets of a digit of the select capping labels. @@ -56,19 +56,19 @@ fn select_start(@builtin(global_invocation_id) id: vec3u) { return; } if i < SELECT_BUCKETS { - put(mark.label_select + i, 0u); + put_label(mark.label_select + i, 0u); } if i == 0u { - put(select_word(0u), 0u); - put(select_word(1u), mark.label_max); - put(select_word(2u), 0u); + put_label(select_word(0u), 0u); + put_label(select_word(1u), mark.label_max); + put_label(select_word(2u), 0u); } } @compute @workgroup_size(256) fn select_count(@builtin(global_invocation_id) id: vec3u) { let row = labeled(id); - let d = word(select_word(2u)); + let d = label_word(select_word(2u)); if row == NONE || mark.label_max == NONE || d >= 3u || !wanting(row) { return; } @@ -76,7 +76,7 @@ fn select_count(@builtin(global_invocation_id) id: vec3u) { let at = digit(d); // Only keys whose higher digits are those found count. let above = at.x + at.y; - if above < 32u && (key >> above) != word(select_word(0u)) { + if above < 32u && (key >> above) != label_word(select_word(0u)) { return; } atomicAdd(&labels[mark.label_select + ((key >> at.x) & (SELECT_BUCKETS - 1u))], 1u); @@ -85,16 +85,16 @@ fn select_count(@builtin(global_invocation_id) id: vec3u) { // The bucket holding the least key still to keep, clearing the buckets for the next digit. @compute @workgroup_size(1) fn select_find() { - let d = word(select_word(2u)); + let d = label_word(select_word(2u)); // A cap of none keeps no label: the cut drops them all. if mark.label_max == NONE || mark.label_max == 0u || d >= 3u { return; } - var left = word(select_word(1u)); + var left = label_word(select_word(1u)); var found = SELECT_BUCKETS; for (var b = 0u; b < SELECT_BUCKETS; b++) { - let n = word(mark.label_select + b); - put(mark.label_select + b, 0u); + let n = label_word(mark.label_select + b); + put_label(mark.label_select + b, 0u); if found == SELECT_BUCKETS { if n >= left { found = b; @@ -105,22 +105,22 @@ fn select_find() { } if found == SELECT_BUCKETS { // Fewer want room than the cap: each keeps its chance. - put(select_word(2u), 4u); + put_label(select_word(2u), 4u); return; } - put(select_word(0u), (word(select_word(0u)) << digit(d).y) | found); - put(select_word(1u), left); - put(select_word(2u), d + 1u); + put_label(select_word(0u), (label_word(select_word(0u)) << digit(d).y) | found); + put_label(select_word(1u), left); + put_label(select_word(2u), d + 1u); } @compute @workgroup_size(256) fn select_cut(@builtin(global_invocation_id) id: vec3u) { let row = labeled(id); - if row == NONE || mark.label_max == NONE || !wanting(row) || word(select_word(2u)) == 4u { + if row == NONE || mark.label_max == NONE || !wanting(row) || label_word(select_word(2u)) == 4u { return; } let at = candidate(row) + CANDIDATE_STATE; - if mark.label_max == 0u || word(candidate(row) + CANDIDATE_KEY) > word(select_word(0u)) { + if mark.label_max == 0u || word(candidate(row) + CANDIDATE_KEY) > label_word(select_word(0u)) { put(at, word(at) & ~WANTS); } } @@ -150,15 +150,15 @@ fn undecided(row: u32) -> bool { fn clear_cells(@builtin(global_invocation_id) id: vec3u) { let i = item(id); if i < frame.cell_columns * frame.cell_rows { - put(frame.cells + i, NONE); + put_label(frame.cells + i, NONE); } } @compute @workgroup_size(256) fn reset_cells(@builtin(global_invocation_id) id: vec3u) { let i = item(id); - if i < frame.cell_columns * frame.cell_rows && word(frame.cells + i) != OCCUPIED { - put(frame.cells + i, NONE); + if i < frame.cell_columns * frame.cell_rows && label_word(frame.cells + i) != OCCUPIED { + put_label(frame.cells + i, NONE); } } @@ -173,7 +173,7 @@ fn claim(@builtin(global_invocation_id) id: vec3u) { let c = cells_of(row); for (var y = c.y; y <= c.w; y++) { for (var x = c.x; x <= c.z; x++) { - if word(cell(x, y)) == OCCUPIED { + if label_word(cell(x, y)) == OCCUPIED { put(at + CANDIDATE_STATE, word(at + CANDIDATE_STATE) | DROPPED); return; } @@ -193,7 +193,7 @@ fn decide(@builtin(global_invocation_id) id: vec3u) { let c = cells_of(row); for (var y = c.y; y <= c.w; y++) { for (var x = c.x; x <= c.z; x++) { - if word(cell(x, y)) != key { + if label_word(cell(x, y)) != key { return; } } @@ -210,7 +210,7 @@ fn occupy(@builtin(global_invocation_id) id: vec3u) { let c = cells_of(row); for (var y = c.y; y <= c.w; y++) { for (var x = c.x; x <= c.z; x++) { - put(cell(x, y), OCCUPIED); + put_label(cell(x, y), OCCUPIED); } } } @@ -225,8 +225,8 @@ fn ask(@builtin(global_invocation_id) id: vec3u) { if (s & PLACED_LABEL) == 0u { return; } - let code = u32(bitcast(state[extra_at(row, mark.label + LABEL_CODE)])); - if (s & ESTIMATED) != 0u { + let code = u32(bitcast(word(extra_at(row, mark.label + LABEL_CODE)))); + if (s & UNSET) != 0u { want(code); } else { seen(code); @@ -240,9 +240,9 @@ fn fade(@builtin(global_invocation_id) id: vec3u) { return; } let at = candidate(row); - // A label placed by an estimate waits for its text to fade in. + // A label placed before its text is set waits for it to fade in. let s = word(at + CANDIDATE_STATE); - let placed = (s & PLACED_LABEL) != 0u && (s & ESTIMATED) == 0u; + let placed = (s & PLACED_LABEL) != 0u && (s & UNSET) == 0u; let alpha = bitcast(word(at + CANDIDATE_ALPHA)); let next = clamp(select(alpha - frame.label_fade, alpha + frame.label_fade, placed), 0.0, 1.0); put(at + CANDIDATE_ALPHA, bitcast(select(next, 0.0, missing_bits(bitcast(next))))); diff --git a/crates/latkit/shaders/compute/normals.wesl b/crates/latkit/shaders/compute/normals.wesl index 3754e758..131197e1 100644 --- a/crates/latkit/shaders/compute/normals.wesl +++ b/crates/latkit/shaders/compute/normals.wesl @@ -1,11 +1,10 @@ -// Vertex normals for faces: each face adds its unit normal to its three corners in fixed point, so the -// sums are exact in any order. +// Vertex normals for faces: each face adds its unit normal to its three corners' normal words, in +// pages of the vertex mark its corners attach to, in fixed point, so the sums are exact in any order. import package::lib::types::{NONE, item}; -import package::lib::bind::mark; +import package::lib::bind::{mark, pages}; import package::lib::triangles::triangle_corner; - -@group(2) @binding(3) var normals: array>; +import package::lib::state::end_normal; @compute @workgroup_size(256) fn accumulate(@builtin(global_invocation_id) id: vec3u) { @@ -16,7 +15,7 @@ fn accumulate(@builtin(global_invocation_id) id: vec3u) { let a = triangle_corner(i, 0u); let b = triangle_corner(i, 1u); let c = triangle_corner(i, 2u); - if !(a.shown && b.shown && c.shown) || a.slot == NONE { + if !(a.shown && b.shown && c.shown) || a.vertex == NONE { return; } let n = cross(b.world - a.world, c.world - a.world); @@ -24,11 +23,15 @@ fn accumulate(@builtin(global_invocation_id) id: vec3u) { if !(length2 > 1e-30) { return; } - let q = vec3i(round(n * inverseSqrt(length2) * 65536.0)); + let q = bitcast(vec3i(round(n * inverseSqrt(length2) * 65536.0))); + let vertices = vec3u(a.vertex, b.vertex, c.vertex); for (var k = 0u; k < 3u; k++) { - let at = select(select(c.slot, b.slot, k == 1u), a.slot, k == 0u) * 3u; - atomicAdd(&normals[at], q.x); - atomicAdd(&normals[at + 1u], q.y); - atomicAdd(&normals[at + 2u], q.z); + let at = end_normal(k, vertices[k]); + if at == NONE { + continue; + } + atomicAdd(&pages[at], q.x); + atomicAdd(&pages[at + 1u], q.y); + atomicAdd(&pages[at + 2u], q.z); } } diff --git a/crates/latkit/shaders/compute/probe.wesl b/crates/latkit/shaders/compute/probe.wesl index 61c11768..ccb6c49a 100644 --- a/crates/latkit/shaders/compute/probe.wesl +++ b/crates/latkit/shaders/compute/probe.wesl @@ -20,7 +20,7 @@ import package::lib::types::{ PROBE_X, PROBE_Y, PROBE_UNDER, PROBE_ROW, PROBE_SEGMENT, PROBE_LOCATE_X, PROBE_LOCATE_Y, PROBE_LOCATED, STEP_NEAREST, STEP_TOPMOST, STEP_DESCRIBE, STEP_HITS, STEP_BOXED, STEP_LOCATE, STEP_UNDER, HIT_WORDS, item, }; -import package::lib::bind::{frame, eye, mark, state}; +import package::lib::bind::{frame, eye, mark, word}; import package::lib::state::row_at; import package::lib::project::{to_screen, clip_of}; import package::lib::points::{point, port_at}; @@ -93,7 +93,7 @@ fn nearest_on(p: vec2f, a: vec3f, b: vec3f) -> vec3f { // Whether drawn row `item`, held as `row`, answers the pointer: one of the mark's live rows, neither // it nor its tile, which is `leaving` or not, fading out. fn answers(item: u32, row: u32, leaving: bool) -> bool { - return item < mark.rows && !leaving && (state[row_at(row) + mark.stride - 1u] & FADE_OUT) == 0u; + return item < mark.rows && !leaving && (word(row_at(row) + mark.stride - 1u) & FADE_OUT) == 0u; } // A point's marker, or past the rows one of their ports. diff --git a/crates/latkit/shaders/draw/glyphs.wesl b/crates/latkit/shaders/draw/glyphs.wesl index 9c07cc22..62ce4b87 100644 --- a/crates/latkit/shaders/draw/glyphs.wesl +++ b/crates/latkit/shaders/draw/glyphs.wesl @@ -5,10 +5,10 @@ import package::lib::types::{ CANDIDATE_WORDS, CANDIDATE_ALPHA, LABEL_CODE, LABEL_COLOR, PORT_WORDS, PORT_CODE, PORT_UV, SIDE_LEFT, - SIDE_RIGHT, SIDE_TOP, TEXT_SPREAD, TEXT_ESTIMATE, SPACE_DIAGRAM, THEME_TEXT, + SIDE_RIGHT, SIDE_TOP, TEXT_SPREAD, TEXT_UNSET, SPACE_DIAGRAM, THEME_TEXT, }; -import package::lib::bind::{frame, eye, mark, state, program, ramp}; -import package::lib::state::extra_at; +import package::lib::bind::{frame, eye, mark, word, program, ramp}; +import package::lib::state::{extra_at, record_at}; import package::lib::read::missing_bits; import package::lib::points::{shown_point, port_at, port_inward}; import package::lib::marker::theme_port_size; @@ -59,7 +59,7 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: } let drawn = drawn_row(instance); let row = drawn.row; - let code_bits = state[extra_at(row, mark.label + LABEL_CODE)]; + let code_bits = word(extra_at(row, mark.label + LABEL_CODE)); if missing_bits(code_bits) { return unshown(); } @@ -67,14 +67,14 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: let uv0 = labels[mark.runs + 2u * code]; let uv1 = labels[mark.runs + 2u * code + 1u]; // Nothing to draw until the text is set. - if (uv0 == 0u && uv1 == 0u) || (uv0 & TEXT_ESTIMATE) != 0u { + if (uv0 == 0u && uv1 == 0u) || (uv0 & TEXT_UNSET) != 0u { return unshown(); } let a = anchor(instance); let size = length_px(mark.label_size); let t = text_at(uv0, uv1, size, a.at, a.align); // A diagram's labels show while they fit and read; elsewhere as placement decided. - var alpha = bitcast(labels[mark.candidates + row * CANDIDATE_WORDS + CANDIDATE_ALPHA]); + var alpha = bitcast(word(record_at(mark.candidates, CANDIDATE_WORDS, row) + CANDIDATE_ALPHA)); if frame.space == SPACE_DIAGRAM { alpha = f32(a.shown && fits(t.hi - t.lo, a.room)) * legibility(size); } @@ -83,7 +83,7 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: if !(alpha > 0.0) { return unshown(); } - let color = unpack4x8unorm(state[extra_at(row, mark.label + LABEL_COLOR)]); + let color = unpack4x8unorm(word(extra_at(row, mark.label + LABEL_COLOR))); return quad(t, corner, vec4f(color.rgb, color.a * alpha)); } diff --git a/crates/latkit/shaders/draw/points.wesl b/crates/latkit/shaders/draw/points.wesl index e4598b53..713c7190 100644 --- a/crates/latkit/shaders/draw/points.wesl +++ b/crates/latkit/shaders/draw/points.wesl @@ -7,7 +7,7 @@ // marker. import package::lib::types::{STATUS, MARKER_ICON, MARKER_SHAPE, MARKER_BLOCK, GROUP, CASTS, SPACE_DIAGRAM}; -import package::lib::bind::{frame, eye, mark, state, ramp, program, focused}; +import package::lib::bind::{frame, eye, mark, word, ramp, program, focused}; import package::lib::state::extra_at; import package::lib::project::{to_screen, to_clip, nearer, culled}; import package::lib::points::{Point, shown_point, reach, port_at}; @@ -91,7 +91,7 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: fn inputs(row: u32) -> array { var input = array(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); for (var i = 0u; i < min(mark.inputs, 8u); i++) { - input[i] = bitcast(state[extra_at(row, mark.input + i)]); + input[i] = bitcast(word(extra_at(row, mark.input + i))); } return input; } diff --git a/crates/latkit/shaders/draw/triangles.wesl b/crates/latkit/shaders/draw/triangles.wesl index 37d84c66..3e40590a 100644 --- a/crates/latkit/shaders/draw/triangles.wesl +++ b/crates/latkit/shaders/draw/triangles.wesl @@ -2,16 +2,15 @@ // shows its exact colors; turned away, a face darkens by the theme's shading. Faces receive the // ground's shadow map. The cast variant draws them through the light's eye into the map. -import package::lib::bind::{frame, eye}; +import package::lib::bind::{frame, eye, word}; import package::lib::project::culled; import package::lib::triangles::{triangle_corner, triangle_shown, corner_clip}; +import package::lib::state::end_normal; import package::lib::marker::{MarkerColor, glow}; import package::lib::focus::focus_of; import package::lib::shade::{caster, lit}; import package::lib::color::output; -// Each vertex's summed face normals, in fixed point. -@group(2) @binding(3) var normals: array; @if(!casts) @group(2) @binding(10) var shadows: texture_2d; @@ -36,8 +35,11 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: } out.clip = corner_clip(c); out.world = c.world; - let s = c.slot * 3u; - out.normal = vec3f(f32(normals[s]), f32(normals[s + 1u]), f32(normals[s + 2u])); + // The normals summed at each vertex, in fixed point. + let n = end_normal(corner, c.vertex); + if n != NONE { + out.normal = vec3f(f32(bitcast(word(n))), f32(bitcast(word(n + 1u))), f32(bitcast(word(n + 2u)))); + } out.color = c.color; out.focus = focus_of(c.row); return out; diff --git a/crates/latkit/shaders/lib/bind.wesl b/crates/latkit/shaders/lib/bind.wesl index 0fee192f..49b261fd 100644 --- a/crates/latkit/shaders/lib/bind.wesl +++ b/crates/latkit/shaders/lib/bind.wesl @@ -3,23 +3,33 @@ import package::lib::types::{Frame, Eye, Mark}; @group(0) @binding(0) var frame: Frame; -// Data pages: column values as f32 or u32 words, in blocks the arena hands out. -@group(0) @binding(1) var pages: array; // Compiled marks: column descriptors, page tables, code, constants, ends. -@group(0) @binding(2) var program: array; -// Selection and hover bits, one word per row. -@group(0) @binding(3) var focus: array; -@group(0) @binding(4) var colormaps: texture_2d_array; -@group(0) @binding(5) var ramp: sampler; +@group(0) @binding(1) var program: array; +@group(0) @binding(2) var colormaps: texture_2d_array; +@group(0) @binding(3) var ramp: sampler; // What the pass draws through, at a dynamic offset. -@group(0) @binding(6) var eye: Eye; +@group(0) @binding(4) var eye: Eye; // The rows each mark's focus draw lights: every mark's, at its `focused`. -@group(0) @binding(7) var focused: array; +@group(0) @binding(5) var focused: array; @group(1) @binding(0) var mark: Mark; -// Evaluated rows of every mark: compute passes write them, render passes read them. -@if(writes) -@group(2) @binding(0) var state: array; +// The pages, in blocks the arena hands out: data pages, column values as f32 or u32 words; and each +// mark's pages of words per row, which compute passes write and render passes read. A pass summing +// into them binds them as atomics. +@if(atomic) +@group(2) @binding(0) var pages: array>; +@elif(writes) +@group(2) @binding(0) var pages: array; @else -@group(2) @binding(0) var state: array; +@group(2) @binding(0) var pages: array; + +// Word `at` of the pages. +@if(atomic) +fn word(at: u32) -> u32 { + return atomicLoad(&pages[at]); +} +@else +fn word(at: u32) -> u32 { + return pages[at]; +} diff --git a/crates/latkit/shaders/lib/focus.wesl b/crates/latkit/shaders/lib/focus.wesl index a7c3a9ab..3ca23288 100644 --- a/crates/latkit/shaders/lib/focus.wesl +++ b/crates/latkit/shaders/lib/focus.wesl @@ -1,14 +1,16 @@ -// Selection and hover: one word of bits per held row; and how present a row is while its mark eases. +// Selection and hover: one word of bits per held row, in pages the page table at `mark.focus` lists; +// and how present a row is while its mark eases. import package::lib::types::{SELECTED, HOVERED, FADE_IN, FADE_OUT}; -import package::lib::bind::{frame, focus, mark}; +import package::lib::bind::{frame, mark, word}; +import package::lib::state::record_at; // The focus bits of held row `row`; none for a row that left. fn focus_of(row: u32) -> u32 { if row >= mark.held - mark.exits { return 0u; } - return focus[mark.focus + row]; + return word(record_at(mark.focus, 1u, row)); } fn is_selected(row: u32) -> bool { diff --git a/crates/latkit/shaders/lib/path.wesl b/crates/latkit/shaders/lib/path.wesl index 8996808e..42cf9e2a 100644 --- a/crates/latkit/shaders/lib/path.wesl +++ b/crates/latkit/shaders/lib/path.wesl @@ -8,9 +8,10 @@ import package::lib::types::{ NONE, VISIBLE, PLACED, DASH, ENDS_COLOR, GEODESIC, STEP_BEFORE, STEP_AFTER, MARK_EDGES, PATH_ENDS, PATH_TRACE, PATH_GEOMETRY, PATH_POLES, EDGE_WIDTH, LINE_WIDTH, GEODESIC_STEP, OWN_HEIGHT, TRIANGLES, MAX_PIECES, BOW_PIECES, SPACE_EARTH, + MISSING, FAMILY_LANES, }; import package::lib::bind::{frame, eye, mark, focused}; -import package::lib::state::{row_at, edge_at, line_at, vertex_slot, Vertex}; +import package::lib::state::{row_at, edge_at, line_at, end_vertex, same_vertex, Vertex}; import package::lib::project::{PI, world, clip_of, shows, to_screen}; import package::lib::read::{missing_bits, column_value, points_of, point_bits}; import package::lib::trace::{trace_run, trace_level, trace_axis, trace_entry}; @@ -154,9 +155,51 @@ fn geometry_count(row: u32) -> u32 { return points_of(mark.geometry, row).count; } +// Whether lane `l` of edge row `row` draws: the lanes of a family draw as the pixels they cover ask, +// so its thousands of lanes take as many as the screen shows, in draws and picks alike. A lane whose +// curve lies off the screen draws nothing; and where a pixel holds many lanes, which bow apart by the +// same step, only every `step`-th lane draws, `step` a power of two, so the lanes drawn lie at least +// a pixel apart. +fn lane_drawn(row: u32, l: Lane) -> bool { + let e = edge_at(row_at(row)); + let a = end_vertex(0u, e.a); + let b = end_vertex(1u, e.b); + let ca = clip_of(world(a.hi, a.lo, a.elevation)); + let cb = clip_of(world(b.hi, b.lo, b.elevation)); + if ca.w <= 0.0 || cb.w <= 0.0 { + return true; + } + let sa = to_screen(ca); + let sb = to_screen(cb); + if (mark.flags & GEODESIC) == 0u { + // The curve keeps within the triangle of its ends and the point that bows it, twice as far + // out as its middle. + let c = between(a, b, 0.5, 2.0 * l.offset * frame.lane_bow); + let cc = clip_of(world(c.hi, c.lo, c.z)); + if cc.w > 0.0 { + let sc = to_screen(cc); + // Room for the stroke's width past the curve. + let margin = vec2f(64.0); + let lo = min(min(sa, sb), sc) - margin; + let hi = max(max(sa, sb), sc) + margin; + if any(hi < vec2f(0.0)) || any(lo > eye.size) { + return false; + } + } + } + // Pixels between neighboring lanes at their middle. + let apart = distance(sa, sb) * abs(frame.lane_bow) * l.step; + if !(apart < 1.0) { + return true; + } + let step = u32(exp2(ceil(log2(1.0 / max(apart, 1e-6))))); + return l.index % step == 0u; +} + // Segments row `row`'s path has, or for an area its triangles: the count a list over the mark's rows // counts. A path between two ends without bends runs in one piece, or bowed into a lane in enough -// for the curve, and along a great circle in one per few degrees of its arc. +// for the curve, none where its lane lies off the screen or is one of many a pixel holds, and along a +// great circle in one per few degrees of its arc. fn row_segments(row: u32) -> u32 { switch PATH { case PATH_ENDS: { @@ -164,7 +207,11 @@ fn row_segments(row: u32) -> u32 { if middle > 0u { return middle + 1u; } - let pieces = select(1u, BOW_PIECES, lane(row) != 0.0); + let l = lane_of(row); + if l.offset != 0.0 && !lane_drawn(row, l) { + return 0u; + } + let pieces = select(1u, BOW_PIECES, l.offset != 0.0); if (mark.flags & GEODESIC) == 0u { return pieces; } @@ -172,8 +219,8 @@ fn row_segments(row: u32) -> u32 { if e.a == NONE || e.b == NONE { return 0u; } - let a = vertex_slot(e.a); - let b = vertex_slot(e.b); + let a = end_vertex(0u, e.a); + let b = end_vertex(1u, e.b); return max(pieces, arcs(a.hi + a.lo, b.hi + b.lo)); } case PATH_GEOMETRY: { @@ -190,9 +237,9 @@ fn row_segments(row: u32) -> u32 { } } -// The vertex in slot `slot` as a point of a path, placed. -fn vertex_point(slot: u32) -> PathPoint { - let v = vertex_slot(slot); +// The vertex held row `held` names through end `end` as a point of a path, placed. +fn vertex_point(end: u32, held: u32) -> PathPoint { + let v = end_vertex(end, held); var p: PathPoint; p.hi = v.hi; p.lo = v.lo; @@ -279,16 +326,16 @@ fn between(a: Vertex, b: Vertex, t: f32, bow: f32) -> PathPoint { // place, so shaders compile small. fn end_point(row: u32, count: u32, i: u32) -> PathPoint { let e = edge_at(row_at(row)); - if e.a == NONE || e.b == NONE || e.a == e.b { + if e.a == NONE || e.b == NONE || same_vertex(e.a, e.b) { return hidden(); } let bends = points_of(mark.geometry, row); let last = select(bends.count + 1u, count, bends.count == 0u); if i == 0u || i >= last { - return vertex_point(select(e.b, e.a, i == 0u)); + return vertex_point(select(1u, 0u, i == 0u), select(e.b, e.a, i == 0u)); } - let a = vertex_slot(e.a); - let b = vertex_slot(e.b); + let a = end_vertex(0u, e.a); + let b = end_vertex(1u, e.b); let t = f32(i) / f32(last); var p: PathPoint; if bends.count == 0u { @@ -383,7 +430,7 @@ fn pole_point(row: u32, i: u32) -> PathPoint { if e.a == NONE { return hidden(); } - var p = vertex_point(e.a); + var p = vertex_point(0u, e.a); if i > 0u { p.z = 0.0; p.half = vec2f(0.0); @@ -424,12 +471,32 @@ fn ends_part(s: Segment) -> bool { } // Lanes from the middle that row `row` runs in among edges joining the same two vertices. -fn lane(row: u32) -> f32 { +// A row's lane among its family, the edges joining the same two vertices: how far it bows, in lanes +// of the theme's bow, its place among the family from one side, and the step between lanes. A family +// spreads as wide as `FAMILY_LANES` lanes at most: past that its lanes pack closer, a step apart. +struct Lane { + offset: f32, + index: u32, + step: f32, +} + +fn lane_of(row: u32) -> Lane { if mark.lane == NONE || row >= mark.held - mark.exits { - return 0.0; + return Lane(0.0, 0u, 1.0); } - let bits = column_value(mark.lane, row).x; - return select(bitcast(bits), 0.0, missing_bits(bits)); + // Twice its lane, past half a word's range, above; its family's size below. + let word = column_value(mark.lane, row).x; + let size = word & 0xffffu; + if word == NONE || word == MISSING || size < 2u { + return Lane(0.0, 0u, 1.0); + } + let twice = i32(word >> 16u) - 32768; + let step = min(1.0, FAMILY_LANES / f32(size)); + return Lane(0.5 * f32(twice) * step, u32((twice + i32(size) - 1) / 2), step); +} + +fn lane(row: u32) -> f32 { + return lane_of(row).offset; } // The part of the segment from clip position `ca` to `cb` in front of the near plane, as fractions diff --git a/crates/latkit/shaders/lib/placement.wesl b/crates/latkit/shaders/lib/placement.wesl index f279ed31..4d24cd4d 100644 --- a/crates/latkit/shaders/lib/placement.wesl +++ b/crates/latkit/shaders/lib/placement.wesl @@ -1,28 +1,37 @@ -// The buffer label placement works in: each labeled mark's candidates, the texts it asks the host to -// set and those its labels show, and the cells of the screen labels claim. +// What label placement works in: each labeled row's candidate, in pages the page table at +// `mark.candidates` lists; and the labels buffer, with the texts each mark asks the host to set and +// those its labels show, its run table, the select capping its labels, and the cells of the screen +// labels claim. import package::lib::types::{CANDIDATE_WORDS, WANTED}; -import package::lib::bind::mark; +import package::lib::bind::{mark, pages}; +import package::lib::state::record_at; @group(2) @binding(13) var labels: array>; -fn word(at: u32) -> u32 { +fn label_word(at: u32) -> u32 { return atomicLoad(&labels[at]); } -fn put(at: u32, value: u32) { +fn put_label(at: u32, value: u32) { atomicStore(&labels[at], value); } +// Where held row `row`'s candidate starts among the pages. fn candidate(row: u32) -> u32 { - return mark.candidates + row * CANDIDATE_WORDS; + return record_at(mark.candidates, CANDIDATE_WORDS, row); +} + +// Writes word `at` of the pages: a candidate's. +fn put(at: u32, value: u32) { + pages[at] = value; } // Asks the host to set the text of `code`. fn want(code: u32) { let asked = atomicAdd(&labels[mark.wanted], 1u); if asked < WANTED { - put(mark.wanted + 1u + asked, code); + put_label(mark.wanted + 1u + asked, code); } } diff --git a/crates/latkit/shaders/lib/points.wesl b/crates/latkit/shaders/lib/points.wesl index 7cb27591..b4385d09 100644 --- a/crates/latkit/shaders/lib/points.wesl +++ b/crates/latkit/shaders/lib/points.wesl @@ -6,7 +6,7 @@ import package::lib::types::{ SIDE_TOP, PORT_WORDS, PORT_CODE, PORT_SHAPE, PORT_COLUMN, HALF_UNITS, OWN_HEIGHT, SPACE_DIAGRAM, }; import package::lib::bind::{frame, eye, mark, program}; -import package::lib::state::{vertex_slot, Vertex}; +import package::lib::state::{row_at, vertex_at, Vertex}; import package::lib::project::{world, clip_of, shows, to_screen}; import package::lib::focus::{focus_of, faded, growth}; import package::lib::read::bits_value; @@ -37,7 +37,7 @@ fn placed(v: Vertex) -> bool { // The marker of row `row` of the current vertex mark. A hovered marker sized in pixels grows; one // sized in units is part of the drawing, and only glows. fn point(row: u32) -> Point { - let v = vertex_slot(mark.state / VERTEX_WORDS + row); + let v = vertex_at(row_at(row)); var half = vec2f(px(v.half.x, VERTEX_HALF)); if (mark.flags & SIZED) != 0u { half.y = px(v.half.y, VERTEX_HALF + 1u); diff --git a/crates/latkit/shaders/lib/read.wesl b/crates/latkit/shaders/lib/read.wesl index f5bf548d..dccc5c5d 100644 --- a/crates/latkit/shaders/lib/read.wesl +++ b/crates/latkit/shaders/lib/read.wesl @@ -1,13 +1,13 @@ // Reading columns: page tables in `program` point at blocks of `pages`. A column's pages are its -// data's, or for a mark drawing a table in part the pages of it the mark holds, a slot each; its -// descriptor says how many rows a page holds. Values stay `u32` bits until arithmetic needs them, so +// data's, or for a mark drawing a table in part the pages of it the mark holds, a slot each; every +// page holds `PAGE_ROWS` rows. Values stay `u32` bits until arithmetic needs them, so // a missing value is never an `f32` constant (WGSL cannot represent NaN constants). import package::lib::types::{ NONE, MISSING, COLUMN_WORDS, COLUMN_FLAGS, COLUMN_ROWS, COLUMN_TABLE, COLUMN_LOW, COLUMN_JOIN, COLUMN_T, - COLUMN_BITS, SAMPLED, STEP, POINT_BITS, POINT_MASK, PATH_PAGE_WORDS, + SAMPLED, STEP, PAGE_BITS, PAGE_MASK, POINT_BITS, POINT_MASK, PATH_PAGE_WORDS, }; -import package::lib::bind::{pages, program, mark}; +import package::lib::bind::{program, mark, word}; // Whether a pipeline's marks read fields of other tables by row id, and fields sampled over an axis // at the shown coordinate: set when it is made, so a scene doing neither compiles neither. @@ -31,18 +31,17 @@ fn page_word(block: u32, offset: u32, fallback: u32) -> u32 { if block == NONE { return fallback; } - return pages[block + offset]; + return word(block + offset); } -// The page of column `column` row `row` lies in, and where in it. -fn page_of(column: u32, row: u32) -> vec2u { - let bits = column_word(column, COLUMN_BITS); - return vec2u(row >> bits, row & ((1u << bits) - 1u)); +// The page row `row` lies in, and where in it. +fn page_of(row: u32) -> vec2u { + return vec2u(row >> PAGE_BITS, row & PAGE_MASK); } // A static column's value as (high, low) bits: the low part carries what f32 drops of an f64. fn static_value(column: u32, row: u32) -> vec2u { - let at = page_of(column, row); + let at = page_of(row); let page = at.x; let local = at.y; let table = column_word(column, COLUMN_TABLE); @@ -57,7 +56,7 @@ fn static_value(column: u32, row: u32) -> vec2u { // A sampled column's value at the shown coordinate, as bits. Each page of rows has a cursor of six // words: block and word of this frame, block and word of the next frame, and both frames' row counts. fn sample_value(column: u32, row: u32) -> u32 { - let at = page_of(column, row); + let at = page_of(row); let cursor = column_word(column, COLUMN_TABLE) + at.x * 6u; let local = at.y; if local >= program[cursor + 4u] { @@ -82,7 +81,7 @@ fn bits_value(column: u32, row: u32) -> u32 { return NONE; } let table = column_word(column, COLUMN_TABLE); - let at = page_of(column, row); + let at = page_of(row); return page_word(program[table + at.x], at.y, NONE); } @@ -122,17 +121,17 @@ fn points_of(column: u32, row: u32) -> Points { return Points(0u, 0u, 0u); } let table = column_word(column, COLUMN_TABLE); - let at = page_of(column, row); + let at = page_of(row); let page = table + PATH_PAGE_WORDS * at.x; let firsts = program[page]; if firsts == NONE || at.y >= program[page + 3u] { return Points(0u, 0u, 0u); } let local = at.y; - let first = pages[firsts + local]; + let first = word(firsts + local); var end = program[page + 2u]; if local + 1u < program[page + 3u] { - end = pages[firsts + local + 1u]; + end = word(firsts + local + 1u); } return Points(table + program[page + 1u], first, end - first); } @@ -141,5 +140,5 @@ fn points_of(column: u32, row: u32) -> Points { fn point_bits(points: Points, k: u32) -> vec2u { let i = points.first + k; let at = program[points.blocks + (i >> POINT_BITS)] + 2u * (i & POINT_MASK); - return vec2u(pages[at], pages[at + 1u]); + return vec2u(word(at), word(at + 1u)); } diff --git a/crates/latkit/shaders/lib/state.wesl b/crates/latkit/shaders/lib/state.wesl index 65e772fa..952172d1 100644 --- a/crates/latkit/shaders/lib/state.wesl +++ b/crates/latkit/shaders/lib/state.wesl @@ -1,14 +1,16 @@ -// Evaluated rows: words of the `state` buffer. A row's kind words sit at `mark.state`, `mark.stride` -// apart, so vertex slots count every vertex mark's rows; its extra words (labels, marker inputs) sit -// at `mark.extra`, `mark.extras` apart. +// Evaluated rows: each mark's words per row, in pages of `PAGE_ROWS` held rows. A row's words are its +// kind words, `mark.stride` of them, then its extra words (labels, marker inputs), `mark.extras`; the +// page table at `mark.state` says where each page of them lies. A mark reads the vertices its ends name +// through the page tables of the vertex marks drawing them, which its ends list. import package::lib::types::{ - NONE, VERTEX_WORDS, VERTEX_POSITION, VERTEX_COLOR, VERTEX_STATUS, VERTEX_HALF, VERTEX_ELEVATION, VERTEX_DEPTH, - VERTEX_RANGE, VERTEX_FLAGS, EDGE_FROM, EDGE_TO, EDGE_COLOR, EDGE_WIDTH, EDGE_FLOW, EDGE_TRAVEL, EDGE_FLAGS, FACE_A, FACE_B, - FACE_C, FACE_COLOR, FACE_FLAGS, LINE_COLOR, LINE_WIDTH, LINE_FLOW, LINE_TRAVEL, LINE_FLAGS, AREA_COLOR, - AREA_FLAGS, MARK_EDGES, MARK_LINES, + NONE, PAGE_BITS, PAGE_MASK, END_WORDS, END_STATE, END_STRIDE, END_NORMALS, VERTEX_POSITION, VERTEX_COLOR, + VERTEX_STATUS, VERTEX_HALF, VERTEX_ELEVATION, VERTEX_DEPTH, VERTEX_RANGE, VERTEX_FLAGS, EDGE_FROM, EDGE_TO, + EDGE_COLOR, EDGE_WIDTH, EDGE_FLOW, EDGE_TRAVEL, EDGE_FLAGS, FACE_A, FACE_B, FACE_C, FACE_COLOR, FACE_FLAGS, + LINE_COLOR, LINE_WIDTH, LINE_FLOW, LINE_TRAVEL, LINE_FLAGS, AREA_COLOR, AREA_FLAGS, MARK_EDGES, MARK_LINES, + NORMAL_WORDS, }; -import package::lib::bind::{state, mark}; +import package::lib::bind::{program, mark, word}; struct Vertex { hi: vec2f, @@ -67,17 +69,22 @@ fn unpack_range(word: u32) -> vec2f { } fn float_at(at: u32) -> f32 { - return bitcast(state[at]); + return bitcast(word(at)); +} + +// Where the words of `row` start among the pages page table `table` lists, `stride` words a row. +fn record_at(table: u32, stride: u32, row: u32) -> u32 { + return program[table + (row >> PAGE_BITS)] + (row & PAGE_MASK) * stride; } // The first kind word of `row` of the current mark. fn row_at(row: u32) -> u32 { - return mark.state + row * mark.stride; + return record_at(mark.state, mark.stride + mark.extras, row); } -// Extra word `word` of `row` of the current mark. -fn extra_at(row: u32, word: u32) -> u32 { - return mark.extra + row * mark.extras + word; +// Extra word `w` of `row` of the current mark. +fn extra_at(row: u32, w: u32) -> u32 { + return row_at(row) + mark.stride + w; } fn vertex_at(at: u32) -> Vertex { @@ -85,45 +92,61 @@ fn vertex_at(at: u32) -> Vertex { return Vertex( vec2f(float_at(p), float_at(p + 1u)), vec2f(float_at(p + 2u), float_at(p + 3u)), - state[at + VERTEX_COLOR], - state[at + VERTEX_STATUS], + word(at + VERTEX_COLOR), + word(at + VERTEX_STATUS), vec2f(float_at(at + VERTEX_HALF), float_at(at + VERTEX_HALF + 1u)), float_at(at + VERTEX_ELEVATION), float_at(at + VERTEX_DEPTH), - unpack_range(state[at + VERTEX_RANGE]), - state[at + VERTEX_FLAGS], + unpack_range(word(at + VERTEX_RANGE)), + word(at + VERTEX_FLAGS), ); } -// The vertex in graph slot `slot`, from any vertex mark. -fn vertex_slot(slot: u32) -> Vertex { - return vertex_at(slot * VERTEX_WORDS); +// The vertex held row `held` of the vertex mark drawing the table end `end` of the current mark names. +fn end_vertex(end: u32, held: u32) -> Vertex { + let e = mark.ends + END_WORDS * end; + return vertex_at(record_at(program[e + END_STATE], program[e + END_STRIDE], held)); +} + +// Whether ends `a` and `b` of an edge, held rows of the vertex marks its two ends attach to, are one +// vertex. +fn same_vertex(a: u32, b: u32) -> bool { + return a == b && program[mark.ends + END_STATE] == program[mark.ends + END_WORDS + END_STATE]; +} + +// Where the normal faces sum at the vertex held row `held` names through end `end` lies, or NONE. +fn end_normal(end: u32, held: u32) -> u32 { + let table = program[mark.ends + END_WORDS * end + END_NORMALS]; + if table == NONE { + return NONE; + } + return record_at(table, NORMAL_WORDS, held); } fn edge_at(at: u32) -> Edge { return Edge( - state[at + EDGE_FROM], - state[at + EDGE_TO], - state[at + EDGE_COLOR], + word(at + EDGE_FROM), + word(at + EDGE_TO), + word(at + EDGE_COLOR), float_at(at + EDGE_WIDTH), float_at(at + EDGE_FLOW), float_at(at + EDGE_TRAVEL), - state[at + EDGE_FLAGS], + word(at + EDGE_FLAGS), ); } fn face_at(at: u32) -> Face { - let corners = vec3u(state[at + FACE_A], state[at + FACE_B], state[at + FACE_C]); - return Face(corners, state[at + FACE_COLOR], state[at + FACE_FLAGS]); + let corners = vec3u(word(at + FACE_A), word(at + FACE_B), word(at + FACE_C)); + return Face(corners, word(at + FACE_COLOR), word(at + FACE_FLAGS)); } fn line_at(at: u32) -> Line { return Line( - state[at + LINE_COLOR], + word(at + LINE_COLOR), float_at(at + LINE_WIDTH), float_at(at + LINE_FLOW), float_at(at + LINE_TRAVEL), - state[at + LINE_FLAGS], + word(at + LINE_FLAGS), ); } @@ -138,5 +161,5 @@ fn travel_words() -> vec2u { } fn area_at(at: u32) -> Area { - return Area(state[at + AREA_COLOR], state[at + AREA_FLAGS]); + return Area(word(at + AREA_COLOR), word(at + AREA_FLAGS)); } diff --git a/crates/latkit/shaders/lib/trace.wesl b/crates/latkit/shaders/lib/trace.wesl index 348773b1..9903cde4 100644 --- a/crates/latkit/shaders/lib/trace.wesl +++ b/crates/latkit/shaders/lib/trace.wesl @@ -9,7 +9,7 @@ // value where it draws; between runs that do not meet, a point is left out, breaking the line. import package::lib::types::{NONE, MISSING, COLUMN_TABLE}; -import package::lib::bind::{pages, program, mark}; +import package::lib::bind::{program, mark, word}; import package::lib::read::{column_word, page_of}; const HEADER: u32 = 4u; @@ -68,14 +68,14 @@ fn trace_axis(slot: u32, entry: u32) -> vec2f { let l = trace_level_at(slot); let e = last_at_most(trace_table() + program[l + 2u], max(program[l + 1u], 1u), 2u, 1u, entry); let w = program[e] + (entry - program[e + 1u]) * 2u; - return vec2f(bitcast(pages[w]), bitcast(pages[w + 1u])); + return vec2f(bitcast(word(w)), bitcast(word(w + 1u))); } // The bits of `row`'s value of entry `entry` of level `slot`: a frame's, or a bucket's least // (`side` 0) or greatest (1). fn trace_entry(row: u32, slot: u32, entry: u32, side: u32) -> u32 { let t = trace_table(); - let at = page_of(mark.trace, row); + let at = page_of(row); let block = at.x; if block >= program[t + 1u] { return MISSING; @@ -91,5 +91,5 @@ fn trace_entry(row: u32, slot: u32, entry: u32, side: u32) -> u32 { if program[e] == NONE || local >= per || entry < program[e + 1u] { return MISSING; } - return pages[program[e] + (entry - program[e + 1u]) * per + local]; + return word(program[e] + (entry - program[e + 1u]) * per + local); } diff --git a/crates/latkit/shaders/lib/triangles.wesl b/crates/latkit/shaders/lib/triangles.wesl index 1512ebdf..36ace24b 100644 --- a/crates/latkit/shaders/lib/triangles.wesl +++ b/crates/latkit/shaders/lib/triangles.wesl @@ -5,7 +5,7 @@ import package::lib::types::{NONE, VISIBLE, PLACED, ENDS_COLOR, PATH_GEOMETRY}; import package::lib::bind::mark; -import package::lib::state::{row_at, face_at, area_at, vertex_slot}; +import package::lib::state::{row_at, face_at, area_at, end_vertex}; import package::lib::project::{world, clip_of, shows}; import package::lib::focus::faded; import package::lib::read::{points_of, point_bits, missing_bits}; @@ -16,8 +16,8 @@ struct Corner { world: vec3f, // Where it stands, in position units. position: vec3f, - // A face corner's vertex slot; `NONE` for an area's. - slot: u32, + // A face corner's vertex, a held row of the vertex mark its end attaches to; `NONE` for an area's. + vertex: u32, color: vec4f, row: u32, // Whether it has a place to stand, and whether it lies in front of the globe's horizon. @@ -77,8 +77,8 @@ fn face_corner(row: u32, corner: u32) -> Corner { c.shown = false; return c; } - c.slot = f.corners[corner]; - let v = vertex_slot(c.slot); + c.vertex = f.corners[corner]; + let v = end_vertex(corner, c.vertex); c.world = world(v.hi, v.lo, v.elevation); c.position = vec3f(v.hi + v.lo, v.elevation); let color = select(unpack4x8unorm(f.color), unpack4x8unorm(v.color), (mark.flags & ENDS_COLOR) != 0u); @@ -93,7 +93,7 @@ fn face_corner(row: u32, corner: u32) -> Corner { fn area_point(t: Segment, offset: u32) -> Corner { var c: Corner; c.row = t.row; - c.slot = NONE; + c.vertex = NONE; let a = area_at(row_at(t.row)); let points = points_of(mark.geometry, t.row); let k = select(3u * t.index + offset, 0u, offset == 0u); diff --git a/crates/latkit/shaders/lib/types.wesl b/crates/latkit/shaders/lib/types.wesl index 1ed16cee..da2e8041 100644 --- a/crates/latkit/shaders/lib/types.wesl +++ b/crates/latkit/shaders/lib/types.wesl @@ -57,31 +57,24 @@ const OP_STORE_CODE: u32 = 44u; // Values the expression stack holds at once. const STACK: u32 = 16u; -// Column descriptor words, eight per column in `meta`. -const COLUMN_WORDS: u32 = 8u; +// Column descriptor words, six per column in `meta`. +const COLUMN_WORDS: u32 = 6u; const COLUMN_FLAGS: u32 = 0u; const COLUMN_ROWS: u32 = 1u; const COLUMN_TABLE: u32 = 2u; const COLUMN_LOW: u32 = 3u; const COLUMN_JOIN: u32 = 4u; const COLUMN_T: u32 = 5u; -// Rows per page of the column's table, as a shift: a page of the data, or a page a table drawn in -// part holds. -const COLUMN_BITS: u32 = 6u; // Column flags. const SAMPLED: u32 = 1u; const STEP: u32 = 2u; const BITS: u32 = 4u; -// Rows per data page, as a shift. -const PAGE_BITS: u32 = 16u; -const PAGE_MASK: u32 = 65535u; -// Rows per page a table drawn in part holds, as a shift: a mark's rows of it lie a page at a time in -// slots, in the order its pyramid draws them. -const HELD_BITS: u32 = 12u; -const HELD_MASK: u32 = 4095u; -// Rows a block holds: what a mark evaluates at a time, a held page. -const BLOCK_ROWS: u32 = 4096u; +// Rows per page, as a shift: a page of the data, of a table drawn in part as its marks hold it, a +// slot at a time, and of a mark's rows, which it evaluates a page at a time. +const PAGE_BITS: u32 = 12u; +const PAGE_MASK: u32 = 4095u; +const PAGE_ROWS: u32 = 4096u; // Items a dispatch over more items than one dimension holds steps through at a time in its second // dimension, 256 workgroups of 256 wide. const DISPATCH_ROWS: u32 = 65536u; @@ -110,12 +103,25 @@ const BLEND_COLOR: u32 = 3u; const BLEND_POSITION: u32 = 4u; // Points per block of a geometry's points, as a shift: a block of x and y words is a chunk. -const POINT_BITS: u32 = 15u; -const POINT_MASK: u32 = 32767u; +const POINT_BITS: u32 = 16u; +const POINT_MASK: u32 = 65535u; // Words of a geometry column's table per page: the block of each row's first point, where the page's // blocks of points are listed in the table, how many points the page has, and how many rows. const PATH_PAGE_WORDS: u32 = 4u; +// Words of each end of a mark in its program: where the vertex mark drawing the end's table keeps +// its rows (the page table of its rows' words and how many words a row has), where it holds each page +// of the order it draws that table in (NONE where it draws the table whole), and the page table of the +// normals faces sum at its rows (NONE where none do). +const END_WORDS: u32 = 4u; +const END_STATE: u32 = 0u; +const END_STRIDE: u32 = 1u; +const END_SLOTS: u32 = 2u; +const END_NORMALS: u32 = 3u; + +// Words of the normal faces sum at a vertex row: x, y, and z in fixed point. +const NORMAL_WORDS: u32 = 3u; + // Vertex state words. const VERTEX_WORDS: u32 = 12u; const VERTEX_POSITION: u32 = 0u; @@ -162,6 +168,8 @@ const GEODESIC_STEP: f32 = 2.0; const MAX_PIECES: u32 = 64u; // Pieces a path bowed into a lane takes at least. const BOW_PIECES: u32 = 16u; +// Lanes a family of edges joining the same two vertices spreads as wide as at most: more pack closer. +const FAMILY_LANES: f32 = 8.0; // Face state words. const FACE_WORDS: u32 = 5u; @@ -204,12 +212,12 @@ const CANDIDATE_ALPHA: u32 = 6u; const TEXT_BASE: f32 = 32.0; const TEXT_SPREAD: f32 = 8.0; // Candidate states: in view and wanting room, placed, dropped, placed in the frame before, and -// measured by an estimate while its text is not set. +// placed while its text is not set. const WANTS: u32 = 1u; const PLACED_LABEL: u32 = 2u; const DROPPED: u32 = 4u; const STICKY: u32 = 8u; -const ESTIMATED: u32 = 16u; +const UNSET: u32 = 16u; // A text run whose distance field the GPU makes, as words at the start of the fields buffer: where its // coverage starts, its coverage's width and height, where its columns' least distances start, and // where its field starts and the words between the starts of its rows. @@ -221,9 +229,9 @@ const FIELD_AT: u32 = 4u; const FIELD_ROW: u32 = 5u; const FIELD_WORDS: u32 = 6u; -// A run table entry whose text is not set yet holds this flag as its first corner and the run's -// estimated size as its second, so its label can be placed before it is set. -const TEXT_ESTIMATE: u32 = 0x80000000u; +// A run table entry whose text is not set yet holds this flag as its first corner and the run's size, +// as measured, as its second, so its label is placed where it will show before it is set. +const TEXT_UNSET: u32 = 0x80000000u; // A cell a placed label holds. const OCCUPIED: u32 = 0u; // CSS pixels a cell of the label grid covers. @@ -511,7 +519,8 @@ struct Mark { rows: u32, exits: u32, // Rows the mark holds evaluated, leaving rows included: of a table drawn whole its rows, of one - // drawn in part its held pages' rows. Every per-row word is a held row's. + // drawn in part its held pages' rows. Every word a mark keeps per row is a held row's, in pages of + // `PAGE_ROWS` held rows each that page tables in the program list. held: u32, // Where the program lists which held rows the drawn rows are, a run at a time, and where the // lists hold each drawn row's word, expanded from them; NONE where every drawn row is the held @@ -520,14 +529,14 @@ struct Mark { drawn: u32, // How far the tiles coming and going have faded, from 0 to 1. reveal: f32, - // Each row's kind words: where row 0's start and how many; then its extra words. + // The page table of each row's words: its kind words, `stride` of them, then its extra words. state: u32, stride: u32, - extra: u32, extras: u32, - // While the mark eases, where row 0's frozen kind and extra words start; NONE otherwise. + // While the mark eases, the page tables of each row's words as it showed when the transition + // began, and as its channels evaluate them, where it settles; NONE otherwise. frozen: u32, - frozen_extra: u32, + settled: u32, motion: f32, // Where the program holds how each word of a row eases, a word each. blends: u32, @@ -535,6 +544,7 @@ struct Mark { // `blocks` of them. block: u32, blocks: u32, + // The page table of each row's focus word. focus: u32, domains: u32, columns: u32, @@ -579,10 +589,10 @@ struct Mark { // Each port of a vertex mark's rows, and how many each row has. port_info: u32, ports: u32, - // The extra word of the label's code, or NONE; the labels buffer's offsets of its candidates, its - // run table (two words per code), its wanted codes (a count, then codes), and its seen codes (a - // bit per code shown in view); the id of its first candidate, unique across marks; the size - // labels and port labels are set at, in length units. + // The extra word of the label's code, or NONE; the page table of each row's candidate; the labels + // buffer's offsets of its run table (two words per code), its wanted codes (a count, then codes), + // and its seen codes (a bit per code shown in view); the id of its first candidate, unique across + // marks; the size labels and port labels are set at, in length units. label: u32, candidates: u32, runs: u32, @@ -597,6 +607,8 @@ struct Mark { // The CSS pixels the mark's size keeps within at any zoom: a vertex's half, a stroke's width. size_min: f32, size_max: f32, + // A vertex mark's page table of the normals faces sum at its rows; NONE where no face reads it. + normals: u32, } // A line or a run of text drawn over the marks, in CSS pixels. diff --git a/crates/latkit/src/data/bytes.rs b/crates/latkit/src/data/bytes.rs index ca00e975..b68bd62b 100644 --- a/crates/latkit/src/data/bytes.rs +++ b/crates/latkit/src/data/bytes.rs @@ -149,6 +149,25 @@ pub(crate) fn encode(patch: &Patch) -> Vec { w.0 } +/// `values` as bytes, as a field holding them writes them: how jobs hand a column over. +#[cfg(feature = "gpu")] +pub(crate) fn values_bytes(values: &Values) -> Vec { + let mut w = Writer(Vec::new()); + w.values(values); + w.0 +} + +/// The values `bytes` hold, every byte of them, as [`values_bytes`] wrote them. +#[cfg(feature = "gpu")] +pub(crate) fn read_values(bytes: &[u8]) -> Result { + let mut r = Reader { bytes, at: 0, remaining: DecodeLimits::default().decoded_bytes }; + let values = r.values()?; + if r.at != bytes.len() { + return Err(malformed("trailing bytes")); + } + Ok(values) +} + /// The patch `bytes` hold, every byte of them. pub(crate) fn decode(bytes: &[u8]) -> Result { match decode_with_limits(bytes, DecodeLimits::default())? { @@ -461,11 +480,14 @@ impl Reader<'_> { std::str::from_utf8(bytes).map(Name::from).map_err(|_| malformed("text is not UTF-8")) } + /// `n` values, copied whole. fn vec(&mut self, n: usize) -> Result> { // Includes conversion into column pages and the sampled levels derived from runs. self.charge(n, 4 * size_of::())?; let bytes = self.take(n.checked_mul(size_of::()).ok_or_else(|| malformed("too large"))?)?; - Ok(bytes.chunks_exact(size_of::()).map(bytemuck::pod_read_unaligned).collect()) + let mut out = vec![T::zeroed(); n]; + bytemuck::cast_slice_mut::(&mut out).copy_from_slice(bytes); + Ok(out) } fn ids(&mut self) -> Result { @@ -595,8 +617,7 @@ impl Reader<'_> { }) .collect::>>()?; let n = self.len(16)?; - let flat: Vec = self.vec(n * 2)?; - let coords = flat.as_chunks::<2>().0.to_vec(); + let coords: Vec<[f64; 2]> = self.vec(n)?; let geometry = Geometry::from_parts(depth, levels, coords).ok_or_else(|| malformed("geometry offsets"))?; if geometry.len() != len { diff --git a/crates/latkit/src/data/column.rs b/crates/latkit/src/data/column.rs index 98b2c15c..19ff17e9 100644 --- a/crates/latkit/src/data/column.rs +++ b/crates/latkit/src/data/column.rs @@ -4,8 +4,9 @@ use super::name::PageId; use super::values::{Kind, Picks, Value, Values}; use crate::Result; -/// Rows per page: pages are the unit of sharing between versions and of upload to the GPU. -pub(crate) const PAGE_BITS: u32 = 16; +/// Rows per page: pages are the unit of sharing between versions, of upload to the GPU, of what a +/// table draws in its order, and of what a mark keeps and evaluates per row. +pub(crate) const PAGE_BITS: u32 = 12; pub(crate) const PAGE_ROWS: usize = 1 << PAGE_BITS; pub(crate) const PAGE_MASK: usize = PAGE_ROWS - 1; diff --git a/crates/latkit/src/data/geometry.rs b/crates/latkit/src/data/geometry.rs index 25c4705f..aeba63d4 100644 --- a/crates/latkit/src/data/geometry.rs +++ b/crates/latkit/src/data/geometry.rs @@ -52,6 +52,16 @@ impl Geometry { &self.coords } + /// How many coordinates its rows span: of a slice, its own rows' only. + pub(crate) fn points(&self) -> usize { + let Some(rows) = self.levels.first() else { return self.coords.len() }; + let mut span = rows[0] as usize..rows[rows.len() - 1] as usize; + for offsets in &self.levels[1..] { + span = offsets[span.start] as usize..offsets[span.end] as usize; + } + span.len() + } + /// Whether row `row` holds exactly what `other`'s row `other_row` does: the same parts of the same /// coordinates, compared by their bits. pub(crate) fn same_row(&self, row: usize, other: &Geometry, other_row: usize) -> bool { @@ -109,18 +119,6 @@ impl Geometry { hasher.finish() } - /// Coordinates the rows span, every part's. - pub(crate) fn coords_spanned(&self) -> usize { - if self.depth == 0 { - return self.len(); - } - let mut span = 0..self.len(); - for offsets in &self.levels { - span = offsets[span.start] as usize..offsets[span.end] as usize; - } - span.len() - } - /// How many points row `row`'s path takes as shaders read it: every coordinate, and a break /// between its parts or rings. pub(crate) fn row_points(&self, row: usize) -> usize { @@ -204,6 +202,25 @@ impl GeometryBuilder { } } + /// Rows `rows` of `source`, in order: each level's offsets and the coordinates copied whole. + pub(crate) fn push_rows(&mut self, source: &Geometry, rows: Range) { + debug_assert_eq!(source.depth, self.depth); + let mut span = rows; + for level in 0..self.levels.len() { + let offsets = &source.levels[level]; + let (first, last) = (offsets[span.start], offsets[span.end]); + // Items of the next level pushed so far, which this run's items follow. + let next = match self.levels.get(level + 1) { + Some(below) => below.len() - 1, + None => self.coords.len(), + }; + let next = u32::try_from(next).expect("a geometry page holds at most 2^32 items"); + self.levels[level].extend(offsets[span.start + 1..=span.end].iter().map(|&o| o - first + next)); + span = first as usize..last as usize; + } + self.coords.extend_from_slice(&source.coords[span]); + } + fn copy(&mut self, source: &Geometry, level: usize, item: usize) { let span = source.span(level, item); if level + 1 == source.levels.len() { @@ -299,6 +316,26 @@ mod tests { assert_eq!(tail.get(1), geometry.get(2)); } + #[test] + fn runs_of_rows_copy_as_rows_do() { + let geometry = polygons(); + let (mut one, mut runs) = (GeometryBuilder::new(3), GeometryBuilder::new(3)); + for row in [0, 1, 2, 1, 2] { + one.push_from(&geometry, row); + } + runs.push_rows(&geometry, 0..3); + runs.push_rows(&geometry, 1..3); + let (one, runs) = (one.finish(), runs.finish()); + assert_eq!( + (0..5).map(|row| runs.get(row)).collect::>(), + (0..5).map(|row| one.get(row)).collect::>() + ); + let tail = geometry.slice(1..3); + let mut sliced = GeometryBuilder::new(3); + sliced.push_rows(&tail, 0..2); + assert_eq!(sliced.finish().get(1), geometry.get(2), "a slice's rows, past its first offsets"); + } + #[test] fn compact_parts_validate() { let geometry = polygons().slice(1..3); diff --git a/crates/latkit/src/data/ids.rs b/crates/latkit/src/data/ids.rs index d02462ea..b27f7698 100644 --- a/crates/latkit/src/data/ids.rs +++ b/crates/latkit/src/data/ids.rs @@ -1,4 +1,5 @@ use std::hash::BuildHasher; +use std::ops::Range; use hashbrown::HashTable; use hashbrown::hash_table::Entry; @@ -253,11 +254,28 @@ impl RowIds { /// The id → row index, past holes; an error names a repeated id. pub(crate) fn index(&self) -> Result { let RowIds::Listed(_) = self else { return Ok(Index::Identity(self.len() as u32)) }; + let mut table = HashTable::with_capacity(self.len()); + self.insert(&mut table, 0..self.len())?; + Ok(Index::Table(table)) + } + + /// Indexes rows `rows` into `table`, past holes; an error names a repeated id. Numbered ids are + /// read page by page, without matching each id's kind. + fn insert(&self, table: &mut HashTable, rows: Range) -> Result<()> { if let Some(pages) = self.numbers() { - return numbered(&pages, self.len()); + let id = |r: u32| pages[r as usize >> PAGE_BITS][r as usize & PAGE_MASK]; + for row in rows { + let n = id(row as u32); + match table.entry(hash(IdRef::Number(n)), |&r| id(r) == n, |&r| hash(IdRef::Number(id(r)))) { + Entry::Occupied(_) => return Err(Error::invalid(format!("id {n} appears twice")).at(row)), + Entry::Vacant(slot) => { + slot.insert(row as u32); + } + } + } + return Ok(()); } - let mut table = HashTable::with_capacity(self.len()); - for row in 0..self.len() { + for row in rows { let Some(key) = self.key(row) else { continue }; let eq = |&r: &u32| self.key(r as usize) == Some(key); let rehash = |&r: &u32| hash(self.key(r as usize).expect("indexed rows have ids")); @@ -268,28 +286,10 @@ impl RowIds { } } } - Ok(Index::Table(table)) + Ok(()) } } -/// The index of numbered ids, read page by page. -fn numbered(pages: &[&[u64]], len: usize) -> Result { - let id = |r: u32| pages[r as usize >> PAGE_BITS][r as usize & PAGE_MASK]; - let mut table = HashTable::with_capacity(len); - for (p, page) in pages.iter().enumerate() { - for (i, &n) in page.iter().enumerate() { - let row = p << PAGE_BITS | i; - match table.entry(hash(IdRef::Number(n)), |&r| id(r) == n, |&r| hash(IdRef::Number(id(r)))) { - Entry::Occupied(_) => return Err(Error::invalid(format!("id {n} appears twice")).at(row)), - Entry::Vacant(slot) => { - slot.insert(row as u32); - } - } - } - } - Ok(Index::Table(table)) -} - /// Finds a row by id without allocating. #[derive(Debug)] pub(crate) enum Index { @@ -302,6 +302,38 @@ pub(crate) fn hash(key: IdRef<'_>) -> u64 { } impl Index { + /// The index of `ids`, which hold the ids this one indexes and others after them, from row + /// `from`: this one with the others added, nothing it holds hashed again. + pub(crate) fn extended(&self, ids: &RowIds, from: usize) -> Result { + match (self, ids) { + (Index::Identity(_), RowIds::Count(n)) => Ok(Index::Identity(*n)), + (Index::Table(table), RowIds::Listed(_)) => { + let mut table = table.clone(); + ids.insert(&mut table, from..ids.len())?; + Ok(Index::Table(table)) + } + _ => ids.index(), + } + } + + /// The index of `ids`, the ids this one indexes but those of the rows `doomed` names, the rest + /// moving up past them: this one without them, renumbered, nothing hashed again. + pub(crate) fn without(&self, doomed: &[bool], ids: &RowIds) -> Result { + let (Index::Table(table), RowIds::Listed(_)) = (self, ids) else { return ids.index() }; + let mut moved = Vec::with_capacity(doomed.len()); + let mut next = 0; + for &gone in doomed { + moved.push(next); + next += u32::from(!gone); + } + let mut table = table.clone(); + table.retain(|r| !doomed[*r as usize]); + for r in &mut table { + *r = moved[*r as usize]; + } + Ok(Index::Table(table)) + } + /// The rows numbered ids `ids` name, `unwired` where one names none or is missing, and whether a /// present id named no row: the loop chosen once for the ids' and the rows' kinds. pub(crate) fn resolve(&self, rows: &RowIds, ids: &Values, unwired: u32) -> (Vec, bool) { @@ -368,6 +400,24 @@ mod tests { assert!(ids.extend(&[Id::from("a")]).is_err(), "texts do not join numbers"); } + #[test] + fn indexes_follow_rows_added_and_removed() { + let ids = RowIds::new(&Ids::from(vec![10_u64, 20, 30])); + let index = ids.index().expect("unique"); + let more = ids.extend(&[Id::Number(40), Id::Number(50)]).expect("numbers"); + let extended = index.extended(&more, 3).expect("unique"); + assert_eq!(extended.get(&more, IdRef::Number(50)), Some(4)); + assert_eq!(extended.get(&more, IdRef::Number(20)), Some(1)); + assert!(index.extended(&more.extend(&[Id::Number(10)]).expect("numbers"), 5).is_err(), "an id twice"); + // Rows 1 and 3 go: the others move up past them. + let doomed = [false, true, false, true, false]; + let kept = more.gather(&[0, 2, 4]); + let fewer = extended.without(&doomed, &kept).expect("unique"); + assert_eq!(fewer.get(&kept, IdRef::Number(50)), Some(2)); + assert_eq!(fewer.get(&kept, IdRef::Number(30)), Some(1)); + assert_eq!(fewer.get(&kept, IdRef::Number(20)), None); + } + #[test] fn repeated_ids_are_invalid() { let ids = RowIds::new(&Ids::from(vec!["a", "b", "a"])); diff --git a/crates/latkit/src/data/mod.rs b/crates/latkit/src/data/mod.rs index 7736b514..c9eb5b5b 100644 --- a/crates/latkit/src/data/mod.rs +++ b/crates/latkit/src/data/mod.rs @@ -40,6 +40,7 @@ pub(crate) use {geometry::Geometry, table::Field}; // What the GPU side reads of the data. #[cfg(feature = "gpu")] pub(crate) use { + bytes::{read_values, values_bytes}, column::{Column, PAGE_ROWS, Page}, frames::{Listed, Manifest}, geometry::GeometryBuilder, diff --git a/crates/latkit/src/data/patch.rs b/crates/latkit/src/data/patch.rs index a46742b7..157c74e6 100644 --- a/crates/latkit/src/data/patch.rs +++ b/crates/latkit/src/data/patch.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; use super::column::Column; use super::dataset::Inner; @@ -404,7 +404,8 @@ fn set(existing: &Table, input: Table) -> Result
{ let mut table = existing.clone(); if !added.is_empty() { table.ids = existing.ids.extend(&added).map_err(|e| e.at("ids"))?; - table.index = Arc::default(); + let index = index.extended(&table.ids, existing.len()).map_err(|e| e.at("ids"))?; + table.index = Arc::new(OnceLock::from(index)); table.structure = Version::next(); } for (name, field) in input.fields { @@ -465,5 +466,7 @@ fn remove(existing: &Table, ids: &Ids) -> Result
{ return Ok(existing.clone()); } let keep: Vec = (0..existing.len()).filter(|&r| !doomed[r]).collect(); - Ok(existing.rows(&keep)) + let mut table = existing.rows(&keep); + table.index = Arc::new(OnceLock::from(index.without(&doomed, &table.ids)?)); + Ok(table) } diff --git a/crates/latkit/src/data/values.rs b/crates/latkit/src/data/values.rs index 6aed9b11..e4bfcd52 100644 --- a/crates/latkit/src/data/values.rs +++ b/crates/latkit/src/data/values.rs @@ -167,6 +167,21 @@ impl Picks<'_> { }, } } + + /// The values in runs, in order: each a source and the first of its rows one after another, or + /// `None` for missing values, and how many. + fn runs(self) -> Vec<(Option<(usize, usize)>, usize)> { + let mut runs: Vec<(Option<(usize, usize)>, usize)> = Vec::new(); + for i in 0..self.len() { + let pick = self.get(i); + match runs.last_mut() { + Some((Some((s, r)), n)) if pick == Some((*s, *r + *n)) => *n += 1, + Some((None, n)) if pick.is_none() => *n += 1, + _ => runs.push((pick, 1)), + } + } + runs + } } impl Values { @@ -322,12 +337,14 @@ impl Values { /// The result keeps the first source's storage; numbers from other sources convert to it. A /// source of another kind is an error. pub(crate) fn assemble(sources: &[&Values], picks: Picks<'_>) -> Result { - let (len, pick) = (picks.len(), |i: usize| picks.get(i)); + let len = picks.len(); let like = sources.first().expect("a source"); let kind = like.kind(); if let Some(other) = sources.iter().find(|s| s.kind() != kind) { return Err(Error::invalid(format!("{:?} values for a {kind:?} field", other.kind()))); } + // Rows one after another in one source copy as a run. + let runs = picks.runs(); macro_rules! numbers { ($variant:ident, $t:ty, $store:expr) => {{ let cast: Vec = sources.iter().map(|s| s.cast(|x| x as $t, $store)).collect(); @@ -338,7 +355,14 @@ impl Values { _ => unreachable!("cast to the first source's storage"), }) .collect(); - Array::$variant((0..len).map(|i| pick(i).map_or(<$t>::default(), |(s, r)| typed[s][r])).collect()) + let mut out: Vec<$t> = Vec::with_capacity(len); + for &(run, n) in &runs { + match run { + Some((s, r)) => out.extend_from_slice(&typed[s][r..r + n]), + None => out.resize(out.len() + n, <$t>::default()), + } + } + Array::$variant(out.into()) }}; } let array = match &like.array { @@ -348,34 +372,44 @@ impl Values { Array::U32(_) => numbers!(U32, u32, Store::U32), Array::U64(_) => numbers!(U64, u64, Store::U64), Array::Bool(_) => Array::Bool( - (0..len) - .map(|i| pick(i).is_some_and(|(s, r)| matches!(&sources[s].array, Array::Bool(v) if v.get(r)))) + runs.iter() + .flat_map(|&(run, n)| (0..n).map(move |k| run.map(|(s, r)| (s, r + k)))) + .map(|pick| pick.is_some_and(|(s, r)| matches!(&sources[s].array, Array::Bool(v) if v.get(r)))) .collect(), ), Array::Text(_) => { let mut builder = TextBuilder::with_capacity(len); - for i in 0..len { - builder.push(pick(i).and_then(|(s, r)| sources[s].text().map(|t| t.get(r))).unwrap_or("")); + for &(run, n) in &runs { + for k in 0..n { + builder.push(run.and_then(|(s, r)| sources[s].text().map(|t| t.get(r + k))).unwrap_or("")); + } } Array::Text(builder.finish()) } Array::Geometry(g) => { let mut builder = GeometryBuilder::new(g.depth()); - for i in 0..len { - match pick(i).and_then(|(s, r)| sources[s].geometry().map(|g| (g, r))) { - Some((geometry, r)) => builder.push_from(geometry, r), - None => builder.push_empty(), + for &(run, n) in &runs { + match run.and_then(|(s, r)| Some((sources[s].geometry()?, r))) { + Some((geometry, r)) => builder.push_rows(geometry, r..r + n), + None => (0..n).for_each(|_| builder.push_empty()), } } Array::Geometry(builder.finish()) } }; + // Every value present where every source's is and none is missing. + let gaps = runs.iter().any(|(run, _)| run.is_none_or(|(s, _)| sources[s].valid.is_some())); + if !gaps { + return Ok(Values { array, valid: None }); + } let mut bits = BitsBuilder::with_capacity(len); let mut all = true; - for i in 0..len { - let valid = pick(i).is_some_and(|(s, r)| sources[s].is_valid(r)); - all &= valid; - bits.push(valid); + for &(run, n) in &runs { + for k in 0..n { + let valid = run.is_some_and(|(s, r)| sources[s].is_valid(r + k)); + all &= valid; + bits.push(valid); + } } Ok(Values { array, valid: (!all).then(|| bits.finish()) }) } diff --git a/crates/latkit/src/derive/lanes.rs b/crates/latkit/src/derive/lanes.rs index fddafae4..4d94152f 100644 --- a/crates/latkit/src/derive/lanes.rs +++ b/crates/latkit/src/derive/lanes.rs @@ -1,8 +1,9 @@ -//! Lanes: outside a diagram, each edge row's lane among the edges joining the same two vertices, so -//! they bow apart instead of drawing over each other. A lane is data, made from the table's ends -//! alone: the stroke bows in world space by its lane, so it never moves as the camera does. Derived -//! again only when the table's rows or ends change: on the frame while that fits its budget, else in -//! a job; a table whose edges share no ends derives nothing. +//! Lanes: outside a diagram, each edge row's lane among the edges joining the same two vertices, its +//! family, so they bow apart instead of drawing over each other. A lane is data, made from the table's +//! ends alone: the stroke bows in world space by its lane, so it never moves as the camera does. A row's +//! lane and its family's size share one word: twice its lane, offset by `HALF`, above, and the size +//! below. Derived again only when the table's rows or ends change: on the frame while that fits its +//! budget, else in a job; a table whose edges share no ends derives nothing. use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; @@ -14,6 +15,10 @@ use crate::host::Job; /// Microseconds a row takes to find its lane, by estimate. const LANE_COST: f64 = 0.06; +/// What a lane word adds to twice the lane, so it is never negative; and the most a family's size +/// says, past which it says this. +const HALF: u32 = 1 << 15; +const MOST: u32 = (1 << 16) - 1; /// What a table's lanes were made from, and a job under way. type Made = Derived<(), Pending>>; @@ -54,7 +59,7 @@ impl Derive for Lanes { let rows = table.len(); if jobs.fits((rows as f64 * LANE_COST) as u64) { made.make(stamp, ()); - changed |= keep(&mut self.tables, key, table, lanes(&a.targets(), &b.targets())); + changed |= keep(&mut self.tables, key, table, words(&a.targets(), &b.targets())); } else { made.start(stamp, jobs.spawn(Job::lanes(&a.targets(), &b.targets()))); } @@ -79,12 +84,13 @@ impl Derive for Lanes { } } -/// Keeps `lanes` beside `table` as its field `lane`, or drops its lanes when there are none; whether -/// anything changed. -fn keep(tables: &mut Tables, key: (Name, Name), table: &Table, lanes: Option>) -> bool { +/// Keeps `lanes`, a lane word each row, beside `table` as its field `lane`, or drops its lanes when +/// there are none; whether anything changed. +fn keep(tables: &mut Tables, key: (Name, Name), table: &Table, lanes: Option>) -> bool { match lanes.filter(|l| l.len() == table.len()) { Some(lanes) => { - let lane = kept("lane", &Values::from(lanes), table, tables.get(&key)); + let words: Vec = lanes.into_iter().map(f64::from).collect(); + let lane = kept("lane", &Values::from(words), table, tables.get(&key)); tables.insert(key, beside(table, [("lane".into(), lane)])); true } @@ -100,16 +106,32 @@ fn reference<'t>(table: &'t Table, name: &str) -> Option<&'t Reference> { } } -/// The lanes a lanes job answered, a little-endian `f32` each; none where it answered nothing. -fn answered(bytes: &[u8]) -> Option> { - (!bytes.is_empty()).then(|| bytes.as_chunks::<4>().0.iter().map(|&w| f32::from_le_bytes(w)).collect()) +/// The lane words a lanes job answered, a little-endian `u32` each; none where it answered nothing. +fn answered(bytes: &[u8]) -> Option> { + (!bytes.is_empty()).then(|| bytes.as_chunks::<4>().0.iter().map(|&w| u32::from_le_bytes(w)).collect()) +} + +/// Each row's lane word, as shaders read it, from the rows' two ends: twice its lane offset by `HALF` +/// above, its family's size below. `None` when no two rows share their ends. +pub(crate) fn words(from: &[u32], to: &[u32]) -> Option> { + let lanes = lanes(from, to)?; + Some( + lanes + .into_iter() + .map(|(lane, size)| { + let twice = (2.0 * lane).clamp(-f32::from(HALF as u16 - 1), f32::from(HALF as u16 - 1)) as i32; + ((twice + HALF as i32) as u32) << 16 | size.min(MOST) + }) + .collect(), + ) } /// Each row's lane among the rows joining the same two vertices, from the rows' two ends /// ([`UNWIRED`] where a reference names no row): offsets from the middle, signed so rows joining them -/// either way bow apart. `None` when no two rows share their ends. Counts each pair once, then hands -/// out lanes only to the pairs shared, so it allocates nothing per pair. -pub(crate) fn lanes(from: &[u32], to: &[u32]) -> Option> { +/// either way bow apart, with how many rows join them. `None` when no two rows share their ends. +/// Counts each pair once, then hands out lanes only to the pairs shared, so it allocates nothing per +/// pair. +pub(crate) fn lanes(from: &[u32], to: &[u32]) -> Option> { let key = |r: usize| { let (a, b) = (from[r], to[r]); (a != UNWIRED && b != UNWIRED && a != b).then(|| u64::from(a.min(b)) << 32 | u64::from(a.max(b))) @@ -132,11 +154,11 @@ pub(crate) fn lanes(from: &[u32], to: &[u32]) -> Option> { Some( (0..rows) .map(|r| { - let Some((k, &n)) = key(r).and_then(|k| Some((k, counts.get(&k)?))) else { return 0.0 }; + let Some((k, &n)) = key(r).and_then(|k| Some((k, counts.get(&k)?))) else { return (0.0, 1) }; let next = taken.entry(k).or_insert(0); let lane = *next as f32 - (n - 1) as f32 / 2.0; *next += 1; - if from[r] > to[r] { -lane } else { lane } + (if from[r] > to[r] { -lane } else { lane }, n) }) .collect(), ) @@ -147,18 +169,24 @@ pub(crate) fn lanes(from: &[u32], to: &[u32]) -> Option> { pub(crate) fn spread(rows: usize, ends: impl Fn(usize) -> Option<[usize; 2]>) -> Option> { let (from, to): (Vec, Vec) = (0..rows).map(|r| ends(r).map_or((UNWIRED, UNWIRED), |[a, b]| (a as u32, b as u32))).unzip(); - lanes(&from, &to).map(|lanes| lanes.into_iter().map(f64::from).collect()) + lanes(&from, &to).map(|lanes| lanes.into_iter().map(|(lane, _)| f64::from(lane)).collect()) } #[cfg(test)] mod tests { - use super::{UNWIRED, lanes}; + use super::{HALF, UNWIRED, lanes, words}; #[test] fn rows_joining_the_same_ends_bow_apart_either_way() { // Rows 0, 1, and 3 join vertices 1 and 2, row 3 the other way; row 2 is alone. let lanes = lanes(&[1, 1, 4, 2, UNWIRED], &[2, 2, 5, 1, 0]).expect("shared ends"); - assert_eq!(lanes, vec![-1.0, 0.0, 0.0, -1.0, 0.0]); + assert_eq!(lanes, vec![(-1.0, 3), (0.0, 3), (0.0, 1), (-1.0, 3), (0.0, 1)]); + } + + #[test] + fn a_lane_word_holds_twice_the_lane_and_the_family() { + let words = words(&[1, 1], &[2, 2]).expect("shared ends"); + assert_eq!(words, vec![(HALF - 1) << 16 | 2, (HALF + 1) << 16 | 2], "lanes of half either side, of two"); } #[test] diff --git a/crates/latkit/src/derive/mod.rs b/crates/latkit/src/derive/mod.rs index 3dcfaaf8..dd085c10 100644 --- a/crates/latkit/src/derive/mod.rs +++ b/crates/latkit/src/derive/mod.rs @@ -23,7 +23,7 @@ pub(crate) mod tiled; pub(crate) mod tiles; mod tiling; -pub(crate) use lanes::{lanes, spread}; +pub(crate) use lanes::{spread, words as lane_words}; use std::hash::{Hash, Hasher}; use std::sync::{Arc, Mutex}; @@ -379,7 +379,7 @@ impl Derivations { let placed = self.places.update(input, jobs); self.lanes.update(input, jobs) | placed | self.stars.update(input, self.places.tables()) }; - changed |= self.shapes.update(input, jobs); + changed |= self.shapes.update(input, &self.tiles, jobs); changed } diff --git a/crates/latkit/src/derive/order.rs b/crates/latkit/src/derive/order.rs index 27139e8f..c243b58a 100644 --- a/crates/latkit/src/derive/order.rs +++ b/crates/latkit/src/derive/order.rs @@ -9,10 +9,9 @@ use std::sync::Arc; use super::pyramid::Pyramid; use super::stamp_of; -use crate::gpu::shaders as wgsl; +use crate::gpu::shaders::PAGE_ROWS; /// Rows a page of an order holds: a held page. -pub(crate) const PAGE: u32 = wgsl::BLOCK_ROWS; #[derive(Clone, Debug)] pub(crate) enum Order { @@ -94,7 +93,7 @@ impl Order { /// Pages of it. pub(crate) fn pages(&self) -> u32 { - self.len().div_ceil(PAGE) + self.len().div_ceil(PAGE_ROWS) } /// The rows a sample draws first: those lifted, then its own. @@ -135,7 +134,7 @@ impl Order { /// The rows page `page` holds, in order. pub(crate) fn page(&self, page: u32) -> Vec { - let (start, end) = (page * PAGE, ((page + 1) * PAGE).min(self.len())); + let (start, end) = (page * PAGE_ROWS, ((page + 1) * PAGE_ROWS).min(self.len())); let Order::Sampled { rows, step, lifted } = self else { return (start..end).map(|d| self.source(d)).collect() }; // The rows lifted it holds, then the sample's in turn, stepping past the places lifted. let m = lifted.len(); @@ -204,11 +203,12 @@ mod tests { assert_eq!(order.sampled(), 5, "the rows lifted, then the sample's own"); assert_ne!(order.id(), sample(10, 3).id(), "pages packed in it are its own"); // A page steps through the sample past the places lifted, as each row's source finds them. - let rows = 3 * PAGE + 17; + let rows = 3 * PAGE_ROWS + 17; let lifted = Lifted::new((0..rows).filter(|r| r % 5 == 1 || r % 7 == 0).collect(), rows, 4); let order = Order::Sampled { rows, step: 4, lifted }; for page in 0..order.pages() { - let one: Vec = (page * PAGE..((page + 1) * PAGE).min(rows)).map(|d| order.source(d)).collect(); + let one: Vec = + (page * PAGE_ROWS..((page + 1) * PAGE_ROWS).min(rows)).map(|d| order.source(d)).collect(); assert_eq!(order.page(page), one, "page {page}"); } } diff --git a/crates/latkit/src/derive/pyramid.rs b/crates/latkit/src/derive/pyramid.rs index 42c47cfd..5401a92a 100644 --- a/crates/latkit/src/derive/pyramid.rs +++ b/crates/latkit/src/derive/pyramid.rs @@ -16,9 +16,9 @@ use std::sync::Arc; use rustc_hash::{FxHashMap, FxHashSet}; -use super::order::PAGE; use super::sight::{Domain, Region, Sight, TILE_PX, meets}; use crate::data::{UNWIRED, Version}; +use crate::gpu::shaders::PAGE_ROWS; use crate::tiles::TileKey; /// Cells across a tile's grid, as a shift: a tile holds at most `4^GRID_BITS` rows of its level, but @@ -530,8 +530,11 @@ pub(crate) struct Tile { /// its levels out coarsest first, so a level's pages lie past those of the levels above it, but for /// one they may share. fn pages_past(ranges: &[(u8, Range)], last: Option) -> (u32, Option) { - let mut spans: Vec<(u32, u32)> = - ranges.iter().filter(|(_, r)| r.start < r.end).map(|(_, r)| (r.start / PAGE, (r.end - 1) / PAGE)).collect(); + let mut spans: Vec<(u32, u32)> = ranges + .iter() + .filter(|(_, r)| r.start < r.end) + .map(|(_, r)| (r.start / PAGE_ROWS, (r.end - 1) / PAGE_ROWS)) + .collect(); spans.sort_unstable(); let (mut pages, mut reached) = (0, last); for (first, end) in spans { @@ -605,7 +608,7 @@ impl Pyramid { let focus = sight.focus(); let limit = TILE_PX * 1.25; let mut out: Vec<(u8, Range)> = Vec::new(); - let (fit, mut held, mut last) = (rows.div_ceil(PAGE), 0_u32, None); + let (fit, mut held, mut last) = (rows.div_ceil(PAGE_ROWS), 0_u32, None); let mut level = vec![TileKey { z: 0, x: 0, y: 0 }]; while !level.is_empty() { let mut found = Vec::new(); @@ -829,10 +832,10 @@ mod tests { fn levels_count_the_pages_they_hold() { // Two sub-cells in one page, one in the next, and a level below reaching the page where the // one above ended. - let above = [(0, 10..20), (0, 50..60), (0, PAGE + 3..PAGE + 9)]; + let above = [(0, 10..20), (0, 50..60), (0, PAGE_ROWS + 3..PAGE_ROWS + 9)]; let (pages, last) = pages_past(&above, None); assert_eq!((pages, last), (2, Some(1))); - let below = [(1, PAGE + 20..3 * PAGE + 1)]; + let below = [(1, PAGE_ROWS + 20..3 * PAGE_ROWS + 1)]; assert_eq!(pages_past(&below, last), (2, Some(3)), "pages 2 and 3; page 1 is held already"); } diff --git a/crates/latkit/src/derive/shapes.rs b/crates/latkit/src/derive/shapes.rs index 1c43fe6a..9ad54259 100644 --- a/crates/latkit/src/derive/shapes.rs +++ b/crates/latkit/src/derive/shapes.rs @@ -1,32 +1,110 @@ //! Shapes: what geometry fields become to draw. Every area's triangles, the point its label hangs from //! first; and on the earth every line's path, unwrapped across the antimeridian and bent with the -//! globe. A table drawn whole is shaped a page of its field at a time, written beside it, and a page -//! written again keeps the shapes of its rows whose geometry did not change, so a tile landing shapes -//! only its own rows. A table drawn in part is shaped a page of its order at a time, the first time -//! the page is drawn: a row whose geometry did not change keeps its shape however its pages come and -//! go. Triangles that do not fit the frame's budget are made in a job, their rows drawing nothing -//! until it lands; bending a path is cheap, and done at once. +//! globe. A table drawn whole is shaped a page of its field at a time, written beside it: a page keeps +//! the shapes made of it before, and a page written over one keeps those of its rows not written, so a +//! write shapes only the rows it wrote. A table drawn in part is shaped a page of its order at a time, +//! the first time the page is drawn: a row whose geometry did not change keeps its shape however its +//! pages come and go. Triangles that do not fit the frame's budget are made in a job, a page whole +//! where every row of it needs them, their rows drawing nothing until it lands; bending a path is +//! cheap, and done at once. Vector tiles bring their shapes, made in the job that decodes them. use rustc_hash::{FxHashMap, FxHashSet}; -use super::order::{Order, PAGE}; -use super::{Derive, Input, Jobs, Paged, Tables, answers, beside, stamp_of}; +use super::order::Order; +use super::tiles::TileSets; +use super::{Input, Jobs, Paged, Tables, beside, stamp_of}; use crate::data::{ - Column, Field, Geometry, GeometryBuilder, Id, Keyed, Name, PAGE_ROWS, Page, PageId, Table, Value, Values, Version, + Column, Field, Geometry, GeometryBuilder, Id, Keyed, Name, Page, PageId, RowIds, Table, Values, Version, }; -use crate::host::{Job, Shaping}; -use crate::scene::areas::triangles_of; -use crate::scene::lines::path_of; -use crate::{Mark, Pending, Space}; +use crate::gpu::shaders::PAGE_ROWS; +use crate::host::{Job, Shaping, shapes_answer}; +use crate::{Mark, Pending, Scene, Space}; /// Microseconds a point takes to triangulate, by estimate. const TRIANGULATE: f64 = 0.5; -/// Rows by id, each with the digest of the geometry it was shaped from. -type Digests = FxHashMap; +/// Rows to shape one at a time: each row, its id, and the digest of its geometry. +type Rows = Vec<(usize, Id, u64)>; -/// Rows to shape: each row, its id, and the digest of its geometry. -type Missing = Vec<(usize, Id, u64)>; +/// What to shape: pages of the field every row of which needs shaping, by id with their values; and +/// rows one at a time. +#[derive(Default)] +struct Missing { + pages: Vec<(PageId, Values)>, + rows: Rows, +} + +impl Missing { + fn is_empty(&self) -> bool { + self.pages.is_empty() && self.rows.is_empty() + } + + /// How many points it shapes. + fn points(&self, column: &Column) -> usize { + let pages: usize = self.pages.iter().map(|(_, values)| values.geometry().map_or(0, Geometry::points)).sum(); + pages + self.rows.iter().map(|&(row, ..)| points_of(column, row)).sum::() + } + + /// What it shapes as the parts of a job, each a geometry column: each page whole, then the rows; + /// and what each part is. + fn parts(self, column: &Column) -> (Vec, Vec) { + let (mut parts, mut asked) = (Vec::new(), Vec::new()); + for (id, values) in self.pages { + parts.push(values); + asked.push(Asked::Page(id)); + } + if !self.rows.is_empty() { + let rows: Vec = self.rows.iter().map(|&(row, ..)| row).collect(); + parts.push(geometry_of(column, &rows)); + asked.push(Asked::Rows(self.rows.into_iter().map(|(_, id, digest)| (id, digest)).collect())); + } + (parts, asked) + } +} + +/// What a part of a job shapes: a page of the field whole, by its id; or rows, by id, each with the +/// digest of its geometry. +enum Asked { + Page(PageId), + Rows(Vec<(Id, u64)>), +} + +/// Shapes made since the last whole set: pages whole, by the id of the page of the field each was +/// made of; and rows, by id, with the digest of the geometry each was made from and where among the +/// parts its shape lies. +#[derive(Default)] +struct Landed { + pages: FxHashMap, + rows: FxHashMap, + parts: Vec, +} + +impl Landed { + /// Takes `shapes`, the shapes of what `asked` names, part by part. + fn land(&mut self, asked: Vec, shapes: Vec) { + for (asked, shapes) in asked.into_iter().zip(shapes) { + match asked { + Asked::Page(id) => { + self.pages.insert(id, Page::new(shapes)); + } + Asked::Rows(rows) => { + let part = self.parts.len() as u32; + for (i, (id, digest)) in rows.into_iter().enumerate() { + self.rows.insert(id, (digest, part, i as u32)); + } + self.parts.push(shapes); + } + } + } + } + + /// Where the shape of the row with `id` lies, made from geometry whose digest is `digest`. + fn row(&self, id: &Id, digest: u64) -> Option<(&Geometry, usize)> { + let &(made, part, at) = self.rows.get(id)?; + (made == digest).then_some(())?; + Some((self.parts[part as usize].geometry()?, at as usize)) + } +} /// A field to shape: its dataset, table, and field, and how. #[derive(Clone, PartialEq, Eq, Hash, Debug)] @@ -40,10 +118,7 @@ struct Want { impl Want { /// The derived field the shapes are written as. fn derived(&self) -> Name { - match self.shaping { - Shaping::Triangles { .. } => triangles_of(&self.field), - Shaping::Path => path_of(&self.field), - } + self.shaping.field_of(&self.field) } } @@ -88,8 +163,8 @@ impl Paging { return None; } let d = self.order.rank(row as u32); - let page = self.shapes.pages.get(&(d / PAGE)).filter(|page| page.whole)?; - Some((page.shapes.geometry()?, (d % PAGE) as usize)) + let page = self.shapes.pages.get(&(d / PAGE_ROWS)).filter(|page| page.whole)?; + Some((page.shapes.geometry()?, (d % PAGE_ROWS) as usize)) } } @@ -103,14 +178,14 @@ struct Shaped { /// The derived field written last, and the stamp of what it was written from. field: Option, stamp: u64, - /// Shapes made since, by row id: the digest of the geometry each was made from, and the shape. - landed: FxHashMap, + /// Shapes made since the last whole set. + landed: Landed, /// Drawn in part, its pages; and those of the order drawn before, whose rows alike pages made /// afresh take their shapes from. paging: Option, before: Option, - /// A job under way, with the digest of each row it shapes. - working: Option<(Pending>, Digests)>, + /// A job under way, with what each of its parts shapes. + working: Option<(Pending>, Vec)>, /// Bumps whenever a job's answer lands. landings: u64, } @@ -121,15 +196,25 @@ pub(crate) struct Shapes { shaped: FxHashMap, } -impl Derive for Shapes { - fn update(&mut self, input: &Input<'_>, jobs: &mut Jobs<'_>) -> bool { +impl Shapes { + /// Brings the shapes up to `input`, taking those `tiles` brought as they are and handing `jobs` + /// what the frame cannot afford; whether its tables changed. + pub(crate) fn update(&mut self, input: &Input<'_>, tiles: &TileSets, jobs: &mut Jobs<'_>) -> bool { let wants = wants(input); self.shaped.retain(|want, _| wants.contains(want)); let mut changed = false; for want in &wants { let Some(source) = input.data(&want.data).and_then(|d| d.table(&want.table)) else { continue }; - let paged = input.paged.and_then(|paged| paged.get(&(want.data.clone(), want.table.clone()))); let shaped = self.shaped.entry(want.clone()).or_default(); + // Vector tiles bring their shapes, row for row. + let brought = tiles.shaped(&want.data).and_then(|d| d.table(&want.table)?.field_named(&want.derived())); + if let Some(field @ Field::Values(column, _)) = brought + && column.len() == source.len() + { + changed |= shaped.take(field); + continue; + } + let paged = input.paged.and_then(|paged| paged.get(&(want.data.clone(), want.table.clone()))); changed |= shaped.update(want, source, paged, jobs); } // Each table drawn whole has a derived table holding every field shaped from it. @@ -151,21 +236,21 @@ impl Derive for Shapes { changed } - fn busy(&self) -> bool { + /// Whether a job shapes rows: until it lands, they draw nothing. + pub(crate) fn busy(&self) -> bool { self.shaped.values().any(|s| s.working.is_some()) } - fn tables(&self) -> &Tables { - &self.tables + /// Whether every row drawn holds its shape. + pub(crate) fn complete(&self) -> bool { + !self.busy() } - fn clear(&mut self) { - self.tables.clear(); - self.shaped.clear(); + /// The tables beside those drawn whole, each holding the fields shaped from it. + pub(crate) fn tables(&self) -> &Tables { + &self.tables } -} -impl Shapes { /// The shapes of table `table` of dataset `data` drawn in part, by the field each is written as. pub(crate) fn paged(&self, data: &Name, table: &Name) -> Vec<(Name, &PagedShapes)> { self.shaped @@ -176,16 +261,33 @@ impl Shapes { } } +/// What mark `mark` shapes, and how: an area its polygons, and on the earth a line its paths. +fn shaping_of(mark: &Mark, earth: bool) -> Option<(Name, Shaping)> { + match mark { + Mark::Areas(areas) => Some((areas.field(), Shaping::Triangles { earth })), + Mark::Lines(lines) if earth => Some((lines.traced(), Shaping::Path)), + _ => None, + } +} + +/// The fields of table `table` of dataset `data` the scene's marks shape, and how, each once. +pub(crate) fn shapings(scene: &Scene, data: &Name, table: &Name) -> Vec<(Name, Shaping)> { + let earth = matches!(scene.space, Space::Earth(_)); + let mut shapings = Vec::new(); + for mark in scene.marks.iter().filter(|m| m.style().data == *data && m.style().table == *table) { + if let Some(shaping) = shaping_of(mark, earth).filter(|s| !shapings.contains(s)) { + shapings.push(shaping); + } + } + shapings +} + /// The fields the scene's marks shape: every area's polygons, and on the earth every line's paths. fn wants(input: &Input<'_>) -> Vec { let earth = matches!(input.scene.space, Space::Earth(_)); let mut wants: Vec = Vec::new(); for mark in &input.scene.marks { - let (field, shaping) = match mark { - Mark::Areas(areas) => (areas.field(), Shaping::Triangles { earth }), - Mark::Lines(lines) if earth => (lines.traced(), Shaping::Path), - _ => continue, - }; + let Some((field, shaping)) = shaping_of(mark, earth) else { continue }; let style = mark.style(); let want = Want { data: style.data.clone(), table: style.table.clone(), field, shaping }; if !wants.contains(&want) { @@ -199,21 +301,18 @@ impl Shaped { /// Brings the shapes up to `source`, drawn in part as `paged` says or else whole; whether they /// changed. fn update(&mut self, want: &Want, source: &Table, paged: Option<&Paged>, jobs: &mut Jobs<'_>) -> bool { - if let Some((pending, asked)) = &self.working + if let Some((pending, _)) = &self.working && let Some(answer) = pending.take() { - for ((_, id), values) in answer.map(|bytes| answers(&bytes, &["shape"])).unwrap_or_default() { - if let (Some(&digest), Some(Some(shape))) = (asked.get(&id), values.into_iter().next()) { - self.landed.insert(id, (digest, shape)); - } + let (_, asked) = self.working.take().expect("a job under way"); + let shapes = answer.ok().and_then(|bytes| shapes_answer(&bytes)).unwrap_or_default(); + if shapes.len() == asked.len() { + self.landed.land(asked, shapes); } - self.working = None; self.landings += 1; } let Some(Field::Values(column, version)) = source.field_named(&want.field) else { return self.forget() }; - let ((Some(depth @ (2 | 3)), Shaping::Triangles { .. }) | (Some(depth @ 1..), Shaping::Path)) = - (column.kind().depth(), want.shaping) - else { + let Some(depth) = column.kind().depth().filter(|&depth| want.shaping.takes(depth)) else { return self.forget(); }; if let Some(paged) = paged { @@ -230,6 +329,13 @@ impl Shaped { self.update_whole(want, source, column, *version, depth, jobs) || had } + /// Takes `field` as its shapes, made elsewhere; whether they changed. + fn take(&mut self, field: &Field) -> bool { + let changed = self.field.as_ref().is_none_or(|f| f.version() != field.version()); + *self = Shaped { field: Some(field.clone()), ..Shaped::default() }; + changed + } + /// Forgets what it shaped, its field no longer shapes; whether it had shaped anything. fn forget(&mut self) -> bool { let had = self.field.is_some() || self.paging.is_some(); @@ -252,25 +358,25 @@ impl Shaped { return false; } self.stamp = stamp; - // Rows shaped on the frame land at once; a second pass writes them. - for _ in 0..2 { - let (derived, missing) = self.build(want, source, column, depth); - if missing.is_empty() { - self.field = Some(Field::Values(derived.clone(), Version::next())); - self.made = Some((source.clone(), column.clone(), derived)); - self.landed.clear(); - self.before = None; - return true; - } - if self.shape_now(want, column, &missing, jobs) { - continue; + let (mut derived, mut missing) = self.build(want, source, column, depth); + // Shapes made on the frame land at once, and the column is made again with them. + if !missing.is_empty() { + match self.shape_now(want, column, missing, jobs) { + Ok(()) => (derived, missing) = self.build(want, source, column, depth), + Err(left) => missing = left, } - self.ask(want, source, column, missing, jobs); - // Until the job lands, its rows draw nothing and the rest draw as they were. - self.field = Some(Field::Values(derived, Version::next())); + } + if missing.is_empty() { + self.field = Some(Field::Values(derived.clone(), Version::next())); + self.made = Some((source.clone(), column.clone(), derived)); + self.landed = Landed::default(); + self.before = None; return true; } - false + self.ask(want, column, missing, jobs); + // Until the job lands, its rows draw nothing and the rest draw as they were. + self.field = Some(Field::Values(derived, Version::next())); + true } /// Brings the shapes of a table drawn in part up to `source`, making each page `paged` draws the @@ -303,7 +409,7 @@ impl Shaped { if !paging.column.shares(column) { let written = column.written_since(&paging.column); for &row in written.iter().filter(|&&row| row < order.len() as usize) { - paging.shapes.pages.remove(&(order.rank(row as u32) / PAGE)); + paging.shapes.pages.remove(&(order.rank(row as u32) / PAGE_ROWS)); } if !paging.shapes.kept.is_empty() { let written: FxHashSet = written.into_iter().map(|row| row as u32).collect(); @@ -327,32 +433,16 @@ impl Shaped { if make.is_empty() && keep.is_empty() { return false; } - // Rows shaped on the frame land at once; a second pass makes their pages. - let mut made: Vec<(u32, Values, bool)> = Vec::with_capacity(make.len()); - let mut kept: Vec<(u32, Values)> = Vec::with_capacity(keep.len()); - let mut missing: Missing = Vec::new(); - for _ in 0..2 { - made.clear(); - kept.clear(); - missing.clear(); - for &page in &make { - let (shapes, rows) = self.make_page(want, order, page, source, column, depth); - made.push((page, shapes, rows.is_empty())); - missing.extend(rows); - } - for &d in &keep { - let (shape, rows) = self.make_rows(want, &[order.source(d)], source, column, depth); - if rows.is_empty() { - kept.push((d, shape)); - } - missing.extend(rows); - } - if missing.is_empty() || !self.shape_now(want, column, &missing, jobs) { - break; + let (mut made, mut kept, mut missing) = self.make_paged(want, order, &make, &keep, source, column, depth); + // Shapes made on the frame land at once, and the pages are made again with them. + if !missing.rows.is_empty() { + match self.shape_now(want, column, missing, jobs) { + Ok(()) => (made, kept, missing) = self.make_paged(want, order, &make, &keep, source, column, depth), + Err(left) => missing = left, } } if !missing.is_empty() { - self.ask(want, source, column, missing, jobs); + self.ask(want, column, missing, jobs); } let paging = self.paging.as_mut().expect("paged"); for (page, shapes, whole) in made { @@ -363,40 +453,75 @@ impl Shaped { // Every page drawn whole and no job under way: what landed is in its pages, and the order // before has served the pages made afresh. if self.working.is_none() && paging.shapes.pages.values().all(|page| page.whole) { - self.landed.clear(); + self.landed = Landed::default(); self.before = None; } true } - /// Shapes `missing` on the frame, if they fit what is left of its budget; whether they did. - fn shape_now(&mut self, want: &Want, column: &Column, missing: &Missing, jobs: &mut Jobs<'_>) -> bool { - let points: usize = missing.iter().map(|&(row, ..)| points_of(column, row)).sum(); - if !jobs.fits((points as f64 * TRIANGULATE).ceil() as u64) { - return false; + /// Pages `make` of `order` shaped, each with whether it is whole; rows kept `keep`, by their place + /// in the order, each shaped; and the rows left to shape. + #[allow(clippy::too_many_arguments, clippy::type_complexity)] + fn make_paged( + &self, + want: &Want, + order: &Order, + make: &[u32], + keep: &[u32], + source: &Table, + column: &Column, + depth: u8, + ) -> (Vec<(u32, Values, bool)>, Vec<(u32, Values)>, Missing) { + let mut missing = Missing::default(); + let mut made = Vec::with_capacity(make.len()); + for &page in make { + let (shapes, rows) = self.make_rows(want, &order.page(page), source, column, depth); + made.push((page, shapes, rows.is_empty())); + missing.rows.extend(rows); } - for (row, id, digest) in missing { - let Some(value) = column.get(*row) else { continue }; - let shape = want.shaping.shape(&value).unwrap_or(value); - self.landed.insert(id.clone(), (*digest, shape)); + let mut kept = Vec::with_capacity(keep.len()); + for &d in keep { + let (shape, rows) = self.make_rows(want, &[order.source(d)], source, column, depth); + if rows.is_empty() { + kept.push((d, shape)); + } + missing.rows.extend(rows); } - true + (made, kept, missing) + } + + /// Shapes `missing` on the frame, if they fit what is left of its budget; else hands them back. + fn shape_now( + &mut self, + want: &Want, + column: &Column, + missing: Missing, + jobs: &mut Jobs<'_>, + ) -> Result<(), Missing> { + if !jobs.fits((missing.points(column) as f64 * TRIANGULATE).ceil() as u64) { + return Err(missing); + } + let (parts, asked) = missing.parts(column); + let shapes = parts.iter().map(|part| want.shaping.shapes_of(part)).collect(); + self.landed.land(asked, shapes); + Ok(()) } - /// Asks a job to shape `missing`, unless one is under way: the rows it leaves wait for the next. - fn ask(&mut self, want: &Want, source: &Table, column: &Column, missing: Missing, jobs: &mut Jobs<'_>) { + /// Asks a job to shape `missing`, unless one is under way: what it leaves waits for the next. + fn ask(&mut self, want: &Want, column: &Column, missing: Missing, jobs: &mut Jobs<'_>) { if self.working.is_some() { return; } - let rows: Vec = missing.iter().map(|&(row, ..)| row).collect(); - let job = Job::shapes(want.shaping, rows_of(source, column, &rows)); - let asked = missing.into_iter().map(|(_, id, digest)| (id, digest)).collect(); - self.working = Some((jobs.spawn(job), asked)); + let (parts, asked) = missing.parts(column); + self.working = Some((jobs.spawn(Job::shapes(want.shaping, &parts)), asked)); } - /// The derived column for `column` of `source`, page by page: a page whose source page is the - /// one shaped last keeps its shapes; others are assembled row by row from shapes landed, or kept - /// from the last set where a row's geometry did not change. Rows with neither are missing. + /// The derived column for `column` of `source`, page by page, and what is left to shape. A page + /// keeps the shapes made of it before, or landed for it; a page written over one shaped keeps + /// those of its rows not written, as does a page at its place in the last whole set those of its + /// rows holding the same. A page none of whose rows can keep a shape is shaped whole; the rows of + /// others take a shape landed or made before while their geometry is the same, and are left to + /// shape otherwise. fn build(&self, want: &Want, source: &Table, column: &Column, depth: u8) -> (Column, Missing) { let made_pages: FxHashMap = self .made @@ -404,27 +529,60 @@ impl Shaped { .flat_map(|(_, field, derived)| field.pages().iter().map(|p| p.id).zip(derived.pages())) .collect(); let depth = want.shaping.depth(depth); + // Rows below this many are the last whole set's, at the same places: rows since were only + // written, or added after them. + let aligned = self.made.as_ref().map_or(0, |(table, ..)| aligned(source, table)); + // Whether rows past them could take a shape made before only by being shaped. + let found = |row: usize| row < aligned || self.made.is_some() && aligned == 0; + let fresh = self.before.is_none() && self.landed.rows.is_empty() && want.shaping != Shaping::Path; let mut pages: Vec = Vec::with_capacity(column.pages().len()); - let mut missing = Vec::new(); + let mut missing = Missing::default(); for (p, page) in column.pages().iter().enumerate() { if let Some(&kept) = made_pages.get(&page.id) { pages.push(kept.clone()); continue; } + if let Some(landed) = self.landed.pages.get(&page.id) { + pages.push(landed.clone()); + continue; + } let Some(geometry) = page.values.geometry() else { pages.push(Page::new(Values::of_geometry(GeometryBuilder::new(depth).finish()))); continue; }; // A path page that needs no bending is its own shape: shared, nothing uploads again. - if want.shaping == Shaping::Path && !bends(geometry.coords()) { + if want.shaping == Shaping::Path && !bends(geometry) { pages.push(page.clone()); continue; } + let start = p * PAGE_ROWS as usize; + let len = page.values.len(); + let kept = self.over(page, &made_pages).or_else(|| { + let (_, field, derived) = self.made.as_ref().filter(|_| start < aligned)?; + let was = &field.pages().get(p)?.values; + let same = (0..len).map(|i| start + i < aligned && page.values.same_row(i, was, i)).collect(); + Some((&derived.pages().get(p)?.values, same)) + }); + if fresh && kept.is_none() && !found(start) { + // Every row is shaped: the page whole, drawing nothing until it lands. + let mut empty = GeometryBuilder::new(depth); + (0..len).for_each(|_| empty.push_empty()); + pages.push(Page::new(Values::of_geometry(empty.finish()))); + missing.pages.push((page.id, page.values.clone())); + continue; + } let mut shapes = GeometryBuilder::new(depth); - for local in 0..page.values.len() { - let row = p * PAGE_ROWS + local; + for local in 0..len { + if let Some((was, keep)) = &kept + && keep.get(local).copied().unwrap_or(false) + && let Some(was) = was.geometry() + { + shapes.push_from(was, local); + continue; + } + let row = start + local; match source.ids.id(row).filter(|_| page.values.is_valid(local)) { - Some(id) => self.shape(want, &mut shapes, (&page.values, local), row, id, &mut missing), + Some(id) => self.shape(want, &mut shapes, (&page.values, local), row, id, &mut missing.rows), None => shapes.push_empty(), } } @@ -434,23 +592,31 @@ impl Shaped { (Column::of_pages(pages, &like), missing) } - /// Page `page` of `order` shaped: the shapes of its rows, in its order, as [`shape`](Shaped::shape) - /// finds them; and the rows left to shape. - fn make_page( - &self, - want: &Want, - order: &Order, - page: u32, - source: &Table, - column: &Column, - depth: u8, - ) -> (Values, Missing) { - self.make_rows(want, &order.page(page), source, column, depth) + /// The shapes made of a page `page` was written over, a few writes back at most, with whether + /// each of its rows keeps its shape: those not written since. + fn over<'a>(&'a self, page: &Page, made: &FxHashMap) -> Option<(&'a Values, Vec)> { + let mut written: Vec = Vec::new(); + let mut at = page.written.as_deref(); + while let Some(w) = at { + written.extend_from_slice(&w.rows); + if let Some(shapes) = made.get(&w.from).copied().or_else(|| self.landed.pages.get(&w.from)) { + let mut keep = vec![true; page.values.len()]; + for &row in &written { + if let Some(k) = keep.get_mut(row as usize) { + *k = false; + } + } + keep.truncate(shapes.values.len()); + return Some((&shapes.values, keep)); + } + at = w.before.as_deref(); + } + None } /// Rows `rows` of `source` shaped, in that order, as [`shape`](Shaped::shape) finds them; and the /// rows left to shape. - fn make_rows(&self, want: &Want, rows: &[u32], source: &Table, column: &Column, depth: u8) -> (Values, Missing) { + fn make_rows(&self, want: &Want, rows: &[u32], source: &Table, column: &Column, depth: u8) -> (Values, Rows) { let mut shapes = GeometryBuilder::new(want.shaping.depth(depth)); let mut missing = Vec::new(); for row in rows.iter().map(|&r| r as usize) { @@ -473,19 +639,15 @@ impl Shaped { (values, i): (&Values, usize), row: usize, id: Id, - missing: &mut Missing, + missing: &mut Rows, ) { let Some(geometry) = values.geometry() else { shapes.push_empty(); return; }; let digest = geometry.row_hash(i); - if let Some((d, shape)) = self.landed.get(&id) - && *d == digest - { - shapes.push_value(shape); - } else if let Some((page, at)) = self.kept(&id, digest) { - shapes.push_from(page, at); + if let Some((landed, at)) = self.landed.row(&id, digest).or_else(|| self.kept(&id, digest)) { + shapes.push_from(landed, at); } else if want.shaping == Shaping::Path { // Bending is cheap, so done here; rows that need none keep their geometry. match values.get(i).and_then(|v| want.shaping.shape(&v)) { @@ -511,7 +673,7 @@ impl Shaped { fn whole(&self, id: &Id, digest: u64) -> Option<(&Geometry, usize)> { let (table, field, derived) = self.made.as_ref()?; let row = table.row(id.as_ref())?; - let (page, at) = (row / PAGE_ROWS, row % PAGE_ROWS); + let (page, at) = (row / PAGE_ROWS as usize, row % PAGE_ROWS as usize); let was = field.pages().get(page)?.values.geometry()?; if !field.pages()[page].values.is_valid(at) || was.row_hash(at) != digest { return None; @@ -520,10 +682,24 @@ impl Shaped { } } -/// Whether any two coordinates in a row of `coords` lie farther apart than an earth path's step: a +/// How many leading rows of `now` are those of `then` at the same places: every row where none came +/// or went, those `then` held where rows were only added after them, else none. +fn aligned(now: &Table, then: &Table) -> usize { + if now.structure == then.structure { + return now.len(); + } + match (&now.ids, &then.ids) { + (RowIds::Count(_), RowIds::Count(_)) => now.len().min(then.len()), + (RowIds::Listed(ids), RowIds::Listed(was)) if ids.kept_below(was, was.len()) => was.len(), + _ => 0, + } +} + +/// Whether any two coordinates of a row of `geometry` lie farther apart than an earth path's step: a /// page with none needs no bending. -fn bends(coords: &[[f64; 2]]) -> bool { +fn bends(geometry: &Geometry) -> bool { let step = crate::geo::shapes::EARTH_STEP; + let coords = geometry.coords(); coords.windows(2).any(|w| (w[1][0] - w[0][0]).abs() > step || (w[1][1] - w[0][1]).abs() > step) } @@ -532,10 +708,8 @@ fn points_of(column: &Column, row: usize) -> usize { column.at(row).and_then(|(values, i)| values.geometry().map(|g| g.row_coords(i).len())).unwrap_or(0) } -/// Rows `rows` of `source`, by id, with their geometry from `column` as field `geometry`: what a -/// shapes job reads. -fn rows_of(source: &Table, column: &Column, rows: &[usize]) -> Table { - let ids: Vec = rows.iter().filter_map(|&row| source.ids.id(row)).collect(); +/// The geometry of rows `rows` of geometry column `column`, in that order. +fn geometry_of(column: &Column, rows: &[usize]) -> Values { let depth = column.kind().depth().expect("a geometry column"); let mut geometry = GeometryBuilder::new(depth); for &row in rows { @@ -544,7 +718,7 @@ fn rows_of(source: &Table, column: &Column, rows: &[usize]) -> Table { _ => geometry.push_empty(), } } - Table::new(source.name.clone(), ids).field("geometry", Values::of_geometry(geometry.finish())) + Values::of_geometry(geometry.finish()) } #[cfg(test)] @@ -572,9 +746,17 @@ mod tests { pages } + /// The derived column of `shapes`' one table drawn whole. + fn derived(shapes: &Shapes) -> Column { + match shapes.tables().get(&("map".into(), "Zone".into())).and_then(|t| t.field_named("geometry.triangles")) { + Some(Field::Values(column, _)) => column.clone(), + _ => panic!("triangles"), + } + } + #[test] fn a_table_drawn_in_part_is_shaped_a_page_drawn_at_a_time() { - let n = 3 * PAGE as usize; + let n = 3 * PAGE_ROWS as usize; let mut data = Data::new("map"); let squares: Vec> = (0..n).map(|i| square(i as f64)).collect(); data.apply(Table::new("Zone", Ids::count(n as u32)).field("geometry", squares)).expect("valid"); @@ -595,14 +777,15 @@ mod tests { dragging: false, paged: Some(&paged), }; - shapes.update(&input, &mut Jobs::new(None, Waker::noop(), &counters, &errors, u32::MAX)); + let mut jobs = Jobs::new(None, Waker::noop(), &counters, &errors, u32::MAX); + shapes.update(&input, &TileSets::default(), &mut jobs); }; update(&mut shapes, &data, vec![1]); let first = made(&shapes); assert_eq!(first.iter().map(|&(page, _)| page).collect::>(), [1], "only the page drawn is shaped"); let paged = shapes.paged(&"map".into(), &"Zone".into()); let page = &paged[0].1.pages[&1]; - assert_eq!(page.shapes.len(), PAGE as usize); + assert_eq!(page.shapes.len(), PAGE_ROWS as usize); let triangles = page.shapes.geometry().expect("triangles").row_coords(0).len(); assert!(triangles >= 3, "its rows' triangles, in the order's order: {triangles} points"); // Drawing another page shapes it alone; the first keeps its shapes. @@ -610,11 +793,58 @@ mod tests { let second = made(&shapes); assert_eq!((second.len(), second[0]), (2, first[0])); // A write to a row drawn in page 2 shapes that page again, and only it. - let row = u64::from(order.source(2 * PAGE + 5)); + let row = u64::from(order.source(2 * PAGE_ROWS + 5)); data.apply(Table::new("Zone", vec![row]).field("geometry", vec![square(-9.0)])).expect("valid"); update(&mut shapes, &data, vec![1, 2]); let third = made(&shapes); assert_eq!(third[0], second[0], "page 1 holds no row written"); assert_ne!(third[1].1, second[1].1, "page 2 is shaped again"); } + + #[test] + fn a_table_drawn_whole_lands_its_shapes_a_page_at_a_time() { + let n = 2 * PAGE_ROWS as usize + 10; + let mut data = Data::new("map"); + let squares: Vec> = (0..n).map(|i| square(i as f64)).collect(); + data.apply(Table::new("Zone", Ids::count(n as u32)).field("geometry", squares)).expect("valid"); + let scene = Scene::new(Plane::default()).draw(Areas::of("map", "Zone")); + let (counters, errors) = (Counters::default(), Arc::new(Mutex::new(Vec::new()))); + let mut shapes = Shapes::default(); + // No budget on the frame: every shape is made in a job, which lands by the next update. + let update = |shapes: &mut Shapes, data: &Data| { + let input = Input { + scene: &scene, + scene_version: 1, + data: std::slice::from_ref(data), + host: None, + dragging: false, + paged: None, + }; + let mut jobs = Jobs::new(None, Waker::noop(), &counters, &errors, 0); + shapes.update(&input, &TileSets::default(), &mut jobs) + }; + let points = |column: &Column, row: usize| { + column.at(row).map_or(0, |(values, i)| values.geometry().expect("triangles").row_coords(i).len()) + }; + assert!(update(&mut shapes, &data)); + assert!(shapes.busy(), "a job shapes them"); + assert_eq!(points(&derived(&shapes), 3), 0, "until it lands, rows draw nothing"); + assert!(update(&mut shapes, &data)); + let whole = derived(&shapes); + assert!(!shapes.busy()); + assert_eq!(whole.pages().len(), 3); + assert!((0..n).all(|row| points(&whole, row) >= 4), "every row holds its label point and triangles"); + // A write to one row shapes it alone: the other pages keep theirs, and its page the shapes of + // the rows not written. + let row = PAGE_ROWS as usize + 7; + data.apply(Table::new("Zone", vec![row as u64]).field("geometry", vec![square(-9.0)])).expect("valid"); + assert!(update(&mut shapes, &data)); + let written = derived(&shapes); + assert_eq!(written.pages()[0].id, whole.pages()[0].id); + assert_eq!(written.pages()[2].id, whole.pages()[2].id); + assert_eq!(points(&written, row), 0, "the row written waits for its job"); + assert!(points(&written, row + 1) >= 4, "the rest of its page keeps theirs"); + assert!(update(&mut shapes, &data)); + assert!(points(&derived(&shapes), row) >= 4, "and lands"); + } } diff --git a/crates/latkit/src/derive/tiled.rs b/crates/latkit/src/derive/tiled.rs index e06db797..cb75c0ac 100644 --- a/crates/latkit/src/derive/tiled.rs +++ b/crates/latkit/src/derive/tiled.rs @@ -12,7 +12,7 @@ use std::ops::Range; use std::sync::Arc; -use super::order::{Lifted, Order, PAGE}; +use super::order::{Lifted, Order}; use super::pyramid::{Leveling, Places, Placing, Pyramid, answer}; use super::sight::{Domain, Sight}; use super::{Jobs, stamp_of}; @@ -20,6 +20,7 @@ use crate::Pending; use crate::data::{Column, Data, Field, Keyed, Name, Named, Reference, Table, UNWIRED, Version}; use crate::expr::cpu::Rows; use crate::expr::{Expr, Node}; +use crate::gpu::shaders::PAGE_ROWS; use crate::host::Job; /// Microseconds placing a row takes, by estimate. @@ -341,7 +342,7 @@ impl Set { } let served = planned.as_is() || self.placed.get(&table.name).is_some_and(|p| p.serves(table)); let appended = table.len().saturating_sub(read.rows); - self.working.is_some() || (served && appended < (read.rows / 8).max(PAGE as usize)) + self.working.is_some() || (served && appended < (read.rows / 8).max(PAGE_ROWS as usize)) }) } diff --git a/crates/latkit/src/derive/tiles/mod.rs b/crates/latkit/src/derive/tiles/mod.rs index f6b11f7b..6572bf18 100644 --- a/crates/latkit/src/derive/tiles/mod.rs +++ b/crates/latkit/src/derive/tiles/mod.rs @@ -166,6 +166,16 @@ impl TileSets { self.sets.iter().any(|s| *s.tiles.name == *name) } + /// The rows vector tiles brought of dataset `name`, each table with the fields its shapes are + /// written as too. + pub(crate) fn shaped(&self, name: &str) -> Option<&Data> { + let set = self.sets.iter().find(|s| *s.tiles.name == *name)?; + match &set.source { + Source::Open(Holds::Vector(vector)) => Some(vector.held()), + _ => None, + } + } + /// Stops fetching the pieces drawn as dataset `name`. pub(crate) fn remove(&mut self, name: &str) { self.sets.retain(|s| s.tiles.name != name); diff --git a/crates/latkit/src/derive/tiles/vector.rs b/crates/latkit/src/derive/tiles/vector.rs index 1b7869ce..0ca1862a 100644 --- a/crates/latkit/src/derive/tiles/vector.rs +++ b/crates/latkit/src/derive/tiles/vector.rs @@ -2,19 +2,22 @@ //! template, given or from a TileJSON document, decoded in jobs, and patched into the dataset the view //! owns. The camera chooses the tiles that draw no larger than their own size where they lie nearest //! its focus, nearest first. Tiles decode only the layers marks draw, and again when the scene draws -//! others. +//! others; the job decoding a tile shapes its rows too, as their marks draw them, so a tile lands +//! with its triangles and paths. The view holds the rows with their shapes, and draws the dataset of +//! the rows as tiles wrote them. use std::sync::Arc; use rustc_hash::{FxHashMap, FxHashSet}; +use super::super::shapes::shapings; use super::super::sight::{Domain, Sight, TILE_PX, meets}; use super::super::tiling::{Decoding, Requested, Source, Tiling}; use super::super::{Input, Jobs, stamp_of}; -use crate::data::{Data, Field, Id, Name, Op, Patch, Table, Values}; +use crate::data::{Data, Field, Id, Keyed, Name, Op, Patch, Table, Values}; use crate::geo::mvt::Kind as Geometry; use crate::geo::pmtiles::{self, Directory, Entry, Header}; -use crate::geo::tiles::{Decoded, Template}; +use crate::geo::tiles::{Decoded, Template, shapes_field}; use crate::host::{Job, Request}; use crate::tiles::TileKey; use crate::{Error, Kind, Mark, Pending, Scene}; @@ -77,6 +80,8 @@ pub(crate) struct Vector { tiling: Tiling, /// The tiles whose rows the dataset holds, with the rows of each table. drawn: FxHashMap)>>, + /// The rows the dataset holds, each table with the fields its shapes are written as too. + held: Data, } impl Vector { @@ -111,7 +116,12 @@ impl Vector { leaf_fetches: FxHashMap::default(), tables: Vec::new(), }; - Vector { tiles, tiling: Tiling::default(), drawn: FxHashMap::default() } + Vector { tiles, tiling: Tiling::default(), drawn: FxHashMap::default(), held: Data::new("held") } + } + + /// The rows the dataset holds, each table with the fields its shapes are written as too. + pub(crate) fn held(&self) -> &Data { + &self.held } /// Whether the source is still opening, its root directory not landed. @@ -154,7 +164,8 @@ impl Vector { (changed, drawn.standing_in, failure.or(drawn.failure)) } - /// Makes `data` hold the rows of `drawn` and no other tile's; whether it changed. + /// Makes `data` hold the rows of `drawn` and no other tile's, as the tiles wrote them, and the + /// rows held those with their shapes; whether it changed. fn draw(&mut self, drawn: &[TileKey], stamp: u64, data: &mut Data) -> bool { let keep: FxHashSet = drawn.iter().copied().collect(); let mut patch = Patch::new(); @@ -166,11 +177,16 @@ impl Vector { .filter(|k| !keep.contains(k) || self.tiling.decoded(k, stamp).is_none()) .copied() .collect(); + // One removal a table, which moves the rows after the first gone once. + let mut removed: Keyed> = Keyed::default(); for key in &gone { for (table, ids) in self.drawn.remove(key).unwrap_or_default() { - if data.table(&table).is_some() { - patch = patch.remove(table, ids); - } + removed.entry(table).or_default().extend(ids); + } + } + for (table, ids) in removed { + if self.held.table(&table).is_some() { + patch = patch.remove(table, ids); } } for &key in drawn { @@ -182,7 +198,7 @@ impl Vector { for op in &tile.ops { if let Op::Set(table) = op { rows.push((table.name.clone(), table.ids().collect())); - patch = patch.set(harmonized(table, data)); + patch = patch.set(harmonized(table, &self.held)); } } self.drawn.insert(key, rows); @@ -190,12 +206,22 @@ impl Vector { if patch.is_empty() { return false; } + let mut held = self.held.clone(); + if held.apply(patch).is_err() { + return false; + } + // The dataset drawn holds each table held without its shapes, sharing all else. + let tables = held.tables().map(|table| { + let mut table = table.clone(); + table.fields.retain(|(name, _)| !shapes_field(name)); + table + }); let mut next = data.clone(); - if next.apply(patch).is_ok() { - *data = next; - return true; + if next.apply(tables.fold(Patch::new(), Patch::replace)).is_err() { + return false; } - false + (*data, self.held) = (next, held); + true } } @@ -299,6 +325,7 @@ fn corrupt(url: &str) -> String { /// The tables marks draw from dataset `name`: a table named after a layer holds the kind of feature /// its marks draw, polygons first, then lines, then points; one named `layer:kind` holds that kind. +/// Each is shaped as the marks drawing its geometry shape it. fn decoded_tables(scene: &Scene, name: &Name) -> Vec { let mut tables: Vec = Vec::new(); for mark in &scene.marks { @@ -327,7 +354,13 @@ fn decoded_tables(scene: &Scene, name: &Name) -> Vec { match tables.iter_mut().find(|d| d.table == table) { Some(d) if rank(kind) < rank(d.kind) => d.kind = kind, Some(_) => {} - None => tables.push(Decoded { layer, kind, table }), + None => { + let shapes = shapings(scene, name, &table) + .into_iter() + .filter_map(|(field, shaping)| (field == "geometry").then_some(shaping)) + .collect(); + tables.push(Decoded { layer, kind, table, shapes }); + } } } crate::order::by_name(&mut tables, |t| t.table.as_str()); diff --git a/crates/latkit/src/expr/compile.rs b/crates/latkit/src/expr/compile.rs index 81e90235..ad8db320 100644 --- a/crates/latkit/src/expr/compile.rs +++ b/crates/latkit/src/expr/compile.rs @@ -260,9 +260,9 @@ impl<'a> Compiler<'a> { Ok(()) } - /// The program column of the derived number field `field`, which shaders read directly. - pub(crate) fn derived_number(&mut self, field: &str) -> u32 { - self.column(ColumnUse::derived(field, Words::Values)) + /// The program column of the derived field `field`, read as words. + pub(crate) fn derived_bits(&mut self, field: &str) -> u32 { + self.column(ColumnUse::derived(field, Words::Bits)) } /// The program column of the derived geometry field `field`, read as points per row. @@ -339,7 +339,7 @@ impl<'a> Compiler<'a> { Ok(()) } - /// Compiles an end storing each row's own vertex slot at `word`: the vertex mark drawing the + /// Compiles an end storing each row itself at `word`: its row of the vertex mark drawing the /// mark's own table, row for row, as a pole stands on. pub(crate) fn own_end(&mut self, table: &Name, word: u32) -> Result<()> { let start = self.begin(); diff --git a/crates/latkit/src/geo/tiles.rs b/crates/latkit/src/geo/tiles.rs index e6a4ada5..e9801089 100644 --- a/crates/latkit/src/geo/tiles.rs +++ b/crates/latkit/src/geo/tiles.rs @@ -1,8 +1,9 @@ //! Vector tiles as data: where tiles come from, and a tile decoded into the rows of the tables marks -//! draw from it. +//! draw from it, each with its shapes. use super::mvt::{self, Kind, Property, Shape}; use crate::data::{Ids, Name, Patch, Table, Values}; +use crate::host::Shaping; use crate::tiles::TileKey; /// Tiles fetched from a URL template, from a TileJSON document or given as one. @@ -65,21 +66,31 @@ impl Template { } } -/// A table a tile decodes into: its layer's features of one kind, as table `table`. +/// A table a tile decodes into: its layer's features of one kind, as table `table`, and the shapes +/// its marks draw of its geometry. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub(crate) struct Decoded { pub(crate) layer: String, pub(crate) kind: Kind, pub(crate) table: Name, + pub(crate) shapes: Vec, } /// Features a tile's table numbers at most; ids beyond its place in the tile never collide. pub(crate) const FEATURES: u64 = 1 << 20; +/// The field a tile's rows hold their geometry as. +const GEOMETRY: &str = "geometry"; + +/// Whether field `name` of a tile's table holds its rows' shapes, which no property is read as. +pub(crate) fn shapes_field(name: &str) -> bool { + [Shaping::Triangles { earth: true }, Shaping::Path].iter().any(|s| *s.field_of(GEOMETRY) == *name) +} + /// Tile `key`'s rows as a patch of every table of `tables`: each feature of the table's layer and -/// kind a row (a multi-point a row per point), its geometry field `geometry` and its properties -/// fields. A row's id is the tile's place along the Hilbert curve times [`FEATURES`], plus its own -/// place among the table's rows. `None` when the bytes are no tile. +/// kind a row (a multi-point a row per point), its geometry field `geometry`, the fields its shapes +/// are written as, and its properties fields. A row's id is the tile's place along the Hilbert curve +/// times [`FEATURES`], plus its own place among the table's rows. `None` when the bytes are no tile. pub(crate) fn decode(bytes: &[u8], key: TileKey, tables: &[Decoded]) -> Option { let bytes = super::gunzipped(bytes).ok()?; let layers = mvt::decode(&bytes, key)?; @@ -107,7 +118,7 @@ pub(crate) fn decode(bytes: &[u8], key: TileKey, tables: &[Decoded]) -> Option

= (0..count as u64).map(|k| first + k).collect(); + let ids: Ids = (0..count as u64).map(|k| first + k).collect::>().into(); let geometry: Values = match decoded.kind { Kind::Points => shapes[..count] .iter() @@ -127,9 +138,14 @@ pub(crate) fn decode(bytes: &[u8], key: TileKey, tables: &[Decoded]) -> Option

>() .into(), }; - let mut table = Table::new(decoded.table.clone(), Ids::from(ids)).field("geometry", geometry); + let depth = geometry.geometry().map_or(0, crate::data::Geometry::depth); + let mut table = Table::new(decoded.table.clone(), ids); + for &shaping in decoded.shapes.iter().filter(|s| s.takes(depth)) { + table = table.field(shaping.field_of(GEOMETRY), shaping.shapes_of(&geometry)); + } + table = table.field(GEOMETRY, geometry); for (k, key) in layer.keys.iter().enumerate() { - if key == "geometry" { + if key == GEOMETRY || shapes_field(key) { continue; } let values: Vec> = properties[..count] @@ -185,7 +201,9 @@ mod tests { fn a_tile_decodes_into_its_tables_rows() { let key = TileKey { z: 3, x: 2, y: 3 }; let bytes = tile("water", 3, &square(100, 100, 1000), "bay"); - let tables = [Decoded { layer: "water".into(), kind: Kind::Polygons, table: "water".into() }]; + let triangles = Shaping::Triangles { earth: true }; + let tables = + [Decoded { layer: "water".into(), kind: Kind::Polygons, table: "water".into(), shapes: vec![triangles] }]; let patch = decode(&bytes, key, &tables).expect("a tile"); let mut data = Data::new("base"); data.apply(patch).expect("valid"); @@ -194,7 +212,10 @@ mod tests { let id = crate::geo::pmtiles::tile_id(key) * FEATURES; assert_eq!(water.get(id, "name"), Some(Value::Text("bay".into())), "stable ids"); assert!(matches!(water.get(id, "geometry"), Some(Value::Polygons(_)))); - let other = [Decoded { layer: "water".into(), kind: Kind::Lines, table: "water".into() }]; + // Its shapes, the triangles areas draw: a label point and a triangle at least. + let triangles = water.get(id, "geometry.triangles"); + assert!(matches!(triangles, Some(Value::Path(points)) if points.len() >= 4)); + let other = [Decoded { layer: "water".into(), kind: Kind::Lines, table: "water".into(), shapes: Vec::new() }]; assert!(decode(&bytes, key, &other).expect("a tile").is_empty(), "no lines in it"); } diff --git a/crates/latkit/src/gpu/arena.rs b/crates/latkit/src/gpu/arena.rs index 8f4ff181..55b406a9 100644 --- a/crates/latkit/src/gpu/arena.rs +++ b/crates/latkit/src/gpu/arena.rs @@ -1,33 +1,45 @@ -//! Data pages on the GPU: one pool of 64 Ki-word chunks in one storage buffer, which doubles as it -//! fills, up to the budget. A chunk is carved into blocks of one power-of-two size, and no page is -//! larger than a chunk: a geometry's points split into pieces of a chunk each. So the room any page -//! leaves serves any other: a chunk whose blocks all come back is carved again for another size. +//! Pages on the GPU: one pool of 128 Ki-word chunks in one storage buffer, which doubles as it fills, +//! up to the budget. It holds the data's pages, which upload, and the pages of words each view's marks +//! keep per row, which the GPU writes. A chunk is carved into blocks of one power-of-two size, and no +//! page is larger than a chunk: a geometry's points split into pieces of a chunk each. So the room any +//! page leaves serves any other: a chunk whose blocks all come back is carved again for another size. +//! Few sizes in few chunks leave little room unused in chunks partly used, which a small budget feels. //! -//! A page is identified by its id, the words of it a program reads, and its piece. Page tables pin -//! the pages they reference, so a page in use is never evicted; unpinned pages stay cached, the least -//! recently used going first when a page needs their room. Uploads stream within a per-frame byte -//! budget. A page that finds no room waits aside until pins let room go, while the pages after it go -//! on: views plan what they draw to fit the pool, so none waits long. +//! A data page is identified by its id, the words of it a program reads, and its piece. What a view +//! draws next reserves every page it reads at once, or none: those in the pool are pinned, and those +//! it lacks take blocks then, evicting the least recently used unpinned pages, so nothing is partly +//! pinned and no page waits for room. Page tables pin the pages they reference, so a page in use is +//! never evicted; unpinned pages stay cached. Uploads stream within a per-frame byte budget into the +//! blocks reserved. Views plan what they draw to fit the pool, so a reservation finding no room is +//! rare: it waits for the next frame, which grows the pool by what it lacked. +//! +//! A page of words per row is a block of its own: cleared as it is handed out, before anything writes +//! it, and freed only once the frame that let it go is done with it. The pool grows only before a +//! frame records writes to the pages, since growing copies what they hold: a block a frame finds no +//! room for then waits for the next. -use std::collections::hash_map::Entry; use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque}; use rustc_hash::FxBuildHasher; -use crate::data::{AxisPage, FramePage, PageId, Value, Values}; +use crate::data::{AxisPage, FRAME_PAGE_WORDS, FramePage, PageId, Value, Values}; use crate::expr::compile::Words; use crate::gpu::shaders::{MISSING, NONE, POINT_BITS}; use crate::gpu::work::Queue; -/// Words in a chunk: the largest block, and the largest page. -pub(crate) const CHUNK_WORDS: u32 = 1 << 16; +/// Words in a chunk, as a shift: the largest block, and the largest page. +const CHUNK_BITS: u32 = 17; +pub(crate) const CHUNK_WORDS: u32 = 1 << CHUNK_BITS; /// Bytes in a chunk. pub(crate) const CHUNK_BYTES: u64 = CHUNK_WORDS as u64 * 4; const MIN_CLASS_BITS: u32 = 6; -const CLASSES: usize = (16 - MIN_CLASS_BITS + 1) as usize; -const INITIAL_CHUNKS: u32 = 16; +const CLASSES: usize = (CHUNK_BITS - MIN_CLASS_BITS + 1) as usize; +/// Words of a chunk's free bits: a bit for each block of the least class. +const FREE_WORDS: usize = 1 << (CHUNK_BITS - MIN_CLASS_BITS - 6); +const INITIAL_CHUNKS: u32 = 8; /// Points in a piece of a geometry's points: a chunk of x and y words. pub(crate) const PIECE_POINTS: usize = 1 << POINT_BITS; +const _: () = assert!(2 * PIECE_POINTS == CHUNK_WORDS as usize, "a piece of points fills a chunk"); /// A page as a program reads it. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] @@ -48,6 +60,14 @@ impl PageKey { } } +/// A page a reservation asks for: what it is, the words it must hold, and, where the pool does not +/// hold them, what it uploads. +pub(crate) struct Ask { + pub(crate) key: PageKey, + pub(crate) need: u32, + pub(crate) content: Option, +} + /// What to upload for a page. pub(crate) enum Content { /// Static values, converted to words for `words`. @@ -85,16 +105,22 @@ impl Content { } } - /// The class of the block it takes: a growing page takes a whole chunk to grow into. + /// The class of the block it takes: a growing page takes a block of the most words a page of its + /// kind holds, to grow into. fn class(&self, words: u32) -> usize { match self { - Content::Frames(page) if page.grows() => CLASSES - 1, - Content::Axis(page) if page.grows() => CLASSES - 1, + Content::Frames(page) if page.grows() => class_of(FRAME_PAGE_WORDS as u32), + Content::Axis(page) if page.grows() => class_of(FRAME_PAGE_WORDS as u32), _ => class_of(words), } } } +/// Words of the block that holds `words`. +pub(crate) fn rounded(words: u32) -> u32 { + class_words(class_of(words)) +} + /// The class of a block holding `words`: the power of two at least that large. fn class_of(words: u32) -> usize { let bits = words.max(1).next_power_of_two().trailing_zeros().max(MIN_CLASS_BITS); @@ -185,7 +211,7 @@ pub(crate) fn path_pieces(values: &Values) -> Vec> { } struct Block { - /// Offset in words, or `NONE` until allocated. + /// Offset in words. offset: u32, class: usize, /// Words uploaded so far. @@ -200,11 +226,11 @@ struct Block { #[derive(Clone, Copy)] struct Chunk { live: u32, - free: [u64; 16], + free: [u64; FREE_WORDS], } impl Chunk { - const FREE: Chunk = Chunk { live: 0, free: [0; 16] }; + const FREE: Chunk = Chunk { live: 0, free: [0; FREE_WORDS] }; /// Takes a free block, returning its slot. fn take(&mut self) -> u32 { @@ -231,11 +257,17 @@ pub(crate) struct Arena { unpinned: BTreeMap, tick: u64, uploads: VecDeque, - /// Pages that found no room, waiting for a pin to let some go. - stuck: Vec, contents: HashMap, /// Bytes of allocated blocks. resident: u64, + /// Blocks of their own handed out and not yet cleared; and those let go, freed once the frame is + /// done with them. + cleared: Vec<(u32, usize)>, + freed: Vec<(u32, usize)>, + /// Whether this frame recorded writes to the pages, after which the pool keeps its buffer; and the + /// words of blocks it found no room for, which the next frame grows the pool by. + writing: bool, + short: u64, } impl Arena { @@ -254,73 +286,176 @@ impl Arena { unpinned: BTreeMap::new(), tick: 0, uploads: VecDeque::new(), - stuck: Vec::new(), contents: HashMap::default(), resident: 0, + cleared: Vec::new(), + freed: Vec::new(), + writing: false, + short: 0, + } + } + + /// Begins a frame of every view drawing from the pool: frees the blocks the last one let go, and + /// grows the pool by what it found no room for, before anything this frame writes the pages. + pub(crate) fn begin_frame(&mut self, device: &wgpu::Device, queue: &Queue) { + for (offset, class) in std::mem::take(&mut self.freed) { + self.release(offset, class); + } + self.writing = false; + let short = std::mem::take(&mut self.short).div_ceil(u64::from(CHUNK_WORDS)); + if short > 0 { + let chunks = self.chunks.len() as u64; + self.grow(device, queue, (chunks + short).max(2 * chunks).min(u64::from(u32::MAX)) as u32); + } + } + + /// Reserves every page `asks` names, or none: pins those the pool holds, and gives those it lacks + /// blocks of their size, evicting the least recently used unpinned pages but never these, queuing + /// what they upload, as for one the pool holds fewer words of than asked. `false` where a page + /// finds no room, or lacks what it uploads: nothing is pinned, and the next frame grows the pool by + /// what it lacked. + pub(crate) fn reserve(&mut self, device: &wgpu::Device, queue: &Queue, asks: &mut [Ask]) -> bool { + // Pages held are pinned first, so placing the rest evicts none of them. + let held: Vec = asks.iter().map(|ask| self.blocks.contains_key(&ask.key)).collect(); + for ask in asks.iter().zip(&held).filter(|&(_, &held)| held).map(|(ask, _)| ask) { + self.pin(ask.key); + } + let mut placed: HashMap = HashMap::default(); + let mut room = true; + for (ask, _) in asks.iter().zip(&held).filter(|&(_, &held)| !held) { + if placed.contains_key(&ask.key) { + continue; + } + let class = ask.content.as_ref().map(|content| content.class(content.len().max(ask.need))); + let Some(offset) = class.and_then(|class| self.allocate(device, queue, class)) else { + room = false; + break; + }; + placed.insert(ask.key, (offset, class.expect("placed"))); + } + if !room { + for (offset, class) in placed.into_values() { + self.release(offset, class); + } + for ask in asks.iter().zip(&held).filter(|&(_, &held)| held).map(|(ask, _)| ask) { + self.unpin(ask.key); + } + return false; + } + for (key, (offset, class)) in placed { + self.resident += u64::from(class_words(class)) * 4; + self.blocks.insert(key, Block { offset, class, written: 0, pins: 0, tick: 0 }); + } + for (ask, held) in asks.iter_mut().zip(held) { + if !held { + self.pin(ask.key); + } + if self.blocks[&ask.key].written < ask.need + && !self.contents.contains_key(&ask.key) + && let Some(content) = ask.content.take() + { + self.contents.insert(ask.key, content); + self.uploads.push_back(ask.key); + } + } + true + } + + /// A block of its own holding `words`, for a page of words per row, cleared by the next + /// [`clear`](Arena::clear); `None` where the pool has no room now, which the next frame grows + /// it by. + pub(crate) fn block(&mut self, device: &wgpu::Device, queue: &Queue, words: u32) -> Option { + let class = class_of(words); + let offset = self.allocate(device, queue, class)?; + self.resident += u64::from(class_words(class)) * 4; + self.cleared.push((offset, class)); + Some(offset) + } + + /// Lets go of block `offset` of `words`, which [`block`](Arena::block) handed out: freed once + /// this frame is done with it. + pub(crate) fn free(&mut self, offset: u32, words: u32) { + self.freed.push((offset, class_of(words))); + } + + /// Clears the blocks handed out since the last clearing, at once: before anything this frame + /// records or writes into them. + pub(crate) fn clear(&mut self, device: &wgpu::Device, queue: &Queue) { + if self.cleared.is_empty() { + return; + } + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("latkit clear") }); + for (offset, class) in self.cleared.drain(..) { + encoder.clear_buffer(&self.buffer, u64::from(offset) * 4, Some(u64::from(class_words(class)) * 4)); } + queue.submit([encoder.finish()]); + } + + /// Notes that this frame records writes to the pages: from now on it keeps the pool's buffer, which + /// growing would copy before they run. + pub(crate) fn written(&mut self) { + self.writing = true; } pub(crate) fn buffer(&self) -> &wgpu::Buffer { &self.buffer } + /// Whether the pool has grown as far as the budget lets it. + pub(crate) fn full(&self) -> bool { + self.chunks.len() as u32 >= self.limit + } + /// Lets the pool grow to at most `bytes` from now on; it never shrinks. pub(crate) fn set_limit(&mut self, device: &wgpu::Device, bytes: u64) { self.limit = limit_of(device, bytes).max(self.chunks.len() as u32); } - /// The block holding at least `need` words of `key`, or `None` while it uploads. - pub(crate) fn get(&mut self, key: PageKey, need: u32, content: impl FnOnce() -> Content) -> Option { - if let Some(offset) = self.offset(key, need) { - return Some(offset); - } - if let Entry::Vacant(entry) = self.contents.entry(key) { - entry.insert(content()); - self.uploads.push_back(key); - } - None - } - /// The block holding at least `need` words of `key`, if one does. pub(crate) fn offset(&self, key: PageKey, need: u32) -> Option { - self.blocks.get(&key).filter(|b| b.offset != NONE && b.written >= need).map(|b| b.offset) + self.blocks.get(&key).filter(|b| b.written >= need).map(|b| b.offset) + } + + /// Bytes of the block `key` takes, or none. + pub(crate) fn bytes_of(&self, key: PageKey) -> u64 { + self.blocks.get(&key).map_or(0, |b| u64::from(class_words(b.class)) * 4) } - /// Whether `key` waits to upload. - pub(crate) fn queued(&self, key: PageKey) -> bool { - self.contents.contains_key(&key) + /// Whether the pool holds, or is uploading, at least `need` words of `key`: a reservation asks + /// for what it uploads only where not. + pub(crate) fn holds(&self, key: PageKey, need: u32) -> bool { + self.offset(key, need).is_some() || (self.blocks.contains_key(&key) && self.contents.contains_key(&key)) } - /// Keeps `key` resident while pinned; pins nest. - pub(crate) fn pin(&mut self, key: PageKey) { - let block = self.blocks.entry(key).or_insert(Block { offset: NONE, class: 0, written: 0, pins: 0, tick: 0 }); - if block.pins == 0 && block.offset != NONE { + /// Keeps `key`, which the pool holds, while pinned; pins nest. + fn pin(&mut self, key: PageKey) { + let block = self.blocks.get_mut(&key).expect("a page reserved"); + if block.pins == 0 { self.unpinned.remove(&block.tick); } block.pins += 1; } + /// Lets go of a pin a reservation took. A page no longer pinned stays cached, but one still + /// uploading, which nothing waits for now: it goes, and its upload with it. pub(crate) fn unpin(&mut self, key: PageKey) { let Some(block) = self.blocks.get_mut(&key) else { return }; block.pins -= 1; if block.pins > 0 { return; } - if block.offset == NONE { - self.blocks.remove(&key); - self.contents.remove(&key); + if self.contents.remove(&key).is_some() { + let block = self.blocks.remove(&key).expect("held"); + self.release(block.offset, block.class); return; } self.tick += 1; block.tick = self.tick; self.unpinned.insert(self.tick, key); - // Room let go may be what a page without room needs: it tries again first. - for key in self.stuck.drain(..).rev() { - self.uploads.push_front(key); - } } - /// Whether uploads are waiting that can make progress. + /// Whether uploads are waiting. pub(crate) fn uploading(&self) -> bool { !self.uploads.is_empty() } @@ -335,62 +470,45 @@ impl Arena { self.resident } - /// Uploads queued pages, at least one and at most `budget` bytes; returns the bytes uploaded. - /// - /// A page that finds no room waits aside, and the pages after it go on; it tries again once a - /// pin lets room go. - pub(crate) fn pump(&mut self, device: &wgpu::Device, queue: &Queue, budget: u64) -> u64 { + /// Uploads queued pages into the blocks reserved for them, at least one and at most `budget` bytes; + /// returns the bytes uploaded. + pub(crate) fn pump(&mut self, queue: &Queue, budget: u64) -> u64 { let mut bytes = 0; // Views drawn earlier in the frame spent it. if budget == 0 { return 0; } + let mut writes: Vec<(u32, Vec)> = Vec::new(); while (bytes == 0 || bytes < budget) && let Some(key) = self.uploads.pop_front() { let Some(content) = self.contents.remove(&key) else { continue }; - let resident = self.blocks.get(&key).filter(|b| b.offset != NONE).map(|b| (b.offset, b.class, b.written)); - let len = content.len(); - let (offset, written) = match resident { - Some((offset, class, written)) if class_words(class) >= len => (offset, written), - _ => { - let Some(offset) = self.place(device, queue, key, content.class(len)) else { - self.contents.insert(key, content); - self.stuck.push(key); - continue; - }; - (offset, 0) - } - }; + let Some(block) = self.blocks.get_mut(&key) else { continue }; + let (offset, written) = (block.offset, block.written); let tail = content.words(written as usize); + block.written = written + tail.len() as u32; if !tail.is_empty() { - queue.upload(&self.buffer, u64::from(offset + written) * 4, bytemuck::cast_slice(&tail)); bytes += tail.len() as u64 * 4; + writes.push((offset + written, tail)); } - self.blocks.get_mut(&key).expect("placed").written = written + tail.len() as u32; } - bytes - } - - /// Allocates a block of `class` for `key`, releasing a smaller one it had. - fn place(&mut self, device: &wgpu::Device, queue: &Queue, key: PageKey, class: usize) -> Option { - let offset = self.allocate(device, queue, class, key)?; - if let Some(old) = self.blocks.get(&key).filter(|b| b.offset != NONE) { - let (old_offset, old_class) = (old.offset, old.class); - self.release(old_offset, old_class); + // Pages placed side by side, as a column's pages are, upload in one write. + writes.sort_unstable_by_key(|&(at, _)| at); + let mut run: Option<(u32, Vec)> = None; + for (at, words) in writes { + match &mut run { + Some((start, joined)) if *start + joined.len() as u32 == at => joined.extend_from_slice(&words), + _ => { + if let Some((start, joined)) = run.replace((at, words)) { + queue.upload(&self.buffer, u64::from(start) * 4, bytemuck::cast_slice(&joined)); + } + } + } } - let block = self.blocks.entry(key).or_insert(Block { offset: NONE, class, written: 0, pins: 0, tick: 0 }); - let pinned = block.pins > 0; - block.offset = offset; - block.class = class; - block.written = 0; - self.resident += u64::from(class_words(class)) * 4; - if !pinned { - self.tick += 1; - block.tick = self.tick; - self.unpinned.insert(self.tick, key); + if let Some((start, joined)) = run { + queue.upload(&self.buffer, u64::from(start) * 4, bytemuck::cast_slice(&joined)); } - Some(offset) + bytes } fn release(&mut self, offset: u32, class: usize) { @@ -409,10 +527,11 @@ impl Arena { self.resident -= u64::from(class_words(class)) * 4; } - /// A block of `class`: a free one of its size, a free chunk carved, the pool grown, or room the - /// least recently used unpinned blocks leave, whatever their size; `None` when every block left - /// is pinned. `keep` is the page it is for, which is never evicted for itself. - fn allocate(&mut self, device: &wgpu::Device, queue: &Queue, class: usize, keep: PageKey) -> Option { + /// A block of `class`: a free one of its size, a free chunk carved, the pool grown while this + /// frame has not written the pages, or room the least recently used unpinned blocks leave, + /// whatever their size; `None` when every block left is pinned or a block of its own, and the + /// next frame grows the pool by it. + fn allocate(&mut self, device: &wgpu::Device, queue: &Queue, class: usize) -> Option { loop { if let Some(&k) = self.partial[class].first() { let chunk = &mut self.chunks[k as usize]; @@ -424,7 +543,7 @@ impl Arena { } if let Some(k) = self.free.pop_first() { let slots = slots(class); - let mut free = [0_u64; 16]; + let mut free = [0_u64; FREE_WORDS]; for (w, word) in free.iter_mut().enumerate() { let bits = slots.saturating_sub(w as u32 * 64).min(64); *word = if bits == 64 { u64::MAX } else { (1 << bits) - 1 }; @@ -433,20 +552,23 @@ impl Arena { self.partial[class].insert(k); continue; } - if self.grow(device, queue) { + if !self.writing && self.grow(device, queue, 2 * self.chunks.len() as u32) { continue; } - let (&tick, &key) = self.unpinned.iter().find(|&(_, k)| *k != keep)?; + let Some((&tick, &key)) = self.unpinned.iter().next() else { + self.short += u64::from(class_words(class)); + return None; + }; self.unpinned.remove(&tick); let block = self.blocks.remove(&key).expect("listed"); self.release(block.offset, block.class); } } - /// Doubles the buffer up to the limit, copying what it holds. - fn grow(&mut self, device: &wgpu::Device, queue: &Queue) -> bool { + /// Grows the buffer to `chunks`, at most the limit, copying what it holds. + fn grow(&mut self, device: &wgpu::Device, queue: &Queue, chunks: u32) -> bool { let before = self.chunks.len() as u32; - let chunks = (before * 2).min(self.limit); + let chunks = chunks.min(self.limit); if chunks <= before { return false; } @@ -489,8 +611,9 @@ mod tests { assert_eq!(class_words(class_of(64)), 64); assert_eq!(class_words(class_of(65)), 128); assert_eq!(class_words(class_of(65536)), 65536); - assert_eq!(CLASSES, 11); - assert_eq!(slots(0), 1024); + assert_eq!(class_words(class_of(CHUNK_WORDS)), CHUNK_WORDS); + assert_eq!(CLASSES, 12); + assert_eq!(slots(0), 2048); assert_eq!(slots(CLASSES - 1), 1); } diff --git a/crates/latkit/src/gpu/atlas.rs b/crates/latkit/src/gpu/atlas.rs index 71bb27c7..824b131e 100644 --- a/crates/latkit/src/gpu/atlas.rs +++ b/crates/latkit/src/gpu/atlas.rs @@ -1,5 +1,7 @@ //! Text on the GPU: runs set once by the host at `TEXT_BASE`, turned into distance fields reaching -//! `TEXT_SPREAD` texels past the text's edge, and packed on shelves of one texture. A distance field +//! `TEXT_SPREAD` texels past the text's edge, and packed on shelves of one texture. Until a run is +//! set, its size is measured from its font's characters, each measured by the host once, so its label +//! is placed where it will show. A distance field //! draws crisp at any size and gives halos for free, so a run is set once whatever size it shows at. //! The host sets coverage; the GPU makes the fields of every run set in a frame at once, before the //! frame draws (`compute/fields.wesl`). When no shelf has room, the shelf used least recently is @@ -10,6 +12,8 @@ use std::collections::HashMap; use std::sync::Arc; +use rustc_hash::FxHashMap; + use crate::gpu::pipelines::Pipelines; use crate::gpu::shaders::{FIELD_AT, FIELD_COLUMNS, FIELD_COVERAGE, FIELD_HEIGHT, FIELD_ROW, FIELD_WIDTH, FIELD_WORDS}; use crate::gpu::shaders::{Shader, TEXT_BASE, TEXT_SPREAD}; @@ -42,21 +46,35 @@ impl Glyphs { } } -/// A run table entry for `run` before it is set: the estimate flag, then the corner a rectangle of -/// its size by a rough measure would reach from the atlas's own corner, so its label can be placed -/// while only labels that show have their texts set. -pub(crate) fn estimate(run: &TextRun) -> [u32; 2] { - let size = run.size_px as f32; - let width = run.text.chars().count() as f32 * 0.58 * size; - let (width, lines) = match run.max_width.map(|w| w as f32) { - Some(wrap) if width > wrap => (wrap, (width / wrap).ceil()), - _ => (width, 1.0), - }; - // The host pads each run a little, and the field reaches past it. - let pad = 2.0 * (2.0 + PAD as f32); - let w = ((width + pad).ceil() as u32).min(0xffff); - let h = ((lines * 1.25 * size + pad).ceil() as u32).min(0xffff); - [crate::gpu::shaders::TEXT_ESTIMATE, w | h << 16] +/// What a font's text takes at the base size, as the host sets it: what a run of no characters +/// takes, what each line past the first adds, and each character's advance, measured once each. +struct Metrics { + empty: [f32; 2], + line: f32, + advances: FxHashMap, +} + +impl Metrics { + /// `font`'s, as `host` measures it. + fn of(host: &dyn Host, font: &Arc) -> Metrics { + let measure = |text: &str, max_width: Option| { + host.measure(&TextRun { text: text.into(), font: font.clone(), size_px: TEXT_BASE as u32, max_width }) + .unwrap_or([0.0, 1.25 * TEXT_BASE]) + }; + let empty = measure("", None); + let line = measure("M M", Some(1))[1] - measure("M", None)[1]; + Metrics { empty, line, advances: FxHashMap::default() } + } + + /// What `c` advances a line by, measured by `host` the first time. + fn advance(&mut self, host: &dyn Host, font: &Arc, c: char) -> f32 { + let empty = self.empty[0]; + *self.advances.entry(c).or_insert_with(|| { + let run = + TextRun { text: c.to_string().into(), font: font.clone(), size_px: TEXT_BASE as u32, max_width: None }; + host.measure(&run).map_or(0.58 * TEXT_BASE, |[w, _]| w - empty) + }) + } } /// The run the atlas sets for `text` shown at `size`, wrapping at `max_width`, both length units: set @@ -98,6 +116,8 @@ pub(crate) struct Atlas { pub(crate) evictions: u64, /// What counts the texts the host is asked to set. counters: Arc, + /// What each font's text takes, by font. + metrics: HashMap, Metrics>, } impl Atlas { @@ -124,9 +144,40 @@ impl Atlas { clock: 0, evictions: 0, counters, + metrics: HashMap::new(), } } + /// The run table entry for `run` while it is not set: the flag saying so, then the corner a + /// rectangle of its size reaches from the atlas's own corner. Its lines break at spaces as the + /// host breaks them, each as wide as its characters' advances, so its label is placed where it + /// will show while only labels that show have their texts set. Nothing without a host. + pub(crate) fn unset(&mut self, host: Option<&dyn Host>, run: &TextRun) -> [u32; 2] { + let Some(host) = host else { return [0, 0] }; + let font = &run.font; + let metrics = self.metrics.entry(font.clone()).or_insert_with(|| Metrics::of(host, font)); + let mut width = |text: &str| text.chars().map(|c| metrics.advance(host, font, c)).sum::(); + let (widest, lines) = match run.max_width.map(|w| w as f32) { + None => (width(&run.text), 1), + Some(max) => { + let space = width(" "); + let mut lines: Vec = Vec::new(); + for word in run.text.split(' ') { + let w = width(word); + match lines.last_mut() { + Some(line) if *line + space + w <= max => *line += space + w, + _ => lines.push(w), + } + } + (lines.iter().copied().fold(0.0, f32::max), lines.len()) + } + }; + let [pad, height] = metrics.empty; + let w = ((widest + pad).ceil() as u32 + 2 * PAD).min(0xffff); + let h = ((height + (lines - 1) as f32 * metrics.line).ceil() as u32 + 2 * PAD).min(0xffff); + [crate::gpu::shaders::TEXT_UNSET, w | h << 16] + } + /// Starts a frame: runs asked for from now on count as used in it. pub(crate) fn tick(&mut self) { self.clock += 1; diff --git a/crates/latkit/src/gpu/mod.rs b/crates/latkit/src/gpu/mod.rs index f699b189..25a2858a 100644 --- a/crates/latkit/src/gpu/mod.rs +++ b/crates/latkit/src/gpu/mod.rs @@ -115,11 +115,13 @@ pub(crate) struct Allowance { } impl Inner { - /// Begins view `view`'s part of a frame: a new frame when it already drew in this one. - pub(crate) fn begin_frame(&mut self, view: u64) { + /// Begins view `view`'s part of a frame: a new frame when it already drew in this one, which + /// frees the pages the last one let go and grows the pool by what it found no room for. + pub(crate) fn begin_frame(&mut self, view: u64, device: &wgpu::Device, queue: &Queue) { if self.frame.drawn.is_empty() || self.frame.drawn.contains(&view) { self.frame = Allowance { drawn: Vec::new(), upload: self.budget.upload_per_frame, work: crate::derive::FRAME_WORK }; + self.arena.begin_frame(device, queue); } self.frame.drawn.push(view); } diff --git a/crates/latkit/src/gpu/packed.rs b/crates/latkit/src/gpu/packed.rs index e48150c9..e2f1bdc7 100644 --- a/crates/latkit/src/gpu/packed.rs +++ b/crates/latkit/src/gpu/packed.rs @@ -9,10 +9,11 @@ use std::sync::Arc; use rustc_hash::FxHashMap; -use crate::data::{Column, Name, PAGE_ROWS, PageId, Reference, Samples, Table, UNWIRED, Version}; -use crate::derive::order::{Order, PAGE}; +use crate::data::{Column, Name, PageId, Reference, Samples, Table, UNWIRED, Version}; +use crate::derive::order::Order; use crate::expr::compile::Words; use crate::gpu::arena::page_words; +use crate::gpu::shaders::PAGE_ROWS; use crate::gpu::shaders::{MISSING, NONE}; use crate::gpu::work::{Count, Counters}; @@ -87,7 +88,7 @@ impl Packed { { if let Some(before) = &pages.source { for row in source.written_since(before) { - let page = (order.rank(row as u32) / PAGE) as usize; + let page = (order.rank(row as u32) / PAGE_ROWS) as usize; if let Some(id) = pages.ids.get_mut(page) { *id = None; } @@ -163,7 +164,7 @@ fn forget(kept: &mut FxHashMap, used: /// The words `column`'s rows `rows` are read as, a page of them, missing past the rows. pub(crate) fn values(column: &Column, rows: &[u32], words: Words, low: bool) -> Vec { let mut out = page_words(&column.pick(rows), words, low); - out.resize(PAGE as usize, if words == Words::Bits { NONE } else { MISSING }); + out.resize(PAGE_ROWS as usize, if words == Words::Bits { NONE } else { MISSING }); out } @@ -177,14 +178,14 @@ pub(crate) fn targets(reference: &Reference, rows: &[u32], rank: Option<&Order>) t => rank.map_or(t, |order| order.rank(t)), }) .collect(); - out.resize(PAGE as usize, NONE); + out.resize(PAGE_ROWS as usize, NONE); out } /// The words of reference page `page` of a table drawn in its own order: the rows it names, as /// `rank`, the order their table draws in, places them. pub(crate) fn ranked_page(reference: &Reference, page: usize, rank: &Order) -> Vec { - let start = page * PAGE_ROWS; + let start = page * PAGE_ROWS as usize; let len = reference.rows.pages().get(page).map_or(0, |p| p.values.len()); (start..start + len) .map(|r| match reference.target_row(r) { @@ -200,7 +201,7 @@ pub(crate) fn frame(samples: &Samples, rows: &[u32], frame: Option) -> Vec< .iter() .map(|&r| frame.and_then(|f| samples.value(r as usize, f)).map_or(MISSING, |v| (v as f32).to_bits())) .collect(); - out.resize(PAGE as usize, MISSING); + out.resize(PAGE_ROWS as usize, MISSING); out } @@ -210,7 +211,7 @@ pub(crate) fn joined(table: &Table, rows: &[u32], other: &Table) -> Vec { .iter() .map(|&r| table.ids.key(r as usize).and_then(|key| other.row(key)).map_or(NONE, |o| o as u32)) .collect(); - out.resize(PAGE as usize, NONE); + out.resize(PAGE_ROWS as usize, NONE); out } @@ -236,7 +237,7 @@ mod tests { #[test] fn a_write_repacks_only_the_pages_its_rows_draw_in() { - let n = 3 * PAGE as usize; + let n = 3 * PAGE_ROWS as usize; let column = Column::new(&Values::from((0..n).map(|i| i as f64).collect::>())); // The order draws the rows backward: the last page of rows is its first page. let order = Order::Pyramid { @@ -251,6 +252,6 @@ mod tests { assert_eq!((after[0], after[1]), (ids[0], ids[1]), "pages not drawing row 5 are kept"); assert_ne!(after[2], ids[2], "row 5 draws in the last page, which packs again"); let words = values(&written, &order.page(2), Words::Values, false); - assert_eq!(f32::from_bits(words[PAGE as usize - 1 - 5]), -1.0, "row 5 packed where it draws"); + assert_eq!(f32::from_bits(words[PAGE_ROWS as usize - 1 - 5]), -1.0, "row 5 packed where it draws"); } } diff --git a/crates/latkit/src/gpu/shaders.rs b/crates/latkit/src/gpu/shaders.rs index 8f10a77b..cf275be8 100644 --- a/crates/latkit/src/gpu/shaders.rs +++ b/crates/latkit/src/gpu/shaders.rs @@ -114,11 +114,6 @@ impl Binding { include!(concat!(env!("OUT_DIR"), "/shaders.rs")); -// A block of rows the GPU evaluates is a held page; a page of the data holds a whole number of them. -const _: () = assert!( - 1 << HELD_BITS == BLOCK_ROWS - && HELD_MASK + 1 == BLOCK_ROWS - && 1 << PAGE_BITS as usize == crate::data::PAGE_ROWS - && crate::data::PAGE_ROWS.is_multiple_of(BLOCK_ROWS as usize) - && DISPATCH_ROWS as usize == crate::data::PAGE_ROWS -); +// One page of rows: the data's, a table's order's, and a mark's, which it evaluates at a time. +const _: () = + assert!(1 << PAGE_BITS == PAGE_ROWS && PAGE_MASK + 1 == PAGE_ROWS && PAGE_ROWS as usize == crate::data::PAGE_ROWS); diff --git a/crates/latkit/src/host/browser.rs b/crates/latkit/src/host/browser.rs index 008abc01..af5ded97 100644 --- a/crates/latkit/src/host/browser.rs +++ b/crates/latkit/src/host/browser.rs @@ -22,8 +22,8 @@ thread_local! { static ENCODERS: RefCell> = const { RefCell::new(None) }; } -/// Jobs the page's workers do at once: as many as its pool keeps, one fewer than its cores, at most -/// four. +/// Jobs handed to the page's workers at once: one fewer than its cores, at most four. The page keeps +/// a worker for each and queues nothing. fn workers() -> usize { let cores = web_sys::window() .map(|window| window.navigator().hardware_concurrency()) @@ -62,7 +62,8 @@ fn pump(run: &js_sys::Function) { impl Browser { /// Has views' jobs done by `run`, which takes a job's bytes as a `Uint8Array` and returns a - /// promise of its answer's: a pool of workers, each holding the page's compiled module. + /// promise of its answer's: workers, each holding the page's compiled module. Jobs queue here and + /// `run` gets one only when a worker is free for it. pub fn set_jobs(run: js_sys::Function) { JOBS.with(|jobs| *jobs.borrow_mut() = Some(run)); } diff --git a/crates/latkit/src/host/job.rs b/crates/latkit/src/host/job.rs index 21e3c512..847ac04a 100644 --- a/crates/latkit/src/host/job.rs +++ b/crates/latkit/src/host/job.rs @@ -2,14 +2,14 @@ //! into triangles, grouping a large table's rows into tiles, writing a picture as a file), handed to the //! host to do off the frame. A job is bytes in and bytes out, so a thread, or a worker holding its own //! copy of latkit, can do it with nothing shared. A view's job answers with a [`Patch`]: the rows it -//! worked out, by id, with their places, the paths their wires run along, or their shapes; or in its -//! own words, lanes as numbers and pyramids as their orders and tiles. An image job answers with the -//! file. +//! worked out, by id, with their places, the paths their wires run along, or a vector tile's rows and +//! their shapes; or in its own words, lanes as numbers, shapes as the columns they fill, and pyramids +//! as their orders and tiles. An image job answers with the file. use std::sync::Arc; use crate::Side; -use crate::data::{Field, GeometryBuilder, Id, Name, Op, Patch, Table, Value, Values, Version}; +use crate::data::{GeometryBuilder, Id, Name, Patch, Table, Value, Values, read_values, values_bytes}; use crate::derive::pyramid::{Leveling, Places, Placing}; use crate::derive::sight::Domain; use crate::geo::mvt::Kind as Geometry; @@ -77,6 +77,40 @@ impl Shaping { } } + /// Whether geometry of `depth` takes this shaping: triangles cut parts or polygons, and a path + /// bends anything but points. + pub(crate) fn takes(self, depth: u8) -> bool { + match self { + Shaping::Triangles { .. } => matches!(depth, 2 | 3), + Shaping::Path => depth >= 1, + } + } + + /// The derived field the shapes of geometry field `field` are written as. + pub(crate) fn field_of(self, field: &str) -> Name { + match self { + Shaping::Triangles { .. } => crate::scene::areas::triangles_of(field), + Shaping::Path => crate::scene::lines::path_of(field), + } + } + + /// The shapes of every row of geometry `values`, in order: a row missing shapes as nothing, and a + /// path needing no bending as itself. + pub(crate) fn shapes_of(self, values: &Values) -> Values { + let depth = values.geometry().map_or(0, crate::data::Geometry::depth); + let mut shapes = GeometryBuilder::new(self.depth(depth)); + for row in 0..values.len() { + match values.get(row) { + Some(value) => { + let shaped = self.shape(&value); + shapes.push_value(shaped.as_ref().unwrap_or(&value)); + } + None => shapes.push_empty(), + } + } + Values::of_geometry(shapes.finish()) + } + /// The shape of `value`, or `None` where it stays as it is: a path that needs no bending. pub(crate) fn shape(self, value: &Value) -> Option { match (self, value) { @@ -144,8 +178,8 @@ impl Job { } /// Does the job, answering in the words its derivation reads: rows by id as a patch's bytes, - /// lanes as numbers, a pyramid as its order and tiles, an image as its file. Bytes that are no - /// job answer nothing. + /// lanes as numbers, shapes as columns, a pyramid as its order and tiles, an image as its file. + /// Bytes that are no job answer nothing. pub fn run(self) -> Vec { let mut input = Reader::new(&self.0); let kind = input.u32(); @@ -158,10 +192,12 @@ impl Job { if kind == Some(Kind::Pyramid as u32) { return run_pyramid(&mut input).unwrap_or_default(); } + if kind == Some(Kind::Shapes as u32) { + return run_shapes(&mut input).unwrap_or_default(); + } let answer = match kind { Some(k) if k == Kind::Layered as u32 || k == Kind::Stress as u32 => run_layout(k, &mut input), Some(k) if k == Kind::Routes as u32 => run_routes(&mut input), - Some(k) if k == Kind::Shapes as u32 => run_shapes(&mut input), Some(k) if k == Kind::Tile as u32 => run_tile(&mut input), Some(k) if k == Kind::GeoJson as u32 => run_geojson(&mut input), _ => None, @@ -212,17 +248,17 @@ impl Job { Job(out.0) } - /// Working out `shaping` for every row of `rows`, whose field `geometry` holds each row's - /// geometry; it answers each row's shape as field `shape`. - pub(crate) fn shapes(shaping: Shaping, rows: Table) -> Job { + /// Working out `shaping` for every row of each geometry column of `parts`; it answers each part's + /// shapes, as [`shapes_answer`] reads them. + pub(crate) fn shapes(shaping: Shaping, parts: &[Values]) -> Job { let mut out = Writer::new(Kind::Shapes); out.u32(shaping.code()); - out.0.extend(Patch::new().set(rows).to_bytes()); + out.columns(parts); Job(out.0) } - /// Decoding tile `key` from `bytes`, gzip-compressed or not, into the rows of `tables`; it answers - /// those rows. + /// Decoding tile `key` from `bytes`, gzip-compressed or not, into the rows of `tables`, each with + /// the shapes its marks draw; it answers those rows. pub(crate) fn tile(key: TileKey, tables: &[Decoded], bytes: &[u8]) -> Job { let mut out = Writer::new(Kind::Tile); out.u32(u32::from(key.z)); @@ -237,6 +273,10 @@ impl Job { Geometry::Polygons => 2, }); out.text(&decoded.table); + out.u32(decoded.shapes.len() as u32); + for shaping in &decoded.shapes { + out.u32(shaping.code()); + } } out.0.extend_from_slice(bytes); Job(out.0) @@ -267,7 +307,7 @@ impl Job { } /// Finding every edge row's lane among the rows joining the same two vertices, from each row's two - /// ends; it answers every row's lane as a little-endian `f32`, nothing when none share. + /// ends; it answers every row's lane word as a little-endian `u32`, nothing when none share. pub(crate) fn lanes(from: &[u32], to: &[u32]) -> Job { let mut out = Writer::new(Kind::Lanes); out.u32(from.len() as u32); @@ -445,26 +485,18 @@ fn run_routes(input: &mut Reader<'_>) -> Option { Some(keyed(&keys, routes, |table, routes| table.field("route", routes))) } -fn run_shapes(input: &mut Reader<'_>) -> Option { +/// The shapes a shapes job answers, a column for each part it was asked: `None` where the bytes +/// are no answer. +pub(crate) fn shapes_answer(bytes: &[u8]) -> Option> { + Reader::new(bytes).columns() +} + +fn run_shapes(input: &mut Reader<'_>) -> Option> { let shaping = Shaping::from_code(input.u32()?)?; - let Op::Set(rows) = Patch::from_bytes(input.rest()).ok()?.ops.into_iter().next()? else { return None }; - let Some(Field::Values(column, _)) = rows.field_named("geometry") else { return None }; - let depth = column.kind().depth()?; - let mut shapes = GeometryBuilder::new(shaping.depth(depth)); - for row in 0..rows.len() { - match column.get(row) { - Some(value) => { - let shaped = shaping.shape(&value); - shapes.push_value(shaped.as_ref().unwrap_or(&value)); - } - None => shapes.push_empty(), - } - } - let shapes = Values::of_geometry(shapes.finish()); - let mut answer = rows.clone(); - answer.fields.clear(); - answer.insert("shape".into(), Field::Values(crate::data::Column::new(&shapes), Version::next())); - Some(Patch::new().set(answer)) + let parts = input.columns()?; + let mut out = Writer(Vec::new()); + out.columns(&parts.iter().map(|part| shaping.shapes_of(part)).collect::>()); + Some(out.0) } fn run_tile(input: &mut Reader<'_>) -> Option { @@ -478,7 +510,9 @@ fn run_tile(input: &mut Reader<'_>) -> Option { 1 => Geometry::Lines, _ => Geometry::Polygons, }; - Some(Decoded { layer, kind, table: input.text()?.into() }) + let table = input.text()?.into(); + let shapes = (0..input.u32()?).map(|_| Shaping::from_code(input.u32()?)).collect::>>()?; + Some(Decoded { layer, kind, table, shapes }) }) .collect::>>()?; decode(input.rest(), key, &tables) @@ -532,8 +566,8 @@ fn run_pyramid(input: &mut Reader<'_>) -> Option> { fn run_lanes(input: &mut Reader<'_>) -> Option> { let rows = input.u32()? as usize; let (from, to) = (input.words(rows)?, input.words(rows)?); - let lanes = crate::derive::lanes(&from, &to)?; - Some(lanes.iter().flat_map(|lane| lane.to_le_bytes()).collect()) + let words = crate::derive::lane_words(&from, &to)?; + Some(words.iter().flat_map(|word| word.to_le_bytes()).collect()) } /// Little-endian words, floats, and keys, written. @@ -574,6 +608,16 @@ impl Writer { self.0.extend(text.as_bytes()); } + /// Columns, each its byte length and its values' bytes. + fn columns(&mut self, columns: &[Values]) { + self.u32(columns.len() as u32); + for values in columns { + let bytes = values_bytes(values); + self.u32(bytes.len() as u32); + self.0.extend(bytes); + } + } + /// A key: 1 then a numbered id, or 2 then a texted one; the table's name after. fn key(&mut self, (table, id): &Key) { match id { @@ -633,6 +677,19 @@ impl<'a> Reader<'a> { String::from_utf8(head.to_vec()).ok() } + /// Columns as [`Writer::columns`] wrote them. + fn columns(&mut self) -> Option> { + let count = self.u32()?; + (0..count) + .map(|_| { + let len = self.u32()? as usize; + let (head, rest) = self.bytes.split_at_checked(len)?; + self.bytes = rest; + read_values(head).ok() + }) + .collect() + } + /// A key as [`Writer::key`] wrote it. fn key(&mut self) -> Option { let id = match self.u32()? { diff --git a/crates/latkit/src/host/mod.rs b/crates/latkit/src/host/mod.rs index 5e2d4ba7..2e641733 100644 --- a/crates/latkit/src/host/mod.rs +++ b/crates/latkit/src/host/mod.rs @@ -21,7 +21,7 @@ pub use browser::Browser; pub use job::Job; pub(crate) use job::Placement; pub(crate) use job::Wiring; -pub(crate) use job::{Key, Shaping}; +pub(crate) use job::{Key, Shaping, shapes_answer}; #[cfg(all(feature = "native", not(target_arch = "wasm32")))] pub use native::Native; diff --git a/crates/latkit/src/scene/edges.rs b/crates/latkit/src/scene/edges.rs index e3d03361..51e41ab4 100644 --- a/crates/latkit/src/scene/edges.rs +++ b/crates/latkit/src/scene/edges.rs @@ -154,7 +154,7 @@ impl Edges { drawing.geometry = c.derived_path("route"); } else { drawing.geometry = self.bends.as_ref().map_or(wgsl::NONE, |bends| c.path(bends)); - drawing.lane = c.derived_number("lane"); + drawing.lane = c.derived_bits("lane"); } Ok(drawing) } diff --git a/crates/latkit/src/scene/theme.rs b/crates/latkit/src/scene/theme.rs index bf2e2a63..a6581ecc 100644 --- a/crates/latkit/src/scene/theme.rs +++ b/crates/latkit/src/scene/theme.rs @@ -66,7 +66,8 @@ pub struct Theme { /// parallel orthogonal wires, which keep twice it apart. pub edge_spacing: f32, /// Elsewhere, how far edges joining the same two vertices bow apart: each lane by this fraction - /// of the edge's length at its middle. A bow is part of the drawing, so zooming never moves it. + /// of the edge's length at its middle, as wide as eight lanes at most, more packing closer. A bow + /// is part of the drawing, so zooming never moves it. pub lane_bow: f32, /// How far an arrowhead reaches past its edge's width. pub arrow: f32, diff --git a/crates/latkit/src/view/drawing.rs b/crates/latkit/src/view/drawing.rs index 2b5bb2d6..4d23c88d 100644 --- a/crates/latkit/src/view/drawing.rs +++ b/crates/latkit/src/view/drawing.rs @@ -15,10 +15,11 @@ use rustc_hash::FxHashSet; use super::View; use crate::data::{Field, Name, Reference, UNWIRED, Version}; -use crate::derive::order::{Order, PAGE}; +use crate::derive::order::Order; use crate::derive::stamp_of; use crate::derive::tiled::{Chosen, Draw, Place}; use crate::gpu::shaders as wgsl; +use crate::gpu::shaders::PAGE_ROWS; /// What marks a slot holding a page of kept rows, beside the page's number. const KEPT: u32 = 1 << 31; @@ -153,13 +154,13 @@ impl Drawing { let idle_kept = before.filter(|b| b.content == lay.content).map_or(0, |b| linger(b, &asked_now, lay.room, &mut held)); let kept = keep(before, &lay.pinned.places, &held, &order); - let kept_pages = kept.len().div_ceil(PAGE as usize); + let kept_pages = kept.len().div_ceil(PAGE_ROWS as usize); let (slots, pages, kept_slots) = place(before, &held, kept_pages); let mut kept_rows: Vec<[u32; 2]> = kept .iter() .enumerate() .filter(|&(_, &d)| d != wgsl::NONE) - .map(|(i, &d)| [d, kept_slots[i / PAGE as usize] * PAGE + (i as u32) % PAGE]) + .map(|(i, &d)| [d, kept_slots[i / PAGE_ROWS as usize] * PAGE_ROWS + (i as u32) % PAGE_ROWS]) .collect(); kept_rows.sort_unstable(); let lays = before.map_or(1, |b| b.lays + 1); @@ -230,9 +231,9 @@ impl Drawing { let mut span = |range: Range, fade: u32, runs: &mut Vec| { let mut d = range.start; while d < range.end { - let page = d / PAGE; - let end = range.end.min((page + 1) * PAGE); - push(pages[page as usize] * PAGE + d % PAGE, end - d, fade, runs); + let page = d / PAGE_ROWS; + let end = range.end.min((page + 1) * PAGE_ROWS); + push(pages[page as usize] * PAGE_ROWS + d % PAGE_ROWS, end - d, fade, runs); d = end; } }; @@ -264,7 +265,7 @@ impl Drawing { /// Held rows: a page's rows in each slot. pub(crate) fn live(&self) -> u32 { - self.slots.len() as u32 * PAGE + self.slots.len() as u32 * PAGE_ROWS } /// How many slots it has, free ones among them. @@ -281,10 +282,10 @@ impl Drawing { pub(crate) fn places(&self, s: usize) -> Vec { match self.holds(s) { Holds::Free => Vec::new(), - Holds::Page(k) => (k * PAGE..((k + 1) * PAGE).min(self.order.len())).collect(), + Holds::Page(k) => (k * PAGE_ROWS..((k + 1) * PAGE_ROWS).min(self.order.len())).collect(), Holds::Kept(j) => { - let start = (j * PAGE) as usize; - self.kept[start..self.kept.len().min(start + PAGE as usize)].to_vec() + let start = (j * PAGE_ROWS) as usize; + self.kept[start..self.kept.len().min(start + PAGE_ROWS as usize)].to_vec() } } } @@ -304,9 +305,9 @@ impl Drawing { /// The held row of place `d` in the order, if it holds it: in its page, or among the rows kept. pub(crate) fn held_of(&self, d: u32) -> Option { - let slot = *self.pages.get((d / PAGE) as usize)?; + let slot = *self.pages.get((d / PAGE_ROWS) as usize)?; if slot != wgsl::NONE { - return Some(slot * PAGE + d % PAGE); + return Some(slot * PAGE_ROWS + d % PAGE_ROWS); } let i = self.kept_rows.partition_point(|&[place, _]| place < d); self.kept_rows.get(i).filter(|&&[place, _]| place == d).map(|&[_, held]| held) @@ -319,10 +320,10 @@ impl Drawing { /// The row of its table held row `h` holds. pub(crate) fn source(&self, h: u32) -> Option { - let d = match self.holds((h / PAGE) as usize) { + let d = match self.holds((h / PAGE_ROWS) as usize) { Holds::Free => return None, - Holds::Page(page) => page.checked_mul(PAGE)? + h % PAGE, - Holds::Kept(j) => *self.kept.get((j * PAGE + h % PAGE) as usize)?, + Holds::Page(page) => page.checked_mul(PAGE_ROWS)? + h % PAGE_ROWS, + Holds::Kept(j) => *self.kept.get((j * PAGE_ROWS + h % PAGE_ROWS) as usize)?, }; (d != wgsl::NONE && d < self.order.len()).then(|| self.order.source(d)) } @@ -369,7 +370,7 @@ impl Drawing { fn asked<'r>(count: usize, ranges: impl Iterator>, ends: &[u32]) -> Vec { let mut asked = vec![false; count]; for range in ranges.filter(|r| r.start < r.end) { - for page in range.start / PAGE..=(range.end - 1) / PAGE { + for page in range.start / PAGE_ROWS..=(range.end - 1) / PAGE_ROWS { if let Some(a) = asked.get_mut(page as usize) { *a = true; } @@ -403,7 +404,8 @@ fn linger(before: &Drawing, asked: &[bool], room: u32, held: &mut [bool]) -> u32 /// kept, `NONE` where a place is free: each keeps its place in `before`, and the rest take the first /// free ones. fn keep(before: Option<&Drawing>, pinned: &[u32], held: &[bool], order: &Order) -> Vec { - let out: Vec = pinned.iter().copied().filter(|&d| d < order.len() && !held[(d / PAGE) as usize]).collect(); + let out: Vec = + pinned.iter().copied().filter(|&d| d < order.len() && !held[(d / PAGE_ROWS) as usize]).collect(); let wanted: FxHashSet = out.iter().copied().collect(); let mut kept: Vec = before.map_or_else(Vec::new, |b| b.kept.to_vec()); let mut placed: FxHashSet = FxHashSet::default(); @@ -490,7 +492,7 @@ impl Ends { if self.pages.len() <= page as usize { self.pages.resize(page as usize + 1, None); } - let rows = order.len().saturating_sub(page * PAGE).min(PAGE); + let rows = order.len().saturating_sub(page * PAGE_ROWS).min(PAGE_ROWS); match &self.pages[page as usize] { Some((had, found)) if *had == rows => found.clone(), _ => { @@ -499,7 +501,7 @@ impl Ends { for reference in ends { let t = reference.target_row(r as usize); if t != UNWIRED && t < target.len() { - found.push(target.rank(t) / PAGE); + found.push(target.rank(t) / PAGE_ROWS); } } } @@ -656,11 +658,12 @@ impl View { let mut paged = crate::data::Keyed::default(); for (key, chosen) in self.choices.iter().filter(|(key, _)| shaped(key)) { let ranges = chosen.ranges.iter().filter(|r| r.start < r.end); - let mut pages: Vec = ranges.flat_map(|r| r.start / PAGE..=(r.end - 1) / PAGE).collect(); + let mut pages: Vec = ranges.flat_map(|r| r.start / PAGE_ROWS..=(r.end - 1) / PAGE_ROWS).collect(); pages.sort_unstable(); pages.dedup(); let pinned = self.pinned(key, &chosen.order); - let kept: Vec = pinned.into_iter().filter(|&d| pages.binary_search(&(d / PAGE)).is_err()).collect(); + let kept: Vec = + pinned.into_iter().filter(|&d| pages.binary_search(&(d / PAGE_ROWS)).is_err()).collect(); paged.insert(key.clone(), crate::derive::Paged { order: chosen.order.clone(), pages, kept }); } paged @@ -677,7 +680,7 @@ impl View { /// the rows the plan gives it fill; else none past those it draws. fn room(&self, key: &(Name, Name)) -> u32 { match self.planning.plans().get(key).map(|p| p.draw) { - Some(Draw::Tiles { rows }) => rows.div_ceil(PAGE), + Some(Draw::Tiles { rows }) => rows.div_ceil(PAGE_ROWS), _ => 0, } } @@ -726,11 +729,11 @@ mod tests { #[test] fn pages_keep_their_slots_as_tiles_come_and_go() { - let order = order(8 * PAGE); + let order = order(8 * PAGE_ROWS); let first = Drawing::lay( None, &Lay { - chosen: &chosen(&order, &[0..PAGE, 3 * PAGE..4 * PAGE]), + chosen: &chosen(&order, &[0..PAGE_ROWS, 3 * PAGE_ROWS..4 * PAGE_ROWS]), pinned: &Pinned::default(), ends: &[], room: 0, @@ -739,12 +742,12 @@ mod tests { false, ); assert_eq!(&*first.slots, [0, 3]); - assert_eq!(first.items, 2 * PAGE); + assert_eq!(first.items, 2 * PAGE_ROWS); // Page 0 leaves, page 5 arrives: it takes the slot page 0 left, and page 3 stays where it was. let next = Drawing::lay( Some(&first), &Lay { - chosen: &chosen(&order, &[3 * PAGE..4 * PAGE, 5 * PAGE..6 * PAGE]), + chosen: &chosen(&order, &[3 * PAGE_ROWS..4 * PAGE_ROWS, 5 * PAGE_ROWS..6 * PAGE_ROWS]), pinned: &Pinned::default(), ends: &[], room: 0, @@ -753,14 +756,14 @@ mod tests { false, ); assert_eq!(&*next.slots, [5, 3]); - assert_eq!(next.held_of(3 * PAGE + 7), Some(PAGE + 7)); - assert_eq!(next.source(7), Some(5 * PAGE + 7)); - assert_eq!(next.item_of(PAGE + 7), Some(7), "page 3's rows draw first"); + assert_eq!(next.held_of(3 * PAGE_ROWS + 7), Some(PAGE_ROWS + 7)); + assert_eq!(next.source(7), Some(5 * PAGE_ROWS + 7)); + assert_eq!(next.item_of(PAGE_ROWS + 7), Some(7), "page 3's rows draw first"); } #[test] fn pages_no_longer_drawn_stay_held_while_there_is_room() { - let order = order(8 * PAGE); + let order = order(8 * PAGE_ROWS); let lay = |ranges: &[Range], room: u32, before: Option<&Drawing>| { Drawing::lay( before, @@ -768,20 +771,20 @@ mod tests { false, ) }; - let first = lay(&[0..PAGE, PAGE..2 * PAGE], 3, None); + let first = lay(&[0..PAGE_ROWS, PAGE_ROWS..2 * PAGE_ROWS], 3, None); assert_eq!(&*first.slots, [0, 1]); // Page 1 is no longer drawn and page 4 is: with room for three pages, page 1 stays held. - let next = lay(&[0..PAGE, 4 * PAGE..5 * PAGE], 3, Some(&first)); - assert_eq!((&*next.slots, next.items), (&[0, 1, 4][..], 2 * PAGE), "page 1 held, not drawn"); + let next = lay(&[0..PAGE_ROWS, 4 * PAGE_ROWS..5 * PAGE_ROWS], 3, Some(&first)); + assert_eq!((&*next.slots, next.items), (&[0, 1, 4][..], 2 * PAGE_ROWS), "page 1 held, not drawn"); // Drawn again, it draws from its slot: no slot takes a page of other rows. - let back = lay(&[0..PAGE, PAGE..2 * PAGE], 3, Some(&next)); + let back = lay(&[0..PAGE_ROWS, PAGE_ROWS..2 * PAGE_ROWS], 3, Some(&next)); assert_eq!(&*back.slots, [0, 1, 4]); assert_eq!(crate::view::transition::bound(Some(&next), &back), Vec::::new()); // With room for two, of the pages no longer drawn the one asked for latest stays. - let tight = lay(std::slice::from_ref(&(6 * PAGE..7 * PAGE)), 2, Some(&back)); + let tight = lay(std::slice::from_ref(&(6 * PAGE_ROWS..7 * PAGE_ROWS)), 2, Some(&back)); assert_eq!(&*tight.slots, [6, 1]); // Their words made from other data or another scene, pages no longer drawn are let go. - let ranges = [0..PAGE, 4 * PAGE..5 * PAGE]; + let ranges = [0..PAGE_ROWS, 4 * PAGE_ROWS..5 * PAGE_ROWS]; let changed = Lay { chosen: &chosen(&order, &ranges), pinned: &Pinned::default(), ends: &[], room: 3, content: 1 }; let changed = Drawing::lay(Some(&back), &changed, false); @@ -790,14 +793,20 @@ mod tests { #[test] fn tiles_fade_in_and_out_then_settle() { - let order = order(4 * PAGE); + let order = order(4 * PAGE_ROWS); let first = Drawing::lay( None, &Lay { chosen: &one(&order, 0..10), pinned: &Pinned::default(), ends: &[], room: 0, content: 0 }, true, ); assert!(!first.fades, "a first drawing has nothing to fade from"); - let lay = Lay { chosen: &one(&order, PAGE..PAGE + 10), pinned: &pinned(&[3]), ends: &[], room: 0, content: 0 }; + let lay = Lay { + chosen: &one(&order, PAGE_ROWS..PAGE_ROWS + 10), + pinned: &pinned(&[3]), + ends: &[], + room: 0, + content: 0, + }; let next = Drawing::lay(Some(&first), &lay, true); assert!(next.fades); let fades: Vec<(u32, u32)> = next.runs.iter().map(|r| (r.len, r.fade)).collect(); @@ -812,24 +821,27 @@ mod tests { #[test] fn rows_kept_lie_in_pages_of_their_own_and_ends_hold_their_pages() { - let order = order(4 * PAGE); + let order = order(4 * PAGE_ROWS); let lay = - Lay { chosen: &one(&order, 0..10), pinned: &pinned(&[2 * PAGE + 1]), ends: &[3], room: 0, content: 0 }; + Lay { chosen: &one(&order, 0..10), pinned: &pinned(&[2 * PAGE_ROWS + 1]), ends: &[3], room: 0, content: 0 }; let drawing = Drawing::lay(None, &lay, false); assert_eq!(&*drawing.slots, [0, 3, KEPT], "the page drawn, the ends', and a page of the row kept"); assert_eq!(drawing.items, 11, "ends' pages are held, not drawn"); - let held = drawing.held_of(2 * PAGE + 1).expect("kept"); - assert_eq!((held, drawing.source(held)), (2 * PAGE, Some(2 * PAGE + 1))); + let held = drawing.held_of(2 * PAGE_ROWS + 1).expect("kept"); + assert_eq!((held, drawing.source(held)), (2 * PAGE_ROWS, Some(2 * PAGE_ROWS + 1))); assert_eq!(drawing.held_pages().collect::>(), [0, 3]); // Another row kept takes the next place among them; the first keeps its own. - let more = Lay { pinned: &pinned(&[PAGE + 7, 2 * PAGE + 1]), ..lay }; + let more = Lay { pinned: &pinned(&[PAGE_ROWS + 7, 2 * PAGE_ROWS + 1]), ..lay }; let next = Drawing::lay(Some(&drawing), &more, false); - assert_eq!((next.held_of(2 * PAGE + 1), next.held_of(PAGE + 7)), (Some(2 * PAGE), Some(2 * PAGE + 1))); + assert_eq!( + (next.held_of(2 * PAGE_ROWS + 1), next.held_of(PAGE_ROWS + 7)), + (Some(2 * PAGE_ROWS), Some(2 * PAGE_ROWS + 1)) + ); assert_eq!(crate::view::transition::bound(Some(&drawing), &next), [2], "only the page of rows kept"); // A row kept whose page comes to be drawn draws from its page. - let drawn = Lay { chosen: &chosen(&order, &[0..10, 2 * PAGE..2 * PAGE + 10]), ..more }; + let drawn = Lay { chosen: &chosen(&order, &[0..10, 2 * PAGE_ROWS..2 * PAGE_ROWS + 10]), ..more }; let paged = Drawing::lay(Some(&next), &drawn, false); - assert_eq!(paged.held_of(2 * PAGE + 1), Some(paged.pages[2] * PAGE + 1)); - assert_eq!(paged.held_of(PAGE + 7), Some(2 * PAGE + 1), "the other kept where it was"); + assert_eq!(paged.held_of(2 * PAGE_ROWS + 1), Some(paged.pages[2] * PAGE_ROWS + 1)); + assert_eq!(paged.held_of(PAGE_ROWS + 7), Some(2 * PAGE_ROWS + 1), "the other kept where it was"); } } diff --git a/crates/latkit/src/view/event.rs b/crates/latkit/src/view/event.rs index 4c3e0df7..3d9c88df 100644 --- a/crates/latkit/src/view/event.rs +++ b/crates/latkit/src/view/event.rs @@ -365,7 +365,7 @@ pub struct Waiting { pub labels: bool, /// A fit due or being measured. pub fit: bool, - /// Row regions laid out again, segment counts being read back, or a transition beginning. + /// Segment counts being read back, or a transition beginning. pub marks: bool, } @@ -411,7 +411,7 @@ impl schemars::JsonSchema for Waiting { fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { schemars::json_schema!({ - "description": "Work under way a frame stands in for: pipelines the scene draws with compiling, until which the view draws nothing new (`compiling`); pages uploading or tables not yet built from them (`data`); layouts, routes, lanes, shapes, tiles, or pyramids (`derived`); a label placement or texts not yet set (`labels`); a fit due or being measured (`fit`); row regions laid out again, segment counts, or a transition beginning (`marks`). Empty once a frame is complete.", + "description": "Work under way a frame stands in for: pipelines the scene draws with compiling, until which the view draws nothing new (`compiling`); pages uploading or tables not yet built from them (`data`); layouts, routes, lanes, shapes, tiles, or pyramids (`derived`); a label placement or texts not yet set (`labels`); a fit due or being measured (`fit`); segment counts, or a transition beginning (`marks`). Empty once a frame is complete.", "type": "array", "items": { "enum": ["compiling", "data", "derived", "labels", "fit", "marks"] }, }) diff --git a/crates/latkit/src/view/fit.rs b/crates/latkit/src/view/fit.rs index ee1c1f24..0f2a6c6b 100644 --- a/crates/latkit/src/view/fit.rs +++ b/crates/latkit/src/view/fit.rs @@ -105,7 +105,8 @@ impl View { // Everything a table drawn through tiles holds frames by its rows' bounds, whatever is drawn. A // mark with no rows has nothing to frame: it holds a fit only while no mark has rows. let compiled = || self.marks.iter().filter(|m| m.compiled.is_some()); - let drawn = |m: &Shown| (m.drawable() && !m.tables.pending()) || (all && self.tiled_bounds(m).is_some()); + let staged = |m: &Shown| self.staged.get(&m.mark.style().data).is_some_and(|s| !s.is_empty()); + let drawn = |m: &Shown| (m.drawable() && !staged(m)) || (all && self.tiled_bounds(m).is_some()); let empty = |m: &Shown| m.tables.synced() && m.held() == 0; let ready = compiled().any(&drawn) && compiled().all(|m| drawn(m) || empty(m)) diff --git a/crates/latkit/src/view/focus.rs b/crates/latkit/src/view/focus.rs index 3425eb25..04e296bd 100644 --- a/crates/latkit/src/view/focus.rs +++ b/crates/latkit/src/view/focus.rs @@ -1,14 +1,15 @@ -//! Selection, hover, and the rows a fit measures, as focus words: one per held row of every mark, -//! written only where they change. The selection keeps only rows a pickable mark shows. The ends of a -//! selected or hovered edge are lit too. Each mark's lit rows are listed, as the rows it draws, for its -//! focus draw, which draws them again over everything, glowing. Hover is state either way, which -//! expressions read; whether it glows is the controls' to say. +//! Selection, hover, and the rows a fit measures, as focus words: one per held row of every mark, in +//! its pages of focus words, written only where they change. The selection keeps only rows a pickable +//! mark shows. The ends of a selected or hovered edge are lit too. Each mark's lit rows are listed, as +//! the rows it draws, for its focus draw, which draws them again over everything, glowing. Hover is +//! state either way, which expressions read; whether it glows is the controls' to say. use rustc_hash::FxHashMap; use super::View; use super::event::{Event, Hit}; use super::sync::{Source, table_of}; +use super::words::Kept; use crate::data::{Field, PAGE_ROWS, Reference}; use crate::expr::compile::Deps; use crate::gpu::shaders as wgsl; @@ -16,8 +17,11 @@ use crate::gpu::work::Queue; use crate::scene::mark::Primitive; use crate::{Row, Rows}; -/// Changed words past which the whole buffer is written at once. -const WHOLE: usize = 512; +/// Words alike between two changed ones of a page, past which they are written apart. +const GAP: u32 = 16; + +/// A focus word's mark and held row. +type At = (usize, u32); #[derive(Default)] pub(crate) struct Focus { @@ -26,8 +30,8 @@ pub(crate) struct Focus { pub(crate) hovered: Option, /// Rows a fit measures. pub(crate) fitting: Vec, - /// Words written, by index into the focus buffer. - written: FxHashMap, + /// Words written, by mark and held row. + written: FxHashMap, pub(crate) dirty: bool, /// Every mark's lit rows, laid out since they were last written. lit: Option>, @@ -49,9 +53,10 @@ pub(crate) struct Lit { } impl View { - /// Writes the focus words that changed; after `fresh`, a new buffer, every word. Channels reading - /// selection or hover evaluate again, and every mark's lit rows are laid out again. - pub(super) fn write_focus(&mut self, queue: &Queue, buffer: &wgpu::Buffer, capacity: u64, fresh: bool) { + /// Writes the focus words that changed into the pages `pages` holds; after `fresh`, pages of focus + /// words new or moved, every word. Channels reading selection or hover evaluate again, and every + /// mark's lit rows are laid out again. + pub(super) fn write_focus(&mut self, queue: &Queue, pages: &wgpu::Buffer, fresh: bool) { if fresh { self.focus.written.clear(); self.focus.dirty = true; @@ -59,7 +64,7 @@ impl View { if !std::mem::take(&mut self.focus.dirty) { return; } - let mut words: FxHashMap = FxHashMap::default(); + let mut words: FxHashMap = FxHashMap::default(); let glows = self.scene.controls.hover; let hovered = self.focus.hovered.as_ref().map(|hit| hit.row.clone()); let selection = self.focus_rows(&self.focus.selection, wgsl::SELECTED, true, true, &mut words); @@ -70,33 +75,33 @@ impl View { self.focus.selection.clone_from(&selection); self.events.tell(Event::Select { rows: Rows::group(selection), trigger: None }); } - let cleared = self.focus.written.keys().filter(|w| !words.contains_key(w)).map(|&w| (w, 0)); - let set = words.iter().filter(|(w, b)| self.focus.written.get(w) != Some(b)).map(|(&w, &b)| (w, b)); - let changes: Vec<(u32, u32)> = cleared.chain(set).filter(|&(w, _)| u64::from(w) < capacity).collect(); - if changes.len() > WHOLE { - let mut all = vec![0_u32; capacity as usize]; - for (&w, &b) in &words { - if let Some(word) = all.get_mut(w as usize) { - *word = b; + let cleared = self.focus.written.keys().filter(|at| !words.contains_key(at)).copied(); + let set = words.iter().filter(|(at, b)| self.focus.written.get(at) != Some(b)).map(|(&at, _)| at); + let mut changes: Vec = cleared.chain(set).collect(); + changes.sort_unstable(); + // A page's changed words go in runs, the words between them as they are. + for run in changes.chunk_by(|a, b| a.0 == b.0 && a.1 >> wgsl::PAGE_BITS == b.1 >> wgsl::PAGE_BITS) { + let (mark, page) = (run[0].0, run[0].1 >> wgsl::PAGE_BITS); + let Some(&block) = self.marks[mark].words.pages(Kept::Focus).blocks.get(page as usize) else { continue }; + let mut start = 0; + for end in 1..=run.len() { + if end < run.len() && run[end].1 - run[end - 1].1 < GAP { + continue; } - } - queue.write_buffer(buffer, 0, bytemuck::cast_slice(&all)); - } else { - for &(w, b) in &changes { - queue.write_buffer(buffer, u64::from(w) * 4, &b.to_le_bytes()); + let (first, last) = (run[start].1, run[end - 1].1); + let bits: Vec = (first..=last).map(|row| words.get(&(mark, row)).copied().unwrap_or(0)).collect(); + let at = block + (first & wgsl::PAGE_MASK); + queue.write_buffer(pages, u64::from(at) * 4, bytemuck::cast_slice(&bits)); + start = end; } } if !changes.is_empty() { - // Channels reading focus evaluate the blocks of the rows whose words changed; every row - // after the words were laid out again. - for shown in &mut self.marks { - let (focus, held) = (shown.region.focus, shown.held()); - let blocks: Vec = changes - .iter() - .filter(|&&(w, _)| w >= focus && w - focus < held) - .map(|&(w, _)| (w - focus) / wgsl::BLOCK_ROWS) - .collect(); - shown.dirty_rows(shown.channels_reading(Deps::FOCUS), (!fresh).then_some(&blocks[..])); + // Channels reading focus evaluate the pages of the rows whose words changed; every row + // where pages of focus words are new. + for (i, shown) in self.marks.iter_mut().enumerate() { + let pages: Vec = + changes.iter().filter(|&&(mark, _)| mark == i).map(|&(_, row)| row / wgsl::PAGE_ROWS).collect(); + shown.dirty_rows(shown.channels_reading(Deps::FOCUS), (!fresh).then_some(&pages[..])); } } // Rows lit only by hover draw no glow when it does not glow. @@ -107,27 +112,20 @@ impl View { /// Sets `bit` in the focus words of `rows` in every mark holding them (with `picked`, every /// pickable one), and with `ends` lights the ends of each edge among them; returns the rows some /// mark shows. A row its data holds stays shown while a table drawn in part holds no page of it. - fn focus_rows( - &self, - rows: &[Row], - bit: u32, - picked: bool, - ends: bool, - words: &mut FxHashMap, - ) -> Vec { + fn focus_rows(&self, rows: &[Row], bit: u32, picked: bool, ends: bool, words: &mut FxHashMap) -> Vec { let mut kept = Vec::new(); for row in rows { let Some(table) = self.data(&row.data).and_then(|d| d.table(&row.table)) else { continue }; let Some(index) = table.row(row.id.as_ref()) else { continue }; let mut shows = false; - for shown in &self.marks { + for (i, shown) in self.marks.iter().enumerate() { let style = shown.mark.style(); if (picked && !shown.mark.picks()) || style.data != row.data || style.table != row.table { continue; } shows = true; if let Some(held) = shown.held_row(index as u32) { - *words.entry(shown.region.focus + held).or_default() |= bit; + *words.entry((i, held)).or_default() |= bit; } let crate::Mark::Edges(edges) = &shown.mark else { continue }; let Some((from, to)) = edges.ends.as_ref().filter(|_| ends) else { continue }; @@ -144,31 +142,31 @@ impl View { } /// Lights the row `reference` names for row `index` in every vertex mark holding its table's. - fn light_end(&self, data: &crate::Name, reference: &Reference, index: usize, words: &mut FxHashMap) { + fn light_end(&self, data: &crate::Name, reference: &Reference, index: usize, words: &mut FxHashMap) { let Some(target) = reference.target(index) else { return }; - for shown in &self.marks { + for (i, shown) in self.marks.iter().enumerate() { let style = shown.mark.style(); if shown.kind() == crate::scene::mark::MarkKind::Vertices && style.data == *data && style.table == reference.to && let Some(held) = shown.held_row(target) { - *words.entry(shown.region.focus + held).or_default() |= wgsl::END; + *words.entry((i, held)).or_default() |= wgsl::END; } } } /// Lists every mark's drawn rows with a bit of `glowing` set for its focus draw, with the /// instances each takes. - fn lay_out_lit(&mut self, words: &FxHashMap, glowing: u32) { + fn lay_out_lit(&mut self, words: &FxHashMap, glowing: u32) { let mut all = Vec::new(); for i in 0..self.marks.len() { let shown = &self.marks[i]; - let (focus, live) = (shown.region.focus, shown.live()); + let live = shown.live(); let mut lit: Vec<(u32, u32)> = words .iter() - .filter(|&(&w, &bits)| w >= focus && w - focus < live && bits & glowing != 0) - .filter_map(|(&w, _)| Some((shown.item_of(w - focus)?, w - focus))) + .filter(|&(&(mark, held), &bits)| mark == i && held < live && bits & glowing != 0) + .filter_map(|(&(_, held), _)| Some((shown.item_of(held)?, held))) .collect(); lit.sort_unstable(); let held: Vec = lit.iter().map(|&(_, h)| h).collect(); diff --git a/crates/latkit/src/view/frame.rs b/crates/latkit/src/view/frame.rs index 71c96b4b..941a01f9 100644 --- a/crates/latkit/src/view/frame.rs +++ b/crates/latkit/src/view/frame.rs @@ -1,15 +1,17 @@ //! One frame, in order: tick the clock, settle readbacks, advance the camera, derive what marks read -//! beside their data, sync columns, stream uploads, fit rows into their regions, freeze transitions, -//! pack the program buffer, write focus and uniforms, then record the compute, shadow, label, render, -//! and probe passes into the caller's encoder. Nothing here submits. +//! beside their data, sync columns, stream uploads, give marks pages of words for their rows, freeze +//! transitions, pack the program buffer, write focus and uniforms, then record the compute, shadow, +//! label, render, and probe passes into the caller's encoder. Nothing here submits but the clearing of +//! pages handed out, which runs before anything the frame records. use web_time::Instant; use super::drawing::Drawn; use super::marks::{End, Holding, Shown}; use super::resources::{EyeSlot, MARK_STRIDE, Resources}; -use super::sync::{Inputs, Rows, Source, Synced, Window, table_of}; +use super::sync::{Inputs, Rebuilt, Rows, Source, Window, table_of}; use super::transition::{bound, holds_alike, in_place, moved}; +use super::words::Kept; use super::{Camera, Event, Frame, View, timer, uniforms}; use crate::data::Keyed; use crate::derive::order::Order; @@ -53,6 +55,7 @@ impl View { } let started = Instant::now(); self.signal.clear(); + self.short = false; let dt = self.clock.tick(frame.time); let gpu = self.gpu.clone(); let (device, queue) = (&gpu.0.device, &gpu.0.queue); @@ -68,18 +71,16 @@ impl View { self.settle(); self.advance(dt); let mut inner = gpu.0.lock(); - inner.begin_frame(self.id); + inner.begin_frame(self.id, device, queue); inner.atlas.tick(); self.settle_labels(&mut inner); self.derive(&mut inner); self.answer_arranging(); self.stream(&mut inner); - let moved = self.lay_out(encoder, &mut inner); + let fresh = self.lay_out(encoder, &mut inner); let (elapsed, travel) = self.effect_clock(); - let resources = self.resources.as_ref().expect("created"); - let (buffer, capacity) = (resources.focus().clone(), resources.focus_words()); - self.focus.dirty |= moved; - self.write_focus(queue, &buffer, capacity, moved); + let pages = inner.arena.buffer().clone(); + self.write_focus(queue, &pages, fresh); if let Some(rows) = self.focus.take_lit() { self.resources.as_mut().expect("created").write_lit(device, queue, &rows); } @@ -137,7 +138,7 @@ impl View { } for shown in &mut self.marks { shown.before = shown.held(); - shown.moved = None; + shown.words.settle(); shown.carry = false; } self.events.extend(inner.atlas.errors.drain(..).map(Event::Error)); @@ -182,9 +183,9 @@ impl View { } } - /// Lays the frame out: where every mark's rows sit, the transitions starting, the ends edges - /// attach to, fitted domains, the buffers they fit, the program, and the labels; whether rows - /// moved. + /// Lays the frame out: every mark's pages of words for its rows, the transitions starting, the + /// ends edges attach to, fitted domains, the buffers they fit, the program, and the labels; whether + /// pages of focus words are new. fn lay_out(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner) -> bool { let gpu = self.gpu.clone(); let device = &gpu.0.device; @@ -192,24 +193,88 @@ impl View { let geodesic = self.geodesic(&self.marks[i]); self.marks[i].geodesic = geodesic; } - let resources = self.resources.get_or_insert_with(|| Resources::new(device)); - resources.forget_retired(); - let moved = self.fit_rows(encoder); + self.resources.get_or_insert_with(|| Resources::new(device)); + let fresh = self.fit_words(inner); + self.fit_easing(inner); + inner.arena.clear(device, &gpu.0.queue); self.transitions(encoder, inner); self.attach_ends(); let domains = self.place_domains(); let resources = self.resources.as_mut().expect("created"); - let vertices: u64 = self - .marks - .iter() - .filter(|m| m.kind() == MarkKind::Vertices) - .map(|m| u64::from(m.region.state / wgsl::VERTEX_WORDS + m.region.capacity)) - .max() - .unwrap_or(0); - resources.fit(device, self.marks.len() as u64, domains, vertices); + resources.fit(device, self.marks.len() as u64, domains); self.pack(inner); self.lay_out_labels(inner); - moved + fresh + } + + /// Gives every mark pages of words for the rows it holds: its state, its focus words, its labels' + /// candidates, and at a vertex mark faces attach to, the normals they sum. Rows whose state words + /// took another shape evaluate again; a mark the arena has no room for now draws nothing until it + /// has. Whether pages of focus words are new. + fn fit_words(&mut self, inner: &mut Inner) -> bool { + let gpu = self.gpu.clone(); + let device = (&gpu.0.device, &gpu.0.queue); + let mut normals = vec![false; self.marks.len()]; + for shown in self.marks.iter().filter(|m| m.kind() == MarkKind::Faces) { + let Some(compiled) = &shown.compiled else { continue }; + for table in &compiled.program.ends { + if let Some(m) = super::graph::end_mark(&self.marks, &shown.mark.style().data, table) { + normals[m] = true; + } + } + } + let (mut fresh, mut short, mut changed) = (false, false, false); + for (i, shown) in self.marks.iter_mut().enumerate() { + let rows = shown.page_rows(); + let labeled = shown.compiled.as_ref().is_some_and(|c| c.drawing.label.is_some()); + let fits = [ + (Kept::State, shown.stride() + shown.extras()), + (Kept::Focus, 1), + (Kept::Candidates, if labeled { wgsl::CANDIDATE_WORDS } else { 0 }), + (Kept::Normals, if normals[i] { wgsl::NORMAL_WORDS } else { 0 }), + ]; + for (kept, width) in fits { + let had = shown.words.pages(kept).blocks.len(); + let Some(new) = shown.words.fit(kept, &rows, width, device, &mut inner.arena) else { + short = true; + continue; + }; + if new.is_empty() && had == shown.words.pages(kept).blocks.len() { + continue; + } + shown.packed = false; + changed = true; + fresh |= kept == Kept::Focus; + // Rows whose state starts over evaluate every channel again. + if kept == Kept::State { + shown.slots_bound(&new); + } + } + } + if short { + self.found_no_room(inner); + } + if changed { + self.hold_to_share(inner); + } + fresh + } + + /// Pages the view draws found no room: what lacks them draws nothing until the next frame grows + /// the arena, or where it is full plans less, while it can. + fn found_no_room(&mut self, inner: &Inner) { + self.short |= !inner.arena.full() || self.planning.overflowed(); + } + + /// Holds what the view draws to its share: what its tables pin, each page once, and the pages of + /// words its marks keep per row. + fn hold_to_share(&mut self, inner: &Inner) { + let mut pins: Vec = self.marks.iter().flat_map(|m| m.tables.pins()).collect(); + pins.sort_unstable_by_key(|key| (key.page, key.piece, key.low, key.words as u8)); + pins.dedup(); + let pages: u64 = pins.into_iter().map(|key| inner.arena.bytes_of(key)).sum(); + let words: u64 = self.marks.iter().map(|m| m.words.bytes()).sum(); + self.planning.held(pages + words); } /// Takes what landed since the last frame: probes, fits, queries, the intents and keys waiting on @@ -335,16 +400,18 @@ impl View { let gpu = self.gpu.clone(); self.lay_out_drawings(); self.sync(inner); - let uploaded = inner.arena.pump(&gpu.0.device, &gpu.0.queue, inner.frame.upload); + let uploaded = inner.arena.pump(&gpu.0.queue, inner.frame.upload); inner.frame.upload = inner.frame.upload.saturating_sub(uploaded); - self.publish(inner); + if self.publish(inner) { + self.hold_to_share(inner); + } } /// Every mark's uniform, at its dynamic offset. pub(super) fn mark_uniforms(&self) -> Vec { let mut marks = vec![0_u8; self.marks.len() * MARK_STRIDE as usize]; for (i, shown) in self.marks.iter().enumerate() { - let uniform = uniforms::mark(i, shown, self.labels.slots(i), &self.clock); + let uniform = uniforms::mark(i, shown, &self.labels, &self.clock); let at = i * MARK_STRIDE as usize; marks[at..at + size_of::()].copy_from_slice(bytemuck::bytes_of(&uniform)); } @@ -388,7 +455,8 @@ impl View { } /// Syncs every mark's columns to its data, and for a table drawn in part to the drawing replacing - /// what draws: tables whose inputs changed are rebuilt once, and wait for their pages. + /// what draws: tables whose inputs changed are rebuilt once and staged with their dataset's, which + /// reserves every page they read at once, or waits for room. fn sync(&mut self, inner: &mut Inner) { let at = self.at; let (across, columns) = self.trace_window(); @@ -400,7 +468,7 @@ impl View { orders.entry(data.clone()).or_default().push((table.clone(), drawing.order.clone())); } } - for shown in &mut self.marks { + for (i, shown) in self.marks.iter_mut().enumerate() { let Some(compiled) = &shown.compiled else { continue }; let style = shown.mark.style(); let Some(data) = self.sources.iter().find(|d| *d.name() == style.data) else { continue }; @@ -412,26 +480,44 @@ impl View { let window = Window { steps: compiled.drawing.flags & (wgsl::STEP_BEFORE | wgsl::STEP_AFTER), ..window }; let orders = orders.get(&style.data).map_or(&[][..], Vec::as_slice); let inputs = Inputs { all: &self.sources, at, window, orders }; - shown.tables.sync(&compiled.program, &source, rows, &inputs, inner); + let staged = self.staged.get(&style.data).and_then(|s| s.of(i)); + if let Some(rebuilt) = shown.tables.rebuild(&compiled.program, &source, rows, &inputs, staged, inner) { + self.staged.entry(style.data.clone()).or_default().put(i, rebuilt, inner); + } + } + let gpu = self.gpu.clone(); + let mut short = false; + for (_, staged) in self.staged.iter_mut() { + short |= !staged.reserve(inner, &gpu.0.device, &gpu.0.queue); + } + if short { + self.found_no_room(inner); } } /// Swaps in what each dataset draws once all of it is ready: every mark's rebuilt tables, and /// the drawing of each of its tables drawn in part, together, so edges never end at another - /// drawing's vertices and a data version appears whole. - fn publish(&mut self, inner: &mut Inner) { + /// drawing's vertices and a data version appears whole; and every mark has room for the pages of + /// rows new to it. Whether any tables were swapped in. + fn publish(&mut self, inner: &mut Inner) -> bool { let mut datasets: Vec = self.marks.iter().filter(|m| m.compiled.is_some()).map(|m| m.mark.style().data.clone()).collect(); datasets.sort(); datasets.dedup(); let now = self.clock.now(); + let mut published = false; for name in datasets { let marks: Vec = (0..self.marks.len()) .filter(|&i| self.marks[i].compiled.is_some() && self.marks[i].mark.style().data == name) .collect(); - if marks.iter().any(|&i| self.marks[i].tables.waiting(inner) == Synced::Waiting) { + if self.staged.get_mut(&name).is_some_and(|s| !s.ready(inner)) { + continue; + } + if !marks.iter().all(|&i| self.room_for(i, inner)) { + self.found_no_room(inner); continue; } + let mut staged = self.staged.remove(&name).unwrap_or_default(); for (key, drawn) in self.drawings.iter_mut() { if key.0 == name && let Some(next) = drawn.next.take() @@ -443,16 +529,59 @@ impl View { } } for i in marks { - self.took(i, inner); + let rebuilt = staged.take(i); + published |= rebuilt.is_some(); + self.took(i, rebuilt, inner); } + staged.release(inner); } + published + } + + /// Whether mark `i` has pages of words for the pages of rows it holds once its dataset publishes, + /// giving it those it lacks: the pages it holds stay as they are until then. + fn room_for(&mut self, i: usize, inner: &mut Inner) -> bool { + let shown = &self.marks[i]; + let style = shown.mark.style(); + let key = (style.data.clone(), style.table.clone()); + let drawing = self.drawings.get(&key).and_then(Drawn::latest); + let live = match drawing { + Some(drawing) => drawing.live(), + None => self.source(&key.0).and_then(|d| d.table(&key.1)).map_or(0, |t| t.len() as u32), + }; + // Pages new to it hold its next rows as `page_rows` will: a slot's whole page, or the table's + // rows a page at a time. + let held = live + shown.exits; + let mut rows = shown.page_rows(); + let had = rows.len(); + for p in had as u32..held.div_ceil(wgsl::PAGE_ROWS) { + let start = p * wgsl::PAGE_ROWS; + let full = drawing.is_some() && start < live; + rows.push(if full { wgsl::PAGE_ROWS } else { held.min(start + wgsl::PAGE_ROWS) - start }); + } + if rows.len() == had { + return true; + } + let labeled = shown.compiled.as_ref().is_some_and(|c| c.drawing.label.is_some()); + let fits = [ + (Kept::State, shown.stride() + shown.extras()), + (Kept::Focus, 1), + (Kept::Candidates, if labeled { wgsl::CANDIDATE_WORDS } else { 0 }), + ]; + let gpu = self.gpu.clone(); + let shown = &mut self.marks[i]; + fits.into_iter().all(|(kept, width)| { + let fitted = shown.words.fit(kept, &rows, width, (&gpu.0.device, &gpu.0.queue), &mut inner.arena); + shown.packed &= fitted.as_ref().is_some_and(Vec::is_empty); + fitted.is_some() + }) } /// Takes what mark `i` draws now its dataset is published: its rebuilt tables, its table's /// drawing, and the rows they hold. Changed columns dirty the channels reading them; rows that /// came, went, or moved evaluate every channel, and with changed static values begin a /// transition. - fn took(&mut self, i: usize, inner: &mut Inner) { + fn took(&mut self, i: usize, rebuilt: Option, inner: &mut Inner) { // A drag moves rows at once. let easing = !self.ease().is_zero() && !self.dragging(); let shown = &mut self.marks[i]; @@ -462,7 +591,7 @@ impl View { let Some(data) = self.sources.iter().find(|d| *d.name() == key.0) else { return }; let Some(table) = data.table(&key.1) else { return }; let drawing = self.drawings.get(&key).and_then(|d| d.current.clone()); - let published = shown.tables.publish(&program, geometry, inner); + let published = rebuilt.map(|rebuilt| shown.tables.publish(rebuilt, &program, geometry, inner)); let holding = Holding { origin: data.origin(), table: table.clone(), drawing: drawing.clone() }; if published.is_none() && shown.holding.as_ref().is_some_and(|h| holds_alike(h, &holding)) { return; @@ -486,7 +615,7 @@ impl View { if moved.is_some() || shown.rows != table.len() as u32 { let rewritten = old.table.len() <= table.len() && moved.as_ref().is_none_or(|(s, _)| in_place(s)); match rewritten.then(|| super::transition::rewritten(&old.table, table)).flatten() { - Some(pages) => shown.rows_rewritten(&super::marks::Blocks::of_pages(&pages)), + Some(pages) => shown.rows_rewritten(&pages), None => shown.all_dirty(), } } @@ -552,10 +681,11 @@ impl View { u64::from(domains) } - /// Points every edge and face mark's ends at the vertex marks drawing their tables. - /// The ends of rows evaluate again where their vertices moved: another vertex mark or region, - /// another order, or for rows that hold no page their ends lie in, other slots. Edges drawn in - /// part hold the pages their ends lie in, whose slots never move under them. + /// Points every edge and face mark's ends at the vertex marks drawing their tables. The ends of + /// rows evaluate again where their vertices moved: another vertex mark, another order, or for rows + /// that hold no page their ends lie in, other slots. Edges drawn in part hold the pages their ends + /// lie in, whose slots never move under them. Where only a vertex mark's pages of words changed, + /// the ends read them where they are, and nothing evaluates. fn attach_ends(&mut self) { for i in 0..self.marks.len() { let ends = ends(&self.marks, i); @@ -568,7 +698,7 @@ impl View { let closed = self.marks[i].drawing().is_some() && matches!(place, Some(Place::Ends(_))); let moved = ends.len() != was.len() || ends.iter().zip(was).any(|(a, b)| { - a.first != b.first || a.order != b.order || (!closed && (a.pages != b.pages || a.kept != b.kept)) + a.mark != b.mark || a.order != b.order || (!closed && (a.pages != b.pages || a.kept != b.kept)) }); // Edges kept find their ends among the rows kept, which move as those change: they alone // evaluate again. @@ -585,9 +715,10 @@ impl View { } } -/// Where each of mark `i`'s ends finds its vertices: the vertex mark it attaches to, its vertex 0's -/// graph slot, and where it holds the pages of the order it draws its table in, if in part. An end -/// attaches to the unnamed vertex mark drawing its table, else to the first one. +/// Where each of mark `i`'s ends finds its vertices: the vertex mark it attaches to, the pages of +/// that mark's rows' words and of the normals faces sum at them, and where it holds the pages of the +/// order it draws its table in, if in part. An end attaches to the unnamed vertex mark drawing its +/// table, else to the first one, while that mark has pages for the rows it holds. fn ends(marks: &[Shown], i: usize) -> Vec { let Some(compiled) = &marks[i].compiled else { return Vec::new() }; let data = &marks[i].mark.style().data; @@ -596,13 +727,20 @@ fn ends(marks: &[Shown], i: usize) -> Vec { .ends .iter() .map(|table| match super::graph::end_mark(marks, data, table) { - Some(m) => End { - first: marks[m].region.state / wgsl::VERTEX_WORDS, - order: marks[m].drawing().map_or(0, |d| d.order.id()), - pages: marks[m].drawing().map(|d| d.pages.clone()), - kept: marks[m].drawing().map(|d| d.kept_rows.clone()), - }, - None => End { first: wgsl::NONE, order: 0, pages: None, kept: None }, + // A vertex mark without pages for its rows draws nothing, and nothing ends at it. + Some(m) if marks[m].pages_held() => { + let (state, normals) = (marks[m].words.pages(Kept::State), marks[m].words.pages(Kept::Normals)); + End { + mark: Some(m), + state: state.blocks.clone(), + stride: state.width, + normals: (normals.width > 0).then(|| normals.blocks.clone()), + order: marks[m].drawing().map_or(0, |d| d.order.id()), + pages: marks[m].drawing().map(|d| d.pages.clone()), + kept: marks[m].drawing().map(|d| d.kept_rows.clone()), + } + } + _ => End::unattached(), }) .collect() } diff --git a/crates/latkit/src/view/labels.rs b/crates/latkit/src/view/labels.rs index 2caccc64..625ca279 100644 --- a/crates/latkit/src/view/labels.rs +++ b/crates/latkit/src/view/labels.rs @@ -1,6 +1,6 @@ -//! Labels: each labeled mark keeps, in the labels buffer, a candidate per row and a run table from -//! its text codes to where the atlas holds each text, or, until the text is set, an estimate of its -//! size. Draws place every label from its anchor each frame. Placement decides which show: it runs +//! Labels: each labeled mark keeps a candidate per row, in pages of candidates, and in the labels +//! buffer a run table from its text codes to where the atlas holds each text, or, until the text is +//! set, its size, measured from its font's characters. Draws place every label from its anchor each frame. Placement decides which show: it runs //! on the GPU when the camera, a labeled mark's state, or a run table changed, as candidates, then //! rounds of claim and decide on a grid of cells, and labels fade in and out after it. Only placed //! labels ask for their texts, so the host sets no more texts than fit on screen however many rows @@ -18,6 +18,7 @@ use super::View; use super::passes::{dispatch, offset}; use super::resources::EyeSlot; use super::sync::{Source, table_of}; +use super::words::Kept; use crate::Space; use crate::derive::{Derived, stamp_of}; use crate::gpu::Inner; @@ -43,22 +44,20 @@ const BUDGET_TIME: Duration = Duration::from_millis(4); /// Where one labeled mark's words lie in the labels buffer. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) struct Slots { - pub(crate) candidates: u32, pub(crate) runs: u32, pub(crate) wanted: u32, pub(crate) seen: u32, - pub(crate) id: u32, /// The select capping the mark's labels, or `NONE` without a cap. pub(crate) select: u32, - /// Rows and codes the slots hold. - rows: u32, + /// Codes the slots hold. codes: u32, } #[derive(Default)] pub(crate) struct Labels { - /// Each labeled mark's slots, by mark. + /// Each labeled mark's slots, by mark; and the id of its first candidate, unique across marks. slots: Vec>, + ids: Vec, /// The cell grid: where it starts, its columns, and its rows. pub(crate) cells: [u32; 3], words: u32, @@ -94,6 +93,11 @@ impl Labels { pub(crate) fn slots(&self, mark: usize) -> Option { self.slots.get(mark).copied().flatten() } + + /// The id of mark `mark`'s first candidate. + pub(crate) fn id(&self, mark: usize) -> u32 { + self.ids.get(mark).copied().unwrap_or(0) + } } impl View { @@ -198,20 +202,11 @@ impl View { }) .collect(); // Wanted codes, then seen codes, so one copy reads them all; then the cells; then each mark's - // candidates and run table. + // run table. let mut at = 0; let codes: Vec = labeled.iter().map(|&i| self.codes_of(i, inner).next_power_of_two().max(64)).collect(); for &i in &labeled { - slots[i] = Some(Slots { - candidates: 0, - runs: 0, - wanted: at, - seen: 0, - id: 0, - select: wgsl::NONE, - rows: 0, - codes: 0, - }); + slots[i] = Some(Slots { runs: 0, wanted: at, seen: 0, select: wgsl::NONE, codes: 0 }); at += 1 + wgsl::WANTED; } for (&i, &codes) in labeled.iter().zip(&codes) { @@ -225,16 +220,13 @@ impl View { let cells = [at, columns, rows]; at += columns * rows; let mut id = 0; + self.labels.ids = vec![0; self.marks.len()]; for &i in &labeled { - let mark_rows = self.marks[i].region.capacity; let s = slots[i].as_mut().expect("labeled"); - s.candidates = at; - s.rows = mark_rows; - at += mark_rows * wgsl::CANDIDATE_WORDS; s.runs = at; at += 2 * s.codes; - s.id = id; - id += mark_rows; + self.labels.ids[i] = id; + id += self.marks[i].held(); if self.marks[i].compiled.as_ref().and_then(|c| c.drawing.label.as_ref()).is_some_and(|l| l.max.is_some()) { s.select = at; at += SELECT_WORDS; @@ -266,7 +258,7 @@ impl View { } /// Writes the run table entries of each labeled mark's codes new since the last frame: where the - /// atlas holds a text, else an estimate of its size, so its label is placed before it is set. + /// atlas holds a text, else its size as measured, so its label is placed before it is set. fn estimate_runs(&mut self, inner: &mut Inner) { for i in 0..self.labels.slots.len() { let Some(slots) = self.labels.slots[i] else { continue }; @@ -285,8 +277,8 @@ impl View { } } - /// The run table words of mark `i`'s codes `codes`: where the atlas holds each text set, and an - /// estimate of the rest. + /// The run table words of mark `i`'s codes `codes`: where the atlas holds each text set, and the + /// size of the rest, as measured. fn run_words(&self, i: usize, codes: Range, inner: &mut Inner) -> Vec { let shown = &self.marks[i]; let written = self.labels.written.get(&i); @@ -299,14 +291,18 @@ impl View { let Some(table) = data.table(&style.table) else { break 'texts None }; let source = Source { data, table, derived: [None; 2], paged: Vec::new() }; let Some((d, t)) = table_of(column, &source, &self.sources) else { break 'texts None }; - Some((inner.dictionaries.field(d, &t.name, &column.name), labeling)) + let dictionary = inner.dictionaries.field(d, &t.name, &column.name); + Some(codes.clone().map(|code| dictionary.text(code).cloned()).collect::>()) + .map(|texts| (texts, labeling)) }; + let host = inner.host.clone(); let mut words = Vec::with_capacity(2 * codes.len()); - for code in codes { + for (k, code) in codes.enumerate() { let entry = match (written.and_then(|w| w.get(&code)), &texts) { (Some((_, glyphs)), _) => glyphs.corners(), - (None, Some((dictionary, labeling))) => dictionary.text(code).map_or([0, 0], |text| { - atlas::estimate(&atlas::run(text.clone(), labeling.font.clone(), labeling.size, labeling.max_width)) + (None, Some((texts, labeling))) => texts[k].as_ref().map_or([0, 0], |text| { + let run = atlas::run(text.clone(), labeling.font.clone(), labeling.size, labeling.max_width); + inner.atlas.unset(host.as_deref(), &run) }), (None, None) => [0, 0], }; @@ -315,15 +311,17 @@ impl View { words } - /// Puts estimates back in run table entries whose runs the atlas no longer holds where they were. - fn drop_evicted(&mut self, inner: &Inner) { + /// Puts measured sizes back in run table entries whose runs the atlas no longer holds where they + /// were. + fn drop_evicted(&mut self, inner: &mut Inner) { + let host = inner.host.clone(); let mut cleared = Vec::new(); for (&i, written) in &mut self.labels.written { let Some(slots) = self.labels.slots.get(i).copied().flatten() else { continue }; written.retain(|&code, (run, glyphs)| { let held = inner.atlas.holds(run, *glyphs); if !held { - cleared.push((slots.runs + 2 * code, atlas::estimate(run))); + cleared.push((slots.runs + 2 * code, inner.atlas.unset(host.as_deref(), run))); } held }); @@ -350,15 +348,14 @@ impl View { /// Starts over the labels of rows whose slots took pages of other rows: their candidates hold /// nothing placed before, and fade in from nothing. - fn clear_bound(&mut self, encoder: &mut wgpu::CommandEncoder) { - let Some(resources) = &self.resources else { return }; - let words = wgsl::BLOCK_ROWS * wgsl::CANDIDATE_WORDS; - for (i, shown) in self.marks.iter_mut().enumerate() { + fn clear_bound(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner) { + let words = u64::from(wgsl::PAGE_ROWS * wgsl::CANDIDATE_WORDS) * 4; + for shown in &mut self.marks { let bound = std::mem::take(&mut shown.bound); - let Some(slots) = self.labels.slots(i) else { continue }; - for slot in bound { - let at = u64::from(slots.candidates + slot * words) * 4; - encoder.clear_buffer(resources.labels(), at, Some(u64::from(words) * 4)); + let blocks = &shown.words.pages(Kept::Candidates).blocks; + for block in bound.iter().filter_map(|&slot| blocks.get(slot as usize)) { + inner.arena.written(); + encoder.clear_buffer(inner.arena.buffer(), u64::from(*block) * 4, Some(words)); } } } @@ -366,7 +363,7 @@ impl View { /// Places labels when what they would show changed, and fades them; reads back the texts /// placement asked for. Labels wait for what places them to compile. pub(super) fn place_labels(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner, eye: &wgsl::Eye) { - self.clear_bound(encoder); + self.clear_bound(encoder, inner); if !self.texts() { return; } @@ -388,23 +385,17 @@ impl View { if !due && !fading { return; } - let gpu = self.gpu.clone(); - let device = &gpu.0.device; - let resources = self.resources.as_mut().expect("created before labels"); if due { self.labels.placed.mark(stamp); if !diagram { self.labels.fading = Some(now + FADE + 0.05); } - for &(_, slots) in &labeled { - encoder.clear_buffer(resources.labels(), u64::from(slots.wanted) * 4, Some(4)); - encoder.clear_buffer( - resources.labels(), - u64::from(slots.seen) * 4, - Some(u64::from(slots.codes / 32) * 4), - ); - } + self.forget_asked(encoder, &labeled); } + let gpu = self.gpu.clone(); + let device = &gpu.0.device; + inner.arena.written(); + let resources = self.resources.as_mut().expect("created before labels"); let groups = resources.groups(&gpu.0, inner); let measuring = resources.pass(device, inner, Shader::Candidates); let group = resources.pass(device, inner, Shader::Labels); @@ -476,6 +467,15 @@ impl View { } } + /// Clears the texts each of `labeled` asked for and showed, before a placement asks again. + fn forget_asked(&self, encoder: &mut wgpu::CommandEncoder, labeled: &[(usize, Slots)]) { + let labels = self.resources.as_ref().expect("created before labels").labels(); + for &(_, slots) in labeled { + encoder.clear_buffer(labels, u64::from(slots.wanted) * 4, Some(4)); + encoder.clear_buffer(labels, u64::from(slots.seen) * 4, Some(u64::from(slots.codes / 32) * 4)); + } + } + /// Reads back the texts the placement of `labeled`, from `stamp`, asked for. fn read_placed( &mut self, diff --git a/crates/latkit/src/view/lists.rs b/crates/latkit/src/view/lists.rs index cdb780df..c8038dfb 100644 --- a/crates/latkit/src/view/lists.rs +++ b/crates/latkit/src/view/lists.rs @@ -158,10 +158,14 @@ impl View { let vertices: Vec<(bool, u64)> = self.marks.iter().filter(|m| m.kind() == MarkKind::Vertices).map(Shown::places_drawn).collect(); let generation = self.resources.as_ref().map_or(0, super::resources::Resources::lists_generation); + // Lanes draw as the screen shows them: they count again as the camera moves. + let projected = self.projected(); + let sight = (projected.clip_from_world.to_cols_array().map(f64::to_bits), self.size.map(f64::to_bits)); let mut due = Vec::new(); for (i, shown) in self.marks.iter_mut().enumerate() { let Some(list) = shown.segments.list.filter(|_| shown.drawable()) else { continue }; - let arcs = shown.geodesic.then(|| (&vertices, shown.places_drawn())); + let laned = shown.laned().then_some(sight); + let arcs = (shown.geodesic || laned.is_some()).then(|| (&vertices, shown.places_drawn(), laned)); let tables = shown.tables.generation(); let drawn = shown.drawing().map(|d| d.made); let stamp = stamp_of((list.header, list.items, list.kept, tables, generation, arcs, drawn)); @@ -173,13 +177,15 @@ impl View { due } - /// Reads back how many segments each curved mark's list holds, so a list too small grows: one - /// count at a time, never dropped for a newer list, which is counted once it lands. + /// Reads back how many segments each mark along great circles holds, so a list too small grows: + /// one count at a time, never dropped for a newer list, which is counted once it lands. A mark + /// bowed into lanes alone has room for every piece of every lane, and reads back nothing. pub(super) fn count_found(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &Inner) { let gpu = self.gpu.clone(); let Some(resources) = &self.resources else { return }; for shown in &mut self.marks { - let Some(list) = shown.segments.list.filter(|_| shown.curved() && shown.drawable()) else { continue }; + let geodesic = shown.curved() && shown.geodesic; + let Some(list) = shown.segments.list.filter(|_| geodesic && shown.drawable()) else { continue }; let built = shown.segments.built; if shown.segments.found.busy() || !shown.segments.found.due(built) { continue; @@ -196,7 +202,7 @@ impl Shown { /// Whether how many segments its list holds is being read back, or is due to be. pub(crate) fn counting(&self) -> bool { let found = &self.segments.found; - found.busy() || (self.curved() && self.drawable() && found.due(self.segments.built)) + found.busy() || (self.curved() && self.geodesic && self.drawable() && found.due(self.segments.built)) } } diff --git a/crates/latkit/src/view/marks.rs b/crates/latkit/src/view/marks.rs index d3482677..7f68ac91 100644 --- a/crates/latkit/src/view/marks.rs +++ b/crates/latkit/src/view/marks.rs @@ -3,9 +3,9 @@ use std::sync::Arc; use super::drawing::Drawing; -use super::rows::Region; use super::sync::Tables; use super::transition::{Begin, Transition}; +use super::words::{Kept, Words}; use crate::data::{Table, Version}; use crate::expr::compile::{Deps, Reads}; use crate::gpu::shaders as wgsl; @@ -27,6 +27,15 @@ pub(crate) struct Offsets { pub(crate) blends: u32, /// The runs of held rows it draws, `NONE` where it draws every row as itself. pub(crate) runs: u32, + /// The page table of each kind of words it keeps per row, by `Kept`; `NONE` where it keeps none. + pub(crate) pages: [u32; 6], +} + +impl Offsets { + /// Where the page table of `kept` lies, or `NONE`. + pub(crate) fn table(&self, kept: Kept) -> u32 { + self.pages[kept as usize] + } } impl Default for Offsets { @@ -41,6 +50,7 @@ impl Default for Offsets { port_info: wgsl::NONE, blends: 0, runs: wgsl::NONE, + pages: [wgsl::NONE; 6], } } } @@ -54,30 +64,40 @@ pub(crate) struct Holding { pub(crate) drawing: Option>, } -/// Where the rows an end of a mark's rows names are held: the graph slot of vertex 0 of the vertex -/// mark drawing their table, and where it holds each page of the order it draws that table in, and -/// each row it keeps whose page it does not hold, by its place in the order. +/// Where the rows an end of a mark's rows names are held: the vertex mark drawing their table, the +/// pages of its rows' words and how many words a row has, and the pages of the normals faces sum at +/// them; and where it holds each page of the order it draws that table in, and each row it keeps whose +/// page it does not hold, by its place in the order. #[derive(Clone, PartialEq, Debug)] pub(crate) struct End { - pub(crate) first: u32, + pub(crate) mark: Option, + pub(crate) state: Arc<[u32]>, + pub(crate) stride: u32, + pub(crate) normals: Option>, pub(crate) order: u64, pub(crate) pages: Option>, pub(crate) kept: Option>, } impl End { - /// Words it takes in a mark's program: two, then where pages and rows kept are held. - fn words(&self) -> usize { - 2 + self.pages.as_ref().map_or(0, |p| 2 + p.len() + 2 * self.kept.as_ref().map_or(0, |k| k.len())) + /// An end no vertex mark draws the table of. + pub(crate) fn unattached() -> End { + End { mark: None, state: Arc::new([]), stride: 0, normals: None, order: 0, pages: None, kept: None } + } + + /// Words it takes in a mark's program past its `END_WORDS`: its page table, where pages and rows + /// kept are held, and its page table of normals. + fn tables(&self) -> usize { + self.state.len() + + self.pages.as_ref().map_or(0, |p| 2 + p.len() + 2 * self.kept.as_ref().map_or(0, |k| k.len())) + + self.normals.as_ref().map_or(0, |n| n.len()) } } /// The end before end `c` of `ends` that holds its rows where it does, whose words it shares: as the /// two ends of an edge between vertices of one mark do. fn shared(ends: &[End], c: usize) -> Option { - let end = &ends[c]; - end.pages.as_ref()?; - ends[..c].iter().position(|e| e == end) + ends[..c].iter().position(|e| *e == ends[c]) } pub(crate) struct Shown { @@ -89,7 +109,7 @@ pub(crate) struct Shown { /// out. pub(crate) rows: u32, pub(crate) exits: u32, - /// Rows the region held when the frame began: what moves and freezes. + /// Rows it held when the frame began: what freezes. pub(crate) before: u32, /// What its state holds, as the tables last published lay it out. pub(crate) holding: Option, @@ -100,9 +120,8 @@ pub(crate) struct Shown { /// Each fitted domain fitted over every row of a table drawn in part: the stamp of what it read, /// and its words. pub(crate) fitted: Vec<(u64, [u32; 2])>, - /// Where its rows sit in every per-row buffer, and where they sat before this frame moved them. - pub(crate) region: Region, - pub(crate) moved: Option, + /// What it keeps per row, in pages of the arena. + pub(crate) words: Words, /// First fitted domain. pub(crate) domains: u32, /// Channels to evaluate, the row blocks they evaluate over, and whether fitted domains reduce, @@ -124,7 +143,7 @@ pub(crate) struct Shown { pub(crate) ports: Vec, /// The mark's region of the program buffer, `[start, capacity]` in words, and where its parts /// are; `packed` is false until the region holds the current words, and `written` what it holds. - pub(crate) words: Option<[u32; 2]>, + pub(crate) region: Option<[u32; 2]>, pub(crate) offsets: Offsets, pub(crate) packed: bool, pub(crate) written: Vec, @@ -168,8 +187,7 @@ impl Shown { reveal: 1.0, bound: Vec::new(), fitted: Vec::new(), - region: Region::EMPTY, - moved: None, + words: Words::default(), domains: 0, dirty: 0, blocks: Blocks::default(), @@ -181,7 +199,7 @@ impl Shown { constants: Vec::new(), ends: Vec::new(), ports: Vec::new(), - words: None, + region: None, offsets: Offsets::default(), packed: false, written: Vec::new(), @@ -265,10 +283,37 @@ impl Shown { self.live() + self.exits } - /// Whether the mark has a program, rows it holds, and columns synced at least once: a mark holding - /// only the rows the ends of edges drawn name evaluates them, and draws none. + /// Whether the mark has a program, rows it holds and pages of words for them, and columns synced + /// at least once: a mark holding only the rows the ends of edges drawn name evaluates them, and + /// draws none. pub(crate) fn drawable(&self) -> bool { - self.compiled.is_some() && self.held() > 0 && self.tables.synced() + self.compiled.is_some() && self.held() > 0 && self.tables.synced() && self.pages_held() + } + + /// Whether its pages of words per row hold every row it holds. + pub(crate) fn pages_held(&self) -> bool { + let state = self.words.pages(Kept::State); + state.width == self.stride() + self.extras() + && state.blocks.len() as u32 >= self.held().div_ceil(wgsl::PAGE_ROWS) + } + + /// Rows each page of its held rows holds: each slot's whole page of a table drawn in part, which + /// evaluates whole, or a table's own rows a page at a time; rows that left follow either. + pub(crate) fn page_rows(&self) -> Vec { + let (live, held) = (self.live(), self.held()); + let full = |start: u32| self.drawing().is_some() && start < live; + (0..held.div_ceil(wgsl::PAGE_ROWS)) + .map(|p| { + let start = p * wgsl::PAGE_ROWS; + if full(start) { wgsl::PAGE_ROWS } else { held.min(start + wgsl::PAGE_ROWS) - start } + }) + .collect() + } + + /// Lets go of what it holds on the GPU, as a mark does when it goes. + pub(crate) fn release(&mut self, gpu: &mut Inner) { + self.tables.release(gpu); + self.words.release(&mut gpu.arena); } /// Kind words per row. @@ -319,9 +364,13 @@ impl Shown { /// Whether its edges curve between their ends, along great circles or bowed into lanes: how many /// pieces each takes is found on the GPU and read back. pub(crate) fn curved(&self) -> bool { + self.path() == wgsl::PATH_ENDS && (self.geodesic || self.laned()) + } + + /// Whether its edges bow into lanes among the edges joining the same two vertices. + pub(crate) fn laned(&self) -> bool { let Some(compiled) = &self.compiled else { return false }; - let laned = compiled.drawing.lane != wgsl::NONE && self.tables.holds(compiled.drawing.lane); - self.path() == wgsl::PATH_ENDS && (self.geodesic || laned) + compiled.drawing.lane != wgsl::NONE && self.tables.holds(compiled.drawing.lane) } /// Instances its list must have room for: every path's segments, each curve's pieces as last @@ -333,7 +382,13 @@ impl Shown { if compiled.drawing.primitive == Primitive::Triangles { return (points / 3 + rows).max(1) as u32; } - let pieces = if self.curved() { (self.segments.found.value as usize).max(rows * 4) } else { 0 }; + let pieces = if !self.curved() { + 0 + } else if self.geodesic { + (self.segments.found.value as usize).max(rows * 4) + } else { + rows * wgsl::BOW_PIECES as usize + }; (points + rows + pieces).max(1) as u32 } @@ -505,11 +560,14 @@ impl Shown { + 4 * self.code.len() + self.constants.len() + (0..self.ends.len()) - .map(|c| if shared(&self.ends, c).is_some() { 2 } else { self.ends[c].words() }) + .map(|c| { + wgsl::END_WORDS as usize + if shared(&self.ends, c).is_some() { 0 } else { self.ends[c].tables() } + }) .sum::() + compiled.drawing.markers.palette.len() + self.ports.len() - + self.drawing().map_or(0, |d| 1 + (d.runs.len() + 1) * wgsl::RUN_WORDS as usize)) as u32 + + self.drawing().map_or(0, |d| 1 + (d.runs.len() + 1) * wgsl::RUN_WORDS as usize) + + self.words.len()) as u32 + self.stride() + self.extras() } @@ -562,35 +620,48 @@ impl Shown { let constants = at(out); out.extend(&self.constants); let ends = at(out); - // Two words an end, then each end's pages: how many, then each one's slot; then the rows it - // keeps: how many, then each one's place and held row. Ends holding their rows alike share - // those words. - let mut tables: Vec = Vec::with_capacity(self.ends.len()); - let mut table = ends + 2 * self.ends.len() as u32; + // `END_WORDS` an end, then each end's tables: the page table of its vertex mark's words; its + // pages, how many, then each one's slot, and the rows it keeps, how many, then each one's place + // and held row; and the page table of normals. Ends holding their rows alike share those words. + let mut heads: Vec<[u32; wgsl::END_WORDS as usize]> = Vec::with_capacity(self.ends.len()); + let mut table = ends + wgsl::END_WORDS * self.ends.len() as u32; + let mut take = |len: usize| { + let at = table; + table += len as u32; + at + }; for (c, end) in self.ends.iter().enumerate() { - out.push(end.first); - let at = match (&end.pages, shared(&self.ends, c)) { - (None, _) => wgsl::NONE, - (Some(_), Some(earlier)) => tables[earlier], - (Some(_), None) => { - let at = table; - table += (end.words() - 2) as u32; - at - } + let head = if let Some(earlier) = shared(&self.ends, c) { + heads[earlier] + } else { + let state = if end.mark.is_some() { take(end.state.len()) } else { wgsl::NONE }; + let slots = end + .pages + .as_ref() + .map_or(wgsl::NONE, |p| take(2 + p.len() + 2 * end.kept.as_ref().map_or(0, |k| k.len()))); + let normals = end.normals.as_ref().map_or(wgsl::NONE, |n| take(n.len())); + [state, end.stride, slots, normals] }; - tables.push(at); - out.push(at); + heads.push(head); + out.extend(head); } for (c, end) in self.ends.iter().enumerate() { - let Some(pages) = &end.pages else { continue }; if shared(&self.ends, c).is_some() { continue; } - out.push(pages.len() as u32); - out.extend(pages.iter()); - let kept = end.kept.as_deref().unwrap_or_default(); - out.push(kept.len() as u32); - out.extend(kept.iter().flatten()); + if end.mark.is_some() { + out.extend(end.state.iter()); + } + if let Some(pages) = &end.pages { + out.push(pages.len() as u32); + out.extend(pages.iter()); + let kept = end.kept.as_deref().unwrap_or_default(); + out.push(kept.len() as u32); + out.extend(kept.iter().flatten()); + } + if let Some(normals) = &end.normals { + out.extend(normals.iter()); + } } let palette = at(out); out.extend(&compiled.drawing.markers.palette); @@ -600,13 +671,17 @@ impl Shown { out.extend(self.blends()); let runs = if self.drawing().is_some() { at(out) } else { wgsl::NONE }; out.extend(self.runs()); - self.offsets = Offsets { columns, ranges, code, constants, ends, palette, port_info, blends, runs }; + let mut pages = [wgsl::NONE; 6]; + for (kept, blocks) in self.words.tables() { + pages[kept as usize] = at(out); + out.extend(blocks); + } + self.offsets = Offsets { columns, ranges, code, constants, ends, palette, port_info, blends, runs, pages }; } } -/// The row blocks of a mark whose rows evaluate next: every block, or those listed, the blocks of the -/// pages that changed. A block is the `BLOCK_ROWS` held rows of a held page; a page of the data holds -/// several. +/// The pages of a mark's rows that evaluate next: every page, or those listed, the pages that +/// changed. #[derive(Clone, PartialEq, Eq, Debug)] pub(crate) enum Blocks { All, @@ -619,15 +694,7 @@ impl Default for Blocks { } } -/// Blocks a page of the data holds. -const PAGE_BLOCKS: u32 = (crate::data::PAGE_ROWS / wgsl::BLOCK_ROWS as usize) as u32; - impl Blocks { - /// The blocks of the data pages `pages`. - pub(crate) fn of_pages(pages: &[u32]) -> Vec { - pages.iter().flat_map(|&p| p * PAGE_BLOCKS..(p + 1) * PAGE_BLOCKS).collect() - } - /// Adds the blocks `blocks` names, or every block for `None`. fn add(&mut self, blocks: Option<&[u32]>) { match (&mut *self, blocks) { @@ -639,7 +706,7 @@ impl Blocks { /// The blocks of a mark of `rows` rows to evaluate, in order, each once. pub(crate) fn of(&self, rows: u32) -> Vec { - let count = rows.div_ceil(wgsl::BLOCK_ROWS); + let count = rows.div_ceil(wgsl::PAGE_ROWS); match self { Blocks::All => (0..count).collect(), Blocks::Listed(listed) => { diff --git a/crates/latkit/src/view/mod.rs b/crates/latkit/src/view/mod.rs index c3f3b8a7..a8d82a59 100644 --- a/crates/latkit/src/view/mod.rs +++ b/crates/latkit/src/view/mod.rs @@ -22,7 +22,6 @@ mod program; mod query; mod resources; mod rig; -mod rows; mod shadows; mod sync; mod timer; @@ -30,6 +29,7 @@ mod transition; mod uniforms; mod variants; mod wake; +mod words; use std::sync::Arc; use std::task::Waker; @@ -107,6 +107,11 @@ pub struct View { /// What the camera chose of each table drawn in part, this frame, and what draws of each. choices: crate::derive::tiled::Choices, drawings: crate::data::Keyed<(Name, Name), drawing::Drawn>, + /// What replaces what the view draws of each dataset, its tables rebuilt, waiting for their pages. + staged: crate::data::Keyed, + /// Pages the view draws found no room this frame, and the next frame makes some: it grows the + /// arena, or plans less. + short: bool, /// What the view derives from the data and the scene: tables beside the tables it draws, row for /// row, which marks read like data. derived: Derivations, @@ -126,10 +131,8 @@ pub struct View { signal: Arc, waker: Waker, resources: Option, - /// Marks' regions of the program buffer. + /// Where each mark's words lie in the program buffer. regions: Regions, - /// Marks came or went: row regions are laid out again. - regions_dirty: bool, /// Something visible changed since the last frame. changed: bool, /// Whether the application was told the device was lost. @@ -193,6 +196,8 @@ impl View { sources: Vec::new(), choices: crate::derive::tiled::Choices::default(), drawings: crate::data::Keyed::default(), + staged: crate::data::Keyed::default(), + short: false, derived: Derivations::default(), marks: Vec::new(), camera, @@ -208,7 +213,6 @@ impl View { signal, resources: None, regions: Regions::default(), - regions_dirty: true, changed: true, told_lost: false, waiting: Waiting::default(), @@ -291,7 +295,7 @@ impl View { let before = old.iter_mut().find(|s| s.as_ref().is_some_and(|s| s.key == shown.key)); if let Some(mut before) = before.and_then(Option::take) { before.tables.release(&mut inner); - shown.inherit(&before); + shown.inherit(&mut before); if easing && shown.compiled.as_ref().map(|c| c.drawing.stride) == Some(before.stride()) { shown.begin(None); } @@ -300,12 +304,15 @@ impl View { }) .collect(); for mut dropped in old.into_iter().flatten() { - dropped.tables.release(&mut inner); + dropped.release(&mut inner); + } + // Marks moved: what they staged is built again for where they are now. + for (_, staged) in std::mem::take(&mut self.staged) { + staged.release(&mut inner); } for shown in self.marks.iter_mut().filter(|s| s.failure.is_none()) { shown.failure = absent(shown, &self.sources, &self.derived.tiles).map(|error| (error, false)); } - self.regions_dirty = true; drop(inner); if !self.camera.fits(&scene.space) { self.camera = Camera::default_for(&scene.space); @@ -358,8 +365,11 @@ impl View { } }; let context = Context { theme: &self.scene.theme, space: &self.scene.space, layout: &self.scene.layout }; - for shown in &mut self.marks { + for (i, shown) in self.marks.iter_mut().enumerate() { if shown.stale(&self.sources[at]) { + if let Some(staged) = self.staged.get_mut(&shown.mark.style().data) { + staged.forget(i, inner); + } let failed = shown.compile(&self.sources, context, inner).err(); let failed = failed.or_else(|| absent(shown, &self.sources, &self.derived.tiles)); shown.failure = failed.map(|error| (error, false)); @@ -397,11 +407,14 @@ impl View { self.drawings.retain(|key, _| key.0 != name); let gpu = self.gpu.clone(); let mut inner = gpu.0.lock(); - for shown in &mut self.marks { + for (i, shown) in self.marks.iter_mut().enumerate() { if shown.mark.style().data == *name { shown.failure = None; } if shown.compiled.as_ref().is_some_and(|c| c.schemas.iter().any(|(n, _)| n == name)) { + if let Some(staged) = self.staged.get_mut(&shown.mark.style().data) { + staged.forget(i, &mut inner); + } shown.tables.release(&mut inner); shown.compiled = None; shown.rows = 0; @@ -724,6 +737,7 @@ impl View { || (moves && self.marks.iter().any(|m| m.compiled.as_ref().is_some_and(|c| c.program.reads().intersects(Deps::TIME)))) || (moves && self.flowing()) || self.gpu.0.lock().arena.uploading() + || self.short } /// Whether the view wants a frame: one that would differ ([`needs_render`](View::needs_render)), @@ -739,12 +753,12 @@ impl View { } /// Whether the next frame, taking `input` or not, does work this one left: pages to upload past - /// the frame's budget, rows to lay out, a fit, probe, or query due, the tiles the camera wants, or - /// a transition to begin. Nothing else would wake the view for it: jobs, texts, fetches, and - /// readbacks do as they land. + /// the frame's budget, pages that found no room this frame, a fit, probe, or query due, the tiles + /// the camera wants, or a transition to begin. Nothing else would wake the view for it: jobs, + /// texts, fetches, and readbacks do as they land. fn due(&self, input: bool) -> bool { self.gpu.0.lock().arena.uploading() - || self.regions_dirty + || self.short || self.fitting.due() || (input && self.answers() && (self.probing.due() || self.queries.due())) || self.marks.iter().any(|m| m.begin.is_some()) @@ -769,11 +783,13 @@ impl View { || self.compiling.is_some() || self.answering.is_some() || self.texting.is_some(), - data: !self.marks.iter().all(given) || marks().any(|m| !m.tables.synced() || m.tables.pending()), + data: !self.marks.iter().all(given) + || marks().any(|m| !m.tables.synced()) + || self.staged.values().any(|s| !s.is_empty()), derived: !self.derived.complete(), labels: !self.labels.complete(), fit: self.fitting.due() || self.fitting.reading(), - marks: self.regions_dirty || marks().any(|m| m.counting() || m.begin.is_some()), + marks: marks().any(|m| m.counting() || m.begin.is_some()), } } @@ -793,7 +809,6 @@ impl View { } fn invalidate(&mut self) { - self.regions_dirty = true; self.regions.relayout = true; self.fitting.plot_due = true; self.wake(); @@ -847,7 +862,10 @@ impl Drop for View { let gpu = self.gpu.clone(); let mut inner = gpu.0.lock(); for shown in &mut self.marks { - shown.tables.release(&mut inner); + shown.release(&mut inner); + } + for (_, staged) in std::mem::take(&mut self.staged) { + staged.release(&mut inner); } inner.ledger.leave(self.id); } diff --git a/crates/latkit/src/view/passes.rs b/crates/latkit/src/view/passes.rs index 98619bf3..cd5da1fb 100644 --- a/crates/latkit/src/view/passes.rs +++ b/crates/latkit/src/view/passes.rs @@ -6,6 +6,7 @@ use super::resources::{EyeSlot, MARK_STRIDE}; use super::sync::{Source, table_of}; +use super::words::Kept; use super::{Frame, View}; use crate::Space; use crate::data::Field; @@ -207,7 +208,7 @@ pub(super) fn dispatch(pass: &mut wgpu::ComputePass<'_>, items: u32) { /// Dispatches over `blocks` listed row blocks of a mark of `rows` held rows: a block's rows wide. fn dispatch_blocks(pass: &mut wgpu::ComputePass<'_>, rows: u32, blocks: u32) { - let wide = rows.min(wgsl::BLOCK_ROWS).div_ceil(WORKGROUP); + let wide = rows.min(wgsl::PAGE_ROWS).div_ceil(WORKGROUP); if wide > 0 && blocks > 0 { pass.dispatch_workgroups(wide, blocks, 1); } @@ -316,15 +317,15 @@ impl super::marks::Shown { } impl View { - /// Evaluates the dirty channels of marks `work`, each over its dirty row blocks, its fitted - /// domains reduced first where they changed: into its state, bound by `state`, or while it eases - /// into its settled mirror, bound by `settling`. Leaves `state` bound. + /// Evaluates the dirty channels of marks `work`, each over its dirty pages of rows, its fitted + /// domains reduced first where they changed: into its state, or while it eases into its settled + /// words. `state` is the expression machine's pass group. fn evaluate( &mut self, pass: &mut wgpu::ComputePass<'_>, inner: &mut Inner, group: &wgpu::BindGroup, - (state, settling): (&wgpu::BindGroup, Option<&wgpu::BindGroup>), + state: &wgpu::BindGroup, work: &[usize], ) { if work.is_empty() { @@ -334,7 +335,7 @@ impl View { let step = super::resources::step; for &i in work { let shown = &mut self.marks[i]; - let into = settling.filter(|_| shown.transition.is_some()).unwrap_or(state); + let into = state; pass.set_pipeline(&inner.pipelines.compute_pipeline(device, Variant::channels(shown.reads()))); pass.set_bind_group(1, group, &[offset(i)]); let rows = shown.live(); @@ -348,7 +349,7 @@ impl View { pass.set_bind_group(2, into, &[step(wgsl::STEP_CHANNELS)]); dispatch_blocks(pass, rows, shown.block_count); let blocks = shown.blocks.of(rows); - let evaluated = blocks.iter().map(|&b| u64::from((rows - b * wgsl::BLOCK_ROWS).min(wgsl::BLOCK_ROWS))); + let evaluated = blocks.iter().map(|&b| u64::from((rows - b * wgsl::PAGE_ROWS).min(wgsl::PAGE_ROWS))); self.gpu.0.counters.add(Count::Evaluated, evaluated.sum()); shown.dirty = 0; shown.blocks = super::marks::Blocks::default(); @@ -398,16 +399,18 @@ impl View { if !measured.is_empty() { encoder.clear_buffer(resources.extents(), 0, Some(extent_bytes)); } + inner.arena.written(); if !faces.is_empty() { - encoder.clear_buffer(resources.normals(), 0, None); + // Faces sum their normals afresh into the pages of the vertex marks they attach to. + let words = u64::from(wgsl::PAGE_ROWS * wgsl::NORMAL_WORDS) * 4; + for shown in &self.marks { + for &block in shown.words.pages(Kept::Normals).blocks.iter() { + encoder.clear_buffer(inner.arena.buffer(), u64::from(block) * 4, Some(words)); + } + } } let groups = resources.groups(&gpu.0, inner); let evaluate_group = resources.pass(device, inner, Shader::Evaluate); - let settling_group = work - .iter() - .any(|&i| self.marks[i].transition.is_some()) - .then(|| resources.settling_group(device, inner)) - .flatten(); let normals_group = (!faces.is_empty()).then(|| resources.pass(device, inner, Shader::Normals)); let bounds_group = (!measured.is_empty()).then(|| resources.pass(device, inner, Shader::Bounds)); let lists_group = @@ -433,7 +436,7 @@ impl View { } pass.set_bind_group(2, &evaluate_group, &[super::resources::step(wgsl::STEP_CHANNELS)]); self.every_row(&mut pass, inner, &groups.mark, "carry", &carried); - self.evaluate(&mut pass, inner, &groups.mark, (&evaluate_group, settling_group.as_ref()), &work); + self.evaluate(&mut pass, inner, &groups.mark, &evaluate_group, &work); // After evaluating, so what a mark shows eases toward what it evaluated this frame. self.every_row(&mut pass, inner, &groups.mark, "ease", &eased); // After evaluating, so comets travel at the flow this frame shows. diff --git a/crates/latkit/src/view/plan.rs b/crates/latkit/src/view/plan.rs index f08da0fc..bff038fb 100644 --- a/crates/latkit/src/view/plan.rs +++ b/crates/latkit/src/view/plan.rs @@ -1,8 +1,13 @@ //! The plan: how each table drawn fits the budget. A table draws whole, in priority order when a mark //! drawing it gives one, unless it is too large to: denser than a screen shows, or more than the -//! view's share of the budget holds with the rest. The GPU's ledger grants each view its share, by -//! what it would draw whole. What the tables cost is worked out again only when the scene or a drawn -//! table's rows or geometry change, and the plan only when those costs or the share do. Then a table a mark places on a map or a plane +//! view's share of the budget holds with the rest. A table costs what the arena allocates for it: its +//! columns' pages and the pages of words its marks keep per row, in the blocks the arena hands out, +//! and its geometry's. The GPU's ledger grants each view its share, by what it would draw whole. What +//! the tables cost is worked out again only when the scene or a drawn table's rows or geometry change, +//! and the plan only when those costs or the share do. What tables drawn in part hold reaches past +//! what the costs foresee, as the vertices the edges drawn end at do: where what the view holds passes +//! its share, or finds no room in a full arena, it plans them within a smaller part of its share from +//! then on, until the share changes; so what it draws and what replaces it fit, with room to cache. Then a table a mark places on a map or a plane //! draws through tiles, the rows of the tiles the camera asks for, as many as its share holds, and //! edges ending at it draw through tiles too, so they show with their ends; a table a mark traces //! draws whole, its traces spanning as few pixel columns as fit; any other table draws the rows that @@ -14,12 +19,14 @@ use rustc_hash::{FxHashMap, FxHasher}; use super::View; use super::marks::Shown; -use crate::data::{Field, Geometry, Keyed, Name, Table}; +use crate::data::{Field, Keyed, Name, Table}; use crate::derive::tiled::{Draw, Place, Planned, Plans}; use crate::derive::{Derived, stamp_of}; use crate::expr::compile::Words; +use crate::gpu::arena::{PIECE_POINTS, path_points, rounded}; use crate::gpu::shaders as wgsl; use crate::gpu::{Ask, Ledger}; +use crate::scene::mark::MarkKind; use crate::{Expr, Kind, Mark, Space}; /// Rows past which a table a mark places draws through tiles: more than a screen shows one to a cell. @@ -34,18 +41,23 @@ const TRACE_COLUMN_BYTES: f64 = 16.0; /// Pixel columns a trace spans at least, however little room is left. const TRACE_COLUMNS: u32 = 32; -/// What a table costs: its rows, bytes each row takes on the GPU, bytes its geometry takes, and bytes -/// its traces take for each pixel column they span. +/// What a table costs, as the arena allocates it: its rows, bytes a page of them takes, bytes its +/// geometry takes, and bytes its traces take for each pixel column they span. struct Cost { rows: usize, - per_row: f64, + per_page: f64, geometry: f64, traced: f64, } impl Cost { fn bytes(&self) -> f64 { - self.rows as f64 * self.per_row + self.geometry + self.rows.div_ceil(wgsl::PAGE_ROWS as usize) as f64 * self.per_page + self.geometry + } + + /// Bytes a row takes on average. + fn per_row(&self) -> f64 { + self.per_page / f64::from(wgsl::PAGE_ROWS) + self.geometry / self.rows.max(1) as f64 } } @@ -60,11 +72,20 @@ struct Costs { } /// The plan, and the costs it weighs, each worked out again only when what it reads changes. -#[derive(Default)] pub(crate) struct Planning { costs: Derived, /// How each table draws, and the pixel columns a trace spans at most. plan: Derived<(Plans, u32)>, + /// The part of the room left for tables drawn in part that the plan gives them: lowered each time + /// what they held found no room in a full arena, until the share changes. + fit: f64, + share: f64, +} + +impl Default for Planning { + fn default() -> Planning { + Planning { costs: Derived::default(), plan: Derived::default(), fit: 1.0, share: 0.0 } + } } impl Planning { @@ -77,6 +98,30 @@ impl Planning { pub(crate) fn trace_columns(&self) -> u32 { self.plan.value.1 } + + /// What tables drawn in part held found no room in a full arena: they draw within less, while + /// any draws more than the least it may. Whether the plan draws less. + pub(crate) fn overflowed(&mut self) -> bool { + self.tighten(0.75) + } + + /// The view holds `bytes` for what it draws: past its share, tables drawn in part draw within as + /// much less as it passes the share by. + pub(crate) fn held(&mut self, bytes: u64) { + if self.share > 0.0 && bytes as f64 > self.share { + self.tighten(self.share / bytes as f64); + } + } + + /// Plans tables drawn in part within `part` of what it gave them, while any draws more than the + /// least it may: whether it does. + fn tighten(&mut self, part: f64) -> bool { + let shrinks = self.plans().values().any(|p| matches!(p.draw, Draw::Tiles { rows } if rows > LEAST)); + if shrinks { + self.fit *= part; + } + shrinks + } } impl View { @@ -97,9 +142,12 @@ impl View { shared: costs.shared.iter().map(|(piece, (_, bytes))| (*piece, *bytes)).collect(), }; let share = ledger.grant(self.id, whole, ask, budget); - let stamp = stamp_of((self.planning.costs.stamp(), share.to_bits())); + if share != self.planning.share { + (self.planning.share, self.planning.fit) = (share, 1.0); + } + let stamp = stamp_of((self.planning.costs.stamp(), share.to_bits(), self.planning.fit.to_bits())); if self.planning.plan.due(stamp) { - let planned = self.plan(costs, share); + let planned = self.plan(costs, share, self.planning.fit); self.planning.plan.make(stamp, planned); } } @@ -121,8 +169,9 @@ impl View { } /// How each table the scene draws draws, within `share` bytes, from what `costs` says each - /// costs, and the pixel columns a trace spans at most. - fn plan(&self, costs: &Costs, share: f64) -> (Plans, u32) { + /// costs, tables drawn in part within `fit` of the room left them; and the pixel columns a trace + /// spans at most. + fn plan(&self, costs: &Costs, share: f64, fit: f64) -> (Plans, u32) { let (mut plans, costs) = (costs.whole.clone(), &costs.tables); let tileable = |key: &(Name, Name), planned: &Planned| planned.place != Place::Nowhere && !self.derived.tiles.owns(&key.0); @@ -176,11 +225,11 @@ impl View { } // Tables drawn through tiles, and traces, share what is left. let traced: f64 = costs.values().map(|c| c.traced).sum(); - let left = (share - whole).max(share / 4.0); + let left = (share - whole).max(share / 4.0) * fit; let each = left / (tiled.len() + usize::from(traced > 0.0)).max(1) as f64; for key in &tiled { // A row drawn in part takes a word more: which held row it is. - let per_row = costs[key].per_row.max(1.0) + 4.0 + costs[key].geometry / costs[key].rows.max(1) as f64; + let per_row = costs[key].per_row().max(1.0) + 4.0; let rows = (each / per_row).min(f64::from(u32::MAX)) as u32; plans.get_mut(key).expect("listed").draw = Draw::Tiles { rows: rows.max(LEAST) }; } @@ -193,9 +242,17 @@ impl View { fn costs(&self) -> Costs { let space = &self.scene.space; let placing = matches!(space, Space::Earth(_) | Space::Plane(_)); + let easing = !self.scene.ease.is_zero(); let mut plans = Plans::default(); let mut costs: Keyed<(Name, Name), Cost> = Keyed::default(); let mut shared: FxHashMap = FxHashMap::default(); + // The tables faces end at keep the normals faces sum at their rows. + let mut summed: Vec<(Name, Name)> = Vec::new(); + for shown in self.marks.iter().filter(|m| m.kind() == MarkKind::Faces) { + let Some(compiled) = &shown.compiled else { continue }; + let data = &shown.mark.style().data; + summed.extend(compiled.program.ends.iter().map(|table| (data.clone(), table.clone()))); + } for shown in &self.marks { let style = shown.mark.style(); let Some(table) = self.source(&style.data).and_then(|d| d.table(&style.table)) else { continue }; @@ -211,8 +268,10 @@ impl View { if placing && planned.place == Place::Nowhere { planned.place = place_of(&shown.mark, table); } - let cost = costs.entry(key).or_insert(Cost { rows: table.len(), per_row: 0.0, geometry: 0.0, traced: 0.0 }); - cost.per_row += row_bytes(shown); + let normals = shown.kind() == MarkKind::Vertices && summed.contains(&key); + let cost = + costs.entry(key).or_insert(Cost { rows: table.len(), per_page: 0.0, geometry: 0.0, traced: 0.0 }); + cost.per_page += page_bytes(shown, easing, normals); cost.geometry += geometry_bytes(shown, table); cost.traced += traced_bytes(shown, table); for (piece, bytes) in self.shared_pages(shown, table) { @@ -225,7 +284,7 @@ impl View { } /// The pages `shown` reads of `table`, and of the tables it joins, that every view drawing the - /// same data shares, as `row_bytes` and `geometry_bytes` count them: each field's, by its version + /// same data shares, as `page_bytes` and `geometry_bytes` count them: each field's, by its version /// and how it is read. fn shared_pages(&self, shown: &Shown, table: &Table) -> Vec<(u64, f64)> { let Some(compiled) = &shown.compiled else { return Vec::new() }; @@ -239,8 +298,8 @@ impl View { None => table.field_named(&c.name), Some((data, joined)) => self.source(data)?.table(joined)?.field_named(&c.name), }; - let width = if c.low { 8.0 } else { 4.0 }; - Some((stamp_of((read?.version(), c.low)), table.len() as f64 * width)) + let pages = table.len().div_ceil(wgsl::PAGE_ROWS as usize) as f64; + Some((stamp_of((read?.version(), c.low)), pages * column_page_bytes(c.low))) }) .collect(); if let Some(field) = geometry_of(&shown.mark).and_then(|f| table.field_named(&f)) { @@ -290,15 +349,29 @@ fn geometry_of(mark: &Mark) -> Option { } } -/// Bytes a row of `shown` takes on the GPU: its state and the frozen mirror a transition keeps, its -/// focus word, a word for each column it reads and two for a position's, and its label's candidate. -fn row_bytes(shown: &Shown) -> f64 { +/// Bytes of the block a page of `width` words per row takes. +fn block_bytes(width: u32) -> f64 { + f64::from(rounded(wgsl::PAGE_ROWS * width)) * 4.0 +} + +/// Bytes a page of a column's rows takes: its values, and its low parts where it reads them. +fn column_page_bytes(low: bool) -> f64 { + block_bytes(1) * if low { 2.0 } else { 1.0 } +} + +/// Bytes a page of `shown`'s rows takes, as the arena allocates them: a page of each column it reads, +/// and of each kind of words it keeps per row: its state, its focus words, its labels' candidates, at +/// a vertex mark faces attach to the normals they sum, and where the scene is `easing`, the frozen +/// and settled state a transition keeps. +fn page_bytes(shown: &Shown, easing: bool, normals: bool) -> f64 { let Some(compiled) = &shown.compiled else { return 0.0 }; - let state = f64::from(compiled.drawing.stride + compiled.drawing.extras) * 8.0; let columns: f64 = - compiled.program.columns.iter().filter(|c| c.words != Words::Path).map(|c| if c.low { 8.0 } else { 4.0 }).sum(); - let label = if compiled.drawing.label.is_some() { f64::from(wgsl::CANDIDATE_WORDS) * 4.0 } else { 0.0 }; - state + 4.0 + columns + label + compiled.program.columns.iter().filter(|c| c.words != Words::Path).map(|c| column_page_bytes(c.low)).sum(); + let state = block_bytes(compiled.drawing.stride + compiled.drawing.extras); + let labels = if compiled.drawing.label.is_some() { block_bytes(wgsl::CANDIDATE_WORDS) } else { 0.0 }; + let normals = if normals { block_bytes(wgsl::NORMAL_WORDS) } else { 0.0 }; + let eased = if easing { 2.0 * state } else { 0.0 }; + columns + state + block_bytes(1) + labels + normals + eased } /// Bytes the traces `shown` draws of `table` take for each pixel column they span: a row's for each @@ -311,12 +384,19 @@ fn traced_bytes(shown: &Shown, table: &Table) -> f64 { traced.count() as f64 * table.len() as f64 * TRACE_COLUMN_BYTES } -/// Bytes the geometry `shown` draws of `table` takes: its points, and an area's triangles about -/// three times as many. +/// Bytes the geometry `shown` draws of `table` takes, as the arena allocates it: for each page of +/// rows, a block of their first points, then their points a chunk at a time; an area's triangles take +/// about three times the points of its outlines. fn geometry_bytes(shown: &Shown, table: &Table) -> f64 { let Some(field) = geometry_of(&shown.mark) else { return 0.0 }; let Some(Field::Values(column, _)) = table.field_named(&field) else { return 0.0 }; - let points: usize = column.pages().iter().filter_map(|p| p.values.geometry()).map(Geometry::coords_spanned).sum(); - let scale = if matches!(shown.mark, Mark::Areas(_)) { 3.0 } else { 1.0 }; - points as f64 * 8.0 * scale + let scale = if matches!(shown.mark, Mark::Areas(_)) { 3 } else { 1 }; + let page = |values: &crate::data::Values| { + let points = path_points(values) * scale; + let (whole, rest) = (points / PIECE_POINTS, points % PIECE_POINTS); + let pieces = whole as u64 * u64::from(rounded(2 * PIECE_POINTS as u32)) + + if rest > 0 { u64::from(rounded(2 * rest as u32)) } else { 0 }; + (u64::from(rounded(values.len() as u32)) + pieces) as f64 * 4.0 + }; + column.pages().iter().map(|p| page(&p.values)).sum() } diff --git a/crates/latkit/src/view/program.rs b/crates/latkit/src/view/program.rs index 33e46495..553e1b56 100644 --- a/crates/latkit/src/view/program.rs +++ b/crates/latkit/src/view/program.rs @@ -59,11 +59,11 @@ impl View { if !self.regions.relayout { for shown in self.marks.iter_mut().filter(|m| !m.packed && m.compiled.is_some()) { let len = shown.packed_len(); - let start = match shown.words { + let start = match shown.region { Some([start, room]) if len <= room => start, _ if self.regions.end + room(len) <= capacity => { let start = self.regions.end; - shown.words = Some([start, room(len)]); + shown.region = Some([start, room(len)]); shown.written.clear(); self.regions.end += room(len); start @@ -93,7 +93,7 @@ impl View { shown.pack(start, &mut words); shown.written = words[start as usize..].to_vec(); words.resize((start + room(len)) as usize, 0); - shown.words = Some([start, room(len)]); + shown.region = Some([start, room(len)]); shown.packed = true; } self.regions.end = words.len() as u32; diff --git a/crates/latkit/src/view/resources.rs b/crates/latkit/src/view/resources.rs index d01c3a54..0d6f5345 100644 --- a/crates/latkit/src/view/resources.rs +++ b/crates/latkit/src/view/resources.rs @@ -1,15 +1,14 @@ //! A view's buffers, textures, and bind groups. Grown buffers grow by half again; replacing one bumps //! the generation, and bind groups are rebuilt when a generation they were built from changes. //! -//! Every shader's pass group (group 2) binds by one convention: 0 state, 1 frozen state, 2 extents, -//! 3 normals, 4 domains, 5 probe words and what queries find, 6 depth, 7 sprites, 8 the text atlas, -//! 9 lists, 10 the shadow map, 11 a level of it, 12 remap words, 13 labels, 14 the settled state, -//! 15 icons, 16 row blocks, 17 the probe's step. +//! Every shader's pass group (group 2) binds by one convention: 0 the pages (the data's, and what +//! marks keep per row), 2 extents, 4 domains, 5 probe words and what queries find, 6 depth, 7 sprites, +//! 8 the text atlas, 9 lists, 10 the shadow map, 11 a level of it, 12 remap words, 13 labels, +//! 15 icons, 16 the pages of rows each mark evaluates, 17 the probe's step. use std::collections::HashMap; use std::num::NonZeroU64; -use super::rows::Sizes; use crate::gpu::shaders::{self as wgsl, Shader}; use crate::gpu::work::Queue; use crate::gpu::{Inner, Shared}; @@ -57,6 +56,10 @@ fn steps(device: &wgpu::Device) -> wgpu::Buffer { }) } +/// What the labels buffer is used as: atomics placement works in, and a copy's source when the texts +/// placement asks for are read back. +const LABELS: wgpu::BufferUsages = wgpu::BufferUsages::STORAGE.union(wgpu::BufferUsages::COPY_SRC); + /// A storage buffer that grows by half again when it runs out of room. struct Grown { buffer: wgpu::Buffer, @@ -88,8 +91,6 @@ fn create(device: &wgpu::Device, label: &str, usage: wgpu::BufferUsages, bytes: }) } -const ROWS: wgpu::BufferUsages = wgpu::BufferUsages::STORAGE.union(wgpu::BufferUsages::COPY_SRC); - /// The bind groups every pipeline shares. #[derive(Clone)] pub(crate) struct Groups { @@ -139,43 +140,24 @@ impl ShadowMap { } } -/// Buffers a layout of rows replaced, kept until their rows are copied out. -pub(crate) struct OldRows { - pub(crate) state: wgpu::Buffer, - pub(crate) frozen: Option, - pub(crate) settled: Option, -} - pub(crate) struct Resources { pub(crate) frame: wgpu::Buffer, eyes: wgpu::Buffer, marks: Grown, program: Grown, - focus: wgpu::Buffer, /// The rows each mark's focus draw lights. lit: Grown, - state: wgpu::Buffer, - frozen: Option, - /// The state's mirror that channels of marks easing toward a change store into, while one does: - /// where their rows settle. - settled: Option, - retired: Option, - /// The row blocks each mark's evaluation covers this frame. + /// The pages of rows each mark's evaluation covers this frame. blocks: Grown, remap: Grown, domains: Grown, extents: Grown, - normals: Grown, /// The probe's words, then what the running query finds. probe: Grown, steps: wgpu::Buffer, sprites: Grown, lists: Grown, labels: wgpu::Buffer, - empty: wgpu::Buffer, - /// What the settled state binds without one: writable, so it never shares a buffer bound - /// read-only. - scratch: wgpu::Buffer, pub(crate) shadows: ShadowMap, /// Changes whenever one of the buffers or textures above is replaced; and whenever the lists /// buffer is, emptied. @@ -183,9 +165,9 @@ pub(crate) struct Resources { lists_generation: u64, /// Built from (arena, colormaps, icons, own) generations. groups: Option<((u64, u64, u64, u64), Groups)>, - /// Pass groups, built at `passes_generation`. + /// Pass groups, built at `passes_generation`: the arena's, the icons', and its own. passes: HashMap, - passes_generation: (u64, u64), + passes_generation: (u64, u64, u64), targets: Option, } @@ -198,17 +180,11 @@ impl Resources { eyes: create(device, "latkit eyes", uniform, 2 * MARK_STRIDE), marks: Grown::new(device, "latkit marks", uniform), program: Grown::new(device, "latkit program", storage), - focus: create(device, "latkit focus", storage, 256), lit: Grown::new(device, "latkit lit", storage), - state: create(device, "latkit state", ROWS, 256), - frozen: None, - settled: None, - retired: None, blocks: Grown::new(device, "latkit blocks", storage), remap: Grown::new(device, "latkit remap", storage), domains: Grown::new(device, "latkit domains", storage), extents: Grown::new(device, "latkit extents", storage | wgpu::BufferUsages::COPY_SRC), - normals: Grown::new(device, "latkit normals", storage), probe: Grown::new(device, "latkit probe", storage | wgpu::BufferUsages::COPY_SRC), steps: steps(device), sprites: Grown::new(device, "latkit sprites", storage), @@ -217,87 +193,18 @@ impl Resources { "latkit lists", storage | wgpu::BufferUsages::INDIRECT | wgpu::BufferUsages::COPY_SRC, ), - labels: create(device, "latkit labels", ROWS, 256), - empty: create(device, "latkit empty", ROWS, 256), - scratch: create(device, "latkit scratch", ROWS, 256), + labels: create(device, "latkit labels", LABELS, 256), shadows: ShadowMap::new(device, 1), generation: 0, lists_generation: 0, groups: None, passes: HashMap::new(), - passes_generation: (u64::MAX, 0), + passes_generation: (u64::MAX, 0, 0), targets: None, } } - /// Lays rows out into new buffers of `sizes`, returning the buffers they replaced so their rows - /// can be copied over; the frozen mirror, while one exists, is replaced alike. - pub(crate) fn replace_rows(&mut self, device: &wgpu::Device, sizes: Sizes) -> OldRows { - let words = |n: u64| (n + 1) * 4; - let state = create(device, "latkit state", ROWS, words(sizes.state)); - self.focus = create(device, "latkit focus", wgpu::BufferUsages::STORAGE, words(sizes.focus)); - let frozen = self.frozen.as_ref().map(|_| create(device, "latkit frozen", ROWS, words(sizes.state))); - let settled = self.settled.as_ref().map(|_| create(device, "latkit settled", ROWS, words(sizes.state))); - let old = OldRows { - state: std::mem::replace(&mut self.state, state), - frozen: std::mem::replace(&mut self.frozen, frozen), - settled: std::mem::replace(&mut self.settled, settled), - }; - self.generation += 1; - old - } - - /// Keeps the buffers rows moved out of until the frame that reads them is submitted. - pub(crate) fn retire(&mut self, old: OldRows) { - self.retired = Some(old); - } - - /// Drops buffers a previous frame retired. - pub(crate) fn forget_retired(&mut self) { - self.retired = None; - } - - pub(crate) fn retired_state(&self) -> Option<&wgpu::Buffer> { - self.retired.as_ref().map(|old| &old.state) - } - - /// Makes the frozen mirror of the state buffer exist. - pub(crate) fn ensure_frozen(&mut self, device: &wgpu::Device) { - if self.frozen.is_none() { - self.frozen = Some(create(device, "latkit frozen", ROWS, self.state.size())); - self.generation += 1; - } - } - - pub(crate) fn drop_frozen(&mut self) { - if self.frozen.take().is_some() { - self.generation += 1; - } - } - - pub(crate) fn frozen(&self) -> Option<&wgpu::Buffer> { - self.frozen.as_ref() - } - - /// Makes the settled mirror of the state buffer exist. - pub(crate) fn ensure_settled(&mut self, device: &wgpu::Device) { - if self.settled.is_none() { - self.settled = Some(create(device, "latkit settled", ROWS, self.state.size())); - self.generation += 1; - } - } - - pub(crate) fn drop_settled(&mut self) { - if self.settled.take().is_some() { - self.generation += 1; - } - } - - pub(crate) fn settled(&self) -> Option<&wgpu::Buffer> { - self.settled.as_ref() - } - - /// Writes the row blocks every mark's evaluation covers this frame. + /// Writes the pages of rows every mark's evaluation covers this frame. pub(crate) fn write_blocks(&mut self, device: &wgpu::Device, queue: &Queue, blocks: &[u32]) { if self.blocks.ensure(device, (blocks.len() as u64 + 1) * 4) { self.generation += 1; @@ -307,31 +214,6 @@ impl Resources { } } - /// Copies `rows` rows of a region's kind and extra words from `source` (the state buffer when - /// `None`) at `from` into the frozen mirror at `to`. - pub(crate) fn copy_frozen( - &self, - encoder: &mut wgpu::CommandEncoder, - source: Option<&wgpu::Buffer>, - from: super::rows::Region, - to: super::rows::Region, - rows: u32, - ) { - let Some(frozen) = &self.frozen else { return }; - let source = source.unwrap_or(&self.state); - let mut copy = |a: u32, b: u32, words: u32| { - if words > 0 { - encoder.copy_buffer_to_buffer(source, u64::from(a) * 4, frozen, u64::from(b) * 4, u64::from(words) * 4); - } - }; - if from.stride == to.stride { - copy(from.state, to.state, rows * to.stride); - } - if from.extras == to.extras { - copy(from.extra, to.extra, rows * to.extras); - } - } - /// Writes the rows every mark's focus draw lights. pub(crate) fn write_lit(&mut self, device: &wgpu::Device, queue: &Queue, rows: &[u32]) { if self.lit.ensure(device, (rows.len() as u64 + 1) * 4) { @@ -349,14 +231,13 @@ impl Resources { queue.write_buffer(&self.remap.buffer, 0, bytemuck::cast_slice(words)); } - /// Makes room for every mark's uniforms, domains, extents, and normals for `vertices` slots. - pub(crate) fn fit(&mut self, device: &wgpu::Device, marks: u64, domains: u64, vertices: u64) { + /// Makes room for every mark's uniforms, domains, and extents. + pub(crate) fn fit(&mut self, device: &wgpu::Device, marks: u64, domains: u64) { let words = |n: u64| (n + 1) * 4; let count = marks + 1; let mut replaced = self.domains.ensure(device, words(domains)); replaced |= self.extents.ensure(device, words(u64::from(wgsl::EXTENT_WORDS) * count)); replaced |= self.marks.ensure(device, MARK_STRIDE * count); - replaced |= self.normals.ensure(device, words(3 * vertices)); if replaced { self.generation += 1; } @@ -396,7 +277,7 @@ impl Resources { /// Replaces the labels buffer with `words` of zeros. pub(crate) fn replace_labels(&mut self, device: &wgpu::Device, words: u64) { - self.labels = create(device, "latkit labels", ROWS, (words + 1) * 4); + self.labels = create(device, "latkit labels", LABELS, (words + 1) * 4); self.generation += 1; } @@ -428,18 +309,6 @@ impl Resources { &self.marks.buffer } - pub(crate) fn focus(&self) -> &wgpu::Buffer { - &self.focus - } - - pub(crate) fn focus_words(&self) -> u64 { - self.focus.size() / 4 - } - - pub(crate) fn state(&self) -> &wgpu::Buffer { - &self.state - } - pub(crate) fn domains(&self) -> &wgpu::Buffer { &self.domains.buffer } @@ -448,10 +317,6 @@ impl Resources { &self.extents.buffer } - pub(crate) fn normals(&self) -> &wgpu::Buffer { - &self.normals.buffer - } - pub(crate) fn probe(&self) -> &wgpu::Buffer { &self.probe.buffer } @@ -486,13 +351,11 @@ impl Resources { layout: &inner.pipelines.view_layout, entries: &[ entry(0, self.frame.as_entire_binding()), - entry(1, inner.arena.buffer().as_entire_binding()), - entry(2, self.program.buffer.as_entire_binding()), - entry(3, self.focus.as_entire_binding()), - entry(4, wgpu::BindingResource::TextureView(&inner.colormaps.view)), - entry(5, wgpu::BindingResource::Sampler(&gpu.sampler)), - entry(6, wgpu::BindingResource::Buffer(eye)), - entry(7, self.lit.buffer.as_entire_binding()), + entry(1, self.program.buffer.as_entire_binding()), + entry(2, wgpu::BindingResource::TextureView(&inner.colormaps.view)), + entry(3, wgpu::BindingResource::Sampler(&gpu.sampler)), + entry(4, wgpu::BindingResource::Buffer(eye)), + entry(5, self.lit.buffer.as_entire_binding()), ], }); let binding = wgpu::BufferBinding { @@ -512,7 +375,7 @@ impl Resources { /// `shader`'s pass group, binding what its WGSL declares. pub(crate) fn pass(&mut self, device: &wgpu::Device, inner: &mut Inner, shader: Shader) -> wgpu::BindGroup { - let generation = (self.generation, inner.icons.generation); + let generation = (inner.arena.generation, inner.icons.generation, self.generation); if self.passes_generation != generation { self.passes.clear(); self.passes_generation = generation; @@ -520,26 +383,11 @@ impl Resources { if let Some(group) = self.passes.get(&shader) { return group.clone(); } - let group = self.bind(device, inner, shader, &self.state); + let group = self.bind(device, inner, shader); self.passes.insert(shader, group.clone()); group } - /// The expression machine's group for a mark easing: its channels store into the settled mirror - /// where the state binds, and the writable stand-in binds where the mirror does. - pub(crate) fn settling_group(&self, device: &wgpu::Device, inner: &mut Inner) -> Option { - let settled = self.settled.as_ref()?; - Some(self.bind_into(device, inner, Shader::Evaluate, settled, &self.scratch)) - } - - /// The freeze pass's group: state read from the buffer rows moved out of when they moved. - pub(crate) fn freeze_group(&mut self, device: &wgpu::Device, inner: &mut Inner, moved: bool) -> wgpu::BindGroup { - match self.retired.as_ref().filter(|_| moved) { - Some(old) => self.bind(device, inner, Shader::Freeze, &old.state), - None => self.pass(device, inner, Shader::Freeze), - } - } - /// A shadow map level's group, reading the level below it. pub(crate) fn level_group(&self, device: &wgpu::Device, inner: &mut Inner, below: usize) -> wgpu::BindGroup { let layout = inner.pipelines.pass_layout(device, Shader::Shade); @@ -553,19 +401,8 @@ impl Resources { }) } - fn bind(&self, device: &wgpu::Device, inner: &mut Inner, shader: Shader, state: &wgpu::Buffer) -> wgpu::BindGroup { - self.bind_into(device, inner, shader, state, self.settled.as_ref().unwrap_or(&self.scratch)) - } - - /// `shader`'s pass group, `state` bound as the state and `settled` as the settled mirror. - fn bind_into( - &self, - device: &wgpu::Device, - inner: &mut Inner, - shader: Shader, - state: &wgpu::Buffer, - settled: &wgpu::Buffer, - ) -> wgpu::BindGroup { + /// `shader`'s pass group, binding what its WGSL declares. + fn bind(&self, device: &wgpu::Device, inner: &mut Inner, shader: Shader) -> wgpu::BindGroup { let layout = inner.pipelines.pass_layout(device, shader); let entries: Vec> = shader .source() @@ -573,10 +410,8 @@ impl Resources { .iter() .map(|binding| { let resource = match binding.slot { - 0 => state.as_entire_binding(), - 1 => self.frozen.as_ref().unwrap_or(&self.empty).as_entire_binding(), + 0 => inner.arena.buffer().as_entire_binding(), 2 => self.extents.buffer.as_entire_binding(), - 3 => self.normals.buffer.as_entire_binding(), 4 => self.domains.buffer.as_entire_binding(), 5 => self.probe.buffer.as_entire_binding(), 6 => wgpu::BindingResource::TextureView( @@ -588,7 +423,6 @@ impl Resources { 10 => wgpu::BindingResource::TextureView(&self.shadows.view), 12 => self.remap.buffer.as_entire_binding(), 13 => self.labels.as_entire_binding(), - 14 => settled.as_entire_binding(), 15 => wgpu::BindingResource::TextureView(&inner.icons.view), 16 => self.blocks.buffer.as_entire_binding(), 17 => wgpu::BindingResource::Buffer(wgpu::BufferBinding { diff --git a/crates/latkit/src/view/rows.rs b/crates/latkit/src/view/rows.rs deleted file mode 100644 index af4ec825..00000000 --- a/crates/latkit/src/view/rows.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! Row regions: each mark's rows in every per-row buffer (kind words, extra words, and focus words), -//! laid out from one capacity per mark with room to grow. When a mark outgrows its room, every region -//! is laid out again into new buffers and each mark's rows move by copy, so what they hold survives: -//! eased state, a transition's frozen rows. -//! -//! Vertex marks come first, so a vertex's slot (its kind words' offset over `VERTEX_WORDS`) counts -//! every vertex mark's rows. - -use super::View; -use crate::scene::mark::MarkKind; - -/// Where a mark's rows sit in each per-row buffer, in words. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub(crate) struct Region { - /// Rows the region holds. - pub(crate) capacity: u32, - pub(crate) stride: u32, - pub(crate) extras: u32, - pub(crate) state: u32, - pub(crate) extra: u32, - pub(crate) focus: u32, -} - -impl Region { - pub(crate) const EMPTY: Region = Region { capacity: 0, stride: 0, extras: 0, state: 0, extra: 0, focus: 0 }; -} - -/// Room for `rows` rows and some to spare, so streaming rows in rarely lays regions out again. -fn room(rows: u32) -> u32 { - rows + rows / 4 + 64 -} - -/// Totals of a layout: words in each buffer. -#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)] -pub(crate) struct Sizes { - pub(crate) state: u64, - pub(crate) focus: u64, -} - -impl View { - /// Fits every mark's rows and exits into its region. When one outgrows its room, or a mark's row - /// shape changed, lays every region out again into new buffers, copying each mark's rows over in - /// `encoder`. True when regions moved: vertex slots changed and focus words are written again. - pub(super) fn fit_rows(&mut self, encoder: &mut wgpu::CommandEncoder) -> bool { - let shape = |m: &super::marks::Shown| (m.stride(), m.extras()); - let fits = self.marks.iter().all(|m| { - let r = &m.region; - m.held() <= r.capacity && (r.stride, r.extras) == shape(m) - }); - if fits && !self.regions_dirty { - return false; - } - self.regions_dirty = false; - let order: Vec = (0..self.marks.len()) - .filter(|&i| self.marks[i].kind() == MarkKind::Vertices) - .chain((0..self.marks.len()).filter(|&i| self.marks[i].kind() != MarkKind::Vertices)) - .collect(); - let mut sizes = Sizes::default(); - let mut regions = vec![Region::EMPTY; self.marks.len()]; - for &i in &order { - let m = &self.marks[i]; - let (stride, extras) = shape(m); - let need = m.held(); - let capacity = if need <= m.region.capacity && (m.region.stride, m.region.extras) == (stride, extras) { - m.region.capacity - } else { - room(need) - }; - regions[i] = - Region { capacity, stride, extras, state: sizes.state as u32, extra: 0, focus: sizes.focus as u32 }; - sizes.state += u64::from(capacity) * u64::from(stride); - sizes.focus += u64::from(capacity); - } - // Extra words follow every mark's kind words. - for &i in &order { - regions[i].extra = sizes.state as u32; - sizes.state += u64::from(regions[i].capacity) * u64::from(regions[i].extras); - } - let device = self.gpu.0.device.clone(); - let resources = self.resources.as_mut().expect("created before rows"); - let old = resources.replace_rows(&device, sizes); - for (shown, new) in self.marks.iter_mut().zip(regions) { - let was = shown.region; - let kept = was.capacity.min(new.capacity).min(shown.before); - let copy = |encoder: &mut wgpu::CommandEncoder, - from: &wgpu::Buffer, - to: &wgpu::Buffer, - a: u32, - b: u32, - words: u32| { - if words > 0 { - encoder.copy_buffer_to_buffer(from, u64::from(a) * 4, to, u64::from(b) * 4, u64::from(words) * 4); - } - }; - if was.stride == new.stride { - copy(encoder, &old.state, resources.state(), was.state, new.state, kept * new.stride); - } - if was.extras == new.extras { - copy(encoder, &old.state, resources.state(), was.extra, new.extra, kept * new.extras); - } - // A running transition's frozen and settled rows move with its rows. - if shown.transition.is_some() && (was.stride, was.extras) == (new.stride, new.extras) { - let rows = was.capacity.min(new.capacity).min(shown.held()); - for (from, to) in [(&old.frozen, resources.frozen()), (&old.settled, resources.settled())] { - if let (Some(from), Some(to)) = (from, to) { - copy(encoder, from, to, was.state, new.state, rows * new.stride); - copy(encoder, from, to, was.extra, new.extra, rows * new.extras); - } - } - } - shown.moved = Some(was); - shown.region = new; - } - resources.retire(old); - true - } -} diff --git a/crates/latkit/src/view/sync.rs b/crates/latkit/src/view/sync.rs index d82178fd..77ec944c 100644 --- a/crates/latkit/src/view/sync.rs +++ b/crates/latkit/src/view/sync.rs @@ -13,26 +13,26 @@ //! //! Only columns whose inputs changed are rebuilt, once each: moving the shown coordinate rebuilds //! sampled columns' cursors and nothing else, and a table drawn in part rebuilds only where the pages -//! its marks hold changed. Rebuilt tables wait, pinning their pages, until every page is resident; -//! the view then swaps them in with every other mark of their dataset, so a data version, and what a -//! camera move draws, appear whole. A rebuilt column that lists pages says which row blocks hold pages -//! that changed, so only their rows evaluate again. While a mark eases, the table its geometry column -//! had when the transition began stays, packed after the rest, so paths morph from it. +//! its marks hold changed. Rebuilt tables are staged with every other mark's of their dataset: the +//! dataset reserves every page they read at once, or none and waits for room, and once every page +//! landed the view swaps them all in, so a data version, and what a camera move draws, appear whole. +//! Staged tables are not built again while they wait, only where what they read changes again. A +//! rebuilt column that lists pages says which pages of rows hold pages that changed, so only their rows +//! evaluate again. While a mark eases, the table its geometry column had when the transition began +//! stays, packed after the rest, so paths morph from it. use std::ops::Range; use std::sync::Arc; use super::drawing::{Drawing, Holds}; -use crate::data::{ - Column, Field, FramePage, GeometryBuilder, PAGE_ROWS, PageId, Run, Samples, Values, Version, detail, -}; +use crate::data::{Column, Field, FramePage, GeometryBuilder, PageId, Run, Samples, Values, Version, detail}; use crate::derive::Beside; -use crate::derive::order::{Order, PAGE}; +use crate::derive::order::Order; use crate::derive::shapes::PagedShapes; use crate::derive::stamp_of; use crate::expr::compile::{ColumnUse, Program, Reads, Words}; use crate::gpu::Inner; -use crate::gpu::arena::{Content, PIECE_POINTS, PageKey, path_pieces, path_points}; +use crate::gpu::arena::{Ask, Content, PIECE_POINTS, PageKey, path_pieces, path_points}; use crate::gpu::packed::{self, Packing}; use crate::gpu::shaders as wgsl; use crate::gpu::work::Count; @@ -108,13 +108,15 @@ impl Rows<'_> { } /// One column on the GPU: its descriptor, whose table offsets are relative to `words`, its page table -/// or cursors, the pages it pins, and the words still waiting for a page. +/// or cursors, and the pages it reads: until its dataset reserves them, what it asks the arena for, +/// each with the word it lands at; then the pages it pins, and the words still waiting for a page. struct ColumnTable { stamp: Stamp, /// How it pages its rows: 0 for the data's pages, else the stamp of the pages held. held: u64, descriptor: [u32; WORDS], words: Vec, + asks: Vec<(usize, Ask)>, pins: Vec, /// Words waiting for a page's offset: the word, the page, and the words it must hold. missing: Vec<(usize, PageKey, u32)>, @@ -149,18 +151,138 @@ struct Join { pages: Vec<(PageId, Arc<[u32]>)>, } -/// Tables rebuilt and waiting for their pages, by index into the current ones; and whether every -/// table was rebuilt, which replaces them all. -struct Pending { +/// A mark's tables rebuilt, by index into its current ones; and whether every table was rebuilt, +/// which replaces them all. +pub(crate) struct Rebuilt { tables: Vec<(usize, ColumnTable)>, reshape: bool, } +impl Rebuilt { + /// Lets go of the pages its tables pin. + fn release(self, gpu: &mut Inner) { + for (_, table) in self.tables { + unpin(&table.pins, gpu); + } + } + + /// Whether every table holds every page it reads, taking the offsets of those that landed. + fn ready(&mut self, gpu: &Inner) -> bool { + let mut ready = true; + for (_, table) in &mut self.tables { + ready &= table.asks.is_empty() && table.resolve(gpu); + } + ready + } +} + +/// What replaces what a view draws of a dataset: its marks' tables rebuilt since the dataset last +/// published, by mark. They wait until every page they read landed. +#[derive(Default)] +pub(crate) struct Staged { + marks: Vec<(usize, Rebuilt)>, +} + +impl Staged { + /// Mark `mark`'s tables staged, if any. + pub(crate) fn of(&self, mark: usize) -> Option<&Rebuilt> { + self.marks.iter().find(|(m, _)| *m == mark).map(|(_, r)| r) + } + + /// Stages mark `mark`'s tables `rebuilt`: tables rebuilt again replace those staged, whose pins + /// go; every table rebuilt replaces them all. + pub(crate) fn put(&mut self, mark: usize, rebuilt: Rebuilt, gpu: &mut Inner) { + let Some(at) = self.marks.iter().position(|(m, _)| *m == mark) else { + self.marks.push((mark, rebuilt)); + return; + }; + if rebuilt.reshape { + let (_, old) = std::mem::replace(&mut self.marks[at], (mark, rebuilt)); + old.release(gpu); + return; + } + let staged = &mut self.marks[at].1; + for (i, table) in rebuilt.tables { + match staged.tables.iter().position(|(j, _)| *j == i) { + Some(k) => { + let (_, old) = std::mem::replace(&mut staged.tables[k], (i, table)); + unpin(&old.pins, gpu); + } + None => staged.tables.push((i, table)), + } + } + } + + /// Reserves every page the tables staged ask for, all or none: `false` where the arena has no room + /// for them now. Tables asking for pages the arena no longer holds, which they did not make, are + /// dropped, to be built again. + pub(crate) fn reserve(&mut self, gpu: &mut Inner, device: &wgpu::Device, queue: &crate::gpu::work::Queue) -> bool { + let gone = |table: &ColumnTable| { + table.asks.iter().any(|(_, a)| a.content.is_none() && !gpu.arena.holds(a.key, a.need)) + }; + for (_, rebuilt) in &mut self.marks { + rebuilt.tables.retain(|(_, table)| !gone(table)); + } + let mut asks: Vec = Vec::new(); + let mut owners: Vec<(usize, usize, usize)> = Vec::new(); + for (m, (_, rebuilt)) in self.marks.iter_mut().enumerate() { + for (t, (_, table)) in rebuilt.tables.iter_mut().enumerate() { + for (at, ask) in table.asks.drain(..) { + owners.push((m, t, at)); + asks.push(ask); + } + } + } + if asks.is_empty() { + return true; + } + let reserved = gpu.arena.reserve(device, queue, &mut asks); + for ((m, t, at), ask) in owners.into_iter().zip(asks) { + let table = &mut self.marks[m].1.tables[t].1; + if reserved { + table.pins.push(ask.key); + table.missing.push((at, ask.key, ask.need)); + } else { + table.asks.push((at, ask)); + } + } + reserved + } + + /// Whether every table staged holds every page it reads, taking the offsets of those that landed. + pub(crate) fn ready(&mut self, gpu: &Inner) -> bool { + self.marks.iter_mut().fold(true, |ready, (_, rebuilt)| rebuilt.ready(gpu) && ready) + } + + /// Takes mark `mark`'s tables staged, to publish them. + pub(crate) fn take(&mut self, mark: usize) -> Option { + let at = self.marks.iter().position(|(m, _)| *m == mark)?; + Some(self.marks.swap_remove(at).1) + } + + /// Lets go of mark `mark`'s tables staged: it compiled again, or went. + pub(crate) fn forget(&mut self, mark: usize, gpu: &mut Inner) { + if let Some(rebuilt) = self.take(mark) { + rebuilt.release(gpu); + } + } + + /// Lets go of every table staged. + pub(crate) fn release(self, gpu: &mut Inner) { + for (_, rebuilt) in self.marks { + rebuilt.release(gpu); + } + } + + pub(crate) fn is_empty(&self) -> bool { + self.marks.is_empty() + } +} + /// What a mark's program reads, column by column: the program's columns, then one per join. #[derive(Default)] pub(crate) struct Tables { current: Vec, - pending: Option, /// The geometry column's table before it was last rebuilt, kept while the mark eases from it. frozen: Option, joins: Vec, @@ -171,17 +293,6 @@ pub(crate) struct Tables { generation: u64, } -/// What a sync found. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub(crate) enum Synced { - /// Nothing waits to replace the current tables. - Same, - /// Rebuilt tables wait for pages; the current ones still draw. - Waiting, - /// Rebuilt tables have every page, ready to replace the current ones. - Ready, -} - /// What publishing rebuilt tables changed: bit `i` of `columns` for each program column whose table /// changed, of `data` for each whose data did, not only where its rows are held; and the row blocks /// whose pages changed, `None` when every row did. @@ -230,16 +341,18 @@ enum Use<'a> { } impl Tables { - /// Brings the tables up to `source` paged as `rows`, with what `inputs` reads: tables whose - /// inputs changed are rebuilt once, and wait until every page they read is resident. - pub(crate) fn sync( + /// The tables of what `source` paged as `rows` reads, with what `inputs` reads, whose inputs changed + /// since `staged`, the tables staged to replace these, or else since the current ones: built once + /// each, asking for their pages. `None` where none changed. + pub(crate) fn rebuild( &mut self, program: &Program, source: &Source<'_>, rows: Rows<'_>, inputs: &Inputs<'_>, + staged: Option<&Rebuilt>, gpu: &mut Inner, - ) -> Synced { + ) -> Option { let joins = joins(program); let fields = program.columns.iter().map(|column| { let join = column.of.as_ref().map_or(wgsl::NONE, |of| { @@ -248,12 +361,15 @@ impl Tables { Use::Field { column, join } }); let uses: Vec> = fields.chain(joins.iter().map(|of| Use::Join(of))).collect(); - let reshape = !self.synced - || self.current.len() != uses.len() - || (uses.is_empty() && self.structure != Some(source.table.structure)); + // Every table rebuilds where they have no tables of this shape yet, but where a reshape is + // staged already: the tables are compared with it. + let reshaped = staged.is_some_and(|s| s.reshape && s.tables.len() == uses.len()); + let reshape = !reshaped + && (!self.synced + || self.current.len() != uses.len() + || (uses.is_empty() && self.structure != Some(source.table.structure))); self.structure = Some(source.table.structure); let held = rows.stamp(); - let pending = self.pending.take(); let mut rebuilt: Vec<(usize, ColumnTable)> = Vec::new(); for (i, use_) in uses.iter().enumerate() { let (stamp, paged) = match use_ { @@ -266,8 +382,7 @@ impl Tables { }, }; // What replaces table `i` once ready, else what draws. - let waiting = - pending.as_ref().filter(|p| !p.reshape || reshape).and_then(|p| p.tables.iter().find(|(j, _)| *j == i)); + let waiting = staged.and_then(|s| s.tables.iter().find(|(j, _)| *j == i)); let latest = waiting.map(|(_, t)| t).or(self.current.get(i)); if !reshape && latest.is_some_and(|t| t.stamp == stamp && t.held == paged) { continue; @@ -289,51 +404,13 @@ impl Tables { }; rebuilt.push((i, builder.finish(stamp, paged, descriptor))); } - // Tables rebuilt again replace those waiting, whose pins go; a reshape replaces them all. - let mut tables = match pending { - Some(p) if p.reshape == reshape || !reshape => p.tables, - Some(p) => { - p.tables.into_iter().for_each(|(_, t)| unpin(&t.pins, gpu)); - Vec::new() - } - None => Vec::new(), - }; - if rebuilt.is_empty() && !reshape { - if !tables.is_empty() { - self.pending = Some(Pending { tables, reshape: false }); - } - return self.waiting(gpu); - } - for (i, table) in rebuilt { - match tables.iter().position(|(j, _)| *j == i) { - Some(at) => { - let (_, old) = std::mem::replace(&mut tables[at], (i, table)); - unpin(&old.pins, gpu); - } - None => tables.push((i, table)), - } - } - self.pending = Some(Pending { tables, reshape }); - self.waiting(gpu) + (reshape || !rebuilt.is_empty()).then_some(Rebuilt { tables: rebuilt, reshape }) } - /// Whether rebuilt tables wait, and for pages: taking the offsets of those that landed. - pub(crate) fn waiting(&mut self, gpu: &Inner) -> Synced { - let Some(pending) = &mut self.pending else { return Synced::Same }; - let mut ready = true; - for (_, table) in &mut pending.tables { - ready &= table.resolve(gpu); - } - if ready { Synced::Ready } else { Synced::Waiting } - } - - /// Swaps in the tables rebuilt, once every page they read is resident: the current ones' pins - /// go, but the geometry column's, `geometry`, which stays frozen while the mark eases from it. - pub(crate) fn publish(&mut self, program: &Program, geometry: u32, gpu: &mut Inner) -> Option { - if self.waiting(gpu) != Synced::Ready { - return None; - } - let Pending { tables: rebuilt, reshape } = self.pending.take().expect("ready"); + /// Swaps in the tables `rebuilt`, every page they read resident: the current ones' pins go, but + /// the geometry column's, `geometry`, which stays frozen while the mark eases from it. + pub(crate) fn publish(&mut self, rebuilt: Rebuilt, program: &Program, geometry: u32, gpu: &mut Inner) -> Published { + let Rebuilt { tables: rebuilt, reshape } = rebuilt; self.generation += 1; let joins = joins(program); let published = if reshape { @@ -376,7 +453,7 @@ impl Tables { published }; self.synced = true; - Some(published) + published } /// Drops the frozen geometry table: the mark no longer eases from it. @@ -421,11 +498,6 @@ impl Tables { self.plan().map_or(0, |plan| plan.points) } - /// Whether rebuilt tables wait to replace the current ones. - pub(crate) fn pending(&self) -> bool { - self.pending.is_some() - } - /// Words the descriptors and tables take in the program buffer. pub(crate) fn len(&self) -> usize { self.packed().map(|t| WORDS + t.words.len()).sum() @@ -436,6 +508,11 @@ impl Tables { self.current.iter().chain(&self.frozen) } + /// The pages the tables drawn pin. + pub(crate) fn pins(&self) -> impl Iterator + '_ { + self.packed().flat_map(|t| t.pins.iter().copied()) + } + /// Appends every descriptor, then every table; `base` is the word of the program buffer the /// first descriptor lands at, so table offsets are absolute. pub(crate) fn pack(&self, base: u32, out: &mut Vec) { @@ -457,8 +534,7 @@ impl Tables { /// Releases every pin. pub(crate) fn release(&mut self, gpu: &mut Inner) { self.thaw(gpu); - let pending = self.pending.take().map(|p| p.tables).unwrap_or_default(); - for table in self.current.drain(..).chain(pending.into_iter().map(|(_, t)| t)) { + for table in self.current.drain(..) { unpin(&table.pins, gpu); } self.synced = false; @@ -480,7 +556,7 @@ impl Tables { let rows: Vec = (0..source.table.len()) .map(|r| source.table.ids.key(r).and_then(|key| other.row(key)).map_or(wgsl::NONE, |row| row as u32)) .collect(); - let pages = rows.chunks(PAGE_ROWS).map(|chunk| (PageId::next(), Arc::from(chunk))).collect(); + let pages = rows.chunks(wgsl::PAGE_ROWS as usize).map(|chunk| (PageId::next(), Arc::from(chunk))).collect(); Join { of: of.clone(), structure, pages } } @@ -490,17 +566,11 @@ impl Tables { } } -/// The row blocks whose pages differ between table `before` and table `now`, a page's every block; -/// `None` when either reads no page alone, or they page their rows apart, so every row changed. +/// The pages of rows whose pages differ between table `before` and table `now`; `None` when either +/// reads no page alone, so every row changed. fn changed_blocks(before: &ColumnTable, now: &ColumnTable) -> Option> { let (Some(old), Some(new)) = (&before.pages, &now.pages) else { return None }; - let bits = now.descriptor[wgsl::COLUMN_BITS as usize]; - if before.descriptor[wgsl::COLUMN_BITS as usize] != bits { - return None; - } - let blocks = 1 << (bits - wgsl::HELD_BITS); - let pages = (0..old.len().max(new.len())).filter(|&p| old.get(p) != new.get(p)).map(|p| p as u32); - Some(pages.flat_map(|p| p * blocks..(p + 1) * blocks).collect()) + Some((0..old.len().max(new.len())).filter(|&p| old.get(p) != new.get(p)).map(|p| p as u32).collect()) } fn unpin(pins: &[PageKey], gpu: &mut Inner) { @@ -597,11 +667,10 @@ fn stamp(column: &ColumnUse, source: &Source<'_>, inputs: &Inputs<'_>) -> Stamp } } -/// Builds one column's table, asking the arena for each page. +/// Builds one column's table, asking for each page it reads. struct Builder<'g> { words: Vec, - pins: Vec, - missing: Vec<(usize, PageKey, u32)>, + asks: Vec<(usize, Ask)>, plan: Option, points: u32, /// The page of each page of rows, while every row reads its own page's. @@ -611,32 +680,20 @@ struct Builder<'g> { impl<'g> Builder<'g> { fn new(gpu: &'g mut Inner) -> Builder<'g> { - Builder { - words: Vec::new(), - pins: Vec::new(), - missing: Vec::new(), - plan: None, - points: 0, - pages: Some(Vec::new()), - gpu, - } + Builder { words: Vec::new(), asks: Vec::new(), plan: None, points: 0, pages: Some(Vec::new()), gpu } } fn finish(self, stamp: Stamp, held: u64, descriptor: [u32; WORDS]) -> ColumnTable { - let Builder { words, pins, missing, plan, points, pages, gpu: _ } = self; - ColumnTable { stamp, held, descriptor, words, pins, missing, plan, points, pages } + let Builder { words, asks, plan, points, pages, gpu: _ } = self; + ColumnTable { stamp, held, descriptor, words, asks, pins: Vec::new(), missing: Vec::new(), plan, points, pages } } - /// Writes at word `at` where page `key` lies once it holds `need` words, pinning it; until it - /// does, `NONE`, and the table waits for it. `content` makes what it uploads. + /// Asks for page `key`, holding `need` words, to land at word `at` once it does; until then + /// `NONE`, and the table waits for it. `content` makes what it uploads, where the arena lacks it. fn place(&mut self, at: usize, key: PageKey, need: u32, content: impl FnOnce() -> Content) { - let offset = self.gpu.arena.get(key, need, content); - self.gpu.arena.pin(key); - self.pins.push(key); - self.words[at] = offset.unwrap_or(wgsl::NONE); - if offset.is_none() { - self.missing.push((at, key, need)); - } + let content = (!self.gpu.arena.holds(key, need)).then(content); + self.asks.push((at, Ask { key, need, content })); + self.words[at] = wgsl::NONE; } /// Appends where page `key` lies, as [`place`](Builder::place) writes it. @@ -656,14 +713,12 @@ impl<'g> Builder<'g> { fn column(&mut self, column: &ColumnUse, source: &Source<'_>, rows: Rows<'_>, inputs: &Inputs<'_>) -> [u32; WORDS] { let mut d = [0; WORDS]; d[wgsl::COLUMN_LOW as usize] = wgsl::NONE; - d[wgsl::COLUMN_BITS as usize] = wgsl::PAGE_BITS; if column.words == Words::Bits { d[wgsl::COLUMN_FLAGS as usize] = wgsl::BITS; } if let Rows::Held(drawing) = rows && let Some(shapes) = source.paged(&column.name).filter(|_| column.derived) { - d[wgsl::COLUMN_BITS as usize] = wgsl::HELD_BITS; d[wgsl::COLUMN_ROWS as usize] = drawing.live(); let packing = Packing { order: drawing.order.id(), @@ -684,7 +739,6 @@ impl<'g> Builder<'g> { d[wgsl::COLUMN_ROWS as usize] = table.len() as u32; let field = table.field_named(&column.name); if let Rows::Held(drawing) = rows { - d[wgsl::COLUMN_BITS as usize] = wgsl::HELD_BITS; d[wgsl::COLUMN_ROWS as usize] = drawing.live(); let packing = Packing { order: drawing.order.id(), @@ -737,7 +791,7 @@ impl<'g> Builder<'g> { self.pages = None; d[wgsl::COLUMN_FLAGS as usize] |= wgsl::SAMPLED; let blocks: Vec<(Samples, usize)> = - (0..table.len().div_ceil(PAGE_ROWS)).map(|b| (samples.clone(), b)).collect(); + (0..table.len().div_ceil(wgsl::PAGE_ROWS as usize)).map(|b| (samples.clone(), b)).collect(); self.trace(samples, &blocks, plan(samples, inputs.window)); } Some(Field::Sampled(samples, _)) => { @@ -773,7 +827,7 @@ impl<'g> Builder<'g> { for s in 0..slots { self.held_page(drawing, s, &packing, Some(values), |gpu, rows| { let mut codes = gpu.dictionaries.field(data, &packing.table, &packing.field).rows(values, rows); - codes.resize(PAGE as usize, wgsl::MISSING); + codes.resize(wgsl::PAGE_ROWS as usize, wgsl::MISSING); codes }); } @@ -843,8 +897,8 @@ impl<'g> Builder<'g> { counters.add(Count::Packed, rows.len() as u64); Content::Computed(packed::frame(samples, &rows, Some(f))) }; - self.place(at + 2 * k, key, PAGE, content); - self.words[at + 4 + k] = PAGE; + self.place(at + 2 * k, key, wgsl::PAGE_ROWS, content); + self.words[at + 4 + k] = wgsl::PAGE_ROWS; } } } @@ -870,7 +924,7 @@ impl<'g> Builder<'g> { let id = self.gpu.packed.page(&packing, &drawing.order, source, page); self.list(Some(id)); let key = PageKey::of(id, packing.words, packing.low); - let held = self.gpu.arena.offset(key, PAGE).is_some() || self.gpu.arena.queued(key); + let held = self.gpu.arena.holds(key, wgsl::PAGE_ROWS); let words = if held { Vec::new() } else { @@ -878,7 +932,7 @@ impl<'g> Builder<'g> { self.gpu.packed.count(rows.len()); pack(self.gpu, &rows) }; - self.push(key, PAGE, || Content::Computed(words)); + self.push(key, wgsl::PAGE_ROWS, || Content::Computed(words)); } /// A join's table: a bits column of the mark's rows, the rows of the other table with their ids. @@ -896,10 +950,8 @@ impl<'g> Builder<'g> { d[wgsl::COLUMN_ROWS as usize] = source.table.len() as u32; d[wgsl::COLUMN_LOW as usize] = wgsl::NONE; d[wgsl::COLUMN_JOIN as usize] = wgsl::NONE; - d[wgsl::COLUMN_BITS as usize] = wgsl::PAGE_BITS; if let Rows::Held(drawing) = rows { d[wgsl::COLUMN_ROWS as usize] = drawing.live(); - d[wgsl::COLUMN_BITS as usize] = wgsl::HELD_BITS; let Some(other) = joined(of, all) else { return d }; let packing = Packing { order: drawing.order.id(), @@ -1022,7 +1074,7 @@ impl<'g> Builder<'g> { let keys: Vec = (0..count).map(|piece| PageKey { page: id, words: Words::Path, low: false, piece: piece as u32 }).collect(); let arena = &self.gpu.arena; - let made = keys.iter().all(|&key| arena.offset(key, 1).is_some() || arena.queued(key)); + let made = keys.iter().all(|&key| arena.holds(key, 1)); let mut pieces = if made { Vec::new() } else { pieces() }; let listed = self.words.len(); self.words.resize(listed + count - 1, wgsl::NONE); @@ -1097,7 +1149,7 @@ impl<'g> Builder<'g> { /// Six words per page of rows: this frame's block and word, the next frame's, and their row /// counts. fn cursors(&mut self, samples: &Samples, frame: Option, rows: usize) { - for block in 0..rows.div_ceil(PAGE_ROWS) { + for block in 0..rows.div_ceil(wgsl::PAGE_ROWS as usize) { let at = self.words.len(); self.words.extend([wgsl::NONE, 0, wgsl::NONE, 0, 0, 0]); for (i, f) in [frame, frame.map(|f| f + 1)].into_iter().enumerate() { @@ -1128,7 +1180,9 @@ fn slot_key<'c>( Holds::Kept(_) => { let rows = drawing.rows(s); let mut pages: Vec = source.map_or_else(Vec::new, |column| { - rows.iter().filter_map(|&r| column.pages().get(r as usize / PAGE_ROWS).map(|page| page.id)).collect() + rows.iter() + .filter_map(|&r| column.pages().get((r >> wgsl::PAGE_BITS) as usize).map(|page| page.id)) + .collect() }); pages.sort_unstable(); pages.dedup(); diff --git a/crates/latkit/src/view/transition.rs b/crates/latkit/src/view/transition.rs index 157a1b6f..b565ca60 100644 --- a/crates/latkit/src/view/transition.rs +++ b/crates/latkit/src/view/transition.rs @@ -1,16 +1,16 @@ //! Transitions: a mark whose description, static data, or rows change eases from what it showed. //! -//! When one starts, what each row shows is frozen into the frozen buffer at the row's new place: -//! copied when the rows are the same, gathered by id when rows were added, removed, or reordered. -//! Rows that left are frozen past the new rows and fade out there. While the mark eases, its channels -//! store into the settled state, a second mirror of the state, once per change; each frame the clock -//! moves, the ease pass writes what every row shows from its frozen and settled state by the mark's -//! progress, and which rows fade, so draws, probes, bounds, and shadows read eased state without -//! knowing a transition runs, and no channel runs to ease. Where no row is the same as one shown -//! before, as when another dataset replaces the one drawn, every row enters, shows what it evaluated, -//! and only fades. The frozen and settled buffers mirror the state buffer's layout and exist only -//! while a transition does. A stroke's geometry column keeps the table it had as the transition -//! began, and its paths morph from it point by point. +//! When one starts, what each row shows is frozen into the mark's frozen words at the row's new +//! place: where it was when the rows are the same, gathered by id when rows were added, removed, or +//! reordered. Rows that left are frozen past the new rows and fade out there. While the mark eases, +//! its channels store into its settled words once per change; each frame the clock moves, the ease +//! pass writes what every row shows from its frozen and settled words by the mark's progress, and +//! which rows fade, so draws, probes, bounds, and shadows read eased state without knowing a +//! transition runs, and no channel runs to ease. Where no row is the same as one shown before, as when +//! another dataset replaces the one drawn, every row enters, shows what it evaluated, and only fades. +//! The frozen and settled words are pages like the state's, kept only while a transition runs. A +//! stroke's geometry column keeps the table it had as the transition began, and its paths morph from +//! it point by point. //! //! Without an ease, a flowing mark whose rows moved still begins a transition, of no length: it //! shows the new state at once, and its comets carry their travel over to the rows' new places. @@ -21,9 +21,10 @@ use super::View; use super::clock::Clock; use super::drawing::{Drawing, Holds}; use super::marks::{Holding, Shown}; -use crate::data::{PAGE_ROWS, RowIds, Table}; -use crate::derive::order::PAGE; +use super::words::Kept; +use crate::data::{RowIds, Table}; use crate::gpu::Inner; +use crate::gpu::shaders::PAGE_ROWS; use crate::gpu::shaders::{self as wgsl, Shader}; use crate::scene::mark::MarkKind; @@ -64,7 +65,7 @@ pub(crate) fn remap(old: &Table, new: &Table) -> Option<(Vec, Vec)> { let mut sources: Vec = (0..new.len()).map(|r| if r < old.len() { r as u32 } else { wgsl::NONE }).collect(); let mut exits = Vec::new(); for block in rewritten(old, new).unwrap_or_default() { - let rows = block as usize * PAGE_ROWS..((block as usize + 1) * PAGE_ROWS).min(old.len()); + let rows = (block * PAGE_ROWS) as usize..(((block + 1) * PAGE_ROWS) as usize).min(old.len()); for r in rows.filter(|&r| old.ids.key(r) != new.ids.key(r)) { sources[r] = wgsl::NONE; if old.ids.key(r).is_some() { @@ -101,11 +102,9 @@ pub(crate) fn remap(old: &Table, new: &Table) -> Option<(Vec, Vec)> { /// place: those whose ids' pages changed, and those past `old`'s rows. `None` where that does not /// tell, as between ids counted and ids listed. pub(crate) fn rewritten(old: &Table, new: &Table) -> Option> { - let blocks = |len: usize| len.div_ceil(PAGE_ROWS) as u32; + let pages = |len: usize| (len as u32).div_ceil(PAGE_ROWS); match (&old.ids, &new.ids) { - (RowIds::Count(a), RowIds::Count(b)) if b >= a => { - Some(((*a as usize / PAGE_ROWS) as u32..blocks(*b as usize)).collect()) - } + (RowIds::Count(a), RowIds::Count(b)) if b >= a => Some((*a / PAGE_ROWS..pages(*b as usize)).collect()), (RowIds::Listed(a), RowIds::Listed(b)) if b.len() >= a.len() => { let (a, b) = (a.pages(), b.pages()); Some((0..b.len()).filter(|&p| a.get(p).is_none_or(|page| page.id != b[p].id)).map(|p| p as u32).collect()) @@ -187,7 +186,7 @@ pub(crate) fn in_place(sources: &[u32]) -> bool { /// The slots of `new` holding rows `old` did not hold there: every slot holding a page without one /// before, those whose page took rows appended since, and those of rows kept that changed. pub(crate) fn bound(old: Option<&Drawing>, new: &Drawing) -> Vec { - let grown = old.map_or(u32::MAX, |old| old.order.len() / PAGE); + let grown = old.map_or(u32::MAX, |old| old.order.len() / PAGE_ROWS); let appended = old.is_some_and(|o| o.order.len() != new.order.len()); (0..new.slot_count()) .filter(|&s| match new.holds(s) { @@ -212,9 +211,9 @@ pub(crate) fn holds_alike(a: &Holding, b: &Holding) -> bool { } impl Shown { - /// Takes over the rows `before` holds, as a changed description of the same mark. - pub(crate) fn inherit(&mut self, before: &Shown) { - self.region = before.region; + /// Takes over the rows `before` holds and their words, as a changed description of the same mark. + pub(crate) fn inherit(&mut self, before: &mut Shown) { + self.words = std::mem::take(&mut before.words); self.before = before.before; self.rows = before.rows; self.holding.clone_from(&before.holding); @@ -246,7 +245,7 @@ impl Shown { pub(crate) fn restart(&mut self, bound: &[u32]) { let mut sources: Vec = (0..self.live()).collect(); for &slot in bound { - let rows = slot * PAGE..((slot + 1) * PAGE).min(sources.len() as u32); + let rows = slot * PAGE_ROWS..((slot + 1) * PAGE_ROWS).min(sources.len() as u32); sources[rows.start as usize..rows.end as usize].fill(wgsl::NONE); } // The rows that left lay past the live rows when the frame began. @@ -291,9 +290,9 @@ impl View { shown.transition = None; shown.exits = 0; shown.all_dirty(); + shown.packed = false; if shown.tables.frozen().is_some() { shown.tables.thaw(inner); - shown.packed = false; } } else { running = true; @@ -301,61 +300,82 @@ impl View { shown.easing = self.clock.moved(); } } - if !running && self.marks.iter().all(|m| m.transition.is_none()) { - let resources = self.resources.as_mut().expect("created"); - resources.drop_frozen(); - resources.drop_settled(); - } running } - /// Records what each starting mark shows into the frozen buffer. + /// Records what each starting mark shows into its frozen words: the remap words say, for each, the + /// pages its rows' words were in as the frame began, how many rows showed, where each new row was + /// where rows moved, and where the vertices its ends name moved where those did. fn freeze(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner, starting: &[usize]) { let gpu = self.gpu.clone(); let (device, queue) = (&gpu.0.device, &gpu.0.queue); - let slots = self.slot_table(starting); - let resources = self.resources.as_mut().expect("created before transitions"); - resources.ensure_frozen(device); - resources.ensure_settled(device); let mut words: Vec = Vec::new(); - let table = slots.map(|table| { - words.extend(table); - 0 - }); - let mut gathers = Vec::new(); + // Where each vertex mark's rows moved: how many it held, then where each went. + let mut moves = vec![wgsl::NONE; self.marks.len()]; + for &i in starting { + let m = &self.marks[i]; + let Some(sources) = m.begin.as_ref().and_then(|b| b.sources.as_ref()) else { continue }; + if m.kind() != MarkKind::Vertices { + continue; + } + moves[i] = words.len() as u32; + let at = words.len() + 1; + words.push(m.before); + words.resize(at + m.before as usize, wgsl::NONE); + for (new, &old) in sources.iter().enumerate() { + if old < m.before { + words[at + old as usize] = new as u32; + } + } + } for &i in starting { let shown = &mut self.marks[i]; let begin = shown.begin.take().expect("starting"); - let was = shown.moved.unwrap_or(shown.region); - let now = shown.region; - match begin.sources { - None => { - let source = if shown.moved.is_some() { resources.retired_state() } else { None }; - let rows = shown.before.min(shown.live()); - resources.copy_frozen(encoder, source, was, now, rows); - } + let before = shown.words.state_before().clone(); + let width = shown.stride() + shown.extras(); + // Rows whose words took another shape show nothing to ease from: they enter, and fade in. + let sources = + if before.width == width { begin.sources } else { Some(vec![wgsl::NONE; shown.held() as usize]) }; + shown.carry = sources.is_some(); + let header = words.len(); + words.extend([0; 4]); + words[header] = words.len() as u32; + words.extend(before.blocks.iter()); + words[header + 1] = shown.before.min(shown.live()); + words[header + 2] = match sources { Some(sources) => { let at = words.len() as u32; - let slot_words = match shown.kind() { - MarkKind::Edges => (1 << wgsl::EDGE_FROM) | (1 << wgsl::EDGE_TO), - MarkKind::Faces => (1 << wgsl::FACE_A) | (1 << wgsl::FACE_B) | (1 << wgsl::FACE_C), - _ => 0, - }; - words.extend([was.state, was.extra, slot_words, table.unwrap_or(wgsl::NONE)]); words.extend(sources); - shown.remap = at; - shown.carry = true; - gathers.push(i); + at } - } - } - if gathers.is_empty() { - return; + None => wgsl::NONE, + }; + // Each kind word naming a vertex follows that vertex where its mark's rows moved. + let ends: Vec<(u32, u32)> = shown + .code + .iter() + .filter(|op| op[0] == wgsl::OP_STORE_END) + .filter_map(|op| Some((op[1], moves[shown.ends.get(op[3] as usize)?.mark?]))) + .filter(|&(_, moved)| moved != wgsl::NONE) + .collect(); + words[header + 3] = if ends.is_empty() { + wgsl::NONE + } else { + let at = words.len(); + words.resize(at + shown.stride() as usize, wgsl::NONE); + for (word, moved) in ends { + words[at + word as usize] = moved; + } + at as u32 + }; + shown.remap = header as u32; } + let resources = self.resources.as_mut().expect("created before transitions"); resources.write_remap(device, queue, &words); - let moved = gathers.iter().any(|&i| self.marks[i].moved.is_some()); + inner.arena.clear(device, queue); + inner.arena.written(); let groups = resources.groups(&gpu.0, inner); - let group = resources.freeze_group(device, inner, moved); + let group = resources.pass(device, inner, Shader::Freeze); let pipeline = inner.pipelines.compute(device, Shader::Freeze, "gather"); // The marks' uniforms this frame writes before it submits hold each mark's remap offset. let mut pass = encoder @@ -363,44 +383,40 @@ impl View { pass.set_pipeline(&pipeline); pass.set_bind_group(0, &groups.view, &[0]); pass.set_bind_group(2, &group, &[]); - for &i in &gathers { - let shown = &self.marks[i]; + for &i in starting { pass.set_bind_group(1, &groups.mark, &[super::passes::offset(i)]); - super::passes::dispatch(&mut pass, shown.held()); + super::passes::dispatch(&mut pass, self.marks[i].held()); } } - /// Where each old vertex slot moved, for the ends of rows easing out; `None` when no mark with - /// ends gathers. - fn slot_table(&self, starting: &[usize]) -> Option> { - let needed = starting.iter().any(|&i| { - let m = &self.marks[i]; - m.kind() != MarkKind::Vertices && m.begin.as_ref().is_some_and(|b| b.sources.is_some()) - }); - if !needed { - return None; - } - let vertices = self.marks.iter().filter(|m| m.kind() == MarkKind::Vertices); - let old_slot = |m: &Shown| m.moved.unwrap_or(m.region).state / wgsl::VERTEX_WORDS; - let size = vertices.clone().map(|m| old_slot(m) + m.before).max().unwrap_or(0); - let mut table = vec![wgsl::NONE; size as usize]; - for m in vertices { - let (from, to) = (old_slot(m), m.region.state / wgsl::VERTEX_WORDS); - match m.begin.as_ref().and_then(|b| b.sources.as_ref()) { - Some(sources) => { - for (new_row, &old_row) in sources.iter().enumerate() { - if old_row != wgsl::NONE && old_row < m.before { - table[(from + old_row) as usize] = to + new_row as u32; - } - } + /// Gives each mark whose transition starts or runs frozen and settled words for every row it + /// holds, and lets them go once it ends; a transition finding no room for them does not start, and + /// its rows show what they evaluate at once. + pub(super) fn fit_easing(&mut self, inner: &mut Inner) { + let gpu = self.gpu.clone(); + let device = (&gpu.0.device, &gpu.0.queue); + for shown in &mut self.marks { + let easing = shown.begin.is_some() || shown.transition.is_some(); + let rows = shown.page_rows(); + let width = if easing { shown.stride() + shown.extras() } else { 0 }; + let mut fits = true; + for kept in [Kept::Frozen, Kept::Settled] { + let had = shown.words.pages(kept).blocks.len(); + match shown.words.fit(kept, &rows, width, device, &mut inner.arena) { + Some(new) => shown.packed &= new.is_empty() && had == shown.words.pages(kept).blocks.len(), + None => fits = false, } - None => { - for row in 0..m.before.min(m.live()) { - table[(from + row) as usize] = to + row; - } + } + if easing && !fits { + for kept in [Kept::Frozen, Kept::Settled] { + shown.words.fit(kept, &[], 0, device, &mut inner.arena); } + shown.begin = None; + shown.transition = None; + shown.exits = 0; + shown.all_dirty(); + shown.packed = false; } } - Some(table) } } diff --git a/crates/latkit/src/view/uniforms.rs b/crates/latkit/src/view/uniforms.rs index 2aba01b8..a40a454d 100644 --- a/crates/latkit/src/view/uniforms.rs +++ b/crates/latkit/src/view/uniforms.rs @@ -5,6 +5,7 @@ use glam::{DMat4, DVec3, DVec4}; use super::camera::{Projected, split}; use super::marks::Shown; +use super::words::Kept; use crate::gpu::shaders as wgsl; use crate::scene::mark::MarkKind; use crate::{Color, Grid, Scene, Space, Theme}; @@ -171,32 +172,31 @@ fn theme_slots(theme: &Theme) -> [[u32; 4]; 4] { std::array::from_fn(|i| std::array::from_fn(|j| slots[i * 4 + j])) } -/// What `mark` evaluates and how it eases by `clock`: the row blocks this frame's evaluation covers, -/// and while it eases, where its frozen rows start, its progress, and how each word eases. +/// What `mark` evaluates and how it eases by `clock`: the pages of rows this frame's evaluation +/// covers, and while it eases, the page tables of its frozen and settled words, its progress, and how +/// each word eases. fn ease(mark: &mut wgsl::Mark, shown: &Shown, clock: &super::clock::Clock) { - let region = &shown.region; mark.block = shown.block; mark.blocks = shown.block_count; mark.blends = shown.offsets.blends; mark.frozen = wgsl::NONE; - mark.frozen_extra = wgsl::NONE; + mark.settled = wgsl::NONE; mark.motion = 1.0; if let Some(transition) = &shown.transition { - mark.frozen = region.state; - mark.frozen_extra = region.extra; + mark.frozen = shown.offsets.table(Kept::Frozen); + mark.settled = shown.offsets.table(Kept::Settled); mark.motion = transition.motion(clock); } } -/// The uniform of mark `index`, easing by `clock`. +/// The uniform of mark `index`, labeled as `labels` says, easing by `clock`. pub(crate) fn mark( index: usize, shown: &Shown, - labels: Option, + labels: &super::labels::Labels, clock: &super::clock::Clock, ) -> wgsl::Mark { let mut mark: wgsl::Mark = bytemuck::Zeroable::zeroed(); - let region = &shown.region; mark.index = index as u32; mark.kind = match shown.kind() { MarkKind::Vertices => wgsl::MARK_VERTICES, @@ -211,12 +211,13 @@ pub(crate) fn mark( mark.row_runs = shown.offsets.runs; mark.drawn = shown.drawn_rows.at.map_or(wgsl::NONE, |[start, _]| start); mark.reveal = shown.reveal; - mark.state = region.state; + mark.state = shown.offsets.table(Kept::State); mark.stride = shown.stride(); - mark.extra = region.extra; mark.extras = shown.extras(); ease(&mut mark, shown, clock); - mark.focus = region.focus; + mark.focus = shown.offsets.table(Kept::Focus); + mark.candidates = shown.offsets.table(Kept::Candidates); + mark.normals = shown.offsets.table(Kept::Normals); mark.domains = shown.domains; mark.columns = shown.offsets.columns; mark.ranges = shown.offsets.ranges; @@ -242,13 +243,12 @@ pub(crate) fn mark( [mark.size_min, mark.size_max] = crate::Size::ANY; mark.label_max = wgsl::NONE; mark.label_select = wgsl::NONE; - if let Some(slots) = labels { + if let Some(slots) = labels.slots(index) { mark.label_select = slots.select; - mark.candidates = slots.candidates; mark.runs = slots.runs; mark.wanted = slots.wanted; mark.seen = slots.seen; - mark.label_id = slots.id; + mark.label_id = labels.id(index); } if let Some(compiled) = &shown.compiled { let program = &compiled.program; diff --git a/crates/latkit/src/view/words.rs b/crates/latkit/src/view/words.rs new file mode 100644 index 00000000..bb715905 --- /dev/null +++ b/crates/latkit/src/view/words.rs @@ -0,0 +1,145 @@ +//! What a mark keeps per row, in pages of the arena: for each page of held rows, a block of each kind +//! of words it keeps, holding as many rows as the page has. A row's state is its kind words then its extra words; beside it are its focus +//! word, its label's candidate, the normals faces sum at a vertex, and while the mark eases, its state +//! as it showed when the transition began and as it settles. A page keeps its blocks while the mark +//! holds rows there, so rows keep their words as the camera moves and nothing moves as a mark holds +//! more rows; every kind is counted in the GPU's budget with the data. + +use std::sync::Arc; + +use crate::gpu::arena::Arena; +use crate::gpu::work::Queue; + +/// A kind of words a mark keeps per row. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum Kept { + State, + Focus, + Candidates, + Normals, + Frozen, + Settled, +} + +const KINDS: [Kept; 6] = [Kept::State, Kept::Focus, Kept::Candidates, Kept::Normals, Kept::Frozen, Kept::Settled]; + +/// One kind's pages: the words each row takes, and each page's block in the arena and the words it +/// holds. +#[derive(Clone, PartialEq, Eq, Debug)] +pub(crate) struct Pages { + pub(crate) width: u32, + pub(crate) blocks: Arc<[u32]>, + words: Vec, +} + +impl Default for Pages { + fn default() -> Pages { + Pages { width: 0, blocks: Arc::new([]), words: Vec::new() } + } +} + +/// A mark's pages of every kind. +#[derive(Default)] +pub(crate) struct Words { + pages: [Pages; KINDS.len()], + /// The state's pages before this frame changed them, while the frame is done with what they held. + before: Option, +} + +impl Words { + pub(crate) fn pages(&self, kept: Kept) -> &Pages { + &self.pages[kept as usize] + } + + /// The state's pages as the frame began: what a transition starting freezes rows from. + pub(crate) fn state_before(&self) -> &Pages { + self.before.as_ref().unwrap_or(&self.pages[Kept::State as usize]) + } + + /// Forgets the state's pages as the frame began, once the frame is recorded. + pub(crate) fn settle(&mut self) { + self.before = None; + } + + /// Fits `kept`'s pages to hold `rows[p]` rows on page `p`, of `width` words each, or none at + /// `width` 0: a page whose block cannot hold its rows takes a new block, cleared, as every page does + /// where a row's words changed width; blocks it no longer has are let go. The pages given new + /// blocks, whose words start over; `None` where the arena has no room for them now, and nothing + /// changed. + pub(crate) fn fit( + &mut self, + kept: Kept, + rows: &[u32], + width: u32, + (device, queue): (&wgpu::Device, &Queue), + arena: &mut Arena, + ) -> Option> { + let rows = if width == 0 { &[][..] } else { rows }; + let have = &self.pages[kept as usize]; + let holds = |p: usize| have.width == width && have.words.get(p).is_some_and(|&w| w >= rows[p] * width); + if have.blocks.len() == rows.len() && (0..rows.len()).all(holds) { + return Some(Vec::new()); + } + let mut next = Pages { width, blocks: Arc::new([]), words: Vec::with_capacity(rows.len()) }; + let mut blocks = Vec::with_capacity(rows.len()); + let mut new = Vec::new(); + for (p, &n) in rows.iter().enumerate() { + if holds(p) { + blocks.push(have.blocks[p]); + next.words.push(have.words[p]); + continue; + } + let words = (n * width).max(1); + let Some(block) = arena.block(device, queue, words) else { + for &p in &new { + arena.free(blocks[p as usize], next.words[p as usize]); + } + return None; + }; + blocks.push(block); + next.words.push(crate::gpu::arena::rounded(words)); + new.push(p as u32); + } + next.blocks = blocks.into(); + let have = std::mem::replace(&mut self.pages[kept as usize], next); + let next = &self.pages[kept as usize]; + for (p, (&block, &words)) in have.blocks.iter().zip(&have.words).enumerate() { + if next.blocks.get(p) != Some(&block) { + arena.free(block, words); + } + } + if kept == Kept::State && self.before.is_none() { + self.before = Some(have); + } + Some(new) + } + + /// Lets go of every page, as a mark does when it goes. + pub(crate) fn release(&mut self, arena: &mut Arena) { + for kept in KINDS { + let pages = std::mem::take(&mut self.pages[kept as usize]); + for (&block, &words) in pages.blocks.iter().zip(&pages.words) { + arena.free(block, words); + } + } + self.before = None; + } + + /// The page tables the program holds: each kind it keeps, and its blocks. + pub(crate) fn tables(&self) -> impl Iterator { + KINDS + .into_iter() + .filter(|&kept| self.pages[kept as usize].width > 0) + .map(|kept| (kept, &self.pages[kept as usize].blocks[..])) + } + + /// Words the page tables take in the program. + pub(crate) fn len(&self) -> usize { + self.tables().map(|(_, blocks)| blocks.len()).sum() + } + + /// Bytes its pages take in the arena. + pub(crate) fn bytes(&self) -> u64 { + self.pages.iter().flat_map(|p| &p.words).map(|&w| u64::from(w) * 4).sum() + } +} diff --git a/crates/latkit/tests/labels.rs b/crates/latkit/tests/labels.rs index 03030859..d09bfd29 100644 --- a/crates/latkit/tests/labels.rs +++ b/crates/latkit/tests/labels.rs @@ -32,6 +32,11 @@ impl Host for Counted { self.0.fetch_add(1, Ordering::Relaxed); Boxes.rasterize(run) } + + // Measuring sets no text. + fn measure(&self, run: &TextRun) -> latkit::Result<[f32; 2]> { + Boxes.measure(run) + } } fn view(gpu: &latkit::Gpu, x: Vec, priority: Vec) -> View { diff --git a/crates/latkit/tests/scale.rs b/crates/latkit/tests/scale.rs index 1d3b6265..aa8979ac 100644 --- a/crates/latkit/tests/scale.rs +++ b/crates/latkit/tests/scale.rs @@ -444,7 +444,7 @@ fn an_edge_kept_draws_between_its_ends() { let Some(gpu) = gpu() else { return }; let budget = gpu.budget(); // So little room that only the coarsest edges draw. - gpu.set_budget(Budget { bytes: 1 << 20, ..budget }); + gpu.set_budget(Budget { bytes: 16 << 20, ..budget }); let scene = Scene::new(plane()) .theme(plain_theme()) .ease(std::time::Duration::ZERO) diff --git a/npm/examples/src/examples/soft-body/index.ts b/npm/examples/src/examples/soft-body/index.ts index 0b453da9..2bbe6e6f 100644 --- a/npm/examples/src/examples/soft-body/index.ts +++ b/npm/examples/src/examples/soft-body/index.ts @@ -187,7 +187,7 @@ export default async function bunny(stage: Stage): Promise { corners: ['a', 'b', 'c'], ...(paint === 'solid' ? { color: '#edccb4' } : {}), pick: false, - shadow: false, + shadow: true, }, }, { diff --git a/npm/schema.json b/npm/schema.json index 2679454e..80141b5e 100644 --- a/npm/schema.json +++ b/npm/schema.json @@ -2364,7 +2364,7 @@ "format": "float" }, "laneBow": { - "description": "Elsewhere, how far edges joining the same two vertices bow apart: each lane by this fraction\nof the edge's length at its middle. A bow is part of the drawing, so zooming never moves it.", + "description": "Elsewhere, how far edges joining the same two vertices bow apart: each lane by this fraction\nof the edge's length at its middle, as wide as eight lanes at most, more packing closer. A bow\nis part of the drawing, so zooming never moves it.", "type": "number", "format": "float" }, @@ -4270,7 +4270,7 @@ "description": "Where a view looks." }, "Waiting": { - "description": "Work under way a frame stands in for: pipelines the scene draws with compiling, until which the view draws nothing new (`compiling`); pages uploading or tables not yet built from them (`data`); layouts, routes, lanes, shapes, tiles, or pyramids (`derived`); a label placement or texts not yet set (`labels`); a fit due or being measured (`fit`); row regions laid out again, segment counts, or a transition beginning (`marks`). Empty once a frame is complete.", + "description": "Work under way a frame stands in for: pipelines the scene draws with compiling, until which the view draws nothing new (`compiling`); pages uploading or tables not yet built from them (`data`); layouts, routes, lanes, shapes, tiles, or pyramids (`derived`); a label placement or texts not yet set (`labels`); a fit due or being measured (`fit`); segment counts, or a transition beginning (`marks`). Empty once a frame is complete.", "type": "array", "items": { "enum": [ diff --git a/npm/src/inputs.ts b/npm/src/inputs.ts index 32847d5c..415f5c40 100644 --- a/npm/src/inputs.ts +++ b/npm/src/inputs.ts @@ -1011,7 +1011,8 @@ export type Theme = edgeSpacing?: number; /** * Elsewhere, how far edges joining the same two vertices bow apart: each lane by this fraction of - * the edge's length at its middle. A bow is part of the drawing, so zooming never moves it. + * the edge's length at its middle, as wide as eight lanes at most, more packing closer. A bow is + * part of the drawing, so zooming never moves it. */ laneBow?: number; /** How far an arrowhead reaches past its edge's width. */ diff --git a/npm/src/jobs.ts b/npm/src/jobs.ts index 46717f1d..33c6f93b 100644 --- a/npm/src/jobs.ts +++ b/npm/src/jobs.ts @@ -19,16 +19,16 @@ interface Slot { } /** - * Runs one job per worker. Idle workers retire after 30 seconds; startup and jobs have a one-minute - * deadline. Failure rejects the affected job and retires the worker; later jobs get a fresh one. + * Keeps the workers that do latkit's jobs alive, one job each. latkit queues its jobs and hands one + * over only when it can run, so the pool grows to as many jobs as run at once and nothing waits here. + * Idle workers retire after 30 seconds; startup and jobs have a one-minute deadline. Failure rejects + * the affected job and retires the worker; later jobs get a fresh one. */ export function jobs( module: WebAssembly.Module, createWorker = () => new Worker(new URL('./worker.js', import.meta.url)), ): (bytes: Uint8Array) => Promise { - const size = Math.max(1, Math.min(4, (globalThis.navigator?.hardwareConcurrency || 2) - 1)); - const queue: Job[] = []; - const slots = new Set(); + const idle = new Set(); const retire = (slot: Slot): void => { clearTimeout(slot.timer); @@ -36,14 +36,13 @@ export function jobs( slot.worker.onerror = null; slot.worker.onmessageerror = null; slot.worker.terminate(); - slots.delete(slot); + idle.delete(slot); }; const fail = (slot: Slot, cause: unknown): void => { const job = slot.job; retire(slot); job?.reject(cause instanceof Error ? cause : new Error(String(cause))); - pump(); }; const send = (slot: Slot): void => { @@ -59,7 +58,6 @@ export function jobs( const start = (): Slot => { const worker = createWorker(); const slot: Slot = { worker, ready: false }; - slots.add(slot); worker.onmessage = ({ data }: MessageEvent) => { if (!slot.ready) { if (!data.ready) return fail(slot, data.error ?? 'worker initialization failed'); @@ -72,8 +70,8 @@ export function jobs( slot.job = undefined; clearTimeout(slot.timer); slot.timer = setTimeout(() => retire(slot), 30_000); + idle.add(slot); job.resolve(data.bytes); - pump(); } }; worker.onerror = (event) => { @@ -84,29 +82,24 @@ export function jobs( return slot; }; - function pump(): void { - while (queue.length) { - let slot = [...slots].find((candidate) => !candidate.job); - if (!slot && slots.size >= size) return; - const job = queue.shift()!; + return (bytes) => + new Promise((resolve, reject) => { + const job = { bytes, resolve, reject }; + let slot = idle.values().next().value; try { - slot ??= start(); + if (slot) idle.delete(slot); + else { + slot = start(); + slot.worker.postMessage(module); + } clearTimeout(slot.timer); slot.job = job; const active = slot; slot.timer = setTimeout(() => fail(active, 'worker job timed out'), 60_000); - if (slot.ready) send(slot); - else slot.worker.postMessage(module); + send(slot); } catch (error) { if (slot) retire(slot); - job.reject(error instanceof Error ? error : new Error(String(error))); + reject(error instanceof Error ? error : new Error(String(error))); } - } - } - - return (bytes) => - new Promise((resolve, reject) => { - queue.push({ bytes, resolve, reject }); - pump(); }); } diff --git a/npm/src/outputs.ts b/npm/src/outputs.ts index d6e4f221..7f37c266 100644 --- a/npm/src/outputs.ts +++ b/npm/src/outputs.ts @@ -259,8 +259,8 @@ export type ViewEvent = * Work under way a frame stands in for: pipelines the scene draws with compiling, until which the * view draws nothing new (`compiling`); pages uploading or tables not yet built from them * (`data`); layouts, routes, lanes, shapes, tiles, or pyramids (`derived`); a label placement or - * texts not yet set (`labels`); a fit due or being measured (`fit`); row regions laid out again, - * segment counts, or a transition beginning (`marks`). Empty once a frame is complete. + * texts not yet set (`labels`); a fit due or being measured (`fit`); segment counts, or a + * transition beginning (`marks`). Empty once a frame is complete. */ export type Waiting = ('compiling' | 'data' | 'derived' | 'labels' | 'fit' | 'marks')[]; diff --git a/npm/src/runtime.ts b/npm/src/runtime.ts index 4290e71f..da1a17c8 100644 --- a/npm/src/runtime.ts +++ b/npm/src/runtime.ts @@ -9,17 +9,14 @@ import type { LoadOptions } from './load.js'; import type { Description, Failure, Hit, Stats, Value, ViewEvent, Work } from './outputs.js'; import { Patch } from './patch.js'; -let run: ((bytes: Uint8Array) => Promise) | undefined; /** The page's first GPU, made as the runtime loads, until `Gpu.create()` takes it. */ let first: Promise | undefined; /** @internal Initializes the bindings before returning their public API. */ export async function initialize(options: LoadOptions) { await init(options.wasm === undefined ? undefined : { module_or_path: options.wasm }); - if (options.createWorker || typeof Worker === 'function') { - run = jobs(wasm.module() as WebAssembly.Module, options.createWorker); - wasm.setJobs(run); - } + if (options.createWorker || typeof Worker === 'function') + wasm.setJobs(jobs(wasm.module() as WebAssembly.Module, options.createWorker)); if (typeof VideoEncoder === 'function') wasm.setEncoder(encoder); // The page's first GPU is made now, so the page does not wait for its device when it draws; it // compiles nothing until a view opens. @@ -134,18 +131,7 @@ export class Data { * as fields. */ static async read(bytes: Uint8Array, name?: string): Promise { - if ([0x4c, 0x41, 0x54, 0x4b].every((b, i) => bytes[i] === b)) - return new Data(wasm.Data.read(bytes, name)); - if (name === undefined) - throw thrown({ code: 'invalid', message: 'GeoJSON is read as a named dataset' }); - const job = wasm.geojsonJob(name, bytes); - const answer = run ? await run(job) : wasm.job(job); - if (answer.length === 0) - throw thrown({ - code: 'invalid', - message: `${name}: the bytes are neither latkit's nor GeoJSON`, - }); - return new Data(wasm.Data.read(answer, name)); + return new Data((await wasm.Data.read(bytes, name)) as wasm.Data); } /** @@ -770,11 +756,7 @@ export class Image { * 100 (90 by default), small, its alpha dropped; or WebP, lossless with alpha, smaller than PNG. */ async encode(format: ImageFormat, options: { quality?: number } = {}): Promise { - const job = this.inner.encodeJob(format, options.quality); - const file = run ? await run(job) : wasm.job(job); - if (file.length === 0) - throw thrown({ code: 'invalid', message: `the image could not be written as ${format}` }); - return file; + return (await this.inner.encode(format, options.quality)) as Uint8Array; } destroy(): void { diff --git a/npm/test/jobs.test.mjs b/npm/test/jobs.test.mjs index ebdf78d1..95c0b01b 100644 --- a/npm/test/jobs.test.mjs +++ b/npm/test/jobs.test.mjs @@ -94,7 +94,7 @@ test('worker construction failure rejects, and later calls can recover', async ( t.mock.timers.tick(30_000); }); -test('queued jobs continue after a busy worker fails', async (t) => { +test('jobs at once get a worker each; a failed one rejects only its job; idle ones are reused', async (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }); const workers = []; const run = jobs(module, () => { @@ -102,18 +102,19 @@ test('queued jobs continue after a busy worker fails', async (t) => { workers.push(worker); return worker; }); - const answers = Array.from({ length: 8 }, (_, i) => run(new Uint8Array([i]))); - const settled = Promise.allSettled(answers); - const limit = workers.length; - assert(limit >= 1 && limit <= 4); + const lost = assert.rejects(run(new Uint8Array([0])), /lost worker/); + const kept = run(new Uint8Array([1])); + assert.equal(workers.length, 2, 'nothing waits in the page: latkit hands over only what runs'); workers[0].onerror({ preventDefault() {}, message: 'lost worker' }); - for (const worker of workers) { - if (worker.terminated) continue; - worker.answer({ ready: true }); - for (let i = 0; i < 8; i++) worker.answer({ bytes: new Uint8Array([1]) }); - } - const results = await settled; - assert.equal(results.filter((result) => result.status === 'rejected').length, 1); + await lost; + workers[1].answer({ ready: true }); + workers[1].answer({ bytes: new Uint8Array([1]) }); + assert.deepEqual(await kept, new Uint8Array([1])); + const again = run(new Uint8Array([2])); + assert.equal(workers.length, 2, 'the idle worker takes the next job'); + assert.deepEqual(workers[1].sent.at(-1), new Uint8Array([2])); + workers[1].answer({ bytes: new Uint8Array([3]) }); + assert.deepEqual(await again, new Uint8Array([3])); t.mock.timers.tick(30_000); assert(workers.every((worker) => worker.terminated)); }); From 30d7285d9a108ca0231dba9c868c5e06599e1900 Mon Sep 17 00:00:00 2001 From: Wyatt Lowery Date: Sat, 24 Oct 2026 08:44:34 -0500 Subject: [PATCH 03/14] perf enhance v2 --- .changeset/drawing-pages.md | 5 + ...dia-geforce-rtx-2080-super-chrome-156.json | 982 +++++++------- benchmark/baselines/work.json | 1156 ++++++++--------- benchmark/judge.ts | 2 +- benchmark/page/scenarios.ts | 12 +- crates/latkit/build/shaders.rs | 13 +- crates/latkit/shaders/draw/areas.wesl | 2 +- crates/latkit/shaders/draw/backdrop.wesl | 17 +- crates/latkit/shaders/draw/points.wesl | 33 +- crates/latkit/shaders/draw/shade.wesl | 21 - crates/latkit/shaders/draw/strokes.wesl | 28 +- crates/latkit/shaders/draw/triangles.wesl | 17 +- crates/latkit/shaders/lib/daylight.wesl | 19 + crates/latkit/shaders/lib/marker.wesl | 5 +- crates/latkit/shaders/lib/points.wesl | 12 +- crates/latkit/shaders/lib/project.wesl | 2 +- crates/latkit/shaders/lib/shade.wesl | 65 - crates/latkit/shaders/lib/types.wesl | 27 +- crates/latkit/src/derive/mod.rs | 9 +- crates/latkit/src/derive/order.rs | 22 +- crates/latkit/src/derive/pyramid.rs | 51 +- crates/latkit/src/derive/tiled.rs | 105 +- crates/latkit/src/gpu/arena.rs | 3 +- crates/latkit/src/gpu/packed.rs | 26 +- crates/latkit/src/gpu/pipelines.rs | 42 +- crates/latkit/src/scene/mark.rs | 35 +- crates/latkit/src/scene/tests.rs | 4 +- crates/latkit/src/scene/theme.rs | 15 +- crates/latkit/src/scene/vertices.rs | 15 + crates/latkit/src/view/drawing.rs | 63 +- crates/latkit/src/view/event.rs | 2 +- crates/latkit/src/view/frame.rs | 101 +- crates/latkit/src/view/labels.rs | 3 +- crates/latkit/src/view/marks.rs | 5 + crates/latkit/src/view/mod.rs | 27 +- crates/latkit/src/view/passes.rs | 8 +- crates/latkit/src/view/plan.rs | 15 +- crates/latkit/src/view/query.rs | 4 +- crates/latkit/src/view/resources.rs | 92 +- crates/latkit/src/view/shadows.rs | 160 --- crates/latkit/src/view/sync.rs | 95 +- crates/latkit/src/view/transition.rs | 2 +- crates/latkit/src/view/uniforms.rs | 37 +- crates/latkit/src/view/variants.rs | 25 +- crates/latkit/tests/graph.rs | 34 +- crates/latkit/tests/render.rs | 2 +- crates/latkit/tests/scale.rs | 98 +- crates/latkit/tests/shadows.rs | 90 -- crates/latkit/tests/validation.rs | 8 + docs/_static/performance.csv | 28 +- docs/_static/theme.json | 12 - docs/guide.md | 16 +- docs/performance.md | 1 + docs/reference/types.md | 39 +- npm/examples/src/examples/colormaps/index.ts | 228 +++- npm/examples/src/examples/diagram/index.ts | 378 +++++- npm/examples/src/examples/index.ts | 1 - .../src/examples/linked-views/index.ts | 116 +- npm/examples/src/examples/live-data/index.ts | 267 +++- npm/examples/src/examples/networks/index.ts | 591 ++++++++- npm/examples/src/examples/playback/index.ts | 222 ---- npm/examples/src/examples/power-flow/index.ts | 139 +- npm/examples/src/examples/soft-body/index.ts | 64 +- npm/examples/src/ui/controls.ts | 48 +- npm/examples/src/ui/export.ts | 73 +- npm/examples/src/ui/meter.ts | 43 +- npm/examples/src/ui/style.css | 15 + npm/schema.json | 49 +- npm/src/inputs.ts | 30 +- npm/src/outputs.ts | 2 +- npm/test/scenes.js | 13 +- 71 files changed, 3458 insertions(+), 2533 deletions(-) delete mode 100644 crates/latkit/shaders/draw/shade.wesl create mode 100644 crates/latkit/shaders/lib/daylight.wesl delete mode 100644 crates/latkit/shaders/lib/shade.wesl delete mode 100644 crates/latkit/src/view/shadows.rs delete mode 100644 crates/latkit/tests/shadows.rs delete mode 100644 npm/examples/src/examples/playback/index.ts diff --git a/.changeset/drawing-pages.md b/.changeset/drawing-pages.md index 8cfbfefa..68190bd5 100644 --- a/.changeset/drawing-pages.md +++ b/.changeset/drawing-pages.md @@ -13,6 +13,11 @@ A table too large to draw whole draws pages of its order, which keep their place - Changed: a vertex shows from as far out as the most important edge ending at it, so an edge drawn always shows with its ends; until tiles land, the first look at a table draws the vertices its edges sampled end at first. - Changed: a hit names the row and the place the frame it answers drew, however the data, the scene, or the camera changed before it landed. - Changed: a GPU's views draw one frame together: `Budget.uploadPerFrame` bounds what all of them upload in it, and jobs go to a worker only as one frees, those nothing waits for any more dropped first. +- Changed: an area's triangles, and on the earth a line's path, are made a page of its field at a time and kept with the page: a write shapes only the rows it wrote, and a page every row of which needs triangles is made whole in a worker. Vector tiles arrive shaped, from the job that decodes them. +- Changed: setting rows of a table, or removing some, keeps its index: ids already there are never hashed again. +- Changed: `shadow` is an option of vertices in a diagram alone: blocks and groups drop a shadow onto the sheet, as the theme's `shadow` (`color`, `lift`) says. +- Removed: shadows cast onto the ground and onto faces: `shadow` on edges, lines, areas, and faces, and the theme's `shadow.light` and `shadow.softness`. Faces still darken as they turn away, as `theme.shading` says. - Changed: a GPU decides its antialiasing once, from the screen it serves, so a browser zoom compiles nothing. - Changed: zooming at the limit a fit sets keeps the center where it was. - Fixed: on a flat map, a table drawn through tiles draws alike at every latitude, where a band of tiles drew finer than the rest; vector tiles split alike too. +- Fixed: a view whose next version finds no room, though it draws as little as it may, lets go of what it draws so that version lands; where even that fits nothing, it tells a `limit` error and rests instead of waiting forever. Rows that land no finer than the coarsest tiles, as edges spanning the map, draw as many as fit. diff --git a/benchmark/baselines/nvidia-turing-nvidia-geforce-rtx-2080-super-chrome-156.json b/benchmark/baselines/nvidia-turing-nvidia-geforce-rtx-2080-super-chrome-156.json index 0e5af80d..d3f5e4a2 100644 --- a/benchmark/baselines/nvidia-turing-nvidia-geforce-rtx-2080-super-chrome-156.json +++ b/benchmark/baselines/nvidia-turing-nvidia-geforce-rtx-2080-super-chrome-156.json @@ -4,15 +4,15 @@ "start": { "10000": { "init": { - "time": 255.5, + "time": 258.1999999284744, "gpu": 0 }, "data": { - "time": 19.399999976158142, + "time": 18, "gpu": 0 }, "load": { - "time": 454.39999997615814, + "time": 504.2000000476837, "gpu": 0 } } @@ -20,531 +20,531 @@ "points": { "1000": { "init": { - "time": 109.30000001192093, + "time": 112.70000004768372, "gpu": 0 }, "data": { - "time": 13.899999976158142, + "time": 14.100000023841858, "gpu": 0 }, "load": { - "time": 438.69999998807907, + "time": 461.60000002384186, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.073728 + "gpu": 0.070464 }, "zoom": { "time": 0.099999994, - "gpu": 0.067584 + "gpu": 0.063488 }, "revisit": { - "time": 3.600000023841858, - "gpu": 0.069632 + "time": 3.5, + "gpu": 0.066688 }, "restyle": { - "time": 4.900000035762787, - "gpu": 0.071328 + "time": 4.300000071525574, + "gpu": 0.067584 }, "one row": { "time": 0.099999994, - "gpu": 0.085664 + "gpu": 0.08128 }, "every row": { - "time": 3.600000023841858, - "gpu": 0.08576 + "time": 3.299999952316284, + "gpu": 0.081536 }, "first pick": { - "time": 8.400000035762787, - "gpu": 0.085536 + "time": 8.700000047683716, + "gpu": 0.081536 }, "pick": { - "time": 7.099999964237213, - "gpu": 0.085664 + "time": 8.400000095367432, + "gpu": 0.081408 }, "hover": { - "time": 10.5, - "gpu": 0.08576 + "time": 12.299999952316284, + "gpu": 0.081536 }, "select": { - "time": 6, - "gpu": 0.085504 + "time": 9.399999976158142, + "gpu": 0.081664 }, "append": { "time": 0.099999994, - "gpu": 0.114688 + "gpu": 0.111552 } }, "10000": { "init": { - "time": 118.5, + "time": 115.69999992847443, "gpu": 0 }, "data": { - "time": 14.100000023841858, + "time": 14, "gpu": 0 }, "load": { - "time": 448.69999998807907, + "time": 455.3000000715256, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.145792 + "gpu": 0.146656 }, "zoom": { "time": 0.099999994, - "gpu": 0.088064 + "gpu": 0.082432 }, "revisit": { - "time": 3, - "gpu": 0.10752 + "time": 4.5, + "gpu": 0.104224 }, "restyle": { - "time": 4.800000011920929, - "gpu": 0.1144 + "time": 5.099999904632568, + "gpu": 0.111872 }, "one row": { "time": 0.099999994, - "gpu": 0.168928 + "gpu": 0.162592 }, "every row": { - "time": 4.5, - "gpu": 0.169088 + "time": 3.799999952316284, + "gpu": 0.162688 }, "first pick": { - "time": 8.899999976158142, - "gpu": 0.168736 + "time": 9.299999952316284, + "gpu": 0.162688 }, "pick": { - "time": 6.599999964237213, - "gpu": 0.168544 + "time": 7, + "gpu": 0.163008 }, "hover": { - "time": 11, - "gpu": 0.16848 + "time": 10.5, + "gpu": 0.163424 }, "select": { - "time": 8.300000011920929, - "gpu": 0.168576 + "time": 8.699999928474426, + "gpu": 0.163424 }, "append": { - "time": 0.39999998, - "gpu": 0.200288 + "time": 0.19999999, + "gpu": 0.19456 } }, "100000": { "init": { - "time": 118.60000002384186, + "time": 120.70000004768372, "gpu": 0 }, "data": { - "time": 16.099999964237213, + "time": 16.5, "gpu": 0 }, "load": { - "time": 440.69999998807907, + "time": 479.90000009536743, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.872448 + "gpu": 0.869184 }, "zoom": { "time": 0.099999994, - "gpu": 0.27968 + "gpu": 0.274688 }, "revisit": { - "time": 3, - "gpu": 0.484864 + "time": 3.399999976158142, + "gpu": 0.482592 }, "restyle": { - "time": 4.899999976158142, - "gpu": 0.551456 + "time": 4.699999928474426, + "gpu": 0.54848 }, "one row": { - "time": 0.19999999, - "gpu": 0.999936 + "time": 0.099999994, + "gpu": 0.974144 }, "every row": { - "time": 8.100000023841858, - "gpu": 0.999968 + "time": 7.599999904632568, + "gpu": 0.973952 }, "first pick": { - "time": 10.100000023841858, - "gpu": 0.999968 + "time": 9.5, + "gpu": 0.973952 }, "pick": { "time": 6.600000023841858, - "gpu": 0.999456 + "gpu": 0.973952 }, "hover": { - "time": 14.199999988079071, - "gpu": 0.999136 + "time": 13.899999976158142, + "gpu": 0.974304 }, "select": { - "time": 7.899999976158142, - "gpu": 0.998944 + "time": 9.100000023841858, + "gpu": 0.974688 }, "append": { - "time": 0.7, - "gpu": 1.05792 + "time": 0.3, + "gpu": 1.044864 } }, "1000000": { "init": { - "time": 112.80000001192093, + "time": 125.39999997615814, "gpu": 0 }, "data": { - "time": 31.599999964237213, + "time": 34.10000002384186, "gpu": 0 }, "load": { - "time": 482.60000002384186, - "gpu": 0.657568 + "time": 516.7000000476837, + "gpu": 1.14432 }, "pan": { "time": 0.099999994, - "gpu": 0.185344 + "gpu": 0.180352 }, "zoom": { "time": 0.099999994, - "gpu": 0.479232 + "gpu": 0.475136 }, "revisit": { - "time": 3.600000023841858, - "gpu": 0.38944 + "time": 3.0999999046325684, + "gpu": 0.388224 }, "restyle": { - "time": 6.200000047683716, - "gpu": 0.377792 + "time": 4.399999976158142, + "gpu": 0.374048 }, "one row": { "time": 0.099999994, - "gpu": 0.196768 + "gpu": 0.193312 }, "every row": { - "time": 36.19999998807907, - "gpu": 0.19664 + "time": 33.10000002384186, + "gpu": 0.193408 }, "first pick": { - "time": 9.699999988079071, - "gpu": 0.19664 + "time": 11, + "gpu": 0.193696 }, "pick": { - "time": 7.399999976158142, - "gpu": 0.196768 + "time": 6.799999952316284, + "gpu": 0.193728 }, "hover": { - "time": 13.699999988079071, - "gpu": 0.197088 + "time": 14.5, + "gpu": 0.193792 }, "select": { - "time": 8.899999976158142, - "gpu": 0.197792 + "time": 8.099999904632568, + "gpu": 0.194304 }, "append": { - "time": 0.19999999, - "gpu": 0.243968 + "time": 0.3, + "gpu": 0.24192 } }, "2000000": { "init": { - "time": 108.59999996423721, + "time": 117, "gpu": 0 }, "data": { - "time": 44.90000003576279, + "time": 48.60000002384186, "gpu": 0 }, "load": { - "time": 746.8999999761581, - "gpu": 1.290688 + "time": 769.1000000238419, + "gpu": 1.263744 }, "pan": { "time": 0.099999994, - "gpu": 0.18432 + "gpu": 0.180512 }, "zoom": { - "time": 0.19999999, - "gpu": 0.490496 + "time": 0.099999994, + "gpu": 0.479552 }, "revisit": { - "time": 3.699999988079071, - "gpu": 0.397312 + "time": 4, + "gpu": 0.395264 }, "restyle": { - "time": 7.100000023841858, - "gpu": 0.376832 + "time": 6.699999928474426, + "gpu": 0.373376 }, "one row": { - "time": 0.19999999, - "gpu": 0.196608 + "time": 0.099999994, + "gpu": 0.193184 }, "every row": { - "time": 56.40000003576279, - "gpu": 0.196608 + "time": 48.800000071525574, + "gpu": 0.193248 }, "first pick": { - "time": 10, - "gpu": 0.196608 + "time": 9.899999976158142, + "gpu": 0.19328 }, "pick": { - "time": 6.599999964237213, - "gpu": 0.196608 + "time": 6.5, + "gpu": 0.193312 }, "hover": { - "time": 13.600000023841858, - "gpu": 0.196864 + "time": 12.300000071525574, + "gpu": 0.194048 }, "select": { - "time": 8.5, - "gpu": 0.19696 + "time": 7.899999976158142, + "gpu": 0.19456 }, "append": { - "time": 0.5, - "gpu": 0.243712 + "time": 0.39999998, + "gpu": 0.241568 } } }, "grid": { "1000": { "init": { - "time": 128.10000002384186, + "time": 129.20000004768372, "gpu": 0 }, "data": { - "time": 14.199999988079071, + "time": 14, "gpu": 0 }, "load": { - "time": 510, + "time": 546.3999999761581, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.122528 + "gpu": 0.125024 }, "zoom": { "time": 0.099999994, - "gpu": 0.124256 + "gpu": 0.126944 }, "revisit": { - "time": 3.300000011920929, - "gpu": 0.123456 + "time": 4.300000071525574, + "gpu": 0.126464 }, "one row": { "time": 0.099999994, - "gpu": 0.131104 + "gpu": 0.134144 }, "first pick": { - "time": 9, - "gpu": 0.13136 + "time": 8, + "gpu": 0.134144 }, "pick": { - "time": 5.699999988079071, - "gpu": 0.13136 + "time": 6.100000023841858, + "gpu": 0.1344 }, "hover": { - "time": 11.400000035762787, - "gpu": 0.13136 + "time": 14, + "gpu": 0.1344 }, "append": { - "time": 0.19999999, - "gpu": 0.208384 + "time": 0.099999994, + "gpu": 0.21504 } }, "10000": { "init": { - "time": 133.9000000357628, + "time": 129.89999997615814, "gpu": 0 }, "data": { - "time": 16.100000023841858, + "time": 16.299999952316284, "gpu": 0 }, "load": { - "time": 549.8000000119209, + "time": 564.5, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.178176 + "gpu": 0.182144 }, "zoom": { "time": 0.099999994, - "gpu": 0.17408 + "gpu": 0.177632 }, "revisit": { - "time": 3.100000023841858, - "gpu": 0.176064 + "time": 3.4000000953674316, + "gpu": 0.178976 }, "one row": { "time": 0.099999994, - "gpu": 0.187456 + "gpu": 0.190144 }, "first pick": { - "time": 8.300000011920929, - "gpu": 0.187488 + "time": 9.799999952316284, + "gpu": 0.190048 }, "pick": { - "time": 6.199999988079071, - "gpu": 0.187808 + "time": 5.600000023841858, + "gpu": 0.190048 }, "hover": { - "time": 11.399999976158142, - "gpu": 0.187328 + "time": 12.300000071525574, + "gpu": 0.189984 }, "append": { - "time": 0.3, - "gpu": 0.280128 + "time": 0.19999999, + "gpu": 0.2848 } }, "100000": { "init": { - "time": 126.69999998807907, + "time": 123.90000009536743, "gpu": 0 }, "data": { - "time": 19.700000047683716, + "time": 19.59999990463257, "gpu": 0 }, "load": { - "time": 514.3000000119209, + "time": 522.7000000476837, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.601696 + "gpu": 0.609536 }, "zoom": { "time": 0.099999994, - "gpu": 0.524288 + "gpu": 0.53248 }, "revisit": { - "time": 3.400000035762787, - "gpu": 0.556224 + "time": 4.399999976158142, + "gpu": 0.566976 }, "one row": { - "time": 0.19999999, - "gpu": 0.634496 + "time": 0.099999994, + "gpu": 0.62048 }, "first pick": { - "time": 12.600000023841858, - "gpu": 0.633824 + "time": 11.400000095367432, + "gpu": 0.620448 }, "pick": { - "time": 6.199999988079071, - "gpu": 0.633824 + "time": 6.699999928474426, + "gpu": 0.620544 }, "hover": { - "time": 14.5, - "gpu": 0.63424 + "time": 16.700000047683716, + "gpu": 0.620608 }, "append": { - "time": 0.7, - "gpu": 0.89696 + "time": 0.3, + "gpu": 0.905216 } }, "1000000": { "init": { - "time": 131.39999997615814, + "time": 127.70000004768372, "gpu": 0 }, "data": { - "time": 61.200000047683716, + "time": 62.800000071525574, "gpu": 0 }, "load": { - "time": 997.5, - "gpu": 0.778208 + "time": 1029.1000000238419, + "gpu": 0.337152 }, "pan": { "time": 0.19999999, - "gpu": 0.610304 + "gpu": 0.616896 }, "zoom": { - "time": 0.3, - "gpu": 1.240192 + "time": 0.39999998, + "gpu": 1.482752 }, "revisit": { - "time": 7.600000023841858, - "gpu": 1.046208 + "time": 8.299999952316284, + "gpu": 1.554784 }, "one row": { - "time": 0.3, - "gpu": 0.63632 + "time": 0.5, + "gpu": 0.641024 }, "first pick": { - "time": 10, - "gpu": 0.63648 + "time": 12.700000047683716, + "gpu": 0.640992 }, "pick": { - "time": 5.800000011920929, - "gpu": 0.63648 + "time": 7.100000023841858, + "gpu": 0.641024 }, "hover": { - "time": 11, - "gpu": 0.639264 + "time": 15.100000023841858, + "gpu": 0.641024 }, "append": { - "time": 0.5, - "gpu": 0.820704 + "time": 0.6, + "gpu": 0.827296 } }, "2000000": { "init": { - "time": 149.70000004768372, + "time": 160.60000002384186, "gpu": 0 }, "data": { - "time": 105, + "time": 122.5, "gpu": 0 }, "load": { - "time": 1634.8999999761581, - "gpu": 0.797664 + "time": 1619.1000000238419, + "gpu": 0.34144 }, "pan": { "time": 0.19999999, - "gpu": 0.62384 + "gpu": 0.628736 }, "zoom": { - "time": 0.3, - "gpu": 1.315808 + "time": 0.39999998, + "gpu": 1.326848 }, "revisit": { - "time": 6.900000035762787, - "gpu": 1.083584 + "time": 8.799999952316284, + "gpu": 1.109728 }, "one row": { - "time": 0.3, - "gpu": 0.641984 + "time": 0.5, + "gpu": 0.802816 }, "first pick": { - "time": 11.900000035762787, - "gpu": 0.88976 + "time": 13.100000023841858, + "gpu": 0.797248 }, "pick": { - "time": 6.399999976158142, - "gpu": 0.89088 + "time": 8.100000023841858, + "gpu": 0.796928 }, "hover": { - "time": 13.699999988079071, - "gpu": 0.896128 + "time": 15.799999952316284, + "gpu": 0.796928 }, "append": { - "time": 0.39999998, - "gpu": 1.178016 + "time": 0.79999995, + "gpu": 1.186592 } } }, "labels": { "10000": { "init": { - "time": 143.9000000357628, + "time": 124.39999997615814, "gpu": 0 }, "data": { @@ -552,972 +552,972 @@ "gpu": 0 }, "load": { - "time": 505.9000000357628, - "gpu": 0.296192 + "time": 504.7999999523163, + "gpu": 0.182528 }, "pan": { - "time": 0.7, - "gpu": 0.33792 + "time": 0.6, + "gpu": 0.318592 }, "zoom": { - "time": 1.1, - "gpu": 0.30176 + "time": 0.9, + "gpu": 0.290368 } }, "100000": { "init": { - "time": 145.0999999642372, + "time": 129.39999997615814, "gpu": 0 }, "data": { - "time": 63.30000001192093, + "time": 62.60000002384186, "gpu": 0 }, "load": { - "time": 651.1999999880791, - "gpu": 1.483008 + "time": 561.5999999046326, + "gpu": 1.335296 }, "pan": { - "time": 1, - "gpu": 1.448224 + "time": 0.7, + "gpu": 1.372416 }, "zoom": { - "time": 1.5999999, - "gpu": 0.719584 + "time": 1.5, + "gpu": 0.644864 } }, "1000000": { "init": { - "time": 131.10000002384186, + "time": 136.70000004768372, "gpu": 0 }, "data": { - "time": 514.4000000357628, + "time": 501, "gpu": 0 }, "load": { - "time": 784, - "gpu": 0.687072 + "time": 685.2999999523163, + "gpu": 0.684608 }, "pan": { - "time": 1.2, - "gpu": 0.446272 + "time": 0.79999995, + "gpu": 0.41456 }, "zoom": { - "time": 1.7, - "gpu": 1.067584 + "time": 1.5, + "gpu": 0.88896 } } }, "lanes": { "100": { "init": { - "time": 151.60000002384186, + "time": 124.5, "gpu": 0 }, "data": { - "time": 14.600000023841858, + "time": 13.799999952316284, "gpu": 0 }, "load": { - "time": 509.5, + "time": 517.2999999523163, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.081184 + "gpu": 0.104928 }, "first pick": { - "time": 10.800000011920929, - "gpu": 0.08128 + "time": 9.400000095367432, + "gpu": 0.105024 }, "pick": { - "time": 9.399999976158142, - "gpu": 0.08128 + "time": 6.399999976158142, + "gpu": 0.10496 } }, "1000": { "init": { - "time": 134.5999999642372, + "time": 126, "gpu": 0 }, "data": { - "time": 14.899999976158142, + "time": 13.400000095367432, "gpu": 0 }, "load": { - "time": 569.1000000238419, + "time": 515.3000000715256, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.16976 + "gpu": 0.153312 }, "first pick": { - "time": 10.5, - "gpu": 0.168608 + "time": 9.300000071525574, + "gpu": 0.1536 }, "pick": { - "time": 6.399999976158142, - "gpu": 0.169376 + "time": 7.199999928474426, + "gpu": 0.1536 } }, "10000": { "init": { - "time": 125.60000002384186, + "time": 121.5, "gpu": 0 }, "data": { - "time": 14.899999976158142, + "time": 13.700000047683716, "gpu": 0 }, "load": { - "time": 517.4000000357628, + "time": 526.6000000238419, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 1.91296 + "gpu": 0.14768 }, "first pick": { - "time": 15.900000035762787, - "gpu": 1.880064 + "time": 8.600000023841858, + "gpu": 0.140768 }, "pick": { - "time": 13.399999976158142, - "gpu": 1.924768 + "time": 5.800000071525574, + "gpu": 0.136992 } }, "100000": { "init": { - "time": 124.19999998807907, + "time": 128.39999997615814, "gpu": 0 }, "data": { - "time": 16.099999964237213, + "time": 16.699999928474426, "gpu": 0 }, "load": { - "time": 496.5, + "time": 530.2999999523163, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 20.007263 + "gpu": 0.177952 }, "first pick": { - "time": 78.59999996423721, - "gpu": 19.996544 + "time": 7.699999928474426, + "gpu": 0.175968 }, "pick": { - "time": 72.59999996423721, - "gpu": 20.10176 + "time": 6.300000071525574, + "gpu": 0.17408 } } }, "areas": { "100": { "init": { - "time": 135.9000000357628, + "time": 127.39999997615814, "gpu": 0 }, "data": { - "time": 15.300000011920929, + "time": 13.5, "gpu": 0 }, "load": { - "time": 464.39999997615814, + "time": 478.5, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.14144 + "gpu": 0.137216 }, "zoom": { "time": 0.099999994, - "gpu": 0.139392 + "gpu": 0.135168 }, "one row": { "time": 0.099999994, - "gpu": 0.296448 + "gpu": 0.291008 }, "first pick": { - "time": 9.300000011920929, - "gpu": 0.296672 + "time": 6.700000047683716, + "gpu": 0.291136 }, "pick": { - "time": 7.699999988079071, - "gpu": 0.296544 + "time": 5.799999952316284, + "gpu": 0.290976 } }, "1000": { "init": { - "time": 124.69999998807907, + "time": 122.20000004768372, "gpu": 0 }, "data": { - "time": 17.400000035762787, + "time": 16.5, "gpu": 0 }, "load": { - "time": 450.9000000357628, + "time": 473.2999999523163, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.219808 + "gpu": 0.21536 }, "zoom": { "time": 0.099999994, - "gpu": 0.218048 + "gpu": 0.214112 }, "one row": { "time": 0.099999994, - "gpu": 0.3584 + "gpu": 0.353632 }, "first pick": { - "time": 7, - "gpu": 0.3584 + "time": 7.399999976158142, + "gpu": 0.353568 }, "pick": { - "time": 7.300000011920929, - "gpu": 0.3584 + "time": 6.899999976158142, + "gpu": 0.353216 } }, "10000": { "init": { - "time": 123.70000004768372, + "time": 121.79999995231628, "gpu": 0 }, "data": { - "time": 31, + "time": 27.799999952316284, "gpu": 0 }, "load": { - "time": 511.5, + "time": 497.39999997615814, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.604384 + "gpu": 0.600352 }, "zoom": { "time": 0.099999994, - "gpu": 0.602752 + "gpu": 0.598752 }, "one row": { "time": 0.099999994, - "gpu": 0.728096 + "gpu": 0.722944 }, "first pick": { - "time": 8.899999976158142, - "gpu": 0.727712 + "time": 7.699999928474426, + "gpu": 0.722944 }, "pick": { - "time": 6.800000011920929, - "gpu": 0.727648 + "time": 7.199999928474426, + "gpu": 0.722784 } }, "30000": { "init": { - "time": 128.5999999642372, + "time": 132.60000002384186, "gpu": 0 }, "data": { - "time": 56.5, + "time": 55.39999997615814, "gpu": 0 }, "load": { - "time": 704.5, - "gpu": 0.096544 + "time": 672.5, + "gpu": 0.092128 }, "pan": { "time": 0.099999994, - "gpu": 1.216128 + "gpu": 1.180448 }, "zoom": { "time": 0.099999994, - "gpu": 1.213248 + "gpu": 1.178208 }, "one row": { "time": 0.099999994, - "gpu": 1.652288 + "gpu": 1.536544 }, "first pick": { - "time": 6.800000011920929, - "gpu": 1.645248 + "time": 7.600000023841858, + "gpu": 1.532544 }, "pick": { - "time": 6.099999964237213, - "gpu": 1.643008 + "time": 6.099999904632568, + "gpu": 1.532544 } } }, "tiles": { "16": { "init": { - "time": 134.60000002384186, + "time": 119.09999990463257, "gpu": 0 }, "data": { - "time": 12.5, + "time": 10.899999976158142, "gpu": 0 }, "load": { - "time": 581.1000000238419, + "time": 478.39999997615814, "gpu": 0 }, "pan": { - "time": 0.3, - "gpu": 0.122976 + "time": 0.099999994, + "gpu": 0.119488 }, "zoom": { - "time": 0.3, - "gpu": 0.11824 + "time": 0.19999999, + "gpu": 0.114688 } }, "256": { "init": { - "time": 129.30000001192093, + "time": 123.39999997615814, "gpu": 0 }, "data": { - "time": 11.899999976158142, + "time": 10.299999952316284, "gpu": 0 }, "load": { - "time": 583.3999999761581, + "time": 479.10000002384186, "gpu": 0 }, "pan": { - "time": 0.19999999, - "gpu": 0.132576 + "time": 0.099999994, + "gpu": 0.129248 }, "zoom": { - "time": 0.3, - "gpu": 0.123392 + "time": 0.19999999, + "gpu": 0.118592 } }, "4096": { "init": { - "time": 123.19999998807907, + "time": 122.60000002384186, "gpu": 0 }, "data": { - "time": 12.100000023841858, + "time": 10.699999928474426, "gpu": 0 }, "load": { - "time": 549.8000000119209, + "time": 507.2999999523163, "gpu": 0 }, "pan": { - "time": 0.3, - "gpu": 0.17152 + "time": 0.19999999, + "gpu": 0.166176 }, "zoom": { - "time": 0.3, - "gpu": 0.135168 + "time": 0.19999999, + "gpu": 0.132096 } } }, "diagram": { "100": { "init": { - "time": 143.29999995231628, + "time": 122.89999997615814, "gpu": 0 }, "data": { - "time": 14.199999988079071, + "time": 14.700000047683716, "gpu": 0 }, "load": { - "time": 497.19999998807907, + "time": 499.90000009536743, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.08192 + "gpu": 0.079872 }, "edit": { "time": 0.39999998, - "gpu": 0.1208 + "gpu": 0.118784 }, "arrange": { - "time": 12.400000035762787, - "gpu": 0.121248 + "time": 12, + "gpu": 0.119456 } }, "1000": { "init": { - "time": 139.0999999642372, + "time": 123, "gpu": 0 }, "data": { - "time": 15.699999988079071, + "time": 15.399999976158142, "gpu": 0 }, "load": { - "time": 506.19999998807907, + "time": 511.8000000715256, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.092576 + "gpu": 0.091328 }, "edit": { "time": 0.6, - "gpu": 0.137376 + "gpu": 0.136736 }, "arrange": { - "time": 14.900000035762787, - "gpu": 0.138208 + "time": 19, + "gpu": 0.137088 } }, "10000": { "init": { - "time": 141.30000001192093, + "time": 141.79999995231628, "gpu": 0 }, "data": { - "time": 20.5, + "time": 22, "gpu": 0 }, "load": { - "time": 593.6999999880791, + "time": 574.6000000238419, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.192032 + "gpu": 0.188768 }, "edit": { - "time": 2.8999999, - "gpu": 0.264256 + "time": 2, + "gpu": 0.262976 }, "arrange": { - "time": 56.40000003576279, - "gpu": 0.264608 + "time": 60.200000047683716, + "gpu": 0.263392 } } }, "traces": { "10": { "init": { - "time": 130.69999998807907, + "time": 125.30000007152557, "gpu": 0 }, "data": { - "time": 17.19999998807907, + "time": 16.899999976158142, "gpu": 0 }, "load": { - "time": 572.7999999523163, + "time": 560.1999999284744, "gpu": 0 }, "stream": { "time": 0.099999994, - "gpu": 0.096256 + "gpu": 0.096032 }, "scrub": { "time": 0.099999994, - "gpu": 0.075776 + "gpu": 0.075744 }, "narrow": { "time": 0.099999994, - "gpu": 0.088064 + "gpu": 0.088416 }, "hover": { - "time": 9.800000011920929, - "gpu": 0.079776 + "time": 10.5, + "gpu": 0.078496 } }, "100": { "init": { - "time": 130.10000002384186, + "time": 123.19999992847443, "gpu": 0 }, "data": { - "time": 19.100000023841858, + "time": 19.299999952316284, "gpu": 0 }, "load": { - "time": 518.5, + "time": 551.8999999761581, "gpu": 0 }, "stream": { "time": 0.099999994, - "gpu": 0.34176 + "gpu": 0.33792 }, "scrub": { "time": 0.099999994, - "gpu": 0.28304 + "gpu": 0.282496 }, "narrow": { "time": 0.099999994, - "gpu": 0.256 + "gpu": 0.247808 }, "hover": { - "time": 13.5, - "gpu": 0.135168 + "time": 11.100000023841858, + "gpu": 0.134688 } }, "1000": { "init": { - "time": 135.10000002384186, + "time": 122.10000002384186, "gpu": 0 }, "data": { - "time": 36.89999997615814, + "time": 36.5, "gpu": 0 }, "load": { - "time": 544.8999999761581, + "time": 549.5, "gpu": 0 }, "stream": { "time": 0.099999994, - "gpu": 2.28896 + "gpu": 2.282528 }, "scrub": { "time": 0.099999994, - "gpu": 1.968192 + "gpu": 1.959424 }, "narrow": { "time": 0.099999994, - "gpu": 1.529856 + "gpu": 1.521664 }, "hover": { - "time": 13, - "gpu": 0.548384 + "time": 12, + "gpu": 0.54736 } }, "10000": { "init": { - "time": 139.60000002384186, + "time": 122.59999990463257, "gpu": 0 }, "data": { - "time": 240.69999998807907, + "time": 223.29999995231628, "gpu": 0 }, "load": { - "time": 561, + "time": 591.5, "gpu": 0 }, "stream": { - "time": 0.3, - "gpu": 23.959265 + "time": 0.19999999, + "gpu": 23.588608 }, "scrub": { "time": 0.19999999, - "gpu": 20.939264 + "gpu": 20.83088 }, "narrow": { - "time": 0.19999999, - "gpu": 15.726592 + "time": 0.099999994, + "gpu": 15.60352 }, "hover": { - "time": 22.100000023841858, - "gpu": 5.122048 + "time": 20.5, + "gpu": 5.154208 } } }, "history": { "1000": { "init": { - "time": 126.19999998807907, + "time": 120.89999997615814, "gpu": 0 }, "data": { - "time": 18.400000035762787, + "time": 16.899999976158142, "gpu": 0 }, "load": { - "time": 530.1000000238419, + "time": 545.7000000476837, "gpu": 0 }, "scrub": { "time": 0.099999994, - "gpu": 0.057472 + "gpu": 0.057344 }, "narrow": { - "time": 0.19999999, - "gpu": 0.074752 + "time": 0.099999994, + "gpu": 0.073792 } }, "10000": { "init": { - "time": 124.89999997615814, + "time": 127.39999997615814, "gpu": 0 }, "data": { - "time": 23.100000023841858, + "time": 22.100000023841858, "gpu": 0 }, "load": { - "time": 524, + "time": 541.3999999761581, "gpu": 0 }, "scrub": { "time": 0.099999994, - "gpu": 0.086304 + "gpu": 0.086656 }, "narrow": { - "time": 0.19999999, - "gpu": 0.094048 + "time": 0.099999994, + "gpu": 0.092896 } }, "100000": { "init": { - "time": 133.30000001192093, + "time": 132.19999992847443, "gpu": 0 }, "data": { - "time": 62.30000001192093, + "time": 57.800000071525574, "gpu": 0 }, "load": { - "time": 558.8999999761581, + "time": 563.7999999523163, "gpu": 0 }, "scrub": { - "time": 0.19999999, - "gpu": 0.293664 + "time": 0.099999994, + "gpu": 0.298464 }, "narrow": { - "time": 0.099999994, - "gpu": 0.220736 + "time": 0.19999999, + "gpu": 0.224256 } }, "1000000": { "init": { - "time": 131.19999998807907, + "time": 147.29999995231628, "gpu": 0 }, "data": { - "time": 390.80000001192093, + "time": 426.5, "gpu": 0 }, "load": { - "time": 534.5, + "time": 577.8000000715256, "gpu": 0 }, "scrub": { - "time": 0.099999994, - "gpu": 0.417024 + "time": 0.19999999, + "gpu": 0.42432 }, "narrow": { - "time": 0.099999994, - "gpu": 0.296512 + "time": 0.19999999, + "gpu": 0.302144 } } }, "transition": { "1000": { "init": { - "time": 124.60000002384186, + "time": 141.70000004768372, "gpu": 0 }, "data": { - "time": 13.199999988079071, + "time": 13.900000095367432, "gpu": 0 }, "load": { - "time": 446.7999999523163, + "time": 505.1999999284744, "gpu": 0 }, "move": { "time": 0.099999994, - "gpu": 0.083968 + "gpu": 0.081024 } }, "10000": { "init": { - "time": 130.69999998807907, + "time": 141.39999997615814, "gpu": 0 }, "data": { - "time": 14.5, + "time": 14.299999952316284, "gpu": 0 }, "load": { - "time": 448.89999997615814, + "time": 454.09999990463257, "gpu": 0 }, "move": { "time": 0.099999994, - "gpu": 0.17408 + "gpu": 0.170048 } }, "100000": { "init": { - "time": 141, + "time": 141.69999992847443, "gpu": 0 }, "data": { - "time": 15.899999976158142, + "time": 15.599999904632568, "gpu": 0 }, "load": { - "time": 436, + "time": 458.10000002384186, "gpu": 0 }, "move": { "time": 0.099999994, - "gpu": 1.267712 + "gpu": 1.259744 } }, "1000000": { "init": { - "time": 136.30000001192093, + "time": 137.10000002384186, "gpu": 0 }, "data": { - "time": 31.69999998807907, + "time": 35.89999997615814, "gpu": 0 }, "load": { - "time": 687.0999999642372, - "gpu": 0.790816 + "time": 730, + "gpu": 0.795104 }, "move": { - "time": 0.099999994, - "gpu": 0.226944 + "time": 0.19999999, + "gpu": 0.224576 } } }, "views": { "1": { "init": { - "time": 131.60000002384186, + "time": 146.60000002384186, "gpu": 0 }, "data": { - "time": 19.5, + "time": 21.90000009536743, "gpu": 0 }, "load": { - "time": 506.2999999523163, + "time": 626, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.927744 + "gpu": 0.925472 } }, "2": { "init": { - "time": 135.80000001192093, + "time": 148.60000002384186, "gpu": 0 }, "data": { - "time": 20.5, + "time": 24.700000047683716, "gpu": 0 }, "load": { - "time": 536.8000000119209, + "time": 590.5, "gpu": 0 }, "pan": { - "time": 0.199999988, - "gpu": 1.502304 + "time": 0.299999984, + "gpu": 1.507392 } }, "4": { "init": { - "time": 121, + "time": 134.39999997615814, "gpu": 0 }, "data": { - "time": 19.700000047683716, + "time": 21.199999928474426, "gpu": 0 }, "load": { - "time": 522.3999999761581, + "time": 592.7999999523163, "gpu": 0 }, "pan": { - "time": 0.199999988, - "gpu": 2.491552 + "time": 0.299999984, + "gpu": 2.052384 } }, "8": { "init": { - "time": 127.09999996423721, + "time": 137.20000004768372, "gpu": 0 }, "data": { - "time": 19.5, + "time": 21.5, "gpu": 0 }, "load": { - "time": 555.1999999880791, + "time": 621.3000000715256, "gpu": 0 }, "pan": { - "time": 0.299999984, - "gpu": 2.767808 + "time": 0.399999978, + "gpu": 2.7855999999999996 } } }, "budget": { "100000": { "init": { - "time": 131.5, + "time": 133.10000002384186, "gpu": 0 }, "data": { - "time": 20.30000001192093, + "time": 20.5, "gpu": 0 }, "load": { - "time": 523.8999999761581, + "time": 579.6999999284744, "gpu": 0 }, "pan": { - "time": 0.19999999, - "gpu": 0.237632 + "time": 0.099999994, + "gpu": 0.233568 }, "zoom": { "time": 0.19999999, - "gpu": 0.123616 + "gpu": 0.112192 } }, "1000000": { "init": { - "time": 132.69999998807907, + "time": 141.29999995231628, "gpu": 0 }, "data": { - "time": 63.10000002384186, + "time": 70.19999992847443, "gpu": 0 }, "load": { - "time": 979.4000000357628, - "gpu": 1.110016 + "time": 1026.1000000238419, + "gpu": 0.472896 }, "pan": { "time": 0.19999999, - "gpu": 0.249856 + "gpu": 0.243712 }, "zoom": { - "time": 0.19999999, - "gpu": 0.126976 + "time": 0.3, + "gpu": 0.123392 } }, "2000000": { "init": { - "time": 135.5, + "time": 148.69999992847443, "gpu": 0 }, "data": { - "time": 114.30000001192093, + "time": 122.5, "gpu": 0 }, "load": { - "time": 1573.1000000238419, - "gpu": 1.368416 + "time": 1653.5, + "gpu": 0.475168 }, "pan": { - "time": 0.19999999, - "gpu": 0.248288 + "time": 0.099999994, + "gpu": 0.24992 }, "zoom": { "time": 0.19999999, - "gpu": 0.126976 + "gpu": 0.125024 } } }, "shots": { "518400": { "init": { - "time": 116, + "time": 134.80000007152557, "gpu": 0 }, "data": { - "time": 20.69999998807907, + "time": 20.700000047683716, "gpu": 0 }, "load": { - "time": 542, + "time": 605.5, "gpu": 0 }, "image": { - "time": 26.600000023841858, - "gpu": 2.5392 + "time": 28.09999990463257, + "gpu": 2.307168 }, "png": { - "time": 10, - "gpu": 2.3034879999999998 + "time": 10.100000023841858, + "gpu": 2.303648 }, "video": { - "time": 213.4000000357628, - "gpu": 1.238176 + "time": 229.29999995231628, + "gpu": 1.243136 }, "held": { - "time": 154.60000002384186, - "gpu": 1.278848 + "time": 176.69999992847443, + "gpu": 1.2776960000000002 } }, "2073600": { "init": { - "time": 119.30000001192093, + "time": 114.39999997615814, "gpu": 0 }, "data": { - "time": 21.30000001192093, + "time": 20.199999928474426, "gpu": 0 }, "load": { - "time": 534.1999999880791, + "time": 550.6999999284744, "gpu": 0 }, "image": { - "time": 40.60000002384186, - "gpu": 2.54304 + "time": 40.10000002384186, + "gpu": 2.478912 }, "png": { - "time": 25.19999998807907, - "gpu": 2.303232 + "time": 26.399999976158142, + "gpu": 2.304704 }, "video": { - "time": 353.69999998807907, - "gpu": 1.282848 + "time": 326.5, + "gpu": 1.2824 }, "held": { - "time": 167.0999999642372, - "gpu": 1.33088 + "time": 179.40000009536743, + "gpu": 1.305952 } }, "8294400": { "init": { - "time": 142.9000000357628, + "time": 117.80000007152557, "gpu": 0 }, "data": { - "time": 23.700000047683716, + "time": 19.699999928474426, "gpu": 0 }, "load": { - "time": 651.6999999880791, + "time": 560, "gpu": 0 }, "image": { - "time": 71, + "time": 71.39999997615814, "gpu": 0 }, "png": { - "time": 82.80000001192093, - "gpu": 2.303776 + "time": 80.20000004768372, + "gpu": 2.3022720000000003 }, "video": { - "time": 705, - "gpu": 1.50528 + "time": 645.3000000715256, + "gpu": 1.5134720000000002 }, "held": { - "time": 203.9000000357628, - "gpu": 1.527072 + "time": 198.19999992847443, + "gpu": 1.533792 } } } diff --git a/benchmark/baselines/work.json b/benchmark/baselines/work.json index 2da4afa0..949671a2 100644 --- a/benchmark/baselines/work.json +++ b/benchmark/baselines/work.json @@ -27,7 +27,7 @@ }, "load": { "uploaded": 200000, - "written": 5228, + "written": 4124, "evaluated": 10000, "packed": 0, "readbacks": 1, @@ -35,7 +35,7 @@ "texts": 0, "pipelines": 27, "late": 0, - "resident": 327680 + "resident": 901120 } } }, @@ -67,7 +67,7 @@ }, "load": { "uploaded": 20000, - "written": 5220, + "written": 4048, "evaluated": 1000, "packed": 0, "readbacks": 1, @@ -75,11 +75,11 @@ "texts": 0, "pipelines": 27, "late": 0, - "resident": 20480 + "resident": 90112 }, "pan": { "uploaded": 0, - "written": 48640, + "written": 33280, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -87,11 +87,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 20480 + "resident": 90112 }, "zoom": { "uploaded": 0, - "written": 48640, + "written": 33280, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -99,11 +99,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 20480 + "resident": 90112 }, "revisit": { "uploaded": 0, - "written": 1216, + "written": 832, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -111,11 +111,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 20480 + "resident": 90112 }, "restyle": { "uploaded": 0, - "written": 2788, + "written": 2384, "evaluated": 1000, "packed": 0, "readbacks": 0, @@ -123,11 +123,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 20480 + "resident": 90112 }, "one row": { "uploaded": 160000, - "written": 48960, + "written": 33600, "evaluated": 40000, "packed": 0, "readbacks": 0, @@ -135,11 +135,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 184320 + "resident": 253952 }, "every row": { "uploaded": 4000, - "written": 1224, + "written": 840, "evaluated": 1000, "packed": 0, "readbacks": 0, @@ -147,11 +147,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 188416 + "resident": 258048 }, "first pick": { "uploaded": 0, - "written": 2440, + "written": 1672, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -159,11 +159,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 188416 + "resident": 258048 }, "pick": { "uploaded": 0, - "written": 2440, + "written": 1672, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -171,11 +171,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 188416 + "resident": 258048 }, "hover": { "uploaded": 0, - "written": 3752, + "written": 2600, "evaluated": 0, "packed": 0, "readbacks": 2, @@ -183,11 +183,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 188416 + "resident": 258048 }, "select": { "uploaded": 0, - "written": 3284, + "written": 2516, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -195,11 +195,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 188416 + "resident": 258048 }, "append": { "uploaded": 816400, - "written": 72576, + "written": 56280, "evaluated": 40820, "packed": 0, "readbacks": 40, @@ -207,7 +207,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 1335296 + "resident": 1409024 } }, "10000": { @@ -237,7 +237,7 @@ }, "load": { "uploaded": 200000, - "written": 5228, + "written": 4124, "evaluated": 10000, "packed": 0, "readbacks": 1, @@ -245,11 +245,11 @@ "texts": 0, "pipelines": 27, "late": 0, - "resident": 327680 + "resident": 901120 }, "pan": { "uploaded": 0, - "written": 48640, + "written": 33280, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -257,11 +257,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 327680 + "resident": 901120 }, "zoom": { "uploaded": 0, - "written": 48640, + "written": 33280, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -269,11 +269,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 327680 + "resident": 901120 }, "revisit": { "uploaded": 0, - "written": 1216, + "written": 832, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -281,11 +281,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 327680 + "resident": 901120 }, "restyle": { "uploaded": 0, - "written": 2796, + "written": 2460, "evaluated": 10000, "packed": 0, "readbacks": 0, @@ -293,23 +293,23 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 327680 + "resident": 901120 }, "one row": { - "uploaded": 1600000, - "written": 49280, - "evaluated": 400000, + "uploaded": 655360, + "written": 33600, + "evaluated": 163840, "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 2949120 + "resident": 1556480 }, "every row": { "uploaded": 40000, - "written": 1232, + "written": 856, "evaluated": 10000, "packed": 0, "readbacks": 0, @@ -317,11 +317,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 3014656 + "resident": 1597440 }, "first pick": { "uploaded": 0, - "written": 2440, + "written": 1672, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -329,11 +329,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 3014656 + "resident": 1597440 }, "pick": { "uploaded": 0, - "written": 2440, + "written": 1672, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -341,11 +341,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 3014656 + "resident": 1597440 }, "hover": { "uploaded": 0, - "written": 3752, + "written": 2600, "evaluated": 0, "packed": 0, "readbacks": 2, @@ -353,11 +353,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 3014656 + "resident": 1597440 }, "select": { "uploaded": 0, - "written": 3284, + "written": 2516, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -365,19 +365,19 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 3014656 + "resident": 1597440 }, "append": { - "uploaded": 8016400, - "written": 72896, - "evaluated": 400820, + "uploaded": 1462800, + "written": 57472, + "evaluated": 73140, "packed": 0, "readbacks": 40, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 16121856 + "resident": 3235840 } }, "100000": { @@ -407,7 +407,7 @@ }, "load": { "uploaded": 2000000, - "written": 6556, + "written": 5816, "evaluated": 100000, "packed": 0, "readbacks": 1, @@ -415,11 +415,11 @@ "texts": 0, "pipelines": 27, "late": 0, - "resident": 2621440 + "resident": 8830976 }, "pan": { "uploaded": 0, - "written": 48640, + "written": 33280, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -427,11 +427,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 2621440 + "resident": 8830976 }, "zoom": { "uploaded": 0, - "written": 48640, + "written": 33280, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -439,11 +439,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 2621440 + "resident": 8830976 }, "revisit": { "uploaded": 0, - "written": 1216, + "written": 832, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -451,11 +451,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 2621440 + "resident": 8830976 }, "restyle": { "uploaded": 0, - "written": 2908, + "written": 3320, "evaluated": 100000, "packed": 0, "readbacks": 0, @@ -463,23 +463,23 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 2621440 + "resident": 8830976 }, "one row": { - "uploaded": 10485760, - "written": 51360, - "evaluated": 2621440, + "uploaded": 655360, + "written": 33600, + "evaluated": 163840, "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 13107200 + "resident": 9486336 }, "every row": { "uploaded": 400000, - "written": 1324, + "written": 1032, "evaluated": 100000, "packed": 0, "readbacks": 0, @@ -487,11 +487,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 13631488 + "resident": 9887744 }, "first pick": { "uploaded": 0, - "written": 2440, + "written": 1672, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -499,11 +499,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 13631488 + "resident": 9887744 }, "pick": { "uploaded": 0, - "written": 2440, + "written": 1672, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -511,11 +511,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 13631488 + "resident": 9887744 }, "hover": { "uploaded": 0, - "written": 3752, + "written": 2600, "evaluated": 0, "packed": 0, "readbacks": 2, @@ -523,11 +523,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 13631488 + "resident": 9887744 }, "select": { "uploaded": 0, - "written": 3284, + "written": 2516, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -535,19 +535,19 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 13631488 + "resident": 9887744 }, "append": { - "uploaded": 27587600, - "written": 74656, - "evaluated": 1379380, + "uploaded": 1373200, + "written": 55232, + "evaluated": 68660, "packed": 0, "readbacks": 40, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 66060288 + "resident": 11526144 } }, "1000000": { @@ -577,7 +577,7 @@ }, "load": { "uploaded": 1638400, - "written": 7376, + "written": 5980, "evaluated": 81920, "packed": 409600, "readbacks": 1, @@ -585,11 +585,11 @@ "texts": 0, "pipelines": 27, "late": 0, - "resident": 1638400 + "resident": 6094848 }, "pan": { "uploaded": 0, - "written": 48752, + "written": 33544, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -597,11 +597,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 1638400 + "resident": 2752512 }, "zoom": { "uploaded": 3932160, - "written": 62376, + "written": 58400, "evaluated": 196608, "packed": 983040, "readbacks": 0, @@ -609,11 +609,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5570560 + "resident": 20054016 }, "revisit": { "uploaded": 0, - "written": 1228, + "written": 1264, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -621,11 +621,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5570560 + "resident": 20054016 }, "restyle": { "uploaded": 0, - "written": 2908, + "written": 2536, "evaluated": 16384, "packed": 0, "readbacks": 0, @@ -633,11 +633,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5570560 + "resident": 20054016 }, "one row": { "uploaded": 0, - "written": 48640, + "written": 33280, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -645,11 +645,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5570560 + "resident": 6684672 }, "every row": { "uploaded": 65536, - "written": 1248, + "written": 864, "evaluated": 16384, "packed": 16384, "readbacks": 0, @@ -657,11 +657,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5636096 + "resident": 6750208 }, "first pick": { "uploaded": 0, - "written": 2440, + "written": 1672, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -669,11 +669,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5636096 + "resident": 6750208 }, "pick": { "uploaded": 0, - "written": 2440, + "written": 1672, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -681,11 +681,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5636096 + "resident": 6750208 }, "hover": { "uploaded": 0, - "written": 3752, + "written": 2600, "evaluated": 0, "packed": 0, "readbacks": 2, @@ -693,11 +693,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5636096 + "resident": 6750208 }, "select": { "uploaded": 81920, - "written": 3776, + "written": 3024, "evaluated": 4096, "packed": 490, "readbacks": 1, @@ -705,11 +705,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5718016 + "resident": 7110656 }, "append": { "uploaded": 3358720, - "written": 73164, + "written": 57444, "evaluated": 167936, "packed": 119785, "readbacks": 40, @@ -717,7 +717,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 9076736 + "resident": 10747904 } }, "2000000": { @@ -747,7 +747,7 @@ }, "load": { "uploaded": 1638400, - "written": 6160, + "written": 5148, "evaluated": 81920, "packed": 409600, "readbacks": 1, @@ -755,11 +755,11 @@ "texts": 0, "pipelines": 27, "late": 0, - "resident": 1638400 + "resident": 6094848 }, "pan": { "uploaded": 0, - "written": 48752, + "written": 33544, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -767,11 +767,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 1638400 + "resident": 2752512 }, "zoom": { "uploaded": 4014080, - "written": 62404, + "written": 58624, "evaluated": 200704, "packed": 1003520, "readbacks": 0, @@ -779,11 +779,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5652480 + "resident": 20414464 }, "revisit": { "uploaded": 0, - "written": 1228, + "written": 1272, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -791,11 +791,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5652480 + "resident": 20414464 }, "restyle": { "uploaded": 0, - "written": 2908, + "written": 2536, "evaluated": 16384, "packed": 0, "readbacks": 0, @@ -803,11 +803,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5652480 + "resident": 20414464 }, "one row": { "uploaded": 0, - "written": 48640, + "written": 33280, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -815,11 +815,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5652480 + "resident": 6766592 }, "every row": { "uploaded": 65536, - "written": 1248, + "written": 864, "evaluated": 16384, "packed": 16384, "readbacks": 0, @@ -827,11 +827,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5718016 + "resident": 6832128 }, "first pick": { "uploaded": 0, - "written": 2440, + "written": 1672, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -839,11 +839,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5718016 + "resident": 6832128 }, "pick": { "uploaded": 0, - "written": 2440, + "written": 1672, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -851,11 +851,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5718016 + "resident": 6832128 }, "hover": { "uploaded": 0, - "written": 3752, + "written": 2600, "evaluated": 0, "packed": 0, "readbacks": 2, @@ -863,11 +863,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5718016 + "resident": 6832128 }, "select": { "uploaded": 81920, - "written": 3776, + "written": 3024, "evaluated": 4096, "packed": 495, "readbacks": 1, @@ -875,11 +875,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5799936 + "resident": 7192576 }, "append": { "uploaded": 3276800, - "written": 72672, + "written": 56952, "evaluated": 163840, "packed": 234500, "readbacks": 40, @@ -887,7 +887,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 9076736 + "resident": 10747904 } } }, @@ -919,7 +919,7 @@ }, "load": { "uploaded": 36352, - "written": 8036, + "written": 6484, "evaluated": 3008, "packed": 0, "readbacks": 2, @@ -927,11 +927,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 36864 + "resident": 180224 }, "pan": { "uploaded": 0, - "written": 58880, + "written": 43520, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -939,11 +939,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 36864 + "resident": 180224 }, "zoom": { "uploaded": 0, - "written": 58880, + "written": 43520, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -951,11 +951,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 36864 + "resident": 180224 }, "revisit": { "uploaded": 0, - "written": 1472, + "written": 1088, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -963,11 +963,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 36864 + "resident": 180224 }, "one row": { "uploaded": 163840, - "written": 59200, + "written": 43840, "evaluated": 40960, "packed": 0, "readbacks": 0, @@ -975,11 +975,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 200704 + "resident": 344064 }, "first pick": { "uploaded": 0, - "written": 2960, + "written": 2192, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -987,11 +987,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 200704 + "resident": 344064 }, "pick": { "uploaded": 0, - "written": 2960, + "written": 2192, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -999,11 +999,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 200704 + "resident": 344064 }, "hover": { "uploaded": 0, - "written": 4536, + "written": 3384, "evaluated": 0, "packed": 0, "readbacks": 2, @@ -1011,11 +1011,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 200704 + "resident": 344064 }, "append": { "uploaded": 835600, - "written": 69312, + "written": 52624, "evaluated": 41780, "packed": 0, "readbacks": 80, @@ -1023,7 +1023,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 1839104 + "resident": 1986560 } }, "10000": { @@ -1053,7 +1053,7 @@ }, "load": { "uploaded": 358400, - "written": 9532, + "written": 6664, "evaluated": 29800, "packed": 0, "readbacks": 2, @@ -1061,11 +1061,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 589824 + "resident": 1802240 }, "pan": { "uploaded": 0, - "written": 58880, + "written": 43520, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -1073,11 +1073,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 589824 + "resident": 1802240 }, "zoom": { "uploaded": 0, - "written": 58880, + "written": 43520, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -1085,11 +1085,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 589824 + "resident": 1802240 }, "revisit": { "uploaded": 0, - "written": 1472, + "written": 1088, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -1097,23 +1097,23 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 589824 + "resident": 1802240 }, "one row": { - "uploaded": 1600000, - "written": 59520, - "evaluated": 400000, + "uploaded": 655360, + "written": 43840, + "evaluated": 163840, "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 3211264 + "resident": 2457600 }, "first pick": { "uploaded": 0, - "written": 2960, + "written": 2192, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -1121,11 +1121,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 3211264 + "resident": 2457600 }, "pick": { "uploaded": 0, - "written": 2960, + "written": 2192, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -1133,11 +1133,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 3211264 + "resident": 2457600 }, "hover": { "uploaded": 0, - "written": 4520, + "written": 3368, "evaluated": 0, "packed": 0, "readbacks": 2, @@ -1145,19 +1145,19 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 3211264 + "resident": 2457600 }, "append": { - "uploaded": 8016400, - "written": 69312, - "evaluated": 400820, + "uploaded": 1462800, + "written": 53888, + "evaluated": 73140, "packed": 0, "readbacks": 80, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 16318464 + "resident": 4096000 } }, "100000": { @@ -1187,7 +1187,7 @@ }, "load": { "uploaded": 3589760, - "written": 9848, + "written": 8692, "evaluated": 298936, "packed": 0, "readbacks": 2, @@ -1195,11 +1195,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 4227072 + "resident": 17661952 }, "pan": { "uploaded": 0, - "written": 58880, + "written": 43520, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -1207,11 +1207,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4227072 + "resident": 17661952 }, "zoom": { "uploaded": 0, - "written": 58880, + "written": 43520, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -1219,11 +1219,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4227072 + "resident": 17661952 }, "revisit": { "uploaded": 0, - "written": 1472, + "written": 1088, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -1231,23 +1231,23 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4227072 + "resident": 17661952 }, "one row": { - "uploaded": 10485760, - "written": 61600, - "evaluated": 2621440, + "uploaded": 655360, + "written": 43840, + "evaluated": 163840, "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 14712832 + "resident": 18317312 }, "first pick": { "uploaded": 0, - "written": 2960, + "written": 2192, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -1255,11 +1255,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 14712832 + "resident": 18317312 }, "pick": { "uploaded": 0, - "written": 2960, + "written": 2192, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -1267,11 +1267,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 14712832 + "resident": 18317312 }, "hover": { "uploaded": 0, - "written": 4520, + "written": 3368, "evaluated": 0, "packed": 0, "readbacks": 2, @@ -1279,19 +1279,19 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 14712832 + "resident": 18317312 }, "append": { - "uploaded": 27472400, - "written": 71072, - "evaluated": 1373620, + "uploaded": 1258000, + "written": 51648, + "evaluated": 62900, "packed": 0, "readbacks": 80, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 67141632 + "resident": 19955712 } }, "1000000": { @@ -1320,20 +1320,20 @@ "resident": 0 }, "load": { - "uploaded": 7913472, - "written": 18120, - "evaluated": 466944, - "packed": 1978368, + "uploaded": 5062656, + "written": 17084, + "evaluated": 294912, + "packed": 1265664, "readbacks": 3, "jobs": 2, "texts": 0, "pipelines": 29, "late": 0, - "resident": 7913472 + "resident": 17842176 }, "pan": { "uploaded": 0, - "written": 62576, + "written": 48512, "evaluated": 0, "packed": 0, "readbacks": 4, @@ -1341,11 +1341,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 7913472 + "resident": 17842176 }, "zoom": { "uploaded": 16285696, - "written": 88428, + "written": 101944, "evaluated": 1040384, "packed": 4071424, "readbacks": 14, @@ -1353,11 +1353,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 24199168 + "resident": 92815360 }, "revisit": { "uploaded": 0, - "written": 3760, + "written": 5448, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -1365,11 +1365,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 24199168 + "resident": 92815360 }, "one row": { "uploaded": 655360, - "written": 59200, + "written": 43840, "evaluated": 163840, "packed": 163840, "readbacks": 0, @@ -1377,11 +1377,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 24854528 + "resident": 93470720 }, "first pick": { "uploaded": 0, - "written": 2960, + "written": 2192, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -1389,11 +1389,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 24854528 + "resident": 93470720 }, "pick": { "uploaded": 0, - "written": 2960, + "written": 2192, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -1401,11 +1401,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 24854528 + "resident": 93470720 }, "hover": { "uploaded": 0, - "written": 4520, + "written": 3368, "evaluated": 0, "packed": 0, "readbacks": 2, @@ -1413,11 +1413,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 24854528 + "resident": 93470720 }, "append": { "uploaded": 3276800, - "written": 69272, + "written": 57392, "evaluated": 163840, "packed": 119300, "readbacks": 80, @@ -1425,7 +1425,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 28131328 + "resident": 97026048 } }, "2000000": { @@ -1454,20 +1454,20 @@ "resident": 0 }, "load": { - "uploaded": 8241152, - "written": 17980, - "evaluated": 483328, - "packed": 2060288, + "uploaded": 5308416, + "written": 20024, + "evaluated": 307200, + "packed": 1327104, "readbacks": 3, "jobs": 2, "texts": 0, "pipelines": 29, "late": 0, - "resident": 8241152 + "resident": 18644992 }, "pan": { "uploaded": 0, - "written": 62576, + "written": 48576, "evaluated": 0, "packed": 0, "readbacks": 4, @@ -1475,11 +1475,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 8241152 + "resident": 18644992 }, "zoom": { "uploaded": 23937024, - "written": 94040, + "written": 113720, "evaluated": 1425408, "packed": 5984256, "readbacks": 14, @@ -1487,11 +1487,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 32178176 + "resident": 127320064 }, "revisit": { "uploaded": 0, - "written": 3760, + "written": 6216, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -1499,11 +1499,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 32178176 + "resident": 127320064 }, "one row": { "uploaded": 589824, - "written": 59168, + "written": 43808, "evaluated": 147456, "packed": 147456, "readbacks": 0, @@ -1511,11 +1511,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 32768000 + "resident": 127909888 }, "first pick": { "uploaded": 0, - "written": 2960, + "written": 2192, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -1523,11 +1523,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 32768000 + "resident": 127909888 }, "pick": { "uploaded": 0, - "written": 2960, + "written": 2192, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -1535,11 +1535,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 32768000 + "resident": 127909888 }, "hover": { "uploaded": 0, - "written": 4520, + "written": 3368, "evaluated": 0, "packed": 0, "readbacks": 2, @@ -1547,11 +1547,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 32768000 + "resident": 127909888 }, "append": { "uploaded": 3276800, - "written": 70792, + "written": 60188, "evaluated": 163840, "packed": 113700, "readbacks": 80, @@ -1559,7 +1559,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 36044800 + "resident": 131465216 } } }, @@ -1591,39 +1591,39 @@ }, "load": { "uploaded": 240000, - "written": 838672, + "written": 712168, "evaluated": 10000, "packed": 0, - "readbacks": 10, + "readbacks": 6, "jobs": 0, - "texts": 214, + "texts": 187, "pipelines": 34, "late": 0, - "resident": 393216 + "resident": 1269760 }, "pan": { "uploaded": 0, - "written": 3187496, + "written": 2125904, "evaluated": 0, "packed": 0, - "readbacks": 43, + "readbacks": 40, "jobs": 0, - "texts": 771, + "texts": 526, "pipelines": 0, "late": 0, - "resident": 393216 + "resident": 1269760 }, "zoom": { "uploaded": 0, - "written": 5470912, + "written": 5144160, "evaluated": 0, "packed": 0, - "readbacks": 43, + "readbacks": 40, "jobs": 0, - "texts": 1296, + "texts": 1228, "pipelines": 0, "late": 0, - "resident": 393216 + "resident": 1269760 } }, "100000": { @@ -1653,39 +1653,39 @@ }, "load": { "uploaded": 2400000, - "written": 2473380, + "written": 1431616, "evaluated": 100000, "packed": 0, - "readbacks": 30, + "readbacks": 6, "jobs": 0, - "texts": 385, + "texts": 187, "pipelines": 34, "late": 0, - "resident": 3145728 + "resident": 12443648 }, "pan": { "uploaded": 0, - "written": 5912000, + "written": 2350640, "evaluated": 0, "packed": 0, - "readbacks": 63, + "readbacks": 40, "jobs": 0, - "texts": 1292, + "texts": 572, "pipelines": 0, "late": 0, - "resident": 3145728 + "resident": 12443648 }, "zoom": { "uploaded": 0, - "written": 8491440, + "written": 6726128, "evaluated": 0, "packed": 0, - "readbacks": 55, + "readbacks": 40, "jobs": 0, - "texts": 1881, + "texts": 1522, "pipelines": 0, "late": 0, - "resident": 3145728 + "resident": 12443648 } }, "1000000": { @@ -1715,39 +1715,39 @@ }, "load": { "uploaded": 1966080, - "written": 4312152, + "written": 2929204, "evaluated": 81920, "packed": 491520, - "readbacks": 37, + "readbacks": 15, "jobs": 1, - "texts": 569, + "texts": 338, "pipelines": 34, "late": 0, - "resident": 1966080 + "resident": 3604480 }, "pan": { "uploaded": 0, - "written": 6503124, + "written": 4017904, "evaluated": 0, "packed": 0, - "readbacks": 43, + "readbacks": 40, "jobs": 0, - "texts": 1103, + "texts": 688, "pipelines": 0, "late": 0, - "resident": 1966080 + "resident": 3604480 }, "zoom": { "uploaded": 4718592, - "written": 12752104, + "written": 11557552, "evaluated": 196608, "packed": 1179648, - "readbacks": 49, + "readbacks": 44, "jobs": 0, - "texts": 1607, + "texts": 1429, "pipelines": 0, "late": 0, - "resident": 6684672 + "resident": 27983872 } } }, @@ -1779,19 +1779,19 @@ }, "load": { "uploaded": 1456, - "written": 8452, + "written": 6524, "evaluated": 116, "packed": 0, - "readbacks": 3, + "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 29, "late": 0, - "resident": 2560 + "resident": 8448 }, "pan": { "uploaded": 0, - "written": 58880, + "written": 45392, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -1799,11 +1799,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 2560 + "resident": 8448 }, "first pick": { "uploaded": 0, - "written": 2960, + "written": 2192, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -1811,11 +1811,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 2560 + "resident": 8448 }, "pick": { "uploaded": 0, - "written": 2960, + "written": 2192, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -1823,7 +1823,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 2560 + "resident": 8448 } }, "1000": { @@ -1853,19 +1853,19 @@ }, "load": { "uploaded": 12256, - "written": 8452, + "written": 5436, "evaluated": 1016, "packed": 0, - "readbacks": 3, + "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 29, "late": 0, - "resident": 13312 + "resident": 51456 }, "pan": { "uploaded": 0, - "written": 58880, + "written": 45392, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -1873,11 +1873,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 13312 + "resident": 51456 }, "first pick": { "uploaded": 0, - "written": 2960, + "written": 2192, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -1885,11 +1885,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 13312 + "resident": 51456 }, "pick": { "uploaded": 0, - "written": 2960, + "written": 2192, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -1897,7 +1897,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 13312 + "resident": 51456 } }, "10000": { @@ -1927,19 +1927,19 @@ }, "load": { "uploaded": 120256, - "written": 8460, + "written": 6580, "evaluated": 10016, "packed": 0, - "readbacks": 3, + "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 29, "late": 0, - "resident": 197632 + "resident": 493824 }, "pan": { "uploaded": 0, - "written": 58880, + "written": 45392, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -1947,31 +1947,31 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 197632 + "resident": 493824 }, "first pick": { "uploaded": 0, - "written": 4448, + "written": 2192, "evaluated": 0, "packed": 0, - "readbacks": 2, + "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 197632 + "resident": 493824 }, "pick": { "uploaded": 0, - "written": 4448, + "written": 2192, "evaluated": 0, "packed": 0, - "readbacks": 2, + "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 197632 + "resident": 493824 } }, "100000": { @@ -2001,19 +2001,19 @@ }, "load": { "uploaded": 1200256, - "written": 8864, + "written": 7632, "evaluated": 100016, "packed": 0, - "readbacks": 3, + "readbacks": 1, "jobs": 1, "texts": 0, "pipelines": 29, "late": 0, - "resident": 1573888 + "resident": 4819200 }, "pan": { "uploaded": 0, - "written": 58880, + "written": 45392, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -2021,31 +2021,31 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 1573888 + "resident": 4819200 }, "first pick": { "uploaded": 0, - "written": 4448, + "written": 2192, "evaluated": 0, "packed": 0, - "readbacks": 2, + "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 1573888 + "resident": 4819200 }, "pick": { "uploaded": 0, - "written": 4448, + "written": 2192, "evaluated": 0, "packed": 0, - "readbacks": 2, + "readbacks": 1, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 1573888 + "resident": 4819200 } } }, @@ -2077,7 +2077,7 @@ }, "load": { "uploaded": 1340368, - "written": 5072, + "written": 4728, "evaluated": 100, "packed": 0, "readbacks": 1, @@ -2085,11 +2085,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 1344512 + "resident": 1575424 }, "pan": { "uploaded": 0, - "written": 48640, + "written": 33280, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -2097,11 +2097,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 1344512 + "resident": 1575424 }, "zoom": { "uploaded": 0, - "written": 48640, + "written": 33280, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -2109,11 +2109,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 1344512 + "resident": 1575424 }, "one row": { "uploaded": 16000, - "written": 50880, + "written": 35520, "evaluated": 4000, "packed": 0, "readbacks": 0, @@ -2121,11 +2121,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 1364992 + "resident": 1595904 }, "first pick": { "uploaded": 0, - "written": 2432, + "written": 1664, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -2133,11 +2133,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 1364992 + "resident": 1595904 }, "pick": { "uploaded": 0, - "written": 2432, + "written": 1664, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -2145,7 +2145,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 1364992 + "resident": 1595904 } }, "1000": { @@ -2175,7 +2175,7 @@ }, "load": { "uploaded": 4172168, - "written": 7720, + "written": 5768, "evaluated": 1000, "packed": 0, "readbacks": 1, @@ -2183,11 +2183,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 4206592 + "resident": 4218880 }, "pan": { "uploaded": 0, - "written": 48640, + "written": 33280, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -2195,11 +2195,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4206592 + "resident": 4218880 }, "zoom": { "uploaded": 0, - "written": 48640, + "written": 33280, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -2207,11 +2207,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4206592 + "resident": 4218880 }, "one row": { "uploaded": 160000, - "written": 50880, + "written": 35520, "evaluated": 40000, "packed": 0, "readbacks": 0, @@ -2219,11 +2219,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4370432 + "resident": 4382720 }, "first pick": { "uploaded": 0, - "written": 2432, + "written": 1664, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -2231,11 +2231,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4370432 + "resident": 4382720 }, "pick": { "uploaded": 0, - "written": 2432, + "written": 1664, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -2243,7 +2243,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4370432 + "resident": 4382720 } }, "10000": { @@ -2273,7 +2273,7 @@ }, "load": { "uploaded": 18104720, - "written": 11840, + "written": 9552, "evaluated": 10000, "packed": 0, "readbacks": 1, @@ -2281,11 +2281,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 18284544 + "resident": 18399232 }, "pan": { "uploaded": 0, - "written": 48640, + "written": 33280, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -2293,11 +2293,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 18284544 + "resident": 18399232 }, "zoom": { "uploaded": 0, - "written": 48640, + "written": 33280, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -2305,23 +2305,23 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 18284544 + "resident": 18399232 }, "one row": { - "uploaded": 1600000, - "written": 51200, - "evaluated": 400000, + "uploaded": 655360, + "written": 35520, + "evaluated": 163840, "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 20905984 + "resident": 19054592 }, "first pick": { "uploaded": 0, - "written": 2432, + "written": 1664, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -2329,11 +2329,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 20905984 + "resident": 19054592 }, "pick": { "uploaded": 0, - "written": 2432, + "written": 1664, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -2341,7 +2341,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 20905984 + "resident": 19054592 } }, "30000": { @@ -2371,7 +2371,7 @@ }, "load": { "uploaded": 40370040, - "written": 40248, + "written": 18468, "evaluated": 30000, "packed": 0, "readbacks": 1, @@ -2379,11 +2379,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 40501248 + "resident": 41369600 }, "pan": { "uploaded": 0, - "written": 48640, + "written": 33280, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -2391,11 +2391,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 40501248 + "resident": 41369600 }, "zoom": { "uploaded": 0, - "written": 48640, + "written": 33280, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -2403,23 +2403,23 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 40501248 + "resident": 41369600 }, "one row": { - "uploaded": 4800000, - "written": 52000, - "evaluated": 1200000, + "uploaded": 655360, + "written": 35520, + "evaluated": 163840, "packed": 0, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 45744128 + "resident": 42024960 }, "first pick": { "uploaded": 0, - "written": 2432, + "written": 1664, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -2427,11 +2427,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 45744128 + "resident": 42024960 }, "pick": { "uploaded": 0, - "written": 2432, + "written": 1664, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -2439,7 +2439,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 45744128 + "resident": 42024960 } } }, @@ -2471,7 +2471,7 @@ }, "load": { "uploaded": 272064, - "written": 5068, + "written": 3528, "evaluated": 64, "packed": 0, "readbacks": 0, @@ -2479,11 +2479,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 278784 + "resident": 525312 }, "pan": { "uploaded": 2160192, - "written": 49848, + "written": 34024, "evaluated": 512, "packed": 0, "readbacks": 0, @@ -2491,19 +2491,19 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 2706176 + "resident": 3936512 }, "zoom": { - "uploaded": 2017824, - "written": 50208, - "evaluated": 1040, + "uploaded": 1951680, + "written": 34280, + "evaluated": 992, "packed": 0, "readbacks": 0, - "jobs": 23, + "jobs": 25, "texts": 0, "pipelines": 0, "late": 0, - "resident": 5284096 + "resident": 7089408 } }, "256": { @@ -2532,20 +2532,20 @@ "resident": 0 }, "load": { - "uploaded": 635904, - "written": 5072, - "evaluated": 1024, + "uploaded": 788640, + "written": 4560, + "evaluated": 1280, "packed": 0, "readbacks": 0, "jobs": 4, "texts": 0, "pipelines": 29, "late": 0, - "resident": 659456 + "resident": 934912 }, "pan": { "uploaded": 5062272, - "written": 50272, + "written": 34464, "evaluated": 8192, "packed": 0, "readbacks": 0, @@ -2553,11 +2553,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 6070272 + "resident": 7014400 }, "zoom": { - "uploaded": 5709024, - "written": 50736, + "uploaded": 5673696, + "written": 34908, "evaluated": 17152, "packed": 0, "readbacks": 0, @@ -2565,7 +2565,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 12826624 + "resident": 14147584 } }, "4096": { @@ -2594,40 +2594,40 @@ "resident": 0 }, "load": { - "uploaded": 2703712, - "written": 7704, + "uploaded": 2638176, + "written": 4596, "evaluated": 16384, "packed": 0, "readbacks": 0, - "jobs": 5, + "jobs": 4, "texts": 0, "pipelines": 29, "late": 0, - "resident": 2752512 + "resident": 3145728 }, "pan": { - "uploaded": 19818144, - "written": 51780, - "evaluated": 172032, + "uploaded": 4673184, + "written": 35984, + "evaluated": 77824, "packed": 0, "readbacks": 0, - "jobs": 12, + "jobs": 8, "texts": 0, "pipelines": 0, "late": 0, - "resident": 23396352 + "resident": 8552448 }, "zoom": { - "uploaded": 17787104, - "written": 55320, - "evaluated": 208896, + "uploaded": 8880000, + "written": 41952, + "evaluated": 73728, "packed": 0, "readbacks": 0, - "jobs": 18, + "jobs": 10, "texts": 0, "pipelines": 0, "late": 0, - "resident": 43253760 + "resident": 18726912 } } }, @@ -2659,7 +2659,7 @@ }, "load": { "uploaded": 24240, - "written": 11508, + "written": 9156, "evaluated": 300, "packed": 0, "readbacks": 1, @@ -2667,11 +2667,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 33792 + "resident": 51200 }, "pan": { "uploaded": 0, - "written": 60352, + "written": 44608, "evaluated": 0, "packed": 0, "readbacks": 39, @@ -2679,11 +2679,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 33792 + "resident": 51200 }, "edit": { "uploaded": 371960, - "written": 65376, + "written": 49632, "evaluated": 4000, "packed": 0, "readbacks": 40, @@ -2691,11 +2691,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 537600 + "resident": 555008 }, "arrange": { "uploaded": 19000, - "written": 4576, + "written": 3424, "evaluated": 100, "packed": 0, "readbacks": 1, @@ -2703,7 +2703,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 564736 + "resident": 582144 } }, "1000": { @@ -2733,7 +2733,7 @@ }, "load": { "uploaded": 242960, - "written": 21652, + "written": 17444, "evaluated": 3000, "packed": 0, "readbacks": 1, @@ -2741,11 +2741,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 335872 + "resident": 475136 }, "pan": { "uploaded": 0, - "written": 60352, + "written": 44608, "evaluated": 0, "packed": 0, "readbacks": 39, @@ -2753,11 +2753,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 335872 + "resident": 475136 }, "edit": { "uploaded": 3463800, - "written": 72544, + "written": 56800, "evaluated": 40000, "packed": 0, "readbacks": 40, @@ -2765,11 +2765,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 6070272 + "resident": 6209536 }, "arrange": { "uploaded": 178144, - "written": 4576, + "written": 3424, "evaluated": 1000, "packed": 0, "readbacks": 1, @@ -2777,7 +2777,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 6352896 + "resident": 6492160 } }, "10000": { @@ -2807,7 +2807,7 @@ }, "load": { "uploaded": 2429800, - "written": 93932, + "written": 89792, "evaluated": 30000, "packed": 0, "readbacks": 1, @@ -2815,11 +2815,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 3014656 + "resident": 4751360 }, "pan": { "uploaded": 0, - "written": 60352, + "written": 44608, "evaluated": 0, "packed": 0, "readbacks": 39, @@ -2827,31 +2827,31 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 3014656 + "resident": 4751360 }, "edit": { - "uploaded": 34375160, - "written": 64992, - "evaluated": 400000, + "uploaded": 14099000, + "written": 51168, + "evaluated": 163840, "packed": 0, "readbacks": 40, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 42336256 + "resident": 27688960 }, "arrange": { - "uploaded": 1711520, - "written": 4600, - "evaluated": 10000, + "uploaded": 721328, + "written": 3520, + "evaluated": 4096, "packed": 0, "readbacks": 1, "jobs": 2, "texts": 0, "pipelines": 0, "late": 0, - "resident": 44236800 + "resident": 28819456 } } }, @@ -2883,7 +2883,7 @@ }, "load": { "uploaded": 49152, - "written": 41216, + "written": 40064, "evaluated": 10, "packed": 0, "readbacks": 1, @@ -2891,11 +2891,11 @@ "texts": 22, "pipelines": 30, "late": 0, - "resident": 524288 + "resident": 524800 }, "stream": { "uploaded": 48, - "written": 181696, + "written": 165952, "evaluated": 0, "packed": 0, "readbacks": 40, @@ -2903,11 +2903,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 524288 + "resident": 524800 }, "scrub": { "uploaded": 0, - "written": 179136, + "written": 163776, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -2915,11 +2915,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 524288 + "resident": 524800 }, "narrow": { "uploaded": 1872, - "written": 197596, + "written": 181852, "evaluated": 0, "packed": 0, "readbacks": 40, @@ -2927,11 +2927,11 @@ "texts": 19, "pipelines": 0, "late": 0, - "resident": 524288 + "resident": 524800 }, "hover": { "uploaded": 0, - "written": 11816, + "written": 10664, "evaluated": 0, "packed": 0, "readbacks": 2, @@ -2939,7 +2939,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 524288 + "resident": 524800 } }, "100": { @@ -2969,7 +2969,7 @@ }, "load": { "uploaded": 417792, - "written": 41232, + "written": 48528, "evaluated": 100, "packed": 0, "readbacks": 1, @@ -2977,11 +2977,11 @@ "texts": 22, "pipelines": 30, "late": 0, - "resident": 786432 + "resident": 788992 }, "stream": { "uploaded": 408, - "written": 181696, + "written": 165952, "evaluated": 0, "packed": 0, "readbacks": 40, @@ -2989,11 +2989,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 786432 + "resident": 788992 }, "scrub": { "uploaded": 0, - "written": 179136, + "written": 163776, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -3001,11 +3001,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 786432 + "resident": 788992 }, "narrow": { "uploaded": 15912, - "written": 197880, + "written": 182144, "evaluated": 0, "packed": 0, "readbacks": 40, @@ -3013,11 +3013,11 @@ "texts": 19, "pipelines": 0, "late": 0, - "resident": 786432 + "resident": 788992 }, "hover": { "uploaded": 0, - "written": 11816, + "written": 10664, "evaluated": 0, "packed": 0, "readbacks": 2, @@ -3025,7 +3025,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 786432 + "resident": 788992 } }, "1000": { @@ -3055,7 +3055,7 @@ }, "load": { "uploaded": 4104192, - "written": 41440, + "written": 40288, "evaluated": 1000, "packed": 0, "readbacks": 1, @@ -3063,11 +3063,11 @@ "texts": 22, "pipelines": 30, "late": 0, - "resident": 4456448 + "resident": 4493312 }, "stream": { "uploaded": 4008, - "written": 181696, + "written": 165952, "evaluated": 0, "packed": 0, "readbacks": 40, @@ -3075,11 +3075,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4456448 + "resident": 4493312 }, "scrub": { "uploaded": 0, - "written": 179136, + "written": 163776, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -3087,11 +3087,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4456448 + "resident": 4493312 }, "narrow": { "uploaded": 156312, - "written": 203272, + "written": 187648, "evaluated": 0, "packed": 0, "readbacks": 40, @@ -3099,11 +3099,11 @@ "texts": 19, "pipelines": 0, "late": 0, - "resident": 4718592 + "resident": 4755456 }, "hover": { "uploaded": 0, - "written": 11816, + "written": 10664, "evaluated": 0, "packed": 0, "readbacks": 2, @@ -3111,7 +3111,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4718592 + "resident": 4755456 } }, "10000": { @@ -3141,7 +3141,7 @@ }, "load": { "uploaded": 40968192, - "written": 48924, + "written": 47620, "evaluated": 10000, "packed": 0, "readbacks": 1, @@ -3149,11 +3149,11 @@ "texts": 22, "pipelines": 30, "late": 0, - "resident": 45088768 + "resident": 41787392 }, "stream": { "uploaded": 40008, - "written": 195388, + "written": 172464, "evaluated": 0, "packed": 0, "readbacks": 40, @@ -3161,11 +3161,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 45088768 + "resident": 42311680 }, "scrub": { "uploaded": 0, - "written": 179136, + "written": 163776, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -3173,11 +3173,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 45088768 + "resident": 42311680 }, "narrow": { "uploaded": 1560312, - "written": 252508, + "written": 235048, "evaluated": 0, "packed": 0, "readbacks": 40, @@ -3185,11 +3185,11 @@ "texts": 19, "pipelines": 0, "late": 0, - "resident": 46923776 + "resident": 43622400 }, "hover": { "uploaded": 0, - "written": 11816, + "written": 10664, "evaluated": 0, "packed": 0, "readbacks": 2, @@ -3197,7 +3197,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 46923776 + "resident": 43622400 } } }, @@ -3229,7 +3229,7 @@ }, "load": { "uploaded": 12000, - "written": 50088, + "written": 48936, "evaluated": 1, "packed": 0, "readbacks": 1, @@ -3237,11 +3237,11 @@ "texts": 26, "pipelines": 30, "late": 0, - "resident": 524288 + "resident": 524800 }, "scrub": { "uploaded": 0, - "written": 179200, + "written": 163840, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -3249,11 +3249,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 524288 + "resident": 524800 }, "narrow": { "uploaded": 0, - "written": 198724, + "written": 182980, "evaluated": 0, "packed": 0, "readbacks": 39, @@ -3261,7 +3261,7 @@ "texts": 19, "pipelines": 0, "late": 0, - "resident": 524288 + "resident": 524800 } }, "10000": { @@ -3291,7 +3291,7 @@ }, "load": { "uploaded": 20000, - "written": 52552, + "written": 51400, "evaluated": 1, "packed": 0, "readbacks": 1, @@ -3299,11 +3299,11 @@ "texts": 26, "pipelines": 30, "late": 0, - "resident": 786432 + "resident": 786944 }, "scrub": { "uploaded": 0, - "written": 179200, + "written": 163840, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -3311,11 +3311,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 786432 + "resident": 786944 }, "narrow": { "uploaded": 120000, - "written": 198548, + "written": 182812, "evaluated": 0, "packed": 0, "readbacks": 39, @@ -3323,7 +3323,7 @@ "texts": 16, "pipelines": 0, "late": 0, - "resident": 1310720 + "resident": 1311232 } }, "100000": { @@ -3353,7 +3353,7 @@ }, "load": { "uploaded": 224992, - "written": 59516, + "written": 53948, "evaluated": 1, "packed": 0, "readbacks": 1, @@ -3361,11 +3361,11 @@ "texts": 26, "pipelines": 30, "late": 0, - "resident": 1572864 + "resident": 1573376 }, "scrub": { "uploaded": 0, - "written": 179200, + "written": 163840, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -3373,11 +3373,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 1572864 + "resident": 1573376 }, "narrow": { "uploaded": 0, - "written": 205780, + "written": 190044, "evaluated": 0, "packed": 0, "readbacks": 39, @@ -3385,7 +3385,7 @@ "texts": 16, "pipelines": 0, "late": 0, - "resident": 1572864 + "resident": 1573376 } }, "1000000": { @@ -3415,7 +3415,7 @@ }, "load": { "uploaded": 281248, - "written": 61856, + "written": 56288, "evaluated": 1, "packed": 0, "readbacks": 1, @@ -3423,11 +3423,11 @@ "texts": 26, "pipelines": 30, "late": 0, - "resident": 1572864 + "resident": 1573376 }, "scrub": { "uploaded": 0, - "written": 179200, + "written": 163840, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -3435,11 +3435,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 1572864 + "resident": 1573376 }, "narrow": { "uploaded": 0, - "written": 212348, + "written": 196612, "evaluated": 0, "packed": 0, "readbacks": 39, @@ -3447,7 +3447,7 @@ "texts": 16, "pipelines": 0, "late": 0, - "resident": 1572864 + "resident": 1573376 } } }, @@ -3479,7 +3479,7 @@ }, "load": { "uploaded": 20000, - "written": 5220, + "written": 4048, "evaluated": 1000, "packed": 0, "readbacks": 1, @@ -3487,11 +3487,11 @@ "texts": 0, "pipelines": 27, "late": 0, - "resident": 20480 + "resident": 90112 }, "move": { "uploaded": 20000, - "written": 75420, + "written": 50808, "evaluated": 2000, "packed": 0, "readbacks": 0, @@ -3499,7 +3499,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 40960 + "resident": 241664 } }, "10000": { @@ -3529,7 +3529,7 @@ }, "load": { "uploaded": 200000, - "written": 5228, + "written": 4124, "evaluated": 10000, "packed": 0, "readbacks": 1, @@ -3537,11 +3537,11 @@ "texts": 0, "pipelines": 27, "late": 0, - "resident": 327680 + "resident": 901120 }, "move": { "uploaded": 200000, - "written": 75436, + "written": 50888, "evaluated": 20000, "packed": 0, "readbacks": 0, @@ -3549,7 +3549,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 655360 + "resident": 2416640 } }, "100000": { @@ -3579,7 +3579,7 @@ }, "load": { "uploaded": 2000000, - "written": 6556, + "written": 4984, "evaluated": 100000, "packed": 0, "readbacks": 1, @@ -3587,11 +3587,11 @@ "texts": 0, "pipelines": 27, "late": 0, - "resident": 2621440 + "resident": 8830976 }, "move": { "uploaded": 2000000, - "written": 74416, + "written": 51768, "evaluated": 200000, "packed": 0, "readbacks": 0, @@ -3599,7 +3599,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5242880 + "resident": 23683072 } }, "1000000": { @@ -3629,7 +3629,7 @@ }, "load": { "uploaded": 1638400, - "written": 391984, + "written": 630384, "evaluated": 98304, "packed": 409600, "readbacks": 1, @@ -3637,11 +3637,11 @@ "texts": 0, "pipelines": 27, "late": 0, - "resident": 1638400 + "resident": 16904704 }, "move": { "uploaded": 655360, - "written": 227616, + "written": 253264, "evaluated": 49152, "packed": 163840, "readbacks": 0, @@ -3649,7 +3649,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 2293760 + "resident": 7110656 } } }, @@ -3681,7 +3681,7 @@ }, "load": { "uploaded": 3589760, - "written": 8328, + "written": 8644, "evaluated": 298936, "packed": 0, "readbacks": 1, @@ -3689,11 +3689,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 4227072 + "resident": 17661952 }, "pan": { "uploaded": 0, - "written": 58880, + "written": 43520, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -3701,7 +3701,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4227072 + "resident": 17661952 } }, "2": { @@ -3731,7 +3731,7 @@ }, "load": { "uploaded": 3589760, - "written": 18576, + "written": 17352, "evaluated": 597872, "packed": 0, "readbacks": 2, @@ -3739,11 +3739,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 4227072 + "resident": 31711232 }, "pan": { "uploaded": 0, - "written": 117760, + "written": 87040, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -3751,7 +3751,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4227072 + "resident": 31711232 } }, "4": { @@ -3781,7 +3781,7 @@ }, "load": { "uploaded": 3589760, - "written": 30240, + "written": 35296, "evaluated": 1195744, "packed": 0, "readbacks": 4, @@ -3789,11 +3789,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 4227072 + "resident": 59809792 }, "pan": { "uploaded": 0, - "written": 235520, + "written": 174080, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -3801,7 +3801,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4227072 + "resident": 59809792 } }, "8": { @@ -3831,7 +3831,7 @@ }, "load": { "uploaded": 3589760, - "written": 59456, + "written": 75536, "evaluated": 2391488, "packed": 0, "readbacks": 8, @@ -3839,11 +3839,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 4227072 + "resident": 116006912 }, "pan": { "uploaded": 0, - "written": 471040, + "written": 348160, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -3851,7 +3851,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4227072 + "resident": 116006912 } } }, @@ -3882,20 +3882,20 @@ "resident": 0 }, "load": { - "uploaded": 3915776, - "written": 10340, - "evaluated": 249856, - "packed": 978944, + "uploaded": 2686976, + "written": 9836, + "evaluated": 163840, + "packed": 671744, "readbacks": 1, "jobs": 2, "texts": 0, "pipelines": 29, "late": 0, - "resident": 3915776 + "resident": 9027584 }, "pan": { "uploaded": 0, - "written": 61024, + "written": 49976, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -3903,19 +3903,19 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 3915776 + "resident": 9027584 }, "zoom": { - "uploaded": 0, - "written": 62388, - "evaluated": 0, - "packed": 0, + "uploaded": 966656, + "written": 53996, + "evaluated": 77824, + "packed": 228944, "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 3915776 + "resident": 9568256 } }, "1000000": { @@ -3944,20 +3944,20 @@ "resident": 0 }, "load": { - "uploaded": 5341184, - "written": 13052, - "evaluated": 323584, - "packed": 1335296, + "uploaded": 3670016, + "written": 10432, + "evaluated": 212992, + "packed": 917504, "readbacks": 1, "jobs": 2, "texts": 0, "pipelines": 29, "late": 0, - "resident": 5341184 + "resident": 12795904 }, "pan": { "uploaded": 0, - "written": 61024, + "written": 48264, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -3965,11 +3965,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5341184 + "resident": 12795904 }, "zoom": { "uploaded": 0, - "written": 62388, + "written": 51216, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -3977,7 +3977,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5341184 + "resident": 12795904 } }, "2000000": { @@ -4006,20 +4006,20 @@ "resident": 0 }, "load": { - "uploaded": 5914624, - "written": 14876, - "evaluated": 352256, - "packed": 1478656, + "uploaded": 3588096, + "written": 12688, + "evaluated": 208896, + "packed": 897024, "readbacks": 1, "jobs": 2, "texts": 0, "pipelines": 29, "late": 0, - "resident": 5914624 + "resident": 12435456 }, "pan": { "uploaded": 0, - "written": 61024, + "written": 48232, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -4027,11 +4027,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5914624 + "resident": 12435456 }, "zoom": { "uploaded": 0, - "written": 62388, + "written": 51136, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -4039,7 +4039,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 5914624 + "resident": 12435456 } } }, @@ -4071,7 +4071,7 @@ }, "load": { "uploaded": 3589760, - "written": 15632, + "written": 16264, "evaluated": 597872, "packed": 0, "readbacks": 2, @@ -4079,11 +4079,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 4227072 + "resident": 31711232 }, "image": { "uploaded": 0, - "written": 8928, + "written": 6624, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -4091,11 +4091,11 @@ "texts": 0, "pipelines": 1, "late": 0, - "resident": 4227072 + "resident": 31711232 }, "png": { "uploaded": 0, - "written": 2944, + "written": 2176, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -4103,11 +4103,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4227072 + "resident": 31711232 }, "video": { "uploaded": 0, - "written": 97248, + "written": 71904, "evaluated": 0, "packed": 0, "readbacks": 30, @@ -4115,11 +4115,11 @@ "texts": 0, "pipelines": 1, "late": 0, - "resident": 4227072 + "resident": 31711232 }, "held": { "uploaded": 0, - "written": 5984, + "written": 4448, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -4127,7 +4127,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4227072 + "resident": 31711232 } }, "2073600": { @@ -4157,7 +4157,7 @@ }, "load": { "uploaded": 3589760, - "written": 18576, + "written": 16264, "evaluated": 597872, "packed": 0, "readbacks": 2, @@ -4165,11 +4165,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 4227072 + "resident": 31711232 }, "image": { "uploaded": 0, - "written": 8928, + "written": 6624, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -4177,11 +4177,11 @@ "texts": 0, "pipelines": 1, "late": 0, - "resident": 4227072 + "resident": 31711232 }, "png": { "uploaded": 0, - "written": 2944, + "written": 2176, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -4189,11 +4189,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4227072 + "resident": 31711232 }, "video": { "uploaded": 0, - "written": 97248, + "written": 71904, "evaluated": 0, "packed": 0, "readbacks": 30, @@ -4201,11 +4201,11 @@ "texts": 0, "pipelines": 1, "late": 0, - "resident": 4227072 + "resident": 31711232 }, "held": { "uploaded": 0, - "written": 5984, + "written": 4448, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -4213,7 +4213,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4227072 + "resident": 31711232 } }, "8294400": { @@ -4243,7 +4243,7 @@ }, "load": { "uploaded": 3589760, - "written": 15632, + "written": 14088, "evaluated": 597872, "packed": 0, "readbacks": 2, @@ -4251,11 +4251,11 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 4227072 + "resident": 31711232 }, "image": { "uploaded": 0, - "written": 8928, + "written": 6624, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -4263,11 +4263,11 @@ "texts": 0, "pipelines": 1, "late": 0, - "resident": 4227072 + "resident": 31711232 }, "png": { "uploaded": 0, - "written": 2944, + "written": 2176, "evaluated": 0, "packed": 0, "readbacks": 0, @@ -4275,11 +4275,11 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4227072 + "resident": 31711232 }, "video": { "uploaded": 0, - "written": 97248, + "written": 71904, "evaluated": 0, "packed": 0, "readbacks": 30, @@ -4287,11 +4287,11 @@ "texts": 0, "pipelines": 1, "late": 0, - "resident": 4227072 + "resident": 31711232 }, "held": { "uploaded": 0, - "written": 5984, + "written": 4448, "evaluated": 0, "packed": 0, "readbacks": 1, @@ -4299,7 +4299,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 4227072 + "resident": 31711232 } } } diff --git a/benchmark/judge.ts b/benchmark/judge.ts index 36df439a..7f9e53ad 100644 --- a/benchmark/judge.ts +++ b/benchmark/judge.ts @@ -150,7 +150,7 @@ export function judge( past('time', fit.time, order.time); past('uploaded bytes', fit.uploaded, order.bytes); // What stays resident is the data's footprint, set by loading; later phases only add to it. - if (phase === 'load') past('resident bytes', fit.resident, order.bytes); + if (phase === 'load') past('resident bytes', fit.resident, order.resident ?? order.bytes); for (const n of at) { const now = bySize[n]![phase]!; const where = `${scenario.name} at ${n}, ${phase}`; diff --git a/benchmark/page/scenarios.ts b/benchmark/page/scenarios.ts index 27a7fa8c..59263486 100644 --- a/benchmark/page/scenarios.ts +++ b/benchmark/page/scenarios.ts @@ -42,12 +42,13 @@ export interface Built { /** * How a phase's cost may grow with size: 0 not at all, 1 no faster than linearly. `time` is a steady - * phase's CPU per frame or an action's milliseconds; `bytes` is what it uploaded and what stays - * resident. A measure left out is held only to the baselines. + * phase's CPU per frame or an action's milliseconds; `bytes` is what it uploaded, and unless + * `resident` says otherwise what stays resident. A measure left out is held only to the baselines. */ export interface Order { time?: 0 | 1; bytes?: 0 | 1; + resident?: 0 | 1; } export interface Phase { @@ -543,10 +544,11 @@ export const scenarios: Record = { unit: 'views', sizes: [1, 2, 4, 8], views: (n) => n, - // Room for eight views' own rows, which the views' shares of a budget hold together, so the - // bytes that grow would be pages copied. + // Room for eight views' own rows, which the views' shares of a budget hold together. What each + // view keeps per row is its own, so what stays resident grows with the views; the pages are + // shared, so what uploads does not. budget: { bytes: 1024 * 2 ** 20, uploadPerFrame: 32 * 2 ** 20 }, - load: { time: 1, bytes: 0 }, + load: { time: 1, bytes: 0, resident: 1 }, build: () => ({ scene: grid('plane'), data: { g: make.lattice(1e5) } }), phases: [pan({ time: 1, bytes: 0 })], }, diff --git a/crates/latkit/build/shaders.rs b/crates/latkit/build/shaders.rs index 88ee5e5b..30ba19c9 100644 --- a/crates/latkit/build/shaders.rs +++ b/crates/latkit/build/shaders.rs @@ -41,18 +41,15 @@ const ENTRIES: &[Entry] = &[ module: "draw::points", variants: &[ ("main", &[]), - ("cast", &["casts"]), ("custom", &["custom"]), - ("custom_cast", &["custom", "casts"]), ("custom_focus", &["custom", "focused"]), ("ports", &["ports"]), ("focus", &["focused"]), ], }, - Entry { module: "draw::strokes", variants: &[("main", &[]), ("cast", &["casts"]), ("focus", &["focused"])] }, - Entry { module: "draw::triangles", variants: &[("main", &[]), ("cast", &["casts"])] }, + Entry { module: "draw::strokes", variants: &[("main", &[]), ("focus", &["focused"])] }, + Entry { module: "draw::triangles", variants: &[("main", &[])] }, Entry { module: "draw::areas", variants: &[("main", &[]), ("focus", &["focused"])] }, - Entry { module: "draw::shade", variants: &[("main", &[])] }, Entry { module: "draw::glyphs", variants: &[("main", &[]), ("ports", &["ports"])] }, Entry { module: "compute::candidates", variants: &[("main", &["writes"])] }, Entry { module: "compute::labels", variants: &[("main", &["writes"])] }, @@ -61,13 +58,13 @@ const ENTRIES: &[Entry] = &[ Entry { module: "compute::fields", variants: &[("main", &[])] }, ]; -const FEATURES: &[&str] = &["writes", "atomic", "casts", "custom", "multisampled", "lists", "ports", "focused"]; +const FEATURES: &[&str] = &["writes", "atomic", "custom", "multisampled", "lists", "ports", "focused"]; /// What a custom marker supplies, for validating its variant. const CUSTOM_MARKER: &str = "fn marker(f: MarkerFragment) -> MarkerColor { return MarkerColor(f.color, 0.0); }"; -/// Uniforms bound at a dynamic offset: each mark's, each pass's eye, and each probe dispatch's step. -const DYNAMIC: &[&str] = &["mark", "eye", "step"]; +/// Uniforms bound at a dynamic offset: each mark's, and each probe dispatch's step. +const DYNAMIC: &[&str] = &["mark", "step"]; /// The values each pipeline constant is made with, as constants of `lib/types.wesl` or booleans: the /// primitive a probe or a label's anchor reads, the path a stroke reads, and whether channels read diff --git a/crates/latkit/shaders/draw/areas.wesl b/crates/latkit/shaders/draw/areas.wesl index 5975ecd4..2c411a8c 100644 --- a/crates/latkit/shaders/draw/areas.wesl +++ b/crates/latkit/shaders/draw/areas.wesl @@ -8,7 +8,7 @@ import package::lib::bind::frame; import package::lib::project::culled; import package::lib::triangles::{triangle_corner, triangle_shown, corner_clip}; import package::lib::focus::focus_of; -import package::lib::shade::daylight; +import package::lib::daylight::daylight; import package::lib::color::output; struct Varyings { diff --git a/crates/latkit/shaders/draw/backdrop.wesl b/crates/latkit/shaders/draw/backdrop.wesl index baf54787..91eb93f6 100644 --- a/crates/latkit/shaders/draw/backdrop.wesl +++ b/crates/latkit/shaders/draw/backdrop.wesl @@ -1,5 +1,5 @@ // The backdrop under every mark: a plane's or a diagram's grid, or the earth's surface and graticule, -// darkened where casters shadow the ground, lit by daylight, and on the globe ringed by its air. Lines +// lit by daylight, and on the globe ringed by its air. Lines // are a pixel wide and antialiased per axis. Their spacing steps by tens, and the next step fades in as // the lines crowd, so no seam shows. They fade where the view grazes the ground, and on a plane away // from what the camera looks at. The round globe writes its depth a hair under its surface, so what @@ -9,9 +9,7 @@ import package::lib::types::{SPACE_EARTH, SPACE_PLANE, SPACE_DIAGRAM, GRID_OFF, import package::lib::bind::{frame, eye}; import package::lib::color::output; import package::lib::project::{PI, NEAR_Z, FAR_Z, globe_center}; -import package::lib::shade::{lit, daylight}; - -@group(2) @binding(10) var shadows: texture_2d; +import package::lib::daylight::daylight; struct Varyings { @builtin(position) clip: vec4f, @@ -74,18 +72,12 @@ fn blend(color: vec4f, over: vec4f, coverage: f32) -> vec4f { return vec4f(mix(color.rgb, over.rgb, coverage * over.a), color.a); } -// `color` darkened where casters shadow ground point `g`. -fn shaded(color: vec4f, g: vec3f, ahead: bool) -> vec4f { - let shadow = select(0.0, 1.0 - lit(shadows, g, true), ahead); - return vec4f(mix(color.rgb, frame.shade.color.rgb, shadow), color.a); -} - // The plane: its grid where the ray meets elevation 0 at `g`, faded where the ray grazes and away // from the center. fn plane(g: vec3f, d: vec3f, ahead: bool) -> vec4f { let p = g.xy + frame.center_hi + frame.center_lo; let w = fwidth(g.xy); - let ground = shaded(frame.background, g, ahead); + let ground = frame.background; if frame.grid_kind == GRID_OFF { return ground; } @@ -130,11 +122,10 @@ fn earth(o: vec3f, d: vec3f, g: vec3f, ahead: bool) -> Ground { var ground = frame.surface; if globe { p = globe_degrees(hit, radius); - ground = vec4f(ground.rgb * daylight(p), ground.a); } else { p = map_degrees(g); - ground = shaded(vec4f(ground.rgb * daylight(p), ground.a), g, ahead); } + ground = vec4f(ground.rgb * daylight(p), ground.a); var color = blend(frame.background, ground, covered); let facing = dot(normalize(hit - center), -d); if frame.graticule != 0u { diff --git a/crates/latkit/shaders/draw/points.wesl b/crates/latkit/shaders/draw/points.wesl index 713c7190..1585a04e 100644 --- a/crates/latkit/shaders/draw/points.wesl +++ b/crates/latkit/shaders/draw/points.wesl @@ -2,11 +2,10 @@ // containers. A quad covers its marker and a pixel of edge, no more, and a small one a later one // covers in a crowded cell is left out. The focus variant draws the rows the mark's focus lights // again, over everything, reaching out to their glow; a group only glows about its border, so what -// it holds still shows. The cast variant draws the same quads through the light's eye into the -// shadow map; the ports variant draws one quad per port of every row, on its side of the row's -// marker. +// it holds still shows. The ports variant draws one quad per port of every row, on its side of the +// row's marker. -import package::lib::types::{STATUS, MARKER_ICON, MARKER_SHAPE, MARKER_BLOCK, GROUP, CASTS, SPACE_DIAGRAM}; +import package::lib::types::{STATUS, MARKER_ICON, MARKER_SHAPE, MARKER_BLOCK, GROUP, SHADOW, SPACE_DIAGRAM}; import package::lib::bind::{frame, eye, mark, word, ramp, program, focused}; import package::lib::state::extra_at; import package::lib::project::{to_screen, to_clip, nearer, culled}; @@ -15,7 +14,7 @@ import package::lib::marker::{ MarkerColor, MarkerFragment, builtin_marker, container, border, dropped, outline_distance, box_distance, shape_distance, filled, over, status_ring, glow, premultiplied, theme_text, theme_corner, theme_lift, }; -import package::lib::shade::{caster, daylight}; +import package::lib::daylight::daylight; import package::lib::mark::fringe; import package::lib::color::output; @@ -28,12 +27,11 @@ struct Varyings { @location(2) color: vec4f, @location(3) @interpolate(flat) half: vec2f, @location(4) @interpolate(flat) status: vec4f, - @location(5) @interpolate(flat) height: f32, - @location(6) @interpolate(flat) shape: u32, + @location(5) @interpolate(flat) shape: u32, // A group's depth; for a port, 1 when its reference names a net. - @location(7) @interpolate(flat) extra: f32, + @location(6) @interpolate(flat) extra: f32, // The row's focus bits, which the focus draw glows by. - @location(8) @interpolate(flat) focus: u32, + @location(7) @interpolate(flat) focus: u32, } // What an instance draws: a row's marker, or with `ports` one of a row's ports. @@ -78,7 +76,6 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: out.color = vec4f(d.p.color.rgb * daylight(d.p.position), d.p.color.a); out.half = max(d.half, vec2f(0.5)); out.status = d.p.status; - out.height = d.p.world.z; out.shape = d.shape; out.extra = d.extra; out.focus = d.p.focus; @@ -134,7 +131,7 @@ fn shaded(v: Varyings) -> MarkerColor { } // Whether the mark draws a diagram's blocks: shapes and blocks on a diagram's sheet, which the theme -// borders, and shadows when they cast them. +// borders. fn sheet_blocks() -> bool { return frame.space == SPACE_DIAGRAM && (mark.marker == MARKER_SHAPE || mark.marker == MARKER_BLOCK); } @@ -152,7 +149,7 @@ fn shaded(v: Varyings) -> MarkerColor { if (mark.flags & STATUS) != 0u && v.status.a > 0.0 { m = status_ring(m, vec4f(v.status.rgb, v.status.a * v.color.a)); } - if (mark.flags & CASTS) != 0u && frame.space == SPACE_DIAGRAM { + if (mark.flags & SHADOW) != 0u { // y is up in a marker: the shadow falls below, measured from its outline lowered. let below = v.p + vec2f(0.0, theme_lift()); let distance = select( @@ -165,7 +162,7 @@ fn shaded(v: Varyings) -> MarkerColor { return m; } -@if(!casts && !focused) +@if(!focused) @fragment fn fragment(v: Varyings) -> @location(0) vec4f { return output(premultiplied(shaded(v))); @@ -181,13 +178,3 @@ fn fragment(v: Varyings) -> @location(0) vec4f { } return output(glow(m, v.focus)); } - -@if(casts) -@fragment -fn fragment(v: Varyings) -> @location(0) vec4f { - let m = shaded(v); - if m.color.a <= 0.01 { - discard; - } - return caster(m.color.a, v.height); -} diff --git a/crates/latkit/shaders/draw/shade.wesl b/crates/latkit/shaders/draw/shade.wesl deleted file mode 100644 index a86b41b2..00000000 --- a/crates/latkit/shaders/draw/shade.wesl +++ /dev/null @@ -1,21 +0,0 @@ -// One level of the shadow map from the level below: coverage averaged, so it softens; the lowest -// caster kept, and the highest. - -@group(2) @binding(11) var below: texture_2d; - -@vertex -fn vertex(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f { - let uv = vec2f(f32((i << 1u) & 2u), f32(i & 2u)); - return vec4f(uv * 2.0 - 1.0, 0.0, 1.0); -} - -@fragment -fn fragment(@builtin(position) p: vec4f) -> @location(0) vec4f { - let size = vec2i(textureDimensions(below)) - 1; - let at = vec2i(p.xy) * 2; - let a = textureLoad(below, min(at, size), 0); - let b = textureLoad(below, min(at + vec2i(1, 0), size), 0); - let c = textureLoad(below, min(at + vec2i(0, 1), size), 0); - let d = textureLoad(below, min(at + vec2i(1, 1), size), 0); - return vec4f((a.r + b.r + c.r + d.r) * 0.25, min(min(a.g, b.g), min(c.g, d.g)), min(min(a.b, b.b), min(c.b, d.b)), 0.0); -} diff --git a/crates/latkit/shaders/draw/strokes.wesl b/crates/latkit/shaders/draw/strokes.wesl index 5e38469e..10161ad1 100644 --- a/crates/latkit/shaders/draw/strokes.wesl +++ b/crates/latkit/shaders/draw/strokes.wesl @@ -2,8 +2,7 @@ // the stroke, or its comets where they are wider, and a pixel of edge; a small segment a later one covers in a crowded cell is left out. A // segment that ends a part of its path carries the arrowhead; in a diagram, a segment of no length is // a junction, drawn as a dot. The focus variant draws the pieces of the rows the mark's focus lights -// again, over everything, reaching out to their glow. The cast variant draws the same quads through -// the light's eye into the shadow map. +// again, over everything, reaching out to their glow. import package::lib::types::{ARROWS, PATH_TRACE, SPACE_DIAGRAM}; import package::lib::path::PATH; @@ -15,8 +14,8 @@ import package::lib::path::{ import package::lib::marker::{MarkerColor, glow}; import package::lib::focus::focus_of; import package::lib::mark::fringe; -import package::lib::stroke::{Stroke, covered, stroke, stroke_distance}; -import package::lib::shade::{caster, daylight}; +import package::lib::stroke::{Stroke, stroke, stroke_distance}; +import package::lib::daylight::daylight; import package::lib::color::output; struct Varyings { @@ -28,13 +27,12 @@ struct Varyings { @location(3) color: vec4f, // Width, distance along at `a` and at `b`, flow. @location(4) @interpolate(flat) style: vec4f, - @location(5) height: f32, // Whether the segment ends in an arrowhead, then whether it dashes. - @location(6) @interpolate(flat) arrow: u32, + @location(5) @interpolate(flat) arrow: u32, // The row's focus bits, which the focus draw glows by. - @location(7) @interpolate(flat) focus: u32, + @location(6) @interpolate(flat) focus: u32, // Distance along at `a` and at `b` in length units, and how far the row's comets traveled. - @location(8) @interpolate(flat) comet: vec3f, + @location(7) @interpolate(flat) comet: vec3f, } // Whether this variant draws lit rows, its quads reaching out to their glow. @@ -101,7 +99,6 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: } out.style = vec4f(width, phase, style.flow); out.comet = vec3f(along, style.travel); - out.height = mix(ends[0].world.z, ends[1].world.z, f); out.arrow = u32(arrow) | (u32(style.dash && !junction) << 1u); out.focus = focus_of(s.row); return out; @@ -119,7 +116,7 @@ fn segment_of(v: Varyings) -> Stroke { return Stroke(v.a, v.b, v.style.yz, v.comet.xy, v.style.x, v.color, v.style.w, v.comet.z, dash, (v.arrow & 1u) != 0u); } -@if(!casts && !focused) +@if(!focused) @fragment fn fragment(v: Varyings) -> @location(0) vec4f { let c = stroke(pixel(v), segment_of(v)); @@ -133,14 +130,3 @@ fn fragment(v: Varyings) -> @location(0) vec4f { let p = pixel(v); return output(glow(MarkerColor(stroke(p, s), stroke_distance(p, s).x), v.focus)); } - -@if(casts) -@fragment -fn fragment(v: Varyings) -> @location(0) vec4f { - let s = segment_of(v); - let alpha = s.color.a * covered(stroke_distance(pixel(v), s), s); - if alpha <= 0.01 { - discard; - } - return caster(alpha, v.height); -} diff --git a/crates/latkit/shaders/draw/triangles.wesl b/crates/latkit/shaders/draw/triangles.wesl index 3e40590a..99f85578 100644 --- a/crates/latkit/shaders/draw/triangles.wesl +++ b/crates/latkit/shaders/draw/triangles.wesl @@ -1,6 +1,5 @@ // Triangles: one instanced triangle per face row, lit from the camera so a flat mesh seen from above -// shows its exact colors; turned away, a face darkens by the theme's shading. Faces receive the -// ground's shadow map. The cast variant draws them through the light's eye into the map. +// shows its exact colors; turned away, a face darkens by the theme's shading. import package::lib::bind::{frame, eye, word}; import package::lib::project::culled; @@ -8,12 +7,8 @@ import package::lib::triangles::{triangle_corner, triangle_shown, corner_clip}; import package::lib::state::end_normal; import package::lib::marker::{MarkerColor, glow}; import package::lib::focus::focus_of; -import package::lib::shade::{caster, lit}; import package::lib::color::output; -@if(!casts) -@group(2) @binding(10) var shadows: texture_2d; - struct Varyings { @builtin(position) clip: vec4f, @location(0) world: vec3f, @@ -45,7 +40,6 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: return out; } -@if(!casts) @fragment fn fragment(v: Varyings) -> @location(0) vec4f { let to_eye = normalize(eye.position.xyz - v.world); @@ -57,12 +51,5 @@ fn fragment(v: Varyings) -> @location(0) vec4f { let shine = pow(max(dot(n, normalize(frame.light.xyz + to_eye)), 0.0), 48.0) * 0.3 * frame.shading; let rim = pow(1.0 - facing, 4.0) * 0.25 * frame.shading; let rgb = min(v.color.rgb * diffuse + shine + rim, vec3f(1.0)); - let shadowed = mix(rgb, frame.shade.color.rgb, 1.0 - lit(shadows, v.world, false)); - return output(glow(MarkerColor(vec4f(shadowed, v.color.a), 0.0), v.focus)); -} - -@if(casts) -@fragment -fn fragment(v: Varyings) -> @location(0) vec4f { - return caster(v.color.a, v.world.z); + return output(glow(MarkerColor(vec4f(rgb, v.color.a), 0.0), v.focus)); } diff --git a/crates/latkit/shaders/lib/daylight.wesl b/crates/latkit/shaders/lib/daylight.wesl new file mode 100644 index 00000000..07c30d5b --- /dev/null +++ b/crates/latkit/shaders/lib/daylight.wesl @@ -0,0 +1,19 @@ +// The earth's daylight: how lit a place is by the sun, night falling to the theme's brightness. + +import package::lib::types::SPACE_EARTH; +import package::lib::bind::frame; + +// How far the sun stands below the horizon, in the cosine of its angle, where night has fallen. +const TWILIGHT: f32 = 0.1; + +// How lit the earth is at longitude and latitude `p`, in degrees: 1 by day, down to the night's +// brightness, eased across the terminator; 1 everywhere without daylight. +fn daylight(p: vec2f) -> f32 { + let sun = frame.sun; + if frame.space != SPACE_EARTH || dot(sun.xyz, sun.xyz) == 0.0 { + return 1.0; + } + let r = radians(p); + let n = vec3f(cos(r.y) * cos(r.x), cos(r.y) * sin(r.x), sin(r.y)); + return mix(sun.w, 1.0, smoothstep(-TWILIGHT, TWILIGHT * 0.5, dot(n, sun.xyz))); +} diff --git a/crates/latkit/shaders/lib/marker.wesl b/crates/latkit/shaders/lib/marker.wesl index b2434781..6687bb72 100644 --- a/crates/latkit/shaders/lib/marker.wesl +++ b/crates/latkit/shaders/lib/marker.wesl @@ -146,9 +146,8 @@ fn border(m: MarkerColor, d: f32, alpha: f32) -> MarkerColor { // again, in the theme's shadow color. fn dropped(m: MarkerColor, distance: f32, alpha: f32) -> MarkerColor { let lift = max(theme_lift(), 0.5); - let shade = frame.shade.color; - let a = shade.a * alpha * (1.0 - smoothstep(-lift, lift, distance)); - return MarkerColor(over(m, MarkerColor(vec4f(shade.rgb, a), distance)).color, m.distance); + let a = frame.shadow.a * alpha * (1.0 - smoothstep(-lift, lift, distance)); + return MarkerColor(over(m, MarkerColor(vec4f(frame.shadow.rgb, a), distance)).color, m.distance); } // A ring of the status color just outside the marker's outline, a part of the drawing in a diagram. diff --git a/crates/latkit/shaders/lib/points.wesl b/crates/latkit/shaders/lib/points.wesl index b4385d09..7d222692 100644 --- a/crates/latkit/shaders/lib/points.wesl +++ b/crates/latkit/shaders/lib/points.wesl @@ -1,9 +1,9 @@ -// Points: where a vertex row's marker stands, how large, and in what color. Draws, casts, probes, -// bounds, and labels all read a point through `point`. +// Points: where a vertex row's marker stands, how large, and in what color. Draws, probes, bounds, +// and labels all read a point through `point`. import package::lib::types::{ - NONE, VISIBLE, PLACED, SIZED, STATUS, CASTS, HOVERED, VERTEX_HALF, VERTEX_WORDS, SIDE_LEFT, SIDE_RIGHT, - SIDE_TOP, PORT_WORDS, PORT_CODE, PORT_SHAPE, PORT_COLUMN, HALF_UNITS, OWN_HEIGHT, SPACE_DIAGRAM, + NONE, VISIBLE, PLACED, SIZED, STATUS, SHADOW, HOVERED, VERTEX_HALF, VERTEX_WORDS, SIDE_LEFT, SIDE_RIGHT, + SIDE_TOP, PORT_WORDS, PORT_CODE, PORT_SHAPE, PORT_COLUMN, HALF_UNITS, OWN_HEIGHT, }; import package::lib::bind::{frame, eye, mark, program}; import package::lib::state::{row_at, vertex_at, Vertex}; @@ -125,10 +125,10 @@ fn port_at(p: Point, k: u32) -> Port { } // CSS pixels a point's quad reaches past its center: the marker, its status ring when it has one, -// its shadow on a diagram's sheet when it casts one, a target pixel of edge, and when `lit`, as the +// its shadow on a diagram's sheet when it drops one, a target pixel of edge, and when `lit`, as the // focus draw draws it, its glow. fn reach(p: Point, lit: bool) -> vec2f { let ring = select(0.0, 3.0 * frame.unit, (mark.flags & STATUS) != 0u && p.status.a > 0.0); - let shadow = select(0.0, 2.0 * theme_lift(), (mark.flags & CASTS) != 0u && frame.space == SPACE_DIAGRAM); + let shadow = select(0.0, 2.0 * theme_lift(), (mark.flags & SHADOW) != 0u); return p.half + max(ring, shadow) + select(0.0, frame.glow, lit) + fringe(); } diff --git a/crates/latkit/shaders/lib/project.wesl b/crates/latkit/shaders/lib/project.wesl index fc7b3ed3..694cad66 100644 --- a/crates/latkit/shaders/lib/project.wesl +++ b/crates/latkit/shaders/lib/project.wesl @@ -1,6 +1,6 @@ // From positions to the screen. Positions are (high, low) pairs: their difference from the camera's // center is taken in parts, so f64 data keeps its precision at any zoom. Every pass projects through -// its eye: the view, the light that casts shadows, or an exported image. +// the view's eye. import package::lib::types::{SPACE_EARTH}; import package::lib::bind::{frame, eye}; diff --git a/crates/latkit/shaders/lib/shade.wesl b/crates/latkit/shaders/lib/shade.wesl deleted file mode 100644 index a6c73007..00000000 --- a/crates/latkit/shaders/lib/shade.wesl +++ /dev/null @@ -1,65 +0,0 @@ -// Light: the ground's shadow map, and the earth's daylight. Casters draw into the map through the -// light's eye, blended by MIN: the most covered (1 − coverage), the lowest caster above the ground, -// and the highest (negated). The ground darkens at the level its lowest caster sets, sharp at contact -// and soft from high up; faces compare their height with the highest caster over them. - -import package::lib::types::SPACE_EARTH; -import package::lib::bind::{frame, eye, ramp}; - -// How far the sun stands below the horizon, in the cosine of its angle, where night has fallen. -const TWILIGHT: f32 = 0.1; - -// How lit the earth is at longitude and latitude `p`, in degrees: 1 by day, down to the night's -// brightness, eased across the terminator; 1 everywhere without daylight. -fn daylight(p: vec2f) -> f32 { - let sun = frame.sun; - if frame.space != SPACE_EARTH || dot(sun.xyz, sun.xyz) == 0.0 { - return 1.0; - } - let r = radians(p); - let n = vec3f(cos(r.y) * cos(r.x), cos(r.y) * sin(r.x), sin(r.y)); - return mix(sun.w, 1.0, smoothstep(-TWILIGHT, TWILIGHT * 0.5, dot(n, sun.xyz))); -} - -// What a caster writes, blended by MIN. -fn caster(coverage: f32, height: f32) -> vec4f { - let h = height + eye.lift; - return vec4f(1.0 - coverage, h, -h, 0.0); -} - -// Where world point `p` falls on the map, cast away from the light onto the ground, and its height. -fn on_map(p: vec3f) -> vec3f { - let l = frame.shade.light.xyz; - let ground = p.xy - l.xy / max(l.z, 1e-3) * p.z; - let r = frame.shade.rect; - return vec3f((ground - r.xy) / (r.zw - r.xy), p.z); -} - -// How much light reaches world point `p`: 1 lit, down to 1 − the shadow's alpha. -fn lit(shadows: texture_2d, p: vec3f, ground: bool) -> f32 { - if frame.shade.on == 0u { - return 1.0; - } - let m = on_map(p); - if any(m.xy < vec2f(0.0)) || any(m.xy > vec2f(1.0)) { - return 1.0; - } - let uv = vec2f(m.x, 1.0 - m.y); - let around = textureSampleLevel(shadows, ramp, uv, 3.0); - let spread = frame.shade.light.w / max(frame.shade.texel, 1e-30); - var shadow = 0.0; - if ground { - // The lowest caster around sets how far the shadow spreads. - let gap = max(around.g, 0.0); - let level = clamp(log2(max(gap * spread, 1.0)), 0.0, frame.shade.levels - 1.0); - shadow = 1.0 - textureSampleLevel(shadows, ramp, uv, level).r; - } else { - // A face is shadowed where something stands higher over it on its light ray. - let gap = max(-around.b - m.z, 0.0); - let level = clamp(log2(max(gap * spread, 1.0)), 0.0, frame.shade.levels - 1.0); - let top = -textureSampleLevel(shadows, ramp, uv, level).b; - let bias = 2.0 * frame.shade.texel; - shadow = smoothstep(bias, bias * 4.0, top - m.z); - } - return 1.0 - frame.shade.color.a * shadow; -} diff --git a/crates/latkit/shaders/lib/types.wesl b/crates/latkit/shaders/lib/types.wesl index da2e8041..d45b45b4 100644 --- a/crates/latkit/shaders/lib/types.wesl +++ b/crates/latkit/shaders/lib/types.wesl @@ -284,7 +284,8 @@ const STATUS: u32 = 16u; const LABELED: u32 = 32u; const STEP_BEFORE: u32 = 64u; const STEP_AFTER: u32 = 128u; -const CASTS: u32 = 256u; +// A vertex mark whose blocks and groups drop shadows onto a diagram's sheet. +const SHADOW: u32 = 256u; // A vertex mark whose rows hold rows: drawn as containers. const GROUP: u32 = 512u; @@ -410,21 +411,6 @@ const LIST_HEADER: u32 = 12u; const SCAN_WORKGROUP: u32 = 256u; -// How shadows fall: the light, how they darken and soften, and the ground the map covers. -struct Shade { - color: vec4f, - // The direction toward the light, in world units, and how much shadows spread per unit of height. - light: vec4f, - // The ground the map covers: least x and y, greatest x and y, in world units. - rect: vec4f, - // World units flat marks cast from, world units per texel of the map, its levels, and whether - // anything casts. - lift: f32, - texel: f32, - levels: f32, - on: u32, -} - // What every draw and dispatch of a frame reads. struct Frame { center_hi: vec2f, @@ -491,23 +477,22 @@ struct Frame { sun: vec4f, // The air glowing around the globe; clear where the earth has none. atmosphere: vec4f, - shade: Shade, + // What a diagram's blocks and groups drop their shadows in. + shadow: vec4f, theme: array, } -// What one pass draws through: the view, or the light casting shadows. +// What a frame draws through: the view's camera. struct Eye { clip_from_world: mat4x4f, world_from_clip: mat4x4f, // The camera's position in world units. position: vec4f, - // The target's size in CSS pixels; the light's map counts CSS pixels of the view it serves. + // The target's size in CSS pixels. size: vec2f, px_per_unit: vec2f, // Target pixels per CSS pixel. pixel_ratio: f32, - // World units casters are raised by: the shadow's lift in the light's eye, 0 elsewhere. - lift: f32, } // One mark's parameters, at a 256-byte aligned dynamic offset. diff --git a/crates/latkit/src/derive/mod.rs b/crates/latkit/src/derive/mod.rs index dd085c10..156cce13 100644 --- a/crates/latkit/src/derive/mod.rs +++ b/crates/latkit/src/derive/mod.rs @@ -2,14 +2,13 @@ //! a stamp of what it reads, so it works again only when that changes; work small enough for the //! frame done on it, within one budget all derivations share; larger work a host job answering a //! patch, kept only while what it was asked from still holds; and tables it writes beside the tables -//! marks draw, row for row, which marks read like data. What the GPU works out (the shadow map, -//! normals, segment counts, label placement) follows the same stamps, its answers read back. +//! marks draw, row for row, which marks read like data. What the GPU works out (normals, segment +//! counts, label placement) follows the same stamps, its answers read back. //! //! The derivations: a diagram's sheet (places, sizes, boxes, wires); elsewhere the lanes of edges //! sharing their ends, the stars of nets, and where the scene has a layout the places of vertex rows -//! without positions; -//! the triangles of every area; and the tiles the camera asks for, which patch datasets the view -//! owns. +//! without positions; the shapes of geometry fields; the triangles of every area; and the tiles the +//! camera asks for, which patch datasets the view owns. pub(crate) mod graph; mod lanes; diff --git a/crates/latkit/src/derive/order.rs b/crates/latkit/src/derive/order.rs index c243b58a..21f8142c 100644 --- a/crates/latkit/src/derive/order.rs +++ b/crates/latkit/src/derive/order.rs @@ -34,11 +34,17 @@ pub(crate) struct Lifted { } impl Lifted { + /// `rows` of a table of `len` rows lifted in a sample of `sample` rows, which they count toward; + /// and the step of the table's own even sample, which takes the rest. + pub(crate) fn within(rows: Vec, len: u32, sample: u32) -> (Lifted, u32) { + let rows = ascending(rows, len); + let step = len.div_ceil(sample.saturating_sub(rows.len() as u32).max(1)).max(1); + (Lifted::new(rows, len, step), step) + } + /// `rows` of a table of `len` rows sampled every `step`th. - pub(crate) fn new(mut rows: Vec, len: u32, step: u32) -> Lifted { - rows.retain(|&r| r < len); - rows.sort_unstable(); - rows.dedup(); + fn new(rows: Vec, len: u32, step: u32) -> Lifted { + let rows = ascending(rows, len); let mut places: Vec = rows.iter().map(|&r| sample_rank(r, len, step)).collect(); places.sort_unstable(); let stamp = stamp_of(&rows); @@ -50,6 +56,14 @@ impl Lifted { } } +/// `rows` of a table of `len` rows, ascending, each once. +fn ascending(mut rows: Vec, len: u32) -> Vec { + rows.retain(|&r| r < len); + rows.sort_unstable(); + rows.dedup(); + rows +} + /// Where row `r` draws in an even sample of `rows` rows every `step`th: the sample first, then the /// rest, each in their own order. fn sample_rank(r: u32, rows: u32, step: u32) -> u32 { diff --git a/crates/latkit/src/derive/pyramid.rs b/crates/latkit/src/derive/pyramid.rs index 5401a92a..ae787748 100644 --- a/crates/latkit/src/derive/pyramid.rs +++ b/crates/latkit/src/derive/pyramid.rs @@ -545,6 +545,21 @@ fn pages_past(ranges: &[(u8, Range)], last: Option) -> (u32, Option)>, mut rows: u32) -> Vec<(u8, Range)> { + let mut out = Vec::new(); + for (z, range) in ranges { + if rows == 0 { + break; + } + let n = range.len().min(rows as usize) as u32; + out.push((z, range.start..range.start + n)); + rows -= n; + } + out +} + /// Sub-cell `cell` of tile `key`, numbered along the curve. fn sub_key(key: TileKey, cell: usize) -> TileKey { let (mut x, mut y) = (0, 0); @@ -599,16 +614,22 @@ impl Pyramid { /// level, every tile its rows reach into what the camera shows from, a large one only where its /// sub-cells do, splitting while a tile draws larger than its size. A drawing holds whole pages of /// the order, so a level whose pages would hold more than `rows` rows goes, coarser ones drawing - /// whole in its place. - pub(crate) fn wanted(&self, sight: &Sight<'_>, domain: &Domain, rows: u32) -> Vec<(u8, Range)> { - if self.tiles.len() == 1 && self.tiles[0].key.z == 0 { - return vec![(0, self.tiles[0].rows.clone())]; + /// whole in its place; the coarsest, where it alone would, draws as many of its first rows as fit, + /// as one whose rows cannot land finer does, edges spanning the square. And whether `rows` held + /// back any of what the camera asks for. + pub(crate) fn wanted(&self, sight: &Sight<'_>, domain: &Domain, rows: u32) -> (Vec<(u8, Range)>, bool) { + let fit = rows.div_ceil(PAGE_ROWS); + if let [tile] = &self.tiles[..] + && tile.key.z == 0 + && tile.rows.len() as u32 <= fit * PAGE_ROWS + { + return (vec![(0, tile.rows.clone())], false); } let region: Region = sight.region(); let focus = sight.focus(); let limit = TILE_PX * 1.25; let mut out: Vec<(u8, Range)> = Vec::new(); - let (fit, mut held, mut last) = (rows.div_ceil(PAGE_ROWS), 0_u32, None); + let (mut held, mut last) = (0_u32, None); let mut level = vec![TileKey { z: 0, x: 0, y: 0 }]; while !level.is_empty() { let mut found = Vec::new(); @@ -648,14 +669,17 @@ impl Pyramid { } // A level whose pages would pass what fits goes: the coarser ones draw in its place. let (pages, reached) = pages_past(&found, last); - if held + pages > fit && !out.is_empty() { - break; + if held + pages > fit { + if out.is_empty() { + out = first(found, fit * PAGE_ROWS); + } + return (out, true); } (held, last) = (held + pages, reached); out.extend(found); level = next; } - out + (out, false) } /// The pyramids a job answered, by the place of the table among those asked, as @@ -839,6 +863,17 @@ mod tests { assert_eq!(pages_past(&below, last), (2, Some(3)), "pages 2 and 3; page 1 is held already"); } + #[test] + fn a_level_alone_past_what_fits_draws_its_first_rows() { + let ranges = vec![(0, 0..3 * PAGE_ROWS), (0, 5 * PAGE_ROWS..6 * PAGE_ROWS)]; + assert_eq!(first(ranges.clone(), 2 * PAGE_ROWS), [(0, 0..2 * PAGE_ROWS)], "its first pages"); + assert_eq!( + first(ranges, 4 * PAGE_ROWS), + [(0, 0..3 * PAGE_ROWS), (0, 5 * PAGE_ROWS..6 * PAGE_ROWS)], + "every range, where they fit" + ); + } + #[test] fn tiles_number_exactly() { let key = TileKey { z: MAX_LEVEL, x: (1 << 24) - 1, y: 12_345 }; diff --git a/crates/latkit/src/derive/tiled.rs b/crates/latkit/src/derive/tiled.rs index cb75c0ac..b1500b57 100644 --- a/crates/latkit/src/derive/tiled.rs +++ b/crates/latkit/src/derive/tiled.rs @@ -80,12 +80,14 @@ pub(crate) struct Ask<'a> { pub(crate) earth: bool, } -/// What a view draws of a table drawn in part: its order, and the ranges of it the camera asks for, -/// finest tiles first, so what matters more draws on top. +/// What a view draws of a table drawn in part: its order, the ranges of it the camera asks for, +/// finest tiles first, so what matters more draws on top, and whether the rows its plan gives it held +/// back some of what the camera asks for: only then does a plan within less draw less of it. #[derive(Clone, Debug, PartialEq)] pub(crate) struct Chosen { pub(crate) order: Order, pub(crate) ranges: Vec>, + pub(crate) capped: bool, } /// What each table drawn in part draws, by dataset and table. @@ -205,9 +207,9 @@ struct Set { working: Option, /// What the pyramids last asked for were asked from, by table. read: Named, - /// Each table's rows that the samples of the edges ending at it end at, and what they were worked - /// out from. - lifted: Named<(u64, Lifted)>, + /// Each table's rows that the samples of the edges ending at it end at, the step of its own sample, + /// and what they were worked out from. + lifted: Named<(u64, Lifted, u32)>, /// Whether a table draws an even sample of its rows until its pyramid lands. sampling: bool, } @@ -265,16 +267,8 @@ impl Set { { (served[i], served[j]) = (false, false); } - let steps: Vec = drawn - .iter() - .map(|(table, planned)| match planned.draw { - Draw::Tiles { rows } | Draw::First { rows } => { - (table.len() as u32).div_ceil(rows.clamp(1, SAMPLE)).max(1) - } - Draw::Whole => 1, - }) - .collect(); - let mut lifted: Named<(u64, Lifted)> = Named::default(); + let (sample, steps) = samples(&drawn, &ends); + let mut lifted: Named<(u64, Lifted, u32)> = Named::default(); for (i, (table, planned)) in drawn.iter().enumerate() { let key = (source.name().clone(), table.name.clone()); let len = table.len() as u32; @@ -285,28 +279,28 @@ impl Set { self.sampling = true; // The rows the edges sampled end at draw first, as they land with them in a pyramid. let edges: Vec = (0..drawn.len()).filter(|&e| ends[e] == Some(i)).collect(); - let rows = if edges.is_empty() { - Lifted::default() + let (rows, step) = if edges.is_empty() { + (Lifted::default(), steps[i]) } else { let was = self.lifted.get(&table.name); - let made = lift(&drawn, &edges, &steps, i, was); - let rows = made.1.clone(); + let made = lift(&drawn, &edges, &steps, i, sample[i], was); + let chosen = (made.1.clone(), made.2); lifted.insert(table.name.clone(), made); - rows + chosen }; - let order = Order::Sampled { rows: len, step: steps[i], lifted: rows }; + let order = Order::Sampled { rows: len, step, lifted: rows }; let sampled = order.sampled(); - Chosen { order, ranges: std::iter::once(0..sampled).collect() } + Chosen { order, ranges: std::iter::once(0..sampled).collect(), capped: sampled < len } } (Some(placed), draw) => { let pyramid = &placed.pyramid; let made = pyramid.rows(); - let ranges = match draw { - Draw::Whole => std::iter::once(0..len).collect(), + let (ranges, capped) = match draw { + Draw::Whole => (std::iter::once(0..len).collect(), false), // The rows that matter most draw last; rows appended since draw too. - Draw::First { rows } => std::iter::once(made - rows.min(made)..len).collect(), + Draw::First { rows } => (std::iter::once(made - rows.min(made)..len).collect(), rows < made), Draw::Tiles { rows } => { - let mut wanted = + let (mut wanted, capped) = ask.sight.map(|s| pyramid.wanted(s, &placed.domain, rows)).unwrap_or_default(); // Finest first, so what matters more draws on top. crate::order::by_key(&mut wanted, |(z, range)| { @@ -316,10 +310,10 @@ impl Set { if len > made { ranges.push(made..len); } - ranges + (ranges, capped) } }; - Chosen { order: Order::Pyramid { pyramid: pyramid.clone(), rows: len }, ranges } + Chosen { order: Order::Pyramid { pyramid: pyramid.clone(), rows: len }, ranges, capped } } }; choices.insert(key, chosen); @@ -398,16 +392,48 @@ impl Set { } } +/// How many rows each table of `drawn` samples until its pyramid lands, as many as its plan gives it, +/// and every how many rows its sample takes one: the rows the sampled edges ending at a table, by +/// `ends`, end at count toward its rows, so edges sample few enough that their ends take half their +/// table's rows at most, the table's own sample the rest. +fn samples(drawn: &[(&Table, &Planned)], ends: &[Option]) -> (Vec, Vec) { + let sample: Vec = drawn + .iter() + .map(|(table, planned)| match planned.draw { + Draw::Tiles { rows } | Draw::First { rows } => rows.clamp(1, SAMPLE), + Draw::Whole => table.len() as u32, + }) + .collect(); + let steps = drawn + .iter() + .enumerate() + .map(|(e, (table, planned))| { + let ends = ends[e].filter(|&i| planned.draw != Draw::Whole && drawn[i].1.draw != Draw::Whole); + let room = ends.map_or(u32::MAX, |i| { + let references = match &planned.place { + Place::Ends(fields) => fields.len().max(1) as u32, + _ => 1, + }; + (sample[i] / 2 / references).max(1) + }); + (table.len() as u32).div_ceil(sample[e].min(room)).max(1) + }) + .collect(); + (sample, steps) +} + /// The rows of table `drawn[at]` that the samples of the edge tables `edges` among `drawn`, each -/// every `steps`th row, end at, lifted in its own sample: `was` where nothing it was worked out from -/// changed, else worked out again; and what they were worked out from. +/// every `steps`th row, end at, lifted in its own sample of `sample` rows, and the step of its own +/// even sample of the rest: `was` where nothing it was worked out from changed, else worked out +/// again; and what they were worked out from. fn lift( drawn: &[(&Table, &Planned)], edges: &[usize], steps: &[u32], at: usize, - was: Option<&(u64, Lifted)>, -) -> (u64, Lifted) { + sample: u32, + was: Option<&(u64, Lifted, u32)>, +) -> (u64, Lifted, u32) { let target = drawn[at].0; let references = |e: usize| -> Vec<&Reference> { let (table, planned) = drawn[e]; @@ -422,13 +448,13 @@ fn lift( }; let from = stamp_of(( target.len(), - steps[at], + sample, edges .iter() .map(|&e| (drawn[e].0.len(), steps[e], references(e).iter().map(|r| r.version).collect::>())) .collect::>(), )); - if let Some(was) = was.filter(|(had, _)| *had == from) { + if let Some(was) = was.filter(|(had, ..)| *had == from) { return was.clone(); } let mut rows: Vec = Vec::new(); @@ -444,7 +470,8 @@ fn lift( } } } - (from, Lifted::new(rows, target.len() as u32, steps[at])) + let (lifted, step) = Lifted::within(rows, target.len() as u32, sample); + (from, lifted, step) } /// The tables of `source` a pyramid places, each with its plan: every table drawn in part, and every @@ -655,16 +682,16 @@ mod tests { let tiles = |place| Planned { draw: Draw::Tiles { rows: 100 }, priority: None, place }; let (at, ends) = (tiles(Place::Nowhere), tiles(Place::Ends(vec!["a".into(), "b".into()]))); let drawn = [(vertices, &at), (edges, &ends)]; - let (from, lifted) = lift(&drawn, &[1], &[10, 10], 0, None); - let order = Order::Sampled { rows: n as u32, step: 10, lifted: lifted.clone() }; + let (from, lifted, step) = lift(&drawn, &[1], &[10, 10], 0, 300, None); + let order = Order::Sampled { rows: n as u32, step, lifted: lifted.clone() }; // Every edge the edges' sample draws ends at a row the vertices' sample draws. for e in (0..n).step_by(10) { for t in [(e * 7) % n, (e * 13 + 1) % n] { assert!(order.rank(t as u32) < order.sampled(), "edge {e} ends at vertex {t}, drawn first"); } } - assert!(order.sampled() < n as u32, "and the rest wait for the pyramid"); - let again = lift(&drawn, &[1], &[10, 10], 0, Some(&(from, lifted))); + assert!(order.sampled() <= 300, "the rows lifted count toward the sample, the rest waiting"); + let again = lift(&drawn, &[1], &[10, 10], 0, 300, Some(&(from, lifted, step))); assert_eq!(again.0, from, "worked out once while its edges hold"); } } diff --git a/crates/latkit/src/gpu/arena.rs b/crates/latkit/src/gpu/arena.rs index 55b406a9..b16bb127 100644 --- a/crates/latkit/src/gpu/arena.rs +++ b/crates/latkit/src/gpu/arena.rs @@ -343,7 +343,6 @@ impl Arena { return false; } for (key, (offset, class)) in placed { - self.resident += u64::from(class_words(class)) * 4; self.blocks.insert(key, Block { offset, class, written: 0, pins: 0, tick: 0 }); } for (ask, held) in asks.iter_mut().zip(held) { @@ -367,7 +366,6 @@ impl Arena { pub(crate) fn block(&mut self, device: &wgpu::Device, queue: &Queue, words: u32) -> Option { let class = class_of(words); let offset = self.allocate(device, queue, class)?; - self.resident += u64::from(class_words(class)) * 4; self.cleared.push((offset, class)); Some(offset) } @@ -539,6 +537,7 @@ impl Arena { if chunk.live == slots(class) { self.partial[class].remove(&k); } + self.resident += u64::from(class_words(class)) * 4; return Some(k * CHUNK_WORDS + slot * class_words(class)); } if let Some(k) = self.free.pop_first() { diff --git a/crates/latkit/src/gpu/packed.rs b/crates/latkit/src/gpu/packed.rs index e2f1bdc7..8f1c27f0 100644 --- a/crates/latkit/src/gpu/packed.rs +++ b/crates/latkit/src/gpu/packed.rs @@ -107,12 +107,28 @@ impl Packed { /// The id of page `page` of `packing`, packed from `source` in `order`. pub(crate) fn page(&mut self, packing: &Packing, order: &Order, source: Option<&Column>, page: u32) -> PageId { + self.pages_of(packing, order, source, &[page])[0] + } + + /// The ids of pages `wanted` of `packing`, packed from `source` in `order`: the column found once. + pub(crate) fn pages_of( + &mut self, + packing: &Packing, + order: &Order, + source: Option<&Column>, + wanted: &[u32], + ) -> Vec { let pages = self.pages(packing, order, source); - let k = page as usize; - if pages.ids.len() <= k { - pages.ids.resize(k + 1, None); - } - *pages.ids[k].get_or_insert_with(PageId::next) + wanted + .iter() + .map(|&k| { + let k = k as usize; + if pages.ids.len() <= k { + pages.ids.resize(k + 1, None); + } + *pages.ids[k].get_or_insert_with(PageId::next) + }) + .collect() } /// Samples `samples`' rows `rows` hold, page `page` of `packing`, gathered once. diff --git a/crates/latkit/src/gpu/pipelines.rs b/crates/latkit/src/gpu/pipelines.rs index eaf96a7a..b181e50c 100644 --- a/crates/latkit/src/gpu/pipelines.rs +++ b/crates/latkit/src/gpu/pipelines.rs @@ -229,9 +229,6 @@ pub(crate) struct Pipelines { gpu: Weak, } -/// Shaders that draw into the shadow map, custom markers' included. -const CASTERS: [Shader; 5] = - [Shader::PointsCast, Shader::StrokesCast, Shader::TrianglesCast, Shader::Shade, Shader::PointsCustomCast]; /// The probe a frame of `samples` per pixel reads its depth with. pub(crate) fn probe(samples: u32) -> Shader { if samples > 1 { Shader::ProbeMultisampled } else { Shader::Probe } @@ -241,53 +238,25 @@ pub(crate) fn probe(samples: u32) -> Shader { fn topology(shader: Shader) -> wgpu::PrimitiveTopology { // Custom markers draw as points do. match shader { - Shader::Backdrop | Shader::Triangles | Shader::TrianglesCast | Shader::Shade => { - wgpu::PrimitiveTopology::TriangleList - } + Shader::Backdrop | Shader::Triangles => wgpu::PrimitiveTopology::TriangleList, _ => wgpu::PrimitiveTopology::TriangleStrip, } } -/// Whether a shader draws into the shadow map, which has no depth. -fn casts(shader: Shader) -> bool { - CASTERS.contains(&shader) -} - /// How a render shader meets depth, which is reversed: the backdrop writes the globe's, triangles /// write theirs, the overlay and labels ignore it, and every other mark draws where nothing nearer /// was drawn. -fn depth(shader: Shader) -> Option { - if casts(shader) { - return None; - } +fn depth(shader: Shader) -> wgpu::DepthStencilState { let compare = match shader { Shader::Backdrop | Shader::Overlay | Shader::Glyphs => wgpu::CompareFunction::Always, _ => wgpu::CompareFunction::GreaterEqual, }; - Some(wgpu::DepthStencilState { + wgpu::DepthStencilState { format: DEPTH, depth_write_enabled: Some(matches!(shader, Shader::Triangles | Shader::Backdrop)), depth_compare: Some(compare), stencil: wgpu::StencilState::default(), bias: wgpu::DepthBiasState::default(), - }) -} - -/// How a shader's fragments blend: premultiplied over what is drawn; casters keep the least of each -/// channel; map levels replace. -fn blend(shader: Shader) -> Option { - let component = |src, dst, operation| wgpu::BlendComponent { src_factor: src, dst_factor: dst, operation }; - match shader { - Shader::Shade => None, - Shader::PointsCast | Shader::StrokesCast | Shader::TrianglesCast | Shader::PointsCustomCast => { - let least = component(wgpu::BlendFactor::One, wgpu::BlendFactor::One, wgpu::BlendOperation::Min); - Some(wgpu::BlendState { color: least, alpha: least }) - } - _ => { - let over = - component(wgpu::BlendFactor::One, wgpu::BlendFactor::OneMinusSrcAlpha, wgpu::BlendOperation::Add); - Some(wgpu::BlendState { color: over, alpha: over }) - } } } @@ -644,7 +613,7 @@ impl Pipelines { buffers: &[], }, primitive: wgpu::PrimitiveState { topology: topology(shader), ..Default::default() }, - depth_stencil: depth(shader), + depth_stencil: Some(depth(shader)), multisample: wgpu::MultisampleState { count: samples, ..Default::default() }, fragment: Some(wgpu::FragmentState { module, @@ -652,7 +621,8 @@ impl Pipelines { compilation_options: options(), targets: &[Some(wgpu::ColorTargetState { format, - blend: blend(shader), + // Premultiplied over what is drawn. + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), write_mask: wgpu::ColorWrites::ALL, })], }), diff --git a/crates/latkit/src/scene/mark.rs b/crates/latkit/src/scene/mark.rs index 329925b3..0c8b7853 100644 --- a/crates/latkit/src/scene/mark.rs +++ b/crates/latkit/src/scene/mark.rs @@ -67,7 +67,6 @@ pub(crate) struct Style { pub(crate) name: Option, pub(crate) interaction: Interaction, pub(crate) label: Option

kgpyaq;Tnorr5!oeyvT)$ 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�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 i^n(+8cWjM^PmlciDtNYu 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%0DkJ@vsf9p@pJs}FN z1mb7^+?|O9GAs&#sfoQ1k{~7pd*qoNo?b>4fseZyu9+>=+xWEaEc@w$Fs(^MgCal| zUIR5#@Ev5hzx!x>*Oqyno#sxX>$=sQV^8NwA5fSxak_?6Y3`pxZ!WMx{2V#|BF;x@ zqM_d+LnaKQJ8<9Q^nE8`W@B)~fd%rimtdfR16~0P`h}Huo_ntDyA@U1?J$@w6H#*l zH#DPe|5pdJXb2?cGN6ACL@=0-DFyN&8#PMxPFWxoQ&wW_=inWImL5EWPHC~3S`a5f z|0zS}@J2U>l#p27<-+Uey6$@J0`3i&8r)4qwWxr7KIo$n+(t#M5b|6Iz5W`dyK(kq za@Gh57yeB-Y@ji1poGLuESfd$f1bElVLyT+aOD2@um$<7xH*>tp+4-RX#J-kg8=5< zJl8tpTz`-*?i2h7S6o9QIYM^`o`@q>-6(Sj=fQw^AsY=j&>|jT`}ch)#b2CGGB6nw z*!UMx>bSY8mW)o)nu6sKgI`ipLhcld_VO=|f_ULapFQFn9xO9PLq%*XgplJuIJCuj zG?^5oE%{`6$$#C+9qaf18X&654pT#Lqy*d*G$_&6(SKt8s=jzSJ2z>k%72T~Pz*RB zQN&cSA0)P)Ok`m{0qlsgKfF{ba;%5>N#3c$)eAV+b9lema`zeb+Ba=flXD(qonu!v zJTr?m%=djkDqnujH~f%k4{!|L4&M7wsqgt>zwG1F`#`qXcIr=i5Ym5-Q@bpzHa=dp z!L=i*M3Z%KfY9wTt1oahMSbcO6DJQMuYN_2b>{h2Mbe5T>rA{0??Cv%rO9Vd&i0)+ zG+nXs&D^_!&}(?9E8)w8=&)oHTGgEZ5{5(0k}=*H`4Av2po>L*J#7Kq2m zd+5ElQ-Zy8@qqc9H0G5ssiZVOsI2=G%~y`UM5=OKp0sy8THzAFPEhAjeJ9rT;~_|( zSKbcXszcvDV}r%@Z_gY$>$730X6UzOn0pyC=PJMo@bjhDrydT>z$G;b`kZZ6&dBSE z^&9_uf1FYM2v^ANpeQ(p&zoxYOH4hSq{~3Gwg-`%?w&;udvUAF3YtHrY zah9man4Wg6#qu^Z^F4c`4-U|hl9Boh>2mD6`$9P1jgTdWk0Ce1e&0iUxo5-_ulpwxb7<@%*NDmam1oxC`)>oc+ctKFlsV{N{ve zqQ2&yl2LRJ6g?|&ue*&bIL?qFo+Q-LI~!KpDcMXHzR~_7S4J%U_6k*VPY*0)m? z2=@~gU)n<_G}Bd%0hO?CCS93wOl#N2tk8qc2dlf>%0e!Qbd_4BjymD3(5mZ0zh_l zVDq!E#O4M%3Tdr>{?hK(_3*W0yQi2?h z5~k40V%j{x@{0S97Ge8;l?5KE`O&B0$V)$<(BJ!-bnl(%&A0EXXcE@w#U7E}24n6TPqO5Y$iA2K{i`4A+}f`LJ0@PAfY($Im$#2L zLQMZ2=asuO>9PqT;IF$6YG*_GKD$n0od?IPQ5>@ir$48Cg;tgfX&$Am86+G9ox8)X z@h_QO=qCd*?5TMvdl!dMm!utfM$jDZ%6OpxqUeSDni}E`d09Ls(3^S&UFa-Y#1-s@ z!73Vmb^VwiEmSgZ^VbQmjAT%f)leyHIbsNRN7~cAsFGNdfJ}>132y2s*E0~yu&dal z#U8&;p}LGWq4!7$WhlC646&`sM>rlIE_Og2hCpDK&gz3E`jQ45=Odj9b$IxfcAXVw z<`Pia_1RLC>hRYUkw8Bx5tMAAA4EgEUXsDONo>a5;S3vi(oz3pk}Ho8vZ74;r`N zZOl*wOlb~xd75TN={DG>v#TWAIzFt$0@}tpC||rh2q3s64S&E4^Ou3(2^m67lcz@& z@1}3^D;mC2Br!Cgwt&(F;H(otkdlJDP~MMsa&gpp$hfb0HW)g>%G&p55TI;6LNIGO zI_u;W9BVrI41}q6mUCN*=vJsi=vX>!UEG?2wt4$u_$v!s=w1XygT1qp^cBddXsEF& z4er`o-4a`9Q!CF%)<;-5V*z#@F#pBb0Nxm+li+8Uxi>fkBLcc;(qb`jWAJGY7D;{qX0PFyCX6N_cnXTW(U!xN7Vra z!acp5M09EEY)(Olc5$XF2FYTQ=k~_Y!;C=1Y>Q#UWGj+-#b?o)GL+MKym1xt_A|BT ztn%2V&8Z$*6nU0dp)_3Cb8_EF9i+Mp<;RHJ&3yX~CV6Wy+PZqqvr;ooY&yP)m)w0s z&+1oT1cdFxz24S4Badp!R#o`)5WfsAb<=Rrgg_{N3~J4Zc4So)HCVntGEUq*O?>^PP}1ZJnLNu85BlQDiQ290tsAM2Rn3p5gtn0((-kY? z2FJJ*-A)B$iZBfW{+@6pAYl<|a|+-`X$x&77bo!p;(yu= zZHW#mNF0#CpXZ?@S8JX_`NKp(4s~?UnHOiLA_1l**|^ zLnKgJ>i#|R%v4ITdPI7UNlms=%+U1TcHU*(u(wd!b4xj;qLEWpm+Oqo}*!ToLi%nI%j{ooK z{GZ~y@h>=W&HrrtAepOl&5p_r()+Pd7~3b%8r8*MSxgz0#! z+^HRXE-^J7hOck6@4^|hHJgkH%y}A}HrN3l**H03?dixNw4!00^%`~AmvRGSVVvX( z2+LXT-$pI4!M-kz+P15Bx*ef4TpzcFW$`1+iD|l4ZFGkCtNq9Dxpn%YKDJK}S+viy z?9i$w-R(R@LF@@&ibOKG_ktGpV+=xNU`-tU2o#!%-J68h5Ww_QbulO zLErXMp6|4+hr*`}duE?(gs^K-9QgWkkxHwZK%P3xrq zhwt)M=>>QE{5dFrkbH9hUhy;x77XI`@helG~ZyQ-rNY zC&7gs;)On@-I$*QckQ}}Wa2CY!98_~(G`ys%zY zOmMM*{l|3LC(%9n%ZIQooWDq*ksG@i`Q4p8*h?0f@Wid&eiMe-U&_>Q?=QYN5!*)* znZNyk&z98Cv8Y>yJ!N2%IZ#YmIA=!D9xU7H}G$~Gl;Iv&)}Kxdoj zw{P;pYFkewc+7zUP|lLLDupx> zRB1vxZleZtu&(=Ssf&^1VL~_Nzkuyt0^>!bu0$X5tDPQ_g+aoec$MVx>OmqANTU+`ihxTY-EBjVR$6SFf39{lF z1e1;t-2H`{W2Vo12cBck5x8vNoOU$I)&7tHmw8LAbZehSXH)ts?@xa!=bp62k<0-MgHpP15t1!^x&1p-o zU}QfAMpTjvmXxO`oSuFkqm_l;DR4GU(jblwI`WQXD=zCfVyPCn!VH%ChnDZ3>I+b_ zFmhL%h}=Jl+rPIhQ=}dzS*&YW9};%D{s=B4}m-4^(mC>+7_+J|@(Z!NZ1AY)%1L z`smZu%NshK&j`&wq+7-5r!pYXX!c*gX?Y$AdXGs?!R!~yu~_RTUcDpTo#-YI+%J<% z#SmWjqrwc#?N1G}W~ft8?)31d(V`!jXEZ~jO|ifCwiwqZd=uC?ebb_(ee~6gaMa%8 zR2((iW;J}HVUi?{SG~Bjsb4F|k4lyrhC&SS{*|kATI~p{caQWY-^eACAvsA~FQmN4X_R*!!uN`*50Ulm!%{8h7uABgM1$2GGu+O2seRqy2U~;3Y zQcZ+2iEnsA5nlm%L|BV%IHJ99lU*%0jn4{sY8vM4#jh|J8cx@49PA6412)|mg!ap= zR1NQ;R&SxnRG(n8WZ(>41Jj}fLV&~oHqBR@yH~Kd4NEHn8LZ(tQ|o6n%?1s32tw4* zGFD`BpTADmX;g2$e&30t>632Bm>cRm8I`9_N799!{2}V%Nqr;021tDx8Ywlpx%+w< zA}#xY|2S+m|5iQbS$8YE1URY_%O56LYMF7|=blqDy%;+cI5-}R^@&};Q-C;jc+P6k z5t*uqhtlcVg~xf+U`@zwV22C!U^?;(c zS#1Ysn`c6`^>tf3&-qTgwaKGd{`-(YeB+)dUT@`2w54G6J?y~&RYxP|JDHD%dP&3S zS8(%Se_o(M>;|mv@Kst35HrWD zaW{V~p~em3*K=V`_AuB)#^hW?CGyjKq{5APcT(nzT7ly#rJb_qJsi`%oHhAq$?81U z9&Bx_i+}{~Uq-N#3AvOe@+Ve_C%DF#V{Px8wkP9l;*#ptc8M&r8hhPgQ0W`B@(@2l z6+2C};znW!jDsQTmha|SP(qOkKTK})-&&-AAJSzQ(dS>%J&Xy&1Jp;8BPbTHfwt~d z*>!B=pEdxsbO+a@7zC{58a6_;b6Sp<0aGUZ?L(FA-`?WT|4PDXbP+BIWwI`xGYC1A z3HuD`<7w5-kGcN$Jz~Y2pj>oTKdonwRW_AU9u0?j&)+J4Fu)4ODgY1@)4P1oKV%#y zKE4@aQ6{08Mm?6ZQRR;m+Ovx7k1Prbi*~o=TTJ*ZP7V$bL z?wrNXPqGfKIA-cZhc&i^5#y<;WuLfbwuo;iYV2ZskRrQ<4^lv9!36kA9_}B!xY{#_k7D9m+2=n_T5yG^YU*GP1T3_} z%^^!FcRN{lLIf!{YjN}IviU=z*)#gH1sYb!FH=&n!R{%d?7;UE+Tg3{DnZ8LfF%`- zj^mwv?Q6OE40a~cKOpEG)44SENT20*D>$>YNs*W|J{fmI9UJX+P?ikXf&(^K#pIrQ zQBJ|D6LL2H7Hj{H=HBKd%;e9C6LXt$_PtEj!oH30O?{ySocZF>@G@o|7psA^u|DTxV%Aq z>l=UUWHp0mkd-2f=*sJ-mL)9skxQfliA6#j2{apYrf=WQQrS9fiA!mldzf5-WC+Kp z))vTrGJ4D6kTweNc7+=o%$o?5klMZ4A0%O6Xc zz{0fh%>1o@T2c-6;&yP%u7Q+yLc&$6xdOV-Y5-A>VBko*JIlY@=7fx1n5dO~v|i?6 zrp#_lyiA15fwS&PYF-LD4x3h_P#DpdlY_lF38FXAZS837qexrk%7$9*)inpk-7Y6T zlzn>ay%x;i%VMQ}z-vcj3t1r}#Ee5@1obU9w>hUCT?lTQ-W9NofrO%FDWm$DIAhSogHQ?K7 z3&X8$4lvShwtf=nF4E#zjT^wcw#eFLax`%Oa?jQCewQ|%SmbW~R;UTm3mRL>Asa7geX+Ic7 z;awhlN)sJ(1+~ogIyp#HjlRmZ1~{G>4K0hpN;g<={L|S}22UMVP>c4m>)Us<8@b-o zdCH{Y$kJm6R^Z3R29Sy6f%t_Bz1~HRGA>}bZSZ&KT|5VS*w&6HkCftdDNS~1gMS1> z|FB}pj1{YE)Y)%bdu)39oAi0bC6{9W*`ox^{0LRtQx+Ze^7 z-kyAIwX%mtV}40mkG7akpRFMZ!^7rm$`x?)<81EzQh@_$8&3Lwy=8~xn5Xp%chOYA zU)%nn$U^+ja`;8}y)6^Y?45b_&LLytj`sfZl5{OJy0eb)BHcogrm!#gtM=onRglMS z+hoH3W_n|Hw&WOUP~*RnI{2l3RlDF`GsE4i*Q)V<3ux(a{}#~x8GgNj`+gEcYG5U& zIG5*Ejpnw0C$$L1Rx@riaL1GHEknA^{HX2H4IUCoF$A)4Tj(_%(bs5}BrV;n@Rj42 z&AQzk4b(eq-(G5=-k~yb1Ca(lH!GN z)PXN&=!qIc{#u~05h1pG+`=Ei5cYxR&)bFbBGH*AGVfq|Ghr!Er)UJ134}ZC#0q^H zq%nbtCbdK|MJ*ZxwOdqqmxUj|lm~$PeGQgqDlC$be1TM8@89Ds+H3x|OmI^uX)njs z0y92E@%j}jg2$a8mM$dFyKHR)DKsiEEIUYdW#T6dB`)g7d%>rlUxYN zo-qr5Hdj_|G<5hzE|hV11zFIu0;w`+$Ce6QJ6lz_G+`8xxdIN)fj1E=?%#sv>fWti z$L#u=L@%Op%h^`}81mPe7R!mJM*^l&DY4Hz_na~>WbXnBdt-EB@D-M%jEFdEb~^40 zRYR%Mpz~0+E3a;p%i&e)o??Yt9>IpcH0$N^3Bmh*il@WJrE#7`cHi*3Wr2a@1$*!_ zap6kv2xRxN3Z4gqRqB@Gh`dG0W#$%QoU19liRubn^LX@MV&l%%DdJ-=fKd-7UI6wJv6uckd#ZG zT^y&OsQe!lkhtL0JSUcl(Fp_wXG+re9m`YPAkP#k&ty+ci{_SwNq~hN)lg6|=!djF z7nrjo968|T8sPKjN9AD&Jx~3a0#uq&UimkBQIHGqM-8zzd&txpa42^-pIp*X4FZcx z0rf7EW-!kgk(j3(omTU}Q=X}ITHkA1puIgIhqU~np-{W9>OxE3t=3rG?7qOCY=fG zHRw5H>ad$`SzFt3^d}UHw7Fm&QdOniX^VBOa-LcQ<-3tFyqWzri7g*diN4dX0D5?J z-3kLjpsNdRI%>}ly1FPrPqAs#+eo?kpG+FxisgKi2?h9APg z^8_J?2m7p>F?a;Lp1EFP4K7)d2oN0CaL}%nPJ4HS%%q!f2cp_bU+zM^oTd2`#@kLI zQhZIj2J%JjLfLc0&T{_BbGqnsuc$bi3ZQ%+HeIC0lT^|Drz#C9Q_n^K@ON9Z)a=yG0?3c`0oN72YIjaKbNi=}Elz7-WUV)3 z@34PQMUp&J9dxr?p~cOLFGQiPiFJ$73$(jPt?gKTZ(s}ri6-$x+wUqeIuO}a!?#GQ zo|yURtW^eZ990Zb85p;i*JAj$N`uL%V|8#upu&a<#sx5cS;Uin+B`XS=&woUJhtLU zAmtwJ1o_=Z+P<1xBh%b~p?zR~J~aJYFCQE56>Az;4UgNSOi`!x{5CJWAtCPR_z(mT_ZT_x%239g@{ zXves;STlSNZQr&+qs}f5elZ9QjrytZI?L|k3CjY0hYH9!FBtF(y7&2No!YUZ5~h(K zB;nnAb0G4ivdaOYz)wF@OHxiWTh9vL@tJJ63)f%*I@M35)j$#eX;k*tIg!@zL$VAlF0Fkv|EWvfduGC*L4m@Voa-&;<*N^cTr^vM z&co9m7CD>#$87+e@&DS3vj%59IBon$~{v zXQ9kL+R^&oq=A8ZcJb7o345~xt>6K4J;!S8K9molyRNMecl5$xe-6#8ws7Aq-{u_4 z;h~AEDaC_>O{PJG+`kNJ%It+2kRMQ3#~s(`vJ8AtWy=iqvghB)W{^b>i3Zp7P^hV>x>ffq@O76`-=Bl6jp@j@{*hPt;=XZo-R~A zZqMej%v6yze?v$9!V@$MZ}oy?%mCC~Z*0KZ;E~$z62P(D<0Sa4Lk)<;Cbcn|f3(1I zH7=^WWfF1YjoUNR>Ii8@*&@5ww|vXwE83CIA43a=u%rftqzrE2pyTZozugJ-8o86y z9E21G(=$b(+jb!4v5pJm^b?!Z*rW|5r@E{|tQ>T+UEx%ZNDik-8tKQAw?Z`CauAK1 z;X7ZxY%<-R=b05!Q!p@ZTfPJV&RvveqW3l>Cs~{EMCvvr(sF{dfHx~_RNxf+zFi5G zS>lf~d!N{|LrJvMM&C)!zo2)glE2LA`j%M8Xn}hFYF21c;?L~GjzprK(l(S<84!PC zNp4xyoG3&h0bEZybaNwGC>EC~GR~eKC*mr!C=%BPYV5bb--PVbGozi>Ppqn2D_2pe zf-k8MHSBW86#T)|_`0>ZX0@g$&wj2uoBOeJcCvgAZoVIPhfp$GiKL(t^gL-b;Y)m& zajjQW*Mw}+(f3;D=j`t!l?td$*E8O?LQKGoGI{68!R)V4t{hgel~gUWjQzVa>Nw9b z4a~8iY)TJlPqeNnDQ}*f?JZ}jYqgik`goWyi!SyOTtagGqZlU0|Jx_>-6OXKqRzFQ zV1_Npm}F|eTJu>q)x5V1O|*6QX2Ke?XaY5{DDvT*>AIlhJM|)2roO{W_<-WR0miV6 zk&2GRY?BF(O4yZvsN^c69?IzZaWz~+{Yke&zwQC`rWE2(;GfYBqUaK0S4L~}pQs_m zoiX6f0a_@=cL?nG(Kh9qm+kgQ*>|wXk6PiJ(KU-h0xa`R_Rsr}A zV(b?|2q1Wa=`!*{nP+GKKs>qqx`QqlM39`~V6`f~`%3Xvm@;e*Y)dUo;xP4pZW^=i ztp~*_82a`&mdbgg&Ap~GNy!!&&plb+78KK(|4xk3bSjN9$vUrxTP!%e1nwrefdO7?ajvN;YKum*w1=pD}ytpZ<3ju@k%7+8w?`s`7 zpdasj8HM7;gZ%o0oRGIdPYf7$G-Cy{V48DfPP%t7nq3lffuP*0?-;Z_8 zsLZkMcFoVNs$`k(0j}FWlu>K}HT8l%@ZGjd-vJa#fVl7%)DV?q)O|!(`eh@;y3&n? zJ+HN#5Om-uffry|Ae7AE%w_@F`XVvhm?es&x^QZ3fOgvoqg1mSwZvR&o|2!LtPb?a z@YJ2&C|C;e*e^bVcCa-I@e+qs=3Ib?g$?lN{FD(L6GVuGDrRb!(8g`hIJqRLVPZh` zteBT{cWnvJG8M?Qx$UO@bAqT=JJd>(njb94mTb+y&+V>#4=9O6z3#n_8AmL-nPYKv zzQH{Nz%s;9L_M?nJwH=}XZ67`2}=Wo2+Tv}%TtwLHufse1S8IqkFtL$@PCvc0>Iy^ z3)2@N%|bDPRju3vIq}H-0obTBTj2$l$TcSU9Vai;afJ0z4p4h&8voJM}JxYGVV+4 zDJ@#VZ&C|X4`(o!?dJa}C@sB=1A-Z7#JZc4^a9UmUxdJiMh$eCO(^8LhWk_q8tj1x zzqmF|I&KRr(@;~%+_iG`pPfOlkSKr1Vfw(Y_0d8rQ8Qt+eqUu7Dvq+8xJY;aW9Hu; zHMdya8w+XE7Vn|z)froWtTOhR3NO$+tQ#eBL@DL7kDgqp+-fAV>NDdHwx+aoU5_qp zUx<}F?4=7Q(7Wlj%=OO{9D&QO*j_D`h#bmejp_=z`udndMA6M;zBi^iL zBC9S^)x8an1bxCa(Ch6k;XcB+qvn|n=~k?g$-tjoLh1;}%YCka69@igp*Lg=cKpCE zD83!RBaE67vk<}Fr~_8 zNP$%x+=H&5hl`|I-Y@mpc}42<>%L>Aa?Zl<3m*Pfw7t4F3)x)Vncm-R635h%cN?>K z6P;6YZrSrI(|R({8s3-Wuw{s+X5svk4Bt^2njZ4fHl1dH>i$2TpErTXGOXiIxc*NA zzs^DT%J_S`={FKNrxP9ra{X#j?8{;z?Rdj#Q$+vDoDcsc$9)cDZO>yE)|cdGv`+wv;w~ z0MWkC%jr3o9y<*wELa8)uf0b)pkm8X`|LM0f8xo7t5qu$59I{SdbPpuvy(8jC3>I2 zz!03xv7z%>0Uu7MA7H?08!*)FhB51E4cne{vDxqOu(9G!+p_L8FP zk(-#32R2dK8j!wM)z3X>F8yS2e(E}e1z$F!F@Yy{UZMvkgF6_ig>Q^wIJ2$OlIx6h zo&|}lP%KOyFOX+bnPHvm*<noZRdm;9=5v(?=5C+Ri(}I*@TT+7k2{5)e z(C-8*Rk-@O)zKyLZ1qQ08kZs}=_>TSipLNmKyapln4KlcJSHOxhnQjHR z->M=Day@K|{`G-X5X4eRY4FIciWvfL0~*K@_pwK3uA)EVQ$}XCqlaKT(Y@qWW~Lrw zk>mUZ5qAzokK$vkJJgdXQ}cpn&XJ_{vt3wk-9C~i{GMNOx|MCrEAWdG@dPO|6^bA-tb zTWzC8_4^^UMEQN>uXIU824|iu^!xMu-%-ZxW`@M3t5@V#e@~mPD_2Q&_i`%@R+IfY z>T8+VRmk7+&cZz>;hhm|9YJ)7CGxsKa$t0gAr_~Axg(oK6=Bs}`pzX+ZodPI^Ccy; zmw@=dzF`CAjY2c<{B6jy0(vdwX)5N_g=EK#ZpoIor3glnra^X%UngB+V$bK$hm0r_ zQ6XLz&n^y8MY~zNPej2lV4DLt^(qV+IONP;-Q%Ev?U!>C)V9_cV~-Z>l}b*ou!s-2 z2oL$(-<`HFS{NBBEAZvE=o)=CMhu%bQ8T$lNO8B^ObwZTY5wBKP03)|TCymYe6+9k zm-Vnmn%`?;>ZaLqVP=2gF9)6{Mi?upp$l^nV20@Pi2NX7|0|rzjh-AAEUgmU>l1o0 z&=rSw)6HtbT%$;ko}-}<$S!*d<-o@iPys;@4j8T7nDeokEWPKDV!i>CCnoV)jF~ol z1$#j_eCVDCEw+Rt3^kjb^FDZ9lL`9L^R;Dpn1-O1@fe0xZnH1Z#La6iuRwl$g+k(F ziFsa4`g#1+N6W&By?=+i%r9NC|GbA02n&Q-(`l~dv{4`4E@@I$(MayQUwgN54N_fU zOZ%IY)3-25uWyJd+m&|)=VI=eC$2wRXMUR!k_9G{6AaPdQh36_k?;Ug@ z4+et-t3?314Gpd(o;J0gd)Y}cJ@y=pEjE+y0^fntLtys%vKjcg*laGO2m32?Qq$lD zd@57OiznSd;{ha{Q#fk~-iGzq)=Xi8et$zt!5YpgsRie4cpL&+x z6eyiND_4^j&7lR8z^I9j8Pn(d5KO8`sioBV0OwyZ2*V*lEV}D>JiWRVDj^{1$`l6k z+Rgpj)6aX5hX#huc~Db5YOORCUacNl7FD@t zO!xHAYSYY0gAGbR^wM8svHoln)Ej552rF8H6gwKNV2oos&HP&>kl(qlnD;xlPcf@z zMkY7J1j5RbQb{ZOawj3P9M(0ySS}eP19%YTuYergl5rMAP@Iiu#58PO4BuomkY;jn z{T~C(?0TMg{#6DN(NoX<3X!8$=*Y^mTK)fh{#}bH>Zvo>mfr!VN(&M4y&1|{3oI+j(>XSiIXFYT$ zf(|fZOxNvDVo~!-2nabV1hiSyI1x%Jq~=AQr*gklSL^O~9D19&E%=I)Y?L_5d7Byb z+gE9xpAwE&2L+vV&>Kg#pW-Y50|uFYr=jCz0ZC#*YloZkp^f-J9%&O(c@Q#I02!n2O&}iF^f6z zdLmY5Y2#LWQ-i59LRFmxkwxpkJVCtPy_gfT9rBTV>5q+k@Fxe%&iR zDz}um+ul)axoNHomAla~|NP)POYo0HTL9foJ{AxhwQhvLRydA{D-0w*V~#8aoES|l z>5^EKxN%yx>JkG@nqIgSFyZ(3AxCmfQEZ8N(CT(dsV|_*%ZLxk(Xw?^Z3OPsmec`a znw&h;;s9TNq9PIO~{ODcTz_ZhNy4pIIff*36lv9f}xcJ#qme zDcrd48_@S3@`GQ`Jz)eST=GCs$aMv=;TB zLgPTdXmHudJ+yLB?eLBt+H16tD(mr%C>d|8Hw769Z3(Y<;wM%rQtuIOV)(W(>oOIulvOcXFZEu53+yvgy!WAk^(1ijqT_7?>a#o<=1IbJP%^6hy8nMkur6yCp+PPoD**hyc4KaTpsy{ zru@G`Oh6%=4VaX5o=UIIM!X5Y6KMQa?lWvE4FeWbHo%+ifQcG&!hg}RgTtyAIeByp z$?+f26g21^iJ>lOWH-tucY=rY+XgUb3HHA-_z3_7zhHQ#?*- z1w7Fmng`Ux?PS!zpHHbadlz}BZ`9WdnCMBo>$Co91v^r}HPO=>=iPjw4$##1x6@u- zEO(c0#r<7gZm0|rhdWM`Ru_yReXFsPw^I@W>s%dg5ns8N4hXS^O(PHrbJ#6qy$@OQ z+w3sTee-R<_nO{Y(;;bRYXH&)>^E2(ZM2ZgcT4wOk(XKC-n`jD;k?I$_bu#+N(Rn% z@jsAb@*WYcu zdZekF^zO1IB|Ne(vJG^jp}CjHo|+E+Rvl_x9o=sQ`(L1ApMy1S{*Y=uK+=b=Jc&#Y zZ2;tA2eYNj7%nX5>|otFdq6vL*F7@7Zn~M- zy#WkkW$eRl_DH=ic?+=JM#t64dp0!XpM($f%*l^DXayhp?DPFl$xu`rL}tZ=Gp(~B z{2HR#42^EQh5_;&a34LyK5(-weCZ1LR;q7&3;~7OZ$Ac@@?&5XItMp%fPQqn2{;1F zq^+s4C?4ggg9=irUN$d3J);{8Qjo23Yja5!)#Y_nkBKrGi7!<4BuW~Luto1|2p5J) z-u&%2{+p)Mu8mC91)DQ~#%02Qu;qQj(T?#YV0!l5!SH+Z!=C-!ADO+odpz#?j6lwc zU-GUX6Zo|bu?UNNQwORUla^ua_(-i1%o)toQ=Zh2M%D-OUsnEDDU#@Rh&3c1_44ut z7udu<8{LZfYG-9moWPH7CkBR|0wBsHN@4ULwtw~AuXr-G>C&&ay}e9Od<$-2t&ryh zfp>tVPv+}Kf0%&+XSoIYN4YOt8hXYeQ<;KabjP}EbedagBV;pm>=C1u7OPqyEW+mL zgupDNnv_H6&@iH|ZE4xo7%@Dv+Ln!?$}RiK7XVRc8`^{@FBXE|j5ErcJi?J)D(|Em z!-?0tbQiREz`CKtP4~zdN%K%H>p-3@u)F49Myw>Pn$GrPY{ATVG=B4Xo}2;$n;yK` zG+2)#tf4WZ=lT3>vco25G`XhG@WPV$Vj*oGdO(MKW?C8ptT}k(=289^4ey8R##5nqc6`&*55D#1On<^B9m_7TTCjY@%=JL^+ z0^BI{R8)r-V9%sod9w3}hPHW;AD)wSA~$<~hTcp21G1$D0rbmmX@Q{FtzPt^Tdh^5 z$C=Ir$k}2+UJiw4f@ULuDLH_%e|YO29dmMyd}00gW{qvchcsoW)yMs~xE<_q z%F1kVLjZxC_&3@O0uE-@bFKOBpKn40Fi-9V;J5 zGIEqW(60!t<`Ul66t@`W4@P@VRU%HRonv@V+!*8FT69_dhcRr)cLIrIBTT6-U?j~N z>Pq(uYIicxOLY(960?eAu^`JNkL>jAHnI{Mf7;tq&>7HRnseS>!Xe6Ct15$ak;nK4 z?Tl!dw`1U)%G$$4qQTghR7XGNhm%pg&uHmUD*SUwbrRZLJbMM8`R5Bo`LD{sb6L}Q zrBXBpXzAR)RTcwrdO!RK3<@>iM4Nzw&tV_OWi~%1wm>E@+^7%+9e;dA)Iv4qVOOvoA@`J6`ix zHc0BK7=d2TwyS>Rz42ta{;pQ@ouYQMm9+?42u>Bw=K)pU3d_kY!aiegJy{2&1P?%_ zqCw|aU$x~akOqD{V;T?l&}MtD!;7omL}+kpz~RQ939e*PMtVT63vR!gUyzJFVX)(+ zdz7>kdgBt#w`bx<7Xr0t^E0eDrJd7iLD7JXZ_jvdF}y5wP)&6mUnSnRaW{VzbW1Te z^!|K%;ELpiT}hn{Qt5kbu+q5NGoH)rVV^+Sh&G{$IKBWGKk2C4)EvlpykGy*DyNa2 zj)10@^0G|D3h}~?Oy#wFtu;Tw2SNuL+#ay716RKN&3ZboNMmqc(O7vr!U*v z@hUYe)_Ow8lbGIEh@a3Vy~561UUDOO&xn?Vnl10Bo~K=)vNks^}SI_*ZcjZJ5~jo!*4 zWtaeaBQE5bdzffxDy7Y7lkVuIXY=4vc}kT%owp&tug9kZ*h|q3qe&y3lyy3ItHm?+ zOr{%@Rqe<9Tr9fQCwe);GuW)<38UO#NF(4558JJF=LKJX<*(a3E4$@kyV=jjMh<9O z>>|dj*|>if5i#^WJz5~ZMJ*`Y)ZXF(zR30X#2zmw-|$+DmEyoaVf(0iR@k>*M?x)W z+=ru{5Hk7^EMJieAYW9(>J;=;I;DY|tSiRWK|DV-9$kZ(>EJF4-|CkK(4iQnRNF3m zu<_|U3H5zOQc${n87#1G42~02N6>Z{c~Tx3)z51byv7Ok_?T_to^|Za#&S#XPLNdD zW`M~8T=Fr7MVD!!<7o#ll%*}6Hji0a)}mn0_S z=jrd$Y@C5aar_w~L&uoBsQ4JcRwAD&S2VE#{aD4iNP)Nhr*~_r?(f9>8s0Jc|Fofh zs}?*$!8g@t^oJFD9E3!twKrR<=Iw!_kzm6e180Yi>t0nZYcl-yfryX?Yprs>3UUzU zTyah))B>m!d9@y}MzPdIfXo(^-B6X?yQ#I}HqM&ywI|R(|x~oP-j= z&q@6A{;Eo*!xRo!PJgxTBE8kXAGnPeR5 z33L7!ovB6gRNf!yl8TF3QyA-rsyJdz9q|*i0*$^3DU>xy1y5oB5zJRF$~pxW?8;<2 z1bg`$x?S-uAUzWSAuOx4zjXurfE%r*Sbmd4l!y?YnKMcQWo5jn?->er<(X!AJ&rr- zvm`7|=VuzdIdU*1tdq(}<|#f@^PBS6wLeF0!3KFOQdG?r#2fEA@t;Sy0v;iPKEsFp zwP3hlr(orU$$$Uh-1)hoa4Cm_#^u9X{ewR~_;nz~0`!^bf{KP)J=HsP) z%co-ieqGsHZ9|w`#OAN%m2XW4b7Mw#d(>IpRD;Fa#`ET?_x6%fk#p2QqHq7GFS0C* zV;8ZJ#iB5JT*C^56@i6YzP;xM=G5?h6DR;g{O6%9Kz9Y7R|WRLIa&Z7nRQrTZ$DAG zw^{k?!B>R72dFd#tUh6Xhtk{NtpcLk`zTwtfS|$k!}o1IbKX*YOx+6*M)zC=Q%g0?k}^QcZxw6_?7FUUpoJk8IY`fj z=Ud@?)DPHe4jVq-U$ne&W&57TdU0tN1fEmR_WjYr>E z#}Q*IZ5$ROdefxW65qXj4|b4wb*lGT4n zEDWq;gK``FiH8QZso_h&QV{_MddpLSm7CgE-uubcOb7ekN$^dS6iaXPWDL~#b^biQ zJ+X*ymKZT*+z~E+`Q~xV+q9^S7^BRBTLO}DuH$XHsQBhy3!-AwEHEI3D_ZI#H;(uK zUW2gefN110KUR{t;$3D`2=puxJtB9^?OITH8E zPovy2(H&Ud|1@czpS{%J>^m0TYC58b`5SzlbWFPNkE!(2B6ep6Pc)n^?+vrZ`W1&wPH*j$PQtf-hHNdTdGvSq%AH7De84PY(tRgK7Ny@{fxwda` zxxkonL!V7<_W66rcotrXiLUfGA^ZJ@Zwn|I))30R!BWUF%jX@b`mmz03VGn+0Bh!3 z5)u4bvti6MjR!9_Ct&3M2^Va46zK-~} z4SMqk6T4iFR0M8>j6(y-O(r)XXWcE5V&S5T{as=Id7Y7ezt3yW!G`0_!)qwJKow;V zi=Cr-0Ze#{p+`Z@I5%sp!?bOAS9Ml0;2&bk-a}+p*v{Xhwj5kF$0DDCAsWgx_ZK^< zW>esQcSKBxtuFd#AK3E?^12eZu!6v#L0v(H-iFRl3+fnUHpQK{9BHpW{_-+x+5-Fj z%U! zx^od{_)mCrJcOZ7-=P~cY|l|n4J`3nIy$-mg?t^vN_yj0F?O6();h%tc|l14)A30p zZvQO}a3PX$Zv$oN6=(z!HoUt zr&w<5#R~JGi!QS3@exRMqWYO#1){eQar;WaBiFPS8C@@#^X*=C;BbC7zEL#XK;Q6c2wW|0gDfm@2|+a{K!Fdi3)z~{`@Zcf*~dg z^-Zuy2TujqwVLIV4HRBUoaU~wEMyx*L6H7RfxYcS_-W?d5R)ql&RodJCgj%)L1K8h z&B+vWvFH4n__?=7uLM z3+R2vRxN`SI*SGAQWsxEMt3r$#~g2SaQvTdWE}2B&1h<*a_ zdU{!a@ScKwhqxW0VmATO;Ojc*E7>*P2pv+K1g0{1X5O1h=NL4 zR|5ADuB%gE+VnI55hX{1L4isO#{ZumxXTt~zJ9=zZS9y%iuwz-%+ub5n z51Lk|UMxu|(jh7ivl~5`edojO=8^9=cWW>P1Ic+<$$EXsVuM;qAU7+C(AFR9C_FVa zA&y*{;ERLuH6dZw;3Jz!hZa{K?U*_N4v@F~kNP9M@0T_@6(z}T!f>{*x#{NXVnk+5 zmW7Xr`@ozbs;?`3VuP<>c0>~u`HuC2KSyY^>3}WqhNu3db0fq!qe{cC0$)Yqc0cs| zC;E~3R+z=0n;arGSAG2DUXYoNxz-CvDXl*#?sE4NeXKs)00_rV`;ew@3l|*pUxqP< z*_k#sl#pCVpP)JbetAnn8@;iV;@@#tOm<*HJW6(kC1%gr1bRU-DrYglTbPfr2O(?DjV8f9yTKTS^&N{afQoMDQfwIupKuD`kEK5cZzANLjXSTzJUdwa zJEo?p2Qg88`_m1SS|ItGKs@ApUHc)u!8~j<7`5X1A$=>0WzTGo2`orvtUyW*K4NRs zCwnWZO{nGfzc@@a8E{;}e{|wAaOM0oV_Lfr(uE&4W~=6UflIZy@}2L`h;#?4Bu-T* z47%46RWHF=X4`p_8RH@$MEIm7*_$A?saECEu5`8WokXFhydzVtPo0w$Ho4!3e6+*% z@y$znADjH=bpPwdXgAk+aKtpUF^kYqnG76gSD=WzaH$<@b>Em_S8p0>hBO~;v7lIR zhOR=FyAR)viS;;oP&VrjCd;bj6tC>K15zaO0GqYV({#zdRXU&xiW^-Rq?lc&$bs+B zf26))Qm}vT=uw=w)jU!i`be{+@;I^IAj3Ti1UBe5A4!fqKUbV()iEm;wm1pu8elpW z)Z-)C&{11@kpi>U840}}EZ%YxoX!WH&^_|v{_!D^$33S5+SQkQkj8w9Hg=1JV)?yE zbRCDQ0d>UnxuhvW$J;2m$4NdsmoU_p5W^0DJo-y9UG=W|L3ilN2IjyipCQRxU)4_u zqh9|Nwc<*_*&T8o5YQhUhxv>D!K{s|xZv;U#Oi)TrnYFnt zvpbnG#Xjkv_l}i^F6xK;v@L2s=%d~ML4Gul9$k(1P^}lIq=kL+<3(XHsh|C~5A7rv z&%PdFqzgDiWfMWVqM83FhFzv^IazhMsBxXuWo&pe-i1N|2cB5EA~<4Ltk7)77n`}- z%ny3`{N+oeRAQu9i>uMX3AcIqtj~uCN1AruFQmfwM6xRM?(mP|!k&l2l0kP}*L*?j z)%gC>-TyhOAvdBJ{MPABf#Jy%FarH6#rsS}KCqTlN2}`bxySS!e@MU9#%cwtobn$1 zbf55*f(8!QiS2DRtk{CAHwU^y;~aMm%86nQ*PnOP`~~Ki+}Jp=Rhl9&)E^*k5V15h zEQTjh&`z&`+JkHY7w&CgdY50rmpRV{Z7ICzv+r6&=?R+h0B256P6^?J?0S54;U@O# zSLT1~9dg!5mD|`d-nF0K-$^JPxf8=TB#tWx6UdI{iQCqM+wRx$$EMCjeS~8sqbM&K zDC2CPd;acokd>%z^xl?x%!q>(oJW!QZ^HTI-@8IN|$~z27E;)Na8d zpY|73xaxxn?ngpoh?Z(xhq-T852%*@|GqTjH0 z6w@<%lGj3Kcp=NTWO#7y2rJuLiX^Kr5Ugfpj%Ieo^N=V9mU9Tj@X?1{}!8`)=L z3~BaAvhQZ#n(msJ9T}~B-!MX$I%TFfkqLR>3S(cfTV|=cM_>x^)#xRmWnQ2kU;n?~ zK%@T21@TPCE)7B8sX@BWJ^_ipt&iF<)e;0typTE5VSuIkF z%zaKMDw!BpgEv_9Rw*gG?VIK-Yk8?@O(EaOpA<728(p2KklGi_nK`5lN5R3^qhO$W z4)grp#Gxyn!hfag&D&uaq7}?BJBa_o`^1vUK*~E3vP#?@RX@DZLU7<#s8XFLJ4k0$ zX<@nXPNLP@;Y*r=FG(Xx;omobT)l6hO6cjxNYyxzTU%v}LrD=F2pQ^i1Fqb>BMUJp zK!>Izs^IQ{Ffpx?!C~LN5w>!+eO}SZotMvd#>wY^qs_bPP1TD#csFgg=uh0@mA%!= zD{oTHp2mBzk5-|0j;=GEj)cjF9DPr~2b z@08c`!h|t`4w2|qQ8NWuqXE?7ClvZFuw;nxF$6x=>y7OX+Y4^x7F7T>C~)dMafO`> zMFjdPULXmw-FR0NWg+|BYDb1CW@Z!(ZQ6vkL->%>n|Uk~!0z=UI{h7cSET2&pJGcg zhL>_pefv6`6q}mfuA-LoXnv2VvNv>!HQnO8rLD8=ik;E!`f&5%z13L+=P+`tOXMY! z8IL-z0Q7+lO2C)R=hVKJ%r^2Rn|;@}1s#guGWpUrS!TlGI7XXy3IbK0CWv73z1~-) zBTe(XeO$Z;!g<_L8TX79tEeji#cY8I_8}1w%pdAqLi-4)nA&ho-eyo7ESH+YD&yQ5 zz(pz^Q^}DJU3S|95({TIUpbQ)kwWbWTM`noIHJ@pVyV-(H?v;;UTef_1Nj{;!cg2k zc0RsxElv7_4SX1kK7X-6WzXB|_X9(p0|%~08dJQ4Br7)1a0ZUf-9zTDgg7#ye-JbE zCr>q_t3Q|+qY>Vpv=EV8vL0`9lAM>nqcE2Js~?2U5Lq`(YEu0{ZS1NVOfhJvYFwKj zY7G1k*k&GS7}wy6G1Gx-&@IIRW43V79)}P=z9ZO4XnFbM^LzGee3obyDqC1%Iz8H= zMl!{SGgwhpc@3Mj8$IhUvm|Mt$Emx;hOtT55)7#DYjpjpmtiIe@YMqKI;ff>birqo zTkx{q3e#Nl+M?;*3)4+0!cDf5C3% zj1r!rlrDJT-ch;JAV^(Id62Y_Eqy^7ai`kO*IbO;&qx%3Z?2aC^A<3@9E7Q8)79XL z>Y-q$>gMJM-`Fir|K45IYgFsUi= zY=Jjbk3|~9``fPfiCE8^poa2SU|*e;31?&mJ?C?XD(lZ5mGjc$Zljxnun5q;Ruqeu z%>>|V24KR~u+`VN{4P6qVjEZqV-|-J`*BN9g3>>1m)&^)8bPUnj^CxY12ki>S|unN zw*t@SuO|azY}pRSMNlY8hU6w@#Pwk=uSH9(CcK@Zhi!fM+esZ*A6mTEByv$!EVAjW zu%YrezZ5wG_5awAXz4tfT%#{*rgIV52!mSjZ;98n2#c1r!GdJsT4u@HQY+w&l(Q5c zohDZJ3ML3|*3na^=8(`X=Ihy3jeav-Hu)f7%W??#AjpGFHM8X*-FHHs`ij56K{Zb8 zL0XdSFFk^4`sMg4+QGU&WAG$^=N-Im5p$OFZPehJyW12#gQ>yfb3*T**xvH4p$B9i zVgRK&k!{H>h#W9NHm0Pr#^sagzHTTF?P&cmI%PKYS3C*zt3L%c3pCMOr}DYaZ~taX z+tvV1S#y-*+EK_2CZ*#r`7V{%YQ|_?G3)EEkNz97?kkrl8;%zDef&wCV_+ZjH}^x; zStwwoZo%YOjcrk*=RxzVwcU+VmIjKvvd%BxgvzurItNCnWv82A(*VZ_8c%4cz^7+F zGBGX@mW|^!0 z0!nF;a^~|HeiIe*GKHW{Q|~M+9Ka&TC_XuQ)qg&=&Oa5&-pCiz{jR{GU`SHu^>kh+ z3J-yFvE1}>8EPCCF>Ho`kf%5?As&(~|M%<_GLZaHVakN7urc2;;01C`*G`wvpSWa0 zS1DZ4c;bIC0z2RQzpPmKI?WD%XOPx=~=WS)bwAOLq@vF#u{ zck`V|d&}y*tSuSlkxhol-r(WeVZoA2({8@Jj>IZxEWys(OP_e&-c|P&256sf>+aj- z;;*=$3`2+hEmZseE1H0ND@G%tRfDk%?}$*q7`cr;(sE&W8OECv0OS zav>bBq!@(F!k`8&cJlQBYFOeCV<`Hh~}Z@ice?%$NM ziwWZJCw32Rbw^Vq!N=IkB`u`Gi<|9FbP*c2I5!yJiG`JdDRLxN06CrmMgV zrV*Wpk+_V8S-#`HXV$29;=RVJnKFwP9-#gB*9VsR?)bcXc@a3Ucu!ytEj+=L zWWLIk3Z&6gZHys1q8v(L+)SPQ(K;qI>ZRBj_VNb@KOf!WwDy$ZcjdRe^}k$epYJx` zou5C<{WZJR>gtVM2h>=5q-9X&lba4u+yR9$bbh|Iu~Rt{HFo&l_aJ43LR>9MHvndC zYl{_98%TMLi?A#o15NeB=3UHgr-@$-`1&aNq9N@hDs>pcp3Li@c4T=oC-!&nR`^cs zc=Rh{V3={9DJ?YOZ^u#~#p87$L#A9mCef{Qtr!siIjl(`MJhc=y&B;|;0r6P^a=p~ZD$Y@Z(XX|rxxlaaFv_u# z^#udHs;@w=Q`-FKS08eN_{^iCb(e zRF9*Zokqry&Osghs>eF|KR?z}EsM)=g`LXIl-Z~8>UXs~8@oj0wil>L&gZj``T40@ zu;8h}4x#UC+r)S2plA*wm+-ZbosiZ}qm!hU#N7pvszk+RWGWr069kTZWFqdr>#2gSR$HUw!PYjirb*4RDD94{Y?x$P@Ic-zP_BF5xv&4WiwDwrk z<}ZI(1#H`>AM1(*kSCjf0K`LhlTX2;)x<-C+Zx{YB*(ugE|7^nuHJ&k!RAPVJa#Y+ zcU3L5gO^aq-!ii=-vUpTJ$SpR=Qw@L$tb*;Tn!{}9WD4~N5_je;E=v2( z1&I!(1ZjZMbM0{7uu;XyCIf3xQ=~bs$JG-XN7l^iY&0Wdpe!@uMimve{!wJ1B@Q5IDlYZ*` z`nCe9<-Cx3}UYc3=wBFjX^lmwK%9htj|C*;h{hRp4=?i0h$y zd|S0B))RH@3AFDN&b0cpl@y>D1a1>nC0Z(POEUtqgafur$inM5bklzc8fMkfcH4jA zX4kc18_K?>8d$B=X39Sr2s{&+i2(7_CrjVQ>p_Ga+7Sf~e84;%qin|2;(&qEWa`j3pqOGxP%6}NIt}=l4X(gEcJ6;B_=c^h1XmazGr=JGw z!Kvkw+uXJ-ch`LL=X_97{s;`aFV#jR*#JyC=vn^vthQqjDDw3(#wyn6lc;g=u};BC z>9e0^?wWPi3(NdHVw0oDm2r!1;IpD3kX(l+ zuj=aGX%(&gAqWZ{BC|06{TUeN{EP}L<*kQt{_L(h8sQK!wG>Xse{L`8S64HG(pHdm zXmPU8*#S{u0o^sz`SB)96Roh6ed*-q$M*qD=GR*?ZhjEk`-LfR4Yz!QHeTS@`sIYH zD8T+ZB%{IJ^GuR|@yu>iP8*hOO}wEYRT{`vF_fOnYv6JG6xUHXQoplcAUjhH{7Iqz zfRk4K6!$3T4}Z;e2W09AJzdZ7WYnl(D}%pSenI1C9kU zV#m+#-HkF&Q9owThO8>s)_rm5t(cC9?il{&#QbPkgES$%%b&(nq%cw2;ID0_6t@!s zxare|`^2AeV_~3X&;wr}XH~c=7o-*iaqnA>oCGpxgR48rgMjVD98>VPU4O`La71r} z&P@99;}AM9$FqI3H<6?T5C$l0r)rwK7 zupO;`D__DC7a}!rC*T&}p0Ez`<(rF^<5VFA37N<*uRnUAzWkw4_o@Ax)zxmkCMo2; z-CuVw(-l!cT|V8iSi_DUd(Z+PjVYDevSwiYBnE~OV*P_>dek6)i2`@Q6L&=IxV}_U zW-~0Rt6n&6viA}su>}vD1>3^nuu9{f=_$%OR<>{^p%wTE8% zt}@7Csf%n$Kc7D`#*9*v$uuH9kmTR9Mi;WV@#IhEeW1I$&YN5d+kIdV_skm1MM#0w zt-xqnP;N;Y$cvK3!Dv?`W3btWTcDR)b}>i%&YRv- zfHXwzd3+Nrr>4Heu|CE(yU;v>jjH5t4~IzNnT1remdJYmOpcL34T&HCb+CSCOBf%2 zGScs?4WjZKAu3qp0&4gcJS|myptYVSAHkz-@z-ljLH(z3G;g!C!MB6m>y+w|lh=w_ z5aiBjMhM^Rb^)o)|Nev!Dc|xeb{RpV9Iq(jp!PPgc4P~{K8H|04~xXe;~(n`Q&F?& z@ebvX5t9~&sB}`J>f-`IfVUZ=5!yAYWGhaww|+q9X+px(g+%F0T6=p@&$@}#nX12w zHilaE;6Sxyo)PAg7%`l93IW@pvKOH+OpMtji&zu$d?n#>DJeql!AzES8pxCWV}OtQ zXMzbN53XN=s@PQc1!$!&NnVd_%n9rL9RsN1*DD#hM7I47nas-z=#0m223e!e@X8cy z7cxyu;6?ZK3ZKDmHbninZd{~?y^)~yAB z9LJz1{ZCQ&{6UaJ&c25pyLa`dl6)ZBHW<75UO;~6tt31_Z6jc^9;l1R=UO6Va%4!K zHu>%g$uM1Js_dy|UO_`cb25_7BsYY^`)P+Rz_wOoS2Il09S7421i9(ib}T>X_v~n@ ztOaMdw_0LEpcN_y=0GM%_1o6KU881*@~%eUP@B$-sppxh8MN^}`>?y(l*yfv7HpU{ z&ES_C4V)`6g#;pX1BeZplQ2^QIMqq3{`>5?UbH0hC6!MidJNwAC=P=F;LExHeE4nV zPv6sq8Q2YHD^%LYiD;v-;$Fa*$SA9=aQiOI2{5OI4Do*orRtV2y!-4=Kdi*O<_|Ba zp7G5FD+#H<>HS{QoL9WHs7N0~Knd1G{p~>OxqO~7*5kqC+ux`(HFA_1=a0WnSM!tq ziS6MN`IQ~Kxr`w&$f1ii@fyziyh`Ajf-yQ~1dOfCMb~}n<19K zHqL-+Z!kCxP*ujrwxB-h@Ufp5)N$}u&@4457PAcvNqh$xpV2aCD zXn2BCph;sDlr6ADPONP7*pB)45?{gI{M4Fe&lY2X1bQW?X8}3|${%zgH&6`QWr=x2 zl~cgfB#{fI46otr?KfAxi80X~5%f7pR?VYH(}Rv%<5T=anW`mcro#gu`j^~dAF&ck z$-#285gtz>VFNx~`EXZ{HB%rc5Y_(z>87H=$v7=j=}+@)_S~+zHZ~R9H?Y=Gu*$br zo55)clV~HzH-Ne!`9cCurcP3WjBH#ZFiPAv>%?UweYtBtcawvk`P_ytO)&?CulSqw zs{VdP50u|YxmJM!t6AH29fW{|%U<9Z4&GE>zA>fmu|##&=%fvEL7{eIPO!o-t3d)u z;vmn1+Zb7eQcb!~!qNnTr92V$4K!2MVSHq1?CK|A5=`o)QD_y4gs!pUt!9DICOH0c z-Oga#ll$rk%=al1TZyWbUGkTXYBQH(^o3b5;}t~Z!D#>9Mi>(s3t0i&PWJ-Am@Ho~ z7TA&OIR-g+M>{9)RDfHYuk71q!7-kYdL=?L0+n2J66SoH!0b!$6Vx#X&CIrS>f2e%tO~;u66M}q{O^;#LrucE`%C*6a<$r(5P2}$+EeoSsi_R33VZh2^ zMP5hAPM?Ca+I!E0F_iL51C~Zqlhbo%s#%mD))WHBc7TO#GJEnw>W?+RVbu|axK0xhTF%-@lgv)p5h!KY)rWjc6fbxR8)XdFPN`WG0S3#%qL#tfTrWJ$fUA4D4r9J6AuPSjg7MKX?5>NK{KG8_DQH-?HkQ&ysI#>UqL5 zt12b@@nmbLQi<~iC$xE7rHGWzery+pKh55xWvx5vgg65*r#sSB2il?k-Ta>`VEN|= zHS51S&j|zQs^M2rK*O22d@o9rh0E86QTh*aZ2Iz5?MDSe1>Sn?-4eJVV}Rv)L~NDJ zmZiEwRvdE%n);CKFI|L*S8k8sl=1`U472v@gD0a@u0lkfbjjyW?Dy|G)bpuq`Yz5gU5f3 zIkTORa(CVM1|yb$W+48)L#)k&P07gnUpAx>N%f}YML{%_G!9crCxzAszxn?*$ndX1 zgM=?MWaP%9o4_f2zU@^-zT*UPb3UY80GS+=U;0LnlXjd~ZTrx_vQ$#|uR;-CU+yzt z+|~Dze3wmbTY-q}nX?a;3>1VpY)WOO6{iyu0!ic_E3PyQKRYErW6Za*Z!{poKD}@i z-0u)@nH4}40;+-MJYI2HOwrr+H@-gL;j5btE-Vg$7P}sKlJZK7W#7+TeL}~Z+}wgs zl0i1Id5^BKkq_l{5}9jPoS===?+kPC+4fQJDTbIzhvj*tcVEG1r;-fX_sbdfj;ZJE zRp0IgL**?`&c~h9Dfpt7`~1*%9+XO|+^0F7iC_r7T3Wf7bGkO)3aX_~F37CUf#?J{ z>A=o7{EBvH8xWXbt@@?Xs_>T5gIG=md-5gZ2NHEUf`cwA+pM4G_iRG@FQ5QI7A4a( zU0?C{h*dueQzKk~fCiy8vW=5JbtPvPI|roVUhD}!{mE!wQW_3Y$%$8+z}Bt5;=bec zcu(KH?lOJtv7)&9{g*jdB+PN>fzzF4N^;6}LX7>h)`4c_(7Z;TzDxH9r@>WI^T!$S$Eh&W+aGYHv*e*&8 zs$ORDX@m)5t8+4Mps}nkak$lL)@A{$P>{$h{!ckua}I6I5G2(l@|!b1OfkKF$IZAM zJ>$%~2EWx9NOAdJ8?In~SVPD=ru9&OTGz{`-%9Y0u6`U-v_vfL!afHCcCy;1yZ%YB z3HDd%eoUjX(?EcCQnktlwWAhk>#OX7>@5fbmR8rBr>^s?S6~{8Cf5ZvJSkfEfPE5a zDNeHB6<{!zxS}JO(gPSr<@;6Bcl_B#eN%mjBfGo~hJJoC$$}*icdO}VwDL3bO_4F>`H59yXOi%!E`_`udBFPgHdVY-jyb`O)OJ%yWzd;(tNO!{4%`&H|<(t*qkn9gX2vN zXzXNRsDITj#-xV7H%uPYb@y-3a_kGIhenm#o$!2nmYj>x#1{6Lp_J&byY`6{)e#ch z#(9PqlBAgKg2$7K?Z{@!Y3(*d(TepS#S`n8^5My9-|Yvpy4&paYllWmS~{#M9|VSO zhL6+CLX+mG*K+1AL|`+2C}lRp&$U-)o9gv!1{=6Ii)w5W2T#&(m z0OQ}neA4nq+Ne>-Y>A@E0?;R>=I7TDVLW{<_|CURwA1mLjcRNy=JPCO)||J1eQOnKC-mABn(Hc%?|SBLo!1Kq15vP%mf zwi7(RF5m5@(l?GzFvW|yL|gSs1%gtZBKL|bCoXC{5gN>1XIE-SjOGZUUZ}lcJee)j zD0*L>+2hh|pso&k@(PV2_4_?KQK1Ga8Z9NhpG-skXu_1dRqlo={Ty^|{}7G;H>v>T zxbv&V%KCSM(YFnY-$zveD+46b=$}EYEP%}1jXVLB1Q?K`bfDDucvr;zQrp43r$7`f z9sll2HBngtdgIkK@!U&~SV}1dmDgR@?`<2pr+SVJfP-C}EICo`kYG8r;(g`04;OO?3hk&%Ohq(n zAlFH;;Gn)>uWzj$DT1J_6k#tZ!{Ot46ul1)bQ~!)3Nz)Oix4E8i#wGB|QZ@U$%sYZ_O3jJY>{wSimXQ~b&y1OmLF7|~ zQxgV~Kr3aMBTzvN*`iaoANJ`)iF}=hHBS{+QwuI@3K257f(|m$ut95f+4;=Q-dSNc zwF}Sohxg)q4fu`0sS$4s*OV_n%aM72P(tXxUc_fo{C%`pRAh3zP!nCCaocu` zqb@kl9reeVl>^yHd+xy%bMM!CzmCjYp1$?#mu-BV+=2)#B6>86w&BE=JZEeN3yMvw zp32NDIMs3cPu(%CrD{nAkq2K2u%OJD!0#&(eEe>|4O`cGR_!fWU3Z7nW;gF zw)$OBAO@Ul;yu5dOqIQrT)>E?OFQ!V`t|*dStl^+SCg+8OSEIT^Dm{#EAnz)`5EAl zzjf^%g#gvmpwyyxpuFHG0Swl> zv=lnKD;*TkT@4GJW6dBAuODAq6nNe}7J&wjy ziNpg^ZPf~ZQ09X;&cdTNE=C=3T#nX)-Xw*WO!%c^)q( zy#Q4`5OqN^IRfg;bD)E!U~^XK%MQ@!^r=oG0gW!t=%DyF zhzyl>A_`1{nifWW8o>L@+D(hnZ6r9hpRb zLooPNNI7`F(I-Wg!U|*PGiTo6jh`}*FDnw&S?X?FPK&9ycrap+T~^ud>n`RYacLWlo1o6a2wA@=771;vV(t*mJCYFwua!wlkg(?q9K0=c4w* z;fg%P;I7c1d9KK`TYVV3uDxDB<^FPfr#~jL_4Ak*p4(c?TY9DZdK#oFptj4ShnLj& zcb7%7mZx7+t$o-7uY2sfYu@*Uxi37HpJx4Q{!)fgB%!KBtBt3GtSI9Ofmp~yu*6As zjgIj)-`WWV(taTH3jfm?Q&YI%qb1pTX1V2Qh&TpQB=lq~VLkR|=8-wg-O<&I%^F>I zebRQlbPx25D^!he!4hZ*9HmXVdVal=TRTM7#PaH>PlbYynIgYu^I6me68o*cya&sv zWTp1yrug*r9;UtKyIN?5vbC^?S#xo(ryqMELN5g;>>3ZP5_Tk;s|LoDjCok3P|Tq$ z#tzISJ^gfUo#us-fn%C`_Th^(g=0*&V#4y%*b?yGL5IZuy(FJcmEGq`NJpve5a+(& zTWNzHAduF~mYzu=mg_9mPY~fIh^6_ztAY5gDGQ`z$j1HZV9D}trXYK?YpzZ7eZgc7 zjT^$=8!_BOQT|C;lUF3&8TO}FuD)mp4G zMFTZSgK@+ff|+lDt}w79g`s>hQOgrZhWl zhbeiEoz>kJr&=#qnBH0-Sv?jZ_Z4)ENt)QbG@``jkp-0qg&CUnIgz zwQye(X8WKU=6%$T*n~?z>1cd8g{sa*Wk2prw?C;kN#peek}Aerm1URD^HnC~z~JFP zeto#5kTY){i*_n(%(3lsZO$IpfYIE^K$HfFi zRzqI=?fzoY?Cw9Y2;(epfJnWqGcf~U5u+r{LtmTr?0e-i?~E2_%K@4G%`at3%JwcI z<1xI+-OTb(L_R$6F0ts=gY#n5{htOaBdMp7G!G$pNWX@kyk)iB63uVE`G19-3pkYN z|NkF{NJOPlISgr2iJ>-!45O`9txAX8)RGaE`j%6Q8HahumP+WbHOZ;6QQJzAh79AB zLk?3ZXT}&z!VCs8%<2Cb`~7Z*{ax3;y{;}-d(|_~^W1aapZjya->-L6eQsi!FvUbk z@sc_!BQ^;-Uqyqda#K&A1FM!a27YiYQf^4!csgeDKxmuI$hp6;hq^nN<5{+X3c}VN zTX(wU>9KoSY_WD~MbplNBU8VHU0OBN8D#h6a&q}=T+1!S_d++{u))Kkx*>lg?)Cq&>R%ErSa9#P*cWmoCm8E4$O|`2dE+< z0Nerqa4a=jwykS8?iY_`8iwZU8R@OzB?75^wx9-I50V?>%ER?zlV~>>K}$|Ku*-ZI z-8J-)+AA?J$>9~=tNBY2`8|)^Cyqa+Db>3lhO&PDSasGB&bZ}keN=%m`berh$;nzff7l8=?=uc4z$;!#>)M%rCSRs4`s) z%t$>9i6Z;0Q7+pKq1B?gwC^#Jx4hce9(M63pMo8@+LI}XHj{S;3)Y?k?&22}4h%`% zzlUT4w}{bQ>?Vunhv2S3vkM@9wr9)B0Ss&0>JVrKB+j3Pg^c%=3acUh4%xE?8i8Ld z?nl*2kQTLzHxh`-+mQLcBNj_8*PYsbY2FLVvWu*bvneMj8citkk&{F6s`GsXLCKO_ zJcI?}7FlfjJ~mdzTv1W+n&;CBQ$Kw}eRdRiioxriGwO^^YB-=9p4sy9ne8;G^GH&r zhr!Rn_Y`~&X|@G}wVpoSPAYp$p8X*D0d?g2DApa<2q*4zoIjhKEgr)*=+e2S{Kftg z$8bY0eV@z|pr+pmAEb#qx>)J-OMp>5?o`;Df9@1*s4!v;wKG)rsxF^y9~yrv$aabnoI(!uw>QW z@YlV}btkbbT1k#BW>ZQ%5lwF<9qsyf*5i^jxyY3TdUC#=BA`B@ z$OpveY;Z%BNjiSy(j>dKc_*a_B_{kRf9V3;8yGvnYm_kke!ed8HI}^o|9ljGav<1s z>HZi!?&-w)@32;_8_8y!;WxZ}{JetPb35pe#EB|e$^PTJ%rc|FDnPDA{;X|eG8Q0V zweusL04TFuZC;p2d*WJleGz>kD>*nFH?%>)(J(M^!PSDPIxB2gBfu;2&IX>qZj8BOuak$WG%*eLL3HVQ945xSEPU_87ch-#6qCbwH95ab|L=&M`+q%{hM z;XBxu(M9Fpm^2K2QjnM~;eRn6vi2((&clt>Gq1?gK7H*+{zwzen&cI0Yc$g)RRabR zQ22hWJz`bjd3dRsBaIb13}n_%6d4i;h`7o@C0J;Mg@q>2JZm_$m@CN078)Ax-x#OX zM-uPiUj}ufc08MS#rQsjgr)wR2MOgTC1aG4?BxeE<33o7E>0WY2YEyMDLrnOo)3=} z2SC!>5Z7v$QO;k>BQ0R^JAFu5+0!cwK5k%qPab6@^mdtuHNSia^0}l97u+^;OA_7j zSYO!m)p~JOr~o}H z(6b#cM>t62peBu9JM_ZcaJ_uLS}OKDRyYVRw9@XM;9!!FWU=EIoN z#HWAcRhHh6MNSm>jF{mkBWR7a9*m%Qo~hYYyjdudepeNjK#9m~W3k}dj?oev_EWRpyu;PYyf zBHj?z3Wr^U2$%GU@HPWTGygpfo+)veSN=Ip_!Et`nhPhY7J9>|CDW$CYj6pGKAL{? zSlGJqKPfpa8!O@16$ZyYe_z%KhhR5JtykgBwof|l^_FA4yeX5P2#*i2RTSenvQ8v#0Jn_bNDRj|8Gj1|~N z1HPdHaNp65HYSdSuzP}jlzaZ*>d#*jQRkE{^R;s^Q)INLK~m2=&0Eh^cAPV$tTbON z5kzeKT{FQH!av2vj*%8ff0R^siZz~Yuzh%ADmrOOuY6!4nGZ_-gOmFM!U}Hr=*`k< z+vZ`DbErPJZ|m3}?F@brO46lISci;Op;GEvUO29td+2@r4LO`&F1Rog*bv7s&mc+x zsVz)%gxKYNYuqgeGM*#%D~~;?l6SX6dF6+=!Z=X~CKr|_hIs1Au}MzcrWSd;ewbleCh zu4n#kB3r&=P(khWUz`Mnj=}S5hK3{ujV1H2b>KnQhV?@jMrLu)#47vI0R)8dE5JHb z4|>!jz4(h{B^TouhR_*Ea#0*B_xxy9nLc0_euIhVsOVaDLw*RzdNEZXj}?e&2@(gX z(6}oSbuC_*&)gZ?J3*LYg)(oOW5*xKbE#;a>Ab*Un|Dkj& zfYSGXuTdLurL@wj?8wT`bBBH*3>=`2-yFC=*~Oo83=%|d;HUWX z(q;S5-r9kew${b{k8^|gJ8_?~z@97a!R9znYVK+bsUHe6(+VqZhEq8Swd9`0Yz2j0 zYR!F(ZUqGi_0K;vf)!P}T=`F_L@kO*rp73WS)?|vE&-F--lB3hOSOq&oGD^^4SLD-b`{zjKfD2gNci(qk7j(D~_f~rU>=>c*PYzF9t+}KJ}XF zuLN9G3SAWYeByc|cJ%V8&Jj;}-kewP*uoImD-1Bnb~nf-vfu3&iDLrSaXYIT5_PLz z-mRkoQD-(PPf3x$8f4%)#0*hQAl6X&EVVYil30n#_VHRdfv7<1%AoN-6YI=ZPG{*q zzL_uYyENECnzd=s3a+Gz7@Q};3bZsbXx z@O^^xIBFocqCTGKcNmB2+GZ&n$WkSL(m`V$ZSdg#;&FjfwSp(QMsT#Fm4LH3H=@@0 zT^f0I5_rYp?Z=PW5-XVjbZCj|3&aEd5YR%@b$rOn2kzEIKh82JXEM8c@vn`ULz`R4 zLeo8>_`^G1d&QJz%4~tOXeQu4KgJ8#-<`q~e<0mWIr)U`5|Yg+MumsIy;*r_|G7$m z3Fcgiwom?KhTBJx!D`|dR^uhUAKgfPoW|0{ESJce>8Lw{salg`p0FEZO(t_(-qHy(h9BiEAF>IsDqCJ4>RJqG@ghh|p| z>;HO88*+BIOc%GCD2_aJMcvw(@?ht;3wa$L6UbY;PggF|3(Y(7@{BcAS|;Exz>Ggi zCe?Z83$kUmz+A%iTpX_Z ztd}IJ2GMuq>H#Z|z6?`pv0?hxe5M_(%UcL~W{?yIQuLZM440WKy9FiKoz`d%wanH+ z3FwoY>2>8@<8cbq$5|TTYUp6cZUw&MoPRdpGf%I`@$Z@G^V5~8?<~IqfSk9f48lcTyF|{$!#z z8Vs<}7f)UjS$po!ML{>y>H6I3VQ!HuSz0gmT zmv)5Aj?Kw=DF2K~(kzIst1Uky_w~Q^hY0LzAnTWN%?(exujh8v0BIJCx7vk)J&U^H zon6p%vSqNLfPl3N6Gdh&$AX8AFU}735T8AeOuN(;pD%oG62LxLtODuRmHt9Tt zxWv;D+875Z=2ZB|mOku{Wu4H|wX}i|+_$9)A>t-2`4u8l+ziS$GpChWX#+n7`@OvM zG4xbRMCE!Lkw8{r+%>dw=_JQ9JDkKT-AKvx>UNVb{u@-KNBW>Wsj?t~MCxXH>dG|=nCt4|%ia%2Uq_&2JHLN3MprAD67dQBzm zdGmYUngbw;Y9BX97~N11HbnH(3aj3UlWuomQ|8e%4ACS#hhgmPVZF>$@)9|W?XKab z)UY*eg)!_Sc*0Fy<iN?f4#g8KBA6x!CdN$#thd zK<**i>OFhOsR0`Cx-~bY!Z^7L8&tJgq{fgfi!X9UF8NRGb{M#UPm7gLmqoly7e!1Emy?G=Xzwfw6v}KiFKTZLqa5#)V&OcYrE(1 z&0hGEZIH*E@Tu(MlbcR4>aO81xc^FrryZA(< zQYUeEI~=KeErUx{{nv-|l!V`Sadz%Zln#tvZA27;i<6fi04mveDmS<2mC`T|Gs?Ty z_V#Drw0m}eN(^7VQMO}e!G-8dvD)y*1+wdusfXC^jONfFBy}v7UQtkm*W^@V#qaz_ zIy|^U@4PV0$kt1Zw4thSOY5QtjejN!Lf=>p1?0(i4Bi?dcfzh*;-+H`fZ zp{sNJE$4<)Jfyp&|Gg<)eM@A0(|vZ-t-4ltWZil-C(3*)8Y6?wS)H4+M*maT{kn^- zE04a|KCW94u6@%Wa_=00L@EfRPn}ITLhdxS0aD<>G*kt<0jUPO-~}Ko%a-`=2G#guO?Uf(O4>}(&w3b3*)CIS0ckBS0Ntd z7#Zy}9{9;3?U$#BQvcmwjy2ma*veX`)HCaP1u-D%g~O2+d~C8DiV_>|1CE*6{>>h# ztg>Oiw77ZPCUb1CB1mYvDrp+#0@KSxSh!Q!q^f|X2T0Gc&+r-QdiwBpXE5AXE&I9_!Y0zvc%it{>?vto+YHI6M%b(xYA#7_EkFT(eo zv>APfQhGmj*$Hd2g+EuGCjtNPnk` zoBaE_=n}i+nj+`X4nK#YuSfa>ZpMH2#U53vx^%y2_EqIMh`D6S<&D|m@g&`r{_bKv zZaR;N#yinFMWZo<(HG!%*?+xp**wE_x)3?F=KKlX+8Edo#89QPoT$=8A>vV&^x5%6 zErSYo-&qz;vi{d`UipUxiZl8bgw6-vvAx3Vk zHrK?(sHXjMG&HlCgnG;f6uzjAWt)GGW1sOc*wr=uY~-%1WB;>NH(ML5kaDj1%2v{N z?HKR~UArXyyGA-XmhBB74iW+eFys_xROF08e0TG_n{hxjHX(4cY^8hsD=$$Zoa2jx zx>pl_->Y!@kSO2}&$GOJNDJ{z?HdeTKDnwjmfCt;)lLZC?!nqud;uhmJw4jeTX;!X zf|q+dY^Hxtx06P>W1c#oc0HF_$J3m{V#KfgC6-#_>k`NoY=Ue)LZwxneAA3>Zu?I{ zbRoAQu_Mf*!5oI>O_fge!mEU#;W zKIJVPigmbr7^Of?h@zI$UtFObl2H^^!v&@413*$^Ab&Rof&F;0aMU8fm^=afzYe!@@?1EnNUc;g2)z&<% z%=Ur{sHujdYn@ln4L+b_5XmFARk6PDellGM2z_B7eJw9C0D)19V8npplG0#ag+ zjwOjdG>30QfNpdj0B$iZvh_o~WOmmX}xplQqzNJogCrZ{sW5`zDi^PhBEkC!-#U`mBw1M4C_@c7HT2`%B5osRk@(R zjokEf;-c#;4M)4Sqh}`vG&p|W#e4@zbp3~c55(pkl2?2yVvIG@CQWFD&>cw{yN(eL zARVK2cm^|~R_8Q*YA3egOh6P!*!!b0qWK`*x)?gN3>waG@HdOh(UM(m4*ft;K`JhJ zqi*4!!iuekia+M}{rKcK4o{b86H0TEJieLk56-wUzSy2hv>;mGw!glQs5j2tCd4EN zxm1)XS=~(Q(ao_5RD_qZQY;JI5tfvidbn)1FciQV77Qkf-E(=hYYNo z&4={7d200XpjsGoexhHwf&~c?vkghZ4GZ-Hjx|@P-1WHhGd` zL*qFF8}K(qDm;V}P2VLS6Ao#Wuz(fO3OSQHTi>Tng#NPtAknl-~Xz%0l84F^ui_COZIM7yuG;3_NBz1w5RS#gtFnMhNT&j zT-fZmknRFV@|1ng2htnZb~YE))gmMLJn2uVqmS_oN-!?uds)D~R6yy=Ir$=zg+z81 z8Sg4CYrj!GpUMm=sc$}|sMxI*!X5oZh8n(jQlx^^&}t^MvqO&cv{qP4>Ie`j^)=l* z^#7QB3W&}76y!o%bjp~}05*a>DS;rhV7H`uEJ~`4kJnVFmhjKmnIOjpqkP1ltOKWS zGG`^}w8DDMI)T)|v~yAQ4tj`h0VDe-obW6m=A_5aD@54tFAe94I|;o7zV_pb0%Jp1_D!7wC`i-QU1X&m67#K6Gi+W50>x~UwKTb|2 z?&?=X*_U>}68e=`Csgyx1#<>IAXCzZI3>6Pg^}m|@(7qG1kuW18LN3Dv__KA8u&8K z7pd&Kp5jEeN{mxS1%n;_!PBMijcC+be#{LJ*!YQr6OWa+)O=Qs9Hvba>J}+9(^_H` zMqCN}6CG29^lHh^8^OJBosAXUdOr*?OMcfNjB2|Zs&C;zyU}+jf!)0tt=MNE)55*k zP-We6`anv*-uj~DMAj}YRnDnm_v^4({$rJTX!Yjm zzpGgqcU#c7ljA@8*zK7>qtgz&!K~7ouY1wkwVn+V3UurwNiqU+N&exMWwKzKs zIu!b&AUq+htg?Uc)o*r8QC}7?sJt{om--l+cd6__()TJH;OQfV3ch!DY74^NJT5Y0 z3_k$H2)y{Vw+6Kk8Y=R&7vF3RZE@y5q=q`j<9m%X!YINOhT}rbFb-z)mZlf zmqP(w&oQX6^2bByue+GCH98nU;ckw+xsPVK`O}j)!JXktUTv_p|1Bgum%@5kK)Q>^ zYvEF;rha`PB~Gj_8IZhkO_U`4K^5yN`6 z>J)x#Lksy$I!TyBQ%h=2K1`Z4koG6hFXq(Sj?Uplu7GJ&{sle=#FmCi$pt>uQf_ul#<%;Rzmy3FVP`U;gVxau<=b-rj3>gJ1JID}t!D>)1tT2>;f)(;!DhJW zBdn-Y;yiH!!ComcQ~J(2h_bs*V>x=m8!Nf6L(o|jOXq~0Q;z((HGHI2aitqTFizT5 z#jcF+toGt8NeIuip5-ay7DE44yslQAv$Vk>4x)nDr^L?SrK;?y)bS>FrZ0g2;@D3; z5A4l7+Bg+CzHor8$}BS`d{Sl%H_zbf4+AFR>ge%*G@6v)VvQvz_C{;|naQ;(sUvLs zoXEMCf5IneKkzgu^x^eoSPKn37XDE8A_S>!5+?ai0XS!X@l+t7zlOW-K1F*7VE$_m zYX;GCYyx{Jm07Q3FV46@n=TI~-Ys5YREV;hp`rF` z+zmBt#}q7bAQjjocuC;tUzK(*Zch$xr7H55FRePlQ*-Gq#)~#I$UgY@oD#cbL6(%R zb@7teLCh-vBYONy?FzQWN_rA{$=1oOJq41nlo1MZa@c(jh1LM;cWq0?$PFepl}y2nYZpnm{e#&+=G))=&7=8wPC5%Kt9_^jkpsi)5BhFXBdn)ZtaQE zG!ZsnWZoGQ$ZbA&b-wwo{=0K^9s(nYIXme&YOu;C_@FqOs`4(e@!?+s2i_Sy&UNwG zXj6s8^mV#;$sK})+&x!B93cgqki1g+VH<26!+!K?p_B`9pl>_VUrx*NX0=THW|p_- zZ46UkGdiTQt7yzFCyb?6NYUK3;3pP+=dQIp^7331icVlX62{&*71>akRE9d=3_os% zEJ5V{ug@n2MUMae=XdVg`%_h7mBV6XH|wt7W}8bovrkJwngHk|pWv}$2GR^n8A_S3 zcTr3N!E+B`tk*beTRNzt8YG z^EYrsN%WPu*9O090X1iKqW`I9)R1bFq6gM-(sHP=eypPs4^OvVDQv(T*0o>^tCIQK z4VC8YA%0>+*#7HEU)N9aOTqYceAK}YqnBsXnYsZ?0<@lNvkI7{t?dU>!&GD`APogc z2PnAD7jP2LbB{KxQmjyt47_?hyjkmFeTw%Mwa=#m@}G2O9Ay7%UCzwc_k#0dS8$7n zCH?5!g!}CqRS+wIY3@#-{-K!=L;?}LaLT&yZ9Qqz{Ul2~S>rnfyP0QJmoSWYojp@B z#gut`dx^hKjX%FJ*$-@KM6#qD)BE0;t~N2P)WOyWq$GjJ=BKA z$j2C*9|9OloPlY0?s0k`-Xy5rnZBTz)>|7OHquhTL=ml$=!HMu*08NU~Ylb?3EWJYM62qqjMi4D| zrSCjHTu8WRJzZ=!m57mT?ivj-3)KjsU!CnBa5pNMxPU=SAP9&(E0y*I9>_lqy(omhJgd4^jB~9$!@o@ z?9WK_B2#qGh}V^YQWJ>Z*zz9R0!b04n+rCB6$agO6;ho}f4Y`X;tPk;Qr+I~qh_wV zpJF~gPtn3I_{wlq@%%3y27Hm|7r(3&W(Hm^D>8*`Vk>aMSHLk%&_Hqf0d5KhtO$1p zL+>-Yg|#$WSX8un=<-zX{@A&2`VcsDE9ub+zAgLhd>;?-D+OYkzj$$s4?_|8;as6~ zh*7{4S8Q*P)@TJ%wmsD_+>MO5~6>;Z)GelzUW5J~z&$zol*Oz?qM zB#J{03M0QO9aY6IiS)Bng?kaLZ@`aUx@dak+F=q46pYvXyh{^x!Zau)A7Gl^-VeSD z%@FkFt48US52GhrK!^xNuETn+Af+1+huBoxdUXqb6nq}j!`G{^-zmu5*#Wu zoo&t`4dDmQlk|eQFKwr=(?AX*{o*a-c~=U)fSccQqv-OT)+< zbD~k%am&?KVfN{Nx^N-YIqFf#%-<%;SnsI1t%SZ^-!$kSw-XV5@48vphBx%G!{XHQx6 z*gM;RbxemE^ud#bNIbarp$$N)-|(B_yq-BuO2}`W`E@FvSCbhxbkWCuHU2P5n_?iE z^acci0?SzXxPvcZR!!lhn-;e#EMT9$wBp5+!o@d`ZSE@-tPIU_VP^jN$re@r>z_<( zg(xQwK>-12gs^?SyJ$|fT8TExUZD7t)R{M1!83Z6m0{}6`x_~le`*++Y7M6q-Ds+A%)+fVWr=}=y_IN$Wt2Kq3Ahy=j0wU&(zflQHCoF1nUXqlxg`TTY#W` z^&EKSr0M!-jWuhX!jgHa)Xq(A*$T#it^E_t3Q_ZyI7a4_SW&bxU|?5)&SFRT<-2j~UqGkDis?gu-Rg1p&EHQLcQJZaU&kioph;j1NGFDFbSe*{h-8Lbr z9>b?8qpcdoXdzMp!PT-=mhH;OZ*yDXp*{{kXTh|JlR?`(T-{>YF<+pAnRQIsd&nEmK6~_-y>SS%!|ceXb+k zb>}lXUxQJo9kSHE`S6Fo{qRpv$ZR;_{+3~FOOkB<$Vn21xeL=V${I`R7VUDp7-^*t zxrOVYIFncQd4G(bfh&EUUf?HpQMZoMoS{Dkb&~TZH_5HX?*w8ueN(6RYgVLgk96!` z3sQxr*;#?rOkRSh%2ebsuG_MRv&3C&|bvIYQxy2?-U6lDgn(3Kgg nD~E%ehlYt%+YSx)g$q+kWj>cw>unJDwSDWZEl+;%Joo#pFyehvXD>7v|6!CWj8r>aP2)oX28eM9Vo{b0&vGIW&jv3U{9( z5|NzebQ`OP2{~n}Flj{+{om{Ue7@h$=kxvjACH=CX1jJ>@9X(`zMjwL>*`}&NBsHW zIwA`rU^G*0aHR+SO5AzqvG5)UWI3n7GFw0Zg>lydtDC48dG$Pwqb810+pnDj+v>|!bW$VH{vMPwVTCy#C$JR;R5h{E{gh~NAJd0m~pT_pYF1tU$J z{_QiKnox-OGCYoiX4a%;3h0KZ!r63+OiCY;3Ryq`76MX`3{{E>B_PqQ5R1K)H`)GO zsX+clq#8poA662iHZZ>1Gk6$gG6JJVMKDB(5KW$ePm7cjBorq{YVj=C1>|CLNq|2C)+@t!k}Tva|}w%uNevKx_>fe025S_*770xfkS_|se4y38FUA~D|i zz@3X*q?kH`4@T@IBv)|oin4Wb@;GHBC_npk_~(iA(Mv}cqv+d56~iZEglk6Q*1X#; z9V{QtTo>=S`6a3|3U0VPA6-|MsXy0KaV*9_Ix!;+Khx2;zDoAAgF<1dW(;H zY>%^qph4k5inw*3Mp(FBES)DKASJ0<9B+}r*%eej9pzMVuyjbI`#vFxf*#d&*zS3RuFzV1n$O&i5=3o zZZl&LE7rpjXtj{v!RvR#hKJ!UFQ8prUGdM`HV>{x=YgZ*1H(J1O==KpH2>sQWo6xh z?XokGXt?SKAuIo-n1Wk)hr%}#BKl~iu6t{sdf?N_JM(+p58MgcQ9JPNTV~}vxM?nV z5x(Px0fHUt5+F9)TIErLH<@YQ%=T*18alIchYXs!E3wiwxTAIi>cT4VjMFlVdCldy zagmz?C8m9g%=#O{THHLjqi3jp{Gza9pX-)%ahO0Z?j_)cwX^+__H;hTkGEVP>-2rZ{G$7@1M~0wkBzJjxkLsPWTdqF*S-~je}p@Xwq{|~gZGdbzt~pYyftj4 za|6zcSkQ=_`WRf|r`$`nxp5-i<(FEy-+tM(ReEMp!_Y6#`=(;W{zr}uC-2N1re)uA zd@!uhVWcmnaU{6EAfTT6)X-rtLA>_V=CsJM8HFd42o$1=usNZLAkg+Q1l^(4>DYnoCf$PV%x<5-v86wLn2*->tLg8)V)?bzRZaN}(8$r!McV)K9V) zEXCQg207IQ>qRD!an|_ne-z^CI$}5jytn1}qNa@)l|P4WT_pr~*tB9*1yZE+@)59? zgJNM<5|UiySksFfyL&{yiKt?s%GEBWX_6@g^bI;iHidHtWr3&GmuMEp1E7^kA;}12 zL0J%zid5BXl~Qw>{HW^W87@1fJrK+-9`g&F;kuU4KLnTjxX7ODdA6G&>Q&tPeGCmT zeAu4e00|GKM80P1~07>IpYhzE5+Y=juZHXs(q_X{e-M?w^HB|cK4!JYU>UGVDy z1_guW6^gNq(SlTf!WLL+q=yitprHhOGR%J$4D)QUd3+n*obAk@=mWIpczZ|*qFbMx zW7Onxyoz`71PbJ1*PdYX5I5kvdW~_vk@)Vo6I+#B$H4Og!z*%WokF$qBJh?Jj(1mxcdg0HyN_9OFQlXM z!W6o%$up=Bl;fwVmbzYXdhedEReRe>(i!BD@oP) zIR$t7JJaNiaX3(-QFMwlM02GTnnY%&$%pUR1?R6)e0jwk9f#~HhED7*m6FB_({hC@ zQ(LhFZvDK9#rVTvtC7&p?%kKLcjkYwN#X2w%y1E1t?7@-Kl5kLaNdQML-ETR;;OR9PYJ~6c+)-9`=l@ zg!hsMqPy-@M_0{6+CT~Pu4e|Nd$zJ`ir1cznq1V7XmVCWkVwj2q=N`JbZ)E(5?V;* z&s+lOSAD{v#ptpSO$_rkwB@AI9+~-G*3pzm?FCKgBi;rM;&v9|y0XmoKAy`rq}nK$ zyE(%ENES4=G1!!1TA+bap#S50`g1VdC^LPKo+_)2PTD_cRNX`=%&MRCZ#WAt$*f05 zFDx#;EE5tCLusYm`IQ`Mo5InHrTXeS8*QzLWZqtS=ihzl)ceLv=gGlPGxw1fp*w2l zzviK5jZjtyAvy)!9XmHIy>7i%B5&{UTU67fr}BvX4m)iL+`L~L`ZY1WP%!-Mt*wg^ zYjRQ>6O&(5v?Ft;d0X|Yc78txpJo|1`QwgxsY1F9?eT7RYb7B&{W14!5$ikoGK}qY zk+(7=*f;uodB#^SeNNMNl0Eiyy#-sB*5lx{W=Ib#)yh2L>9_n31=ZX_ z&c4%VG4>I!;=C+~zP(Eb^CQekK#D*HfN=o-_1D6uT&<*6R|0~fFr=mCj2u!BL2=|k zrUVXN+Tk#3k(#+SEEjmwBKZBd*@8R%Se;P5d`84&O(+&pqQTc9w3Y=>wx~aMz5B>@*+>b9__~+s zS*g(tMb2JBFY94ctHk5%tz~D=LQ+gi8LVCw;6m~?GnZ##={_?JC1K#Ql0W4*nRq4U!^z#fLtR$hh6Q83mNq~6cWL_bo2Av)$ zA%s~qga9%Nweq323W1R%@CwuW1@lSzXr3}zr3Vej`1&XW@VFJS1dN&iz#9D40K7f1 zI1QjU#K&ohIY3~%ByGI_aBcw0d>}V0;Pin~VGcI$5}M}*p4CgqiEYt@krx*}i?jbl zpaGTs0od8DVm*X(Vnxx80XK?S?L0~ZqYAaA&)V$SPQJaZELwnjb%KX%803$q%xaD)lQGD!k4AaO-S(#hyQE{f? z6sYIv2(=JrcqEj(4_k0P=+T*d`TcB&)%z*97l3hya*7O@98_J~(9a&0Q=++&gMzA? zn)=DjQw=(K*2*f%!Bs8YfTHDv={-^zb{ITdaigN&k=wL%aCm-ug(I%PO4UL}+o_2C zHZ=!IpY3UD18gpO{NiB$;R=GGgrKmnFxVJFSWIKJ@qHQ17>}Qt>2#M$;b=j1VRbml z2eEAMb@Y8+zWQq-d{SIMiZoE@6+p?MaF{h5Q`tF8U9wpEd(5uH9z0hY(NS5JL&VGtS6- z4nYZ0lVau}mK^~0*w8}qF}A1omTq&=vcA9H`aVL555W{^0XbggVy80Q=cM6DT91*? zH22oV0E_fjf!*q@&nb@P&R$OQNQkR0XwI)h&iAH^W22-6#Dt`f zgvyB2yvxqtE_dM^!79+%vh8q+OswXn2KcIP-lzxzmOR!-k7^PSOX<_B(x-Nw#YKKX zkGYzThcn&DMbC1-MBPd_!!Yp$J1TUjvOLTzX5?4@Y0sA~L|DHBwfS?4Gi)K9X#<1-?s2|r|RAXTNY>D#bT*Jf>GP&ug4 z1cyC0iiq9Dm%rmoyN?|Yn7h25Y<;%C@Rehwaf9ucX*a8cnsM^_T6)pqZ2Bzy1EIvV z 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


H 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%0A{!ySI(RTCFtaxhu4NY-Z&q&% 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%0DK!lF9e0|7_ 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 Ml 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

G7R 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%0AcLku;4;D>#xki?0PcCA7f~sf4xd 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�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

nl%j8Mg1H+`hpV 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%0Dd%7SzlB9Xy!7XVdaow1(~+GF?_IEG(L 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 >hgA(8KS`>6 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 KpK|MiHAYNOv;OkK2vWFnI-dcZ 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 >$0F%s%ALKpA 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≻S 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 z%0D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⪻*!ET4yez6ihYzn70T7H-h34SPc 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

d? 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
>cg7*w<^-3g2Yi0s1% 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

Xq}IUHF4X+)mDmqT736=Zk9RZC;OCA0I! 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 V-~49w1`dDuO$$_I`-(L&{{g6X$h!al delta 2082301 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

y9b^*ChFlFKJ0g55>i7D3Uj5Ofcx9NKwvlN)L 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

=37E%-eIVl>&<_xE@8xzg8{BHZH&P5 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

89@ye7GDsfaBxV&Q7;H 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⪻f!hqG36r#H6S3gU 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 =5va>#a}t{6;@MxU)iQ zyQm9XqJG7WiRv~hA)CTo2*MVYaJ4YR!}>4uLkdRI^hi7!xLzzV-IsCA_(GS0^0(NJ zzu9y_Pkm8`{8;ZzqpQy3_s!WE7&s*^W|r0za5=tI{X}bZ;+eSo(fJ?2QA2r{KBHf+ z(Enz{jS~S?Cy+QuC|wvVwR>fEIg!;VV(!|g$M~{?S6n=L>4VNmD%8IOS|L)PV7y{> z4uniZ6~K&Kr}$sN@bCL2%C=HvD9BHWk(XcQ1#|!-?nb}^VAD8LetY39gNN980re}=G(2mLX@8F|6-%v{mN}8?{qU2lgz$J(%{^`8x_wl{Jp zIf=`3TFTYN(Zvz>xBu)3+tV|BkQay9zaNic8|^>aUL7GCR`EVr`Q(>GWR4wK!AJlu zR}r}T$+&FzezRIt+7x&?I0UF@ZLv>9pAurXphvv z-H!cq*#7Kn^w5+g>|`&mMck!^Jmug>S722jrK3IFjjb=txbA8r3k^z`P7k?8@G&9m z=!{=gFbLT4+f`@Ln?eY_N*?5`T)1p*RnbZ|i4RWR;!byoQzI1*!5d7dnH(gZypK9+ za5!zlRnA$h@9HRA5aRM;3Zfb^VtEib<6DC{5QrL#f=mBeL9POn6YddqFea%YQRqsD z7P5IzN~jK)O+9{qu2;P8)@qszm%t#2p;N?Zt9Ek*%}AGfC0^#uyh|LXg0`0Z9~3uw z@2)ZLS(+r_VBGt!r*SWBZGT{Y0iMftd-`W6V5wC9sY>mz*hsNu3S@q=B&NV+*B}nT zmpB~*4UXX7V?{A&Fi|f3a~?dC)hIwJHBkNwNL52T5M$+km8bu@9w`-RL2X4xSL(sUg*YYKe8_~3pifHbm_T`*3CYub280WBYm+TAk~8L zW;$#UNVi zg)7}Qvvu)d+B|4~;O{&wpJ+hdfmM-+@I9ltTuLBlsr8y|NOIfA`c>?}oL}(lhgn!8 z6%E!kRSRT5rmx%2%cbtSSSY3@Pbw&Gfxj`kYxMPChCpu;7UO{i*hlIyA}p8t&k0#E zi}$%i&i$CKVC*X<@p@5BS5d-k*Kz%lcCXUZTp@~B2qHA7G+!X;{v(At3AD@os9vU7mCkXDylRro;;}Gs<3SOYCt+hduTdK3Pw%axUTaWs_;@?*U;ns%M^3w^YujT&Qn(dFA_ebz;7zxb2qaHmz@(TN0* zsuP`?4Qa>FKKX3v{zI6%9sj^SXEF9pqN6vEyp;FtdTD!Vd!pXuL;y*__k{c7@!&3E zJ5pV>u5jeO@Sm-X5`z)0Fhl z))TVgOMne7QvHa1d)A|9kq5(Zw z{!?xrhStN06w9>x5I>jFsJYmE%XWTl6&#I3&eXk()JQ%IB|v71Xem#L!{r1{&V{P% zIHv-nyXxp&-oO_Ajk_fFiR~R7ynq}g_p3+G?@Ry3p8b)i~-vTd)k@};3E5dp5{WpJoi=C-Kg(jkxOSn~(cdMZbF?Ugj21Hfv>PKY z7yb3P$#2%}nYU9Lf1he}J=Jj1IgwUkmLP}sJpI*?!)b}H(K%2szF%qPj{~UR_|8|K zpi~voGw9&!#(Hr@cQo0!fu(En(74#EY4rO$_v`?A_@i`Jg1(Ln;~bEp8~2}M=6}3H zbWM7b)yEdc+Q8K9oyEN6PDVr6JG_zrOW{<5%zH}$f5=+Tli6{a`7AUb2F{*p#>7xF z*K8szg)JN6f0Ov{~$l<)umqD9nNIy z|4#B?bxIT4JX-STAH%%tB96}S>8_@Uq)yqbi2{!wyqTKPfxHLbEzw$G685phUI6y-zCCok#{);(SelWlK*JR>XQR?0G0`h?@p0f1-eU@_N+ ze^{R@w#il!OoUO7>OJ=rzk$~TcNlRU4)~fiuy)m1)N==GgOXKS?m_ zUT>ClJpHxh){IXrvMS@~?Oqk6V|Mb%ejb?T4!)5UMS0&4IXvox&Yanj?P;9WR9cvf zfB1ASQ!FoY&YP_DXl^N(F3ASKo@#*y7Pt{^-XG%Y!oR#vi@_)aCQhSP)T;;YE$`Sl zIPjghkp*f7$^h2o9Cynf7py)5Ga?p>=59rOet0)N^M_+K=>T7*OX-MwP-D;cVOyT8 z*sT^qJIReUTn+NuT%x`&c5L3Ebh`W05PfUh^nD?@wE3Cg6-(54^okv{<33Jsuf&fW zU+tFv+0m_ZcnyxmM}Nj{0$ke0t>p85s#eCv3p224SOUi1aG9+y^Gbzo99=F!GWYV} zjx7ykVBs^fF5DhYYP!=+ByV@$L5RBQf}vLOBvNFRh*V};W~5~&RV?sfoIP|6()LOl|USrY`=I{IXe_2>Oy-)Jdk zASU0pTf$#28x<_!ix#)~R~%XN$U4EQ`oyaN++ZN`7sFct`c)(P2+;TABPepY93 zeai8SLbe=LvwP7wnp!q$BVQ1gaTzpH{ArkAjP&S5Eyeitkvn|iIp^AJ2Rt%*Dj)o0 zm(bl!nV&UBXGcbbb;xlivH?3Ao!A*`q0?kw!_$MyOQ#Y@imz6&!%TTYiYM$2S=ig` z(6QlMvWOMYLiIk)`9 zwUzUYs0s#a1~M{bF%(>;MdfEJx!X_v!M{I$rzkUQ<*PI*g%$y|ZbF0nDrBeS=;aWi zW2VOb-0UN0zAj82SFZ!cQ&p$vbl^t$%BYt4qP2vf-UG2JbfP(8Z*w+*rB=$BsD9t|>I(C5+C<_X5PW+Z_Jm-+K_%)TsW1{j9@F`Vb3FF&k-u)GiTA*oa78a7Z z?==j=+OLH8kNU_>8!+be+E8Okh0@O`KmzUCwxrXKRpehBUm63UGx~F2xIhhNZpD?X z@&#&lg`w?1-4=HyAV(g?G>m4!)X_GzNICc%JCJ6F za*g%{@r++}rMtb%_DaNOu>C`Oxva|2*@xrXr#gUT_1@#SWb6g-tXbG2lU z$c*{u*_dgmeYydq$I49Dsk}Iv9*sMl;yNmyBxj#&SW_JgY)%M54W3^rx0%?%=9ll| z9E)fE-_5(gH#*hRGOg_xa3F0JK@HymP<7x9W+U@VSPvVj z7vLN(V3QABJ{{%}4q`l%Jq`%Rnw;ZhuO0vgi8ExzpV8PD-Rm)`kiLG}=%agqPusD$ z;|IQST{8;D)o-a|%}4?4n#*8ZtM@B7n?tk5x=+Y(;5S;|==TRDqiNPcIzk6-!kPej z>(_rnQ8f;1atFPS1^@38v(nCD9-;Ffc;sqRGr zFW(-&31u(Z?^ELu);{9|3cO6!#1wO;;xu)bG+#xVwcFCC&bvM#)1y&D_i&K1c=Pl^ zzxf#b{S(!h$&aUG$T1BIL?&-nW;1D@H(I1zG|U-1&#K)KX~#9eE?exkH`t6v2H`BM zk9TRJc-jq3SuADT1+f)*Th4>ctb)Q2gJlMAKp2w#-&n@8d$DA*VP_}d6Qu{?oLmya z$03Hhoym~B8T|Ak^?juP!I15k#XeG_U+x?ln9*)N zbP($a)h8a^TR==Js3?GMwIN=9?HhVluk-gYnIT(R%{go_xa)*}R2D#`2Craz)<(i*WFUtio9Q-x?G|_e`F&VyW@u z7fko`#dNGDR#Q8TGg<^&;QyzFBO6F2NhAnuC)nGuub_4sG=P0Zu(FV_8*u{Qi0!*Z zlwA<-w5_=uYZ0ZgB4wSRnbPbIZsRO0E%!GCuwJzxGB*@Bh{gfgeCUlpIQJ&iak^J9v z2Jyc%UTxv9|86~&BAV28gFop1>R*^?(8Y**`T>jJT_Szi__DxM$#~O6K^}0GgQcDi`PJ-#^;Bqsam3ls%$77@^mCsI; z9gnA}jj}u+Qx>JE+|4&uv~}-mo5ae=kTd%Fez({3&;Qx*vwk3Zzqt*$&R?c!yMm%g zDAIAipd(SXj22VS6XxP(mz7nu!z^NO@07RZc2e90ORS*VCeq5N|IuLr$J|Mobct(J z9gt3L!JDED*rb6*VNU11L8Zi{2v%i#wo|#)vt_EhYKe8QYqc{^df@qR2bd#QkJ$m? zDRFUts#URd3Riow68)Y)X$Z>(0oVO@-$v@T#wS%CAEVLUWu;|Ku0y;z>P(`~>JT18 zmG2#`-H~15*&ToENDF_N_DFs>=Q1l&SJI^C*Jo*rGv2WaA$tja+WfN>v@g}iX#LM( z8s>U~N7}6!H3h4@Z7sdVJ?wA>-F&|4<{WU{`%e7v^M;>=QH{Am^!d7Hj34u*c=5I1 z5nM?hQE*DU%fL>{XHlUhPxoZHP*aj|v!5ZaaG6+J4{ZI?)+Yc}f&_Lx$Nr?mkV2=U zKX8taK=j3u~wX$l_w2E*%NZws4sQ$7Ng^4n4iL~8qzhbFb;3cDHxs~US zoQc~)-J)f%@vJ7hEyo#YOnR}?D7-~WJ+9;0mHSiu{b?h!qN5SW&SzDY;|vUPrJzwd_FS;!9j5$!A#JGr*9fL{)g!E)4T%N3Mf?kyMl_ zztXyn+BX!A7E1(K{-E3CV<^#6Qkhy5C{(6>S;Tfi$iQlsl^1uN&E1GM*HagTUj!}~ zxz9?ATo~K@Tq>|{H0oPbsjX+GPBS>FKVN9xA^z36k1g+BsdG28aYjTY-=j}ev%x;A z96anv)8Gd>o#K0bE@eA55bj+wC`^`G53EA5` zjCj`v_3JCNZkLU-YBum!6G7DNI$qtgqelhJqg4!yM2pPU4qC zNGMU}yqTgow7fqlv92Ybjcy6{K-w|HVjiIj5><_SN#hYtV=r=`zqCZ^^6ZzM&W?6TXM}vsaibb=fUq)wCl~^CT27{o1Z2Zt z#G#bVgFU>Yl@5E~j;H;NqL7L*y^T4?28(?!V9&oJ^fFRSG?a63&ehxFm8Itb8Y!}~#dsUYb8RJGU3#_i zv=R%$yGv)c;KlAdncjwUCAG5ZqJC&d!@zZ_XBNsjxQwp?#?6v5avd87hVDkuHR zL$WndYgxHYU4kY(%|2rxZi2xe(%gD+HMsO!jPV|6*cI)j->s)pNhpuLdEes|pzvAO zxD>Hnve0*F5!lbt>Lqtbsk$c352S)y1IgQxIwUR>z=NTP%$Ho#qtY@=L%7D@MUJ)G zyIV{evNXR*&va48^Wt3?M*1xlzoPFU*NH$?wU%slTqCoi!`{h7r1IS0nctsZ$HyBa z5qws)MAb~FZd^bqY&t%E%VME-0{SnBJV-TN(V%g!(q#YU=_bZtj8gNal35qVmrBOS zYp!|Hd}R%KVRMR@rbYaAnSgoD?QLZp)B)4@C)28UE6aq1ZZ7!~9@W zt-1K787pGLekq{%Fs7{vvNXV*pV`%A=am(_r-3a`p{!sD=ONX$TIn?xAkRmRN?-Pz zeK-i$sM23=-%90m&+ZVPoT$bk`Sne+^CEg#^LtCYUAG3nl#dLHZRkhZ2aBzvS z{9(GjHFN_&|F-ip$uHwHk*-v#z_Eu{th{Hj(Uw}qAx?~S#(JCn^{9)IN2z0L#Vt8S zGG#SE0f6S_Ac>6_KvnQ68tyFSlep-S*<1`;2;~s%m@JxgDO$aJT;ls!URU6tHU&RH zIGOb(#aaoFZASMNO{8Q+7U6WgPC4f}mvK`3bok?%ylz3nGOcU25AB=UNu$ctJhe|nmkl~aYX)`qDj9Zu7uk0;2F zu~h5VYLMFurC;qpY0OR9*WXaHG6|FeAzB+`S74c)=YOZzRHopSx9}OHnj|IcLXhOY zchqO#scl5Zr49Q|7>wvf@X5k35g1%``Zgv#Gqxoz0 z8eU|lJ=75h(rdgInGGSKG_U4TTT&mhiGhbqi5DkM+O1?!?n0WZ39o$6Qz|by#q8{1 z1z*^>)N+M~k(Tn-(_5qe$U*N2nvl zgC(PYLi$|X*W=ybncPx&tWSO9wMF=K{vc*0rCzNF#}&i0%hj?!8o+SkSvm?>kyL zYIj80adSt%HO+r;Ul7E40cG+ON3x5mok#t{CXX*)ol$GitRYzmnnYgTISEnYb9y8q!CQDUoGTb7h#nYJTsbLM(Tqk7d%%$&!9woAv zyQ-VUTN3l9_E%3z8d}tZPaLTEl7ikFp3qM>y>xA{Ce@d4r8U%!3BK__SEt|4sQ?6_YjC@x7 z%1dlrpnCqx)(zy94D!(-(e*+gQK?PI>Hz90EhRE3R4#=CS~6`@Odx3Q)J&=j`p@)I zU=uVUWelrfo38OA!Wks_f0HOL1bImDL16BpIc6CV?4!Qq=Ds5-fr!7#e<#tfrQR5h zI*GLDZ@-s?HvFE@sLx>UIojj_8 zeBUku(~3%)`j$5=P;i~Vd0F(i)%aPhTcvunq0&vytj(L+?GB;eQ}(!(cjKC<2b)Ip z{i8T`NoY;lN2q4sX$M9KCrnky+OLvsg~K|eqc!eSGa`lX#tIQ3zPTIVemZ#8g&CQU zQ7Yc~)w%|Z9jpBOda8??{XUF!(0;Tx2;0G<;XdCYHGBplHGC^0H9lSNFy`}4vSmz< z-0IZ)^OVK)H$k%k;VlxXvHs8o{hL43@7NiqUQIviGO*)lp?<|g&>5>fgki`59eJ+U zgZgz*h-7$xo}OI=-@`@|VH~fuhSTsZK*bWfjQY&3E&@*-U?8JILe>#@TvA+$+jidW z$(RFR%|l81Pb)1EgDlXL>ZO}3uR<$sz1kQ1pog0{-oboMAnOof4t`N*jeF} z7#|#mlUiez(OvzMHuL<%-ql7Qh1S+k>G}&fY?Px?c4=Vty?P3~P<5TSBH%M5j#=c1_>g>a1 z!Di{JB;zA3H~IU|SruIjgXZucM5ZbgDwHN9EnT|YaTUfK8OuV2e0FvP zl|#bK17kf^Sb^gu)j46UOn>KSaISK&+}_O-R}XI?$6NDd zoIA3r;xOr1NfZrj(N+o|{RUJF4iqtSJUq3(#s1!xVXY@uKtKF#vPN?zr{)Dv!C|4j zDgZg=tVTspq8!?TbQ3yH<9k7gr6!Qm7H18j_dobX7I0nd?P=3PX`pJD*{~i~w00 zb;^tTA2OTix&`IPgIS~tMm{tho~-~Gn1K=I!hIwRHZfr&bwT4xh!DViZ7KCn9)3_o zxhLd?&~w0RbbR<^UydeQ-eRP0UYs8o%Nt|)p*x#ZE?I3QHPzdg>fe*?v(TJ7JLaX! z&dYX~$4^%%**KxQ@nr+Njl<;h?YSG_IUl$}ssrixjt3VQMrru`!|6Smvyk9L*&-=u zHtdeY7O@N3BgYl1Vus%&f8kRb)K(WyROUy#(~DA|I#vyO_MUVuH{K&FcvN|CjpFiT zT-!fC`nrBfB+NgQ6FUCle0`evB-8xobQjMmpT1_FTMOFXO9V)pT=O-7$N&jOwMzT3 z*$S;ekQ!Bwm_srf+Ys~-#ENd9DOsB@4)jJ(Nx16qW_b)99Z2^zn0Ksn!O`qlX2Wm$ zrCaFl{YRPUF;z=jsxtY*?bbQT#bar6uZoseK@hB@7g4{Q-)~sB*1PLLYw$LLgkPG0 z!M!d%FT#-zn!x$4iHnNMpQVe_fP^{e&}cfekZ@wVck?qICEBz+N_*O!`w|#~o{gbJ z;>mAvBbN#c9qaw6{z25D`EuXpk(@{iq6$jt;CKVd={S%UCQH7pZW&UF{Bkp+302~8EQxt zWM|`AE#UcMi-zYvy9a?yp`(rz%+Vduv)YR2wx}k7qd^ht5+7o^RKzM9*SaWjJ}g9> z`~hV*br2VqprZjm5*@zO4JX0Kmo2Tfvr}Oq3JXSekh(XWkxtOz@c(lCppENjW^+1Y zgt8l;2$hWh(HGfr_#RBRL(z;t=dF-e9;Ik<`d-1zxb*^y9MGiEx~bF&Hd80;D`%Qy zVPr3D)%1y57=@ZiP50$fA_f})Wk2hv0Ob(Duxik);^TLsPK0l>2F&gvispoc+Aswl zI9oFat1oz~82@l{3vhUo8(283;l8|?8%BArd71NItjEI^k#41hr;rVqvEj;N3eVKt z9Sm`7ww=lBhua9uV^Kk*HW4Yp{SVJ_bC?u)B_K- zBDEc0qcySCobbHD-c*tIelk%jSNQiN-90WVbR??H$|JS ziWuxIH-vHv)8$}tV|d&2JGF^_P$+Lk_Jx_*aP;C0k0+U(i4}N!lJj?ij_rrrv4b|U zCIS^gBjv4K?R_C89^%MqWhUYh0fR&fZbCMawNiy23-&0DmZCDSkkX87K0iM+^xGBt zn`KQi{Gqjc>!(&=StoIbfJ+i=-stD|pirluadbiWy#St$q>fOSLT_{H^lyTXoQj?V zt&z={O4T|eCBa5flLH;zSc>aI)esH5CscwY2ZgFZ@4OyoiqSjLjy-np5<#s;VPi#Q z&H2|sY*YE0{kN?%=->URt?bu8Xf6a_>aCiJ(1JyW21PZG$*#ps`rHfDP!|^xufZ41 zegEd>yZ$wSDrk1G1*~TOn(i_?$0ZM;{+AL-r=I?v@StQdb_1fQnAqw|pfn~Zazro0 zaPOjGxT_&E(zrQ9r8tckD3Q5NDqcJ!T*{09+a)4(z(Qc&vU@|a6+yG#s{1q z;yz5-|1ZL=F5-;1ESZHgwEOPhsdxZs?OiFwrQ52L<=pu zn{yVL`Z(9MrS;FqElF^tvElp0&Zg#DQb7O%iy57Dc69~T+j7Pi zD$^TqS6f`|B{KHRXiMhrm>v6iy=ULjaySDsvUMKWs{FFBkEtg*gWMxZX2vRqIgELH z?#eKgcp|v@E+_SxSE$9HS^_wjE5#NG0#o8LH+3zj@o%%T^j-E_#odl5MTz!Y&uB$- z3^v0N%svozil1ll+PW6eMN#;6(aSdUgDl+3U2{`}MMBViy|tTH#)LTvyG<^lIXU5h zoblV-XyrTP7WWQ@;;U)jTE?;96DyO$r3S&da((>KF;3I_cIqi08XMdtxP$Zw=-lbu z{Sy+PXkyowTTr#dnO5RZ(4tCPoVPmM>r>mCu++a{*1UIYm0{C^vk_IVqfJ)u+WK?}FyQ~Df9=i6w1aSVHa{ME!~$*HsY$Cd5~yLT}n z&r&Rasu`AS=Jt2;AQ2Yh==tS(|Ko<_oaM(3hs6w9o@ZJ+SfM0XCE57yPBtqJc1!cg ziiFeGzi}Ppl`+wbT5RhR__!L8-;EqC(q9dwrzx8MZq*}e+GE9-;LZ?9R~t!`@?Fog zb{-cJ6N_bfH+Q^4rc*=1!m1M{S3D-t&}s}Myt%nm9=P#ZJM(S zqsd1XzbAGWIaHCWcD)PHlwL7Ywp;b>)))i%N6!YAgH!TD&`qfFll=UV~=$JEb4V z^#=*i-0*-#$7vQD@r;M_a0%lanGy-Q;kN~oqO#b6GsL&a=8>tpbGOy6r#~oGm~B5V zTQqwoyeP*#c1px8cP=u>HHR#_Zu)6gmCc{NJQbEt+#ZQZ-B`snmeXfUCCoS!%*_CC ztwOsx^$bF{0iM*#iL;2*-EMuIJiMoZZk;I!Z;2NrZ~{h8_Q{23Wafrk!7i0j12)Rd z@XwZ9gx9k)HGU%aWC@17^F5l(Z07{G656zxm9wblcPmM3#3Y>Fdm=VNi)WRot=Xq{ zY=OUZ8z-mG(Y}wb6P1T?+gS&sF>%+}^l|Fiw3z4pW0;E2jxxjYKkfD7PGy{Cud;2; zGqV0@&0hq;srb=;t%=5IkHxu!&)nxd;B@)rg@rW^8zMqhQG^cBvF7gsH_>iXZ* zIZgJ5-ujaE%h9(2&nKGcTb;2zdE48}dH#)Joq0<||pZzOj;tU+Zu+~mD z`tg-knuPX*fMN3~zT6)toQoNGvz0}UI&^-lv%lc^BYmp?sin#OjXT@b$j5`M*)RaS@)HSQoH ze9rzuTABNa{MqTHHC(hH9BH!1kc3Y+cd%3ItYR}JHGx5(^P>xgbEd-ETZU?uTk~>r zvgwkmAd zw+RNrSdXs(PR}p(jm(A824=jXoh7UsY(q);gU3&Bay^%QTPOOT+~uBZm)@_zduwnL zceQao*uexhQ+zCbuqg_Xz5(RTvuuzpU(GSZY@>FPG!CHP*7kxppmD z|0V%=!kl3Do_HX)=CtS;#VBdQ&!{>iZ#*9arQB6hj#CE(zB5gZd;F*_TA3Mpk9>6? zVX~Pl(cmMFruhBw&|hXVn$bQsZOgPMZJJSw@EM94e9x^DSKH9MdByDZs4X52g3t9@q#j%3fy$L~_EYCZAdJe)#Yk?d(B7ZFBHIAq|K)Ep% z&O=AR6Od8eF2-`72&R+bfbg>#tAq(;WIn2Z^%+olfaqM7=!}U(6Q1NOlOYupWC$Pf ztp;M-O_kgW+X#^F4MOKG2K-9}WmCHk`!9R<_n4C-;}UnH+nv3*FGjImD%q zP$aNA>mDa-mjS}Zh#}fxdDPNT@R5VBRJUsyM?5P?RBQHi&7zs&b^GBn`1;T~V<(=o zP^aaYd-}#D;?H8^gyqI$7%ACA_oMl{0X_5gYioDfH*X4A&;)Mw#7YW7i4_|iKDb|T z!$evYu8~}lH81ueQyTV9IStSG-N-jCc62SPQUwl~r>JVv$rcfk4i3+(Sm3>YfK?XA zhadk<;QF`H=$-2^`fv8HUfazHHl_(eOdLSTy>145ECc13g~PcEb$PA%GbOa|H36E8 zrP|m0Zkq0uCto(7uXgn;W3)5T@=AXDc6Z^5lVcjq^rD#WS)IPNSjmdai0d2NV52dU zq8A-|vJq47TT=xHr2m3mBLMKEQpNP57s%Cu3(QkNN91aJpseZv)WB3Nh7ggeS+evi zka+P-DnL(I9=w{qJ*|P;mF>37$scmsk8Bzgv(8A2kq-h>ISCW5za9X(+5H@@MZXXyRDligjrj1E0^UFvA z_3Ju1-P~|Zn>ZPJ*Waqr80>9X>)wluJ*6@jgvj!~0`!Fy`wDo>ak9$O-Qgn@6@g;BSiXujvPocYUd-7V%!zlrSZP*5 z{$OpqqrBgYLWd+SPhXh4L&D9$eSPGaB%s7|+Z4{{fAoeomer&tXA+_cr+2r&f(v6BJ#FC&&Of(c{E-Hp`9)j}wvVpYK zxHi9B8+fs%vGRTCAu$WHaf{t3+5h z9@r>nF1N zgvL9bvh-W!88O`Vsqz&l7{03s*4eUkT)J4%^7lrK|IH=BJ@v%(%YS|&QF(V0+%l8Mh&8*#<@1G1r0bA%_%ntU3OhU=rj)}esk3E9XwJbjJOC6 zzqNOV*5`EeAkw(L!k#A!dcH8f@8o zDsEWCI;FD}=V~SLqY8yvX%-J}HLf&IH7m-2?4#xjkU-IVY&tM74A1~eZe@&&n|F`d zk~ChW|J@k;UBOzP?Cil)>Qc7*C<;Vvb7{K4+txWEA#4m8lR$QuK$yIprZ|;^~eW(tUKX@Yb4Utx>SoW!O_2lGIVzt7aq?wgl9OzXaFBj|S zbpIIV@s%y_>|`<~{K-rA>Y@jSX;IK1sYaXLlS zHwvrjnq>357SKT5R7$MOa(ByNl_}r-%5HpxQ(fo#T7g`*Aa2vJDv7);?&rmGhMAI^ zAR|&seQeIFa9_u{!`CKbFRkAvw%Q103Tm3wj9K6%*JNG{wJf;U`5#QoR0nCTjd*+K z=Cyya;l|f)#`tHg!0+}wRMmo9_!W){GoZVy<78#lYWO1Vu%*_;WPU$1ivL9>77Pu$dw1)jt(bIj zL^=TLQERL8U&ujpi5*+byxu-anTG*=a&fecEm-0Rk2LILtxN6sB6W@3x3bIg<9KJ& zgJ9%@!cPE}F9ygfk#~HHh6c^rS=25DXZmxou@6rW(a!^Y3yZx9=H=0AvfA+mYDR*ai>Ofwr#c4! zkMy(K2m}e$U;}vsP3*u92IJ1NzhrkR@a0Q*^)=Wq2`lhL{iw&kt!g(2kzpKe>@KK8 z1<-{^+jPbL<KCiyq7A7krruqx|iZ>B&W_fbnJ!TY5afeZ%-y-r{y{fIT>(zIl zE%gxq$-Yf8G8eUdYo>s{y8`QHJ9Au?_fqO{m^!mK$j5yfgtf!tiI(DP4`#ffQvtEP z_Un6o0+oktCq2Gfna82c&JX@VQ2$Gecku-Zm0TAU4d?o7P~DGYjA(mV`KrOQHI#`7 zl4Yw+rg;X0p0K~*S`J~y4u*NPmR~?CtM_pbr{e4r;rzz16=x;Y(T^DNVCyT*o7`pk(t=QNpJRt{DreCi!#^F4~CGs6Yz zVg)>Rll2j&g8EPX^kWn)g!Z2|N!s3v@%}+`5F!E(05?jQ`JOyw|7Q6y>I%cIE} zotPhPZR@<~wII5)7T)dK#jR+(%vPg5eyG#&P08kF6)=5MH#7`z!fQ&jEO z&Zi2JNxt0{KYc%=*^0`h`kc^}1=*b18AAP|2vD>=dP3-8tj3WoMOE@^{c^tm#JYfh z1Mm~FAtJ;jz4p)ArOlkWHtffaSi~|071GQ)Ku}t=m@h2l;zgrqr~~WW(Kyx+iis4b zKPy&;7WEq<)=s-;w@XBkjC-bM>EI9=7Pew)?#_vo1SqxIxcBYHut-Qwf35u1BI2ct zO&x{mqfDu;gA(o(JlT|}c^?k)1S7WA78o3A2Zr~FTG&IL@Kf9E<=?3tci;|ex=-Io zS4mp`K9yrd-}QsWTl&&^anzr74M9no-%Ve7CfU2Foqd0aYo`%1#{#*7by_d%kXkes zw8$u01&{&!;i9vkM!|nqTYK-_(830>&<(Mnd}?ayg1&zwPT%R{WSzxwt}uG)jxDpI zq-j08#D9)^0k~XG7KrUu4T{rZgkazGflbv_s9t%&U5f$6sAb8%&lKv^JdDPK}WE1)I-JGS$@8 z?P36RAdYY%&1Psjn{%Pr9q)mgC5^y7E@}g8s?fIkbDLgsz31Px>btto>-^0n(QB-L z+jlyLr5U2-oEKJ55DNAD!fld7EO?OD#Dk{ZaaA}<;XE|7^vulK^uEg6f#C+;ZA4Fx;*!1D^>0== zOK-@aXC34M){By5pIoc1n?2x?_jso6vr0xGwjxApEGJCMndHLTiPc|q65-M?!i+{n zqR`BM={eqEMzS`t;}&@q%{vbgziD7mfo`-MtR-4K+SC-M-YRmVQx)lH5SI}v_^M2x zPKQ4t5@9NDJ?a%=%=goeaFQo1;}VKXrRFZ1#v3B$m3%)YX- zJP(3sUxbfc(}Ku0ads|UT|JliMmwOttJ0~W8MxPgzM=IsC1+&kRU77iuO|MUmVg_q zw@aTu18YXclqm6bE9}88thQlI_bzSP0PoM@>Vn=^WRG~7WoNBt%K^y`DJBJviWb_7 zsO-1sn;%Es+}tYTSK_<*Zh1(9ZO&|`SM>9WM%*E8*T`g~XJ&8+wO58uc;SLaC@X~0 zs44FppE^^o3$}8=zX7cpLTI#;hRIhMS<`&o ztaLGWfS>vbCwn?UNGv`Cy)vqf#E+@rWH_cC0SlC`jblj&oTI z)?C{m!2fkV0cp~;>!(G}cYSh>x6s(refg(Q>2+TA{V~;d*f!l+a|U^8JD|<}wfnNc z`ucBk%l{U)Fim<_uA)d;kdj-0VgTltYYMXIE+VQ%j#rv&1$e5JSAXhQF zAkvcDylgE*^a3yS(Ru_smFs0v@yk$yg?yEKYgfqh63z`7ls4Zcuk`dPBRrl~<=7|6 z?In`aC;zi})MVL9Ni={@RE!lXwqt#}o_cAkhWlUx&}36)cfh6r)tSf?XOMQt= zbY%Zl`fQ3xf1y?)m+1d6%ybX4>#3ikV0QpJ*Ql#eQ%Ja5J!vV{?u!Uc{ek^L_g!#? zsh!$YZ%BdFP+PlvkV}cUt&5r#Y+P8LR22At(Cz(tY1x!I$`u>BLLXsmoyzQNB3h_| z-kW<612Vk3)@o^<`_$GmsNK-cTTt>4sWEZwxYi87Nrd2GT%sq4V}_QGH-@&7aj0PD zSE`i;m{F?{&b->OK_*>QG5=Gwnf{}6&&X%j`cgv;$MVxyorVr$gN`GT3awpT zw&`cSJyA*CcAU`IV@hwPh^>%?z=*gH5*FC^HA@hV(FB8sdF}OiQ#O~!aH}d&)#E6& zJNt(F>CEA$dOg@j!v=IY#gbEv>pg0vTUnQLFM2UJ44BvIwe?%9^CjhE zkFd(Pi(clrT@S`$e=Ou3s*{p%oo!wC7)jFsA(667LdxbB;AhL33mlGCTtDJCp5~r* zQJlL?!?g$#UZbpRI0|wxX%;-OgicU>YjFxG&_%rAcu|#7%34RLC8tIY5>Is0;Ce z3{YBUX1HcEjELs*48En;0i2vk!>w`1U+U{`heWJNF4Iy})z*t4yM-8ObcHI;wl6X( zKx0BUap?2bWtGM@<;2=EDO^uEGuKxr9}h;LWJk2AmAIJDj`a-c@z^!PTCXl89HCvX znQzlJk}b1X+pBkQ*(UMAa?m=oetio-dK7MDXf||!984Xy;Y0)x$2K5RZ!NxoFoI=Z z$N1!n{R-WSpz6EfDl5msxY+l}_;qwBd|gN=e{I`@V2s>`%8-RIE!0aJQYll2R;3|6 z<)IQL@Kj3vl^zz({oaLqNSkx)6Ifn09@8k|?8e9*wr zer%#q%T}C`IHmWSk)zjeig^o-9u6#H@h*6HZsN!*LjWe!-~IJ2d~S%j678|EG*(Zt zM_I@^b?31uq2|y^Gj*48e-T!AN5zFMwaL?@`_aWOqgzZgwdh3+$?8M4+OuM_io0T+ zm9*UM=S8t9e@+H>`BeC;dyH@?lBJKiB^(H54u|t>%RP1P9|k^sRl-QBZW+t}+oM18 zzl&JP?gYil=S!1r!^Pt|DHGqnTw{7J2WySD4cvnG_T25!!k8v7XS8h7?MdX?&%D7W zw2I%^_4_@iV?_l1(Sn!P;eE~5@|BWJ;(Pl;gbzQ>48eh5T_EQdeqXP#ykhP8oFKc$ zRG|&5)NGV53{O=0f=WWkjlF}GiW8sXZp6;oPOVX_<%rK8be@|qS@H-p6v^p2SM+(B z`L2QZvD}pxs|8cF}-ShwZE@_#(OSzTDk@t3NJ_Xiyp7rUtG z`QZD)>psnPwZ*$FQ*3@hZ??Tz-n3WXaL2pdwbVlr-X9Z zV8QdvRO5_(_15ZVTlK5A1X^-ZY6;q0HUMn9XRL5RQu0MoE}G*#zMLlcj-)KMCX$QF z*e2k-bYJt{Nh{=U#hQ1Zd9M4E8j@yC9#8_e>wNV6rbe6t~bUf|6qL-{y7;TczRn|#)h(t ztD#0}CT67-Lk_Jpdf>vX2SeY4qUsU{m<59#^TNx4mYMxN?w#K1a;N3s3qeq}4y;oH zyxrx42N9e;N!^#fT(8BBOOvM2zv4d17LtAsw7exq5<@a7$6gXRV3&MaQ#o%1&Xde0 zU6kTz;SGjcN!kNk928-sUrvWYjwd8i8<=_{HaB+Z6?Tjw9=7hI#1VPx$5+|$y|(~s zDcm77HxR2)E5-vMHnxT|R#URhEjaLc#3dxZiI5Y6u$U67uVy5iZKk`B9v_ufgZaHH zc3O|Kg^3o`Z?12?{TEW?W^Ek<&dc*}a)-m)aa7e1@ctOEAvpvtKLn2*H0hI^aL7<# z*;2Gy{z>@xqC^IhVM_~Sam1uMD=9#I`6?03Jv3_=)X{JXi#&z(g8w>Bqp%dMBM;ei zNe4cn-9m}WA}hUQTEKI)d*}S`PF|B;jD{Eq%m~<1B1F;kOQfo@(!#W%4Wh;3oT_@) z4dGYMwxFB}N+Ku07D$h4>C&;tHCG7_i+U;HePPI!v6|MuPrc{p=9SH63_iK=@xMRA z^{5S4J#ox|!oE>u_Pp;+fjT(gA53K1{TFxHY7!FY{mQ~JL;AuMlfgo-F-tEmVd?^B z^n5soWiAqpbGbQrtNh^&vBV?zU9z@>uTV*7(GNq{Y78Wl=`rzVYohDWX9a3B{red2 z<}#pnhYmHnqgjj*Q0^GO5UIlk1a2AX8o|iMleRKXqz+aM_*q?>xZf;Qdjewep zS}MBoYD9Z%eCU0+rKr;L$Rj$ju>5%b^&kP`>n-2fU)8^`w6PQWUOpm&iP-qjoeE=P zn{PfPTzY>fqu6(_etcKfp`w=Vwua<0HMUo$Io;k$av@QpIK4}eTX*L=-_%p44!GUH z?+-Vyi4BY?;iHa}6YlS8kF46Y#1m(UJEguy2vEst5i17YIC=6ysSo7^a$9)Yna=4g@aF7R#Y6RcN{z@WJcjslFErP- zTrl6v=4_@eikWS3{0mmkyZ^*9B{AEj8o z9bZXpIj~Vd=#?+okDFeu5k_TxSgiHUl3h09TrL#(rJr_2{arI0ee zff6<)2C134@q{OO~mTrE7@HJUDq6?B+?O&j(?90LtP!D4jrG{*xT5r(C(_N>4l+#L>(8R zCHTx8ul>D*$Wnzt=bJT>s? z{<9^Hr=0C#v2l9~2n&1R%-`)ndTMC=`u@ma9NJV}MpY?&O5-RHQ|&J@UH@t14IdSk zQ<2|_JU^PaIY^Ql5fu6WW@k-JJaH}tZ2i3WLrd(z)h$DiR-5#U@HGp*zTg6VR} zCJ0FjbqSY5xv8)~3v=nuB5Sg*Y(uz0s)lJebLviV8ndV@EW(4HO2VVke&Od{gq{C< zpr-m*j9gGBYXh7-1rbihdLJ^wwdRhG_;I+}8VbT7`0ssEv$M<_)d3z!r+xkdszoxR zhv{t{5TyO%53`(i)#V+b8nsDF-p`N1Ty$bpWtMxeQ2qyUd*N?=8t~yyBr`p#19DM# ztbuS~GkHpp=18gmR0L5LDg`Yd1Co6BPH}afh*>cI=!LhuEAleT& zkH8ki#Yo*a43=4n%Xgd#(S>g1I4*AGWy|e&d!^#f+iyOgf?@aaNd59d;a}@41N0$6 ziHq|1OL&>|i$y30;N3lFOaXsKEs<&ngELOO^9KB9@t7WMHDSNKmQ%T#pRxe0zx5(X zFmv(=x6V!dI##1;w6miUU_myF)jWQ8b{Z$3wWUEWz5Kf8$K5euDSR?u9hJjtvm zu&sYr+u<#?Upn1;are1O<0m}NEz6gaLaiM>Vl}jy{f=F&wOYk-s(DoZ;cXdR>D08j zW>oQV760nN2 z|E`^)@Kedd0K$JTc=-AFf@TMRDLZopvyzbwpnUp7767R;FI7-|xWy}~&b3P=;V%Y4 z@{Wucpz&n^kE0^dev0L4 z>Mo{*unY+6%Zr{$^`B=Y2;`5H+Qk@TmO5+E0hRHVabNTPdOZ$}zn58l-Z7vy9w|ud zJTyHgIlUIVV9Hc|Yt&-nIWM&eSHbd`_-91LyFC|0<=}dY{Gw!mumF6Tba5?Bn%P^kHzZ{UPgtm%jH_9~ zW0xwJ&}tXsNjDb&ynVxS>r8WqIxez`hi!JP=6Lnx@!Unf{gfZm#pNT%wevloJ=M&I zA4Q6KN>BdJgRzJDD;#uKGH>R-K5(qrKD&^ob(%J@gX!55%n(sPz6lZB{)s+l3{(YR zLOCsngx2?OMbo78X8+KrnbwYW7$}T6Sh*77l5dfau3*Vhltdk$J3h9t0i|203KGe$ z66QS`5gHQ2;#`Bk+#fpmu?wF5s#^|!_a3kyBRsn%5Ua!k%&6ttopz@_pGtNAY>$@` z=F^s9zHB*O^@|hdRhE10^5SSDTSh9?JMZaImZnPGJ&@g)4c&(R@>n!|w*DGS`sYx6 zWX71^4Nq@JhauB_nbty37-iOX`NM}}d8i~4s-^%jK7?TewUQmROE`8xHS21z$TVx1 zmOy-NX-OLewr|Z+HR$Zr_F!RQu|0ZFhO}=+Gv7}3ZVake$z8N6c|DGogi~5&L&chd zch#T_M&0;Z&3WR)x%~djEdo~uw@!U)VDB!`+U}O6uHiGnb%Zxn(7nq`3XjQZ8GN=Q zmmYJqX-qyfeRCS=EIL+dPCehT4rrnSS&qn=#4qhpD0sB!b`#giI-rLUI2;kxT=hxIqG&_da z8k*mgOH^DWTN@&NWF)_td!=Gn8T2K`C8^dylDtW^Qyq?(0|D zv$3@{I1*)oUP2UGtB5Eh*gBJm^v8t9j{{Oi4ii^1RuGygJS(WLayD=1v1)T27xFIm zLKP>>7i$e~5cQ+ayB4miDejHGkUMNs7gs2<<#w4?;cm0MOMzTPB)Q-pgb?k~oiCG) zsk^uMpeaj>v89DMb-JkjQ&DuZKFi)tr7I{cz{07BNmmP!!3V*y@!X~hG z>tVg@%u>)~Ij7=<&GwK|Ci7D|HnW{t)ZsGMV}czGwdlW+SlV8+M()CR%FvqvP};fk z`ul#yEp?GUSJZX+Y>%3meUU`X5%%(!==%~DP}PvKHAVG^{8ew04Tql7b$LY;&ANYt zJRopbI&e5t=SM-;z`-x|*WVX%1G^LJobRVxTV1rc{ViieG`quXJNbQ~$fO$AaTV8Z z`))=*mx(>QgLSwBEris8Q1JCBL%a(r!%2xP`>BreU1D;^;@ri)IVfILMy?xj@3SU; zU$}Koz1r&D;mM6|5^O9Y5k^mMLD@fOYnY7Yca#;ZlOiTUEN)~?-U?e7lP()aA@VQB7;5$h8PZx5BPzTTv8qy1@2ooe*_M{*Uwu5 zx&KNVc`q1}EceC+{4!nR{ADh~?wAm}UyoB$RTn9(LR}%C2(vi%bCPZN6vb_SjYL zU2mUYFJoNZ`QO7X)wT@2^DRI4DscGUIjbVw-BPDq$8=ehtN?+OP$8cS)9;y@ zLE_I^Ecz8j%=<5EgCMLv`9fbive_#@y%%BhTdM}^Fl3RHjD=F2AKGA)r=!KLqZ&jf zxrjupR!+W-+^!%o($E~9I*zeN*O$$Jg3+B!HbG_>Me8obOPl>ow{vrI6QU1dZueAAE7ZN7)Fb&kIsxv>PtEgvWs@N_xPSdXX+?i`qb{o$Ncvi~c#v<4G zQX#v^GCHC3`qXx8BG78IrAA{lfz1wI^)qjU_=2c&l5@SZujS^q7o5EI$Yr^IwFh1B zS~CaO{7JvdYT*nuk+{Tb z`K=ld{;zi2nY&nVL(SGPEQ|~@Qp?T)aOHW^O8>&~vu~Q-HKAuaHs*PV7V4v+I89%- za5RX$zbp3Bm+%wY5qQs(TV1g6_)6e+vF-*-o7w7xX}2IQ7rpereIqkLd~~G!gF8QO zgqL`qhH~uI`-kODz%6SLKnPmsGF_*PpzrVE=kV(LNYjb z1_uU4*ZohE=3<*)iHl1UGbq;>Q9{ci=^Fh==O_dc>q8ds9aJ9lPz9CCpU5WB|la%LS zw6qhytuJuQ67Y$q(+jxIN`BVXsIz z1q`a+GZ+>C7G$F=EKY?JhmP$0zO-I|{2og0DsZRT&QT050Az;GHV7O&0Ob#KcN4r= z7sb2pN<|8+MBw?WN7$n)i)xU~S3FGL=6Qu!X|*%G2PLQscK+h_LLv38Nf@=bOHL-v zTqVNoVn<^%uKlBta)kl@>Cu5yVLTOZH{cB*0C#OJS*X|;{A zV>!bLPVaHFilbE&%q9#JW(<1l`}0X-cZqtJ-cD_5uahUDF!}wQ^QBBPdTpky$B!*i zM$OY8Ar|Z~@%$|8fg-j2&C@WY=ohtws$AF+e8S`F0jZJ$k~4?f$TU`n$qjK4H}|Te z3G;VsE%c=8%osZj>c1Zm3}d4@wE+bIKXp17)D4qXU5ufR2ryE6b}L|LwLkg*eOL z1>EaiEBJhm0t`i3@;o9>HZzNx(osZ`Cel|D;eB?-t)4M>QnIonX^M9D%4a^u8O4OY zgmZ~CL&H)v6*KGQ>dfkqu)Rj+1YH>^qh@NyDP|n6+HKt=-IZD;EFbooskeNhGGHUO zw6Jnpxx^T^WbWRjC}rC?Q769zv5^^<7G|%+RpNYyJbUS zQe_vXG&1XA0-+6gx9z@bNBn>66VK2^_S^05T_M|7vo0JYcsMKsPe%itT@3rgu5)IV zQFrfguRoTGNWjfvqJoArIuh}MM5O2XP*^HBrKxUbq0^!;We~bt1V``!Z$L4z{&cjh zFb~oJV{!=xQCEgPP+=zf-#!Y!&(fj{i@D`_1&&j^wCS>`DDPr!agq%^UcO}vAs!5* zx-`nrFroWzj2)SG49WNy$Yzx5pZ%_f+9rU~s2VbivOw&Dcym0F`9;>=QH+nC@Q#@P31R&5GMHHb4MK z)*xq9(u66UL`+GoMs0n~V0&Y$_=beZZ1N5p8$j6r0PG4-uD{68u|i)PHU9qZ^eZdK z&Py1W*;o;=@6!21SBt@9mU+!TENthgYL2mjrI!Q%2N$?FZ*s0IwpNQHS*Z^%|Lnr9jg0; zgecM!C4bG#o|Q<^WfNulaAv)| z+=#O7S@UpyfhtD=qvYo=0HcX`KB?(n$(VYHNbpL5ffgHq;tSdXdwr(s6Y63d-t^SO zG~DG=dS$u!%B}|kO0f0fzHXtMnBneH+&U#v{0%iU{g-q5yT2h=Q{Pj^Hmp}`t$=@C+`ci|c zlHRw&&i0_StDVhcuhfV_ZSE=&5OzbrWBRCJbTxC=_#irH5)z-49>CBTQg#XRn;l*d ze?+D23oc?$3-%F$X7amgfw9fKz&o+`jrW#_TuWi!qW4y0f5bS?5nfZnBXD1^rI}06 z9UUI@wV$(o3>5;w2z|Yt8dY1!Y?Oar{YLuE&fcxrFAoy0^{0kS#65eL`B7lp-CKSh zk00ZQA4-B4B*hY|`G52-$l&tGn@uyxSVEV(z_U-t$Pa&U1ZUrRPx4n9EW+KlXIouPVAavg*6`T1CipdLJIzW8aKe z!PFXz&W;-c$q zkW`ouYAy|s%B`y2`&B`5qAjd;S>xw@<#OS=b&~FHJU-8b%f*kMHFm*vZ$#qtTs98; zZ;ZD~CYTJo*U+E6t6f)fn(69VR~5#W5XGjXr267kwNpn|_k@VM$6uYi{27BK5u|;7 zKWl4(PyOE0YYBA5SXsp)yB{%k0}JlBo~CvuITg<*VE@Qad-%b@AL$SeJnX`+IG?=K z$-2@BD{#cZLeap7TdQb)yL#%Ke#2I#VSLpaC8ugYDpNKYF3gw=An9-zM`Jge@0D$j z?9RZ1Ir~;y<;{2Un=q}vr7R#KqUOJRHOaXXQWXu7ixsq>DR+8v#0?dbqR}ekB}%W? zS~+$0l-J$KFH_}^WAO?8baAV2T}x!H4KMn!9rD;F9*v*hsi{x>Q2gysLiBmZ`bKZo#JesUM zoX!b&DW`hIXLmnt_F1*IWU8+{v8+>SY2)C}ucwFXlg7QW+_L}|KIH3%q<@juJ#Kr= zduo5eOp*(hs&Jl{SAI=((dGs8(Y}9IZKU4uAwcrGMYxn!iSu&B8zfUaEuC@)Y}QX{ zvR0B>p5IC(OKMR*GVo~)ISV4sM_1h2594_(>^HllC1>t(AGhCn zPG~Cy2xt3vRF|Mlep0Lxmi+0?BWPiPwi$=iyoA0=V_(kbXQf-vLY89o-s4{R%=*xB z*$6~bUS_hh1IoS%+B_O*volOd1rVHe8;hm`Sn#>rP&@^Eozj5g%G@GjeUBaLp|Rri zuPm$(bE&qvu+g{u=RX25ZsSxvxMi*xo6CHlL+;Ezfq^z|?T2BbR?P9sU=~sh<#8Bhfd&%7L-2=(1NPT@aI0VK)Qco`rAyC-{R@YAczvftP)B3aa!4o(X|bFf zuMnU@PHe`ROtQu`*V_3f@u)b)`mxA=s{1jj+HBNEkXw^p6kKdau&)u)FpvY9itM`# zu)l!g4mDnXG3*}DBM#1Q2tTJ+6#tnw`c7UIDR3!pQ+DYHlBwgu`o6&?R%LfgEVLjB z%JmTpXXwD>LNi(0xvU^kcLDS$-|10dC|I}Ko%0@u^!JV_;4TKgnk6XonWl6k11yi~ zee34#+OHE!s_<Qi(cn!ie0XxI7{NbDhoOLqNr!O*sU= zhT)xDtVc>5n=&#oSLlWiR0^SRoQ+PlUvW<}+llhHCBp^mrsmt$tW5a+Fj+bznKo?) z{x%)rIrVA5rhrR;Dyq`8c?(UkWLZ+gUD0JW-aTDh9}t$nf$L#wDWf9_D|2%4Dz_K( zZl8#*EaW87v~;Msp5l&MFlM!9F$!VWujK=)5&!~HOG}ILXd@6k1-UnA{5r)!f6A$D zPHLBGQ0Vw^?b+jbD~mOZne{O!kInKL5`x(Weu=RVb{ zpK+8*KIiD_Sm?L*j;24m(7gr&hPMP@K31MN z^_}Gz%pW-R2F6O&y3RUr{%JwZ3Q=uq*fC|PWqW)NE#RCTwq;08yr^V%@kld~ztyx6 z)kvGhM3N3TnVnks@`%cvluyqOjb5+#vNyx$(2)OcY-wj|Lbm0of_O^I1ge5~mLsN% ziD8*S!2!EW_W5Mf@P%W)5trAefDeW%|7YLvExx^fW*zzx{QDC(qD7E>%8^SDE_+Ez z6I7f}3^lBd8hU7Orm&}*{pbdmXSdpXv z!OkiAF%ri2giw(>NS!*5xV3-u(E7+B6F*;k2zO}god;|r-%Uu@s2JddJR`A_jo5?> zcH*I&c7m8we$23QKC1IPl_-*ZS}o;i0@@9xGU~gdesAAOr=lgd&u}KDZRC>K6f&*h zI$L0?>EjqATR-r36bz3p~f-v3IxK9c$eXA1YajAS)Xm?vV2xj@fLjD zwZc-f0V>Tl#Nv?`^3E{{`_dU)ddKw#B-uo+H3xFo){;%sUW z*&{2tbWyPcbkRvzu0l4JP?B|MlmZ=veXfYl`1{+5Mz?VuL}K3~#NkcMr?$skOYvR3 zD5ZatGxJ^y9r0*$sSJ=>JO;Gd1^A{HAIdS_fr6m=T#f1+O~)AQj)WUv*8It zro(r7%T_X?9k+{SOD|n~P!@|mNCE?n#{R4zEu28VyK46%cG%fC1CotKtlGd`t-6%O zT8e|M&xHen*XlA`+{Qb-JVpQ|{GM)L4c|LoW7|Zy0WGd*ZgMibda#01(EsCBJmz=N zZ$V(j*v7&p13=rg4L3LK6_X?H9zov^z-_SDo^)V%9C2G{;laDG0@Q?>zi%PU#3g6h z|F=~{_k3YDaCshQ3 zpjODv5M|GH*Z50cxaxgOOZ6gj;j;mUkIJx!5ZOL9$kns(=*z67b?(G7wY$a1+k8i+ro9fSZrq zzhT8Q6spP|>Rmxa>Hjl5Gwh`hc+X2LTe%7>@zWSB)BhW`XSwz(YwvzN5A-6$y!|PK z80AyqS99O@t%#BtTeKIo1008%2IQi|| zL~kWlA?5IE-Y)3-KFg|4yG;^hY@o$y^RTn(6PTuvHo&Moaky_ALIhCJM@h|=r9R^p6&3_r0xJY1h! z?hublZts3vTylJS{#}IKR`yuc868{bnn!Xq~ZlQ4VU+HJ0TfsrrBeOJJzlu=OrL3Jx&kUhJ!aZ~_GROex$Og0ik?Xfdq$OFg`E)-brAIX@ z;F{3Kk%vPiOq!0xK#AK)0)E|hcqxl2ojsRgO|2qTkH}Kp&`$K*6inNyWg|b%OXwZ1 z-?NIV;Gd^QUVpthzu4M9Doyb`>=xMV zy&$F8?Jeue#YL5(mseb=n~h7UGup7XZDdp$@NR8qI$sg)w|9f2V!cO$0D<#Hv*w3#)9Erk34mSuRE^Ad@L)qgekDiVBKV6t!+r0 z;Hz*SD0ltpJsj29yD@dO;Ex}?-|~$`Nk8K+{8E2-GowscgsybA`)<*joA;W~XXH+5 zG#RiRXiQDI|6#iT4Dwc+pSYo(71q{`N*3hXGPc5sQip=}m+`ib)eT)e2EgXw3@h@j zOWZlMQ_8E7WoC2MQ%~K7?b|}o<}%WvibKGz?v*EBLO+N7km4ox1fo4V9j^(OmjxTAlPCUc1k&pI`InlcV{!)ZFDs<4gaX zn9XT3+2WnS6+f`m%D&z(zNLR(#)mcYdDXjIUZ1I$a48oJbLq4G$1z1UOD@yBwD0C= zIg&oCwCu)YX~9vNTw-M^#c^4K>NALD_0KQ}bkGF_FW}q27MkEWoI=w>4@T*xjf9CH z8taAAW*?^Q8NDPoQKmrB5{kgc>Gy93hYVtP9}MvhzP`NcsVKRZYt*~pm?>Sn2q4lg z^|AmN@y7X$Vnzh!iddI3I};~AD^LEJ8Un10Zg5b-nD?Wrb;GlhQL}&6$6r#F`uc5r z`F_#tTZV065o5oR(S?M@y^cd6dr|p}PN^O_%Um z8XEuBsJHBw(7s3N0`H=8y~>?e7TWHMS8q0$jXdvO%z$5Z<+wS{h#*nR+G8_5b5PPZ`INoq+>Qikey29krMRnlKRGIV z$7M3f-t9*k;0)Qq0zqPn7Y#C`@jyyDjV@ld!c6AeN0ccv-A*mmlwy0># zdL=dOj3N7`6~cYjRUwX@IZoz>Qr0*DSJoblIx%n96l=iLyvW~79~;p| zd?mgZz*(Xv{{Ns8{%L7(XaB8WIT4_8H}Ci5Z5rF+N5r%Y{WVJ5jj7)f`- zBKkci=^rYo`ClIYxXnN2H67~U)_vYs_W8=*{SpdqQ@7m_ea0T;IfJgVEon2Hu4O=l z*J!<(jqjf0r)mTi0r}!OW=Kg%1W02L(S=!xw;MH@q8SrMbd%JRIPE@+tj0rf))j^8 zWc9zD2LlVUydC3n!{8!+!08HU(Tu#TgDKaWL=?S0G$ec&YH$EhI>iWE8+cP1H{CZ) z&N`ldNb!2Ow@mxUI!U)ue$DUKD)kbuWLw%wd0imPGZ|I{q|%65Chu6m(YQm02id?c{$i4UyI%d9Haovu#wco{n4Sxbij+8(u_Lz% zSR%@2NOg&wKZm@G^eu?&4&GDEmw;4dN-vG2o!%Nn(*yK15GzsK&rxs#la$<|bA<}u zLaX#Li(`vPEzpR*lq$|-;(7~ir1}9joWV0;?6c=!EBdxef2 zWjB-HojWa93=7y1IpGygZXSKy#2THfeq#!=_I*=KH!^$(R#Tv`h@>N=2PS>^q={oI zEcEP5wj#Jl$IcW$$bI&u&uAt?A+Tuv+yNxc};0L?n*9$ zk+69>K75g7t54FxtC~zwBKb-K;&CI}#<%&7_jge#uIR&mrab08$1|4n;*7?%;NxGj zjZX~fj3neoolx-9YUyS!%x1n4n68dHs&VcVs6DXGoxmkMG}h(abt)V5|kI=?XZR?oGwvms4^?HAM(IeacY zk?8p~LSdJG*tK?PSFFPe3Y%An0Jy5lD_8?;)+T2Ou^*w2z2`t?V+(l-D}Fo=Dv5Q( zr;F&_^^*2e#JU=5_j*Lf*V|hN_QllpmI!xV-oJI%Cc-48nL_X6Rbv34blFq|YKzMJ4R(>$^o`6=gxVz1mA#i&h*> zij*Kfj~6R+#fS0~1gS7mrx{(LfZG6$o|KAzt;k(6Z@kM_+iTjg-YWON zsjKCeL*=&BrJ(CPr-QsKdZd5TFRSwe2KzVpHJzZr*Vn631<||v9F6_90l?PK+SuK&bABO+UmhU`Fnu$zY1BkI;~gU|C~ttuv8+v!bZXY zEI$Wq>;qVkD#Uzkq^WoF_-1i*`_-ICSU9s6wBSwGgLM1uj*LhGk*YTvp+|YQVPBv$ z;eMkWx4Bw%8589&6)MpOp#^ynMbL`lsZ0XW?a=tiys;axxrpVzus|E)K6sSRkIMu$ zym7|cWs&C@d(6R2`qrh}L+%fpV= zXV5x|41PN!!Scz}4r2EV^1N}i^xK=D0a3A<-GLjz_uAW1(`XjELw(%iadHQYcpI(e z&IWU%=+gn^-QIB?9#M4`cy^0DRg>9JTeHDMB+et zhF@Gtd_M{#eVJpW=E7P1jWH66Sw|hzUdNC&KE=IbKDbq(XKko=uhreXvGS3=Q7uIm z5}1aV^Vnsu^E&Q(Xmryv$hqmWIcR!5UCV2oaJ8g^0l`ycMr}q7_16@2J)!!4sp}xK z$JDa55~zO$7HvMJrWO@bXa!SYM1_Eqc6KmNeA2vD?NrRaMO^fl{}gc*2n|}5qKIo2 zMO;(M#179nPVm>4U!rjt`TJwkFaPb~lHr-?D9ePL9Dpt^Xw|4=Mh5=hQ8%hci){@F z?HM{S4?*SVa}WPJwNk)!Vm(2H>qQ_&F;(vMFMR!9_Bvf?ne`LM)H)Z}J+n&3H36N^#b{p!ACPbs&2sDeagjtMjX8FC z2LWONn>GIaeAd)p8=K)n&|^yQpjl7S&<(~yu;Tet<&=;prU_nEl`yN!8!HjC)7sJJ zMn;t`-o`WALet|c1~Nwun;r#fEUtr6%@ZEZ&Z&bt*4at@!4(G&1X85J7EV0etKp5~ z4tFEvQN+a~N4-oah#i6?EnTgBl*S>Bp9p&*#enEKU(A^$XeFS~&ZqeXZw2lADi3^1 zQPaJ@)9p0Fbp8w6342+TAPM?|6Q0oLx;&V+rXm=j?9}|h?eNkuVBoe^iLxL) zA~Y}02D$YQZ6#1jn%)<+G?4sC0@LG_4WC}@9o5>Ai(pVUzkUkkh)fVEDfzUXSwFtb zNYoktuSc;iwyw(o^SJSIXrmcc!C2B^yyxaZ=yos}>_iQlp|Nr`4$ zI;2Q>bqu+J?zd%py>iO0QPpx(xN_)rZnv~9Im=4=GWBacs}BFlW)%8Sj_PvfdLvDX zum&3Hcp_SJ6L&*sbuwuF1Gk)tt}!JD+qP)s0Wc?Q^?s3@j>~F(rx4Y6aslQyU+F(& z$0yq~zxOoo68>LY8IvzbUjZ1@j=23Zz=L9}mn#E6t3v}6f}L9ZA%IWmJPGgge|MiWlKy|8kB8Xa$l} zd8tpi9@RPZ@LY$8HR1x=i{uIAjf9;VHau#E#&u8maJQGaSLYP==z+NQ5ycBm2X>8K z65AzVuzPUMDa{Uir3JqJ;t5Ydsd=L^H_dp>D4{|S9Sh7tJ#7}1E)%5B5nSKw(HLp?PlrC9j;$E zNKXgGI<5y~Zhv_Dc&_NAa~8_Jo*IcZXjvyTpm+qmBp_$G6!cyKQ@P2Z;|PkP zoPC2%`TxntFKoPL5O)q{msyW1K9)12RCLkX^F3+f(LeL^%SFcxpSv5`v%=dznUU33 zKx4rxSkB$uD~bw8k|{TP{*b!(u=$er7u6)Lx2zI{-jB|1HkY)Ivzm^x@QkxKSugd+ zbn{2k&)XbRKACPi+n)?%jEyAdmR&y{NSD;??7KAfHg`ZdF}pI)aK6Kztc<69gjo$w z7+Sb3DE1lKqs9-V(%*$Bo>uE5)5o8ZoX=H$x>T2I+0e#Ly|{f4v|5P+D7sOksC-sA z)-CnwXkzO$YNqGrpbs$sF@W6hcfNVG=Gh1Us3$Rwp8KZnr{3P+NBjQ9JDKZzdS@&% zJBJila$&B_oBUhv3VI&dIwuYFYOTojsLB5c>lpk)T7!AkMMdz({o4=peCDdgcRE(r zPuN|^Vs_1s;pX}Mp6h}@oOg>|?u@d3P-l{E0y6tyOb3Xu!>NUwN!li^SQab;6$Z|BM+H4?7uG$UHeH z!RzvN$bRf?|B=v80mS8RMP2IDbYT8a2fruE40<)R^4K9y9N!*FC|Ym@G<5j3pU9iZ zPI4btOQ=XUW$lY3xjJiYHQ0Lagr0>#^lhqxtE=Fgk>%Vot6SMLs-m{1+W682ayp4* z)$hLeiv!;=J4szox!RCGNAx|UJg)pQC)m|W?u^127V7v*ALPS{AD(i0cWdUGChuMN zIptw8ffgYZ0I&Y~U!ykR%qBwyuYR8OS5|Oj$YE~O0Wg)#k)>>1+j_476AIib7?*Vu zpnMgT<4RatQnH>ZIqLZ_)NsLqfxE=MZU!|jms~2eH&3GyCb{W`V!)FbDVCheV=b}W z=|CC1!PwWq{Xh#p)2k*n*Pr`cYvHEq*K+$lKtgLa{tw}dwKDWPz3->Hsgrnaf2hvs zwB`jfYI6v*H&7o4(~n5Dmqh!H>jLVBf+fm@la6X}%45{qx6j4HBD=b&JL;e-9xVVQpbuU^j zlv~CjND@W0mkp%qxX7A8s+#@+izF2_wr%p-qLFQad>$&LZq92*+(rBOl1%7)e;g7XbUzi&+ori3B~i_j@cLzSczkHwik}nPI=^5 z+puC5p>jl&%Qp%ajCm{CvvY60PHC&i?knx#LIc*_=>FG43FZez6 z9ugDcwktzYi$eUd<6Y-1NeKY2#&`7vnmS#(wt-0JFGiyV!Zn7z^Brk>zoiH1im4p` zrmu0pJkoK)HPs_Gg*dNOZ{He`dXhl$0M%d2mL?5}SP2YnRR2NKz4>)|{vfq#m!dt) ztJ6=D$RdXoEh@La!Vo;9mZW{Lj~(-GhaaO5gt`#8eUm!p__m3bWg@QTaT%W4l&#pd zz2}gcU6EFKQB&_8J!fg1gR7t9aEAMEit*E?4^%@ZfzwHhEAm`@O0e>77r>f*jUa==B9!sRFJY%b`{HQa?-Rq6r|QA}Et=&7CvKk%51{AuXPUf# znuz0`_=r2_QI%K*ZeVzfBdIoc?07>@I>jB#4n}ah7ID#1N|h6S2I53(Rg)VS8+eo# zROUkXF%%EHYfmKi$L>!poVCHFZByr+&dC|Kbl(JLSwV8>l+BJE*G$)(G;E@V?n|5v z>3xC7{mMc9O%I9J5^dka3UXLHftqG(r3iL(30F@iJ6TKS;l%N{e(+A|iWA)TZ2Zbn zj2AY09MVUiE&!$uo`aB9F@A{!QhjAi1@h42VWzXR-oECGFT}1`n<%&Hl9BEvHWEOK zo6D~+E>3oG#kElA*;S#2?QXqeAZ;21M4xy5cv06(M`72l?BLtXF}RJcLCXvzSlmHgjHIJQm=4blVz;y^x(wFDag^R_lj{=>_O)Ka+=`kp+U>GM%60tUjv z{_3UvFP$A^x?crlxrZ_RD~UpPWe@mo?D(`CdjMtUri|8dXw?DePAH1{15^vPCH~V} zW%SVR8k4&TN(0JSa7M_GTO^oPRuxR|s z8vh>xohqY*;N(Gn7_pnR-P%q9AHdr~-3?Iv$k1%*+POb|>z@r}x?Z$+M-$lR>4G4)p zvlas~A1bZUw1-mxnp8`!PpwtFdHm)kc(lP7PJ=DS+Q}idv~*7`eMffTb-7}3kiDv& z8P21fYw^kNhZGO&irsG-OW>9_pF7w{)uoW8SRC$zxum3V?K}vTA2;uuyJWe1R=R9) z>7OCrg)a^D+uB>~lg9rP6OgA*bmr;~2Qa9zhd=c(m=W1dDFEjH3LuY@P~T*%>w z=);+og0Dga(311lg>`T>F>4X|YGwng%co>-u*~_fcQclhgr2HXSlV9rgHtmun&lfW zc`>qkA&B=v7NW^yDeSU@MT&VxVOe>ng{c$qBZ%_hJdHeL-@nz;BUaNtcf$N}!J4!|?4~|HWOr|~C)Y$fz+dj*!lOHX( zGlsF@KuNwhA)0of6mw@+Z(xFF#&=)F*PlVYSA9XQA*U|!7grWcx<6w5raALt;fLPM zVd)F^6eGTz<0!r)es}NCr9;UupV;paCS=wp#O5BxTUdzbZ`1<9F!tTRsJYVP#eI^#(V&bfFM;13uS*G%+nwj(W4tDIAxE}}TALvt;_PcYcz^43am~k;t zjLNh(CW+0}$KDm+C}wyf(l7sP9|<`ju!aG*1gDAY0t@5+*;Z*&Zz&HKfhzCZz6{}U8JZZo^=V7<8ZUsk&f>w zZI^#Dhd7|sb=M`D7_XFl7ZZI6YB&=LLT~j3g_RC$GAj^%Z?q$WCrP>*H z930|eUGq5Bo60ZWdy@x`D&-O`1631?D^U_`Na<1)>)0ld>zg=K3O>9n0F}jq0fRKm z`UQ!IZLPtN4j=8|6HlAoBTi6N14A5ef>Tk5S9;z#p%8{fA^^(1e>2d1$ffsleTPrw zC=P7Pia}gK9oRzdD$QIcO1% zY|6h>)x7I6^C)sOr<0W@q!@k-@3k|ue#bMh-bXl#0|Ne5dCgXDwKJ zMK=E2FL`=E?;Ry?xdSgz#>WtqSP+>EG@#-Eo@ujMyTshWBz| zib?i+eD<@c2>R0~Fb~K(Xi#DeaG)(Z(uNKi4~(=@@XzjBi-l}>R!!$p$$r>idZydQ zttAbXvM5D#6(2|)N%wTKgHTO$y{JeS9At@{dv?md0`JO|ZiDXn?Ey?nzf=dpTK zH1^+s(yl#u6TQm&Ahe(3mx@>y8_E5@@K&{Q z=S|T#xubew^n6o)F&s(Y23giPYshA*4m)T?++M9rjXPkJPE{FvyK>;k%O{Nj1l4z& z6}!uUDg?SyBENgn3M9;#6%sDCYq=;3LPX3fGDsaWQo0mDcb^fNX>aMkZKp?wLniw3 zozi07dks|;g8~W+^P0|_oxzz7%qWrWihyJj9F#C$j8t`~{aIw!g4{R5$ zr@lR5mtG>h!AOvDbr9l03=KZ;?62lJQxEHfGPEwGsGG{)%bksk)!v~+T3>u4sPFDE zv6JfFIx#P`@A3Um|96iP>sohQ+=`*7e~#M*uVJjW`zR$Jd|j#fWHpD}={X4#XBSjJ zO`d}FBuQAfc5cn$D+djjan$Vn43ZyUL$MugPvQ-xS0awDPm)VPl{hE*Jl z#DkL@q-N)L8JB`td-R0Yv;qPMlft$@VRCUbxuO^`Xl+c+mBw)ca}q9n_^=)StOPca z>~2a_HZyA{&KTAwr58;wig7+~aw82tA53|0eDg-ugKxGAKWVxpcEKV@jt>YowKYM3Q(7vi z&5|e*O(-hHU|Q{wXDeZdTZQPQ(w;-2#CaoSHfhWhg89bSx1psM;C$sx5Z8ov=Qzo6_j z`I&9(uxsvJdYF;H6^1r@r!)`d`1d!nn52Z}4u|S^e=L~ts@wjQ5ziWKP13_dl@jtjgFsd=H3Ooj(zo!4ux3sv=GwM5>8FaR^1IrRAx ziN)XDq8vbyze=#l6pJ^sT|56o?9gbHMuw%7JM+Bpm!H)Qaqq@f9kHbAuCDJYZEa83 zt=HYtcrhnE<09YsVo7tY%IzqD;oBYecF8^)%r?fTv7L{1@%C3lx@gXT|GjWHwtu<3 z?^TI;Ua6ZT6^e-#P6E)L+whOY9B1A?9mZF)sQWpjd{CnyT|SAO;9Eu9OZSntlZbT? zp^Uo3?*DkcU#mCO?7_dpTmK4lTf?054#mb+0oFe0jO`%-cL@nuqQ2XgYvXT8u9t)? zl3&fttZ6b#RT+M|r%BR1J%_hAOOw>QbE3Jc%)16ZtW`6PhTa_-_^H*O@^{%!xhaL$O67sF(D(wigNM8B18+pO|V$U z)KI^RbNG9Tam5l4lKcK<%^4-N0J(ob^psw^fkj4wpo0sK`7`Dkp)weQ>@~vKT=$|r z{l+DxPkFt;6)W0nZX=~nef&btFJFCOe(~>R?>--3!z{(9U-onu(^j+GT)kI>VF*w6 zKH{c>a6oQil%YQ7T5%+x51@z~;b>MN=+~)2A`CRGOl*k?6z35`=$Zg`4Kp>SU+M?? zUk$v&818WHsI+@rD@i%b^Gu3${}^X(a3$GP4vR9!{5h#^ca)U^AU`)7XIGhKGhcgg z#40x|-_?(mGyPX~8oupCzQNOX@L=U7@?(o3C!{YE$Zzd@HKNR#4JC7M1Be%kL)Wac ztBk4BsB#_|P3Q;dlT!mnNpSkH{vi?2hK5a7L(Lps5UfcV5lkEC+u8?rUDJP$__Uoq zJQVr|02aHZ&kDi4rNWUrJp!E!?lH5gqvx{@PYX`Bws!UQxE*}X((oKW^2OwJDobf>qjxIm~t zUib8VhD71Q9`6Ao1tPb9WG6|VUqA1ZnU%3i^TOinOPAw^dh=T48@9|Ppp?5`g+jh;pejC)oN;`wF_VuB-XWffra&>j!5d@Z>a$Ynp)BkIC3cH7Oav)<5O7;OlxR(Yq>Xd zsrWnqnI*H6t6TN7h)3f(l>K0OT0!a+r^pjGRez|C?^^_i#Gj$-UV@zyV`ptZlc z)$H@FYwRo95>HznnaftC*1M`_x{0Vi_!W`5`(e3Q5IE`L~+Hv33@eEK*W zm+ZoTUhU8LN{a!<8OfuDz0WM?O5&{Q#hVPzH47h$t_ZAU#{-NeLH=XEvEpr^?X^V3 zkFX#Ayw9z!x*spOkEVNA6~oKuzOBPe1szjPsbnXuCvfTKkdyzi#7L2>WAuP>ryaxpwEcS@@k%T^i}dL{O{ZdRW8Xjd>%(^ch} zxG-&{ca`gHe?4~T&n0q~9;zL9qTa9#el}Z^Ur=`X?c*6n_H1Q)ASxZ_B}B*(i?>zR zCuiJ8r!*(RgLd8#RWNq%_L~txeDj(@cxy{X9NqmC;w(MoscE$7xrEjEJ99l9SaA7B zk?6RT?BFx_%=^8?W*4$8rKNA6{hgkI<Pfxk&}xbM{-I6`x5cZi@0NDGsk5V5 z#h_&^6lJh=w4t=+Tk~gQH)VZ$@;bsMPK&k!vvUMjr-!HmBi@0;p{DhYC~}aO8=q1J z$`oVY-ztq01~DncX}O~d7Hp;vN~)$@=$O=-d!EO-_NpQ<2?SuAnXBJgzvjC!ceeUG zI7J#^SehL!n&Xw45nL{&8)_0_EXD0b4LWM6Tyw2_Z?#Y8Dg2SKB2u+)8{3Lv!u+2> zYmzDty~Zzc>Vg`vS0?r7sbcxJQUROR&D;h?!EbH{VIQj#05^P{OkiD1=o`Z?RRi=yg|W)l%hJkrne9lr>Fo3 z4z2*Q4b4lbSW8to5~hX#%UCB!1HSWpz#XOE_269~THW(p!c~4ZPTJeLqfh)^byxs_ zYjT3s;oLK=s;2O32eCP0O}dK`Y_k`(hj+~8gkL)oM^-Ox*lQ2oSb8$m39_TlUwD`O z20)lGLLsD*^Rf8@j3|@kai%K--+ozzglI;cXyUgS$uX*u)_t1-j&Iz>9$5n&d6S3} zsW%??`39i5-%MvE0KZFFd@v83cBU+uS?p%F*VL4RanvT)9#+1Lzo--uWI=+8(Iq8lnEqAb;jSt znz_7<0rq-sY1PRZbu%*wgfm(S%Zvms2_cNT>y7axc7$$DaIb@=>t*= zAANPUdtZwb(`5gC}x*Yg{wSbX?Az(wMdlCG+Q4L8);M_q% zpc3P-h(1-P`H`_BTXQw+3M|CI`>8l2uN67K8hn(nRkiv!rJGr|?p(8KDBEf!^S?YF zyc@JCR5Oz#~5lAa@@PL{MB{6o7?d(VQDM5VDi zBX5#oSan>UG$OWy3FN_YVzT9uhNecf^8pT({&VG^BYR;fP7%awsRb^dxPJ<8V_e>f3_+-_x*vA80@%E#f(dJ$&wN!Klk>soPQX8 z1u^MNglQLFF&mTBNDSXwbt*$M=p$M)W>Q;0F4K+E@l}3y((<3PmQn$RtCcAL6_sj5 zL}CNgj}1$G-?wjsP}WH>`3^{Y#ttf;_VuwKi#1FqmSTto;5-V-l{F~Hi=L2@;f8{j2&E-{^IMrs~u1Dza36Rh@%{zg1+CyU+u98 z?dfR$O!d8aDYWuk?dAxBeDi|gAxX=Eu&aw~C%K@mE@)ATgE8sT){r|7XHk{yuI@i5 zo;_Z23`{T*QWw->sj*#e% zpxwt5N5`_z1KVuS@lC7-BBQ+bCH!C(l`h6rcQ4ghsS2=BWisYu=TyN^Y#~R{PhLH) zVwG39!_#Kk^u?{UCiJ?PhT1{3vBDWkXE=4}xLqVrhdHDhHk5V2RXnX!qLxsPRk4Jb zP8@P2*nrht4ImYEE=j|E#r6wYnVVo^-f*!*5AhW!-JUjxc7myCLK)h8|wQQZw2Q}f1l!0v&Ii7xtv7AB;Z*DEwRjTJIoq^6=P zzYSFC1!9x3D$XsJW^0#?y~|~i7UrK3)zde&Z>|jiobo9+gQ$;m(@Zj*0Vtu{3PcIk zYj$|!8^`r!n~ut4fzqymab~)w^&SDHVmbs#2@DKeR9QU-kK?dwI=F z=j?^5fo&{<975K_Hi_)ucu*kXFP=!Rbm}b{^?rhH^9%HGv zmQ|nr_{X}^C8ex-Ezb>`7O&uBCRCa`*w>nGvTofrekYtcH9Xnq*RRPzH$RjTkS?m@ zz7}TS%mdbRXyB0sTphlZpe_3GSI!L|kSFRpeKune#Hb-!%;f{O7Q?LFQbDjD3_xN$ zJmh?FO8|eL_MOs)Wap$q9&lv`M;(4h#5yxp^VMuD&3(epHW_l>9u})eF6Y{uqeMf*MW*~+m5lr7#VktvSW2ssmb zls#+uzG?7rmS1({|0iQ@ZZoFfe*Jo0qK_D?+%?Wr(YOq8)khvv>KZojDE}F5<ErF}2|o?~8$i&hyJR z{%mm}Al=2dsV2pt#|@2nE-kUusz?52S$&d#x%JQEZq zJD8+4QTaviqn`c#M2s+vt!naq)cT6~vtGk2HdY3~E3Yme%@lRQa$$Xv&($bjmD}%F zza<%y1yKr}Iv+2t6#5_C{F=Lk^JDhnC!;}h&OueGV)4k>D1B^!0~04^$NE+H8mnOo z&K)yFByCMjci?#yhWs%s=t3pZbjvY~C-^cvif|=J&`^i_eP=?1kIlJc3Rap@$6w)I zb`0&p%I8(m*q*w-;GnBYbxbo;&cnLZg}=JGB^B+%?T2p>mw%tpmXz15HGz zWp7!#;qKw!N0En8&;X6t8%WpO9E`0E;J(O|O;4|a-|>t_toymm$O@ad*=Djr9nY^k zp}cDDUD8=t-gjTfdc;i#0SKupxh0J7t{Mu>@x9(a8@ZoLc6WDW3^**?hwaJM_Un_Y znYyQsTIw+`co7jIx_N#~0hN+JWKO;eyq&+=5m%CCiyGUVOfY$buZNUhNqtGQ4EB5r zr{Lj{25%w>>;|;3D9&u<3rJQ^aBm+yBhT27fwnbxsi-`UN7inti%)s!o^rK?R~K21 zoiHpf1O$FgB1yl7yx0mVIr3r}7-@>U2W5=3cfd_%9v|Da?%o4J5-{5OyQhcKBm0 z2Y+nb`Oihoyl%~fY7zG!;g?4R(gxA{-^t>*)DoZBAO* znWdN1-O|xHPrs_;Qgqe8!tCdZ=GLCSrJ5%7R|`a>&8Coh{J*E2>=;K_&suUn_+hGv z#}0i7b3)=06Qdl6I2xNJ_3W82{1jwS9;rTV_q)_sg-cCq>it zQ6D$px;!|#Oau^Sw8iXp6)2g_yEX8J-U1+{eL}T(F~Qjb9m%@TG!rBz7{?8}FTymw zE{;a$2R>(&3j{9V3eMmfC4fXvBz`}Wu=FFz9e$d(roo%;i_k`TQTvhA%;whnUR!E; zhGYymrS=~S^WRS4uNccC##$tV1?69{TEqM(?+~xc%40jHt+Shaol}(7^8rzmU=-l< z=wgfwM@0aVm%{H`-Q?@f_<0|Qxj2$o6crL5A!(si={zrP!F_1`A&z^;ef4vH2dkCf zW#^O&^by!F)L!K=L!0TE-ah76aYCD%S=VT)4Yigs(D(F&lc)^_dMnb*#fI(>l& z<(GGIp+!V16xi(wjn|~xfxT`3TDcXuWXrUdLG=CV#IS%;!lnxht_}02s?>6BCC;qN zrq9@6gH!`6{R~M;2MnhbB{)9R0x^ezUR8#U67r$|{Uo_jSL;AW@z&0E+00MRgQ?4K zxo!O-$w&M7Sg`-ckP`wXvra%Va${(sEjF3?Jc8lhedMR=Sl+x8V$yuE5)v?{&z(rz z-)}XJ)G_>|MgU5lW9Nc%0p&x78&ImJkT?DC1o2AV#Ms_$Se%RLVjzs@D=N)qH%U!@ zPOGSvkS@9IJJyrXp7?$YI9^iEOFDn`m`&l1C-;%#m=;P%bPi^Z=zU=ItE?`SnmB+a^@O?m3a}XDEddG)P~C*4 zznZ+s)j;>j%RO(RJo6|wvaE!tSw6npHSRXp2xNxlCSf$3p;uI!oY=5@?-xPAc4s0z zy@(O-)eTQQEHu~7YYfs6RA@h)a*CjYCp#165?UB!bZV)RE~QOoXVw7#?$t>br2KOh z$<%~3nqq{)OfAwe3@wIt8&kgtlFNd$ug`~13(95Id- z@DT0f0oF%Gq7+QidRYW}U|R)Zt7w2j$0eQq?SxxHh~{ha6!0i$$5wPY*XLT^O#zb| zc&~(1v2#?y72{zrHyGR;++e0H+(j$+Ej0XT#Im`8p(3~(_?N}kuMl9K_>uIwrG*{45X(|r(1VYAYkX>} z0;To2kPsZ96OK>jfCQ0&H*%M_k zZ+|K1tvR@16BQN_^edAM-h_1Q!UAKFuV0JB?XqtW^5i$>`Adrn(GLaGt^YlWt-o6= zu$-=QagdLy%gZB?H1A!=%Pf$gD%aimuX+zWl`6-}%DXKhv3R?>=ZvT7b=+Cqzc)I9 z&A0>mR{{u1+nGKNm!f`8=gAdmKR$5ZO_}&hfffqBJv@{34whyoIQr#fq-~oMfcKZ zx5tGG+{H2Q`umTFi-#*7Yt{C@f-yPWZ-kxo4vTnDAikv%qWxfBJo{4iYWQxhGqX$) zZk3?V&x741qZ!OI53Lp-Se?l;|9o*BTnUH<95rMq)`+p!eT6pOCXxbpKTKAa`ih_C zY!@SMYE8})Zgf7BKb4;P69n?>fv64eSX$7w*GWpgZ3jhMY9^^DBpzyNkdMryJ~al! zJv6xHxI-!|spm7tzW78lHwp_=Hr`GD8GO+_oznPWkJRGlM*kzR^3de$uy;J2B9Ql< zM(&1QTWoEq!gKHdij#ivqr`=jKW0QYwCwO)`}dXCxHTQS;ut6NB)0BPxj#5~$MWY- zKL7G?_2Jr8yl?%FG&2xS*LtPhMz#YsF2o&(;fLb(a?q zgW3?8YbVbVY}id(CkWb6vZuCqF<@sX6~WHVQ9k)1fYpvrW}q_JQzi(fB)MTMZvcTR(M#WoGx%llmCf6yT_o4>i<8` zQZeX7l&r6Z&wEv%5MzV89@+${ug-Flxe=N%mk7q$qiE(j@0>~hZh9`>eOPo!gd(|F zr)Tg7oX0xHXMyjQJ{lI#Iv&7Ji1Tp*4X*;)*O&vW*JLeS1cNm}p}|@>W0_|V36w%0 znoXR(Z!a7OS3=_LYBjS>QUr3xvy%er*wzq}aoB#KxRqkWQJ+kpaQvV(1Z3Lnx=Dni zMRrra0iPDk=hTN>HiG~O$H$X)&Mx(U2Ifas1mm0I zor~Ot93&Ye7GHy4vef2eBFIK*nyn%MH)RfOeDbkvZ#LD=xHHz>+2J-&SD&k42d89T zFk;C09MUkjC&aRMSHX|cR1WJI>$*hUi&4Ba?0XJ1)(yko5}Aq8vRZcQov{E&tCCps z-EiOV#BInr@%CyZ0x%EoszQOQc5Hwx+PsuH1-!{&ustxPN(OhvW}5uKF0zd_>d5E; zKB=9$6(hAAWm7nKz15@^+kJpnfCeF`?&I%niJE@bxhA%0)@>@uTv!sVpUe-_V~l-* z-GvFD%h$K$hx>z+Rq&y_rOMM%-9#J3uq+MFdu#0~zq;WN29G2?6Qh-Ttv@6WA&o!zh(gt+r^8B+Vl@@8Ca*w{V| z%JHsVXF31LtYf@uv4XMqx74zmD~wGf$S>I{;V$ef+$^iGLOu<>aeeUoKE)2$t$+BP z73Z9LC$i$2et66Q84h2F4@J2(V8GwPP80dx+l~*#EAC?Q&0OtyTIm!BBzzSe-u)q4 z!a{T1dRaMlmpql8-1K^57`f2zvnsY<;mGT}ypni?s*N-|Q3$hhHK}CDj)=YlwOGT| z%B=a1P}Kb~TLd<)7&=pEs+J`epBpm|-(^JwD-c?sezvS86)k56Mx9e#`yYl>uq}>u z_T?m={CO3f0kN)cgWFcR_|2W)%R{k%Mg}7Lza^(-$?YZ<`XZPM#nRH>Uqtzw6%aVp zvGi9FP;F!^_2g~;e<$3ywk-v{cFckjJEqvgmYI>##IDxKQ#181vRGm7e}M!wRVtaH z4wDjCSbs%ckpr3SS*IcOY}2>F9^E%k9TBHXCh)FS0+$_$Stsux1FOD5t7K;0;y0^T zKij%#XUGQyGFf6M!Q4;2J)Zn5pxAL+#H?)zjD#^f`ii^mzpw7E^r?L+ePrtcI#c;u zV^a4AT~I`^D!}=g7OV0+MAo=)>Liic`ddL4_0v9OQY#ee^S@@?#R-GPavpTmTxVx0 zTMjH-=cV-thk@HQ;YCj7G`=%f@!VY|b3z}b9A`);dIaL`9so-NWv?_kPM>(sfgL)& z51MSz6>Hb?_0Q_3F50E&Rua1{pfsBHbIfXq7X|5p7yh9D>^tjvKdp~a8vS%}okEy& zQsW_e1nNI}VEZ#}S7_Q`it0B(->?4#)iESqmI9AeJptyLQP8jztRK=fy>Adn^7@H>U3Mg8xk!sP8+ z!(%>o#ubtHM?VU{@!`X>jMB@H*UGACg+Vj6F|uTV_HHR5R@Z=O@4WZ)*3?r9!=snd zy?lowypE)LrsP<2w`kN%^w^ZnYLEf>K0Qgf)2kMv;hRJusJ|Jnin>{7U0aKC;`hJezeoJ6>N-{~n+v2Rt_-BP$x5%(VsT~C& z3>n-D_xdvndOvu(k9{lup;mQBR>y5#2gFpG(jrK)b&bCd4Q zmiuPJ*dtBl#YrejUs@_MK6#Fyt;FN4j8gdwi{bf@5;QRDq@!8|5&j4+_n%hNM{q>% zNtl1o`rI_ z1x^oyS2Be(+XPYLC-n-LTDH7%?p@}}0f+OT`ySuqv@#DEa7y-@YsXa$=EW)T(BJC! zX9r<(la4D{J3f?D)DU_u^%~w&)gKd{|75kOE8+9iFFDb5PSG8Q!xFwY9rQ4y9HEfL z1jSIu&d$z-WAO|{ZNDCq9Ut9l6G47N1FcM7sscLZd(|F(og{GFzYqX-sI{P-y#&)Xd z2$^t?YCEgj^3JbrQdnuP!s&cU-|w%!K;=%as~QYczSg7?M-E;wZw3)WgB(fqVFP*@ z%#89@lckoCyZ<(ZHyS;kE3001hL{D`+{{D9zLnzic5P4JrN4^W*6OhSviO z=kF5d*5e>wYKz_i^%Mb;hGL4MkqW^*0IOCQocY zn#R~s@W4zY03fj39`OU3x{*mFNwPJU`OtIoYVAnmkL$MbJdV(`kAg-84n_oFa*Qx83lZfjuW+TFXNKX}Eo);<9h z@?TSu4i)Vjz8+@4eHqsvmv@VfB@Y7Xc<47=h#6T11rr07tM~- zM-QC`P!wNC;M`*&OUYK8IMKMId1P&m&el-;hw}9yWwzv3286SIzDhDejnWFumjS=irWA zyt{3Aw;Wyze;q;~*M*qQ?h$=m_j>eNaR(XOjNCdB8Cqb`XY{B>*7Va2h56IX)K>WW zsxERmG7By|6s-5E)V72t&uMniMB0b^P~<_-b5YsYx<%S$~Wk z{qMVBHzdiKBvMpPGG#ktfaQ+rw2EypXJhEy$HJZr+GC;CvCwWc@x5F>6qdF>J|0Jz zSt?u{ybwY+H~#2$4u9=t>`9;n>)P1ldb);>cJsLu%_C% zr^^R#?K2&_i_%vD{_sN{9`Ax-H~-0%>;NDx5O8{}fSO9~)LEBx2k%5x(`cSSvg85s z0~`og2{Q-lsghT@i;WZA8j~N`gx2#$@GjYBFZIvLOZ9{NM!5)UYL1NL6z0^8BnG36 zGZVe2Avtt8QENh)Mb&oeH1)WW2{cs7DKcN0g{vI{UIX_G^lL9To=y!tohE2GzRoh* zhi2zK&0B@q!}Rqe>d9aV8bp~W><>5O{C_dMHs8~v<0Hl$&5H}p&p)>wP-D05!y}9l zM*LZj<#q-(tbeh+hyeAi)X!?@{250jjLH#xi|XzjQ~$sPnZHO*G^X6->(dCIf8O!< zyJK;qwqbW^ldfkp5Tjj->*B|56xySL8d2_uHRMtLCLI}-WxGUFY~+7x$zn8OqDIlg zid-6_S6=j1*ETp=tZlKEv|f4gwe>Duc}&zgKgTPf8^9OHfnJpp*x7PcXWii+3Lb`3weP>f&j5=V5kkHm(5WF?^DFZ!k2&t*g*<7Rx&w8)Vhvy`YhFLle7p81*JHjQ1>*a!(1 z-kT*#PaD93y$%akneEO!gzHOl2v_fo$HygFI#FPCPl#D)NS58RnyG5dy}XUbbpe%9 zEiJZk`+ys(NIsE7L59Gw=AO5VJg#?eSjT5xPJTj{I#Wf3a2dF$ok${~s=mqGeCiN< z(6z}YRP#pLbYUdn>+~N?vtwm+w%rV-4@K*`Ous1qvMFMrC48l+eggfSW+>L#*9;!- z=|YPzxTvd(9rVFoBP-Qt3?8;vA?ekpov`irF2~I{&|+G?oVg#HV>_HXXj z5|@aOtDjcPI!W%rE}Ktzr^A!`)^Y;1R!!&gEN%GEZx*~H$wV<}^eV+(?fe=j?~)sO z2>1~9DTQoLAB=RR*X;Zd$qM*z<(vaWbTuazvM36%{(Alj>D6_GwnR$&(8uDPvGBugN6*C`&(aN={BbVqvV_*k{p2 zB9D>RSu!R=Rr`$h3ZvcH689$n(q0XMzG3P;{ELLaU1GkyK;P=?rT~Q@#jnvW-x|>5 zphzEc`4KCRK^*r`{JcLY|BG3v?Hgae$E5oCF_-=>g-fdX^f5OrPQ8i8r>12j4RMx!pcU*7yTyf`EAQXR2KHHz(Vym56@o@lY3Stj9UXx1*+YIUB0< z?J#dYySA5hN?%4t6lS73cJ!LY8;n27o#~HhiquH&YRhARDPFNXDXnj(1nDD5k6&)9+2(-~${2Zc8bmM31ya837UjnzOT$%5S*8$od)v=8p-(v+| zxu=aen4TUV3_FW5?OwzWjfy@mDpA%0IRTvT2iB(+j_gpH&Q*Q471DXb`uJABw9rRk z`@=fH!6eR}%riUw=%J*=FmXEd!TqP^EZgw%woBI?LS0O2DiqBzTsVm}xGr7i&(R(B6Dw5vL$tl4 zw5qQa<<-cqcy(mNj2@Z4T*9VMiR){h*G;}^T~+rduhB@H-D#y4be+yn20e~*D8KF7 zyMXa@iP_7qPBhwB|8D={eQtl&kFHG~UkVD#dq$(amvyWK|KO57(G88`AFyQUIf=xuYX#N}t4e`vGo; ztU-smkg#jaH6p4gwKDnqT~g(JnXlpEdl$PbrKa4)GscdYS!iFD&D=mfMH2KAkJ${Kwg1ypmCsKczBni68g0l1xwJTEipBBZrp!qQ!|6&Qe;AlG4s^sn(dD!r7F{xQNpMe6WT<##T+!oSM#Y zsw?~a*#NhTwWvi^CMYIbAlJsPMA7{-L`MSxQU3o*)>&Ytja-Gol^@}DAqpLb_3ny* zty8O>K3}TtEu$C;sGp2K6U}t1<7;Rq96p*A+she0VcHsiiA9BFb&lxNcu$kwIIfiC4$r%!)}g^f^u@Dc#kN$ZOtWJ>B_-lG2?5WbtS7$AOjyN>RV92!;x zWlXqbTZUOmb8CJ{aTiaI4s)VLn}zb@u1huBo>w|jS#lxfC_uCry<#KCnN#q}-m#j$ znb3uP0tSn;3+8ZkkK|66#JA!u^<_?%T0ekT6pKWFmV2A0EFf|74spN@Ie<_#AzKSx zVOUIR;quiK(-Oer)dx$Dff*fdq-~Y%-Mvl1>-$>J&0-5jmHM}P%+@FDG6@kh>_2}% zyUYfyuOEGCw`&`qMNOZU6Lw6~oup`GJLss6;A1zeOk8F;pJ@4@*-=19)6{ERMi-Fv zb=Ot(XEy%i0U(0y|LJ09Lu)(_zOzNJ6U8@KT!&F3Jjo=ZAWPV&iobg8ss&pNA+Y)b z(AC{(9i|676@Et7ONAS5zR2MG{Wb8O0}kq?eoC`P50c4kS6hb_)R4qI`H|OHFyMyq ztK*d6y*7WO+Xt3heF^U-PA-PO^97b;(*Po(elf&)d+779lee8zmVNW4mXWY1$Tjfw zY-6I0NebE`?KoRh;E<4CanXCB6T615URnvrNw{^o_J8&o>C>e+MUoUg zIAJn2Z$L^R-djN8YI9>(wOUyJhXQxkIjaQ$3Ga_~XR5D)mgPAMdOQk#m%%S5P-gS! z<6l@eCz-@5#|@3HawQ!Zg6#J8L%5yqM%{$cGG=E}h9WSAI4&$Ho4iHM>M0>d)B#BE@Md`0pd{4gtG`P%%A?zS z8uiV|{%wiR0ym-07+wnn)BFYXlg0s;Elh{KNZRbfd&)d(RH9`ZpWOH&TRn%%)T@|3 zQkvU#n4G$INOl@Nn`9ZC@YK<}^4W;r^p*)4UXQC;BJt8P4dJYh$Im!AONxwWACbv= zX3f_v!qe{qP^yG`#}z@1DQS%(dq9Ak57;AiBloS`-KhIQsRbhf)CF&#j3o`hjI{1V zI{~xn!#btecLDD|f#E~jBcvy_bH{}!BscCAUCny*;IF89)u`$D8+JK(%LeTsetH)?MhV@*5#MIZ8I#uQ7>kxB$eSZ z0+d#6Uw;fb=R`TtsZ(SdHZIQtcn;h*)6LE*MN03df6e0$N5{uP>06Y8Y~;WPoYd^R zf#3srA2ad_N})){#GIvTx9;Z^l_tl^$&$tQQW`(dn%6J|t|?2q@P}6+r=ejv_I;1; zTUYna4!ZQ>;X_W;ucMEk|3{YKF^yT@oMU5g+=-;O~U8kyG4bKL zlS`?L1Mam#>^SAs)QuPyY_!~nr;TIJigP&d7@ZXH@L(TG$U&O5j%=ww`$`$+e?pE- z3VMv=nYaWNVpy&~(C;llVFe}KESImAU8*U!HpZ*Y%GNu#3QHiYc2VvV5Fm)OkOh|T zp@;;CT2aAtFYx%r#CK2jWee*KXw!69?S8ofT^a&3u}rX?agE3^>~fJS z=dsiLHX)WBmeBi3Q2!oEs+C)ZWOojxqApg8mbS7IlHGpiIekI2+j z5)YLr@Oby(+jnoM{Anb8-1@dwMo3(fJuQZ;ucKkrU7Qh~bd=xSU?E6hD82`sm=x@r zUHtl}h%YaXo2?%vv42CNmDe;GTv5|&Eyo^J{ejn9lW-t6JCCK{W$Wo$F>yz-F3iY0 zn>D^N+ucOj_k2a!rM&*m?lVs=@M5+mpjjW-v+;h*iG%A7cImFDE)3$?h$baVVxr9v z@a~^9Z;5l7b)|${5k1^fNx&{X07C1(S>6%MtpdN0fOm^hdiMVzQnx|#wiKd7>Htq3 zN~6};-S@9&i>*NvxcmQSxc!;CE2-$mHDau@H@G?lqI{jLM}u; z``?|>6tkPkH={H9R785RkP?Y;9mBhj5nUn(c0x*`HafN`Cb%c|b(g?{XTP``4hJ=r zsWicy#0PZuA018S?B48+syRvhrLR1zYk^6R@yIpoLxlQ zwPzV*Y9V2&B06?`T$3-j{-Eql8$zv6c z=VB@j6Hs0eaz01?kWSeik1~62(5s94ZTD66!t1>CNC+iRulX&7yzBIqW}OW!8!Ih~ zl$Q+a2lj9wXs#Uxlqh%-f3MtG#c$MpkR_ zApI*25Vz_wYFwKM&TkT+nm~t#)hOxUrI#?hkBhl$_jj}xwc4*#-L-T0*J=6kpasrunA}ik+#LIh<;43WkC5`% z^(E)CXP)T^E>5ME6~AI`6RAFreIcY`T5AfqyocXBL}i8#6(vc_lStA6~f5bDoR;* zMy%@aD09A4SisqhVV7Hi8!Juwbc%89h{ZWMd5xKePM>=-SW4Iab*a$MtE|YyFtMw7 z3xa1a8;~ZH_L`g12?umWYGMV4>dnqfl}@zg^!Hz%ORUi(O`bPynr*%fG~>Dw+*{P( zv%{o_o|-zo1Kr;S5b1P_wUNu481aHvx3?I4G88EMoUwNz0__&Mis8Trz{}gB15@e} ztVVahWK-M@6@CHCel-eHcG0t>zf1tc!HH5y13uf!p!b3tR>EO|kwHhLP79FgGljHtjQ4`T&kz+=t zuUvXht;*>(C0!JWFLNqvrUJ6Zbk)#Rb__-g3*jc5zZceAZ@6c+2_qXtRRvlFHQ&^0 zOOd_wMJb3Io?6P-s;#utV0@D1SuY#I*Y9)UZR2A9>W)cc<9UaV$1`f+);bSCxl>R- z@Jt6RVrzmu>!29ugf_jKRwT1`c2m7RNa=jbgr6cG;Fd4To!^XIRkv*G8_Hve8go6H z9l|^&1Kbs!m3ux+YSSKSX_$+{XeKSZgVd`~WC<$x@#b6Cl;n*Z8wSQMpaN);8S_v*+1Iy_T z3Kui6xJ}d2VA_pe(jJmA5ij5UW^&z|dl}B}PHv)y(BEBo#&8{euV}82gAY?w=+tk| z)?PHkbirNLnv})`x{H@R`z7L!*jM5NPuw2eAEKX!gsn<~7S4M9)wRV(-zxT$=QH^x zeRmB8C_8Y8iHAJ^dkMqirD#pt==HmbLmDlNPwkL2Cx^2Rr(}38)DSO`_eDHv3dM0~ z20_XEE$-!^k6D|u!;Y=ZbzM|fn)+87e+a*ha74jTA^megO9&|{=pWY7VxMQpoEvtx zj^Q%?>h*U>>$_q(I_YU5fR>zZ3md7?pzzNl@T|j}*^lD{T-bl?`Fa~kud4<9XG-{l zcve%%u;hA1lcA5;v|jeB3P%SS_NQX?oU_M-dd11|*Rz9bgZ%uwXUku2=3J5Je$q%9 z$0vO9#shZuc`$S0hyN>sb4lB~o5btvF%kakUEd>&_n*lg%_Dr-R9lfQQUYcD0=vxD z1FuRS?~*{FazEJ3Z{=EX>V%yZm*4(BBs}@$RMcb4?Bp8QMIr6&`VWi)*d#Q!kLwWw zG?(}@JoyNEMZ@?gPqZ!^)``og5L(aMQVTTaT1Gm6*b?EB)o45MedEwM_la%9LhiZT zY4KW1x@l?{S~h)?w+|c6~$=xo-mvZD>j9dT@w_@A#W&@vSPb+|?i|4-yUrZq& zhvBC1g`qV<<_^0(aTH=g#tamq_GO?P?mJXpcDJTR4A3z`PKYP)=}qqXA~{KEWpoeG zdm54`*^${ksqG4Bijt?Gr|bnE#~DHl6A_X4vn_j;0j0IGqlU58!LGtH!X$s)kQ(iBxtP((IBCy{@U*^AP3Kmbbk)Rr%$;8Z^II?7KF*M^h2_qV)1 zHEzP6V&X^%J{(PD;Upd$tj61J@$#8WxcnhR;I2f%%C5p&cA*=)JW);&&f)7y5(^Gd zaAltcR?UDGT#dG-D$2sG#Et9k`KX{hMWQnG`ujvK{17}8N({}9g9lh=na@+tdxi$KTL%A)c9HM>=+V7pgVvOXE5A z$aN|^37n?T@bc_4N{UA>mIgkkoTxAJ&5vK)GjerSCT|P~{dJZj|I}sTz==i*nBAfE z9FBTDwCVMllP@eKs<~QNFywpPZMc^*tcY0kj`-F?Z-zEm#|VuL7LHeaJbsjaoyf84 z$FED@{O}7+R$hS&d2*R$64&%3MVu#Ho-p-5zxUJ|uegR&z`mLtiLP=hNkyh37qidA% z(@!?GQ!vu1RSWM|U4pSZJul;c+V9M0B#KT~Gx&jBWw{PWlQXj9Usu3s6w_|22A+hC zSjc{|q@k835nZ?9iSGj$1UL2+y?-wSZG%F5mD-x@Xl^wm_#??ms^4KDf-I(L{niSL z(1$mgCJtW3r%vhb=UfC=d!@pzEenIg7-+RQY;xk?QZJ1otNltN)8jHsyr0Xj8E|2i zuTJ!G;i6taH#4Ukky3Ko#LW6>`od^){`5&C;@)srvGgmq>KUnB-~6m@&$VNAqE9B9 zc4af#L{P9kFA1H+nEK>r|PoptUisXW0Zd_`%y z?x$Yw+KEfwoR_RF+>Sy_ScR?=u)eDrNhW@MM+WF$@0c0xSfiDf5Qi;F?48qMd{uqY z?9@S{?zX(lq2W=AYL^kGh!1P}ktcT#H=d#r8>&e`fS*5a076kXP?_g_V?4|hzU_x1 zu$k7`c^xAw3galEU~@~$xs{FBhd8s)-j+@;oSL48M`)y zFG^1<3aC8&yQ%3jO%G2Ncp>m{jDOjH364SaMzGKI3`8SFa%|x14a*?RN ziSXkPiZ2nbf9I+2~u0!{JBqvW1O;{B#U@;v6>@G>1|HQf=L3sQd1Nu zE@qv+`@IuAm8RrMa(bF9-Sm3?8EYX#+gjd^TD_Anl(872ee=y;QD$gWgQR}qLNgj| z20iMvy;(j}T%YBw&&%)T1E{%s)n%<+ZQfIsm{Gv*p>7CrO|1nL1?{)bD$QHBeEj~q z)#6?_LvwqwlP10z8vuarjed5NC(NGD8w_prcnf6Pw(TZyIwr|C2h;MGZ{AAE^ZHLl z+Z1%Ws9W%Ng3zz)U6b{?H@HwIK*F2f)aS48@6QV7SyZ%5)+Ft-=XJfDTVB6v zT2^7$;!i8b@h=fxVpHyQ%z?LO_%};Pe4jI%QOgdCdn+ej+di|6=4NtKHs*4sZsO&q zkv?{{L0a0bm(z{2g%8{wT<0ZQ$0Kk%( zXYlqf*~;ER>=L=nV@D~$cG}oQNclS9fw48&CAF0fks$lK4y=(K$q%MImwFq?J(ntp z0XO1+0Kt#{3*3>M)>77tE##`dvmj!1Sc)4cv)#8W?XB5Czl9`+AY^P?K$_n(xlwFx zf7_|-k1gU37aLDpG`@dJ1(f#mnMO(CEj=6+Rf3W*+3r;)ma+^{ZZ1hQ1#S{$8xe>( z^BNIbq1)%arJ~h8IcpKkKRIhF2vH=x0UAeFcIJL_`?dHDLH!|dgD0Irwbkw1MbP?v zSMd{DiAaVtZ;>3L(FF76|MY9Chr>@F9AUFxL^dKJNJz-S^-I%$JhiNdkgnSer62ub z{}&Iy&ou>h5lDlN9Y#b^CH}yF>;OKykos1pY?Ty(jbPt_XCNx#tEbRPk_yYBSo`0# z+1OkB6y~G;-J4Bu+E7jAK|@gd-~D1JG4uRyznI3mvRZKlu!{9pt@C#D4q4>*fsOWG zKYjn%m~WYCuY3fG^7Ilyc^v;uQBU78trfY6^7+dZ3EUQIk|Tdo)$xZvEfSQW-fv4? z%9MUgt7@bg?LxCoOMb>pxSSDU3J7&uF;c4M{5Cr3^2K+Q{8%D>nC zQ-&{Liw1J5NLqH~wgDBb%EQDfvw>Hk4fie|Ke&!+Qt+ycKQFhB2z5DW+4DZhGsHNd z+efWt)NqWqa6*EidwM$oYc={{RhR95^1xcoSl#?}MR75lD@NV3gQ;udsGmfISj~ec z3I7dbWNcyMlq8^P8jHf6+835;Jn4u%kVTg*%C8`uT?+S0Ye&)^*V&K4ZrN@|U*E~} zi>+$sbIrYYjXT;_s#Tjpv6~ME3QA(~K?fRg2V-R#Z)(oI{wBWHJX;~$itMiFQAaz0 zA|5@uvE4uoVlF$n3{fh{CS~X!0afd&aN&z>`rOv}F{nqBV%d^y<(Qh_@Yiv36l5b! zkPCwW;FkLus(wKw4mR=|_V9DA)_6nDeTix&2;U|NJ1YOPq*%HJq3b=%o?Li%WUa(y z9D|tcVvQ5iB%n9oXXcznsJvekhC@4+LpT5&sLb$i2A16pN?mp)Ul9Z`5(@~Hherk9 z?=5HoYgp;D63y(xd{*3LF5|y33pEz|ycc^19O7FY4SIHJcN})8tamB+y_&zXI7!6n zhq~&9;`4>y?&}uL_v%j9UyEsYm@?H=a5)GBQ_Aj&YcwUuMRG^qH(q(&e8_R^xV8X5 zZWA}HyykBidjF+F)|sB#B;j4*cSeFwnw>kG-Fl&!xN{vp6@R^sD~?j>mJ+dg?&56t z%kHMS?GZMM$$isS6;L>JJ>qpnkFRt7oohcjYG(!v^C_y+-YjJ1XFqO4t+3nHEQ-J{ zE*_@ib1f#NRDepU0XAybgc$xhw1j^<*|$20WYE7?-wue4AM32wQi(TCyK5j&8`?TQ z-%}ko_xpwD(SjA~LZhnN=k3bwM= znbcF5%R#Ppe$@qDy#(%=H%boa4{ejka|=k0GW6^&X!G((_@w!+&i-;DkxpWrgUm+F z1l9UU9i5t*irAt^v1sVEp2aGRy+_RBT?Vpyu9p@}j^20tJCMt`)TONbeL&U8b-L9@ ziAWmt+N%9|B7*+1?XTVA$%oH;kJAgA2*{xoJ%8X(cEQ%pZf6T<|K=&V14ljw$#;CS z@OK9~nM-S#%9p*DFSX`G;@?COf#}Z)_>Sp4Q+lug*#L9sR7d9i(n`<4 z$cmb3P65!;16quyQUqWQ>L*+;xI{eWti=X2@<|ZvCDh>wFIMTcP)uhapXH%zs1Cjl zPwf4!nmO#@M}wo>lQPFSmAQOI-sO?a@Nt~e7Y9K5`i&1)UedONpku~E>ul>;VA>VU zTk>y6Mv%gPQt&BQGhVA8=)aWk^2$elbh~+zp|#hXj<>9%+H%L?Av9i;E`*AQ;i#>k z7M6?k7IjYYgeF;y(_9jL=EC!aL%1IPZZ+_G9y)5iw-(MEDw~XE#^QQ%2IuIHsf?A4 zO+CpM5U{vn(mv=rVcM?m2EiscffVEbdw#XHt;R8!C+K*8Gw$E2-!dAnd z4gb91R~Ll%FHqjk{m%oo!v|pN#g&6oztfsbAmJGudnl+MnWR6N45Jj_n14w42mz=; zg)lVImBMX2`6Gcl7T)NQ)#%}bvbC8Ms~^8$mqOrV*oltgwkuoF~`@m^!yOZt5e? zMpIYX+^aOWdCS2lPADl2<^n6X3imr%G_@l8Pyh-jA0oN^A7Qm&u&}BLRiiD(LV~2| z(;AE4!aR=2*;p4SQOk{@11_h*q8%QnA5t*f#~*9JLbhXE8Az0ZQOl866KUoN zyA9Xph9QeL!UbO_Lv01VT~J{f@=-4;=>m1)9+!JJ>?#_<1Oy}824+KpWl=}9IhQ-) zh4+`>y*Efn_XS>CXBkOXS=vSiiRA0Z@ll(hCW)#E; zA7~}fBD6Ny2QNW^u+Jif6LyTtn;>M#D8CyEby=eyE2%d%hZ^+#R?WH+;u2E>vn#wi z_*}8Qa2;zqgJOB#5}JF0zVY4Msp<=8%fZ;3Kk_POf%N0ktZVc`Z(B2JpDl+#{+h9s z(d1}S3o}=Eg$dTkkZyF%ZaANnX43=er}a6SU%w^k|Ni(fA6^Uf{}h+BS76DX)4^1I zHW^(r?38Ngz4GRr>oPDz&wgBy?e0d%mHQ%vvys{&*i3&=udBm_#^XDOTvbec=@EAj z;HhtieILN>a2QiV6s5=Rxf6Ac_Ns*k9YxCc8XQ0GP;m(m;X8Yi>#;WQ55^^^`{PDz zgp%hMI)3nBU+?JXOj$fL%+fYW;oXnkvbOf~S!Uq&X;X0x%3QO6aEU;Fux<|7;Ufr8 zqWzldviAC-F!2FZw`Ezk?h45|d&_QkO?+tF(ZyMf75>{b_E+_WwmLJmi^6O|J=>O5 zIw6vZ|J)}Hq%2d8a^MWi#kbS}!IhDb0pmUzb7RmIZW;M?&5G zKK&m)D_N+KdzQUQUBl+*#mW}TiYq`O`iw6lWWnmknDzqiZXL1bCmpB>vje1{H&pkf zB}y+OJm0D9@4gvTCkuOZ*;Gc4LaVO-8V{3hFv=8yYt*Rbn>E*&D`z+za~31`+~D$a zpyL$6{pus`ct0OIgm{4D>RGax%2P$vGis3spqlm&cv=At1r8A2jW(hJ!bZg56ecoE zuJvjXe+=$0J9JQ(_)v*EMHL+G=O|p;#YULk58yzdN_LlT$YR*N$A{9?zQ+fT(GO(B z2=AMxc&dAprl+YFNRiS{kfnhIPny^ z0&H>b%*Lw<=b>6C{2>WO%KC%+Fwkb?knFf2M<*#M6sjp<7uEqJD;M?N^L(gt!%Yc+ z*MJD0sED2x7%^7l7{m<^J7IlZgA)?m316pwe3bx088<4l^JWX^( zN8x~jOQ%%W)nyhNV+&oEn})QykOKFx8ab>rsE0c^45Kfa+Rk|~0;HF)7Z&tiY=<+f zc69OcD4xNj&{cdT+m1k6t*^7+Y>(!<9=EMXQ+;`G_D;!6JN0wib?QEYcCu55i5`C_ zNgjVF1m`s4(a$0$9T|2_oXk+{4$j}!Va;XBIhTZsGEOue&0irHQJYB)@DfhZkqw*N zOf3BPVX-!eeG`9SrKWv`=k3P@!StRWvI>7Y-FsYgBE`x7F{;aacA!)H{7OM$*^<5N zBd8V!p^I(>l=+QiRA9u-mc-aCKuo+=kv!F8ttoL%X}^%lGG74YR@xE3zNKfdVwnZD z=4vaFfIIn*jb3%E4WlK#Ot&vT`lp?*Ry<~l+*PiBvTx(gCr$aI>SY&hY>`hT&X= z0O#ss{PACJb^cq=|F@j4Ac{7!nHS)x*(m#Y8r%lj3yyfgExKYb1QrNExPu^wJ6!Qub-lh%O%UZiHx6Sx`NFzSPQ>{NB^i+ev=di#85G)cm$Iu`~Wi zs5|56$Q*waQRT7R1Wo^&^4C zwy#HN+F{Ie_qz)V!3x8c=m@H#2mgeUOV{!fL4mg%{VcGMGJ#}HY2l1xBuJlj3HxxJud z;UGU1iAp^=k~gZ5RBO%|3oqA&U8`Q_PRiI@^W5BET#!8#gmL%rd>r8=8TO`c8Q~t+ z%00C>zv4G$tTAaYyXBi+0C>BCc}!G}Af;uZ)X@d=G=YCzbrVWWDc$PX|cmLqFx%lJY#w^BluWTsxqHT*jN{48ePjLd=>TLU|WSInG# z_qp+_VbMJ4UOHnIg)CLsQ^ji99s2{TsEcHgW5im^;dR;`pN!3S|Fg%3oV`t!CA8}? z+G&Bmom-#Td>bxC&|6N&=DYg#T?~@cZufvg@ZG1-mpC@aWQNKJp$+kyMICi-_T%tn z({eN^ga0U6bbxxrCRlEr$gV$+n1rB~!cTC=mA_R4DBXU7u|_1(ZNv2o3coq^=2>6| zf$MubWtoDP)SX$_cj-V2T=&nx?$Nn&3yGhr1p=L~v>Ma%3JwHa-vT!$!UZqP(khds zP~<}-!uOk$6?8$NsNWZE>Cu^gm3xL8Zy%ZdXJJNN0^Pf20_zc`BM!Z=gg$<}mN1yV zl8vF5Bwe~966oBo=rQXns+1{De&$Wa3jg}iEZ4)sNRJsiV_S>DtGc>FpPR<^X=E24 zYCG!rusmV#dwo8)E#uTqJ&2l&-%`d9=@ud6*v;1jP|7Y%(K~lm39u!z+>lG_F>;W3 z))$RfAx$wMkku!e9atkp50 zmY_q5Lny1!uAkPj&Fe0i+Oq5|4g7Jyr$SD&7ag*ypSUBdSL2Q|f8FZxC(PI9PBoguY@KS3%dzP}I6nCvAyc@URJ8z~%*4Yn@ zTI?a=s4Y~2*&rhvQmne=s|uaN^mi~}XTIT<*wcBroh+Sp{R#c+D{A;a%F68dV+ zy4gtchn#g=De*Gidi&Vs9E)G+eYWM{WS#rVI6>Ben07|X8-CjUHc9vK+1V@m)#X2p zVPM`G9th@{7B|Xlt(NCI`NFl01q1oxM&&L)>^^@THCW2-C@kPcB)nKUudWg36dkhL z+^DS%uK@bDUYMnTsD=L6g$4mxG}F)R{YR?zJI)=nQor`gPgvZe-sYuxha}7=T3m{= z(k4zmSDNXj8#-O7_`GUiWmVyK#o2;P#{-9BViXjHPUNouY0K4STAG2{^SK&()`Ms3 zfH)&i@R!sS>vE}4-~IZY{mN3(`)}||ucmqe04Pr#^mtgX;QfU8=cpIXQd9;zK&5Cu z2FC;TJt8=wci$rK939X>&1|c@eA|`L0w~gm7&8CmRoh;kY^O&bm|yp|n8uQ#VR9<}AF5gWf3PJ@w2rKux_$Irn}>6f zX{>d_m9|S&H8*(KedB>xm@vCvoqZ~Qwr!$B^{bH^$t#H@saR@3zZnkX6hE3s{0A+E zwoycV#9~ci~=w-aCjNF95)%-^P{IbDVjPT$hMl{40(M@1C9M&5)2 zv7QVp?haJimZxX3V!3W3ZD$~RS{IlzN5QxrtWT(|$!XiwQ3vOawRu`3y!;LBFR>|; zX|#gc<4g*ahbDB|NRZ14XJQKfiaL>$J2nds`6BN#8*4nFrPs=?(H`G_yWme_=DL?$ zJ)LPL#xAxN^?$0!o6h4==9EJXKcTByyWr`;sawvEN3^(pWyF5#;D+_$ z62>^YKIA`sN0*0$q)or0!n;3)OhG`A^3#;r`n{y=40%Z9W}VD-5A~H{<5+c=&-L$* z8z1rKBRM(VyYa;e$0FkCGNpy5<5F%-h3*O4V3GGe*4y@R^>7PMd5|&;!gKrfEtl z?nWjD52Vy;d|j?b7ueHYV$`o``39=P$C(1g*~VyJ>mjv~mJdhiMHS~4j*e^15s5!) zX{aC;Jv{i!vpydA%)efz31voJq3j%Xa>o4UqBlHxrbw9Bb+$EUwz5Sa0L6R4tb?E$9z`6;-vdmK+&Q&A`KxAz=xSsmE7 zG$!w9ls#TG=4#^T0@!9wrkiF5y=-x2;`KL&LgD)HJFN*{86Mt3(z?&#G_5gj(Kiis zc*NhM^P(WH@|ppI7GvX=w~Ot&6b(on$OTsq)FV`kOcuy%8n`He`K4g!MmfH~7ejeD zYz_;wD`Y-1>oYgD2Lva?FULC_Y zTULii@c#0IS(Etbp0VL~d$eu;NYAN@n~rC|Y@cpya4RLwj%f24q5Yg6Cr}!G!%k1n z{B>>>byow=11ET2YSLIjJ%uDHynj4^%Y?!{L(#pOS}?VP`bD_~;3n)sF2&CK@}4Rc z-c;Qy0(Oo~tyn@i1Z!b9TKDpSkuBBp8Z%>yG6%}TY?B=cMuzq!id=$DfWK;Mg-3E1 zPEuPk9Z^(`*HbdyQaBI-#@?YQ!dRc2-9iA$RY3U=x*OmZ(!A~HjMC4~Sb=tMCX-CH zI-0B*Tpe&0U{)uv?J$U+dYSa~Ws<=!t*UTPWYuOs>W z0%dUfyM1`qTn{XwYx6uA1Om;lcr0AC*u24Pw=~{xWR>5leLHl>4t?cFvmX4w$kI{K zV5}3V&T90Z-gfHteR5$>HGzO$=H8l~c6l5Ifzm18?)(z9uvr3xARqN_C zT~P}l%BHOB(fg>hwpkh_LxTCPhT5lFNZ&1Gf4W+lu2iwDc|ClA>mpv-rlr}aFI2Y}AD8h(d)qq1lR>OwGhiu0XIJSR6@91V38F`gr+U*s|^^LCG zB%;~a9gtH7wQ>1j)#pa7!%DCDm0qj$bI!Xtd9@QpeE9hcsO$5Gek3zh>NjVD4>ugL zS;NAXWr@|q^9RQ0{%#jy4wY`E)NX&JIE@CFV{(KXnKl^3G*lbW_jn z9t5||VsK2n(K2c7-oFidbdUilIPhOs0a}yzVpXtYlw!@}@$In^2NsQ*W<-)|8Es{g z+MmH-h3>Gi%?l5PwQ%HH<`S2=Y?pFO?h^FHu`n*~6)M@dSIlcs;1)OA?$G=ZSRwh5 zoVVK(=!g_sScW;E8-K4fqH4diJ%e+4L0|_EmP%cqfv{#=rmIh~=@pd+(4J*Th^M3FK`pz zREeMcm_%t1wYAGLn-yBkU%jlNYAm2}@h*=!`!frXRha?PNB?%qp6xsp+sMIB_m~P7 zoNNn}juvg#ZmURG99uPJM3s``mZ+xc9-3)sx^0QmN*+G~qOWL^wd%Wv%qYqce~Wks zklGSP_hB~^tE%nV^uWh{`cvv{kk+f16ms#lh3>knEi! zXk1%;LPvhNHBY}Mgl3wQ4Dz6AG%L;XJ-__`?1D#X_3dJ!On?M-ns8Kyyd6ZTf&6Su;tYxdMtFM?@HGHNy;_?{e0$> z6!Dgj1Hn8ma8vbpULLvI=o?m0AvA?phHI%#GPWdJlr08PJVMy0f+&mFIxqZ-Ui{wf zt@6y9QbYw|P4Fm-vX>}p)}6ylP0-L$PATTqTEaZmqRR0#-69ihDufMRr0ijew!S=K zb%oHXjW$&27`bDNUXmFod(-1h$;4`+gawjZyy5DxYeGaxCMv~vB_&iMczQ*a{eKVr z)a3qOV0tbk>qGojA$ZtEaP$AHCi`E&bZiB{m(w~x;3xXoEUo~sT*>+FD71_dV|4sR zX+K?X?Al(j*_q{y=I&azLX!-~T)v0vxd4^?pK7v@Epq+xG1c0DrYq#!O#}8;U%r>u%)D(rQ~Xq#QQq6 zY`YgluxCzbJ>@_4>%EGG3Ytv}32QRkjxP!dYG441TE7gQmlgKj<@(ccjL4O^?BjrR z1EE9NGIJ^wB|Wh<>~(yE*H=#D6`rD1hKuSxJ!#H%8Cf-zcJg4=7Li@0tlONT<_X!; zsV51XyKjY;o2Hwqox$&{7xxMV2*&QXati@en`(TEp0dHz;-YlROfz*)8*Bv;{Y3t-2)=t_5+f@ChE{7qDBN zi8_hUEzii`Uv7DnxZ>oYqNaXdxAN_0<(JOBx#W>v$G7o1Lhs2hFPw|sm+tADtxMm5 zIU=ULJ^0*u36&reBGT;jEgH9sdb#2959?^6^V8Q;Ywd~HR{8I#9dG=STf6)R!?u`+ zwK{(5ej?+_nJVJc{ew1V#~<>L=j-i5-C@q7xelaZZpX)!P{b!MT5kuWvhi{jRZU?h zrwTII1bF9tm-e>*OB7zjtbNCxu70%q;>M51b30Jt69ttUDM(s!BB%sVxsHnv!2R`n z_-s&@a{oFTUNpKYt1$53@D3wP!r4Jw?w2oj`Yx72M$H3?$>U*Pm()ZEDMqF4KBPx# zoK9;XuvuNpBexn!1hk^``a1R;{;*FPoy)Dmpcn<{53RoQb%#WZN4@c8aLyNMc?#$L z++K&+7q#yIsBP=n&9IWglQy<9Ew3`T{T)ww&T_-~>7|bKH91XxAK7q#1Nzp3FZtqp z&-!l7Tq|aIk$HlXugC1PJw9gL6r2stFL9yUbPjMMK3>|>ab+`Y`R0Ds#f^Yc&s_6o zOBa7GHUPJeFuD(LyMoE2EB>0<|{=8 zFI3)IGMBQIb~ig%%!>!353MU+}hVm)9wuRwf z$H&?$6wR}zstg>U@=KnWgjFM_wbadXg^HvOO^HwCC4Vld{Yk)ZbgUk@SpIjnx6!)? zUN0x3dn9~xR4o@mbS782?90I%TFw~l{Tk-;`&b$&n6qyMsXB#OS7%4>*RMisygGwkM8qQ?WYexiC;uiBqU^sqNey-iw>T2tk@>PYo7c6QojxCMEOJ3e7r{H z8A^icsABeh+5HU~B3Dc4T9w+l^-{$;A+b8%VvWuT4;`IYl(y*;Re96PnSof`*SFrc z$!F^bS>N$nF1$sqV+MbHK12EHK7ibnX_l%i;-tWHRJfvy@^Zs!M<5u@fBi5EW^THAlp^?W zHHX%3w*J9Wi!ffGm@Q0`^`&Hv>GAe#16NujE2ir(ZY@p-gxyjO@Fhb8+plR&lwP+1 z`n>&o_GwqwbP4t{;@nF124Ag=@EOOlpq}!)-25t?P@>6No*mdpKEW(qy}j}4_wpa8 zWO6^CJ(46um}yPtQ&F${2t+ki;7RK34U*aKy7+eQem>UGYhwxuylY31=;0H1P_@pzjVV|++?%Y0o`Gnuk$iMprt)+myIzyh)44B{FELUY z@qs6Oq&hcYu`xDBx2<&@uO$j|QPOO}YDKjWPQM(doQ_&eQTmr+iCt1SK$hdt->pVI zv{c7rXlUrDgCMVHYpWhG!7cYs_eaO9DvQFe=KRslA^n_87;ItlrMRXuQCgxLM0joG z1d;A{u#>6W9InK0hUE{^;m;IxQ<9B4q%833J)Eo6y zo`7O$y`(2gbw!=N*w(y}k4yzdlYzI4?5NMLNokuL<2_0JlU3f!EvVU+@#DjilRXu$ z)3B{zjJm>)t`0wj{@IwbZI>o$%wK!>PF_V8I5jcroc{BuI_}Y5@j+Nc9CvkO!~pn# zy=gI4>7V}^P202ZAjI< z_TD2aZi|h)9&~Qe`T8QvTM&R0&)!Y1X{Iyzg`FQp_O?m?&U?MD%V4^-t!0lnlOxuM zrGz_}&vT!SZ$WPM#KrPUrzs#_P!fHhlK!nrf*b9S2Kr&Rf+6$XAY9mUBY6SPv%?qs z_)v=4)Y8Vgv!4PpRH~84tgTg=l0-4t4<3}p2romD3iZ+{swgiIA> zh=@96h%#iGN~MX0^PEb?6vs>|Bo@h(45dQG206WDN~R=aKBh{sv5i~Q5t6cF8{T{M zJpcFqf8Xo9uG6{Bbx^X`UhDV$-uL~vvBNK!fU6pm`o?%i0AXK^(HVXBO$JLZSYecP z>v*yXcC>eG4yZBZjBFA-b6~$HFbDCagLh7C1S^!KpV)*x7%e?zGZ8UD_)G`XZ|A(mp@qhdu=}^M!50#Ds%l?Ae#DEt$Eii9*0$YNSVkopOvLM1z9V5L^#L z_qMm^?jLg4{e%dH$1hGtvf{4t} zdn&ktgv1i|4b|GrtK8MQcP_MR+!x{HX^5E)$(_6emA<-MmOn#jJxnXs@c{>pW4Pi`52fh9tAuL0`Mi zlZ&atiLf&cumlvQW1adDsnILcllGZVc|qB^ui*-jY#EsC@z?9%#W*O_1Uew?^OURm zq-Rrz>}y~nPW+pFKiS*7TJxfhTD}kL5DX6wk67^@s$`D3YqRkxQ<;;m1L689{Y$<5 zgCwBEge{tCQGOV?u`0XGmj~(k>}(Yf?)x$iFnpV^Eb*c)!A5HN02r@>`>T(uWxu@% z=ghOt#zK*H>opZe-zH?~Jtv3@Hr(K673E4PvGnTf{Z(d&cV=T_ymBe7tG|TO zLyRsqYEYAPUKMZ!xoK&y1}|xTL*~lWr%v5^_shbRZcq)Mm_VgY%=?^gJRaX4%@3Q; zOPc%68=|+x3p#jMDtHd%DLO^fkbLsv%O+luQGk$S9jx)pxY+Q?)7y zcan5+Tfd&nGjXVvY_4d~D!ReRsiB;AgERDSHlLlcnZ0sRzL2E496HFrgR95Kudg)W zE&?AMz_VTfmPWD~EYQH=tqppxaNudo3Qp7+v&wnWtC<_!g=L<3JHMW%yvuOv@g_fzr;tu096 zy@?)IK<|T5Z+ezal8| zlu}P&<}tQoZGe(9noZ%Du2;(;k*a`V!{}4#^TIlmNROs7iVI&!^}t44Q|!(gVx*ea zgT1XTi7f}W+=Qc)28TTti1~mGfUHP$ia$NnhkCd)#nnSp6h&R|{CX_K{kT-?qCdi&`z?dBvR9%LK=bqmJ*8pf zc-QsRXTx_}R;ew-*)#Ioa{gkd$q4?Vlb-c6NqIcqQbm{EKRWOUd+X?STCtrZTziPr z0Iaqz2h^W4g|g@iS?M2Mc92=QZ3UetW9+y3A|JRdxg}AcVfGFc8S&9@Odz(Cs3ut- zne#-DSuoWmn)3 znfnLrjdS@3Hrz*Tm4$!5kNY>63wnDVuj;|0foh8@>2IQaUnyLwUcwMV)}B}=b985lUuLJ> z)VbW5 z9|Mzwf-IIFbDN=JY<qVt<2vNv`mVL{;z4{<3`(1M)B!vC0sjQl!GI>j9#5@^ojo0& zyza+Q%a@~OkBUyO<)J!-5K+x}(D8a4hIJ^E-#=3{ra#Ch+sC|Yi!KO$h*%2~ry3f46In3F2zHQU~v%SI1+<*NR_zo-Ddf}0uKC4;) zfF4oD7iX6bZS|Ho70#waKw!DQ)cT^rW_5`wyMimAUPMF#WPMOyq;7!gvv zp>XVm$@r)#x{MOv;lb|8#Z$d)gUY@0`gDTMQ>lf=QV2&gR-_UF?g4e#t{#)>r?xa~ zBSbmnQi=B#joXqMap98g+;hB9rfz&$wa)rT@Lx>-DvS#QA;RK-b=)-Ww2MQ#i6XM7sB+|W=rl^` zUP+si|A=Gc;1rAi9?qMh3qUgW9JxDIGMc7`Z=^gZQDrrO_O>4Qx69BqN~e@Lsbs15 z8yV*peqcg(SzPp33QHitOGGL)zi{{C(8FI{l}qpEPiOFQ5&Thy2s8Z=QCghlKCb1^ z=%vX&n&)D0yP_JQHkn^qzY)G99RI1Ij8zbiEHM=x;_W{tAj)BM>+i^T$3(TF2>1B* z&i>}hV>-QCjE3sM%O&NUbMp3HHLZ3|#Gw~2;+aX%5MEJTUEn7$F1NRww{u4}BFWA5 z$o@ATvZL>6r=_i47}@N;MPA#gf%!Rh1o*mpPf#m^KtT-9R_D+%C_5L&>(!$zDiZz&rDGnLIL_TaUM z*Y@~aUA+GAY)@ncP+gMuw#zPEt)FI-KmDmLgOBpU0UL`MC?PBTXbi28z(caB-6LRa zvHpS+V7VwE^ZjmbeH{$~_)py09WXFT9yz+F>z+AEi z-T;9c3uaSXMmph^6zN)fWO*hxk-2(t&|`5qw`38X?ic5|VU@8(vZ}&M`mt4byU8XU zOnNWYbXrn`cr~Da`4zoJt$f~vMe;t2UXLiT3;yu?cV3U2Hg*vb=}a*Y8hR!G6l6B( zHT7CmLhZ}BmLyGTaU|vO5~WwWV0O3HO6Q{fI@l9oRPsjHYVP}*)lrAw>1#Tff!_*t z@?DCeJR-eLZ-07HMg=dhq}6e(U*Gt$3fiCt`TyYNk3$u+T)w_1}A2A+7U+9XNBI^!wtU-q6susPP420 z{0*$@05#`_!MhwJO<$AoODPU$QQ^u~HIBdtXi4vN>|?!b-uzPRCUHVFmR{}|TwZR4 z;%x~>5X@6k13-8{ox+#i0OcnLl%X7x>ioeQ?dvUN!{D);a-dmC!xlmUa0b-Xp>Yqa z@#(9(VD7)X{n8QD+9;0(;Cpif%}1`rBAPrrB8J<^$QEPXa0jU3AHrGLW_46 z8p&;l`|lJxb*e{N&+E!~sy$w!b4k%F!Ff%AA-r&m1GW@{EB;`sD$^Fpb(EG6d#TyJ zlBx%N(>7vB10_yE&2xyZ#oTt3y@rLmopb4FoFh|PR9?K|jYREEZ0g6Gj*N@Eakq{l zG9g<>p=Icdq4$l)Ot6zOF3u|}ZtEMAPnZ!ll|nr3-3CQ3`0}46^&Tig{gOGwU^v+M z1IVInBqCZkiH>&EFKx#VJ69zjG34r4rCr_V{89W&FQGV#5~BeLqk9sOc5HlznC}LEj|fICn4OaQadM>$?E| z7Woi2^dZoNHi5S%CuAmw1xd$~4KQwZ%bdnUpHX4rz-i0RZ)Cz}uO5_--ju{9 zMek7G6p1GP+#Q*q2G^xZ9;PyzhpwPz;gcGb%>vrye3SE*D$^-i3Hs!ZRR7+ zfbCZDn$PIhB>n##q&c{W#Q&b86J#3r6iA2Pb6)&onBu!)NcVpVpJ23wgqI+ z)>DlKJ%4$s1R+@G3CU{}U8K`#t@)^{ua92}{`l5_PT)tbG<~^Cva(LHSu=^<1-2iZ z(Z*=|4>2AeTXWrS5p0ui7k58C=G$u+wtIWol6UsX$pijRa`MZI*xF0=4AdYv9_i*P zTlrG`s2o4xSe-;9O4QM!j72G=9eQw!{H*IxkEgL)MAT%ZhE$Sx>#$9WZ4NbT6QGDC z3H7|(I1&}}!}+xq(orQeG|h%b&!)g8St?NWG%PYj$s)NDj`>)uMrT+vYwAUb>3LHB zXSe;!w@vyH?e*Hk&u~Y}0g0vIO@F0(kGBY)ez1Cf&Iw@!3AOQWpXyTvyHV%+nb({P z#c|v_<>U!|tO>X|<>TT^R5-+{6QvD{gp5p4H@5w~*sYCNj}*vupgy=)k}D67CJ3Ld zKN_pe6?4CTJ;w0R-#~u7YPCpybz*NhuHd$|@u2J{dcGSN8Q6HD&t+8Ak}wiQj#D}m zh=enRXnUc2-{4lE_lB3vrYa7?HG$-AuJbO#cUI!*mGD8GP#5IFQ60~SO2lsp^79E> zLyO6Ef_hq2WiX1d;>$t2etH#Sb zdvZ#C97Sc#1y{&+{asnek)E_hwP!eW9LgEPCS<1|(^Td5>{CaU=elWEG@KR|@>fDZ zvDX(Q!0v%zb>oH-b{`wt{)c0~fmnZ5YeXQ$q15slWB=!kR`~88iIR>?n{$J&cf-lX zVcnxD7pA#>PYSjF3F&EOHYC; z^okFu96oT5On1}Bly8A_s;Ad;ooNCFNKWaU4yBQTH0^-T;xFGtD`?A#vkC^=CX%Mv z=HV6o-TGK?*eI+AB9HFiL&gF;M>vl^E_A%h^T=r+_>q=;WUO!YGLNOxY+CWUg{TcS z?BsAui=@#a>ug)ca42x+3c0DZM6Aqc%@oNr9m3u{D&e9o3VnFE61euGqu)SBC;@2|zaqeZASWd{@rW#0_c)`bNxlm~QFH>j^bvfJq{ zf&Onp)PJjgYX~!-r@(ZgA0=RXf8u8{kd+~s4 z(e97nF65J+%NfOiP^2yU$!#a+EywptXY&-r%20I_pt7E*x#C{nIQF6M*5X;4s7oU} zY3pQU-t_xRwf8SSZOy16Uf)>Q%n=}77CyBB{98w9{ znY{34ma3aX86J1n5=R7{*DWA%jNMmiw%gpv*sVo=b((2rO~ZlamxtnsjhT6{;s9vI z$L|&Dc9ocR<(%Hqz73>h{+u#o80*dRp*Zy@=kCt*tXP<4dDS0FzxrrHW;ESWX=)3J z2edGo?N3OP?+-k1=P@Fo#wQ%0Vt{x{?BLsIwc(V4C?Oiky8}=%V5m?Yhm*@n<42Jq zte%SVG0O8FC-eNi2*&!=b`F0@Ek2i5+;m@H&eYYoc;FGReL~?6?`>$-E-smUv^USy zbJES+jy=4cF67ABh&A{Y^2DJ>7iGpiEHAzhSd=j%bt|wH5+aa-+xEDoyh!`-=ZGju zGkxcni&x-$gNRO8ST`a2Lm4K8&q$q7oWi=Fu+0%c@3%gz?^lvNUPcx;woPf13&-Jq zj7D(0T<{mq7|D>PJnJhhT%lKBq3qvj>E-KNNnR=(z6+Aqqh#odH*?0QvPBdI%fEfJ zNBkhuR0?*<#IWtHx<~wHW-cbx-LTLL`ynRMUgBVxyC!@)%15a$Sa1y^R?6{lixD@8 zIPM^~j%w~o@WvJ{o8f6bBZ<61=I@6^GJ!y5GeF1=On1U z&D|!%z`+R#QD}?n9@{1ZZKmTx*=3oBDAGZY$kNUFm-8;?7D8kjyG|_y7nuJ%c4hOU zJI^;A;cpf=?wol@oH()^SVvSH5hf1jynGVM0iVk5WK4b?K&qtV-k9G{1=&XG&z4Ec72Y#tu`5dHIMX|WD_sxn_nuO&9BDBd(~t0Xcqvvi+ks`3O&z-ab~B%M*}n-_F#3T;z2|bcxUL-boTqEm$GT}2MjpiRKd9yGYV7w% zH0(Q=CT8N~xB7~`*`c7Y|IBx@P$DH-)0wYly^(f?C!XX2-TWSq^FqMbTV1cNT@1R zXxMDs6|vy^Z*Y^ADID7fPuu$C7Yvj|3RgRib9hxpYip*HsEN?`ImdP@-U?h*Ntpna z3j0!hBKCK-Wn4UOHF!jg{m;{ca5P5T%ofi|Ozq#haQ)qd?@|JFA4IoWfd+N}M9EEm zJh_pxnQDFG%DDCVz8dt*8(6O|R9oNF6!u#=nl`6h1MHxOpQ7BQ=w|Om1%T3FRkuxP z?8W=9h4LRuy_!n)tamH^#v+$|EW3M{!&A(M@rS==@b*)>El6tH2wd|LB&(Bbff%;g zyGccOM5ym;^z2y0+<~<3HOeBH-zdhU&C&-O)G^D}Jdj;Dfv)gsFw_y6@t*1PKiTW2 zXEpv;!5U%myFCFO_UL!=aXf~cGu zXvCu)y#6uHUZdk}1Fs5L8e_ke?cGa1Ey)x1gWwcf$zhbd7U?Jn|EYw5v(|HG{fx2D zpjuCPGtNHvOMQh4qPMTo^SxbhVUNPH+XDi$G#OyXL0%>=`WLr?L%N@)SWJ}1t6i`Oxkgj?nSJuYI-%l`P}EB-9UMQ zNuY`Y2%LS0o$-SQsMl6iC66(8ib+5KfPcrRza>%4;TDSb7!2-%eVP;G;GoJpW7X&{oU(QZ$}Q8?xuW#9$XHbxxdy+DM3gwLkJ2Zt0;69Al~Bplwg zm#f|@A;F81!6AGv_(0;Lx^JK2%G@iW6Z}JupDx{j^#aB1an%|P27u8Lne)=!$x3N+ zx0DBK*R-xP5cQ88A17577jvom%FhGSRA`?fMxt;&62JHbVg3-nVk^PvE&^)Uj#_A? zEk0NF_VjJQLsg`3%y;fs`s<0%X}}SXdW?2MshWhE)sWiKyt&_370NNgC~s(FD*MLO zOvPt`O<6|xehOK46;-=B7XZ&KFV`nQ3C5R+;=DXugys~A-5+lF&yAP#120#`HrClJ z%%-0kb!&s4#s?{KCx9U^dtt|x%JTLt!wA$ehc9bq%T_##h|hRjXoeqxA{UMe3h>X|%^ z&Vry0rIntZ=lK+g?CohkA1ZtO^W~)V_;wrKoirE`L9KFk8 zv?4q9sJE@Jq{X2Wi@V6F{i@sk$A4V}UiEnR{Ex{+v3iWit&d$w7w_yn*Yv>pW?9;d zZ)@Wen)LKNZYNz~q&QlbByxO@j^g}Ayy!zA@E$t*XH}GBUE^uoV z5%$4~Mj1w&u(qKF`|Qoqoitu4;sC)H0$sRGGi`b0g<@LD+GQ7(37%Z8ct-2~?^uF` zedI9Z3%=fR7Ek82y!)};SKel8l(uCQMyS8Uo?@%l?u|9Ho zRP`K1teDhiZN1KgNt&P7Cl`$GA3q?F;*{Zd>brku=YfLKt@eFBL7$z~4@G~}!M~iG z54zny6ul`v8Es-XM7H`*kb{BAXp)|*>YO^=$)grneVv`1HS95Q_h(_ntEdluc5i$4 zNHp0|BWm_adzKlPn1H(vGit3scx21S%2Ku%BMRyM`1eOzQw-b+$9`1KbSQxsa#Ziv z*Y+@SpWKG#6f^F6#AUBxviBHHYKL8RU&(F}0(6o@p%$90k&1!v{xlu=bU%V>sda5-X|ju|(pP0#vw z5N)Ud8#5k74F0Q7<0%jR<8#@H=@f9)_Vu%#hGPpG02m$IikaLjGEKi8ymc)92rEPT za#>>9D5{o{E84Q($5{Al;C^GPytKah?Vvmy-^{ESZtha0ki+ z^cVO$C4gSc8>FbUT3~gufR;ouPF;7$fHlJsJX9}zN)U)5)i66n*xm&?p1jhmQMX#h)?NRR|>)I zPq!f(1YWDx&4~~wUwHEIW_AlnDG7f%;iam(!TS$8rZ_OP{pDm+ZmCz+lo3gG)^yHy z$dmKWgGU`59m79d5~_4HQ0N6@XFJF*1nRb^#R-Z|yrVW+`v1f2sU|nUIN+{tm{*2` znz#7U*XMdAi*r&aU(=-7cOGn+cX_Bh63|w6x%K^81gp=H&uKV;J=>rbp$M-8zvz20 zr0;frHF|6;Bro%wYH|p-_o- zMAd5NmW1YVzF4voY48(uhb@HvH&xMvH*|qIB`B_3frRyu+nERDO)&_%7e@Tax%=PNR*!8Myx8 zIq5_84RP9txQ2rd2ZAU3>W-C8IK0iN2;JR(@9x#>_n%p)enwxW9C;?rQ)*3z|Cez! z-R_Er-w*%3f0d|l1^xc5*Bu-0uUy^ZV9+x#EL0rFW_fxJZn5e-B2lk1{U*V-kT&p6 zrp;ETdvyM*AUHYm!-dUS#h=UeJ7KZF8-4Vn_F z;e$$W@S?a`?&OQ9OE~JcyseUtB^gYVKd(cSbN(<@la1(#JQ{dri$#ed3^)gxCV|HHyV#Ki8mGxGQE_{{pZ?IDst=rr3_wiA#=)OlxYBXFFZDP z@R$rTNip!d0{*W0v)0nj_hRoqxy|fk)ac~oCrGE3r|!w{s?1anbPF&!Zkr#FqB?>? z0RJhPzrR0CoBu`5=v~cD^;6%U-r4NXdp&0|!-yQ>)*9fBY*Ng-QB&|;hLzklLi}Q4 zM@26iZS8B4@ge(aqqQyD5Rt3_j`}casvuKZnMBl{jzN5<1(-1e93B>O58QQWMdR8& z+&#Pcx*e8m*9B1k(C@>_)AK+bTOru_zSE46rYB2Vrd~D!Jz?PKfyF9!n zCVFt|NlvWeSS#n-Byc8|`X)VlG~GW^ zfq)~ifr1p%9I_c`?>}tS9Z-n}*-eoO%$ud|sLfA>ZmDabN~N;al6xomde5dP<#Bvs z{~hA)CnCBmsDhnbF+9R+cok63-~v`}Q<6*0#}Q=XU2-bYkbGJWD*Ud<0gVup6>Tic z`)e6CB-HsG^nOTqVwu=tc;R~7F_%-V)#h85O|=YogB3Cr2erCg%eqa>uzBQ#ZCOZ2N;e4EIYy)zjk$wNG#I zMbt56^E_|Ya9}IgZtiXz>SO+t9iYH6r*wR#hxB!)gg*3$;vPaF=e|si-=JmL=^x%t zyy@7B_A+X|cJvPim(nxo(4)xpD~GyWS_w~7cv0n>7~Jk~R-V@4HCDO(>@O_>mVk=UkH!s;s%J$^8I zD`}=cn1dt;#KS*lKe5-SE#6dXnOto3IQk&ZBHQV)7xY^D0;CGbu_VQFYyE<`PXzhdrR`I8PXT-eHkUy#1kHUz~f^Spl`p3|(V01-8V*gD%Xl4Nj+1_Q*D%yO4jk z(Uq-q+r0SU9%7&M_$ds)hc2rttV}#b~5&u!VZ7e_01Mg zn*Y>L0B+6>tCN$PM~d?-b0vjm*3|L)a|XAZbzA#qyXh?iHShnvetE(^OFMNn4KG7G zLJ$FGVyH_H#b+lH*-4I~K_$&_T>tW9JBksn<EPUCkJk*vgT>Bv78UO<4vq)ZXS{}YFXUz&s9gu2UT z^lxz&{^>Y$f_o*2@IgEl;UvHn|98z9HN*x|AHe70BST$~exW)4KXf!2Da!C{uh#Kq z?)sT*xG1C{u7?r!;%O;Y!Czx6iFajni0`MU3fkC%4N%ytV7vy8J!C6S3Qsb7O^6=> z3mwIb;{q7+KaFrf7+;&Gl_NKXX4@+#ddTc`icz&1{xWV}t3|XbqM*E`U=6WJ?y4f% z!Ck#WXA}60`8MT=7uRU1CHk%Q4mA=r;g)0EJkK>vkdzdxYr_58vVA4nHKdCCZmel8 z*he9PIk3p+P+B}Y)UBBXFvk zJYw8AL3q}OZXka=*H63DrW$+s^KpfQGIeQ<81Yfoij&b?3$8J4t*M4>7dlf1sazNC zxb6z6xMJP%w(?A0mJHf>`{?F+pNl85c`-pkNyPFp)u+GZ{aUq#r1q?hnVvAWO|UY? zVx}&s1{U2$gn1wL=*W3)G+8yJE67)6_9NtUQTJ6u{; z6@BP-`K7k3cN;Lt;YT?P3pkv+7!S=-Nm?T)wm%clVTSTG81Mcp~5Af*_{qCq(8 zRz@MW?I_j!mO>1mt%$%q{oj61_3aZ98ydDrNeQf&VfkiQY=)?XRI%{y&gSDgkXU-e z(L=KtnSD{>e*-6nn6Arp+W{#>mS%9?56eml!wK^n8DMjxR-e_^goQ1upan~b@TLuA zy#@tcSHca3-V&BR?T2U!Yl=0ZU7UMSY%wL)8D)O?fy@L;fGi$jFaC^>sKpl%DEJt$ zvaX}e53dahF`oZq=xVEc)RPs!dgozqJG{JRGp9oPr8AOy!wx9$vi)BUHA#E{eJ<8(zL3ScAelZXW$DIr$WjIngU{n%IfVp7fwgX z3;0Z$p|76J8_u}ImO%*c1{NQsxwRy+H4*wNF z3+4J$H3d@FFaLthS%r&mtAh_qj6!KF-9*nB`cr0RR#+Y9z;Of9FLv~Bk+y7igq7j; zSnK(hbGwvfFRk~xeQHLx^v{>-OP;ZQ^Rq%P7vy4M{@Bv3s!U4kvu%}0(o;)Hc;(*c zV&Kco!SncSjB0*@o~y_24jcS3iUVt1x=yV&5lg)BT32Q??SU=*rRe$rZrTl*I~JZE zWnsN`Qf?6vvfVFIGA0nU7QK2X)U1i0skY3?rB54|xdL z=9EqBu%20E9a+E7YvT=;{TAL6<_f*8=<01hGu{PTn*gh~adB{iS@~-h?=OY@SN0nG zJOcWeUu8hW5AT$v1)DQ3*;+4NF}sWXZ88-0+gZwzB22L!6Mh7qDS!LNxAO2{AMS6% znS1Avt$7xzCsh-uH4aW~;Fmd3pr5KmgTmE0Ws4RsEI6i&r~%{YA6(P5*O3Ca0YMt^ z#xyT!zl@{5VF9J{)Nklm8XBw;HUooX0bONkAi?+K3hYW^(;v-fP85?nN^0JQvUOg1 z#(eDCeI}1sqrffM&~bQ^sd`Lwmj1$HQxpz0q7WJ;>`!5Pi8vp_+XwBXg3gPHM${yo z;FR0RnEW+|+J~UK_2bB?#0xU0fT>A*HRd<0u&(i3DR=Voq#r}?+Lw6Lk8rv^JJdY4 zLJ(CmqIu*FxhSt+Pzy7Q&Ny*+GsstR|Q^&q2*k1!5pp+GbZ)k1*bC1#fto7 z#hj+qG$pb3(1EtY3GV@#;APu&-aO(6a@B9&rt$QTgKp~!@^|cjiv|2FZKgB-6;dnx zhTB739Mu%nro_~$AWNZ_M=vj9($VCx$?x~i%WX~fxT&#lnF|k0`-6sv$nIXge>|^+ zVt+$jhgH%rw4gCXz0_$X~A zU#$+5Yrcyy=|l6SY_#!2>_eO9v=PF}DB7sIoNpuscZdx0{57~cs9BwcR^Ya__iMA0 zyd`{qf7-%|viR)W=6p~~NbGze6``?EDTO1nWD+2{87$*Q?vi7aPjj>_oyl*0X@_I_ z)S16eM*(EW4SOJnaYNr9$LGDM>3FK1BYaXN9(HIB4!8uk5!Qi)w_0{i1W^X;<|v4W zwsyqPzZL|wr{C1~y@t2VdnV^pSNWuDYkzMLD>JSfAC4<$?Rnva#ZMJ#w+!t&+6cHCA;^CC zC&Eh~kzb5-{;2TYLP0U+Gr~QaREjB*9~%BR62UK_^5n(+GlJCO3OOy8QAHrbB}RQ% zrDJLSRU3A;EBve!Ev5XL9T

Gt-}t%F4h-y0lu1Y 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

0CX*Ysg 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�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

  • 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 vb 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

    T^h*DPmQ|vg+*myUw0*+}JT~v<4 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�n^5 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 z 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

  • pg}WZx;+rhG>mRLLC9VGcA} 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%0DF`8j%;O8xd4b-Y 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 z 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 z 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

    0X$#+dBYLXtwfrJK;_2r6Kmy! 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Ð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

    ?=wzF8`KoJY4Opz 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
    bJzt7hS$o; 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ⅆ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 z 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
    y&dL(-Ub%2+9x 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≀Aow#nSj#pc%> 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 +function load(options?: LoadOptions): Promise; ``` ### LoadOptions ```ts -/** Options for the page's shared WASM runtime. Omitted options reuse its existing configuration. */ +/** Loading latkit's WebAssembly: once a page, before data, views, and pictures can be made. */ +/** How latkit loads. Options left out keep what loading began with. */ interface LoadOptions { /** The matching package's WASM asset, or an already compiled module. Defaults to the bundled asset. */ wasm?: string | URL | WebAssembly.Module; - /** Creates a worker running the standalone `latkit/worker` bundle. Defaults to the bundled worker. */ - createWorker?: () => Worker; + /** Makes a worker running the standalone `latkit/worker` bundle. Defaults to the bundled worker. */ + worker?: () => Worker; } ``` -### Latkit +### Wait ```ts -/** - * The initialized data and rendering API. In a browser the page's first GPU is made as the runtime - * loads, and the first `Gpu.create()` takes it; later calls request devices of their own. - */ -interface Latkit { - Data: typeof import('./runtime.js').Data; - Gpu: typeof import('./runtime.js').Gpu; - Shot: typeof import('./runtime.js').Shot; - colormapCss: typeof import('./runtime.js').colormapCss; +/** How long a promise may wait: until `signal` aborts, by default 30 seconds. */ +interface Wait { + signal?: AbortSignal; } ``` @@ -52,90 +48,165 @@ interface Latkit { ```ts /** * What latkit throws: an `Error` with a `code`, and for invalid input the `path` to the part, such - * as `marks[2].vertices.color`. + * as `marks[2].vertices.color`. `unloaded` is a call made before `await load()`. */ interface LatkitError extends Error { - code: Failure['code']; + code: Failure['code'] | 'unloaded'; path?: string; } ``` ## Data and patches -Datasets of named tables, changed by patches written without WebAssembly. +Datasets of named tables, changed by patches written without WebAssembly, and what views draw them from. ### Data -```ts +````ts /** - * A named collection of tables, changed in place by patches. A snapshot keeps a version, sharing - * every page with it, so keeping earlier versions, as an undo history does, costs only the pages - * they hold apart. A view draws the version it was given until it is given another. + * A dataset that changes in place: named tables of columns, changed only by patches, every change + * of one or none. A view given it draws its latest version, from the frame after a patch applies. A + * snapshot keeps one version, sharing every page with it, so earlier versions kept, as an undo + * history does, cost only the pages they hold apart. + * + * ```ts + * const grid = new Data('grid').apply(new Patch().replace('Bus', [1, 2], { kv: [230, 115] })); + * view.sources = [grid]; + * grid.apply(new Patch().set('Bus', [2], { kv: [69] })); // drawn next frame + * ``` */ class Data { + /** An empty dataset named `name`. */ constructor(name: string); - /** Releases this handle. Views and snapshots keep the versions they hold. */ - destroy(): void; - [Symbol.dispose](): void; - /** - * The dataset `bytes` hold: latkit's bytes, renamed `name` when given, or GeoJSON, gzip-compressed - * or not, read in a worker as dataset `name`: points, line strings, and polygons as tables - * `points`, `lines`, and `polygons`, each with its geometry as field `geometry` and its properties - * as fields. - */ - static read(bytes: Uint8Array, name?: string): Promise; /** - * The dataset its JSON document builds, `{"name": ..., "patch": [...]}`, the patch loading as - * `Patch.load` loads one. + * The dataset `source` holds, as its first bytes say: latkit's bytes; GeoJSON, gzip-compressed or + * not, read in a worker, its points, line strings, and polygons rows of tables `points`, `lines`, + * and `polygons`, each with its geometry as field `geometry` and its properties as fields; or its + * JSON document, `{"name": ..., "patch": [...]}`, its patch read as `Patch.read` reads one. `source` + * is bytes, a `Blob`, a `Response`, a stream, a URL to fetch, or a document as a JSON value. `name` + * names it, as GeoJSON needs; `base` is what a document's slices of files are relative to, by + * default the URL it was fetched from. */ - static load(json: string, base?: string): Promise; + static read( + source: DataSource, + options?: { + name?: string; + base?: string; + } & Wait, + ): Promise; get name(): string; - /** The names of the tables. */ - get tables(): string[]; - /** Every row's id of `table`, in row order. */ - ids(table: string): Id[]; - /** The names of `table`'s fields. */ - fields(table: string): string[]; - /** The first and last axis coordinates the sampled fields span. */ - get span(): [number, number] | undefined; /** Applies every change of `patch`, or none when one is invalid. */ apply(patch: Patch | Uint8Array): this; - /** This version, kept while `apply` changes this one: O(1). */ - snapshot(): Data; - /** The value of `field` for the row with `id` in `table`, at axis coordinate `at` when given. */ - get(table: string, id: Id, field: string, at?: number): Value; + /** The version it holds now, kept as it is while patches change it: O(1). */ + snapshot(): Snapshot; + /** Makes `snapshot`, a version of this dataset, the one it holds: what an undo does. */ + restore(snapshot: Snapshot): this; + /** What it holds now: see {@link Snapshot.describe}. */ + describe(): Description; + /** A value it holds now: see {@link Snapshot.get}. */ + get( + table: string, + id: Id, + field: string, + options?: { + at?: number; + }, + ): Value; + /** The rows a query keeps of what it holds now: see {@link Snapshot.query}. */ + query(query: Query): QueryResult; + /** Rows' extremes over spans of what it holds now: see {@link Snapshot.envelope}. */ + envelope(request: EnvelopeRequest): Envelope; + /** What it holds now as latkit's bytes: the patch that builds it. */ + toBytes(): Uint8Array; + /** Frees this handle; views drawing the dataset and its snapshots keep it. */ + destroy(): void; + [Symbol.dispose](): void; +} +```` + +### Snapshot + +```ts +/** One version of a dataset, as it was when taken: O(1) to keep. A view given one draws it as it is. */ +class Snapshot { + get name(): string; /** What the dataset holds: tables, rows, fields and their kinds, the axis and its span. */ describe(): Description; + /** The value of `field` for the row with `id` in `table`, at axis coordinate `at` when given. */ + get( + table: string, + id: Id, + field: string, + options?: { + at?: number; + }, + ): Value; /** * The rows of a table `query` keeps, filtered, sorted, and paged, sampled fields read at its * coordinate, on the CPU through the expressions marks draw with. */ - query(query: Query): Table; + query(query: Query): QueryResult; /** - * Each row's least and greatest value of sampled `field` of `table` over `spans` equal spans of - * `range`, of every row or of rows `ids`: row after row, NaN where a span holds no frame. Reads the - * coarsest levels of detail that fit, so a span costs a few reads however many frames it covers. + * Each row's least and greatest value of a sampled field over equal spans of a range, of every row + * or of the rows named: row after row, NaN where a span holds no frame. It reads the coarsest + * levels of detail that fit, so a span costs a few reads however many frames it covers. */ - envelope( - table: string, - field: string, - range: [number, number], - spans: number, - ids?: readonly Id[], - ): Envelope; - /** The dataset as latkit's bytes: the patch that builds it. */ + envelope(request: EnvelopeRequest): Envelope; + /** The version as latkit's bytes: the patch that builds it. */ toBytes(): Uint8Array; + destroy(): void; + [Symbol.dispose](): void; } ``` -### Table +### Tiles + +```ts +/** + * A dataset fetched in pieces as views ask for them: plain data naming where they lie. Each view + * draws the pieces its camera and plot window ask for as a dataset of its own, and every view of a + * GPU shares what is fetched. The source's first bytes say what it holds. + * + * - Time tiles, a `frames.json` manifest as `latkit tile` writes it: a dataset whose sampled + * fields' histories are fetched in spans. A line tracing a field fetches the detail its plot + * window draws, a mark showing one the frames about the view's coordinate, and a span still + * loading draws the coarser span above it. + * - Vector tiles: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` template, fetched as + * the camera asks. Each layer is a table of its features of the kind marks draw it as (areas + * polygons, lines lines, vertices points), or named `layer:polygons`, `layer:lines`, or + * `layer:points`. + */ +class Tiles { + readonly name: string; + readonly url: string; + /** The tiles at `url`, drawn as dataset `name`. */ + constructor(name: string, url: string); +} +``` + +### Source + +```ts +/** What a view draws a dataset from, by its name. */ +type Source = Data | Snapshot | Tiles; +``` + +### DataSource + +```ts +/** What `Data.read` reads: latkit's bytes, GeoJSON, or a dataset's JSON document. */ +type DataSource = + Uint8Array | ArrayBuffer | Blob | Response | ReadableStream | string | object; +``` + +### QueryResult ```ts /** * The rows a query keeps, in the forms a patch writes: numbers as a `Float64Array` (missing NaN), - * other values as arrays (missing `null`), a reference as its target ids. + * other values as arrays (missing `null`), a reference as its target's ids. */ -interface Table { +interface QueryResult { table: string; ids: Id[]; fields: Record< @@ -171,20 +242,32 @@ interface Envelope { * latkit's bytes as it is built. * * ```ts - * const patch = Patch.to('grid') + * const patch = new Patch('grid') * .replace('Bus', [101, 102], { x: Float64Array.of(0, 10), name: ['North', 'South'] }) - * .append(0.5, 'Bus', { vm: Float32Array.of(1.01, 0.99) }); + * .append('Bus', 0.5, { vm: Float32Array.of(1.01, 0.99) }); * ``` */ class Patch { - /** The dataset the patch changes, when it names one; `Data.apply` refuses it for another. */ - readonly data: string | undefined; - /** An empty patch, for whichever dataset it is applied to. */ + /** An empty patch: to dataset `data`, which it names, or to whichever it is applied to. */ constructor(data?: string); - /** An empty patch to dataset `data`. */ - static to(data: string): Patch; - /** The patch `bytes` hold: one frame, as `toBytes` or a stream writes it. */ - static fromBytes(bytes: Uint8Array): Patch; + /** The dataset the patch changes, when it names one; `Data.apply` refuses it for another. */ + get data(): string | undefined; + /** + * The patch `source` holds: latkit's bytes, one frame as `toBytes` writes it, or its JSON, what + * any language writes without latkit: a list of changes, each named by its method with its + * parameters by name, such as `{"set": {"table": "Bus", "ids": [1], "fields": {"kv": [345]}}}`. + * Slices of binary files, `{file, dtype, offset, count}`, are fetched at once relative to `base`, + * by default the URL the JSON was fetched from. `source` is bytes, a `Blob`, a `Response`, a URL + * to fetch, or the JSON as a value. Bytes read without WebAssembly; JSON loads it, as `load()` + * does. A frame longer than `limit` bytes (256 MiB unless given) is refused. + */ + static read( + source: PatchSource, + options?: { + base?: string; + limit?: number; + } & Wait, + ): Promise; /** * The patches arriving back to back in `stream`, each as soon as its last byte does: a response * body, a file, or a socket's messages. Each frame says its length up front, so a patch longer @@ -203,14 +286,6 @@ class Patch { limit?: number; }, ): AsyncGenerator; - /** - * The patch its JSON makes: a list of changes, each named by its method with its parameters by - * name, such as `{"set": {"table": "Bus", "ids": [1], "fields": {"kv": [345]}}}`. Slices of - * binary files, `{file, dtype, offset, count}`, are fetched at once relative to `base`, the - * document's own URL. Reading JSON takes latkit's WebAssembly, which this loads as - * `loadLatkit()` does. - */ - static load(json: string, base?: string): Promise; /** Names the axis sampled fields are recorded over, such as `('time', 's')`. */ axis(name: string, unit: string): this; /** Replaces table `table` with these rows and fields, or adds it. */ @@ -223,26 +298,30 @@ class Patch { /** Removes the rows of `table` with `ids`. */ remove(table: string, ids: RowIds): this; /** - * Records a frame at each axis coordinate of `at`: for each field, a value per row at every - * coordinate, frame-major; of every row of `table`, or of rows `ids`, the rest holding their last + * Records a frame of `table` at each axis coordinate of `at`: for each field, a value per row at + * every coordinate, frame-major; of every row, or of rows `ids`, the rest holding their last * values. A field's first frame makes it sampled. Appends of float arrays to the same fields of * every row, one after another, write as one change, so a frame a call costs no more than one call. */ append( - at: number | readonly number[], table: string, + at: number | readonly number[], fields: Readonly>, - ids?: RowIds, + { + ids, + }?: { + ids?: RowIds; + }, ): this; /** Forgets every sampled field's frames before axis coordinate `before`. */ forget(before: number): this; /** - * Holds a run of sampled field `field`'s entries of level `level`, numbered from `first`: what a - * time tile holds, as `latkit tile` writes them. At level 0 the entries are frames, each a - * coordinate in `at` and a value per row of `table` in `values`. At level `k` they are buckets of - * `8^k` frames, each its first and last frame's coordinates in `at`, and in `values` each row's - * least value over each bucket, then each row's greatest. NaN is missing. No entry may be held - * already; frames past the field's widen it. + * Holds a run of sampled field `field`'s entries of level `level` (0 unless given), numbered from + * `first`: what a time tile holds, as `latkit tile` writes them. At level 0 the entries are frames, + * each a coordinate in `at` and a value per row of `table` in `values`. At level `k` they are + * buckets of `8^k` frames, each its first and last frame's coordinates in `at`, and in `values` + * each row's least value over each bucket, then each row's greatest. NaN is missing. No entry may + * be held already; frames past the field's widen it. */ frames( table: string, @@ -250,12 +329,14 @@ class Patch { { level, first, + at, + values, }: { level?: number; first: number; + at: Float64Array | readonly number[]; + values: Float32Array | readonly number[]; }, - at: Float64Array | readonly number[], - values: Float32Array | readonly number[], ): this; /** Appends `next`'s changes, to this patch's dataset. */ extend(next: Patch): this; @@ -264,6 +345,13 @@ class Patch { } ```` +### PatchSource + +```ts +/** What `Patch.read` reads: latkit's bytes or a patch's JSON. */ +type PatchSource = Uint8Array | ArrayBuffer | Blob | Response | string | object; +``` + ### RowIds ```ts @@ -339,7 +427,7 @@ interface ReadableBytes { ## Expressions -What varies by row: builders writing the expressions scenes take. +What varies by row: builders writing the expressions and colors scenes take. ### Expr @@ -351,7 +439,7 @@ class Expr { constructor(node: ExprNode); toJSON(): ExprNode; /** Reads this expression's fields from `table` of dataset `data`, joined by row id. */ - of(data: string, table: string): Expr; + from(data: string, table: string): Expr; /** Reads sampled fields at their last frame without interpolating. */ step(): Expr; add(other: ExprInput): Expr; @@ -382,10 +470,10 @@ class Expr { exp(): Expr; /** Whether a text field equals `text`. */ is(text: string): Expr; - /** Whether a text field holds `text`, ignoring case. Queries read it; marks do not. */ + /** Whether a text field holds `text`, ignoring case: in a mark, a field of its own table. */ contains(text: string): Expr; - /** The value through `map`: a catalog name, or your own colors. */ - colormap(map: Colormap): Mapped; + /** The value through `map`, a color for every row: a catalog name, or your own colors. */ + colormap(map: Colormap): Colormapped; /** The value mapped linearly onto `[low, high]`, clamped. */ scale(low: number, high: number): Mapped; } @@ -394,15 +482,70 @@ class Expr { ### ExprInput ```ts -/** What an expression is written as: an {@link Expr}, or its JSON form. */ +/** What a value is written as: an {@link Expr}, or its JSON form, in which a string names a field. */ type ExprInput = Expr | ExprNode; ``` +### Mapped + +```ts +/** A value mapped onto a range, whose domain can be set. */ +class Mapped extends Expr { + /** The input values that map to the ends; it fits the values shown unless set. */ + domain(low: number, high: number): Mapped; +} +``` + +### Missing + +```ts +/** A missing value, which options take the theme's for: a value and a color alike. */ +class Missing extends Expr { + readonly node: MissingNode; + constructor(); + toJSON(): MissingNode; +} +``` + +### Paint + +```ts +/** + * A color for every row of a mark: a CSS color, a value through a colormap, or a conditional choosing + * between colors. Color options take one, or a CSS color as it is. + */ +class Paint { + readonly node: PaintNode; + /** A color from its JSON form; use the builders for common colors. */ + constructor(node: PaintNode); + toJSON(): PaintNode; +} +``` + +### PaintInput + +```ts +/** + * What a color is written as: a CSS color, a {@link Paint}, or its JSON form. A string is a CSS + * color here; a field reads with {@link field}. + */ +type PaintInput = Color | Paint | PaintNode; +``` + +### Colormapped + +```ts +/** A value through a colormap, whose domain can be set. */ +class Colormapped extends Paint { + /** The input values that map to the ends; it fits the values shown unless set. */ + domain(low: number, high: number): Colormapped; +} +``` + ### Color ```ts -/** A CSS color, such as `'#ffb000'` or `'rebeccapurple'`. Where an option takes an expression, a - * string starting with `#` is a hex color and any other string names a field. */ +/** A CSS color, such as `'#ffb000'` or `'rebeccapurple'`: what a string is in a color option. */ type Color = string; ``` @@ -417,67 +560,105 @@ const field: (name: string) => Expr; ```ts /** A color constant. */ -const color: (css: Color) => Expr; +const color: (css: Color) => Paint; ``` ### missing ```ts /** A missing value: options that read one take the theme's. */ -const missing: () => Expr; +const missing: () => Missing; ``` ### when ```ts -/** Starts a conditional: `when(c).then(a).when(d).then(b).otherwise(e)`. */ +/** + * Starts a conditional: `when(c).then(a).when(d).then(b).otherwise(e)`. Its values are colors, a + * string a CSS color, where its first is one; else values. + */ function when(condition: ExprInput): When; ``` ### When ```ts -declare class When { - constructor(cases: [ExprNode, ExprNode][], condition: ExprNode); - then(value: ExprInput): Then; +/** A conditional's first case, waiting for its value where the condition holds. */ +class When { + /** The color where the condition holds: every value of the conditional is a color. */ + then(value: PaintInput): Then; + /** The value where the condition holds. */ + then(value: ExprInput): Then; } ``` ### Then ```ts -declare class Then { - constructor(cases: [ExprNode, ExprNode][]); - when(condition: ExprInput): When; - otherwise(value: ExprInput): Expr; +/** A conditional's cases so far, waiting for another or the value where none holds. */ +class Then { + /** Another case, tried when those before it do not hold. */ + when(condition: ExprInput): Next; + /** The value where no case holds. */ + otherwise(value: InputOf): T; } ``` -### Mapped +### Next ```ts -/** A value through a colormap or onto a range, whose domain can be set. */ -class Mapped extends Expr { - /** The input values that map to the ends; it fits the values shown unless set. */ - domain(low: number, high: number): Mapped; +/** A conditional's next case, waiting for its value where the condition holds. */ +class Next { + /** The value where the condition holds. */ + then(value: InputOf): Then; } ``` +### InputOf + +```ts +/** What a conditional's values are written as, a value's or a color's. */ +type InputOf = T extends Paint ? PaintInput : ExprInput; +``` + +### px + +```ts +/** + * A size in CSS pixels, whatever the space's own unit: a stroke keeps its width on screen as a + * diagram zooms. A number makes a theme's length too. + */ +function px(size: number): { + px: number; +}; +function px(size: ExprInput): Px; +``` + ### units ```ts /** - * A size in position units, so it scales as the view zooms, kept within `atLeast` and `atMost` CSS - * pixels when given: `units('r', { atLeast: 2 })` grows with the map and never vanishes. A marker - * keeps its proportions, its larger side held. + * A size in position units, whatever the space's own unit, so it scales as the view zooms, kept + * within `atLeast` and `atMost` CSS pixels when given: `units('r', { atLeast: 2 })` grows with the map + * and never vanishes. A marker keeps its proportions, its larger side held. A number alone makes a + * theme's length too. */ -const units: ( - size: ExprInput, - px?: { - atLeast?: number; - atMost?: number; - }, -) => Size; +function units(size: number): { + units: number; +}; +function units(size: ExprInput, within?: Within): Units; +``` + +### Within + +```ts +/** The CSS pixels a size in position units keeps within. */ +interface Within { + /** CSS pixels it keeps at least, however far the view zooms out. */ + atLeast?: number; + /** CSS pixels it keeps at most, however far the view zooms in. */ + atMost?: number; +} ``` ### selected @@ -537,8 +718,8 @@ interface RgbaImage { ```ts /** - * A CSS gradient through `map`, a catalog name or colors of your own, for legends: the colors the - * GPU draws, a band each for a categorical map's categories. + * A CSS gradient through `map`, a catalog name or colors of your own, toward `direction`: the colors + * the GPU draws, a band each for a categorical map's categories. */ function colormapCss(map: Colormap, direction?: string): string; ``` @@ -550,28 +731,21 @@ A GPU makes views, and canvases that show them. ### Gpu ```ts -/** The GPU device views draw with. Views of one GPU draw in one animation frame. */ +/** The GPU device views draw with. Views of one GPU draw in one animation frame, sharing its memory. */ class Gpu { - /** Requests a WebGPU device: the first call takes the one made as latkit loaded. */ + /** Asks for a WebGPU device, loading latkit first: the first call takes the one made as it loaded. */ static create(): Promise; /** - * A view of `scene`; its marks draw once {@link View.setData} gives their data. A {@link Canvas} - * shows it, and a {@link Shot} draws it with or without one. + * A view of `scene`, drawing the datasets its `sources` give. A {@link Canvas} shows it, and a + * {@link Shot} draws it with or without one. */ - view(scene: Scene): View; + view(scene: Scene, options?: ViewOptions): View; /** - * A canvas showing views in `panels` of `target`: a `` element, which sizes itself to its - * CSS box and passes each view the input landing on its panel, or an `OffscreenCanvas` in a - * worker, which keeps the size it is given at `pixelRatio` pixels a CSS pixel. Panels are - * `[x, y, width, height]` fractions, one per view; by default one view fills the canvas. + * A canvas showing views as `layout` places them in `target`: a `` element, which sizes + * itself to its CSS box and passes each view the input landing on it, or an `OffscreenCanvas` in a + * worker, which keeps the size it is given at `pixelRatio` pixels a CSS pixel. */ - canvas( - target: HTMLCanvasElement | OffscreenCanvas, - options?: { - panels?: readonly Panel[]; - pixelRatio?: number; - }, - ): Canvas; + canvas(target: HTMLCanvasElement | OffscreenCanvas, options?: CanvasOptions): Canvas; /** * Calls `listener` with the frame's timestamp every animation frame, before the views draw, so * what it changes shows in that frame. Returns a function that stops it. @@ -597,15 +771,14 @@ class Gpu { step(now: number): void; /** * Resolves once the GPU finished the work submitted to it, every readback landed, and every - * pipeline a view or shot asked for compiled: a frame stepped then draws all of it. A `timeout` - * error after `deadline` seconds. + * pipeline a view or shot asked for compiled: a frame stepped then draws all of it. */ - done({ deadline: seconds }?: { deadline?: number | undefined }): Promise; + done(wait?: Wait): Promise; /** * Resolves when work a view waits on lands after this call (a readback, a compile, a job, a text, - * a fetch), or a view asks for a frame. A `timeout` error after `deadline` seconds. + * a fetch), or a view asks for a frame. */ - landed({ deadline: seconds }?: { deadline?: number | undefined }): Promise; + landed(wait?: Wait): Promise; /** Releases this GPU's canvases, views, and frame listeners. Other GPUs and data stay usable. */ destroy(): void; [Symbol.dispose](): void; @@ -615,128 +788,118 @@ class Gpu { ### View ````ts -/** A scene drawn from data it is given by name: shown by a canvas, which passes it input, or drawn by shots. */ +/** A scene drawn from the sources it is given: shown by a canvas, which passes it input, or drawn by shots. */ class View { - /** Draws `scene` instead; an invalid scene throws and changes nothing. */ - show(scene: Scene): void; + /** The scene drawn; setting an invalid one throws and keeps the scene drawn. */ + get scene(): Scene; + set scene(scene: Scene); /** - * Gives the view `data` as it is now, replacing the dataset of its name: give it again after - * applying patches. A mark it no longer fits draws nothing, and the next frame reports why as an - * error, unless a scene shown before then replaces the mark. + * What the view draws, each a dataset by name: a `Data`, its latest version from the frame after + * each patch applies; a `Snapshot`, as it is; `Tiles`, the pieces the view asks for. A mark its + * dataset no longer fits draws nothing, and the next frame tells why. Two sources of one name are + * invalid. */ - setData(data: Data): void; - /** - * Fetches the data at `url` in pieces as the view asks for them, kept as dataset `name`, which the - * view owns; the source's first bytes say what it is. - * - * - Time tiles, a `frames.json` manifest as `latkit tile` writes it: a dataset whose sampled - * fields' histories are fetched in spans. A line tracing a field fetches the detail its plot - * window draws, a mark showing one the frames about the view's coordinate, and a span still - * loading draws the coarser span above it. - * - Vector tiles: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` template, fetched as - * the camera asks. Each layer is a table of its features of the kind marks draw it as (areas - * polygons, lines lines, vertices points), or named `layer:polygons`, `layer:lines`, or - * `layer:points`. - */ - setTiles(name: string, url: string): void; - /** Stops drawing the dataset named `name`, given or fetched as tiles. */ - removeData(name: string): void; - /** The dataset named `name`, as given or as the view fetched it, as a snapshot. */ - data(name: string): Data | undefined; + get sources(): readonly Source[]; + set sources(sources: readonly Source[]); /** The axis coordinate shown: sampled fields read their last frame at or before it. */ get at(): number; set at(at: number); + /** Where the view looks; setting it looks there at once. */ get camera(): Camera; set camera(camera: Camera); - /** Eases to `camera`. */ - fly(camera: Camera): void; - /** Frames what `fit` names on the next frame, keeping pitch and bearing. */ - fit(fit?: Fit): void; - /** Where `position` (x, y, elevation) lands on screen, CSS pixels from the top left. */ - point(position: [number, number, number]): [number, number] | undefined; - /** The position on the ground under screen point `point`, if the view shows ground there. */ - ground(point: [number, number]): [number, number, number] | undefined; /** CSS pixels kept clear at the top, right, bottom, and left, as when panels cover the view. */ - get insets(): [number, number, number, number]; - set insets(insets: [number, number, number, number]); + get insets(): Insets; + set insets(insets: Insets); /** Degrees per second a map or plane turns around its center; 0 stops it. A diagram never turns. */ get orbit(): number; set orbit(degreesPerSecond: number); - /** The rows selected, a set per table. */ - get selection(): Rows[]; - /** - * Selects `rows`, and no others. Like every setter it tells nothing: `select` events tell what - * the user and the data changed. - */ - select(rows: readonly Rows[]): void; + /** The rows selected, a set per table; setting it selects those and no others. */ + get selection(): readonly Rows[]; + set selection(rows: readonly Rows[]); /** What the user points at: the row under the pointer, or the row the keys stepped to. */ get hovered(): Hit | null; + /** The work under way the last frame stood in for; empty once it is complete. */ + get waiting(): Waiting; + /** + * Whether the last frame drew everything as the data, scene, and camera say, with nothing standing + * in for work under way: what `waiting` lists. A shot waits for it. + */ + get complete(): boolean; /** Every mark that draws nothing because its data no longer fits it, and why. */ - get failures(): LatkitError[]; + get failures(): readonly LatkitError[]; + /** What the view does each frame. */ + get stats(): Stats; + /** Whether the view wants a frame: it changed, or work it waited on landed. */ + get wantsFrame(): boolean; + /** Frames what `fit` names on the next frame, keeping pitch and bearing. */ + fit(fit?: Fit): void; + /** Eases to `camera`. */ + fly(camera: Camera): void; + /** Where `position` lands on screen; `null` behind the camera. */ + screen(position: Position): Point | null; + /** The position on the ground under screen point `point`; `null` where the view shows none there. */ + position(point: Point): Position | null; + /** Where `row` lies on screen, or `null` when no mark draws it in view, once a frame draws the view. */ + screenOf(row: Row, wait?: Wait): Promise; /** - * Every row of a mark that picks within `radius` CSS pixels of `point`, nearest first, once a frame - * draws the view: a `timeout` error after `deadline` seconds without one. + * Every row of a mark that picks within `radius` CSS pixels (0 unless given) of `point`, nearest + * first, once a frame draws the view. */ pick( - point: [number, number], - radius: number, - { - deadline: seconds, - }?: { - deadline?: number | undefined; - }, + point: Point, + options?: { + radius?: number; + } & Wait, ): Promise; /** - * Where `row` lies on screen, CSS pixels from the top left, or `null` when no mark draws it in - * view, once a frame draws the view: a `timeout` error after `deadline` seconds without one. + * The rows joined to `row`, a set per table: a vertex's neighbors across edges and nets, or an + * edge's or a net's vertices. */ - locate( - row: Row, - { - deadline: seconds, - }?: { - deadline?: number | undefined; - }, - ): Promise<[number, number] | null>; - /** The rows joined to `row`, a set per table: a vertex's neighbors across edges and nets, or an - * edge's or a net's vertices. */ neighbors(row: Row): Rows[]; /** * Lays every vertex row without a position out again by the scene's layout: a diagram's blocks, or * points on a plane or the earth. Places a layout gave otherwise hold, so rows added later are * placed around the rest. Once the layout lands, answers a patch to each dataset writing each row - * laid out into its mark's position fields; applied, the places are the data's own. A `timeout` - * error after `deadline` seconds without it. + * laid out into its mark's position fields; applied, the places are the data's own. * * ```ts - * for (const patch of await view.arrange()) if (patch.data === data.name) data.apply(patch); + * for (const patch of await view.arrange()) if (patch.data === grid.name) grid.apply(patch); * ``` */ - arrange({ deadline: seconds }?: { deadline?: number | undefined }): Promise; - get stats(): Stats; - /** Whether the view wants a frame: it changed, or work it waited on landed. */ - get wantsFrame(): boolean; + arrange(wait?: Wait): Promise; /** - * Calls `listener` with each event of `type` told while it listens; returns a function that - * stops it. What lasts is state to read: `hovered`, `selection`, `camera`, `stats.waiting`, and - * `failures`. + * The version of dataset `name` the view draws: the latest version of a `Data` it took, a snapshot + * given, or what it holds of `Tiles`, the pieces its camera asked for. + */ + snapshot(name: string): Snapshot | undefined; + /** + * Calls `listener` with each event of `type` told while it listens; returns a function that stops + * it. What lasts is state to read, as the property each change is named for. */ on(type: T, listener: (event: ViewEvents[T]) => void): () => void; - /** Frees the view, and rejects what it was asked and has not answered; a canvas showing it keeps - * its last frame. */ + /** Frees the view, rejecting what it was asked and has not answered; a canvas showing it keeps its last frame. */ destroy(): void; [Symbol.dispose](): void; } ```` +### ViewOptions + +```ts +/** What a view is made with: the sources of the datasets its scene names. */ +interface ViewOptions { + sources?: readonly Source[]; +} +``` + ### ViewEvents ```ts /** * What a view tells its listeners, by type. Intents (`click`, `open`, `menu`, `drag`, `connect`, - * `delete`) are told once each, in the order the user acted. Changes (`hover`, `select`, `camera`, - * `waiting`) tell only their latest, and their state can always be read. A view tells only what the - * application did not do itself: its setters change state silently. + * `delete`) are told once each, in the order the user acted. Changes (`hovered`, `selection`, + * `camera`, `waiting`) are named like the property they changed, and tell only their latest. A view + * tells only what the application did not do itself: its setters change state silently. */ type ViewEvents = { [E in ViewEvent as E['type']]: E extends { @@ -747,24 +910,81 @@ type ViewEvents = { }; ``` +### Insets + +```ts +/** CSS pixels kept clear at the top, right, bottom, and left. */ +type Insets = [top: number, right: number, bottom: number, left: number]; +``` + +### Point + +```ts +/** A point on screen: CSS pixels from the top left. */ +type Point = [x: number, y: number]; +``` + +### Position + +```ts +/** A position in the space's coordinates: x, y, and elevation. */ +type Position = [x: number, y: number, elevation: number]; +``` + ### Canvas ```ts -/** Views shown in the panels of a `` or an `OffscreenCanvas`. */ +/** + * Views shown in a `` element, which sizes itself to its CSS box and passes each view the + * input landing on its panel, or an `OffscreenCanvas` in a worker, which keeps the size it is given. + */ class Canvas { - /** Shows `views`, one per panel, in place of those it showed. A view shows in one canvas at a time. */ - show(views: readonly View[]): this; + /** What it draws into. */ + get element(): HTMLCanvasElement | OffscreenCanvas; + /** + * The views shown and where, a later panel over an earlier one; setting it shows those in place of + * the ones shown. A view shows in one panel of one canvas at a time. + */ + get layout(): readonly Panel[]; + set layout(layout: readonly Panel[]); + /** + * A picture of exactly what the canvas shows: its views as its layout places them, at its size in + * CSS pixels times `scale`. + */ + shot(options?: ShotOptions): Shot; /** Stops drawing; the views remain, and the element keeps its last frame. */ destroy(): void; [Symbol.dispose](): void; } ``` +### CanvasOptions + +```ts +/** How a canvas shows its views. */ +interface CanvasOptions { + /** The views shown and where; a later panel draws over an earlier one. */ + layout?: readonly Panel[]; + /** Pixels a CSS pixel of an `OffscreenCanvas`; an element follows its device. */ + pixelRatio?: number; +} +``` + ### Panel ```ts -/** A view's panel of a canvas or a picture: `[x, y, width, height]` as fractions of it. */ -type Panel = readonly [number, number, number, number]; +/** A view's place in a canvas or a picture: where it draws, all of it unless `at` says. */ +interface Panel { + view: View; + at?: Rect; +} +``` + +### Rect + +```ts +/** `[x, y, width, height]` as fractions of a canvas or a picture. */ +type Rect = readonly [x: number, y: number, width: number, height: number]; ``` ## Pictures and video @@ -775,46 +995,67 @@ Shots draw views into images and video. ````ts /** - * A picture of views, each drawn into its panel of `size` pixels at `scale` pixels per CSS pixel: - * plain data, like a scene. A view a shot draws holds its canvas's last frame meanwhile. + * A picture of views, each drawn into its panel of `size` pixels as `layout` places it: drawn while a + * canvas showing it holds its last frame, each view's data as it was when the picture began. A + * canvas's `shot()` takes one of what it shows. * * ```ts - * const png = await (await new Shot([3840, 2160]).scale(2).image([view])).encode('png'); + * const png = await (await canvas.shot({ scale: 2 }).image()).encode('png'); * ``` */ class Shot { readonly size: readonly [number, number]; - /** A picture with width and height in pixels. */ - constructor(size: readonly [number, number]); - /** Draws `scale` pixels per CSS pixel: a view lays out `size / scale` CSS pixels. */ - scale(scale: number): Shot; - /** Where each view draws, `[x, y, width, height]` as fractions, one per view in order; by default one fills it. */ - panels(panels: readonly Panel[]): Shot; - /** Keeps the picture's alpha: views clear to transparent where their themes' backgrounds are. */ - transparent(): Shot; - /** The shot's deadline in seconds, including readback; rejects on timeout. Defaults to 30. */ - deadline(seconds: number): Shot; - /** Fixes elapsed-time expressions and drawing effects at `seconds` while transitions settle. */ - elapsed(seconds: number): Shot; - /** The picture of `views`, one per panel, once every one is complete and still. */ - image(views: readonly View[]): Promise; + readonly options: ShotOptions & { + layout?: readonly Panel[]; + }; + /** A picture `size` pixels wide and high, of the views of `options.layout`. */ + constructor( + size: readonly [number, number], + options?: ShotOptions & { + layout?: readonly Panel[]; + }, + ); + /** The picture, once every view is complete as `capture` says. */ + image(wait?: Wait): Promise; /** * A video of these pictures at `fps` frames a second, H.264 in MP4, written to `output` as it is - * drawn, at `bitrate` bits a second or about 0.15 bits a pixel a frame. Width and height are - * multiples of 4 and 2. `signal` aborts it: the output is aborted and the views are given back. + * drawn, at `bitrate` bits a second or about 0.15 bits a pixel a frame. Its width rounds down to a + * multiple of 4 and its height of 2, the blocks H.264 takes. `signal` aborts it: the output is + * aborted and the views are given back. */ - video( - gpu: Gpu, - fps: number, - output: WritableStream, - options?: { - bitrate?: number; - signal?: AbortSignal; - }, - ): Video; + video({ + fps, + output, + bitrate, + signal, + }: { + fps: number; + output: WritableStream; + bitrate?: number; + } & Wait): Video; } ```` +### ShotOptions + +```ts +/** How a picture is taken. */ +interface ShotOptions { + /** Pixels per CSS pixel: a view lays out `size / scale` CSS pixels, so 2 draws twice as sharp. 1 unless given. */ + scale?: number; + /** Keeps alpha where the themes' backgrounds would be. */ + transparent?: boolean; + /** + * When the picture is taken: once its views are complete and still, transitions, flights, and + * label fades run on until they land (`still`, the default); or once they are complete, as they + * stand now, what moves held where it is (`now`). + */ + capture?: 'still' | 'now'; + /** A still's effect time in seconds, where elapsed time and flowing comets read it. */ + elapsed?: number; +} +``` + ### Image ```ts @@ -832,7 +1073,7 @@ class Image { format: ImageFormat, options?: { quality?: number; - }, + } & Wait, ): Promise; destroy(): void; [Symbol.dispose](): void; @@ -851,21 +1092,20 @@ type ImageFormat = 'png' | 'jpeg' | 'webp'; ```ts /** A video being drawn, encoded, and written. */ class Video { - /** Seconds into the video the next frame shows at; retained after completion. */ + /** Seconds into the video the next frame shows at; kept once it is done. */ get time(): number; - /** Frames drawn or held so far; retained after completion. */ + /** Frames drawn or held so far; kept once it is done. */ get frames(): number; - /** Draws and writes one frame, waiting for output backpressure. Only one frame may run at a time. */ - frame(views: readonly View[]): Promise; + /** Draws and writes one frame, waiting for the output's backpressure. One frame draws at a time. */ + frame(): Promise; /** Records until `step(time)` returns false; any step, drawing, output, or progress error aborts it. */ record( - views: readonly View[], step: (time: number) => boolean | Promise, onProgress?: (progress: { frames: number; time: number }) => void, ): Promise; /** Finishes pending work, writes the end of the file, and closes and unlocks the output. Idempotent. */ finish(): Promise; - /** Cancels drawing and encoding, aborts the output, and unlocks it after pending writes settle. Idempotent. */ + /** Cancels drawing and encoding, aborts the output, and unlocks it once pending writes settle. Idempotent. */ abort(reason?: unknown): Promise; } ``` diff --git a/docs/reference/types.md b/docs/reference/types.md index 82661f98..72dbce8e 100644 --- a/docs/reference/types.md +++ b/docs/reference/types.md @@ -4,7 +4,7 @@ Generated from the same schema as the package types. Each field documents its me ## Scenes -A scene: its space, its marks, and how it looks and answers input. +A scene: its space, how it answers input, and how it is laid out. ### Scene @@ -39,18 +39,18 @@ interface Scene { ```ts /** * The coordinate space a scene's positions are in: a name, or - * `{ plane | diagram | earth | plot: options }`. A diagram is a flat sheet seen from straight - * above: y runs down, and every length is a unit, a CSS pixel at zoom 0, except glow, halo, and - * pick radius. Only a diagram has ports, groups, nets, orthogonal routes, and layouts. + * `{ plane | diagram | earth | plot: options }`. Its own unit is what lengths are in unless they + * say theirs: units in a diagram, a flat sheet seen from straight above with y running down, a + * unit a CSS pixel at zoom 0; CSS pixels elsewhere. Only a diagram has ports, groups, nets, and + * orthogonal routes. */ type Space = | 'plane' | 'diagram' - | 'mercator' - | 'globe' + | 'earth' | 'plot' | { - /** Longitude and latitude in degrees, on a map or a globe. */ + /** Longitude and latitude in degrees, on a flat map or a globe. */ earth: Earth; } | { @@ -80,12 +80,13 @@ interface Plane { ### Earth ```ts -/** The earth: positions are longitude and latitude in degrees. */ +/** + * The earth: positions are longitude and latitude in degrees, its ground in the theme's earth + * colors. + */ interface Earth { - /** A flat map or a globe. */ + /** A flat map or a globe; a flat map unless set. */ projection?: Projection; - /** The ground's color; the theme's surface unless set. */ - surface?: Color | null; /** Whether lines of longitude and latitude cross the ground, in the theme's grid color. */ graticule?: boolean; /** @@ -111,9 +112,10 @@ type Projection = 'mercator' | 'globe'; ```ts /** * A sheet for diagrams, flat and seen from straight above: the camera pans and zooms. Positions - * are units with y down, and every length is a unit, a CSS pixel at zoom 0 (sizes, padding, ports, - * strokes, text, and the theme's), except glow, halo, and pick radius. Zooming scales one fixed - * picture. Only a diagram has ports, groups, nets, and orthogonal routes; it lays out blocks. + * are units with y down, and lengths are units, a unit a CSS pixel at zoom 0 (sizes, padding, + * ports, strokes, text, and the theme's), but for what says `px`, a halo, focus's glow, and the + * pick radius. Zooming scales one fixed picture. Only a diagram has ports, groups, nets, and + * orthogonal routes; it lays out blocks. */ interface Diagram { /** The grid under the marks. */ @@ -197,140 +199,6 @@ type Grid = }; ``` -### Theme - -```ts -/** - * Colors and sizes marks take where their options read nothing, and the look of focus and motion: - * a base theme's name, or options over one (`base`, dark unless set). Lengths are CSS pixels; in a - * diagram they are units, except `glow` and `halo`. - */ -type Theme = - | 'dark' - | 'light' - | { - /** Behind everything. */ - background?: Color; - /** The earth's ground, and a group's container. */ - surface?: Color; - /** A plane's or a diagram's grid. */ - grid?: Color; - /** Vertex color. */ - vertex?: Color; - /** Vertex radius. */ - vertexRadius?: number; - /** - * Edge color; `null` blends the colors of each edge's two vertices, and in a diagram takes the - * text color. - */ - edge?: Color | null; - /** Edge width. */ - edgeWidth?: number; - /** Line color. */ - line?: Color; - /** Line width. */ - lineWidth?: number; - /** Area color. */ - area?: Color; - /** How much a face turned away from the camera darkens, 0 to 1. */ - shading?: number; - /** Text: labels, axis ticks, and titles. */ - text?: Color; - /** The CSS font family text is set in. */ - font?: string; - /** Text size. */ - fontSize?: number; - /** The line a plot draws at the shown coordinate. */ - playhead?: Color; - /** What a missing color reads as. */ - missing?: Color; - /** The glow around hovered rows. */ - hover?: Color; - /** The glow around selected rows; `null` glows in each row's own color. */ - selected?: Color | null; - /** How far focus glows reach, CSS pixels. */ - glow?: number; - /** The opacity of rows outside a selection. */ - dim?: number; - /** How much a hovered vertex sized in CSS pixels grows. */ - hoverScale?: number; - /** Length of a dash. */ - dash?: number; - /** How comets flowing along edges and lines look. */ - flow?: Flow; - /** - * In a diagram, distance between the lanes of edges joining the same two vertices, and between - * parallel orthogonal wires, which keep twice it apart. - */ - edgeSpacing?: number; - /** - * Elsewhere, how far edges joining the same two vertices bow apart: each lane by this fraction of - * the edge's length at its middle, as wide as eight lanes at most, more packing closer. A bow is - * part of the drawing, so zooming never moves it. - */ - laneBow?: number; - /** How far an arrowhead reaches past its edge's width. */ - arrow?: number; - /** CSS pixels of background around label text. */ - halo?: number; - /** Distance between a group's members and its outline. */ - groupPadding?: number; - /** The border of a diagram's blocks and groups, a unit wide; `null` draws none. */ - outline?: Color | null; - /** The radius of a diagram's block and group corners; a block's marker may set its own. */ - corner?: number; - /** How wide a diagram's ports are. */ - portSize?: number; - /** The radius of an orthogonal wire's bends. */ - bend?: number; - /** How a diagram's blocks and groups drop shadows onto its sheet. */ - shadow?: Shadow; - /** The air glowing around the globe, where the earth has an atmosphere. */ - atmosphere?: Color; - /** How bright the earth's night side stays where it is lit by daylight, 0 to 1. */ - night?: number; - /** The theme these options change; dark unless set. */ - base?: 'dark' | 'light'; - }; -``` - -### Flow - -```ts -/** - * How comets flowing along edges and lines look: each a round head, brightest at its middle, - * trailing a tail that thins and fades. - */ -interface Flow { - /** A comet's color; its alpha is how strongly a comet covers its stroke. */ - color?: Color; - /** Distance between comets. */ - spacing?: number; - /** How far a comet's tail trails its head. */ - length?: number; - /** How wide a comet's head is, CSS pixels, or units in a diagram; never narrower than its stroke. */ - width?: number; - /** - * Whether comets are part of the drawing: their spacing, length, and each row's flow in position - * units, degrees on the earth, so they zoom with the view and stay where they are on their lines. - * Otherwise they keep their size and speed on screen, and slide along their lines as it zooms. - */ - units?: boolean; -} -``` - -### Shadow - -```ts -/** How a diagram's blocks and groups drop shadows onto its sheet. */ -interface Shadow { - /** What a shadow darkens toward; its alpha is how dark it gets under the block. */ - color?: Color; - /** How far a block's or a group's shadow falls below it, and as far again it softens. */ - lift?: number; -} -``` - ### Controls ```ts @@ -359,8 +227,8 @@ type Controls = */ keyboard?: boolean; /** - * The row the user points at glows. It is hovered either way: events tell it, and `hovered()` - * reads it. + * The row the user points at glows. It is hovered either way: events tell it, and `hovered` reads + * it. */ hover?: boolean; /** CSS pixels around the pointer that picks reach; touch reaches 8 more. */ @@ -372,10 +240,10 @@ type Controls = selectBox?: boolean; /** * Dragging from a port, from a vertex with Alt, or an edge off its end draws a connection, told as - * `Event::Connect`. + * a `'connect'` event. */ connect?: boolean; - /** Delete and Backspace tell `Event::Delete` with the selection. */ + /** Delete and Backspace tell a `'delete'` event with the selection. */ delete?: boolean; /** * Clicking again where nothing moved selects the next row under the point, nearest first, so rows @@ -389,7 +257,7 @@ type Controls = press?: number; /** * How much the view moves on its own: eases, flights, coasting, orbits, label fades, comets, and - * what reads `elapsed()`. + * what reads `elapsed`. */ motion?: Motion; }; @@ -416,7 +284,7 @@ type Zoom = 'wheel' | 'modifier' | 'off'; * How the view places vertex rows without positions: a diagram's blocks, sized, or the points of a * plane or the earth, among the rows linked to them, gaps in the space's units: a name, or * `{ stress | layered: options }`. A place, once given, holds: rows added later are placed around - * the rest, and `View::arrange` lays every row out again. + * the rest, and `view.arrange` lays every row out again. */ type Layout = | 'none' @@ -491,6 +359,254 @@ interface Budget { } ``` +## Themes + +How a scene looks, a group for each kind of thing it draws, and the lengths its options take. + +### Theme + +```ts +/** + * Colors and sizes marks take where their options read nothing, and the look of text, focus, + * motion, and each space, grouped by what they style: a base theme's name, or options over one + * (`base`, dark unless set), each group's over the base's. Lengths are in the space's unit, units + * in a diagram and CSS pixels elsewhere, but for a text's halo and focus's glow, CSS pixels + * everywhere. + */ +type Theme = + | 'dark' + | 'light' + | { + /** Behind everything. */ + background?: Color; + /** A plane's or a diagram's grid, and the earth's graticule. */ + grid?: Color; + /** What a missing color reads as. */ + missing?: Color; + /** Length of a dash. */ + dash?: number; + /** How much a face turned away from the camera darkens, 0 to 1. */ + shading?: number; + /** Labels, axis ticks, and titles. */ + text?: TextTheme; + /** Vertices where their options say nothing. */ + vertex?: VertexTheme; + /** Edges where their options say nothing, their lanes, and their arrowheads. */ + edge?: EdgeTheme; + /** Lines where their options say nothing. */ + line?: LineTheme; + /** Areas where their options say nothing. */ + area?: AreaTheme; + /** How hovered and selected rows glow, and rows outside a selection dim. */ + focus?: FocusTheme; + /** How comets flowing along edges and lines look. */ + flow?: FlowTheme; + /** How a diagram's blocks, groups, ports, and wires look. */ + diagram?: DiagramTheme; + /** How the earth looks. */ + earth?: EarthTheme; + /** How a plot looks. */ + plot?: PlotTheme; + /** The theme these options change; dark unless set. */ + base?: 'dark' | 'light'; + }; +``` + +### TextTheme + +```ts +/** Labels, axis ticks, and titles. */ +interface TextTheme { + /** Text's color. */ + color?: Color; + /** The CSS font family text is set in. */ + font?: string; + /** Text's size. */ + size?: number; + /** CSS pixels of background around label text. */ + halo?: number; +} +``` + +### VertexTheme + +```ts +/** Vertices where their options say nothing. */ +interface VertexTheme { + /** A vertex's color. */ + color?: Color; + /** A vertex's radius. */ + radius?: number; +} +``` + +### EdgeTheme + +```ts +/** Edges where their options say nothing, their lanes, and their arrowheads. */ +interface EdgeTheme { + /** + * An edge's color; `null` blends the colors of each edge's two vertices, and in a diagram takes + * the text color. + */ + color?: Color | null; + /** An edge's width. */ + width?: number; + /** + * In a diagram, distance between the lanes of edges joining the same two vertices, and between + * parallel orthogonal wires, which keep twice it apart. + */ + spacing?: number; + /** + * Elsewhere, how far edges joining the same two vertices bow apart: each lane by this fraction of + * the edge's length at its middle, as wide as eight lanes at most, more packing closer. A bow is + * part of the drawing, so zooming never moves it. + */ + bow?: number; + /** How far an arrowhead reaches past its edge's width. */ + arrow?: number; +} +``` + +### LineTheme + +```ts +/** Lines where their options say nothing. */ +interface LineTheme { + /** A line's color. */ + color?: Color; + /** A line's width. */ + width?: number; +} +``` + +### AreaTheme + +```ts +/** Areas where their options say nothing. */ +interface AreaTheme { + /** An area's color. */ + color?: Color; +} +``` + +### FlowTheme + +```ts +/** + * How comets flowing along edges and lines look: each a round head, brightest at its middle, + * trailing a tail that thins and fades. Their spacing and length are in one unit, and each row's + * flow is in it too: in position units, degrees on the earth, comets are part of the drawing, + * zooming with the view and staying where they are on their lines; otherwise they keep their size + * and speed as it zooms. + */ +interface FlowTheme { + /** A comet's color; its alpha is how strongly a comet covers its stroke. */ + color?: Color; + /** Distance between comets. */ + spacing?: Length; + /** How far a comet's tail trails its head. */ + length?: Length; + /** How wide a comet's head is; never narrower than its stroke. */ + width?: number; +} +``` + +### FocusTheme + +```ts +/** How hovered and selected rows glow, and rows outside a selection dim. */ +interface FocusTheme { + /** The glow around hovered rows. */ + hover?: Color; + /** The glow around selected rows; `null` glows in each row's own color. */ + selected?: Color | null; + /** How far focus glows reach, CSS pixels. */ + glow?: number; + /** The opacity of rows outside a selection. */ + dim?: number; + /** How much a hovered vertex sized in CSS pixels grows. */ + grow?: number; +} +``` + +### DiagramTheme + +```ts +/** How a diagram's blocks, groups, ports, and wires look. */ +interface DiagramTheme { + /** What a group's container is filled with. */ + group?: Color; + /** The border of blocks and groups, a unit wide; `null` draws none. */ + outline?: Color | null; + /** The radius of block and group corners; a block's marker may set its own. */ + corner?: number; + /** How wide ports are. */ + portSize?: number; + /** The radius of an orthogonal wire's bends. */ + bend?: number; + /** Distance between a group's members and its outline. */ + groupPadding?: number; + /** How blocks and groups drop shadows onto the sheet. */ + shadow?: ShadowTheme; +} +``` + +### ShadowTheme + +```ts +/** How a diagram's blocks and groups drop shadows onto its sheet. */ +interface ShadowTheme { + /** What a shadow darkens toward; its alpha is how dark it gets under the block. */ + color?: Color; + /** How far a block's or a group's shadow falls below it, and as far again it softens. */ + lift?: number; +} +``` + +### EarthTheme + +```ts +/** How the earth looks. */ +interface EarthTheme { + /** The ground's color. */ + ground?: Color; + /** The air glowing around the globe, where the earth has an atmosphere. */ + atmosphere?: Color; + /** How bright the night side stays where daylight lights the earth, 0 to 1. */ + night?: number; +} +``` + +### PlotTheme + +```ts +/** How a plot looks. */ +interface PlotTheme { + /** The line drawn at the shown coordinate. */ + playhead?: Color; +} +``` + +### Length + +```ts +/** + * A length: a number, in the space's unit, units in a diagram and CSS pixels elsewhere; `{ px }`, + * CSS pixels; or `{ units }`, position units. + */ +type Length = + | number + | { + /** CSS pixels, however the view zooms. */ + px: number; + } + | { + /** Position units, which scale as the view zooms. */ + units: number; + }; +``` + ## Marks What a scene draws each table as. @@ -541,17 +657,20 @@ interface Vertices { * flat. */ elevation?: ExprInput; - /** CSS pixels, or position units with `units`; units in a diagram. */ + /** + * Each row's radius, in the space's unit unless it says its own: units in a diagram, CSS pixels + * elsewhere. + */ radius?: Size | null; /** - * Width and height, for markers that are not round: CSS pixels, or position units with `units`; - * units in a diagram, where a block without a size fits its label and ports. + * Width and height, for markers that are not round, in the space's unit unless they say their own; + * in a diagram a block's are units, and a block without a size fits its label and ports. */ size?: [Size, Size] | null; /** How each row is drawn. */ marker?: Marker; /** A ring around each row's marker in this color; a missing or transparent color draws none. */ - ring?: ExprInput | null; + ring?: PaintInput | null; /** * A port on each row for each reference field naming a net, which an `edges` mark of the net table * without ends draws. Diagrams only. @@ -568,7 +687,7 @@ interface Vertices { */ shadow?: boolean; /** Each row's color; the theme's unless set. */ - color?: ExprInput | null; + color?: PaintInput | null; /** Whether each row is drawn and picked. */ visible?: ExprInput; /** @@ -632,18 +751,20 @@ interface Edges { * reference field names it, as a star from their middle to each. */ joins?: [table: string, reference: string] | null; - /** CSS pixels, or position units with `units`; units in a diagram. */ + /** + * Each row's width, in the space's unit unless it says its own: units in a diagram, CSS pixels + * elsewhere. + */ width?: Size | null; /** Whether each row is dashed. */ dash?: ExprInput; /** - * Comets flowing along each row at this speed a second: CSS pixels, units in a diagram, or - * position units where the theme's flow is in units; negative flows backward. A speed that changes - * carries comets on from where they are. + * Comets flowing along each row at this speed a second, in the lengths of the theme's flow; + * negative flows backward. A speed that changes carries comets on from where they are. */ flow?: ExprInput; /** Each row's color; the theme's unless set. */ - color?: ExprInput | null; + color?: PaintInput | null; /** Whether each row is drawn and picked. */ visible?: ExprInput; /** @@ -695,18 +816,20 @@ interface Lines { trace?: string | null; /** How each row runs between its samples, on a plot. */ interpolation?: Interpolation; - /** CSS pixels, or position units with `units`; units in a diagram. */ + /** + * Each row's width, in the space's unit unless it says its own: units in a diagram, CSS pixels + * elsewhere. + */ width?: Size | null; /** Whether each row is dashed. */ dash?: ExprInput; /** - * Comets flowing along each row at this speed a second: CSS pixels, units in a diagram, or - * position units where the theme's flow is in units; negative flows backward. A speed that changes - * carries comets on from where they are. + * Comets flowing along each row at this speed a second, in the lengths of the theme's flow; + * negative flows backward. A speed that changes carries comets on from where they are. */ flow?: ExprInput; /** Each row's color; the theme's unless set. */ - color?: ExprInput | null; + color?: PaintInput | null; /** Whether each row is drawn and picked. */ visible?: ExprInput; /** @@ -753,7 +876,7 @@ interface Areas { /** The polygon field each row fills: one polygon's rings, or polygons. `geometry` unless set. */ geometry?: string | null; /** Each row's color; the theme's unless set. */ - color?: ExprInput | null; + color?: PaintInput | null; /** Whether each row is drawn and picked. */ visible?: ExprInput; /** @@ -802,7 +925,7 @@ interface Faces { */ corners?: [a: string, b: string, c: string] | null; /** Each row's color; the theme's unless set. */ - color?: ExprInput | null; + color?: PaintInput | null; /** Whether each row is drawn and picked. */ visible?: ExprInput; /** @@ -957,18 +1080,6 @@ type Port = type Side = 'left' | 'right' | 'top' | 'bottom'; ``` -### End - -```ts -/** A vertex a connection ends at, and its port when it has the one the connection meets. */ -interface End { - /** The vertex. */ - row: Row; - /** The port, by the reference field naming its net. */ - port?: string; -} -``` - ### Route ```ts @@ -991,13 +1102,16 @@ type Label = | { /** A text field. */ text: ExprInput; - /** CSS pixels, or units in a diagram; the theme's font size unless set. */ + /** In the space's unit: units in a diagram, CSS pixels elsewhere; the theme's text size unless set. */ size?: number | null; /** Each row's text color; the theme's text color unless set. */ - color?: ExprInput | null; + color?: PaintInput | null; /** Where labels would overlap, the higher shows; the mark's priority unless set. */ priority?: ExprInput | null; - /** How far a line runs before it wraps: CSS pixels, or units in a diagram. */ + /** + * How far a line runs before it wraps, in the space's unit: units in a diagram, CSS pixels + * elsewhere. + */ maxWidth?: number | null; /** The CSS font family list its texts are set in; the theme's font unless set. */ font?: string | null; @@ -1012,14 +1126,29 @@ type Label = ### Size ```ts -/** A size: CSS pixels, or position units with `units`. */ -type Size = ExprInput | Units; +/** + * A size, in the space's unit unless it says its own: units in a diagram, CSS pixels elsewhere; + * `px` keeps CSS pixels, and `units` takes position units, which scale as the view zooms. + */ +type Size = ExprInput | Px | Units; +``` + +### Px + +```ts +/** A size in CSS pixels, whatever the space's own unit. */ +interface Px { + px: ExprInput; +} ``` ### Units ```ts -/** A size in position units, so it scales as the view zooms, kept within CSS pixels when given. */ +/** + * A size in position units, whatever the space's own unit, so it scales as the view zooms, kept + * within CSS pixels when given. + */ interface Units { units: ExprInput; /** CSS pixels it keeps at least, however far the view zooms out. */ @@ -1031,7 +1160,7 @@ interface Units { ## Expressions -Expressions as JSON: a constant, a field, or a node of these kinds. +Expressions as JSON: a constant, a field, or a node of these kinds; and colors, which take CSS strings where values take field names. ### ExprNode @@ -1054,14 +1183,24 @@ type ExprNode = | ColormapNode; ``` +### PaintNode + +```ts +/** + * A color for every row of a mark: a CSS color, a value through a colormap, or a conditional + * choosing between colors. + */ +type PaintNode = Color | ColorNode | ColormapNode | WhenPaintNode | MissingNode; +``` + ### FieldNode ```ts -/** A field of each row: of the mark's own table, or of another dataset's table joined by row id. */ +/** A field of each row: of the mark's own table, or from another dataset's table joined by row id. */ interface FieldNode { field: string; /** The dataset and table the field is read from, joined by row id. */ - of?: [data: string, table: string] | null; + from?: [data: string, table: string] | null; /** Reads a sampled field at its last frame without interpolating. */ step?: boolean; } @@ -1122,6 +1261,16 @@ interface WhenNode { } ``` +### WhenPaintNode + +```ts +/** A conditional's color: the first case's whose condition holds, else `otherwise`'s. */ +interface WhenPaintNode { + when: [condition: ExprInput, color: PaintInput][]; + otherwise: PaintInput; +} +``` + ### IsNode ```ts @@ -1134,7 +1283,7 @@ interface IsNode { ### ContainsNode ```ts -/** Whether a text field holds the text, ignoring case: read by queries, which hold texts. */ +/** Whether a text field holds the text, ignoring case: in a mark, a field of its own table. */ interface ContainsNode { contains: [field: ExprInput, text: string]; } @@ -1412,15 +1561,40 @@ interface Query { offset?: number; /** The most rows read; a limit picks them in linear time before sorting only those. */ limit?: number | null; - /** Fields to read by name: a field as it is, anything else as numbers. Every field unless set. */ + /** + * Fields to read by name: a field as it is, anything else as numbers. Every field unless set; none + * set reads the ids alone. + */ select?: { [name: string]: ExprInput; - }; + } | null; /** The axis coordinate sampled fields are read at; their last frame unless set. */ at?: number | null; } ``` +### EnvelopeRequest + +```ts +/** + * Rows' least and greatest values of a sampled field over equal spans of the axis, to read back: + * field `field` of table `table` over `spans` equal spans from `range[0]` to `range[1]`, of every + * row or of the rows `ids` names, in its order. Plain data, like a query. + */ +interface EnvelopeRequest { + /** The table read. */ + table: string; + /** The sampled field read. */ + field: string; + /** The axis coordinates the spans cover, from the first to the last. */ + range: [number, number]; + /** How many equal spans the range is cut into. */ + spans: number; + /** The rows read, in this order; every row unless set. */ + ids?: Ids | null; +} +``` + ## Results What views tell: events, hits, descriptions, and what frames cost. @@ -1428,7 +1602,7 @@ What views tell: events, hits, descriptions, and what frames cost. ### ViewEvent ```ts -/** What happened in a view, for the application to act on. Drain them with `View::events`. */ +/** What happened in a view, for the application to act on, as `view.on` tells it. */ type ViewEvent = | { type: 'click'; @@ -1497,7 +1671,7 @@ type ViewEvent = }; } | { - type: 'hover'; + type: 'hovered'; detail: { /** The row now pointed at; `null` once it left every row. */ hit: Hit | null; @@ -1506,7 +1680,7 @@ type ViewEvent = }; } | { - type: 'select'; + type: 'selection'; detail: { /** The rows selected now. */ rows: Rows[]; @@ -1553,6 +1727,18 @@ interface Hit { } ``` +### End + +```ts +/** A vertex a connection ends at, and its port when it has the one the connection meets. */ +interface End { + /** The vertex. */ + row: Row; + /** The port, by the reference field naming its net. */ + port?: string; +} +``` + ### Failure ```ts @@ -1720,11 +1906,6 @@ interface Stats { tiles: number; /** Tiles being fetched or decoded, of every tile source. */ tilesLoading: number; - /** - * The work under way the last frame stood in for; nothing when it drew everything as the data, - * scene, and camera say: see `View::complete`. - */ - waiting: Waiting; /** Milliseconds the last frame took to record on the CPU. */ cpuMs: number; /** @@ -1759,6 +1940,16 @@ interface Work { written: number; /** Rows whose channels the GPU evaluated. */ evaluated: number; + /** + * Rows of edges and faces whose ends the GPU found where their vertices are held: rows that + * changed, and every row held where the vertices they end at moved. + */ + resolved: number; + /** + * Bytes of pages of words per row the GPU cleared: those read before anything writes them, and the + * normals faces sum afresh. + */ + cleared: number; /** * Rows packed into pages of the order a table drawn in part draws in: each page once, the first * time a view draws it, and again only where a write changed its rows. diff --git a/npm/README.md b/npm/README.md index 7db8d906..39196c02 100644 --- a/npm/README.md +++ b/npm/README.md @@ -4,10 +4,10 @@ Columnar data drawn as vertices, edges, lines, and areas with WebGPU. [Documentation](https://latkit.readthedocs.io). ```ts -import { loadLatkit, Patch, field, type Scene } from 'latkit'; +import { Data, Gpu, Patch, field, load, type Scene } from 'latkit'; -const { Data, Gpu } = await loadLatkit(); -const data = new Data('grid').apply( +await load(); +const grid = new Data('grid').apply( new Patch().replace('Bus', [101, 102], { x: [0, 30], y: [0, 20], kv: [230, 115] }), ); @@ -25,30 +25,35 @@ const scene: Scene = { ], }; const gpu = await Gpu.create(); -const view = gpu.view(scene); -gpu.canvas(document.querySelector('canvas')!).show([view]); -view.setData(data); +const view = gpu.view(scene, { sources: [grid] }); +gpu.canvas(document.querySelector('canvas')!, { layout: [{ view }] }); view.fit(); ``` -- **Plain data needs no WebAssembly.** Node/server code can import `latkit/core` without DOM types. Scenes, expressions, and patches are built without loading - anything, so a worker or a Node server writes patches as well as a page. `loadLatkit()` loads the +- **Plain data needs no WebAssembly.** Scenes, expressions, and patches are built without loading + anything, so a worker or a Node server writes patches as well as a page. `load()` loads the WebAssembly once per page; `Gpu.create()` requests a WebGPU device. +- **Views draw sources.** `view.sources` names the datasets a view draws: a `Data` at its latest + version from the frame after each patch applies, a `Snapshot` as it is, or `Tiles` fetched in + pieces as the camera and plot window ask. - **Data changes in place.** `data.apply(patch)` makes every change or none. `data.snapshot()` keeps a - version in O(1), sharing its pages, as an undo history does. A view draws the version it was - given until `setData` gives it another. + version in O(1), sharing its pages, as an undo history does; `data.restore(snapshot)` brings it + back. - **Patches are bytes.** A patch is a frame of latkit's byte format, written in TypeScript as it is built; servers in any language write the same frames. `Patch.stream(body)` yields them as they - arrive, and `Data.read(bytes)` reads a dataset's bytes or GeoJSON. + arrive, and `Data.read(source)` reads a dataset's bytes, its JSON, or GeoJSON. - **Data reads back** without a GPU, through the expressions marks draw with: `data.query({ of, filter, sort, select, limit })`, `data.describe()`, `data.envelope(...)`. - **Types come from the schema.** Scene, query, camera, and fit types (`inputs.ts`) and events, hits, and stats (`outputs.ts`) are written from latkit's JSON Schema; `latkit schema` prints it. +- **Waits end at a signal.** Picks, locations, layouts, pictures, and encodings take `{ signal }` + and give up after 30 seconds unless it says otherwise: with `timeout` when it timed out, with + `cancelled` when it was aborted or the view went. - **Errors** throw a `LatkitError`: a `code` (`invalid`, `unsupported`, `limit`, `io`, `device-lost`, - `cancelled`, `timeout`) and, for invalid input, the `path` to the part. -- **Handles** (`Data`, `Gpu`, `View`, `Canvas`, `Image`) have idempotent `destroy()` and - `Symbol.dispose`. Picks, locations, and layouts reject with `cancelled` when their view is - destroyed, and with `timeout` when no frame draws it in time. + `cancelled`, `timeout`, or `unloaded` before `load()`) and, for invalid input, the `path` to the + part. +- **Handles** (`Data`, `Snapshot`, `Gpu`, `View`, `Canvas`, `Image`) have idempotent `destroy()` and + `Symbol.dispose`. Workers start when a view first needs one, share the compiled module, and retire after 30 seconds idle. For your own asset hosting, pass the matching `latkit/wasm` and `latkit/worker` exports; the @@ -58,7 +63,7 @@ worker is a standalone script, fine as a Blob URL: import wasmUrl from 'latkit/wasm?url'; import workerUrl from 'latkit/worker?url'; -const kit = await loadLatkit({ wasm: wasmUrl, createWorker: () => new Worker(workerUrl) }); +await load({ wasm: wasmUrl, worker: () => new Worker(workerUrl) }); ``` Serve WebAssembly as `application/wasm`, in a secure context for WebGPU. diff --git a/npm/docs.mjs b/npm/docs.mjs index b330c6d3..028b08ab 100644 --- a/npm/docs.mjs +++ b/npm/docs.mjs @@ -27,8 +27,11 @@ function declarations(file) { .printNode(ts.EmitHint.Unspecified, node, source) .replace(/\bexport declare /g, '') .replace(/\bexport /g, '') - .replace(/^\s*(?:#private;|private .*;)\s*$/gm, ''); - found.set(name, signature.trim()); + .replace(/^declare /gm, '') + .replace(/^\s*(?:#private;|private .*;)\s*$/gm, '') + .trim(); + // An overloaded function's signatures, each in turn. + found.set(name, found.has(name) ? `${found.get(name)}\n${signature}` : signature); } return found; } @@ -58,22 +61,29 @@ function page(head, files, sections) { const pages = { 'api.md': page( - '# JavaScript API\n\nGenerated from the shipped declarations. `loadLatkit()` returns `Data`, `Gpu`, `Shot`, and\n`colormapCss`; builders and patches need no WebAssembly. Options take the [scene and result\ntypes](types.md).\n\n', - ['load', 'patch', 'scene', 'runtime'].map((name) => `npm/dist/${name}.d.ts`), + '# JavaScript API\n\nGenerated from the shipped declarations. `load()` loads the WebAssembly that datasets, views,\nand pictures are made with; scenes, expressions, and patches need none. Options take the [scene\nand result types](types.md).\n\n', + ['load', 'internal', 'data', 'patch', 'scene', 'colormap', 'gpu', 'view', 'canvas', 'shot'].map( + (name) => `npm/dist/${name}.d.ts`, + ), [ [ - 'Loading', - 'Load the runtime once a page.', - ['loadLatkit', 'LoadOptions', 'Latkit', 'LatkitError'], + 'Loading and waiting', + 'Load the WebAssembly once a page; every wait ends at its signal.', + ['load', 'LoadOptions', 'Wait', 'LatkitError'], ], [ 'Data and patches', - 'Datasets of named tables, changed by patches written without WebAssembly.', + 'Datasets of named tables, changed by patches written without WebAssembly, and what views draw them from.', [ 'Data', - 'Table', + 'Snapshot', + 'Tiles', + 'Source', + 'DataSource', + 'QueryResult', 'Envelope', 'Patch', + 'PatchSource', 'RowIds', 'Values', 'Geometry', @@ -85,10 +95,15 @@ const pages = { ], [ 'Expressions', - 'What varies by row: builders writing the expressions scenes take.', + 'What varies by row: builders writing the expressions and colors scenes take.', [ 'Expr', 'ExprInput', + 'Mapped', + 'Missing', + 'Paint', + 'PaintInput', + 'Colormapped', 'Color', 'field', 'color', @@ -96,8 +111,11 @@ const pages = { 'when', 'When', 'Then', - 'Mapped', + 'Next', + 'InputOf', + 'px', 'units', + 'Within', 'selected', 'hovered', 'pointer', @@ -111,12 +129,24 @@ const pages = { [ 'Views', 'A GPU makes views, and canvases that show them.', - ['Gpu', 'View', 'ViewEvents', 'Canvas', 'Panel'], + [ + 'Gpu', + 'View', + 'ViewOptions', + 'ViewEvents', + 'Insets', + 'Point', + 'Position', + 'Canvas', + 'CanvasOptions', + 'Panel', + 'Rect', + ], ], [ 'Pictures and video', 'Shots draw views into images and video.', - ['Shot', 'Image', 'ImageFormat', 'Video'], + ['Shot', 'ShotOptions', 'Image', 'ImageFormat', 'Video'], ], ], ), @@ -126,7 +156,7 @@ const pages = { [ [ 'Scenes', - 'A scene: its space, its marks, and how it looks and answers input.', + 'A scene: its space, how it answers input, and how it is laid out.', [ 'Scene', 'Space', @@ -139,9 +169,6 @@ const pages = { 'Tick', 'Format', 'Grid', - 'Theme', - 'Flow', - 'Shadow', 'Controls', 'Motion', 'Zoom', @@ -153,6 +180,25 @@ const pages = { 'Budget', ], ], + [ + 'Themes', + 'How a scene looks, a group for each kind of thing it draws, and the lengths its options take.', + [ + 'Theme', + 'TextTheme', + 'VertexTheme', + 'EdgeTheme', + 'LineTheme', + 'AreaTheme', + 'FlowTheme', + 'FocusTheme', + 'DiagramTheme', + 'ShadowTheme', + 'EarthTheme', + 'PlotTheme', + 'Length', + ], + ], [ 'Marks', 'What a scene draws each table as.', @@ -168,19 +214,20 @@ const pages = { 'Bitmap', 'Port', 'Side', - 'End', 'Route', 'Interpolation', 'Label', 'Size', + 'Px', 'Units', ], ], [ 'Expressions', - 'Expressions as JSON: a constant, a field, or a node of these kinds.', + 'Expressions as JSON: a constant, a field, or a node of these kinds; and colors, which take CSS strings where values take field names.', [ 'ExprNode', + 'PaintNode', 'FieldNode', 'ColorNode', 'StateNode', @@ -188,6 +235,7 @@ const pages = { 'UnaryNode', 'BinaryNode', 'WhenNode', + 'WhenPaintNode', 'IsNode', 'ContainsNode', 'ScaleNode', @@ -204,7 +252,7 @@ const pages = { [ 'Rows, cameras, and queries', 'Naming rows, framing them, and asking for them.', - ['Id', 'Ids', 'Row', 'Rows', 'Camera', 'Fit', 'Query'], + ['Id', 'Ids', 'Row', 'Rows', 'Camera', 'Fit', 'Query', 'EnvelopeRequest'], ], [ 'Results', @@ -212,6 +260,7 @@ const pages = { [ 'ViewEvent', 'Hit', + 'End', 'Failure', 'Phase', 'Trigger', @@ -238,6 +287,11 @@ for (const [name, content] of Object.entries(pages)) { ...(await prettier.resolveConfig(file)), parser: 'markdown', }); + // The reference names things as npm does: no path of Rust's, and in the types the schema writes + // from Rust's documentation, no call of Rust's. + const rust = name === 'types.md' ? /::|\w\(\)/ : /::/; + const leftover = formatted.split('\n').filter((line) => rust.test(line)); + assert.deepEqual(leftover, [], `${file}: Rust's names in the TypeScript reference`); if (check) assert.equal( fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), diff --git a/npm/examples/gate/gate.ts b/npm/examples/gate/gate.ts index ef2f8f4e..271cfd6a 100644 --- a/npm/examples/gate/gate.ts +++ b/npm/examples/gate/gate.ts @@ -31,6 +31,18 @@ const FIRST_FRAME_BUDGET = 1000; * vblanks slip one at a time. Nothing compiles on Chrome's GPU thread, so no frame waits on latkit; * what it compiles where first used is counted apart, as `work.late`. */ const MISSED = 1; +/** Milliseconds between display frames past which the gate tells what the views were doing. */ +const LONG = 50; + +/** A gap between display frames past `LONG`: how long, each view's last frame on the CPU and the + * GPU and what it waited on, and the work the GPU did since the frame before. */ +interface Long { + gap: number; + cpuMs: number[]; + gpuMs: number[]; + waiting: string[][]; + work: Record; +} const here = path.dirname(new URL(import.meta.url).pathname.replace(/^\/(\w:)/, '$1')); const examples = path.dirname(here); @@ -214,14 +226,14 @@ async function settled(page: Page, budget: number): Promise { await page.waitForFunction( () => { const w = window as unknown as { - stage?: { views: { stats: { frames: number; waiting: string[] } }[] }; + stage?: { views: { stats: { frames: number }; waiting: string[] }[] }; opened?: Set; refitting?: Map; }; const views = w.stage?.views ?? []; w.opened ??= new Set(); w.refitting ??= new Map(); - views.forEach(({ stats: { frames, waiting } }, i) => { + views.forEach(({ stats: { frames }, waiting }, i) => { if (!waiting.length) w.opened!.add(i); if (waiting.join() !== 'fit') w.refitting!.delete(i); else if (!w.refitting!.has(i)) w.refitting!.set(i, frames); @@ -250,28 +262,62 @@ async function visit(url: string, id: string): Promise { }); page.on('pageerror', (error) => failures.push(`page: ${error.message.slice(0, 300)}`)); // The longest gap between animation frames since the last reset, and the display's frame - // interval: the median of the latest gaps. - await page.addInitScript(() => { - const w = window as unknown as { gap: number; resetGap(): void; interval(): number }; + // interval: the median of the latest gaps; and each gap past `LONG`, with what the views did. + await page.addInitScript((long: number) => { + interface Stage { + views: { stats: { cpuMs: number; gpuMs: number }; waiting: string[] }[]; + gpu: { work: Record }; + } + const w = window as unknown as { + gap: number; + long: Long[]; + resetGap(): void; + interval(): number; + stage?: Stage; + }; let last = 0; + let work: Record = {}; const gaps: number[] = []; w.gap = 0; + w.long = []; w.resetGap = () => { w.gap = 0; + w.long = []; last = 0; }; w.interval = () => [...gaps].sort((a, b) => a - b)[gaps.length >> 1] ?? 1000 / 60; const tick = (now: number) => { + const stage = w.stage; + const done = stage?.gpu.work ?? {}; if (last) { - w.gap = Math.max(w.gap, now - last); - gaps.push(now - last); + const gap = now - last; + w.gap = Math.max(w.gap, gap); + gaps.push(gap); if (gaps.length > 120) gaps.shift(); + if (gap > long && stage) { + const delta = Object.entries(done) + .map(([kind, n]) => [kind, n - (work[kind] ?? 0)] as const) + .filter(([, n]) => n > 0); + w.long.push({ + gap, + cpuMs: stage.views.map((view) => view.stats.cpuMs), + gpuMs: stage.views.map((view) => view.stats.gpuMs), + waiting: stage.views.map((view) => view.waiting), + work: Object.fromEntries(delta), + }); + } } + work = done; last = now; requestAnimationFrame(tick); }; requestAnimationFrame(tick); - }); + }, LONG); + /** Tells the frames past `LONG` since the last reset, while `doing`. */ + const tell = async (doing: string) => { + const long = await page.evaluate(() => (window as unknown as { long: Long[] }).long); + for (const frame of long) console.log(` ${id}: ${describe(frame)} ${doing}`); + }; const opened = performance.now(); await deadline(page.goto(`${url}#${id}`), STEP, `open ${id}`); /** Waits until every view has drawn more than `frames` frames, for at most `budget` ms. */ @@ -303,14 +349,17 @@ async function visit(url: string, id: string): Promise { await deadline(settled(page, STEP), STEP + 1000, `${id}'s opening`); await deadline(page.waitForTimeout(500), STEP, 'settle'); const opening = await page.evaluate(() => (window as unknown as { gap: number }).gap); + await tell('opening'); let longest = { gap: 0, step: '' }; - /** Does `step` and waits two frames; keeps the longest gap between frames and its step. */ + /** Does `step` and waits two frames; keeps the longest gap between frames and its step, and + * tells each frame past `LONG`. */ const act = async (step: string, action: () => Promise) => { await page.evaluate(() => (window as unknown as { resetGap(): void }).resetGap()); await deadline(action(), STEP, step); await frames(page); const gap = await page.evaluate(() => (window as unknown as { gap: number }).gap); if (gap > longest.gap) longest = { gap, step }; + await tell(`on ${step}`); }; const canvases = await page.locator('main canvas').all(); for (const [c, canvas] of canvases.entries()) { @@ -356,10 +405,9 @@ async function visit(url: string, id: string): Promise { } catch { // What the views waited on, and what the page said, name why. const said = await page.evaluate(() => { - const views = ( - window as unknown as { stage?: { views: { stats: { waiting: string[] } }[] } } - ).stage?.views; - const waiting = (views ?? []).map((view) => view.stats.waiting.join('+') || 'nothing'); + const views = (window as unknown as { stage?: { views: { waiting: string[] }[] } }).stage + ?.views; + const waiting = (views ?? []).map((view) => view.waiting.join('+') || 'nothing'); const status = [...document.querySelectorAll('.status, [role=status], output')] .map((element) => element.textContent?.trim()) .filter(Boolean); @@ -396,6 +444,18 @@ async function visit(url: string, id: string): Promise { return failures; } +/** A frame past `LONG` as the gate tells it: what each view's last frame took on the CPU and the GPU + * and waited on, and the work the GPU did, bytes in MB. */ +function describe({ gap, cpuMs, gpuMs, waiting, work }: Long): string { + const ms = (list: number[]) => list.map((n) => n.toFixed(1)).join('/'); + const waited = waiting.map((list) => list.join('+') || 'nothing').join(', '); + const bytes = new Set(['uploaded', 'written', 'cleared']); + const done = Object.entries(work) + .map(([kind, n]) => `${kind} ${bytes.has(kind) ? `${(n / 1e6).toFixed(2)} MB` : n}`) + .join(', '); + return `frame of ${gap.toFixed(0)} ms (cpu ${ms(cpuMs)} ms, gpu ${ms(gpuMs)} ms, waiting on ${waited}; ${done || 'no work'})`; +} + /** Serves the suite and visits `only` its pages, or every page its bar lists. */ async function suite(only: string[]): Promise { const server = await createServer({ diff --git a/npm/examples/gate/package.ts b/npm/examples/gate/package.ts index b2e88618..62b631a9 100644 --- a/npm/examples/gate/package.ts +++ b/npm/examples/gate/package.ts @@ -25,15 +25,6 @@ execFileSync( [path.join(root, 'node_modules/typescript/bin/tsc'), '-p', consumer], { stdio: 'inherit' }, ); -execFileSync( - process.execPath, - [ - path.join(root, 'node_modules/typescript/bin/tsc'), - '-p', - path.join(consumer, 'tsconfig.core.json'), - ], - { stdio: 'inherit' }, -); execFileSync( process.execPath, [ diff --git a/npm/examples/package.json b/npm/examples/package.json index e2ff9bee..94b6b43a 100644 --- a/npm/examples/package.json +++ b/npm/examples/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "tsc --noEmit && vite build", - "preview": "vite preview" + "preview": "vite preview", + "history": "node scripts/history.ts" }, "dependencies": { "latkit": "link:.." diff --git a/npm/examples/public/history/0/0/0.latk b/npm/examples/public/history/0/0/0.latk new file mode 100644 index 0000000000000000000000000000000000000000..5fc4d2408e28129ac01f66f7ed59b126c4b07b3c GIT binary patch literal 67650 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

    iqk3-mN@vL9MI;vRC!7`@Q> zzv{y_!>x3}G55~(r03AsXAu6I^)f$yExvQ=dtLHdUUX??*xzOzX=vdXlG4>uR{`1m;g-g&L>*@H5`qtOg;tkZo{Qlo%yzvM_oP+ z)_dq9|Bd%5r0%KI9P1pOm}=NkU&H#Ue~Q~y(dYPb-6ZF9YU`82P|C9_Z;69a@SoG) z-{RH&v|4rin-32xZmc)d&6#%J?f;U!mc#ab*NR!!>D}2}_K2GTv|x?kohGpS9*<@RAZQnMTv*zvEoB+9zz? zt7;-N!v6*3n%eK6IH`(@hME6VpTny+zGPQ9y8kXZ zce(s9$T{h=`h#`E*=;aT!v1+xvG}Ii`-=J!A7(NCj~|hD<&VN*?M8a}f?vBdr(^Z2 z6a3$%55Kd?8)D}~U0S=gUapw_;x+r8X>iaI^{jgD)h^C0iRFU@Xbuli^PN#{7kcE~t-*mOawix-C zb}HV9|6%CZC-`rz{Q>d$QjVD&=>b=Fksi%=u?n{Xp$BbdK5s?l%2Mt7m}6`_x;f^woXuh<7NF&T+?_IO`T3*iCPj9cKy+&AgL=HK$UB9t~^Ks_9F_+r52t11a^>=$F=OgjN z*VX#6Yn^C_!0b#l;*&*bpVR7>TVa-ub~crV%F$mmSxMY@WRg52fA0OtHFoH?1!w$U zl3yq5Q#Mc=%6Z$Kb$@f*z1RO<3k>h|`99(*<@ZjC@7K4Rj)|6{{{|57xt*Pw#8UHVS zS$-Fjx8vNDXYf`$6$y{3*K*gCALWZhZ{g4}o_P@egJ_DI`Rxf<_c8No|Drrg(|5G? zS&eadJNpvsUm+jA;g+7`>?Q2fM^34KVf;(+(n`D^hS!}>CPEq>)om6vQa)CrH|Z< z#{Un#jVb34b3I5a;*6boip$ykR1?>dSMK_TPhXW!9+ppNoPmGS?!$bBf0xObx#i)L zIPr1|XI=13%5x}L_W7MnG1N)C-_LfV|YznB^_1|}YY|FX`VmQ=6dus`JJGDqdV>$Japm?^L0XMX+^CqBpiDSQ9( zXo8!&)xy?v>HgH7%tkRc`+xX<6dpQ4^N+RW(==E&y)xJCh?lRvh*M{ZZS{Xk_-RMq z9j-|uEuvxi(#Ah}#%-NgTH}9rsj~lV0yfGnzuac7pTTwZIsEJ1#X38esz17y4*Yo< z?SL!p#7%d=K;<_0i~gzgnZNm^y1kuTa%{wBGTcTxU9cyR(Ox^hX3`TXi-$@!akg*- z{C-OJ)2KV{*Ykeed?x6f&!=tpse*WFx7BZ=n9Cq~{F%A>-VN1Ca@Lwsbgr0tL;hd0 z(0R&*YQbmp+%mF5etovL^rgi~y1yd+H-_}~)N8q||MoY;(;l2Ie%`kydG{-x``bik z(7#RfbQhuIJ*Mz_adO`X~HR!G6k@`lDCvA^mHgj6E-W zE4Iq(RW;F9#(OKP`OPu-@9~Bj=pT7~I<6mphcn{3MRE@gJ7R5V=FkS)+5K)jA#)V?N@f!vp#s{BZ8P^TDfN1SfwZo*reFNrm75?o##; zAH|Pb2eG|aO!=*_*6sC0^6G1vd$uHV=?Z%O+9iawcZzMoeAm3`}7b2h20UOiv$p+Wb^bx-5rXAg>{*Zltf zaAQMx1IMJzYCqLFXTo+$z*Z>(UKidgV#rYEBjB{&K7VnH zxb0{@9oT;gOlQXRc;F$N(lrH()f3@ZHnsG_@}=u;IIix)VJT;K$3^rN3ySsb&KzZ; z3&htg>G)v-o%wPi+z*3)r^mhW-_kGn_yGO8*YhsF=&?B`n{w}B)?fA~xfbtcZRbqG zFxV5T6L6m&&h@)*?AlsfWW~LmX~kpK?K3yeI1lrK+_^{mh@Degm4;GgQ}>J4-aj zI^_0-bblt_4@bSXOgzc?WykXSg-G}xpLM78FP{>J-|!z>|8EIy`ahEHGTf@FYXdOd z-AH$Lmz(a8?(UYBI>3=eKn{P0kBvUsBxOeR~;>!nYkW0;D?$J0Xs*Wu58F|tra+>Klh|7|9GhI0mT;{NVAwKuFEr4G9fmXPUZ{3lBe?YJr)|1)nn0LH{v+#2E_fN5Z+eD_m<3yCJLKd|$ci zaI>9!rJg?eJWIIy>==N`3^w`_%N zqs+A|z)jT{uHw{*^W&t|*#6Dn%%=S8nVAK;iWpOk58f>83U|xk+v49QKD~RGT1g=| zUrkLuhqw+mchF&-Y|eN!!^8hkBPxx5KQ@n+z-Rg01z1+0E&Z&wPq|%o*!N^I-{T{@ z@`-tHv`2RJ@as4zei|kxZeBty87^Kq&7S$eG}o9*{u%SS&KiASHZk`+n-v2K;o}8h z*%UYv#GgoC%<%jIr_AuAg3o!x$fmWdOWa%{zKrTZMt^t~KoGxTnf1uR{tvL*Jn9+n zGLAg;xHvq8y#Gz*SsggsaG!ku4?px!TmD8ZGO1n%EZ*5rK2#NFaPQO;==qTIE?dMB zGTS5e{*xA$D~kuStxjXiNaV+D=rTBakr@BW2V3`?!1=Do^~R_L!pY`|#Q!$(fu_!( zcZFv!ar@kC)$eO0#gkXdiIUmt8~sBzunM015Z>9JwQxZEyYRyLim9dE!QsiG;Lqw` znfR&P_(f0s-|6%(|GA%5O&7N~z{a}c2AQle=2;w8?fY00oTM&ZfFmDmEWb%ChQXUO zXL0CUW&+ssLp-R%H{@|xkB0q6_!CB4<{x5Yl1KPSUb&5!xUVG+c~b40{ZD#D9>qLM zhW_84g82hHJLE?^g8a5vS3dV0gRgZE&r@%dQ?&@5K~@KBY`)h1AIcSL-ctX(|G)S4 z9iiJ`eI*;43ijl4^4Tr%=wlLGz;*vh?EmfM2oIeJXDhjIqAjrTk1}Sc_;%yWcqh9q zGg{s&hMp5ITHpj%`|HC6dzV2<^ef*e(_@nTQ-lmJF2>S2Xoc|cmkW^`^@DXYCfa&wdkj$c>F#+ zZZ=nrEx-QUGRQfcJ>+x!Vag=2>GMM|_my)(>~n(tcM^WLjr_BP;eQF$KXQ_jHS7+9 zE4+ujBRwN0znJs_*H-UeDsuVc>L&ty4`B`x`@LJ6TjH~=6e237x^;aN(WsV zrj$^x4nD)_c2D*FaQths9E*P6kpc~Z8>pGhHox#A{K^o8 zXA3=u$HDx+@@@9?MSk#rkBF1gH__!)n4bkVS`!jT@Kl_!|8-4pnV4k%Oihd&>vm;h zZ0=YqJ?RSkpDYVs(btIzKm2|6gLieBxF27&i3x~1h(s|5m zuEh1C%Z#MkT5`EM1dSb9`44c?U*TJ*=iPTRuc4 z;{Om2lfuZd^6hBqQF=X}?5p{DYr=mER#%()-p^w4%?Rc{*!PrE`fPj6*+i6s7GO6; z{hPd_(nFjiI1l?vEB43d+lS(jMGg7SQ?;n*YPjrUU`aiPuK(lz!S}^1CFA1O8~eOD zhEZZWyW2g2uRVYttzkWV#=&QfG-U(qZFma((@Xp|GW)jwNZsMXli)KW)T-Ejp|)}@ z_`ltHD=$!E5`Q0!F>8=r9Se@F^Z&$j(E0=WEX?oxZi-q$+zZ}u*}$`|>hPH(xC^XZ zSxSwf7aqc<_cxZ477O|yzYQlRISGftd$y|Y9xd(~?wcfzHR2m>=)c-b@tUn=td5^W zwFdrhauS=Pzt(f~fb6gH8uDTnMfi5T5AIQgoNqNL9)+}dtMOu5# zQ)D{(3WFw^NBztf`E>4LFphkd^V3__dY=FN9pd>|>O^tNyRQ2d!j+cMTfVUDr>2~o zj>p2`evSDRS@nCx*YJs{t;}e$n@ae9Y5p2kAL>8$@<9Fn4jFf~|19=%L_F%h$C=RY zLINeeW&dK=OEwyIz5WClF2TXR>0vf30{dk@ukCS{b+lWKd(~AtW=H3n>C=$oaK8%& z<4!aZ|F3x#OHMPw^0oT6odqGQz8A%ePU@7io%x?D=H5{MyCG)TUnhH*v)O&v+$nMk zKAWxq{dX)5M^59xm%RrNXX}2KkFmv!`Ec3q)KfO`kqBx)rS;PEdUhVSNA-d@S@rOI zexf*3^mj3#s^_1?r|Ts0_S*8iS?qeHKHL=;J_+ws7imdOHJY1UjSSymOGsY%aC36~ z-Cm2ZH~IX!hUB%H4#~e8Tj?FjM#wssy-yyUGxx+#7XtZ5yd!q0JxFFEb&9AAQ!#ZH-F zfe)E>s@v}7JO8TTRD#_3>@V z*z_HYE2$2Q@BHby;bBOS^YnyAoj=P5lGsOOToE5wO#e+`$HYu9Iyqaq9umkx#;fH& z^Pb{nGxTQ5h6JAXWIwB%e;~s>H$CT&{pKRmmSXGqBru45FL~@eybah9j<6s+`+tOY zHpT1N(6~$f4FiAqR^JJK>DfqLe$f8ywZ-4!U0r@cufdt4A@|{CPxueR_x4ez!T)}Q zsnd&#iPzPXOXAAnVzGVBD*A3Ub9X27Ddeg-*bFtJj`3tL*H2r^Cde!=&<|AfY?WHf`-XY8RavqqnOPsvLH{sP$>$~=ieB`?N=S@7Q zGMk*~EQPhNW&eXp$?xvszyuUeDhd+%{4mzCI|4jbTDn`fZ*JQ{H7c1EX96j(ZgTnIjpbz9rg+v;)op|dhma< z4>6;|F5GU#SAGhsC4II65i8)BYqzRR>G=at)x55 zS>pQPwMd=Lx zZ-p~I`qexj{qKO=iTOkD`L{E~tjwO`M%^S`JWJ-!-{2Nww4j^U}i{b=MG_ ze-6%l7SGJ4j;Kc-d5!uAU9bLyEnUH#;bDo!;;er!eW`CC)=#~uX6ZXm=%O}1EqN72 z!J-ur@XjB_r4r7Ae8E$5datJSRK@?MRnhMrCTCawDxFK-loD^9sQyd;so{R^|M;tT z8lx?q=;zGm--_d*UeGhBH!zi-LIfSgO=XfyAU%?rE;3Hq{FNfOI z8*w6__I}p$CfohlI_}V>aJh(sg~odeVui{)eDGq z_s6Owz@OR4U?>dU^hIxAhkABey>j@lTns2%kU#$;w%uT}1KImjd0Y~ojidg}*5>_= zZ^8BHZSbEBe#U33x!$Pa^4s+|3xCcSojfzir`6t)gu>VC*6~oj!rvEUfdlM6cSgCH zc$>%HdD&GH*E`Gi!i*BL$YQ^1MM@yYf;~gd;b!C#?>G2H)=M&)&;KmePtwoJ#eVzA zw+h2u*Gut?uH)$SoeQ2_k#}a-Uu4JKm#RzfhrDscyUF4ojC;u*0*n6F|EXFIM@hr} zD(N-8f$i+-WD&D6UCjEx|4jOy^Zmcvb2WhN>RRI6<0fWIQpzdhTa8NVL3PJl4&k$V z?W>Eq=5uP2eZ`Sh@b8wjJj7jU>Y-1;%@W||>>-|?#U-y;f7){rHMcE#dUW5ir(F6u zETo4D@yHObY9jtOKBK0n{!tYEg*1nEVfZAQ3eF&1jiVm=hqzG`_r~c`%Xb50@vUn$ z6!V+$zoK+mY`mGM1#056`O{&w=KK7W?4JFBU)EuBd+^14{5*)OBb;F+<6P>gmlKII zaPLAJ@7H6`FFVSKahRw>@VZH2_Epcoy~q~mv%qqHTWe=)J0$Lo;^XaL>}I&}v9;^K z-Xr|&j(wGHsD@dKfAELsa4s#M98!@##Dv?4{4;JG7g>M<^M3QuZitpmm6 zz6`~6xKYPk8NHp zABI7b;B|cWZ&05d(A$n5@vBtM9hVcQe$wB0t6svVKL6?Y*Aw~13i~0aGx}e(`1?kB zsO@L(>_2f`XZx+Yoj7sPo@-Yl|0QNM^;5pU*WG-iX&G^KG8>Oi?jzZ>xsnD2)!^fK zUMiXFjcoSrcQc`E{l^*J9Z(eqt*rK%-~Q|)_-yGB$;B>wJB)R-B$Ge~{zFe+;amC= zuGLcPnI;xC}}+4)aPowu;3OUrI~zQyj2m?pZ!WMCSa=Nf{gfSlC8IlJ7fB4hG{~8? z8|;kz$EVlP>G8(b`oNLY3XXbD#YVQ<9RAfTpguMRhmrHGe5!^qUwnBePfF|z z!%*>TwD~{lz9Kdzm4~&e03YDr`myTmu&3o*b_eh3;*J-h;o>E%F)ptEM7(W=lU@x8 zth;Pa->JDi6YJ)|%L$(A2Cv`f33iANhsvvA(pOowR584sKAX?nRX*86Prj)=43<+} zSBnu_ejDxo=k@g1!HpBKt@rB@X zQn$j}XkTH^aj`b9-WdO%zEK}-qnNT4E@iSG_Z&nIyT+Pz&L9`%OXFR?8u=A-k2JN^ zCPMN5%rI|(9G^XJjmkdZ?TekB$v=o67t)6$lSc4U5_PS4S?p=Fp1-wMbM0KK&D|Y@;nVcb>NtniQ*EM**|@IiA2out zBlCLVZGi}K*PrwOkFc2d;Ilfjx_9)~Fa%$(echQLKi?AYx#8vxubOj#Ej!plwgGY!{AC0Er&kjg z+e<8lr@!gTES^B3QG9(-f@+d6#hAdZue->h~H zaIHL{7`+x@m&w?)d?ojH@_+K~2bVrt&p|d@uOWPDDkh2@NhaFU3HgD0RcC|2GpFO< ziDTvUyYZ7T?fEGE6vkI3!1Jp|)W7NCpMIY6&|81?9n89-XU#``g}Y;Mj$YHrE1LIB zwk99){52meSPR?P>ZJ|#1lu#)M`!D~Qccc`7k}@BZ$xFLh#G2>^18N zQ&yelD|h5^@aV)syp`|o8b;?Z;U}10qBCD%OL5NXqs4_Q;=p)vT};2jsky;mS@i?6sA$UEfqul&QJ&wcbo2B$4GLmh1T)Fy?KtcAgJA+)C z@0a?_ZfomzmK87AL}I=ZIS!tJle8nhV(cph{#P)b+W&p~&S|cVuG6aj7UeUM@SMNL z;Cy3oD}Hq5n!G#M`>5-j3>!+%RyW`<7e9#K@Ne^E^}0XRT3Y;XcKBflI)sTYBeQX9 z&bu6sT+b)0wf$o}=@0ywt<3P9kYb)k@r_<|TX8p_S3o|k=&4!?vYm@_u9E8<3km!P zn`75!AAG97cJDvwq;JTEzVOHN)yxO;uR^X_X(AmzR13qs?!)48aJB9S{*^$Ti#}7Z z;qxiQ&jg+cn3&D;y=9d`0vY@9pJryP-J{j_>UOZdCO>;3j&_eGpIQman}h^@YGgi@ zj1u0%b=m*YJmR{&9fD<(!#LA*g8rlGFZ=#UxyOQCdIbyF${KxAn6mn^c?k0Si|&pk zR<~sTpQ1a92D=mDEV*x}apzalW%D~;!n$AdO`hZb`hd0g-LP{?~Uuz|QR*Y$JCDH5^!>*6=Ni84pM#@;6;*Z+;d zj@j3nB`_c*JZ=jU+`o(Oyk&e&p zmiNAfl{I|^uSlB(7g2NTe@ATIh69qxt4VyORcPQ6{XdH2EKWVOLNP1MF*OQ0yie|x z*#9PWJqvb6{YFj6o`>=EoX6Bkrl{j*A{TaYu!uGNW)5k(=V**DPq_gm{VZ;1bQ`Kq6@Wn2HTHo~a`CcIJcY-8T!a{P#n+ zn2w*~|IrJnr`S&%_0L0RVDTif%%P{t{+HrTTe9Of+4MH!$vxQHg?Mt3)#_3D-32bd z4j7x^BOHB#<2+)EvFRPwR~649ICHWry=#gu@ztQSv=i4>;0Z9ZLOs2O_PFn6dpx3! zZ~!I_(__ZNujl1U17K@Z=blgM6IR2&`M^uIzoex8MC9}fXP%gTlpnR_ zYu0d`9A?f}x91zZ#r{a_IB_{=@$bpG=xe%r74`iv>LSHp$>AVBV|T&#zm!=ezY+I_ zv(Pk&Q~mF)K2auT582By@uW>wR*WQaK@V6$3od^3v zZSA2rhok;+RIQW`O+4!?({=XN+uT?TvdV6T3+6_>g>RB?@V%aSimFd-GXLKq5%zY}jF3{w+N~QBxzoi4IYJO)3d7((4ykoK3eJw| z|FY?9p=!(jIlsr>5=Cd1d|GLb^9bf@5GzL`!Dv|!;H-j!S!3y1^e$anr_3GVYn|Rl>dAXulm_*BYv0` z#yu^rW?f3``-yJo>afqAg*!1yupKy2hF;R~^?~eYadUHtF!OV9+yL*IoRwXKsA1CW z&@`THB3}#FQtd>Hh(Ac}F{8Yl%pdUGf*tH%pZZWceE+;$Zv-1`q_!Og-*e5k5Aikr zpPNs7x(ypH>FL1eH}7y{n2?ve3W+(vJiPQ9F)WvSWFV}I!MB3;;%A@N+RrjI0d|toPGPeB4fF z_)qBr=4bd_N4Z%Y81gN7N9YW1`R%5bYQlH;Jq&qwg8m=K@A>Tu_|z;So3f8~?wOR_ zb}qoB*!}oEp1JYvf6spnH%=dlUtepD6ZGK!+dLzxfIjzebwjdkl{`4VBS#ozR<8`M z%kKB*#^tK;KlZmnJUUGmIjZtCdtOGqKlFzG1H|x}YP2)ytBZaj?lbjYIoTDuwyq)j z^)=xCs??slos-_#^Cj}F+|ydZ;y$tPM0l29r1x`Pp|hDd6HfQFs#@S;e4A`@@c*M> z%n%(^ui%4SKjPl>ADo38q5c!wn$qDO_x}?An$ZES_kiwJB!z$Z)MMFymfC9V?|0CGt(WL66@Tb)>y6Hfk--O&lpR=EmQQ#PUKOg>2!siaLw}s{KKt8jMUoW}A&v1G?aojAj3w!l_No@EP`6ito*YGgaY4 z$T9VpCF&pD@E>;g;i4JP!8a?8Kkr(4d=jA=c*iZ!X z);H-Tj?X6xas&P}DI}0tzcPn-HRxyc(zp2f6f%HQNA9vOdabSgGqONvAXX#mCbQ#W z$#6U~5{!$Jiu{-B(aDc`lKtUJY%c>IgZG4z$7%k&%LOlbJM(hbEbR`woG--inGFlobL4^@xeE72+sA+Ey$j^S z@U#-?N+Zzi;FdpXGKnaAGmy>N@X#hQZ$##K-!GQ;AZYVu_?G>+mPVU>(KhDCx;4?tqvfl+?ypxE%3?!3u z>~bwx^lYL|-B*o{FRfq~tK#sz&1$pOHy;1#tG2li|GN{7uja<@lE7-X{LZ~zyZ4ea zd^DajTlQbB5?@}ZzHVJt>+=6$>Po3yqK zFf-db&$-mwA6yszpQ!)G){d2x!$yQt@b$L`YESkbiU*W!$VTij4F4Y`E(hyHaem~F zY^cw9xhnaGp9bus-{)08A zzUp6*;a{M*x=srDbZvMM8CPeoM`7$GwbSjeEhK@OAPoI>i5?SQEj3*Ji|Yr^U(Ni) zPslZVLpdy-c(1qTu>Q&ZhQiVhe7g<~LjJYc?pNPW)n9*Ie432wbs0i0>@*VJD>ciU zzH45zzMIQDBhTLrf7V-*7Y$?cS8$$mh0NFB8kNekiKoH5SFD~7OZdR3cw(vhw{wkM zd(|)ghGQ`J1rC>JmpzKPJ)?=?xKX4Z)f&p;^Kzn(?O|gY*Apu?iwP}p$Tj>Vc7Ai< z`qh7T1`fgSAX}#G&>|hk!)MvfMVu&kN4e8Xx$Sr6oU7taiD0F8IG0@0;HA|w zd!`eAOPGc)x^AA;V!;^u9L|Q>!(Z$z8vDsUA2tWzl3cbqdu&-z-dRO&=X-UDRO-%r zZ}V$)e%G5wcV`ReZ@ghYY^iElIn#EZX~TEYi6dF%mgB^xEo3W(RN()a9=V_EZAptS ziHUJ8u?Idk6fS?s=h;Ruc9R%c_Z9!)8`C$+|Jcz+b2tU*vFJ_hT`-Z6vu5zeW@{W4hok7H(qA-ULkeD<1R z1zW`*roDhwvz_m#ju*p(AN(x%PVjp4J#_?K?-vtrundWD?$PYBKb!L16Fq!qk$dbC z^DEFXpBfg6f5Nx}DPgMow@75%BZt|2`2S@IPAHx(5M!p}lZ^_9o%Yjnow)M~5BgDj zxMl{QP27k7PwD?xm@>Yxy6-Ue`%^FF8rz}MJzwcdjQJ;(b4E+qb~16*o;uv|{A=H7 zh$Hv1|AV{r#&USB?__5(r{TZ+CVvw-FF%Ymk)5t`wrVvW=HK0{fG7P;O~7(BSPo`8E~^Z#I9U?YDme9nF0|M?U0fAKH&#sBq` zuaAJ)xXB&J_q!IEk5rMX$P-V%x`E=*caiuK`!CWF4t5r=`TC!}y9V9_=QH+^$(I*up&R|o z-V08mKY7&QC$Q&(IN`IqL-3XnY6Q4QaGtk5{+wcmnhyN0f5|&o;p_Ab@~ijeE}p^Z zq3r9l=Yq12`Ge(UV$Ifa@&jCPhwC><&OhuYRg%!a`rpJY`bfXeoE&U;oe&32FRzwA zgb@oH^pGRb@sKU_-d9bCZ(JE|{;@0^%0-v(D*P!q%MIsEZix3&&HuUgA@*J!&PKTh zPh$l8%Vv4_<4RaHS?vGSnjX8)^G^CI@8#pLq9uE*y+}@+S1qTIzg=$@yzALVzCyQC z*!ESJnl&AKzalmi3C@(uQ~uD;xsUhE_1xL_?2#^SHP=&yk2iiXHTSyHgCB7b)t@a4v&t&tos`_O{xmA15&mp_j z@UQa|b(SV<2G@zqFNfp2Ra>j^m(VZWukZJQeCQ?Fbn*BN|2V+^{W%v9gT%|#57Yz6 zDzRE;veV)Uju4;kjk#tx-?*k%G*~z9n4;pP?K=u65k! z13#&kgudC!cJgdeTFiQ-cDYq~r;K2b{`;GH8{(V9H zGZ581n|bbNK{@^Rcd5-aEKbsSC_fM-^9^z_8#uHiGq_i2Q#D4z`xP{hbRZ zEsmee=C|KiFN|2#mj5r7FMMT__tgOH|1MvzcL4uM&HtV4y_eTD$@P6i*fCXJl2e@L z5AToDK|`2*15WkA<(kqdU$4R*KiOmFAH;`I?4PZ!y`om}Gt3spI@tGtbGVzhyv ztQY^H%7Y84;U1Q^!T+0h)|;2sy4!p${~xg&CxlCBBgkRkboz~QPxa6FaAKS|aFU#A z_AsA>haTtm-T8WfoMNl%H%V=u&CDO%k#o||Gr#Y$USGPX9yvQ&UYAd^x6%343gY1V z{pn*F+*Z#H;>-jxEme34%2eF{;&;00)euA@7iJG@m0Q7 z3KvMTLLcNWH7Ii4CI*)GyG-Ko&sq4}0`p67_!3;I{n`A_V|vNy{Qo04(FFV@niyGmAa@E#@`I)#=z6hL!iIwbW=4p8JNStJ^2YkK& zpUn0nu5-+Dw7!?uUKa~sPplZYObYfAja}u&ix!Jf_r*5#+L6|}y^H<*3tI!Qvkm?y z_S9XX_I}v&C~<`GKZ&i*3{;t?H*?MdHP>`WFh=dyhkp`R-@Mo&Q;(z_{5VNa4mwm z%3q!@POrz>slz?N>Et;{`iTvz`7OO{hSwc&qNSOg!?FHL9o2~Qz&tVgN_V)tEXeEi zFJha=Ua98$1vm2Z`tILpEgbUiGON@C;`;0b{k56)non*gr)(QnpG$o%tNToz>$(Ze zq*P}s6~u4cU@mOkq-OhP7Vn7Vn?;w~R~`P?S8t;n8@K-pU**IzS^%!H<-cKONcq8Kaww>`+KCB@IT>w7r8($ zn4Ve;u>Xm_v7hd+ET8vw*JC&0^>TVTY|V@KXbEenkXhZu^@GpbIR^i0U8aLZ@Gib{ zFV;3Gk~wI;wfb9mPigotRlJSkx_5)T6OR@DAKQOVxVmb&{Og3+@;#oemtO3s{G*&_ z9oClr-gkZb%0ZvSehv-P!%LR0^js!({%vbq)&BdyiwNJ4iCXO`aq_h|e4wyAWvBdp zsQdqER@}Y6u=lkvp(g)rUDoGd=e_p!b&RdBkt*!(qTk&R2TqFJ$LheXT4b~zAHXRN z{lcD3yMGN>#LpYz6cum6QgxSLPII!MIt2a}JkNULTm7hk;z)D)fd7yF6@#+ixZ;0= z+weA*+9z9ic1QmQj!!(#KCOMn0M}*zNzKdWdqO4;*!Ty1efxja25%C_dro#9vLZRQ z$0Y`xN zAbpSdM_pjJ{3pBTthm>W1a!pz-?-nTz|h^pJE?>r=K$FKa1GRZnnOxg?YGzd?^;)zk@S!!hz+TNnkT!;<@%& zf9yXuPPfAKYx30vzMJ{9o<$DW5lZIPbG5TLcZILO?9jaYuL@rIAD$2!SA(hFaPVClVRxHX;>R=a zk>I>QFyCJ+?$03OQcs2iQt{78^dHmyBgasmJK&5%P0v4$DvpX5i@pm9q{^l?+bT5h z;5E6b$^28xcdYpsz0Y}y@5(n@k$uZfdK1;(+?5*5qj&pc*)J$-!w&HrJqjHZ(a%lc?6CR!0 z9^zTs_n;|rd$~^xGD%EN!#jw1n>_QQCk`+WXBZR``0kAuGZ62Pn@n)Ow1>1>!;V*rM@8Y^l&|W&aQ~kAWVP$&1jL@SY<`4wT$OWY7i0I8cmCNPGk?Mt zKFPtvlnLe4zWeJw{JxcZp1;0X5VTYH93Q^3yQ6$}&NTQm%kvo2q91%Tr^Hsm<@UXa zIQbVf8P}}*yLz5F=>>c>>2xzZY%{y-bzA4!F!WSx`RHi%ir?({vw1SMQ!>2gU%B^U zu{a3~7(;gVQp$7r^h5QA@_Z~-Zs&gB@WTgq{$(@B%Y$=P`X6j)A&jUxCdfWIio;du@K-tZ_t{*}I%fT84&#^{HkbXdcGii3TSu1bOc7V)2h;G$m%peF&xL!B*+LYxm^Jh!K0WM)E6Fv2@2k3l zKgG*v_Hc{uRO7!FoXK6Pp9MGC<4Jd(!>KxI#s}2|`PNGNui*bT*=_4JViXx(r;o)x zH}sBNyR@41c-)l@?cOc#`CSdKx)>+c6^?0!iH#hgr!ZmlmE^y#d&s#aJ2%b#M@EJJ z?0jVdwZ6Zd!HPg<9r%9+dBF_VPO5eZ!+WpSZ=#E5`}{txStxkBK+MeoJJbIRhx~sv zAMLk8-i=H2-KAc2N2;CoOheMpyKPahI!(PtGNzPPa!}`tL=rG4kIC@Im zA-(6-Efbrf$jxEvMLsb@%t#tuesnJ+Fng4m3t!JaSZ!#le1je;oRL3}$-~=t&Pp}6 zxVYqF&zxX`2h@|BTgNdrKC%`5Z_N#G!dQIhL@Rxy95_Tzdxfo`zsi^4^dFBrJ9Dzw zMDDdS;u3Ti_6PYlyh@t^_v8al;6*FzYu{8}0t+uAUi9KSw+rE>_N& zMTcx)?Q8r&oGCEgXLHMa`q}?THR<-|&BUjDeZ3>)A7^Nm%9&G%Da~>HmbgVz@jtIv z(AzVirr}3l@x?1@jHkqOv144`;5;Y}jXTvogNGL*qYa*IxEJ=Sf9I8tjPko==1T_f z(TAhd1frPHlnZTnC4CbHib-5d`*Z&=^^)c)hcr`1`d6sKBN7s zCzV#O?~VB1ivNn$ckkmcFzZ}?-y{2R&(ulyTsHY{X8gVJ7BP?fi&uny^k3?GJn^C4 ziZ$%r4>wXeFD&;h597{=|FP+Ae@i}aQEjy)|L=s;(eaI=)4ay9#eBMtxzAE^ z$p`KuwiIG78&|L+Yc0rEk33T^O6WW>E;@UgYqQbW@5G1)_M8~j4-N_Zz7|g~|Cyu$ zPB;snb^Rz6@$~`tFB`ripK9tJts0r@Vh^=$;JIvUD&MJIfE~lsg308!k#M#$aAL1n zi9PsurJ(^s9ddX2#3zrXmhUc6*Bm2u50YO*V*6~M-Fx#BHO(U>GgJJ;vzH#Li#Dgr z#&XTfd;!OP%-6Q3W#3ESKQ3^Ho~n)W-O~EMHTYLbwNdhF34ia+XQOl3_DDZF1`jX6 zbZhx%q54TCoa}&E=H1p}Pp$ho+e!|5yPCPVEZ65(>t}ejFF(4h{@Fc8aDK_WVFEE5 zZU*0JG}pghEm8kT;p}Wl{353~oz$M~?`UWCzY9mL$^Nas?IN5fHNIC%{xp~WZZLzq zN^Nx#OoXF@GpaMZVLxKUNSr!~{S^D?ck~&22Vki^ID0&CvWEHQ`10Xf>|1{(fjHO> zK1Q*p2&?558J&sgr)J(7-c_}4pZmb3lJ>?2Uy|Q7_MaUef-~W#(CuOJfK{mWVwvNsZ)uhUqsU4-(yGGuaMNI(qZl&kyhs0t&-uNY6Beo{ltS4n(-}MovWATp@ zYP2EbLH`$Lnqv*)e9|{+)bMpPKWTAa%)n`0(0{~2e96ymWzd^>DsGh(1J;HFZqQ@v zJ8G(T&6cv&O)1qR-D4a(-3h~6@|_kxShM&a4riEnR4%w!E}2N(^gHL0qu`^3;QSl0 zioBZ6u=o3HEsu(ff-G8fcmDr7_w~Ei^g4g4I(}vEziZ+h zQvZ4$L?)OymQ3*QMb`WuxeaTncAkxW=9Y&ZF;8Nz$D6>Nqxd>~RW7w>;4EOt%6$L0H{>ge@36WrAPcZthQ63VgmkS*VO!q>m1alaCt9V3QR zD=Ck?%U6GqCk)3c>&a`#yL&kN17;8NnF{XJVLAH<*ee+qgC!Tgs&BsLU+a9IE+g=d zbl24y&fzYV)el1TKF+F_xL?XzY6Gw*0$*N2?seYd`S{kdXP$q<#>1XfXU8o+v*}WB zevf+u;Bi)e|40Ax#DZOi_(gK@=STjC^Q5dQX5~|FJtu!Bue#-6*$r`={guTgn?%z@-R1TU$JhvXL@18Q78chUaajZ|9}|GKY}A120+lZhEy)yTH7 zYnXKi=ibHtM(h9lYu#!1|15RQh_0DzsX5tGc*uJ-*+hJHjymNg+&Z3IfZyest9F=< zZP!=B;QuN4)-^tIcD~OoGaKjsb;#=u?$mdEu-_)O<9R#&_jKBi(%U#EN4W+cydxvRdE0OsRsk$UJYb=FfJNPx48|L3n*>#u&6)cj{B zIZJu(p(E2O%|imWli2_7Y^i~G7}hmY$nWKUH}T|rHTaTvI@12ilK17d>Khkv;i9;I zOSnsRxyf#`-yd_m9lObRoLuuL+b$2E|B|<6mYeYLwywYQH4d6uod8~Z_ZTO`9jA%O z(Q&uKdvVuNFi*X9c3(MDkiSorPvMS-3gb)US~{(L|Dg_Z&@*F};9c~ch~B$d&mK0_ z%m0VGW{d1EFAkUYj``ab-gS%D+_SdxFfqpzb&Qsw=7;qa#ha_JG4Be#9@}~@s()q1 zYZv07uxa-YJ%TK_^euIl!g77_FBx7^z9E0;V*aBwUQrYOfk#iP(f?>R?0O;mdizy4 zX)W(?qnhPl2^zCR%> zo&}SF^NC-!$v0r&6|+ve#f)V5Ps`+N1un%e0{>gc5%t6;K5_Obxf&MxPl-!?;)9#$ zY?@s1gnP#?kB6j!hkU3sPA~!guDV>lGFqRMufLg6;F?ne2ZLENYH}M550h4vWwDU%{B6fnV7B%S-&C zGFh;lpWyJJjQG?~>LdIs278G^UL*7q{$Ni*E;DPXKG|e^$#vWFm07SpA32QdphoIv zk>F+(eD@8EIB&+jIvYz&FL!^?`r$eAOFoe1mEkre# zQJ0wX&I}kCWTNZnZPe8!1pD#Uyhg8Z1pkV_PRO;?A+^3T*1E=XTPiuv3a?JFpWgUN z<4-spd#UlpynS{tCRAM_2Yi+zY+k7kCC)6_$=BDKd7|&wnc$I}BRkx?f-g+#jd!0U zV|*+d`M=KZntbA9c;~zL|7HFE=ky=LtXF&XUyA)N5>r1fQSYGtMC@gnI_E(gCJbD! z+(?h^1`eD{FK3lG1pC;Z11{&3OWmNq+~VW{`A$OmdqGBUEjW|XYnpzKYeiW>&unW( zX?Q69cZ305`A*{+`Vug_mwMLxsC0YXyiXIjl9|o@=$ycxYOv&!QG7YV7gCD<6WijQ ze4^QJ;@J$`-u^C;^~o~od>_QWd2ozuw!O3VVC*B8iV@{$uw%CS&V=3R#GDrIUv>Z*7f(t@E>Eo_i!-&xHw74;zED*aai54KE5P2oc>Nc3`;Hx`RqM8!)zE# zHao6Zll?b#&BXGG<8W;CdiXQ|Ht3&xiX>;P<&2DY{FH5!9iY}qXH{XsWd1sCo0+8a z{6^_`Cu~ZC+)Fx`cO8lGB?X&5Fh1k2Ub5+Hm-hoZ_y!L~8Y2 z@o(2m^$)$OcWf|X1aTSPDHmm!-irDsT{V2J_Pm;(T@w>rFS(q27+alB$IFl4lI$;T zZqI*8E2j|a$`sc}+O1cuzSIGZm7k?1imx0ipw|VbBD-dl!RnbyJx4vKc?VDC!(j{SeR0y|p4__nZ~{>xM|{~(r?;x~&L;yY~a*_6=0 z7%}@;YcsNJCbe}3X9{meb#6KuJIbprG*La8p1zP}S?hUMQg7AX>P5r-aNl|2MS8NV z2ZzpX(SK=Yp75x6PEKvk(}8sy`dwe1?N;t*5Br_Rvfg`QRnlRwlW$b@{n2vrWa<~& zU3=7skif6}|7srhD6S7Xi{DPC|E&0AV_30OFZ_&kz=jfw9-0?$6wTg6N zM{f32+x!qcRk*+|`9uDV_!@uvB0g0Zp#BAu619|q|no3-#R z-avUgObpCW6Xw5lN6Tqek;iIwFW$U=u1~r|9?yq{-wX-t!Cjxf#NBSI8+EfDa*fd$ z7vmRMajzp+tvSE*Y39|#@#&-DUiGms?mIIp@V^S#7Q@RH{VB)gFaLH|k2oto%z*de z6fyWjaK`^K8!kzfUHM9S{5+9f-_Z(k68`cfjq_FPy7utUz*qLajlG4^^D7+S>jrbp zf72O^{jr9#M*$e>K21uic}AtL9O5kuydf@cppS0X)B)>}%^ddLT~7dZ1m|Ig)D_2% z25X%36rCT^|6cpgQrHYY4r{5X7Bi7O?~pIU&W@GMEgi+{#ll8A^{3_Zo#x>)VbpWy z%ekMLe~69$!jq{v6^SZhk$qHhSd$;doET&a*oNaPs^eCV7Odl z$yb*1!MX5vE}gFx8z1^vmBaFvpXJJR*dJM!=Wnq-t9?B)=lj_ilY{2^`1xwKI6&;E z*GI0vMyjy??6}tS81T>ji^LHl^#~Kym&>xpJk9a=On4)#$(~AoV5nNhU^QNt_XsvD z#;2ow!Nr6ALjUbye2geQ7hgVas8bbyYW5#wU8|OGJ=Ye~=T!!ifsXiOJ-C zDE_fhY+57cW@Qs(FwvjjdzO;l^*+^%>MFZnNnEu~IbeUXi51l84zp%#IKcz{*#s7< z*VQ7U7xJamV$iXGJ`4YN%Kn}T~dKC7I9qnO)NJUyIe54%pXgcAK4zsa1?-$4$EVOj>*a zw>;F4-MMGavU2p&&Krai8#D0d!u-m=`!BGcy=(>^PPez`mvJxh4CYGZJDZ=vXNqql zAAGn)as975ypGK@9pI{C3=w`XmOW$q7mlk=}3EiDmBR|YeD6bdJe32cl^4=Qx+84oj(rr7kK;l4ny@MRpODe) z*Dq#eU{jG9`k~_gb!*Sf{$4#)$Nk>f0(#6W{vEZ}VEl-suJA9LezY|tuwQ)KG!s0; zPv1-J3Z9QE39@d!&I4EvJisa0jcy)9d4sVaUkD1|+H!tnu{4f0% zZEQw5ym+|={>Kz259nXYb^4x`uLb!-0rro>Eb#Biuwy0t25o=VaX1QRo?WFE*gPn? z8O@Qi$p6)SkxRD@PWY<5>*%;^Oa(;7AAl+5#sYp(mbQO%GR`OaB@9z;_MsSbJLg zqkOI#{O%0npLj>zFg1I8=KTV3$=;Kb&4BG@j6U+qw`BUQdbD}1{3Ydt_Mhkl+e|@+ z^nd!k{ArGO^N0HXTRt*NFaM1BjZS>Lj+oD8$|N-_ct{>TfSfX`E!sy!xh)|H(VyDFdM@6TpgpECg~)D%X@e?P@x^Gn20dbzw0 z{~0DuSHqLo-qQir`bw?gnE8!_xav5Zr2@T_RqMDL%*$XWy;VD?FEtYXgRNCy>DWZ{ zz+T=@z}sQxFmdaFJbo$uKP#!37;9c4H*A|aI6tD6+SiQ3A?v`AmXT4XeCoaSpK>H# zx<{@j4!t6);Pd(uT{GiWfc*^hJ6O<@?TzCDz4>ajuj&OG{hz<*xgghr;eBH3r;+t* zpDpmtnG*IHv84J}4%j>^Brpk9T&_*8bei*DbG>3r>4^Ar5wR3@ALsvh>>)7{I@#%N@+*bQc30M34<&1v2p@G0Wb5CsPx-{54c#k|f(#G9SwddTfgUuWe1vG$>C8t%2_Plw2^SuA#*a=m-LlZAg?$jtX)z}OY~RIt4NaP^Y} zW;~ACFL@*!B_4+n&sv#P#ldI8xZtyn|7#=GvDYbc@Z&0SZt_V&4hxc#8Cg$xBB!OR ztN7+r`Kp>AUR+lwL2nBWPe@c|F_tB?9chxz5XZmPTT$ zY_3~txj<_+&=Xd!GXM3-e8XY!bdwnaTyYD`Y%&cW7q4?YGw%1Yw=39N7qtm7qqrT2 z`Od3S=9+(mUElIM*sz|jMmsFF|E~Udi@(FXak=#%`TUeC-fi|PjPUy;x%`Z+CM1Je zyI?W=uaeE@r^22{p2gw5h97_afnN^cpKsN?bJCRQTe%&6wfCI5>|ZXsjfU|qS(@9Jsl9379acQzG#*Y=CE zcuQOMtNynbCf2>B_JE&9IDkX)rIhUc9AEmb2l?f};|r*JjW-i+|HJIJ&t|cjuY7T> zLGwI|?yUKZ?eO`CdTS$ct)k}^oqblu5yk&2uK!HEtD4{aLH~8cm5ejZo7!jErS^r_ zu0F5-%~peX^5+EX#af^5R}a>}p^dO$yxI}}Yuk(u zv9XO`%wb;VE3o@%3-yf^p6#puRrj5G7p}1KrP#uzV(h|6KkM;z#!s#1%tvp(XkWE}zM#ubp*Iu5JS8O1+o>sh1N=thJ+com&Fmda#Fd_B%=bE9Nymi4(xO?QHLxS!(O?)#9wH5~qmeID*lf;sQ6(j)!s;>$^Q zi#7H?IkqzvO~j}(_FM&DlNVLtFKj=D{`Y}YIF@*Lj|^Y1@d%6bA>hW|Jo=6Hb42~;V_A8bb&jvE{tY9K^z=+j z7*nLEXE?#tpU0~c+v9T>wy}cxKiM^e3$?1tYl^AyjN=<{{oEeC7xkJQY&!T(vY(sV zKa39MXm3m7wbRIm-&~K46Z(7tcp7|%)_bwMR4y3ufezSGr)J{Wb3J^rpEAsx>S{Ih z)oMLuaDnG);@SC+y5^t}dVV-xtmtY;8R;^MdDZLs9_i(Sd~Mio@Xh|;+56|B>Mc|8 zksE4$t?+O4zqjnak6zF2bK*F^sWC+Tm>jbVG>7sHT{nh3v0!D0IwT)&!Dc(y`y-sR zXA3y-1K%oxALN!74wC==?7Tt*9Fe}O#KDUzt7o`pw4v(n_J4h?`}66n+x^b|Nel$29-5|7Cyc6GIaI%FfhCO!#=HmOJa%SXiBPq=CJPI6-R+Jkcj z-^X01Fk5FsN%_Ih&3x@MJ;TPMYM-U8vBhN^A-Wla8aSC;;$v)B|GgQ(3w&)Pea2V+ z_jlq2X2N{0y?paXDs!~_COiD=b3$zj4vfQr&Ln{&;_i^g>UDIzsI7P)zWfEN^V3iK z$odWV(xYLpey-=z&5@&aVP|Zuu6_T__q&ln@VV4`)Lk0OXJ_I6@5Je4g~ZBT>bBOp zDX0F4XXfD2a--n$S9ZhSny`8POL=c5xf^+vbiMD7m{(t* zAGZ{4!T;YjsB4l{p)2}-FJPwg59L>fH|E*lLwip~w?=BgA;l}4)%b|QOnYcVVL_Rkkm#&~5 z;J#(aE$IXKX900hO}QRUJ;QpB@~NL)_grfBTTne&zHyX}r(Bg^4aUdSNFK703UvCf zHLv1(vEGOuMsgVKS5nHdx%cF_^Yv`*J+_*`O$NcI46c**B%Y^Fl&D0yd;6?P`5B6NT zq&AxXzU2`+_;>eS^qWrTF>`?suXVZeN>jbK@=f%!G^iiL=4i zo%ZT%S8(=hGJo7euA|w;KR8?;_P+#&`Etapp?XT=0(@q_xFr5ff>)1cn}06>W5v30 zO~lb|=Edw`=LWWOh}~4z|0}0XG6;9&Prs#s)xP&(p?asdaaK(4!e?jj{Ul^}^t${j z)XY*ny|>;l(*CphGmah1cEk|m> zhu3tH&&I&1r?KNZ&f~MU(mCN)GS37%2>;n?D)zGQD~w`?d(M*WPikX3)Tj2ctG6(l zZtuqT9dU2rE4Fz`O+a3^nJqjaw_p!Hq8O9&J9uDUmAk2}&(>!bkGhTu2}~@n9+M^b zoOAIS)=eFNcaXv14}2NcTuCmsWM}zH@DsK$bA)-GNotGmBVq$Y0Z-!4+#Xui8*Zkp7=MFpJkz& zKgX)y<9ijZs(tXM>&1q84hD>djmz0z`_0ytj-LL;?ZvQeZ`6Ly;<5B!oc^Pff#dl? z1I4UsRvI`PX7>^$kvx@G(8{?-T#=HG5og zf$geU){7V#7)ag~Bh!uj=jo*%-&HOxUi^~;@0cOKhuzb0mS%g!t*~Tqz)WIZ+)ZpK zabM0G*E(SRi_7#U|Eog}PspL{o56wkuzn2t&!5;#Ax;)N|B|=I|7v(ae_5Q(^Pj9K z*z4|)li!tQFAL0fRAQfl%`C^1{}g9C_t<<)_P|d5J8TwkIsW)P44G5B=({@G>Kw#7_CYXTV*hoGS(6)%UgQ(|m0LpIa>kM^#UZ%&e?l2TNxb!fEVzonNmVqqa9l+~bafSem~~C`1ZaR=K};>_+3TwUw74}>cf^;+~EDa-Pb%mTXc^a z&O7Rv*1A!=l^f|?M+aJsHZ4CyZ+5Smz8HLXm}bSNLEq-by4JGEdC_aM-&(!?9QZg- z{Tv2=D@JCxmrs}2Up9^DCYRN^^Q+hM)w$v`%XJU1 zaRQ7z$N!29lyBX}%dORl+5F$)CKH~V!GDJr+V^gL=Ce&N*|$dO061Y^7aXY0*!&eW z3b->Ql(vTzDST~pceOm)aKK<1mo|Ph+1#1hO!JOrN#=Og(@cB#6YlwkXKC=qt~c}t z?$ITc?ElYx=ikM3`b39mwprF9HikX}6Z7#^we9H5aIb<{hl6@UboxD-xV*h6^`_^r zh0`UpepDef*#vQvp{AH0-b}ZT=jmeH)}a-SV$U<;;6-uX=6zZq2KM6I&u+?}>D@T5 z7gh`SPVSUC+s}M&J-^T5OT%Wt;e2{y^6We`!0!d=4u2bCed#6D&IgF)T52DuYCU7{ z(DUZ3zGeUS+&3OhvHo256WLf^l1p!t#q%e{FaOCtE+5G;TrNpNG?q{zSH;aBLN1U5ZB7H7A^!Mn^7B;k>K!5l8$ zhjW3JiGg*&JWrZEN%6kfuZlmqrd}oaj(aSQ6Mq=zT5)C=#Z!*h?pcpt!P7Jtdi(<2 zkrwQM+&;}3#p@t``qCS06>ToGt68@}xU`{XHmPY08w_LCn|px4y}b89KlbyvLlexx zX1T_1_V5(_3@fgP%~m)v%e~IIYWCwgTSd!*OFB!-4pnLW>;vvEO}#~ZDX1GS(Kjj< zqF>2J#>2jt)pC7y&EWsl`qEqU&uOn8Z7&~c3>)@%jaF%w>i!GBG}kJgnuJFlL>#MuxEB{ z^?f=0XuppaGh=$=5$mr(?{%_2*Fv2M8wC?!_p}|Zb;SML#n=1vq;9D}T%vPA-7i1x zanj6fF*8zqJ@?|6a|kPN{66zFbnL+4lOn_EpVr4c=cK;;_G5Kj`uqu7&k&3EB7Apj zaauMKuJF5x?A5Ov?Y9^wETBv1!8Ye;@H>A0p18zu4TtDMxTl9h#ZUn}VSUZg=_yzh z^)jqT*L$GtC-D7_Z^;ig(weT3pf32djl2%7*AG>n$J1f`>F&LxZ*DGGGlY5QZaa$^mA6BS*LQ{eAA|wB%3LUCd0>&8p7#iT|GBJwA$V zjPgumempM(#`eOiZE@gH=aT-X-vf_ER7}K=`b)L(^lddv_Rk&7?`FV?zp9zQusrng zPPSjY(6y{Tv$Gji{yd?LS&FCXd+@#II`f0pyH+jhfcTpFJv|qNlkvyCFkD&L=WSJk zqF>63n;x__4_Rb2IPBRp)5o>>KO}rT?m1bKK}a z2jL+;F@gUF&qTZ5L>|EY1G=jtj?gE@2O;7o@X0UgsguLQ!DICf_~R~da#{S3a?O6p zW}mEeDf>6+CAa4vhiI2zJy$C5ky`Fot-0oDK8#B)o^n2Iy&M}iwYJX%Bb=?cFBbE_ z8eE&t`ZGUNGb%$5Zs3cV>Ko#ICI9SG$={q+k6B^w%CKowT#{+u#MAIbxN8cXqNbJ{ zXWq~J-`73mWi5O!cYAtpqS~04j!)6&+D3ynGZ$yAaq;GN*fAa#FDRkzJdF>{GxyUK z*R6!x?(abdHi(v&Pn7S8vDvldWM}aAEqj*7Jm6pW=WBAt6usSZas_^Kh_0Bt0Zw)F z{v7i^v_#8*U+{nQK{3=ZA>?to3J{Y|1A zYbA~olUv)#5$LCiX*iCrm%#&hzm#jfZQhN)-f;di32q#vhl4W+NsIV|_({kc)N|zW z{4iD=1>X^x&W9G`;G7!nZ;$%^ID2LdpL|9CR#9&jqv!oUG?o9a(Oasf2P4LFv zH-^8j+vWSDaZMEa+>euwQ5*jaEjTk^k?XL?gqxy6$-~z8Dcl zZ}eBg;1e(0ph0NjGuim^f_37*rSxQe@mpuDek{z-c}A_TNMdA5d;GQeheG^%q2E2a zKR)skxSmd9jPFP5j5X&Qf(A{Rp=Xeg41D{%>je#|-*$x*_2P>}aX}%Wr=lr}n%RU$WI@_*NIc2G3aA zEKaNQ|Ddk3@`0Qk4oq!G50>{@7d;zTmp0I>OcdUxe=h9Ai8b`Rttb3|{D%MMZjhhg z{~Z4H#&+2HCSRBT7tB^C`xC6=yQRfuti8U1v+nhn6nTqYJ|q9T`4~pQwl50OtoO_k z(IV}}uoXLo(;2}TmPK3S$t`K|;by?#?e;pb182>#h9TaYfiup2s;3`ABZ%qX9O<^2 z;(HoR#I{@c?Q`&d@@{pOaNG$m7C$Fe2kAwhb*-X&haUys1>ANM{1!|5HUrGE>UB}*^SBv;X>|Sl_{TJ13 zz5j7ry%Fn~3)kA5P;pf+rV90T6v~WC-oII;ePNh!}apR^{}3>T@01r z|J^od{P_G}XSXu+KV83b2u$VQj~jXZNg@8f%$yt_Y1)Hdvgx~GrSWGl3HF3q zcVarHU-v4k$Z^dCB^p|cX}^eId06VH>E%yu607G2+5Zi?gj z&lC&nQTnEy9DjOsw=-?W^m6dQ%qw^zO#h2NUKYC%URyBTx=YbxG|M|X)DqdVtbDo} zPOj(P+m=-mo~vG|CYJ0yQ)sL#Yi|L2=Uw3U|Aluy%16r4iT3Fm+!bN{aWBYWapaB# z^5j3;`x9!UZPdKt@yZG_FE0hXKb&O$vwhx7F_n7No-8v5(tr;nnPIr+tQl>4v53#2 z7k2D6AAM F`G4EzcdGyZ literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/0/1.latk b/npm/examples/public/history/0/0/1.latk new file mode 100644 index 0000000000000000000000000000000000000000..cb0e05755be80c4f4846f6703104f0ddf7928d0e GIT binary patch literal 67650 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 literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/0/10.latk b/npm/examples/public/history/0/0/10.latk new file mode 100644 index 0000000000000000000000000000000000000000..cf031e6ea6c386f5a5857d52e888648415879484 GIT binary patch literal 67650 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�%NX)n1)p2P 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Π*8T{Y&3Zb9{Xo{a;^6$N+4q`b(MMT-r;n=WH|h7A$^Mdi zj(SfFbS9Uaf))h-|HB&Z6B};zQRExA_NFrRFMKR)JN3_u_$;44fSnE`b~?fThk}2z z9EmMyXu5Es{p|0VFznxj`rR2~Ch^aib4`y=wk6)RjiH7~4ql@^z01rwbOZ0A0Y5e$ zrk=Ei{uj>jD?VUeNVry<TpI21bSDu<>G?*}gd2E574d9yF!NH>RKd}Bc zthJu6Dv$Eto z4ldajJr9lW9h`T0Bi_LhL{ISxaw%}vUSkhj2!93_O5+pn$t@a}sB#V%tIF)vu{Bq2FdNN`ELiUnR~W@fv+V#O!zYXrHlYQI+W@ z8-uP{6fLN|B1efK?bB2LnalG+HR;`2PTxE+WCpoxNF}gm06tb6yeL7h9r?y~=GNd3 z`dcY-3hW^bpFHbM-%Ge6_m0qGn@YZ%)!sV<*F@Lt$reFyvlPpdxO2Mc|g6WJvkn+XXi!Eq&4+D{3eC>vs@z% zL_I+-0%rNu!m`VOz{RKG!J{rRQU-N^{hEjhC zP{bDuZo{5mfd@>ehhOGXpI?{H3VG(TlMKjZ{sUf_6*Y{j(D`2T1As|Tkp=I{iEFHJ^1KZh+Yd~bo(Og$$7ZM zK%R9V|M4OAB!QNi6-{xIsJTm zzE~{vn%UrADw^3(c;$Uv`f|S$3;AAEo`t;y7OsX5JXB@DH{y(G(3{Hhja;u_cIt6! zu)j)R|1NTm3-B2BZty;8s>7(4N23*Cr=_Q|C++B=tO%A78~QnEvho2vI&guN*tZM1 zL1Ff*!ddtR^Xi?9UQWCiL7bTo!2ZDX?!(2l4S?^%$7)yP9&m^y)A3LIbq*MpoQwBg zC}?wkST}yq1iNY73{4Pwf1io^*K7Llu(7YpIp=5S(41lK1o%&N@*isFs~4~axN7mm z=rqgeZ)dFoz0sGlq37Yl#a7Zg%bvJKV;5j&frZrQu;;lAnLoV73*5X0|1Ax7j{By_ z+re<0{=9ny?l_mv1~SKH=g=uw|7zm-1pI!7Cq28X;Wbz2>1RFr@u$xCWx6}W-)`tH zVAS6BJma*L+@Lr-2p?EM+`b;KNiuub8+>xI<1~KLY8!oZ{5|y`8e=bV*k+s!_LWK= zcMJYo5xd#8M3Ia`;0DZVMOSKCYpEUAA;0gdNJ0Sl2IsI4Ke)pBi(W9~aRU5+`0znt z{hs8@_}5%lw1X1%et0x%?r|nPy*0h59l?PzHv#`fU^_d}vZ(E38wa<23~v6xSKujs z!HPnhL1OQJ?{jk_XPe9Ueq)b*hN)tJRolR0m&RzEt*QUJqrFXFk6qAwoZ;{){9`Qf zzdXG>oR#A+YRIf13|pE6{_TVRcrRnmClIIc-Th!qSGd7{f9Ga4fCDwoiIWB@9 zTqm{?7}SZ_7fuh%PrgsC@)f(?KZ);+f@g5w`-RaJXMr`u&iqZVJM!VZ>|0c0@GqAB zrh??sYtYoMk{|X`<>V!LEjZH-<>1gh#Nb8r48l(@c%l`(hL3ui(32f=?|db}pV>#+CwkI?MWQP%|LN8X3m!dZH=hSizj|<41Tcjd*x@AWho`(Hp1y&16uCl;7@XU}-v0qxioM|dR2{%U@G)CYaPKb9pu!Kk z2a<0`QOhWS_C~A&l!9B)tUHz6Wt<#Sck^9pfkP~K@F)1&*hzf zqY-<`fR)eDu&@?$-NCc!+{+Ri0An?F#0e1x*?4G_cvoa|2F#* zaTxAZ3co7+1Z`^&n1fCh<;?jKC+z*MY2;~vTq87@^DRrB{1OZTzZZ@L)4;)^;LB9_ z{^+Wl2WM2GC;I0-@bDe)i=4+hLczb9Zq$FX!hbWP7qk8f@3E!BXh4t1XR)VXa_#*7 z?ajm2TzKC1GB()rxWNp#NN|Q*`F^wj|u#xNiw^ zgswvrS;g;NX3&?Ln>yf3wAFd&`CoVk8$7emZt~YY@ZG*(qRRd)B=#f_qrk$))5!gc zkTXt(hr%a*TE*ClU*x}s@&6G)@FsYQ%PYipo3V$@V!gq<6$FQZ>uh9GEfSuRqiy;1O1!vzr#RiFgjU(v^TZ>j$5KL_k zuZjR?Dx;mQL_es6Z=GQeZt~2J$@@__*X?WQkz-wlPQovp$QQvTwg<- z1Iv`lUmd?>MHPofD^jB2DAI1{}%y+lk(c6A-_9^U&Sou`GT+D z6~8Nb7u*=g^Jdsc;l9*utI=nVPqyyHe)m?SCHKC43%`RqH7`cYen>4MmiMZ_)pq0p zkHEWgONfnI3~}Z=8fUsC7rkci1dlr4)?Tz9eBjwO>bUs-lv2b1;?rYd=}gvBWeqvf z4tfhhc`v|!gRZdp0j!c|J*?AYz!90 zyON7OMW61$z7ogxmIRNwgDnLVDa<}JOd%#ZupeOD+LemrosW)7Pt;O!4!ghQdJcv& z?;N#=;a};qy-%LH6znXHU++h6!8iEh*OF(QD)P>1 zFzn`b<^r$Vxs^EHhrK1gb#KXia%vL5n$`Z~8qQ#CFEHRR{YsVSAHfffM8S2((sRMR z=LM2)xRVFGL9=ZR$IU_=Xs;qmD_}=g>Eno@UX_vFO0Z%$u_QY&s>4iOI)YVgf~g(A z6JM9mMaP#t=Af+}0}snlqpyLMiT&=aj;`YYKfxy6;FoQ@$v@Bn%EEzn!bKAkGb zMd^h)sLS}D^a6wVyBm;ydXTe?p{Bw0Jtm+Z+(Ki{P41B$n?H#rbBo-j6?F(=?!B8l zPsLtW(0Q*%QPzJ8doM@LxCgx>__aH^VS4cYc11W&E#eJ%a-r97NcPAd>}ZA!mwQUy zgguTW7MyY*|0hqH3(m~v`-c~!k9A`X%-t0`t5XmgL*I8Qq)W>*Rg6MB|NK{xP`GIR z6rOjP!n1~>!IhtAUhGRevE(?owu|%eB2IbTKzGc5u5p291c#ENPsLvR*;D-gC7jRy zGPXe++&hBjX_}D7!Id+EQ9ByJjs3~FMiNW%!fjLFpV(YKV%YQ{@L=qJ8T@Yw{{MnF ze1rRaoWc6J)*NgyW*hxluGls4-z5Ug08Wtw4Py;^)y9F|8un?FFMPBF>pzMv3%+a( z=KQh2-}lMkx&PBV^cx3IZ!5ulsDF9F&z+ir0pAo!oDcpJr^+0sXVV95tPFe#%&&Tq z7#BcpltYor%=7v_;tg@#Z5#OyV*wecAvHj!0QaimW1Hb$*RbaW3Rrs(TqVaEI-B}a zcQ_Gw#fa17Wt`zu?AiG|edX}+f_*rfT8i`_rfj7DsVUzX2uE!Gmih;n8#apg+>BaZ zSNgHjBBgQuq!L~a`$b;O_CVOEYThQ@~LVI)wge@W@`XvcC(-{{L%ekwkrJS7_hj;z>a}D^{-Y9y<@XP-mMapgiEi}m#gFNI9pKsI)u6Yp~r~m2pA$kho zB_8F_zlobV_Rym+KE{1-x1|3$jc2%=(CNU-s%ycpX<$9J@D#tCLjCVaPrKd%mOkQh zR;AEtvl` z&UKGUeFhtv_=B~OtM&owtGz+9Bi{VtJbH%H<7|@0fISMa?*;jOGV2~e>}k1+UKTJR zW*WWY?CaXY^b%mNo6eGR=jVA?;zd4m?PCYge~a?}1Oa28acyU?>LGrc=^`2`oMiR{ z;``G`@drmb27=4rV^k(dB;7s&G_N^cB$%ajTzeJ71g*fpY zEj5OI%!AZlv!VqPn_Bjy?sW~ktb_gs2H4}v66~o@1^CA}e)orG&St^mUPj94?%3f| za+1Mt_ek=8>OU8VS37>fF+<^N#QCk*ND1y8(GvSTLQhyA^-8qLgS)}K1LSqJ@Nwo> zY!1)=?ZcxeN1M6>&-9_s zaV?q^IQ5+KYO_$6#F@M!y#Voz9ARDoyS7CBk0y3<9r_|Te1v>6g)_I|Gg-&*ZX&Lu z+K3rGnuNl=-@_kbIENJSud`rugf|+}0Nx$C6Mx@G4(r4kUX$y?YwddJ4M#N54`_XL z`Tt*lL5;SuPIRS;V6W?GRfM$`d`pcif@i0{>C)pJeQxxStqUVo_D53~M_m!W-*g|& z3IF}f`e!Vr$Br{f2qH&=A5G7Q{gOK^NFxX61P|l8ZQuvf`|>`DRphMIiIecPSa@0i zuxL|z`pHAkHOtWJc!YW^HLnf(;m5tHsgEXa@dKBL@t016CkD3J1KeNEnyEAF8b-fW zeew(J^ejGPfBq^Mu0H|n_6GkC$B9{s!P{3syoVZJ_a8=&D>n27+`WhP)fPSyhkc9$JMPZm*?~j!ND%}7kfYSN zMNE4Ewz8MGZ}I-JJN*AZ0(p67UUd5*2#=p6K{Wm<{ zoSyjHmKa7a`6hVMB>Z@m8@zzaBO0v^Lne>2KA6nac;)M@B*obw#{=#!r_jrbs{Vvo34f6~7&_;0mr!G_A zH(QJHo)j=_?`~|D{c_rl?sk=Yi1~EB&%26<$FsnJm(|G6@RNN0_)?L-S6rVpKi!G8ITn57CiCb9$Kk9Bz_rJ;!avZz;@QvH1{eSzUUrK9zf_)oEDhJ^ z`o8#F{lC0#5Z*Ls74^ksaE^C;cLFxey(cc>9&n25?dhr52Pa$0do$1~|4d}9FTtS- z*!+8Hf~;$eJMZS$O+NCT`7EQR!5PG6qpmuV9v!}4i1h~#hc|KlfA7)XRhW1>A6+7q z+@~b=MJ#GCiN0%a<#SnGzQpsM6yl9*G3tRw=`UHqGxEOhXZT@t^zsbEliK*s$5r4^ zI`rpC#65DgS$sbB7`jv?G=Ko`V=>yx8MNUJ^h5uk=Ntd4Hw`VR05Rwu`WH5w7i?bJ zmiPbHM9-lAfAk~jc>g2muH&t&wk`mZ0wSe!m$U*R9WNjq3P^*5f~2$v`@lJ*(jeU^ z4HDAQDJ>;PcXuNo@jd&yf83u}Je>W$Yt1?47-P=0c2*qrLwMltUFSFCfD>yA!(BXS z>l%52zc1d81BLhJiCM~5&S1j7F1YrYBy=3?i)D`LNBh6!*L}rz&5QqSblw1Ve*+VW#x`$S&b;;+^`4fi zjVtt5@B3aBFR*3>U0S=x|IV=FqyMgzmVY0~&*JiUuUbt%efAw3Tc>wzJucNX6Es_l zKbQYy!diyk4eM`>#JkkBeuwz0_(C5T_hf>#?XAAs%V8<$h;C{~E?5J5=GWns-s53f z(*lwFkMjHix%YQFJq@)j4-B5^Gv7-eer_7LMnsVBq`8hPzW=2W&Pe~iN5B8_KK<83 z4mG6L;o4Mxo=Uw>wF-CLX8(&hg?!wz%DGQ)@1|+>Yu5Q^e4H?X*Rh_54NrfdqaU3& zFVJ4!C!ae9YnuanOz*yL4b+yWy<^twqWOts`jF-m+raeG zJ=E|Y`H*s}z1TQ&g5R@FFTImzYsT~2CikM-Lu-}oL$kl}<$h+EBFAIR;W>53E7q8G z<6SL0V1`^hp5kX;iT~eM+w1dYbW%7|*eD{X3Ew{S_gbqDQso`c~#eZx+n}3%+0q!$3 zCI8r3cDsW!C&s~HR=CGHOq@+WZw7y>eo#7nJpZkHS2fl9@1_q@K6i(O7#JL3|HXg7 z7Uoypnh&fbPTQGE?YxN=2oE>?1 zJd5;Yk2(KxJruOQ&O3-}mimOV>hJarHlL0Pnq<<(00cY6Ot zG|ao6?YpAAf(-t9M|!KBn8_$!E~|U1tJA#VAVFhuvf{03Zn5B*+AQER^>B{4zB3ni z?0N6MBdIvtg!klv|1tQ#Ji&EM)Et_4u;*9-Po}7=lP|-z-uT}U_s)p7ga69r0?U!P1UwH;j&>p{} zFOJg!^$w}iWA*Rw{Gn4}7!Gqv4*x-ueV_(kei;g4;kBdUyLW}g3*Yy4xQxB_uCwsc z|8Tv}?O8mraq}m>Xox0A>b*Ss^gZ-O7jDycj>hEsCeYSc3vJ>f}e)7hitv&`D zr_5W3eM3l?4vdn^M1Jb0RMk=d^|gVaV^#Sy5brJ>{gq{ z!+@4C)XX+KDxPDM+WXF5p`dSmd9Y0k_2v1if7$sz|L*5wI}=d5j5CK(d~Wq`pZH&e zLk0FaRz`YN9a=8$Tc43rcWBx2u<|{AmU!<|9v^K*>*B#r{?to^CpB}M1;#fOX?BI-OAC-0(!qb`(QDx z_xDo%lJD$T1w)32+oqY}pFQs}n{LNvo(^@UZ-$vzH87(;YpwRy+V39v9Qx`fLP3eP zVn+R&SP92XZZ;shd(Gp~2Ke7;_+5B_dCNumb%puA&&)X2Gym#cMl>=1JBt6&J^bw6 zMct`ig8w9TQr0qi(AWIZNAjH~(?-0nqem0tidXH)!x3_BsQE25ASn!d8{fOYxD^fc zNW$C>Pgq%#pDQ=ItWvAt{JpPw1)IJ9ih9oM4CI59qMg3bn@Pj#fW2p6(}lm?f$7<5 zG~i<&v-ax5MtJhF8~@;z9;s(N+SVNVZN0O*>UaSjHXkMV4H#pO(|X6GheN>%?|bA4 z9y^e>!L9yjE5dcE{bufyJ-aL2^7nOJi{YjDKx`UdWFE#YVS;9d3L%zpmW zL-V1y=|EbxG;KP)6d&g|v!Qb1ta$qici3P(ez;E?s|}gpN~1iuC$H!Jw{$PeO*D%S zDQ;S*e>dS+itK%Ze)9hU|9fdYJ~!>1H-~d6Z_H%pcdp6%|MeZ;heip{GDpK<{(P-B z?B^aTLfY{=jnQ z9pLrlrs`@*dc*z?Yh@;6IWHumyc16(_jyO#q&b(u7e5n1F zsNlI+xo^LgHd4#;(UqzBOS{~I|E@dUt>qDU@UK|!2*Vb)QybO4K_ltaXFP;W@b4cQ zDH?qDIiD=!w|)qZd16KOg@OZZ)TY5^nsA9D@9|&d^XTJnr63Jng4aZgWQB1@Yw*wZ z(8M^~$q#&%^=z+>j7+Rw6rx4?!RKiFUp(eSX?xe7m$zDcKj$Cf9vSKMSor*wQ@B?e zTmXK?dqSIb(O-Sbn@$b~)r*_;)fu1t)z>t{YkcCQc@R0!AtxO+1;2*LZN-0-&rRQ0 z?6$?FU|0MQ&h6QEmgASmi9O@>EB~RJYS=Gv*=Ce;C9t7>Z*v%CXJ*2>JoiGuI_vv# zk^DdC?irq4K{}x+4)A?zI>x_0-9nfDXda*fpJYBy^q?80EPCzJ`4oBSkFVTqKEWB2 zB=~M6`|JIi@vHWU*R1K(l`i5VDepKd{yNV5Sb{!5P&FdRTLd?`Do5aDZ1q1M+-SYT zS)f*)I}IN$g_&XNkuSEjR?|O)C+*}*{I2i^9=xP^w!*;AU`c0s?za;(RAHVUZzuiZ==+7kR6zyB1zK2qymjkbnW`5-v=?N7}i-7?cv)|#cpwUf(5nD!1n zeKo!Fu<)oa{y)5m9wtm5^PO`gr(i7pA6qUbYz}+ff6&n9zS*XBOx3^E=UV0WuJ^m% zX^T@7adw*b(d0lV=(QQ|w?^rP(Ff;nKA3iFr5IJ0;^G|f{^e&{%V#ib@u~m$?+rh~ zvD#Y`y6?y0^g=#oLa&+^JL*13z5Tbt^{fuN&l#>Zd|?l_%Hfs1^Th93c@V!I!k@AnAOC!Fb7O0yhm{aGsWU$OPS{M@k~I74-@MNbreN1Q!1pW&VJ!>W#Z zL+;{+8MtoIv~swib1L=ant0Fh8c&F3rnsNGC;IbF=uepR8~uGc7ycX9GvHF);qd#*%}ZtXjDPFnhunYK z**QYzA4aRe(Fm_2^Df^JsoH z=}qyLh#x)#E^dNLYWSi%>P~O7=jVCQk0OKMkk1gGONW|&?iS%rWLjLForBdU?Ngm8 z{AqP+&n=jGo&JAEFHHT2ew=TVkUtd*pBImLuIq7n?(Vqvuj>D~W;ih{?eGpyTRyd` zpq6{aB>rx>5@!A<@E`S3QZ*2-cX8t3c=!d_HAYVyX4SofH>gV^zlOVssauJfP7WAyf)@Vl14jYdYwc(wRmf0OO8Kk;rD(` zgZDQ3CH_B;j-#r11)J%4Dv zR;$*j2e2oj`riy6|KnZrJTdqMdfsCr>`6?%VejzyEpre4`N0)@Nlq-w1!v>+4gTJ3 zzCgXHQQO@x^7g9}-fQbh zz4PsK(IGWV{C@+RYHs2O_QdVgp2+fY=z+c(u3TxeeH=lvz_sr9PouSF9ABD$!fEcj z&~x>>j`^9ee>4{FieA{CeQ)L}u6~RCE`h^L#`SZlf4AY?sr+!gw|X?)IaF(P4u8EZ zCh`=bE7h#^`EdJh%ot7QS=fs?YI7sHF1I*7eGLz517mBMx5Z)h(HX_$R@3$Pl3KlU zu(R_&@O75(7I4_Y{K=8xb1=Pk;i7fD0n`75d9m;exHYjHES%tuB%iSyX73+|M;zz5 z!@<_2^a}m=0dJhg!tDmDokgpgHJqwOs+E_-bfOOU>u^5uYy4MCr=!jDiK%4c#Kbx0 z_+Z5`t_>jnO^<`^xgrkAze?bL$;OL2DfAU;1cn!KqT-8;g+kI$&nG)@{EE4TOQ;~9T> z5D|2+e>3ZuK`em#mcp&di=_dc5uf`zEB#Aj48jXqSG31?b`3mp&>!v^t7ZQS`km@` z&r$AHfR%%vx@%W_d~FXBp9}>}?e8CQqw@foX{H(+Pn;#jxBSkhi{Y%AKc<~Cf78Wd zb$8{BwimDAzu$Y@?`SZ?oX|)!tz*r$sDqE^!aw_8|1kePTSU+_jv2l`ap+BEa(3$x zrPgEf_a5VYU2(GUa{tgV{)cz_)U)^9k4p`r-FNbIU*Uw==`!`N;0Zl1So%P`j!%TE z=XFK`{_i|!Pp$XyDEHg_M*nR!C*MZRutpC$nZby}9h%D3Ir{XlESh>#fydLFuGzcQ zosDv+&`)}kPa=X`(d?7Id;3T8ULWfZ;Zix_Sl^%EVR6rF|GPBQC+q1B@jvh&`B_6= zZNz`p*y}p{nM6~)tM?zLmYn!46f}ePD_)3`lK4*xF?ootK118Br=RkhS1Q6=+(SDQ zgYCQB^V=7OT@#B#y{lS(5mx7TPfQQz+05XH$%~jc(YG1&1g+z9dv?rTtt?C{JmSm3 z&>$0TiQ8lt?7osrdg^jwNI7TI)vN9JZTFIT4r9y`er`tCJB+wMA3bvx#P5peL*IWx zTeh>Vhs^@iHsd%1516cP1p`){c7ON+=Ra5Q^6JU|OwLZ)vpY#>xSROzNf^-!hp&wP z_Ey`^xtq%TbAA3r&*f&s)tH5Rv(`A>fAXaupK@sxyiiZcK>^n!a`CnH)|`8`nuCX5~+Qe7>c9-6x+eUQi!TihCGdkA`Sr&1&GPJz!mI zx$?_7{Z(FcReWrY^|)36cEYjIalQX@dAJFluF?N2L*r&N+pDIO+%K-`n#1Jvt%bXj z^O~vp#5qKqEUAA^=&4VlF5aDC|MY)4<5!8)`jP5?x%%#9poe?F`zf*2nZ^1H3tvoDy>gDkN%0*cGLXJLFcWk<-c&RFD`IR zOuzUGHp!DOBXO=M@25+LqTyh{FXCOQ{Yttd(vGTEq?~<)z*HO)W5>+YT^H%>jD!S zigUcPrF#3fpF7#X9V|3kPC%H}9)XR>t2ZsvDm@bH5@j zeH^}2 zkGAfSIQ1M0{QJaB)|meJSd9O((d@uc@7~c_6<*NG{rVqCcsllMaY39~jz;1mn`zpq z;${`QPvdu_mS`6r;VKsu}9Qpj0$GzLUUy{k}{M zgsJ(w$E6{DuA$z8+MaW>Gk8n&2+HYQ`Prda@C!Nh>>|E*7LIptAJQy-JzZYmeRoup${*%>OjhSt>x4qsiuJ_Igz1lsviCVn$3O=R&)h_KWmoM!#4%;j% zt`l;nj2y3V%sJkTX3e(3uuJAQarHIo=hZ^yWs15VOqBg=7(zIq}Fr5W%^*QJFcu_!)89StKYfZ^V-J&YImtG z@i%+-d^{{#4yUc%*dLuM9N{eLUfNi#Z<(GZ8Q?C!&z)~grzY*NF4OhWt3_|rr6 zzX{LbG!K3$KiWF15(~Ls$q8$9q`x^jJifXdE|Nu$7uI*fKT6?<9Y)dwp0kfNxiknj z`iIBR6{pA;&UJN#uaR1ut;E|hHXr zeNSN{&mN7|SC1B7b|5~K)eFz1mYM8&7r)L9`_B?VY$^1~Tcok3Ox;e@BaGSj5O<+X^Yx&~= zzRoN?s~_;Y9D1kt+q9iHJsr2r{*RG=8TF&H#E`!`aeA1rrzxMS5)L?yC!9k+{XXp@ z{}a@Aj;xN_cZ5&47ti|3T}*iV*+Ke4V)>qU%_ROu_>7e|+{wNQu75z!tGnyV>tROm zmwvEr)-t)ZYfpzJ6q_e&@J{g9r1(yF_Vh>x{HFwd1&e1Dp&99`>p0v2`~S`GP;iT% zH+l_juZDF$Do!gq6Z)=r-YZV+%ha!&HzSm{JWZ5TRpyptK>gv4b_MczR-nU ze}9s?PCI47nP1@r)5Uew4fxeXy~7-4+vU%U!uqOH%>&?IFJka(;BOvvcFR8ByQ8N5 z2HPtopj9Jr#q_*(&s2_2HM%RTKVXkG*fV>$L4A1fXG9Rd*3`TBBkIr?HSBRLJWq|; zGeRE(PTsfPPw|!(;{QkWugMK3!8c8FF{}*)MdVKmTGl`z_P-fb`jOX(GFHI8R znD8)%|CZ>9x!&FM%v0+X$Bbzsv&z-H$3}NTPUB(otg4Brsrlvh2|cPw;!&Qg%4qLD z)LU(VgC8~zfMaI-E-DC;niZ1+g;Vh&M)wLbq^HqJ>fOqZs$JZ#@HYINH+nPjJCk?| zd(R)ho0DwKZ2sqw`c>GSVmy)3F>pnEptu?`69#{}kDoQ3pY^Y`hVzAr`uELro&9eomxjQ$ zk}xoRJ)YsmJd|nbV_83+2M7DxyMGH;V({s#d$DscFB`8D2agN?i&W(ie%Cwb=N<1Z zKy#gPAHr843;;!I4Md z7&Dy-S6`nLqAA32e0Rcx?+~m>Bc)o#hv6fw$c;ye=j2<=gm{irA-#>{?o{L*<%iGH zOS#jbp65BqmpR0b#yKwzHtQuOw&1Vf?@j&$N1SgTSLO5WvGr9WBZFIX!XMw_a^B-v z595WIxaDmM9IZZT6KcJbtUiFT3ole8Wi!!&sLs!cSl{5cB6C}l1()j+T&(!66 zsm#cU|MyP#xn$NbFYh3i-v=kBMBohD%&L0tJ@&8vP`T)TBQwy6wZ$V3zRD)?ecpW^ z@aIN4dkVv=im!xt_tOOU+#EXZy4vO$KNKS^@t;jud0+YU(|0-}tnR*mbD3MH&)y|5u6AUwnKXR*U=3Vr2LC3Z@8DWM3&nPyRy1c=CgI~v{EnKw z6Zc`rOaR$etcoD(^5xOecbnEE@fLcvJyl~UblwvvaF#r$D)clqz} zuIf=lXS(=_zMT3$X)$l{UFW;4=b9P#&|Q1l-}je%%vaxKE@QHDg|j1qC?3e}aXgMf z<^+CoCLB+zX|K2Mfq!bp+x~iXr$WKW3v%eO^FHvRmH+?Teq6@YKH*zL!o*B?Z@)G2 z5tiP86`j=Hzj~N0!ik@&FZb!rX;;i0rQ>b(#Rc1$S^p5n$!Rvp`>&|V|Jcn#6Jv*Q zk6XQ*(^7{n^%eIi%nn4kBP2Wg>lYP7b#U)$NpoO%dI!_QR0Lej?Xwc%POs$r-lggr z4z~(cW%2vIzoGX-59Q#0?jK_<@t=CL@MRjW0v`L{FdoNHwH~hS*erMCa6julsJ!~p zNqq6+Q?|y}vgr+o`?q4X$qSxUMe`D#JFoxWQ2o1RulM6peQ@CIN$KjF_U;@F?!O%z z#KV>Uy9b1VSG&wJ?sL`+$9zr(P@BfQCy+)XSjP4w89({7L*Ce)Fz%??7>GSZi#eec#{$6eEP6~T-qY6DKE_&lP zU)qb(zrp9{dJ!Wc_=4&T-aJ!HSelkkHIDxi3#YOFS>bar{HHEVk2kn?kZJ`UGFNS~ zUiW!*nLOv2((Vwalh>x|^+%~m9Ow=0`3JTNCX~bb=@6yIIB;=`- zh7~~_cuf1(*`+Si*&Y9KCz{W%b{7UNr>z>`z9l1rE$hru_0o$L|EJ_s@o9XFA?APN zb9g3n(IQ-{a`3 zfn8$hdFoqSxh(JD#kmjQ&g78(124EV{KhBm%ePb2G5Cv&e_gO zG%!1edt{JX-`3Pi$Ah!?QX{+3Q;F>{UZ3(bkMl4O8K%7xUoXacW>izRe@#QtOEK`V zDR{zRYyZmUyc@$i(yV{);3a*b2J)k(wWIYn(mWe!*v4?_8m$KZ52zy-#s83@G{`Bx z_ait6pJrwk8)fYINc~EEmP1SQ&WgjS_-2gcMk_h>2$ru)%d@IP(>F3p){tkUe$^dm zj+JMahR!J10hX!{T{D<TZ@$q#vd%;C;dHUhY@E{rpA`ze%b|9wxl{3g zJj|w9z*}FvG~g; z{!gpp3*bUs_5PNcFyLGLzlyZuBei)X?+4CIqm}xKi}vbRH?`>$-S+Ap4b+JTp&qXB zesS=>ytu{WEcmO~IY-Z2yb=o9meAMVZ60z?M9|5erCu5l1Zr*@Txxth{T~>U@SZw= zYyLT zaO{iSaqogWm}0)SV2xQtHMwJNe`kc*%~$##4ftdY)go(ONFM(!UUwYl|JJgmFX+kD z{7pZ5a<%WBkHQ<}U1q$Y7B6NJf3^Ydt9M~PGrCU@6mahN6hFv+8<kBHi~sY*@Rr}5uPWvIO+`Ka z`S$-M9M8y0y8wrJ;!|SrQgQyTxt4g3ox9!WES&w{gkzPzEWho4h5_=Zx_Q-qok4pM z3I=|n_jVuufu|E;>p0K%NKPD@3?~Ege*sSSi$0zA4bP<{_Gh1arWk4Hi>=ll1|GpD z!ru+MdW`qVjU%+=#jN0^;twm1)0X35-Yxhh{>#gq=2rG_%@sLsb@%{b}lJ~ z+qXAQcF6smFt76(_3nWg|7_-ox0@5GWS-jdm%hX2zbjYi)W+o_f~H5+r^RMle&S~6ZG-a<&O zM&QbsJwN}m4qkg7CtVBg-r_%<@#zn9xWn4NAN_#$fiL`+$@(m!RYLk3{cr&MsD< z2M_(u0?zO}*PE(rFL8(BV&|ZoKCp?GQQXB@*P(A&(m(^ z_hDit_&%nhKFJy$6mC>r{$=yCiRDyovj?%h;@QC2y}8}Jw%3^l&)FmvpRtg{{xp=!0#df9%7pVSEDX6YU-ISaNNd{m%B*{J>!|i7+tR2EA+^ z&be)ofuY>sPns!RGc&@wc@6*YdF#{l$?43kxKMVUDy(}7-^Rr;|0SoEM(Go(yQ8wI zJuvLrE4~j5ew&uJSVO*j&I9$V|7=quX@bo-QV|^e@PE8!db*hUyUy?W$=Z#4Pn}Z# zdieQ%{hez*jE@$C#quQYLGQ7YeyVHTapTJB`NehNdC^d?olZNjgr9X?U6_l<;elu5 z(D!On_%59Ja&^9+Pbk*%-{!rgH(z$dY*8J!4C5}nZgcUWVdQ+jvDTC2Q&wv8Xur-JbD$4)M_F*WkTz*(6I3}KRu2c(POesE3 zi^a}(PCI)H{$7#T>n)4o2+*<%YF4#%)geW3 zz3;b6#GiV2A2+LvBd40G2YEmrCXV~RN0@uPn4a0J%>ejU6fRs* zBW8)`HJ&!uorhCg95wU{{z=w`)`MDhl1F}omrXICusVKcff}lTGw7})NT2{F%7&@ zkM<;`y~~LAw`LRYj>@gQgPNA~F72>`2jhKDedJCTeCX=i$lz0$@%~HoO-@E+a2}!= z{&6odxNeVo;x3c2@VE3M7B^P^Vd|$1^}qbSjr+}HG)a$9>*8w=|A!OnOYD&6Oe2m?7qnp^N!nNId{mrn$;#0ycLJH zm+)Lh;vp~L@|jRjF}i)doqGs3?uUX0us!^J>$P#&9Yx@;JjtrAR!z<8xWy0p z*E|10|K!o{!G*_s?3_n2Jw`a$7*N&|iyPyc@g z&5s+!7hea&eN>rr`y2NO!vLPQ6b++foI^kJ>>u8COqav_}mMz?z0Pchq~X$jUi_3o6dj!LoZw}Ok4Q(o*z1wA&#oe=9}SSMY@>(?Sw-ep4I!sh#CUm?v9kW@wC=1{nM5Gx;S4Tljkq@R*zDVG}K8y7q0VqI@r3u}kHELS*XvUg z9(9+CH0l)i*Vg`rTIzW02!N~+gG)W-z0 zPCOhWpIEPJ-n5SI()nDDW*4i~%>jkz^BT^||7<>}Bri(-SKG|X@_Q@F_p!LnI6lzN zz0J1Z|6huio%mIif^cx3`5$^|aY6G&k$e?#eRzyBY-jl_c*8AuQC(bA?PaD*p2jL5 zSLFGK@6Eq@o=3c{66#Kbf6pJA-z{cl(G!ng@(0u4q0egq-#hg*cfp&9_}7}^@q1uV zCeNCb_mXKdp5pH|S?0`x-?=WSb3NVk*6e@x2)`d*Ogu)LsvEN#(?{>XKU`q38v6>S z_PEH$Re0u{X(w>IsVL7;54f8hje= zF>WnfgN@H?`N^COY@r(D!@d6QkdAFNNGZeA7918nj(AxUtPVffzq?!1>WWW{{@Y z*K@dZN?2HvzfqJ&asW4JtB)Vh-lcI`9JXjDwPzEaBo0>6Y)5gx3k`9zs=b1yo-wYq zpY^xC&PV37dxU}o*7tCfJ7Qp0Xr%s4Y}l@@jh@Gc9&R58;s0V}Yi7K`@61FyY$?L0 z{M<~=bKVu*b$KbjV70yQ-}1@tFYR?J^|mm)=rG58)eLcwlOF$FkB2vT?miBb0NM;{ zkl$*}@;*^7;orgQ_;zQX^^|WXf3DD$i{bNYc$vElZ#18HeK+jOhk_;a>1Ld(hi7lY zFUkx5jz!bYU!nfD!ND}*7CziZp5Zf^{xuKJo>n@9_jHU5+R^_d#Y$vbwYjGEu|B{2gu9l& zEB4b%@bfGW=4Z7h{QZBmW`=@F;=lC(^FKe^1DH_7KTEffe=W^aSm!b7!^#)>Ab!`I z2Cxt}ObTPe^GjtmMg;*}yOVgJI~A+!BmPzoo*d!z61{v+T{UNc|TzhzNp8-6r1taenF$J1fNd(HpXqwRu| z&44k#;+fBn%c;1|QmoNGe5>zCqtsE)i{Y+Gj?3qHIA9Ta`VdZ=#Jw1}NWP)sGq1B= zIDI?V6n~ZT8-CaO>i;qEGH(ssj_DrhXL^vY#p7Vupq_5DwsmtlpVBW{no8OE~UR|688LJ9>s3`Z+6$%m3`YHzghs~SJ4h3K1zhA05<n^Y+ap zRQ-_uHI}by|6dMKS8xnK6*2pH6=x6K;q+}LzDH+g=)Qvq-T0IPL&4(+x$%@w9tFc|;?ua{ z2O(!8#96Bk%=pTQ+@qZnYzuEfcwPzp1UZp%jNcI@_QY|-0DgXKIW5O4_I20RVZF^H zdRq45bSawF9#koyzkAy3$A|Xnl~|6)Z-CcB4%+MgX!ca}jCg&ZE_989d%)-Pp0D!9 z<|kvCL(8rn*K$5&j(Z#r>;F~o94p|c`ZrY$?G?`lX@+(gtW8{fnnv`D^{Bbb-5dv; zzo2#hiwB>lxg(*L*==ioWUhTxua52JlT7gY;q4FoabCF;k;*%%^}oaE=9$Fn9UP*A znLs|?-`+O^91N`Cf`<`7pJI67C!S$~^OBXlzy0{}I{&ycY>pSoM} zvbolw&dJ~^=jBgI>$MnGX2UIN$i;ZAU?BXTq5j>4@jJZ#H9X+p40psX%!OV%^>fC=L&^XKL0uuS@K?Qj5bpJSLk*kDG&v)x+2i;u4V&;OiYJ?c)^PbjT6 z!iu6eNK3JiyRdVZ&K<^B&S&9`U2MuqHP-;guZ(j?xqMF$FCyqs1E={B zKkzv<#9heqw8kB~q%}kLhdGQ4I0~+Jav@BMiH~}gSDE$Nlk&9=!|=1T^$^~N99;ef zFZ`kSAE)lEf&tm+a9G`xmR%Ue)Y5vkpV9uCy#GCCSofHpw9oy{L52Z=W6x&DS2^+@69yX*GA(0D$V}G3D`XbH;HTR?X4OC-_Fwzr{MIG ze*CZL&RoaG_piv=+O)z9_YuLA;!*n96P?vdM57PVcS|GYPSYUc%@xMs)8V)$e+>oq zZt~<#@ITd@Ae%c;Yl#1~Vs?st#%`K`ehJ|B zR`EQ~+HLh2JJh9HFVt|_>}p9hF&X_ME_&5=hNLC#!s8g%jn;rq6JYuNpI~Qsv)OWS zN_joF+xk*h&6}0eCx>Y@S)<# zcqu&o$oqeJ)XbFpy`2mPyXvgFe=mg-rrnLl_`654>A8BoSL^t2Fleuu{kj*<)RG31 z|8eNdA_eh4>-=RsXEfe-&vP6-yL9$x7|%MRzM`BjB?rIAZjCNFZ!q0_h8i}nJS~M6 zRI7Q zjQXF)vlVI?(!YkO(fbBTHalN1*#0+h{-FvlRXrQm%Gp>PD_c7Hx4Jo*_tX+``u;-t z_79q#=QLynK5GB-{o@(x@*Bi@zi)6qd7i@FWt!toz#`sF?VZuqIT^X$s5#C6U#inT zWwyYLd%WYeaJPq8_D(-!au1#TDTtr8%xw-LVfecRB7y|?#UycX_&kr7uN&>~<>Na@StL=6`tvG|_gvV&FbG8z0vz$qVXd{sWh5t!`}i6DLaIoZ%_; z4~FE400w_4TkIwK%y}R{ev6m47kc zSB`gj4Ufj3*NE>%U#UIn&bcpmaMmuB*qs~bm*Ne{)%J37c;z@ewznA~*jpz7p96<) z0lS9te>Pgbf;r5VUNtYjnI~esldJ#5_v>j~GJCwkOxIVuAzX8^*xQ=bY_!_d%x9d! zjTUxx=Ff8-%+FV>$z$EDC%VqezqNVznR6XtF6T}@1q{phjoGuNz9Xn1&Kd>R-{6FD zb+6pX4)3!3r?#ZGkMvemYZ#eHolHh2cC`1c%$w|mvkCmZw4NXS_kUnkq?J2R@TdwK zoTZTC58HX)f^^H5=28FizP)L+J9_1V^xwr;j81Bh9NF%@V&ulH%pz22r0?*pzHoZ^ ztPZS)7a96`o(8l&-g7w`uBQ$@g14vDgeCmsP-8tA{5LcbpS5Sd(SLQ}{*(GNNgdp+ zIh?ou#YdPsrvFaf#9t@Tf4FK@yk^TwpKZN9g{@zTi_O3B(cpCS&2-NL-imeo=r1$t zxXf?7j_gz2JF!hK878E`8NwcLp8etTYIsIv+;I+nXM#D2;_kNo+CAeboW02sK7*=G zSA_3VoQvsaMhxb?rKP)zoxJvJF%SLxIlfCZ@0^-eU9Ug<)cSeup?GADqxQg>&c!p{ z$c1#cbu0NFyQo*Fgh;n_KH2ZPh)jtqh~VyLp2$wv`EhE(!%DDD#9{mz@57nge@{tE@~wpQ12 zc*c&QAj?iQqLjNi?_Kw=@*-|%1j55MCs z?w{SO`!64(a45J8f7h?!lVp|4(bfF=p&%1(x^$p3K~4A{=55M(-rw-dR@QrWMe}MS zJ?A8Icthd6{>SS@xRiXoJwQzNS0}cqQ5kv%8N}pe^{q-zIy4@9&Wr!4Kg+8*H@J#c zI-zg=h5JL{Ow6CG={&PmwEMKRuoQo~GL^@d&pV635f^DVd~UaU2SSPIwdTB}$?mU$ z@yTjM1U(O1+vxmHm^c9b$Hhmg7E>>0!R{UEqk0g8=+WQc@d~k73hvNSr}EIj9rX|< z>v?y_`#w%d_C}#)d?-qTI#a*29Ip?004QlFN=2EQjz8az6?rweIb@nYEjFVgQ zd|ueEtChi7V7zD(Uh?cbz3k7z?|8$WR`?l*J7osvTkCk6$Jf`~q@V8vuPduHt9dYo zub4TyDBtDe8u8NQU0#D{fBRwB--m_xZ>v{6FNL{|yfmwN8|_#9@o9RKHSumSv{SzS zgZ~}l|F)~bdmqRnvL<`|+0!e0HSs@~mry+&Eh5)@)A(`j(O3MxWH?8c0eHHYD;vmV zv6A2KO!AjGLK^Gx0iNk}TpvhwATXD%a-ar0L=Nz^;WAr2x zEX~eucxas;+ozGZ=tTPSTRc%77hdcf*7r0IJS;MXAK@8Jm7(YU!9T50^LO!1e5n~6 ze_DcHm=5PxBNx?i{zFVPoM*NJH`!SPU!r-!^Q@ck{_wr~eddb;`7y37{u(KtD!@nm zmL_8DkT~l4+&O7_V-~J)cCGKL$<14RYW~qbOYhQieCzB;EZ#tgP%sx}#GLOt^5W}T zTkF+4(MB{^a_^t-8Q-+8cUk1jXFVR^l3qbmUg)b&XoO$ve`$GdMrf)!R%j3H_A&mT z9<84#mhp}BVsLdeI>(=7Z>RUz)8|iu5AZZ)SH1(zxhuIfNN8RLN84Quuf|=^h?Rx< z6>F-PD;W)Aaib9IoqXJT-3bM$XXr5;bKlbn{;%gAD*ijr3`@$wqd%SBA7m!32R#&5 z?9+cS$MFutX9SG=D6<|KUwt|J$ye9>+hB80)5HER{;RGm91#j0!shj7)rpwSAK}BN z&d>wAfw$k`SZYWA8g%-RP*A6!bAMUH?|icydDI(qZdXP8f0LO3{hPyZES;P!(M7$Z z1@`v+-+9$%x6Pk@;NHhPkwMB2eeQZ*Jx?LS<_PC^^eg<_7HeH|J#LsnJe`LN>(u&A zQ9;xB?!Ssnr^)}0Jm}f*?cPRybb5X;to>Y$j~&T#6ie~vnv}OMFwpz=;8gV_oCL-!rRtQ!M2Y%aIG+A>y6SGSMb&zNKA2D#O@ ziL(=v;N)()ZZ?faTf~%$18~RJtG$c7*!_Wb$m*GK=Z8)4*)g!PhW9y%r|=Haq@|5x z$-_+iE;XaP{akmIpOeg9`>bq>V90lRO1;h6(72szy3ckGe^z~~Bc5x$!TXvytL>d{ ze&BxpO==kJT$fk(#|bqbU(O+?i{kCKvbhi7jCm%tb3Xs;sJdH_{>=%4-@=GKYFUvv z@L&B4W#F%XVT?IOH~$7F@v4}o!uKces=sh%kZxXxi~TJ3 zN5{Z3yPGZdcSpPwYxL8q7HY>vbN3JAuDThnsnO+pe(O`E27Y;5O)mta#A>U9YHv~A zV+Cg+)rrcP^`eW40Uk%@dFCy|?4MtUg0*sR%RDpkOYEifDzMp|NW;X&RCi3;!%<1y ze^3dBUMS~&(UVa(Z>Ytyt^QL2Z)KA&Rh4E zpZ5O*ZSYnc7Fi}&X; zuhsbte9TK`mKxxV;}yN^2qCwJfUk!KruMczLlFEXdK z>m8j3LeIQ-5Pf=3llHs|Nara ztJYl;XRXamv(Q7u(6dmG&U>Z%S}geg#3T3=tGjsv@iSnrGjU01$6@rU|NVlq_Lsw{6ZH-< zlrsNQNbaSl3!aC+9|spG$_vC(F5^u%)$`}{?-%OvLiqPlDtjaTfA~bs^B*%4$w)=TK z$|FPde(>Ivt@-Fp_{um@qvZM+t#Ibi&JAYm6Eq!x+x6jx;QyoHW%^%yPlNnkE{_iK z-EYwUJK$Xg@sibZ&*MiuwibVVWR_>u|GvL$q`zNOE*|o;V~<4yjsCSCF!0JZa;Owf znve0~?@(}19E87frQ>Pm8cyP=Cqlu~d%TgMdV%fDoOu7Y>hNW}VSXcPc+wfR2z%x4 z_Vd{tf3@DF`6=pO$)9K{7_??5Ec;p>(m%V}i>n{ZE#Koc;J?XXY>ksVg3&Z49lC{% z$)B2bPm7;Ms{g#5Hu8TEujagZw(Tj6IKvETTO3J!UDhk~|KFb+Q~vKNj)z;{W1090 z-W$&K}}vX|KxIQ>2SHU3Bal3#fzi#+RhV!Mbu=!7TGqbsK3 zJ>P_ay>hPO_CCQfYql5v&-fnB_?8EVN4&Bp@A5YiJr4yX@!j|Hh+kZ?gFRmlH`@)s z?d?VH<7QcgJJ*+6pZTCU{u1s4xr5is&9klizE|+^5s##v8M_JinZJLF{;kr+%!YTl z^_Eut-OL7!(aE}eDX-^^*GI}pN3ZoxU3&-TCi2%|%a{TYK>#0z`usvLDm+8d*t1^2 zody*Z+lBEK*gw{Deg4?|Yf{+XQXHqyOHJ<_$!J_J4PO_&?&s^yQVYr+>JvoWu-|lU zC<$G|8;vE;-W|)g9AG9YYbe-;+m`VDBlGYQe$(5KtM#|jHD%PtLF$nF-)OHtHXT4lj$Y9QKXxuFJxA!iiFSqsO^Y4AA65_Q0Da zYD-%8v)kWC^kvsk&Zc}RztxAyx8z?9=c!Ke5wh_!Ho5blD4cz%mY1?0^>CI$cy$Lj z_S)K8gB-l1EH~7l*}Q)1+~o$YA>O;+hRc`DVZo4kAM+Dvqupb0sxM$LzL5NPvr4$b z>>uz=_3LY1O4%d5g3XuUfgBx_&;Hf6|4-c2QUaH?b$S%ci8Z;eXM2faV;+e zX00AgJ4|sGXC{0;jk8@p(og^3z-r`AVsfUO{@4EQvpw9qm=^U1s`d-lD*GjB6|Ou>Ilj5GT8MGbj9b;C2;_|GZz&-)i&q1U$E z{BtS&l`r%{hMS?qcbmb3-*fU@Xpfyc+8L-&(U`{I7rB@R2^yCzgMmck-U&`F;3K_$<>^HRW6UHF?=2s05e`oS;eB=gAyR91BxUUvG*PAw@l`)2UNcb9W<;u$~VT@Cc)tyBX z_y=|Edv%^w7d^NHdZ!`0<&nG(>z_l;nEXoXd*`(m={b6D$|f^OOZa9lotGQzUEy3} zy#8o+9@lUDeleIvAEFyycg1nFDuy0zYqMeEZM6OW_h>|rK~H;v_%Ah2{KDbk8{qqY zJnTR5;Sbd%x!<-Q{)pqoEUC_P_dV-4>lE)c*;))PdULW$+f>rQ?jUNv?(+$gMv*uuP8HG9=3>SUtocU07abGI=JlTB2 zYtOcx{_%6^Xr88U@Z-t6fhFz_!AB0Qkq<3kq(AGG-Yh3qS zU-@3M8?@OGp4hMW;t>Atd}kwCnlrwI_wtT*zOhDUoE^w#j-WYD>;3Ru9OmD$^WxNs zNA*3AzEF|==6{y+m*o5S1N`sL-M?MRc^&`$7ah@mE6z57pEN_Qq;FU7xeDRn2dq<- zNuGBsZur<)4gTLYu{&y&xj~w(jT%x8ZpOW=XK=xMaSy!d0gUf1mzv0lJVw&A6W(J`W zuXGCRhjW=n;n{P{hp6L8 z;F}9#;X3&AX7zZ+K+ktZ-LjT(tjRwKc-8jk__yYH=!_22a7Nz4{A_erI{qKrTIckmL`G0FZ zcb}>JUjwt_bTZ#u(`;rH{jUQ2fn@fzFa4Di9^={T>7IHEdIbj~-3^dbZ|e{JD)`qo z0xwa=8|-m*sH3_O%Xy=fIHO+g*lPNp_Wy7rzUCV7&J&pB=l9`A-_x+saPPC<>Labh zcbfAX^~v8`VE$46XEjbxAGfOjORG-d(G-XOojjBH-)Wy?*#C>-Zw*ZE1>fFmhCkML z^&01^^bbZxgVmqwGv(z0!?wk0McHEV**h+;uO`EV^zh@al=dLP+GnQkYWsc}+I-LN z=2>y#G%I0y7aVScd8M8>q4!^?Z~v`)yFp*PeTZAh|EcgIS7&zx!nn&k?4!A05dB|K z4&UJaJ(%Ns6-{tr5}#ZgR;c4l_G%vHQ92w4>i{>ufP<^)uuAgx=3zX05FZ0)8|>$9 zsGUdD>Ld0m|6z4@ke3%SW}JZ0ec6UADM4o*766uXVv}ui5FtH zgBtLO7*$U%JPHMq>+@6^;{w0QSv|dfs_GAAb`Nt>{HBCH$lhMT<{k8(_|Ho--kM{t zC z7yp}cI{QZ7t|UElo!)K2kEIJ9$J1Z?gQuIq3^C2ps5o!%AU=g3Z1LwKdeR#Wc-=6e z%U*pn&v=P<|GjWVm3dHmg+&m2B?lEdfr z)+Z>okpEEvPy1YtQ@tBppI;9XyYaGec!$^d)w?L~A7Lf}7KZ!Z;n}1VKg+?7BZE14 z?f*gdXCI@N7x4!^i3o=C?$W`6LHJ*59?zJrW?s6OHHfQ!n*r9hbM9sa4?}H8SCv(x)5ob7K8Bpc4lXSqxNj#(j|T8aU`Bc_^AXe4ej3IQWI{O|n*p~G(8Y==0FLlo3klAQ!u@>L1u`m?$_t|0pw=EoR`zsG0 z7Ps(j!%xy9@$pfd=P0i6@gsM9?{g+@gMLsIc-5IFr&bqupO}$6xC3Y~D#c`Mrx5Ggp5QH_7ia>+wOpXzSe9etYU2JEZ52 zc;9w^>NCva$Mw`7=Bb6hN2ss;kKa-MryF1D7raXD9-2dQS^L}l7 z-bH-SUfzMZw{gr?Y5B>;%z@}PRxILk)!3`naOMM=V4=Ix{I0u;Lcx4B>;U|~))jVV zbvN$;Tnp!T0Tbrq5I2VLmS54vAMrq9@?_58d-C*x`)>>Uu8#xL@-D+AS9u4|x&$Ze zMKgx)1AXT;?NpW~!3%TVwf`C2X|{p)nTMyBRIh%C`%wOnCl|x>Tf98)fA+O}Ehtab z&)l@e(Xudkf!Y!1FC0+M)`s+evp^%QpI>Q|q*PTQ6-eYgT)RWc+zs{3feeNS)0IU=5XI<0q_iRQH? zd&J1g&UC+ey5zo^cg%Xx)YEB!vS-C{EZRN2dwF0%ywT>B<2V;w$XxwU_4I`LA72lu zkN%fE+^t74t2`XhU!4kH59fhv9s2~EQ>kfm{v6z>aVtIdQamkrmsc&{N2|vF-Wg6c z<^C-idYxYMA@|@$qm|~VnK)d8f9CUh+f}x%t#L3lE!Qt_bP)`Fp^J^P30G=zQ z9PSLyuapRX&t$J4YL7cn|G|&f`dK*FPYfT}B6d2`|7yb4OE4xeKfxYn{z*Q=c7Yk@>_1}vo2RdtX0)j=74sX9Z3Y2 zUt2qv{|ipsH#Ri@Tsz3;F3Uk`Z& zKk{DYn}?BC!|dG1m7c_2;rzv}a%DbB>e;+xydxpWBypMT=_@c+LU!Z$bzBNOXi zePV`Xoc-x!#!yYz?dQThgx+F08h+R7U;CFDM}B}u%{L!`uS~t+`2t+he}6CqZo!0X zxY<1Tzp6Ct_|Obbau|Rc9aJ+veH027)pXD3AMU9=?|tn5BJp_(r@cyZ<*UyB`bXTY zv!`amKDQPHJ=e=J?#}PXtL@;kGxXx~>5syybPdf3=zlaA&wH$fAIazJ_;8~ZdM6E> zE#2lj3<}ZA@_$czUeqhtD6YeE>}AICkmO}+pSMcJ)}`#!s>5m33|F-8gntUMC@J8j~wTh zs1=Xj#jDEkpnUGyJh&5XxYo1eDMf45)@M4;Uu>fv1K$hea5ldzd=+ye=9qb;8Hx_m zOWxu;WcUL=Wuia(!gE|HdrSRs{JiVmbOb#4PwZ`l%SF}tKi8RGvHz=9!f-k#lCRNC zy^nF&IoTL;^l~T|)s99y=g+=S>nrh%)V~*E=eNgno&B%c#a$*1@Zj(GU{fQ5V)@mv zyyh?Jz#M+iQaCwqiE}ADi^V^{{(mBa3^aIm^)IOy_;)+}|A&Viu5mou&SL*vT%&43 zHC}#K@_9)|xtCDQYM3e%oPMeo%}dHJUrLo1v(tF1-@0G+YyAk`#3Wc91E>7$jouKw z^+zM~1t3E2))3yw?G~j+#;Yr8)$cU*j)D+Q}dLk|-Ih zAHtiui(kP0Wgptl6z;^KPpZnFRo3XU!oL5)v%k5{|A};8PbFvP=kXeI`FA|JD1CPK zG91`$rnv*X&{SP|9SYLcH!m~B{3RU8a}_6;Cf+wX8}x#gVgDc6|L>-XEBnaSx!D{Q)~WhGN#HBY?MD;8=N)>{2=VvvdAm7tP>X-=eG7`m9@RtUe&BOH z_mJZb8Fr{eW99S;crT7G;7U8-WzCvC2M39X+ZD}C!}U~~I^nN+P`5|opyF>PuO`h% zJ>};#MP5GJ7W07OeWV)k41cVFrxdgQiD5^d1iXMAJe=arrmB-&CYlf07M`8NtzlC> zdy_4hdhT7r-)Yqt7Is!AzVs|nFrrK$SWN5e>FgYH{7`UWzd1|2{>`2x-DrHNTPXO- zKE7urY=V6mH^3RmWH4y0=RWOoR-4IcixcN@59=u0uBm%<yP1Y z-%0#~d}0aTX>m#qYbH)^zos|${M&j58F)XZt!F}UwK^GGe~)hh|J!)ZGY8-&f3*2k z^D+4EJG)`dBeR4@Lcv)%v3-et-desee290`eGIT>{k7h~4_Em_cVPShu`wSvNa8M- zW$+q4J*UA=sBtq(Im1DB#JJou*i<(%XqwS^`agLYxK~_jQV}O^I9E?pzK)eoCD-e7 z;58X~z;C%zLM=M|zI%w|P+gZg_ z?u7i5{wd*%ObPE|ZHf%BeqDIGJ}2y(YRTzlV_w56&VDh%*5P;>5oHvdmK?7Rx9i|_d3@yNSvp%e83Zx(=perw|Lez(dFQl(Q z&+KuwY@r?-%={9@eyOg_J?{RmF`hBIdh{jzx7^>YjPJmqTc3*2hWrNWHp}y59nNc! zJDsxNKzVqF-oJ9Wh#+dD@1((D?x=rT7n)zR=4*a5KR;bw6wu$8Y>q9TGv5E%*QNAQ za?f;DuWlf3T|Hd&zFCJj>h5{-!!Y7UG1X$2`_atkZdmE(-!oJ3G5lYxKLk&A(0|im zL0xf}Reb!s2}gxJOWvCKf?2KJ)59CehoS#2&kyrD_g`=H{VG}QH~tfE8(o@^{<+2* ziNpVrKmY7g6W51=_u)_LJN`NfPh8Z*y%Km&O0#pBZ$|_fCY!U5kOyL)O~Hpsk1+`=M3NqOL zH}=2vG8%f5fA@~TP2O8<{Q=x!QEWA}zCNn=egaRgY;`{u|8sRO-17qch0k5aKdxNT z!-GBB_`kL1nCEWIBigRk%Kuoedj{Lb(Cd-zf|WywS~+`)8z+-TpW)dHpPS*Z?ki)` z|IPoeq&tt(v2NP{ZVN-jh&;)bCR=u8NL<-cwqYjO%aW)kdMz>cJ!1wfw!v%n5Z;E! zZe*V^lm{tfDO;4XJQZ0YW0`!O@BMlI=+jVh-`DlK&ht2q^El7n@3K$!QTt(H%@VAn z;XLb->YJ&ZaBo{WOuP@O2FpwraHeH0H1U#XlVF056LTwEuTC9W&H5R^zMS z_Jg!)eQQ#yFRZzv_NLPgh^H5>@rz6GEa<<9t@+S&#=h@(tM2XfP54%wIW3QUK<&ia zEndy4)Sv?2=F(icT8%jm2h`#(w>;=mn)jNj8TJ& zuWfWuug#HjYt$@L_$aH@lPk@_{zgl{lXB0gb(-jXX6r|}zQ|nHnjEzTPvV6yX@xm7 zI6SRNm!%D{XYm(tjUT34x4VH_7q>2W-)z1(+z_qLfFG`2he5eG?h|`2a6z=cFSAbm z!QZL;zYVUrX;;1~tgo!^b$pbwYjW&O8R7iv0%mjZU#PKqcaZO}u0Q*|&(^_x&$@gc zIYB3U>e|-Swhse_U?-T$?ptXNu%GBn^o;;U7)uB zhpI&O30=nRqwSfigKPG?-iCSyTlH&rEv3pjuj&hDaxQa^OYNhq<=jzq>aVlxVQ59y z7twEwwn66$@=f7HxhWU@av3l+_zj$JA`&f}%X+KeKo$xV+-y z&bRS$CepCyZBZ+o=n67}qzs=YGI%GZ)M#k3`%g3Fal&qex&h?|M*%BB@ z&(spn&(HOFCF~8}iKixogZoixht7VlU^p1i2}g>)vjuR&v`ONm*3BQY<>v;yqQOj^pZWe#q~@6WsdVIzhf*96LzQ3$9Z90gLL}lH2o(pKNJ6> z%9@c{qW?cwf4V14w^w~khc*9DeFIya(jMIQnVt9H#BQlU01u}niDxlTbt3%>`*z{P zD!AkJJidA(aV*!f7g8ED@uX^1)Zx!d`;f-T*_4C4dl&zgTB;F1zR#PsZ8>7NON~#+APd=}l z*aoZOr^_wzk$I25CPx2;!I@$&>8$=7zq6qAdB!yjA7P%Qkav&Kyx*OmyT1zsKTPJ= zH=}jsaJBPzWt^UHoS);JQ_x7yHXm(**S69nkICueIcn0P_T3)U|H?z#;_hZ`^h?s* zuXx+7<{ojFmbKn*iuvXp@M<^p!47i?H^fAu=YPWNv0M$W!-3zaq1688Zt@dPm~EGf zP2Zzo)axZOy<2X-wWmP}s1Ix8*oU7M4l10XZ^TXYR8Jj;WYX4EC&5;{cE2P3p9BBs zg_n=g4$r{{`S=ffa3T?=meUWN?X1jpv}a*lKVJ^=+}_PKi#ADo{SXRDi}wl#-9KzE zdp{hMp9aG!@IOc4J)9K3kjL619DL*Z#{Z&n| z_y1Z~x+|8ST-f~o8+1SZ%iv3NT224a8i{I&sA~L`f%Sb4mCr>W~`b zW+*7|HvFcMj>4Upz9Vh9HSLOTV_mO%|0!Ni*55^oi}luQ$+tYw8S-&6PfQ%ez|x1p zv`8UyMg7IC&yB{94QraSzRx>rVWz1P%s;@B!!MDubRVG4kL~5BJge_SQ=eMt`KyB# z)1OmtO2T0A%B$;gk=CW1@-No!`anLm5+Cw+;ClYdT5|>ecD~9qSYOOJ$m;)Vyoe2S z_3bV+_(I$;n1}SddYS+6rFA`(?HuW8^kz%XGbbGExE%`iKc(L6;XZL(GHwrMri{6G1*? zKZDOIdtaLMJOtei!`@vguC-P7KY5&fI~@+r!rRC^eu>-GnOAd>2I*xT z+BKkQ;81;@$`i}QiS-{!dzR)qUHCE-oWUbmZ}b1Um`748SL3hG*{+XA|K0u2H7vkA zzGHk2Z3EBWAF6ip?WMK>hze!{i;2pzDcCVh|I<1RsjJS#|2J{@i0 z>C=zYL$Ov{)wZ#?XsKtnUA*`0ZNCJ(ioB<*evWvyZr|1p2bcd+^PGXD^i{_*W^Ts7 z_5<>V{)oI2@I(_@m3}_6k8V3fYx%j4{LD>q{*9P8@Q^-T9o$R9jiSS+jj>;DtlFUs zFNxM}(%19cXZ~@9`n|T^%S?3|-O^%+b5`hucX8yoIbrVt*C#ld9Hi2*dtlah{FeyV zi+!Tk%fI>4Gp+cUKEuoApIlcm9X8=-`(xs*!MFMdpPT(%rDyB9rWWHFs(&k;6F)Tk z9{OqgC_Md^y*IwEq3^!6z`dxu+FYT3GR@L{q2|ixJPX{q!g~JkQ^~So4)-%w*$@dSjf4W-S`+w-5h<7+mH2 zA0AotJMK$fmT*md<@r;J+382azr4Qq!RuGQhi!>&X6 za~t2`1L&o7uYzZOH>){P&%+utR+~MX&)TVb?sn$OzeOj&f|24d4wje0)zu0``Y-Zd z>~7x@3i9IL*OJx0rFi}+W7|Nrgdu2 zq4I6Oa{2o|^g<qkMU$Ly6>-Iyo3?%(b^QDKZn4z zd$1>WHm-*`1z^WPaT(d$aZx?^?g@N0i`PC82f^Oqd-z9<&4yl)cS~tXd-k^E!cY1& zE7O@u_dJ&+da@1SX||lr8xF?DJ45ljxvOXN#pamJ5nF3;NC$kH(MGT5CJbF-4pKey zeIv6#G(j>9Xtj$DbRWxJun%{$S+pNRLCp%D;ZU=#{HW_?d2JuTds^_1P2LFubB@9P zXZQM!?|J>!_M>*Ag4yTsv^nff{|86fc&AO0-cf5a=38k!@!qft-IqhJ;G9M^aK|}0 zT+^9B3*agJku=Pn#!u-UYt_A!v*_rOe4X5j&n=DH`wTO?1sCh>Gviv6UvgJXn!wi( zTUXR9=ZA~amVEo~%|c((Tlym$q{FuidhEkZe4o7P!vx+fE{eP>B`m*Uj?gFY`#l;r zf>*cskM-PZ`C4jt&p%e|WM$ERdID+v`I^08LOmY4I)7<~g~jez zyg9p{e18EJ@`QG$`wSW*7LQ!tWuNVN&*&__@?T-^bq)tX9AAU(I7tV#^DN5k66X(! zp$V`~z1Fd>8g`0U!ky>j^xO*kpU=!B!QL0e>i9@}&mZMojrw>K@fJ_9-d6Jp+582% zdMpk7Z})#ipZcHpt;i)_g?MZ`&v$%IQ$uO)}?JQVoJb$6e8T z>;((H;UB`osM>m?;wxv3y!+5xQbBv^aAu*W_+Rorp%v`;ovzE0!+PTN`FP(JG3&Yp z=Z*7w&F$$G|DW6m2POB0g18@@!A7GEr2FQ3wsr8_Hd-;`lzFX}=&e^_&N%q}m>4e1 zudyD(nwWiENdLV-_saiD@TZVm->H`C-<3wzb|BrqiyD;U_J2&rjH2&i~j` zMvmmtR4aU5bA8|DuwLCXpa73!2OgIH>+o2}^SBOc+sOZ#a%aE{wI7bYiW|p2>8!lR zVQ))$bBvDGbLs};j?k{l9>BrzG({`(4@;7R*J;^PFWIMcQC^GxY8&;9o8nvgr(}1y zUY>qPG)su@cjL`UYTNx6!@%>ZBXSMshL7r2DKR+(y8T6%*--PuyLM%^nJt?$_e9q|Te4GXT+ezBQ zcXi;QPSWFTCg1DBths~iM}jRyQ=R=mmz+IjwnOa@Qt#(`FC4`20#bUKIsRKuO#QYg zAFu7Cy_Id$VTa8O!PvR}T)PENQ9bi$4|Uf_So@ec+-0x>@2r5A6UFvux;c5InJzju zL45Y3V@6x&*YVH?^W<~`{D+U4z<@SY;cgDRP7*_l4KmMOOyUL{0AiF#~sH z<4I>*xVELE?e&C%{TrAKnuYI=>aB;I9U=z*Ztc3n=|WrQ-Unm89581 zcl0{ARr?D^l8(oZL6^Vj1-Cg3F_3Ld#kQtf8W1V*M99v@h`0W z|9=Al{{2!wfd6W#u)o(N#6Q2Z+(`xKG$1c;H@w5)>nAz zYrOUK-uh;5eY>~5+gsn~tsnB%k9q5-y!Er*`bBU3s<(c_TfgnC-}lxZd+X1=^;h2d zJ8%7yxBksr|LLv&_142*`~SWKME>{Z{;2={^vC@7XQ{Z}dO~kKiMO7@TTkPyXYkfD zd+XV~_1xZies8^yw_eO!m$zQlTd&})SMkpi{oKHmBOZ{76PU2lDmw?52UALXr&^VTPN>r=h;ncn&wZ+*VEzSvt|?yax(*4KIK zo4oaH-ufy!9*IdZ@R4%Ui$atv~YCpLy#qz4f==`bTg5 ztGE8cTmR#&|9Aagd_?;9=l&@F{`ANA_h+d%-g*LWJ+Zf*+*?oWt*7_aGkNRTy!Bk( zdOmNxptoMsTQBLYm+{uid+U|G_3GYwZEwAm9uHF5Y?%Z@ss- z-rrj{ymiN0ALy+Q_0~sv>tnt33Euh?Z+(WhKHFQL=dCaD)|YwftGxBK-ugyweXF;= z(_7!;tsn5#k9g}Ry!F%G`gw2tvbTQSTfgb8-}TlXdh1WU^%vgy8*lxCxBkUj|L(2- z_SVCN{)>-@|Nh(``0r1D^nZVritVk(_tq16>&d+JRNi_zZ#|>8p4D5=>8-90KEzue;jNGH*2jD6lfCun-g=0)KG$1c=&djH)>nG#YrORh-uf1AeTTQc+gso7 ztsnN*k9+HC@xX}$Ff-g*{qJ%_iR$6L?utrzyzi{B`9 zCAO;pkuBXx>S)0`L*23%I#u6M!ReN6S9Y}QwW-+GZ0#KGX!;&g#yd;1OIxze+nT=6 z(P!7xkguki4Rm$$KUZbaI;!)|*7#($Vq791T)leXQ`}UpUPQ80?x3lK@mw9dVrY9#OPinBO0vjQ)QGNH$kMUYt}0J; zb-auz>#V8PlTCfBN~_yzpSSI)-mrEDTl4doYRi52Vq02} z*wyq*?15uycsxs)9-6xCxH{FyR!gpR4BvMqg`h)rd2>z*$zWZRq$HTUUQs>NC&QhK`o@*KyRJ@9r|h)PM+OxFBBW4bi zG1TOyt#9>Qt=ebmUPebTDiKGK4EX~b<^OG|b{AswjHz3p+)EWl*NFd`!!0=tE!~r& z)K?w7%4{m@Bum9>yLubW)$-c5{tmRYG}O}W?WTT3A(l&<+P}lq=aHuT>n(lHW~+N# zS3i=Q+P0j%TX65rC%agJtRaFl)v`J2E}#F?h1 zZ*bJpa#XFRse|(@{eAAJK{Qv5Zrd6$+Sb-Ow)THCl_56gzrax6q<$?OYAUo9@!#84 zjyaB+O*NE>pXmU0R9kPT*lJfBSc@LdRBmu-Vn0*!KbtD|ka%P-7qoD-aE+-r;7pZb zt~T@U72{ai!F83m=qhG^;`tPLoZ3+7Ao8oBUn{wXs_a+vPd-(;VyjPEzt*j^^m~h; zZQxYD6|Rc5HPvRNt+sCsRZMEA(sDCD-#u~cb47&enVDC_7tF+7iRJUWM*jc6()G53(cuaIS?UPrMs1H9P9@9vCl z>Id<4lz*?j*HE^vj(&`{)M=8b{KR*R&4v!}eM30^H5p88U*;;#1n?vdcoE>&++3!r zM6{Injj6jEU8Rd=%LTKaUGOPpJx3>d`<1h{qtO#xrATOK?p0gwUBArcmYTOR)V8Ce z0M0+&IAXA!tuKj8ovduAF0pqev#miz`K*hko{=|~?>XAL&CrhjEY(VGsn=4jbqg5K z##DUn|Mgf`U*u})D017iwd#_krtH%zVkO61L$m9<`X0?v;mq9sVq1%%x@z3k&@AHX z)^10Aa~RqWu6^sn?`1Ppt25_!(NLOkNVag?vIUxCRTHRN+6uJ!5CC}LrkUtiCg+Eb9Yo9$@qa`Kiq zyL--7+1-XBw=uMrTo}O_CE03g{WeoSdpSzGz|pD6rV3vL!^oErxg8xNCPr~CuAltB z>*xxY73UW*7z+#vXDbu9G`y;*J5@|QFkA(tw3PEZ`vv~BD-4EBFjS|8spy=0Le9g8 z3x*D`bdz&^_0myFaG-oAOP^ocYEao!j2fRMY3 zcM#LxeCnJFe(LI1hBA(-{~~|k^%)x+eJbu(?PKg|SwpYafd{9+;SQz>Oy=Ak8EQ?O z++*)Qmb3ILoWt*1ibM|0f$P&nGZkf`rOWX1iP(-Rf&aU{kl)~8=8Udlv!7R<+dB9I ze*Xk+g_=q++tlTzrrNb}HLHWAjYTZIt8Qyl5l1uNj#R{NqrZ;)#ND$xu4V*V>M_sM z;V+Kj`~VZ+zf@z1#Z#vK5JP>)sa8w4o?utQ;`90Ez{(q@RxftdmV3%!E|3wCcU7AUHAcSw|iAn2NQ^0)HLVuglL6Z#6E#SJ`Y8$YN<8 z=Q}gKsjdrMog9ybAyy_4TT|LQGI|?I45qw#!hT=#X%G3`vZSr@qf8AO>S$zpQyEth zrwJY1W>3ZxM`QTVJm(ypyW=VX*i)NWZipuMn8j6Fc(?T!M~h+@nsm_CoVtE>=e}~@ zhrciR)E~UxzrnBVg)JR!Y{=sN#}0-!id*`EcBsAGP$=gxdYP>N_-ljXXl5@s%Vkg5 z!*OQ~g#(X|HUY;nIGR?R^DfLa^7}^)x_Ug#QIR0{|FNy}FD;EkE3{1rmQ5y)WBYaM zEwP`%QX24V7{5Dtm!Wl$EDb~V-5?%2fYVtU*cw1ibprqLb~Lp)Oq(4=!+`%u;rJs@ zP0c_rw740D_of2b(;R6nO{hpda1R#w-(#FlktV|J&Cs00Y2k9@<#0nq;QNd-{W^>0 z)K)|8R7Xdfxr(+4UaMy-0sC1lilK;)&|3q+p#khqHA`b6Ir_cXQG4`88u050d(s3f z4Vv$0O;1xJPg=^|%2jHxH2YawW6+h6&RSZM#!}#PQ`LwoGr3=RhMO9d+fv`v@LzvN zEz6nebpU-)2<5IYRy2^`FG}8&JA#N^rBC(207|J-mg;6Om$>G!c}zi0WFc@j;StHZ5^)y z@02%Gfpec1h7n--q1&b!*WjEdgRh$m4H#vqbuvpO!Mb~#!OBRc_AR$m?wO;ZaPRA+ zj&fA7wFTW(9jzK%g7c3^JX@~TX0r6OHTxNCt5I)9Yo{4{Hw+C+o=x21)00!KrscI2 za@SBU;=dbuWG}g0^@y!-3rtNO%-aVbM%ZlTq>@|~54Ver4yc|%iw`4oD} z)-~`yTd<>uXx7_>(7NLsZ34%vq-Y;_X%F0RGd*~km7F+Xsq1j~{Jf*)#Q!JupyU!u zK7MxNB|~YenmU`-uf+AJRfwG~N8lf@b!Xz=dc(EnV^y1QTrdGUaHqsj?G z55B;;A>dXlQ<=cLqiCI&oaeJ7uByU`<(9**V@*A$J{!py7F%QMX%R!0%fj`A(E4z6 z#6ymHP50^Z7tUpXUl)nP215+pWp9Rjck~uc9{bJKMl{&ZZ9cWB?dX7GNz}c=`Pn*f zRoOOZ3Uhzx0PzAxL(dNB$c5jOhKkHN`J z;rLHYId8CfV_(h`?Q?hr_|?ajzbIHS+tw@c>ChKLbM_nZH9_07qqdo1Y5f9QyZGMT z>|OpCmhuOojch~9v%v!uY*hf8Ka+E1mKoa3wYKHwr*`Jtm$*uq4egr9RiU?z5`lkF zqu~#rD+aE@I~aye%I)Z&>C#GR|u&-Y-budW zcW{w@fRW_IOz^QwL2!ANsY~Eg(LPUIAn+pp30sMX z<2E~d3M7UXwel+t|J{PvSPmwR9p-G%L;Y3|Zo~gc86Qo+UO4B_H}K=r7WhV+Z5@l}=ISv{Bzs_v=ssG`);&0%j7^dzw<2$q9dquPL4Bd5L3|w5& zQGy$e!c+g$DQhVLzSHqzw*D*QC>ShkL{10KcD3wAqHM| zaP{jaScPBH{*qgDZ zS!E7FPZeapmg2Rs7w6%VOP5V`gMY?6wzYSUt+)7Jy}0&V?-eEx&}o#&`^ z2$}$1>R#2*&cYIM9Y8)b_ ziM>ywO=Yi3O#zRedtoWsVYKBEpIW1PkLUNR758wv9QYF6`2S}%nB~_7Fr^{ds^cZJ zDws1qtEE<4Z|!x|snNL(_G0=wIE_CC9YZT$l0qO~PI z(D@zi;s@-(|B8lIf@j9De>-R5V|+8z2pn9Jz^}{s(01!h{iaS!#QDlZ8_h-Iwu9>u zfRl5mk?;-Ppo`-F#ao5{Z&aneM-v}wNUgKY)oH#vPa{`l8@o!(zrR5Hyf{e>L48#Z z{p0%JDztR_{kE<|@oNg_Z=zG8P4y`tD>+xhuStpVwl=^w{P&FAXdQU8BJuv1oQV0} zr<25Plbx0}T_&Hvo7(s<<__vf@^N4r;)0*ggU)O?lKdcMCQuXhrH*bYJT&5eXct=< zM%sGvo3o>)9C!sDzvOCiP4?swwf1mZi^14?C%}BP_^a@))^PsosEOw#50NAM8A-zqB{-J%BGw1ax|9{U3ST_PSHB(pUWy#G*+I&Pn>tl9j! zcgj}f*VIyp4E02J_V@X9n;Z^AlO~Jc$cHxa4JAiY+e#N==v*N6Pe!y^8T=dY#JmN* z-y;sl^?JoPYdE|faXdOR`X?NoF1gp{wW%HO`&`cK`UqQ5GEpm>w)7UP`!pYqn>w#% zRKI$Tq|RIcUK_S_%+Zs`rq&bV>(;=*+<(9It`@*$d-?qRaCKF3J~!WQY(<;I1FvJ? z`93$PeZY(J+%MYTF@9XJ=$7Jlaa9DK+|mp$;h9f~FW5?m*PAgPT-*`AFrlGCPtc@z zLVuo`x?;ht=(?k!e0LN}k+Y-!(A^n{|9W6^$vtS=;^04AU!*2pUoA&p7aKZEO{a3E zYM73;UPXs)1T(0W)`I^t2E(W1Th)Q+=5Drfac@uFbH?1~HndwH+#R|f%tEKcyGO0O z63urVtcRZ(q%d@cx^_9Wah}(PqP3*T8+`YHI4gGxZ(4YYGN~ji<@w`o!mx>YURc zODRg4D!LQxb%bCIWq;$15EI9uQNMxLvLdf{Pb)%rS!Pg)(MJRs8KF3uZ{Mx)Oym!!t$4QJ;^UII^8Hm1Y zlbKqIK2i!i@pE(VaOxZSPF!ExfR+k2wXYL=jUE{nWT<`uV)!keOWd6UuZocWhu#z4 zh4Gouh1E`jdBlG*e3dd$O!cPzJB20>Bp+^&NBPiyeHU9AS{&Yg0GESv(eeLASHsuZ zgH~v5YkNgLkJxxgeP5Km#)B&OuDe{-tqZ)PSj&Z!1f;W9jfEi#zSx4zj}PU6G1oB_SFvNIfkhFbadkj zpP7TGK{MGpb>5OotnP=KzHc(Lh-=slR<*&;$S@PF&H4X^g97TZcj)kgTYVb67f#{) z-96lYO5)`Ynthb1Gsn@Q2kA`>rsqZd&?q(9AI(%FD*ZbAsh79GV;|mWfUUJ>>AhAm zG>`i>{-P5Cx#uO|Ip=>5%@V7iU%OIT`a_H?2CGxYa+RAiyoF~my%71|mN?vLt1CLK zd@1&Z?_0&W&im}B1nZ^o`i6YRv*COC#YXcdq<55po+bGAj##=yToeX#n$$vza$QAg z;WgoHg@O+k*tfQg!OAAK_Kb71lKk%*h#vgysM~g*wi2gzz@4yKZ*6w)whP)A%nTh% zZ!#Qb1V20*!JeX>pJYJ`QaeRue@-qkm2DC=aeg#;HFR2Wv_870ZXC|~Ej`g!j&i}T zyTIj2^uPL&)A7F=`rQ|to9bu}XRw)Cf5~G*OBVT5t^v7~4xVd+j^X;t%pivvJ338# zJb4GF%_Ha8n~RyijxoeXe@ivP>JfB9idoZMh)iAI9_@+#8@3r!jF!BBT};?TYDzyW;M z9C$i>UJm%Uz)Sk8$-)1Qwld*u@^#c9oppZx6=22d@WJ zGt}umJaWfUUT`}18G0~2dR^erkZm#4&N28F?xXbkCWWcv|Q-)VU~ib!|}xN zdw6N+HTbt(*nZMWo{Q(5n_BNS^$7ggll)nGnfeJ1oS4GZPq?QD8tCUHpIYTaiv{`h z1^iDx(p5vwzkD3_W~g5yhI9VpQ@3^WS{D#ocT9DL&j)fnMK1dk1>XBg-BBJb^SuN4 z*3OX&*2PJ{^~cAL;pg{l;F^Be>a-jGhgi7=zxCtiGLdfqMera$;uEYz+YG|DXadgH zrFT=))XqZm>-*U1$ev!r7wY{R?Us;QrU1EDi<<7UrKA_=g>qfF!OushsCU0mQ=Bn$ zbSpjTwD-c`H@tE+N3WfM|4E{@d37_{Eny3caH92}7p4dn%cbabD zznS^~_D$%I*E1UYhAY2Quf#-8o#9#sw_sn1|0WaYeGxNV?}LBiTwMc8Eb#B`OZ=LZ zj_&@Z-}cMUo)Xj`cy>+pIobg3=BLjx?lc4_wHcfWCVo8~=2_9>;0zz|vwL^YV*%@@1>$`VWZtN%DHA^U%+D_;{|6Cs z?KzjBoqQ_t(pG75ZbqW885LJ!ThiOD3y*XDk>J<{t-<6p)N5$TgJ5__KKL>M`W{~- zGxbUS3CuK*6UTNC^BuwY|Nci=n6DT(Gx!1<*dhne+42j z2h-5i-s3*yFu|rWcp~t%G2YP3akgIGLc>L($8g)$$x`@R)CsGTQ71IQt7qS<5pQ{? zp!KtHMv3t&(J-5cHyb?I7((xOHTCFB>h+zDvJeyJzJnvN;6d&&Jlv6SCODghI`}L6 zmxi7I8X#yYzGzeYpRwRVdGzH#cs&|DqSmhFAH!cIHYb;M^eZXe(rxmq5g3ME8umJgvzQu8 z>}NXS=mdG$xd}6A*O@!x{C-f|bqlezqaWwbSq&p*3bAh!Qn{*#cGp$B!{5x%bqC+T zy!~VGxbnbj6MVWz9PCFM5A@R)?uRx*H#~yhPw;v9?t%%_mu=(nnea?7F%Se!t}bCI zRvq*IGaE@`;JuUYF~IshhOLKj{Tkie))aJEn$|uk0WnPdpL`NN7QNmYWtd+WOdnw< zu~m?HJ#_b(Kx+Jh_&(h@y8vQ}n(!#U7f>0loNj1DF?u@0%b{BE^GD__+QC2I!L-GW z)=~c~VBdRCOZ};Zb|zOVk{>(&k~bC6zoW@=DY=OmD$aWy^;$%{u3R?tHk$0zZffEZ)Wpm1pE$$+))E8M4q@{um8c27 zEXKR&!5Y6;xft_4aQF@KKi&}wUkE>CGJSD4;$m59iA3R5&X0STi~jiw{x9BXtN3bqLU9a@Jd1wqj!ut=&vFY5@`(AP z8uY6pqeb@NxkQHVE%UyxTjO?tp!X?S8`*b96WKW9j6#P$1>ivG}O{7-iU zzGNDDYt$guy@>2iYhle=- zpux6WusT9vLoIjXoeu`T;K3B|&ch?%e>?a!H?xjI(68**CH8g1D{9?qxH3fuDlL-1(Flm&MeT@ZGR| zK1~d@<)D9$uf^9H0+*bj|8WRU0#C53&(f9km?qs^%QLczNCzu`e*V>kEzl6T|YCP$;ZHaa8e|<%?$1-BQBmQq;zs8iIcSc-o zX8v(8pLw(*{oj1l7^4kE>djmOd33KidrFVtNFGBQY7?(ln9nO`=`8nno%oyq{uV7k zEx4B15cG)4IgTLK@^KFNh|&8~>A$1@;&XnfzS`;nZW_zb5AViy4e3gwI;n#Zff@}Mgw+p^3 zu{R!Vu;K!}gq76M6X~(9Vs1Dq=W*WFeQ-BYNoGRfx`<$7dHAguynH?@*WMZb<1Rll zh2Gj5G%@k?ftu`HDPjctIdG5t%fj5vF6w=1ijw5WsB82GsWaoVuU~&y8k~ptZ(->A z9cn{-h)2xq^31i(1D8hm)h{>q&=LM8&u@U6x8D<| zgV3{E@Y}!mRH!j|^o+Wj{>NPSHbQ*7n61ofQ0uR+XsN_{JUukRV0bk*XSKJ5rP806 z`%lRp5EmWL(*1hi@lvZ_?L++xPB)--`%ONyAI$7PAR3`6Kg;(8<}}rh{7+ZQ&;qXI zJYLG%@rJVYpjMy4^`IBxqRp%gc)@5a<2gNX>i_9@x?N_2pNrR#&r!xE^wf!=`?2xl^TL1Ne+Tln z^ey~%Eo;I7t85kg%iJqD zUbzG_dlS(VI}ADG*=+WrZAN;f!|(>!yW!;iC$KjH*qv}8JoUgieKFm{XPsK@Y55~B$^pb<)5L?^-zfoRZ{)Npkl@>$?ta&qA6BWj$r zpJt1+5I~9 zgm}+|ww^&gL|~R=FFbml=L*Qv{9xw{?rY6R=1@!1cP!0!oU?U{7>Zxr)a)qC8Wv^$ zX5zJ_p|3asebg3D80_Bjg&O=OvlHyW%0oQs!P%XrhF#GBOr623SbuO6zo7G8`oM4D zz{d22EU@<(b1BT{N5ezR0RLSbZYcFv`ad=Keez`k_kSlU&)9&m`fe&qdS2zpL@n$fXOy{{}C&g0uUUqI1xzCGh~FgGZrgygA44*NMxf zZ+(i0W{CsWPYCbG;_OyZUrY+-IS%GOCe*Z4F^FC-xcay(J(UPNLqx8}>I&Z6^y&9j z;&(B1u8WRIiH0Ljx}Tz+8j4>_uG|0<>f#-JCojqq--%DaX?Qrx57W~m2m4Y#G|IsF zeu2|g(8Jq}p1Xh-!@g&3hW-VoLWePnh`)cITwHzyEj|uSJ|c7`i1R9#Ua^>g~sz zfo@RKE9)JB%Ge zT?&^}{{yz>AV+VbziUw6w`ER@xX%#*Y%tL+58w;pa|ImGX{oCo&CrcO%K64OTor-)-5H*H&5c*Em(K-1 z;;aH^`Mkq5z<%;_L|vao6$hW^ySmI7E^XxMI{w1(?|g4r=6~_vdR+6NQP9zF$+m^? z)ERoSoNuI4KIW9sP_5u$K5JThJn<#?;b_N$@0sP=ir>45StQOsY_{lUM(US{%+pPw zp4~+MYaM!{CVfWEVK#UWhR_}Qn@V{FAFmr8?6(NEClN=y7dq<0(z zZ%5R0oYTDe%xFYo9t^w zQ>p*MQ$G%y0IwW$RXio< zbOu`t;^{wPmj1wBKRspeGEZB)VD{!Y`0(O1*hnp&e;;#fujnlw2J03Q+nM~@7LPh| z1RifopOQCbEvsM4+tJ&A|FUzv6&|2NxyJh7McvHc5!g|37CrQz#4h!6n8)t>MLiUk zend2|n7E#QjQ#$|KG)>A3TlDn2l-j<|Gx^uzi%n0_y|3hySE=!#`h424iEA2F%OC87j za~^WEAbvc0>sEAn81Pge+;F*{tJTESWAJ}^F`vG_q3+A=*TZSlBbn)c^z!M$Vro44 z-=|s=Gf#Zl0Uv}TZ`&lGfAi2%{XF3RA)lVYH%ssx9_(fQCm%8UlxO<<|E~8@FZvT1 z=>OJ&6NwW)aXAQ#JOlo(%7Y%xh~M;*{>mw|Xd!anW}XZjUx@$LWgfWs%2fX}%(J0o z=dj;}!Nz?_nDu+^)A(NWv};-F%za#l?ky%Wy>JzSr-irNQX zYcHCtEIn4PXZT&82CN}J@8el;?!D1)wZXbN@5v*+Z`}lDonE7*(MYGj{8i=X!6vY@ z{Xg(+C-r4+FlIZ?;gp0s2H+D@Gatok*`Js?;|2dch#Ke*^U^iwOVi(I0Dq>8jCM`U znFrHP1YcL*hu7Cq{~bd&A3Qpl&3~14o4K&j-KgGPb9Lfv9Hl# z+wi)DuJCdH-SM__!dK|u#k0VJ1k_rE;J?tYnKbkt`*xq2WiA~4owCsIC(cAD% zXJTvrX1uHohBmQJv(W}=$l2bU_p-6{0?3C%@JjNS%wMCsBA`2JHDi_*Jzu39v%aD9 z1aKS_L)7OOfU%z?ondg`OExi zMSAZR&l{X%*1aCJF#g0@dJzrr67mu=Q5q71X!LhCi2rx^OvLPnA8_0ueDG%UKR58* z#7W2g@L?uqOjh!|0Qb;vIlcSX_)1Bb*^kGpLn&MFIsbF$+G)h=f6Sq`K8}8AZKx~P zU-$^KWWjiF1L%JiU?vjoEX%oPAVx~fr$5Tio(r*6@0qR1ap^UpPnITT-T;gYh{GA8 z|8|YXqsNmP--!B+JQ_vqI|n{#!}<8h;R$^1&I)+YP4NbiB_9QuizG(KZuwOg2S_;L7I-^{E63wA8bYB z8Xgh@i?*Y2H{(^2XN~rN^Yq~7!<9Ah3f}xjZ8VWsDT393N%XLf!Sm?46q%X1f)jU+B3{Jd^aAzTr&^pf{zQW_JlBCXPIZtvaV9uApBiBfT5%LHI|%*lz`a?hwO*px zBCQ2jBYw4BW7i=cO}|uO|%s{fp+>#k~fC5$M1c#NDEDivzJ?KC1YQjPvW^<@IPENa4WfiE_C&jdmUTM z`u{rsk`Rkys6p$^0K35RO)J>Eu`g>Gv z{JhHaQ`q}f`RPTr#2cQ@tQNj%OYW&x1-$)pV0Svds-S-=)aE%|>YC%!5mN_p{?|AI z{`&}d-h%6nG8ImGV`(e=zYQO%Ycl4@TJ!u*D6>=S|NcJo(uldf+<)L_JjYe^Fzdp} z_a{6FSk4uP8M46*(W3@y}?dx5XBc6F77{Xb#ijp6@g>W803{7*K~ zFKw6&u0@}aT#dboS;BSb5Hx3Q@aQYNS$3XJnQrr37SGMUjLys)9%~EgpM=~)@;yAG zL+%v)?$g@`=+ON1n@d|dZ~-s92fm9b%rZS>er-5jOf2g5 z2zU^kd6&o?Tg~wQZjs|1(W-wR`{d-|dQIk^nE#rv6Tb~yJ>HtxVq)qc99wG>-&e<{ z_L1=Q=MhWfj-Arf7jm)j6XJE5p^3zPW$403h~ukE@o*aPYy-1v#le}Ti-|on)V7{@r+)Mg-1!#lTns;rI7ke`|1YXD7r^(O zCclezCccT`r~JGBBC{P_&!8$ii*=RzBtHVdXNUZI^$0$^%gjC+@M%|{672zp@IMxm z!Phv8Uy$VAI{{KA_v zbBZ&+hb|1O(LQ|vf6LL|ZUzSi;+=sr;lPiB0I?GpSvoGg}rDJmT-tt>XC(eAu}7AHTkXS(V^|^mw>jbBCGupGm{?C|+||tsKU~ zUGZO7%f6fb)K|Qj8DN#)ry))7Zg+vF@IceO_{DGV7m}eb$cghOsoxCvWrL|NrU{^mZ0JOyal(wMe*Ja6Q-afLblhUal9utBr06)Q3}J`o5xes=;gkSUiK@Ey~%wBkv=HaLq^2{7u1z%vvWWIUUQ`-%US@HLuy?h^D6c~uEb zJgpg?aCN`RjUc89QkPNl_Cy<(JV+n86W2Q*KHcV1Mn2=}E_}sc-ebf01qlxk?OAdL zJ>%AB#c%Wh(TM_ zkIe7!`^gVGC)arx3_bvdqN~=`g@4-d{+D*(e+c(R43GXsUzzx_(MREV{&^#sx*_*6 z?LNAq9eX+kokG1-96WiE6`s8EZ~iwBeGouwPuj~2aWVL)68`T&=Fm%m*=ML7!R55s zm=BB1v*KGErRvA~AHvQ)72fXw>Y!=(KeO-|I-=tuf2c zJ?AcTN0gc_&oEe897G(ZT8>_+ z#Py5-|8f>Y+hu01lv*l0oRtLro1BmuA|rFz?8gz#rQ|VihPomFwP4yQ%w(WV=aEla z&~IV7ab`HYld05IN9b>GcDV|<>UERvsb(rw13c~`%qvu(|MQccD)IRzi0h*k31Z(W z?FRQh!N=51If%D<)XTkl@eIikJP0&%WcFe3LcXs**s=kA)(!n0%6#^4-i`AM%x>V* zf`_(B@cd)d7SzO_>GQ*{alpcQ-NAn_Yz^A$8!-|TgT4(pm?tw@rx-J#JSP~!-p$*L z$Jx!+qzU}_l|BgA+aG)=oETk%?ieu=EcuK79R&}Ld4ksGm=%I&gVBEFPFu<{f#-)} zk?WE8E^uSb6-O@`P|J^`o{LC7h1h$_nf^I|H;i^Hy&B$WN==ZF_Zbi`Q{dMw>6x1Z z?{<(+Rl?2~59LLE_OA!e&NbuNyAb#a{x~_C96*0oli{;H z^mA{cBy*K#Gx<@LyoPfNvbX8Mmv2*iJf>(4vqbEDLtue%o{AHPdJG_68d#0`?;6%j|@NWCY~}xq|Uoe{|lca zJGc`3i2dvjMz<%24}(oV@d)P=M@i5t_;}w2f#ap9VU9Ap0nb*XPHBz4i_?)AAn^J< zT6^$dychClC)f9VF?~>eFXL$Zm3{b0o%n3dwkYwx3asz+fOYo4h=y;&dFS{8uX1+f zKTrcQe^U&t*I_>%;Bel5Ld-n9kGI1a@5oMG!ignQa^8*Mzfsg=)$v$zGrt}O{{Lzy z1in6c8vKt6UhM?;bMf3!CO)e&+K$?YgzS=aE8KY$6dvBgarM9U?>?Vj!?ZkPtgyXWaSD}e{{~7sz`UCoUHuZT? z`v2R}^PJ6Q&MYz4HETW3z%-_o&jdy*mBWe3kLY~ z6Ko%iP8%N{zw8W{zM&DEa2%kd1)v>twQ3x0MYoYfWmG!pIH0DZH7-Vytc_k_F&Fl3otz1oBZ2?-TtvgwWv+K095f2<;QUYbL@!f&x8j+n zpoQ?yc5nc_ksWRDiL(rkuW*U_ry$r-0ZdPKjc0v|a_w1o{^1Te4F2VQO@9H*8bJT2 zcr84O%G3^1&{gU2T$58%M&tCCNOxmVduP!g{NLcDJR=ZdsroSbB@yU}MZ;&Ys2R!sD2;h0E;;YSJ8Q@-NG z>>X|NL?1C9x0IM5_8#)PVg3I($C&kwZtES-|6OikO9`mGqWF|~7jfK?zYD;BMazWi zL7dJ)H-ldZ-t%6aI)>g>K_>@LJJx2d^d>bvd-cb_)4zllco+@haQ$}T#rVDn-69^hY(ZRpNroV=4Kd7j_QP)pWbWc} z4d$CIdPl_2f8rTIII(qV{0DT>(&b!K@tIE7@;w*i* zTD+^O4e{NLd+tn(7r{&XhW~jCt$G*j^_d!HDf!lt+OjA$RUvACNB9{3Q70{=22X*H zLoLwc4SjMS=a2TNScd+`4&IN0CMcT-ogB*Z&~rK83Ctc)!&W9vYFpq5*!h9KZ^`+c znn~ZA92>-bguO>L>HyxEIF@TNyLr%h9 zDdCfgpLLU-;Pr3Rzmusw7Z4k<;ZZbot)zI&IjGZO{i}aAmbBC?ovjZ(`hdxqYam~{ zzd*;*7i|Hihs|03&Q0$CJ^FbEb?8{m;|H^k@$k;z+2rI;0&0^+ct-QG;2|vH9Uo2M zDRgDxJmf}V@S+6TF9Uoc&YidmginT^r#9WrTnN`Xf&DJmf_}Gy{;t5juYt$X5byQy znW+B~&q1$sf}eYVtMI{+^OpKicOKqGuK(frlQe$K!prEEl;Ym0?p0`>oa`}9AP zlRL!s&)#4!_tiNk{sw!zHWzbv8K`Zq;1O~D5o)6igV29yqHfK&E_7kqMf7lq2fGPA zOeMG{KC=So=zIL#Rj_*LQsRsl8$_H;T@Syb0Uv?cce>LjOvU_vP4)&X$~v8%E*#g3 z_)N^q`W$k2T6XGxV!ZTybktJbgHWG&i{y9{I5ThigQ4 zMBv`4a25m3!vSDsYR)h0ooKIyzy}B5Rxoo*SLPp1!jX7(709=h#LL>M;HOD$!cp}K z8QQpn-fcUy99(q^jTktXeqwB%jetuVZp8~j{|*GxqHVFH+F&O6f21({cAfeTo_=TW ztgIie@(p`}&iFJLKP?0e%bvCfWL9@4vlv5pw^m=~P8z^}*O}9NfafrQoP;YsMrE!G zyzWsG{)aEpHp1&A|MPyLr?7y!w*Z*M++~_d zUchVRcVnSRvhL$sfSgK6uGRz-#_Z#reQ9|9Cjw_3*;cWeu9m_FvxiYjM)qkbIk$xQuUFf^&2~IH z$2sjhhYy>PXNoE_JK=J^?}B+pG1yj*_l(8I;{n6Mr6B*l*~*4SGtq;K+4InQoIU3< zbQ``U8mv)Y{P;!aEb8}jiRginKZ`5ULuP#`9$_)~K}+C?wfyW0W`L8x-*}RVeldsF z9W6}!JA6jWl+5Nwh9~wh%RQ2_-bHU|0QJ}*o`rb|SHb^(&vIsMnQJUTJVhgauHs); zXFrKqyFK6iGFXGqOcmj}X6T{(MI1GzzFf*V9=he~?j!Uc&j=QbLJgLVxuw{|Z!KH* zsPCi3;rzjgzT{LEVq)bNgLlxvCAq*TxW4aVypzJp;ZOXLrGn`|`x2WNPK^A8FI zhpBaTcIW%@fEm;2zp#g!u5yoD&(;rUOwKgJF>193^hk-bjjWxW0UR>wXuE^9YV8a^jzcjifM-TcNYnXpHjTcHxm1)R~ z`&!;#^cb&;eN0MT&PtE}LH*bQEp?juzx)`y`V)NrTb?7ijMtg~?Eqgt|3(f3!*^M! zVY`y&rFIH1 z0f*zFbIZYb!0IWtPDsc}A|`Ovg4+oG$vwuF<>%lIE{qwsDx z^wq$uc%nQLbQexpRvFLpKKI0R%__*b?7;8Mi&qOzhCM_1C?|88#i;qo|CjLkoJx4} zXq|jdIs5VG-%DuUzHk`lcN$!K`2$Rjg~lgVd)z<|Pogg$4zD0Q{$VeAB;(1&RCt@K z$h9)mK!x#eHqw*1jK{~>WupGe4JXt(z_U2yd?@!{Hzhv9ALhQu&3SN6l}7vyHB={X za6IS#{R%T9?EUU+=+E(C@BM*$$HL3is9`pR^`QJ(F@t+dPhTamPgerL8D^praQ|ht z`ZT!=8lF90dV%Lf(E|UWJNM#49*Bm9Hkf~{hdxA$)GAL+4gNKZhA*3x=VFqAcl_>l zVr@H|7&@C8bqW05i)-LJh78Bs4`xoH6ZJ29kcn$AhyN4lHF}S;dW_c?BNb-|20sPA z=6{Eyhxt^TbIK!r?>RVxSD7O}aX>Cism5M|qua=_rfKn;!}MZIc&r08(HZ=R4D?hl z5PucXm6gc%KfzkSxks+Y^P=!k?2F8o;Qu`V3v1`Z6T*XVqf*P|v4Rx6UYY;@hgsAm?EM7rYZsdHP=7Fuyz7bHFbngXC_ZT< zbot4m=(vP$U*z@NBG`()DJt+c+ZHH4%Dgr(KSCha9_bbH7$hazY2Y^!BF*4^en;0 zadY9HxI7aIUnN3gwg1k$)Zm$f{C=y7{2baNH{T!jjyfxpz57evJr0(?6E!S7!AJ1G zNBZiC?gXnZId!y#Ujxz0i%se+w9%mw=!h)%fgSOSXHid0WKI?j>877&1lh~=#NU1J zY2R=B1kUp8bNbNd!O4--WMIOQp6taZVr3yS3<1m~ps|qK*nx7la3FN z;}Ny*Dl}kyW=KWFW z=zSOA-ocooADD@XNjP>Ed3or0C&hoW1#|2ikA!RF$J z#v#7JpVMfaXBp8Hk9bcQ`LHn^&$z7i>Cn^cuswWj(*I(g)@@-PWgl??p6?;9Y%t*0G()Klzr=8$i;z+zp@jSSZ zpBV>YzJGISGvc~lR=o5~uEOm?6PMzB{`88QrzD@)(-y1IFMQs6Jp7m^@%<0re@~@` zUF@nbenhl{@N#VWt4X*v=3g)ULDTnTzaP>obck!}t$>BR*E>8~wI$klFFe_pIL!}+ ze8S&jU%xJ6j%+4+>=w@?b6+VAv3KA{a_%n*{NHv4_%_?odg3m8GU7E0{&xbNlaFYs zJ3Y0oFW_MQJ2Bi@8|6os$3@8^@(cxYyP^A!Eo z{%iH}Tl~yAw4?1i+V{9LOqDSClfnPLt1kZCKlsUdblYYYDyf;>qMkiE?gc;l;Y-Qe z(SDP~>1cNw?{(+MRK2z_&L8yU$;rvPv1yUb?(%72?(8^S^9A4HzTWIJ+)6!~VQ&sr zq&;Zmt*7uKvAPlV{FBN3Cu-vs{9&?Ml_}&5$$0l5wDtS@gavEynV19I5e7Sd?_$>Q zL)z{KXVdgQ>s`e`Dv4oOouR*bJ=D+C>h7}~&i`%2izd;!;xsD$Q#q6VuK(_@c8ob< zE+ZGMkBbb*p~peP4TQlv;D4~g?-;@(?xT0|9KSE*-1=|)PuSPvo|)=a?#74RrL0|+ zF79-Pm7V1O^}aZsx>xx(IRe98{G?}!QPx+*)yLU{It&|6or!eJ^;B_y_&M z*EH}YnxL7zfU5_11N~Cc>#=wmiNb=94tTG?edooas$ptnvhijj&zTKyCbp(MEMv~` zb~4-m=3Gt1Q-`tX;BhsWB(wvwHvry4|XV??>m;m z%QI}E>*}gaPxYME@qOmuTyU%u>!%imfL(< zCv$c7y@t;!2iw-rKYRE9ljP+nn9%+-PGtW3L0Xy-C+nNfJ5R>@ofUBCTs$@&RMs!T zf`Ny5sMfuTSQ<9V{MHk^W;HIPX70P`d51ZZ=KGB+;S9@3`kY5nbg25^IU;K6x76}) z_4u;M)uG}nT^fB;^O*yE*T(oxm7AR0av71zT5D#D|gf4W1dDvZqI_v@?e&+WUXcm6GK7kHw^Q_8^*_Z4y{~hj;3pnx+H&v@|J$059(m%O z4{`MgJWJ&Ln@vBapO@eSa5C;c)})gDkJ@~K2FYn1%MImWoN$h-qTcum9DarKKlE?X z>-;)gt2PdhZMgM|8=mt8GM_y7&eYkZ)n(o4b`6EG}ddm z_cI(<3-i6hSGJi?Sth3L`}aKe*^Au)n_sU-yjEW5yx3NCTyC6egYOkIm*P&OoZO2}X4!3->-clg(! zpyh5@-4}-$#Scu*$6BxNJIGlw|1Ljno-ieTSY8YbbN&<88Qc?Q$&I3(A-JReTadpM zTMy!<`>F7NW;-JL-DWCx^KFK^k8GfMfqJ|g&-&dPvznfN?l69_8hBG}dOeySm5ink zf01XC=X^}#nstc|uii9s{+|wyQ(S(P3o~yA*Uf*e$b_%(ubo zutof@FuBpu9WHteajKXz|H}NJ*r_s~H$!)BrL`vU54x+JVf@J7VR5_S=1Uvff1lkQ zckJ<#UeFl(ol74KJ& ze3$9CW)6PmG&Z)=s%9h8k|mv8koBDqj!bEnnZVI%&_Eak2F?+Y!={YTa*F|_%S9*5eK zeTJBAME`B|-P)=BVj?1_BIr9Ax#o>V?4Ch_~U z34FgrbZ0ejdMuLfn~82Bzn|cuJ=73>QkdV2ZyD<4sl<>l4v z>W><*L;X!?U()oT{HT0Y2TtjJ&)Q_PIL`(ElDwcNX1Fixo6Wg!hKN z6}K>d{I76-g);|qR^?jmeS89gH-!i9x6oRCcif3S6CORB;4@%$x6NjcPK1JY>S#$b zLfLjW6SYKbl-n!iMrZu@DsFRjKv*zBTyI=sCUhSDTV1~;w_NMQ1L31BvZizA@;xKW zC&<4n@Mx%gjCUCKt;rizJBwxGLF>Wx-p>Dttl?F78sfjEHUDUa8vnq3q4mUQugHE5 z4xYpOTV0+o|8u+Fy&l(`P}$ml;9PzzURXb$@tdCEA(-LwqRNi~?>!^_yVhsieIY;A z_75(@jy40`^M0H^SRGGUqtDdM{_~tCjh6#|hJpp~zf_FDLDO0G6*dez6$;A1rG?9B z2pT4om6vxz4;MFzLZ@#@=FXCF>Z$r)!kQIZ!Q=Y}hl7nH@PXg&;dP_b33-uK3=E~K zN*&>oi}Amg%8B-Pw9iWQR39Dwr&q_i`mRN9;fJ%u=w07^A1yhQS9%MFo6LJC?mXcT zYp;&H+{|~RQ&y|@`yV(9WG%8z=6!u6UOp6m8P$pN{IzXnzG;W*1)YEUmlmX-Vytop zOMZR4vHBl%&GfDf1#j*EH(vkn0VJam@Crz>^ zyuY;5-ILcDhP8CZRaZTM3*wt@xR_9Vh0Af|%4_SD@(P}fk392yNnk=z>mPY1So{=b z40@`6L-e?F<4X9+`#;<%u0I)Z7Y~pVRTJ>q_R(q8@wY3yp)cWiUj5Id>RML1LCx$F zLqBDj-?JC*lc$9S(|t7Wq$&84-<$h1tc@-1veL3@(A*w8AviHQfjRvVxa~$hLRa&0 z|Iu}0@T$4y%eK)aW};5w&_(uy1sBA8J2#q-~x zb>GN^k$Loe?%5xIwoWXy!1GRTgwb-~hMJTfey10Mnfzxg9oI5}Tv=^C5_i828-{(u z55fVO#;2`r(bzrZz8qdQ(cIuz^*^T`$`#)wrZuXg{|%#Je`Y?m9j)#8aSksT)MY>Hyj!Mr}#(zYBCOvvv21KpGr(O&P-f3eaJ6)xUHSb*-1}%wm)u|$$Tx3=;M8>^zZyxc{OPrOvoe_ zZs7H87kOvKpZwBgHenOL3};zeh(~`_eW^ibi-oSZ%7&=&&+oc2mLK0iZ$>UBvZgVA!hh+? z;qra1+IC)jsx`wIng0HJEB*7BC-|%1TV`;O_^z32T>q-St8M?+!J7xTPNLS{e|aYq zB%Mcl6?T_mY3D<%Z-WBv3GHH5%6=V)W-dMkALA$bVxTq2U`B!d$r^5Ufd-u-{^sCS zZEyRzs_u?Sum8CwEa=w8{Z!4>ayh&BAMZsJ!+%ww|NE&Wi}fyfBnv##f4K2jI1_6t zVRLaaUSrziAM9f5h}Qn?2l&&ZI8|1b8EzEz(-pc#Ir@%K1eI)(pM)fvX`#o@e& z;BU3NGu+t7*X;tATP7PEL~G2GFGF|6&@22-&l-pQaHW2CGPz|=Xzn#5# zIogb$tG=&J?92du+L!K*^wlfR;2&<+!%xeD_4iZX`Ob-9VlUV@Tp!22eBDfcqnmx& z>kKJ;ZPVF#b}_!_y86cRjNTR}7+^k?AJKD`nUG!lRd{qqY;}|`C4Nvt)#FP|XwUlL zLBz=Z!Gh;z5f(U)a@pFB(?75dFZ=rpIe#}CXZQT;_!A?||7;PbWvq4IdAP(UX2a_P)CMGyO+B z$mizM`3})XI4g2H6m&nr&j~mGvQSUpm0G(e6l{hMZOwmd@|mUK|0~ZQ4X*!nkhenr zOh{y(%8J#~&Y<97@qX5)PUCsEI}ccj9;KPHz2X)BB5vW|e=GF)@$bxy=%EAn_-yCI zUzz*MWNxOU^HZz%2?NzPydsysFE~iv)uQK~&}^%CED3N~`+8uD80E*!edu1?8Ty^| z%v6=)%e68$SckXJ*L@M^XrX+3Uz#Y49$)xg|G>|`E$!^rTs?5lcseidRSqQLf5$95 zD9B*u;BHzokNlrS+d{$2x#n5Dhvs6WepTy+`n!`u+*_nZb;rB^D9ry{7orKo&O)97 z{{Q}--ralW)sFg(a-n;4a}nZgc3eGKylrqiJ*;ol#HILZSu+s!V2FD4Z8!7NYIag< z5dFGY(=catR*LHk_P0@FJckEC3H4k3`>?0?Xcuz-jnAlfTFpFTy(+7>@GpT3sEA)S}`)To<)}73~x5Y2jxh3DY?~pF- zFNQ0=@cdVB@&e{D(uh@Dc?LfIpbbqkQS5xyzNy_!jN`n68!0|=|FxgbL{Cgj zZ=XCvVSM|({X4IA4*M4W;YG#8O;6%KVSC)UxJ!*XZ;x@gglF|0NC zXBDFb#Z5K)(XJm~L~O_0Nz;_!q4cDc;6Pp9A>wFQu;(oA^go!*=WCzRxdr)KJsh9n zO|F@OdpyHCV9u!|yqJmjq%P4hSN|@h-f&vE1uX zf|e@kY(hq!(>$C|{X5VgEO`GFKVScMm3)2i39MhoFSOP~{9G??|y>^l@5p%y*o`-RhQzl8nYdp1JRRb1}`dS>0^ zbrUCj%lDA~;rL3;BK(AL>V)3MS$IAf|Njb3)qx*B`Hu0|ijkhq(!K8scD?3^ivXCrBq}s7yq+8|2O8=@9^VJM!3MSUaPt5;LdJjv-m^WHUs}53UBJ_Rn)qi8-n&IC6kOkfp zP`_}==TG$fYv_~I#cOz336}7~Vc3iu>fdVb_<~DcY=gf6Oxi9^@TfI$%=X|Dsnoh3 z`}`N!_T~A@pZ##NOdj(oznCBQ{3-L&1onSLYH_-Q@2PH= zif8_*rk){AnNJ?=}>qzhQD~A=Ltexln;H7^tts7?RS^!F#g|0uj->y)h|uUpQnsxODXw`bP*OSlp}2ag)A z&!+vB{mMg$$A1dPL1uXlJh4R=@$Ed`-MSHGhs|lqm#xXYuZLH1B|d%IOpiE<`G{Eya~I~H&f>HWidr`uW(uB>R6XCMXFF_CSn$jC zQ1I%yyPx3vMC-mvzV1rztb^R?DL+1&W`WIu}jOUM7V9*Z`Z4dwA2k;9kO`!4qSbvBqq|8_5+ z=P$=g@Wh7TLd&n{8y^^e*THfze@;v-US~e{p_$}Zyd3^V;blCtFU?u;PO2q!wy7pO zT*}iN%9pQ4|Bj<+{-m#QlsfQegY_&U{_n=Xar?OUvWd^??2N@}x}v;0Tq1Dlf!>uO zS6j9>uhx#Q3g4=gbS`2UPvCFQBWG)Su8#9*tCQYAm|m}oKi>K`97jD$s?MZRA1c6* zeSTL?pZ992-UBZC&n+GkPjDFyw`Z*x$ep2JUjp79|K~e6aEbpnv$!(>7o5BQ#yO5;PE?nk=MFymS=5Z7VZcICAXV@XhT=g=GWpzp7np?VncM{8CI`QAgZrXaxtkX+|Drm+-<*$^+B*R%IKRHqybZ4|VH#&j)55S4*0Ls?%*LaiL)YgiNB@oI&%>zCamNxr(JX#{bn)M- zru!{vfaHyxgN2tXVEGeuv)mK=ze(L}A)eB@<1`MveBSS(2dcES*HPVn1&e-{{{!Iq z;w$*pBYd;G8nWF?1Rb)2$GGozbwKPyoudZ$ynIv5*r^FstXKQo{F-(AlU!jzg3|g` zvH4$zdA_jnBzzgGhClddaPVfHGe>?$vhuXz=dc&XRfkvIbBSyGXCdFK(OWnN_xi%3 z{p#V!T;8h%>k`BI{q=Z)W#Ds!=Y^e52RWmqcXhlS{@akpBK|}5Xw8;pKmAOdy;g!%^|SGm=;S3%hFQf8)#L#Z@8SwG|Jqq566T=NzAg{%Jxhu7nACBi+pZ z?ZW>T(HQyd^O#TQh-29-OZoAy|ppME_&$WYTv!4&oEyt!q+ci(X{ zIm2PS&)y{-E6eW>6Y)(a@c6Uw2i}DRBlVhE6&n<^e1#v__e5&fMcVJ~NqGws(u?Kt zFzQufoN$om_CYL+>m<%SV>kSzc4sqfVkP!rvyl(*&pNOn_5WrfCw=7%`!hYTxis7Z z^&*5*?8Dbn3=A?X&@X5nd6w1ebsHQXUPZ{&&$_zfK+T_*MSbg~hucWsV`?PUiN{Xn zro>AUp3CF{{BgCWRXVuRQ~yV-E^5p#ZfKtSzIlvC{Qu$lNAln?K0Gys*(td?mxmWa z4t9IyOl(QDa{m>_;!b3qM}1_ldjB zJ!$ho~*VmvX7D0YqC@EU&ekqd*Xyc=7{>>DdvNeI5$xR+FbaVHy03{GDmK(W7d{6nk?e zDc=Wo%L5v@>_sQ@?nDGQpjRxcT;|BHAjU!_twUBc)yq3~4 z^kv$ZhqMNJQ##+G2FDZ6?W(Fl$p+!waA7OmVqKngP~-H~tI#eB3;h54yi>u(hw@aO z$BJiuV=>HL9|{t5wZHH)mwM4wZHc@GVR{Plrk+24YybNkuWCL`<+Gl`?J(ZqhLQt< zJ=P>W{c}C3{fCt+zcNes5guaxCAmE;*c|te*A-#HVCR3{(G0C=vWhr;ar=%-1|GA;Ubp6t#5IEo|C+$CBQzwKc4y_+^xM- z{?`!mW%)(^eiNQ{?>bEMJ4UG&@rR4!ulOBm=XCMk*!ow34fmtt>-0}jvrb{~dUj=J zF5jzVE8Tw)$DM$lp&R|1emfsBh2DWWpXx91|C2u52Klnoowzlu%bS5g{TKKj3|S^O zUfg%4cL@JyozLB;cOsATMicuyrv3NL(fAHmdcve0IOh<(pR4N7MwmE9?ugGn%r*{} zgUMIX%AIjPda-+Uvm*H0clNh?KXE<5eDWw7VY0h*>_f`kd|ZG3FpYT$JT3BljU{X8 zuY84jSf`@2_m3m}JNjT`YP{kHK0a*P>i0J}8wy%`wrBjG;yj6Lxam~BPE+1cMe+Yh zJ~LR>{&UX|?-9(hpDSpVrZ~|PxwEw|?G%CctLKwGhB!6Ovp|NV`sABxP z{qp}<C!D7UJjJr2avMYGxn8^jhBOU90~mqT|Yk?b%Jfd?DKBGd=Y^&iafppHYg3`UP+B zl36vib;?3HA^tiicMpe{=>Hl2PflD@FVEPw{145m;FFQ{vY~odthnbNCiVt}yib_! z>7f44)&GBwyB6Xv!~ZO4%>Qih9dPp6gFSy1XO855zMo)yRr}S;d&1@aU2(YM8#RpH zjZM!ai{maVI(ET+*a;gu!-UkQ%*3_u{A7IYNXU-iPxI>R&T(>x7qFCOWS>c9G|Zr{`Y zXYt^2dKQ(`Ou8?M+CSq!e_pXQOefd>!adD!pXufOBkl|Borzbr6_#YQzI?e|=HIe( z;a9@)+8^-stMDyK{bjMG@r)4as|%6c_ixcQT0e7tQ%z@6FTl zDVB=UjUCNIwS^hv+^OEdT1h|e45AGcPd zcaHG?mhd;5nL{1S&l*X$#HX=%hkxPFQ}Dm+Q}pS@TTyFxRE<9WDZa78dHlKNlGVDw zm2kN1W}>FkH6i`)o@UmP$T8S(=cS%_5@++(u;1pn=WsE98dk(iD}Oxy^P_qZcwWom zaBYxWp#QE6;Zx<}n0OEdVh#V=s88e^EfqlK4P#s zop6UQF!d;(FN+w5uNUIufq2riPU<^=l1$E(tgl3P(D`*G?*Hr!QBYqJGc9K-vi zW8eEdUwf99$K35gAJ4ok9%!d!cjc75=)VRpjBg$ScC7!5wwiCg`y=>WQ6Das^HiJ7 zqU=)NJl-A7$sNW$p8(S{qv zV|kisd?IzBn0g^+v%>Hpu)6eo8l3+9R}3Di41e+ETGRC9y1_Ovnx(fnC$%`H@A-}= z_U4q?lGVJk9z2->aQ&J(l!50d7W=nUo8^UNI2_=p$XLf`NU_dUS-`)wnF;rz;{_O0*N zFzt8eqj8*!abT7_U1{y&`u;m`%6Q_b!DeeK|9fqNbz*g~8g}VhJ&Q#A6*JgA-O^wybS+a*T~#SJwDAxVcvH;Fo;<0-Wfc8^e^h~?=WeReqt$F z(7tZ>opXuL8SpN#XSyN&CR@iMNqHLfe{(~9ws?9LgVdAx@cR^BC6DtVpV5NisY*ii zwKeM;1+@ZAucIP#NwaKj21Tz3~;;RqU zboH-(6!9i5I_z_O3V&)Wj@|1PSI-@f5&>1n@g+WY;B_H z>6F)_gUzk7<5Oed=0Y>B>%2=??HHhs)1nL=@PoXi|9(1)ABEA+mDF1C*EhO%Cp6Pn z|A4o{_h}L7f5XlXcJfu~I{)N3Voy*LU}#PCpvn=R*Wc>U*LuOe^WUxQeND47--;tz zI{gl@Bi8>NjZ@GpqTj?&-luB>~V%i%$LZGQxqx$1E$l2am0%F z|EI7Shg9Ch2@kfa2`LlFN%}X|ME5lQ$HmCr?1xFu#H;rE|Tyt)c^NY-I4bLUAxBoOIhA^ zQ#I90^y1lSTM2yFGf%Om6Mo{Ol(aV1dov$$QGPW;-Cw@QpTUOcQPlp=abf!K=qvki zh^NUfh>*=3{!@chqVg$JA=H zxaVK_(AvGAb=88%cMAOs!y@ls9kbhd&hh-2>EBs2HO^UVD-GQRM}cP%YT;d+=%Bsm zPY1W2Y=%gEU)fHLufp5T&4(#z{$VIArz_?UcPH2y@lO-ogvpn8nE#n*|5m#9ATh7X z?`Wr19}_#Ll8VQt;`D%?sCdgR4z9#9({mNC#NoR2!b$jlS7B>vwcu4R=jrI1wVuDz zeqKp?_0#uQmK@)E?_CTn`7u!=yhDRmPJ3(><69q_Z{mw|^Ya67*I4*}$p|y;FX4Yd zd4GhKlbV)>UkW_cRPUpM&3hkdnj$%k^h&;a}6^S z=lNso_KrY&AU)0XzMmPAJ~V$OxGqjkj)jF6=qz0NfX~ko#^=kbA6Kb=uqOjux1C>9 z1|nrbeqrM*?(&&~BtVZEE>g`7JQdvB77|FmX@ z@zgvt!YVwXh}33k!a0 zPyfz@3G~4F&v-8IKDNmAW;XwUyPZ883aV{36M+AxfETH8vMmqw+Q0Sh zi-^l|JliN@4F4I|*u2X`bE?+sOBhq6Gk$)MEwK@m| zhj94i?VLHW$Gf`F#xKn^%Z+DG+@YRbPX#V@T}&Iv*|PRz{a*K7!rS_MqD|_}dpxfZ zOjw^&ZwK}*JRz<>jO>$`|G*a>wD)HX)aA@D&K_if#|wSG+_+jJ{NbMUEVib9aJq(> z!u>w;zWu3>r^wZzVzlEgFgCIC{pHnjSP=h0D5xaImf^c2$I}dWe99#H%roRS9G;F7 zEZN25TID4TmP|EWJVOIOicXLEDeXK*<2 z-WMjk{gei_SAPwqr&8(vd}s#dUvtB-q|c}3iF~)k8(@4K@zu*7i1j3%C)o_^$6F{e z%?yUMFD0&HnL8|bj80R}N~iYBa;t4unq9tSsz`sRL$UFmlK(~Cr@(u8LTh!`w}de- zUx|Ui;%mFR5Ad&dokKx*48HDFH4)}z7ytSH2?fji*@TJSL&`HR_bwFFXikI7p%0Qd zmsZgEcOJo%csLH+@1{<_Ew4tyx;N@U`mN4-#njLB=U=|zlgo(`GtCA3p$7dC5yfV{HulY>=F|NaD}?~-$xhS)2nyVOkGSR{-eN& zCUyP%LEgm(^Us6KGM*4aMF$4s@0&5<3vUsN57mY=t>_V0G`*pDr{BzK`~J@w@rKll z@fp0;xBu!uggW>uFEAN=SPf66(KDMK@aI3I;c${|--Ln=c;DcQw(@~ zc)Gq=Ri0;8Ya`cp<+cZfY1GlY`Ih=7KfCuNf$!n@)3;XNN;>0EpSGiow~EuhfOZj=$`2X3iywNcE)==YMRQ$pPLHOEK?TyLt!2Yd6T zFbk?agf4`-hnb`&zn>_x(*V$CM(>J8_*6 zJ0Py}nE(8m=jr)dHV*~AsSEw+oOkkR1N=={%xAvgkI-=wMtEOOYQCm5*$Knj@^By7 zgQ)O*pLqCIyp{2L^3*jy0Q*vj@6LEt<~+`8`91sLUzR`AxK+-g1w^<--~F@75T1*?9b`V_M!`)f4OrJnQ)(hJgF^_*smtw#ZjvIo#JFE z{QlrmoF^B)ydpek`MF%#Af9W}IB@z3e%m}L@3yRaT~bEQ37SP!U;o_dyu8mlQAhos zMPI_wNlWzMe-q=$ogqFhPjKeHDw`h@dy#kSe@H|1r};k?$Dh#uuk!J$3b}Km68u|Y zE*~bYw~x8p{~sHE<*Z~LbiKPK@z!R$)!CeSj9r{Zz#VhouYo;%kj(j~d~%kqpHdwD zztS5~>k7i4OrC!@{`g}jdsTZt@SpwqERlOQ`EFM?!p}qc^hfw}v`p0bVZmPd@11-e zE!P^u|8B9>iZB}7Gqi>E3HZ(h>7c>qBG0qvDUZi3=;mVScpJRAE4WVNj7O0h{6zUu zAAdX=Re$|oJ;G;jwkREs|HYOA2^;XgOVG%RBk!Eoj~;-(EeZvNs_HX&|J88yswi*$ zCA=PxR17V!IVQn@4X}I+!AFv}TE$@GkM={QK2X{>^{$oQ-*CO{c4R{{8a4Vm=3WmP-9g4x|PPEgQ)xP6B{8v5{Ix4T`=#lPlwxTN? zGt*s<^Tf|WM_~K*W%tiX&^Hjf!r>Tp1Vcnf_ zc+N9(0QPVuOgr;dP9JwRdN#j#tv$a7Yufo)Ji3?~ov)?*SF@(ble2fs;ps80Ye!dF zvz>7IL*F6Sdb1FP^bfDotLn}gv0rGPH40aU;@gif{5CP|{LpNEa{dauSeD4Fh&aun z=ESV#?+4fem_1LwZN>uJYpePK1Ge*c_SZ7sxykv_G<@Sfd7aLSjL+k<-&(6Dp&;ji zfkE>|5y91P-b8BLSxo20rK-XB{gZLM@6E(b#((YiSUm49HSyENw1s&7?r)flmt^?f z{HlFC+1CtauK&#hrJ;42Pt=b;D+a{d{6o&bk9Ds{THntaY=aT0#?WrCeo9Q-_dLCr zm@n=5ABo|D)~$cfP>|<)d!EuACL5iFmQRQ1+tzBqCC`6SE?isgoe1&Gfzzx%gog)v zPN*wy`Db(W!PA>#R{w9~{U0m|1xruhjQsEC_Hu+huG=5~wP!y(=LyWeq8`>9u1EN;)xyY`+oXe4f-@Wf33V(1i4e9hq_$d`IXB&=Q#W;+A-H4p6+LS z$JhK|@pRbwEk^grMArX6$OmLI`C`Zt07U-HH*wDqr1#Jw!|O_VUbyI<*s9L~QN zaR2vU^DLgV%@p3e?=@+8eeVF+#? z-JJ~LVK;8D690|WN6n(U8&)wVOK&Id=bjyS5qY+(@F_e&{;zpMAM%PW!n9T5rS5j~ zWL4?X<@nSi_1gdcdy4ly#`amc@qnM?YD=++|19T0G{w7vxaRx1(Vr1;Ql5{TsfQy5 z(~Z)1UckG=_ab2ZelfTyo>|9lctF`{iAw5Z6>}f0@hNpHvfr~aHsA6J{hZHvjD_ZL z)V`_--M_gY6kJUoc^8^9cZZ#c@wu0h$*H{hmFC|gGMT>+|25_H*Q4PKd~W{KJFvyu zsG?!Pp>O5e6Fmz5et0@DCa0@y<$v&hc2?yJCgDk4petL!$p>MN}U*u3SeSFkd9+AUGMsqo%Q>f=heT8aPT-zJv}yI<*# z+=my|uAaCp0JH9_2n8MSx!3NTdn-?yi>34PLqS*=zuKL+pPFeYagCl(!$!BUHfr$q zIR6LgaYbjR#&%E_YU!)Qr#OmfHaQz1AUd@kbn9}^` zJo;ZV#Ket3K?F{)P%ZKbMq$ zfX~Hr|4|8bb4m^KAF*Mi&%8}X-Y8)HDFpxdPzkO5us7CwAs@J{`>WKo5B}jT@oW2? zrS&?hMHTqVv(2!g(E!hJ9f`ghqLMfcK_e~C&T&tqlcLd!38JJ=ULED zf71(_GxJ4YM;5V_&fG;M`#NpTfZ)A4bIUW%ms^qVm+trl{%x&RZZcbR4A-kccXi-5 z`n(?E|3)5M>!o;TiYFY!KY5)6vfz4aa85tFoK~(F5ek}YbvEHH{^xfVp#LA^d9m9% zx7b1-NzEFi?=lE?Y5>D$itz|ong#xz;gen&Cm-vnSJmW2RlR6=mJOc#gpXzVf+swR z9|N-@y3iea-0uS8ud59WtU+D6I%R$RntQmM_+P(bK&qmFWlXai$Cq; z+-ZB?r$5j8OZks)JgO`<58zPZ{}b5wK{@C4td%Aj&{hAJUJC_@M&m!~ z)cTY>nk@8&z3&^H7M5=p6SlwE0R-q0)tv|N#lru+a zN`+hILulOAH~4?kox2;Y-sONDAB#O2=@~C(zURMwRqygpC`cln@-t@GrCZ*3LX+ae9$9z1~reuMw?(U(W*G z)|;r0XMHBZ)orJEG}-v7N$r7L@A81}(cS!ab$-0~Z$cAS7ZWk2LbcaW}~(YZeR&TGpXaai@Z?x%d)w#Qt|o<^P`-F9!zk1Mp|hp0u(WKs!B$xhoSm zgOSJm?_&GY8N6QDJ>ZJ9$r1|cw8sVb|7BsyxsztHzogYX*Ah8aYnO9v@F?4KJb^A~ zd({5reCbpE$1g{nwT9t~aPuLpSG1mPN zAHYgjesYdFe=6Lpp7Sg?(3{J8YsZ~8_zZV|A8+k{n0PFB5QgD@fBW~_khL=*1xs+r)gsEIKF+tT|#)$Zo2Z;HE~@4j;oLD%k!>!@)xM?1l$8sayWBnXnSh2s= z$u;uhC`^1~wsFINV8PF3_7cD`I^c5Qh+t@HUao(imA`bOjk%=55y7f-Jhq(fB-*7< z<#Rsu4DSc&S7ql3;h`x#cl=LzR9DO*)X{IPD);yD7U{8$us&^9IgkcFU*U`j&+LpE zT|1lEe0(rvhsg6g>TC;`SdLGU5WZjJbHc0pbz$Ek{P#YLua7S#!VNRy$8^Y{y8MaH ze3zAe&i?;Z+q{|ePI?+{ z3=Zi#{oB|2+WS*8ohQJL7M~G+V&d@EFw$DLtVowHz?Jt73N{omd&Kv+2WR&0b$;Xr zwLN?PU_n*0jThVxvE2NuSp99IcNQ$*`Qa~%@b{T%VQ0wPnYjKkjq{9fw#6)Xj*r~` z+AO3!?Auly>|stcprPer^1))+R3De#G70QDPjxlq9|2;43 zTrqxGtNFkn>(+=M{StT|Uv8-fUzGB9;wRx}=2oAp@0&uw$dBkqK4tOm)KK}=^MZFF zt@5se7Ige=J&hRrHr(;!)@t5jJ~~e*A@6qzo>k`%jK#s;-{VWa*JBqelPkd$vn?@3 z^7`yotjcn;n7QnHVL>Ta8ZBJh3<(A2)xX6sEUY))u${IXE#IvFxsh;K9oY$E?(!3c zwXI zidAackfhG_bT_*>iO+%;PfH3nz8w&JCqB=>zlSfwf<8^1e@N#%*Ie_Qv|*k5G!3rZ zLw=9HVn17kg0DyMsk`u<)s3(}d{!ZSfbYzou2su;EP1Dxv4Nq@hKSo5=7}dcGoGIx z1;2X!$Fmjxo4%3nE5&epSTvhY`eS6T0S>Og2|vQAoI||5ldoz|R>;+akv)f`G`9Fp zD!z}Uq{DEB%P;s=sqJqYeEYaJUtZqW2ksgg!dUyNb-}sQT7x>AJh@kN#^&3Z=yo|1XWR8=6abqVQnFZs7G;1YlOBn&^A!hGEna}jE50)EVI@+WLJ@5wq3 z9m`|?&rGB_ z(oezlpUCGG&S(0w4hi{C$LxO&y_sCrezf>gr?Seu`NPFCzo)TvIz0$~w*M#IIa6_* zUvkmAS31i{-+78!7gF~V{YpJHrFp9O*5> zzXi^J!NqreCNYhEp=Z2?&hVokj(-WJt6c{c^K81Q z9ar?M;^QGl%sQzp7lz?!`P~h9)BIn0vj@1v;D_!ftI1Ece#g|!{pK!*z|bF_nGJ^- zJ$CVg2AEmjBk$qK{#bt29)Bsl0T_l#lSZ^O!Tw3b8MQrHtm== z8GRSWIq!<{WPRBGoqar;g8w_l+1azaR(rfK1J07y``^~kQgis<)~hWZ-?^UNX>$0l zUNq-3#HC&H__Gq7_#oruKFPED&OcTC~zgnC`tbMN^p6ikR}*3rC7_2=RguMXD}{bmge zcMj#4pR2nZc19CNvCV9U_YX4gB)VkPgQx#0$p1(C&3*nNPSy93ebt`@@KG*rSxbL+ z;s4d-8(5RCVf9z?C+Uy+zklIn?esr!(-|QPR4j1{9nFa)lar`^0 z;N@TD;&7yg>UmdvhYfUaN4@e?_@ zjrW7I?Xd?f)!vxup*!A2wRm<P>q>T#8wp`c$1 zu^q!qjkxcEcV3Ud`#3x>csz)Y0k2LB(LYz`uluw5p6}Ec9+B8AQpUNQB9Yz+4kDjw z_oCV4Pwe^X=J#fB&O88Q0_u8dVv#&-3LmrtI*{1iP z9?ZdwJE==worg&m_(@$t!FdvS(MIh>pQTkQ$--9F*x_K?%v;QM`E!&I}I;;f?I zn`#mLphwiFrF_g|kFTjm$9YX^?cpP4D3z1&4>9Wduf4tV&3|{ zguR_p>-$!7$AX$SrV%aj-aJlecgy;`>}u7?fA}%#+pMenZXCYqd*7o?L~sa}?Sjj7 z&V_;%eefUkuO==%O5Uat)0g1zZS{5|y?3Y@o^mx5l(0^He)JbeAzlRjNji~Ch9%@aYaqLa=~nE$Q@=dW)jWPcaJ;nYw)w+Ow7V!wS<`m ze(nB}{Oyuq!N3}N#P)p+e{Rtfo~X~-BxfeHRZDlcqwRrN^SaJUOjj%Ds|TH@?*5%U{^nPBR-4wAd zw`V4Tb#KJ}H2HN_Z{!0!49>-i!n2_nTi~g~&b$9%F8tdk*K*JZNyY0^b1G?Qx6-up zjZo0HoxMoueb#^Awc%!=SG$X-ie5YY6aT&$>I3{p_}^^7pkM*rcASR$uB%yY^=JD^ z+yUo!?{`m49O7TuGw;aRsD_>M`O8L_YkDEBuSW#24%3Y>oJpV0r%6Rqcf(7Q^G0ul zf=#&Nc>Dh#G$7bBTupdP^Q#;Ec?IQ?@W1fiJdNSsXRvXZzQ`+|lUzMl|L$d>5nzA! z>ehvREbyIm&4KTlHM;SwyNjxteH`rWl&k*k2lIKn!yRI;CajO=`PaS0Q{?kDIKr|Fj+S3NN#{Iv7q2GyzJmU4B{r-xVP=65rc^u5$ z4v*zV>^;tZd9GJ+B7%rwY79JR;`?uan`hPOM`}lY&;Qq{5H5t9ehR0>OUs?&6W+A0 zEWdui#}>**b-dPVy|k~*1@GryP1g_S?|%CuUxxmQybtamPWU5i+aNxci<<*_*ERfH z3|KWwpQea<-Kz+1R1KfA!W{*dLcx!l_1H$Z&r3ZDgSoMaxsN)rnt}huZm-uTjw9!V zTAb!P%D34+I3M!0enL@vjQ?7Ao!Oi?dRi5E82|Bo%6k9BKK-zUxPM9B9GC5C^Q#XM~5QU|A0bHjJyAhceIWAwtW`aHYg zO-J{|(=1U(i_IhEvfy8{V?5kH%`qJJep8&Waw6wZ>4OKbWNkma+Vu1xzCQ9QUtmf9 zAOk*g*vwkdr`}IMJ3Us1=XR7IWBEU$&1MvY<=bc?&%ab2R7i#UFLchMAZ)mXd*Q}K z{QG7d^z0w#RUe>ZPw=Pi@=NWoba_Q~!^n1S8d=#9eF#OLKTYcym z7VJuEE+@C=A0%f=(m(z?^=x;-^@K<2Y~3&UkI7&`L;gWF_c7rEm5w@_ylS9#yqRIa zLCemBHi?!V3Nzb&sOI3TZSeR(?ajx* znvXx{kHf#r@~5^qo#5Z+_jmgylKycT0*#Eux%ZJV;tNY_hhJw$Z>jh?W=PE33oICP9Pv`5z z>(JE^!9TOqzX4|XR>R?6%`umw|Eyi^vSM{ZC`j!yqHMvlW{bOy{Dl0o&Ze6<&EeJo7kAN}i_@i6y657mjlmi>{sgu!hqoI67^IE0gUrza+zK>Y* zd9nPy&bh=co&7J*CQ{Qrz99ec-&y|-4BpTIUBuhoOFTorXXOF&D=;P9JUnBV*#q?@ z{!8Z{aHDa~4+sU{&afs&B7zbV;Sww_+JwKm zOU=GwhJPw7pX-jWVWA*f8uKsW;WAE~aS^_FDVs2 zHKyJP{&@Dh8K5Wfr#S!bUp|J~S}7N8WhVPrU->O=esk_<)EE3c9$r^GtlTJmUJm`n zn!Kj^{7*5|Bexm)6EONG{bG7Q*CX+I7p5kqv+4K*ZQzRh|L8Zl-U0s89Q$Bc1G@4# zO_*#kzV^cTwDL#_Kh6QqNtoU#JF|VznweK@1crR}6T+n-Nr2aH>J~cY; z5C04JpGj`R|7Gq8QEOM_r{CN1^+$Lf{jU!2E4A+(^BJB7N3LBqqat5A-3$c_@Q~Oz zL{uET(`9pSaHpl5I^KZ4Xnm?T({G(E|F>8>-{Fsp^0&3mTWMAJ!kN2ro10@So|M#9v^Rd7QcURtuVYTY7XkM z_}i=}Di1TzYLWL}-;N>oPxI&t;C*TR|A+WugxQ9weD}_@S9HCe`;m8H(9=oHZ69=x zKmMPflz88Q|5@|S^6Ag-X?QjJ(OCPRpEu_F1Vj0do__*P)C~W7RbId84W2Z`e9S|h zNmo1!|4(xUcZ8?QVRh!JG!Skw{X2R(xqRcpWPGWA2J51#aakMC!C85iu%MftFIUpt zT6_6q$#I$u;^!WmSrrN{;#k?_&5DH9Ol|%mulX_0H&#AS7Mj;iw)-*Ce^PU3I7N4tj2q1!&JQUVC0-9E?E|D ztnJJOoEqjC#&p0buep!F+FjTrXKO7LUq4S?5=&C;aeUwQsQi}f&gdg1zSF>sc^`k%_| z9=Zz7`KV2Ob9=sCdSt!4gJ~e1QeFR?2NGj2PvRFk50)nU+wA4rP%!+J^9|kciz@CR z^L#I@!ykX(Oi6I+(P~tK|IPe2j&1MDxeLnLtvjc8Q^PwE-%(cS!)y8B#Bf1x+fR!%I0e-AsGk6DT{;Y4Te*sp75 z5b&vL{;nN8@vD5?RgI^*(Ei_qi8#l6|EVSZ3xB8n-FHu#yk4K(-r;}MzfqT8she`B z{T1`zVmQsq$a|P@DzTAzlYT^1e~zCQ*exbT>HFYTw+HeA=HmdT@X->!`(gKX?6b$X zb2T^;woksM4G)fOAH)lBSBhuYE(d~Wo^?0xTiyI%2=CT9Z(r$o8pHTvux`EHXm?%& zJiGlfFYv8C=t=Qk-uhnR`OqcjZsBPy;lH(Lo5Xvs8vFN?y&D1FO`;y=QtKM6zqY8O$$T>qvy~qUF6rN?!>^^uR9aI&i9=a=B$(WeuYb(mSc;y^B8{* z1;M&d5REU=t*QDi?w+*9_2t87>Rm*3d_-PvjjJalHtu!uJzBX}Lj6ySkF3B!3(Nm) zzQdoMakIMIU7fvA(42zx$vL-w5aG|NAA}LGVw(KEqlV25=P!H~f`K|KkZuJ_{4dQJ8|^#|@}6K`>0;zaRL*Ym&d`-`lwZ(Yr79_|-3DQ(Sm zh6j`7!&h{}iu5?qUGo9-|8n2=`WyX#-g-p;nt`vR#`6YB`W@eYV4c*7QC(;hUT#tP zZbAV#Fi~9l-?P+;QeS&tP%&rC;mV0D&bRS|Q`@`Uv+zHCgZkoZnCGZK|Me{w7F^5C zH>}N{fTi`}SN<8|G$r3@lxMQXTj{3S@8mx&+O#!4GZx?Eni_=v^vXep^tINXsQXLl zM0}*&T{G_Vej_oFG!Z@}H(pet2kn2Bb+q~*{+B&lwi(4bdRPtI^Ewkw`cL8epW}c}aP#;6{i>gM z636(*_{j`dQ1CL%+tIu9t~(cvU+nh$Vg7e3^|ThA(Wip@Av3AGStM)!3r(}?QNJK|3B7CgztziTpWsXGk3+#{ztE;M(H!+^d{Xy6mcwb=_}yy! z^TX!0hU;%14Fw0Q=?k6mxz;W=y#G~AU%K85;&1MZf2=pROU{jfrS)lN9Ig%wds4@q z6w%X*?~b7`-m6%gUCLas{mWFw+1{DnGZDqw;VR=Bnm@W@ev}5i2p=*I;A`db-WB-w zeJ}i4Or_edj?sr(lZf#MGu&;RvwH@sKBXtu!yOo~5qHiEd&eE{UB%)t1)=<|XXbcF6d zMlUTe-}NcoJPrr^f&YW+HnU#wJ!AZX{DJB?adI4Rvz~;Q?Taf`8LNH{cWzg%>=~$M zTwnde!ODDLo%8n(-k;awzXFT9@SUyCh%on3mMY_ zLH~>h3tDFI-d|pNzt_ASdLn*Wwb9Qn{)>i&4dcI|e~P;I_Eo>&<>rCGwc74KDQOnh z{>Linok;MkviZj@_*>cd=6~&f;~SBlvzm-|zgea3(g~NlVW{HZZ=X0+)SMZ(r54F3O{a-Z=Hz*bnJpP3) zv-W$ZJM+y;YVAAxzmo1ePUm`y12`eF6^248%4G(HOO*KCLUv-hmdHLBxrOjNX2y)I zo2`aXma>!#QtC>Q1`{DeQjDb#*{--`8HTal_w(wHUZpY5^ZS0k=X}oRe9k%FUw?XC z;y3u;4tJ;CC69`8$5B3h53?q8SJTd+0ma*&SLNJA^b7T)k-&sA?6Uew7-5Ft4g9Rz z5;aI1?2Aik@^HMPSdkyjC4}79ouVIZt$H5*pHqn6;iPTG;gXkWHhbCKcAP(f?!c~Z ze2tI8=xMO6@XulbEn#8iBQxX<`DxH% z-F^11T{*n;#lJM%XnS`Z4|{GO{R0P!UEw|eIqTJp<^}owj8!qNyzjMj*k$mbQqi_>^|7H5`oBiDbFvtBjV&{1LCuV9m$f>N4_zE4S zAx@p2F72Gs#oQz|Uf=njouE#6$eaibqT+1*kNkVtMmAbUO-cUSOueFg4n5d@9r8VM zg_2_#HMl z)H}Lyr(TO-_2XukZzTqe%v|SnfP?(%e-=4|ASF=>+oMOZKMTwQw+C zlwPEI>a4SzzgW9}1B_jO8+}Jpx z9Q#?mc0ldzBic2dbn2>k7@u448`@xbwmzDs`$s5jqAeinC}AM}+&9EIDv^!UL4XlIy}q5pYDVsHn> z1f%HgYveey^#AJrSd<4(X1aHw6RooZeGs=u^=!L+UK8AOV@Qsz4iLH1r@+TBZ7B|y z3lDuq{cG-`k+=NNR_!$!kKawZ!?`~E#98>P)5Xl!in&=d@p)_}n{So=L!DPnRp@1y z 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 WUBUG=_O=QBlU+fd_z>E%81z559ZDbo literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/0/8.latk b/npm/examples/public/history/0/0/8.latk new file mode 100644 index 0000000000000000000000000000000000000000..81a541f9f128541c3da412017162dcba0a60e428 GIT binary patch literal 67650 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ⅆ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 literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/0/9.latk b/npm/examples/public/history/0/0/9.latk new file mode 100644 index 0000000000000000000000000000000000000000..aabd88c0f8942e9eb37baaaf9e61a850a6b58df7 GIT binary patch literal 67650 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

  • vB*!TaD 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

    Ecs@VyE$`#?o$^_H?6KcX==6GOooOGr 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%0A{@c$0J&A{hN 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

    Ld3KpzmFB)4l}znefB% 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

    Gbw&O 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 Y@9%zLWJ)!M4$c1|+%!srm({BO1I-QRHUIzs literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/1/2.latk b/npm/examples/public/history/0/1/2.latk new file mode 100644 index 0000000000000000000000000000000000000000..8c2e8cad8548867d17747a910cf368d9b7a2e44b GIT binary patch literal 109890 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 literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/3/0.latk b/npm/examples/public/history/0/3/0.latk new file mode 100644 index 0000000000000000000000000000000000000000..b4142238a58289e4e025e3c57fcb5e4daecd2f2b GIT binary patch literal 5874 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 EUl2y3wEzGB literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/4/0.latk b/npm/examples/public/history/0/4/0.latk new file mode 100644 index 0000000000000000000000000000000000000000..7cf635ecea3a29d53de92ad079d1a9243b3cc7ca GIT binary patch literal 594 zcmYk1OGwmV0K|7UTgfPsM2bQJC98v7qAfi9%<2%^Q>B+AtZ5nP>gu}tf7yQDM^Khp zLHK|=*$WSeLQ#r`x(SvmkV$+)&WKX6|_u??8zePDFO+F4AGzUYpU$BWQtE4B{WP7PY=cG-|SS8~f7Du90 zq_!HQpD-v|U@+jW=JAMuCnJPsiN)YM8`pT0Cs{$f-)&YERuEq6aBHc>nBSny3b85C z;M3g@X*VnmHW-Z7I`}(+_&g3f9)@Y!Y0%{}DM&Dh4_6^dEfRgzT0my@`=!N3cx?p7|_|7?J!M`9l@XVPz`&M%=d;jjk`Jo^PX2-a3HKX^; zKlj7UY83s>YSk3Ip>gg@otJCA4%$49pb7kXz?7DD8>jcLqb5s&c{FHob0x7fxm_OOow9O4+C z@C7G0#aDd8cl^K^&hZPsG0Jhigb7^5b=<@hVw7m(9u}~SCs;uTud#_O>|hV?@c~Ep zh|f4N54!99S1q^H-Dt*)@AX^mQPte1yRumJbzN_V$uX!&4+Y=>-%octxe S@%2VE+~)8Mu6I`kPs~5<(%C8i literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/frames.json b/npm/examples/public/history/frames.json new file mode 100644 index 00000000..19212357 --- /dev/null +++ b/npm/examples/public/history/frames.json @@ -0,0 +1,16 @@ +{ + "data": "base.latk", + "chunk": 256, + "url": "{field}/{level}/{index}.latk", + "frames": [ + { + "table": "Signal", + "field": "value", + "levels": 4, + "frames": [ + 0, + 5760 + ] + } + ] +} \ No newline at end of file diff --git a/npm/examples/scripts/history.ts b/npm/examples/scripts/history.ts new file mode 100644 index 00000000..46fe3a37 --- /dev/null +++ b/npm/examples/scripts/history.ts @@ -0,0 +1,87 @@ +/** + * Writes the recorded day the Live data page reads as time tiles, into `public/history/`: the voltages + * of 64 buses in six areas, every 15 s for a day, as a load curve swings them and three disturbances + * pull areas down and recover. Deterministic: run it again and the same bytes come out. Run it from + * the examples with `node scripts/history.ts`; it tiles the day with the repository's `latkit tile`. + */ +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { Patch } from 'latkit'; + +/** Buses, the areas they lie in, and seconds between samples, over a day. */ +const BUSES = 64; +const AREAS = ['North', 'South', 'East', 'West', 'Coast', 'Valley']; +const EVERY = 15; +const DAY = 24 * 60 * 60; +/** When each disturbance strikes, the area it strikes, and how deep it pulls. */ +const DISTURBANCES = [ + { at: 9.2 * 3600, area: 2, depth: 0.05 }, + { at: 13.8 * 3600, area: 4, depth: 0.07 }, + { at: 18.5 * 3600, area: 0, depth: 0.06 }, +]; + +/** Deterministic numbers in [0, 1), as the examples' `random` makes them. */ +function random(seed: number): () => number { + return () => { + seed = (seed + 0x6d2b79f5) | 0; + let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +/** The system's load at second `t` of the day, from 0 to 1: low at night, peaking morning and evening. */ +function load(t: number): number { + const hour = t / 3600; + const peak = (at: number, width: number) => Math.exp(-(((hour - at) / width) ** 2)); + return ( + 0.35 + 0.35 * peak(8.5, 2.5) + 0.45 * peak(19, 3) + 0.1 * Math.sin((hour / 24) * 2 * Math.PI) + ); +} + +/** How far a disturbance pulls an area down, `since` seconds after it struck. */ +function dip(since: number, depth: number): number { + return since < 0 ? 0 : depth * Math.exp(-since / 240) * Math.min(1, since / 20); +} + +const next = random(17); +const area = Float64Array.from({ length: BUSES }, (_, i) => i % AREAS.length); +const name = Array.from( + { length: BUSES }, + (_, i) => `${AREAS[i % AREAS.length]} ${Math.floor(i / AREAS.length) + 1}`, +); +const offset = Float32Array.from({ length: BUSES }, () => next() - 0.5); +const at = Array.from({ length: DAY / EVERY }, (_, k) => k * EVERY); +const values = new Float32Array(at.length * BUSES); +for (const [k, t] of at.entries()) { + const swing = load(t); + for (let i = 0; i < BUSES; i++) { + const a = area[i]!; + const struck = DISTURBANCES.filter((d) => d.area === a).reduce( + (sum, d) => sum + dip(t - d.at, d.depth), + 0, + ); + const noise = (next() - 0.5) * 0.004; + values[k * BUSES + i] = + 1.03 + 0.025 * offset[i]! - 0.06 * swing - (0.01 * a * swing) / 5 - struck + noise; + } +} +const day = new Patch('scada') + .axis('time', 's') + .set('Signal', { count: BUSES }, { name, area }) + .append('Signal', at, { value: values }); + +const examples = path.resolve(import.meta.dirname, '..'); +const out = path.join(examples, 'public', 'history'); +const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'latkit-history-')), 'day.latk'); +fs.writeFileSync(file, day.toBytes()); +fs.rmSync(out, { recursive: true, force: true }); +execFileSync( + 'cargo', + ['run', '--quiet', '-p', 'latkit-cli', '--', 'tile', file, '--output', out, '--chunk', '256'], + { cwd: path.resolve(examples, '../..'), stdio: 'inherit', timeout: 10 * 60_000 }, +); +fs.rmSync(path.dirname(file), { recursive: true, force: true }); +console.log(`wrote ${out}`); diff --git a/npm/examples/src/examples/colormaps/index.ts b/npm/examples/src/examples/colormaps/index.ts index c5b86362..eb4ec629 100644 --- a/npm/examples/src/examples/colormaps/index.ts +++ b/npm/examples/src/examples/colormaps/index.ts @@ -14,7 +14,7 @@ import { type Expr, type Mark, type Scene, -} from '../../latkit.js'; +} from 'latkit'; import { h } from '../../ui/dom.js'; import type { Stage } from '../../ui/stage.js'; import { theme } from '../../ui/theme.js'; @@ -248,16 +248,16 @@ export default function page(stage: Stage): void { const view = stage.view(scene(), { label: 'A field of hexagons through a colormap: hover a cell to read it', }); + view.sources = [data]; const legend = stage.legend(); const rebuild = () => { const [columns, rows] = size; data.apply(hexagons(columns, rows, value)); - view.setData(data); stage.detail(`${(columns * rows).toLocaleString()} cells`); }; const restyle = () => { - view.show(scene()); + view.scene = scene(); legend.ramp( mapOf(colormap), `${colormap}${CATEGORICAL.has(colormap) ? ', ten classes' : reversed ? ', reversed' : ''}`, @@ -383,7 +383,7 @@ export default function page(stage: Stage): void { if (show === 'class' || show === 'others') restyle(); }); const reading = shown.note('Hover a cell.'); - view.on('hover', ({ hit }) => { + view.on('hovered', ({ hit }) => { if (!hit) return void (reading.textContent = 'Hover a cell.'); const v = data.get('Cell', hit.row.id, 'value') as number | null; reading.textContent = `Cell ${hit.row.id}: ${v?.toFixed(3) ?? '–'}`; @@ -404,7 +404,7 @@ export default function page(stage: Stage): void { if (brushing) return; brushing = true; void view - .pick([event.offsetX, event.offsetY], reach) + .pick([event.offsetX, event.offsetY], { radius: reach }) .then((hits) => { const values = hits.map((hit) => data.get('Cell', hit.row.id, 'value') as number); const mean = values.reduce((sum, v) => sum + v, 0) / Math.max(1, values.length); diff --git a/npm/examples/src/examples/diagram/index.ts b/npm/examples/src/examples/diagram/index.ts index 2ca4c97e..949eef99 100644 --- a/npm/examples/src/examples/diagram/index.ts +++ b/npm/examples/src/examples/diagram/index.ts @@ -5,6 +5,7 @@ */ import { Data, + Snapshot, Patch, color, field, @@ -21,7 +22,7 @@ import { type Marker, type Scene, type Side, -} from '../../latkit.js'; +} from 'latkit'; import { h } from '../../ui/dom.js'; import { each, one } from '../../ui/rows.js'; import type { Stage } from '../../ui/stage.js'; @@ -204,14 +205,14 @@ class Sketch { } } -/** The surfaces groups may take, on the dark and the light sheet. */ -const SURFACES = { +/** The colors groups may take, on the dark and the light sheet. */ +const GROUP_COLORS = { slate: ['#1a2230', '#e6ebf2'], teal: ['#14302f', '#dcefec'], plum: ['#2a1f33', '#efe6f4'], } as const; -/** A pipeline written as a JSON document, as `Data.load` reads one: a change per method, with its +/** A pipeline written as a JSON document, as `Data.read` reads one: a change per method, with its * parameters by name. */ const PIPELINE = { name: 'studio', @@ -359,19 +360,19 @@ const PRESETS: readonly (readonly [string, () => Data | Promise])[] = [ return s.data(); }, ], - ['Written as JSON', () => Data.load(JSON.stringify(PIPELINE))], + ['Written as JSON', () => Data.read(PIPELINE)], ]; /** - * Earlier versions of the data, to step back to and forward again: snapshots, each held here or - * live, never both, and freed once forgotten. + * Earlier versions of the data, to step back to and forward again: snapshots, freed once + * forgotten. */ class History { - readonly #undo: Data[] = []; - #redo: Data[] = []; + readonly #undo: Snapshot[] = []; + #redo: Snapshot[] = []; /** Remembers `before`, a snapshot of the version an edit started from. */ - commit(before: Data): void { + commit(before: Snapshot): void { this.#undo.push(before); if (this.#undo.length > 100) this.#undo.shift()!.destroy(); for (const after of this.#redo) after.destroy(); @@ -379,15 +380,17 @@ class History { } /** The version before `current`, which redo then steps forward to. */ - undo(current: Data): Data | undefined { + undo(current: Snapshot): Snapshot | undefined { const before = this.#undo.pop(); if (before) this.#redo.push(current); + else current.destroy(); return before; } - redo(current: Data): Data | undefined { + redo(current: Snapshot): Snapshot | undefined { const after = this.#redo.pop(); if (after) this.#undo.push(current); + else current.destroy(); return after; } @@ -418,12 +421,12 @@ export default function page(stage: Stage): void { portSize: 7, bend: 6, groupPadding: 16, - edgeSpacing: 4, + wireSpacing: 4, arrow: 7, lift: 3, - fontSize: 12, + textSize: 12, dash: 6, - groups: 'slate' as keyof typeof SURFACES, + groups: 'slate' as keyof typeof GROUP_COLORS, keyboard: true, cycle: false, press: 0.5, @@ -436,12 +439,18 @@ export default function page(stage: Stage): void { off: 'off', }; + /** The tables the diagram holds now. */ + const held = () => new Set(data.describe().tables.map((t) => t.name)); + /** Every id of `table`. */ + const ids = (table: string) => data.query({ of: table, select: {} }).ids; + const scene = (): Scene => { + const tables = held(); const marks: Mark[] = []; - // Containers take the theme's surface, tinted by how deep they sit; their titles head them, and + // Containers take the theme's group color, tinted by how deep they sit; their titles head them, and // they and the blocks they hold lift off the sheet. const title = { text: 'name', size: 11, color: light ? '#5b6b7d' : '#7f93a8' }; - if (data.tables.includes('Station')) { + if (tables.has('Station')) { marks.push({ vertices: { of: ['studio', 'Station'], @@ -451,7 +460,7 @@ export default function page(stage: Stage): void { }, }); } - if (data.tables.includes('Group')) { + if (tables.has('Group')) { marks.push({ vertices: { of: ['studio', 'Group'], @@ -476,7 +485,7 @@ export default function page(stage: Stage): void { }, }); // A mark for each kind of block the data holds; a table the data lacks would fail. - for (const kind of kinds.filter((k) => data.tables.includes(k))) { + for (const kind of kinds.filter((k) => tables.has(k))) { // Without a size, a block fits its label and ports. marks.push({ vertices: { @@ -497,18 +506,18 @@ export default function page(stage: Stage): void { layout, theme: { ...(light ? { base: 'light', background: '#f4f6f9' } : theme), - corner: style.corner, - outline: style.outline ? (light ? '#00000024' : '#ffffff1f') : null, - portSize: style.portSize, - bend: style.bend, - groupPadding: style.groupPadding, - edgeSpacing: style.edgeSpacing, - arrow: style.arrow, - fontSize: style.fontSize, dash: style.dash, - shadow: { color: light ? '#00000038' : '#00000073', lift: style.lift }, - // Groups take the theme's surface. - surface: SURFACES[style.groups][light ? 1 : 0], + text: { size: style.textSize }, + edge: { spacing: style.wireSpacing, arrow: style.arrow }, + diagram: { + corner: style.corner, + outline: style.outline ? (light ? '#00000024' : '#ffffff1f') : null, + portSize: style.portSize, + bend: style.bend, + groupPadding: style.groupPadding, + group: GROUP_COLORS[style.groups][light ? 1 : 0], + shadow: { color: light ? '#00000038' : '#00000073', lift: style.lift }, + }, }, controls: { connect: true, @@ -523,26 +532,26 @@ export default function page(stage: Stage): void { }; /** An id no row of the data has yet. */ const fresh = () => - 1 + Math.max(0, ...data.tables.flatMap((t) => data.ids(t).map((id) => Number(id) || 0))); + 1 + Math.max(0, ...[...held()].flatMap((t) => ids(t).map((id) => Number(id) || 0))); const view = stage.view(scene(), { label: 'A diagram: drag blocks, drag from a port to wire it, Delete removes', }); - /** Shows the current version: `next`, or the data as it is now, and a mark for each kind it holds. */ - let kindsShown = data.tables.join(); - const show = (next = data) => { - data = next; - view.setData(data); - if (data.tables.join() !== kindsShown) { - kindsShown = data.tables.join(); - view.show(scene()); + /** Takes what the data holds now: a mark for each kind of block it holds, and its description. */ + let kindsShown = [...held()].join(); + const changed = () => { + const kindsHeld = [...held()].join(); + if (kindsHeld !== kindsShown) { + kindsShown = kindsHeld; + view.scene = scene(); } describe(); }; /** Applies `patch` as one edit that undo steps back over. */ const edit = (patch: Patch, said: string) => { history.commit(data.snapshot()); - show(data.apply(patch)); + data.apply(patch); + changed(); note(said); }; /** Shows `next`, a diagram of its own: nothing to undo back to. */ @@ -550,16 +559,16 @@ export default function page(stage: Stage): void { history.clear(); data.destroy(); data = next; - view.show(scene()); - show(next); + view.sources = [data]; + kindsShown = [...held()].join(); + view.scene = scene(); + changed(); view.fit(); note(said); - const blocks = kinds.reduce( - (n, kind) => n + (data.tables.includes(kind) ? data.ids(kind).length : 0), - 0, - ); + const tables = held(); + const blocks = kinds.reduce((n, kind) => n + (tables.has(kind) ? ids(kind).length : 0), 0); stage.detail( - `${blocks.toLocaleString()} blocks · ${data.ids('Signal').length.toLocaleString()} signals`, + `${blocks.toLocaleString()} blocks · ${ids('Signal').length.toLocaleString()} signals`, ); }; const load = async (choice: (typeof PRESETS)[number]) => { @@ -572,7 +581,7 @@ export default function page(stage: Stage): void { const isBlock = (table: string): table is Kind => table in KINDS; // Moving: a block goes where the drag says, moves at once, and lands as one edit. - let before: Data | undefined; + let before: Snapshot | undefined; view.on('drag', ({ phase, hit, position }) => { if (!isBlock(hit.row.table)) return; if (phase === 'start') { @@ -583,12 +592,13 @@ export default function page(stage: Stage): void { x: [position[0]], y: [position[1]], }); + data.apply(at); + changed(); if (phase === 'end') { if (before) history.commit(before); before = undefined; - show(data.apply(at)); note(`Moved ${name(hit.row)}.`); - } else show(data.apply(at)); + } }); // Wiring: an output and an input join on the output's signal, made when it has none; an input @@ -640,14 +650,14 @@ export default function page(stage: Stage): void { const reveal = (row: Row) => { const joined = view.neighbors(row); const near = [one(row), ...joined]; - view.select(near); + view.selection = near; view.fit({ rows: near }); describe(); note(`${name(row)} and ${each(joined).length} joined to it.`); }; /** Selects `row` and puts its name up for editing. */ const rename = (row: Row) => { - view.select([one(row)]); + view.selection = [one(row)]; describe(); input.focus(); input.select(); @@ -661,7 +671,7 @@ export default function page(stage: Stage): void { view.on('menu', ({ hits, trigger }) => { const row = hits[0]?.row; if (row && !view.selection.some((s) => s.table === row.table && s.ids.includes(row.id))) - view.select([one(row)]); + view.selection = [one(row)]; describe(); const rows = view.selection; const count = each(rows).length; @@ -689,9 +699,10 @@ export default function page(stage: Stage): void { label: 'Undo', key: 'z', run: () => { - const before = history.undo(data); + const before = history.undo(data.snapshot()); if (!before) return; - show(before); + data.restore(before); + changed(); note('Undone.'); }, }, @@ -699,9 +710,10 @@ export default function page(stage: Stage): void { label: 'Redo', key: 'y', run: () => { - const after = history.redo(data); + const after = history.redo(data.snapshot()); if (!after) return; - show(after); + data.restore(after); + changed(); note('Redone.'); }, }, @@ -711,20 +723,24 @@ export default function page(stage: Stage): void { // Every block lets go of its position, the layout places them all again, and the places it // answers become the blocks' positions: one edit undo steps back over. run: () => { - const patch = Patch.to(data.name); - for (const kind of kinds.filter((k) => data.tables.includes(k))) { - const ids = data.ids(kind); - if (ids.length) patch.set(kind, ids, { x: ids.map(() => null), y: ids.map(() => null) }); + const patch = new Patch(data.name); + const tables = held(); + for (const kind of kinds.filter((k) => tables.has(k))) { + const rows = ids(kind); + if (rows.length) + patch.set(kind, rows, { x: rows.map(() => null), y: rows.map(() => null) }); } history.commit(data.snapshot()); - show(data.apply(patch)); + data.apply(patch); + changed(); view.fit(); void view.arrange().then((patches) => { // The places of every table, written as one change. const placed = patches .filter((p) => p.data === data.name) - .reduce((all, p) => all.extend(p), Patch.to(data.name)); - show(data.apply(placed)); + .reduce((all, p) => all.extend(p), new Patch(data.name)); + data.apply(placed); + changed(); note('Arranged.'); }); }, @@ -775,7 +791,7 @@ export default function page(stage: Stage): void { ]; look.choice('Layout', layouts, layout, (next) => { layout = next; - view.show(scene()); + view.scene = scene(); view.fit(); }); look.choice( @@ -787,18 +803,18 @@ export default function page(stage: Stage): void { route, (next) => { route = next; - view.show(scene()); + view.scene = scene(); }, ); look.toggle('Flow', 'c', comets, (on) => { comets = on; - view.show(scene()); + view.scene = scene(); }); look.toggle('Light', 'l', light, (on) => { light = on; - view.show(scene()); + view.scene = scene(); }); - const restyle = () => view.show(scene()); + const restyle = () => (view.scene = scene()); look.toggle('Shapes', 'h', style.shapes, (on) => ((style.shapes = on), restyle())); look.toggle( 'Diamond ports', @@ -828,10 +844,10 @@ export default function page(stage: Stage): void { | 'portSize' | 'bend' | 'groupPadding' - | 'edgeSpacing' + | 'wireSpacing' | 'arrow' | 'lift' - | 'fontSize' + | 'textSize' | 'dash', min: number, max: number, @@ -849,9 +865,9 @@ export default function page(stage: Stage): void { knob('Ports', 'portSize', 4, 14); knob('Wire bends', 'bend', 0, 16); knob('Group padding', 'groupPadding', 6, 32, 2); - knob('Wire spacing', 'edgeSpacing', 2, 12); + knob('Wire spacing', 'wireSpacing', 2, 12); knob('Arrowheads', 'arrow', 3, 14); - knob('Text', 'fontSize', 9, 16); + knob('Text', 'textSize', 9, 16); knob('Dashes', 'dash', 2, 14); knob('Shadow lift', 'lift', 0, 10); sheet.choice( @@ -893,9 +909,7 @@ export default function page(stage: Stage): void { const chosen = picker.files?.[0]; picker.value = ''; if (!chosen) return; - void chosen - .arrayBuffer() - .then((bytes) => Data.read(new Uint8Array(bytes), 'studio')) + void Data.read(chosen, { name: 'studio' }) .then((read) => open(read, `Opened ${chosen.name}.`)) .catch((error: unknown) => note(`Could not open ${chosen.name}: ${String(error)}`)); }, @@ -943,7 +957,7 @@ export default function page(stage: Stage): void { input.value = editable ? name(row) : ''; tag.hidden = true; if (row && isBlock(row.table)) - void view.locate(row).then((point) => { + void view.screenOf(row).then((point) => { if (!point || each(view.selection)[0]?.id !== row.id) return; tag.textContent = `${row.table} · ${name(row)}`; tag.style.translate = `${point[0]}px ${point[1] - 18}px`; @@ -955,7 +969,7 @@ export default function page(stage: Stage): void { .tables.map((t) => `${t.name} ${t.rows.toLocaleString()}`) .join(' · '); }; - view.on('select', describe); + view.on('selection', describe); stage.meter(view); void load(preset).catch(stage.fail); diff --git a/npm/examples/src/examples/linked-views/index.ts b/npm/examples/src/examples/linked-views/index.ts index 8d471235..b08d16fe 100644 --- a/npm/examples/src/examples/linked-views/index.ts +++ b/npm/examples/src/examples/linked-views/index.ts @@ -17,7 +17,7 @@ import { type Scene, type Size, type View, -} from '../../latkit.js'; +} from 'latkit'; import type { Stage } from '../../ui/stage.js'; import { theme } from '../../ui/theme.js'; @@ -63,7 +63,7 @@ function record(): Data { signal[f * n + i] = 0.5 + 0.45 * Math.sin(0.37 * x[i]! + 1.3 * t) * Math.cos(0.31 * y[i]! - 0.7 * t); }); - return new Data('signals').apply(patch.append(times, 'Node', { signal })); + return new Data('signals').apply(patch.append('Node', times, { signal })); } export default function page(stage: Stage): void { @@ -88,7 +88,7 @@ export default function page(stage: Stage): void { : 3.4; const lattice = (): Scene => ({ space: { plane: { grid: 'off' } }, - theme: { ...theme, dim: 0.25 }, + theme: { ...theme, focus: { dim: 0.25 } }, marks: [ { edges: { of: ['signals', 'Link'], ends: ['from', 'to'], width: 1, color: '#3a4556' } }, ...(look.poles @@ -118,7 +118,7 @@ export default function page(stage: Stage): void { space: { plot: { x: { label: 'Time (s)' }, y: { label: 'Signal', format: 'fixed', precision: 2 } }, }, - theme: { ...theme, dim: 0.2 }, + theme: { ...theme, focus: { dim: 0.2 } }, ease: 0, marks: [ { @@ -137,20 +137,19 @@ export default function page(stage: Stage): void { }); const [plane, plot] = stage.panels([lattice(), traces()], { label: 'A lattice on poles beside its signals traced: one playhead moves both', - panels: [ + at: [ [0, 0, 0.55, 1], [0.55, 0, 0.45, 1], ], }) as [View, View]; const data = record(); - plane.setData(data); - plot.setData(data); + plane.sources = plot.sources = [data]; plane.camera = { center: [(SIDE - 1) / 2, (SIDE - 1) / 2, 0], zoom: 3, pitch: 50, bearing: 20 }; plane.fit(); plot.camera = { x: [0, duration], y: [0, 1] }; const restyle = () => { - plane.show(lattice()); - plot.show(traces()); + plane.scene = lattice(); + plot.scene = traces(); }; // What either view selects, the other selects too. @@ -159,8 +158,8 @@ export default function page(stage: Stage): void { [plane, plot], [plot, plane], ] as const) - from.on('select', () => { - if (!same(from.selection, to.selection)) to.select(from.selection); + from.on('selection', () => { + if (!same(from.selection, to.selection)) to.selection = from.selection; }); const views = [ @@ -200,7 +199,7 @@ export default function page(stage: Stage): void { few, (next) => { few = next; - plot.show(traces()); + plot.scene = traces(); }, ); play.toggle('Hold samples', 'h', look.held, (on) => ((look.held = on), restyle())); @@ -210,7 +209,7 @@ export default function page(stage: Stage): void { { min: 0, max: 6, step: 0.25, value: look.height, format: (v) => `${v.toFixed(2)} units` }, (height) => { look.height = height; - plane.show(lattice()); + plane.scene = lattice(); }, ); shape.choice( @@ -221,9 +220,9 @@ export default function page(stage: Stage): void { ['Units of the plane', 'units'], ] as const, look.size, - (size) => ((look.size = size), plane.show(lattice())), + (size) => ((look.size = size), (plane.scene = lattice())), ); - shape.toggle('Poles', 'p', look.poles, (on) => ((look.poles = on), plane.show(lattice()))); + shape.toggle('Poles', 'p', look.poles, (on) => ((look.poles = on), (plane.scene = lattice()))); shape.note('Select nodes on the lattice or traces on the plot: both views select them.'); const legend = stage.legend('top'); legend.ramp('viridis', 'Signal 0–1'); diff --git a/npm/examples/src/examples/live-data/index.ts b/npm/examples/src/examples/live-data/index.ts index 940020a5..e12124f8 100644 --- a/npm/examples/src/examples/live-data/index.ts +++ b/npm/examples/src/examples/live-data/index.ts @@ -2,10 +2,13 @@ * Thousands of signals streaming in, traced over a scrolling window: frames append as they arrive * and the oldest are forgotten. Read the trace under the pointer, rank the largest changes with a * query, color traces by area or by their value now, and strike a disturbance to watch it spread. + * Or draw a recorded day instead, fetched as time tiles a span at a time, at the detail the window + * draws. */ import { Data, Patch, + Tiles, colormapCss, field, type ColormapName, @@ -15,7 +18,8 @@ import { type Interpolation, type Query, type Scene, -} from '../../latkit.js'; + type Snapshot, +} from 'latkit'; import { random } from '../../random.js'; import { h } from '../../ui/dom.js'; import { each } from '../../ui/rows.js'; @@ -39,6 +43,10 @@ const LASTS = 6; /** Signals the hot list names, and how often it reads them again. */ const HOT = 12; const HOT_EVERY = 250; +/** The recorded day: time tiles `scripts/history.ts` writes, of 64 buses' voltages every 15 s. */ +const RECORDED = '/history/frames.json'; +/** Seconds of the recorded day a second of playing shows. */ +const DAY_RATE = 600; /** * What the signals measure: each its unit, the range it keeps to, the digits it reads to, and @@ -157,10 +165,19 @@ function depth(since: number): number { : 0; } +/** Second `at` of a day as a clock reads it. */ +function clock(at: number): string { + const s = Math.max(0, Math.round(at)); + const two = (n: number) => String(n).padStart(2, '0'); + return `${two(Math.floor(s / 3600))}:${two(Math.floor(s / 60) % 60)}:${two(s % 60)}`; +} + export default function page(stage: Stage): void { + let source: 'live' | 'recorded' = 'live'; let count = 2000; let quantity: Quantity = 'voltage'; let window = 20; + let dayWindow = Infinity; let rate = 50; let interpolation: Interpolation = 'linear'; let auto = true; @@ -175,6 +192,8 @@ export default function page(stage: Stage): void { let signals = new Signals(count, quantity); let values = new Float32Array(count); let data = new Data('scada'); + /** The span of the recorded day that landed. */ + let day: [number, number] | undefined; let t = 0; let carry = 0; const shown = () => QUANTITIES[quantity]; @@ -206,13 +225,22 @@ export default function page(stage: Stage): void { }, ...(axes.title ? { - title: `${label} live`, - subtitle: `${count.toLocaleString()} signals · ${rate} samples a second`, + title: `${label} ${source === 'live' ? 'live' : 'over a recorded day'}`, + subtitle: + source === 'live' + ? `${count.toLocaleString()} signals · ${rate} samples a second` + : '64 buses · a sample every 15 s', } : {}), // Traces draw the theme's line width, and plain, its line color; what a signal does not report // reads as missing. - theme: { ...theme, dim, playhead, lineWidth: width, line: '#7fb3ff', missing: '#ff4fd8' }, + theme: { + ...theme, + focus: { dim }, + plot: { playhead }, + line: { color: '#7fb3ff', width }, + missing: '#ff4fd8', + }, ease: 0, marks: [ { @@ -238,31 +266,69 @@ export default function page(stage: Stage): void { label: 'Live signals: hover a trace to read it, click it to pick it out', }); + /** + * `f` of what the view draws of the signals now: the live record, or what it fetched of the + * recorded day; `undefined` before it has any. + */ + const read = (f: (held: Data | Snapshot) => T): T | undefined => { + if (source === 'live') return f(data); + const held = view.snapshot('scada'); + if (!held) return undefined; + try { + return f(held); + } finally { + held.destroy(); + } + }; + /** Frames what `at` shows: the window before it, filling from the record's start until the * record is as long; or all that is kept. */ const frame = (at: number) => { - const first = data.span?.[0] ?? at; + const first = read((held) => held.describe().span?.[0]) ?? at; + const span = source === 'live' ? window : dayWindow; const x: [number, number] = - window === Infinity - ? [first, at] - : [Math.max(first, at - window), Math.max(at, first + window)]; + span === Infinity + ? [first, Math.max(at, first + 1)] + : [Math.max(first, at - span), Math.max(at, first + span)]; const range = shown().range; view.camera = { x, y: auto ? null : [range[0], range[1]] }; }; - /** Takes a patch the feed brought: the record grows, and the view draws the next version. */ + /** Takes a patch the feed brought: the record grows, and the view draws it the next frame. */ const take = (patch: Patch) => { data.apply(patch); - view.setData(data); - playback.record(data.span!); + playback.record(data.describe().span!); }; /** The stream patches come through when they come as bytes, made again with each record. */ let feed: Feed | undefined; stage.signal.addEventListener('abort', () => feed?.close()); - /** Starts the record again: `n` signals measuring the quantity shown. */ + /** Draws the source chosen: a live record started again, of `n` signals measuring the quantity + * shown; or the recorded day, drawn as the tiles of it the window asks for land. */ const restart = (n = count) => { count = n; + // Patches of the record before arrive no more. + feed?.close(); + feed = undefined; + data.destroy(); + view.selection = []; + hot.replaceChildren(); + for (const section of [feedSection, disturb]) section.element.hidden = source === 'recorded'; + liveWindows.parentElement!.hidden = source === 'recorded'; + dayWindows.parentElement!.hidden = source === 'live'; + if (source === 'recorded') { + day = undefined; + quantity = 'voltage'; + measure.value = String(Object.keys(QUANTITIES).indexOf(quantity)); + data = new Data('scada'); + view.sources = [new Tiles('scada', RECORDED)]; + playback.live = false; + playback.rate = DAY_RATE; + restyle(); + detail(); + describe(null); + return; + } signals = new Signals(n, quantity); signals.dropouts = dropouts; values = new Float32Array(n); @@ -270,23 +336,21 @@ export default function page(stage: Stage): void { carry = 0; // The record starts with its first frame, so the value field is there from the start. signals.sample(0, values); - data.destroy(); data = new Data('scada').apply( new Patch() .axis('time', 's') .set('Signal', { count: n }, { name: signals.name, area: signals.area }) - .append(0, 'Signal', { value: values }), + .append('Signal', 0, { value: values }), ); - view.select([]); - view.setData(data); - // Patches of the record before arrive no more. - feed?.close(); + view.sources = [data]; const current = data; feed = new Feed((patch) => data === current && take(patch)); + playback.rate = 1; + playback.live = true; + restyle(); frame(0); detail(); describe(null); - hot.replaceChildren(); }; const playback = stage.playback({ @@ -295,13 +359,29 @@ export default function page(stage: Stage): void { view.at = at; frame(at); }, + format: (at) => (source === 'live' ? `${at.toFixed(2)} s` : clock(at)), }); const meter = stage.meter(view); - meter.note(() => `${((count * rate) / 1000).toFixed(0)}k samples/s`); + meter.note(() => + source === 'live' ? `${((count * rate) / 1000).toFixed(0)}k samples/s` : 'tiles of a day', + ); let listed = 0; + let spanned = 0; stage.frame((dt) => { if (performance.now() - listed > HOT_EVERY) list(); + // The recorded day spans what of it landed, growing as its spans do: until the transport is + // moved, it shows to the end. + if (source === 'recorded' && performance.now() - spanned > HOT_EVERY) { + spanned = performance.now(); + const span = read((held) => held.describe().span); + if (span && (span[0] !== day?.[0] || span[1] !== day?.[1])) { + const end = !day || playback.at >= day[1]; + day = span; + playback.record(span); + if (end) playback.seek(span[1]); + } + } if (playback.mode !== 'live') return; meter.measure('append', () => { carry += dt * rate; @@ -312,7 +392,7 @@ export default function page(stage: Stage): void { for (let k = 0; k < frames; k++) { t += 1 / rate; signals.sample(t, values); - patch.append(t, 'Signal', { value: values }); + patch.append('Signal', t, { value: values }); } patch.forget(t - HISTORY); if (streamed) feed!.send(patch); @@ -323,19 +403,24 @@ export default function page(stage: Stage): void { const readout = stage.section('Reading'); const reading = h('p', { class: 'series' }); readout.content.append(reading); - const name = (id: Id) => (data.get('Signal', id, 'name') as string | null) ?? `Signal ${id}`; + const name = (id: Id) => + (read((held) => held.get('Signal', id, 'name')) as string | null | undefined) ?? `Signal ${id}`; const format = (value: number) => { const { unit, digits } = shown(); return `${value.toFixed(digits)} ${unit}`; }; + const when = (at: number) => (source === 'live' ? `${at.toFixed(2)} s` : clock(at)); /** Signal `id`'s least and greatest values over the window shown, read from its levels of detail. */ const spread = (id: Id) => { const camera = view.camera; - if (!('x' in camera) || !data.span) return ''; - const { least, greatest } = data.envelope('Signal', 'value', camera.x, 1, [id]); - return Number.isNaN(least[0]) - ? '' - : `; ${format(least[0]!)} to ${format(greatest[0]!)} in the window`; + if (!('x' in camera)) return ''; + const envelope = read((held) => + held.describe().span + ? held.envelope({ table: 'Signal', field: 'value', range: camera.x, spans: 1, ids: [id] }) + : undefined, + ); + if (!envelope || Number.isNaN(envelope.least[0])) return ''; + return `; ${format(envelope.least[0]!)} to ${format(envelope.greatest[0]!)} in the window`; }; const describe = (hit: Hit | null) => { const picked = each(view.selection) @@ -343,13 +428,13 @@ export default function page(stage: Stage): void { .join(', '); const sample = hit?.sample; reading.textContent = sample - ? `${name(hit.row.id)} at ${sample.at.toFixed(2)} s: ${format(sample.value)}${spread(hit.row.id)}` + ? `${name(hit.row.id)} at ${when(sample.at)}: ${format(sample.value)}${spread(hit.row.id)}` : picked ? `Picked ${picked}.` : 'Hover a trace.'; }; - view.on('hover', ({ hit }) => describe(hit)); - view.on('select', () => { + view.on('hovered', ({ hit }) => describe(hit)); + view.on('selection', () => { describe(null); list(); }); @@ -382,7 +467,6 @@ export default function page(stage: Stage): void { hottest.content.append(hot); const list = () => { listed = performance.now(); - if (!data.span) return; const { low, range } = shown(); const query: Query = { of: 'Signal', @@ -392,21 +476,26 @@ export default function page(stage: Stage): void { select: { name: 'name', value: 'value' }, at: view.at, }; - let rows = data.query({ ...query, offset: page * HOT }); - // Past the last page, the last. - if (!rows.ids.length && page > 0) { - page = Math.max(0, Math.ceil(rows.total / HOT) - 1); - rows = data.query({ ...query, offset: page * HOT }); - } - paged.textContent = rows.total - ? `${page * HOT + 1}–${page * HOT + rows.ids.length} of ${rows.total.toLocaleString()}` + const found = read((held) => { + if (!held.describe().span) return undefined; + let rows = held.query({ ...query, offset: page * HOT }); + // Past the last page, the last. + if (!rows.ids.length && page > 0) { + page = Math.max(0, Math.ceil(rows.total / HOT) - 1); + rows = held.query({ ...query, offset: page * HOT }); + } + return rows; + }); + if (!found) return; + paged.textContent = found.total + ? `${page * HOT + 1}–${page * HOT + found.ids.length} of ${found.total.toLocaleString()}` : 'None'; - const names = rows.fields.name as readonly string[]; - const read = rows.fields.value as Float64Array; + const names = found.fields.name as readonly string[]; + const readings = found.fields.value as Float64Array; const picked = new Set(each(view.selection).map((row) => String(row.id))); hot.replaceChildren( - ...rows.ids.map((id, k) => { - const value = read[k]!; + ...found.ids.map((id, k) => { + const value = readings[k]!; const span = (value - range[0]) / (range[1] - range[0]); const score = Math.min(1, Math.max(0, low ? 1 - span : span)); const button = h( @@ -418,7 +507,7 @@ export default function page(stage: Stage): void { h('span', { class: 'bar', style: `--score: ${(score * 100).toFixed(0)}%` }), ); button.addEventListener('click', () => { - view.select([{ data: 'scada', table: 'Signal', ids: [id] }]); + view.selection = [{ data: 'scada', table: 'Signal', ids: [id] }]; describe(null); list(); }); @@ -428,8 +517,21 @@ export default function page(stage: Stage): void { }; const record = stage.section('Data'); - record.note('Simulated data'); record.choice( + 'Source', + [ + ['Live feed', 'live'], + ['Recorded day', 'recorded'], + ] as const, + source, + (next) => { + source = next; + restart(); + }, + ); + const feedSection = stage.section('Live feed'); + feedSection.note('Simulated data'); + const measure = feedSection.choice( 'Measure', Object.entries(QUANTITIES).map( ([key, q]) => [`${q.label} (${q.unit})`, key as Quantity] as const, @@ -437,11 +539,10 @@ export default function page(stage: Stage): void { quantity, (next) => { quantity = next; - restyle(); restart(); }, ); - record.choice( + feedSection.choice( 'Signals', [ ['500', 500], @@ -451,7 +552,7 @@ export default function page(stage: Stage): void { count, restart, ); - record.choice( + feedSection.choice( 'Samples', [ ['20 per second', 20], @@ -464,7 +565,7 @@ export default function page(stage: Stage): void { restyle(); }, ); - record.choice( + feedSection.choice( 'Feed', [ ['In the page', false], @@ -473,14 +574,14 @@ export default function page(stage: Stage): void { streamed, (next) => (streamed = next), ); - record.toggle('Dropouts', 'o', dropouts, (on) => { + feedSection.toggle('Dropouts', 'o', dropouts, (on) => { dropouts = on; signals.dropouts = on; }); - record.actions([{ label: 'Reset', key: 'n', run: () => restart() }]); + feedSection.actions([{ label: 'Reset', key: 'n', run: () => restart() }]); const show = stage.section('Display'); - show.choice( + const liveWindows = show.choice( 'Window', [ ['5 s', 5], @@ -494,6 +595,20 @@ export default function page(stage: Stage): void { frame(view.at); }, ); + const dayWindows = show.choice( + 'Window', + [ + ['10 minutes', 600], + ['An hour', 3600], + ['Six hours', 6 * 3600], + ['The day', Infinity], + ], + dayWindow, + (w) => { + dayWindow = w; + frame(view.at); + }, + ); show.choice( 'Color by', [ @@ -512,7 +627,7 @@ export default function page(stage: Stage): void { { min: 0.5, max: 3, step: 0.5, value: width, format: (v) => `${v} px` }, (next) => { width = next; - view.show(scene()); + view.scene = scene(); }, ); show.choice( @@ -525,7 +640,7 @@ export default function page(stage: Stage): void { interpolation, (next) => { interpolation = next; - view.show(scene()); + view.scene = scene(); }, ); show.range( @@ -533,7 +648,7 @@ export default function page(stage: Stage): void { { min: 0, max: 1, step: 0.05, value: dim, format: (v) => `${Math.round(v * 100)}%` }, (next) => { dim = next; - view.show(scene()); + view.scene = scene(); }, ); show.choice( @@ -546,14 +661,14 @@ export default function page(stage: Stage): void { playhead, (next) => { playhead = next; - view.show(scene()); + view.scene = scene(); }, ); show.toggle('Auto range', 'a', auto, (on) => { auto = on; frame(view.at); }); - show.toggle('Title', undefined, axes.title, (on) => ((axes.title = on), view.show(scene()))); + show.toggle('Title', undefined, axes.title, (on) => ((axes.title = on), (view.scene = scene()))); const axesSection = stage.section('Axes', false); axesSection.choice( 'Values', @@ -564,15 +679,20 @@ export default function page(stage: Stage): void { ['Engineering', 'engineering'], ] as const, axes.format, - (format) => ((axes.format = format), view.show(scene())), + (format) => ((axes.format = format), (view.scene = scene())), + ); + axesSection.toggle('Grid', 'g', axes.grid, (on) => ((axes.grid = on), (view.scene = scene()))); + axesSection.toggle( + 'Limits', + 'l', + axes.limits, + (on) => ((axes.limits = on), (view.scene = scene())), ); - axesSection.toggle('Grid', 'g', axes.grid, (on) => ((axes.grid = on), view.show(scene()))); - axesSection.toggle('Limits', 'l', axes.limits, (on) => ((axes.limits = on), view.show(scene()))); axesSection.toggle( 'Time axis', undefined, axes.time, - (on) => ((axes.time = on), view.show(scene())), + (on) => ((axes.time = on), (view.scene = scene())), ); const areas = show.series(AREAS.map(([label, color]) => ({ label, color }))); const maps = h('div', { class: 'swatches' }); @@ -593,7 +713,7 @@ export default function page(stage: Stage): void { show.content.append(maps); const legend = stage.legend('top'); const restyle = () => { - view.show(scene()); + view.scene = scene(); const { label, unit, range } = shown(); areas.hidden = colorBy !== 'area'; maps.hidden = legend.hidden = colorBy !== 'value'; @@ -624,8 +744,9 @@ export default function page(stage: Stage): void { const detail = () => stage.detail( - `${count.toLocaleString()} signals · ${shown().label.toLowerCase()} · ${HISTORY} s kept`, + source === 'live' + ? `${count.toLocaleString()} signals · ${shown().label.toLowerCase()} · ${HISTORY} s kept` + : '64 buses · voltage · a recorded day, fetched as the window draws it', ); - restyle(); restart(); } diff --git a/npm/examples/src/examples/networks/cases.ts b/npm/examples/src/examples/networks/cases.ts index 1c6aea4c..71abd11b 100644 --- a/npm/examples/src/examples/networks/cases.ts +++ b/npm/examples/src/examples/networks/cases.ts @@ -9,7 +9,7 @@ * by frame as int16 of `speedScale` per unit, `dt` seconds apart, and the fault the simulation * applied. */ -import { Data, Patch, reference } from '../../latkit.js'; +import { Data, Patch, reference } from 'latkit'; interface Header { readonly case: string; @@ -167,7 +167,7 @@ export function readCase(id: string, buffer: ArrayBuffer, downloaded = buffer.by const speeds = Float32Array.from(array('machine.speed', Int16Array), (value) => value * scale); patch.axis('time', 's'); for (let f = 0; f < frames; f++) - patch.append(f * dt, 'Machine', { speed: speeds.subarray(f * g, (f + 1) * g) }); + patch.append('Machine', f * dt, { speed: speeds.subarray(f * g, (f + 1) * g) }); simulation = { frames, dt, diff --git a/npm/examples/src/examples/networks/continent.ts b/npm/examples/src/examples/networks/continent.ts index b5f32490..3c2aa923 100644 --- a/npm/examples/src/examples/networks/continent.ts +++ b/npm/examples/src/examples/networks/continent.ts @@ -6,7 +6,7 @@ import { random } from '../../random.js'; * switching stations, the busiest by double or triple circuits, with rural substations and power * plants along the way. Seeded, so every build draws the same grid, in time linear in its size. */ -import { Data, Patch, reference } from '../../latkit.js'; +import { Data, Patch, reference } from 'latkit'; import { levelOf, type Case } from './cases.js'; /** Metro areas: name, longitude, latitude, and millions of people. */ diff --git a/npm/examples/src/examples/networks/index.ts b/npm/examples/src/examples/networks/index.ts index 20b273c3..aca0d860 100644 --- a/npm/examples/src/examples/networks/index.ts +++ b/npm/examples/src/examples/networks/index.ts @@ -21,6 +21,7 @@ import { zoom, type ColormapName, type Controls, + type Data, type Expr, type Fit, type Mark, @@ -29,7 +30,7 @@ import { type Scene, type Size, type Zoom, -} from '../../latkit.js'; +} from 'latkit'; import { h } from '../../ui/dom.js'; import { each } from '../../ui/rows.js'; import type { Stage } from '../../ui/stage.js'; @@ -96,6 +97,8 @@ const fixed = (value: number) => (value >= 100 ? value.toFixed(0) : value.toPrec interface Grid { readonly id: GridId; readonly label: string; + /** Its dataset; none for the map alone. */ + readonly data?: Data; readonly buses: number; readonly lines: number; readonly summary: string; @@ -123,7 +126,7 @@ function extent(positions: Float32Array): Fit { /** A case's buses zoned by the `ZONE`-degree square each stands in: a zone for each square, and a * reference from each bus to its zone, which an edges mark draws as a star joining its buses. */ function zones(found: Case): Patch { - const ids = found.data.ids('Bus'); + const ids = found.data.query({ of: 'Bus', select: {} }).ids; const squares = new Map(); const zone = new Uint32Array(ids.length); for (let i = 0; i < ids.length; i++) { @@ -173,7 +176,7 @@ export default async function page(stage: Stage): Promise { glow: 5, dim: 0.35, halo: 2, - hoverScale: 1.25, + grow: 1.25, night: 0.28, comets: 'screen' as 'screen' | 'map', cometScale: 1, @@ -217,7 +220,6 @@ export default async function page(stage: Stage): Promise { : { earth: { projection: state.projection === 'globe' ? 'globe' : 'mercator', - surface: LOOK.ground, graticule: state.graticule, atmosphere: state.atmosphere, ...(state.daylight === 'off' @@ -227,18 +229,14 @@ export default async function page(stage: Stage): Promise { }, theme: { ...theme, - area: LOOK.fill, - atmosphere: '#5aa9ff99', - night: look.night, - glow: look.glow, - dim: look.dim, - halo: look.halo, - hoverScale: look.hoverScale, - laneBow: look.bow, + area: { color: LOOK.fill }, + earth: { ground: LOOK.ground, atmosphere: '#5aa9ff99', night: look.night }, + text: { halo: look.halo }, + focus: { glow: look.glow, dim: look.dim, grow: look.grow }, // Marks without a color of their own take the theme's: plain buses and lines; lines set to // blend take the colors of the buses they join. - vertex: '#d9e2ec', - edge: look.lineColor === 'ends' ? null : '#8fa3b8', + vertex: { color: '#d9e2ec' }, + edge: { color: look.lineColor === 'ends' ? null : '#8fa3b8', bow: look.bow }, // Comets kept soft, as rivers carry them across the map too: on screen, pixels apart; on the // map, degrees apart, so they zoom with it. flow: @@ -246,9 +244,8 @@ export default async function page(stage: Stage): Promise { ? { color: '#d6e6ffa6', width: 3 * look.cometScale, - length: 0.35 * look.cometScale, - spacing: 1.2 * look.cometScale, - units: true, + length: units(0.35 * look.cometScale), + spacing: units(1.2 * look.cometScale), } : { color: '#d6e6ffa6', @@ -425,7 +422,7 @@ export default async function page(stage: Stage): Promise { }); // Fits keep clear of the readout over the view's top and the credit and legend over its foot. view.insets = [32, 16, 32, 16]; - const restyle = () => view.show(scene()); + const restyle = () => (view.scene = scene()); /** Frames the grid, whatever the map beneath it spans; the map alone, all of it. */ const frame = () => view.fit(grid?.bounds ?? 'all'); const legend = stage.legend(); @@ -549,7 +546,7 @@ export default async function page(stage: Stage): Promise { canvas.addEventListener( 'pointermove', (event) => { - const ground = view.ground([event.offsetX, event.offsetY]); + const ground = view.position([event.offsetX, event.offsetY]); where.textContent = ground ? state.projection === 'plane' ? `Pointer at ${ground[0].toFixed(2)}, ${ground[1].toFixed(2)}` @@ -697,7 +694,7 @@ export default async function page(stage: Stage): Promise { const lookSection = stage.section('Look', false); const slider = ( label: string, - name: 'glow' | 'dim' | 'halo' | 'hoverScale' | 'night' | 'cometScale', + name: 'glow' | 'dim' | 'halo' | 'grow' | 'night' | 'cometScale', o: { min: number; max: number; step: number; format: (v: number) => string }, ) => lookSection.range(label, { ...o, value: look[name] }, (value) => { @@ -712,7 +709,7 @@ export default async function page(stage: Stage): Promise { format: (v) => `${Math.round(v * 100)}%`, }); slider('Label halo', 'halo', { min: 0, max: 5, step: 0.5, format: (v) => `${v} px` }); - slider('Hover growth', 'hoverScale', { + slider('Hover growth', 'grow', { min: 1, max: 2, step: 0.05, @@ -799,10 +796,12 @@ export default async function page(stage: Stage): Promise { const row = hoveredRow ?? each(view.selection)[0]; list.replaceChildren(); if (!row) return void (described.textContent = 'Select an item.'); - const data = view.data(row.data); + const data = view.snapshot(row.data); if (!data) return; - const value = (name: string) => data.get(row.table, row.id, name, view.at); - const fields = data.fields(row.table).filter((name) => !HIDDEN.test(name)); + const at = view.at; + const value = (name: string) => data.get(row.table, row.id, name, { at }); + const table = data.describe().tables.find((t) => t.name === row.table); + const fields = (table?.fields ?? []).map((f) => f.name).filter((name) => !HIDDEN.test(name)); const name = fields.find((f) => /^name$/i.test(f)); described.textContent = `${row.table} · ${name ? format(value(name)) : `#${row.id}`}${hoveredRow ? '' : ' · selected'}`; // The grid's figures first, then the rest, leaving out what this row has no value for. @@ -818,25 +817,26 @@ export default async function page(stage: Stage): Promise { list.append(dt, dd); if (++shown === 18) break; } + data.destroy(); }; - view.on('hover', ({ hit }) => { + view.on('hovered', ({ hit }) => { hoveredRow = hit?.row ?? null; inspect(); }); - view.on('select', inspect); + view.on('selection', inspect); /** Puts the tag where its bus now stands on screen, or hides it where none shows. */ function pin(): void { - const point = tagged && view.point(tagged); + const point = tagged && view.screen(tagged); tag.hidden = !point; if (point) tag.style.translate = `${point[0]}px ${point[1]}px`; } /** Selects every bus whose name holds `text`, frames them, and tags the first. */ function find(text: string): void { - const data = view.data('grid'); + const data = grid?.data; tagged = undefined; look.found = ''; - if (!text || !data?.tables.includes('Bus') || !grid?.case?.named) { + if (!text || !data || !grid?.case?.named) { pin(); restyle(); if (text) described.textContent = 'This grid has no named buses.'; @@ -861,7 +861,7 @@ export default async function page(stage: Stage): Promise { const [lon, lat] = [found.fields.lon as Float64Array, found.fields.lat as Float64Array]; look.found = names[0]!; restyle(); - view.select(rows); + view.selection = rows; // One found is brought into view as the camera stands; more are framed together. view.fit(found.ids.length === 1 ? { reveal: first } : { rows }); tagged = [lon[0]!, lat[0]!, 0]; @@ -892,16 +892,18 @@ export default async function page(stage: Stage): Promise { legend.ramp(colormap, `${label} 0 – ${fixed(grid.peaks[state.color])} MW`); } - /** The grid's figures in the map's states, when it has states, and what they color by. */ + /** The grid's figures in the map's states, when it has states, and what they color by; and the + * view given what the map and the grid hold. */ function join(): void { - joined?.data.destroy(); + if (joined) map?.loaded.states?.restore(joined.before); + joined?.before.destroy(); joined = undefined; const states = map?.loaded.states; if (states && grid?.case && state.statesBy !== 'none') { joined = joinStates(states, grid.case); - view.setData(joined.data); mapSummary.textContent = `${map!.loaded.summary} · the grid reaches ${joined.states} states`; } + view.sources = [...(map?.loaded.sources ?? []), ...(grid?.data ? [grid.data] : [])]; restyle(); showLegend(); } @@ -915,7 +917,6 @@ export default async function page(stage: Stage): Promise { const started = performance.now(); let next: Grid; if (id === 'none') { - view.removeData('grid'); next = { id, label, @@ -927,10 +928,10 @@ export default async function page(stage: Stage): Promise { }; } else if (id.startsWith('lattice')) { const built = lattice(id); - view.setData(built.data); next = { id, label, + data: built.data, buses: built.buses, lines: built.lines, bounds: built.bounds, @@ -948,11 +949,11 @@ export default async function page(stage: Stage): Promise { // Cases' buses are zoned by where they stand; a continent's are too many to zone in the page. const zoned = found.named; if (zoned) found.data.apply(zones(found)); - view.setData(found.data); const peak = (values: Float32Array) => values.reduce((top, v) => Math.max(top, v), 0); next = { id, label, + data: found.data, buses: found.buses, lines: found.lines, case: found, @@ -969,7 +970,7 @@ export default async function page(stage: Stage): Promise { }; } grid = next; - view.select([]); + view.selection = []; hoveredRow = null; search.value = ''; tagged = undefined; @@ -1004,17 +1005,18 @@ export default async function page(stage: Stage): Promise { async function loadMap(choice: MapChoice | undefined): Promise { mapLoad?.abort(); const load = (mapLoad = new AbortController()); - map?.loaded.unload(view); - map = undefined; + if (joined) map?.loaded.states?.restore(joined.before); + joined?.before.destroy(); joined = undefined; + map = undefined; credit.textContent = ''; mapSummary.textContent = choice ? 'loading…' : ''; - restyle(); + join(); if (!choice) return; - const loaded = await choice.load(view, load.signal, (status) => { + const loaded = await choice.load(load.signal, (status) => { if (!load.signal.aborted) mapSummary.textContent = status; }); - if (load.signal.aborted) return loaded.unload(view); + if (load.signal.aborted) return; map = { choice, loaded }; credit.textContent = choice.attribution; mapSummary.textContent = loaded.summary; diff --git a/npm/examples/src/examples/networks/lattice.ts b/npm/examples/src/examples/networks/lattice.ts index 6da25fee..92daa540 100644 --- a/npm/examples/src/examples/networks/lattice.ts +++ b/npm/examples/src/examples/networks/lattice.ts @@ -3,7 +3,7 @@ * size. Each bus joins its right and lower neighbours, and a few lines span the whole box as great * circles. Built in time linear in the bus count. */ -import { Data, Patch, reference, type Fit } from '../../latkit.js'; +import { Data, Patch, reference, type Fit } from 'latkit'; interface Spec { readonly cols: number; diff --git a/npm/examples/src/examples/networks/maps.ts b/npm/examples/src/examples/networks/maps.ts index c831360b..5c6a91e2 100644 --- a/npm/examples/src/examples/networks/maps.ts +++ b/npm/examples/src/examples/networks/maps.ts @@ -4,7 +4,7 @@ * repository; or OpenStreetMap's planet as OpenFreeMap's vector tiles. GeoJSON downloads as bytes * and is read in a worker; tiles are fetched as the camera asks for them. */ -import { Data, field, type ColormapName, type Mark, type View } from '../../latkit.js'; +import { Data, Tiles, field, type ColormapName, type Mark, type Source } from 'latkit'; import { download, megabytes } from './cases.js'; /** The page's colors, as CSS. */ @@ -28,22 +28,22 @@ export interface Styling { readonly states?: { readonly by: string; readonly peak: number; readonly colormap: ColormapName }; } -/** A map, loaded: its marks, drawn under the grid, and its states when it has them. */ +/** A map, loaded: its marks, drawn under the grid, what they draw, and its states when it has them. */ export interface Loaded { readonly summary: string; marks(styling: Styling): Mark[]; + /** The datasets its marks draw. */ + readonly sources: readonly Source[]; /** The dataset of states the grid's figures color, its rows in table `polygons`. */ readonly states?: Data; - /** Takes back from the view what loading gave it. */ - unload(view: View): void; } export interface MapChoice { readonly id: string; readonly label: string; readonly attribution: string; - /** Loads the map into `view`, telling `progress` how it goes. */ - load(view: View, signal: AbortSignal, progress: (status: string) => void): Promise; + /** Loads the map, telling `progress` how it goes. */ + load(signal: AbortSignal, progress: (status: string) => void): Promise; } const NE = 'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/'; @@ -55,7 +55,7 @@ const borders: MapChoice = { id: 'borders', label: 'Coasts and borders', attribution: NATURAL_EARTH, - async load(view, signal) { + async load(signal) { const started = performance.now(); const files = ['coastline', 'countries', 'provinces'] as const; const sets = await Promise.all( @@ -64,11 +64,11 @@ const borders: MapChoice = { `${import.meta.env.BASE_URL}maps/${name}.geojson.gz`, signal, ); - return Data.read(bytes, name); + return Data.read(bytes, { name, signal }); }), ); - for (const data of sets) view.setData(data); return { + sources: sets, summary: `coasts and borders in ${seconds(started)}`, marks: () => [ { @@ -77,7 +77,6 @@ const borders: MapChoice = { { lines: { of: ['countries', 'lines'], color: LOOK.line, width: 1 } }, { lines: { of: ['coastline', 'lines'], color: LOOK.coast, width: 1 } }, ], - unload: (view) => files.forEach((name) => view.removeData(name)), }; }, }; @@ -88,7 +87,7 @@ function states(scale: '50m' | '10m', label: string): MapChoice { id: `states-${scale}`, label, attribution: NATURAL_EARTH, - async load(view, signal, progress) { + async load(signal, progress) { const started = performance.now(); const { bytes, received } = await download( `${NE}ne_${scale}_admin_1_states_provinces.geojson`, @@ -96,9 +95,9 @@ function states(scale: '50m' | '10m', label: string): MapChoice { (got, total) => progress(`downloading ${megabytes(got, total)}`), ); const fetched = performance.now(); - const data = await Data.read(bytes, 'states'); - view.setData(data); + const data = await Data.read(bytes, { name: 'states', signal }); return { + sources: [data], states: data, summary: `${(received / 1e6).toFixed(1)} MB in ${seconds(started)}, read in ${seconds(fetched)}`, marks: ({ labels, states }) => [ @@ -115,7 +114,6 @@ function states(scale: '50m' | '10m', label: string): MapChoice { }, { lines: { of: ['states', 'polygons'], color: LOOK.line, width: 0.7 } }, ], - unload: (view) => view.removeData('states'), }; }, }; @@ -126,9 +124,9 @@ const planet: MapChoice = { id: 'planet', label: 'Planet, vector tiles', attribution: 'OpenFreeMap · © OpenMapTiles · © OpenStreetMap contributors', - load(view) { - view.setTiles('base', 'https://tiles.openfreemap.org/planet'); + load() { return Promise.resolve({ + sources: [new Tiles('base', 'https://tiles.openfreemap.org/planet')], summary: 'tiles fetch as the camera asks for them', marks: ({ labels }) => [ { areas: { of: ['base', 'landcover'], color: LOOK.landcover } }, @@ -157,7 +155,6 @@ const planet: MapChoice = { ] : []), ], - unload: (view) => view.removeData('base'), }); }, }; diff --git a/npm/examples/src/examples/networks/states.ts b/npm/examples/src/examples/networks/states.ts index 527345fb..6e3b8fd9 100644 --- a/npm/examples/src/examples/networks/states.ts +++ b/npm/examples/src/examples/networks/states.ts @@ -3,7 +3,7 @@ * color by: buses, load, and generation counted where each bus stands, and where the grid was * simulated, how far its machines' speeds swing in each state, frame by frame. */ -import { Patch, type Data, type Id } from '../../latkit.js'; +import { Patch, type Data, type Id, type Snapshot } from 'latkit'; import type { Case } from './cases.js'; /** What each state's figures hold, and in what unit. */ @@ -16,7 +16,8 @@ export const STATE_FIELDS = { export type StateField = keyof typeof STATE_FIELDS; export interface Joined { - readonly data: Data; + /** The states as they were before the grid's figures were written into them. */ + readonly before: Snapshot; /** How many states the grid reaches. */ readonly states: number; /** Each figure's largest value in any state. */ @@ -46,7 +47,7 @@ function inside(state: State, x: number, y: number): boolean { return crossings % 2 === 1; } -/** A snapshot of `states` with the grid's figures for every state it reaches. */ +/** Writes the grid's figures into `states` for every state it reaches, keeping what they were. */ export function joinStates(states: Data, grid: Case): Joined { const { positions, simulation } = grid; const extent = [Infinity, Infinity, -Infinity, -Infinity]; @@ -56,7 +57,7 @@ export function joinStates(states: Data, grid: Case): Joined { extent[2] = Math.max(extent[2]!, positions[b * 2]!); extent[3] = Math.max(extent[3]!, positions[b * 2 + 1]!); } - const ids: Id[] = states.ids('polygons'); + const ids: Id[] = states.query({ of: 'polygons', select: {} }).ids; const found: State[] = []; ids.forEach((id, row) => { const polygons = (states.get('polygons', id, 'geometry') ?? []) as Ring[][]; @@ -126,8 +127,10 @@ export function joinStates(states: Data, grid: Case): Joined { swing[k] = Math.max(swing[k]!, Math.abs(simulation.speeds[f * g + m]!)); } peaks.swing = Math.max(peaks.swing, peak(swing)); - patch.append(f * simulation.dt, 'polygons', { swing }); + patch.append('polygons', f * simulation.dt, { swing }); } } - return { data: states.snapshot().apply(patch), states: reached.size, peaks }; + const before = states.snapshot(); + states.apply(patch); + return { before, states: reached.size, peaks }; } diff --git a/npm/examples/src/examples/power-flow/index.ts b/npm/examples/src/examples/power-flow/index.ts index c9213843..bbaab9b8 100644 --- a/npm/examples/src/examples/power-flow/index.ts +++ b/npm/examples/src/examples/power-flow/index.ts @@ -14,13 +14,14 @@ import { field, icon, reference, + units, when, type Hit, type Marker, type Motion, type RgbaImage, type Scene, -} from '../../latkit.js'; +} from 'latkit'; import { each } from '../../ui/rows.js'; import type { Stage } from '../../ui/stage.js'; import { theme } from '../../ui/theme.js'; @@ -252,20 +253,16 @@ export default async function page(stage: Stage): Promise { }, }); const scene = (): Scene => ({ - space: { earth: { projection: 'mercator', surface: LOOK.ground, graticule: false } }, + space: { earth: { graticule: false } }, theme: { ...theme, - text: LOOK.text, - hover: LOOK.hover, + earth: { ground: LOOK.ground }, + text: { color: LOOK.text, font: 'Jura, sans-serif', size: 12, halo: 3 }, // Selected rows glow blue, or each in its own color. - selected: state.ownGlow ? null : LOOK.hover, + focus: { hover: LOOK.hover, selected: state.ownGlow ? null : LOOK.hover, glow: 6 }, // Comets are part of the map, in degrees: they zoom with it and stay on their branches. - flow: { color: LOOK.comet, spacing: 1.35, length: 0.6, width: 6, units: true }, - font: 'Jura, sans-serif', - fontSize: 12, - halo: 3, - glow: 6, - laneBow: 0.22, + flow: { color: LOOK.comet, spacing: units(1.35), length: units(0.6), width: 6 }, + edge: { bow: 0.22 }, }, ease: state.ease, // Easing, pulses, and comets hold still where the system asks for reduced motion, unless set. @@ -293,11 +290,10 @@ export default async function page(stage: Stage): Promise { const view = stage.view(scene(), { label: 'A state’s grid: hover or click a substation or branch to inspect it', }); - view.setData(border); - view.setData(data); + view.sources = [border, data]; const { xMin, xMax, yMin, yMax } = texas.bounds; view.fit({ bounds: { min: [xMin, yMin, 0], max: [xMax, yMax, 0] } }); - const restyle = () => view.show(scene()); + const restyle = () => (view.scene = scene()); // ── Grid ── const gridSection = stage.section('Grid'); @@ -398,8 +394,8 @@ export default async function page(stage: Stage): Promise { : `${s[from]!.name} → ${s[to]!.name} · ${Math.round(value('mw'))} MW · ${Math.round(value('loading'))}% of ${b.pmax} MW`; } }; - view.on('hover', ({ hit }) => ((hovered = hit), inspect())); - view.on('select', inspect); + view.on('hovered', ({ hit }) => ((hovered = hit), inspect())); + view.on('selection', inspect); const meter = stage.meter(view); meter.note(() => `${grid.trips} trips`); @@ -407,7 +403,6 @@ export default async function page(stage: Stage): Promise { /** Shows the grid as it stands. */ function show(): void { data.apply(patchOf(grid)); - view.setData(data); const served = grid.injection.reduce((sum, p, i) => sum + (grid.load[i] ? -p : 0), 0); const out = grid.out.reduce((a, b) => a + b, 0); totals.textContent = `${Math.round(served).toLocaleString()} MW served · ${out} branches out · ${grid.dark} loads dark`; diff --git a/npm/examples/src/examples/soft-body/index.ts b/npm/examples/src/examples/soft-body/index.ts index 2274ae48..44f0bda4 100644 --- a/npm/examples/src/examples/soft-body/index.ts +++ b/npm/examples/src/examples/soft-body/index.ts @@ -13,7 +13,7 @@ import { type ColormapName, type Grid, type Scene, -} from '../../latkit.js'; +} from 'latkit'; import type { Stage } from '../../ui/stage.js'; import { theme } from '../../ui/theme.js'; import { Body, ROOM, STEP, type Controls } from './body.js'; @@ -169,7 +169,12 @@ export default async function bunny(stage: Stage): Promise { return { space: { plane: { grid: GRIDS[lit.grid] } }, // The mesh's dots and springs draw the theme's sizes. - theme: { ...THEME, shading: lit.shading, vertexRadius: inspect ? lit.dots : 0, edgeWidth: 1 }, + theme: { + ...THEME, + shading: lit.shading, + vertex: { radius: inspect ? lit.dots : 0 }, + edge: { width: 1 }, + }, controls: { pickRadius: 14 }, marks: [ { @@ -242,6 +247,7 @@ export default async function bunny(stage: Stage): Promise { const space = stage.view(room(true), { label: 'The bunny: drag it to pull and fling it, click it to poke it', }); + space.sources = [data]; space.camera = { center: [0, 0], zoom: 6, pitch: 58, bearing: 30 }; space.fit({ bounds: { min: [-ROOM, -ROOM, 0], max: [ROOM, ROOM, 2.4] } }); @@ -316,13 +322,13 @@ export default async function bunny(stage: Stage): Promise { const legend = stage.legend(); const repaint = (p: string) => { paint = p; - space.show(room(playback.mode === 'live')); + space.scene = room(playback.mode === 'live'); legend.ramp(PAINTS[p]!.colormap, PAINTS[p]!.legend); legend.hidden = p === 'solid'; }; material.toggle('Mesh', 'i', false, (on) => { inspect = on; - space.show(room(playback.mode === 'live')); + space.scene = room(playback.mode === 'live'); }); material.choice( 'Color', @@ -336,7 +342,7 @@ export default async function bunny(stage: Stage): Promise { { min: 0, max: 1, step: 0.05, value: lit.shading, format: (v) => `${Math.round(v * 100)}%` }, (shading) => { lit.shading = shading; - space.show(room(playback.mode === 'live')); + space.scene = room(playback.mode === 'live'); }, ); light.range( @@ -344,7 +350,7 @@ export default async function bunny(stage: Stage): Promise { { min: 0.5, max: 4, step: 0.25, value: lit.dots, format: (v) => `${v.toFixed(2)} px` }, (dots) => { lit.dots = dots; - space.show(room(playback.mode === 'live')); + space.scene = room(playback.mode === 'live'); }, ); light.choice( @@ -357,7 +363,7 @@ export default async function bunny(stage: Stage): Promise { lit.grid, (grid) => { lit.grid = grid; - space.show(room(playback.mode === 'live')); + space.scene = room(playback.mode === 'live'); }, ); @@ -374,14 +380,15 @@ export default async function bunny(stage: Stage): Promise { }, { label: 'Center height, speed, and volume', height: 220 }, ); + plot.sources = [data]; const playback = stage.playback({ replay: { seconds: 5, rate: 0.3 }, show(at) { - const pieces = data.get('Body', 0, 'phase', at) === 1; + const pieces = data.get('Body', 0, 'phase', { at }) === 1; if (pieces !== fragments) { fragments = pieces; - space.show(room(playback.mode === 'live')); + space.scene = room(playback.mode === 'live'); } space.at = at; plot.at = at; @@ -390,7 +397,7 @@ export default async function bunny(stage: Stage): Promise { }, change(live) { // Paused, a press pans instead of grabbing. - space.show(room(live)); + space.scene = room(live); if (!live) { body.release(true); body.endSquash(); @@ -426,9 +433,9 @@ export default async function bunny(stage: Stage): Promise { shadow.opacity[0] = 0.42 / (1 + height * height * 0.4); } const patch = new Patch() - .append(body.time, 'Vertex', { ...frame }) - .append(body.time, 'Shadow', shadow) - .append(body.time, 'Body', { + .append('Vertex', body.time, { ...frame }) + .append('Shadow', body.time, shadow) + .append('Body', body.time, { height: [height], speed: [speed], volume: [Number.isFinite(volume) ? volume : null], @@ -437,14 +444,12 @@ export default async function bunny(stage: Stage): Promise { .forget(body.time - HISTORY); if (body.burst.active || wasBurst || !recordedChunks) { body.burst.sample(chunkFrame, STEP); - patch.append(body.time, 'ChunkVertex', { ...chunkFrame }); + patch.append('ChunkVertex', body.time, { ...chunkFrame }); recordedChunks = true; } wasBurst = body.burst.active; data.apply(patch); - space.setData(data); - plot.setData(data); - playback.record(data.span!); + playback.record(data.describe().span!); popButton.firstChild!.textContent = body.burst.active ? 'Gather' : 'Pop'; }; @@ -474,9 +479,9 @@ export default async function bunny(stage: Stage): Promise { const meter = stage.meter(space); meter.note(() => { const at = playback.at; - const height = (data.get('Body', 0, 'height', at) as number | null) ?? 0; - const speed = (data.get('Body', 0, 'speed', at) as number | null) ?? 0; - const volume = data.get('Body', 0, 'volume', at) as number | null; + const height = (data.get('Body', 0, 'height', { at }) as number | null) ?? 0; + const speed = (data.get('Body', 0, 'speed', { at }) as number | null) ?? 0; + const volume = data.get('Body', 0, 'volume', { at }) as number | null; const state = playback.mode === 'live' ? body.state : playback.mode === 'playing' ? 'replay' : 'paused'; return `${state} · center ${height.toFixed(2)} m · ${speed.toFixed(1)} m/s · ${volume !== null && Number.isFinite(volume) ? `volume ${(volume * 100).toFixed(0)}%` : '24 pieces'}`; diff --git a/npm/examples/src/latkit.ts b/npm/examples/src/latkit.ts deleted file mode 100644 index 6cf31602..00000000 --- a/npm/examples/src/latkit.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** The example application explicitly loads its shared rendering runtime. */ -import { loadLatkit } from 'latkit'; -export * from 'latkit'; -export type Data = import('latkit').Data; -export type Gpu = import('latkit').Gpu; -export type Shot = import('latkit').Shot; -export const { Data, Gpu, Shot, colormapCss } = await loadLatkit(); diff --git a/npm/examples/src/main.ts b/npm/examples/src/main.ts index bc8b251b..14ef6329 100644 --- a/npm/examples/src/main.ts +++ b/npm/examples/src/main.ts @@ -1,4 +1,4 @@ -import { Gpu } from './latkit.js'; +import { Gpu } from 'latkit'; import { examples } from './examples/index.js'; import { Shell } from './ui/shell.js'; import './ui/style.css'; diff --git a/npm/examples/src/ui/export.ts b/npm/examples/src/ui/export.ts index 08ce74e9..3870aa2e 100644 --- a/npm/examples/src/ui/export.ts +++ b/npm/examples/src/ui/export.ts @@ -1,4 +1,4 @@ -import { Shot, type ImageFormat, type View } from '../latkit.js'; +import type { ImageFormat } from 'latkit'; import { h } from './dom.js'; import type { Stage } from './stage.js'; @@ -14,12 +14,13 @@ const FORMATS: Record { - const shown = stage.shown - .map(({ view, element, panel: [x, y, w, h] }) => { - const canvas = element.getBoundingClientRect(); - const [left, top] = [canvas.left + x * canvas.width, canvas.top + y * canvas.height]; - const [width, height] = [w * canvas.width, h * canvas.height]; - return { - view, - box: { left, top, width, height, right: left + width, bottom: top + height }, - }; - }) - .filter(({ box }) => box.width > 0 && box.height > 0); - const left = Math.min(...shown.map((s) => s.box.left)); - const top = Math.min(...shown.map((s) => s.box.top)); - const width = Math.max(...shown.map((s) => s.box.right)) - left; - const height = Math.max(...shown.map((s) => s.box.bottom)) - top; - const scale = Math.min(2, devicePixelRatio); - const size: [number, number] = [ - Math.max(align[0], Math.floor((width * scale) / align[0]) * align[0]), - Math.max(align[1], Math.floor((height * scale) / align[1]) * align[1]), - ]; - const panels = shown.map( - ({ box }) => - [ - (box.left - left) / width, - (box.top - top) / height, - box.width / width, - box.height / height, - ] as const, - ); - return [new Shot(size).scale(size[0] / width).panels(panels), shown.map((s) => s.view)]; - }; + /** Pixels a CSS pixel of what is saved: as sharp as the screen, at most twice. */ + const scale = () => Math.min(2, devicePixelRatio); const picture = async () => { status.textContent = 'Taking a picture…'; const { extension, type, alpha } = FORMATS[format]; - const [plain, views] = shot([1, 1]); - const taken = transparent && alpha ? plain.transparent() : plain; - // A picture waits for its views to be still; the page holds its own motion meanwhile. - const image = await stage.hold(() => taken.image(views)); + // The page runs on as it is taken: a picture of the moment, not of where motion settles. + const shot = canvas.shot({ scale: scale(), transparent: transparent && alpha, capture: 'now' }); + const image = await shot.image(); try { const file = await image.encode(format, { quality }); const name = `views.${extension}`; @@ -84,17 +53,18 @@ export function exportSection(stage: Stage): void { write: (chunk) => void chunks.push(chunk.slice()), }); recording = new AbortController(); - const [taken, views] = shot([4, 2]); - const bitrate = bits && Math.round(taken.size[0] * taken.size[1] * FPS * bits); + const shot = canvas.shot({ scale: scale() }); + const bitrate = bits && Math.round(shot.size[0] * shot.size[1] * FPS * bits); const started = performance.now(); try { - const recorder = taken.video(stage.gpu, FPS, output, { + const recorder = shot.video({ + fps: FPS, + output, signal: recording.signal, ...(bitrate ? { bitrate } : {}), }); // Each frame waits for the page to reach its time, so the video runs as the page does. await recorder.record( - views, async (time) => { while (performance.now() - started < time * 1000) await nextFrame(); return time < SECONDS; diff --git a/npm/examples/src/ui/legend.ts b/npm/examples/src/ui/legend.ts index 0b0fcf5e..9ce93b17 100644 --- a/npm/examples/src/ui/legend.ts +++ b/npm/examples/src/ui/legend.ts @@ -1,4 +1,4 @@ -import { colormapCss, type Colormap } from '../latkit.js'; +import { colormapCss, type Colormap } from 'latkit'; import { h } from './dom.js'; import type { Stage } from './stage.js'; diff --git a/npm/examples/src/ui/menu.ts b/npm/examples/src/ui/menu.ts index 323e5285..355e3abc 100644 --- a/npm/examples/src/ui/menu.ts +++ b/npm/examples/src/ui/menu.ts @@ -29,9 +29,10 @@ export class Menu { /** Opens `items` at `point`, CSS pixels over `view`, kept inside the main view. */ open(view: View, point: [number, number], items: readonly Item[]): void { - const shown = this.stage.shown.find((s) => s.view === view); - if (!shown) return; - const { element: canvas, panel } = shown; + const canvas = this.stage.canvases.find((c) => c.layout.some((p) => p.view === view)); + const panel = canvas?.layout.find((p) => p.view === view)?.at ?? [0, 0, 1, 1]; + const element = canvas?.element; + if (!(element instanceof HTMLCanvasElement)) return; this.#element.replaceChildren( ...items.map(({ label, run, disabled }) => { const button = h('button', { type: 'button', role: 'menuitem', disabled }, label); @@ -45,8 +46,8 @@ export class Menu { this.#element.hidden = false; const main = this.stage.regions.main; const [width, height] = [this.#element.offsetWidth, this.#element.offsetHeight]; - const x = canvas.offsetLeft + panel[0] * canvas.clientWidth + point[0]; - const y = canvas.offsetTop + panel[1] * canvas.clientHeight + point[1]; + const x = element.offsetLeft + panel[0] * element.clientWidth + point[0]; + const y = element.offsetTop + panel[1] * element.clientHeight + point[1]; const left = Math.min(x, main.clientWidth - width - 8); const top = Math.min(y, main.clientHeight - height - 8); this.#element.style.left = `${Math.max(8, left)}px`; diff --git a/npm/examples/src/ui/playback.ts b/npm/examples/src/ui/playback.ts index 05b69327..5f6d1deb 100644 --- a/npm/examples/src/ui/playback.ts +++ b/npm/examples/src/ui/playback.ts @@ -12,6 +12,8 @@ export interface PlaybackOptions { replay?: { seconds: number; rate: number }; /** Called as playback goes live or leaves it. */ change?(live: boolean): void; + /** Coordinate `at` as the transport writes it; seconds unless given. */ + format?(at: number): string; } /** Live, paused, or playing a recorded span, with the transport that drives it. */ @@ -21,7 +23,7 @@ export class Playback { end = 0; #at = 0; #rate: number; - readonly #live: boolean; + #live: boolean; readonly #toggle = h('button', { type: 'button', class: 'primary' }); readonly #slider = h('input', { type: 'range', @@ -90,6 +92,19 @@ export class Playback { if (this.mode === 'playing') this.#rate = rate; } + /** Whether the data records as it plays, from now on: live follows its end; otherwise it pauses, + * and playing loops. */ + set live(live: boolean) { + this.#live = live; + this.#set(live ? 'live' : 'paused'); + } + + /** Shows coordinate `at`, paused. */ + seek(at: number): void { + this.#set('paused'); + this.#show(at); + } + /** The recorded span changed; live playback shows its end. */ record([start, end]: readonly [number, number]): void { this.start = start; @@ -135,7 +150,7 @@ export class Playback { #show(at: number): void { this.#at = at; this.#slider.value = String(at); - this.#time.value = `${at.toFixed(2)} s`; + this.#time.value = this.options.format?.(at) ?? `${at.toFixed(2)} s`; this.options.show(at); } } diff --git a/npm/examples/src/ui/rows.ts b/npm/examples/src/ui/rows.ts index ec4e1f89..5c550b8a 100644 --- a/npm/examples/src/ui/rows.ts +++ b/npm/examples/src/ui/rows.ts @@ -1,5 +1,5 @@ /** Sets of rows, as a view selects and tells them, and the rows in them. */ -import type { Row, Rows } from '../latkit.js'; +import type { Row, Rows } from 'latkit'; /** Each row of `sets`, in order. */ export const each = (sets: readonly Rows[]): Row[] => diff --git a/npm/examples/src/ui/stage.ts b/npm/examples/src/ui/stage.ts index 21a72737..68324328 100644 --- a/npm/examples/src/ui/stage.ts +++ b/npm/examples/src/ui/stage.ts @@ -1,4 +1,4 @@ -import type { Canvas, Gpu, Panel, Scene, View } from 'latkit'; +import type { Canvas, Gpu, Rect, Scene, View } from 'latkit'; import { Section } from './controls.js'; import { h } from './dom.js'; import { Legend } from './legend.js'; @@ -19,12 +19,8 @@ export class Stage { /** Aborted when the page closes: pass it to listeners the page adds itself. */ readonly signal = this.#abort.signal; readonly #views = new Set(); - readonly #canvases = new Set(); - /** Each view a canvas of the page shows, its element, and the panel of it the view fills, in the - * order they were made. */ - readonly shown: { view: View; element: HTMLCanvasElement; panel: Panel }[] = []; + readonly #canvases: Canvas[] = []; readonly #keys = new Map void>(); - #holding = 0; constructor( readonly gpu: Gpu, @@ -52,15 +48,20 @@ export class Stage { return [...this.#views]; } + /** Every canvas the page shows its views in, in the order they were made. */ + get canvases(): readonly Canvas[] { + return this.#canvases; + } + /** A view in a canvas of its own, in `into`, the main region unless given. */ view(scene: Scene, options: { label: string; into?: HTMLElement; height?: number }): View { return this.panels([scene], options)[0]!; } /** - * Views sharing one canvas in `into`, the main region unless given: each in its panel, its x, y, - * width, and height as fractions of the canvas, side by side unless given. Input goes to the view - * under it. + * Views sharing one canvas in `into`, the main region unless given: each `at` its x, y, width, and + * height as fractions of the canvas, side by side unless given, and on a narrow screen one above + * the other. Input goes to the view under it. */ panels( scenes: readonly Scene[], @@ -68,41 +69,26 @@ export class Stage { label, into = this.regions.main, height, - panels = scenes.map((_, i) => [i / scenes.length, 0, 1 / scenes.length, 1] as const), - }: { label: string; into?: HTMLElement; height?: number; panels?: readonly Panel[] }, + at = scenes.map((_, i): Rect => [i / scenes.length, 0, 1 / scenes.length, 1]), + }: { label: string; into?: HTMLElement; height?: number; at?: readonly Rect[] }, ): View[] { - const canvas = h('canvas', { tabindex: 0, 'aria-label': label }); - if (height) canvas.style.height = `${height}px`; - into.append(canvas); - const views = scenes.map((scene) => { - const view = this.gpu.view(scene); - // Leave room for the full font bounds above a plot's value-axis title. - if (typeof scene.space === 'object' && 'plot' in scene.space) view.insets = [12, 0, 0, 0]; - return view; - }); + const element = h('canvas', { tabindex: 0, 'aria-label': label }); + if (height) element.style.height = `${height}px`; + into.append(element); + const views = scenes.map((scene) => this.gpu.view(scene)); const narrow = matchMedia('(max-width: 700px)'); const layout = () => - narrow.matches && scenes.length > 1 - ? panels.map(([x, y, width, height]) => [y, x, height, width] as const) - : panels; - let current = layout(); - let surface = this.gpu.canvas(canvas, { panels: current }).show(views); - this.#canvases.add(surface); - const shown = views.map((view, i) => ({ view, element: canvas, panel: current[i]! })); - this.shown.push(...shown); - if (scenes.length > 1) - narrow.addEventListener( - 'change', - () => { - surface.destroy(); - this.#canvases.delete(surface); - current = layout(); - shown.forEach((item, i) => (item.panel = current[i]!)); - surface = this.gpu.canvas(canvas, { panels: current }).show(views); - this.#canvases.add(surface); - }, - { signal: this.signal }, - ); + views.map((view, i) => { + const [x, y, width, height] = at[i]!; + return { + view, + at: narrow.matches && views.length > 1 ? ([y, x, height, width] as const) : at[i], + }; + }); + const canvas = this.gpu.canvas(element, { layout: layout() }); + this.#canvases.push(canvas); + if (views.length > 1) + narrow.addEventListener('change', () => (canvas.layout = layout()), { signal: this.signal }); views.forEach((view) => { // What could not be fetched only goes undrawn; anything else stops the page. view.on('error', (error) => @@ -113,14 +99,12 @@ export class Stage { return views; } - /** Calls `step` every frame before the views draw, with the seconds since the last, at most 0.1; - * not while the page holds still. */ + /** Calls `step` every frame before the views draw, with the seconds since the last, at most 0.1. */ frame(step: (dt: number) => void): void { let last: number | undefined; const stop = this.gpu.on('frame', (now) => { - const dt = last === undefined || this.#holding ? 0 : Math.min(0.1, (now - last) / 1000); + const dt = last === undefined ? 0 : Math.min(0.1, (now - last) / 1000); last = now; - if (this.#holding) return; try { step(dt); } catch (error) { @@ -130,16 +114,6 @@ export class Stage { this.signal.addEventListener('abort', stop); } - /** Holds the page's frame steps still while `work` runs, as a picture of it waits for stillness. */ - async hold(work: () => Promise): Promise { - this.#holding++; - try { - return await work(); - } finally { - this.#holding--; - } - } - /** Runs `action` when `key` is pressed outside a text field. */ key(key: string, action: () => void): void { this.#keys.set(key.toLowerCase(), action); diff --git a/npm/examples/test/setup.ts b/npm/examples/test/setup.ts index 9695a8f2..0dbb989e 100644 --- a/npm/examples/test/setup.ts +++ b/npm/examples/test/setup.ts @@ -1,8 +1,9 @@ /** - * The page models' tests run in node, where fetch reads no files: the runtime's WebAssembly, which - * it fetches from beside its module, is read from disk instead. + * The page models' tests run in node, where fetch reads no files: latkit's WebAssembly, which it + * fetches from beside its module, is read from disk instead, and loaded before the tests make data. */ import { readFile } from 'node:fs/promises'; +import { load } from 'latkit'; const fetch = globalThis.fetch; globalThis.fetch = async (input, init) => { @@ -11,3 +12,4 @@ globalThis.fetch = async (input, init) => { const type = url.endsWith('.wasm') ? 'application/wasm' : 'application/octet-stream'; return new Response(await readFile(new URL(url)), { headers: { 'Content-Type': type } }); }; +await load(); diff --git a/npm/examples/tsconfig.json b/npm/examples/tsconfig.json index 6a0250c8..11d13109 100644 --- a/npm/examples/tsconfig.json +++ b/npm/examples/tsconfig.json @@ -9,5 +9,5 @@ "types": ["vite/client", "node"], "lib": ["ES2022", "DOM", "DOM.Iterable", "ESNext.Disposable"] }, - "include": ["src", "test", "vite.config.ts"] + "include": ["src", "scripts", "test", "vite.config.ts"] } diff --git a/npm/package.json b/npm/package.json index 8a42cbfb..c2c5559c 100644 --- a/npm/package.json +++ b/npm/package.json @@ -15,11 +15,7 @@ "default": "./dist/index.js" }, "./wasm": "./pkg/latkit_bg.wasm", - "./worker": "./dist/worker.js", - "./core": { - "types": "./dist/core.d.ts", - "default": "./dist/core.js" - } + "./worker": "./dist/worker.js" }, "files": [ "dist", diff --git a/npm/schema.json b/npm/schema.json index b7c466ea..f379e862 100644 --- a/npm/schema.json +++ b/npm/schema.json @@ -48,7 +48,7 @@ "required": [ "space" ], - "description": "What a view draws and how it answers input: a space, marks in drawing order, a layout, a theme,\ncontrols, how long changes ease, and the title written over it. Plain data: as JSON, each field by\nits name in camelCase, each mark `{ vertices | edges | faces | lines | areas: options }`.\n\n```\n# use latkit::{Colormap, Edges, Earth, Scene, Vertices, field};\nlet voltage = field(\"vm\").colormap(Colormap::BALANCE).domain(0.95..=1.05);\nlet scene = Scene::new(Earth::globe())\n .draw(Edges::of(\"grid\", \"Branch\").ends(\"from\", \"to\"))\n .draw(Vertices::of(\"grid\", \"Bus\").position(\"lon\", \"lat\").color(voltage).legend(\"Bus voltage · pu\"))\n .title(\"Voltage\");\n```" + "description": "What a view draws and how it answers input: a space, marks in drawing order, a layout, a theme,\ncontrols, how long changes ease, and the title written over it. Plain data: as JSON, each field by\nits name in camelCase, each mark `{ vertices | edges | faces | lines | areas: options }`.\n\n```\n# use latkit::{Colormap, Edges, Earth, Projection, Scene, Vertices, field};\nlet voltage = field(\"vm\").colormap(Colormap::BALANCE).domain(0.95..=1.05);\nlet scene = Scene::new(Earth::default().projection(Projection::Globe))\n .draw(Edges::of(\"grid\", \"Branch\").ends(\"from\", \"to\"))\n .draw(Vertices::of(\"grid\", \"Bus\").position(\"lon\", \"lat\").color(voltage).legend(\"Bus voltage · pu\"))\n .title(\"Voltage\");\n```" }, "Space": { "anyOf": [ @@ -57,15 +57,14 @@ "enum": [ "plane", "diagram", - "mercator", - "globe", + "earth", "plot" ] }, { "oneOf": [ { - "description": "Longitude and latitude in degrees, on a map or a globe.", + "description": "Longitude and latitude in degrees, on a flat map or a globe.", "type": "object", "properties": { "earth": { @@ -119,7 +118,7 @@ ] } ], - "description": "The coordinate space a scene's positions are in: a name, or `{ plane | diagram | earth | plot:\noptions }`. A diagram is a flat sheet seen from straight above: y runs down, and every length is a\nunit, a CSS pixel at zoom 0, except glow, halo, and pick radius. Only a diagram has ports, groups,\nnets, orthogonal routes, and layouts." + "description": "The coordinate space a scene's positions are in: a name, or `{ plane | diagram | earth | plot:\noptions }`. Its own unit is what lengths are in unless they say theirs: units in a diagram, a flat\nsheet seen from straight above with y running down, a unit a CSS pixel at zoom 0; CSS pixels\nelsewhere. Only a diagram has ports, groups, nets, and orthogonal routes." }, "Earth": { "type": "object", @@ -127,15 +126,7 @@ "properties": { "projection": { "$ref": "#/$defs/Projection", - "description": "A flat map or a globe." - }, - "surface": { - "description": "The ground's color; the theme's surface unless set.", - "tsType": "Color", - "type": [ - "string", - "null" - ] + "description": "A flat map or a globe; a flat map unless set." }, "graticule": { "type": "boolean", @@ -154,7 +145,7 @@ "description": "Whether air glows around the globe, in the theme's atmosphere color, brighter where it is day." } }, - "description": "The earth: positions are longitude and latitude in degrees." + "description": "The earth: positions are longitude and latitude in degrees, its ground in the theme's earth\ncolors." }, "Projection": { "oneOf": [ @@ -365,7 +356,7 @@ "description": "The grid under the marks." } }, - "description": "A sheet for diagrams, flat and seen from straight above: the camera pans and zooms. Positions are\nunits with y down, and every length is a unit, a CSS pixel at zoom 0 (sizes, padding, ports,\nstrokes, text, and the theme's), except glow, halo, and pick radius. Zooming scales one fixed\npicture. Only a diagram has ports, groups, nets, and orthogonal routes; it lays out blocks." + "description": "A sheet for diagrams, flat and seen from straight above: the camera pans and zooms. Positions are\nunits with y down, and lengths are units, a unit a CSS pixel at zoom 0 (sizes, padding, ports,\nstrokes, text, and the theme's), but for what says `px`, a halo, focus's glow, and the pick\nradius. Zooming scales one fixed picture. Only a diagram has ports, groups, nets, and orthogonal\nroutes; it lays out blocks." }, "Mark": { "oneOf": [ @@ -488,7 +479,7 @@ "type": "null" } ], - "description": "CSS pixels, or position units with `units`; units in a diagram." + "description": "Each row's radius, in the space's unit unless it says its own: units in a diagram, CSS pixels\nelsewhere." }, "size": { "type": [ @@ -500,7 +491,7 @@ }, "minItems": 2, "maxItems": 2, - "description": "Width and height, for markers that are not round: CSS pixels, or position units with `units`;\nunits in a diagram, where a block without a size fits its label and ports." + "description": "Width and height, for markers that are not round, in the space's unit unless they say their\nown; in a diagram a block's are units, and a block without a size fits its label and ports." }, "marker": { "$ref": "#/$defs/Marker", @@ -509,7 +500,7 @@ "ring": { "anyOf": [ { - "$ref": "#/$defs/Expr" + "$ref": "#/$defs/Paint" }, { "type": "null" @@ -560,7 +551,7 @@ "color": { "anyOf": [ { - "$ref": "#/$defs/Expr" + "$ref": "#/$defs/Paint" }, { "type": "null" @@ -643,7 +634,7 @@ "type": "boolean" }, { - "description": "A field's name, or a hex color when it starts with `#`.", + "description": "A field's name.", "type": "string" }, { @@ -688,7 +679,7 @@ "field": { "type": "string" }, - "of": { + "from": { "type": [ "array", "null" @@ -714,7 +705,7 @@ "required": [ "field" ], - "description": "A field of each row: of the mark's own table, or of another dataset's table joined by row id." + "description": "A field of each row: of the mark's own table, or from another dataset's table joined by row id." }, "ColorNode": { "type": "object", @@ -924,7 +915,7 @@ "required": [ "contains" ], - "description": "Whether a text field holds the text, ignoring case: read by queries, which hold texts." + "description": "Whether a text field holds the text, ignoring case: in a mark, a field of its own table." }, "ScaleNode": { "type": "object", @@ -1133,16 +1124,32 @@ "description": "How a colormap spans values." }, "Size": { - "description": "A size: CSS pixels, or position units with `units`.", + "description": "A size, in the space's unit unless it says its own: units in a diagram, CSS pixels elsewhere; `px` keeps CSS pixels, and `units` takes position units, which scale as the view zooms.", "anyOf": [ { "$ref": "#/$defs/Expr" }, + { + "$ref": "#/$defs/Px" + }, { "$ref": "#/$defs/Units" } ] }, + "Px": { + "type": "object", + "additionalProperties": false, + "properties": { + "px": { + "$ref": "#/$defs/Expr" + } + }, + "required": [ + "px" + ], + "description": "A size in CSS pixels, whatever the space's own unit." + }, "Units": { "type": "object", "additionalProperties": false, @@ -1172,7 +1179,7 @@ "required": [ "units" ], - "description": "A size in position units, so it scales as the view zooms, kept within CSS pixels when given." + "description": "A size in position units, whatever the space's own unit, so it scales as the view zooms, kept\nwithin CSS pixels when given." }, "Marker": { "anyOf": [ @@ -1416,6 +1423,63 @@ ], "description": "RGBA texels, straight alpha, sRGB, rows top to bottom." }, + "Paint": { + "$ref": "#/$defs/PaintNode", + "tsType": "PaintInput" + }, + "PaintNode": { + "description": "A color for every row of a mark: a CSS color, a value through a colormap, or a conditional choosing between colors.", + "anyOf": [ + { + "description": "A CSS color, such as `#ffb000` or `rebeccapurple`.", + "type": "string", + "tsType": "Color" + }, + { + "$ref": "#/$defs/ColorNode" + }, + { + "$ref": "#/$defs/ColormapNode" + }, + { + "$ref": "#/$defs/WhenPaintNode" + }, + { + "$ref": "#/$defs/MissingNode" + } + ] + }, + "WhenPaintNode": { + "type": "object", + "additionalProperties": false, + "properties": { + "when": { + "type": "array", + "items": { + "type": "array", + "prefixItems": [ + { + "$ref": "#/$defs/Expr" + }, + { + "$ref": "#/$defs/Paint" + } + ], + "minItems": 2, + "maxItems": 2 + }, + "tsType": "[condition: ExprInput, color: PaintInput][]" + }, + "otherwise": { + "$ref": "#/$defs/Paint" + } + }, + "required": [ + "when", + "otherwise" + ], + "description": "A conditional's color: the first case's whose condition holds, else `otherwise`'s." + }, "Port": { "anyOf": [ { @@ -1506,7 +1570,7 @@ "$ref": "#/$defs/Expr" }, "size": { - "description": "CSS pixels, or units in a diagram; the theme's font size unless set.", + "description": "In the space's unit: units in a diagram, CSS pixels elsewhere; the theme's text size unless\nset.", "type": [ "number", "null" @@ -1517,7 +1581,7 @@ "description": "Each row's text color; the theme's text color unless set.", "anyOf": [ { - "$ref": "#/$defs/Expr" + "$ref": "#/$defs/Paint" }, { "type": "null" @@ -1536,7 +1600,7 @@ ] }, "maxWidth": { - "description": "How far a line runs before it wraps: CSS pixels, or units in a diagram.", + "description": "How far a line runs before it wraps, in the space's unit: units in a diagram, CSS pixels\nelsewhere.", "type": [ "number", "null" @@ -1657,7 +1721,7 @@ "type": "null" } ], - "description": "CSS pixels, or position units with `units`; units in a diagram." + "description": "Each row's width, in the space's unit unless it says its own: units in a diagram, CSS\npixels elsewhere." }, "dash": { "$ref": "#/$defs/Expr", @@ -1665,12 +1729,12 @@ }, "flow": { "$ref": "#/$defs/Expr", - "description": "Comets flowing along each row at this speed a second: CSS pixels, units in a diagram,\nor position units where the theme's flow is in units; negative flows backward. A speed\nthat changes carries comets on from where they are." + "description": "Comets flowing along each row at this speed a second, in the lengths of the theme's\nflow; negative flows backward. A speed that changes carries comets on from where they\nare." }, "color": { "anyOf": [ { - "$ref": "#/$defs/Expr" + "$ref": "#/$defs/Paint" }, { "type": "null" @@ -1794,7 +1858,7 @@ "color": { "anyOf": [ { - "$ref": "#/$defs/Expr" + "$ref": "#/$defs/Paint" }, { "type": "null" @@ -1909,7 +1973,7 @@ "type": "null" } ], - "description": "CSS pixels, or position units with `units`; units in a diagram." + "description": "Each row's width, in the space's unit unless it says its own: units in a diagram, CSS\npixels elsewhere." }, "dash": { "$ref": "#/$defs/Expr", @@ -1917,12 +1981,12 @@ }, "flow": { "$ref": "#/$defs/Expr", - "description": "Comets flowing along each row at this speed a second: CSS pixels, units in a diagram,\nor position units where the theme's flow is in units; negative flows backward. A speed\nthat changes carries comets on from where they are." + "description": "Comets flowing along each row at this speed a second, in the lengths of the theme's\nflow; negative flows backward. A speed that changes carries comets on from where they\nare." }, "color": { "anyOf": [ { - "$ref": "#/$defs/Expr" + "$ref": "#/$defs/Paint" }, { "type": "null" @@ -2040,7 +2104,7 @@ "color": { "anyOf": [ { - "$ref": "#/$defs/Expr" + "$ref": "#/$defs/Paint" }, { "type": "null" @@ -2228,181 +2292,65 @@ "type": "string", "tsType": "Color" }, - "surface": { - "description": "The earth's ground, and a group's container.", - "type": "string", - "tsType": "Color" - }, "grid": { - "description": "A plane's or a diagram's grid.", - "type": "string", - "tsType": "Color" - }, - "vertex": { - "description": "Vertex color.", + "description": "A plane's or a diagram's grid, and the earth's graticule.", "type": "string", "tsType": "Color" }, - "vertexRadius": { - "description": "Vertex radius.", - "type": "number", - "format": "float" - }, - "edge": { - "description": "Edge color; `None` blends the colors of each edge's two vertices, and in a diagram takes the\ntext color.", - "type": [ - "string", - "null" - ], - "tsType": "Color" - }, - "edgeWidth": { - "description": "Edge width.", - "type": "number", - "format": "float" - }, - "line": { - "description": "Line color.", + "missing": { + "description": "What a missing color reads as.", "type": "string", "tsType": "Color" }, - "lineWidth": { - "description": "Line width.", + "dash": { + "description": "Length of a dash.", "type": "number", "format": "float" }, - "area": { - "description": "Area color.", - "type": "string", - "tsType": "Color" - }, "shading": { "description": "How much a face turned away from the camera darkens, 0 to 1.", "type": "number", "format": "float" }, "text": { - "description": "Text: labels, axis ticks, and titles.", - "type": "string", - "tsType": "Color" - }, - "font": { - "description": "The CSS font family text is set in.", - "type": "string" + "description": "Labels, axis ticks, and titles.", + "$ref": "#/$defs/TextTheme" }, - "fontSize": { - "description": "Text size.", - "type": "number", - "format": "float" - }, - "playhead": { - "description": "The line a plot draws at the shown coordinate.", - "type": "string", - "tsType": "Color" - }, - "missing": { - "description": "What a missing color reads as.", - "type": "string", - "tsType": "Color" - }, - "hover": { - "description": "The glow around hovered rows.", - "type": "string", - "tsType": "Color" - }, - "selected": { - "description": "The glow around selected rows; `None` glows in each row's own color.", - "type": [ - "string", - "null" - ], - "tsType": "Color" + "vertex": { + "description": "Vertices where their options say nothing.", + "$ref": "#/$defs/VertexTheme" }, - "glow": { - "description": "How far focus glows reach, CSS pixels.", - "type": "number", - "format": "float" + "edge": { + "description": "Edges where their options say nothing, their lanes, and their arrowheads.", + "$ref": "#/$defs/EdgeTheme" }, - "dim": { - "description": "The opacity of rows outside a selection.", - "type": "number", - "format": "float" + "line": { + "description": "Lines where their options say nothing.", + "$ref": "#/$defs/LineTheme" }, - "hoverScale": { - "description": "How much a hovered vertex sized in CSS pixels grows.", - "type": "number", - "format": "float" + "area": { + "description": "Areas where their options say nothing.", + "$ref": "#/$defs/AreaTheme" }, - "dash": { - "description": "Length of a dash.", - "type": "number", - "format": "float" + "focus": { + "description": "How hovered and selected rows glow, and rows outside a selection dim.", + "$ref": "#/$defs/FocusTheme" }, "flow": { "description": "How comets flowing along edges and lines look.", - "$ref": "#/$defs/Flow" - }, - "edgeSpacing": { - "description": "In a diagram, distance between the lanes of edges joining the same two vertices, and between\nparallel orthogonal wires, which keep twice it apart.", - "type": "number", - "format": "float" - }, - "laneBow": { - "description": "Elsewhere, how far edges joining the same two vertices bow apart: each lane by this fraction\nof the edge's length at its middle, as wide as eight lanes at most, more packing closer. A bow\nis part of the drawing, so zooming never moves it.", - "type": "number", - "format": "float" - }, - "arrow": { - "description": "How far an arrowhead reaches past its edge's width.", - "type": "number", - "format": "float" - }, - "halo": { - "description": "CSS pixels of background around label text.", - "type": "number", - "format": "float" - }, - "groupPadding": { - "description": "Distance between a group's members and its outline.", - "type": "number", - "format": "float" - }, - "outline": { - "description": "The border of a diagram's blocks and groups, a unit wide; `None` draws none.", - "type": [ - "string", - "null" - ], - "tsType": "Color" + "$ref": "#/$defs/FlowTheme" }, - "corner": { - "description": "The radius of a diagram's block and group corners; a block's marker may set its own.", - "type": "number", - "format": "float" + "diagram": { + "description": "How a diagram's blocks, groups, ports, and wires look.", + "$ref": "#/$defs/DiagramTheme" }, - "portSize": { - "description": "How wide a diagram's ports are.", - "type": "number", - "format": "float" - }, - "bend": { - "description": "The radius of an orthogonal wire's bends.", - "type": "number", - "format": "float" - }, - "shadow": { - "description": "How a diagram's blocks and groups drop shadows onto its sheet.", - "$ref": "#/$defs/Shadow" - }, - "atmosphere": { - "description": "The air glowing around the globe, where the earth has an atmosphere.", - "type": "string", - "tsType": "Color" + "earth": { + "description": "How the earth looks.", + "$ref": "#/$defs/EarthTheme" }, - "night": { - "description": "How bright the earth's night side stays where it is lit by daylight, 0 to 1.", - "type": "number", - "format": "float" + "plot": { + "description": "How a plot looks.", + "$ref": "#/$defs/PlotTheme" }, "base": { "description": "The theme these options change; dark unless set.", @@ -2415,40 +2363,254 @@ "additionalProperties": false } ], - "description": "Colors and sizes marks take where their options read nothing, and the look of focus and motion: a\nbase theme's name, or options over one (`base`, dark unless set). Lengths are CSS pixels; in a\ndiagram they are units, except `glow` and `halo`." + "description": "Colors and sizes marks take where their options read nothing, and the look of text, focus,\nmotion, and each space, grouped by what they style: a base theme's name, or options over one\n(`base`, dark unless set), each group's over the base's. Lengths are in the space's unit, units in a\ndiagram and CSS pixels elsewhere, but for a text's halo and focus's glow, CSS pixels everywhere." }, - "Flow": { + "TextTheme": { "type": "object", "additionalProperties": false, "properties": { "color": { - "description": "A comet's color; its alpha is how strongly a comet covers its stroke.", + "description": "Text's color.", + "tsType": "Color", + "type": "string" + }, + "font": { + "type": "string", + "description": "The CSS font family text is set in." + }, + "size": { + "type": "number", + "format": "float", + "description": "Text's size." + }, + "halo": { + "type": "number", + "format": "float", + "description": "CSS pixels of background around label text." + } + }, + "description": "Labels, axis ticks, and titles." + }, + "VertexTheme": { + "type": "object", + "additionalProperties": false, + "properties": { + "color": { + "description": "A vertex's color.", "tsType": "Color", "type": "string" }, + "radius": { + "type": "number", + "format": "float", + "description": "A vertex's radius." + } + }, + "description": "Vertices where their options say nothing." + }, + "EdgeTheme": { + "type": "object", + "additionalProperties": false, + "properties": { + "color": { + "description": "An edge's color; `None` blends the colors of each edge's two vertices, and in a diagram takes\nthe text color.", + "tsType": "Color", + "type": [ + "string", + "null" + ] + }, + "width": { + "type": "number", + "format": "float", + "description": "An edge's width." + }, "spacing": { "type": "number", "format": "float", - "description": "Distance between comets." + "description": "In a diagram, distance between the lanes of edges joining the same two vertices, and between\nparallel orthogonal wires, which keep twice it apart." }, - "length": { + "bow": { + "type": "number", + "format": "float", + "description": "Elsewhere, how far edges joining the same two vertices bow apart: each lane by this fraction\nof the edge's length at its middle, as wide as eight lanes at most, more packing closer. A bow\nis part of the drawing, so zooming never moves it." + }, + "arrow": { + "type": "number", + "format": "float", + "description": "How far an arrowhead reaches past its edge's width." + } + }, + "description": "Edges where their options say nothing, their lanes, and their arrowheads." + }, + "LineTheme": { + "type": "object", + "additionalProperties": false, + "properties": { + "color": { + "description": "A line's color.", + "tsType": "Color", + "type": "string" + }, + "width": { + "type": "number", + "format": "float", + "description": "A line's width." + } + }, + "description": "Lines where their options say nothing." + }, + "AreaTheme": { + "type": "object", + "additionalProperties": false, + "properties": { + "color": { + "description": "An area's color.", + "tsType": "Color", + "type": "string" + } + }, + "description": "Areas where their options say nothing." + }, + "FocusTheme": { + "type": "object", + "additionalProperties": false, + "properties": { + "hover": { + "description": "The glow around hovered rows.", + "tsType": "Color", + "type": "string" + }, + "selected": { + "description": "The glow around selected rows; `None` glows in each row's own color.", + "tsType": "Color", + "type": [ + "string", + "null" + ] + }, + "glow": { + "type": "number", + "format": "float", + "description": "How far focus glows reach, CSS pixels." + }, + "dim": { + "type": "number", + "format": "float", + "description": "The opacity of rows outside a selection." + }, + "grow": { "type": "number", "format": "float", + "description": "How much a hovered vertex sized in CSS pixels grows." + } + }, + "description": "How hovered and selected rows glow, and rows outside a selection dim." + }, + "FlowTheme": { + "type": "object", + "additionalProperties": false, + "properties": { + "color": { + "description": "A comet's color; its alpha is how strongly a comet covers its stroke.", + "tsType": "Color", + "type": "string" + }, + "spacing": { + "$ref": "#/$defs/Length", + "description": "Distance between comets." + }, + "length": { + "$ref": "#/$defs/Length", "description": "How far a comet's tail trails its head." }, "width": { "type": "number", "format": "float", - "description": "How wide a comet's head is, CSS pixels, or units in a diagram; never narrower than its stroke." + "description": "How wide a comet's head is; never narrower than its stroke." + } + }, + "description": "How comets flowing along edges and lines look: each a round head, brightest at its middle, trailing\na tail that thins and fades. Their spacing and length are in one unit, and each row's flow is in it\ntoo: in position units, degrees on the earth, comets are part of the drawing, zooming with the\nview and staying where they are on their lines; otherwise they keep their size and speed as it\nzooms." + }, + "Length": { + "description": "A length: a number, in the space's unit, units in a diagram and CSS pixels elsewhere; `{ px }`, CSS pixels; or `{ units }`, position units.", + "anyOf": [ + { + "type": "number" }, - "units": { - "type": "boolean", - "description": "Whether comets are part of the drawing: their spacing, length, and each row's flow in position\nunits, degrees on the earth, so they zoom with the view and stay where they are on their lines.\nOtherwise they keep their size and speed on screen, and slide along their lines as it zooms." + { + "type": "object", + "properties": { + "px": { + "type": "number", + "description": "CSS pixels, however the view zooms." + } + }, + "required": [ + "px" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "units": { + "type": "number", + "description": "Position units, which scale as the view zooms." + } + }, + "required": [ + "units" + ], + "additionalProperties": false + } + ] + }, + "DiagramTheme": { + "type": "object", + "additionalProperties": false, + "properties": { + "group": { + "description": "What a group's container is filled with.", + "tsType": "Color", + "type": "string" + }, + "outline": { + "description": "The border of blocks and groups, a unit wide; `None` draws none.", + "tsType": "Color", + "type": [ + "string", + "null" + ] + }, + "corner": { + "type": "number", + "format": "float", + "description": "The radius of block and group corners; a block's marker may set its own." + }, + "portSize": { + "type": "number", + "format": "float", + "description": "How wide ports are." + }, + "bend": { + "type": "number", + "format": "float", + "description": "The radius of an orthogonal wire's bends." + }, + "groupPadding": { + "type": "number", + "format": "float", + "description": "Distance between a group's members and its outline." + }, + "shadow": { + "$ref": "#/$defs/ShadowTheme", + "description": "How blocks and groups drop shadows onto the sheet." } }, - "description": "How comets flowing along edges and lines look: each a round head, brightest at its middle, trailing\na tail that thins and fades." + "description": "How a diagram's blocks, groups, ports, and wires look." }, - "Shadow": { + "ShadowTheme": { "type": "object", "additionalProperties": false, "properties": { @@ -2465,6 +2627,40 @@ }, "description": "How a diagram's blocks and groups drop shadows onto its sheet." }, + "EarthTheme": { + "type": "object", + "additionalProperties": false, + "properties": { + "ground": { + "description": "The ground's color.", + "tsType": "Color", + "type": "string" + }, + "atmosphere": { + "description": "The air glowing around the globe, where the earth has an atmosphere.", + "tsType": "Color", + "type": "string" + }, + "night": { + "type": "number", + "format": "float", + "description": "How bright the night side stays where daylight lights the earth, 0 to 1." + } + }, + "description": "How the earth looks." + }, + "PlotTheme": { + "type": "object", + "additionalProperties": false, + "properties": { + "playhead": { + "description": "The line drawn at the shown coordinate.", + "tsType": "Color", + "type": "string" + } + }, + "description": "How a plot looks." + }, "Controls": { "anyOf": [ { @@ -3171,11 +3367,14 @@ "description": "The most rows read; a limit picks them in linear time before sorting only those." }, "select": { - "type": "object", + "type": [ + "object", + "null" + ], "additionalProperties": { "$ref": "#/$defs/Expr" }, - "description": "Fields to read by name: a field as it is, anything else as numbers. Every field unless set." + "description": "Fields to read by name: a field as it is, anything else as numbers. Every field unless set;\nnone set reads the ids alone." }, "at": { "type": [ @@ -3206,6 +3405,54 @@ ], "description": "Which way a sort runs; rows a key reads nothing for come last either way." }, + "EnvelopeRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "table": { + "type": "string", + "description": "The table read." + }, + "field": { + "type": "string", + "description": "The sampled field read." + }, + "range": { + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "minItems": 2, + "maxItems": 2, + "description": "The axis coordinates the spans cover, from the first to the last." + }, + "spans": { + "type": "integer", + "format": "uint", + "minimum": 0, + "description": "How many equal spans the range is cut into." + }, + "ids": { + "anyOf": [ + { + "$ref": "#/$defs/Ids" + }, + { + "type": "null" + } + ], + "description": "The rows read, in this order; every row unless set." + } + }, + "required": [ + "table", + "field", + "range", + "spans" + ], + "description": "Rows' least and greatest values of a sampled field over equal spans of the axis, to read back:\nfield `field` of table `table` over `spans` equal spans from `range[0]` to `range[1]`, of every\nrow or of the rows `ids` names, in its order. Plain data, like a query." + }, "Camera": { "anyOf": [ { @@ -3473,10 +3720,9 @@ "type": "boolean", "description": "Whether alpha is kept where the themes' backgrounds would be." }, - "deadline": { - "type": "number", - "format": "double", - "description": "Seconds a picture, or a video frame, may take; 30 unless set." + "capture": { + "$ref": "#/$defs/Capture", + "description": "When the picture is taken: once its views are complete and still, or as they are now." }, "elapsed": { "type": [ @@ -3490,7 +3736,7 @@ "required": [ "size" ], - "description": "A picture of views, each drawn into its panel of `size` pixels at `scale` pixels per CSS pixel:\nplain data, like a scene.\n\n```no_run\n# async fn example(view: &mut latkit::View) -> latkit::Result<()> {\nuse latkit::Shot;\nlet image = Shot::new([3840, 2160]).scale(2.0).image(&mut [view]).await?;\nstd::fs::write(\"view.png\", image.encode(latkit::ImageFormat::Png)?).expect(\"written\");\n# Ok(()) }\n```" + "description": "A picture of views, each drawn into its panel of `size` pixels at `scale` pixels per CSS pixel,\nonce they are complete as its capture says: plain data, like a scene.\n\n```no_run\n# async fn example(view: &mut latkit::View) -> latkit::Result<()> {\nuse latkit::Shot;\nlet image = Shot::new([3840, 2160]).scale(2.0).image(&mut [view]).await?;\nstd::fs::write(\"view.png\", image.encode(latkit::ImageFormat::Png)?).expect(\"written\");\n# Ok(()) }\n```" }, "Panels": { "type": "array", @@ -3504,6 +3750,21 @@ "maxItems": 4 }, "description": "Where each of several views draws in a target: x, y, width, and height as fractions of it, a\npanel per view, in order; a later panel draws over an earlier one. None given is one view filling\nthe target.\n\n```\nuse latkit::Panels;\nlet map_over_plot = Panels::from([[0.0, 0.0, 1.0, 0.8], [0.0, 0.8, 1.0, 0.2]]);\nassert_eq!(map_over_plot.len(), 2);\nassert_eq!(Panels::default().len(), 1);\n```" + }, + "Capture": { + "oneOf": [ + { + "type": "string", + "const": "still", + "description": "Once every view is complete and still: transitions, flights, and label fades run on, a\nsixtieth of a second a step, until they land." + }, + { + "type": "string", + "const": "now", + "description": "Once every view is complete, as it stands now: what moves holds where it is." + } + ], + "description": "When a picture is taken." } }, "outputs": { @@ -3746,7 +4007,7 @@ "properties": { "type": { "type": "string", - "const": "hover" + "const": "hovered" }, "detail": { "type": "object", @@ -3784,7 +4045,7 @@ "properties": { "type": { "type": "string", - "const": "select" + "const": "selection" }, "detail": { "type": "object", @@ -3852,7 +4113,7 @@ "type", "detail" ], - "description": "What the last frame stood in for changed; nothing once it is complete:\n[`View::complete`](crate::View::complete)." + "description": "What the last frame stood in for changed; nothing once it is complete:\n[`View::waiting`](crate::View::waiting)." }, { "type": "object", @@ -3872,7 +4133,7 @@ "description": "Something failed, told once. A mark its data no longer fits also lists in\n[`View::failures`](crate::View::failures)." } ], - "description": "What happened in a view, for the application to act on. Drain them with\n[`View::events`](crate::View::events)." + "description": "What happened in a view, for the application to act on, as [`View::events`](crate::View::events)\ntells it." }, "Hit": { "type": "object", @@ -4324,10 +4585,6 @@ "minimum": 0, "description": "Tiles being fetched or decoded, of every tile source." }, - "waiting": { - "$ref": "#/outputs/Waiting", - "description": "The work under way the last frame stood in for; nothing when it drew everything as the data,\nscene, and camera say: see [`View::complete`](crate::View::complete)." - }, "cpuMs": { "type": "number", "format": "float", @@ -4347,7 +4604,6 @@ "instances", "tiles", "tilesLoading", - "waiting", "cpuMs", "gpuMs" ], @@ -4374,6 +4630,18 @@ "minimum": 0, "description": "Rows whose channels the GPU evaluated." }, + "resolved": { + "type": "integer", + "format": "uint64", + "minimum": 0, + "description": "Rows of edges and faces whose ends the GPU found where their vertices are held: rows that\nchanged, and every row held where the vertices they end at moved." + }, + "cleared": { + "type": "integer", + "format": "uint64", + "minimum": 0, + "description": "Bytes of pages of words per row the GPU cleared: those read before anything writes them, and\nthe normals faces sum afresh." + }, "packed": { "type": "integer", "format": "uint64", @@ -4415,6 +4683,8 @@ "uploaded", "written", "evaluated", + "resolved", + "cleared", "packed", "readbacks", "jobs", diff --git a/npm/schema.mjs b/npm/schema.mjs index 06138c9c..3c75edf2 100644 --- a/npm/schema.mjs +++ b/npm/schema.mjs @@ -9,7 +9,7 @@ import path from 'node:path'; import prettier from 'prettier'; /** The types npm takes as plain data; everything they are built of comes with them. */ -const INPUTS = ['Scene', 'Query', 'Camera', 'Fit', 'Budget', 'ColormapKind']; +const INPUTS = ['Scene', 'Query', 'EnvelopeRequest', 'Camera', 'Fit', 'Budget', 'ColormapKind']; /** The types npm reads back. */ const OUTPUTS = [ @@ -25,7 +25,7 @@ const OUTPUTS = [ ]; /** The hand-written types a `tsType` names, and the module that declares them. */ -const IMPORTS = { './scene.js': ['Color', 'ExprInput'] }; +const IMPORTS = { './scene.js': ['Color', 'ExprInput', 'PaintInput'] }; /** `inputs.ts` and `outputs.ts` of `schema.json` at `file`, formatted as the repository's sources are. */ export async function typescript(file) { @@ -181,10 +181,32 @@ function declare(defs, prefix, roots, declared) { return { names, source, borrowed: [...reached].filter((n) => declared.has(n) && used(n)) }; } +/** Rust's names npm calls otherwise. */ +const RENAMED = { 'View::events': 'view.on' }; + +/** + * A code span of Rust's documentation as npm names what it names: an event as the event npm tells + * (`Event::Connect` a `'connect'` event), a member as one of an instance where it starts lowercase + * (`View::complete` `view.complete`, `Controls::connect` `controls.connect`) and of its type else, a + * call as what it calls (`hovered()` `hovered`). + */ +function named(code) { + const camel = (name) => name.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); + const lower = (name) => name[0].toLowerCase() + name.slice(1); + if (RENAMED[code]) return `\`${RENAMED[code]}\``; + const event = code.match(/^Event::(\w+)$/); + if (event) return `a \`'${lower(event[1])}'\` event`; + const span = code + .replace(/\b([A-Z]\w*)::([a-z]\w*)/g, (_, owner, member) => `${lower(owner)}.${camel(member)}`) + .replace(/\b([A-Z]\w*)::(\w+)/g, '$1.$2') + .replace(/\(\)/g, ''); + return `\`${span}\``; +} + /** * Documentation as the forms write it, wrapped anew: paragraphs kept, lines at most 96 wide, Rust's - * links to its own items as plain code, its `None` as the `null` JSON writes, and its examples, - * which are Rust, left out. + * links to its own items as plain code, its names as npm's, its `None` as the `null` JSON writes, + * and its examples, which are Rust, left out. */ function doc(text) { const lines = []; @@ -192,7 +214,8 @@ function doc(text) { .replace(/```[\s\S]*?```/g, '') .replace(/\*\//g, '*\\/') .replace(/\[(`[^`]*`)\]\([^)]*\)/g, '$1') - .replaceAll('`None`', '`null`'); + .replaceAll('`None`', '`null`') + .replace(/`([^`]*)`/g, (_, code) => named(code)); for (const paragraph of plain.split('\n\n').filter((p) => p.trim())) { if (lines.length) lines.push(''); let line = ''; diff --git a/npm/src/canvas.ts b/npm/src/canvas.ts new file mode 100644 index 00000000..d929d818 --- /dev/null +++ b/npm/src/canvas.ts @@ -0,0 +1,129 @@ +/** Canvases: views shown in a `` or an `OffscreenCanvas`, as a layout places them. */ + +import type * as bindings from '../pkg/latkit.js'; +import { hold, inner, release } from './internal.js'; +import { Shot, type ShotOptions } from './shot.js'; +import { handleOf, type View } from './view.js'; + +/** `[x, y, width, height]` as fractions of a canvas or a picture. */ +export type Rect = readonly [x: number, y: number, width: number, height: number]; + +/** A view's place in a canvas or a picture: where it draws, all of it unless `at` says. */ +export interface Panel { + view: View; + at?: Rect; +} + +/** How a canvas shows its views. */ +export interface CanvasOptions { + /** The views shown and where; a later panel draws over an earlier one. */ + layout?: readonly Panel[]; + /** Pixels a CSS pixel of an `OffscreenCanvas`; an element follows its device. */ + pixelRatio?: number; +} + +/** The whole of a canvas or a picture. */ +const WHOLE: Rect = [0, 0, 1, 1]; + +/** + * Views shown in a `` element, which sizes itself to its CSS box and passes each view the + * input landing on its panel, or an `OffscreenCanvas` in a worker, which keeps the size it is given. + */ +export class Canvas { + #target: HTMLCanvasElement | OffscreenCanvas; + #pixelRatio: number; + #layout: readonly Panel[] = []; + #released: () => void; + + private constructor( + device: bindings.Gpu, + target: HTMLCanvasElement | OffscreenCanvas, + options: CanvasOptions, + released: () => void, + ) { + const layout = options.layout ?? []; + hold( + this, + device.canvas( + target, + layout.map(({ view }) => handleOf(view)), + placed(layout), + options.pixelRatio, + ), + ); + this.#target = target; + this.#pixelRatio = options.pixelRatio ?? 1; + this.#layout = [...layout]; + this.#released = released; + } + + /** @internal A canvas of `device` in `target`, which calls `released` once destroyed. */ + static made( + device: bindings.Gpu, + target: HTMLCanvasElement | OffscreenCanvas, + options: CanvasOptions, + released: () => void, + ): Canvas { + return new Canvas(device, target, options, released); + } + + /** What it draws into. */ + get element(): HTMLCanvasElement | OffscreenCanvas { + return this.#target; + } + + /** + * The views shown and where, a later panel over an earlier one; setting it shows those in place of + * the ones shown. A view shows in one panel of one canvas at a time. + */ + get layout(): readonly Panel[] { + return this.#layout; + } + + set layout(layout: readonly Panel[]) { + canvas(this).setLayout( + layout.map(({ view }) => handleOf(view)), + placed(layout), + ); + this.#layout = [...layout]; + } + + /** + * A picture of exactly what the canvas shows: its views as its layout places them, at its size in + * CSS pixels times `scale`. + */ + shot(options: ShotOptions = {}): Shot { + const scale = options.scale ?? 1; + const target = this.#target; + const [width, height] = + 'clientWidth' in target + ? [target.clientWidth, target.clientHeight] + : [target.width / this.#pixelRatio, target.height / this.#pixelRatio]; + const size = [ + Math.max(1, Math.round(width * scale)), + Math.max(1, Math.round(height * scale)), + ] as const; + return new Shot(size, { ...options, layout: this.#layout }); + } + + /** Stops drawing; the views remain, and the element keeps its last frame. */ + destroy(): void { + const freed = release(this); + if (!freed) return; + freed.free(); + this.#released(); + } + + [Symbol.dispose](): void { + this.destroy(); + } +} + +function canvas(owner: Canvas): bindings.Canvas { + return inner(owner, 'Canvas'); +} + +/** @internal Where each panel of `layout` draws, four fractions after another. */ +export function placed(layout: readonly Panel[]): Float32Array { + return Float32Array.from(layout.flatMap(({ at }) => at ?? WHOLE)); +} diff --git a/npm/src/colormap.ts b/npm/src/colormap.ts new file mode 100644 index 00000000..b1b4156f --- /dev/null +++ b/npm/src/colormap.ts @@ -0,0 +1,12 @@ +/** Colormaps in the page: the colors the GPU draws, as CSS, for legends. */ + +import type { Colormap } from './inputs.js'; +import { wasm } from './internal.js'; + +/** + * A CSS gradient through `map`, a catalog name or colors of your own, toward `direction`: the colors + * the GPU draws, a band each for a categorical map's categories. + */ +export function colormapCss(map: Colormap, direction = 'to right'): string { + return wasm().colormapCss(JSON.stringify(map), direction); +} diff --git a/npm/src/core.ts b/npm/src/core.ts deleted file mode 100644 index a24e2721..00000000 --- a/npm/src/core.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** Plain data and builders for Node, browsers, and workers; no DOM or WebAssembly types required. */ -export type * from './inputs.js'; -export type * from './outputs.js'; -export * from './patch.js'; -export * from './scene.js'; diff --git a/npm/src/data.ts b/npm/src/data.ts new file mode 100644 index 00000000..f381ddbb --- /dev/null +++ b/npm/src/data.ts @@ -0,0 +1,256 @@ +/** Datasets: a `Data` changing in place, a `Snapshot` of one version, and `Tiles` fetched in pieces. */ + +import type * as bindings from '../pkg/latkit.js'; +import type { EnvelopeRequest, Id, Query } from './inputs.js'; +import { hold, inner, release, wasm, waited, wrap, type Wait } from './internal.js'; +import { load } from './load.js'; +import type { Description, Value } from './outputs.js'; +import { framed, type Patch } from './patch.js'; + +/** + * The rows a query keeps, in the forms a patch writes: numbers as a `Float64Array` (missing NaN), + * other values as arrays (missing `null`), a reference as its target's ids. + */ +export interface QueryResult { + table: string; + ids: Id[]; + fields: Record; + /** The rows the query kept before paging. */ + total: number; +} + +/** Rows' least and greatest values over spans of the axis: `at` holds one more edge than spans. */ +export interface Envelope { + at: Float64Array; + least: Float32Array; + greatest: Float32Array; +} + +/** What `Data.read` reads: latkit's bytes, GeoJSON, or a dataset's JSON document. */ +export type DataSource = + Uint8Array | ArrayBuffer | Blob | Response | ReadableStream | string | object; + +/** What reads a version of a dataset, as the bindings' `Data` and `Snapshot` both do. */ +type Reads = bindings.Data | bindings.Snapshot; + +/** + * A dataset that changes in place: named tables of columns, changed only by patches, every change + * of one or none. A view given it draws its latest version, from the frame after a patch applies. A + * snapshot keeps one version, sharing every page with it, so earlier versions kept, as an undo + * history does, cost only the pages they hold apart. + * + * ```ts + * const grid = new Data('grid').apply(new Patch().replace('Bus', [1, 2], { kv: [230, 115] })); + * view.sources = [grid]; + * grid.apply(new Patch().set('Bus', [2], { kv: [69] })); // drawn next frame + * ``` + */ +export class Data { + /** An empty dataset named `name`. */ + constructor(name: string) { + hold(this, new (wasm().Data)(name)); + } + + /** + * The dataset `source` holds, as its first bytes say: latkit's bytes; GeoJSON, gzip-compressed or + * not, read in a worker, its points, line strings, and polygons rows of tables `points`, `lines`, + * and `polygons`, each with its geometry as field `geometry` and its properties as fields; or its + * JSON document, `{"name": ..., "patch": [...]}`, its patch read as `Patch.read` reads one. `source` + * is bytes, a `Blob`, a `Response`, a stream, a URL to fetch, or a document as a JSON value. `name` + * names it, as GeoJSON needs; `base` is what a document's slices of files are relative to, by + * default the URL it was fetched from. + */ + static async read( + source: DataSource, + options: { name?: string; base?: string } & Wait = {}, + ): Promise { + await load(); + const { bytes, base } = await bytesOf(source, options); + const read = wasm().Data.read(bytes, options.name, options.base ?? base); + return wrap(Data, await waited(read, options, 'reading a dataset')); + } + + get name(): string { + return reads(this).name; + } + + /** Applies every change of `patch`, or none when one is invalid. */ + apply(patch: Patch | Uint8Array): this { + inner(this, 'Data').apply(patch instanceof Uint8Array ? patch : framed(patch)); + return this; + } + + /** The version it holds now, kept as it is while patches change it: O(1). */ + snapshot(): Snapshot { + return wrap(Snapshot, inner(this, 'Data').snapshot()); + } + + /** Makes `snapshot`, a version of this dataset, the one it holds: what an undo does. */ + restore(snapshot: Snapshot): this { + inner(this, 'Data').restore(inner(snapshot, 'Snapshot')); + return this; + } + + /** What it holds now: see {@link Snapshot.describe}. */ + describe(): Description { + return describe(reads(this)); + } + + /** A value it holds now: see {@link Snapshot.get}. */ + get(table: string, id: Id, field: string, options: { at?: number } = {}): Value { + return get(reads(this), table, id, field, options); + } + + /** The rows a query keeps of what it holds now: see {@link Snapshot.query}. */ + query(query: Query): QueryResult { + return reads(this).query(JSON.stringify(query)) as QueryResult; + } + + /** Rows' extremes over spans of what it holds now: see {@link Snapshot.envelope}. */ + envelope(request: EnvelopeRequest): Envelope { + return envelope(reads(this), request); + } + + /** What it holds now as latkit's bytes: the patch that builds it. */ + toBytes(): Uint8Array { + return reads(this).toBytes(); + } + + /** Frees this handle; views drawing the dataset and its snapshots keep it. */ + destroy(): void { + release(this)?.free(); + } + + [Symbol.dispose](): void { + this.destroy(); + } +} + +/** One version of a dataset, as it was when taken: O(1) to keep. A view given one draws it as it is. */ +export class Snapshot { + private constructor() {} + + get name(): string { + return reads(this).name; + } + + /** What the dataset holds: tables, rows, fields and their kinds, the axis and its span. */ + describe(): Description { + return describe(reads(this)); + } + + /** The value of `field` for the row with `id` in `table`, at axis coordinate `at` when given. */ + get(table: string, id: Id, field: string, options: { at?: number } = {}): Value { + return get(reads(this), table, id, field, options); + } + + /** + * The rows of a table `query` keeps, filtered, sorted, and paged, sampled fields read at its + * coordinate, on the CPU through the expressions marks draw with. + */ + query(query: Query): QueryResult { + return reads(this).query(JSON.stringify(query)) as QueryResult; + } + + /** + * Each row's least and greatest value of a sampled field over equal spans of a range, of every row + * or of the rows named: row after row, NaN where a span holds no frame. It reads the coarsest + * levels of detail that fit, so a span costs a few reads however many frames it covers. + */ + envelope(request: EnvelopeRequest): Envelope { + return envelope(reads(this), request); + } + + /** The version as latkit's bytes: the patch that builds it. */ + toBytes(): Uint8Array { + return reads(this).toBytes(); + } + + destroy(): void { + release(this)?.free(); + } + + [Symbol.dispose](): void { + this.destroy(); + } +} + +/** + * A dataset fetched in pieces as views ask for them: plain data naming where they lie. Each view + * draws the pieces its camera and plot window ask for as a dataset of its own, and every view of a + * GPU shares what is fetched. The source's first bytes say what it holds. + * + * - Time tiles, a `frames.json` manifest as `latkit tile` writes it: a dataset whose sampled + * fields' histories are fetched in spans. A line tracing a field fetches the detail its plot + * window draws, a mark showing one the frames about the view's coordinate, and a span still + * loading draws the coarser span above it. + * - Vector tiles: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` template, fetched as + * the camera asks. Each layer is a table of its features of the kind marks draw it as (areas + * polygons, lines lines, vertices points), or named `layer:polygons`, `layer:lines`, or + * `layer:points`. + */ +export class Tiles { + /** The tiles at `url`, drawn as dataset `name`. */ + constructor( + readonly name: string, + readonly url: string, + ) {} +} + +/** What a view draws a dataset from, by its name. */ +export type Source = Data | Snapshot | Tiles; + +/** @internal `source` as the bindings take it, each time anew. */ +export function sourceOf(source: Source): bindings.Source { + if (source instanceof Tiles) return wasm().Source.tiles(source.name, source.url); + return source instanceof Data + ? inner(source, 'Data').source() + : inner(source, 'Snapshot').source(); +} + +/** @internal A snapshot around the bindings' `snapshot`. */ +export function snapshotOf(snapshot: bindings.Snapshot): Snapshot { + return wrap(Snapshot, snapshot); +} + +function reads(owner: Data | Snapshot): Reads { + return inner(owner, owner instanceof Data ? 'Data' : 'Snapshot'); +} + +function describe(reads: Reads): Description { + return JSON.parse(reads.describe()) as Description; +} + +function get(reads: Reads, table: string, id: Id, field: string, { at }: { at?: number }): Value { + return JSON.parse(reads.get(table, id, field, at)) as Value; +} + +function envelope(reads: Reads, request: EnvelopeRequest): Envelope { + return reads.envelope(JSON.stringify(request)) as Envelope; +} + +/** The bytes `source` holds, and the URL they came from. */ +async function bytesOf( + source: DataSource, + wait: Wait, +): Promise<{ bytes: Uint8Array; base: string }> { + if (typeof source === 'string') { + const response = await waited( + fetch(source, { signal: wait.signal }), + wait, + `fetching ${source}`, + ); + return bytesOf(response, wait); + } + if (source instanceof Uint8Array) return { bytes: source, base: '' }; + if (source instanceof ArrayBuffer) return { bytes: new Uint8Array(source), base: '' }; + if (source instanceof Response) { + if (!source.ok) throw new Error(`${source.url}: ${source.status} ${source.statusText}`); + return { bytes: new Uint8Array(await source.arrayBuffer()), base: source.url }; + } + if (source instanceof Blob) + return { bytes: new Uint8Array(await source.arrayBuffer()), base: '' }; + if (source instanceof ReadableStream) + return { bytes: new Uint8Array(await new Response(source).arrayBuffer()), base: '' }; + return { bytes: new TextEncoder().encode(JSON.stringify(source)), base: '' }; +} diff --git a/npm/src/gpu.ts b/npm/src/gpu.ts new file mode 100644 index 00000000..2dd75c3e --- /dev/null +++ b/npm/src/gpu.ts @@ -0,0 +1,132 @@ +/** The GPU: the device views draw with, the memory they share, and the frames they draw in. */ + +import type * as bindings from '../pkg/latkit.js'; +import { Canvas, type CanvasOptions } from './canvas.js'; +import type { Budget, Scene } from './inputs.js'; +import { hold, inner, release, wasm, waited, type Wait } from './internal.js'; +import { firstDevice, load } from './load.js'; +import type { Work } from './outputs.js'; +import { View, type ViewOptions } from './view.js'; + +/** The GPU device views draw with. Views of one GPU draw in one animation frame, sharing its memory. */ +export class Gpu { + #views = new Set(); + #canvases = new Set(); + #listeners = new Set<() => void>(); + + private constructor(device: bindings.Gpu) { + hold(this, device); + } + + /** Asks for a WebGPU device, loading latkit first: the first call takes the one made as it loaded. */ + static async create(): Promise { + await load(); + return new Gpu(await (firstDevice() ?? wasm().Gpu.create())); + } + + /** + * A view of `scene`, drawing the datasets its `sources` give. A {@link Canvas} shows it, and a + * {@link Shot} draws it with or without one. + */ + view(scene: Scene, options: ViewOptions = {}): View { + const view = View.made(inner(this, 'Gpu'), scene, () => this.#views.delete(view)); + this.#views.add(view); + if (options.sources) view.sources = options.sources; + return view; + } + + /** + * A canvas showing views as `layout` places them in `target`: a `` element, which sizes + * itself to its CSS box and passes each view the input landing on it, or an `OffscreenCanvas` in a + * worker, which keeps the size it is given at `pixelRatio` pixels a CSS pixel. + */ + canvas(target: HTMLCanvasElement | OffscreenCanvas, options: CanvasOptions = {}): Canvas { + const canvas = Canvas.made(inner(this, 'Gpu'), target, options, () => + this.#canvases.delete(canvas), + ); + this.#canvases.add(canvas); + return canvas; + } + + /** + * Calls `listener` with the frame's timestamp every animation frame, before the views draw, so + * what it changes shows in that frame. Returns a function that stops it. + */ + on(type: 'frame', listener: (now: number) => void): () => void { + const handle = inner(this, 'Gpu').onFrame(listener); + const stop = (): void => { + if (this.#listeners.delete(stop)) handle.free(); + }; + this.#listeners.add(stop); + return stop; + } + + /** Whether the device was lost; a new `Gpu` is needed. */ + get lost(): boolean { + return inner(this, 'Gpu').lost; + } + + /** How much GPU memory data takes. */ + get budget(): Budget { + return JSON.parse(inner(this, 'Gpu').budget) as Budget; + } + + set budget(budget: Budget) { + inner(this, 'Gpu').setBudget(JSON.stringify(budget)); + } + + /** What this GPU did since it was created, counted exactly; see {@link Work}. */ + get work(): Work { + return JSON.parse(inner(this, 'Gpu').work) as Work; + } + + /** Bytes on the GPU every view of it draws from: data, and what each view keeps per row. */ + get resident(): number { + return inner(this, 'Gpu').resident; + } + + /** Bytes waiting to upload, for every view of this GPU. */ + get uploading(): number { + return inner(this, 'Gpu').uploading; + } + + /** + * Draws one frame now at `now`, milliseconds on the clock animation frames give: the frame + * listeners, then every canvas that needs a frame. From the first step no animation frames are + * asked for: a harness, as tests and benchmarks are, steps every frame itself, so a run draws the + * same frames however fast the device is. + */ + step(now: number): void { + inner(this, 'Gpu').step(now); + } + + /** + * Resolves once the GPU finished the work submitted to it, every readback landed, and every + * pipeline a view or shot asked for compiled: a frame stepped then draws all of it. + */ + async done(wait: Wait = {}): Promise { + await waited(inner(this, 'Gpu').done(), wait, 'the GPU'); + } + + /** + * Resolves when work a view waits on lands after this call (a readback, a compile, a job, a text, + * a fetch), or a view asks for a frame. + */ + async landed(wait: Wait = {}): Promise { + await waited(inner(this, 'Gpu').landed(), wait, 'work landing'); + } + + /** Releases this GPU's canvases, views, and frame listeners. Other GPUs and data stay usable. */ + destroy(): void { + const device = release(this); + if (!device) return; + for (const canvas of this.#canvases) canvas.destroy(); + for (const view of this.#views) view.destroy(); + for (const stop of this.#listeners) stop(); + device.free(); + } + + [Symbol.dispose](): void { + this.destroy(); + } +} diff --git a/npm/src/index.ts b/npm/src/index.ts index fe3683d9..0384c74b 100644 --- a/npm/src/index.ts +++ b/npm/src/index.ts @@ -1,24 +1,43 @@ +/// /** - * Scenes, expressions, and patches are plain data, usable without WebAssembly. Call loadLatkit() to - * read data and draw. + * latkit: columnar data drawn with WebGPU. Scenes, expressions, and patches are plain data, usable + * anywhere; `await load()` loads the WebAssembly that datasets, views, and pictures are made with. + * Its handles dispose with `using`, whatever library a project compiles against. */ export type * from './inputs.js'; export type * from './outputs.js'; -export * from './patch.js'; -export * from './scene.js'; -export { loadLatkit, type LoadOptions, type Latkit } from './load.js'; -export type { - Canvas, +export { load, type LoadOptions } from './load.js'; +export type { LatkitError, Wait } from './internal.js'; +export { Data, - Envelope, - Gpu, - Image, - ImageFormat, - LatkitError, - Panel, - Shot, - Table, - Video, + Snapshot, + Tiles, + type DataSource, + type Envelope, + type QueryResult, + type Source, +} from './data.js'; +export { + Patch, + reference, + type Fields, + type Geometry, + type PatchSource, + type ReadableBytes, + type Reference, + type RowIds, + type Values, +} from './patch.js'; +export * from './scene.js'; +export { colormapCss } from './colormap.js'; +export { Gpu } from './gpu.js'; +export { View, - ViewEvents, -} from './runtime.js'; + type Insets, + type Point, + type Position, + type ViewEvents, + type ViewOptions, +} from './view.js'; +export { Canvas, type CanvasOptions, type Panel, type Rect } from './canvas.js'; +export { Image, Shot, Video, type ImageFormat, type ShotOptions } from './shot.js'; diff --git a/npm/src/inputs.ts b/npm/src/inputs.ts index 2ec702db..c48766f9 100644 --- a/npm/src/inputs.ts +++ b/npm/src/inputs.ts @@ -1,6 +1,12 @@ /** Written by `node npm/build.mjs schema` from `npm/schema.json`, what latkit reads: do not edit. */ -import type { Color, ExprInput } from './scene.js'; +import type { Color, ExprInput, PaintInput } from './scene.js'; + +/** Areas where their options say nothing. */ +export interface AreaTheme { + /** An area's color. */ + color?: Color; +} /** * Rows drawn as the areas a polygon field holds: parcels, states, lakes, the land and water of a @@ -14,7 +20,7 @@ export interface Areas { /** The polygon field each row fills: one polygon's rings, or polygons. `geometry` unless set. */ geometry?: string | null; /** Each row's color; the theme's unless set. */ - color?: ExprInput | null; + color?: PaintInput | null; /** Whether each row is drawn and picked. */ visible?: ExprInput; /** @@ -202,7 +208,7 @@ export interface ColormapNode { domain?: [low: number, high: number] | null; } -/** Whether a text field holds the text, ignoring case: read by queries, which hold texts. */ +/** Whether a text field holds the text, ignoring case: in a mark, a field of its own table. */ export interface ContainsNode { contains: [field: ExprInput, text: string]; } @@ -232,8 +238,8 @@ export type Controls = */ keyboard?: boolean; /** - * The row the user points at glows. It is hovered either way: events tell it, and `hovered()` - * reads it. + * The row the user points at glows. It is hovered either way: events tell it, and `hovered` reads + * it. */ hover?: boolean; /** CSS pixels around the pointer that picks reach; touch reaches 8 more. */ @@ -245,10 +251,10 @@ export type Controls = selectBox?: boolean; /** * Dragging from a port, from a vertex with Alt, or an edge off its end draws a connection, told as - * `Event::Connect`. + * a `'connect'` event. */ connect?: boolean; - /** Delete and Backspace tell `Event::Delete` with the selection. */ + /** Delete and Backspace tell a `'delete'` event with the selection. */ delete?: boolean; /** * Clicking again where nothing moved selects the next row under the point, nearest first, so rows @@ -262,7 +268,7 @@ export type Controls = press?: number; /** * How much the view moves on its own: eases, flights, coasting, orbits, label fades, comets, and - * what reads `elapsed()`. + * what reads `elapsed`. */ motion?: Motion; }; @@ -281,24 +287,44 @@ export interface CustomColormap { /** * A sheet for diagrams, flat and seen from straight above: the camera pans and zooms. Positions - * are units with y down, and every length is a unit, a CSS pixel at zoom 0 (sizes, padding, ports, - * strokes, text, and the theme's), except glow, halo, and pick radius. Zooming scales one fixed - * picture. Only a diagram has ports, groups, nets, and orthogonal routes; it lays out blocks. + * are units with y down, and lengths are units, a unit a CSS pixel at zoom 0 (sizes, padding, + * ports, strokes, text, and the theme's), but for what says `px`, a halo, focus's glow, and the + * pick radius. Zooming scales one fixed picture. Only a diagram has ports, groups, nets, and + * orthogonal routes; it lays out blocks. */ export interface Diagram { /** The grid under the marks. */ grid?: Grid; } +/** How a diagram's blocks, groups, ports, and wires look. */ +export interface DiagramTheme { + /** What a group's container is filled with. */ + group?: Color; + /** The border of blocks and groups, a unit wide; `null` draws none. */ + outline?: Color | null; + /** The radius of block and group corners; a block's marker may set its own. */ + corner?: number; + /** How wide ports are. */ + portSize?: number; + /** The radius of an orthogonal wire's bends. */ + bend?: number; + /** Distance between a group's members and its outline. */ + groupPadding?: number; + /** How blocks and groups drop shadows onto the sheet. */ + shadow?: ShadowTheme; +} + /** A direction on screen. */ export type Direction = 'right' | 'left' | 'down' | 'up'; -/** The earth: positions are longitude and latitude in degrees. */ +/** + * The earth: positions are longitude and latitude in degrees, its ground in the theme's earth + * colors. + */ export interface Earth { - /** A flat map or a globe. */ + /** A flat map or a globe; a flat map unless set. */ projection?: Projection; - /** The ground's color; the theme's surface unless set. */ - surface?: Color | null; /** Whether lines of longitude and latitude cross the ground, in the theme's grid color. */ graticule?: boolean; /** @@ -311,6 +337,40 @@ export interface Earth { atmosphere?: boolean; } +/** How the earth looks. */ +export interface EarthTheme { + /** The ground's color. */ + ground?: Color; + /** The air glowing around the globe, where the earth has an atmosphere. */ + atmosphere?: Color; + /** How bright the night side stays where daylight lights the earth, 0 to 1. */ + night?: number; +} + +/** Edges where their options say nothing, their lanes, and their arrowheads. */ +export interface EdgeTheme { + /** + * An edge's color; `null` blends the colors of each edge's two vertices, and in a diagram takes + * the text color. + */ + color?: Color | null; + /** An edge's width. */ + width?: number; + /** + * In a diagram, distance between the lanes of edges joining the same two vertices, and between + * parallel orthogonal wires, which keep twice it apart. + */ + spacing?: number; + /** + * Elsewhere, how far edges joining the same two vertices bow apart: each lane by this fraction of + * the edge's length at its middle, as wide as eight lanes at most, more packing closer. A bow is + * part of the drawing, so zooming never moves it. + */ + bow?: number; + /** How far an arrowhead reaches past its edge's width. */ + arrow?: number; +} + /** * Rows drawn as strokes between the vertices two reference fields name: branches, links, wires. In * a diagram, a row without ends is a net: it joins every vertex port whose reference field names @@ -340,18 +400,20 @@ export interface Edges { * reference field names it, as a star from their middle to each. */ joins?: [table: string, reference: string] | null; - /** CSS pixels, or position units with `units`; units in a diagram. */ + /** + * Each row's width, in the space's unit unless it says its own: units in a diagram, CSS pixels + * elsewhere. + */ width?: Size | null; /** Whether each row is dashed. */ dash?: ExprInput; /** - * Comets flowing along each row at this speed a second: CSS pixels, units in a diagram, or - * position units where the theme's flow is in units; negative flows backward. A speed that changes - * carries comets on from where they are. + * Comets flowing along each row at this speed a second, in the lengths of the theme's flow; + * negative flows backward. A speed that changes carries comets on from where they are. */ flow?: ExprInput; /** Each row's color; the theme's unless set. */ - color?: ExprInput | null; + color?: PaintInput | null; /** Whether each row is drawn and picked. */ visible?: ExprInput; /** @@ -382,6 +444,24 @@ export interface Edges { legend?: string | null; } +/** + * Rows' least and greatest values of a sampled field over equal spans of the axis, to read back: + * field `field` of table `table` over `spans` equal spans from `range[0]` to `range[1]`, of every + * row or of the rows `ids` names, in its order. Plain data, like a query. + */ +export interface EnvelopeRequest { + /** The table read. */ + table: string; + /** The sampled field read. */ + field: string; + /** The axis coordinates the spans cover, from the first to the last. */ + range: [number, number]; + /** How many equal spans the range is cut into. */ + spans: number; + /** The rows read, in this order; every row unless set. */ + ids?: Ids | null; +} + /** A value for every row of a mark: a constant, a field, view state, or arithmetic of them. */ export type ExprNode = | number @@ -413,7 +493,7 @@ export interface Faces { */ corners?: [a: string, b: string, c: string] | null; /** Each row's color; the theme's unless set. */ - color?: ExprInput | null; + color?: PaintInput | null; /** Whether each row is drawn and picked. */ visible?: ExprInput; /** @@ -444,11 +524,11 @@ export interface Faces { legend?: string | null; } -/** A field of each row: of the mark's own table, or of another dataset's table joined by row id. */ +/** A field of each row: of the mark's own table, or from another dataset's table joined by row id. */ export interface FieldNode { field: string; /** The dataset and table the field is read from, joined by row id. */ - of?: [data: string, table: string] | null; + from?: [data: string, table: string] | null; /** Reads a sampled field at its last frame without interpolating. */ step?: boolean; } @@ -476,23 +556,34 @@ export type Fit = /** * How comets flowing along edges and lines look: each a round head, brightest at its middle, - * trailing a tail that thins and fades. + * trailing a tail that thins and fades. Their spacing and length are in one unit, and each row's + * flow is in it too: in position units, degrees on the earth, comets are part of the drawing, + * zooming with the view and staying where they are on their lines; otherwise they keep their size + * and speed as it zooms. */ -export interface Flow { +export interface FlowTheme { /** A comet's color; its alpha is how strongly a comet covers its stroke. */ color?: Color; /** Distance between comets. */ - spacing?: number; + spacing?: Length; /** How far a comet's tail trails its head. */ - length?: number; - /** How wide a comet's head is, CSS pixels, or units in a diagram; never narrower than its stroke. */ + length?: Length; + /** How wide a comet's head is; never narrower than its stroke. */ width?: number; - /** - * Whether comets are part of the drawing: their spacing, length, and each row's flow in position - * units, degrees on the earth, so they zoom with the view and stay where they are on their lines. - * Otherwise they keep their size and speed on screen, and slide along their lines as it zooms. - */ - units?: boolean; +} + +/** How hovered and selected rows glow, and rows outside a selection dim. */ +export interface FocusTheme { + /** The glow around hovered rows. */ + hover?: Color; + /** The glow around selected rows; `null` glows in each row's own color. */ + selected?: Color | null; + /** How far focus glows reach, CSS pixels. */ + glow?: number; + /** The opacity of rows outside a selection. */ + dim?: number; + /** How much a hovered vertex sized in CSS pixels grows. */ + grow?: number; } /** How an axis writes its tick values. */ @@ -540,13 +631,16 @@ export type Label = | { /** A text field. */ text: ExprInput; - /** CSS pixels, or units in a diagram; the theme's font size unless set. */ + /** In the space's unit: units in a diagram, CSS pixels elsewhere; the theme's text size unless set. */ size?: number | null; /** Each row's text color; the theme's text color unless set. */ - color?: ExprInput | null; + color?: PaintInput | null; /** Where labels would overlap, the higher shows; the mark's priority unless set. */ priority?: ExprInput | null; - /** How far a line runs before it wraps: CSS pixels, or units in a diagram. */ + /** + * How far a line runs before it wraps, in the space's unit: units in a diagram, CSS pixels + * elsewhere. + */ maxWidth?: number | null; /** The CSS font family list its texts are set in; the theme's font unless set. */ font?: string | null; @@ -571,7 +665,7 @@ export interface Layered { * How the view places vertex rows without positions: a diagram's blocks, sized, or the points of a * plane or the earth, among the rows linked to them, gaps in the space's units: a name, or * `{ stress | layered: options }`. A place, once given, holds: rows added later are placed around - * the rest, and `View::arrange` lays every row out again. + * the rest, and `view.arrange` lays every row out again. */ export type Layout = | 'none' @@ -586,6 +680,29 @@ export type Layout = layered: Layered; }; +/** + * A length: a number, in the space's unit, units in a diagram and CSS pixels elsewhere; `{ px }`, + * CSS pixels; or `{ units }`, position units. + */ +export type Length = + | number + | { + /** CSS pixels, however the view zooms. */ + px: number; + } + | { + /** Position units, which scale as the view zooms. */ + units: number; + }; + +/** Lines where their options say nothing. */ +export interface LineTheme { + /** A line's color. */ + color?: Color; + /** A line's width. */ + width?: number; +} + /** * Rows drawn as lines: along a path field (roads, rivers, borders) or a polygon field's rings, * outlining its areas; on a plot, through a sampled field's history across the data's axis, what a @@ -603,18 +720,20 @@ export interface Lines { trace?: string | null; /** How each row runs between its samples, on a plot. */ interpolation?: Interpolation; - /** CSS pixels, or position units with `units`; units in a diagram. */ + /** + * Each row's width, in the space's unit unless it says its own: units in a diagram, CSS pixels + * elsewhere. + */ width?: Size | null; /** Whether each row is dashed. */ dash?: ExprInput; /** - * Comets flowing along each row at this speed a second: CSS pixels, units in a diagram, or - * position units where the theme's flow is in units; negative flows backward. A speed that changes - * carries comets on from where they are. + * Comets flowing along each row at this speed a second, in the lengths of the theme's flow; + * negative flows backward. A speed that changes carries comets on from where they are. */ flow?: ExprInput; /** Each row's color; the theme's unless set. */ - color?: ExprInput | null; + color?: PaintInput | null; /** Whether each row is drawn and picked. */ visible?: ExprInput; /** @@ -742,6 +861,12 @@ export type Motion = 'system' | 'full' | 'reduced'; /** Which way a sort runs; rows a key reads nothing for come last either way. */ export type Order = 'ascending' | 'descending'; +/** + * A color for every row of a mark: a CSS color, a value through a colormap, or a conditional + * choosing between colors. + */ +export type PaintNode = Color | ColorNode | ColormapNode | WhenPaintNode | MissingNode; + /** A plane: positions are plane units. */ export interface Plane { /** The grid under the marks. */ @@ -756,6 +881,12 @@ export interface Plot { y?: Axis; } +/** How a plot looks. */ +export interface PlotTheme { + /** The line drawn at the shown coordinate. */ + playhead?: Color; +} + /** * A vertex's port: where a net its reference field names meets it, on one side of its marker: a * side, or options. An output drives its net: layouts rank a net from its outputs, and its arrows @@ -785,6 +916,11 @@ export type Port = /** How the earth is drawn. */ export type Projection = 'mercator' | 'globe'; +/** A size in CSS pixels, whatever the space's own unit. */ +export interface Px { + px: ExprInput; +} + /** The rows of a table to read back, run through the expressions marks draw with. */ export interface Query { /** The table read. */ @@ -805,8 +941,11 @@ export interface Query { offset?: number; /** The most rows read; a limit picks them in linear time before sorting only those. */ limit?: number | null; - /** Fields to read by name: a field as it is, anything else as numbers. Every field unless set. */ - select?: { [name: string]: ExprInput }; + /** + * Fields to read by name: a field as it is, anything else as numbers. Every field unless set; none + * set reads the ids alone. + */ + select?: { [name: string]: ExprInput } | null; /** The axis coordinate sampled fields are read at; their last frame unless set. */ at?: number | null; } @@ -869,7 +1008,7 @@ export interface Scene { } /** How a diagram's blocks and groups drop shadows onto its sheet. */ -export interface Shadow { +export interface ShadowTheme { /** What a shadow darkens toward; its alpha is how dark it gets under the block. */ color?: Color; /** How far a block's or a group's shadow falls below it, and as far again it softens. */ @@ -882,23 +1021,26 @@ export type Shape = 'ellipse' | 'rectangle' | 'rounded' | 'diamond'; /** A side of a marker. */ export type Side = 'left' | 'right' | 'top' | 'bottom'; -/** A size: CSS pixels, or position units with `units`. */ -export type Size = ExprInput | Units; +/** + * A size, in the space's unit unless it says its own: units in a diagram, CSS pixels elsewhere; + * `px` keeps CSS pixels, and `units` takes position units, which scale as the view zooms. + */ +export type Size = ExprInput | Px | Units; /** * The coordinate space a scene's positions are in: a name, or - * `{ plane | diagram | earth | plot: options }`. A diagram is a flat sheet seen from straight - * above: y runs down, and every length is a unit, a CSS pixel at zoom 0, except glow, halo, and - * pick radius. Only a diagram has ports, groups, nets, orthogonal routes, and layouts. + * `{ plane | diagram | earth | plot: options }`. Its own unit is what lengths are in unless they + * say theirs: units in a diagram, a flat sheet seen from straight above with y running down, a + * unit a CSS pixel at zoom 0; CSS pixels elsewhere. Only a diagram has ports, groups, nets, and + * orthogonal routes. */ export type Space = | 'plane' | 'diagram' - | 'mercator' - | 'globe' + | 'earth' | 'plot' | { - /** Longitude and latitude in degrees, on a map or a globe. */ + /** Longitude and latitude in degrees, on a flat map or a globe. */ earth: Earth; } | { @@ -931,10 +1073,24 @@ export interface Stress { gap?: number; } +/** Labels, axis ticks, and titles. */ +export interface TextTheme { + /** Text's color. */ + color?: Color; + /** The CSS font family text is set in. */ + font?: string; + /** Text's size. */ + size?: number; + /** CSS pixels of background around label text. */ + halo?: number; +} + /** - * Colors and sizes marks take where their options read nothing, and the look of focus and motion: - * a base theme's name, or options over one (`base`, dark unless set). Lengths are CSS pixels; in a - * diagram they are units, except `glow` and `halo`. + * Colors and sizes marks take where their options read nothing, and the look of text, focus, + * motion, and each space, grouped by what they style: a base theme's name, or options over one + * (`base`, dark unless set), each group's over the base's. Lengths are in the space's unit, units + * in a diagram and CSS pixels elsewhere, but for a text's halo and focus's glow, CSS pixels + * everywhere. */ export type Theme = | 'dark' @@ -942,84 +1098,34 @@ export type Theme = | { /** Behind everything. */ background?: Color; - /** The earth's ground, and a group's container. */ - surface?: Color; - /** A plane's or a diagram's grid. */ + /** A plane's or a diagram's grid, and the earth's graticule. */ grid?: Color; - /** Vertex color. */ - vertex?: Color; - /** Vertex radius. */ - vertexRadius?: number; - /** - * Edge color; `null` blends the colors of each edge's two vertices, and in a diagram takes the - * text color. - */ - edge?: Color | null; - /** Edge width. */ - edgeWidth?: number; - /** Line color. */ - line?: Color; - /** Line width. */ - lineWidth?: number; - /** Area color. */ - area?: Color; - /** How much a face turned away from the camera darkens, 0 to 1. */ - shading?: number; - /** Text: labels, axis ticks, and titles. */ - text?: Color; - /** The CSS font family text is set in. */ - font?: string; - /** Text size. */ - fontSize?: number; - /** The line a plot draws at the shown coordinate. */ - playhead?: Color; /** What a missing color reads as. */ missing?: Color; - /** The glow around hovered rows. */ - hover?: Color; - /** The glow around selected rows; `null` glows in each row's own color. */ - selected?: Color | null; - /** How far focus glows reach, CSS pixels. */ - glow?: number; - /** The opacity of rows outside a selection. */ - dim?: number; - /** How much a hovered vertex sized in CSS pixels grows. */ - hoverScale?: number; /** Length of a dash. */ dash?: number; + /** How much a face turned away from the camera darkens, 0 to 1. */ + shading?: number; + /** Labels, axis ticks, and titles. */ + text?: TextTheme; + /** Vertices where their options say nothing. */ + vertex?: VertexTheme; + /** Edges where their options say nothing, their lanes, and their arrowheads. */ + edge?: EdgeTheme; + /** Lines where their options say nothing. */ + line?: LineTheme; + /** Areas where their options say nothing. */ + area?: AreaTheme; + /** How hovered and selected rows glow, and rows outside a selection dim. */ + focus?: FocusTheme; /** How comets flowing along edges and lines look. */ - flow?: Flow; - /** - * In a diagram, distance between the lanes of edges joining the same two vertices, and between - * parallel orthogonal wires, which keep twice it apart. - */ - edgeSpacing?: number; - /** - * Elsewhere, how far edges joining the same two vertices bow apart: each lane by this fraction of - * the edge's length at its middle, as wide as eight lanes at most, more packing closer. A bow is - * part of the drawing, so zooming never moves it. - */ - laneBow?: number; - /** How far an arrowhead reaches past its edge's width. */ - arrow?: number; - /** CSS pixels of background around label text. */ - halo?: number; - /** Distance between a group's members and its outline. */ - groupPadding?: number; - /** The border of a diagram's blocks and groups, a unit wide; `null` draws none. */ - outline?: Color | null; - /** The radius of a diagram's block and group corners; a block's marker may set its own. */ - corner?: number; - /** How wide a diagram's ports are. */ - portSize?: number; - /** The radius of an orthogonal wire's bends. */ - bend?: number; - /** How a diagram's blocks and groups drop shadows onto its sheet. */ - shadow?: Shadow; - /** The air glowing around the globe, where the earth has an atmosphere. */ - atmosphere?: Color; - /** How bright the earth's night side stays where it is lit by daylight, 0 to 1. */ - night?: number; + flow?: FlowTheme; + /** How a diagram's blocks, groups, ports, and wires look. */ + diagram?: DiagramTheme; + /** How the earth looks. */ + earth?: EarthTheme; + /** How a plot looks. */ + plot?: PlotTheme; /** The theme these options change; dark unless set. */ base?: 'dark' | 'light'; }; @@ -1035,7 +1141,10 @@ export interface UnaryNode { args: [ExprInput]; } -/** A size in position units, so it scales as the view zooms, kept within CSS pixels when given. */ +/** + * A size in position units, whatever the space's own unit, so it scales as the view zooms, kept + * within CSS pixels when given. + */ export interface Units { units: ExprInput; /** CSS pixels it keeps at least, however far the view zooms out. */ @@ -1044,6 +1153,14 @@ export interface Units { atMost?: number | null; } +/** Vertices where their options say nothing. */ +export interface VertexTheme { + /** A vertex's color. */ + color?: Color; + /** A vertex's radius. */ + radius?: number; +} + /** Rows drawn as markers: points of a map, nodes of a graph, blocks of a diagram. */ export interface Vertices { /** The dataset and table drawn. */ @@ -1060,17 +1177,20 @@ export interface Vertices { * flat. */ elevation?: ExprInput; - /** CSS pixels, or position units with `units`; units in a diagram. */ + /** + * Each row's radius, in the space's unit unless it says its own: units in a diagram, CSS pixels + * elsewhere. + */ radius?: Size | null; /** - * Width and height, for markers that are not round: CSS pixels, or position units with `units`; - * units in a diagram, where a block without a size fits its label and ports. + * Width and height, for markers that are not round, in the space's unit unless they say their own; + * in a diagram a block's are units, and a block without a size fits its label and ports. */ size?: [Size, Size] | null; /** How each row is drawn. */ marker?: Marker; /** A ring around each row's marker in this color; a missing or transparent color draws none. */ - ring?: ExprInput | null; + ring?: PaintInput | null; /** * A port on each row for each reference field naming a net, which an `edges` mark of the net table * without ends draws. Diagrams only. @@ -1087,7 +1207,7 @@ export interface Vertices { */ shadow?: boolean; /** Each row's color; the theme's unless set. */ - color?: ExprInput | null; + color?: PaintInput | null; /** Whether each row is drawn and picked. */ visible?: ExprInput; /** @@ -1124,5 +1244,11 @@ export interface WhenNode { otherwise: ExprInput; } +/** A conditional's color: the first case's whose condition holds, else `otherwise`'s. */ +export interface WhenPaintNode { + when: [condition: ExprInput, color: PaintInput][]; + otherwise: PaintInput; +} + /** What zooms a view. */ export type Zoom = 'wheel' | 'modifier' | 'off'; diff --git a/npm/src/internal.ts b/npm/src/internal.ts new file mode 100644 index 00000000..d72d9b89 --- /dev/null +++ b/npm/src/internal.ts @@ -0,0 +1,112 @@ +/** + * What the package's classes share and the page never reaches: the WebAssembly bindings, once + * loaded; the bindings' object behind each public one; and how latkit throws and waits. + */ + +import * as bindings from '../pkg/latkit.js'; +import type { Failure } from './outputs.js'; + +/** + * What latkit throws: an `Error` with a `code`, and for invalid input the `path` to the part, such + * as `marks[2].vertices.color`. `unloaded` is a call made before `await load()`. + */ +export interface LatkitError extends Error { + code: Failure['code'] | 'unloaded'; + path?: string; +} + +/** How long a promise may wait: until `signal` aborts, by default 30 seconds. */ +export interface Wait { + signal?: AbortSignal; +} + +let ready = false; + +/** @internal Marks the bindings loaded: classes may make their objects from now on. */ +export function loaded(): void { + ready = true; +} + +/** @internal The bindings, or an `unloaded` error before `load()` finished. */ +export function wasm(): typeof bindings { + if (!ready) + throw thrown({ code: 'unloaded', message: 'latkit is not loaded: call `await load()` first' }); + return bindings; +} + +/** @internal The error `failure` describes, as latkit throws them. */ +export function thrown({ + code, + message, + path, +}: Pick & { code: LatkitError['code'] }): LatkitError { + return Object.assign(new Error(message), path === undefined ? { code } : { code, path }); +} + +/** The bindings' object behind each public one. */ +const held = new WeakMap(); + +/** @internal Gives `owner` the bindings' object `inner`. */ +export function hold(owner: object, inner: object): void { + held.set(owner, inner); +} + +/** @internal `owner`'s object of the bindings, or an error naming `what` once it was destroyed. */ +export function inner(owner: object, what: string): T { + const found = held.get(owner); + if (!found) throw new Error(`this ${what} has been destroyed`); + return found as T; +} + +/** @internal Takes `owner`'s object of the bindings away, to free it; `undefined` once it was. */ +export function release(owner: object): T | undefined { + const found = held.get(owner); + held.delete(owner); + return found as T | undefined; +} + +/** @internal A public object of class `type` around the bindings' `inner`, made without its constructor. */ +export function wrap(type: { prototype: T }, inner: object): T { + const made = Object.create(type.prototype) as T; + hold(made, inner); + return made; +} + +/** + * @internal What `pending` brings: a promise, or a task of the bindings, which is cancelled, so its + * work is skipped, when `signal` (by default 30 seconds) aborts first. Aborting throws a `timeout` + * error naming `what` for a timeout, `cancelled` for any other reason. + */ +export async function waited( + pending: Promise | bindings.Task, + { signal = AbortSignal.timeout(30_000) }: Wait, + what: string, +): Promise { + const task = pending instanceof Promise ? undefined : pending; + const promise = (task ? task.promise : pending) as Promise; + let stop = (): void => {}; + const aborted = new Promise((_, reject) => { + stop = () => { + task?.cancel(); + reject(abortError(signal, what)); + }; + }); + if (signal.aborted) stop(); + else signal.addEventListener('abort', stop, { once: true }); + try { + return await Promise.race([promise, aborted]); + } finally { + signal.removeEventListener('abort', stop); + task?.free(); + } +} + +/** Why `signal` aborted what was waiting for `what`, as latkit throws it. */ +function abortError(signal: AbortSignal, what: string): LatkitError { + const reason = signal.reason as { name?: string } | undefined; + return reason?.name === 'TimeoutError' + ? thrown({ code: 'timeout', message: `${what} timed out` }) + : Object.assign(thrown({ code: 'cancelled', message: `${what} was cancelled` }), { + cause: reason, + }); +} diff --git a/npm/src/jobs.ts b/npm/src/jobs.ts index 33c6f93b..9fb3e13b 100644 --- a/npm/src/jobs.ts +++ b/npm/src/jobs.ts @@ -26,7 +26,7 @@ interface Slot { */ export function jobs( module: WebAssembly.Module, - createWorker = () => new Worker(new URL('./worker.js', import.meta.url)), + worker = () => new Worker(new URL('./worker.js', import.meta.url)), ): (bytes: Uint8Array) => Promise { const idle = new Set(); @@ -56,9 +56,8 @@ export function jobs( }; const start = (): Slot => { - const worker = createWorker(); - const slot: Slot = { worker, ready: false }; - worker.onmessage = ({ data }: MessageEvent) => { + const slot: Slot = { worker: worker(), ready: false }; + slot.worker.onmessage = ({ data }: MessageEvent) => { if (!slot.ready) { if (!data.ready) return fail(slot, data.error ?? 'worker initialization failed'); slot.ready = true; @@ -74,11 +73,11 @@ export function jobs( job.resolve(data.bytes); } }; - worker.onerror = (event) => { + slot.worker.onerror = (event) => { event.preventDefault(); fail(slot, event.message || 'worker failed'); }; - worker.onmessageerror = () => fail(slot, 'could not read worker message'); + slot.worker.onmessageerror = () => fail(slot, 'could not read worker message'); return slot; }; diff --git a/npm/src/load.ts b/npm/src/load.ts index b5477c06..aefc01a8 100644 --- a/npm/src/load.ts +++ b/npm/src/load.ts @@ -1,63 +1,80 @@ -/** Options for the page's shared WASM runtime. Omitted options reuse its existing configuration. */ +/** Loading latkit's WebAssembly: once a page, before data, views, and pictures can be made. */ + +import init, * as bindings from '../pkg/latkit.js'; +import { encoder } from './encoder.js'; +import { loaded, thrown } from './internal.js'; +import { jobs } from './jobs.js'; + +/** How latkit loads. Options left out keep what loading began with. */ export interface LoadOptions { /** The matching package's WASM asset, or an already compiled module. Defaults to the bundled asset. */ wasm?: string | URL | WebAssembly.Module; - /** Creates a worker running the standalone `latkit/worker` bundle. Defaults to the bundled worker. */ - createWorker?: () => Worker; + /** Makes a worker running the standalone `latkit/worker` bundle. Defaults to the bundled worker. */ + worker?: () => Worker; } -/** - * The initialized data and rendering API. In a browser the page's first GPU is made as the runtime - * loads, and the first `Gpu.create()` takes it; later calls request devices of their own. - */ -export interface Latkit { - Data: typeof import('./runtime.js').Data; - Gpu: typeof import('./runtime.js').Gpu; - Shot: typeof import('./runtime.js').Shot; - colormapCss: typeof import('./runtime.js').colormapCss; -} +let configured: { wasm?: string | WebAssembly.Module; worker?: () => Worker } | undefined; +let loading: Promise | undefined; -let configured: LoadOptions | undefined; -let loading: Promise | undefined; +/** The page's first device, made as latkit loads, until `Gpu.create()` takes it. */ +let first: Promise | undefined; /** - * Loads the runtime once per page. Concurrent calls share one promise; a failed load can be retried. - * Options explicitly supplied after the first call must match that call. Importing the package alone - * neither initializes WASM nor requests a GPU or worker; loading asks for the page's GPU adapter - * while the WASM compiles. + * Loads latkit's WebAssembly once a page: data, views, and pictures can be made once it resolves. + * Concurrent calls share one load, and a failed one can be tried again; options given after the first + * call must match it. Importing the package neither loads WebAssembly nor asks for a GPU; loading asks + * for the page's GPU adapter while the WebAssembly compiles. `Gpu.create`, `Data.read`, and + * `Patch.read` load it themselves. */ -export function loadLatkit(options: LoadOptions = {}): Promise { - // Snapshot URLs so a caller cannot change the configuration while it loads. +export function load(options: LoadOptions = {}): Promise { + // URLs as text, so a caller cannot change the configuration while it loads. const wasm = options.wasm instanceof URL ? options.wasm.href : options.wasm; if (loading) { if ( (wasm !== undefined && wasm !== configured?.wasm) || - (options.createWorker !== undefined && options.createWorker !== configured?.createWorker) - ) { + (options.worker !== undefined && options.worker !== configured?.worker) + ) return Promise.reject( - Object.assign(new Error('latkit is already loading or loaded with different options'), { - code: 'io', - }), + thrown({ code: 'io', message: 'latkit is already loading or loaded with other options' }), ); - } return loading; } - configured = { wasm, createWorker: options.createWorker }; + configured = { wasm, worker: options.worker }; startGpuProcess(); - loading = import('./runtime.js') - .then((runtime) => runtime.initialize(configured!)) - .catch((cause: unknown) => { - configured = undefined; - loading = undefined; - throw Object.assign(new Error('could not load latkit', { cause }), { code: 'io' }); - }); + loading = initialize(configured).catch((cause: unknown) => { + configured = undefined; + loading = undefined; + throw Object.assign(thrown({ code: 'io', message: 'could not load latkit' }), { cause }); + }); return loading; } +/** Initializes the bindings, their workers and video encoder, and the page's first device. */ +async function initialize(options: { wasm?: string | WebAssembly.Module; worker?: () => Worker }) { + await init(options.wasm === undefined ? undefined : { module_or_path: options.wasm }); + if (options.worker || typeof Worker === 'function') + bindings.setJobs(jobs(bindings.module() as WebAssembly.Module, options.worker)); + if (typeof VideoEncoder === 'function') bindings.setEncoder(encoder); + loaded(); + // The page's first device is made now, so the page does not wait for it when it draws; it + // compiles nothing until a view opens. + if ((globalThis.navigator as { gpu?: unknown } | undefined)?.gpu) { + first = bindings.Gpu.create(); + first.catch(() => {}); + } +} + +/** @internal The page's first device, once: what `Gpu.create()` takes before it asks for another. */ +export function firstDevice(): Promise | undefined { + const device = first; + first = undefined; + return device; +} + /** - * Asks for a GPU adapter as latkit's own request does, while the runtime loads: the browser starts - * its GPU process meanwhile, which takes most of a cold device's 280 ms, and latkit's request finds - * it running. + * Asks for a GPU adapter as latkit's own request does, while the WebAssembly loads: the browser starts + * its GPU process meanwhile, which takes most of a cold device's 280 ms, and latkit's request finds it + * running. */ function startGpuProcess(): void { const gpu = ( diff --git a/npm/src/outputs.ts b/npm/src/outputs.ts index 8c3ea494..199b9fc8 100644 --- a/npm/src/outputs.ts +++ b/npm/src/outputs.ts @@ -122,11 +122,6 @@ export interface Stats { tiles: number; /** Tiles being fetched or decoded, of every tile source. */ tilesLoading: number; - /** - * The work under way the last frame stood in for; nothing when it drew everything as the data, - * scene, and camera say: see `View::complete`. - */ - waiting: Waiting; /** Milliseconds the last frame took to record on the CPU. */ cpuMs: number; /** @@ -165,7 +160,7 @@ export interface Trigger { */ export type Value = number | boolean | string | null | [number, number] | unknown[][]; -/** What happened in a view, for the application to act on. Drain them with `View::events`. */ +/** What happened in a view, for the application to act on, as `view.on` tells it. */ export type ViewEvent = | { type: 'click'; @@ -234,7 +229,7 @@ export type ViewEvent = }; } | { - type: 'hover'; + type: 'hovered'; detail: { /** The row now pointed at; `null` once it left every row. */ hit: Hit | null; @@ -243,7 +238,7 @@ export type ViewEvent = }; } | { - type: 'select'; + type: 'selection'; detail: { /** The rows selected now. */ rows: Rows[]; @@ -272,6 +267,16 @@ export interface Work { written: number; /** Rows whose channels the GPU evaluated. */ evaluated: number; + /** + * Rows of edges and faces whose ends the GPU found where their vertices are held: rows that + * changed, and every row held where the vertices they end at moved. + */ + resolved: number; + /** + * Bytes of pages of words per row the GPU cleared: those read before anything writes them, and the + * normals faces sum afresh. + */ + cleared: number; /** * Rows packed into pages of the order a table drawn in part draws in: each page once, the first * time a view draws it, and again only where a write changed its rows. diff --git a/npm/src/patch.ts b/npm/src/patch.ts index 6b571a71..678979bb 100644 --- a/npm/src/patch.ts +++ b/npm/src/patch.ts @@ -5,6 +5,8 @@ */ import type { Id, Ids } from './inputs.js'; +import { waited, wasm, type Wait } from './internal.js'; +import { load } from './load.js'; /** Row ids as a patch takes them: listed, `{ count }` for rows `0..count`, or a typed array. */ export type RowIds = Ids | readonly (Id | null)[] | Uint32Array | Float64Array; @@ -64,19 +66,28 @@ const STORE = { f64: 0, f32: 1, i32: 2, u32: 3, u64: 4, bool: 5, text: 6, geomet const SAFE = Number.MAX_SAFE_INTEGER; const utf8 = new TextEncoder(); +/** What `Patch.read` reads: latkit's bytes or a patch's JSON. */ +export type PatchSource = Uint8Array | ArrayBuffer | Blob | Response | string | object; + +/** @internal The frame `patch` writes, without copying it: read at once, before the patch changes. */ +export let framed: (patch: Patch) => Uint8Array; + /** * Changes to a dataset, applied in order and all at once by `Data.apply`: plain data, written as * latkit's bytes as it is built. * * ```ts - * const patch = Patch.to('grid') + * const patch = new Patch('grid') * .replace('Bus', [101, 102], { x: Float64Array.of(0, 10), name: ['North', 'South'] }) - * .append(0.5, 'Bus', { vm: Float32Array.of(1.01, 0.99) }); + * .append('Bus', 0.5, { vm: Float32Array.of(1.01, 0.99) }); * ``` */ export class Patch { - /** The dataset the patch changes, when it names one; `Data.apply` refuses it for another. */ - readonly data: string | undefined; + static { + framed = (patch) => patch.#frame(); + } + + #data: string | undefined; #writer: Writer; #count = 0; /** Where the operations start: after the header, the name, and their count. */ @@ -85,39 +96,65 @@ export class Patch { * one: a field records its frames in one write, however many calls made them. */ #run: Appends | undefined; - /** An empty patch, for whichever dataset it is applied to. */ - constructor(data?: string); - /** @internal */ - constructor(data: string, owned: Uint8Array); - constructor(data?: string, owned?: Uint8Array) { - this.data = data || undefined; - this.#writer = new Writer(owned); - if (owned) { - this.#ops = 0; - return; - } + /** An empty patch: to dataset `data`, which it names, or to whichever it is applied to. */ + constructor(data?: string) { + this.#data = data || undefined; + this.#writer = new Writer(); this.#writer.bytes(MAGIC); this.#writer.u16(VERSION); this.#writer.u16(0); this.#writer.zeros(8); - this.#writer.str(this.data ?? ''); + this.#writer.str(this.#data ?? ''); this.#writer.u32(0); this.#ops = this.#writer.length; } - /** An empty patch to dataset `data`. */ - static to(data: string): Patch { - return new Patch(data); - } - - /** The patch `bytes` hold: one frame, as `toBytes` or a stream writes it. */ - static fromBytes(bytes: Uint8Array): Patch { - return Patch.#fromOwnedFrame(bytes.slice()); + /** The dataset the patch changes, when it names one; `Data.apply` refuses it for another. */ + get data(): string | undefined { + return this.#data; } - static #fromOwnedFrame(bytes: Uint8Array): Patch { + /** + * The patch `source` holds: latkit's bytes, one frame as `toBytes` writes it, or its JSON, what + * any language writes without latkit: a list of changes, each named by its method with its + * parameters by name, such as `{"set": {"table": "Bus", "ids": [1], "fields": {"kv": [345]}}}`. + * Slices of binary files, `{file, dtype, offset, count}`, are fetched at once relative to `base`, + * by default the URL the JSON was fetched from. `source` is bytes, a `Blob`, a `Response`, a URL + * to fetch, or the JSON as a value. Bytes read without WebAssembly; JSON loads it, as `load()` + * does. A frame longer than `limit` bytes (256 MiB unless given) is refused. + */ + static async read( + source: PatchSource, + options: { base?: string; limit?: number } & Wait = {}, + ): Promise { + let base = ''; + if (typeof source === 'string') { + source = await waited( + fetch(source, { signal: options.signal }), + options, + `fetching ${source}`, + ); + } + if (source instanceof Response) { + if (!source.ok) throw new Error(`${source.url}: ${source.status} ${source.statusText}`); + base = source.url; + source = await source.arrayBuffer(); + } + if (source instanceof Blob) source = await source.arrayBuffer(); + if (source instanceof ArrayBuffer) source = new Uint8Array(source); + const bytes = + source instanceof Uint8Array ? source : new TextEncoder().encode(JSON.stringify(source)); + if (MAGIC.every((byte, i) => bytes[i] === byte)) + return Patch.#owning(bytes.slice(), options.limit); + await load(); + const read = wasm().readPatch(bytes, options.base ?? base); + return Patch.#owning(await waited(read, options, 'reading a patch'), options.limit); + } + + /** The patch the frame `bytes` hold, keeping them. */ + static #owning(bytes: Uint8Array, limit = 1 << 28): Patch { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); - const length = frameLength(bytes, Infinity); + const length = frameLength(bytes, limit); if (length === undefined || length !== bytes.length) throw malformed('not one whole frame'); const nameLength = bytes.length >= HEADER + 4 ? view.getUint32(HEADER, true) : -1; const start = HEADER + 4 + nameLength + 4; @@ -130,7 +167,9 @@ export class Patch { } catch { throw malformed('text is not UTF-8'); } - const patch = new Patch(name, bytes); + const patch = new Patch(); + patch.#data = name || undefined; + patch.#writer = new Writer(bytes); patch.#count = view.getUint32(start - 4, true); patch.#ops = start; return patch; @@ -169,7 +208,7 @@ export class Patch { buffer = frame; } if (used === length) { - const patch = Patch.#fromOwnedFrame(buffer); + const patch = Patch.#owning(buffer, limit); buffer = new Uint8Array(HEADER); used = 0; length = undefined; @@ -180,20 +219,6 @@ export class Patch { if (used !== 0) throw malformed('the stream ended inside a patch'); } - /** - * The patch its JSON makes: a list of changes, each named by its method with its parameters by - * name, such as `{"set": {"table": "Bus", "ids": [1], "fields": {"kv": [345]}}}`. Slices of - * binary files, `{file, dtype, offset, count}`, are fetched at once relative to `base`, the - * document's own URL. Reading JSON takes latkit's WebAssembly, which this loads as - * `loadLatkit()` does. - */ - static async load(json: string, base = ''): Promise { - const { loadLatkit } = await import('./load.js'); - await loadLatkit(); - const { readPatch } = await import('./runtime.js'); - return Patch.fromBytes(await readPatch(json, base)); - } - /** Names the axis sampled fields are recorded over, such as `('time', 's')`. */ axis(name: string, unit: string): this { return this.#op(OP.axis, (w) => { @@ -224,16 +249,16 @@ export class Patch { } /** - * Records a frame at each axis coordinate of `at`: for each field, a value per row at every - * coordinate, frame-major; of every row of `table`, or of rows `ids`, the rest holding their last + * Records a frame of `table` at each axis coordinate of `at`: for each field, a value per row at + * every coordinate, frame-major; of every row, or of rows `ids`, the rest holding their last * values. A field's first frame makes it sampled. Appends of float arrays to the same fields of * every row, one after another, write as one change, so a frame a call costs no more than one call. */ append( - at: number | readonly number[], table: string, + at: number | readonly number[], fields: Readonly>, - ids?: RowIds, + { ids }: { ids?: RowIds } = {}, ): this { const frames = typeof at === 'number' ? [at] : at; const entries = Object.entries(fields); @@ -272,19 +297,27 @@ export class Patch { } /** - * Holds a run of sampled field `field`'s entries of level `level`, numbered from `first`: what a - * time tile holds, as `latkit tile` writes them. At level 0 the entries are frames, each a - * coordinate in `at` and a value per row of `table` in `values`. At level `k` they are buckets of - * `8^k` frames, each its first and last frame's coordinates in `at`, and in `values` each row's - * least value over each bucket, then each row's greatest. NaN is missing. No entry may be held - * already; frames past the field's widen it. + * Holds a run of sampled field `field`'s entries of level `level` (0 unless given), numbered from + * `first`: what a time tile holds, as `latkit tile` writes them. At level 0 the entries are frames, + * each a coordinate in `at` and a value per row of `table` in `values`. At level `k` they are + * buckets of `8^k` frames, each its first and last frame's coordinates in `at`, and in `values` + * each row's least value over each bucket, then each row's greatest. NaN is missing. No entry may + * be held already; frames past the field's widen it. */ frames( table: string, field: string, - { level = 0, first }: { level?: number; first: number }, - at: Float64Array | readonly number[], - values: Float32Array | readonly number[], + { + level = 0, + first, + at, + values, + }: { + level?: number; + first: number; + at: Float64Array | readonly number[]; + values: Float32Array | readonly number[]; + }, ): this { const path = `${table}.${field}`; if (!Number.isInteger(level) || level < 0 || level > LEVELS) @@ -328,11 +361,11 @@ export class Patch { /** The patch as bytes: one frame, naming its dataset when it has one. */ toBytes(): Uint8Array { - return this.frame().slice(); + return this.#frame().slice(); } - /** @internal The frame, without copying it: read at once, before the patch changes. */ - frame(): Uint8Array { + /** The frame, without copying it: read at once, before the patch changes. */ + #frame(): Uint8Array { this.#flush(); const view = this.#writer.view(); const data = new DataView(view.buffer, view.byteOffset, view.byteLength); diff --git a/npm/src/runtime.ts b/npm/src/runtime.ts deleted file mode 100644 index da1a17c8..00000000 --- a/npm/src/runtime.ts +++ /dev/null @@ -1,917 +0,0 @@ -/// -/** WebAssembly-backed data and drawing, loaded only by loadLatkit(). */ - -import init, * as wasm from '../pkg/latkit.js'; -import { encoder } from './encoder.js'; -import type { Budget, Camera, Colormap, Fit, Id, Query, Row, Rows, Scene } from './inputs.js'; -import { jobs } from './jobs.js'; -import type { LoadOptions } from './load.js'; -import type { Description, Failure, Hit, Stats, Value, ViewEvent, Work } from './outputs.js'; -import { Patch } from './patch.js'; - -/** The page's first GPU, made as the runtime loads, until `Gpu.create()` takes it. */ -let first: Promise | undefined; - -/** @internal Initializes the bindings before returning their public API. */ -export async function initialize(options: LoadOptions) { - await init(options.wasm === undefined ? undefined : { module_or_path: options.wasm }); - if (options.createWorker || typeof Worker === 'function') - wasm.setJobs(jobs(wasm.module() as WebAssembly.Module, options.createWorker)); - if (typeof VideoEncoder === 'function') wasm.setEncoder(encoder); - // The page's first GPU is made now, so the page does not wait for its device when it draws; it - // compiles nothing until a view opens. - if ((globalThis.navigator as { gpu?: unknown } | undefined)?.gpu) { - first = wasm.Gpu.create(); - first.catch(() => {}); - } - return Object.freeze({ Data, Gpu, Shot, colormapCss }); -} - -/** @internal The bytes of the patch its JSON makes, for `Patch.load`. */ -export async function readPatch(json: string, base: string): Promise { - return (await wasm.loadPatch(json, base)) as Uint8Array; -} - -/** - * What latkit throws: an `Error` with a `code`, and for invalid input the `path` to the part, such - * as `marks[2].vertices.color`. - */ -export interface LatkitError extends Error { - code: Failure['code']; - path?: string; -} - -/** - * What a view tells its listeners, by type. Intents (`click`, `open`, `menu`, `drag`, `connect`, - * `delete`) are told once each, in the order the user acted. Changes (`hover`, `select`, `camera`, - * `waiting`) tell only their latest, and their state can always be read. A view tells only what the - * application did not do itself: its setters change state silently. - */ -export type ViewEvents = { - [E in ViewEvent as E['type']]: E extends { type: 'error' } ? LatkitError : E['detail']; -}; - -/** The error `failure` describes, as latkit throws them. */ -function thrown({ code, message, path }: Failure): LatkitError { - return Object.assign(new Error(message), path === undefined ? { code } : { code, path }); -} - -/** `promise`, or a `timeout` error once `seconds` pass first. */ -function deadline(promise: Promise, seconds: number, what: string): Promise { - let timer: ReturnType | undefined; - const late = new Promise((_, reject) => { - timer = setTimeout( - () => - reject( - thrown({ - code: 'timeout', - message: `${what} took over ${seconds} s: is the view drawn?`, - }), - ), - seconds * 1000, - ); - }); - return Promise.race([promise, late]).finally(() => clearTimeout(timer)); -} - -/** - * The rows a query keeps, in the forms a patch writes: numbers as a `Float64Array` (missing NaN), - * other values as arrays (missing `null`), a reference as its target ids. - */ -export interface Table { - table: string; - ids: Id[]; - fields: Record; - /** The rows the query kept before paging. */ - total: number; -} - -/** Rows' least and greatest values over spans of the axis: `at` holds one more edge than spans. */ -export interface Envelope { - at: Float64Array; - least: Float32Array; - greatest: Float32Array; -} - -/** - * A named collection of tables, changed in place by patches. A snapshot keeps a version, sharing - * every page with it, so keeping earlier versions, as an undo history does, costs only the pages - * they hold apart. A view draws the version it was given until it is given another. - */ -export class Data { - #inner?: wasm.Data; - - /** @internal */ - get inner(): wasm.Data { - if (!this.#inner) throw new Error('this Data has been destroyed'); - return this.#inner; - } - - constructor(name: string); - /** @internal */ - constructor(name: wasm.Data); - constructor(name: string | wasm.Data) { - this.#inner = typeof name === 'string' ? new wasm.Data(name) : name; - } - - /** Releases this handle. Views and snapshots keep the versions they hold. */ - destroy(): void { - this.#inner?.free(); - this.#inner = undefined; - } - - [Symbol.dispose](): void { - this.destroy(); - } - - /** - * The dataset `bytes` hold: latkit's bytes, renamed `name` when given, or GeoJSON, gzip-compressed - * or not, read in a worker as dataset `name`: points, line strings, and polygons as tables - * `points`, `lines`, and `polygons`, each with its geometry as field `geometry` and its properties - * as fields. - */ - static async read(bytes: Uint8Array, name?: string): Promise { - return new Data((await wasm.Data.read(bytes, name)) as wasm.Data); - } - - /** - * The dataset its JSON document builds, `{"name": ..., "patch": [...]}`, the patch loading as - * `Patch.load` loads one. - */ - static async load(json: string, base = ''): Promise { - return new Data((await wasm.Data.load(json, base)) as wasm.Data); - } - - get name(): string { - return this.inner.name; - } - - /** The names of the tables. */ - get tables(): string[] { - return this.inner.tables(); - } - - /** Every row's id of `table`, in row order. */ - ids(table: string): Id[] { - return this.inner.ids(table) as Id[]; - } - - /** The names of `table`'s fields. */ - fields(table: string): string[] { - return this.inner.fields(table); - } - - /** The first and last axis coordinates the sampled fields span. */ - get span(): [number, number] | undefined { - const span = this.inner.span; - return span ? [span[0]!, span[1]!] : undefined; - } - - /** Applies every change of `patch`, or none when one is invalid. */ - apply(patch: Patch | Uint8Array): this { - this.inner.apply(patch instanceof Patch ? patch.frame() : patch); - return this; - } - - /** This version, kept while `apply` changes this one: O(1). */ - snapshot(): Data { - return new Data(this.inner.snapshot()); - } - - /** The value of `field` for the row with `id` in `table`, at axis coordinate `at` when given. */ - get(table: string, id: Id, field: string, at?: number): Value { - return JSON.parse(this.inner.get(table, id, field, at)) as Value; - } - - /** What the dataset holds: tables, rows, fields and their kinds, the axis and its span. */ - describe(): Description { - return JSON.parse(this.inner.describe()) as Description; - } - - /** - * The rows of a table `query` keeps, filtered, sorted, and paged, sampled fields read at its - * coordinate, on the CPU through the expressions marks draw with. - */ - query(query: Query): Table { - return this.inner.query(JSON.stringify(query)) as Table; - } - - /** - * Each row's least and greatest value of sampled `field` of `table` over `spans` equal spans of - * `range`, of every row or of rows `ids`: row after row, NaN where a span holds no frame. Reads the - * coarsest levels of detail that fit, so a span costs a few reads however many frames it covers. - */ - envelope( - table: string, - field: string, - range: [number, number], - spans: number, - ids?: readonly Id[], - ): Envelope { - const listed = ids === undefined ? undefined : JSON.stringify(ids); - return this.inner.envelope(table, field, Float64Array.from(range), spans, listed) as Envelope; - } - - /** The dataset as latkit's bytes: the patch that builds it. */ - toBytes(): Uint8Array { - return this.inner.toBytes(); - } -} - -/** The GPU device views draw with. Views of one GPU draw in one animation frame. */ -export class Gpu { - #inner?: wasm.Gpu; - #views = new Set(); - #canvases = new Set(); - #listeners = new Set<() => void>(); - - private constructor(inner: wasm.Gpu) { - this.#inner = inner; - } - - /** @internal */ - get inner(): wasm.Gpu { - if (!this.#inner) throw new Error('this Gpu has been destroyed'); - return this.#inner; - } - - /** Requests a WebGPU device: the first call takes the one made as latkit loaded. */ - static async create(): Promise { - const inner = first ?? wasm.Gpu.create(); - first = undefined; - return new Gpu(await inner); - } - - /** - * A view of `scene`; its marks draw once {@link View.setData} gives their data. A {@link Canvas} - * shows it, and a {@link Shot} draws it with or without one. - */ - view(scene: Scene): View { - const view: View = new View( - (tell) => this.inner.view(JSON.stringify(scene), tell), - () => this.#views.delete(view), - ); - this.#views.add(view); - return view; - } - - /** - * A canvas showing views in `panels` of `target`: a `` element, which sizes itself to its - * CSS box and passes each view the input landing on its panel, or an `OffscreenCanvas` in a - * worker, which keeps the size it is given at `pixelRatio` pixels a CSS pixel. Panels are - * `[x, y, width, height]` fractions, one per view; by default one view fills the canvas. - */ - canvas( - target: HTMLCanvasElement | OffscreenCanvas, - options: { panels?: readonly Panel[]; pixelRatio?: number } = {}, - ): Canvas { - const canvas: Canvas = new Canvas( - this.inner.canvas(target, JSON.stringify(options.panels ?? []), options.pixelRatio), - () => this.#canvases.delete(canvas), - ); - this.#canvases.add(canvas); - return canvas; - } - - /** - * Calls `listener` with the frame's timestamp every animation frame, before the views draw, so - * what it changes shows in that frame. Returns a function that stops it. - */ - on(type: 'frame', listener: (now: number) => void): () => void { - const handle = this.inner.onFrame(listener); - const stop = (): void => { - if (this.#listeners.delete(stop)) handle.free(); - }; - this.#listeners.add(stop); - return stop; - } - - /** Whether the device was lost; a new `Gpu` is needed. */ - get lost(): boolean { - return this.inner.lost; - } - - /** How much GPU memory data takes. */ - get budget(): Budget { - return JSON.parse(this.inner.budget) as Budget; - } - - set budget(budget: Budget) { - this.inner.setBudget(JSON.stringify(budget)); - } - - /** What this GPU did since it was created, counted exactly; see {@link Work}. */ - get work(): Work { - return JSON.parse(this.inner.work) as Work; - } - - /** Bytes on the GPU every view of it draws from: data, and what each view keeps per row. */ - get resident(): number { - return this.inner.resident; - } - - /** Bytes waiting to upload, for every view of this GPU. */ - get uploading(): number { - return this.inner.uploading; - } - - /** - * Draws one frame now at `now`, milliseconds on the clock animation frames give: the frame - * listeners, then every canvas that needs a frame. From the first step no animation frames are - * asked for: a harness, as tests and benchmarks are, steps every frame itself, so a run draws the - * same frames however fast the device is. - */ - step(now: number): void { - this.inner.step(now); - } - - /** - * Resolves once the GPU finished the work submitted to it, every readback landed, and every - * pipeline a view or shot asked for compiled: a frame stepped then draws all of it. A `timeout` - * error after `deadline` seconds. - */ - async done({ deadline: seconds = 30 } = {}): Promise { - await deadline(this.inner.done() as Promise, seconds, 'the GPU'); - } - - /** - * Resolves when work a view waits on lands after this call (a readback, a compile, a job, a text, - * a fetch), or a view asks for a frame. A `timeout` error after `deadline` seconds. - */ - async landed({ deadline: seconds = 30 } = {}): Promise { - await deadline(this.inner.landed() as Promise, seconds, 'work landing'); - } - - /** Releases this GPU's canvases, views, and frame listeners. Other GPUs and data stay usable. */ - destroy(): void { - if (!this.#inner) return; - for (const canvas of this.#canvases) canvas.destroy(); - for (const view of this.#views) view.destroy(); - for (const stop of this.#listeners) stop(); - this.#inner.free(); - this.#inner = undefined; - } - - [Symbol.dispose](): void { - this.destroy(); - } -} - -/** A view's panel of a canvas or a picture: `[x, y, width, height]` as fractions of it. */ -export type Panel = readonly [number, number, number, number]; - -/** Views shown in the panels of a `` or an `OffscreenCanvas`. */ -export class Canvas { - #inner?: wasm.Canvas; - - /** @internal */ - constructor( - inner: wasm.Canvas, - private readonly released: () => void, - ) { - this.#inner = inner; - } - - /** Shows `views`, one per panel, in place of those it showed. A view shows in one canvas at a time. */ - show(views: readonly View[]): this { - if (!this.#inner) throw new Error('this Canvas has been destroyed'); - this.#inner.show(views.map((v) => v.handle())); - return this; - } - - /** Stops drawing; the views remain, and the element keeps its last frame. */ - destroy(): void { - if (!this.#inner) return; - this.#inner.free(); - this.#inner = undefined; - this.released(); - } - - [Symbol.dispose](): void { - this.destroy(); - } -} - -/** A scene drawn from data it is given by name: shown by a canvas, which passes it input, or drawn by shots. */ -export class View { - #listeners = new Map void>>(); - #inner?: wasm.View; - - /** @internal Makes the view with what takes its events, a JSON batch at a time, from the start. */ - constructor( - make: (tell: (json: string) => void) => wasm.View, - private readonly released: () => void, - ) { - this.#inner = make((json) => this.#tell(json)); - } - - private get inner(): wasm.View { - if (!this.#inner) throw new Error('this View has been destroyed'); - return this.#inner; - } - - /** @internal The view, for a canvas to show or a shot to draw. */ - handle(): wasm.Handle { - return this.inner.handle(); - } - - /** Draws `scene` instead; an invalid scene throws and changes nothing. */ - show(scene: Scene): void { - this.inner.show(JSON.stringify(scene)); - } - - /** - * Gives the view `data` as it is now, replacing the dataset of its name: give it again after - * applying patches. A mark it no longer fits draws nothing, and the next frame reports why as an - * error, unless a scene shown before then replaces the mark. - */ - setData(data: Data): void { - this.inner.setData(data.inner); - } - - /** - * Fetches the data at `url` in pieces as the view asks for them, kept as dataset `name`, which the - * view owns; the source's first bytes say what it is. - * - * - Time tiles, a `frames.json` manifest as `latkit tile` writes it: a dataset whose sampled - * fields' histories are fetched in spans. A line tracing a field fetches the detail its plot - * window draws, a mark showing one the frames about the view's coordinate, and a span still - * loading draws the coarser span above it. - * - Vector tiles: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` template, fetched as - * the camera asks. Each layer is a table of its features of the kind marks draw it as (areas - * polygons, lines lines, vertices points), or named `layer:polygons`, `layer:lines`, or - * `layer:points`. - */ - setTiles(name: string, url: string): void { - this.inner.setTiles(name, url); - } - - /** Stops drawing the dataset named `name`, given or fetched as tiles. */ - removeData(name: string): void { - this.inner.removeData(name); - } - - /** The dataset named `name`, as given or as the view fetched it, as a snapshot. */ - data(name: string): Data | undefined { - const data = this.inner.data(name); - return data ? new Data(data) : undefined; - } - - /** The axis coordinate shown: sampled fields read their last frame at or before it. */ - get at(): number { - return this.inner.at; - } - - set at(at: number) { - this.inner.at = at; - } - - get camera(): Camera { - return JSON.parse(this.inner.camera) as Camera; - } - - set camera(camera: Camera) { - this.inner.setCamera(JSON.stringify(camera)); - } - - /** Eases to `camera`. */ - fly(camera: Camera): void { - this.inner.fly(JSON.stringify(camera)); - } - - /** Frames what `fit` names on the next frame, keeping pitch and bearing. */ - fit(fit: Fit = 'all'): void { - this.inner.fit(JSON.stringify(fit)); - } - - /** Where `position` (x, y, elevation) lands on screen, CSS pixels from the top left. */ - point(position: [number, number, number]): [number, number] | undefined { - const point = this.inner.point(Float64Array.from(position)); - return point ? [point[0]!, point[1]!] : undefined; - } - - /** The position on the ground under screen point `point`, if the view shows ground there. */ - ground(point: [number, number]): [number, number, number] | undefined { - const position = this.inner.ground(Float32Array.from(point)); - return position ? [position[0]!, position[1]!, position[2]!] : undefined; - } - - /** CSS pixels kept clear at the top, right, bottom, and left, as when panels cover the view. */ - get insets(): [number, number, number, number] { - const [top, right, bottom, left] = this.inner.insets; - return [top!, right!, bottom!, left!]; - } - - set insets(insets: [number, number, number, number]) { - this.inner.insets = Float32Array.from(insets); - } - - /** Degrees per second a map or plane turns around its center; 0 stops it. A diagram never turns. */ - get orbit(): number { - return this.inner.orbit; - } - - set orbit(degreesPerSecond: number) { - this.inner.orbit = degreesPerSecond; - } - - /** The rows selected, a set per table. */ - get selection(): Rows[] { - return JSON.parse(this.inner.selection) as Rows[]; - } - - /** - * Selects `rows`, and no others. Like every setter it tells nothing: `select` events tell what - * the user and the data changed. - */ - select(rows: readonly Rows[]): void { - this.inner.select(JSON.stringify(rows)); - } - - /** What the user points at: the row under the pointer, or the row the keys stepped to. */ - get hovered(): Hit | null { - return JSON.parse(this.inner.hovered) as Hit | null; - } - - /** Every mark that draws nothing because its data no longer fits it, and why. */ - get failures(): LatkitError[] { - return (JSON.parse(this.inner.failures) as Failure[]).map(thrown); - } - - /** - * Every row of a mark that picks within `radius` CSS pixels of `point`, nearest first, once a frame - * draws the view: a `timeout` error after `deadline` seconds without one. - */ - async pick( - point: [number, number], - radius: number, - { deadline: seconds = 5 } = {}, - ): Promise { - const hits = this.inner.pick(Float32Array.from(point), radius) as Promise; - return JSON.parse(await deadline(hits, seconds, 'a pick')) as Hit[]; - } - - /** - * Where `row` lies on screen, CSS pixels from the top left, or `null` when no mark draws it in - * view, once a frame draws the view: a `timeout` error after `deadline` seconds without one. - */ - locate(row: Row, { deadline: seconds = 5 } = {}): Promise<[number, number] | null> { - const point = this.inner.locate(JSON.stringify(row)) as Promise<[number, number] | null>; - return deadline(point, seconds, 'locating a row'); - } - - /** The rows joined to `row`, a set per table: a vertex's neighbors across edges and nets, or an - * edge's or a net's vertices. */ - neighbors(row: Row): Rows[] { - return JSON.parse(this.inner.neighbors(JSON.stringify(row))) as Rows[]; - } - - /** - * Lays every vertex row without a position out again by the scene's layout: a diagram's blocks, or - * points on a plane or the earth. Places a layout gave otherwise hold, so rows added later are - * placed around the rest. Once the layout lands, answers a patch to each dataset writing each row - * laid out into its mark's position fields; applied, the places are the data's own. A `timeout` - * error after `deadline` seconds without it. - * - * ```ts - * for (const patch of await view.arrange()) if (patch.data === data.name) data.apply(patch); - * ``` - */ - async arrange({ deadline: seconds = 60 } = {}): Promise { - const frames = (await deadline(this.inner.arrange(), seconds, 'arranging')) as Uint8Array[]; - return frames.map((frame) => Patch.fromBytes(frame)); - } - - get stats(): Stats { - return JSON.parse(this.inner.stats) as Stats; - } - - /** Whether the view wants a frame: it changed, or work it waited on landed. */ - get wantsFrame(): boolean { - return this.inner.wantsFrame; - } - - /** - * Calls `listener` with each event of `type` told while it listens; returns a function that - * stops it. What lasts is state to read: `hovered`, `selection`, `camera`, `stats.waiting`, and - * `failures`. - */ - on(type: T, listener: (event: ViewEvents[T]) => void): () => void { - let set = this.#listeners.get(type); - if (!set) this.#listeners.set(type, (set = new Set())); - set.add(listener); - return () => set.delete(listener); - } - - /** Hands a batch of the view's events to the listeners of each one's type. */ - #tell(json: string): void { - if (this.#listeners.size === 0) return; - for (const told of JSON.parse(json) as ViewEvent[]) { - const listeners = this.#listeners.get(told.type) as Set<(event: unknown) => void> | undefined; - if (!listeners?.size) continue; - const event = told.type === 'error' ? thrown(told.detail) : told.detail; - for (const listener of listeners) listener(event); - } - } - - /** Frees the view, and rejects what it was asked and has not answered; a canvas showing it keeps - * its last frame. */ - destroy(): void { - if (!this.#inner) return; - for (const listeners of this.#listeners.values()) listeners.clear(); - this.#listeners.clear(); - this.#inner.free(); - this.#inner = undefined; - this.released(); - } - - [Symbol.dispose](): void { - this.destroy(); - } -} - -type ShotSettings = Readonly<{ - scale: number; - panels: readonly Panel[]; - transparent: boolean; - deadline: number; - elapsed?: number; -}>; - -/** - * A picture of views, each drawn into its panel of `size` pixels at `scale` pixels per CSS pixel: - * plain data, like a scene. A view a shot draws holds its canvas's last frame meanwhile. - * - * ```ts - * const png = await (await new Shot([3840, 2160]).scale(2).image([view])).encode('png'); - * ``` - */ -export class Shot { - /** A picture with width and height in pixels. */ - constructor(size: readonly [number, number]); - /** @internal */ - constructor(size: readonly [number, number], settings: ShotSettings); - constructor( - readonly size: readonly [number, number], - /** @internal */ - readonly settings: ShotSettings = { scale: 1, panels: [], transparent: false, deadline: 30 }, - ) {} - - /** Draws `scale` pixels per CSS pixel: a view lays out `size / scale` CSS pixels. */ - scale(scale: number): Shot { - return new Shot(this.size, { ...this.settings, scale }); - } - - /** Where each view draws, `[x, y, width, height]` as fractions, one per view in order; by default one fills it. */ - panels(panels: readonly Panel[]): Shot { - return new Shot(this.size, { ...this.settings, panels }); - } - - /** Keeps the picture's alpha: views clear to transparent where their themes' backgrounds are. */ - transparent(): Shot { - return new Shot(this.size, { ...this.settings, transparent: true }); - } - - /** The shot's deadline in seconds, including readback; rejects on timeout. Defaults to 30. */ - deadline(seconds: number): Shot { - return new Shot(this.size, { ...this.settings, deadline: seconds }); - } - - /** Fixes elapsed-time expressions and drawing effects at `seconds` while transitions settle. */ - elapsed(seconds: number): Shot { - return new Shot(this.size, { ...this.settings, elapsed: seconds }); - } - - /** The picture of `views`, one per panel, once every one is complete and still. */ - async image(views: readonly View[]): Promise { - return new Image( - (await wasm.image( - this.#config(), - views.map((v) => v.handle()), - )) as wasm.Image, - ); - } - - /** - * A video of these pictures at `fps` frames a second, H.264 in MP4, written to `output` as it is - * drawn, at `bitrate` bits a second or about 0.15 bits a pixel a frame. Width and height are - * multiples of 4 and 2. `signal` aborts it: the output is aborted and the views are given back. - */ - video( - gpu: Gpu, - fps: number, - output: WritableStream, - options: { bitrate?: number; signal?: AbortSignal } = {}, - ): Video { - options.signal?.throwIfAborted(); - const writer = output.getWriter(); - try { - return new Video( - gpu.inner.video(this.#config(), fps, options.bitrate), - writer, - fps, - options.signal, - ); - } catch (error) { - writer.releaseLock(); - throw error; - } - } - - /** The shot as the crate reads it: JSON, times in seconds. */ - #config(): string { - return JSON.stringify({ size: [...this.size], ...this.settings }); - } -} - -/** RGBA pixels, straight alpha, rows top to bottom. */ -export class Image { - #inner?: wasm.Image; - - /** @internal */ - constructor(inner: wasm.Image) { - this.#inner = inner; - } - - private get inner(): wasm.Image { - if (!this.#inner) throw new Error('this Image has been destroyed'); - return this.#inner; - } - - get width(): number { - return this.inner.width; - } - - get height(): number { - return this.inner.height; - } - - /** The pixels, copied. */ - get rgba(): Uint8Array { - return this.inner.rgba; - } - - /** - * The image as a file, written in a worker: PNG, lossless with alpha; JPEG at `quality` from 1 to - * 100 (90 by default), small, its alpha dropped; or WebP, lossless with alpha, smaller than PNG. - */ - async encode(format: ImageFormat, options: { quality?: number } = {}): Promise { - return (await this.inner.encode(format, options.quality)) as Uint8Array; - } - - destroy(): void { - this.#inner?.free(); - this.#inner = undefined; - } - - [Symbol.dispose](): void { - this.destroy(); - } -} - -/** How an image is written as a file. */ -export type ImageFormat = 'png' | 'jpeg' | 'webp'; - -/** A video being drawn, encoded, and written. */ -export class Video { - #inner?: wasm.Video; - #state: 'open' | 'finishing' | 'aborting' | 'closed' = 'open'; - #operation?: Promise; - #finished?: Promise; - #aborted?: Promise; - #reason: unknown; - #frames = 0; - #removeAbort?: () => void; - - /** @internal */ - constructor( - inner: wasm.Video, - private readonly writer: WritableStreamDefaultWriter, - private readonly fps: number, - signal?: AbortSignal, - ) { - this.#inner = inner; - if (signal) { - const abort = () => { - void this.abort(signal.reason); - }; - signal.addEventListener('abort', abort, { once: true }); - this.#removeAbort = () => signal.removeEventListener('abort', abort); - if (signal.aborted) abort(); - } - } - - #check(): wasm.Video { - if (!this.#inner || this.#state === 'aborting' || this.#state === 'closed') - throw Object.assign( - thrown({ code: 'cancelled', message: 'this Video was finished or aborted' }), - { cause: this.#reason }, - ); - return this.#inner; - } - - /** Seconds into the video the next frame shows at; retained after completion. */ - get time(): number { - return this.frames / this.fps; - } - - /** Frames drawn or held so far; retained after completion. */ - get frames(): number { - return this.#inner?.frames ?? this.#frames; - } - - /** Draws and writes one frame, waiting for output backpressure. Only one frame may run at a time. */ - frame(views: readonly View[]): Promise { - if (this.#state !== 'open' || this.#operation) - return Promise.reject(new Error('a Video draws one frame at a time, before finish')); - const operation = (async () => { - const bytes = (await this.#check().frame(views.map((v) => v.handle()))) as Uint8Array; - this.#frames = this.#check().frames; - if (bytes.length) await this.writer.write(bytes); - })(); - this.#operation = operation; - return operation - .catch(async (error: unknown) => { - await this.abort(error); - throw error; - }) - .finally(() => { - if (this.#operation === operation) this.#operation = undefined; - }); - } - - /** Records until `step(time)` returns false; any step, drawing, output, or progress error aborts it. */ - async record( - views: readonly View[], - step: (time: number) => boolean | Promise, - onProgress?: (progress: { frames: number; time: number }) => void, - ): Promise { - try { - while (await step(this.time)) { - await this.frame(views); - onProgress?.({ frames: this.frames, time: this.time }); - } - await this.finish(); - } catch (error) { - await this.abort(error); - throw error; - } - } - - /** Finishes pending work, writes the end of the file, and closes and unlocks the output. Idempotent. */ - finish(): Promise { - if (this.#finished) return this.#finished; - if (this.#state !== 'open') return Promise.reject(new Error('this Video was aborted')); - this.#state = 'finishing'; - const frame = this.#operation; - const operation = (async () => { - await frame; - const bytes = (await this.#check().finish()) as Uint8Array; - this.#check(); - if (bytes.length) await this.writer.write(bytes); - this.#check(); - await this.writer.close(); - })(); - this.#operation = operation; - this.#finished = operation - .then(() => { - if (this.#state === 'finishing') this.#release(); - }) - .catch(async (error: unknown) => { - await this.abort(error); - throw error; - }); - return this.#finished; - } - - /** Cancels drawing and encoding, aborts the output, and unlocks it after pending writes settle. Idempotent. */ - abort(reason?: unknown): Promise { - if (this.#aborted) return this.#aborted; - if (this.#state === 'closed') return Promise.resolve(); - this.#state = 'aborting'; - this.#reason = reason; - this.#frames = this.frames; - this.#inner?.cancel(); - this.#aborted = Promise.allSettled([this.#operation, this.writer.abort(reason)]).then(() => - this.#release(), - ); - return this.#aborted; - } - - #release(): void { - this.#frames = this.frames; - this.#inner?.free(); - this.#inner = undefined; - this.#removeAbort?.(); - this.#removeAbort = undefined; - this.writer.releaseLock(); - this.#state = 'closed'; - } -} -/** - * A CSS gradient through `map`, a catalog name or colors of your own, for legends: the colors the - * GPU draws, a band each for a categorical map's categories. - */ -export function colormapCss(map: Colormap, direction = 'to right'): string { - return wasm.colormapCss(JSON.stringify(map), direction); -} diff --git a/npm/src/scene.ts b/npm/src/scene.ts index 535876ea..0e2ae379 100644 --- a/npm/src/scene.ts +++ b/npm/src/scene.ts @@ -1,26 +1,36 @@ /** - * The expression builders that write scenes' options. A scene is plain data the view reads, its - * types written from the forms latkit reads (`inputs.ts`); nothing here needs WebAssembly. + * The builders that write scenes' options. A scene is plain data the view reads, its types written + * from the forms latkit reads (`inputs.ts`); nothing here needs WebAssembly. */ import type { Binary, Colormap, + ColormapNode, ExprNode, FieldNode, Marker, - Size, + MissingNode, + PaintNode, + Px, + ScaleNode, State, Unary, + Units, } from './inputs.js'; -/** A CSS color, such as `'#ffb000'` or `'rebeccapurple'`. Where an option takes an expression, a - * string starting with `#` is a hex color and any other string names a field. */ +/** A CSS color, such as `'#ffb000'` or `'rebeccapurple'`: what a string is in a color option. */ export type Color = string; -/** What an expression is written as: an {@link Expr}, or its JSON form. */ +/** What a value is written as: an {@link Expr}, or its JSON form, in which a string names a field. */ export type ExprInput = Expr | ExprNode; +/** + * What a color is written as: a CSS color, a {@link Paint}, or its JSON form. A string is a CSS + * color here; a field reads with {@link field}. + */ +export type PaintInput = Color | Paint | PaintNode; + const nodeOf = (input: ExprInput): ExprNode => (input instanceof Expr ? input.node : input); /** A value for every row of a mark: a constant, a field, view state, or arithmetic of them. */ @@ -33,8 +43,8 @@ export class Expr { } /** Reads this expression's fields from `table` of dataset `data`, joined by row id. */ - of(data: string, table: string): Expr { - return new Expr(mapFields(this.node, (f) => ({ ...f, of: [data, table] }))); + from(data: string, table: string): Expr { + return new Expr(mapFields(this.node, (f) => ({ ...f, from: [data, table] }))); } /** Reads sampled fields at their last frame without interpolating. */ @@ -126,14 +136,14 @@ export class Expr { return new Expr({ is: [this.node, text] }); } - /** Whether a text field holds `text`, ignoring case. Queries read it; marks do not. */ + /** Whether a text field holds `text`, ignoring case: in a mark, a field of its own table. */ contains(text: string): Expr { return new Expr({ contains: [this.node, text] }); } - /** The value through `map`: a catalog name, or your own colors. */ - colormap(map: Colormap): Mapped { - return new Mapped({ colormap: map, value: this.node }); + /** The value through `map`, a color for every row: a catalog name, or your own colors. */ + colormap(map: Colormap): Colormapped { + return new Colormapped({ colormap: map, value: this.node }); } /** The value mapped linearly onto `[low, high]`, clamped. */ @@ -142,24 +152,56 @@ export class Expr { } } -/** A value through a colormap or onto a range, whose domain can be set. */ +/** A value mapped onto a range, whose domain can be set. */ export class Mapped extends Expr { /** The input values that map to the ends; it fits the values shown unless set. */ domain(low: number, high: number): Mapped { - return new Mapped({ ...(this.node as MappedNode), domain: [low, high] }); + return new Mapped({ ...(this.node as ScaleNode), domain: [low, high] }); + } +} + +/** + * A color for every row of a mark: a CSS color, a value through a colormap, or a conditional choosing + * between colors. Color options take one, or a CSS color as it is. + */ +export class Paint { + /** A color from its JSON form; use the builders for common colors. */ + constructor(readonly node: PaintNode) {} + + toJSON(): PaintNode { + return this.node; + } +} + +/** A value through a colormap, whose domain can be set. */ +export class Colormapped extends Paint { + /** The input values that map to the ends; it fits the values shown unless set. */ + domain(low: number, high: number): Colormapped { + return new Colormapped({ ...(this.node as ColormapNode), domain: [low, high] }); } } -type MappedNode = Extract; +/** A missing value, which options take the theme's for: a value and a color alike. */ +export class Missing extends Expr { + declare readonly node: MissingNode; + + constructor() { + super({ missing: true }); + } + + override toJSON(): MissingNode { + return this.node; + } +} const unary = (op: Unary, a: ExprInput): Expr => new Expr({ op, args: [nodeOf(a)] }); const binary = (op: Binary, a: ExprInput, b: ExprInput): Expr => new Expr({ op, args: [nodeOf(a), nodeOf(b)] }); -/** `node` with every field read through `f`; a string starting with `#` is a color, not a field. */ +/** `node` with every field read through `f`. */ function mapFields(input: ExprInput, f: (field: FieldNode) => FieldNode): ExprNode { const node = nodeOf(input); - if (typeof node === 'string') return node.startsWith('#') ? node : f({ field: node }); + if (typeof node === 'string') return f({ field: node }); if (typeof node !== 'object') return node; const map = (child: ExprInput) => mapFields(child, f); if ('field' in node) return f(node); @@ -179,7 +221,7 @@ const state = (name: State): Expr => new Expr({ state: name }); /** The field `name` of each row. */ export const field = (name: string): Expr => new Expr({ field: name }); /** A color constant. */ -export const color = (css: Color): Expr => new Expr({ color: css }); +export const color = (css: Color): Paint => new Paint({ color: css }); /** Whether the row is selected. */ export const selected = (): Expr => state('selected'); /** Whether the pointer is over the row. */ @@ -191,42 +233,118 @@ export const elapsed = (): Expr => state('elapsed'); /** The camera's zoom: the web mercator level on the earth, log₂ CSS pixels per unit elsewhere. */ export const zoom = (): Expr => state('zoom'); /** A missing value: options that read one take the theme's. */ -export const missing = (): Expr => new Expr({ missing: true }); +export const missing = (): Missing => new Missing(); + +/** The CSS pixels a size in position units keeps within. */ +export interface Within { + /** CSS pixels it keeps at least, however far the view zooms out. */ + atLeast?: number; + /** CSS pixels it keeps at most, however far the view zooms in. */ + atMost?: number; +} + /** - * A size in position units, so it scales as the view zooms, kept within `atLeast` and `atMost` CSS - * pixels when given: `units('r', { atLeast: 2 })` grows with the map and never vanishes. A marker - * keeps its proportions, its larger side held. + * A size in CSS pixels, whatever the space's own unit: a stroke keeps its width on screen as a + * diagram zooms. A number makes a theme's length too. */ -export const units = (size: ExprInput, px: { atLeast?: number; atMost?: number } = {}): Size => ({ - units: nodeOf(size), - ...px, -}); +export function px(size: number): { px: number }; +export function px(size: ExprInput): Px; +export function px(size: ExprInput): Px { + return { px: nodeOf(size) }; +} -/** Starts a conditional: `when(c).then(a).when(d).then(b).otherwise(e)`. */ +/** + * A size in position units, whatever the space's own unit, so it scales as the view zooms, kept + * within `atLeast` and `atMost` CSS pixels when given: `units('r', { atLeast: 2 })` grows with the map + * and never vanishes. A marker keeps its proportions, its larger side held. A number alone makes a + * theme's length too. + */ +export function units(size: number): { units: number }; +export function units(size: ExprInput, within?: Within): Units; +export function units(size: ExprInput, within: Within = {}): Units { + return { units: nodeOf(size), ...within }; +} + +/** A case of a conditional: its condition, and its value or color. */ +type Case = [ExprNode, ExprNode | PaintNode]; + +/** What a conditional's values are written as, a value's or a color's. */ +type InputOf = T extends Paint ? PaintInput : ExprInput; + +/** + * Starts a conditional: `when(c).then(a).when(d).then(b).otherwise(e)`. Its values are colors, a + * string a CSS color, where its first is one; else values. + */ export function when(condition: ExprInput): When { - return new When([], nodeOf(condition)); + return new When(nodeOf(condition)); } +/** A conditional's first case, waiting for its value where the condition holds. */ class When { + /** @internal */ + constructor(private readonly condition: ExprNode) {} + + /** The color where the condition holds: every value of the conditional is a color. */ + then(value: PaintInput): Then; + /** The value where the condition holds. */ + then(value: ExprInput): Then; + then(value: PaintInput | ExprInput): Then | Then { + const cases: Case[] = [[this.condition, written(value)]]; + return colored(value) + ? new Then(cases, (node) => new Paint(node as PaintNode)) + : new Then(cases, (node) => new Expr(node as ExprNode)); + } +} + +/** A conditional's cases so far, waiting for another or the value where none holds. */ +class Then { + /** @internal */ constructor( - private readonly cases: [ExprNode, ExprNode][], - private readonly condition: ExprNode, + private readonly cases: readonly Case[], + private readonly make: (node: object) => T, ) {} - then(value: ExprInput): Then { - return new Then([...this.cases, [this.condition, nodeOf(value)]]); + + /** Another case, tried when those before it do not hold. */ + when(condition: ExprInput): Next { + return new Next(this.cases, nodeOf(condition), this.make); } -} -class Then { - constructor(private readonly cases: [ExprNode, ExprNode][]) {} - when(condition: ExprInput): When { - return new When(this.cases, nodeOf(condition)); + /** The value where no case holds. */ + otherwise(value: InputOf): T { + return this.make({ when: this.cases, otherwise: written(value) }); } - otherwise(value: ExprInput): Expr { - return new Expr({ when: this.cases, otherwise: nodeOf(value) }); +} + +/** A conditional's next case, waiting for its value where the condition holds. */ +class Next { + /** @internal */ + constructor( + private readonly cases: readonly Case[], + private readonly condition: ExprNode, + private readonly make: (node: object) => T, + ) {} + + /** The value where the condition holds. */ + then(value: InputOf): Then { + return new Then([...this.cases, [this.condition, written(value)]], this.make); } } +/** `value` as it is written. */ +function written(value: ExprInput | PaintInput): ExprNode | PaintNode { + return value instanceof Expr || value instanceof Paint ? value.node : value; +} + +/** + * Whether a conditional's first value makes it choose colors, as the types say: a CSS color, a color, + * a missing value, or a color's own form. + */ +function colored(value: ExprInput | PaintInput): boolean { + if (typeof value === 'string' || value instanceof Paint || value instanceof Missing) return true; + if (value instanceof Expr || typeof value !== 'object') return false; + return 'color' in value || 'colormap' in value || 'missing' in value; +} + /** Straight-alpha RGBA pixels, compatible with browser ImageData and server-produced pixels. */ export interface RgbaImage { width: number; diff --git a/npm/src/shot.ts b/npm/src/shot.ts new file mode 100644 index 00000000..39261a55 --- /dev/null +++ b/npm/src/shot.ts @@ -0,0 +1,308 @@ +/** Pictures and videos of views: images to encode, and H.264 video written as it is drawn. */ + +import type * as bindings from '../pkg/latkit.js'; +import { placed, type Panel } from './canvas.js'; +import { hold, inner, release, thrown, waited, wasm, type Wait } from './internal.js'; +import { handleOf } from './view.js'; + +/** How a picture is taken. */ +export interface ShotOptions { + /** Pixels per CSS pixel: a view lays out `size / scale` CSS pixels, so 2 draws twice as sharp. 1 unless given. */ + scale?: number; + /** Keeps alpha where the themes' backgrounds would be. */ + transparent?: boolean; + /** + * When the picture is taken: once its views are complete and still, transitions, flights, and + * label fades run on until they land (`still`, the default); or once they are complete, as they + * stand now, what moves held where it is (`now`). + */ + capture?: 'still' | 'now'; + /** A still's effect time in seconds, where elapsed time and flowing comets read it. */ + elapsed?: number; +} + +/** + * A picture of views, each drawn into its panel of `size` pixels as `layout` places it: drawn while a + * canvas showing it holds its last frame, each view's data as it was when the picture began. A + * canvas's `shot()` takes one of what it shows. + * + * ```ts + * const png = await (await canvas.shot({ scale: 2 }).image()).encode('png'); + * ``` + */ +export class Shot { + /** A picture `size` pixels wide and high, of the views of `options.layout`. */ + constructor( + readonly size: readonly [number, number], + readonly options: ShotOptions & { layout?: readonly Panel[] } = {}, + ) {} + + /** The picture, once every view is complete as `capture` says. */ + async image(wait: Wait = {}): Promise { + const taken = wasm().image(this.#json(this.size), this.#views()); + const image = await waited(taken, wait, 'a picture'); + return Image.made(image); + } + + /** + * A video of these pictures at `fps` frames a second, H.264 in MP4, written to `output` as it is + * drawn, at `bitrate` bits a second or about 0.15 bits a pixel a frame. Its width rounds down to a + * multiple of 4 and its height of 2, the blocks H.264 takes. `signal` aborts it: the output is + * aborted and the views are given back. + */ + video({ + fps, + output, + bitrate, + signal, + }: { fps: number; output: WritableStream; bitrate?: number } & Wait): Video { + signal?.throwIfAborted(); + const writer = output.getWriter(); + try { + const [width, height] = this.size; + const blocks = [ + Math.max(4, width - (width % 4)), + Math.max(2, height - (height % 2)), + ] as const; + return Video.made( + wasm().video(this.#json(blocks), this.#views(), fps, bitrate), + writer, + fps, + signal, + ); + } catch (error) { + writer.releaseLock(); + throw error; + } + } + + /** The shot of `size` pixels as the bindings read it: JSON, times in seconds. */ + #json(size: readonly [number, number]): string { + const { + scale = 1, + transparent = false, + capture = 'still', + elapsed, + layout = [], + } = this.options; + const placing = Array.from(placed(layout)); + const panels = Array.from({ length: layout.length }, (_, i) => placing.slice(i * 4, i * 4 + 4)); + return JSON.stringify({ size: [...size], scale, panels, transparent, capture, elapsed }); + } + + #views(): bindings.Handle[] { + return (this.options.layout ?? []).map(({ view }) => handleOf(view)); + } +} + +/** RGBA pixels, straight alpha, rows top to bottom. */ +export class Image { + private constructor(image: bindings.Image) { + hold(this, image); + } + + /** @internal An image of the bindings' `image`. */ + static made(image: bindings.Image): Image { + return new Image(image); + } + + get width(): number { + return pixels(this).width; + } + + get height(): number { + return pixels(this).height; + } + + /** The pixels, copied. */ + get rgba(): Uint8Array { + return pixels(this).rgba; + } + + /** + * The image as a file, written in a worker: PNG, lossless with alpha; JPEG at `quality` from 1 to + * 100 (90 by default), small, its alpha dropped; or WebP, lossless with alpha, smaller than PNG. + */ + encode(format: ImageFormat, options: { quality?: number } & Wait = {}): Promise { + return waited( + pixels(this).encode(format, options.quality), + options, + 'writing an image', + ); + } + + destroy(): void { + release(this)?.free(); + } + + [Symbol.dispose](): void { + this.destroy(); + } +} + +function pixels(owner: Image): bindings.Image { + return inner(owner, 'Image'); +} + +/** How an image is written as a file. */ +export type ImageFormat = 'png' | 'jpeg' | 'webp'; + +/** A video being drawn, encoded, and written. */ +export class Video { + #state: 'open' | 'finishing' | 'aborting' | 'closed' = 'open'; + #operation?: Promise; + #finished?: Promise; + #aborted?: Promise; + #reason: unknown; + #frames = 0; + #removeAbort?: () => void; + #writer: WritableStreamDefaultWriter; + #fps: number; + + private constructor( + video: bindings.Video, + writer: WritableStreamDefaultWriter, + fps: number, + signal?: AbortSignal, + ) { + hold(this, video); + this.#writer = writer; + this.#fps = fps; + if (signal) { + const abort = () => { + void this.abort(signal.reason); + }; + signal.addEventListener('abort', abort, { once: true }); + this.#removeAbort = () => signal.removeEventListener('abort', abort); + if (signal.aborted) abort(); + } + } + + /** @internal A video of the bindings' `video`, written to `writer`. */ + static made( + video: bindings.Video, + writer: WritableStreamDefaultWriter, + fps: number, + signal?: AbortSignal, + ): Video { + return new Video(video, writer, fps, signal); + } + + #check(): bindings.Video { + const video = + this.#state === 'aborting' || this.#state === 'closed' ? undefined : encoder(this); + if (!video) + throw Object.assign( + thrown({ code: 'cancelled', message: 'this Video was finished or aborted' }), + { + cause: this.#reason, + }, + ); + return video; + } + + /** Seconds into the video the next frame shows at; kept once it is done. */ + get time(): number { + return this.frames / this.#fps; + } + + /** Frames drawn or held so far; kept once it is done. */ + get frames(): number { + return encoder(this)?.frames ?? this.#frames; + } + + /** Draws and writes one frame, waiting for the output's backpressure. One frame draws at a time. */ + frame(): Promise { + if (this.#state !== 'open' || this.#operation) + return Promise.reject(new Error('a Video draws one frame at a time, before it finishes')); + const operation = (async () => { + const bytes = (await this.#check().frame()) as Uint8Array; + this.#frames = this.#check().frames; + if (bytes.length) await this.#writer.write(bytes); + })(); + this.#operation = operation; + return operation + .catch(async (error: unknown) => { + await this.abort(error); + throw error; + }) + .finally(() => { + if (this.#operation === operation) this.#operation = undefined; + }); + } + + /** Records until `step(time)` returns false; any step, drawing, output, or progress error aborts it. */ + async record( + step: (time: number) => boolean | Promise, + onProgress?: (progress: { frames: number; time: number }) => void, + ): Promise { + try { + while (await step(this.time)) { + await this.frame(); + onProgress?.({ frames: this.frames, time: this.time }); + } + await this.finish(); + } catch (error) { + await this.abort(error); + throw error; + } + } + + /** Finishes pending work, writes the end of the file, and closes and unlocks the output. Idempotent. */ + finish(): Promise { + if (this.#finished) return this.#finished; + if (this.#state !== 'open') return Promise.reject(new Error('this Video was aborted')); + this.#state = 'finishing'; + const frame = this.#operation; + const operation = (async () => { + await frame; + const bytes = (await this.#check().finish()) as Uint8Array; + this.#check(); + if (bytes.length) await this.#writer.write(bytes); + this.#check(); + await this.#writer.close(); + })(); + this.#operation = operation; + this.#finished = operation + .then(() => { + if (this.#state === 'finishing') this.#release(); + }) + .catch(async (error: unknown) => { + await this.abort(error); + throw error; + }); + return this.#finished; + } + + /** Cancels drawing and encoding, aborts the output, and unlocks it once pending writes settle. Idempotent. */ + abort(reason?: unknown): Promise { + if (this.#aborted) return this.#aborted; + if (this.#state === 'closed') return Promise.resolve(); + this.#state = 'aborting'; + this.#reason = reason; + this.#frames = this.frames; + encoder(this)?.cancel(); + this.#aborted = Promise.allSettled([this.#operation, this.#writer.abort(reason)]).then(() => + this.#release(), + ); + return this.#aborted; + } + + #release(): void { + this.#frames = this.frames; + release(this)?.free(); + this.#removeAbort?.(); + this.#removeAbort = undefined; + this.#writer.releaseLock(); + this.#state = 'closed'; + } +} + +/** The video's encoder, while it has one. */ +function encoder(owner: Video): bindings.Video | undefined { + try { + return inner(owner, 'Video'); + } catch { + return undefined; + } +} diff --git a/npm/src/view.ts b/npm/src/view.ts new file mode 100644 index 00000000..364039de --- /dev/null +++ b/npm/src/view.ts @@ -0,0 +1,275 @@ +/** + * Views: a scene drawn from the sources it is given. What a view keeps is a property, read and set + * alike, setters silent; what the user changes is told by an event named like its property; what the + * view is asked is a method, answered by the frames that draw it. + */ + +import type * as bindings from '../pkg/latkit.js'; +import { snapshotOf, sourceOf, type Snapshot, type Source } from './data.js'; +import type { Camera, Fit, Row, Rows, Scene } from './inputs.js'; +import { hold, inner, release, thrown, waited, type LatkitError, type Wait } from './internal.js'; +import type { Failure, Hit, Stats, ViewEvent, Waiting } from './outputs.js'; +import { Patch } from './patch.js'; + +/** + * What a view tells its listeners, by type. Intents (`click`, `open`, `menu`, `drag`, `connect`, + * `delete`) are told once each, in the order the user acted. Changes (`hovered`, `selection`, + * `camera`, `waiting`) are named like the property they changed, and tell only their latest. A view + * tells only what the application did not do itself: its setters change state silently. + */ +export type ViewEvents = { + [E in ViewEvent as E['type']]: E extends { type: 'error' } ? LatkitError : E['detail']; +}; + +/** What a view is made with: the sources of the datasets its scene names. */ +export interface ViewOptions { + sources?: readonly Source[]; +} + +/** CSS pixels kept clear at the top, right, bottom, and left. */ +export type Insets = [top: number, right: number, bottom: number, left: number]; + +/** A point on screen: CSS pixels from the top left. */ +export type Point = [x: number, y: number]; + +/** A position in the space's coordinates: x, y, and elevation. */ +export type Position = [x: number, y: number, elevation: number]; + +/** A scene drawn from the sources it is given: shown by a canvas, which passes it input, or drawn by shots. */ +export class View { + #listeners = new Map void>>(); + #scene: Scene; + #sources: readonly Source[] = []; + #released: () => void; + + private constructor(device: bindings.Gpu, scene: Scene, released: () => void) { + hold( + this, + device.view(JSON.stringify(scene), (json: string) => this.#tell(json)), + ); + this.#scene = scene; + this.#released = released; + } + + /** @internal A view of `scene` drawn with `device`, which calls `released` once destroyed. */ + static made(device: bindings.Gpu, scene: Scene, released: () => void): View { + return new View(device, scene, released); + } + + /** The scene drawn; setting an invalid one throws and keeps the scene drawn. */ + get scene(): Scene { + return this.#scene; + } + + set scene(scene: Scene) { + view(this).setScene(JSON.stringify(scene)); + this.#scene = scene; + } + + /** + * What the view draws, each a dataset by name: a `Data`, its latest version from the frame after + * each patch applies; a `Snapshot`, as it is; `Tiles`, the pieces the view asks for. A mark its + * dataset no longer fits draws nothing, and the next frame tells why. Two sources of one name are + * invalid. + */ + get sources(): readonly Source[] { + return this.#sources; + } + + set sources(sources: readonly Source[]) { + view(this).setSources(sources.map(sourceOf)); + this.#sources = [...sources]; + } + + /** The axis coordinate shown: sampled fields read their last frame at or before it. */ + get at(): number { + return view(this).at; + } + + set at(at: number) { + view(this).at = at; + } + + /** Where the view looks; setting it looks there at once. */ + get camera(): Camera { + return JSON.parse(view(this).camera) as Camera; + } + + set camera(camera: Camera) { + view(this).setCamera(JSON.stringify(camera)); + } + + /** CSS pixels kept clear at the top, right, bottom, and left, as when panels cover the view. */ + get insets(): Insets { + const [top, right, bottom, left] = view(this).insets; + return [top!, right!, bottom!, left!]; + } + + set insets(insets: Insets) { + view(this).insets = Float32Array.from(insets); + } + + /** Degrees per second a map or plane turns around its center; 0 stops it. A diagram never turns. */ + get orbit(): number { + return view(this).orbit; + } + + set orbit(degreesPerSecond: number) { + view(this).orbit = degreesPerSecond; + } + + /** The rows selected, a set per table; setting it selects those and no others. */ + get selection(): readonly Rows[] { + return JSON.parse(view(this).selection) as Rows[]; + } + + set selection(rows: readonly Rows[]) { + view(this).setSelection(JSON.stringify(rows)); + } + + /** What the user points at: the row under the pointer, or the row the keys stepped to. */ + get hovered(): Hit | null { + return JSON.parse(view(this).hovered) as Hit | null; + } + + /** The work under way the last frame stood in for; empty once it is complete. */ + get waiting(): Waiting { + return JSON.parse(view(this).waiting) as Waiting; + } + + /** + * Whether the last frame drew everything as the data, scene, and camera say, with nothing standing + * in for work under way: what `waiting` lists. A shot waits for it. + */ + get complete(): boolean { + return view(this).complete; + } + + /** Every mark that draws nothing because its data no longer fits it, and why. */ + get failures(): readonly LatkitError[] { + return (JSON.parse(view(this).failures) as Failure[]).map(thrown); + } + + /** What the view does each frame. */ + get stats(): Stats { + return JSON.parse(view(this).stats) as Stats; + } + + /** Whether the view wants a frame: it changed, or work it waited on landed. */ + get wantsFrame(): boolean { + return view(this).wantsFrame; + } + + /** Frames what `fit` names on the next frame, keeping pitch and bearing. */ + fit(fit: Fit = 'all'): void { + view(this).fit(JSON.stringify(fit)); + } + + /** Eases to `camera`. */ + fly(camera: Camera): void { + view(this).fly(JSON.stringify(camera)); + } + + /** Where `position` lands on screen; `null` behind the camera. */ + screen(position: Position): Point | null { + const point = view(this).screen(Float64Array.from(position)); + return point ? [point[0]!, point[1]!] : null; + } + + /** The position on the ground under screen point `point`; `null` where the view shows none there. */ + position(point: Point): Position | null { + const position = view(this).position(Float32Array.from(point)); + return position ? [position[0]!, position[1]!, position[2]!] : null; + } + + /** Where `row` lies on screen, or `null` when no mark draws it in view, once a frame draws the view. */ + screenOf(row: Row, wait: Wait = {}): Promise { + return waited(view(this).screenOf(JSON.stringify(row)), wait, 'locating a row'); + } + + /** + * Every row of a mark that picks within `radius` CSS pixels (0 unless given) of `point`, nearest + * first, once a frame draws the view. + */ + async pick(point: Point, options: { radius?: number } & Wait = {}): Promise { + const hits = view(this).pick(Float32Array.from(point), options.radius ?? 0); + return JSON.parse(await waited(hits, options, 'a pick')) as Hit[]; + } + + /** + * The rows joined to `row`, a set per table: a vertex's neighbors across edges and nets, or an + * edge's or a net's vertices. + */ + neighbors(row: Row): Rows[] { + return JSON.parse(view(this).neighbors(JSON.stringify(row))) as Rows[]; + } + + /** + * Lays every vertex row without a position out again by the scene's layout: a diagram's blocks, or + * points on a plane or the earth. Places a layout gave otherwise hold, so rows added later are + * placed around the rest. Once the layout lands, answers a patch to each dataset writing each row + * laid out into its mark's position fields; applied, the places are the data's own. + * + * ```ts + * for (const patch of await view.arrange()) if (patch.data === grid.name) grid.apply(patch); + * ``` + */ + async arrange(wait: Wait = {}): Promise { + const frames = await waited(view(this).arrange(), wait, 'arranging'); + return Promise.all(frames.map((frame) => Patch.read(frame))); + } + + /** + * The version of dataset `name` the view draws: the latest version of a `Data` it took, a snapshot + * given, or what it holds of `Tiles`, the pieces its camera asked for. + */ + snapshot(name: string): Snapshot | undefined { + const snapshot = view(this).snapshot(name); + return snapshot ? snapshotOf(snapshot) : undefined; + } + + /** + * Calls `listener` with each event of `type` told while it listens; returns a function that stops + * it. What lasts is state to read, as the property each change is named for. + */ + on(type: T, listener: (event: ViewEvents[T]) => void): () => void { + let set = this.#listeners.get(type); + if (!set) this.#listeners.set(type, (set = new Set())); + set.add(listener); + return () => set.delete(listener); + } + + /** Hands a batch of the view's events to the listeners of each one's type. */ + #tell(json: string): void { + if (this.#listeners.size === 0) return; + for (const told of JSON.parse(json) as ViewEvent[]) { + const listeners = this.#listeners.get(told.type) as Set<(event: unknown) => void> | undefined; + if (!listeners?.size) continue; + const event = told.type === 'error' ? thrown(told.detail) : told.detail; + for (const listener of listeners) listener(event); + } + } + + /** Frees the view, rejecting what it was asked and has not answered; a canvas showing it keeps its last frame. */ + destroy(): void { + const freed = release(this); + if (!freed) return; + for (const listeners of this.#listeners.values()) listeners.clear(); + this.#listeners.clear(); + freed.free(); + this.#released(); + } + + [Symbol.dispose](): void { + this.destroy(); + } +} + +function view(owner: View): bindings.View { + return inner(owner, 'View'); +} + +/** @internal The view as a canvas shows it or a shot draws it, each time anew. */ +export function handleOf(owner: View): bindings.Handle { + return view(owner).handle(); +} diff --git a/npm/test/consumer/core.ts b/npm/test/consumer/core.ts deleted file mode 100644 index 46ec0a5f..00000000 --- a/npm/test/consumer/core.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Patch, field, icon, type Scene } from 'latkit/core'; -const scene: Scene = { - space: 'plane', - marks: [{ vertices: { of: ['grid', 'T'], position: [field('x'), field('y')] } }], -}; -const patch = new Patch().replace('T', [1], { x: [1], y: [2] }); -const marker = icon( - [{ width: 1, height: 1, data: new Uint8ClampedArray([255, 0, 0, 255]) }], - field('i'), -); -void [scene, patch.toBytes(), marker]; diff --git a/npm/test/consumer/main.ts b/npm/test/consumer/main.ts index 15b04f45..e9feca38 100644 --- a/npm/test/consumer/main.ts +++ b/npm/test/consumer/main.ts @@ -1,4 +1,4 @@ -import { loadLatkit, field, Patch, type Scene } from 'latkit'; +import { load, field, Data, Gpu, Patch, Shot, type Scene } from 'latkit'; import { quickstart } from './quickstart'; import wasmUrl from 'latkit/wasm?url'; import workerUrl from 'latkit/worker?url'; @@ -52,44 +52,42 @@ async function run(custom: boolean, compiled: boolean) { new Blob([await (await fetch(workerUrl)).text()], { type: 'text/javascript' }), ); } - const createWorker = () => { + const worker = () => { createdWorkers++; return new Worker(blob!); }; const started = performance.now(); const source = compiled ? await WebAssembly.compileStreaming(fetch(wasmUrl)) : wasmUrl; - const loading = loadLatkit( - custom ? { wasm: source, createWorker } : compiled ? { wasm: source } : undefined, - ); - check(loading === loadLatkit(), 'concurrent loads must share their promise'); - const kit = await loading; + const loading = load(custom ? { wasm: source, worker } : compiled ? { wasm: source } : undefined); + check(loading === load(), 'concurrent loads must share their promise'); + await loading; const initialized = performance.now(); - check(kit === (await loadLatkit()), 'subsequent loads must reuse the API'); + check(load() === loading, 'later loads must reuse the first'); let conflict = false; try { - await loadLatkit({ wasm: '/a-different-module.wasm' }); + await load({ wasm: '/a-different-module.wasm' }); } catch { conflict = true; } check(conflict, 'conflicting configuration must reject'); check(createdWorkers === 0, 'loading must not create workers'); - const { Data, Gpu, Shot } = kit; const data = new Data('grid').apply( - Patch.to('grid').replace('Bus', { count: 1 }, { x: [0], y: [0], radius: [12] }), + new Patch('grid').replace('Bus', { count: 1 }, { x: [0], y: [0], radius: [12] }), ); const empty = new Data('grid'); empty.destroy(); empty.destroy(); destroyed(() => empty.name); const decoded = await Data.read(data.toBytes()); + const kept = decoded.snapshot(); // Data reads back; patches stream back to back. const grid = new Data('grid').apply( new Patch() .axis('time', 's') .replace('Branch', [7, 8, 9], { loading: [40, 120, 95] }) - .append([0, 1, 2], 'Branch', { flow: Float32Array.of(1, 2, 3, 4, 5, 6, 7, 8, 9) }), + .append('Branch', [0, 1, 2], { flow: Float32Array.of(1, 2, 3, 4, 5, 6, 7, 8, 9) }), ); const hot = grid.query({ of: 'Branch', @@ -112,7 +110,7 @@ async function run(custom: boolean, compiled: boolean) { }); for await (const next of Patch.stream(pieces)) grid.apply(next); check( - grid.ids('Branch').join() === '7,9' && grid.get('Branch', 7, 'loading') === 1, + grid.query({ of: 'Branch' }).ids.join() === '7,9' && grid.get('Branch', 7, 'loading') === 1, 'patches stream', ); grid.destroy(); @@ -123,23 +121,24 @@ async function run(custom: boolean, compiled: boolean) { const first = gpu.view(scene); const second = gpu.view(scene); const third = secondGpu.view(scene); - gpu.canvas(canvas('first')).show([first]); - gpu.canvas(canvas('second')).show([second]); - secondGpu.canvas(canvas('third')).show([third]); + gpu.canvas(canvas('first'), { layout: [{ view: first }] }); + gpu.canvas(canvas('second'), { layout: [{ view: second }] }); + secondGpu.canvas(canvas('third'), { layout: [{ view: third }] }); // A view no canvas shows still draws in shots. const headless = gpu.view(scene); for (const view of [first, second, third, headless]) { view.on('error', (error) => { throw error; }); - view.setData(decoded); + view.sources = view === third ? [kept] : [decoded]; } - decoded.destroy(); // views retain the data + decoded.destroy(); // views keep the dataset + kept.destroy(); await until(() => [first, second, third].every((view) => view.stats.frames > 0 && view.stats.drawn === 1), ); const firstFrame = performance.now(); - const picture = await new Shot([64, 64]).image([headless]); + const picture = await new Shot([64, 64], { layout: [{ view: headless }] }).image(); check(picture.width === 64 && picture.height === 64, 'a view without a canvas draws in a shot'); const png = await picture.encode('png'); const jpeg = await picture.encode('jpeg', { quality: 80 }); @@ -170,29 +169,32 @@ async function run(custom: boolean, compiled: boolean) { chunks.push(bytes); }, }); - const video = new Shot([64, 64]).video(gpu, 30, output); - await video.record([headless], (time) => time < 0.1); + const video = new Shot([64, 64], { layout: [{ view: headless }] }).video({ fps: 30, output }); + await video.record((time) => time < 0.1); check(video.frames === 3 && !output.locked, 'video retains counters and releases its output'); check( chunks.some((bytes) => new TextDecoder().decode(bytes).includes('ftyp')), 'video writes an MP4 header', ); const cancelledOutput = new WritableStream(); - const cancelled = new Shot([64, 64]).video(gpu, 30, cancelledOutput); - const pending = cancelled.frame([headless]); + const cancelled = new Shot([64, 64], { layout: [{ view: headless }] }).video({ + fps: 30, + output: cancelledOutput, + }); + const pending = cancelled.frame(); const settled = Promise.allSettled([pending]); await Promise.resolve(); await cancelled.abort('consumer cancellation'); check((await settled)[0].status === 'rejected', 'cancellation interrupts the WASM frame'); check(!cancelledOutput.locked, 'cancelled video releases the writer'); - const restored = await new Shot([64, 64]).image([headless]); + const restored = await new Shot([64, 64], { layout: [{ view: headless }] }).image(); restored.destroy(); } headless.destroy(); check( (() => { try { - gpu.canvas(document.createElement('canvas')).show([second]); + gpu.canvas(document.createElement('canvas'), { layout: [{ view: second }] }); return false; } catch { return true; @@ -200,7 +202,7 @@ async function run(custom: boolean, compiled: boolean) { })(), 'a view shows in one canvas at a time', ); - const hits = await first.pick([128, 128], 8); + const hits = await first.pick([128, 128], { radius: 8 }); check( hits.some((hit) => hit.row.table === 'Bus'), 'GPU picking must see the rendered vertex', @@ -214,8 +216,8 @@ async function run(custom: boolean, compiled: boolean) { destroyed(() => first.stats); check(second.stats.drawn === 1, 'destroying one view must preserve its sibling'); const pending = Promise.allSettled([ - second.pick([128, 128], 8), - second.locate({ data: 'grid', table: 'Bus', id: 0 }), + second.pick([128, 128], { radius: 8 }), + second.screenOf({ data: 'grid', table: 'Bus', id: 0 }), ]); gpu.destroy(); gpu.destroy(); @@ -231,7 +233,7 @@ async function run(custom: boolean, compiled: boolean) { 'query must reject with cancellation', ); } - check(data.ids('Bus').length === 1, 'GPU cleanup must preserve independent data'); + check(data.query({ of: 'Bus' }).ids.length === 1, 'GPU cleanup must preserve independent data'); const stoppedTicks = ticks; let otherTicks = 0; const stopOther = secondGpu.on('frame', () => otherTicks++); @@ -250,8 +252,8 @@ async function run(custom: boolean, compiled: boolean) { ], }), ); - const places = await Data.read(bytes, 'places'); - check(places.ids('points').length === 1, 'worker must decode GeoJSON'); + const places = await Data.read(bytes, { name: 'places' }); + check(places.query({ of: 'points' }).ids.length === 1, 'worker must decode GeoJSON'); if (custom) check(createdWorkers > 0, 'custom worker factory must be used'); places.destroy(); secondGpu.destroy(); @@ -262,7 +264,7 @@ async function run(custom: boolean, compiled: boolean) { const example = await quickstart(exampleCanvas); await until(() => example.view.stats.drawn === 2); example.gpu.destroy(); - example.data.destroy(); + example.grid.destroy(); exampleCanvas.remove(); const transient = await Gpu.create(); await new Promise((resolve) => { diff --git a/npm/test/consumer/quickstart.ts b/npm/test/consumer/quickstart.ts index 237b64de..b46ff5c2 100644 --- a/npm/test/consumer/quickstart.ts +++ b/npm/test/consumer/quickstart.ts @@ -1,9 +1,9 @@ -import { loadLatkit, Patch, field, type Scene } from 'latkit'; +import { Data, Gpu, Patch, field, type Scene } from 'latkit'; /** A complete, type-checked example, also rendered by the installed-package test. */ export async function quickstart(canvas: HTMLCanvasElement) { - const { Data, Gpu } = await loadLatkit(); - const data = new Data('grid').apply( + const gpu = await Gpu.create(); + const grid = new Data('grid').apply( new Patch().replace('Bus', [101, 102], { x: [0, 30], y: [0, 20], kv: [230, 115] }), ); const scene: Scene = { @@ -19,10 +19,8 @@ export async function quickstart(canvas: HTMLCanvasElement) { }, ], }; - const gpu = await Gpu.create(); - const view = gpu.view(scene); - gpu.canvas(canvas).show([view]); - view.setData(data); + const view = gpu.view(scene, { sources: [grid] }); + gpu.canvas(canvas, { layout: [{ view }] }); view.fit(); - return { gpu, view, data }; + return { gpu, view, grid }; } diff --git a/npm/test/consumer/tsconfig.core.json b/npm/test/consumer/tsconfig.core.json deleted file mode 100644 index 19f0bcdc..00000000 --- a/npm/test/consumer/tsconfig.core.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2022"], - "types": [], - "strict": true, - "skipLibCheck": false, - "noEmit": true - }, - "files": ["core.ts"] -} diff --git a/npm/test/data.test.mjs b/npm/test/data.test.mjs index 578a0adf..65fcbfe2 100644 --- a/npm/test/data.test.mjs +++ b/npm/test/data.test.mjs @@ -8,20 +8,23 @@ import { test } from 'node:test'; import { pathToFileURL } from 'node:url'; const directory = process.env.LATKIT_TEST_PACKAGE ?? path.resolve(import.meta.dirname, '..'); -const { loadLatkit, Patch, reference, field } = await import( +const { load, Data, Patch, reference, field } = await import( pathToFileURL(path.join(directory, 'dist/index.js')).href ); const wasm = new WebAssembly.Module(fs.readFileSync(path.join(directory, 'pkg/latkit_bg.wasm'))); -const { Data } = await loadLatkit({ wasm }); +await load({ wasm }); /** Every chunk of `bytes`, `size` at a time, as a socket might hand them over. */ function* pieces(bytes, size) { for (let at = 0; at < bytes.length; at += size) yield bytes.subarray(at, at + size); } -test('patches carry every kind of value through the bytes', () => { +/** Every id of `table` of `data`, in row order. */ +const ids = (data, table) => data.query({ of: table }).ids; + +test('patches carry every kind of value through the bytes', async () => { const grid = new Data('grid').apply( - Patch.to('grid') + new Patch('grid') .axis('time', 's') .replace('Bus', [101, 102, 103], { kv: Float64Array.of(230, 115, 69), @@ -120,7 +123,7 @@ test('patches carry every kind of value through the bytes', () => { ['number', 'bool', 'text', 'point', 'path', 'polygons'], ); // A dataset's bytes are the patch that builds it, read back alike. - const again = new Data('grid').apply(Patch.fromBytes(grid.toBytes())); + const again = new Data('grid').apply(await Patch.read(grid.toBytes())); assert.deepEqual(again.describe(), grid.describe()); assert.deepEqual(again.get('Bus', 103, 'area'), get(103, 'area')); grid.destroy(); @@ -134,20 +137,23 @@ test('changes apply in place, a snapshot keeps a version, and a patch names its new Patch() .set('Bus', [3, 4], { x: [30, 40] }) .remove('Bus', [1]) - .append([0, 1], 'Bus', { v: Float32Array.of(1, 2, 3, 4, 5, 6) }) - .append(2, 'Bus', { v: [9] }, [4]), + .append('Bus', [0, 1], { v: Float32Array.of(1, 2, 3, 4, 5, 6) }) + .append('Bus', 2, { v: [9] }, { ids: [4] }), ); - assert.deepEqual(data.ids('Bus'), [2, 3, 4]); - assert.deepEqual(before.ids('Bus'), [1, 2, 3], 'the snapshot holds'); + assert.deepEqual(ids(data, 'Bus'), [2, 3, 4]); + assert.deepEqual(ids(before, 'Bus'), [1, 2, 3], 'the snapshot holds'); assert.equal(data.get('Bus', 4, 'x'), 40); - assert.equal(data.get('Bus', 4, 'v', 2), 9, 'rows named take the frame'); - assert.equal(data.get('Bus', 2, 'v', 2), 4, 'the rest hold their last values'); - assert.deepEqual(data.span, [0, 2]); + assert.equal(data.get('Bus', 4, 'v', { at: 2 }), 9, 'rows named take the frame'); + assert.equal(data.get('Bus', 2, 'v', { at: 2 }), 4, 'the rest hold their last values'); + assert.deepEqual(data.describe().span, [0, 2]); data.apply(new Patch().forget(1)); - assert.deepEqual(data.span, [1, 2]); - assert.throws(() => data.apply(Patch.to('weather')), { code: 'invalid' }); + assert.deepEqual(data.describe().span, [1, 2]); + assert.throws(() => data.apply(new Patch('weather')), { code: 'invalid' }); assert.throws(() => data.apply(new Patch().set('Bus', [1], { x: ['a'] })), { code: 'invalid' }); assert.equal(data.get('Bus', 2, 'x'), 2, 'a failed patch changes nothing'); + data.restore(before); + assert.deepEqual(ids(data, 'Bus'), [1, 2, 3], 'restored to the snapshot'); + assert.throws(() => new Data('other').restore(before), { code: 'invalid' }); data.destroy(); before.destroy(); }); @@ -157,36 +163,48 @@ test('appends one after another write as one change, each frame copied at its ca const patch = new Patch().replace('Bus', [1, 2], { x: [1, 2] }); for (let f = 0; f < 4; f++) { frame.set([f, 10 * f]); - patch.append(f, 'Bus', { v: frame }); + patch.append('Bus', f, { v: frame }); } const bytes = patch.toBytes(); const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); // After the header and the empty name: the count of changes. assert.equal(view.getUint32(20, true), 2, 'the replace, and one append of every frame'); const data = new Data('grid').apply(patch); - const v = (t) => data.get('Bus', 2, 'v', t); + const v = (at) => data.get('Bus', 2, 'v', { at }); assert.deepEqual([v(0), v(1), v(2), v(3)], [0, 10, 20, 30], 'each frame as it was at its call'); + const envelope = data.envelope({ table: 'Bus', field: 'v', range: [0, 4], spans: 2, ids: [2] }); + assert.deepEqual([...envelope.at], [0, 2, 4]); + assert.deepEqual( + [...envelope.least, ...envelope.greatest], + [0, 20, 10, 30], + 'row 2, span by span', + ); + assert.throws(() => data.envelope({ table: 'Nope', field: 'v', range: [0, 1], spans: 1 }), { + code: 'invalid', + path: 'Nope', + }); data.destroy(); }); -test('time tiles hold runs of a sampled field, holes between them', () => { +test('time tiles hold runs of a sampled field, holes between them', async () => { const data = new Data('grid').apply(new Patch().replace('Bus', [1, 2], { x: [1, 2] })); data.apply( new Patch() - .frames('Bus', 'v', { first: 100 }, [100, 101], Float32Array.of(1, 2, 3, 4)) - .frames('Bus', 'v', { level: 1, first: 0 }, [0, 7], [0.5, 0.25, 1.5, 1.25]), + .frames('Bus', 'v', { first: 100, at: [100, 101], values: Float32Array.of(1, 2, 3, 4) }) + .frames('Bus', 'v', { level: 1, first: 0, at: [0, 7], values: [0.5, 0.25, 1.5, 1.25] }), ); - assert.deepEqual(data.span, [0, 101]); - assert.equal(data.get('Bus', 2, 'v', 101), 4); - assert.equal(data.get('Bus', 1, 'v', 50), null, 'a hole holds no frame'); - const again = new Patch().frames('Bus', 'v', { first: 100 }, [100], [1, 2]); + assert.deepEqual(data.describe().span, [0, 101]); + assert.equal(data.get('Bus', 2, 'v', { at: 101 }), 4); + assert.equal(data.get('Bus', 1, 'v', { at: 50 }), null, 'a hole holds no frame'); + const again = new Patch().frames('Bus', 'v', { first: 100, at: [100], values: [1, 2] }); assert.throws(() => data.apply(again), { code: 'invalid' }, 'frames held already'); - assert.throws(() => new Patch().frames('Bus', 'v', { level: 1, first: 0 }, [0, 7, 9], [1]), { - code: 'invalid', - }); - const back = new Data('grid').apply(Patch.fromBytes(data.toBytes())); + assert.throws( + () => new Patch().frames('Bus', 'v', { level: 1, first: 0, at: [0, 7, 9], values: [1] }), + { code: 'invalid' }, + ); + const back = new Data('grid').apply(await Patch.read(data.toBytes())); assert.deepEqual(back.describe(), data.describe()); - assert.equal(back.get('Bus', 1, 'v', 100.5), 1); + assert.equal(back.get('Bus', 1, 'v', { at: 100.5 }), 1); data.destroy(); back.destroy(); }); @@ -199,13 +217,13 @@ test('a failed change leaves the patch as it was', () => { assert.deepEqual(patch.toBytes(), bytes); const extended = new Patch().set('Bus', [2], { x: [2] }).extend(patch); const data = new Data('grid').apply(extended); - assert.deepEqual(data.ids('Bus'), [2, 1]); + assert.deepEqual(ids(data, 'Bus'), [2, 1]); data.destroy(); }); test('patches stream back to back, split anywhere, and refuse what is too long', async () => { const sent = [ - Patch.to('grid').replace('Bus', [7, 8, 9], { x: [1, 2, 3] }), + new Patch('grid').replace('Bus', [7, 8, 9], { x: [1, 2, 3] }), new Patch().set('Bus', [7], { x: [10] }), new Patch().remove('Bus', [8]), ]; @@ -218,13 +236,11 @@ test('patches stream back to back, split anywhere, and refuse what is too long', data.apply(patch); } assert.deepEqual(read, ['grid', undefined, undefined]); - assert.deepEqual(data.ids('Bus'), [7, 9]); + assert.deepEqual(ids(data, 'Bus'), [7, 9]); assert.equal(data.get('Bus', 7, 'x'), 10); data.destroy(); } - const long = Patch.to('grid') - .replace('Bus', [1], { name: ['x'.repeat(100)] }) - .toBytes(); + const long = new Patch('grid').replace('Bus', [1], { name: ['x'.repeat(100)] }).toBytes(); const read = (stream, options) => Array.fromAsync(Patch.stream(stream, options)); await assert.rejects(read(pieces(long, 16), { limit: 64 }), { code: 'limit' }); await assert.rejects(read(pieces(long.subarray(0, 40), 16)), /ended inside a patch/); @@ -232,16 +248,18 @@ test('patches stream back to back, split anywhere, and refuse what is too long', await assert.rejects(read([json]), { code: 'invalid' }); }); -test('JSON, latkit bytes, and GeoJSON read as datasets', async () => { - const json = JSON.stringify({ +test('JSON, latkit bytes, and GeoJSON read as datasets, as their first bytes say', async () => { + const document = { name: 'grid', patch: [{ replace: { table: 'Bus', ids: { count: 2 }, fields: { x: [1, 2] } } }], - }); - const loaded = await Data.load(json); + }; + const loaded = await Data.read(document); assert.equal(loaded.get('Bus', 1, 'x'), 2); - const read = await Data.read(loaded.toBytes(), 'renamed'); + const text = await Data.read(new TextEncoder().encode(JSON.stringify(document))); + assert.deepEqual(text.describe(), loaded.describe(), 'a document read from its bytes'); + const read = await Data.read(loaded.toBytes(), { name: 'renamed' }); assert.equal(read.name, 'renamed'); - const patch = await Patch.load('[{"set": {"table": "Bus", "ids": [1], "fields": {"x": [20]}}}]'); + const patch = await Patch.read([{ set: { table: 'Bus', ids: [1], fields: { x: [20] } } }]); assert.equal(read.apply(patch).get('Bus', 1, 'x'), 20); const geojson = new TextEncoder().encode( JSON.stringify({ @@ -251,10 +269,14 @@ test('JSON, latkit bytes, and GeoJSON read as datasets', async () => { ], }), ); - const places = await Data.read(geojson, 'places'); + const places = await Data.read(geojson, { name: 'places' }); assert.deepEqual(places.get('points', 0, 'geometry'), [1, 2]); await assert.rejects(Data.read(geojson), { code: 'invalid' }, 'GeoJSON takes a name'); - for (const data of [loaded, read, places]) data.destroy(); + const aborted = AbortSignal.abort(); + await assert.rejects(Data.read(geojson, { name: 'places', signal: aborted }), { + code: 'cancelled', + }); + for (const data of [loaded, text, read, places]) data.destroy(); }); test('queries filter, sort, page, and count', () => { @@ -280,7 +302,7 @@ test('queries filter, sort, page, and count', () => { data.destroy(); }); -test('LATK v1 fixture and long-running sample indexes agree with Rust', () => { +test('LATK v1 fixture and long-running sample indexes agree with Rust', async () => { const fixture = Buffer.from( fs .readFileSync( @@ -290,18 +312,18 @@ test('LATK v1 fixture and long-running sample indexes agree with Rust', () => { .trim(), 'hex', ); - const patch = new Patch().frames('T', 'v', { first: 2 ** 32 + 3 }, [0, 1], [1, 2]); + const patch = new Patch().frames('T', 'v', { first: 2 ** 32 + 3, at: [0, 1], values: [1, 2] }); assert.deepEqual(Buffer.from(patch.toBytes()), fixture); const data = new Data('test').apply(new Patch().replace('T', { count: 1 }).extend(patch)); - data.apply(new Patch().append(2, 'T', { v: [3] })); + data.apply(new Patch().append('T', 2, { v: [3] })); assert.equal(data.get('T', 0, 'v'), 3); - assert.deepEqual(data.span, [0, 2]); + assert.deepEqual(data.describe().span, [0, 2]); data.destroy(); const version = Uint8Array.from(fixture); version[4] = 2; - assert.throws(() => Patch.fromBytes(version), /version/); + await assert.rejects(Patch.read(version), /version/); assert.throws( - () => new Patch().frames('T', 'v', { first: Number.MAX_SAFE_INTEGER }, [0], [1]), + () => new Patch().frames('T', 'v', { first: Number.MAX_SAFE_INTEGER, at: [0], values: [1] }), /safe/, ); }); diff --git a/npm/test/load.test.mjs b/npm/test/load.test.mjs index fddae27c..56d05ee3 100644 --- a/npm/test/load.test.mjs +++ b/npm/test/load.test.mjs @@ -15,34 +15,33 @@ test('installed package imports without WASM, retries failures, and shares initi headers: { 'Content-Type': 'application/wasm' }, }); }); - const { loadLatkit, field, Patch } = await import( + const { load, field, Data, Patch } = await import( pathToFileURL(path.join(directory, 'dist/index.js')).href ); assert.equal(requests, 0); assert.deepEqual(field('x').toJSON(), { field: 'x' }); - // Patches are written without WASM. + // Patches are written without WASM; datasets wait for it. const patch = new Patch().replace('Node', { count: 1 }, { x: [42] }); + assert.throws(() => new Data('early'), { code: 'unloaded' }); assert.equal(requests, 0); - const first = loadLatkit({ wasm: 'https://example.invalid/latkit.wasm' }); - assert.equal(first, loadLatkit()); + const first = load({ wasm: 'https://example.invalid/latkit.wasm' }); + assert.equal(first, load()); await assert.rejects(first, { code: 'io' }); - const retry = loadLatkit({ wasm: new URL('https://example.invalid/latkit.wasm') }); - assert.equal(retry, loadLatkit({ wasm: 'https://example.invalid/latkit.wasm' })); - const kit = await retry; - assert.equal(kit, await loadLatkit()); + const retry = load({ wasm: new URL('https://example.invalid/latkit.wasm') }); + assert.equal(retry, load({ wasm: 'https://example.invalid/latkit.wasm' })); + await retry; + assert.equal(load(), retry, 'loaded once'); assert.equal(requests, 2); - await assert.rejects(loadLatkit({ wasm: 'https://example.invalid/other.wasm' }), { - code: 'io', - }); + await assert.rejects(load({ wasm: 'https://example.invalid/other.wasm' }), { code: 'io' }); await assert.rejects( - loadLatkit({ - createWorker: () => { + load({ + worker: () => { throw new Error('unused'); }, }), { code: 'io' }, ); - const data = new kit.Data('test').apply(patch); + const data = new Data('test').apply(patch); const kept = data.snapshot(); data.apply(new Patch().set('Node', [0], { x: [7] })); assert.equal(data.get('Node', 0, 'x'), 7); diff --git a/npm/test/scenes.js b/npm/test/scenes.js index 6f717b06..b0d3b086 100644 --- a/npm/test/scenes.js +++ b/npm/test/scenes.js @@ -5,32 +5,32 @@ /** @type {import('../src/inputs.js').Scene[]} */ export default [ { "space": "plane" }, - { "space": "mercator", "ease": 0 }, - { "space": "globe", "theme": "light" }, + { "space": "earth", "ease": 0 }, + { "space": { "earth": { "projection": "globe" } }, "theme": "light" }, { "space": "plot" }, { "space": "diagram", "layout": "stress" }, { "space": { "plane": { "grid": "off" } } }, { "space": { "diagram": { "grid": { "dots": { "pitch": 10, "snap": true } } } }, "layout": { "layered": { "flow": "down", "rankGap": 60 } } }, - { "space": { "earth": { "projection": "globe", "surface": "#203040", "graticule": false } } }, + { "space": { "earth": { "projection": "globe", "graticule": false } }, "theme": { "earth": { "ground": "#203040" } } }, { "space": { "earth": { "projection": "globe", "daylight": 1782043200, "atmosphere": true } }, "marks": [ { "areas": { "of": ["base", "water"], "color": "#1d3557" } }, { "areas": { "of": ["city", "polygons"], "geometry": "geometry", "color": { "colormap": "tab10", "value": "use" }, "label": "name" } }, - { "lines": { "of": ["city", "polygons"], "width": 1, "color": "#ffffff40" } }, + { "lines": { "of": ["city", "polygons"], "width": 1, "color": "rgba(255, 255, 255, 0.25)" } }, { "lines": { "of": ["base", "transportation"], "width": { "op": "div", "args": ["lanes", 2] } } } ], - "theme": { "area": "#223344", "atmosphere": "#5aa9ff88", "night": 0.35 } + "theme": { "area": { "color": "#223344" }, "earth": { "atmosphere": "#5aa9ff88", "night": 0.35 } } }, { "space": { "plot": { "x": { "label": "Time (s)" }, "y": { "grid": false } } } }, { - "space": "globe", + "space": { "earth": { "projection": "globe" } }, "marks": [ { "edges": { "of": ["grid", "Branch"], "ends": ["from", "to"], - "width": { "units": 2 }, + "width": { "units": 2, "atLeast": 1 }, "dash": { "op": "gt", "args": ["loading", 0.9] }, "flow": { "op": "div", "args": ["p", 20] }, "color": { "colormap": "amber", "value": "loading", "domain": [0, 1] }, @@ -45,10 +45,10 @@ export default [ "radius": { "op": "add", "args": [{ "op": "div", "args": ["kv", 100] }, 1] }, "marker": "diamond", "color": { - "when": [[{ "state": "selected" }, { "color": "#ffb000" }]], + "when": [[{ "state": "selected" }, "#ffb000"]], "otherwise": { "colormap": "balance", "value": { "field": "vm", "step": true }, "domain": [0.95, 1.05] } }, - "visible": { "op": "and", "args": [{ "op": "ge", "args": ["kv", 115] }, { "is": ["kind", "substation"] }] } + "visible": { "op": "and", "args": [{ "op": "ge", "args": ["kv", 115] }, { "contains": ["name", "north"] }] } } }, { @@ -57,11 +57,17 @@ export default [ "name": "load", "size": [12, { "units": 4 }], "marker": { "block": { "corner": 2 } }, - "color": { "scale": [0, 1], "value": { "field": "load", "of": ["weather", "Station"] } } + "color": { "colormap": "viridis", "value": { "field": "load", "from": ["weather", "Station"] } } } } ], - "theme": { "base": "dark", "vertexRadius": 5, "edge": null, "selected": "#ff8800", "shadow": { "color": "#00000055", "lift": 4 } } + "theme": { + "base": "dark", + "vertex": { "radius": 5 }, + "edge": { "color": null }, + "focus": { "selected": "#ff8800" }, + "diagram": { "shadow": { "color": "#00000055", "lift": 4 } } + } }, { "space": { "plane": { "grid": { "lines": { "pitch": 1 } } } }, @@ -71,7 +77,7 @@ export default [ { "edges": { "of": ["mesh", "Spring"], "ends": ["a", "b"], "color": "#ffffff14" } }, { "vertices": { "of": ["mesh", "Vertex"], "position": ["x", "y"], "elevation": "z", "select": false, "drag": true } } ], - "theme": { "shading": 0.7, "font": "monospace", "fontSize": 12, "playhead": "#ffe5d4" } + "theme": { "shading": 0.7, "text": { "font": "monospace", "size": 12 }, "plot": { "playhead": "#ffe5d4" } } }, { "space": { "plot": { "x": { "label": "Time (s)" } } }, @@ -86,14 +92,14 @@ export default [ "of": ["mesh", "Body"], "trace": "speed", "interpolation": "stepAfter", - "label": { "text": "name", "size": 11, "priority": "rank", "maxWidth": 120 } + "label": { "text": "name", "size": 11, "priority": "rank", "maxWidth": 120, "color": "white" } } }, { "vertices": { "of": ["grid", "Bus"], "marker": { "gauge": { "fill": "load", "shape": "rounded" } }, - "ring": { "color": "#ff0000" }, + "ring": "red", "label": "name", "radius": { "op": "mul", "args": [{ "state": "zoom" }, 2] } } @@ -103,7 +109,7 @@ export default [ { "vertices": { "of": ["grid", "Custom"], "marker": { "wgsl": { "source": "fn marker(f: MarkerFragment) -> MarkerColor { return MarkerColor(f.color, 0.0); }", "inputs": ["load"] } } } }, { "edges": { "of": ["grid", "Branch"], "ends": ["from", "to"] } } ], - "theme": { "edgeSpacing": 5, "arrow": 8, "halo": 2 } + "theme": { "edge": { "spacing": 5, "arrow": 8 }, "text": { "halo": 2 }, "flow": { "spacing": { "units": 40 }, "length": { "units": 12 } } } }, { "space": "diagram", @@ -111,7 +117,7 @@ export default [ "controls": { "connect": true, "delete": true }, "marks": [ { "vertices": { "of": ["plant", "Area"], "contains": ["Device", "area"], "label": "name", "shadow": true } }, - { "edges": { "of": ["plant", "Wire"], "ends": ["from", "to"], "bends": "path", "arrows": true } }, + { "edges": { "of": ["plant", "Wire"], "ends": ["from", "to"], "bends": "path", "arrows": true, "width": { "px": 1.5 } } }, { "edges": { "of": ["plant", "Net"], "route": "orthogonal", "arrows": true } }, { "vertices": { @@ -125,6 +131,6 @@ export default [ } } ], - "theme": { "groupPadding": 16, "shadow": { "color": "#00000040", "lift": 4 } } + "theme": { "diagram": { "groupPadding": 16, "portSize": 9, "group": "#1b2530", "shadow": { "color": "#00000040", "lift": 4 } } } } ]; diff --git a/npm/test/video.test.mjs b/npm/test/video.test.mjs index 84404ce7..958a1a66 100644 --- a/npm/test/video.test.mjs +++ b/npm/test/video.test.mjs @@ -4,7 +4,7 @@ import { pathToFileURL } from 'node:url'; import { test } from 'node:test'; const directory = process.env.LATKIT_TEST_PACKAGE ?? path.resolve(import.meta.dirname, '..'); -const { Video, Shot } = await import(pathToFileURL(path.join(directory, 'dist/runtime.js')).href); +const { Video, Shot } = await import(pathToFileURL(path.join(directory, 'dist/shot.js')).href); function recording(sink = {}, options = {}) { const calls = { free: 0, cancel: 0, abort: 0, close: 0 }; @@ -34,12 +34,12 @@ function recording(sink = {}, options = {}) { }, ...options, }; - return { video: new Video(inner, stream.getWriter(), 30), stream, calls, inner }; + return { video: Video.made(inner, stream.getWriter(), 30), stream, calls, inner }; } test('finish retains counters and releases the output exactly once', async () => { const { video, stream, calls } = recording(); - await video.frame([]); + await video.frame(); const finished = video.finish(); assert.equal(video.finish(), finished); await finished; @@ -56,7 +56,6 @@ test('record aborts and unlocks on step and progress failures', async () => { const error = new Error('record failed'); await assert.rejects( video.record( - [], () => { if (!progress) throw error; return true; @@ -79,7 +78,7 @@ test('write failures close the encoder and release the output lock', async () => throw new Error('disk full'); }, }); - await assert.rejects(video.frame([]), /disk full/); + await assert.rejects(video.frame(), /disk full/); assert.equal(stream.locked, false); assert.equal(calls.free, 1); assert.equal(calls.cancel, 1); @@ -121,29 +120,19 @@ test('concurrent frames reject without disrupting the frame already drawing', as }), }, ); - const frame = video.frame([]); - await assert.rejects(video.frame([]), /one frame/); + const frame = video.frame(); + await assert.rejects(video.frame(), /one frame/); resolveFrame(new Uint8Array()); await frame; await video.finish(); }); -test('locked outputs do not create encoders, and setup failures unlock outputs', () => { +test('locked outputs make no encoder, and failing to make one unlocks the output', () => { const shot = new Shot([64, 64]); const stream = new WritableStream(); const writer = stream.getWriter(); - let created = 0; - const gpu = { - inner: { - video() { - created++; - throw new Error('no encoder'); - }, - }, - }; - assert.throws(() => shot.video(gpu, 30, stream)); - assert.equal(created, 0); + assert.throws(() => shot.video({ fps: 30, output: stream }), /locked/); writer.releaseLock(); - assert.throws(() => shot.video(gpu, 30, stream), /no encoder/); + assert.throws(() => shot.video({ fps: 30, output: stream }), { code: 'unloaded' }); assert.equal(stream.locked, false); }); From 3a9ab68ab607ebdd54f68fdb8ed009d91d3a9e83 Mon Sep 17 00:00:00 2001 From: Wyatt Lowery Date: Sun, 25 Oct 2026 01:21:08 -0500 Subject: [PATCH 05/14] prforamnce improvments and memory --- .changeset/sources.md | 2 +- benchmark/README.md | 18 +- ...dia-geforce-rtx-2080-super-chrome-156.json | 950 +++++++++--------- benchmark/baselines/work.json | 568 +++++++++-- benchmark/judge.ts | 28 +- benchmark/run.ts | 42 +- crates/latkit/shaders/lib/ends.wesl | 47 +- crates/latkit/src/data/column.rs | 31 +- crates/latkit/src/data/values.rs | 7 + crates/latkit/src/gpu/arena.rs | 42 +- crates/latkit/src/gpu/work.rs | 14 +- crates/latkit/src/host/job.rs | 5 +- crates/latkit/src/layout/boxes.rs | 220 ++++ crates/latkit/src/layout/mod.rs | 1 + crates/latkit/src/layout/rect.rs | 5 + crates/latkit/src/layout/route.rs | 136 +-- crates/latkit/src/sheet/mod.rs | 125 ++- crates/latkit/src/sheet/wires.rs | 451 ++++++--- crates/latkit/src/tiles/sets.rs | 22 +- crates/latkit/src/view/drawing.rs | 160 ++- crates/latkit/src/view/ends.rs | 93 +- crates/latkit/src/view/frame.rs | 45 +- crates/latkit/src/view/kept.rs | 193 ++++ crates/latkit/src/view/marks.rs | 2 +- crates/latkit/src/view/mod.rs | 12 +- crates/latkit/src/view/plan.rs | 5 +- crates/latkit/src/view/program.rs | 63 +- crates/latkit/src/view/resources.rs | 24 +- crates/latkit/src/view/sync.rs | 35 +- crates/latkit/src/view/wake.rs | 15 +- crates/latkit/tests/scale.rs | 53 +- crates/latkit/tests/tiles.rs | 17 +- docs/reference/types.md | 7 +- npm/examples/gate/gate.ts | 54 +- npm/schema.json | 9 +- npm/src/outputs.ts | 7 +- 36 files changed, 2390 insertions(+), 1118 deletions(-) create mode 100644 crates/latkit/src/layout/boxes.rs create mode 100644 crates/latkit/src/view/kept.rs diff --git a/.changeset/sources.md b/.changeset/sources.md index c40a9ae2..f1730462 100644 --- a/.changeset/sources.md +++ b/.changeset/sources.md @@ -11,7 +11,7 @@ Views draw sources, and say so in one word each: a `Data` its latest version, a - Added: colors are typed: in a color option a string is a CSS color, `field(name)` reads a field, and `color(css)`, colormaps, and `when(...)` make colors (`Paint`, `PaintInput`). - Added: `px(n)` and `units(n)` say a length's unit: CSS pixels on a plane, the earth, and a plot, position units in a diagram, unless said. A stroke in pixels keeps its width as a diagram zooms. - Added: the theme in groups: `text`, `vertex`, `edge`, `line`, `area`, `focus`, `flow`, `diagram`, `earth`, `plot`; a comet's `flow.spacing` and `flow.length` take `{ px }` or `{ units }`. A diagram's `diagram.outline`, `corner`, `portSize`, and `bend` set its blocks' and groups' borders, their corners, its ports' width, and its wires' bends; `shadow: true` on its blocks and groups lays a soft shadow `diagram.shadow.lift` below them. -- Added: `Gpu.resident` and `Gpu.uploading`, bytes of data on the GPU and waiting to upload for every view of it; `Work.packed`, `Work.resolved` (rows whose ends were found where their vertices are held), and `Work.cleared` (bytes of pages cleared). +- Added: `Gpu.resident` and `Gpu.uploading`, bytes of data on the GPU and waiting to upload for every view of it; `Work.packed`, `Work.resolved` (rows whose ends were found where their vertices are held), `Work.cleared` (bytes of pages cleared), and `Work.uploads` (pages, or pieces of them, uploaded). - Changed: `view.scene`, `view.selection`, `view.screen(position)`, `view.position(point)`, and `view.screenOf(row)`; the `hovered` and `selection` events; `view.waiting`. - Changed: `new Patch(dataset)` names a patch's dataset; `Patch.read(source)` reads a patch's bytes or JSON; `Data.read(source, { name, base })` a dataset's bytes, JSON, or GeoJSON; `patch.append(table, at, fields, { ids })` and `patch.frames(table, field, { level, first, at, values })`; `data.envelope({ table, field, range, spans, ids })`; `field(name).from(data, table)` reads another dataset. - Changed: the `'earth'` space, `{ earth: { projection: 'mercator' | 'globe', graticule, daylight, atmosphere } }`; the ground's color is the theme's `earth.ground`. diff --git a/benchmark/README.md b/benchmark/README.md index d3c75832..d5fc34e2 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -6,7 +6,7 @@ clock, so a run draws the same frames on any device; how frames pace a page is t ```sh cargo xtask bench --quick # each scenario once at its smallest size: phases and work -cargo xtask bench # every size while the last fits, 3 repeats +cargo xtask bench # every size while the last fits at its fastest, 3 repeats cargo xtask bench --only grid,labels # some scenarios cargo xtask bench --software # SwiftShader on the CPU: work is judged, times only reported cargo xtask bench --update # lay this run's results over the baselines @@ -16,13 +16,15 @@ A run fails on: - an error, a frame drawn while nothing changed, a pipeline compiled where it was first used (`late`), or resident data over its budget; -- a phase growing with size faster than its order; -- more work than `baselines/work.json`: exactly for what the data sets (`uploaded`, `evaluated`, - `packed`, `jobs`, `pipelines`, `late`, `resident`) in a phase that acts once, and within 2× for - what moves with the frame a job or a fetch lands in (`written`, `readbacks`, `texts`, every count - of a phase acting each frame, and all of a scenario fed by fetches); -- in a full sweep, on the machine `baselines/.json` was taken on, a phase over 1.25× slower, - and for a phase acting once, later by over a timer tick (16 ms). +- a phase growing with size faster than its order, its uploads counted in pages (`uploads`), each at + least a page of bytes; +- more work than `baselines/work.json`: exactly for what the data sets (`uploaded`, `uploads`, + `evaluated`, `packed`, `jobs`, `pipelines`, `late`, `resident`) in a phase that acts once, and + within 2× for what moves with the frame a job or a fetch lands in (`written`, `readbacks`, `texts`, + every count of a phase acting each frame, and all of a scenario fed by fetches); +- in a full sweep, on the machine `baselines/.json` was taken on, a phase whose fastest + repeat is over 1.25× slower, and for a phase acting once, later by over a timer tick (16 ms): a + stall of the machine slows one repeat, a regression every one. On a machine with two GPUs, Chrome draws on the fast one. Reports and results land in `target/bench/`. Scenarios are in `page/scenarios.ts`, the data they draw in `page/data.ts`, and how diff --git a/benchmark/baselines/nvidia-turing-nvidia-geforce-rtx-2080-super-chrome-156.json b/benchmark/baselines/nvidia-turing-nvidia-geforce-rtx-2080-super-chrome-156.json index d3f5e4a2..c319d3bc 100644 --- a/benchmark/baselines/nvidia-turing-nvidia-geforce-rtx-2080-super-chrome-156.json +++ b/benchmark/baselines/nvidia-turing-nvidia-geforce-rtx-2080-super-chrome-156.json @@ -4,15 +4,15 @@ "start": { "10000": { "init": { - "time": 258.1999999284744, + "time": 232.20000004768372, "gpu": 0 }, "data": { - "time": 18, + "time": 17.600000023841858, "gpu": 0 }, "load": { - "time": 504.2000000476837, + "time": 604.2000000476837, "gpu": 0 } } @@ -20,599 +20,599 @@ "points": { "1000": { "init": { - "time": 112.70000004768372, + "time": 149.5, "gpu": 0 }, "data": { - "time": 14.100000023841858, + "time": 13.799999952316284, "gpu": 0 }, "load": { - "time": 461.60000002384186, + "time": 531.1999999284744, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.070464 + "gpu": 0.07088 }, "zoom": { "time": 0.099999994, "gpu": 0.063488 }, "revisit": { - "time": 3.5, - "gpu": 0.066688 + "time": 3.600000023841858, + "gpu": 0.0656 }, "restyle": { - "time": 4.300000071525574, - "gpu": 0.067584 + "time": 4.599999904632568, + "gpu": 0.065952 }, "one row": { "time": 0.099999994, - "gpu": 0.08128 + "gpu": 0.081824 }, "every row": { - "time": 3.299999952316284, - "gpu": 0.081536 + "time": 2.1999999284744263, + "gpu": 0.0816 }, "first pick": { - "time": 8.700000047683716, - "gpu": 0.081536 + "time": 8.5, + "gpu": 0.0816 }, "pick": { - "time": 8.400000095367432, - "gpu": 0.081408 + "time": 8, + "gpu": 0.08176 }, "hover": { - "time": 12.299999952316284, - "gpu": 0.081536 + "time": 13.5, + "gpu": 0.08176 }, "select": { - "time": 9.399999976158142, - "gpu": 0.081664 + "time": 5.799999952316284, + "gpu": 0.080896 }, "append": { - "time": 0.099999994, - "gpu": 0.111552 + "time": 0.19999999, + "gpu": 0.09072 } }, "10000": { "init": { - "time": 115.69999992847443, + "time": 124.89999997615814, "gpu": 0 }, "data": { - "time": 14, + "time": 14.100000023841858, "gpu": 0 }, "load": { - "time": 455.3000000715256, + "time": 480.5, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.146656 + "gpu": 0.141728 }, "zoom": { "time": 0.099999994, - "gpu": 0.082432 + "gpu": 0.08192 }, "revisit": { - "time": 4.5, - "gpu": 0.104224 + "time": 3.299999952316284, + "gpu": 0.102624 }, "restyle": { "time": 5.099999904632568, - "gpu": 0.111872 + "gpu": 0.110592 }, "one row": { "time": 0.099999994, - "gpu": 0.162592 + "gpu": 0.16368 }, "every row": { - "time": 3.799999952316284, - "gpu": 0.162688 + "time": 5, + "gpu": 0.163648 }, "first pick": { - "time": 9.299999952316284, - "gpu": 0.162688 + "time": 9.399999976158142, + "gpu": 0.163808 }, "pick": { - "time": 7, - "gpu": 0.163008 + "time": 6, + "gpu": 0.16384 }, "hover": { - "time": 10.5, - "gpu": 0.163424 + "time": 10.799999952316284, + "gpu": 0.16384 }, "select": { - "time": 8.699999928474426, - "gpu": 0.163424 + "time": 8.5, + "gpu": 0.16384 }, "append": { "time": 0.19999999, - "gpu": 0.19456 + "gpu": 0.172032 } }, "100000": { "init": { - "time": 120.70000004768372, + "time": 107.89999997615814, "gpu": 0 }, "data": { - "time": 16.5, + "time": 16.199999928474426, "gpu": 0 }, "load": { - "time": 479.90000009536743, + "time": 459, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.869184 + "gpu": 0.868352 }, "zoom": { - "time": 0.099999994, - "gpu": 0.274688 + "time": 0, + "gpu": 0.2744 }, "revisit": { - "time": 3.399999976158142, - "gpu": 0.482592 + "time": 3.600000023841858, + "gpu": 0.48128 }, "restyle": { - "time": 4.699999928474426, - "gpu": 0.54848 + "time": 4.400000095367432, + "gpu": 0.548288 }, "one row": { "time": 0.099999994, - "gpu": 0.974144 + "gpu": 0.973568 }, "every row": { - "time": 7.599999904632568, - "gpu": 0.973952 + "time": 6.899999976158142, + "gpu": 0.973856 }, "first pick": { - "time": 9.5, - "gpu": 0.973952 + "time": 10.300000071525574, + "gpu": 0.973856 }, "pick": { - "time": 6.600000023841858, - "gpu": 0.973952 + "time": 5.5, + "gpu": 0.973856 }, "hover": { - "time": 13.899999976158142, + "time": 13.100000023841858, "gpu": 0.974304 }, "select": { - "time": 9.100000023841858, - "gpu": 0.974688 + "time": 7.399999976158142, + "gpu": 0.973664 }, "append": { "time": 0.3, - "gpu": 1.044864 + "gpu": 0.984064 } }, "1000000": { "init": { - "time": 125.39999997615814, + "time": 137.60000002384186, "gpu": 0 }, "data": { - "time": 34.10000002384186, + "time": 40.10000002384186, "gpu": 0 }, "load": { - "time": 516.7000000476837, - "gpu": 1.14432 + "time": 614.2000000476837, + "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.180352 + "gpu": 0.17888 }, "zoom": { - "time": 0.099999994, - "gpu": 0.475136 + "time": 0.19999999, + "gpu": 0.479328 }, "revisit": { - "time": 3.0999999046325684, - "gpu": 0.388224 + "time": 4.100000023841858, + "gpu": 0.387008 }, "restyle": { - "time": 4.399999976158142, - "gpu": 0.374048 + "time": 5.199999928474426, + "gpu": 0.370976 }, "one row": { "time": 0.099999994, - "gpu": 0.193312 + "gpu": 0.192512 }, "every row": { - "time": 33.10000002384186, - "gpu": 0.193408 + "time": 28.600000023841858, + "gpu": 0.192512 }, "first pick": { - "time": 11, - "gpu": 0.193696 + "time": 8.199999928474426, + "gpu": 0.192512 }, "pick": { - "time": 6.799999952316284, - "gpu": 0.193728 + "time": 6.199999928474426, + "gpu": 0.192832 }, "hover": { - "time": 14.5, - "gpu": 0.193792 + "time": 11.899999976158142, + "gpu": 0.192832 }, "select": { - "time": 8.099999904632568, - "gpu": 0.194304 + "time": 9.600000023841858, + "gpu": 0.193376 }, "append": { - "time": 0.3, - "gpu": 0.24192 + "time": 0.5, + "gpu": 0.21984 } }, "2000000": { "init": { - "time": 117, + "time": 122.60000002384186, "gpu": 0 }, "data": { - "time": 48.60000002384186, + "time": 49.89999997615814, "gpu": 0 }, "load": { - "time": 769.1000000238419, - "gpu": 1.263744 + "time": 839.0999999046326, + "gpu": 1.265664 }, "pan": { "time": 0.099999994, - "gpu": 0.180512 + "gpu": 0.180224 }, "zoom": { - "time": 0.099999994, - "gpu": 0.479552 + "time": 0.19999999, + "gpu": 0.480352 }, "revisit": { - "time": 4, - "gpu": 0.395264 + "time": 3.600000023841858, + "gpu": 0.391968 }, "restyle": { - "time": 6.699999928474426, - "gpu": 0.373376 + "time": 4.899999976158142, + "gpu": 0.370688 }, "one row": { "time": 0.099999994, - "gpu": 0.193184 + "gpu": 0.192512 }, "every row": { - "time": 48.800000071525574, - "gpu": 0.193248 + "time": 49.09999990463257, + "gpu": 0.192512 }, "first pick": { - "time": 9.899999976158142, - "gpu": 0.19328 + "time": 9.5, + "gpu": 0.192512 }, "pick": { - "time": 6.5, - "gpu": 0.193312 + "time": 6.700000047683716, + "gpu": 0.192512 }, "hover": { - "time": 12.300000071525574, - "gpu": 0.194048 + "time": 13, + "gpu": 0.192512 }, "select": { - "time": 7.899999976158142, - "gpu": 0.19456 + "time": 8.299999952316284, + "gpu": 0.193248 }, "append": { "time": 0.39999998, - "gpu": 0.241568 + "gpu": 0.219584 } } }, "grid": { "1000": { "init": { - "time": 129.20000004768372, + "time": 115.39999997615814, "gpu": 0 }, "data": { - "time": 14, + "time": 13.700000047683716, "gpu": 0 }, "load": { - "time": 546.3999999761581, + "time": 511.60000002384186, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.125024 + "gpu": 0.124928 }, "zoom": { "time": 0.099999994, "gpu": 0.126944 }, "revisit": { - "time": 4.300000071525574, - "gpu": 0.126464 + "time": 2.899999976158142, + "gpu": 0.126432 }, "one row": { "time": 0.099999994, - "gpu": 0.134144 + "gpu": 0.134112 }, "first pick": { - "time": 8, - "gpu": 0.134144 + "time": 7.900000095367432, + "gpu": 0.134048 }, "pick": { - "time": 6.100000023841858, - "gpu": 0.1344 + "time": 5.600000023841858, + "gpu": 0.133632 }, "hover": { - "time": 14, - "gpu": 0.1344 + "time": 10.899999976158142, + "gpu": 0.133632 }, "append": { "time": 0.099999994, - "gpu": 0.21504 + "gpu": 0.165184 } }, "10000": { "init": { - "time": 129.89999997615814, + "time": 120, "gpu": 0 }, "data": { - "time": 16.299999952316284, + "time": 14, "gpu": 0 }, "load": { - "time": 564.5, + "time": 537.5, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.182144 + "gpu": 0.182272 }, "zoom": { "time": 0.099999994, - "gpu": 0.177632 + "gpu": 0.178336 }, "revisit": { - "time": 3.4000000953674316, - "gpu": 0.178976 + "time": 3.200000047683716, + "gpu": 0.180224 }, "one row": { "time": 0.099999994, - "gpu": 0.190144 + "gpu": 0.190848 }, "first pick": { - "time": 9.799999952316284, - "gpu": 0.190048 + "time": 6, + "gpu": 0.190912 }, "pick": { - "time": 5.600000023841858, - "gpu": 0.190048 + "time": 6.5, + "gpu": 0.19088 }, "hover": { - "time": 12.300000071525574, - "gpu": 0.189984 + "time": 10.199999928474426, + "gpu": 0.190912 }, "append": { "time": 0.19999999, - "gpu": 0.2848 + "gpu": 0.222464 } }, "100000": { "init": { - "time": 123.90000009536743, + "time": 116.79999995231628, "gpu": 0 }, "data": { - "time": 19.59999990463257, + "time": 19.100000023841858, "gpu": 0 }, "load": { - "time": 522.7000000476837, + "time": 530.5, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.609536 + "gpu": 0.609376 }, "zoom": { "time": 0.099999994, "gpu": 0.53248 }, "revisit": { - "time": 4.399999976158142, - "gpu": 0.566976 + "time": 3, + "gpu": 0.565856 }, "one row": { "time": 0.099999994, - "gpu": 0.62048 + "gpu": 0.621824 }, "first pick": { - "time": 11.400000095367432, - "gpu": 0.620448 + "time": 11.100000023841858, + "gpu": 0.62144 }, "pick": { - "time": 6.699999928474426, - "gpu": 0.620544 + "time": 5.299999952316284, + "gpu": 0.622176 }, "hover": { - "time": 16.700000047683716, - "gpu": 0.620608 + "time": 12.199999928474426, + "gpu": 0.621408 }, "append": { "time": 0.3, - "gpu": 0.905216 + "gpu": 0.733184 } }, "1000000": { "init": { - "time": 127.70000004768372, + "time": 120.29999995231628, "gpu": 0 }, "data": { - "time": 62.800000071525574, + "time": 62.10000002384186, "gpu": 0 }, "load": { - "time": 1029.1000000238419, - "gpu": 0.337152 + "time": 978.2000000476837, + "gpu": 0.36048 }, "pan": { - "time": 0.19999999, - "gpu": 0.616896 + "time": 0.099999994, + "gpu": 0.612352 }, "zoom": { - "time": 0.39999998, - "gpu": 1.482752 + "time": 0.3, + "gpu": 1.366016 }, "revisit": { - "time": 8.299999952316284, - "gpu": 1.554784 + "time": 6, + "gpu": 1.552384 }, "one row": { - "time": 0.5, - "gpu": 0.641024 + "time": 0.3, + "gpu": 0.644448 }, "first pick": { - "time": 12.700000047683716, - "gpu": 0.640992 + "time": 8.899999976158142, + "gpu": 0.643648 }, "pick": { - "time": 7.100000023841858, - "gpu": 0.641024 + "time": 6.100000023841858, + "gpu": 0.643648 }, "hover": { - "time": 15.100000023841858, - "gpu": 0.641024 + "time": 11.100000023841858, + "gpu": 0.644672 }, "append": { "time": 0.6, - "gpu": 0.827296 + "gpu": 0.709824 } }, "2000000": { "init": { - "time": 160.60000002384186, + "time": 126.5, "gpu": 0 }, "data": { - "time": 122.5, + "time": 118.29999995231628, "gpu": 0 }, "load": { - "time": 1619.1000000238419, - "gpu": 0.34144 + "time": 1597.8000000715256, + "gpu": 0.364544 }, "pan": { "time": 0.19999999, - "gpu": 0.628736 + "gpu": 0.627328 }, "zoom": { - "time": 0.39999998, - "gpu": 1.326848 + "time": 0.3, + "gpu": 1.323008 }, "revisit": { - "time": 8.799999952316284, - "gpu": 1.109728 + "time": 5.799999952316284, + "gpu": 1.104 }, "one row": { - "time": 0.5, - "gpu": 0.802816 + "time": 0.3, + "gpu": 0.657408 }, "first pick": { - "time": 13.100000023841858, - "gpu": 0.797248 + "time": 10.399999976158142, + "gpu": 0.654816 }, "pick": { - "time": 8.100000023841858, - "gpu": 0.796928 + "time": 5.899999976158142, + "gpu": 0.654816 }, "hover": { - "time": 15.799999952316284, - "gpu": 0.796928 + "time": 11.799999952316284, + "gpu": 0.653632 }, "append": { - "time": 0.79999995, - "gpu": 1.186592 + "time": 0.6, + "gpu": 0.72144 } } }, "labels": { "10000": { "init": { - "time": 124.39999997615814, + "time": 119.30000007152557, "gpu": 0 }, "data": { - "time": 18, + "time": 17.899999976158142, "gpu": 0 }, "load": { - "time": 504.7999999523163, - "gpu": 0.182528 + "time": 505.7999999523163, + "gpu": 0.181952 }, "pan": { - "time": 0.6, - "gpu": 0.318592 + "time": 0.5, + "gpu": 0.31712 }, "zoom": { "time": 0.9, - "gpu": 0.290368 + "gpu": 0.289728 } }, "100000": { "init": { - "time": 129.39999997615814, + "time": 131, "gpu": 0 }, "data": { - "time": 62.60000002384186, + "time": 65.09999990463257, "gpu": 0 }, "load": { - "time": 561.5999999046326, - "gpu": 1.335296 + "time": 546.5, + "gpu": 1.322784 }, "pan": { - "time": 0.7, - "gpu": 1.372416 + "time": 0.6, + "gpu": 1.36688 }, "zoom": { - "time": 1.5, - "gpu": 0.644864 + "time": 1.7, + "gpu": 0.644512 } }, "1000000": { "init": { - "time": 136.70000004768372, + "time": 118.79999995231628, "gpu": 0 }, "data": { - "time": 501, + "time": 489.59999990463257, "gpu": 0 }, "load": { - "time": 685.2999999523163, - "gpu": 0.684608 + "time": 687.5, + "gpu": 0.667648 }, "pan": { "time": 0.79999995, - "gpu": 0.41456 + "gpu": 0.415008 }, "zoom": { "time": 1.5, - "gpu": 0.88896 + "gpu": 0.886464 } } }, "lanes": { "100": { "init": { - "time": 124.5, + "time": 117.19999992847443, "gpu": 0 }, "data": { @@ -620,113 +620,113 @@ "gpu": 0 }, "load": { - "time": 517.2999999523163, + "time": 515.6000000238419, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.104928 + "gpu": 0.105344 }, "first pick": { - "time": 9.400000095367432, - "gpu": 0.105024 + "time": 8.899999976158142, + "gpu": 0.105408 }, "pick": { - "time": 6.399999976158142, - "gpu": 0.10496 + "time": 5.699999928474426, + "gpu": 0.105408 } }, "1000": { "init": { - "time": 126, + "time": 123.10000002384186, "gpu": 0 }, "data": { - "time": 13.400000095367432, + "time": 14.5, "gpu": 0 }, "load": { - "time": 515.3000000715256, + "time": 541.2000000476837, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.153312 + "gpu": 0.152032 }, "first pick": { - "time": 9.300000071525574, - "gpu": 0.1536 + "time": 9.899999976158142, + "gpu": 0.15344 }, "pick": { - "time": 7.199999928474426, - "gpu": 0.1536 + "time": 6, + "gpu": 0.15344 } }, "10000": { "init": { - "time": 121.5, + "time": 114.70000004768372, "gpu": 0 }, "data": { - "time": 13.700000047683716, + "time": 14.100000023841858, "gpu": 0 }, "load": { - "time": 526.6000000238419, + "time": 524.1999999284744, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.14768 + "gpu": 0.143648 }, "first pick": { - "time": 8.600000023841858, - "gpu": 0.140768 + "time": 7.700000047683716, + "gpu": 0.139264 }, "pick": { - "time": 5.800000071525574, - "gpu": 0.136992 + "time": 5.099999904632568, + "gpu": 0.137216 } }, "100000": { "init": { - "time": 128.39999997615814, + "time": 119.19999992847443, "gpu": 0 }, "data": { - "time": 16.699999928474426, + "time": 16.09999990463257, "gpu": 0 }, "load": { - "time": 530.2999999523163, + "time": 529.6000000238419, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.177952 + "gpu": 0.176512 }, "first pick": { - "time": 7.699999928474426, - "gpu": 0.175968 + "time": 8.199999928474426, + "gpu": 0.175104 }, "pick": { - "time": 6.300000071525574, - "gpu": 0.17408 + "time": 6.199999928474426, + "gpu": 0.172768 } } }, "areas": { "100": { "init": { - "time": 127.39999997615814, + "time": 117.10000002384186, "gpu": 0 }, "data": { - "time": 13.5, + "time": 14.299999952316284, "gpu": 0 }, "load": { - "time": 478.5, + "time": 485.89999997615814, "gpu": 0 }, "pan": { @@ -735,196 +735,196 @@ }, "zoom": { "time": 0.099999994, - "gpu": 0.135168 + "gpu": 0.134816 }, "one row": { "time": 0.099999994, - "gpu": 0.291008 + "gpu": 0.291328 }, "first pick": { - "time": 6.700000047683716, - "gpu": 0.291136 + "time": 7.299999952316284, + "gpu": 0.2912 }, "pick": { - "time": 5.799999952316284, - "gpu": 0.290976 + "time": 5.899999976158142, + "gpu": 0.290944 } }, "1000": { "init": { - "time": 122.20000004768372, + "time": 119.30000007152557, "gpu": 0 }, "data": { - "time": 16.5, + "time": 16.700000047683716, "gpu": 0 }, "load": { - "time": 473.2999999523163, + "time": 481.10000002384186, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.21536 + "gpu": 0.215104 }, "zoom": { "time": 0.099999994, - "gpu": 0.214112 + "gpu": 0.213824 }, "one row": { "time": 0.099999994, - "gpu": 0.353632 + "gpu": 0.353888 }, "first pick": { - "time": 7.399999976158142, - "gpu": 0.353568 + "time": 7.900000095367432, + "gpu": 0.353728 }, "pick": { - "time": 6.899999976158142, - "gpu": 0.353216 + "time": 6.100000023841858, + "gpu": 0.3536 } }, "10000": { "init": { - "time": 121.79999995231628, + "time": 121.89999997615814, "gpu": 0 }, "data": { - "time": 27.799999952316284, + "time": 27.5, "gpu": 0 }, "load": { - "time": 497.39999997615814, + "time": 512.8999999761581, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.600352 + "gpu": 0.600064 }, "zoom": { "time": 0.099999994, - "gpu": 0.598752 + "gpu": 0.598016 }, "one row": { "time": 0.099999994, - "gpu": 0.722944 + "gpu": 0.722912 }, "first pick": { - "time": 7.699999928474426, - "gpu": 0.722944 + "time": 6.899999976158142, + "gpu": 0.722816 }, "pick": { - "time": 7.199999928474426, - "gpu": 0.722784 + "time": 5.800000071525574, + "gpu": 0.722752 } }, "30000": { "init": { - "time": 132.60000002384186, + "time": 115.39999997615814, "gpu": 0 }, "data": { - "time": 55.39999997615814, + "time": 53.89999997615814, "gpu": 0 }, "load": { - "time": 672.5, - "gpu": 0.092128 + "time": 667.7999999523163, + "gpu": 0.087712 }, "pan": { "time": 0.099999994, - "gpu": 1.180448 + "gpu": 1.139136 }, "zoom": { "time": 0.099999994, - "gpu": 1.178208 + "gpu": 1.138016 }, "one row": { - "time": 0.099999994, - "gpu": 1.536544 + "time": 0.19999999, + "gpu": 1.651264 }, "first pick": { - "time": 7.600000023841858, - "gpu": 1.532544 + "time": 8.5, + "gpu": 1.651392 }, "pick": { - "time": 6.099999904632568, - "gpu": 1.532544 + "time": 6.5, + "gpu": 1.648 } } }, "tiles": { "16": { "init": { - "time": 119.09999990463257, + "time": 116, "gpu": 0 }, "data": { - "time": 10.899999976158142, + "time": 11, "gpu": 0 }, "load": { - "time": 478.39999997615814, + "time": 472.5, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.119488 + "gpu": 0.118784 }, "zoom": { "time": 0.19999999, - "gpu": 0.114688 + "gpu": 0.114496 } }, "256": { "init": { - "time": 123.39999997615814, + "time": 122.60000002384186, "gpu": 0 }, "data": { - "time": 10.299999952316284, + "time": 11, "gpu": 0 }, "load": { - "time": 479.10000002384186, + "time": 479.5, "gpu": 0 }, "pan": { - "time": 0.099999994, - "gpu": 0.129248 + "time": 0.19999999, + "gpu": 0.128544 }, "zoom": { - "time": 0.19999999, - "gpu": 0.118592 + "time": 0.099999994, + "gpu": 0.12064 } }, "4096": { "init": { - "time": 122.60000002384186, + "time": 120.79999995231628, "gpu": 0 }, "data": { - "time": 10.699999928474426, + "time": 11.800000071525574, "gpu": 0 }, "load": { - "time": 507.2999999523163, + "time": 504, "gpu": 0 }, "pan": { - "time": 0.19999999, - "gpu": 0.166176 + "time": 0.099999994, + "gpu": 0.143072 }, "zoom": { - "time": 0.19999999, - "gpu": 0.132096 + "time": 0.099999994, + "gpu": 0.130624 } } }, "diagram": { "100": { "init": { - "time": 122.89999997615814, + "time": 118.89999997615814, "gpu": 0 }, "data": { @@ -932,7 +932,7 @@ "gpu": 0 }, "load": { - "time": 499.90000009536743, + "time": 498.39999997615814, "gpu": 0 }, "pan": { @@ -940,43 +940,43 @@ "gpu": 0.079872 }, "edit": { - "time": 0.39999998, + "time": 0.3, "gpu": 0.118784 }, "arrange": { - "time": 12, - "gpu": 0.119456 + "time": 11, + "gpu": 0.119744 } }, "1000": { "init": { - "time": 123, + "time": 119.10000002384186, "gpu": 0 }, "data": { - "time": 15.399999976158142, + "time": 14.599999904632568, "gpu": 0 }, "load": { - "time": 511.8000000715256, + "time": 514.1000000238419, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.091328 + "gpu": 0.09088 }, "edit": { - "time": 0.6, - "gpu": 0.136736 + "time": 0.39999998, + "gpu": 0.134272 }, "arrange": { - "time": 19, - "gpu": 0.137088 + "time": 15.199999928474426, + "gpu": 0.13488 } }, "10000": { "init": { - "time": 141.79999995231628, + "time": 126.19999992847443, "gpu": 0 }, "data": { @@ -984,157 +984,157 @@ "gpu": 0 }, "load": { - "time": 574.6000000238419, + "time": 600.4000000953674, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.188768 + "gpu": 0.188832 }, "edit": { - "time": 2, - "gpu": 0.262976 + "time": 0.7, + "gpu": 0.263232 }, "arrange": { - "time": 60.200000047683716, - "gpu": 0.263392 + "time": 53.60000002384186, + "gpu": 0.263008 } } }, "traces": { "10": { "init": { - "time": 125.30000007152557, + "time": 124.10000002384186, "gpu": 0 }, "data": { - "time": 16.899999976158142, + "time": 16.100000023841858, "gpu": 0 }, "load": { - "time": 560.1999999284744, + "time": 572.2000000476837, "gpu": 0 }, "stream": { "time": 0.099999994, - "gpu": 0.096032 + "gpu": 0.095904 }, "scrub": { "time": 0.099999994, - "gpu": 0.075744 + "gpu": 0.075776 }, "narrow": { "time": 0.099999994, - "gpu": 0.088416 + "gpu": 0.087712 }, "hover": { - "time": 10.5, - "gpu": 0.078496 + "time": 10.399999976158142, + "gpu": 0.077888 } }, "100": { "init": { - "time": 123.19999992847443, + "time": 122.39999997615814, "gpu": 0 }, "data": { - "time": 19.299999952316284, + "time": 19.399999976158142, "gpu": 0 }, "load": { - "time": 551.8999999761581, + "time": 564, "gpu": 0 }, "stream": { "time": 0.099999994, - "gpu": 0.33792 + "gpu": 0.333248 }, "scrub": { "time": 0.099999994, - "gpu": 0.282496 + "gpu": 0.276416 }, "narrow": { "time": 0.099999994, - "gpu": 0.247808 + "gpu": 0.24512 }, "hover": { - "time": 11.100000023841858, - "gpu": 0.134688 + "time": 9.300000071525574, + "gpu": 0.131936 } }, "1000": { "init": { - "time": 122.10000002384186, + "time": 126.10000002384186, "gpu": 0 }, "data": { - "time": 36.5, + "time": 36.799999952316284, "gpu": 0 }, "load": { - "time": 549.5, + "time": 665, "gpu": 0 }, "stream": { - "time": 0.099999994, - "gpu": 2.282528 + "time": 0.19999999, + "gpu": 2.235552 }, "scrub": { "time": 0.099999994, - "gpu": 1.959424 + "gpu": 1.912416 }, "narrow": { "time": 0.099999994, - "gpu": 1.521664 + "gpu": 1.492992 }, "hover": { - "time": 12, - "gpu": 0.54736 + "time": 13.200000047683716, + "gpu": 0.539168 } }, "10000": { "init": { - "time": 122.59999990463257, + "time": 121.20000004768372, "gpu": 0 }, "data": { - "time": 223.29999995231628, + "time": 221.19999992847443, "gpu": 0 }, "load": { - "time": 591.5, + "time": 589.8999999761581, "gpu": 0 }, "stream": { - "time": 0.19999999, - "gpu": 23.588608 + "time": 0.3, + "gpu": 23.267328 }, "scrub": { "time": 0.19999999, - "gpu": 20.83088 + "gpu": 20.334593 }, "narrow": { - "time": 0.099999994, - "gpu": 15.60352 + "time": 0.19999999, + "gpu": 14.946752 }, "hover": { - "time": 20.5, - "gpu": 5.154208 + "time": 20.90000009536743, + "gpu": 4.993408 } } }, "history": { "1000": { "init": { - "time": 120.89999997615814, + "time": 120.30000007152557, "gpu": 0 }, "data": { - "time": 16.899999976158142, + "time": 16.699999928474426, "gpu": 0 }, "load": { - "time": 545.7000000476837, + "time": 536.2000000476837, "gpu": 0 }, "scrub": { @@ -1143,381 +1143,381 @@ }, "narrow": { "time": 0.099999994, - "gpu": 0.073792 + "gpu": 0.074016 } }, "10000": { "init": { - "time": 127.39999997615814, + "time": 133.19999992847443, "gpu": 0 }, "data": { - "time": 22.100000023841858, + "time": 24.700000047683716, "gpu": 0 }, "load": { - "time": 541.3999999761581, + "time": 623, "gpu": 0 }, "scrub": { "time": 0.099999994, - "gpu": 0.086656 + "gpu": 0.084864 }, "narrow": { - "time": 0.099999994, - "gpu": 0.092896 + "time": 0.19999999, + "gpu": 0.09312 } }, "100000": { "init": { - "time": 132.19999992847443, + "time": 130.79999995231628, "gpu": 0 }, "data": { - "time": 57.800000071525574, + "time": 61.10000002384186, "gpu": 0 }, "load": { - "time": 563.7999999523163, + "time": 565.6999999284744, "gpu": 0 }, "scrub": { "time": 0.099999994, - "gpu": 0.298464 + "gpu": 0.282304 }, "narrow": { - "time": 0.19999999, - "gpu": 0.224256 + "time": 0.099999994, + "gpu": 0.213696 } }, "1000000": { "init": { - "time": 147.29999995231628, + "time": 133.79999995231628, "gpu": 0 }, "data": { - "time": 426.5, + "time": 413.2999999523163, "gpu": 0 }, "load": { - "time": 577.8000000715256, + "time": 598.5, "gpu": 0 }, "scrub": { - "time": 0.19999999, - "gpu": 0.42432 + "time": 0.099999994, + "gpu": 0.39936 }, "narrow": { - "time": 0.19999999, - "gpu": 0.302144 + "time": 0.099999994, + "gpu": 0.28528 } } }, "transition": { "1000": { "init": { - "time": 141.70000004768372, + "time": 151.40000009536743, "gpu": 0 }, "data": { - "time": 13.900000095367432, + "time": 12.899999976158142, "gpu": 0 }, "load": { - "time": 505.1999999284744, + "time": 481.60000002384186, "gpu": 0 }, "move": { "time": 0.099999994, - "gpu": 0.081024 + "gpu": 0.081056 } }, "10000": { "init": { - "time": 141.39999997615814, + "time": 122.80000007152557, "gpu": 0 }, "data": { - "time": 14.299999952316284, + "time": 13.299999952316284, "gpu": 0 }, "load": { - "time": 454.09999990463257, + "time": 432.10000002384186, "gpu": 0 }, "move": { "time": 0.099999994, - "gpu": 0.170048 + "gpu": 0.169568 } }, "100000": { "init": { - "time": 141.69999992847443, + "time": 118.5, "gpu": 0 }, "data": { - "time": 15.599999904632568, + "time": 15.399999976158142, "gpu": 0 }, "load": { - "time": 458.10000002384186, + "time": 445.2000000476837, "gpu": 0 }, "move": { "time": 0.099999994, - "gpu": 1.259744 + "gpu": 1.259776 } }, "1000000": { "init": { - "time": 137.10000002384186, + "time": 119.10000002384186, "gpu": 0 }, "data": { - "time": 35.89999997615814, + "time": 30.799999952316284, "gpu": 0 }, "load": { - "time": 730, - "gpu": 0.795104 + "time": 688.2000000476837, + "gpu": 0.789696 }, "move": { - "time": 0.19999999, - "gpu": 0.224576 + "time": 0.099999994, + "gpu": 0.223264 } } }, "views": { "1": { "init": { - "time": 146.60000002384186, + "time": 117.10000002384186, "gpu": 0 }, "data": { - "time": 21.90000009536743, + "time": 18.699999928474426, "gpu": 0 }, "load": { - "time": 626, + "time": 496.10000002384186, "gpu": 0 }, "pan": { - "time": 0.099999994, - "gpu": 0.925472 + "time": 0, + "gpu": 0.925728 } }, "2": { "init": { - "time": 148.60000002384186, + "time": 113.79999995231628, "gpu": 0 }, "data": { - "time": 24.700000047683716, + "time": 19.199999928474426, "gpu": 0 }, "load": { - "time": 590.5, + "time": 528.5, "gpu": 0 }, "pan": { - "time": 0.299999984, - "gpu": 1.507392 + "time": 0.099999994, + "gpu": 1.43424 } }, "4": { "init": { - "time": 134.39999997615814, + "time": 115.19999992847443, "gpu": 0 }, "data": { - "time": 21.199999928474426, + "time": 19.600000023841858, "gpu": 0 }, "load": { - "time": 592.7999999523163, + "time": 535.6999999284744, "gpu": 0 }, "pan": { - "time": 0.299999984, - "gpu": 2.052384 + "time": 0.199999988, + "gpu": 2.3380159999999997 } }, "8": { "init": { - "time": 137.20000004768372, + "time": 114.39999997615814, "gpu": 0 }, "data": { - "time": 21.5, + "time": 19, "gpu": 0 }, "load": { - "time": 621.3000000715256, + "time": 549.3000000715256, "gpu": 0 }, "pan": { - "time": 0.399999978, - "gpu": 2.7855999999999996 + "time": 0.299999984, + "gpu": 2.7792320000000004 } } }, "budget": { "100000": { "init": { - "time": 133.10000002384186, + "time": 116.90000009536743, "gpu": 0 }, "data": { - "time": 20.5, + "time": 18.800000071525574, "gpu": 0 }, "load": { - "time": 579.6999999284744, + "time": 525.8999999761581, "gpu": 0 }, "pan": { "time": 0.099999994, - "gpu": 0.233568 + "gpu": 0.233472 }, "zoom": { - "time": 0.19999999, - "gpu": 0.112192 + "time": 0.099999994, + "gpu": 0.128576 } }, "1000000": { "init": { - "time": 141.29999995231628, + "time": 122.89999997615814, "gpu": 0 }, "data": { - "time": 70.19999992847443, + "time": 64, "gpu": 0 }, "load": { - "time": 1026.1000000238419, - "gpu": 0.472896 + "time": 941.6999999284744, + "gpu": 0.471168 }, "pan": { - "time": 0.19999999, - "gpu": 0.243712 + "time": 0.099999994, + "gpu": 0.24336 }, "zoom": { - "time": 0.3, - "gpu": 0.123392 + "time": 0.099999994, + "gpu": 0.122944 } }, "2000000": { "init": { - "time": 148.69999992847443, + "time": 116.70000004768372, "gpu": 0 }, "data": { - "time": 122.5, + "time": 111.69999992847443, "gpu": 0 }, "load": { - "time": 1653.5, - "gpu": 0.475168 + "time": 1492.5999999046326, + "gpu": 0.4744 }, "pan": { "time": 0.099999994, - "gpu": 0.24992 + "gpu": 0.244544 }, "zoom": { - "time": 0.19999999, - "gpu": 0.125024 + "time": 0.099999994, + "gpu": 0.123328 } } }, "shots": { "518400": { "init": { - "time": 134.80000007152557, + "time": 107.29999995231628, "gpu": 0 }, "data": { - "time": 20.700000047683716, + "time": 18.40000009536743, "gpu": 0 }, "load": { - "time": 605.5, + "time": 514.3999999761581, "gpu": 0 }, "image": { - "time": 28.09999990463257, - "gpu": 2.307168 + "time": 25.899999976158142, + "gpu": 2.307104 }, "png": { - "time": 10.100000023841858, - "gpu": 2.303648 + "time": 10.199999928474426, + "gpu": 2.303264 }, "video": { - "time": 229.29999995231628, - "gpu": 1.243136 + "time": 221.19999992847443, + "gpu": 1.2333759999999998 }, "held": { - "time": 176.69999992847443, - "gpu": 1.2776960000000002 + "time": 140.29999995231628, + "gpu": 1.274592 } }, "2073600": { "init": { - "time": 114.39999997615814, + "time": 111.39999997615814, "gpu": 0 }, "data": { - "time": 20.199999928474426, + "time": 19.200000047683716, "gpu": 0 }, "load": { - "time": 550.6999999284744, + "time": 507.10000002384186, "gpu": 0 }, "image": { - "time": 40.10000002384186, - "gpu": 2.478912 + "time": 36, + "gpu": 2.301984 }, "png": { - "time": 26.399999976158142, - "gpu": 2.304704 + "time": 23.899999976158142, + "gpu": 2.301984 }, "video": { - "time": 326.5, - "gpu": 1.2824 + "time": 278.8000000715256, + "gpu": 1.2743039999999999 }, "held": { - "time": 179.40000009536743, - "gpu": 1.305952 + "time": 135, + "gpu": 1.322976 } }, "8294400": { "init": { - "time": 117.80000007152557, + "time": 107.89999997615814, "gpu": 0 }, "data": { - "time": 19.699999928474426, + "time": 20.799999952316284, "gpu": 0 }, "load": { - "time": 560, + "time": 579, "gpu": 0 }, "image": { - "time": 71.39999997615814, - "gpu": 0 + "time": 72.60000002384186, + "gpu": 2.301088 }, "png": { - "time": 80.20000004768372, - "gpu": 2.3022720000000003 + "time": 80.60000002384186, + "gpu": 2.300768 }, "video": { - "time": 645.3000000715256, - "gpu": 1.5134720000000002 + "time": 606, + "gpu": 1.5112 }, "held": { - "time": 198.19999992847443, - "gpu": 1.533792 + "time": 180.69999992847443, + "gpu": 1.524576 } } } diff --git a/benchmark/baselines/work.json b/benchmark/baselines/work.json index 9fa5e2ec..97ef58a2 100644 --- a/benchmark/baselines/work.json +++ b/benchmark/baselines/work.json @@ -3,6 +3,7 @@ "10000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -17,6 +18,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -31,7 +33,8 @@ }, "load": { "uploaded": 200000, - "written": 4124, + "uploads": 15, + "written": 3124, "evaluated": 10000, "resolved": 0, "cleared": 40960, @@ -49,6 +52,7 @@ "1000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -63,6 +67,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -77,7 +82,8 @@ }, "load": { "uploaded": 20000, - "written": 4048, + "uploads": 5, + "written": 3892, "evaluated": 1000, "resolved": 0, "cleared": 4096, @@ -91,6 +97,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 33280, "evaluated": 0, "resolved": 0, @@ -105,6 +112,7 @@ }, "zoom": { "uploaded": 0, + "uploads": 0, "written": 33280, "evaluated": 0, "resolved": 0, @@ -119,6 +127,7 @@ }, "revisit": { "uploaded": 0, + "uploads": 0, "written": 832, "evaluated": 0, "resolved": 0, @@ -133,7 +142,8 @@ }, "restyle": { "uploaded": 0, - "written": 2384, + "uploads": 0, + "written": 2228, "evaluated": 1000, "resolved": 0, "cleared": 0, @@ -147,6 +157,7 @@ }, "one row": { "uploaded": 160000, + "uploads": 40, "written": 33600, "evaluated": 40000, "resolved": 0, @@ -161,6 +172,7 @@ }, "every row": { "uploaded": 4000, + "uploads": 1, "written": 840, "evaluated": 1000, "resolved": 0, @@ -175,6 +187,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 1672, "evaluated": 0, "resolved": 0, @@ -189,6 +202,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 1672, "evaluated": 0, "resolved": 0, @@ -203,6 +217,7 @@ }, "hover": { "uploaded": 0, + "uploads": 0, "written": 2600, "evaluated": 0, "resolved": 0, @@ -217,6 +232,7 @@ }, "select": { "uploaded": 0, + "uploads": 0, "written": 2516, "evaluated": 0, "resolved": 0, @@ -231,6 +247,7 @@ }, "append": { "uploaded": 816400, + "uploads": 200, "written": 53528, "evaluated": 40820, "resolved": 0, @@ -247,6 +264,7 @@ "10000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -261,6 +279,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -275,7 +294,8 @@ }, "load": { "uploaded": 200000, - "written": 4124, + "uploads": 15, + "written": 3956, "evaluated": 10000, "resolved": 0, "cleared": 40960, @@ -289,6 +309,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 33280, "evaluated": 0, "resolved": 0, @@ -303,6 +324,7 @@ }, "zoom": { "uploaded": 0, + "uploads": 0, "written": 33280, "evaluated": 0, "resolved": 0, @@ -317,6 +339,7 @@ }, "revisit": { "uploaded": 0, + "uploads": 0, "written": 832, "evaluated": 0, "resolved": 0, @@ -331,7 +354,8 @@ }, "restyle": { "uploaded": 0, - "written": 2460, + "uploads": 0, + "written": 2292, "evaluated": 10000, "resolved": 0, "cleared": 0, @@ -345,6 +369,7 @@ }, "one row": { "uploaded": 655360, + "uploads": 40, "written": 33600, "evaluated": 163840, "resolved": 0, @@ -359,6 +384,7 @@ }, "every row": { "uploaded": 40000, + "uploads": 3, "written": 856, "evaluated": 10000, "resolved": 0, @@ -373,6 +399,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 1672, "evaluated": 0, "resolved": 0, @@ -387,6 +414,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 1672, "evaluated": 0, "resolved": 0, @@ -401,6 +429,7 @@ }, "hover": { "uploaded": 0, + "uploads": 0, "written": 2600, "evaluated": 0, "resolved": 0, @@ -415,6 +444,7 @@ }, "select": { "uploaded": 0, + "uploads": 0, "written": 2516, "evaluated": 0, "resolved": 0, @@ -429,6 +459,7 @@ }, "append": { "uploaded": 1462800, + "uploads": 200, "written": 54720, "evaluated": 73140, "resolved": 0, @@ -445,6 +476,7 @@ "100000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -459,6 +491,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -473,7 +506,8 @@ }, "load": { "uploaded": 2000000, - "written": 4984, + "uploads": 125, + "written": 4660, "evaluated": 100000, "resolved": 0, "cleared": 401408, @@ -487,6 +521,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 33280, "evaluated": 0, "resolved": 0, @@ -501,6 +536,7 @@ }, "zoom": { "uploaded": 0, + "uploads": 0, "written": 33280, "evaluated": 0, "resolved": 0, @@ -515,6 +551,7 @@ }, "revisit": { "uploaded": 0, + "uploads": 0, "written": 832, "evaluated": 0, "resolved": 0, @@ -529,7 +566,8 @@ }, "restyle": { "uploaded": 0, - "written": 3320, + "uploads": 0, + "written": 2996, "evaluated": 100000, "resolved": 0, "cleared": 0, @@ -543,6 +581,7 @@ }, "one row": { "uploaded": 655360, + "uploads": 40, "written": 33600, "evaluated": 163840, "resolved": 0, @@ -557,6 +596,7 @@ }, "every row": { "uploaded": 400000, + "uploads": 25, "written": 1032, "evaluated": 100000, "resolved": 0, @@ -571,6 +611,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 1672, "evaluated": 0, "resolved": 0, @@ -585,6 +626,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 1672, "evaluated": 0, "resolved": 0, @@ -599,6 +641,7 @@ }, "hover": { "uploaded": 0, + "uploads": 0, "written": 2600, "evaluated": 0, "resolved": 0, @@ -613,6 +656,7 @@ }, "select": { "uploaded": 0, + "uploads": 0, "written": 2516, "evaluated": 0, "resolved": 0, @@ -627,6 +671,7 @@ }, "append": { "uploaded": 1373200, + "uploads": 200, "written": 52480, "evaluated": 68660, "resolved": 0, @@ -643,6 +688,7 @@ "1000000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -657,6 +703,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -671,7 +718,8 @@ }, "load": { "uploaded": 1638400, - "written": 5148, + "uploads": 100, + "written": 5700, "evaluated": 81920, "resolved": 0, "cleared": 262144, @@ -685,6 +733,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 33544, "evaluated": 0, "resolved": 0, @@ -699,7 +748,8 @@ }, "zoom": { "uploaded": 3932160, - "written": 58400, + "uploads": 240, + "written": 57304, "evaluated": 196608, "resolved": 0, "cleared": 786432, @@ -713,6 +763,7 @@ }, "revisit": { "uploaded": 0, + "uploads": 0, "written": 1264, "evaluated": 0, "resolved": 0, @@ -727,7 +778,8 @@ }, "restyle": { "uploaded": 0, - "written": 2536, + "uploads": 0, + "written": 2340, "evaluated": 16384, "resolved": 0, "cleared": 0, @@ -741,6 +793,7 @@ }, "one row": { "uploaded": 0, + "uploads": 0, "written": 33280, "evaluated": 0, "resolved": 0, @@ -755,6 +808,7 @@ }, "every row": { "uploaded": 65536, + "uploads": 4, "written": 864, "evaluated": 16384, "resolved": 0, @@ -769,6 +823,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 1672, "evaluated": 0, "resolved": 0, @@ -783,6 +838,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 1672, "evaluated": 0, "resolved": 0, @@ -797,6 +853,7 @@ }, "hover": { "uploaded": 0, + "uploads": 0, "written": 2600, "evaluated": 0, "resolved": 0, @@ -811,6 +868,7 @@ }, "select": { "uploaded": 81920, + "uploads": 5, "written": 3024, "evaluated": 4096, "resolved": 0, @@ -825,6 +883,7 @@ }, "append": { "uploaded": 3358720, + "uploads": 205, "written": 54692, "evaluated": 167936, "resolved": 0, @@ -841,6 +900,7 @@ "2000000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -855,6 +915,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -869,7 +930,8 @@ }, "load": { "uploaded": 1638400, - "written": 5148, + "uploads": 100, + "written": 4868, "evaluated": 81920, "resolved": 0, "cleared": 262144, @@ -883,6 +945,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 33544, "evaluated": 0, "resolved": 0, @@ -897,7 +960,8 @@ }, "zoom": { "uploaded": 4014080, - "written": 58624, + "uploads": 245, + "written": 57520, "evaluated": 200704, "resolved": 0, "cleared": 802816, @@ -911,6 +975,7 @@ }, "revisit": { "uploaded": 0, + "uploads": 0, "written": 1272, "evaluated": 0, "resolved": 0, @@ -925,7 +990,8 @@ }, "restyle": { "uploaded": 0, - "written": 2536, + "uploads": 0, + "written": 2340, "evaluated": 16384, "resolved": 0, "cleared": 0, @@ -939,6 +1005,7 @@ }, "one row": { "uploaded": 0, + "uploads": 0, "written": 33280, "evaluated": 0, "resolved": 0, @@ -953,6 +1020,7 @@ }, "every row": { "uploaded": 65536, + "uploads": 4, "written": 864, "evaluated": 16384, "resolved": 0, @@ -967,6 +1035,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 1672, "evaluated": 0, "resolved": 0, @@ -981,6 +1050,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 1672, "evaluated": 0, "resolved": 0, @@ -995,6 +1065,7 @@ }, "hover": { "uploaded": 0, + "uploads": 0, "written": 2600, "evaluated": 0, "resolved": 0, @@ -1009,6 +1080,7 @@ }, "select": { "uploaded": 81920, + "uploads": 5, "written": 3024, "evaluated": 4096, "resolved": 0, @@ -1023,6 +1095,7 @@ }, "append": { "uploaded": 3276800, + "uploads": 200, "written": 54200, "evaluated": 163840, "resolved": 0, @@ -1041,6 +1114,7 @@ "1000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -1055,6 +1129,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -1069,7 +1144,8 @@ }, "load": { "uploaded": 36352, - "written": 8660, + "uploads": 7, + "written": 8360, "evaluated": 3008, "resolved": 1984, "cleared": 12288, @@ -1083,6 +1159,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 43520, "evaluated": 0, "resolved": 0, @@ -1097,6 +1174,7 @@ }, "zoom": { "uploaded": 0, + "uploads": 0, "written": 43520, "evaluated": 0, "resolved": 0, @@ -1111,6 +1189,7 @@ }, "revisit": { "uploaded": 0, + "uploads": 0, "written": 1088, "evaluated": 0, "resolved": 0, @@ -1125,6 +1204,7 @@ }, "one row": { "uploaded": 163840, + "uploads": 40, "written": 43840, "evaluated": 40960, "resolved": 0, @@ -1139,6 +1219,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -1153,6 +1234,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -1167,6 +1249,7 @@ }, "hover": { "uploaded": 0, + "uploads": 0, "written": 3384, "evaluated": 0, "resolved": 0, @@ -1181,6 +1264,7 @@ }, "append": { "uploaded": 835600, + "uploads": 200, "written": 50704, "evaluated": 41780, "resolved": 0, @@ -1197,6 +1281,7 @@ "10000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -1211,6 +1296,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -1225,7 +1311,8 @@ }, "load": { "uploaded": 358400, - "written": 7772, + "uploads": 25, + "written": 8528, "evaluated": 29800, "resolved": 19800, "cleared": 122880, @@ -1239,6 +1326,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 43520, "evaluated": 0, "resolved": 0, @@ -1253,6 +1341,7 @@ }, "zoom": { "uploaded": 0, + "uploads": 0, "written": 43520, "evaluated": 0, "resolved": 0, @@ -1267,6 +1356,7 @@ }, "revisit": { "uploaded": 0, + "uploads": 0, "written": 1088, "evaluated": 0, "resolved": 0, @@ -1281,6 +1371,7 @@ }, "one row": { "uploaded": 655360, + "uploads": 40, "written": 43840, "evaluated": 163840, "resolved": 0, @@ -1295,6 +1386,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -1309,6 +1401,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -1323,6 +1416,7 @@ }, "hover": { "uploaded": 0, + "uploads": 0, "written": 3368, "evaluated": 0, "resolved": 0, @@ -1337,6 +1431,7 @@ }, "append": { "uploaded": 1462800, + "uploads": 200, "written": 51968, "evaluated": 73140, "resolved": 0, @@ -1353,6 +1448,7 @@ "100000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -1367,6 +1463,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -1381,7 +1478,8 @@ }, "load": { "uploaded": 3589760, - "written": 9972, + "uploads": 223, + "written": 10376, "evaluated": 298936, "resolved": 199080, "cleared": 1204224, @@ -1395,6 +1493,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 43520, "evaluated": 0, "resolved": 0, @@ -1409,6 +1508,7 @@ }, "zoom": { "uploaded": 0, + "uploads": 0, "written": 43520, "evaluated": 0, "resolved": 0, @@ -1423,6 +1523,7 @@ }, "revisit": { "uploaded": 0, + "uploads": 0, "written": 1088, "evaluated": 0, "resolved": 0, @@ -1437,6 +1538,7 @@ }, "one row": { "uploaded": 655360, + "uploads": 40, "written": 43840, "evaluated": 163840, "resolved": 0, @@ -1451,6 +1553,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -1465,6 +1568,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -1479,6 +1583,7 @@ }, "hover": { "uploaded": 0, + "uploads": 0, "written": 3368, "evaluated": 0, "resolved": 0, @@ -1493,6 +1598,7 @@ }, "append": { "uploaded": 1258000, + "uploads": 200, "written": 49728, "evaluated": 62900, "resolved": 0, @@ -1509,6 +1615,7 @@ "1000000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -1523,6 +1630,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -1537,7 +1645,8 @@ }, "load": { "uploaded": 4980736, - "written": 18404, + "uploads": 304, + "written": 16872, "evaluated": 290816, "resolved": 69632, "cleared": 835584, @@ -1551,6 +1660,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 48480, "evaluated": 0, "resolved": 0, @@ -1565,9 +1675,10 @@ }, "zoom": { "uploaded": 12845056, - "written": 187356, + "uploads": 784, + "written": 213052, "evaluated": 868352, - "resolved": 430080, + "resolved": 380928, "cleared": 3440640, "packed": 3195384, "readbacks": 16, @@ -1579,6 +1690,7 @@ }, "revisit": { "uploaded": 0, + "uploads": 0, "written": 5088, "evaluated": 0, "resolved": 0, @@ -1593,9 +1705,10 @@ }, "one row": { "uploaded": 819200, - "written": 165296, + "uploads": 50, + "written": 194240, "evaluated": 184320, - "resolved": 430080, + "resolved": 53248, "cleared": 0, "packed": 198065, "readbacks": 2, @@ -1607,6 +1720,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -1621,6 +1735,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -1635,6 +1750,7 @@ }, "hover": { "uploaded": 0, + "uploads": 0, "written": 3368, "evaluated": 0, "resolved": 0, @@ -1649,6 +1765,7 @@ }, "append": { "uploaded": 3276800, + "uploads": 200, "written": 48916, "evaluated": 163840, "resolved": 0, @@ -1665,6 +1782,7 @@ "2000000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -1679,6 +1797,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -1693,7 +1812,8 @@ }, "load": { "uploaded": 5144576, - "written": 20948, + "uploads": 314, + "written": 17644, "evaluated": 299008, "resolved": 69632, "cleared": 851968, @@ -1707,6 +1827,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 48512, "evaluated": 0, "resolved": 0, @@ -1721,9 +1842,10 @@ }, "zoom": { "uploaded": 18939904, - "written": 236160, + "uploads": 1156, + "written": 274688, "evaluated": 1175552, - "resolved": 434176, + "resolved": 385024, "cleared": 4669440, "packed": 4719371, "readbacks": 16, @@ -1735,6 +1857,7 @@ }, "revisit": { "uploaded": 0, + "uploads": 0, "written": 5696, "evaluated": 0, "resolved": 0, @@ -1749,6 +1872,7 @@ }, "one row": { "uploaded": 589824, + "uploads": 36, "written": 43808, "evaluated": 147456, "resolved": 0, @@ -1763,6 +1887,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -1777,6 +1902,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -1791,6 +1917,7 @@ }, "hover": { "uploaded": 0, + "uploads": 0, "written": 3368, "evaluated": 0, "resolved": 0, @@ -1805,7 +1932,8 @@ }, "append": { "uploaded": 3276800, - "written": 132948, + "uploads": 200, + "written": 181604, "evaluated": 163840, "resolved": 0, "cleared": 16384, @@ -1823,6 +1951,7 @@ "10000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -1837,6 +1966,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -1851,7 +1981,8 @@ }, "load": { "uploaded": 240000, - "written": 710504, + "uploads": 18, + "written": 709440, "evaluated": 10000, "resolved": 0, "cleared": 368640, @@ -1865,6 +1996,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 2125904, "evaluated": 0, "resolved": 0, @@ -1879,6 +2011,7 @@ }, "zoom": { "uploaded": 0, + "uploads": 0, "written": 5139048, "evaluated": 0, "resolved": 0, @@ -1895,6 +2028,7 @@ "100000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -1909,6 +2043,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -1923,7 +2058,8 @@ }, "load": { "uploaded": 2400000, - "written": 1430784, + "uploads": 150, + "written": 1431184, "evaluated": 100000, "resolved": 0, "cleared": 3612672, @@ -1937,6 +2073,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 2350640, "evaluated": 0, "resolved": 0, @@ -1951,14 +2088,15 @@ }, "zoom": { "uploaded": 0, - "written": 6697904, + "uploads": 0, + "written": 6729684, "evaluated": 0, "resolved": 0, "cleared": 0, "packed": 0, "readbacks": 40, "jobs": 0, - "texts": 1517, + "texts": 1523, "pipelines": 0, "late": 0, "resident": 12443648 @@ -1967,6 +2105,7 @@ "1000000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -1981,6 +2120,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -1995,7 +2135,8 @@ }, "load": { "uploaded": 1966080, - "written": 2930036, + "uploads": 120, + "written": 2930500, "evaluated": 81920, "resolved": 0, "cleared": 4653056, @@ -2009,6 +2150,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 4017904, "evaluated": 0, "resolved": 0, @@ -2023,14 +2165,15 @@ }, "zoom": { "uploaded": 4718592, - "written": 11450480, + "uploads": 288, + "written": 11478636, "evaluated": 196608, "resolved": 0, "cleared": 12582912, "packed": 1179648, "readbacks": 44, "jobs": 0, - "texts": 1411, + "texts": 1414, "pipelines": 0, "late": 0, "resident": 27983872 @@ -2041,6 +2184,7 @@ "100": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2055,6 +2199,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2069,7 +2214,8 @@ }, "load": { "uploaded": 1456, - "written": 5436, + "uploads": 7, + "written": 6192, "evaluated": 116, "resolved": 100, "cleared": 768, @@ -2083,6 +2229,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 45392, "evaluated": 0, "resolved": 0, @@ -2097,6 +2244,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -2111,6 +2259,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -2127,6 +2276,7 @@ "1000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2141,6 +2291,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2155,7 +2306,8 @@ }, "load": { "uploaded": 12256, - "written": 6476, + "uploads": 7, + "written": 6240, "evaluated": 1016, "resolved": 1000, "cleared": 4352, @@ -2169,6 +2321,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 45392, "evaluated": 0, "resolved": 0, @@ -2183,6 +2336,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -2197,6 +2351,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -2213,6 +2368,7 @@ "10000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2227,6 +2383,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2241,7 +2398,8 @@ }, "load": { "uploaded": 120256, - "written": 7680, + "uploads": 13, + "written": 6296, "evaluated": 10016, "resolved": 10000, "cleared": 41216, @@ -2255,6 +2413,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 45392, "evaluated": 0, "resolved": 0, @@ -2269,6 +2428,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -2283,6 +2443,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -2299,6 +2460,7 @@ "100000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2313,6 +2475,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2327,7 +2490,8 @@ }, "load": { "uploaded": 1200256, - "written": 7640, + "uploads": 79, + "written": 6172, "evaluated": 100016, "resolved": 100000, "cleared": 401664, @@ -2341,6 +2505,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 45392, "evaluated": 0, "resolved": 0, @@ -2355,6 +2520,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -2369,6 +2535,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 2192, "evaluated": 0, "resolved": 0, @@ -2387,6 +2554,7 @@ "100": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2401,6 +2569,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2415,7 +2584,8 @@ }, "load": { "uploaded": 1340768, - "written": 4128, + "uploads": 52, + "written": 3136, "evaluated": 100, "resolved": 0, "cleared": 512, @@ -2429,6 +2599,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 33280, "evaluated": 0, "resolved": 0, @@ -2443,6 +2614,7 @@ }, "zoom": { "uploaded": 0, + "uploads": 0, "written": 33280, "evaluated": 0, "resolved": 0, @@ -2457,6 +2629,7 @@ }, "one row": { "uploaded": 16000, + "uploads": 40, "written": 35520, "evaluated": 4000, "resolved": 0, @@ -2471,6 +2644,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 1664, "evaluated": 0, "resolved": 0, @@ -2485,6 +2659,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 1664, "evaluated": 0, "resolved": 0, @@ -2501,6 +2676,7 @@ "1000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2515,6 +2691,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2529,7 +2706,8 @@ }, "load": { "uploaded": 4180168, - "written": 6620, + "uploads": 147, + "written": 6252, "evaluated": 1000, "resolved": 0, "cleared": 4096, @@ -2543,6 +2721,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 33280, "evaluated": 0, "resolved": 0, @@ -2557,6 +2736,7 @@ }, "zoom": { "uploaded": 0, + "uploads": 0, "written": 33280, "evaluated": 0, "resolved": 0, @@ -2571,6 +2751,7 @@ }, "one row": { "uploaded": 160000, + "uploads": 40, "written": 35520, "evaluated": 40000, "resolved": 0, @@ -2585,6 +2766,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 1664, "evaluated": 0, "resolved": 0, @@ -2599,6 +2781,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 1664, "evaluated": 0, "resolved": 0, @@ -2615,6 +2798,7 @@ "10000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2629,6 +2813,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2643,7 +2828,8 @@ }, "load": { "uploaded": 18151952, - "written": 11728, + "uploads": 645, + "written": 11672, "evaluated": 10000, "resolved": 0, "cleared": 40960, @@ -2657,6 +2843,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 33280, "evaluated": 0, "resolved": 0, @@ -2671,6 +2858,7 @@ }, "zoom": { "uploaded": 0, + "uploads": 0, "written": 33280, "evaluated": 0, "resolved": 0, @@ -2685,6 +2873,7 @@ }, "one row": { "uploaded": 655360, + "uploads": 40, "written": 35520, "evaluated": 163840, "resolved": 0, @@ -2699,6 +2888,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 1664, "evaluated": 0, "resolved": 0, @@ -2713,6 +2903,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 1664, "evaluated": 0, "resolved": 0, @@ -2729,6 +2920,7 @@ "30000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2743,6 +2935,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2757,7 +2950,8 @@ }, "load": { "uploaded": 40413432, - "written": 25780, + "uploads": 1561, + "written": 30572, "evaluated": 30000, "resolved": 0, "cleared": 122880, @@ -2771,6 +2965,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 33280, "evaluated": 0, "resolved": 0, @@ -2785,6 +2980,7 @@ }, "zoom": { "uploaded": 0, + "uploads": 0, "written": 33280, "evaluated": 0, "resolved": 0, @@ -2799,6 +2995,7 @@ }, "one row": { "uploaded": 655360, + "uploads": 40, "written": 35520, "evaluated": 163840, "resolved": 0, @@ -2813,6 +3010,7 @@ }, "first pick": { "uploaded": 0, + "uploads": 0, "written": 1664, "evaluated": 0, "resolved": 0, @@ -2827,6 +3025,7 @@ }, "pick": { "uploaded": 0, + "uploads": 0, "written": 1664, "evaluated": 0, "resolved": 0, @@ -2845,6 +3044,7 @@ "16": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2859,6 +3059,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -2872,9 +3073,10 @@ "resident": 0 }, "load": { - "uploaded": 272320, - "written": 3668, - "evaluated": 64, + "uploaded": 277592, + "uploads": 12, + "written": 3764, + "evaluated": 80, "resolved": 0, "cleared": 256, "packed": 0, @@ -2883,11 +3085,12 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 296192 + "resident": 304640 }, "pan": { "uploaded": 437472, - "written": 35944, + "uploads": 18, + "written": 35112, "evaluated": 512, "resolved": 0, "cleared": 512, @@ -2897,11 +3100,12 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 773376 + "resident": 781824 }, "zoom": { - "uploaded": 747184, - "written": 36568, + "uploaded": 745656, + "uploads": 38, + "written": 35848, "evaluated": 1056, "resolved": 0, "cleared": 1024, @@ -2911,14 +3115,17 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 1619968 + "resident": 1620736 } }, "256": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, + "resolved": 0, + "cleared": 0, "packed": 0, "readbacks": 0, "jobs": 0, @@ -2929,8 +3136,11 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, + "resolved": 0, + "cleared": 0, "packed": 0, "readbacks": 0, "jobs": 0, @@ -2940,47 +3150,59 @@ "resident": 0 }, "load": { - "uploaded": 788640, - "written": 4560, + "uploaded": 642048, + "uploads": 32, + "written": 3864, "evaluated": 1280, + "resolved": 0, + "cleared": 5120, "packed": 0, "readbacks": 0, "jobs": 4, "texts": 0, "pipelines": 29, "late": 0, - "resident": 934912 + "resident": 719872 }, "pan": { - "uploaded": 5062272, - "written": 34464, + "uploaded": 648192, + "uploads": 32, + "written": 35412, "evaluated": 8192, + "resolved": 0, + "cleared": 8192, "packed": 0, "readbacks": 0, "jobs": 8, "texts": 0, "pipelines": 0, "late": 0, - "resident": 7014400 + "resident": 1449984 }, "zoom": { - "uploaded": 5673696, - "written": 34908, - "evaluated": 17152, + "uploaded": 1132288, + "uploads": 79, + "written": 36500, + "evaluated": 18176, + "resolved": 0, + "cleared": 16384, "packed": 0, "readbacks": 0, - "jobs": 22, + "jobs": 21, "texts": 0, "pipelines": 0, "late": 0, - "resident": 14147584 + "resident": 3264512 } }, "4096": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, + "resolved": 0, + "cleared": 0, "packed": 0, "readbacks": 0, "jobs": 0, @@ -2991,8 +3213,11 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, + "resolved": 0, + "cleared": 0, "packed": 0, "readbacks": 0, "jobs": 0, @@ -3003,39 +3228,48 @@ }, "load": { "uploaded": 2638176, - "written": 4596, + "uploads": 258, + "written": 6640, "evaluated": 16384, + "resolved": 0, + "cleared": 65536, "packed": 0, "readbacks": 0, "jobs": 4, "texts": 0, "pipelines": 29, "late": 0, - "resident": 3145728 + "resident": 3194880 }, "pan": { - "uploaded": 4673184, - "written": 35984, - "evaluated": 77824, + "uploaded": 1286320, + "uploads": 128, + "written": 43068, + "evaluated": 73728, + "resolved": 0, + "cleared": 98304, "packed": 0, "readbacks": 0, "jobs": 8, "texts": 0, "pipelines": 0, "late": 0, - "resident": 8552448 + "resident": 4710400 }, "zoom": { - "uploaded": 8880000, - "written": 41952, - "evaluated": 73728, + "uploaded": 1894624, + "uploads": 260, + "written": 51500, + "evaluated": 49152, + "resolved": 0, + "cleared": 98304, "packed": 0, "readbacks": 0, "jobs": 10, "texts": 0, "pipelines": 0, "late": 0, - "resident": 18726912 + "resident": 8093696 } } }, @@ -3043,6 +3277,7 @@ "100": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3057,6 +3292,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3071,7 +3307,8 @@ }, "load": { "uploaded": 25440, - "written": 10252, + "uploads": 22, + "written": 8792, "evaluated": 300, "resolved": 0, "cleared": 5120, @@ -3085,6 +3322,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 44608, "evaluated": 0, "resolved": 0, @@ -3099,6 +3337,7 @@ }, "edit": { "uploaded": 273560, + "uploads": 149, "written": 49552, "evaluated": 4000, "resolved": 0, @@ -3113,6 +3352,7 @@ }, "arrange": { "uploaded": 19000, + "uploads": 8, "written": 3432, "evaluated": 100, "resolved": 0, @@ -3129,6 +3369,7 @@ "1000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3143,6 +3384,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3157,7 +3399,8 @@ }, "load": { "uploaded": 254960, - "written": 17564, + "uploads": 50, + "written": 17192, "evaluated": 3000, "resolved": 0, "cleared": 40960, @@ -3171,6 +3414,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 44608, "evaluated": 0, "resolved": 0, @@ -3185,6 +3429,7 @@ }, "edit": { "uploaded": 770360, + "uploads": 149, "written": 56720, "evaluated": 40000, "resolved": 0, @@ -3199,6 +3444,7 @@ }, "arrange": { "uploaded": 104608, + "uploads": 19, "written": 3472, "evaluated": 1000, "resolved": 0, @@ -3215,6 +3461,7 @@ "10000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3229,6 +3476,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3243,7 +3491,8 @@ }, "load": { "uploaded": 2484264, - "written": 91260, + "uploads": 366, + "written": 89552, "evaluated": 30000, "resolved": 0, "cleared": 409600, @@ -3257,6 +3506,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 44608, "evaluated": 0, "resolved": 0, @@ -3271,6 +3521,7 @@ }, "edit": { "uploaded": 2479352, + "uploads": 149, "written": 50928, "evaluated": 163840, "resolved": 0, @@ -3285,6 +3536,7 @@ }, "arrange": { "uploaded": 333632, + "uploads": 50, "written": 3696, "evaluated": 4096, "resolved": 0, @@ -3303,6 +3555,7 @@ "10": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3317,6 +3570,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3331,7 +3585,8 @@ }, "load": { "uploaded": 49152, - "written": 40064, + "uploads": 2, + "written": 39916, "evaluated": 10, "resolved": 0, "cleared": 256, @@ -3345,6 +3600,7 @@ }, "stream": { "uploaded": 48, + "uploads": 2, "written": 165952, "evaluated": 0, "resolved": 0, @@ -3359,6 +3615,7 @@ }, "scrub": { "uploaded": 0, + "uploads": 0, "written": 163776, "evaluated": 0, "resolved": 0, @@ -3373,6 +3630,7 @@ }, "narrow": { "uploaded": 1872, + "uploads": 2, "written": 181852, "evaluated": 0, "resolved": 0, @@ -3387,6 +3645,7 @@ }, "hover": { "uploaded": 0, + "uploads": 0, "written": 10664, "evaluated": 0, "resolved": 0, @@ -3403,6 +3662,7 @@ "100": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3417,6 +3677,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3431,7 +3692,8 @@ }, "load": { "uploaded": 417792, - "written": 40080, + "uploads": 3, + "written": 39928, "evaluated": 100, "resolved": 0, "cleared": 512, @@ -3445,6 +3707,7 @@ }, "stream": { "uploaded": 408, + "uploads": 2, "written": 165952, "evaluated": 0, "resolved": 0, @@ -3459,6 +3722,7 @@ }, "scrub": { "uploaded": 0, + "uploads": 0, "written": 163776, "evaluated": 0, "resolved": 0, @@ -3473,6 +3737,7 @@ }, "narrow": { "uploaded": 15912, + "uploads": 2, "written": 182144, "evaluated": 0, "resolved": 0, @@ -3487,6 +3752,7 @@ }, "hover": { "uploaded": 0, + "uploads": 0, "written": 10664, "evaluated": 0, "resolved": 0, @@ -3503,6 +3769,7 @@ "1000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3517,6 +3784,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3531,7 +3799,8 @@ }, "load": { "uploaded": 4104192, - "written": 40288, + "uploads": 17, + "written": 40096, "evaluated": 1000, "resolved": 0, "cleared": 4096, @@ -3545,6 +3814,7 @@ }, "stream": { "uploaded": 4008, + "uploads": 2, "written": 165952, "evaluated": 0, "resolved": 0, @@ -3559,6 +3829,7 @@ }, "scrub": { "uploaded": 0, + "uploads": 0, "written": 163776, "evaluated": 0, "resolved": 0, @@ -3573,6 +3844,7 @@ }, "narrow": { "uploaded": 156312, + "uploads": 3, "written": 187648, "evaluated": 0, "resolved": 0, @@ -3587,6 +3859,7 @@ }, "hover": { "uploaded": 0, + "uploads": 0, "written": 10664, "evaluated": 0, "resolved": 0, @@ -3603,6 +3876,7 @@ "10000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3617,6 +3891,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3631,7 +3906,8 @@ }, "load": { "uploaded": 40968192, - "written": 51652, + "uploads": 158, + "written": 46872, "evaluated": 10000, "resolved": 0, "cleared": 40960, @@ -3645,6 +3921,7 @@ }, "stream": { "uploaded": 40008, + "uploads": 4, "written": 172464, "evaluated": 0, "resolved": 0, @@ -3659,6 +3936,7 @@ }, "scrub": { "uploaded": 0, + "uploads": 0, "written": 163776, "evaluated": 0, "resolved": 0, @@ -3673,6 +3951,7 @@ }, "narrow": { "uploaded": 1560312, + "uploads": 9, "written": 235048, "evaluated": 0, "resolved": 0, @@ -3687,6 +3966,7 @@ }, "hover": { "uploaded": 0, + "uploads": 0, "written": 10664, "evaluated": 0, "resolved": 0, @@ -3705,6 +3985,7 @@ "1000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3719,6 +4000,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3733,7 +4015,8 @@ }, "load": { "uploaded": 12000, - "written": 52968, + "uploads": 2, + "written": 48788, "evaluated": 1, "resolved": 0, "cleared": 256, @@ -3747,6 +4030,7 @@ }, "scrub": { "uploaded": 0, + "uploads": 0, "written": 163840, "evaluated": 0, "resolved": 0, @@ -3761,6 +4045,7 @@ }, "narrow": { "uploaded": 0, + "uploads": 0, "written": 200228, "evaluated": 0, "resolved": 0, @@ -3777,6 +4062,7 @@ "10000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3791,6 +4077,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3805,7 +4092,8 @@ }, "load": { "uploaded": 20000, - "written": 51400, + "uploads": 3, + "written": 51248, "evaluated": 1, "resolved": 0, "cleared": 256, @@ -3819,6 +4107,7 @@ }, "scrub": { "uploaded": 0, + "uploads": 0, "written": 163840, "evaluated": 0, "resolved": 0, @@ -3833,6 +4122,7 @@ }, "narrow": { "uploaded": 120000, + "uploads": 2, "written": 198096, "evaluated": 0, "resolved": 0, @@ -3849,6 +4139,7 @@ "100000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3863,6 +4154,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3877,7 +4169,8 @@ }, "load": { "uploaded": 224992, - "written": 57980, + "uploads": 6, + "written": 53776, "evaluated": 1, "resolved": 0, "cleared": 256, @@ -3891,6 +4184,7 @@ }, "scrub": { "uploaded": 0, + "uploads": 0, "written": 163840, "evaluated": 0, "resolved": 0, @@ -3905,6 +4199,7 @@ }, "narrow": { "uploaded": 0, + "uploads": 0, "written": 208184, "evaluated": 0, "resolved": 0, @@ -3921,6 +4216,7 @@ "1000000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3935,6 +4231,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -3949,7 +4246,8 @@ }, "load": { "uploaded": 281248, - "written": 60320, + "uploads": 6, + "written": 56116, "evaluated": 1, "resolved": 0, "cleared": 256, @@ -3963,6 +4261,7 @@ }, "scrub": { "uploaded": 0, + "uploads": 0, "written": 163840, "evaluated": 0, "resolved": 0, @@ -3977,6 +4276,7 @@ }, "narrow": { "uploaded": 0, + "uploads": 0, "written": 217356, "evaluated": 0, "resolved": 0, @@ -3995,6 +4295,7 @@ "1000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4009,6 +4310,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4023,7 +4325,8 @@ }, "load": { "uploaded": 20000, - "written": 4048, + "uploads": 5, + "written": 3892, "evaluated": 1000, "resolved": 0, "cleared": 4096, @@ -4037,7 +4340,8 @@ }, "move": { "uploaded": 20000, - "written": 51640, + "uploads": 5, + "written": 50808, "evaluated": 2000, "resolved": 0, "cleared": 65536, @@ -4053,6 +4357,7 @@ "10000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4067,6 +4372,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4081,7 +4387,8 @@ }, "load": { "uploaded": 200000, - "written": 4124, + "uploads": 15, + "written": 3956, "evaluated": 10000, "resolved": 0, "cleared": 40960, @@ -4095,6 +4402,7 @@ }, "move": { "uploaded": 200000, + "uploads": 15, "written": 50888, "evaluated": 20000, "resolved": 0, @@ -4111,6 +4419,7 @@ "100000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4125,6 +4434,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4139,7 +4449,8 @@ }, "load": { "uploaded": 2000000, - "written": 4984, + "uploads": 125, + "written": 4660, "evaluated": 100000, "resolved": 0, "cleared": 401408, @@ -4153,6 +4464,7 @@ }, "move": { "uploaded": 2000000, + "uploads": 125, "written": 51768, "evaluated": 200000, "resolved": 0, @@ -4169,6 +4481,7 @@ "1000000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4183,6 +4496,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4197,7 +4511,8 @@ }, "load": { "uploaded": 1638400, - "written": 629552, + "uploads": 100, + "written": 629272, "evaluated": 98304, "resolved": 0, "cleared": 5296640, @@ -4211,7 +4526,8 @@ }, "move": { "uploaded": 655360, - "written": 245776, + "uploads": 40, + "written": 252432, "evaluated": 49152, "resolved": 0, "cleared": 557056, @@ -4229,6 +4545,7 @@ "1": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4243,6 +4560,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4257,7 +4575,8 @@ }, "load": { "uploaded": 3589760, - "written": 8788, + "uploads": 223, + "written": 9192, "evaluated": 298936, "resolved": 199080, "cleared": 1204224, @@ -4271,6 +4590,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 43520, "evaluated": 0, "resolved": 0, @@ -4287,6 +4607,7 @@ "2": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4301,6 +4622,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4315,7 +4637,8 @@ }, "load": { "uploaded": 3589760, - "written": 16552, + "uploads": 223, + "written": 19536, "evaluated": 597872, "resolved": 398160, "cleared": 2408448, @@ -4329,6 +4652,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 87040, "evaluated": 0, "resolved": 0, @@ -4345,6 +4669,7 @@ "4": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4359,6 +4684,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4373,7 +4699,8 @@ }, "load": { "uploaded": 3589760, - "written": 40128, + "uploads": 223, + "written": 32536, "evaluated": 1195744, "resolved": 796320, "cleared": 4816896, @@ -4387,6 +4714,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 174080, "evaluated": 0, "resolved": 0, @@ -4403,6 +4731,7 @@ "8": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4417,6 +4746,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4431,7 +4761,8 @@ }, "load": { "uploaded": 3589760, - "written": 89456, + "uploads": 223, + "written": 71592, "evaluated": 2391488, "resolved": 1592640, "cleared": 9666560, @@ -4445,6 +4776,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 348160, "evaluated": 0, "resolved": 0, @@ -4463,6 +4795,7 @@ "100000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4477,6 +4810,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4491,7 +4825,8 @@ }, "load": { "uploaded": 2686976, - "written": 24156, + "uploads": 164, + "written": 27784, "evaluated": 163840, "resolved": 49152, "cleared": 442368, @@ -4505,7 +4840,8 @@ }, "pan": { "uploaded": 0, - "written": 47944, + "uploads": 0, + "written": 48644, "evaluated": 0, "resolved": 0, "cleared": 0, @@ -4519,9 +4855,10 @@ }, "zoom": { "uploaded": 1376256, - "written": 234160, + "uploads": 84, + "written": 366852, "evaluated": 98304, - "resolved": 442368, + "resolved": 114688, "cleared": 229376, "packed": 302439, "readbacks": 0, @@ -4535,6 +4872,7 @@ "1000000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4549,6 +4887,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4563,7 +4902,8 @@ }, "load": { "uploaded": 3588096, - "written": 30972, + "uploads": 219, + "written": 35448, "evaluated": 208896, "resolved": 49152, "cleared": 589824, @@ -4577,6 +4917,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 48232, "evaluated": 0, "resolved": 0, @@ -4591,6 +4932,7 @@ }, "zoom": { "uploaded": 0, + "uploads": 0, "written": 51136, "evaluated": 0, "resolved": 0, @@ -4607,6 +4949,7 @@ "2000000": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4621,6 +4964,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4635,7 +4979,8 @@ }, "load": { "uploaded": 3588096, - "written": 33504, + "uploads": 219, + "written": 36432, "evaluated": 208896, "resolved": 49152, "cleared": 589824, @@ -4649,6 +4994,7 @@ }, "pan": { "uploaded": 0, + "uploads": 0, "written": 48232, "evaluated": 0, "resolved": 0, @@ -4663,6 +5009,7 @@ }, "zoom": { "uploaded": 0, + "uploads": 0, "written": 51136, "evaluated": 0, "resolved": 0, @@ -4681,6 +5028,7 @@ "518400": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4695,6 +5043,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4709,7 +5058,8 @@ }, "load": { "uploaded": 3589760, - "written": 16552, + "uploads": 223, + "written": 15184, "evaluated": 597872, "resolved": 398160, "cleared": 2408448, @@ -4723,6 +5073,7 @@ }, "image": { "uploaded": 0, + "uploads": 0, "written": 6624, "evaluated": 0, "resolved": 0, @@ -4737,6 +5088,7 @@ }, "png": { "uploaded": 0, + "uploads": 0, "written": 2176, "evaluated": 0, "resolved": 0, @@ -4751,6 +5103,7 @@ }, "video": { "uploaded": 0, + "uploads": 0, "written": 71904, "evaluated": 0, "resolved": 0, @@ -4765,6 +5118,7 @@ }, "held": { "uploaded": 0, + "uploads": 0, "written": 4448, "evaluated": 0, "resolved": 0, @@ -4781,6 +5135,7 @@ "2073600": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4795,6 +5150,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4809,7 +5165,8 @@ }, "load": { "uploaded": 3589760, - "written": 16552, + "uploads": 223, + "written": 15184, "evaluated": 597872, "resolved": 398160, "cleared": 2408448, @@ -4823,6 +5180,7 @@ }, "image": { "uploaded": 0, + "uploads": 0, "written": 6624, "evaluated": 0, "resolved": 0, @@ -4837,6 +5195,7 @@ }, "png": { "uploaded": 0, + "uploads": 0, "written": 2176, "evaluated": 0, "resolved": 0, @@ -4851,6 +5210,7 @@ }, "video": { "uploaded": 0, + "uploads": 0, "written": 71904, "evaluated": 0, "resolved": 0, @@ -4865,6 +5225,7 @@ }, "held": { "uploaded": 0, + "uploads": 0, "written": 4448, "evaluated": 0, "resolved": 0, @@ -4881,6 +5242,7 @@ "8294400": { "init": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4895,6 +5257,7 @@ }, "data": { "uploaded": 0, + "uploads": 0, "written": 0, "evaluated": 0, "resolved": 0, @@ -4909,7 +5272,8 @@ }, "load": { "uploaded": 3589760, - "written": 16552, + "uploads": 223, + "written": 19536, "evaluated": 597872, "resolved": 398160, "cleared": 2408448, @@ -4923,6 +5287,7 @@ }, "image": { "uploaded": 0, + "uploads": 0, "written": 6624, "evaluated": 0, "resolved": 0, @@ -4937,6 +5302,7 @@ }, "png": { "uploaded": 0, + "uploads": 0, "written": 2176, "evaluated": 0, "resolved": 0, @@ -4951,6 +5317,7 @@ }, "video": { "uploaded": 0, + "uploads": 0, "written": 71904, "evaluated": 0, "resolved": 0, @@ -4965,6 +5332,7 @@ }, "held": { "uploaded": 0, + "uploads": 0, "written": 4448, "evaluated": 0, "resolved": 0, diff --git a/benchmark/judge.ts b/benchmark/judge.ts index 7f9e53ad..d2b056ff 100644 --- a/benchmark/judge.ts +++ b/benchmark/judge.ts @@ -5,12 +5,13 @@ * GPU, so what the data and the frames drawn set (`EXACT`) is held exactly in a phase that acts once * and settles, unless fetched data feeds it; the rest, and every count of a phase acting each frame, * moves with the frame a job or a fetch lands in, and is held within `STEADY`. Then, on the machine - * the times baseline was taken on, no phase much slower. + * the times baseline was taken on, no phase much slower on its fastest repeat: a stall of the machine + * slows one repeat, a regression every one. */ import type { Work } from 'latkit'; import type { Listed, Run } from './page/main.ts'; -/** A phase at a size, across repeats: times their medians, work its least. */ +/** A phase at a size, across repeats: its fastest, and its least work. */ export interface Summary { /** A steady phase's CPU per frame, else its milliseconds. */ time: number; @@ -41,16 +42,25 @@ const SLACK = 0.2; /** Milliseconds a phase acting once may land later than its baseline by: a timer tick, which is * 15.6 ms on Windows, so readbacks answer one tick or the next. */ const TICK = 16; -/** Bytes a changed row uploads at least: its page, 4,096 rows of four bytes. */ +/** Bytes an upload counts as at least: a page, 4,096 rows of four bytes. */ const PAGE = 4096 * 4; /** Below these a cost counts as these: too small to fit an exponent to, or to call slower. A frame's * CPU time is read on a page's clock, which steps by 0.1 ms, so it counts from five steps; a phase * acting once lands on a tick. */ const FLOOR = { frame: 0.5, action: TICK, bytes: 64 * 1024 }; -/** How much slower than the baseline a median may be. */ +/** How much slower than the baseline the fastest repeat may be. */ const SLOWER = 1.25; /** Work the data and the frames drawn alone set, which repeats exactly once a phase settles. */ -const EXACT = new Set(['uploaded', 'evaluated', 'packed', 'jobs', 'pipelines', 'late', 'resident']); +const EXACT = new Set([ + 'uploaded', + 'uploads', + 'evaluated', + 'packed', + 'jobs', + 'pipelines', + 'late', + 'resident', +]); /** How much more of the rest a phase may do than the baseline: the frame a job or a fetch lands in * moves label placements and tiles by up to about 1.7×, and a per-frame blowup by far more. */ const STEADY = 2; @@ -81,8 +91,8 @@ function summarize(runs: Run[]): By

    { const work = costs.map((c) => ({ ...c.work, resident: c.resident })); ((out[scenario] ??= {})[size] ??= {})[phase] = { steady, - time: median(costs.map((c) => (steady ? c.cpu : c.ms))), - gpu: median(costs.map((c) => c.gpu)), + time: Math.min(...costs.map((c) => (steady ? c.cpu : c.ms))), + gpu: Math.min(...costs.map((c) => c.gpu)), frames: median(costs.map((c) => c.frames)), work: Object.fromEntries( Object.keys(work[0]!).map((k) => [ @@ -136,8 +146,8 @@ export function judge( const fit: Growth = { time: grows((s) => s.time, steady ? FLOOR.frame : FLOOR.action), gpu: grows((s) => s.gpu, FLOOR.frame), - // Pages are what changes upload: below a page a frame drawn, bytes count as that. - uploaded: grows((s) => Math.max(s.work.uploaded, s.frames * PAGE), FLOOR.bytes), + // Pages are what changes upload: below a page an upload, bytes count as that. + uploaded: grows((s) => Math.max(s.work.uploaded, s.work.uploads * PAGE), FLOOR.bytes), resident: grows((s) => s.work.resident, FLOOR.bytes), }; (exponents[scenario.name] ??= {})[phase] = fit; diff --git a/benchmark/run.ts b/benchmark/run.ts index 9dd7a05a..9a6ce1f1 100644 --- a/benchmark/run.ts +++ b/benchmark/run.ts @@ -9,10 +9,11 @@ * on the CPU, as Linux always does: its work counts the same, its times only report, and no shader * can hang the GPU. * - * A scenario climbs its sizes while the last one's phases, grown as their orders say, fit a phase's - * limit. A failure ends its scenario; a lost device, or a page that stopped answering, ends the - * sweep, since the next scenario would meet a resetting driver, and resets in a row are what wedge - * one. Every wait races a deadline. + * A scenario climbs its sizes while the last one's phases, each at its fastest repeat and grown as + * its order says, fit a phase's limit: a stall of the machine slows one repeat, a scenario too large + * for the next size every one. A failure ends its scenario; a lost device, or a page that stopped + * answering, ends the sweep, since the next scenario would meet a resetting driver, and resets in a + * row are what wedge one. Every wait races a deadline. */ import fs from 'node:fs'; import path from 'node:path'; @@ -73,12 +74,19 @@ function watched(work: Promise, heard: () => number, what: string): Promis return Promise.race([work, silent]).finally(() => clearInterval(timer)); } -/** The first phase of `last` that, grown to `size` as its order says (linearly where it says - * nothing), would pass a phase's limit. */ -function outgrown(last: Run, size: number, orders: Listed['orders']): string | undefined { - const grown = ([phase, cost]: [string, Run['phases'][string]]) => - cost.ms * (size / last.size) ** (orders[phase]?.time ?? 1); - return Object.entries(last.phases).find((entry) => grown(entry) > LIMIT)?.[0]; +/** The first phase of the repeats `last` at one size that, at its fastest and grown to `size` as its + * order says (linearly where it says nothing), would pass a phase's limit, and its fastest. */ +function outgrown( + last: Run[], + size: number, + orders: Listed['orders'], +): [string, number] | undefined { + const from = last[0]!.size; + for (const phase of Object.keys(last[0]!.phases)) { + const ms = Math.min(...last.map((run) => run.phases[phase]?.ms ?? Infinity)); + if (ms * (size / from) ** (orders[phase]?.time ?? 1) > LIMIT) return [phase, ms]; + } + return undefined; } const server = await createServer({ @@ -170,23 +178,25 @@ try { try { // A first run fills the shader cache, so the measured ones load warm; `start` alone loads cold. if (!scenario.cold && !flags.quick) await measure(warm, scenario, sizes[0]!); - let last: Run | undefined; + let last: Run[] = []; for (const size of sizes) { - const phase = last && outgrown(last, size, scenario.orders); - if (last && phase) { - const ms = last.phases[phase]!.ms.toFixed(0); + const outgrew = last.length ? outgrown(last, size, scenario.orders) : undefined; + if (outgrew) { + const [phase, ms] = outgrew; notes.push( - `${scenario.name} stops before ${size}: its ${phase} took ${ms} ms at ${last.size}`, + `${scenario.name} stops before ${size}: its ${phase} took ${ms.toFixed(0)} ms at ${last[0]!.size}`, ); break; } + last = []; for (let r = 0; r < repeats; r++) { const run = await measure(warm, scenario, size); console.log( ` ${scenario.name} at ${size}: loaded in ${run.phases.load?.ms.toFixed(0)} ms` + (run.errors.length ? `, ${run.errors.length} errors` : ''), ); - runs.push((last = run)); + runs.push(run); + last.push(run); } } } catch (error) { diff --git a/crates/latkit/shaders/lib/ends.wesl b/crates/latkit/shaders/lib/ends.wesl index 84048949..648e3693 100644 --- a/crates/latkit/shaders/lib/ends.wesl +++ b/crates/latkit/shaders/lib/ends.wesl @@ -12,9 +12,10 @@ import package::lib::state::record_at; import package::lib::read::bits_value; // Where place `place` of the order a table is drawn in is held, by where the program lists its held -// rows at `slots`: how many pages, then each one's slot; then how many rows it keeps whose pages it -// does not hold, then each one's place and held row, ascending by place. NONE where it holds the -// place nowhere; a table drawn whole, with no such list, holds every row as itself. +// rows at `slots`: how many pages, then each one's slot; then the table of the rows it keeps whose +// pages it does not hold: how many buckets, then each one's place and held row, NONE where empty, a +// place in the first empty bucket from where its bits hash to, as `view/kept.rs` keeps them. NONE +// where it holds the place nowhere; a table drawn whole, with no such list, holds every row as itself. fn held_of(slots: u32, place: u32) -> u32 { if slots == NONE || place == NONE { return place; @@ -28,23 +29,39 @@ fn held_of(slots: u32, place: u32) -> u32 { } } let kept = slots + 1u + pages; - let count = program[kept]; - var lo = 0u; - var hi = count; - while lo < hi { - let mid = (lo + hi) / 2u; - if program[kept + 1u + 2u * mid] < place { - lo = mid + 1u; - } else { - hi = mid; - } + let buckets = program[kept]; + if buckets == 0u { + return NONE; } - if lo < count && program[kept + 1u + 2u * lo] == place { - return program[kept + 2u + 2u * lo]; + var b = home(place, buckets); + // At most four fifths of the buckets are full, so a probe meets an empty one within the table. + for (var probe = 0u; probe < buckets; probe++) { + let at = kept + 1u + 2u * b; + let held = program[at]; + if held == place { + return program[at + 1u]; + } + if held == NONE { + return NONE; + } + b += 1u; + if b == buckets { + b = 0u; + } } return NONE; } +// The bucket a probe for place `d` starts at among `buckets`, as `view/kept.rs` finds it: its bits +// hashed. +fn home(d: u32, buckets: u32) -> u32 { + var h = d ^ (d >> 16u); + h *= 0x7feb352du; + h ^= h >> 15u; + h *= 0x846ca68bu; + return (h ^ (h >> 16u)) % buckets; +} + // Writes where each end of live row `row` of the current mark is held: an edge's two, a face's three // corners. fn resolve(row: u32) { diff --git a/crates/latkit/src/data/column.rs b/crates/latkit/src/data/column.rs index 19ff17e9..99cc6f56 100644 --- a/crates/latkit/src/data/column.rs +++ b/crates/latkit/src/data/column.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; use super::name::PageId; use super::values::{Kind, Picks, Value, Values}; @@ -13,13 +13,15 @@ pub(crate) const PAGE_MASK: usize = PAGE_ROWS - 1; /// Writes a page remembers back: past them, it reads as written whole. const REMEMBERED: u8 = 8; -/// One page of a column: up to [`PAGE_ROWS`] values, the id of exactly these contents, and, when it -/// was written over another page, which page and which rows. +/// One page of a column: up to [`PAGE_ROWS`] values, the id of exactly these contents, when it was +/// written over another page, which page and which rows, and the points its geometry takes. #[derive(Clone, Debug)] pub(crate) struct Page { pub(crate) id: PageId, pub(crate) values: Values, pub(crate) written: Option>, + /// Counted the first time they are asked for: a page never changes. + points: OnceLock, } /// What a page was written from: the page it copied, the rows written over it there, offsets into @@ -34,7 +36,7 @@ pub(crate) struct Written { impl Page { pub(crate) fn new(values: Values) -> Page { - Page { id: PageId::next(), values, written: None } + Page { id: PageId::next(), values, written: None, points: OnceLock::new() } } /// `values`: page `old` with `rows` written over it, offsets into the page. @@ -42,7 +44,12 @@ impl Page { let before = old.written.clone().filter(|w| w.depth < REMEMBERED); let depth = before.as_ref().map_or(1, |w| w.depth + 1); let written = Written { from: old.id, rows: rows.into(), before, depth }; - Page { id: PageId::next(), values, written: Some(Arc::new(written)) } + Page { id: PageId::next(), values, written: Some(Arc::new(written)), points: OnceLock::new() } + } + + /// The points its geometry takes as shaders read them, as [`Values::points`] counts them. + pub(crate) fn points(&self) -> usize { + *self.points.get_or_init(|| self.values.points()) } /// The rows written since page `before`, offsets into the page, in order: `None` unless this page @@ -406,6 +413,20 @@ mod tests { assert_eq!(far.changed_since(&column), vec![3, PAGE_ROWS + 4], "compared where the pages forgot"); } + #[test] + fn a_page_counts_its_points_once_and_a_page_written_over_it_its_own() { + let path = |n: usize| (0..n).map(|i| [i as f64, 0.0]).collect::>(); + let column = Column::new(&Values::from(vec![path(3), path(5)])); + let page = &column.pages()[0]; + assert_eq!(page.points.get(), None, "nothing counted until asked"); + assert_eq!(page.points(), 8); + assert_eq!(page.points.get(), Some(&8), "counted once, then kept"); + assert_eq!(column.clone().pages()[0].points.get(), Some(&8), "a copy of the column shares the count"); + let source = Column::new(&Values::from(vec![path(10)])); + let written = column.write(column.len(), &[(0, 0)], &source).expect("paths"); + assert_eq!(written.pages()[0].points(), 15, "the page written over counts its own points"); + } + #[test] fn gather_keeps_leading_pages() { let column = Column::new(&numbers(PAGE_ROWS + 5)); diff --git a/crates/latkit/src/data/values.rs b/crates/latkit/src/data/values.rs index e4bfcd52..4d7ac964 100644 --- a/crates/latkit/src/data/values.rs +++ b/crates/latkit/src/data/values.rs @@ -327,6 +327,13 @@ impl Values { } } + /// Points its geometries take as shaders read them: every coordinate, and a break between a row's + /// parts or rings; none where it holds no geometry. + pub(crate) fn points(&self) -> usize { + let Some(geometry) = self.geometry() else { return 0 }; + (0..self.len()).filter(|&row| self.is_valid(row)).map(|row| geometry.row_points(row)).sum() + } + /// Geometries as values, every one present. pub(crate) fn of_geometry(geometry: Geometry) -> Values { Values { array: Array::Geometry(geometry), valid: None } diff --git a/crates/latkit/src/gpu/arena.rs b/crates/latkit/src/gpu/arena.rs index d4fd71ba..fb8ef36a 100644 --- a/crates/latkit/src/gpu/arena.rs +++ b/crates/latkit/src/gpu/arena.rs @@ -72,11 +72,12 @@ impl PageKey { } /// The pieces of the geometry pages drawn lately, by each page's id: its rows' table, then its pieces -/// of points, keyed by what they hold. A page drawn again finds its pieces without making them; one -/// written again makes them, and uploads those the GPU lacks. +/// of points, keyed by what they hold, and how many points it has. A page drawn again finds its +/// pieces and its points without making or counting them; one written again makes them, and uploads +/// those the GPU lacks. #[derive(Default)] pub(crate) struct Pieces { - keys: HashMap, FxBuildHasher>, + keys: HashMap, usize), FxBuildHasher>, order: VecDeque, } @@ -84,14 +85,15 @@ pub(crate) struct Pieces { const PIECED_PAGES: usize = 1 << 14; impl Pieces { - /// The pieces of page `page`, when kept. - pub(crate) fn of(&self, page: PageId) -> Option> { + /// The pieces of page `page`, and its points, when kept. + pub(crate) fn of(&self, page: PageId) -> Option<(Arc<[PageKey]>, usize)> { self.keys.get(&page).cloned() } - /// Keeps `keys` as page `page`'s pieces, forgetting the page kept longest when too many are. - pub(crate) fn keep(&mut self, page: PageId, keys: Arc<[PageKey]>) { - if self.keys.insert(page, keys).is_none() { + /// Keeps `keys` as page `page`'s pieces, of `points` points, forgetting the page kept longest when + /// too many are. + pub(crate) fn keep(&mut self, page: PageId, keys: Arc<[PageKey]>, points: usize) { + if self.keys.insert(page, (keys, points)).is_none() { self.order.push_back(page); } while self.order.len() > PIECED_PAGES { @@ -205,13 +207,6 @@ pub(crate) fn page_words(values: &Values, words: Words, low: bool) -> Vec { } } -/// Points a geometry page's rows take as shaders read them: every coordinate, and a break between a -/// row's parts or rings. -pub(crate) fn path_points(values: &Values) -> usize { - let Some(geometry) = values.geometry() else { return 0 }; - (0..values.len()).filter(|&row| values.is_valid(row)).map(|row| geometry.row_points(row)).sum() -} - /// A geometry page as `lib/read.wesl` reads it: its rows' table, two words a row (the piece its /// points begin in, among the page's, shifted up by `POINT_BITS` over its first point there; and how /// many points it has); then its pieces of points, every point an x and a y word. @@ -564,19 +559,22 @@ impl Arena { } // Pages placed side by side, as a column's pages are, upload in one write. writes.sort_unstable_by_key(|&(at, _)| at); - let mut run: Option<(u32, Vec)> = None; + let mut run: Option<(u32, Vec, u64)> = None; for (at, words) in writes { match &mut run { - Some((start, joined)) if *start + joined.len() as u32 == at => joined.extend_from_slice(&words), + Some((start, joined, pages)) if *start + joined.len() as u32 == at => { + joined.extend_from_slice(&words); + *pages += 1; + } _ => { - if let Some((start, joined)) = run.replace((at, words)) { - queue.upload(&self.buffer, u64::from(start) * 4, bytemuck::cast_slice(&joined)); + if let Some((start, joined, pages)) = run.replace((at, words, 1)) { + queue.upload(&self.buffer, u64::from(start) * 4, bytemuck::cast_slice(&joined), pages); } } } } - if let Some((start, joined)) = run { - queue.upload(&self.buffer, u64::from(start) * 4, bytemuck::cast_slice(&joined)); + if let Some((start, joined, pages)) = run { + queue.upload(&self.buffer, u64::from(start) * 4, bytemuck::cast_slice(&joined), pages); } bytes } @@ -716,7 +714,7 @@ mod tests { // The last row's two parts are broken by a missing point. assert_eq!(entry(&page, 2), (1, 10, 4)); assert_eq!(page.pieces[1].len(), 2 * (10 + 4)); - assert_eq!(path_points(&values), PIECE_POINTS + 10 + 4); + assert_eq!(values.points(), PIECE_POINTS + 10 + 4); } #[test] diff --git a/crates/latkit/src/gpu/work.rs b/crates/latkit/src/gpu/work.rs index 1448b968..dbc334d0 100644 --- a/crates/latkit/src/gpu/work.rs +++ b/crates/latkit/src/gpu/work.rs @@ -55,12 +55,15 @@ macro_rules! work { work! { /// Bytes of data pages uploaded. uploaded: Uploaded, + /// Data pages, or pieces of them, uploaded: what a change uploads counts its pages here, and its + /// bytes in `uploaded`. + uploads: Uploads, /// Bytes written besides pages: uniforms, programs, label runs, the selection, textures. written: Written, /// Rows whose channels the GPU evaluated. evaluated: Evaluated, /// Rows of edges and faces whose ends the GPU found where their vertices are held: rows that - /// changed, and every row held where the vertices they end at moved. + /// changed, and every row of a page of them naming a vertex now held elsewhere. resolved: Resolved, /// Bytes of pages of words per row the GPU cleared: those read before anything writes them, and /// the normals faces sum afresh. @@ -103,7 +106,8 @@ impl Queue { self.raw.write_buffer(buffer, offset, bytes); } - /// Writes in command order, so a reused scratch buffer keeps earlier passes' inputs intact. + /// Writes in command order, so a reused scratch buffer keeps earlier passes' inputs intact, and + /// what a copy recorded before puts in place is written over after it. pub(crate) fn write_buffer_in( &self, device: &wgpu::Device, @@ -134,10 +138,12 @@ impl Queue { encoder.clear_buffer(buffer, offset, Some(size)); } - /// Writes a data page's words into `buffer` at `offset`, counted as uploaded. + /// Writes the words of `pages` data pages, or pieces of them, side by side into `buffer` at + /// `offset`, counted as uploaded. #[expect(clippy::disallowed_methods, reason = "the counted upload")] - pub(crate) fn upload(&self, buffer: &wgpu::Buffer, offset: u64, bytes: &[u8]) { + pub(crate) fn upload(&self, buffer: &wgpu::Buffer, offset: u64, bytes: &[u8], pages: u64) { self.counters.add(Count::Uploaded, bytes.len() as u64); + self.counters.add(Count::Uploads, pages); self.raw.write_buffer(buffer, offset, bytes); } diff --git a/crates/latkit/src/host/job.rs b/crates/latkit/src/host/job.rs index c660f516..301e5a59 100644 --- a/crates/latkit/src/host/job.rs +++ b/crates/latkit/src/host/job.rs @@ -15,6 +15,7 @@ use crate::derive::sight::Domain; use crate::geo::mvt::Kind as Geometry; use crate::geo::shapes::{earth_parts, triangles}; use crate::geo::tiles::{Decoded, decode}; +use crate::layout::boxes::Boxes; use crate::layout::route::{Net, Terminal, route}; use crate::layout::{layered::layered, levels::Graph, stress::stress}; use crate::tiles::TileKey; @@ -486,7 +487,7 @@ fn run_routes(input: &mut Reader<'_>) -> Option { .collect::>>()?; nets.push(Net { terminals }); } - let routes = route(&obstacles, &groups, &nets, &fixed, spacing); + let routes = route(&Boxes::new(obstacles), &groups, &nets, &fixed, spacing); Some(keyed(&keys, routes, |table, routes| table.field("route", routes))) } @@ -765,7 +766,7 @@ mod tests { spacing: 2.0, }; let answer = Job::routes(&wiring).run(); - let want = route(&obstacles, &[], &nets, &[], 2.0).swap_remove(0); + let want = route(&Boxes::new(obstacles.to_vec()), &[], &nets, &[], 2.0).swap_remove(0); assert_eq!(answered(&answer, "Wire", 3, "route"), Some(Value::Parts(want))); } diff --git a/crates/latkit/src/layout/boxes.rs b/crates/latkit/src/layout/boxes.rs new file mode 100644 index 00000000..d7fca5f0 --- /dev/null +++ b/crates/latkit/src/layout/boxes.rs @@ -0,0 +1,220 @@ +//! Boxes kept indexed as they change: a packed R-tree over them, whose leaves refit as their boxes +//! change, and the boxes given since it was packed, which every query reads too until there are +//! enough to pack it again. A query and a change each take about log n, and the root holds the box +//! around them all, so what keeps boxes indexed never walks them all again. + +use super::rect::{EMPTY, Rect, meets, union, valid}; + +/// Children a node. +const NODE: usize = 16; + +/// Boxes given since packing past which they are packed again. +const FRESH: usize = 64; + +/// Boxes by item, `EMPTY` where an item has none, indexed. +pub(crate) struct Boxes { + rects: Vec, + index: Index, + /// Each item's leaf; `usize::MAX` for one packed without a box. + leaves: Vec, + /// Items given a box since packing, which have no leaf. + fresh: Vec, +} + +impl Default for Boxes { + fn default() -> Boxes { + Boxes::new(Vec::new()) + } +} + +impl Boxes { + /// `boxes`, by item, packed. + pub(crate) fn new(boxes: Vec) -> Boxes { + let index = Index::new(&boxes); + let mut leaves = vec![usize::MAX; boxes.len()]; + for (leaf, &item) in index.items.iter().enumerate() { + leaves[item] = leaf; + } + Boxes { rects: boxes, index, leaves, fresh: Vec::new() } + } + + /// Item `item`'s box is now `rect`, `EMPTY` where it has none. + pub(crate) fn set(&mut self, item: usize, rect: Rect) { + if item >= self.rects.len() { + self.rects.resize(item + 1, EMPTY); + self.leaves.resize(item + 1, usize::MAX); + } + if self.rects[item] == rect { + return; + } + self.rects[item] = rect; + match self.leaves[item] { + usize::MAX if valid(&rect) && !self.fresh.contains(&item) => { + self.fresh.push(item); + if self.fresh.len() > FRESH { + *self = Boxes::new(std::mem::take(&mut self.rects)); + } + } + usize::MAX => {} + leaf => self.index.refit(leaf, if valid(&rect) { rect } else { EMPTY }), + } + } + + /// Item `item`'s box, where it has one. + pub(crate) fn get(&self, item: usize) -> Option { + self.rects.get(item).copied().filter(valid) + } + + /// Visits the items whose boxes meet `region` until `visit` returns true; whether it did. + pub(crate) fn some(&self, region: Rect, visit: &mut impl FnMut(usize) -> bool) -> bool { + let mut seen = |item: usize| self.get(item).is_some_and(|b| meets(&b, ®ion)) && visit(item); + self.index.some(region, &mut seen) || self.fresh.iter().any(|&item| seen(item)) + } + + /// The box around every box. + pub(crate) fn extent(&self) -> Rect { + self.fresh.iter().fold(self.index.root(), |extent, &item| union(extent, self.rects[item])) + } + + /// Every item's box, `EMPTY` where it has none, as a job is given them. + pub(crate) fn all(&self) -> &[Rect] { + &self.rects + } +} + +/// A packed R-tree over the valid boxes: leaves in Z-order of their middles, [`NODE`] children a +/// node. +struct Index { + /// The item at each leaf. + items: Vec, + /// Each node's box: the leaves, then each level up to the root. + nodes: Vec, + /// Where each level's nodes start, leaves first; the last is the node count. + starts: Vec, +} + +impl Index { + /// An index over `boxes`, leaving out those that are not [`valid`]. + fn new(boxes: &[Rect]) -> Index { + let mut items: Vec = (0..boxes.len()).filter(|&item| valid(&boxes[item])).collect(); + let extent = items.iter().fold(EMPTY, |extent, &item| union(extent, boxes[item])); + let cell = + |v: f64, lo: f64, hi: f64| if hi > lo { ((v - lo) / (hi - lo) * f64::from(u16::MAX)) as u32 } else { 0 }; + crate::order::by_key(&mut items, |&item| { + let middle = [boxes[item][0].midpoint(boxes[item][2]), boxes[item][1].midpoint(boxes[item][3])]; + let at = morton(cell(middle[0], extent[0], extent[2]), cell(middle[1], extent[1], extent[3])); + crate::order::pair(at, item as u32) + }); + let mut nodes: Vec = items.iter().map(|&item| boxes[item]).collect(); + let mut starts = vec![0]; + let mut level = 0; + loop { + let next = nodes.len(); + starts.push(next); + for first in (level..next).step_by(NODE) { + let node = nodes[first..(first + NODE).min(next)].iter().fold(EMPTY, |node, child| union(node, *child)); + nodes.push(node); + } + if nodes.len() - next <= 1 { + break; + } + level = next; + } + starts.push(nodes.len()); + Index { items, nodes, starts } + } + + /// Visits the items meeting `region` until `visit` returns true; whether it did. + fn some(&self, region: Rect, visit: &mut impl FnMut(usize) -> bool) -> bool { + let top = self.starts.len() - 2; + !self.items.is_empty() + && meets(&self.nodes[self.starts[top]], ®ion) + && self.descend(top, self.starts[top], region, visit) + } + + /// [`Index::some`] below `node` of `level`. + fn descend(&self, level: usize, node: usize, region: Rect, visit: &mut impl FnMut(usize) -> bool) -> bool { + let first = self.starts[level - 1] + (node - self.starts[level]) * NODE; + let last = (first + NODE).min(self.starts[level]); + (first..last).any(|child| { + meets(&self.nodes[child], ®ion) + && if level > 1 { self.descend(level - 1, child, region, visit) } else { visit(self.items[child]) } + }) + } + + /// Leaf `leaf`'s box is now `rect`: it, and every node above it, fit their children again. + fn refit(&mut self, leaf: usize, rect: Rect) { + self.nodes[leaf] = rect; + let mut child = leaf; + for level in 1..self.starts.len() - 1 { + let parent = self.starts[level] + (child - self.starts[level - 1]) / NODE; + let first = self.starts[level - 1] + (parent - self.starts[level]) * NODE; + let last = (first + NODE).min(self.starts[level]); + self.nodes[parent] = self.nodes[first..last].iter().fold(EMPTY, |node, child| union(node, *child)); + child = parent; + } + } + + /// The box around its leaves. + fn root(&self) -> Rect { + if self.items.is_empty() { EMPTY } else { self.nodes[self.starts[self.starts.len() - 2]] } + } +} + +/// Position along a Z-order curve on a 65536² grid. +fn morton(x: u32, y: u32) -> u32 { + let spread = |v: u32| { + let v = v & 0xffff; + let v = (v | (v << 8)) & 0x00ff_00ff; + let v = (v | (v << 4)) & 0x0f0f_0f0f; + let v = (v | (v << 2)) & 0x3333_3333; + (v | (v << 1)) & 0x5555_5555 + }; + spread(x) | (spread(y) << 1) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::layout::rect::grow; + + /// A stream of random numbers below `n`. + fn random(seed: &mut u64, n: u32) -> f64 { + *seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407); + f64::from(((*seed >> 33) % u64::from(n)) as u32) + } + + /// A random box within 1000 units, at most 30 across. + fn somewhere(seed: &mut u64) -> Rect { + let [x, y] = [random(seed, 1000), random(seed, 1000)]; + [x, y, x + random(seed, 30), y + random(seed, 30)] + } + + #[test] + fn boxes_moved_found_and_bounded_as_every_box_says() { + let mut seed = 3; + let mut all: Vec = (0..500).map(|_| somewhere(&mut seed)).collect(); + let mut boxes = Boxes::new(all.clone()); + for round in 0..300 { + // A box moves, goes, or comes past the last. + let item = random(&mut seed, 560) as usize; + let rect = if round % 7 == 0 { EMPTY } else { somewhere(&mut seed) }; + if item >= all.len() { + all.resize(item + 1, EMPTY); + } + all[item] = rect; + boxes.set(item, rect); + let region = grow(somewhere(&mut seed), 40.0); + let mut found = Vec::new(); + boxes.some(region, &mut |item| { + found.push(item); + false + }); + found.sort_unstable(); + let want: Vec = (0..all.len()).filter(|&i| valid(&all[i]) && meets(&all[i], ®ion)).collect(); + assert_eq!(found, want, "round {round}"); + let extent = all.iter().filter(|r| valid(r)).fold(EMPTY, |e, &r| union(e, r)); + assert_eq!(boxes.extent(), extent, "round {round}"); + } + } +} diff --git a/crates/latkit/src/layout/mod.rs b/crates/latkit/src/layout/mod.rs index 94e56c18..c9ecd3a7 100644 --- a/crates/latkit/src/layout/mod.rs +++ b/crates/latkit/src/layout/mod.rs @@ -1,5 +1,6 @@ //! Layouts on the CPU: layered drawing and stress over nested graphs, packing of parts, overlap //! removal, and a diagram's orthogonal routes. Each is pure, so it runs as well in a host's job. +pub(crate) mod boxes; pub(crate) mod layered; pub(crate) mod levels; pub(crate) mod overlap; diff --git a/crates/latkit/src/layout/rect.rs b/crates/latkit/src/layout/rect.rs index 08c31932..901ddb0f 100644 --- a/crates/latkit/src/layout/rect.rs +++ b/crates/latkit/src/layout/rect.rs @@ -33,3 +33,8 @@ pub(crate) fn grow(r: Rect, by: f64) -> Rect { pub(crate) fn meets(a: &Rect, b: &Rect) -> bool { a[0] <= b[2] && a[2] >= b[0] && a[1] <= b[3] && a[3] >= b[1] } + +/// Whether a box is finite and not inverted. +pub(crate) fn valid(r: &Rect) -> bool { + r.iter().all(|v| v.is_finite()) && r[0] <= r[2] && r[1] <= r[3] +} diff --git a/crates/latkit/src/layout/route.rs b/crates/latkit/src/layout/route.rs index 5e86d274..cde99e4a 100644 --- a/crates/latkit/src/layout/route.rs +++ b/crates/latkit/src/layout/route.rs @@ -11,7 +11,8 @@ use std::cmp::Ordering; use std::collections::BinaryHeap; -use super::rect::{EMPTY, Point, Rect, dot, grow, meets, union}; +use super::boxes::Boxes; +use super::rect::{Point, Rect, dot, grow, meets, union, valid}; use crate::scene::Side; /// Where a wire meets a vertex: the point on its boundary, the side it leaves by (None: any), and the @@ -45,7 +46,7 @@ pub(crate) struct Net { /// than two terminals at finite points gets no polylines; obstacles with a non-finite or inverted box /// are left out. pub(crate) fn route( - obstacles: &[[f64; 4]], + obstacles: &Boxes, groups: &[[f64; 4]], nets: &[Net], fixed: &[Vec<[f64; 2]>], @@ -118,9 +119,6 @@ const EPSILON: f64 = 1e-6; /// Points nearer than this are one point. const SAME: f64 = 1e-9; -/// Children per index node. -const NODE: usize = 16; - /// Attach points a branch tries, nearest first. const ATTACHES: usize = 8; @@ -181,11 +179,11 @@ struct Router<'a> { impl<'a> Router<'a> { /// A router among `boxes` and `groups`, tracks `spacing` apart; a negative or non-finite spacing /// is zero. - fn new(boxes: &'a [Rect], groups: &[Rect], spacing: f64) -> Router<'a> { + fn new(boxes: &'a Boxes, groups: &[Rect], spacing: f64) -> Router<'a> { let spacing = if spacing.is_finite() { spacing.max(0.0) } else { 0.0 }; let clearance = spacing * CLEARANCE; let groups = Groups { boxes: groups.iter().copied().filter(valid).collect(), clearance }; - Router { obstacles: Obstacles::new(boxes), groups, spacing, clearance, search: Search::new(2.0 * clearance) } + Router { obstacles: Obstacles { boxes }, groups, spacing, clearance, search: Search::new(2.0 * clearance) } } /// The shortest stub, and the least a track keeps a stub's end off its terminal. @@ -1102,27 +1100,21 @@ impl Groups { } } -/// The obstacles routing avoids, with an index over them. +/// The obstacles routing avoids, as boxes kept indexed give them. struct Obstacles<'a> { - boxes: &'a [Rect], - index: Index, + boxes: &'a Boxes, } -impl<'a> Obstacles<'a> { - /// The obstacles `boxes`, indexed. - fn new(boxes: &'a [Rect]) -> Obstacles<'a> { - Obstacles { boxes, index: Index::new(boxes) } - } - +impl Obstacles<'_> { /// The box of obstacle `owner`, when there is one. fn owned(&self, owner: Option) -> Option { - owner.and_then(|owner| self.boxes.get(owner as usize)).copied().filter(valid) + owner.and_then(|owner| self.boxes.get(owner as usize)) } /// Visits the obstacles meeting `region`, each grown by `by`, until `visit` returns true; whether /// it did. fn some(&self, region: Rect, by: f64, mut visit: impl FnMut(Rect) -> bool) -> bool { - self.index.some(region, &mut |item| visit(grow(self.boxes[item], by))) + self.boxes.some(region, &mut |item| visit(grow(self.boxes.get(item).expect("a box visited"), by))) } /// The obstacles meeting `region`, each grown by `by`. @@ -1142,84 +1134,6 @@ impl<'a> Obstacles<'a> { } } -/// A packed R-tree over the valid boxes: leaves in Z-order of their middles, [`NODE`] children a -/// node. -struct Index { - /// The box at each leaf. - items: Vec, - /// Each node's box: the leaves, then each level up to the root. - nodes: Vec, - /// Where each level's nodes start, leaves first; the last is the node count. - starts: Vec, -} - -impl Index { - /// An index over `boxes`, leaving out those that are not [`valid`]. - fn new(boxes: &[Rect]) -> Index { - let mut items: Vec = (0..boxes.len()).filter(|&item| valid(&boxes[item])).collect(); - let extent = items.iter().fold(EMPTY, |extent, &item| union(extent, boxes[item])); - let cell = - |v: f64, lo: f64, hi: f64| if hi > lo { ((v - lo) / (hi - lo) * f64::from(u16::MAX)) as u32 } else { 0 }; - crate::order::by_key(&mut items, |&item| { - let middle = [boxes[item][0].midpoint(boxes[item][2]), boxes[item][1].midpoint(boxes[item][3])]; - let at = morton(cell(middle[0], extent[0], extent[2]), cell(middle[1], extent[1], extent[3])); - crate::order::pair(at, item as u32) - }); - let mut nodes: Vec = items.iter().map(|&item| boxes[item]).collect(); - let mut starts = vec![0]; - let mut level = 0; - loop { - let next = nodes.len(); - starts.push(next); - for first in (level..next).step_by(NODE) { - let node = nodes[first..(first + NODE).min(next)].iter().fold(EMPTY, |node, child| union(node, *child)); - nodes.push(node); - } - if nodes.len() - next <= 1 { - break; - } - level = next; - } - starts.push(nodes.len()); - Index { items, nodes, starts } - } - - /// Visits the boxes meeting `region` until `visit` returns true; whether it did. - fn some(&self, region: Rect, visit: &mut impl FnMut(usize) -> bool) -> bool { - let top = self.starts.len() - 2; - !self.items.is_empty() - && meets(&self.nodes[self.starts[top]], ®ion) - && self.descend(top, self.starts[top], region, visit) - } - - /// [`Index::some`] below `node` of `level`. - fn descend(&self, level: usize, node: usize, region: Rect, visit: &mut impl FnMut(usize) -> bool) -> bool { - let first = self.starts[level - 1] + (node - self.starts[level]) * NODE; - let last = (first + NODE).min(self.starts[level]); - (first..last).any(|child| { - meets(&self.nodes[child], ®ion) - && if level > 1 { self.descend(level - 1, child, region, visit) } else { visit(self.items[child]) } - }) - } -} - -/// Position along a Z-order curve on a 65536² grid. -fn morton(x: u32, y: u32) -> u32 { - let spread = |v: u32| { - let v = v & 0xffff; - let v = (v | (v << 8)) & 0x00ff_00ff; - let v = (v | (v << 4)) & 0x0f0f_0f0f; - let v = (v | (v << 2)) & 0x3333_3333; - (v | (v << 1)) & 0x5555_5555 - }; - spread(x) | (spread(y) << 1) -} - -/// Whether a box is finite and not inverted. -fn valid(r: &Rect) -> bool { - r.iter().all(|v| v.is_finite()) && r[0] <= r[2] && r[1] <= r[3] -} - /// Whether box `a` lies within box `b`. fn within(a: &Rect, b: &Rect) -> bool { a[0] >= b[0] && a[1] >= b[1] && a[2] <= b[2] && a[3] <= b[3] @@ -1440,14 +1354,17 @@ mod tests { fn side_by_side_boxes_join_by_a_straight_run() { let boxes = [[0.0, 0.0, 20.0, 20.0], [100.0, 0.0, 120.0, 20.0]]; let nets = [Net { terminals: vec![on([20.0, 10.0], Side::Right, 0), on([100.0, 10.0], Side::Left, 1)] }]; - assert_eq!(route(&boxes, &[], &nets, &[], SPACING), vec![vec![vec![[20.0, 10.0], [100.0, 10.0]]]]); + assert_eq!( + route(&Boxes::new(boxes.to_vec()), &[], &nets, &[], SPACING), + vec![vec![vec![[20.0, 10.0], [100.0, 10.0]]]] + ); } #[test] fn a_wire_bends_around_an_obstacle_between() { let boxes = [[0.0, 0.0, 20.0, 20.0], [200.0, 0.0, 220.0, 20.0], [80.0, -30.0, 140.0, 50.0]]; let nets = [Net { terminals: vec![on([20.0, 10.0], Side::Right, 0), on([200.0, 10.0], Side::Left, 1)] }]; - let wire = &route(&boxes, &[], &nets, &[], SPACING)[0]; + let wire = &route(&Boxes::new(boxes.to_vec()), &[], &nets, &[], SPACING)[0]; assert!(orthogonal(wire), "{wire:?}"); assert!(segments(wire).len() > 2, "it bends: {wire:?}"); assert!(outside(wire, &boxes), "{wire:?}"); @@ -1460,9 +1377,9 @@ mod tests { let boxes = [[20.0, 45.0, 40.0, 55.0], [160.0, 45.0, 180.0, 55.0]]; let group = [90.0, 44.0, 110.0, 56.0]; let nets = [Net { terminals: vec![on([40.0, 50.0], Side::Right, 0), on([160.0, 50.0], Side::Left, 1)] }]; - let through = &route(&boxes, &[], &nets, &[], SPACING)[0]; + let through = &route(&Boxes::new(boxes.to_vec()), &[], &nets, &[], SPACING)[0]; assert_eq!(segments(through).len(), 1, "without the group, straight: {through:?}"); - let around = &route(&boxes, &[group], &nets, &[], SPACING)[0]; + let around = &route(&Boxes::new(boxes.to_vec()), &[group], &nets, &[], SPACING)[0]; assert!(outside(around, &[group]), "with it, around: {around:?}"); assert!(orthogonal(around) && one_tree(around, &nets[0].terminals), "{around:?}"); } @@ -1496,7 +1413,7 @@ mod tests { [100.0, -400.0, 120.0, 90.0], ]; let nets = [Net { terminals: vec![on([20.0, 10.0], Side::Right, 0), on([200.0, 10.0], Side::Left, 1)] }]; - let wire = &route(&boxes, &[], &nets, &[], SPACING)[0]; + let wire = &route(&Boxes::new(boxes.to_vec()), &[], &nets, &[], SPACING)[0]; assert!(orthogonal(wire) && outside(wire, &boxes), "{wire:?}"); } @@ -1512,7 +1429,7 @@ mod tests { Net { terminals: vec![on([10.0, 20.0], Side::Top, 0), on([100.0, 110.0], Side::Left, 1)] }, Net { terminals: vec![on([10.0, 200.0], Side::Bottom, 2), on([120.0, -90.0], Side::Right, 3)] }, ]; - let routes = route(&boxes, &[], &nets, &[], SPACING); + let routes = route(&Boxes::new(boxes.to_vec()), &[], &nets, &[], SPACING); for (net, wire) in nets.iter().zip(&routes) { assert!(orthogonal(wire) && outside(wire, &boxes), "{wire:?}"); for terminal in &net.terminals { @@ -1535,7 +1452,7 @@ mod tests { on([100.0, 70.0], Side::Left, 2), ], }; - let wire = &route(&boxes, &[], std::slice::from_ref(&net), &[], SPACING)[0]; + let wire = &route(&Boxes::new(boxes.to_vec()), &[], std::slice::from_ref(&net), &[], SPACING)[0]; assert!(wire.len() >= 3, "a junction splits it: {wire:?}"); assert!(orthogonal(wire) && outside(wire, &boxes), "{wire:?}"); assert!(one_tree(wire, &net.terminals), "{wire:?}"); @@ -1549,7 +1466,7 @@ mod tests { Net { terminals: vec![on([20.0, 10.0], Side::Right, 0), on([100.0, 70.0], Side::Left, 2)] }, Net { terminals: vec![on([20.0, 40.0], Side::Right, 1), on([100.0, 100.0], Side::Left, 3)] }, ]; - let routes = route(&boxes, &[], &nets, &[], SPACING); + let routes = route(&Boxes::new(boxes.to_vec()), &[], &nets, &[], SPACING); // A segment as the axis it runs along, its line, and its extent along it. let run = |[a, b]: [Point; 2]| { let axis = usize::from(a[0] == b[0]); @@ -1581,7 +1498,7 @@ mod tests { let mut boxes = vec![[0.0, 0.0, 20.0, 20.0], [100.0, 0.0, 120.0, 20.0]]; boxes.extend(ring); let nets = [Net { terminals: vec![on([20.0, 10.0], Side::Right, 0), on([100.0, 10.0], Side::Left, 1)] }]; - let wire = &route(&boxes, &[], &nets, &[], SPACING)[0]; + let wire = &route(&Boxes::new(boxes), &[], &nets, &[], SPACING)[0]; assert!(orthogonal(wire) && one_tree(wire, &nets[0].terminals), "{wire:?}"); } @@ -1621,8 +1538,9 @@ mod tests { Net { terminals } }) .collect(); - let routes = route(&boxes, &[], &nets, &[], SPACING); - assert_eq!(routes, route(&boxes, &[], &nets, &[], SPACING), "the same input routes the same"); + let obstacles = Boxes::new(boxes.clone()); + let routes = route(&obstacles, &[], &nets, &[], SPACING); + assert_eq!(routes, route(&obstacles, &[], &nets, &[], SPACING), "the same input routes the same"); for (net, wire) in nets.iter().zip(&routes) { assert!(orthogonal(wire), "{wire:?}"); assert!(one_tree(wire, &net.terminals), "{net:?}: {wire:?}"); @@ -1642,7 +1560,7 @@ mod tests { Net { terminals: vec![free] }, Net { terminals: vec![free, Terminal { at: [f64::NAN, 0.0], ..free }] }, ]; - assert_eq!(route(&[], &[], &nets, &[], SPACING), vec![Vec::>::new(); 3]); + assert_eq!(route(&Boxes::default(), &[], &nets, &[], SPACING), vec![Vec::>::new(); 3]); } #[test] @@ -1654,7 +1572,7 @@ mod tests { Terminal { at: [100.0, 50.0], side: None, owner: None }, ], }]; - let wire = &route(&boxes, &[], &nets, &[], SPACING)[0]; + let wire = &route(&Boxes::new(boxes.to_vec()), &[], &nets, &[], SPACING)[0]; assert!(orthogonal(wire) && outside(wire, &boxes), "{wire:?}"); assert_eq!(leaving(wire, [20.0, 10.0])[1], 10.0, "it leaves by the side it is on: {wire:?}"); } diff --git a/crates/latkit/src/sheet/mod.rs b/crates/latkit/src/sheet/mod.rs index 639d1c87..061ceff2 100644 --- a/crates/latkit/src/sheet/mod.rs +++ b/crates/latkit/src/sheet/mod.rs @@ -22,7 +22,8 @@ use crate::expr::cpu::Rows; use crate::gpu::atlas; use crate::gpu::shaders::{HEADER_BAND, TEXT_BASE}; use crate::host::{Job, Key, Placement, TextRun}; -use crate::layout::rect::{Rect, around, union}; +use crate::layout::boxes::Boxes; +use crate::layout::rect::{EMPTY, Rect, around, union, valid}; use crate::{Layout, Marker, Pending, Shape, Side, Theme, Vertices}; /// Texts measured past which the sheet forgets them all. @@ -38,14 +39,16 @@ pub(crate) struct Sheet { /// Places layouts gave rows without positions, kept by row, also after a row leaves, so undo /// puts it back where it was. places: FxHashMap, - /// Each orthogonal wire's route, by edge row. - routes: FxHashMap, + /// Each orthogonal wire's route, by edge row, and those due to route again. + routes: wires::Routes, /// The size each run of text takes, as its host measured it. measured: FxHashMap, /// The box of every placed vertex row the last update made: what a change moved. boxes: FxHashMap, - /// The box holding every placed vertex and every wire the last update made. - extent: Option<[f64; 4]>, + /// Every vertex's box, y up, as routing avoids it, none for a group: refit as vertices move. + blocks: Boxes, + /// The groups' boxes, y up, which wires cross at a cost. + groups: Vec, /// What the derived tables were last made from. stamp: u64, /// Bumps when places start over, so layouts asked before are dropped. @@ -187,9 +190,13 @@ impl Sheet { self.boxes.get(row).copied() } - /// The box holding every placed vertex and every wire, units with y down, once anything is placed. + /// The box holding every placed vertex and every wire, units with y down, once anything is placed: + /// the boxes of its blocks, its groups, and its wires, as it keeps them. pub(crate) fn extent(&self) -> Option<[f64; 4]> { - self.extent + let wires = self.drawn.as_ref()?.wired.extent(); + let up = self.groups.iter().fold(self.blocks.extent(), |extent, &group| union(extent, group)); + let extent = union([up[0], -up[3], up[2], -up[1]], wires); + valid(&extent).then_some(extent) } /// Takes the answers that landed: places asked in the current generation, and routes. @@ -247,12 +254,13 @@ impl Sheet { self.stand(v); } stand_groups(&mut vertices, &self.places, &header, padding); + self.blocks = Boxes::new(vertices.iter().map(obstacle).collect()); + self.groups = groups(&vertices); self.place(&vertices, vertex_marks, edge_marks, &header, padding, input, jobs); let moved = wires::moved(&self.boxes, &vertices); let mut drawn = Drawn { vertices, wired: wires::Wired::default(), reads: Vec::new(), from: 0 }; self.wire(&mut drawn, marks, &wires::Change { moved: &moved, touched: None }, input, jobs); self.boxes = drawn.vertices.iter().filter_map(|v| Some((v.key.clone(), v.bounds?))).collect(); - self.extent = extent(&drawn); write(vertex_marks, &drawn.vertices, edge_marks, &drawn.wired.parts, &mut self.tables); drawn } @@ -298,7 +306,16 @@ impl Sheet { touched.sort_unstable(); touched.dedup(); } - self.place(&drawn.vertices, vertex_marks, edge_marks, &header, padding, input, jobs); + for &v in &touched { + self.blocks.set(v, obstacle(&drawn.vertices[v])); + } + if touched.iter().any(|&v| drawn.vertices[v].group) { + self.groups = groups(&drawn.vertices); + } + // A layout places only rows without a place: an edit asks for one only where it took one away. + if touched.iter().any(|&v| drawn.vertices[v].bounds.is_none()) { + self.place(&drawn.vertices, vertex_marks, edge_marks, &header, padding, input, jobs); + } let written = self.wire(&mut drawn, marks, &wires::Change { moved: &moved, touched: Some(&touched) }, input, jobs); for &v in &touched { @@ -308,7 +325,6 @@ impl Sheet { None => self.boxes.remove(&vertex.key), }; } - self.extent = extent(&drawn); if !write_rows(marks, &drawn, &touched, &written, &mut self.tables) { write(vertex_marks, &drawn.vertices, edge_marks, &drawn.wired.parts, &mut self.tables); } @@ -341,6 +357,8 @@ impl Sheet { vertices: &drawn.vertices, marks: vertex_marks, edges: edge_marks, + blocks: &self.blocks, + groups: &self.groups, spacing: f64::from(theme.edge.spacing), bend: f64::from(theme.diagram.bend), port: f64::from(theme.diagram.port_size), @@ -612,10 +630,15 @@ fn framing(vertex_marks: &[VertexMark<'_>], theme: &Theme) -> (Vec, f64) { (vertex_marks.iter().map(|vm| header(vm.mark, theme)).collect(), f64::from(theme.diagram.group_padding)) } -/// The box holding every placed vertex and every wire. -fn extent(drawn: &Drawn) -> Option { - let points = drawn.wired.parts.iter().flatten().flatten().flatten().map(|&[x, y]| [x, y, x, y]); - drawn.vertices.iter().filter_map(|v| v.bounds).chain(points).reduce(union) +/// Vertex `v`'s box as routing avoids it, y up; none for a group, which holds wires rather than +/// turning them away. +fn obstacle(v: &Vertex) -> Rect { + v.bounds.filter(|_| !v.group).map_or(EMPTY, |b| [b[0], -b[3], b[2], -b[1]]) +} + +/// The groups' boxes, y up, which wires cross at a cost and keep off. +fn groups(vertices: &[Vertex]) -> Vec { + vertices.iter().filter(|v| v.group).filter_map(|v| v.bounds).map(|b| [b[0], -b[3], b[2], -b[1]]).collect() } /// The outline a vertex mark's markers draw: groups and blocks are boxes, the rest their shape. @@ -824,7 +847,8 @@ mod tests { const BLOCKS: u64 = 24; /// Blocks with labels and ports, most in one of two groups, wired by orthogonal nets, straight - /// edges with lanes between the same two blocks, and orthogonal edges with ends. + /// edges with lanes between the same two blocks, and orthogonal edges with ends, some leaving one + /// block by the same side. fn diagram() -> (Scene, Data) { let scene = Scene::new(Diagram { grid: Grid::Off }) .draw(Vertices::of("d", "G").contains("B", "group")) @@ -860,7 +884,7 @@ mod tests { .set(Table::new("N", (0..BLOCKS / 2).collect::>())) .set(blocks) .set(pairs(vec![0, 0, 3, 7], vec![1, 1, 9, 15], "E")) - .set(pairs(vec![4, 5, 11, 20], vec![10, 17, 12, 21], "F")), + .set(pairs(vec![4, 5, 11, 20, 4, 4, 9], vec![10, 17, 12, 21, 11, 16, 4], "F")), ) .expect("valid"); (scene, data) @@ -885,10 +909,57 @@ mod tests { None } + /// Brings `sheet` up to `data`, routing on the frame as while rows are dragged; whether it drew + /// only what an edit reached. + fn derive(sheet: &mut Sheet, scene: &Scene, data: &Data) -> bool { + let (counters, errors) = (Counters::default(), Arc::new(Mutex::new(Vec::new()))); + let now = data.snapshot(); + let input = Input { + scene, + scene_version: 1, + data: std::slice::from_ref(&now), + host: None, + dragging: true, + paged: None, + }; + let edit = sheet.drawn.as_ref().is_some_and(|drawn| edited(drawn, &input, &marks(&input).0).is_some()); + sheet.update(&input, &mut Jobs::new(None, Waker::noop(), &counters, &errors, crate::derive::FRAME_WORK)); + edit + } + + #[test] + fn a_wire_routes_again_around_a_block_moved_into_its_way() { + let scene = Scene::new(Diagram { grid: Grid::Off }) + .draw(Edges::of("d", "F").ends("from", "to").route(Route::Orthogonal)) + .draw(Vertices::of("d", "B").position("x", "y").label(Label::new("name"))); + let blocks = Table::new("B", vec![0_u64, 1, 2]) + .field("x", vec![0.0, 400.0, 200.0]) + .field("y", vec![0.0, 0.0, 400.0]) + .field("name", vec!["a", "b", "c"]); + let wire = Table::new("F", Ids::count(1)).reference("from", "B", vec![0_u64]).reference("to", "B", vec![1_u64]); + let data = Data::new("d"); + data.apply(Patch::new().set(blocks).set(wire)).expect("valid"); + let mut sheet = Sheet::default(); + derive(&mut sheet, &scene, &data); + // The third block moves into the wire's way; the wire meets neither it nor anything it moved. + data.apply(Table::new("B", vec![2_u64]).field("y", vec![0.0])).expect("valid edit"); + assert!(derive(&mut sheet, &scene, &data), "an edit"); + let b = sheet.bounds(&Row { data: "d".into(), table: "B".into(), id: 2_u64.into() }).expect("placed"); + let Some(Value::Parts(parts)) = sheet.tables.get(&("d".into(), "F".into())).and_then(|t| t.get(0_u64, "route")) + else { + panic!("a route"); + }; + for [p, q] in parts.iter().flat_map(|part| part.windows(2)).map(|pq| [pq[0], pq[1]]) { + for t in (0..=32).map(|t| f64::from(t) / 32.0) { + let [x, y] = [p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t]; + assert!(!(b[0] < x && x < b[2] && b[1] < y && y < b[3]), "{parts:?} runs through {b:?}"); + } + } + } + #[test] fn an_edit_derives_what_deriving_every_row_does() { let (scene, data) = diagram(); - let (counters, errors) = (Counters::default(), Arc::new(Mutex::new(Vec::new()))); let (mut edits, mut whole) = (Sheet::default(), Sheet::default()); let mut seed = 0x9e37_79b9_7f4a_7c15_u64; let mut random = |n: u64| { @@ -921,27 +992,13 @@ mod tests { } data.apply(edit).expect("valid edit"); } - let now = data.snapshot(); - let input = Input { - scene: &scene, - scene_version: 1, - data: std::slice::from_ref(&now), - host: None, - dragging: true, - paged: None, - }; - if let Some(drawn) = &edits.drawn { - redrawn += usize::from(edited(drawn, &input, &marks(&input).0).is_some()); - } + redrawn += usize::from(derive(&mut edits, &scene, &data)); whole.drawn = None; - for sheet in [&mut edits, &mut whole] { - sheet - .update(&input, &mut Jobs::new(None, Waker::noop(), &counters, &errors, crate::derive::FRAME_WORK)); - } + derive(&mut whole, &scene, &data); if let Some(difference) = differ(&edits.tables, &whole.tables) { panic!("step {step}: {difference}"); } - assert_eq!(edits.extent, whole.extent, "step {step}"); + assert_eq!(edits.extent(), whole.extent(), "step {step}"); } assert!(redrawn > 30, "edits derived only what they reached: {redrawn} of 60"); } diff --git a/crates/latkit/src/sheet/wires.rs b/crates/latkit/src/sheet/wires.rs index a926d8ec..571bfa95 100644 --- a/crates/latkit/src/sheet/wires.rs +++ b/crates/latkit/src/sheet/wires.rs @@ -5,7 +5,9 @@ //! around every block. A route holds until its terminals move or a box near it changes. Then it //! routes again on the frame when few do or rows are being dragged, else in a host job, keeping its //! old path while its terminals hold, or an elbow while they moved, until the job answers. After an -//! edit of a few vertices, only the wires meeting them and those it makes route again are worked out. +//! edit of a few vertices, only the wires meeting them, those leaving by a side it changed, and those +//! near what it moved are worked out: the sides, every route's box, and every wire's are kept as +//! edits change them, so neither finding those wires nor the box around them all walks every wire. use std::hash::{Hash, Hasher}; @@ -16,6 +18,7 @@ use crate::data::{Row, Table, Value}; use crate::derive::graph::{EdgeMark, Joins, VertexMark, key_of, port_codes}; use crate::derive::{Input, Jobs, answers, spread}; use crate::host::{Job, Key, Wiring}; +use crate::layout::boxes::Boxes; use crate::layout::rect::{EMPTY, Point, Rect, dot, grow, meets, union}; use crate::layout::route::{Net, Terminal, finish, route, simplify}; use crate::{Pending, Route, Shape, Side}; @@ -32,34 +35,232 @@ const CLEARANCE: f64 = 2.0; /// A wire's parts. pub(crate) type Parts = Vec>; -/// A route: its polylines, y down, before bends round; a stamp of the terminals it joins; the box it -/// spans; and whether it is to route again, a box having moved near it while it waited. +/// A route: its polylines, y down, before bends round; a stamp of the terminals it joins; and the box +/// it spans. pub(crate) struct Routed { lines: Parts, terminals: u64, reach: Rect, - stale: bool, } impl Routed { fn new(lines: Parts, terminals: u64) -> Routed { let reach = lines.iter().flatten().fold(EMPTY, |reach, &p| union(reach, dot(p))); - Routed { lines, terminals, reach, stale: false } + Routed { lines, terminals, reach } } - /// A wire waiting for its first route: due until it has one, and in nothing's way. + /// A wire waiting for its first route, in nothing's way. fn waiting() -> Routed { - Routed { lines: Vec::new(), terminals: 0, reach: EMPTY, stale: true } + Routed { lines: Vec::new(), terminals: 0, reach: EMPTY } + } +} + +/// Every orthogonal wire's route, by edge row, each in a slot of its own; the box each spans, by slot, +/// indexed, which finds the routes a box that moved is near; and the rows due to route again: near a +/// box that moved, or due before and not routed since. +#[derive(Default)] +pub(crate) struct Routes { + by_row: FxHashMap, + /// The row each slot holds the route of; `None` where it is free. + slots: Vec>, + free: Vec, + reaches: Boxes, + due: FxHashSet, +} + +impl Routes { + /// Row `row`'s route, where it has one. + fn get(&self, row: &Row) -> Option<&Routed> { + self.by_row.get(row).map(|(routed, _)| routed) + } + + /// Row `row` routes along `routed`, due no longer. + fn routed(&mut self, row: Row, routed: Routed) { + self.due.remove(&row); + self.put(row, routed); + } + + /// Row `row` routes again: until it does, it keeps its route, or waits for its first. + fn due(&mut self, row: &Row) { + if !self.by_row.contains_key(row) { + self.put(row.clone(), Routed::waiting()); + } + self.due.insert(row.clone()); + } + + /// Row `row`'s route is `routed`, in its slot, or in a free one. + fn put(&mut self, row: Row, routed: Routed) { + let reach = routed.reach; + let slot = if let Some((was, slot)) = self.by_row.get_mut(&row) { + *was = routed; + *slot + } else { + let slot = self.free.pop().unwrap_or(self.slots.len()); + if slot == self.slots.len() { + self.slots.push(None); + } + self.slots[slot] = Some(row.clone()); + self.by_row.insert(row, (routed, slot)); + slot + }; + self.reaches.set(slot, reach); + } + + /// Forgets the routes of the rows `kept` does not hold. + fn retain(&mut self, kept: impl Fn(&Row) -> bool) { + let gone: Vec = self.by_row.keys().filter(|row| !kept(row)).cloned().collect(); + for row in gone { + if let Some((_, slot)) = self.by_row.remove(&row) { + self.slots[slot] = None; + self.free.push(slot); + self.reaches.set(slot, EMPTY); + } + self.due.remove(&row); + } + } + + /// The rows whose routes reach into `region`. + fn near(&self, region: Rect) -> Vec { + let mut near = Vec::new(); + self.reaches.some(region, &mut |slot| { + near.extend(self.slots[slot].clone()); + false + }); + near } } /// What wiring keeps between updates, so an edit works out only the wires it reaches: each edge mark's -/// rows' parts, the edge and net rows meeting each vertex, and each edge mark's lanes. +/// rows' parts and the boxes they run within, indexed, the edge and net rows meeting each vertex, each +/// edge mark's lanes, and the sides orthogonal edges with ends leave their vertices by. #[derive(Default)] pub(crate) struct Wired { pub(crate) parts: Vec>, meeting: Vec>, lanes: Vec>>, + /// Each edge row's box, y down, by its place among every edge mark's rows. + boxes: Boxes, + /// Where each edge mark's rows start among them. + firsts: Vec, + sides: Sides, +} + +impl Wired { + /// The box around every wire, y down. + pub(crate) fn extent(&self) -> Rect { + self.boxes.extent() + } + + /// The box edge mark `e`'s row `row` runs within, y down: its parts' points. + fn bounds(&self, e: usize, row: usize) -> Rect { + self.parts[e][row].iter().flatten().fold(EMPTY, |b, &p| union(b, dot(p))) + } +} + +/// Where each orthogonal edge with ends leaves its two vertices, and by vertex and side the ends +/// leaving there, ordered along it by where their other ends lie, so wires leave a side without +/// crossing. Kept between edits, so an edit orders again only the sides it changed, and works out +/// again only the edges leaving by them. +#[derive(Default)] +struct Sides { + /// How each edge leaves its vertex at either end. + of: FxHashMap, + /// The ends leaving by each vertex's side, in order. + at: FxHashMap<(usize, Side), Vec>, +} + +/// An edge: its edge mark and row. +type Edge = (usize, usize); + +/// How an end of an edge leaves: its vertex, the side it leaves by, and where the other end lies +/// along that side. +type Facing = (usize, Side, f64); + +/// An end leaving by a side: where its other end lies along the side, its edge, and which end. +type Leaving = (f64, Edge, usize); + +impl Sides { + /// The sides each of `edges`, between the vertices it names, leaves them by. + fn new(edges: impl Iterator)>, vertices: &[Vertex]) -> Sides { + let mut sides = Sides::default(); + for (edge, ends) in edges { + let Some(facings) = facings(ends, vertices) else { continue }; + for (end, &(v, side, along)) in facings.iter().enumerate() { + sides.at.entry((v, side)).or_default().push((along, edge, end)); + } + sides.of.insert(edge, facings); + } + for leaving in sides.at.values_mut() { + crate::order::by_wide_key(leaving, order); + } + sides + } + + /// Edge `edge`, between the vertices `ends` names, leaves them by the sides they face each other + /// by now: out of the sides it left by, into these. The sides it left and joined go into + /// `changed`. + fn face(&mut self, edge: Edge, ends: Option<[usize; 2]>, vertices: &[Vertex], changed: &mut Vec<(usize, Side)>) { + let now = facings(ends, vertices); + if self.of.get(&edge).copied() == now { + return; + } + for (end, (v, side, along)) in self.of.remove(&edge).into_iter().flatten().enumerate() { + let leaving = self.at.get_mut(&(v, side)).expect("an end leaves by its side"); + let it = (along, edge, end); + leaving.remove(leaving.partition_point(|x| order(x) < order(&it))); + if leaving.is_empty() { + self.at.remove(&(v, side)); + } + changed.push((v, side)); + } + let Some(now) = now else { return }; + for (end, &(v, side, along)) in now.iter().enumerate() { + let leaving = self.at.entry((v, side)).or_default(); + let it = (along, edge, end); + leaving.insert(leaving.partition_point(|x| order(x) < order(&it)), it); + changed.push((v, side)); + } + self.of.insert(edge, now); + } + + /// Edge `edge`'s end `end`: its vertex, the side it leaves by, its place along it, and how many + /// leave there. + fn place(&self, edge: Edge, end: usize) -> Option<(usize, Side, u32, u32)> { + let (v, side, along) = self.of.get(&edge)?[end]; + let leaving = self.at.get(&(v, side))?; + let it = (along, edge, end); + let place = leaving.partition_point(|x| order(x) < order(&it)); + Some((v, side, place as u32, leaving.len() as u32)) + } + + /// The edges leaving by any of `sides`. + fn leaving<'s>(&'s self, sides: &'s [(usize, Side)]) -> impl Iterator + 's { + sides.iter().filter_map(|side| self.at.get(side)).flatten().map(|&(_, edge, _)| edge) + } +} + +/// Where an end lies along its side, then its edge: within a side, no edge leaves twice, as its ends +/// stand at two vertices. +fn order(&(along, (e, row), _): &Leaving) -> u128 { + crate::order::wide(crate::order::number(along), crate::order::pair(e as u32, row as u32)) +} + +/// How an edge between vertices `ends` leaves each: by the sides they face each other by, where the +/// other end lies along each; `None` unless both stand, apart. +fn facings(ends: Option<[usize; 2]>, vertices: &[Vertex]) -> Option<[Facing; 2]> { + let [a, b] = ends.filter(|[a, b]| a != b)?; + let (ba, bb) = (vertices[a].bounds?, vertices[b].bounds?); + let (side_a, side_b) = facing(ba, bb); + let along = |side: Side, other: Rect| match side { + Side::Left | Side::Right => center(other)[1], + Side::Top | Side::Bottom => center(other)[0], + }; + Some([(a, side_a, along(side_a, bb)), (b, side_b, along(side_b, ba))]) +} + +/// Whether an edge mark routes orthogonal edges with ends, which share the sides they leave by. +fn sided(em: &EdgeMark<'_>) -> bool { + em.mark.route == Some(Route::Orthogonal) && em.mark.ends.is_some() } /// What changed since wires were last worked out: the boxes that moved, added, or went, and after an @@ -79,12 +280,15 @@ pub(crate) struct Routing { clearance: f64, } -/// What wires are worked out from: the diagram's vertices, vertex marks, and edge marks, and in -/// units how far apart parallel wires run, how round their bends are, and how wide ports are. +/// What wires are worked out from: the diagram's vertices, vertex marks, and edge marks; the blocks +/// wires route around, by vertex, and the groups they route among, y up; and in units how far apart +/// parallel wires run, how round their bends are, and how wide ports are. pub(crate) struct Context<'a> { pub(crate) vertices: &'a [Vertex], pub(crate) marks: &'a [VertexMark<'a>], pub(crate) edges: &'a [EdgeMark<'a>], + pub(crate) blocks: &'a Boxes, + pub(crate) groups: &'a [Rect], pub(crate) spacing: f64, pub(crate) bend: f64, pub(crate) port: f64, @@ -112,16 +316,19 @@ struct Ask { /// Takes a routing job's answer: each route it worked out, as routed from the terminals it was asked /// for; one near a box that moved since it was asked routes again. -pub(crate) fn land(routes: &mut FxHashMap, routing: &Routing, bytes: &[u8]) { +pub(crate) fn land(routes: &mut Routes, routing: &Routing, bytes: &[u8]) { for (key, values) in answers(bytes, &["route"]) { let (Some((row, terminals)), Some(Some(Value::Parts(lines)))) = (routing.rows.get(&key), values.into_iter().next()) else { continue; }; - let mut routed = Routed::new(flipped(&lines), *terminals); - routed.stale = routing.moved.iter().any(|m| meets(&grow(routed.reach, routing.clearance), m)); - routes.insert(row.clone(), routed); + let routed = Routed::new(flipped(&lines), *terminals); + let moved = routing.moved.iter().any(|m| meets(&grow(routed.reach, routing.clearance), m)); + routes.routed(row.clone(), routed); + if moved { + routes.due(row); + } } } @@ -155,30 +362,27 @@ pub(crate) fn paths( jobs: &mut Jobs<'_>, change: &Change<'_>, wired: &mut Wired, - (routes, routing): (&mut FxHashMap, &mut Option), + (routes, routing): (&mut Routes, &mut Option), ) -> Vec> { let (vertices, edge_marks) = (context.vertices, context.edges); let codes: Vec> = context.marks.iter().map(|vm| port_codes(vm.mark)).collect(); let joins: Vec> = edge_marks.iter().map(|em| Joins::new(context.marks, &codes, em)).collect(); let orthogonal = |e: usize| edge_marks[e].mark.route == Some(Route::Orthogonal); + // A route near a box that moved routes again. + let clearance = CLEARANCE * 2.0 * context.spacing; + for &m in change.moved { + for row in routes.near(grow(m, clearance)) { + routes.due(&row); + } + } let rows = if let Some(touched) = change.touched { - reached(context, touched, change.moved, wired, routes, routing.as_ref()) + reached(context, &joins, touched, wired, routes, routing.as_ref()) } else { - wired.parts = edge_marks.iter().map(|em| vec![Vec::new(); em.table.len()]).collect(); - wired.meeting = vec![Vec::new(); vertices.len()]; - wired.lanes = (0..edge_marks.len()) - .map(|e| { - let em = &edge_marks[e]; - let spaced = !orthogonal(e) && em.mark.bends.is_none(); - if spaced { spread(em.table.len(), |r| joins[e].ends(r)) } else { None } - }) - .collect(); - edge_marks.iter().map(|em| (0..em.table.len()).collect()).collect() + every_row(context, &joins, wired) }; // Which vertices each row meets is told once, as every row is worked out. let telling = change.touched.is_none(); let mut asks: Vec = Vec::new(); - let mut edges: Vec<(usize, [usize; 2])> = Vec::new(); for (e, em) in edge_marks.iter().enumerate() { let joins = &joins[e]; let orthogonal = orthogonal(e); @@ -197,8 +401,8 @@ pub(crate) fn paths( continue; } if orthogonal { - edges.push((asks.len(), [a, b])); - asks.push(Ask { mark: e, row, key, ends: Vec::new(), outputs: 1 }); + let ends = faced(&wired.sides, (e, row), vertices); + asks.push(Ask { mark: e, row, key, ends, outputs: 1 }); } else { let through = em.mark.bends.as_ref().map(|field| bends(em.table, field, row)).unwrap_or_default(); let shift = wired.lanes[e].as_ref().map_or(0.0, |lanes| lanes[row] * context.spacing); @@ -239,20 +443,59 @@ pub(crate) fn paths( } } } - face(&mut asks, &edges, vertices); wire(context, input, jobs, &asks, change, (routes, routing), &mut wired.parts); + // The box each row worked out runs within, which the extent of every wire is kept from. + if telling { + let boxes = (0..edge_marks.len()).flat_map(|e| (0..edge_marks[e].table.len()).map(move |row| (e, row))); + wired.boxes = Boxes::new(boxes.map(|(e, row)| wired.bounds(e, row)).collect()); + } else { + for (e, rows) in rows.iter().enumerate() { + for &row in rows { + let bounds = wired.bounds(e, row); + wired.boxes.set(wired.firsts[e] + row, bounds); + } + } + } rows } -/// The rows an edit of vertices `touched` reaches, by edge mark, in order: those meeting them; where -/// one is an orthogonal edge with ends, every such edge, as they share the sides they leave by; and the -/// orthogonal wires due to route again: stale, asked of the job under way, or near a box that `moved`. +/// Every row of every edge mark, by edge mark, with `wired` started over for them: no parts yet, each +/// mark's lanes, where its rows start among them all, and the sides its orthogonal edges with ends +/// leave by. +fn every_row(context: &Context<'_>, joins: &[Joins<'_>], wired: &mut Wired) -> Vec> { + let edge_marks = context.edges; + wired.parts = edge_marks.iter().map(|em| vec![Vec::new(); em.table.len()]).collect(); + wired.meeting = vec![Vec::new(); context.vertices.len()]; + wired.lanes = (0..edge_marks.len()) + .map(|e| { + let em = &edge_marks[e]; + let spaced = em.mark.route != Some(Route::Orthogonal) && em.mark.bends.is_none(); + if spaced { spread(em.table.len(), |r| joins[e].ends(r)) } else { None } + }) + .collect(); + wired.firsts = edge_marks + .iter() + .scan(0, |first, em| { + *first += em.table.len(); + Some(*first - em.table.len()) + }) + .collect(); + let edges = (0..edge_marks.len()) + .filter(|&e| sided(&edge_marks[e])) + .flat_map(|e| (0..edge_marks[e].table.len()).map(move |row| (e, row))); + wired.sides = Sides::new(edges.map(|(e, row)| ((e, row), joins[e].ends(row))), context.vertices); + edge_marks.iter().map(|em| (0..em.table.len()).collect()).collect() +} + +/// The rows an edit of vertices `touched` reaches, by edge mark, in order: those meeting them; the +/// orthogonal edges with ends leaving by a side whose ends it changed; and the orthogonal wires due to +/// route again, or asked of the job under way. fn reached( context: &Context<'_>, + joins: &[Joins<'_>], touched: &[usize], - moved: &[Rect], - wired: &Wired, - routes: &FxHashMap, + wired: &mut Wired, + routes: &Routes, routing: Option<&Routing>, ) -> Vec> { let edge_marks = context.edges; @@ -260,16 +503,24 @@ fn reached( for &[e, row] in touched.iter().flat_map(|&v| &wired.meeting[v]) { rows[e as usize].push(row as usize); } - let sided = |e: usize| edge_marks[e].mark.route == Some(Route::Orthogonal) && edge_marks[e].mark.ends.is_some(); - if (0..edge_marks.len()).any(|e| sided(e) && !rows[e].is_empty()) { - for e in (0..edge_marks.len()).filter(|&e| sided(e)) { - rows[e] = (0..edge_marks[e].table.len()).collect(); + // An edge with ends meeting what the edit moved leaves by other sides, or from other places along + // them: so do the edges leaving by a side it left or joined. + let mut changed = Vec::new(); + for (e, rows) in rows.iter_mut().enumerate().filter(|(e, _)| sided(&edge_marks[*e])) { + rows.sort_unstable(); + rows.dedup(); + for &row in rows.iter() { + wired.sides.face((e, row), joins[e].ends(row), context.vertices, &mut changed); } } - let clearance = CLEARANCE * 2.0 * context.spacing; - let due = routes.iter().filter(|(_, r)| r.stale || moved.iter().any(|m| meets(&grow(r.reach, clearance), m))); + crate::order::by_key(&mut changed, |&(v, side)| crate::order::pair(v as u32, side.code())); + changed.dedup(); + let leaving: Vec = wired.sides.leaving(&changed).collect(); + for (e, row) in leaving { + rows[e].push(row); + } let asked = routing.into_iter().flat_map(|r| r.rows.values().map(|(row, _)| row)); - for (e, row) in due.map(|(row, _)| row).chain(asked).filter_map(|row| drawing(context, row)) { + for (e, row) in routes.due.iter().chain(asked).filter_map(|row| drawing(context, row)) { rows[e].push(row); } for rows in &mut rows { @@ -293,17 +544,16 @@ fn wire( jobs: &mut Jobs<'_>, asks: &[Ask], change: &Change<'_>, - (routes, routing): (&mut FxHashMap, &mut Option), + (routes, routing): (&mut Routes, &mut Option), out: &mut [Vec], ) { - let moved = change.moved; if let Some(routing) = routing.as_mut() { - routing.moved.extend_from_slice(moved); + routing.moved.extend_from_slice(change.moved); } // Routes of rows no longer drawn are forgotten, once every row is worked out. if change.touched.is_none() { let drawn: FxHashSet<&Row> = asks.iter().map(|ask| &ask.key).collect(); - routes.retain(|row, _| drawn.contains(row)); + routes.retain(|row| drawn.contains(row)); } if asks.is_empty() { return; @@ -313,9 +563,9 @@ fn wire( let clearance = CLEARANCE * tracks; let stamps: Vec = asks.iter().map(|ask| stamp(&ask.ends, vertices)).collect(); let mut due: Vec = (0..asks.len()) - .filter(|&k| match routes.get(&asks[k].key) { - Some(r) => r.stale || r.terminals != stamps[k] || moved.iter().any(|m| meets(&grow(r.reach, clearance), m)), - None => true, + .filter(|&k| { + let key = &asks[k].key; + routes.due.contains(key) || routes.get(key).is_none_or(|r| r.terminals != stamps[k]) }) .collect(); let asked = |k: &usize| routing.as_ref().is_some_and(|r| r.rows.contains_key(&key_of(&asks[*k].key))); @@ -324,8 +574,6 @@ fn wire( due.retain(|k| !asked(k)); } if !due.is_empty() { - let (obstacles, owners) = obstacles(vertices); - let groups = groups(vertices); let cost = ROUTE_COST * due.len() as u64; let inline = if input.dragging { jobs.spend(ROUTE_COST * due.len().min(DRAGGED) as u64); @@ -335,10 +583,11 @@ fn wire( }; if inline { let routed = &due[..due.len().min(DRAGGED)]; - let nets: Vec = routed.iter().map(|&k| net(&asks[k], &owners)).collect(); + let nets: Vec = routed.iter().map(|&k| net(&asks[k], vertices)).collect(); let fixed = fixed(context, asks, routed, routes, 4.0 * clearance); - for (&k, lines) in routed.iter().zip(route(&obstacles, &groups, &nets, &fixed, tracks)) { - routes.insert(asks[k].key.clone(), Routed::new(flipped(&lines), stamps[k])); + let lines = route(context.blocks, context.groups, &nets, &fixed, tracks); + for (&k, lines) in routed.iter().zip(lines) { + routes.routed(asks[k].key.clone(), Routed::new(flipped(&lines), stamps[k])); } if let Some(routing) = routing.as_mut() { let done: FxHashSet<&Row> = routed.iter().map(|&k| &asks[k].key).collect(); @@ -346,12 +595,12 @@ fn wire( } due.drain(..routed.len()); } else if routing.is_none() { - let nets: Vec = due.iter().map(|&k| net(&asks[k], &owners)).collect(); + let nets: Vec = due.iter().map(|&k| net(&asks[k], vertices)).collect(); let fixed = fixed(context, asks, &due, routes, 4.0 * clearance); let keys: Vec = due.iter().map(|&k| key_of(&asks[k].key)).collect(); let wiring = Wiring { - obstacles: &obstacles, - groups: &groups, + obstacles: context.blocks.all(), + groups: context.groups, nets: &nets, keys: &keys, fixed: &fixed, @@ -363,7 +612,7 @@ fn wire( } // Wires still due route later; until then they keep their routes while their terminals hold. for &k in &due { - routes.entry(asks[k].key.clone()).or_insert_with(Routed::waiting).stale = true; + routes.due(&asks[k].key); } } for (k, ask) in asks.iter().enumerate() { @@ -407,38 +656,16 @@ fn port_reach(b: Rect, size: f64) -> f64 { (size / 2.0).min((b[2] - b[0]).min(b[3] - b[1]) / 4.0) } -/// An end leaving by a side: where its other end lies along the side, its edge, and which end. -type Leaving = (f64, usize, usize); - -/// Gives each orthogonal edge its ends: on the sides of its vertices facing each other, spread along -/// each side by where the other ends lie, so wires leave a side without crossing. -fn face(asks: &mut [Ask], edges: &[(usize, [usize; 2])], vertices: &[Vertex]) { - let bounds = |v: usize| vertices[v].bounds.unwrap_or(EMPTY); - // Each vertex's side: the ends leaving by it, each where its other end lies along the side, the - // edge, and which end. - let mut sides: FxHashMap<(usize, Side), Vec> = FxHashMap::default(); - for (k, &(_, [a, b])) in edges.iter().enumerate() { - let (ba, bb) = (bounds(a), bounds(b)); - let (side_a, side_b) = facing(ba, bb); - let along = |side: Side, other: Rect| match side { - Side::Left | Side::Right => center(other)[1], - Side::Top | Side::Bottom => center(other)[0], - }; - sides.entry((a, side_a)).or_default().push((along(side_a, bb), k, 0)); - sides.entry((b, side_b)).or_default().push((along(side_b, ba), k, 1)); - } - let mut ends: Vec<[Option; 2]> = vec![[None; 2]; edges.len()]; - for ((v, side), mut placed) in sides { - crate::order::by_wide_key(&mut placed, |x| crate::order::wide(crate::order::number(x.0), x.1 as u64)); - let count = (placed.len() as u32).min(255); - for (place, (_, k, end)) in placed.into_iter().enumerate() { - let code = side.code() | ((place as u32).min(255) << 2) | (count << 10); - ends[k][end] = Some(End { at: port_point(bounds(v), code), side: Some(side), vertex: v, rim: 0.0 }); - } - } - for (k, &(ask, _)) in edges.iter().enumerate() { - asks[ask].ends = ends[k].iter().flatten().copied().collect(); - } +/// The ends of orthogonal edge `edge`, as `sides` gives them: on the sides of its vertices facing each +/// other, spread along each by where the other ends lie. +fn faced(sides: &Sides, edge: Edge, vertices: &[Vertex]) -> Vec { + (0..2) + .filter_map(|end| { + let (v, side, place, count) = sides.place(edge, end)?; + let code = side.code() | (place.min(255) << 2) | (count.min(255) << 10); + Some(End { at: port_point(vertices[v].bounds?, code), side: Some(side), vertex: v, rim: 0.0 }) + }) + .collect() } /// The sides of boxes `a` and `b` that face each other, y down: across the wider gap between them. @@ -466,42 +693,26 @@ fn stamp(ends: &[End], vertices: &[Vertex]) -> u64 { hasher.finish().max(1) } -/// The blocks wires route around, y up, and the obstacle of each vertex that is one. Groups hold -/// wires rather than turn them away. -fn obstacles(vertices: &[Vertex]) -> (Vec, Vec>) { - let mut boxes = Vec::new(); - let owners = vertices - .iter() - .map(|v| { - let b = v.bounds.filter(|_| !v.group)?; - boxes.push([b[0], -b[3], b[2], -b[1]]); - Some(boxes.len() as u32 - 1) - }) - .collect(); - (boxes, owners) -} - -/// The groups wires route among, y up: they cross a group's border at a cost, and keep off it. -fn groups(vertices: &[Vertex]) -> Vec { - vertices.iter().filter(|v| v.group).filter_map(|v| v.bounds).map(|b| [b[0], -b[3], b[2], -b[1]]).collect() -} - -/// The net routing `ask`, y up. -fn net(ask: &Ask, owners: &[Option]) -> Net { - let terminal = |end: &End| Terminal { at: [end.at[0], -end.at[1]], side: end.side, owner: owners[end.vertex] }; +/// The net routing `ask`, y up: each end owned by its vertex's block, which a group has none of. +fn net(ask: &Ask, vertices: &[Vertex]) -> Net { + let terminal = |end: &End| Terminal { + at: [end.at[0], -end.at[1]], + side: end.side, + owner: (!vertices[end.vertex].group).then_some(end.vertex as u32), + }; Net { terminals: ask.ends.iter().map(terminal).collect() } } /// The routes of the wires near those about to route, which keep their tracks, y up: within `reach` /// of the ends of the wires `routed`, in the order the edge marks draw them. -fn fixed(context: &Context<'_>, asks: &[Ask], routed: &[usize], routes: &FxHashMap, reach: f64) -> Parts { +fn fixed(context: &Context<'_>, asks: &[Ask], routed: &[usize], routes: &Routes, reach: f64) -> Parts { let region = routed.iter().flat_map(|&k| &asks[k].ends).fold(EMPTY, |region, end| union(region, dot(end.at))); - let region = grow(region, reach); let routing: FxHashSet<&Row> = routed.iter().map(|&k| &asks[k].key).collect(); - let mut near: Vec<((usize, usize), &Routed)> = routes - .iter() - .filter(|(row, r)| !routing.contains(row) && meets(&r.reach, ®ion)) - .filter_map(|(row, r)| Some((drawing(context, row)?, r))) + let mut near: Vec<(Edge, &Routed)> = routes + .near(grow(region, reach)) + .into_iter() + .filter(|row| !routing.contains(row)) + .filter_map(|row| Some((drawing(context, &row)?, routes.get(&row)?))) .collect(); crate::order::by_key(&mut near, |&((e, row), _)| crate::order::pair(e as u32, row as u32)); near.into_iter().flat_map(|(_, r)| flipped(&r.lines)).collect() diff --git a/crates/latkit/src/tiles/sets.rs b/crates/latkit/src/tiles/sets.rs index d74db3f0..b627f766 100644 --- a/crates/latkit/src/tiles/sets.rs +++ b/crates/latkit/src/tiles/sets.rs @@ -64,9 +64,6 @@ struct TileSet { drawing: Option, /// Whether the dataset changed since it was last taken. changed: bool, - /// The rows of vector tiles the view took last, with their shapes: the rows it draws, which a - /// later update may have moved past until the next frame takes them. - taken: Option, /// Whether a failure was told: a source that went away tells it once. told: bool, /// Whether a piece the view wanted at the last update drew an ancestor, or nothing, instead. @@ -81,7 +78,6 @@ impl TileSet { tiles, drawing: None, changed: false, - taken: None, told: false, standing_in: true, } @@ -198,20 +194,18 @@ impl TileSets { self.sets.iter().any(|s| *s.tiles.name == *name) } - /// The rows of vector tiles of dataset `name` the view took, each table with the fields its - /// shapes are written as too: row for row the dataset it draws. + /// The rows vector tiles brought of dataset `name`, each table with the fields its shapes are + /// written as too: the view takes them as the frame begins, so row for row what it draws. pub(crate) fn shaped(&self, name: &str) -> Option<&Snapshot> { - self.sets.iter().find(|s| *s.tiles.name == *name)?.taken.as_ref() + let set = self.sets.iter().find(|s| *s.tiles.name == *name)?; + match &set.drawing { + Some(Drawing::Vector(vector)) => Some(vector.held()), + _ => None, + } } - /// The datasets the updates changed since last taken; the rows of vector tiles they hold become - /// those their shapes are taken from. + /// The datasets the updates changed since last taken. pub(crate) fn take_changed(&mut self) -> Vec { - for set in self.sets.iter_mut().filter(|s| self.changed.iter().any(|d| d.name() == s.data.name())) { - if let Some(Drawing::Vector(vector)) = &set.drawing { - set.taken = Some(vector.held().clone()); - } - } std::mem::take(&mut self.changed) } diff --git a/crates/latkit/src/view/drawing.rs b/crates/latkit/src/view/drawing.rs index d2cffb2c..aa00f612 100644 --- a/crates/latkit/src/view/drawing.rs +++ b/crates/latkit/src/view/drawing.rs @@ -15,6 +15,7 @@ use std::sync::Arc; use rustc_hash::FxHashSet; use super::View; +use super::kept::KeptTable; use crate::data::{Field, Name, Reference, UNWIRED, Version}; use crate::derive::order::Order; use crate::derive::stamp_of; @@ -71,9 +72,9 @@ pub(crate) struct Drawing { /// The rows kept whose pages it does not hold, by their place among the rows kept: each one's /// place in the order, `NONE` where a place is free. kept: Arc<[u32]>, - /// The rows kept by their place in the order, ascending, each with its held row: what the ends of - /// edges find kept vertices by. - pub(crate) kept_rows: Arc<[[u32; 2]]>, + /// The rows kept by their place in the order, with their held rows: what the ends of edges find + /// kept vertices by. + pub(crate) kept_table: KeptTable, /// The rows drawn whatever their tiles, as places in the order, ascending. pinned: Arc<[u32]>, pub(crate) runs: Vec, @@ -100,9 +101,10 @@ pub(crate) struct Drawing { pub(crate) laid: u64, /// What tells it from every other drawing: what lists of its rows are kept by. pub(crate) made: Version, - /// What tells where it holds the rows edges end at: the drawing's before it while none of those - /// rows moved, so the edges naming them find them where they were. - pub(crate) held_ends: Version, + /// The places the edges held or the rows drawn whatever their tiles name that the drawing before + /// it held elsewhere, ascending: the rows naming them find their ends again. With what tells them + /// from the moves of the drawings before, the drawing's before it while none moved. + pub(crate) moved: (Version, Arc<[u32]>), /// What its rows kept were laid out from: the rows drawn whatever their tiles, what the edges held /// ask of it, and the pages it holds. A drawing laid out from the same keeps them where they were. kept_from: u64, @@ -179,18 +181,7 @@ impl Drawing { }; let kept_pages = kept.len().div_ceil(PAGE_ROWS as usize); let (slots, pages, kept_slots) = place(before, &held, kept_pages); - let kept_rows: Arc<[[u32; 2]]> = if let Some(b) = same { - b.kept_rows.clone() - } else { - let mut rows: Vec<[u32; 2]> = kept - .iter() - .enumerate() - .filter(|&(_, &d)| d != wgsl::NONE) - .map(|(i, &d)| [d, kept_slots[i / PAGE_ROWS as usize] * PAGE_ROWS + (i as u32) % PAGE_ROWS]) - .collect(); - rows.sort_unstable(); - rows.into() - }; + let kept_table = if let Some(b) = same { b.kept_table.clone() } else { keep_table(before, &kept, &kept_slots) }; let lays = before.map_or(1, |b| b.lays + 1); let asked: Arc<[u64]> = slots .iter() @@ -209,7 +200,7 @@ impl Drawing { slots: slots.into(), pages: pages.into(), kept, - kept_rows, + kept_table, pinned: lay.pinned.places.clone(), runs: Vec::new(), by_held: Vec::new(), @@ -224,13 +215,16 @@ impl Drawing { content: lay.content, laid: lay.stamp(), made: Version::next(), - held_ends: before.map_or_else(Version::next, |b| b.held_ends), + moved: before.map_or_else(|| (Version::next(), Arc::default()), |b| b.moved.clone()), kept_from, layout: 0, }; drawing.layout = stamp_of((drawing.order.id(), drawing.order.len(), &*drawing.slots, &*drawing.kept)); - if before.is_some_and(|b| b.kept_from != kept_from && ends_moved(b, &drawing, lay.ends, &lay.pinned.places)) { - drawing.held_ends = Version::next(); + if let Some(b) = before.filter(|b| b.kept_from != kept_from) { + let places = moved(b, &drawing, lay.ends, &lay.pinned.places); + if !places.is_empty() { + drawing.moved = (Version::next(), places.into()); + } } drawing.run(&entering, &lay.pinned.places); drawing.fades = !entering.is_empty() || !drawing.leaving.is_empty(); @@ -348,8 +342,7 @@ impl Drawing { if slot != wgsl::NONE { return Some(slot * PAGE_ROWS + d % PAGE_ROWS); } - let i = self.kept_rows.partition_point(|&[place, _]| place < d); - self.kept_rows.get(i).filter(|&&[place, _]| place == d).map(|&[_, held]| held) + self.kept_table.held(d) } /// The held row of row `r` of its table, if it holds it. @@ -419,30 +412,56 @@ fn asked<'r>(count: usize, ranges: impl Iterator>, ends: & asked } -/// Whether a row `ends` or `pinned` name, held in `before`, is held elsewhere in `now`: then the rows -/// naming it find their ends again. A page held in both keeps its slot, and a row kept in both its -/// place among the rows kept, so only rows of a page held in one alone move: those named of a page -/// `before` held, and those `before` kept of a page `now` holds. A row `before` did not hold is named -/// only by rows new to the edges, which find their ends as they arrive. -fn ends_moved(before: &Drawing, now: &Drawing, ends: &Ending, pinned: &[u32]) -> bool { +/// The table of the rows `kept`, by their places among the rows kept, held in the pages of rows kept +/// `kept_slots` says, laid out from the one `before` keeps: the rows whose places changed come and go, +/// and every other keeps its bucket. +fn keep_table(before: Option<&Drawing>, kept: &[u32], kept_slots: &[u32]) -> KeptTable { + let held = |i: usize| kept_slots[i / PAGE_ROWS as usize] * PAGE_ROWS + (i as u32) % PAGE_ROWS; + let was: &[u32] = before.map_or(&[], |b| &b.kept); + let at = |list: &[u32], i: usize| list.get(i).copied().unwrap_or(wgsl::NONE); + let (mut gone, mut came) = (Vec::new(), Vec::new()); + for i in 0..kept.len().max(was.len()) { + let (old, new) = (at(was, i), at(kept, i)); + if old != new { + gone.extend((old != wgsl::NONE).then_some(old)); + came.extend((new != wgsl::NONE).then(|| [new, held(i)])); + } + } + let rows: Vec<[u32; 2]> = + kept.iter().enumerate().filter(|&(_, &d)| d != wgsl::NONE).map(|(i, &d)| [d, held(i)]).collect(); + KeptTable::laid(before.map_or(&KeptTable::default(), |b| &b.kept_table), &rows, &gone, &came) +} + +/// The places `ends` or `pinned` name that `before` held and `now` holds elsewhere or not at all, +/// ascending: the rows naming them find their ends again. A page held in both keeps its slot, and a +/// row kept in both its place among the rows kept, so only rows of a page held in one alone move: +/// those named of a page `before` held, and those `before` kept of a page `now` holds. A row `before` +/// did not hold is named only by rows new to the edges, which find their ends as they arrive. +fn moved(before: &Drawing, now: &Drawing, ends: &Ending, pinned: &[u32]) -> Vec { + /// Those of `places`, ascending, lying in page `p`. + fn within(places: &[u32], p: u32) -> &[u32] { + let start = places.partition_point(|&d| d < p * PAGE_ROWS); + &places[start..places.partition_point(|&d| d < (p + 1) * PAGE_ROWS)] + } let held = |drawing: &Drawing, page: usize| drawing.pages.get(page).is_some_and(|&slot| slot != wgsl::NONE); - // Whether any of `places`, ascending, lies in `page`. - let within = |places: &[u32], page: u32| { - let start = places.partition_point(|&d| d < page * PAGE_ROWS); - places.get(start).is_some_and(|&d| d < (page + 1) * PAGE_ROWS) - }; - let kept_in = |page: u32| { - let start = before.kept_rows.partition_point(|&[place, _]| place < page * PAGE_ROWS); - before.kept_rows.get(start).is_some_and(|&[place, _]| place < (page + 1) * PAGE_ROWS) - }; - (0..before.pages.len().max(now.pages.len())).any(|p| { - let page = p as u32; - match (held(before, p), held(now, p)) { - (true, false) => within(&ends.rows, page) || within(pinned, page), - (false, true) => kept_in(page), - _ => false, + let mut moved = Vec::new(); + let mut gained = vec![false; now.pages.len()]; + for p in 0..before.pages.len().max(now.pages.len()) { + if held(before, p) && !held(now, p) { + moved.extend_from_slice(within(&ends.rows, p as u32)); + moved.extend_from_slice(within(pinned, p as u32)); } - }) + } + for (p, gained) in gained.iter_mut().enumerate() { + *gained = held(now, p) && !held(before, p); + } + if gained.contains(&true) { + let gained = |d: u32| gained.get((d / PAGE_ROWS) as usize).copied().unwrap_or(false); + moved.extend(before.kept.iter().copied().filter(|&d| d != wgsl::NONE && gained(d))); + } + moved.sort_unstable(); + moved.dedup(); + moved } /// Holds the pages `before` held that are no longer `asked` for while `room` leaves room, those @@ -598,6 +617,16 @@ impl Ends { self.asked.1.clone() } + /// Whether the ends of edge page `page` lie at any of `places`, ascending; so where the page was + /// never worked out. + fn lie_in(&self, page: u32, places: &[u32]) -> bool { + let Some(Some((_, found))) = self.pages.get(page as usize) else { return true }; + // Each of the fewer looked for among the more. + let (few, many) = + if found.places.len() < places.len() { (&found.places[..], places) } else { (places, &found.places[..]) }; + few.iter().any(|d| many.binary_search(d).is_ok()) + } + /// The places in `target` that edge page `page` of `order` ends at, by `ends`. fn of(&mut self, order: &Order, target: &Order, ends: &[&Reference], page: u32) -> Arc { if self.pages.len() <= page as usize { @@ -683,6 +712,19 @@ impl Drawn { self.next.as_ref().or(self.current.as_ref()) } + /// The slots of `drawing`, of this table's edges, whose rows end at any of `places` of the table + /// they end at, ascending: each page by where its ends lie, and every page of rows kept. + pub(crate) fn naming(&self, drawing: &Drawing, places: &[u32]) -> Vec { + (0..drawing.slot_count()) + .filter(|&s| match drawing.holds(s) { + Holds::Page(page) => self.ends.lie_in(page, places), + Holds::Kept(_) => true, + Holds::Free => false, + }) + .map(|s| s as u32) + .collect() + } + /// What every edge table drawn ending at it asks of it, as one; merged again only where any /// changed. fn ending(&mut self, endings: &[Arc]) -> Arc { @@ -998,19 +1040,35 @@ mod tests { let next = Drawing::lay(Some(&drawing), &Lay { ends: &more, ..lay }, false); assert_eq!(next.held_of(2 * PAGE_ROWS + 9), Some(2 * PAGE_ROWS + 1)); assert_eq!(next.held_of(PAGE_ROWS), Some(2 * PAGE_ROWS + 2)); - assert_eq!(next.held_ends, drawing.held_ends, "no row ends name moved"); + assert_eq!(next.moved, drawing.moved, "no row ends name moved"); // Rows appended to the order leave what the rows kept are laid out from: they stay as they were. let Order::Pyramid { pyramid, .. } = &order else { unreachable!("a pyramid") }; let grown = Order::Pyramid { pyramid: pyramid.clone(), rows: 4 * PAGE_ROWS + 1 }; let appended = Drawing::lay(Some(&next), &Lay { chosen: &one(&grown, 0..10), ends: &more, ..lay }, false); - assert!(Arc::ptr_eq(&appended.kept_rows, &next.kept_rows), "the rows kept as they were"); - assert_eq!(appended.held_ends, next.held_ends); + assert!(Arc::ptr_eq(&appended.kept, &next.kept), "the rows kept as they were"); + assert_eq!((&appended.kept_table, &appended.moved), (&next.kept_table, &next.moved)); // Page 2 drawn: the rows of it held one at a time move into it, and the edges ending there find - // them again. + // them again; the row kept of page 1 stays where it was. let drawn = Lay { chosen: &chosen(&order, &[0..10, 2 * PAGE_ROWS..2 * PAGE_ROWS + 10]), ends: &more, ..lay }; let paged = Drawing::lay(Some(&next), &drawn, false); assert_eq!(paged.held_of(2 * PAGE_ROWS + 9), Some(paged.pages[2] * PAGE_ROWS + 9)); - assert_ne!(paged.held_ends, next.held_ends, "rows ends name moved"); + assert_eq!(paged.held_of(PAGE_ROWS), next.held_of(PAGE_ROWS)); + assert_ne!(paged.moved.0, next.moved.0); + assert_eq!(&*paged.moved.1, [2 * PAGE_ROWS + 5, 2 * PAGE_ROWS + 9], "the rows of page 2 named moved"); + } + + #[test] + fn edges_find_their_ends_again_where_their_pages_name_what_moved() { + let order = order(3 * PAGE_ROWS); + let whole = one(&order, 0..3 * PAGE_ROWS); + let lay = Lay { chosen: &whole, pinned: &Pinned::default(), ends: &Ending::default(), room: 0, content: 0 }; + let edges = Drawing::lay(None, &lay, false); + let at = |places: &[u32]| Some((PAGE_ROWS, Arc::new(Places { places: places.to_vec(), runs: Vec::new() }))); + let pages = vec![at(&[3, 9]), at(&[PAGE_ROWS + 1]), at(&[7, 2 * PAGE_ROWS])]; + let drawn = Drawn { ends: Ends { pages, ..Ends::default() }, ..Drawn::default() }; + assert_eq!(drawn.naming(&edges, &[9]), [0], "the page of edges ending there"); + assert_eq!(drawn.naming(&edges, &[7, PAGE_ROWS + 1]), [1, 2]); + assert!(drawn.naming(&edges, &[5]).is_empty(), "no edge held ends there"); } #[test] diff --git a/crates/latkit/src/view/ends.rs b/crates/latkit/src/view/ends.rs index 1a4fc211..54f8468a 100644 --- a/crates/latkit/src/view/ends.rs +++ b/crates/latkit/src/view/ends.rs @@ -3,13 +3,15 @@ //! that table holds its pages, and the rows it keeps whose pages it does not hold, where its drawing //! lays them out, which moves as the camera does, a row at a time where the vertices the edges end at //! lie sparse. So no channel stores where an end is held: a step of the expression machine -//! (`lib/ends.wesl`) finds it for the rows whose ends changed, and for every row once the vertices -//! moved, and writes it into the row's kind word of the end's number, where draws, probes, and -//! normals read it in one step. +//! (`lib/ends.wesl`) finds it for the rows whose ends changed and the rows naming the vertices the +//! vertex mark's drawing says moved, and for every row once another mark or order holds them, and +//! writes it into the row's kind word of the end's number, where draws, probes, and normals read it +//! in one step. use std::sync::Arc; use super::View; +use super::kept::KeptTable; use super::marks::{Blocks, Shown}; use super::passes::{dispatch_blocks, offset}; use super::resources::step; @@ -24,8 +26,7 @@ use crate::gpu::work::Count; /// Where the rows an end of a mark's rows names are held: the vertex mark drawing their table, the /// pages of its rows' words and how many words a row has, and the pages of the normals faces sum at /// them; and where it holds each page of the order it draws that table in, and each row it keeps whose -/// page it does not hold, by its place in the order, and what tells where it holds the rows edges end -/// at. +/// page it does not hold, by its place in the order, and the places its drawing moved. #[derive(Clone, PartialEq, Debug)] pub(crate) struct End { pub(crate) mark: Option, @@ -34,8 +35,8 @@ pub(crate) struct End { pub(crate) normals: Option>, pub(crate) order: u64, pub(crate) pages: Option>, - pub(crate) kept: Option>, - pub(crate) held_ends: Option, + pub(crate) kept: Option, + pub(crate) moved: Option<(Version, Arc<[u32]>)>, } impl End { @@ -49,22 +50,31 @@ impl End { order: 0, pages: None, kept: None, - held_ends: None, + moved: None, } } - /// Whether the vertices it names are held elsewhere than `before` holds them: by another mark, in - /// another order, or where the rows edges end at moved. Where only the vertex mark's pages of words - /// or rows no edge ends at moved, every end is held where it was. - fn moved(&self, before: &End) -> bool { - self.mark != before.mark || self.order != before.order || self.held_ends != before.held_ends + /// Whether every vertex it names is held elsewhere than `before` holds it: by another mark, or in + /// another order. Where only the vertex mark's pages of words, or the rows its drawing moved, + /// changed, the rest are held where they were. + fn moved_whole(&self, before: &End) -> bool { + self.mark != before.mark || self.order != before.order + } + + /// The places its drawing moved since `before` was attached, if any; where it moved none, none. + fn moved_since(&self, before: &End) -> &[u32] { + match (&self.moved, &before.moved) { + (Some((version, places)), Some((was, _))) if version != was => places, + _ => &[], + } } /// Words it takes in a mark's program past its `END_WORDS`: its page table, where pages and rows /// kept are held, and its page table of normals. fn tables(&self) -> usize { + let kept = |k: &KeptTable| 2 * k.buckets().len(); self.state.len() - + self.pages.as_ref().map_or(0, |p| 2 + p.len() + 2 * self.kept.as_ref().map_or(0, |k| k.len())) + + self.pages.as_ref().map_or(0, |p| 2 + p.len() + self.kept.as_ref().map_or(0, kept)) + self.normals.as_ref().map_or(0, |n| n.len()) } } @@ -84,8 +94,9 @@ pub(super) fn words(ends: &[End]) -> usize { /// Appends the words of a mark's `ends`, which the program's `uses` name, starting at word `at` of /// the program buffer: `END_WORDS` an end, then each end's tables: the page table of its vertex mark's -/// words; its pages, how many, then each one's slot, and the rows it keeps, how many, then each one's -/// place and held row; and the page table of normals. Ends holding their rows alike share tables. +/// words; its pages, how many, then each one's slot, and the table of the rows it keeps, how many +/// buckets, then each one's place and held row; and the page table of normals. Ends holding their rows +/// alike share tables. pub(super) fn pack(ends: &[End], uses: &[EndUse], at: u32, out: &mut Vec) { let mut heads: Vec<[u32; wgsl::END_WORDS as usize]> = Vec::with_capacity(ends.len()); let mut table = at + wgsl::END_WORDS * ends.len() as u32; @@ -99,10 +110,8 @@ pub(super) fn pack(ends: &[End], uses: &[EndUse], at: u32, out: &mut Vec) { heads[earlier] } else { let state = if end.mark.is_some() { take(end.state.len()) } else { wgsl::NONE }; - let slots = end - .pages - .as_ref() - .map_or(wgsl::NONE, |p| take(2 + p.len() + 2 * end.kept.as_ref().map_or(0, |k| k.len()))); + let kept = end.kept.as_ref().map_or(0, |k| 2 * k.buckets().len()); + let slots = end.pages.as_ref().map_or(wgsl::NONE, |p| take(2 + p.len() + kept)); let normals = end.normals.as_ref().map_or(wgsl::NONE, |n| take(n.len())); [state, end.stride, slots, normals, wgsl::NONE] }; @@ -120,7 +129,7 @@ pub(super) fn pack(ends: &[End], uses: &[EndUse], at: u32, out: &mut Vec) { if let Some(pages) = &end.pages { out.push(pages.len() as u32); out.extend(pages.iter()); - let kept = end.kept.as_deref().unwrap_or_default(); + let kept = end.kept.as_ref().map_or(&[][..], KeptTable::buckets); out.push(kept.len() as u32); out.extend(kept.iter().flatten()); } @@ -131,9 +140,10 @@ pub(super) fn pack(ends: &[End], uses: &[EndUse], at: u32, out: &mut Vec) { } impl View { - /// Points every edge and face mark's ends at the vertex marks drawing their tables. Where the - /// vertices an end names are held elsewhere, every row finds its ends again; where only a vertex - /// mark's pages of words moved, the ends read them where they are, and nothing runs. + /// Points every edge and face mark's ends at the vertex marks drawing their tables. Where another + /// mark or another order holds the vertices an end names, every row finds its ends again; where + /// the vertex mark's drawing moved some, the rows naming them; where only a vertex mark's pages of + /// words moved, the ends read them where they are, and nothing runs. pub(super) fn attach_ends(&mut self) { for i in 0..self.marks.len() { let ends = ends(&self.marks, i); @@ -141,15 +151,36 @@ impl View { if ends == *was { continue; } - let moved = ends.len() != was.len() || ends.iter().zip(was).any(|(end, before)| end.moved(before)); - self.marks[i].ends = ends; - self.marks[i].packed = false; - if moved { - self.marks[i].unresolved = Blocks::All; + let whole = ends.len() != was.len() || ends.iter().zip(was).any(|(end, before)| end.moved_whole(before)); + let mut places: Vec = + ends.iter().zip(was).flat_map(|(end, before)| end.moved_since(before).iter().copied()).collect(); + places.sort_unstable(); + places.dedup(); + let blocks = match (whole, places.is_empty()) { + (true, _) => None, + (false, true) => Some(Vec::new()), + (false, false) => self.naming(i, &places), + }; + let shown = &mut self.marks[i]; + match blocks { + Some(blocks) => shown.unresolved.add(Some(&blocks)), + None => shown.unresolved = Blocks::All, } + shown.ends = ends; + shown.packed = false; } } + /// The row blocks of mark `i` whose rows end at any of `places` of the vertices its ends attach + /// to, ascending: its drawing's pages whose ends lie there; `None` for a table drawn whole, whose + /// rows end wherever. + fn naming(&self, i: usize, places: &[u32]) -> Option> { + let shown = &self.marks[i]; + let style = shown.mark.style(); + let drawn = self.drawings.get(&(style.data.clone(), style.table.clone()))?; + Some(drawn.naming(shown.drawing()?, places)) + } + /// Finds where the ends of `marks`' rows whose ends changed are held, each over the row blocks it /// lists for them, with the expression machine each mark's channels run on; `group` is its group. pub(super) fn resolve_ends( @@ -205,8 +236,8 @@ fn ends(marks: &[Shown], i: usize) -> Vec { normals: (normals.width > 0).then(|| normals.blocks.clone()), order: marks[m].drawing().map_or(0, |d| d.order.id()), pages: marks[m].drawing().map(|d| d.pages.clone()), - kept: marks[m].drawing().map(|d| d.kept_rows.clone()), - held_ends: marks[m].drawing().map(|d| d.held_ends), + kept: marks[m].drawing().map(|d| d.kept_table.clone()), + moved: marks[m].drawing().map(|d| d.moved.clone()), } } _ => End::unattached(), diff --git a/crates/latkit/src/view/frame.rs b/crates/latkit/src/view/frame.rs index 08e27da6..b5208aed 100644 --- a/crates/latkit/src/view/frame.rs +++ b/crates/latkit/src/view/frame.rs @@ -60,11 +60,17 @@ impl View { } return; } + // The frame's size first: the tiles its sources bring up are those it shows. What landed is + // taken before them, so what they start and lands at once wakes the next frame. + self.pixel_ratio = frame.pixel_ratio.max(0.01); + let ratio = f64::from(self.pixel_ratio); + self.size = [f64::from(frame.size[0]) / ratio, f64::from(frame.size[1]) / ratio]; + let landed = self.signal.take(); if !self.ready_for(frame.format, frame.pixel_ratio, input) { + self.signal.leave(landed); return; } let started = Instant::now(); - self.signal.clear(); let starved = std::mem::take(&mut self.room) == Room::Starved; let dt = self.clock.tick(frame.time); let gpu = self.gpu.clone(); @@ -75,9 +81,6 @@ impl View { // Runs the callbacks of finished readbacks; a no-op in browsers, which run them themselves. let _ = device.poll(wgpu::PollType::Poll); self.tell_gpu_errors(); - self.pixel_ratio = frame.pixel_ratio.max(0.01); - let ratio = f64::from(self.pixel_ratio); - self.size = [f64::from(frame.size[0]) / ratio, f64::from(frame.size[1]) / ratio]; self.measure_gutters(); self.settle(); self.advance(dt); @@ -212,7 +215,7 @@ impl View { let domains = self.place_domains(); let resources = self.resources.as_mut().expect("created"); resources.fit(device, self.marks.len() as u64, domains); - self.pack(inner); + self.pack(encoder, inner); self.lay_out_labels(inner); fresh } @@ -357,26 +360,18 @@ impl View { } /// Derives what marks read beside their data, from the data and the scene alone, handing the - /// host what the frame cannot afford. First the pieces of tiles the camera and the plot window - /// ask for; then the orders tables drawn in part draw in, and the ranges of them the camera asks - /// for; then every other derivation, from the data. + /// host what the frame cannot afford. First the orders tables drawn in part draw in, and the + /// ranges of them the camera asks for; then every other derivation, from the data. fn derive(&mut self, inner: &mut Inner) { let host = inner.host.clone(); let projected = self.projected(); let earth = matches!(self.scene.space, crate::Space::Earth(_)); let placing = earth || matches!(self.scene.space, crate::Space::Plane(_)); let sight = derive::sight::Sight { projected: &projected, size: self.size, earth }; - // One budget for the frame: the tiles, what is drawn in part, then every other derivation. + // One budget for the frame: what is drawn in part, then every other derivation. let (waker, counters) = (self.waker.clone(), self.gpu.0.counters.clone()); let errors = self.gpu.0.errors.clone(); let mut jobs = derive::Jobs::new(host.as_deref(), &waker, &counters, &errors, inner.frame.work); - let mut tiles = std::mem::take(&mut self.derived.tiles); - let reading = self.reading(); - let asked = crate::tiles::Asked { sight: earth.then_some(&sight), reading: &reading }; - // What the pieces change draws from the next frame, which takes it before asking what its - // marks read with. - tiles.update(&self.derive_input(host.as_deref()), &asked, &mut inner.tiles, &jobs); - self.derived.tiles = tiles; let budget = inner.budget.bytes; self.update_plan(&mut inner.ledger, budget); self.trace_columns = self.planning.trace_columns(); @@ -399,6 +394,24 @@ impl View { inner.frame.work = jobs.left() as u32; } + /// Brings every tile set up to what the camera and the plot window ask, from what the GPU's + /// cache holds, with what landed for them since. Their pieces fetch and decode as they land, so + /// nothing here spends the frame's work. + pub(super) fn update_tiles(&mut self, inner: &mut Inner) { + let host = inner.host.clone(); + let projected = self.projected(); + let earth = matches!(self.scene.space, crate::Space::Earth(_)); + let sight = derive::sight::Sight { projected: &projected, size: self.size, earth }; + let reading = self.reading(); + let asked = crate::tiles::Asked { sight: earth.then_some(&sight), reading: &reading }; + let (waker, counters) = (self.waker.clone(), self.gpu.0.counters.clone()); + let errors = self.gpu.0.errors.clone(); + let jobs = derive::Jobs::new(host.as_deref(), &waker, &counters, &errors, 0); + let mut tiles = std::mem::take(&mut self.derived.tiles); + tiles.update(&self.derive_input(host.as_deref()), &asked, &mut inner.tiles, &jobs); + self.derived.tiles = tiles; + } + /// What derivations read this frame. fn derive_input<'a>(&'a self, host: Option<&'a dyn crate::host::Host>) -> derive::Input<'a> { derive::Input { diff --git a/crates/latkit/src/view/kept.rs b/crates/latkit/src/view/kept.rs new file mode 100644 index 00000000..17c73fd1 --- /dev/null +++ b/crates/latkit/src/view/kept.rs @@ -0,0 +1,193 @@ +//! The rows a drawing keeps whose pages it does not hold, by their place in its order: an +//! open-addressed table whose buckets hold a place and its held row, `[NONE, NONE]` where empty, laid +//! out three fifths full, any number of buckets. A table laid out from another takes out the rows that +//! went and puts in those that came while it stays between three tenths and four fifths full: only +//! their buckets, and the few after a row taken out that move back, change, so the end table's words +//! change where rows did; past those it is laid out again, a little larger or smaller, never more +//! than it must be. A probe for a place ends within a few buckets, on the GPU as here +//! (`lib/ends.wesl`). + +use std::sync::Arc; + +use crate::gpu::shaders as wgsl; + +/// Buckets a table has at least, however few rows it keeps. +const LEAST: usize = 64; +const EMPTY: [u32; 2] = [wgsl::NONE, wgsl::NONE]; + +#[derive(Clone, Default, Debug, PartialEq)] +pub(crate) struct KeptTable(Arc<[[u32; 2]]>); + +impl KeptTable { + /// The table of `rows`, each a place and its held row, laid out from `before`: where `before` has + /// room for them, its buckets with the places `gone` taken out and the rows `came` put in; else + /// laid out afresh. + pub(crate) fn laid(before: &KeptTable, rows: &[[u32; 2]], gone: &[u32], came: &[[u32; 2]]) -> KeptTable { + let buckets = before.0.len(); + // The least table stays however few rows it keeps. + if buckets > 0 && rows.len() * 5 <= buckets * 4 && (rows.len() * 10 >= buckets * 3 || buckets == LEAST) { + let mut table = before.0.to_vec(); + for &d in gone { + remove(&mut table, d); + } + for &row in came { + insert(&mut table, row); + } + return KeptTable(table.into()); + } + if rows.is_empty() { + return KeptTable::default(); + } + let mut table = vec![EMPTY; (rows.len() * 5).div_ceil(3).max(LEAST)]; + for &row in rows { + insert(&mut table, row); + } + KeptTable(table.into()) + } + + /// The held row of place `d`, if it keeps it. + pub(crate) fn held(&self, d: u32) -> Option { + if self.0.is_empty() { + return None; + } + let mut b = home(d, self.0.len()); + loop { + match self.0[b] { + [place, held] if place == d => return Some(held), + [wgsl::NONE, _] => return None, + _ => b = next(b, self.0.len()), + } + } + } + + /// Its buckets, as the end table packs them. + pub(crate) fn buckets(&self) -> &[[u32; 2]] { + &self.0 + } +} + +/// The bucket a probe for place `d` starts at among `buckets`, as `lib/ends.wesl` finds it: its bits +/// hashed. +fn home(d: u32, buckets: usize) -> usize { + let mut h = d ^ (d >> 16); + h = h.wrapping_mul(0x7feb_352d); + h ^= h >> 15; + h = h.wrapping_mul(0x846c_a68b); + (h ^ (h >> 16)) as usize % buckets +} + +/// The bucket after `b`, the first after the last. +fn next(b: usize, buckets: usize) -> usize { + if b + 1 == buckets { 0 } else { b + 1 } +} + +/// Puts `row` in the first empty bucket from where its place's probe starts. +fn insert(table: &mut [[u32; 2]], row: [u32; 2]) { + let mut b = home(row[0], table.len()); + while table[b][0] != wgsl::NONE { + b = next(b, table.len()); + } + table[b] = row; +} + +/// Takes place `d` out, moving back each row after it whose probe passed its bucket, so every probe +/// still ends at the first empty bucket. +fn remove(table: &mut [[u32; 2]], d: u32) { + let mut hole = home(d, table.len()); + while table[hole][0] != d { + if table[hole][0] == wgsl::NONE { + return; + } + hole = next(hole, table.len()); + } + let mut b = hole; + loop { + b = next(b, table.len()); + let [place, _] = table[b]; + if place == wgsl::NONE { + break; + } + // A row whose probe starts after the hole, up to where it lies, stays: its probe never passed. + let home = home(place, table.len()); + let stays = if hole <= b { hole < home && home <= b } else { hole < home || home <= b }; + if !stays { + table[hole] = table[b]; + hole = b; + } + } + table[hole] = EMPTY; +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use super::*; + + /// A stream of random numbers below `n`. + fn random(seed: &mut u64, n: u32) -> u32 { + *seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407); + ((*seed >> 33) % u64::from(n)) as u32 + } + + #[test] + fn a_table_holds_every_row_kept_and_no_other() { + let mut seed = 7; + let mut kept: BTreeMap = BTreeMap::new(); + let mut table = KeptTable::default(); + for round in 0..200 { + // Places come or go, each once a layout. + let (mut gone, mut came) = (Vec::new(), Vec::new()); + for _ in 0..random(&mut seed, 40) { + let d = random(&mut seed, 5_000); + if gone.contains(&d) || came.iter().any(|row: &[u32; 2]| row[0] == d) { + continue; + } + if kept.remove(&d).is_some() { + gone.push(d); + } else { + kept.insert(d, round); + came.push([d, round]); + } + } + let rows: Vec<[u32; 2]> = kept.iter().map(|(&d, &held)| [d, held]).collect(); + table = KeptTable::laid(&table, &rows, &gone, &came); + let buckets = table.buckets().len(); + assert!(buckets == 0 || kept.len() * 5 <= buckets * 4, "at most four fifths full"); + for d in 0..5_000 { + assert_eq!(table.held(d), kept.get(&d).copied(), "place {d} after round {round}"); + } + } + } + + #[test] + fn rows_coming_and_going_change_the_buckets_near_them_alone() { + let rows: Vec<[u32; 2]> = (0..1_000).map(|d| [d * 7, d]).collect(); + let table = KeptTable::laid(&KeptTable::default(), &rows, &[], &[]); + let gone: Vec = (0..10).map(|d| d * 70).collect(); + let came: Vec<[u32; 2]> = (0..10).map(|d| [100_000 + d, d]).collect(); + let now: Vec<[u32; 2]> = + rows.iter().filter(|r| !gone.contains(&r[0])).copied().chain(came.iter().copied()).collect(); + let next = KeptTable::laid(&table, &now, &gone, &came); + assert_eq!(next.buckets().len(), table.buckets().len(), "laid in place"); + let changed = table.buckets().iter().zip(next.buckets()).filter(|(a, b)| a != b).count(); + assert!(changed <= 60, "{changed} buckets changed for 20 rows that came or went"); + } + + #[test] + fn a_table_is_laid_out_again_past_four_fifths_full_and_under_three_tenths() { + let rows = |n: u32| (0..n).map(|d| [d, d]).collect::>(); + let small = KeptTable::laid(&KeptTable::default(), &rows(10), &[], &[]); + assert_eq!(small.buckets().len(), LEAST, "few rows, the least buckets"); + let full = KeptTable::laid(&small, &rows(100), &[], &(10..100).map(|d| [d, d]).collect::>()); + assert_eq!(full.buckets().len(), 167, "past four fifths of 64, laid out three fifths full"); + let more = KeptTable::laid(&full, &rows(130), &[], &(100..130).map(|d| [d, d]).collect::>()); + assert_eq!(more.buckets().len(), 167, "up to four fifths full, in place"); + let gone: Vec = (51..130).collect(); + assert_eq!(KeptTable::laid(&more, &rows(51), &gone, &[]).buckets().len(), 167, "three tenths, in place"); + let gone: Vec = (20..130).collect(); + assert_eq!(KeptTable::laid(&more, &rows(20), &gone, &[]).buckets().len(), LEAST, "under three tenths"); + let gone: Vec = (0..130).collect(); + assert_eq!(KeptTable::laid(&more, &[], &gone, &[]), KeptTable::default(), "no rows, no buckets"); + } +} diff --git a/crates/latkit/src/view/marks.rs b/crates/latkit/src/view/marks.rs index 1ae91bcd..000ef007 100644 --- a/crates/latkit/src/view/marks.rs +++ b/crates/latkit/src/view/marks.rs @@ -651,7 +651,7 @@ impl Default for Blocks { impl Blocks { /// Adds the blocks `blocks` names, or every block for `None`. - fn add(&mut self, blocks: Option<&[u32]>) { + pub(crate) fn add(&mut self, blocks: Option<&[u32]>) { match (&mut *self, blocks) { (Blocks::All, _) => {} (Blocks::Listed(listed), Some(more)) => listed.extend_from_slice(more), diff --git a/crates/latkit/src/view/mod.rs b/crates/latkit/src/view/mod.rs index af5f8e37..fe45c8fb 100644 --- a/crates/latkit/src/view/mod.rs +++ b/crates/latkit/src/view/mod.rs @@ -12,6 +12,7 @@ mod frame; mod gestures; mod graph; mod input; +mod kept; mod labels; mod legends; mod lists; @@ -420,11 +421,14 @@ impl View { Some(&self.snapshots[i]) } - /// Takes what tiles brought since the last frame, and the latest version of every dataset given to - /// change in place, where it changed: before a frame asks what its marks read with, so a field a - /// version adds never reads through what was not asked for. A picture draws the versions the view - /// held when it began, while the tiles it waits on land. + /// Brings every source up to this frame before anything reads it: the tiles the camera and the + /// plot window ask for, with what landed for them, and the latest version of every dataset given + /// to change in place, where it changed. A frame's marks compile against, ask pipelines for, + /// derive from, and draw one version of each, so a field a version adds never reads through what + /// was not asked for. A picture draws the versions the view held when it began, while the tiles it + /// waits on land. fn refresh(&mut self, inner: &mut crate::gpu::Inner) { + self.update_tiles(inner); for data in self.derived.tiles.take_changed() { self.take_data(data, inner); } diff --git a/crates/latkit/src/view/plan.rs b/crates/latkit/src/view/plan.rs index 805c984d..2775e65b 100644 --- a/crates/latkit/src/view/plan.rs +++ b/crates/latkit/src/view/plan.rs @@ -23,7 +23,7 @@ use crate::data::{Field, Keyed, Name, Table}; use crate::derive::tiled::{Draw, Place, Planned, Plans}; use crate::derive::{Derived, stamp_of}; use crate::expr::compile::Words; -use crate::gpu::arena::{path_page_bytes, path_points, rounded}; +use crate::gpu::arena::{path_page_bytes, rounded}; use crate::gpu::shaders as wgsl; use crate::gpu::{Ask, Ledger}; use crate::scene::mark::MarkKind; @@ -398,6 +398,5 @@ fn geometry_bytes(shown: &Shown, table: &Table) -> f64 { let Some(field) = geometry_of(&shown.mark) else { return 0.0 }; let Some(Field::Values(column, _)) = table.field_named(&field) else { return 0.0 }; let scale = if matches!(shown.mark, Mark::Areas(_)) { 3 } else { 1 }; - let page = |values: &crate::data::Values| path_page_bytes(values.len(), path_points(values) * scale) as f64; - column.pages().iter().map(|p| page(&p.values)).sum() + column.pages().iter().map(|p| path_page_bytes(p.values.len(), p.points() * scale) as f64).sum() } diff --git a/crates/latkit/src/view/program.rs b/crates/latkit/src/view/program.rs index cbf996f8..ff9c2577 100644 --- a/crates/latkit/src/view/program.rs +++ b/crates/latkit/src/view/program.rs @@ -1,7 +1,9 @@ //! The program buffer: each mark's descriptors, tables, ranges, code, constants, ends, and ports in a //! region of its own, rewritten alone when only that mark changed, and then only where its words -//! changed. A region that outgrows its room moves to the end of the buffer; when the end passes the -//! buffer, every region is laid out again. +//! changed. A region that outgrows its room moves to the end of the buffer; a buffer too small for it +//! grows, what it holds copied into the new one on the GPU, so no other region is written again. Once +//! regions that moved leave more of the buffer behind than regions take, every region is laid out +//! again, each writing only its own words. use super::{Event, View}; use crate::gpu::Inner; @@ -35,8 +37,9 @@ fn changed(was: &[u32], words: &[u32]) -> Vec<[usize; 2]> { } impl View { - /// Writes every mark whose words changed into its region of the program buffer. - pub(super) fn pack(&mut self, inner: &mut Inner) { + /// Writes every mark whose words changed into its region of the program buffer; a buffer grown + /// has what it held copied over in `encoder`, and what is written after follows the copy there. + pub(super) fn pack(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner) { let gpu = self.gpu.clone(); let font = self.scene.theme.text.font.clone(); for shown in self.marks.iter_mut().filter(|m| !m.packed) { @@ -55,29 +58,45 @@ impl View { } } let resources = self.resources.as_mut().expect("created before packing"); - let capacity = resources.program_words() as u32; + let (device, queue) = (&gpu.0.device, &gpu.0.queue); + // Whether the buffer grew this frame: its words are copied in `encoder`, so writes follow there. + let mut grown = false; + let write = |encoder: &mut wgpu::CommandEncoder, grown: bool, buffer: &wgpu::Buffer, at: u32, words: &[u32]| { + let (at, bytes) = (u64::from(at) * 4, bytemuck::cast_slice(words)); + if grown { + queue.write_buffer_in(device, encoder, buffer, at, bytes); + } else { + queue.write_buffer(buffer, at, bytes); + } + }; if !self.regions.relayout { + // The room regions take; the rest of the buffer up to its end, regions that moved left. + let mut live: u32 = self.marks.iter().filter_map(|m| m.region).map(|[_, room]| room).sum(); for shown in self.marks.iter_mut().filter(|m| !m.packed && m.compiled.is_some()) { let len = shown.packed_len(); let start = match shown.region { Some([start, room]) if len <= room => start, - _ if self.regions.end + room(len) <= capacity => { - let start = self.regions.end; + region => { + let (start, end) = (self.regions.end, self.regions.end + room(len)); + live = live - region.map_or(0, |[_, room]| room) + room(len); + if end > 2 * live { + self.regions.relayout = true; + break; + } + if u64::from(end) > resources.program_words() { + let kept = u64::from(self.regions.end); + grown |= resources.grow_program(device, encoder, u64::from(end), kept); + } shown.region = Some([start, room(len)]); shown.written.clear(); - self.regions.end += room(len); + self.regions.end = end; start } - _ => { - self.regions.relayout = true; - break; - } }; let mut words = Vec::with_capacity(len as usize); shown.pack(start, &mut words); for [from, to] in changed(&shown.written, &words) { - let at = u64::from(start) * 4 + from as u64 * 4; - gpu.0.queue.write_buffer(resources.program(), at, bytemuck::cast_slice(&words[from..to])); + write(encoder, grown, resources.program(), start + from as u32, &words[from..to]); } shown.written = words; shown.packed = true; @@ -85,19 +104,21 @@ impl View { } if std::mem::take(&mut self.regions.relayout) { let total: u64 = self.marks.iter().map(|m| u64::from(room(m.packed_len()))).sum(); - resources.fit_program(&gpu.0.device, total); - let mut words = Vec::with_capacity(total as usize); + resources.fit_program(device, total); + // Each region's own words, at the start of its room: the room past them, which nothing reads + // until the region grows into it and writes it, is not written. + let mut start = 0; for shown in self.marks.iter_mut().filter(|m| m.compiled.is_some()) { - let start = words.len() as u32; let len = shown.packed_len(); + let mut words = Vec::with_capacity(len as usize); shown.pack(start, &mut words); - shown.written = words[start as usize..].to_vec(); - words.resize((start + room(len)) as usize, 0); + write(encoder, grown, resources.program(), start, &words); + shown.written = words; shown.region = Some([start, room(len)]); shown.packed = true; + start += room(len); } - self.regions.end = words.len() as u32; - gpu.0.queue.write_buffer(resources.program(), 0, bytemuck::cast_slice(&words)); + self.regions.end = start; } } } diff --git a/crates/latkit/src/view/resources.rs b/crates/latkit/src/view/resources.rs index 3eac42fd..1e8839ac 100644 --- a/crates/latkit/src/view/resources.rs +++ b/crates/latkit/src/view/resources.rs @@ -128,7 +128,8 @@ impl Resources { frame: create(device, "latkit frame", uniform, size_of::() as u64), eye: create(device, "latkit eye", uniform, size_of::() as u64), marks: Grown::new(device, "latkit marks", uniform), - program: Grown::new(device, "latkit program", storage), + // A copy's source as it grows, keeping what it holds. + program: Grown::new(device, "latkit program", storage | wgpu::BufferUsages::COPY_SRC), lit: Grown::new(device, "latkit lit", storage), blocks: Grown::new(device, "latkit blocks", storage), remap: Grown::new(device, "latkit remap", storage), @@ -200,6 +201,27 @@ impl Resources { replaced } + /// Makes room for `words` of program keeping its first `kept`: a buffer replaced has them copied + /// into the new one in `encoder`, so what is written into it this frame follows the copy. True when + /// it was replaced. + pub(crate) fn grow_program( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + words: u64, + kept: u64, + ) -> bool { + let old = self.program.buffer.clone(); + if !self.program.ensure(device, (words + 1) * 4) { + return false; + } + if kept > 0 { + encoder.copy_buffer_to_buffer(&old, 0, &self.program.buffer, 0, kept * 4); + } + self.generation += 1; + true + } + /// Makes room for `words` probe words, what the running query finds included. pub(crate) fn fit_probe(&mut self, device: &wgpu::Device, words: u32) { if self.probe.ensure(device, u64::from(words.max(wgsl::PROBE_WORDS)) * 4) { diff --git a/crates/latkit/src/view/sync.rs b/crates/latkit/src/view/sync.rs index b7756da9..d5f70ac5 100644 --- a/crates/latkit/src/view/sync.rs +++ b/crates/latkit/src/view/sync.rs @@ -32,7 +32,7 @@ use crate::derive::shapes::PagedShapes; use crate::derive::stamp_of; use crate::expr::compile::{ColumnUse, Program, Reads, Words}; use crate::gpu::Inner; -use crate::gpu::arena::{Ask, Content, PageKey, PathPage, path_pieces, path_points}; +use crate::gpu::arena::{Ask, Content, PageKey, PathPage, path_pieces}; use crate::gpu::packed::{self, Packing}; use crate::gpu::shaders as wgsl; use crate::gpu::work::Count; @@ -995,8 +995,7 @@ impl<'g> Builder<'g> { self.words.resize(wgsl::PATH_PAGE_WORDS as usize * pages.len(), 0); for (p, page) in pages.iter().enumerate() { self.list(Some(page.id)); - let points = path_points(&page.values); - self.path_page(p, page.id, points, page.values.len() as u32, || path_pieces(&page.values)); + self.path_page(p, page.id, page.values.len() as u32, || page.points(), || path_pieces(&page.values)); } } @@ -1013,13 +1012,12 @@ impl<'g> Builder<'g> { }; self.list(Some(id)); let rows = drawing.rows(s); - let points = rows_points(column, &rows); let counters = self.gpu.packed.counters(); let pieces = || { counters.add(Count::Packed, rows.len() as u64); path_pieces(&column.pick(&rows)) }; - self.path_page(s, id, points, rows.len() as u32, pieces); + self.path_page(s, id, rows.len() as u32, || rows_points(column, &rows), pieces); } } @@ -1059,30 +1057,39 @@ impl<'g> Builder<'g> { self.list(Some(id)); let rows = shaped.len() as u32; let counters = self.gpu.packed.counters(); - let points = path_points(&shaped); let pieces = || { counters.add(Count::Packed, u64::from(rows)); path_pieces(&shaped) }; - self.path_page(s, id, points, rows, pieces); + self.path_page(s, id, rows, || shaped.points(), pieces); } } - /// Page `p` of a geometry column's table, `id`, of `points` points and `rows` rows: its rows' - /// table, then its pieces of points, each keyed by what it holds, so a page written again uploads - /// only the pieces whose rows' points changed. `pieces` makes them where the GPU lacks one. - fn path_page(&mut self, p: usize, id: PageId, points: usize, rows: u32, pieces: impl FnOnce() -> PathPage) { + /// Page `p` of a geometry column's table, `id`, of `rows` rows: its rows' table, then its pieces of + /// points, each keyed by what it holds, so a page written again uploads only the pieces whose + /// rows' points changed. `pieces` makes them where the GPU lacks one, and `points` counts its + /// points where the page's pieces are not kept. + fn path_page( + &mut self, + p: usize, + id: PageId, + rows: u32, + points: impl FnOnce() -> usize, + pieces: impl FnOnce() -> PathPage, + ) { let per = wgsl::PATH_PAGE_WORDS as usize; - self.points += points as u32; let arena = &self.gpu.arena; - let kept = self.gpu.pieces.of(id).filter(|keys| keys.iter().all(|&key| arena.holds(key, 1))); + let known = self.gpu.pieces.of(id); + let points = known.as_ref().map_or_else(points, |&(_, points)| points); + self.points += points as u32; + let kept = known.map(|(keys, _)| keys).filter(|keys| keys.iter().all(|&key| arena.holds(key, 1))); let (keys, mut made) = if let Some(keys) = kept { (keys, Vec::new()) } else { let PathPage { rows, pieces } = pieces(); let made: Vec> = std::iter::once(rows).chain(pieces).collect(); let keys: Arc<[PageKey]> = made.iter().map(|words| PageKey::piece(words)).collect(); - self.gpu.pieces.keep(id, keys.clone()); + self.gpu.pieces.keep(id, keys.clone(), points); (keys, made) }; let listed = self.words.len(); diff --git a/crates/latkit/src/view/wake.rs b/crates/latkit/src/view/wake.rs index 2473d06c..054d56d0 100644 --- a/crates/latkit/src/view/wake.rs +++ b/crates/latkit/src/view/wake.rs @@ -26,9 +26,18 @@ impl Signal { self.landed.load(Ordering::Acquire) } - /// Takes what landed: the frame beginning settles it. - pub(crate) fn clear(&self) { - self.landed.store(false, Ordering::Release); + /// Takes what landed as a frame begins, before it takes anything: whether anything did. What + /// lands from then on, as work the frame starts and the host answers at once, wakes the next. + pub(crate) fn take(&self) -> bool { + self.landed.swap(false, Ordering::AcqRel) + } + + /// Leaves what landed, `landed`, to the next frame: as a frame not ready to draw does, taking + /// none of it. + pub(crate) fn leave(&self, landed: bool) { + if landed { + self.landed.store(true, Ordering::Release); + } } pub(crate) fn set_waker(&self, waker: Waker) { diff --git a/crates/latkit/tests/scale.rs b/crates/latkit/tests/scale.rs index 20c069cf..ea5b6425 100644 --- a/crates/latkit/tests/scale.rs +++ b/crates/latkit/tests/scale.rs @@ -673,8 +673,8 @@ fn a_first_look_draws_within_its_plan_while_pyramids_are_worked_out() { assert!(gpu.resident() <= 8 << 20, "{} bytes resident", gpu.resident()); } -/// A grid of 500 by 500 vertices a unit apart, and an edge from each to the vertex `far` says: what -/// `far` gives each from a stream of random numbers. +/// A grid of 500 by 500 vertices a unit apart, each of value 0, and an edge from each to the vertex +/// `far` says: what `far` gives each from a stream of random numbers. fn far_apart(far: impl Fn(u32, u64) -> u64) -> Data { let n = 250_000_u32; let mut seed = 1_u64; @@ -690,7 +690,8 @@ fn far_apart(far: impl Fn(u32, u64) -> u64) -> Data { .set( Table::new("Bus", Ids::count(n)) .field("x", (0..n).map(|i| f64::from(i % 500)).collect::>()) - .field("y", (0..n).map(|i| f64::from(i / 500)).collect::>()), + .field("y", (0..n).map(|i| f64::from(i / 500)).collect::>()) + .field("v", vec![0.0; n as usize]), ) .set( Table::new("Line", Ids::count(n)) @@ -709,10 +710,11 @@ fn edges_ending_far_apart_hold_the_rows_they_end_at() { // Each edge joins a vertex to one far off: the ends of the fewest edges drawn lie in every page of // the vertices, more pages than the budget holds, so the rows they end at are held one at a time. let data = far_apart(|_, random| random % 250_000); - let scene = Scene::new(plane()) - .theme(plain_theme()) - .draw(Edges::of("grid", "Line").ends("from", "to")) - .draw(Vertices::of("grid", "Bus").position("x", "y")); + let scene = Scene::new(plane()).theme(plain_theme()).draw(Edges::of("grid", "Line").ends("from", "to")).draw( + Vertices::of("grid", "Bus") + .position("x", "y") + .color(field("v").colormap(latkit::Colormap::GRAYS).domain(0.0..=1.0)), + ); let mut view = View::new(&gpu, scene).expect("valid scene"); view.set_sources([&data]).expect("a source"); view.fit(Fit::All); @@ -722,6 +724,15 @@ fn edges_ending_far_apart_hold_the_rows_they_end_at() { assert!(view.stats().drawn > 0, "edges and vertices draw"); assert!((gpu.work() - before).resolved > 0, "edges found where their ends are held"); assert!(gpu.resident() <= 8 << 20, "{} bytes resident", gpu.resident()); + // Every vertex's value changes and none moves: the rows held stay where they are, so no edge finds + // its ends again. + let before = gpu.work(); + data.apply(Table::new("Bus", Ids::count(250_000)).field("v", vec![1.0; 250_000])).expect("valid"); + frame(&gpu, &mut view); + assert_eq!(errors(&mut view), []); + let work = gpu.work() - before; + assert!(work.uploaded > 0, "the values upload"); + assert_eq!(work.resolved, 0, "no end is found again"); } #[test] @@ -739,17 +750,29 @@ fn edges_ending_far_apart_draw_between_their_ends() { .draw(Vertices::of("grid", "Bus").position("x", "y").radius(1.0).color(Color::rgb(0.0, 0.0, 1.0))); let mut view = View::new(&gpu, scene).expect("valid scene"); // Rows eight pixels apart, the middle row of pixels between two of them. - view.set_camera(Camera::Map { center: [250.0, 250.5, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }); + let near = Camera::Map { center: [250.0, 250.5, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }; + view.set_camera(near); view.set_sources([&data]).expect("a source"); + let level = |pixels: &[[u8; 4]], when: &str| { + let center = SIZE / 2; + for x in 0..SIZE { + let between = at(pixels, x, center); + assert!(close(between, [0, 0, 0, 255]), "{when}: nothing crosses between the rows at {x}: {between:?}"); + } + let along = (0..SIZE).filter(|&x| close(at(pixels, x, center + 4), [0, 255, 0, 255])).count(); + assert!(along > 0, "{when}: edges run along a row"); + }; let pixels = frame(&gpu, &mut view); assert_eq!(errors(&mut view), []); - let center = SIZE / 2; - for x in 0..SIZE { - let between = at(&pixels, x, center); - assert!(close(between, [0, 0, 0, 255]), "nothing crosses between the rows at {x}: {between:?}"); - } - let along = (0..SIZE).filter(|&x| close(at(&pixels, x, center + 4), [0, 255, 0, 255])).count(); - assert!(along > 0, "edges run along a row"); + level(&pixels, "first"); + // Farther out, the edges drawn end at more rows held one at a time: the table of them grows, and + // the program holding it with it. Back where it was, every edge still finds its ends. + view.set_camera(Camera::Map { center: [250.0, 250.5, 0.0], zoom: 1.0, pitch: 0.0, bearing: 0.0 }); + frame(&gpu, &mut view); + view.set_camera(near); + let pixels = frame(&gpu, &mut view); + assert_eq!(errors(&mut view), []); + level(&pixels, "after the table grew"); } #[test] diff --git a/crates/latkit/tests/tiles.rs b/crates/latkit/tests/tiles.rs index 73b797a1..b0d200b7 100644 --- a/crates/latkit/tests/tiles.rs +++ b/crates/latkit/tests/tiles.rs @@ -9,7 +9,7 @@ mod common; use std::collections::BTreeMap; use std::sync::{Arc, Mutex}; -use common::{SIZE, at, errors, frame, gpu, plain_theme}; +use common::{SETTLE, SIZE, at, errors, frame, gpu, plain_theme, step}; use latkit::Tiles; use latkit::host::{Host, Raster, Request, TextRun}; use latkit::{ @@ -153,10 +153,19 @@ fn tiles_the_camera_asks_for_draw_as_a_dataset() { assert_eq!(served.fetched.lock().expect("unpoisoned").len(), asked, "fetched once for every view"); assert_eq!(ids(&second), before, "the same rows"); drop(second); - // Zooming in asks for finer tiles, which replace their ancestors. + // Zooming in asks for finer tiles, which replace their ancestors in the frame the last of them + // lands in. view.set_camera(Camera::Map { center: [10.0, 10.0, 0.0], zoom: 6.0, pitch: 0.0, bearing: 0.0 }); - frame(&gpu, &mut view); - assert!(ids(&view).iter().all(|id| !before.contains(id)), "finer tiles' rows"); + let mut loading = false; + for _ in 0..SETTLE { + step(&gpu, &mut view); + let now = view.stats().tiles_loading > 0; + if loading && !now { + break; + } + loading |= now; + } + assert!(ids(&view).iter().all(|id| !before.contains(id)), "finer tiles' rows, the frame they landed in"); view.set_sources::([]).expect("no sources"); assert!(view.snapshot("base").is_none(), "what no source gives goes"); } diff --git a/docs/reference/types.md b/docs/reference/types.md index 72dbce8e..29ad560b 100644 --- a/docs/reference/types.md +++ b/docs/reference/types.md @@ -1936,13 +1936,18 @@ type Waiting = ('compiling' | 'data' | 'derived' | 'labels' | 'fit' | 'marks')[] interface Work { /** Bytes of data pages uploaded. */ uploaded: number; + /** + * Data pages, or pieces of them, uploaded: what a change uploads counts its pages here, and its + * bytes in `uploaded`. + */ + uploads: number; /** Bytes written besides pages: uniforms, programs, label runs, the selection, textures. */ written: number; /** Rows whose channels the GPU evaluated. */ evaluated: number; /** * Rows of edges and faces whose ends the GPU found where their vertices are held: rows that - * changed, and every row held where the vertices they end at moved. + * changed, and every row of a page of them naming a vertex now held elsewhere. */ resolved: number; /** diff --git a/npm/examples/gate/gate.ts b/npm/examples/gate/gate.ts index 271cfd6a..440cee06 100644 --- a/npm/examples/gate/gate.ts +++ b/npm/examples/gate/gate.ts @@ -3,7 +3,9 @@ * every page of the suite in a fresh browser and moves the pointer over it. It fails on a pipeline * over budget or failing to compile, a console error, a page that draws no frame in time, a page * missing frames in a row while it opens or under the pointer, or a pipeline compiled where first - * used. Every wait races a deadline. + * used. A page late, drawing no frame in time or missing frames, opens again, cold: it fails late + * only on every open, as a stall of the machine slows one open and a slow page all. Every wait races + * a deadline. * * On SwiftShader (`--software`, as Linux always is), the CPU draws, slower than any budget, so the * gate only checks that every pipeline compiles: pages and times are judged on a GPU. @@ -33,6 +35,15 @@ const FIRST_FRAME_BUDGET = 1000; const MISSED = 1; /** Milliseconds between display frames past which the gate tells what the views were doing. */ const LONG = 50; +/** Cold opens a page may take to draw its first frame in time and keep its frames. What any open + * does wrong fails at once. */ +const OPENS = 2; + +/** What an open of a page did wrong, and how it was late. */ +interface Opened { + wrong: string[]; + slow: string[]; +} /** A gap between display frames past `LONG`: how long, each view's last frame on the CPU and the * GPU and what it waited on, and the work the GPU did since the frame before. */ @@ -246,10 +257,21 @@ async function settled(page: Page, budget: number): Promise { ); } -/** Opens page `id` in a fresh browser, waits for its first frames, then moves the pointer over - * every view: hovering a grid of points, zooming, panning, and clicking. Returns the failures. */ +/** Opens page `id` until an open is in time, at most `OPENS` times: what an open did wrong, else + * how the last was late. */ async function visit(url: string, id: string): Promise { - const failures: string[] = []; + for (let n = 1; ; n++) { + const { wrong, slow } = await open(url, id); + if (wrong.length || !slow.length || n === OPENS) return [...wrong, ...slow]; + console.log(` ${id}: ${slow.join('; ')}; opening it again`); + } +} + +/** Opens page `id` in a fresh browser, waits for its first frames, then moves the pointer over + * every view: hovering a grid of points, zooming, panning, and clicking. */ +async function open(url: string, id: string): Promise { + const opened: Opened = { wrong: [], slow: [] }; + const { wrong, slow } = opened; const chrome = await cold(); try { const context = await chrome.browser.newContext({ @@ -258,9 +280,9 @@ async function visit(url: string, id: string): Promise { }); const page = await context.newPage(); page.on('console', (message) => { - if (message.type() === 'error') failures.push(`console: ${message.text().slice(0, 300)}`); + if (message.type() === 'error') wrong.push(`console: ${message.text().slice(0, 300)}`); }); - page.on('pageerror', (error) => failures.push(`page: ${error.message.slice(0, 300)}`)); + page.on('pageerror', (error) => wrong.push(`page: ${error.message.slice(0, 300)}`)); // The longest gap between animation frames since the last reset, and the display's frame // interval: the median of the latest gaps; and each gap past `LONG`, with what the views did. await page.addInitScript((long: number) => { @@ -318,7 +340,7 @@ async function visit(url: string, id: string): Promise { const long = await page.evaluate(() => (window as unknown as { long: Long[] }).long); for (const frame of long) console.log(` ${id}: ${describe(frame)} ${doing}`); }; - const opened = performance.now(); + const started = performance.now(); await deadline(page.goto(`${url}#${id}`), STEP, `open ${id}`); /** Waits until every view has drawn more than `frames` frames, for at most `budget` ms. */ const drawn = (frames: number, budget: number) => @@ -339,10 +361,10 @@ async function visit(url: string, id: string): Promise { `${id}'s first frame`, ); } catch { - failures.push(`no frame within ${FIRST_FRAME_BUDGET} ms`); - return failures; + slow.push(`no frame within ${FIRST_FRAME_BUDGET} ms`); + return opened; } - const first = performance.now() - opened; + const first = performance.now() - started; // What stalls while the views open, from their first frames: what the page did loading before // them shows nothing to stall. await page.evaluate(() => (window as unknown as { resetGap(): void }).resetGap()); @@ -413,7 +435,7 @@ async function visit(url: string, id: string): Promise { .filter(Boolean); return `views waiting on ${waiting.join(', ')}; the page said ${status.join(' | ') || 'nothing'}`; }); - failures.push(`no picture exported: ${said}`); + wrong.push(`no picture exported: ${said}`); } const { gap, step } = longest; const { interval, late } = await page.evaluate(() => { @@ -431,17 +453,15 @@ async function visit(url: string, id: string): Promise { ); const missed = (ms: number) => Math.round(ms / interval) - 1; if (opening > budget) - failures.push(`missed ${missed(opening)} frames in a row opening (${opening.toFixed(0)} ms)`); + slow.push(`missed ${missed(opening)} frames in a row opening (${opening.toFixed(0)} ms)`); if (gap > budget) - failures.push(`missed ${missed(gap)} frames in a row on ${step} (${gap.toFixed(0)} ms)`); + slow.push(`missed ${missed(gap)} frames in a row on ${step} (${gap.toFixed(0)} ms)`); if (late > 0) - failures.push( - `${late} pipelines compiled where first used, holding back the frames using them`, - ); + wrong.push(`${late} pipelines compiled where first used, holding back the frames using them`); } finally { await chrome.close(); } - return failures; + return opened; } /** A frame past `LONG` as the gate tells it: what each view's last frame took on the CPU and the GPU diff --git a/npm/schema.json b/npm/schema.json index f379e862..502d6891 100644 --- a/npm/schema.json +++ b/npm/schema.json @@ -4618,6 +4618,12 @@ "minimum": 0, "description": "Bytes of data pages uploaded." }, + "uploads": { + "type": "integer", + "format": "uint64", + "minimum": 0, + "description": "Data pages, or pieces of them, uploaded: what a change uploads counts its pages here, and its\nbytes in `uploaded`." + }, "written": { "type": "integer", "format": "uint64", @@ -4634,7 +4640,7 @@ "type": "integer", "format": "uint64", "minimum": 0, - "description": "Rows of edges and faces whose ends the GPU found where their vertices are held: rows that\nchanged, and every row held where the vertices they end at moved." + "description": "Rows of edges and faces whose ends the GPU found where their vertices are held: rows that\nchanged, and every row of a page of them naming a vertex now held elsewhere." }, "cleared": { "type": "integer", @@ -4681,6 +4687,7 @@ }, "required": [ "uploaded", + "uploads", "written", "evaluated", "resolved", diff --git a/npm/src/outputs.ts b/npm/src/outputs.ts index 199b9fc8..6387efd1 100644 --- a/npm/src/outputs.ts +++ b/npm/src/outputs.ts @@ -263,13 +263,18 @@ export type Waiting = ('compiling' | 'data' | 'derived' | 'labels' | 'fit' | 'ma export interface Work { /** Bytes of data pages uploaded. */ uploaded: number; + /** + * Data pages, or pieces of them, uploaded: what a change uploads counts its pages here, and its + * bytes in `uploaded`. + */ + uploads: number; /** Bytes written besides pages: uniforms, programs, label runs, the selection, textures. */ written: number; /** Rows whose channels the GPU evaluated. */ evaluated: number; /** * Rows of edges and faces whose ends the GPU found where their vertices are held: rows that - * changed, and every row held where the vertices they end at moved. + * changed, and every row of a page of them naming a vertex now held elsewhere. */ resolved: number; /** From d4af984f2cdca260e75d639724c139f7e16b7098 Mon Sep 17 00:00:00 2001 From: Luke Lowery Date: Mon, 26 Oct 2026 11:58:44 -0500 Subject: [PATCH 06/14] polish and refine prepare for PR --- .changeset/sources.md | 51 +- CONTRIBUTING.md | 29 +- README.md | 14 +- RELEASING.md | 44 +- benchmark/README.md | 38 +- benchmark/baselines/work.json | 164 +-- benchmark/docs.ts | 9 +- benchmark/judge.ts | 74 +- benchmark/page/data.ts | 27 +- benchmark/page/main.ts | 40 +- benchmark/page/scenarios.ts | 115 +- benchmark/page/stage.ts | 98 +- benchmark/report.ts | 6 +- benchmark/run.ts | 83 +- benchmark/tiles.ts | 9 +- benchmark/vite.config.ts | 2 +- crates/latkit-cli/README.md | 15 +- .../examples/branch-opening-orbit/README.md | 42 +- .../examples/branch-opening/README.md | 50 +- .../latkit-cli/examples/bus-fault/README.md | 26 +- .../examples/wecc240-fault/README.md | 34 +- crates/latkit-cli/src/args.rs | 136 +-- crates/latkit-cli/src/inputs.rs | 54 +- crates/latkit-cli/src/main.rs | 36 +- crates/latkit-cli/src/render.rs | 44 +- crates/latkit-cli/src/report.rs | 18 +- crates/latkit-cli/src/sinks.rs | 48 +- crates/latkit-cli/tests/cli.rs | 34 +- crates/latkit-js/src/canvas.rs | 17 +- crates/latkit-js/src/convert.rs | 27 +- crates/latkit-js/src/data.rs | 46 +- crates/latkit-js/src/error.rs | 6 +- crates/latkit-js/src/gpu.rs | 36 +- crates/latkit-js/src/lib.rs | 27 +- crates/latkit-js/src/pending.rs | 14 +- crates/latkit-js/src/shot.rs | 50 +- crates/latkit-js/src/view.rs | 48 +- crates/latkit/README.md | 7 +- crates/latkit/build.rs | 3 +- crates/latkit/build/colormaps.rs | 4 +- crates/latkit/build/shaders.rs | 68 +- crates/latkit/shaders/compute/bounds.wesl | 19 +- crates/latkit/shaders/compute/candidates.wesl | 21 +- crates/latkit/shaders/compute/compose.wesl | 14 +- crates/latkit/shaders/compute/evaluate.wesl | 119 +- crates/latkit/shaders/compute/fields.wesl | 43 +- crates/latkit/shaders/compute/freeze.wesl | 21 +- crates/latkit/shaders/compute/labels.wesl | 44 +- crates/latkit/shaders/compute/lists.wesl | 32 +- crates/latkit/shaders/compute/normals.wesl | 5 +- crates/latkit/shaders/compute/probe.wesl | 82 +- crates/latkit/shaders/draw/areas.wesl | 12 +- crates/latkit/shaders/draw/backdrop.wesl | 48 +- crates/latkit/shaders/draw/glyphs.wesl | 20 +- crates/latkit/shaders/draw/overlay.wesl | 6 +- crates/latkit/shaders/draw/points.wesl | 35 +- crates/latkit/shaders/draw/strokes.wesl | 42 +- crates/latkit/shaders/draw/triangles.wesl | 8 +- crates/latkit/shaders/lib/bind.wesl | 17 +- crates/latkit/shaders/lib/color.wesl | 12 +- crates/latkit/shaders/lib/daylight.wesl | 8 +- crates/latkit/shaders/lib/drawn.wesl | 14 +- crates/latkit/shaders/lib/ends.wesl | 34 +- crates/latkit/shaders/lib/focus.wesl | 10 +- crates/latkit/shaders/lib/label.wesl | 40 +- crates/latkit/shaders/lib/lists.wesl | 9 +- crates/latkit/shaders/lib/mark.wesl | 26 +- crates/latkit/shaders/lib/marker.wesl | 38 +- crates/latkit/shaders/lib/path.wesl | 179 ++- crates/latkit/shaders/lib/placement.wesl | 11 +- crates/latkit/shaders/lib/points.wesl | 39 +- crates/latkit/shaders/lib/project.wesl | 33 +- crates/latkit/shaders/lib/read.wesl | 34 +- crates/latkit/shaders/lib/reduce.wesl | 5 +- crates/latkit/shaders/lib/state.wesl | 34 +- crates/latkit/shaders/lib/stroke.wesl | 34 +- crates/latkit/shaders/lib/trace.wesl | 33 +- crates/latkit/shaders/lib/triangles.wesl | 29 +- crates/latkit/shaders/lib/types.wesl | 359 +++--- crates/latkit/src/data/bits.rs | 9 +- crates/latkit/src/data/buffer.rs | 2 +- crates/latkit/src/data/bytes.rs | 197 ++-- crates/latkit/src/data/column.rs | 132 ++- crates/latkit/src/data/describe.rs | 89 +- crates/latkit/src/data/forms.rs | 120 +- crates/latkit/src/data/frames.rs | 92 +- crates/latkit/src/data/geometry.rs | 39 +- crates/latkit/src/data/ids.rs | 56 +- crates/latkit/src/data/json.rs | 125 +- crates/latkit/src/data/mod.rs | 6 +- crates/latkit/src/data/name.rs | 36 +- crates/latkit/src/data/ordered.rs | 10 +- crates/latkit/src/data/patch.rs | 109 +- crates/latkit/src/data/query.rs | 86 +- crates/latkit/src/data/samples.rs | 356 +++--- crates/latkit/src/data/snapshot.rs | 60 +- crates/latkit/src/data/source.rs | 42 +- crates/latkit/src/data/table.rs | 68 +- crates/latkit/src/data/tests.rs | 35 +- crates/latkit/src/data/text.rs | 2 +- crates/latkit/src/data/values.rs | 85 +- crates/latkit/src/derive/graph.rs | 55 +- crates/latkit/src/derive/lanes.rs | 66 +- crates/latkit/src/derive/matches.rs | 25 +- crates/latkit/src/derive/mod.rs | 199 ++-- crates/latkit/src/derive/order.rs | 87 +- crates/latkit/src/derive/places.rs | 43 +- crates/latkit/src/derive/pyramid.rs | 272 +++-- crates/latkit/src/derive/shapes.rs | 247 ++-- crates/latkit/src/derive/sight.rs | 78 +- crates/latkit/src/derive/stars.rs | 19 +- crates/latkit/src/derive/tiled.rs | 189 +-- crates/latkit/src/error.rs | 22 +- crates/latkit/src/expr/color.rs | 9 +- crates/latkit/src/expr/colormap.rs | 54 +- crates/latkit/src/expr/compile.rs | 117 +- crates/latkit/src/expr/cpu.rs | 35 +- crates/latkit/src/expr/form.rs | 171 +-- crates/latkit/src/expr/mod.rs | 132 ++- crates/latkit/src/expr/ramp.rs | 34 +- crates/latkit/src/geo/earcut.rs | 106 +- crates/latkit/src/geo/geojson.rs | 58 +- crates/latkit/src/geo/mod.rs | 7 +- crates/latkit/src/geo/mvt.rs | 117 +- crates/latkit/src/geo/pmtiles.rs | 47 +- crates/latkit/src/geo/shapes.rs | 78 +- crates/latkit/src/geo/tiles.rs | 52 +- crates/latkit/src/gpu/arena.rs | 232 ++-- crates/latkit/src/gpu/atlas.rs | 177 ++- crates/latkit/src/gpu/colormaps.rs | 3 +- crates/latkit/src/gpu/dictionary.rs | 29 +- crates/latkit/src/gpu/icons.rs | 10 +- crates/latkit/src/gpu/mod.rs | 161 +-- crates/latkit/src/gpu/packed.rs | 76 +- crates/latkit/src/gpu/pipelines.rs | 199 ++-- crates/latkit/src/gpu/readback.rs | 28 +- crates/latkit/src/gpu/shaders.rs | 20 +- crates/latkit/src/gpu/work.rs | 70 +- crates/latkit/src/host/browser.rs | 96 +- crates/latkit/src/host/job.rs | 139 +-- crates/latkit/src/host/mod.rs | 70 +- crates/latkit/src/host/native.rs | 44 +- .../src/host/native/media_foundation.rs | 129 +- crates/latkit/src/host/queue.rs | 18 +- crates/latkit/src/image.rs | 47 +- crates/latkit/src/json.rs | 10 +- crates/latkit/src/layout/boxes.rs | 45 +- crates/latkit/src/layout/layered.rs | 107 +- crates/latkit/src/layout/levels.rs | 57 +- crates/latkit/src/layout/mod.rs | 5 +- crates/latkit/src/layout/overlap.rs | 40 +- crates/latkit/src/layout/pack.rs | 16 +- crates/latkit/src/layout/rect.rs | 12 +- crates/latkit/src/layout/route.rs | 323 ++--- crates/latkit/src/layout/stress.rs | 61 +- crates/latkit/src/lib.rs | 20 +- crates/latkit/src/order.rs | 30 +- crates/latkit/src/panels.rs | 21 +- crates/latkit/src/pending.rs | 34 +- crates/latkit/src/resource.rs | 10 +- crates/latkit/src/scene/areas.rs | 21 +- crates/latkit/src/scene/compile.rs | 6 +- crates/latkit/src/scene/controls.rs | 83 +- crates/latkit/src/scene/edges.rs | 59 +- crates/latkit/src/scene/faces.rs | 14 +- crates/latkit/src/scene/form.rs | 26 +- crates/latkit/src/scene/label.rs | 58 +- crates/latkit/src/scene/layout.rs | 41 +- crates/latkit/src/scene/lines.rs | 38 +- crates/latkit/src/scene/mark.rs | 229 ++-- crates/latkit/src/scene/marker.rs | 38 +- crates/latkit/src/scene/mod.rs | 69 +- crates/latkit/src/scene/port.rs | 23 +- crates/latkit/src/scene/space.rs | 102 +- crates/latkit/src/scene/tests.rs | 14 +- crates/latkit/src/scene/theme.rs | 196 ++-- crates/latkit/src/scene/vertices.rs | 94 +- crates/latkit/src/schema.rs | 23 +- crates/latkit/src/sheet/groups.rs | 125 ++ crates/latkit/src/sheet/mod.rs | 297 +++-- crates/latkit/src/sheet/wires.rs | 229 ++-- crates/latkit/src/shot.rs | 183 +-- crates/latkit/src/tiles/cache.rs | 41 +- crates/latkit/src/tiles/frames.rs | 73 +- crates/latkit/src/tiles/mod.rs | 55 +- crates/latkit/src/tiles/sets.rs | 66 +- crates/latkit/src/tiles/tiling.rs | 89 +- crates/latkit/src/tiles/vector.rs | 91 +- crates/latkit/src/time.rs | 18 +- crates/latkit/src/url.rs | 36 +- crates/latkit/src/video/mod.rs | 106 +- crates/latkit/src/video/mp4.rs | 97 +- crates/latkit/src/view/axes.rs | 63 +- crates/latkit/src/view/camera.rs | 128 +- crates/latkit/src/view/clock.rs | 30 +- crates/latkit/src/view/drawing.rs | 565 +++++---- crates/latkit/src/view/ends.rs | 84 +- crates/latkit/src/view/event.rs | 138 +-- crates/latkit/src/view/fit.rs | 58 +- crates/latkit/src/view/focus.rs | 61 +- crates/latkit/src/view/frame.rs | 211 ++-- crates/latkit/src/view/gestures.rs | 160 +-- crates/latkit/src/view/graph.rs | 16 +- crates/latkit/src/view/input.rs | 40 +- crates/latkit/src/view/kept.rs | 62 +- crates/latkit/src/view/labels.rs | 120 +- crates/latkit/src/view/legends.rs | 33 +- crates/latkit/src/view/lists.rs | 58 +- crates/latkit/src/view/marks.rs | 218 ++-- crates/latkit/src/view/mod.rs | 386 +++--- crates/latkit/src/view/passes.rs | 177 +-- crates/latkit/src/view/plan.rs | 139 +-- crates/latkit/src/view/probe.rs | 58 +- crates/latkit/src/view/program.rs | 70 +- crates/latkit/src/view/query.rs | 77 +- crates/latkit/src/view/resources.rs | 81 +- crates/latkit/src/view/rig.rs | 47 +- crates/latkit/src/view/sync.rs | 274 ++--- crates/latkit/src/view/timer.rs | 26 +- crates/latkit/src/view/transition.rs | 122 +- crates/latkit/src/view/uniforms.rs | 35 +- crates/latkit/src/view/variants.rs | 102 +- crates/latkit/src/view/wake.rs | 35 +- crates/latkit/src/view/words.rs | 42 +- crates/latkit/src/web.rs | 238 ++-- crates/latkit/tests/areas.rs | 22 +- crates/latkit/tests/common/mod.rs | 29 +- crates/latkit/tests/fixtures/latk-v1.hex | 1 - crates/latkit/tests/fixtures/patch.hex | 1 + crates/latkit/tests/gestures.rs | 114 +- crates/latkit/tests/graph.rs | 35 +- crates/latkit/tests/groups.rs | 20 +- crates/latkit/tests/labels.rs | 44 +- crates/latkit/tests/layouts.rs | 56 +- crates/latkit/tests/markers.rs | 17 +- crates/latkit/tests/picks.rs | 23 +- crates/latkit/tests/render.rs | 158 ++- crates/latkit/tests/scale.rs | 224 ++-- crates/latkit/tests/scenes.rs | 3 +- crates/latkit/tests/schema.rs | 18 +- crates/latkit/tests/shots.rs | 40 +- crates/latkit/tests/tiles.rs | 56 +- crates/latkit/tests/transitions.rs | 52 +- crates/latkit/tests/validation.rs | 4 +- crates/latkit/tests/videos.rs | 24 +- docs/_static/performance.csv | 28 +- docs/cli.md | 68 +- docs/examples/bloom.md | 6 +- docs/examples/clifford.md | 6 +- docs/examples/currents.md | 6 +- docs/examples/diagram.md | 6 +- docs/examples/frequency.md | 6 +- docs/examples/globe.md | 6 +- docs/examples/grid.md | 6 +- docs/examples/helios.md | 6 +- docs/examples/index.md | 6 +- docs/examples/opening.md | 20 +- docs/examples/terrain.md | 6 +- docs/examples/texas.md | 6 +- docs/examples/tides.md | 6 +- docs/examples/torus.md | 4 +- docs/gallery/bloom.ts | 6 +- docs/gallery/clifford.ts | 6 +- docs/gallery/currents.ts | 14 +- docs/gallery/diagram.ts | 9 +- docs/gallery/flow.ts | 30 +- docs/gallery/frequency.ts | 8 +- docs/gallery/globe.ts | 8 +- docs/gallery/grid.ts | 14 +- docs/gallery/grids.ts | 19 +- docs/gallery/helios.ts | 4 +- docs/gallery/look.ts | 34 +- docs/gallery/opening.ts | 39 +- docs/gallery/record.ts | 8 +- docs/gallery/render.ts | 8 +- docs/gallery/terrain.ts | 9 +- docs/gallery/texas.ts | 2 +- docs/gallery/tides.ts | 2 +- docs/gallery/torus.ts | 5 +- docs/guide.md | 490 ++++---- docs/index.md | 27 +- docs/performance.md | 62 +- docs/reference/api.md | 775 +++++++----- docs/reference/cli.md | 150 +-- docs/reference/types.md | 1036 ++++++++++------- npm/README.md | 6 +- npm/build.mjs | 7 +- npm/docs.mjs | 52 +- npm/examples/gate/browser.ts | 28 +- npm/examples/gate/gate.ts | 128 +- npm/examples/public/history/0/0/0.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/1.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/10.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/11.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/12.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/13.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/14.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/15.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/16.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/17.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/18.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/19.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/2.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/20.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/21.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/22.latk | Bin 33858 -> 33854 bytes npm/examples/public/history/0/0/3.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/4.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/5.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/6.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/7.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/8.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/0/9.latk | Bin 67650 -> 67646 bytes npm/examples/public/history/0/1/0.latk | Bin 135234 -> 135230 bytes npm/examples/public/history/0/1/1.latk | Bin 135234 -> 135230 bytes npm/examples/public/history/0/1/2.latk | Bin 109890 -> 109886 bytes npm/examples/public/history/0/2/0.latk | Bin 47586 -> 47582 bytes npm/examples/public/history/0/3/0.latk | Bin 5874 -> 5870 bytes npm/examples/public/history/0/4/0.latk | Bin 594 -> 590 bytes npm/examples/public/history/base.latk | Bin 1350 -> 1346 bytes npm/examples/scripts/history.ts | 21 +- npm/examples/src/examples/colormaps/index.ts | 53 +- npm/examples/src/examples/diagram/index.ts | 113 +- npm/examples/src/examples/index.ts | 2 +- .../src/examples/linked-views/index.ts | 35 +- npm/examples/src/examples/live-data/index.ts | 103 +- .../src/examples/networks/cases.test.ts | 8 +- npm/examples/src/examples/networks/cases.ts | 51 +- .../src/examples/networks/continent.ts | 53 +- npm/examples/src/examples/networks/index.ts | 103 +- npm/examples/src/examples/networks/lattice.ts | 10 +- npm/examples/src/examples/networks/maps.ts | 33 +- npm/examples/src/examples/networks/states.ts | 24 +- .../src/examples/power-flow/grid.test.ts | 6 +- npm/examples/src/examples/power-flow/grid.ts | 55 +- npm/examples/src/examples/power-flow/index.ts | 57 +- .../src/examples/soft-body/body.test.ts | 6 +- npm/examples/src/examples/soft-body/body.ts | 134 ++- npm/examples/src/examples/soft-body/burst.ts | 12 +- .../src/examples/soft-body/fracture.ts | 10 +- npm/examples/src/examples/soft-body/index.ts | 45 +- npm/examples/src/examples/soft-body/mesh.ts | 16 +- npm/examples/src/random.ts | 3 +- npm/examples/src/ui/controls.ts | 28 +- npm/examples/src/ui/dom.ts | 3 +- npm/examples/src/ui/export.ts | 27 +- npm/examples/src/ui/legend.ts | 5 +- npm/examples/src/ui/menu.ts | 9 +- npm/examples/src/ui/meter.ts | 11 +- npm/examples/src/ui/playback.ts | 31 +- npm/examples/src/ui/rows.ts | 6 +- npm/examples/src/ui/shell.ts | 5 +- npm/examples/src/ui/stage.ts | 42 +- npm/examples/src/ui/theme.ts | 2 +- npm/examples/test/setup.ts | 5 +- npm/examples/vite.config.ts | 2 +- npm/release.mjs | 18 +- npm/schema.json | 757 ++++++------ npm/schema.mjs | 59 +- npm/src/canvas.ts | 43 +- npm/src/colormap.ts | 8 +- npm/src/data.ts | 176 +-- npm/src/encoder.ts | 53 +- npm/src/gpu.ts | 74 +- npm/src/index.ts | 9 +- npm/src/inputs.ts | 903 ++++++++------ npm/src/internal.ts | 108 +- npm/src/jobs.ts | 23 +- npm/src/load.ts | 52 +- npm/src/outputs.ts | 127 +- npm/src/patch.ts | 277 +++-- npm/src/scene.ts | 128 +- npm/src/shot.ts | 150 ++- npm/src/view.ts | 153 ++- npm/src/worker.ts | 5 +- npm/test/consumer/main.ts | 10 +- npm/test/consumer/quickstart.ts | 2 +- npm/test/data.test.mjs | 76 +- npm/test/jobs.test.mjs | 6 +- npm/test/load.test.mjs | 4 +- npm/test/release.test.mjs | 2 +- npm/test/video.test.mjs | 4 +- xtask/src/main.rs | 59 +- 383 files changed, 13624 insertions(+), 11180 deletions(-) create mode 100644 crates/latkit/src/sheet/groups.rs delete mode 100644 crates/latkit/tests/fixtures/latk-v1.hex create mode 100644 crates/latkit/tests/fixtures/patch.hex diff --git a/.changeset/sources.md b/.changeset/sources.md index f1730462..7ee4859c 100644 --- a/.changeset/sources.md +++ b/.changeset/sources.md @@ -2,28 +2,31 @@ 'latkit': minor --- -Views draw sources, and say so in one word each: a `Data` its latest version, a `Snapshot` as it is, `Tiles` fetched in pieces. `load()` loads the WebAssembly and every class imports from `latkit`; every wait ends at an abort signal; colors and lengths say what they are; a table too large to draw whole draws pages of its order that keep their place as the camera moves, and edges ending far apart draw within the budget. +Views draw from sources, every wait can be cancelled, colors and lengths are typed, and large tables draw in pages that stay on the GPU. -- Added: `view.sources` and `gpu.view(scene, { sources })`: `Data`, drawn at its latest version from the frame after each patch applies; `Snapshot`, a version kept as it is (`data.snapshot()`, `data.restore(snapshot)`); and `Tiles(name, url)`, time tiles or vector tiles, fetched and shaped once for every view of a GPU. `view.snapshot(name)` is what a view draws of a source now. -- Added: `load({ wasm, worker })`; a call before it finishes throws `unloaded`. Every class and builder imports from `latkit`. -- Added: every wait takes `{ signal }` and ends at it, 30 seconds unless given: picks, `screenOf`, `arrange`, pictures, encodings, videos, `gpu.done()`, `gpu.landed()`. A timeout rejects with `timeout`, an abort or a destroyed view with `cancelled`, and the work under way is dropped. -- Added: `gpu.canvas(element, { layout: [{ view, at }] })` and `canvas.layout`; `canvas.shot()`, a picture of exactly what a canvas shows; `new Shot(size, { scale, layout, transparent, capture, elapsed })`, where `capture: 'now'` takes views as they stand; `shot.video({ fps, output, bitrate, signal })`. -- Added: colors are typed: in a color option a string is a CSS color, `field(name)` reads a field, and `color(css)`, colormaps, and `when(...)` make colors (`Paint`, `PaintInput`). -- Added: `px(n)` and `units(n)` say a length's unit: CSS pixels on a plane, the earth, and a plot, position units in a diagram, unless said. A stroke in pixels keeps its width as a diagram zooms. -- Added: the theme in groups: `text`, `vertex`, `edge`, `line`, `area`, `focus`, `flow`, `diagram`, `earth`, `plot`; a comet's `flow.spacing` and `flow.length` take `{ px }` or `{ units }`. A diagram's `diagram.outline`, `corner`, `portSize`, and `bend` set its blocks' and groups' borders, their corners, its ports' width, and its wires' bends; `shadow: true` on its blocks and groups lays a soft shadow `diagram.shadow.lift` below them. -- Added: `Gpu.resident` and `Gpu.uploading`, bytes of data on the GPU and waiting to upload for every view of it; `Work.packed`, `Work.resolved` (rows whose ends were found where their vertices are held), `Work.cleared` (bytes of pages cleared), and `Work.uploads` (pages, or pieces of them, uploaded). -- Changed: `view.scene`, `view.selection`, `view.screen(position)`, `view.position(point)`, and `view.screenOf(row)`; the `hovered` and `selection` events; `view.waiting`. -- Changed: `new Patch(dataset)` names a patch's dataset; `Patch.read(source)` reads a patch's bytes or JSON; `Data.read(source, { name, base })` a dataset's bytes, JSON, or GeoJSON; `patch.append(table, at, fields, { ids })` and `patch.frames(table, field, { level, first, at, values })`; `data.envelope({ table, field, range, spans, ids })`; `field(name).from(data, table)` reads another dataset. -- Changed: the `'earth'` space, `{ earth: { projection: 'mercator' | 'globe', graticule, daylight, atmosphere } }`; the ground's color is the theme's `earth.ground`. -- Changed: edges ending far apart draw within the budget: the vertices they end at are held a row at a time where few lie in a page, and edges find where each end is held on the GPU whenever the vertices move. -- Changed: a plot measures the room its axes' labels and titles take. -- Changed: a table drawn in part holds whole pages of its order, each in a place of its own, and a page it no longer draws stays held while the plan leaves room, so drawing it again costs nothing; appending to it costs a page and never thins it; lanes, the stars of nets, joins, and fitted domains read the data, not the rows drawn. -- Changed: a view's first frame waits only for what draws its marks, a browser's first GPU is made while the WebAssembly loads, pipelines compile through one queue per GPU, the most urgent first, and only for what their marks are and read; the wasm is 12% smaller. -- Changed: a hit names the row and the place the frame it answers drew; a GPU's views draw one frame together, `Budget.uploadPerFrame` bounding what all of them upload in it. -- Changed: a diagram's blocks and groups are bordered in `diagram.outline`; corners, status rings, and ports' rings zoom with the drawing; a block makes room for its top and bottom ports' labels; text is set in the system's sans-serif face unless the theme names one. -- Changed: the globe's atmosphere is a thin rim and a halo; a GPU decides its antialiasing once; zooming at a fit's limit keeps the center. -- Removed: `view.setData`, `view.setTiles`, `view.removeData`, and `view.data(name)` (`view.sources`); `view.show(scene)`, `view.select(rows)`, `view.point`, `view.ground`, and `view.locate`; the `hover` and `select` events; `Stats.waiting`, `Stats.uploading`, `Stats.resident`, and `Stats.budget`. -- Removed: `loadLatkit()` and its result object, and `latkit/core`; `createWorker` (`worker`); `{ deadline }` options; `canvas.show(views)` and the canvas's `panels`; `Shot`'s builder methods, `shot.image(views)`, and `shot.video(gpu, fps, output)`. -- Removed: `Patch.to`, `Patch.fromBytes`, `Data.load`, `Data.tables`, `data.ids()`, `data.fields()`, `data.span` (`describe()`), and the positional `envelope`; `expr.of(data, table)` (`from`). -- Removed: the `'mercator'` and `'globe'` spaces; the theme's flat keys, such as `vertexRadius`, `fontSize`, and `edgeSpacing`; a string read as a color only when it began with `#`; `Flow.units`; shadows on edges, lines, areas, and faces, and the theme's `shadow.light` and `shadow.softness`. -- Fixed: a view whose next version finds no room lets go of what it draws so that version lands; where even that fits nothing, it tells a `limit` error and rests. A mark ordered by `priority` draws the whole trace it traces. A scene with two marks of one kind drawing one table asks for a `name`. A plot's axes start below its title. +- Added: `view.sources` and `gpu.view(scene, { sources })`. A source is a `Data` (drawn at its latest version from the frame after each patch), a `Snapshot` (one fixed version: `data.snapshot()`, `data.restore(snapshot)`), or `Tiles(name, url)` (time tiles or vector tiles, fetched once per GPU). `view.snapshot(name)` returns what a view currently draws of a source. +- Added: `load({ wasm, worker })` loads the WebAssembly; using the library before it finishes throws `unloaded`. Every class and builder is exported from `latkit`. +- Added: every wait takes `{ signal }` and times out after 30 seconds by default: picks, `screenOf`, `arrange`, pictures, encodings, videos, `gpu.done()`, and `gpu.landed()`. A timeout rejects with `timeout`; an abort or a destroyed view rejects with `cancelled` and drops the work in progress. +- Added: `gpu.canvas(element, { layout: [{ view, at }] })` and `canvas.layout`; `canvas.shot()` captures exactly what a canvas shows; `new Shot(size, { scale, layout, transparent, capture, elapsed })`, where `capture: 'now'` takes views as they are; `shot.video({ fps, output, bitrate, signal })`. +- Added: typed colors. In a color option a string is a CSS color, `field(name)` reads a field, and `color(css)`, colormaps, and `when(...)` build colors (`Paint`, `PaintInput`). +- Added: `px(n)` and `units(n)` give a length's unit. Lengths default to CSS pixels on planes, the earth, and plots, and to position units in diagrams. A stroke in pixels keeps its width as a diagram zooms. +- Added: the theme in groups: `text`, `vertex`, `edge`, `line`, `area`, `focus`, `flow`, `diagram`, `earth`, `plot`. A comet's `flow.spacing` and `flow.length` take `{ px }` or `{ units }`. `diagram.outline`, `corner`, `portSize`, and `bend` style blocks, groups, ports, and wire bends; `shadow: true` on blocks and groups casts a soft shadow `diagram.shadow.lift` below them. +- Added: `Gpu.resident` and `Gpu.uploading`, the bytes on the GPU and waiting to upload across all its views; `Work.packed`, `Work.resolved`, `Work.cleared`, and `Work.uploads`. +- Changed: `view.scene`, `view.selection`, `view.screen(position)`, `view.position(point)`, `view.screenOf(row)`, `view.waiting`, and the `hovered` and `selection` events replace the methods and events removed below. +- Changed: every error the package throws is a `LatkitError` with a `code`. A failed fetch or an error status is `io`; a call on a destroyed object has the new code `destroyed`; a bad `Patch.stream` limit and a `Video` drawn out of turn are `invalid`. Errors from the video encoder and worker jobs keep their codes, and an abort during a fetch rejects as `cancelled` or `timeout`. +- Changed: latkit's bytes carry no version or flags. A frame's header is the magic `LATK` and the frame's length, 12 bytes; files written by earlier versions no longer open. +- Changed: `theme.missing` is the color a colormap gives a missing value. An option given `missing()` still takes its theme value. +- Changed: `new Patch(dataset)` names its dataset; `Patch.read(source)` reads a patch's bytes or JSON, from bytes, a `Blob`, a `Response`, a byte stream, or a URL; `Data.read(source, { name, base })` reads a dataset's bytes, JSON, or GeoJSON; `patch.append(table, at, fields, { ids })`; `patch.frames(table, field, { level, first, at, values })`; `data.envelope({ table, field, range, spans, ids })`; `field(name).from(data, table)` reads another dataset. +- Changed: the `'earth'` space, `{ earth: { projection: 'mercator' | 'globe', graticule, daylight, atmosphere } }`. The ground color is the theme's `earth.ground`. +- Changed: a table too large to draw whole draws in pages that keep their GPU slots as the camera moves. Pages no longer drawn stay on the GPU while the budget allows, so coming back is free, and a write rebuilds only the pages it touched. Appending rows never thins what draws. Lanes, net stars, joins, and fitted domains follow the data, not the camera. +- Changed: edges whose ends lie far apart draw within the budget: their endpoint rows load individually, and only edges whose endpoints moved look them up again. +- Changed: a hit describes the frame it came from. A GPU's views draw one frame together, and `Budget.uploadPerFrame` bounds what they upload in it. +- Changed: editing a diagram rederives only what the edit reaches: the moved blocks, the groups around them, and the wires near them. Map tiles draw in the frame they finish loading. +- Changed: a plot measures the room its axis labels and titles need. A GPU picks its antialiasing once. Zooming at a fit's limit keeps the center. +- Removed: `view.setData`, `view.setTiles`, `view.removeData`, and `view.data(name)` (use `view.sources`); `view.show(scene)`, `view.select(rows)`, `view.point`, `view.ground`, and `view.locate`; the `hover` and `select` events; `Stats.waiting`, `Stats.uploading`, `Stats.resident`, and `Stats.budget`. +- Removed: `loadLatkit()` and its result object; `latkit/core`; `createWorker` (use `load({ worker })`); `{ deadline }` options (use `{ signal }`); `canvas.show(views)` and the canvas's `panels`; `Shot`'s builder methods, `shot.image(views)`, and `shot.video(gpu, fps, output)`. +- Removed: `Patch.to`, `Patch.fromBytes`, `Data.load`, `Data.tables`, `data.ids()`, `data.fields()`, `data.span` (use `describe()`), the positional `envelope`, and `expr.of(data, table)` (use `from`). +- Removed: the `'mercator'` and `'globe'` spaces (use `earth`); flat theme keys such as `vertexRadius`, `fontSize`, and `edgeSpacing`; reading a string as a color only when it begins with `#`; `Flow.units`; shadows on edges, lines, areas, and faces, and the theme's `shadow.light` and `shadow.softness`. +- Fixed: a view whose next version does not fit releases what it draws so that version can load; when nothing fits, it reports a `limit` error and stops. +- Fixed: on the earth, sizes in `units(n)` are degrees, as comet lengths already were, not mercator radians. +- Fixed: a text the host cannot rasterize reports one error, not one every frame it shows, and a text with nothing to draw is no error. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d59f203..b7fc11bd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,22 +2,24 @@ ## Repository -| Part | What it is | -| --------------------------------- | -------------------------------------------------------------- | -| [`latkit`](crates/latkit) | The Rust library: data, expressions, scenes, views, and video | -| [npm `latkit`](npm) | The library in the browser, with types for every scene | -| [`latkit-cli`](crates/latkit-cli) | Pictures and video of scenes, from JSON or binary data | -| [Examples](npm/examples) | Every feature on a page, and the browser gate that drives them | -| [Benchmark](benchmark) | Every scenario's phases up its sizes, held to committed work | -| [Docs](docs) | The site on [Read the Docs](https://latkit.readthedocs.io) | -| [Gallery](docs/gallery) | The documentation's pictures, each drawn by latkit | +| Part | What it is | +| --------------------------------- | ----------------------------------------------------------------------- | +| [`latkit`](crates/latkit) | The Rust library: data, expressions, scenes, views, and video | +| [`latkit-js`](crates/latkit-js) | The WebAssembly bindings the npm package wraps | +| [npm `latkit`](npm) | The library in the browser, with types for every scene | +| [`latkit-cli`](crates/latkit-cli) | Pictures and video of scenes, from JSON or binary data | +| [Examples](npm/examples) | A page for every feature, and the browser gate that tests them | +| [Benchmark](benchmark) | Every scenario at increasing sizes, checked against committed baselines | +| [Docs](docs) | The site on [Read the Docs](https://latkit.readthedocs.io) | +| [Gallery](docs/gallery) | The documentation's pictures, each drawn by latkit | +| [`xtask`](xtask) | Repository tasks, run as `cargo xtask ` | The `@latkit/*` TypeScript packages, a lighter alternative without WebAssembly, are maintained on the [`npm/js`](https://github.com/lukelowry/latkit/tree/npm/js) branch. ## Develop -Requires Rust (pinned in `rust-toolchain.toml`), Node.js 24, pnpm 10.30, and for the docs, uv. +Requires Rust (pinned in `rust-toolchain.toml`), Node.js 24, pnpm 10.30, and uv for the docs. ```sh pnpm install @@ -28,8 +30,8 @@ cargo xtask bench --quick # every benchmark scenario once, at its smallest s ## Documentation -The site builds with Python alone, from what the repository checks in; each generated part has the -command that writes it again: +The site builds with Python alone from checked-in files. These commands rebuild the site and +regenerate its generated parts: ```sh uv run --project docs --group docs sphinx-build -W -b html docs target/docs # the site @@ -40,4 +42,7 @@ node docs/gallery/render.ts [name...] # the pictures, drawn by th node benchmark/docs.ts # the performance table, from a baseline ``` +Drawing the pictures also needs the npm package built (`cargo xtask wasm`). The CLI reference, +`docs/reference/cli.md`, is regenerated by `LATKIT_BLESS=1 cargo test -p latkit-cli`. + [Releasing](RELEASING.md) covers versions and publishing. diff --git a/README.md b/README.md index 5c788f9d..704a3f3b 100644 --- a/README.md +++ b/README.md @@ -64,12 +64,12 @@ view.fit(); grid.apply(new Patch().set('Bus', [102], { kv: [345] })); // drawn the next frame ``` -Scenes, expressions, and patches are plain data, typed from latkit's schema, and build without -loading WebAssembly: in a worker or on a server too. +Scenes, expressions, and patches are plain data with types generated from latkit's schema. Building +them needs no WebAssembly, so they also work in a worker or on a server. ## On the command line -The same scene and data, as JSON, render to a picture, or with `--frames` to video: +The same scene and data, written as JSON, render to a picture, or to video with `--frames`: ```sh latkit render scene.json --data grid.json --output grid.png @@ -127,12 +127,12 @@ latkit render scene.json --data grid.json --frames frames.jsonl --fps 60 --outpu A Clifford strange attractor of 260,000 translucent points, colored by the way each step turns

    -Every picture is drawn by latkit; [the examples](https://latkit.readthedocs.io/en/latest/examples/) hold them all with the code that draws them. +latkit drew every picture here; [the examples](https://latkit.readthedocs.io/en/latest/examples/) show each one with the code that draws it. ## Documentation -Guides, examples, and the reference are on [Read the Docs](https://latkit.readthedocs.io); the Rust -API is on [docs.rs](https://docs.rs/latkit). [Contributing](CONTRIBUTING.md) covers the repository and -how to build it. +Guides, examples, and the reference are on [Read the Docs](https://latkit.readthedocs.io), and the +Rust API is on [docs.rs](https://docs.rs/latkit). [Contributing](CONTRIBUTING.md) covers the +repository and how to build it. Created by [Luke Lowery](https://lukelowry.github.io/). [MIT License](LICENSE). diff --git a/RELEASING.md b/RELEASING.md index d160e635..95c0e139 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,30 +1,32 @@ # Releasing -One version is shared by npm `latkit`, Cargo `latkit`, and Cargo `latkit-cli` (binary `latkit`). -Changesets describes changes and opens the version PR; `cargo xtask version` updates Cargo's -workspace and dependency versions structurally. +npm `latkit`, Cargo `latkit`, and Cargo `latkit-cli` (binary `latkit`) share one version. +Changesets records changes and opens the version PR. `cargo xtask version` copies the npm version +into the Cargo workspace (its package version and its `latkit` dependency) and updates `Cargo.lock`. 1. Merge the version PR and let CI pass on that commit. -2. The release workflow builds `cargo xtask package` and runs `cargo xtask package-test` against - the exact npm tarball, including strict TypeScript consumers and browser rendering. -3. `cargo xtask release` verifies the clean commit, tarball hash, and test receipt, and packages - both Cargo crates. It publishes or verifies npm, the core crate, and the CLI in order. -4. Tags are created only after all registry artifacts match the build. A retry skips identical - artifacts and completes any missing publication. Different content at an existing version - stops the release; registry errors are never treated as absence. Once a version is tagged, +2. The release workflow builds the npm tarball with `cargo xtask package` and runs + `cargo xtask package-test` against that exact tarball, including strict TypeScript consumers and + browser rendering. +3. `cargo xtask release` verifies the clean commit, the tarball hash, and the test receipt, and + packages both Cargo crates. It then publishes, or verifies already published, npm, the core + crate, and the CLI, in that order. +4. Tags are created only after every registry artifact matches the build. A retry skips identical + artifacts and completes any missing publication. Different content at an existing version stops + the release, and a registry error never counts as a missing artifact. Once a version is tagged, later commits at that version publish nothing. -For recovery, rerun the failed workflow, or dispatch **Release** on `main`; manual recovery -requires a successful CI run for the exact commit. Do not bump versions just to recover a partial -release. Rebuild and retest after changing or committing source files. +To recover, rerun the failed workflow, or dispatch **Release** on `main`; a manual dispatch needs a +successful CI run for the exact commit. Do not bump versions just to recover a partial release. +Rebuild and retest after changing or committing source files. -The workflow needs an npm trusted publisher for repository `lukelowry/latkit`, workflow -`release.yml`, and a `CARGO_REGISTRY_TOKEN` allowed to publish both crates. These are registry/account -settings, not files in this repository. First-publication ownership and authentication must be -configured before invoking release. See [npm trusted publishers](https://docs.npmjs.com/trusted-publishers/). +The workflow needs an npm trusted publisher for repository `lukelowry/latkit` and workflow +`release.yml`, and a `CARGO_REGISTRY_TOKEN` allowed to publish both crates. These are registry and +account settings, not files in this repository. Set up package ownership and authentication before +the first release. See [npm trusted publishers](https://docs.npmjs.com/trusted-publishers/). -`cargo install latkit-cli` is the normal user command. `--locked` optionally reproduces the -dependency versions tested for that release; CI and release builds use it for reproducibility. +Users install the CLI with `cargo install latkit-cli`. Adding `--locked` reproduces the dependency +versions a release was tested with; CI and release builds always use it. -LATK's format version is separate from package versions. V1 fixtures must remain stable across -compatible releases. A new layout requires a new format version and a migration policy. +latkit's binary format carries no version. A release that changes its layout says in its changeset +that files written before it no longer open, and updates `crates/latkit/tests/fixtures/patch.hex`. diff --git a/benchmark/README.md b/benchmark/README.md index d5fc34e2..9cc169d6 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -1,8 +1,10 @@ # Benchmark -Each scenario's phases, up its sizes, in Chrome: time, GPU time, and the work the GPU did, held to -committed baselines. The page steps every frame itself, each waiting for the GPU rather than the -clock, so a run draws the same frames on any device; how frames pace a page is the gate's to judge. +The benchmark runs each scenario in Chrome at increasing sizes and measures each phase's time, GPU +time, and GPU work, checked against committed baselines. The page steps every frame itself, starting +each one when the GPU has finished the last rather than on the display's clock, so a run draws the +same frames on any device. Frame pacing is judged by the browser gate (`npm/examples/gate/gate.ts`) +instead. ```sh cargo xtask bench --quick # each scenario once at its smallest size: phases and work @@ -14,18 +16,20 @@ cargo xtask bench --update # lay this run's results over the baselines A run fails on: -- an error, a frame drawn while nothing changed, a pipeline compiled where it was first used - (`late`), or resident data over its budget; -- a phase growing with size faster than its order, its uploads counted in pages (`uploads`), each at - least a page of bytes; -- more work than `baselines/work.json`: exactly for what the data sets (`uploaded`, `uploads`, - `evaluated`, `packed`, `jobs`, `pipelines`, `late`, `resident`) in a phase that acts once, and - within 2× for what moves with the frame a job or a fetch lands in (`written`, `readbacks`, `texts`, - every count of a phase acting each frame, and all of a scenario fed by fetches); -- in a full sweep, on the machine `baselines/.json` was taken on, a phase whose fastest - repeat is over 1.25× slower, and for a phase acting once, later by over a timer tick (16 ms): a - stall of the machine slows one repeat, a regression every one. +- An error, a frame drawn while nothing changed, a pipeline compiled where it was first used + (`late`), or resident data over its budget. +- A phase whose time or uploaded bytes (and, for loading, resident bytes) grow with size faster + than its declared order. Each upload (`uploads`) counts as at least a page of bytes. +- More work than `baselines/work.json`. In a phase that acts once, the counts the data alone sets + (`uploaded`, `uploads`, `evaluated`, `packed`, `jobs`, `pipelines`, `late`, `resident`) may not + exceed the baseline. The other counts (`written`, `resolved`, `cleared`, `readbacks`, `texts`), + every count of a phase that acts each frame, and every count of a scenario fed by fetches may + reach 2× the baseline, since they depend on which frame a job or fetch finishes in. +- In a full sweep on the machine that recorded `baselines/.json`, a phase whose fastest + repeat is more than 1.25× slower than its baseline, plus one timer tick (16 ms) for a phase that + acts once. Judging the fastest repeat means a stall of the machine, which slows one repeat, does + not fail the run; a regression slows every repeat. -On a machine with two GPUs, Chrome draws on the fast one. Reports and results land in -`target/bench/`. Scenarios are in `page/scenarios.ts`, the data they draw in `page/data.ts`, and how -a phase is measured in `page/stage.ts`. +On a machine with two GPUs, Chrome is made to draw on the faster one. Reports and results are +written to `target/bench/`. Scenarios are defined in `page/scenarios.ts`, the data they draw in +`page/data.ts`, and how a phase is measured in `page/stage.ts`. diff --git a/benchmark/baselines/work.json b/benchmark/baselines/work.json index 97ef58a2..8a4f7b62 100644 --- a/benchmark/baselines/work.json +++ b/benchmark/baselines/work.json @@ -719,7 +719,7 @@ "load": { "uploaded": 1638400, "uploads": 100, - "written": 5700, + "written": 4868, "evaluated": 81920, "resolved": 0, "cleared": 262144, @@ -749,7 +749,7 @@ "zoom": { "uploaded": 3932160, "uploads": 240, - "written": 57304, + "written": 56424, "evaluated": 196608, "resolved": 0, "cleared": 786432, @@ -961,7 +961,7 @@ "zoom": { "uploaded": 4014080, "uploads": 245, - "written": 57520, + "written": 56640, "evaluated": 200704, "resolved": 0, "cleared": 802816, @@ -1145,7 +1145,7 @@ "load": { "uploaded": 36352, "uploads": 7, - "written": 8360, + "written": 7272, "evaluated": 3008, "resolved": 1984, "cleared": 12288, @@ -1646,7 +1646,7 @@ "load": { "uploaded": 4980736, "uploads": 304, - "written": 16872, + "written": 17280, "evaluated": 290816, "resolved": 69632, "cleared": 835584, @@ -1676,7 +1676,7 @@ "zoom": { "uploaded": 12845056, "uploads": 784, - "written": 213052, + "written": 212164, "evaluated": 868352, "resolved": 380928, "cleared": 3440640, @@ -1704,19 +1704,19 @@ "resident": 76759040 }, "one row": { - "uploaded": 819200, - "uploads": 50, - "written": 194240, - "evaluated": 184320, - "resolved": 53248, + "uploaded": 655360, + "uploads": 40, + "written": 43840, + "evaluated": 163840, + "resolved": 0, "cleared": 0, - "packed": 198065, - "readbacks": 2, + "packed": 163840, + "readbacks": 0, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 77578240 + "resident": 77414400 }, "first pick": { "uploaded": 0, @@ -1731,7 +1731,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 77578240 + "resident": 77414400 }, "pick": { "uploaded": 0, @@ -1746,7 +1746,7 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 77578240 + "resident": 77414400 }, "hover": { "uploaded": 0, @@ -1761,22 +1761,22 @@ "texts": 0, "pipelines": 0, "late": 0, - "resident": 77578240 + "resident": 77414400 }, "append": { "uploaded": 3276800, "uploads": 200, - "written": 48916, + "written": 128232, "evaluated": 163840, "resolved": 0, - "cleared": 0, + "cleared": 16384, "packed": 119300, "readbacks": 40, "jobs": 0, "texts": 0, "pipelines": 0, "late": 0, - "resident": 80855040 + "resident": 80969728 } }, "2000000": { @@ -1813,7 +1813,7 @@ "load": { "uploaded": 5144576, "uploads": 314, - "written": 17644, + "written": 17640, "evaluated": 299008, "resolved": 69632, "cleared": 851968, @@ -1843,7 +1843,7 @@ "zoom": { "uploaded": 18939904, "uploads": 1156, - "written": 274688, + "written": 272660, "evaluated": 1175552, "resolved": 385024, "cleared": 4669440, @@ -1982,7 +1982,7 @@ "load": { "uploaded": 240000, "uploads": 18, - "written": 709440, + "written": 711104, "evaluated": 10000, "resolved": 0, "cleared": 368640, @@ -2012,14 +2012,14 @@ "zoom": { "uploaded": 0, "uploads": 0, - "written": 5139048, + "written": 5151984, "evaluated": 0, "resolved": 0, "cleared": 0, "packed": 0, "readbacks": 40, "jobs": 0, - "texts": 1227, + "texts": 1230, "pipelines": 0, "late": 0, "resident": 1269760 @@ -2059,7 +2059,7 @@ "load": { "uploaded": 2400000, "uploads": 150, - "written": 1431184, + "written": 1428688, "evaluated": 100000, "resolved": 0, "cleared": 3612672, @@ -2089,14 +2089,14 @@ "zoom": { "uploaded": 0, "uploads": 0, - "written": 6729684, + "written": 6721996, "evaluated": 0, "resolved": 0, "cleared": 0, "packed": 0, "readbacks": 40, "jobs": 0, - "texts": 1523, + "texts": 1522, "pipelines": 0, "late": 0, "resident": 12443648 @@ -2136,7 +2136,7 @@ "load": { "uploaded": 1966080, "uploads": 120, - "written": 2930500, + "written": 2926340, "evaluated": 81920, "resolved": 0, "cleared": 4653056, @@ -2166,14 +2166,14 @@ "zoom": { "uploaded": 4718592, "uploads": 288, - "written": 11478636, + "written": 11551716, "evaluated": 196608, "resolved": 0, "cleared": 12582912, "packed": 1179648, "readbacks": 44, "jobs": 0, - "texts": 1414, + "texts": 1426, "pipelines": 0, "late": 0, "resident": 27983872 @@ -2215,7 +2215,7 @@ "load": { "uploaded": 1456, "uploads": 7, - "written": 6192, + "written": 6240, "evaluated": 116, "resolved": 100, "cleared": 768, @@ -2307,7 +2307,7 @@ "load": { "uploaded": 12256, "uploads": 7, - "written": 6240, + "written": 6192, "evaluated": 1016, "resolved": 1000, "cleared": 4352, @@ -2399,7 +2399,7 @@ "load": { "uploaded": 120256, "uploads": 13, - "written": 6296, + "written": 7384, "evaluated": 10016, "resolved": 10000, "cleared": 41216, @@ -2491,7 +2491,7 @@ "load": { "uploaded": 1200256, "uploads": 79, - "written": 6172, + "written": 7260, "evaluated": 100016, "resolved": 100000, "cleared": 401664, @@ -2585,7 +2585,7 @@ "load": { "uploaded": 1340768, "uploads": 52, - "written": 3136, + "written": 3968, "evaluated": 100, "resolved": 0, "cleared": 512, @@ -2707,7 +2707,7 @@ "load": { "uploaded": 4180168, "uploads": 147, - "written": 6252, + "written": 6244, "evaluated": 1000, "resolved": 0, "cleared": 4096, @@ -2829,7 +2829,7 @@ "load": { "uploaded": 18151952, "uploads": 645, - "written": 11672, + "written": 11664, "evaluated": 10000, "resolved": 0, "cleared": 40960, @@ -2951,7 +2951,7 @@ "load": { "uploaded": 40413432, "uploads": 1561, - "written": 30572, + "written": 21412, "evaluated": 30000, "resolved": 0, "cleared": 122880, @@ -3075,7 +3075,7 @@ "load": { "uploaded": 277592, "uploads": 12, - "written": 3764, + "written": 2932, "evaluated": 80, "resolved": 0, "cleared": 256, @@ -3085,7 +3085,7 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 304640 + "resident": 304896 }, "pan": { "uploaded": 437472, @@ -3103,19 +3103,19 @@ "resident": 781824 }, "zoom": { - "uploaded": 745656, - "uploads": 38, - "written": 35848, - "evaluated": 1056, + "uploaded": 792992, + "uploads": 43, + "written": 36000, + "evaluated": 1136, "resolved": 0, "cleared": 1024, "packed": 0, "readbacks": 0, - "jobs": 25, + "jobs": 27, "texts": 0, "pipelines": 0, "late": 0, - "resident": 1620736 + "resident": 1673472 } }, "256": { @@ -3167,7 +3167,7 @@ "pan": { "uploaded": 648192, "uploads": 32, - "written": 35412, + "written": 35288, "evaluated": 8192, "resolved": 0, "cleared": 8192, @@ -3180,19 +3180,19 @@ "resident": 1449984 }, "zoom": { - "uploaded": 1132288, - "uploads": 79, - "written": 36500, - "evaluated": 18176, + "uploaded": 1103232, + "uploads": 77, + "written": 36600, + "evaluated": 18688, "resolved": 0, "cleared": 16384, "packed": 0, "readbacks": 0, - "jobs": 21, + "jobs": 22, "texts": 0, "pipelines": 0, "late": 0, - "resident": 3264512 + "resident": 3207168 } }, "4096": { @@ -3229,7 +3229,7 @@ "load": { "uploaded": 2638176, "uploads": 258, - "written": 6640, + "written": 6632, "evaluated": 16384, "resolved": 0, "cleared": 65536, @@ -3242,34 +3242,34 @@ "resident": 3194880 }, "pan": { - "uploaded": 1286320, - "uploads": 128, - "written": 43068, - "evaluated": 73728, + "uploaded": 1948272, + "uploads": 192, + "written": 44912, + "evaluated": 77824, "resolved": 0, - "cleared": 98304, + "cleared": 114688, "packed": 0, "readbacks": 0, "jobs": 8, "texts": 0, "pipelines": 0, "late": 0, - "resident": 4710400 + "resident": 5464064 }, "zoom": { - "uploaded": 1894624, + "uploaded": 1747792, "uploads": 260, - "written": 51500, - "evaluated": 49152, + "written": 52420, + "evaluated": 65536, "resolved": 0, "cleared": 98304, "packed": 0, "readbacks": 0, - "jobs": 10, + "jobs": 11, "texts": 0, "pipelines": 0, "late": 0, - "resident": 8093696 + "resident": 8028160 } } }, @@ -3308,7 +3308,7 @@ "load": { "uploaded": 25440, "uploads": 22, - "written": 8792, + "written": 9880, "evaluated": 300, "resolved": 0, "cleared": 5120, @@ -3400,7 +3400,7 @@ "load": { "uploaded": 254960, "uploads": 50, - "written": 17192, + "written": 16104, "evaluated": 3000, "resolved": 0, "cleared": 40960, @@ -3492,7 +3492,7 @@ "load": { "uploaded": 2484264, "uploads": 366, - "written": 89552, + "written": 89544, "evaluated": 30000, "resolved": 0, "cleared": 409600, @@ -3586,7 +3586,7 @@ "load": { "uploaded": 49152, "uploads": 2, - "written": 39916, + "written": 43948, "evaluated": 10, "resolved": 0, "cleared": 256, @@ -4512,7 +4512,7 @@ "load": { "uploaded": 1638400, "uploads": 100, - "written": 629272, + "written": 630104, "evaluated": 98304, "resolved": 0, "cleared": 5296640, @@ -4527,7 +4527,7 @@ "move": { "uploaded": 655360, "uploads": 40, - "written": 252432, + "written": 249104, "evaluated": 49152, "resolved": 0, "cleared": 557056, @@ -4638,7 +4638,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 19536, + "written": 17360, "evaluated": 597872, "resolved": 398160, "cleared": 2408448, @@ -4700,7 +4700,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 32536, + "written": 36880, "evaluated": 1195744, "resolved": 796320, "cleared": 4816896, @@ -4762,7 +4762,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 71592, + "written": 71568, "evaluated": 2391488, "resolved": 1592640, "cleared": 9666560, @@ -4826,7 +4826,7 @@ "load": { "uploaded": 2686976, "uploads": 164, - "written": 27784, + "written": 27780, "evaluated": 163840, "resolved": 49152, "cleared": 442368, @@ -4841,7 +4841,7 @@ "pan": { "uploaded": 0, "uploads": 0, - "written": 48644, + "written": 47996, "evaluated": 0, "resolved": 0, "cleared": 0, @@ -4856,7 +4856,7 @@ "zoom": { "uploaded": 1376256, "uploads": 84, - "written": 366852, + "written": 366848, "evaluated": 98304, "resolved": 114688, "cleared": 229376, @@ -4903,7 +4903,7 @@ "load": { "uploaded": 3588096, "uploads": 219, - "written": 35448, + "written": 34672, "evaluated": 208896, "resolved": 49152, "cleared": 589824, @@ -4980,7 +4980,7 @@ "load": { "uploaded": 3588096, "uploads": 219, - "written": 36432, + "written": 34680, "evaluated": 208896, "resolved": 49152, "cleared": 589824, @@ -5059,7 +5059,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 15184, + "written": 19536, "evaluated": 597872, "resolved": 398160, "cleared": 2408448, @@ -5273,7 +5273,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 19536, + "written": 17360, "evaluated": 597872, "resolved": 398160, "cleared": 2408448, diff --git a/benchmark/docs.ts b/benchmark/docs.ts index f6ed281c..c51f279e 100644 --- a/benchmark/docs.ts +++ b/benchmark/docs.ts @@ -1,8 +1,11 @@ /** - * The documentation's performance table, from a committed baseline: each scenario at the largest - * size it ran, what opening took, and what a frame took while it moved and was picked from. + * Writes the documentation's performance table, docs/_static/performance.csv, from a committed + * times baseline. Each row is a scenario at the largest size it ran: its `load` time, the slowest + * CPU and GPU frame times among its moving phases, and its slowest pick or hover. * * node benchmark/docs.ts [baselines/MACHINE.json] + * + * Without an argument it reads the last machine baseline in sorted file name order. */ import fs from 'node:fs'; import path from 'node:path'; @@ -32,7 +35,7 @@ const file = ); const baseline = JSON.parse(fs.readFileSync(file, 'utf8')) as Baseline; -/** Phases that run every frame, as the view moves or plays. */ +/** Phases that act every frame while the view moves or plays. */ const MOVING = ['pan', 'zoom', 'move', 'scrub', 'narrow', 'stream']; /** Phases that answer the pointer. */ const PICKING = ['pick', 'hover']; diff --git a/benchmark/judge.ts b/benchmark/judge.ts index d2b056ff..504a7ee7 100644 --- a/benchmark/judge.ts +++ b/benchmark/judge.ts @@ -1,23 +1,26 @@ /** - * What the runs say. Rules that hold on any machine: no errors, nothing drawn idle, no pipeline - * compiled where it was first used, resident data within its budget, every phase growing no faster - * than its order, and no more work than the work baseline. Frames are stepped, each waiting for the - * GPU, so what the data and the frames drawn set (`EXACT`) is held exactly in a phase that acts once - * and settles, unless fetched data feeds it; the rest, and every count of a phase acting each frame, - * moves with the frame a job or a fetch lands in, and is held within `STEADY`. Then, on the machine - * the times baseline was taken on, no phase much slower on its fastest repeat: a stall of the machine - * slows one repeat, a regression every one. + * Judges the runs. These rules hold on any machine: no errors, no frames drawn while idle, no + * pipeline compiled at first use, resident data within its budget, no phase growing faster than its + * order, and no more work than the work baseline. + * + * Frames are stepped, each waiting for the GPU. So in a phase that acts once and settles, the + * `EXACT` counts, which only the data and the frames drawn set, may not exceed the baseline at all, + * unless the scenario fetches its data. Other counts, and every count of a steady phase, vary with + * the frame a job or fetch finishes in, so they may reach `STEADY` times the baseline. + * + * On the machine the times baseline was taken on, no phase's fastest repeat may be much slower than + * its baseline. A machine stall slows one repeat, but a regression slows every one. */ import type { Work } from 'latkit'; import type { Listed, Run } from './page/main.ts'; -/** A phase at a size, across repeats: its fastest, and its least work. */ +/** A phase at one size, summarized across repeats: its fastest times and its least work. */ export interface Summary { - /** A steady phase's CPU per frame, else its milliseconds. */ + /** Milliseconds: CPU time per frame for a steady phase, else the whole phase. */ time: number; gpu: number; steady: boolean; - /** Frames drawn, median. */ + /** Median frames drawn. */ frames: number; work: Work & { resident: number }; } @@ -33,24 +36,26 @@ export interface Verdict { failures: string[]; notes: string[]; summaries: By; - /** The exponent each measure grows by, by scenario and phase. */ + /** The fitted growth exponent of each measure, by scenario and phase. */ exponents: Record>; } -/** How far a fitted exponent may pass its order: noise, and a change of regime between sizes. */ +/** How far a fitted exponent may exceed its order, allowing for noise and a change of regime + * between sizes. */ const SLACK = 0.2; -/** Milliseconds a phase acting once may land later than its baseline by: a timer tick, which is - * 15.6 ms on Windows, so readbacks answer one tick or the next. */ +/** Milliseconds a phase that acts once may finish later than its baseline: one timer tick. A tick + * is 15.6 ms on Windows, so a readback comes back on one tick or the next. */ const TICK = 16; -/** Bytes an upload counts as at least: a page, 4,096 rows of four bytes. */ +/** Minimum bytes an upload counts as: one page, 4,096 rows of four bytes. */ const PAGE = 4096 * 4; -/** Below these a cost counts as these: too small to fit an exponent to, or to call slower. A frame's - * CPU time is read on a page's clock, which steps by 0.1 ms, so it counts from five steps; a phase - * acting once lands on a tick. */ +/** Floors for costs too small to fit an exponent to or to call slower. Smaller costs count as + * these. A frame's CPU time comes from the page's clock, which steps by 0.1 ms, so its floor is + * five steps. A phase that acts once finishes on a timer tick. */ const FLOOR = { frame: 0.5, action: TICK, bytes: 64 * 1024 }; -/** How much slower than the baseline the fastest repeat may be. */ +/** Factor by which the fastest repeat may be slower than the baseline. */ const SLOWER = 1.25; -/** Work the data and the frames drawn alone set, which repeats exactly once a phase settles. */ +/** Work counts that only the data and the frames drawn set. They repeat exactly once a phase + * settles. */ const EXACT = new Set([ 'uploaded', 'uploads', @@ -61,10 +66,10 @@ const EXACT = new Set([ 'late', 'resident', ]); -/** How much more of the rest a phase may do than the baseline: the frame a job or a fetch lands in - * moves label placements and tiles by up to about 1.7×, and a per-frame blowup by far more. */ +/** Factor by which other work counts may exceed the baseline. The frame a job or fetch finishes in + * changes label placements and tiles by up to about 1.7×. A per-frame blowup changes far more. */ const STEADY = 2; -/** Below these, work counts as these when it is held within `STEADY`: bytes, and other counts. */ +/** Minimum baseline values for work held within `STEADY`: one for byte counts, one for the rest. */ const WORK_FLOOR = { bytes: 64 * 1024, count: 8 }; const BYTES = new Set(['uploaded', 'written', 'resident']); @@ -73,7 +78,7 @@ const median = (xs: number[]) => const sum = (xs: number[]) => xs.reduce((a, b) => a + b, 0); const mib = (n: number) => `${(n / 2 ** 20).toFixed(1)} MiB`; -/** The least-squares slope of log cost against log size: the exponent the cost grows by. */ +/** The least-squares slope of log cost against log size, which is the cost's growth exponent. */ function slope(points: (readonly [number, number])[]): number { const xs = points.map(([n]) => Math.log(n)); const ys = points.map(([, c]) => Math.log(c)); @@ -120,7 +125,7 @@ export function judge( for (const [phase, { work }] of Object.entries(r.phases)) if (work.late) failures.push( - `${at}, ${phase}: ${work.late} pipelines compiled where first used, not asked for ahead`, + `${at}, ${phase}: ${work.late} pipelines compiled at first use instead of ahead of time`, ); const resident = Math.max(...Object.values(r.phases).map((c) => c.resident)); if (resident > r.budget) @@ -146,7 +151,7 @@ export function judge( const fit: Growth = { time: grows((s) => s.time, steady ? FLOOR.frame : FLOOR.action), gpu: grows((s) => s.gpu, FLOOR.frame), - // Pages are what changes upload: below a page an upload, bytes count as that. + // Changes upload whole pages, so each upload counts as at least a page of bytes. uploaded: grows((s) => Math.max(s.work.uploaded, s.work.uploads * PAGE), FLOOR.bytes), resident: grows((s) => s.work.resident, FLOOR.bytes), }; @@ -159,7 +164,7 @@ export function judge( }; past('time', fit.time, order.time); past('uploaded bytes', fit.uploaded, order.bytes); - // What stays resident is the data's footprint, set by loading; later phases only add to it. + // Resident bytes are the data's footprint, which loading sets. Later phases only add to it. if (phase === 'load') past('resident bytes', fit.resident, order.resident ?? order.bytes); for (const n of at) { const now = bySize[n]![phase]!; @@ -171,7 +176,7 @@ export function judge( const exact = !now.steady && !scenario.fetches && EXACT.has(k); const most = exact ? was : Math.max(was, floor) * STEADY; if (is > most) failures.push(`${where}: ${k} ${is}, was ${was}`); - else if (is < was) notes.push(`${where}: ${k} ${is}, was ${was}; --update keeps it`); + else if (is < was) notes.push(`${where}: ${k} ${is}, was ${was}; --update records it`); } const then = sameMachine ? base.times!.times[scenario.name]?.[n]?.[phase] : undefined; const floor = now.steady ? FLOOR.frame : TICK; @@ -179,7 +184,7 @@ export function judge( failures.push(`${where}: ${now.time.toFixed(2)} ms, was ${then.time.toFixed(2)}`); else if (then && now.time < then.time / SLOWER) notes.push( - `${where}: ${now.time.toFixed(2)} ms, was ${then.time.toFixed(2)}; --update keeps it`, + `${where}: ${now.time.toFixed(2)} ms, was ${then.time.toFixed(2)}; --update records it`, ); } } @@ -200,8 +205,8 @@ const map = (v: Verdict, read: (s: Summary) => T): By => ]), ); -/** `run` laid over `base`, scenario by size: each size this run measured replaces the base's, and - * the rest stays. */ +/** Merges `run` into `base`. Each scenario size this run measured replaces the base's entry, and + * the rest is kept. */ function over(run: By, base: By = {}): By { const out = structuredClone(base); for (const [scenario, bySize] of Object.entries(run)) @@ -209,13 +214,14 @@ function over(run: By, base: By = {}): By { return out; } -/** The work baseline: this run's laid over `base`. */ +/** The new work baseline: this run's work merged into `base`. */ export function work(v: Verdict, base?: WorkBaseline): WorkBaseline { const measured = map(v, (s) => s.work); return over(measured, base); } -/** The times baseline of `machine`: this run's laid over `base`, where `base` was taken on it. */ +/** The new times baseline for `machine`: this run's times merged into `base` if `base` was taken + * on the same machine, else this run's times alone. */ export function times(v: Verdict, machine: string, base?: TimeBaseline): TimeBaseline { const measured = map(v, (s) => ({ time: s.time, gpu: s.gpu })); return { machine, times: over(measured, base?.machine === machine ? base.times : undefined) }; diff --git a/benchmark/page/data.ts b/benchmark/page/data.ts index 0ab07404..4805a393 100644 --- a/benchmark/page/data.ts +++ b/benchmark/page/data.ts @@ -1,14 +1,15 @@ /** - * What the scenarios draw, made before anything is measured: deterministic, so every run counts the - * same work, and linear in size. Each returns a `Write` that only hands latkit the arrays. + * The scenarios' data, made before anything is measured. It is deterministic, so every run counts + * the same work, and linear in size. Each function returns a `Write` that only hands latkit the + * arrays. */ import { reference, type Values } from 'latkit'; import type { Write } from './stage.ts'; -/** Axis coordinates a second. */ +/** Samples per second on the time axis. */ const RATE = 60; -/** Numbers in [0, 1), the same every run. */ +/** A seeded generator of numbers in [0, 1), the same every run. */ function random(seed: number): () => number { let s = seed >>> 0 || 1; return () => (s = (Math.imul(s, 1664525) + 1013904223) >>> 0) / 2 ** 32; @@ -24,8 +25,8 @@ export function scatter(n: number, seed = 1, named = false): Write { return (p) => p.set('Point', { count: n }, fields); } -/** About `n` buses in a square lattice over most of the globe, each joined to its right and lower - * neighbours: a grid's shape at any size. */ +/** About `n` buses in a square lattice over most of the globe, each joined by a line to its + * neighbours in the next column and the next row. It has a grid's shape at any size. */ export function lattice(n: number): Write { const side = Math.max(2, Math.round(Math.sqrt(n))); const count = side * side; @@ -58,7 +59,8 @@ export function lattice(n: number): Write { .set('Line', { count: lines }, { from: reference('Bus', from), to: reference('Bus', to) }); } -/** `n` edges among 16 vertices on a ring, each joining a vertex to the next: lanes by the thousand. */ +/** `n` edges among 16 vertices on a ring, each joining a vertex to the next, so each pair has about + * `n / 16` parallel edges drawn as lanes. */ export function lanes(n: number): Write { const k = 16; const x = Float64Array.from({ length: k }, (_, i) => Math.cos((2 * Math.PI * i) / k)); @@ -71,7 +73,8 @@ export function lanes(n: number): Write { .set('Link', { count: n }, { from: reference('Point', from), to: reference('Point', to) }); } -/** `n` regular 32-gons in a grid over the mercator world, each with a value. */ +/** `n` regular polygons of `sides` sides (32 by default) in a grid over the Mercator world, each + * with a value. */ export function polygons(n: number, sides = 32): Write { const across = Math.ceil(Math.sqrt(2 * n)); const [w, h] = [340 / across, 140 / Math.ceil(n / across)]; @@ -90,7 +93,8 @@ export function polygons(n: number, sides = 32): Write { return (p) => p.set('Region', { count: n }, { rings, v }); } -/** `n` blocks in trees of 64: each block's output is a net, which its two children's inputs name. */ +/** `n` blocks in binary trees of 64. Each block's output is a net, and its two children's inputs + * reference that net. */ export function blocks(n: number): Write { const input = new Array(n); const name = new Array(n); @@ -111,7 +115,7 @@ export function blocks(n: number): Write { const wave = (r: number, f: number) => Math.sin(r * 0.37 + f * 0.05) + (r % 10) * 0.2; -/** `rows` traces of `frames` samples at 60 a second, each its own wave. */ +/** `rows` traces of `frames` samples at `RATE` samples per second, each a different wave. */ export function traces(rows: number, frames: number): Write { const all = new Float32Array(rows * frames); for (let f = 0; f < frames; f++) for (let r = 0; r < rows; r++) all[f * rows + r] = wave(r, f); @@ -124,7 +128,8 @@ export function traces(rows: number, frames: number): Write { }; } -/** `count` patches recording the frames from `from` on, each forgetting what falls `history` behind. */ +/** `count` patches, each appending the next sample from frame `from` on and forgetting samples more + * than `history` frames behind. */ export function feed(rows: number, from: number, count: number, history: number): Write[] { return Array.from({ length: count }, (_, i): Write => { const f = from + i; diff --git a/benchmark/page/main.ts b/benchmark/page/main.ts index 9c0115a5..62d9416f 100644 --- a/benchmark/page/main.ts +++ b/benchmark/page/main.ts @@ -1,9 +1,12 @@ /** - * The bench page, one implementation of the contract the runner drives: `bench.ready()` resolves - * once a GPU device can be made; `bench.list()` names the scenarios and the orders their phases - * keep; `bench.run(name, size)` builds one at a size on a fresh GPU and measures it; `bench.lost()` - * says whether that GPU's device was lost. The runner opens a fresh page for every run, so nothing - * carries over but the browser's shader cache. + * The bench page. It implements the interface the runner drives: + * + * - `bench.ready()` resolves once a GPU device can be made. + * - `bench.list()` lists the scenarios and the growth orders of their phases. + * - `bench.run(name, size)` builds a scenario at a size on a fresh GPU and measures it. + * - `bench.lost()` returns whether that GPU's device was lost. + * + * The runner opens a fresh page for every run, so only the browser's shader cache carries over. */ import { Gpu } from 'latkit'; import { LOAD, scenarios, type Order } from './scenarios.ts'; @@ -22,30 +25,34 @@ export interface Listed { export interface Run { scenario: string; size: number; - /** The adapter and browser: times compare only on the same machine. */ + /** The adapter and browser. Times compare only on the same machine. */ machine: string; /** Every phase in order: `init`, `data`, `load`, then the scenario's. */ phases: Record; - /** Frames drawn over frames stepped once everything settled: none, for views that sleep. */ + /** Frames drawn in the idle frames stepped after everything settled. Views that sleep draw + * none. */ idle: number; - /** Bytes the budget lets data take. */ + /** The data budget, in bytes. */ budget: number; - /** What the views told went wrong. */ + /** Errors the views reported. */ errors: string[]; } -/** What data may take unless a scenario sets it: fixed, so the work counted is every device's. */ +/** The GPU budget unless a scenario sets one. It is fixed, not the device's default, so the work + * counted is the same on every device. */ const BUDGET = { bytes: 256 * 2 ** 20, uploadPerFrame: 32 * 2 ** 20 }; -/** The GPU of the run under way, which `lost` asks after. */ +/** The GPU of the current run, which `lost` checks. */ let current: Gpu | undefined; -/** Resolves once a GPU device can be made: a wedged driver hangs here, so the runner asks first. */ +/** Resolves once a GPU device can be made. A wedged driver hangs here, so the runner calls this + * first. */ async function ready(): Promise { (await Gpu.create()).destroy(); } -/** Whether the run's device was lost: the runner asks after a failure, and stops the sweep if so. */ +/** Whether the run's device was lost. The runner checks after a failure, and stops the sweep if + * so. */ function lost(): boolean { return current?.lost ?? false; } @@ -78,7 +85,7 @@ async function run(name: string, size: number): Promise { const built = scenario.build(size); const stage = new Stage(gpu, await canvases(scenario.views?.(size) ?? 1), size); const none = { frames: 0, cpu: 0, cpu95: 0, gpu: 0, resident: 0, steady: false }; - // Each phase says when it starts, so a page that stops answering is known by where it stopped. + // Each phase announces its start, so a page that stops responding shows where it stopped. const named = (phase: string, run: () => Promise) => { console.debug(`phase ${phase}`); return run().catch((error: unknown) => { @@ -104,7 +111,8 @@ async function run(name: string, size: number): Promise { }; } -/** `n` canvases filling the page in a grid, laid out before any view draws. */ +/** Adds `n` canvases filling the page in a grid, and waits a frame so they are laid out before any + * view draws. */ async function canvases(n: number): Promise { const main = document.querySelector('main')!; main.style.gridTemplateColumns = `repeat(${Math.ceil(Math.sqrt(n))}, minmax(0, 1fr))`; @@ -113,7 +121,7 @@ async function canvases(n: number): Promise { return made; } -/** The adapter, asked for as latkit asks for its own, and the browser. */ +/** Describes the adapter, requested with the options latkit uses, and the Chrome version. */ async function machine(): Promise { interface Adapter { info?: { vendor?: string; architecture?: string; description?: string }; diff --git a/benchmark/page/scenarios.ts b/benchmark/page/scenarios.ts index 980aba6c..1f545b64 100644 --- a/benchmark/page/scenarios.ts +++ b/benchmark/page/scenarios.ts @@ -1,10 +1,17 @@ /** - * Every scenario: what it sweeps, the scene and data at each size, and what it does. Each phase - * declares how its cost may grow with size, so the sweep checks the README's rules: CPU per frame - * stays flat in the rows, a one-row change costs the same at any size, restyling moves no data, - * views share pages, coming back where the camera was costs nothing, appending never thins what - * draws, and loading grows no faster than the data. `init`, `data`, and `load` come - * first in every scenario, and idle frames are counted after the last. + * The benchmark scenarios: the sizes each sweeps, its scene and data at each size, and its phases. + * Each phase declares how its cost may grow with size. Together the phases check that: + * + * - CPU time per frame stays flat as rows grow; + * - a one-row change costs the same at any size; + * - restyling uploads no data; + * - views share pages; + * - returning the camera to where it was packs, uploads, and evaluates nothing; + * - appending never thins what draws; + * - loading grows no faster than the data. + * + * Every scenario starts with `init`, `data`, and `load`, and idle frames are counted after its last + * phase. */ import { Shot, @@ -19,39 +26,41 @@ import * as make from './data.ts'; import { deadline, type Cost, type Stage, type Write } from './stage.ts'; export interface Scenario { - /** What it stresses. */ + /** What the scenario stresses. */ about: string; - /** What a size counts, and the sizes swept, smallest first; `--quick` runs the smallest. */ + /** What a size counts, such as rows or buses. */ unit: string; + /** The sizes swept, smallest first. `--quick` runs only the smallest. */ sizes: readonly number[]; build(size: number): Built; phases: readonly Phase[]; - /** How `load` may grow; with the data, in time and bytes, unless set. */ + /** How `load` may grow. Defaults to `LOAD`: linear in time and bytes, like the data. */ load?: Order; - /** Views of one GPU drawing the scene; one unless set. */ + /** Number of views on one GPU drawing the scene. Defaults to one. */ views?(size: number): number; - /** The GPU's budget, where the scenario is about it. */ + /** The GPU budget, for a scenario that needs its own. */ budget?: Budget; - /** Run in a fresh browser without a shader cache: what a first visit costs. */ + /** Run in a fresh browser without a shader cache, to measure a first visit. */ cold?: boolean; - /** Its data is fetched as the camera asks, so what it uploads moves with the frame a fetch lands in. */ + /** Data is fetched as the camera needs it, so uploads vary with the frame a fetch arrives in. */ fetches?: boolean; } export interface Built { scene: Scene; - /** Each dataset, made beforehand, so `data` times latkit alone. */ + /** A writer per dataset, with arrays made beforehand so `data` times latkit alone. */ data: Record; - /** Vector tiles by dataset name: URL templates the bench server answers. */ + /** Vector tile URL templates by dataset name, served by the bench server. */ tiles?: Record; - /** Where the view looks; it fits everything unless set. */ + /** The starting camera. The view fits everything if it is not set. */ camera?: Camera; } /** - * How a phase's cost may grow with size: 0 not at all, 1 no faster than linearly. `time` is a steady - * phase's CPU per frame or an action's milliseconds; `bytes` is what it uploaded, and unless - * `resident` says otherwise what stays resident. A measure left out is held only to the baselines. + * How a phase's cost may grow with size: 0 means not at all, 1 means no faster than linearly. + * `time` is a steady phase's CPU time per frame, or a once phase's milliseconds. `bytes` limits the + * bytes uploaded, and for `load` also the bytes resident unless `resident` is set. A measure left + * out is checked only against the baselines. */ export interface Order { time?: 0 | 1; @@ -67,7 +76,7 @@ export interface Phase { export const LOAD: Order = { time: 1, bytes: 1 }; export const DECADES = [1e3, 1e4, 1e5, 1e6, 2e6]; -/** Frames a steady phase acts in: past its warm-up, enough for a steady median, and no more. */ +/** Frames a steady phase acts in: enough past its warm-up for a stable median, and no more. */ export const FRAMES = 40; export const HISTORY = 1024; type MapCamera = Extract; @@ -134,7 +143,8 @@ const sheet: Scene = { // Phases. -/** Pans and zooms gently about where the views looked, a frame at a time, back to where it began. */ +/** Pans and zooms gently around the starting camera, a step each frame, looping back to the + * start. */ const pan = (order: Order = { time: 0 }): Phase => ({ name: 'pan', order, @@ -153,7 +163,7 @@ const pan = (order: Order = { time: 0 }): Phase => ({ }, }); -/** Zooms two levels in and back: labels give way, tiles refine. */ +/** Zooms two levels in and back out, which re-places labels and refines tiles. */ const zoom = (): Phase => ({ name: 'zoom', order: { time: 0 }, @@ -166,8 +176,8 @@ const zoom = (): Phase => ({ }, }); -/** Zooms two levels in, then back where it was: measured, the way back, which draws the pages it held - * and so packs, uploads, and evaluates nothing. */ +/** Zooms two levels in, then back to the start. Only the way back is measured. It draws pages that + * are still resident, so it packs, uploads, and evaluates nothing. */ const revisit = (): Phase => ({ name: 'revisit', order: { time: 0, bytes: 0 }, @@ -182,14 +192,14 @@ const revisit = (): Phase => ({ }, }); -/** Recolors through `scene`'s colormap: the GPU evaluates again; no data moves. */ +/** Recolors through `scene`'s colormap. The GPU evaluates again, but no data is uploaded. */ const restyle = (scene: Scene): Phase => ({ name: 'restyle', order: { bytes: 0 }, run: (s) => s.once(() => (s.scene = scene)), }); -/** Changes one row's value every frame: what one row costs at any size. */ +/** Changes one row's value every frame, to measure what one row costs at any size. */ const one = (data: string, table: string, name: string): Phase => ({ name: 'one row', order: { time: 0, bytes: 0 }, @@ -197,7 +207,8 @@ const one = (data: string, table: string, name: string): Phase => ({ s.steady(FRAMES, (i) => s.apply(data, (p) => p.set(table, [i], { [name]: [(i % 10) / 10] }))), }); -/** Appends a row to `table` every frame, as a feed of new rows does: what draws never thins. */ +/** Appends a row to `table` every frame, like a feed of new rows. Fails if any view draws fewer + * rows than at the start. */ const append = ( data: string, table: string, @@ -216,7 +227,7 @@ const append = ( thinned = `${now.join(', ')} drawn, ${start.join(', ')} before`; s.apply(data, (p) => p.set(table, [n + i], row(i))); }); - if (thinned) throw new Error(`appending thinned what draws: ${thinned}`); + if (thinned) throw new Error(`appending reduced the rows drawn: ${thinned}`); return cost; }, }); @@ -232,8 +243,8 @@ const every = (data: string, table: string, name: string): Phase => ({ }, }); -/** Asks what lies under the middle, as a click does: how long the answer takes. The first ask in a - * page makes the query's pipelines; later asks reuse them. */ +/** Picks what lies under the middle, as a click does, and measures how long the answer takes. The + * first pick in a page creates the query's pipelines, and later picks reuse them. */ const pick = (): Phase[] => [ { name: 'first pick', @@ -247,7 +258,8 @@ const pick = (): Phase[] => [ }, ]; -/** Moves the pointer onto a row near the middle and waits for the view to name it. */ +/** Moves the pointer onto a row near the middle and waits for the view's `hovered` event to report + * it. */ const hover = (): Phase => ({ name: 'hover', order: { time: 0, bytes: 0 }, @@ -288,7 +300,7 @@ const hover = (): Phase => ({ }, }); -/** Selects a hundred rows: the glow, and what dims; a table drawn through tiles keeps them drawn. */ +/** Selects 100 rows, which glow while the rest dim. A paged table keeps the selected rows drawn. */ const select = (data: string, table: string): Phase => ({ name: 'select', order: { time: 0 }, @@ -298,7 +310,8 @@ const select = (data: string, table: string): Phase => ({ ), }); -/** Moves every row to a new place at once, easing: CPU per frame while they fly. */ +/** Moves every row to a new position at once, easing, and measures CPU time per frame while they + * move. */ const move = (): Phase => ({ name: 'move', order: { time: 0, bytes: 1 }, @@ -308,7 +321,8 @@ const move = (): Phase => ({ }, }); -/** Records a frame of every trace each animation frame, as a live feed does, forgetting the oldest. */ +/** Appends a sample to every trace each frame, like a live feed, and forgets samples older than + * `HISTORY` frames. */ const stream = (): Phase => ({ name: 'stream', order: { time: 1, bytes: 1 }, @@ -318,7 +332,8 @@ const stream = (): Phase => ({ }, }); -/** Moves the coordinate shown across what was recorded: the playhead, and colors read at it. */ +/** Moves the views' `at` coordinate across the recorded span, which moves the playhead and the + * colors read at it. */ const scrub = (): Phase => ({ name: 'scrub', order: { time: 0, bytes: 0 }, @@ -330,8 +345,8 @@ const scrub = (): Phase => ({ }, }); -/** Narrows the window shown from everything to its last tenth: levels of detail give way, each - * trace's finer one uploading as it first draws. */ +/** Narrows the visible window from the whole span to its last tenth. Each trace switches to finer + * levels of detail, uploading each level as it first draws. */ const narrow = (order: Order = { time: 0, bytes: 0 }): Phase => ({ name: 'narrow', order, @@ -343,7 +358,8 @@ const narrow = (order: Order = { time: 0, bytes: 0 }): Phase => ({ }, }); -/** Renames a block every frame, so it resizes and its tree routes again: an edit at any size. */ +/** Renames a block every frame, which resizes it and routes its tree again, to measure an edit at + * any size. */ const edit = (): Phase => ({ name: 'edit', order: { time: 0, bytes: 0 }, @@ -360,7 +376,7 @@ const arrange = (): Phase => ({ run: (s) => s.once(() => s.view.arrange()), }); -/** A shot of every view side by side, the scenario's size in pixels, at 16:9. */ +/** A shot of every view side by side, at 16:9, with about the scenario's size in pixels. */ function shot(s: Stage) { const width = Math.round(Math.sqrt((s.size * 16) / 9) / 4) * 4; const height = Math.round((width * 9) / 16 / 2) * 2; @@ -384,7 +400,8 @@ const png = (): Phase => ({ }, }); -/** Thirty frames of video, turning; held, nothing moves, so frames after the first cost nothing. */ +/** Records 30 frames of video while the camera turns. Unless `moving`, the camera holds still, so + * frames after the first need no new drawing. */ const video = (moving: boolean): Phase => ({ name: moving ? 'video' : 'held', order: moving ? { time: 1 } : {}, @@ -458,10 +475,10 @@ export const scenarios: Record = { ], }, labels: { - about: 'every vertex labeled: placement, collisions, and the texts set', + about: 'every vertex labeled: placement, collisions, and text layout', unit: 'rows', - // From ten thousand rows on, more labels show than a frame sets texts for, so a frame's CPU - // holds at the text budget however many rows are labeled. + // From 10,000 rows on, more labels show than a frame sets texts for, so a frame's CPU time + // stays at the text budget however many rows are labeled. sizes: [1e4, 1e5, 1e6], build: (n) => ({ scene: dots('plane', 'viridis', 'name'), @@ -509,7 +526,7 @@ export const scenarios: Record = { phases: [pan(), zoom(), one('m', 'Region', 'v'), ...pick()], }, tiles: { - about: 'vector tiles fetched as the camera asks for them, from a local server', + about: 'vector tiles fetched from a local server as the camera needs them', unit: 'squares a tile', sizes: [16, 256, 4096], fetches: true, @@ -537,7 +554,7 @@ export const scenarios: Record = { unit: 'traces', sizes: [10, 1e2, 1e3, 1e4], build: (n) => ({ scene: plot, data: { t: make.traces(n, HISTORY) } }), - // Every trace narrowed uploads its finer level. + // Narrowing uploads each trace's finer level, so its bytes grow with the traces. phases: [stream(), scrub(), narrow({ time: 0, bytes: 1 }), hover()], }, history: { @@ -559,16 +576,16 @@ export const scenarios: Record = { unit: 'views', sizes: [1, 2, 4, 8], views: (n) => n, - // Room for eight views' own rows, which the views' shares of a budget hold together. What each - // view keeps per row is its own, so what stays resident grows with the views; the pages are - // shared, so what uploads does not. + // A budget large enough that the views' shares of it fit the per-row data of eight views. + // Each view keeps its own per-row data, so resident bytes grow with the views. The pages are + // shared, so uploads do not. budget: { bytes: 1024 * 2 ** 20, uploadPerFrame: 32 * 2 ** 20 }, load: { time: 1, bytes: 0, resident: 1 }, build: () => ({ scene: grid('plane'), data: { g: make.lattice(1e5) } }), phases: [pan({ time: 1, bytes: 0 })], }, budget: { - about: 'a 16 MiB budget: what draws fits, through tiles, and nothing fails', + about: 'a 16 MiB budget: paged tables fit, and nothing fails', unit: 'buses', sizes: [1e5, 1e6, 2e6], budget: { bytes: 16 * 2 ** 20, uploadPerFrame: 4 * 2 ** 20 }, diff --git a/benchmark/page/stage.ts b/benchmark/page/stage.ts index f0c9f60d..390080e6 100644 --- a/benchmark/page/stage.ts +++ b/benchmark/page/stage.ts @@ -1,12 +1,15 @@ /** - * Where a run happens, and the one way anything in it is measured. The stage steps every frame - * itself, a sixtieth of a second apart on its own clock, and each frame waits for the GPU (its work - * done, its readbacks landed, the pipelines asked for compiled), never for time: a run draws the same - * frames however fast the device is, so what it counts repeats. A phase is an action, timed from when - * it starts until everything settled: no view wants a frame, every view is complete, and no tile is - * loading; work under way that wakes no frame is waited for as it lands. A steady phase acts before - * each of a number of frames, so each act shows in its own frame, and its CPU and GPU time are per - * frame. A lost device fails the phase at once, and every wait races a deadline. + * The stage a run happens on, and the single way anything in a run is measured. + * + * The stage steps every frame itself, 1/60 s apart on its own clock. Each frame waits for the GPU + * (its work done, its readbacks back, and the requested pipelines compiled), never for time. So a + * run draws the same frames however fast the device is, and its work counts repeat. + * + * A phase is an action, timed from its start until everything settles: no view wants a frame, + * every view is complete, and no tile is loading. Work in progress that wakes no frame is awaited + * until it finishes. A steady phase acts before each of a set number of frames, so each action + * shows in its own frame, and its CPU and GPU times are per frame. A lost device fails the phase at + * once, and every wait races a deadline. */ import { Data, @@ -22,42 +25,44 @@ import { /** Writes a dataset's rows into a patch, from arrays made beforehand. */ export type Write = (patch: Patch) => Patch; -/** What a phase cost. */ +/** The cost of a phase. */ export interface Cost { /** Milliseconds from the action until everything settled. */ ms: number; - /** Frames drawn meanwhile. */ + /** Frames drawn in that time. */ frames: number; - /** Milliseconds every view together took to record a frame on the CPU: median, 95th percentile. */ + /** CPU milliseconds all views together took to record a frame: the median here, and the 95th + * percentile in `cpu95`. */ cpu: number; cpu95: number; - /** Milliseconds a frame took on the GPU, median; 0 where the device cannot measure. */ + /** Median GPU milliseconds per frame, or 0 where the device cannot measure it. */ gpu: number; - /** What the GPU did until everything settled. */ + /** The GPU work done until everything settled. */ work: Work; - /** Bytes of data pages on the GPU afterwards. */ + /** Bytes resident on the GPU afterwards: data pages, and each view's per-row data. */ resident: number; - /** Whether it acted every frame, so `cpu` is its time; else `ms` is. */ + /** Whether the phase acted every frame. If so, `cpu` is its time; otherwise `ms` is. */ steady: boolean; } -/** Milliseconds between stepped frames: a display's sixtieth of a second. */ +/** Milliseconds between stepped frames: 1/60 s, as on a 60 Hz display. */ const FRAME = 1000 / 60; -/** The most frames a phase may take to settle: ten seconds of a display's frames. */ +/** Maximum frames a phase may take to settle: 10 s at 60 frames a second. */ const MOST = 600; -/** Frames stepped once everything settled, in which nothing should draw. */ +/** Frames stepped after everything settled, in which nothing should draw. */ const IDLE = 10; -/** Milliseconds a frame, or work landing, may take: past it, something hangs. */ +/** Maximum milliseconds a frame, or a wait for work to finish, may take. Longer means a hang. */ const DEADLINE = 30_000; -/** The longest a phase may take; the runner grows sizes within it. */ +/** Milliseconds a phase should take at most. The runner stops growing a scenario's size once a + * phase would exceed it. */ export const LIMIT = 10_000; -/** Frames a steady phase takes before it measures. */ +/** Frames a steady phase runs before it starts measuring. */ export const WARM = 10; const quantile = (xs: number[], q: number) => xs.length ? [...xs].sort((a, b) => a - b)[Math.floor(q * (xs.length - 1))]! : 0; -/** `work`, or an error naming `what` once `ms` pass. */ +/** Races `work` against a timer that rejects after `ms` with an error naming `what`. */ export function deadline(work: Promise, ms: number, what: string): Promise { let timer: ReturnType | undefined; const late = new Promise((_, reject) => { @@ -66,7 +71,7 @@ export function deadline(work: Promise, ms: number, what: string): Promise return Promise.race([work, late]).finally(() => clearTimeout(timer)); } -/** What the frames a phase stepped drew. */ +/** The frames a phase drew, and the CPU and GPU times of those measured. */ interface Drawn { frames: number; cpu: number[]; @@ -79,15 +84,16 @@ export class Stage { /** The scenario's size. */ readonly size: number; readonly views: View[] = []; - /** Errors the views told: a mark that does not fit its data fails the run. */ + /** Errors the views reported. Any error, such as a mark that does not fit its data, fails the + * run. */ readonly errors: string[] = []; readonly #data = new Map(); - /** The stepped frames' clock, milliseconds. */ + /** The clock of the stepped frames, in milliseconds. */ #now = performance.now(); constructor(gpu: Gpu, canvases: readonly HTMLCanvasElement[], size: number) { [this.gpu, this.canvases, this.size] = [gpu, canvases, size]; - // Stepped from the start, so no animation frame draws while views load either. + // Step from the start, so no animation frame draws while the views load either. gpu.step(this.#now); } @@ -116,12 +122,13 @@ export class Stage { .tables.find((t) => t.name === table)!.rows; } - /** The axis coordinates dataset `name` recorded. */ + /** The range of axis coordinates dataset `name` recorded, or [0, 1] if none. */ span(name: string): [number, number] { return this.#data.get(name)!.describe().span ?? [0, 1]; } - /** Takes each dataset in: latkit's work alone, the arrays made beforehand. */ + /** Applies each dataset to a new `Data`. The arrays are made beforehand, so this times latkit + * alone. */ ingest(data: Record): Promise { return this.once(() => { for (const [name, write] of Object.entries(data)) @@ -129,7 +136,8 @@ export class Stage { }); } - /** Opens a view on each canvas and draws `scene`: from nothing to everything drawn. */ + /** Opens a view on each canvas and draws `scene`, measuring from nothing until everything is + * drawn. */ load(scene: Scene, tiles: Record = {}, camera?: Camera): Promise { const sources = [ ...this.#data.values(), @@ -154,12 +162,13 @@ export class Stage { for (const view of this.views) view.scene = scene; } - /** Writes a patch into dataset `name`, which every view draws from the next frame. */ + /** Applies a patch to dataset `name`. Every view draws the change from the next frame. */ apply(name: string, write: Write): void { this.#data.get(name)!.apply(write(new Patch())); } - /** Sends a pointer event at `point`, CSS pixels in the first view, as a person's would arrive. */ + /** Dispatches a pointer event at `[x, y]`, in CSS pixels in the first view, as a real pointer + * would. */ pointer(type: string, [x, y]: [number, number], buttons = 0): void { const canvas = this.canvases[0]!; const box = canvas.getBoundingClientRect(); @@ -176,7 +185,7 @@ export class Stage { ); } - /** Acts once, and measures until everything settled. */ + /** Performs `act` once and measures until everything settles. */ once(act: () => unknown): Promise { return this.#phase(false, async (drawn) => { await this.until(Promise.resolve().then(act), drawn, true); @@ -184,8 +193,9 @@ export class Stage { }); } - /** `answer`, stepping frames until it comes: what waits on a frame, as a pick or a hover told - * does, gets one, and what waits on work landing is waited for. */ + /** Steps frames until `answer` settles, then returns it. A frame is stepped whenever a view wants + * one, as a pending pick or hover event does. Otherwise it waits for work to finish. Throws if + * the device is lost, or after `MOST` frames or `DEADLINE` ms. */ async until( answer: Promise, drawn: Drawn = { frames: 0, cpu: [], gpu: [] }, @@ -211,7 +221,8 @@ export class Stage { return answer; } - /** Calls `act(i)` before each of `count` frames, and measures until everything settled. */ + /** Calls `act(i)` before each of `count` frames, and measures until everything settles. Frames + * after the first `WARM` are measured. */ steady(count: number, act: (i: number) => void): Promise { return this.#phase(true, async (drawn) => { for (let i = 0; i < count; i++) { @@ -222,7 +233,8 @@ export class Stage { }); } - /** Frames drawn over `IDLE` frames stepped once everything settled: none, for views that sleep. */ + /** Counts the frames drawn in `IDLE` frames stepped after everything settles. Views that sleep + * draw none. */ async idle(): Promise { const drawn: Drawn = { frames: 0, cpu: [], gpu: [] }; await this.#settle(drawn, false); @@ -253,8 +265,8 @@ export class Stage { }; } - /** Steps one frame, once the GPU is done with the last, and counts what the views drew in it: - * their CPU and GPU time, when `measuring`. */ + /** Steps one frame and waits for the GPU to finish it. Counts the frame if any view drew, and + * when `measuring` records the views' summed CPU and GPU times. */ async #frame(drawn: Drawn, measuring: boolean): Promise { if (this.gpu.lost) throw new Error('the GPU device was lost'); const before = this.views.map((v) => v.stats.frames); @@ -274,9 +286,9 @@ export class Stage { } } - /** Steps frames until everything settled: no view wants a frame, every view is complete, and no - * tile is loading. Work under way that wakes no frame, as a layout or a fetch, is waited for as it - * lands. What a once phase draws is `measuring`. */ + /** Steps frames until everything settles: no view wants a frame, every view is complete, and no + * tile is loading. Work in progress that wakes no frame, such as a layout or a fetch, is awaited + * until it finishes. `measuring` is true for a once phase, whose every frame is measured. */ async #settle(drawn: Drawn, measuring: boolean): Promise { await deadline(this.gpu.done(), DEADLINE, 'the GPU'); for (let frames = 0; frames < MOST;) { @@ -287,14 +299,14 @@ export class Stage { continue; } if (this.views.every((v) => v.complete && !v.stats.tilesLoading)) return; - await deadline(this.gpu.landed(), DEADLINE, `${this.#waiting()} landing`); + await deadline(this.gpu.landed(), DEADLINE, `waiting on ${this.#waiting()}`); } throw new Error( `still drawing after ${MOST} frames: waiting on ${this.#waiting() || 'nothing'}`, ); } - /** What the views wait on. */ + /** Lists what the views are waiting on, including tiles. */ #waiting(): string { const tiles = this.views.some((v) => v.stats.tilesLoading) ? ['tiles'] : []; return [...new Set(this.views.flatMap((v) => v.waiting)), ...tiles].join(', '); diff --git a/benchmark/report.ts b/benchmark/report.ts index f8fe7101..ee45fefc 100644 --- a/benchmark/report.ts +++ b/benchmark/report.ts @@ -1,6 +1,6 @@ /** - * The characterization: per scenario, a row per phase and measure, a column per size, and the - * exponent each grows by. + * Formats the results as a text table: per scenario, a row per phase and measure, a column per + * size, and each row's growth exponent. */ import type { Listed } from './page/main.ts'; import type { Summary, Verdict } from './judge.ts'; @@ -11,7 +11,7 @@ const bytes = (v: number) => const count = (n: number) => n >= 1e6 ? `${+(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${+(n / 1e3).toFixed(1)}k` : `${n}`; -/** Each scenario's phases at each size, and the exponent each grows by. */ +/** Formats each scenario's phases at each size, with each measure's growth exponent. */ export function report(list: Listed[], { summaries, exponents }: Verdict): string { const out: string[] = []; for (const scenario of list) { diff --git a/benchmark/run.ts b/benchmark/run.ts index 9a6ce1f1..31d505d0 100644 --- a/benchmark/run.ts +++ b/benchmark/run.ts @@ -1,19 +1,22 @@ /** - * The benchmark: each scenario up its sizes, each run in a fresh page, judged and reported. + * The benchmark runner. It runs each scenario at increasing sizes, each run in a fresh page, then + * judges and reports the results. * * cargo xtask bench [--quick] [--only a,b] [--repeats n] [--update] [--software] * - * --quick runs each scenario once at its smallest size, a smoke test of every phase and the work - * gate in under a minute on a GPU; how phases grow, and their times, take the full sweep. --update lays this run's - * baselines over the committed ones, keeping what it did not run; --software draws with SwiftShader - * on the CPU, as Linux always does: its work counts the same, its times only report, and no shader - * can hang the GPU. + * `--quick` runs each scenario once at its smallest size. It checks every phase and the work + * baseline in under a minute on a GPU. Growth and times are judged only in a full sweep. + * `--repeats` sets the runs per size (3 by default). `--update` merges this run's results into the + * committed baselines, keeping entries it did not run; only a full sweep on the GPU writes times. + * `--software` draws with SwiftShader on the CPU, as Linux always does. Work counts the same there, + * times are only reported, and no shader can hang the GPU. * - * A scenario climbs its sizes while the last one's phases, each at its fastest repeat and grown as - * its order says, fit a phase's limit: a stall of the machine slows one repeat, a scenario too large - * for the next size every one. A failure ends its scenario; a lost device, or a page that stopped - * answering, ends the sweep, since the next scenario would meet a resetting driver, and resets in a - * row are what wedge one. Every wait races a deadline. + * A scenario moves to its next size only while every phase's fastest repeat at the last size, + * scaled up by the phase's order, stays within `LIMIT`. The fastest repeat is used because a + * machine stall slows one repeat, while a scenario too large for the next size slows every one. A + * failure ends its scenario. A lost device or a page that stops responding ends the sweep, since + * the next scenario would meet a resetting driver, and repeated resets are what wedge a driver. + * Every wait races a deadline. */ import fs from 'node:fs'; import path from 'node:path'; @@ -43,17 +46,18 @@ const software = flags.software || process.platform === 'linux'; const here = path.dirname(fileURLToPath(import.meta.url)); const baselines = path.join(here, 'baselines'); const results = path.resolve(here, '../target/bench'); -/** Timestamps, for what a frame costs the GPU. The page steps its frames, each as soon as the GPU - * finished the last, so the display's pace never enters a run. */ +/** Gives WebGPU timestamps, which measure each frame's GPU time, full precision; Chrome otherwise + * rounds them to 100 µs. The page steps each frame as soon as the GPU finishes the last, so the + * display's frame rate never affects a run. */ const FLAGS = ['--enable-webgpu-developer-features']; -/** The same page on every machine, so what it counts compares, and small enough that SwiftShader - * draws a frame quickly. */ +/** The page size. It is the same on every machine so work counts compare, and small enough that + * SwiftShader draws a frame quickly. */ const PAGE = { viewport: { width: 800, height: 500 }, deviceScaleFactor: 2 }; -/** Milliseconds a page may say nothing: each phase says when it starts, then acts and settles - * within its limits. */ +/** Maximum milliseconds between a page's phase announcements. Each phase announces its start, then + * acts and settles within its own limits. */ const QUIET = 2 * LIMIT + 20_000; -/** A failed run, and whether the sweep stops there. */ +/** A failed run. A `fatal` failure stops the sweep. */ class Failure extends Error { readonly fatal: boolean; constructor(message: string, fatal: boolean) { @@ -62,20 +66,22 @@ class Failure extends Error { } } -/** `work`, or an error once the page has said nothing for `QUIET` ms since `heard()`. */ +/** Resolves with `work`, or rejects once more than `QUIET` ms have passed since `heard()`, the time + * the page last announced a phase. */ function watched(work: Promise, heard: () => number, what: string): Promise { let timer: NodeJS.Timeout | undefined; const silent = new Promise((_, reject) => { timer = setInterval(() => { if (performance.now() - heard() > QUIET) - reject(new Error(`${what} said nothing for ${QUIET} ms`)); + reject(new Error(`${what} reported no phase for ${QUIET} ms`)); }, 1_000); }); return Promise.race([work, silent]).finally(() => clearInterval(timer)); } -/** The first phase of the repeats `last` at one size that, at its fastest and grown to `size` as its - * order says (linearly where it says nothing), would pass a phase's limit, and its fastest. */ +/** Finds the first phase that would exceed `LIMIT` at `size`. Each phase's fastest time among the + * repeats `last`, all at one size, is scaled to `size` by its time order, linearly if it has none. + * Returns that phase and its fastest time. */ function outgrown( last: Run[], size: number, @@ -104,8 +110,8 @@ const failures: string[] = []; const notes: string[] = []; let list: Listed[] = []; -/** One run of `scenario` at `size`: in a fresh page of `warm`, or, for a cold scenario, of a fresh - * Chrome without a shader cache. */ +/** Runs `scenario` once at `size` in a fresh page of `warm`. A cold scenario runs in a fresh Chrome + * without a shader cache instead. */ async function measure(warm: Chrome, scenario: Listed, size: number): Promise { if (!scenario.cold) return visit(warm, scenario, size); const cold = await launch([...FLAGS, '--disable-gpu-shader-disk-cache'], software); @@ -116,8 +122,8 @@ async function measure(warm: Chrome, scenario: Listed, size: number): Promise { const page = await deadline(chrome.browser.newPage(PAGE), STEP, 'open a page'); const errors: string[] = []; @@ -141,7 +147,7 @@ async function visit(chrome: Chrome, scenario: Listed, size: number): Promise window.bench.lost()), 5_000, - 'ask after the device', + 'check whether the device was lost', ).catch(() => true); throw new Failure( `${scenario.name} at ${size}, ${phase}: ${String(error).split('\n')[0]}`, @@ -156,11 +162,11 @@ const warm = await launch(FLAGS, software); try { const page = await deadline(warm.browser.newPage(PAGE), STEP, 'open a page'); await deadline(page.goto(url), STEP, 'open the bench page'); - // Making a device is where a wedged driver hangs, so the sweep starts only once one is made. + // A wedged driver hangs when a device is made, so the sweep starts only after one is made. await deadline( page.evaluate(() => window.bench.ready()), 15_000, - 'make a GPU device: a wedged driver hangs here, and needs a reset before benchmarking', + 'create a GPU device (a hung driver blocks here; reset it before benchmarking)', ); list = await deadline( page.evaluate(() => window.bench.list()), @@ -176,7 +182,8 @@ try { for (const scenario of list.filter((s) => !only || only.includes(s.name))) { const sizes = flags.quick ? scenario.sizes.slice(0, 1) : scenario.sizes; try { - // A first run fills the shader cache, so the measured ones load warm; `start` alone loads cold. + // An unmeasured first run fills the shader cache, so the measured runs load warm. Only cold + // scenarios (`start`) load cold. if (!scenario.cold && !flags.quick) await measure(warm, scenario, sizes[0]!); let last: Run[] = []; for (const size of sizes) { @@ -205,7 +212,7 @@ try { console.log(` ${failure.message.slice(0, 300)}`); if (failure.fatal) { failures.push( - 'the GPU lost its device or stopped answering, so the sweep stops: reset the driver before running again', + 'the GPU device was lost or the page stopped responding, so the sweep stopped. Reset the driver before running again', ); break; } @@ -223,7 +230,7 @@ const write = (file: string, value: unknown) => const machines = [...new Set(runs.map((r) => r.machine))]; if (machines.length > 1) failures.push( - `runs drew on different GPUs, so their times cannot compare: ${machines.join('; ')}`, + `runs used different GPUs, so their times are not comparable: ${machines.join('; ')}`, ); if (!runs.length) failures.push('nothing ran'); const machine = machines[0] ?? 'unknown'; @@ -233,10 +240,12 @@ const slug = machine .replace(/^-|-$/g, ''); const workFile = path.join(baselines, 'work.json'); const timesFile = path.join(baselines, `${slug}.json`); -// A quick run, once each without a warm-up, holds no times to the baselines its warm repeats took. +// Times are held to a baseline, and written to one, only by a full sweep on the GPU. A quick run +// makes one run per scenario without a warm-up, and SwiftShader's times measure the CPU. +const timed = !flags.quick && !software; const verdict = judge(runs, list, { work: read(workFile), - times: flags.quick ? undefined : read(timesFile), + times: timed ? read(timesFile) : undefined, }); console.log(`\n${machine}`); console.log(report(list, verdict)); @@ -244,15 +253,15 @@ if (runs.length) { fs.mkdirSync(results, { recursive: true }); write(path.join(results, `${Date.now()}.json`), { machine, runs }); } -// Baselines come only from runs that all completed without errors; what they measured, slower or -// faster, is what --update accepts. +// Baselines are written only when every run completed without errors. --update accepts whatever +// they measured, slower or faster. if (flags.update) { if (failures.length || runs.some((r) => r.errors.length)) { failures.push('baselines not written: a run failed or reported errors'); } else { fs.mkdirSync(baselines, { recursive: true }); write(workFile, work(verdict, read(workFile))); - write(timesFile, times(verdict, machine, read(timesFile))); + if (timed) write(timesFile, times(verdict, machine, read(timesFile))); } } console.log(''); diff --git a/benchmark/tiles.ts b/benchmark/tiles.ts index eeb52baa..98b402bb 100644 --- a/benchmark/tiles.ts +++ b/benchmark/tiles.ts @@ -1,7 +1,7 @@ /** - * Vector tiles made on request, so the tiles scenario fetches over HTTP as a map does, the same - * bytes every run and nothing from the network: `/tiles////.mvt`, one layer - * `cells` of that many squares in a grid over the tile. + * A Vite plugin that serves vector tiles made on request, so the tiles scenario fetches over HTTP + * as a map does, with the same bytes every run and no network. `/tiles////.mvt` + * returns one layer, `cells`, of that many squares in a grid over the tile. */ import type { Plugin } from 'vite'; @@ -32,7 +32,8 @@ const bytes = (field: number, payload: number[]) => [ ...payload, ]; -/** A tile of `squares` squares, clockwise exterior rings in the tile's 4096 units. */ +/** Encodes a tile of `squares` squares as exterior rings, clockwise in the tile's 4096-unit extent + * (y down). */ function tile(squares: number): Uint8Array { const side = Math.ceil(Math.sqrt(squares)); const step = Math.floor(4096 / side); diff --git a/benchmark/vite.config.ts b/benchmark/vite.config.ts index b76f2314..02acda61 100644 --- a/benchmark/vite.config.ts +++ b/benchmark/vite.config.ts @@ -3,7 +3,7 @@ import { tiles } from './tiles.ts'; export default defineConfig({ plugins: [tiles()], - // The package loads its wasm next to itself; prebundling would move it. + // latkit loads its wasm from beside its own module, which prebundling would move. optimizeDeps: { exclude: ['latkit'] }, build: { target: 'es2022' }, }); diff --git a/crates/latkit-cli/README.md b/crates/latkit-cli/README.md index 7aca9443..260ea8c6 100644 --- a/crates/latkit-cli/README.md +++ b/crates/latkit-cli/README.md @@ -1,8 +1,8 @@ # latkit CLI -Draws latkit scenes of datasets into images, image sequences, raw frames, and videos, on the -machine's GPU, with the same renderer as the browser package. Inputs are latkit's JSON, so any -language can write them. The package is `latkit-cli`; the executable is `latkit`. +Renders latkit scenes to images, image sequences, raw frames, and video on the machine's GPU, with +the same renderer as the browser package. Inputs are latkit's JSON, so any language can write them. +The package is `latkit-cli`; the executable is `latkit`. ```sh cargo install latkit-cli @@ -14,8 +14,9 @@ latkit tile history.bin -o history/ # time tiles a view fetches in spans: --ti latkit schema scene > scene.schema.json ``` -Commands, inputs, frames, outputs, and diagnostics are in -[the documentation](https://latkit.readthedocs.io/en/latest/cli.html), and -[its examples](https://latkit.readthedocs.io/en/latest/examples/) are drawn by this CLI. +[The documentation](https://latkit.readthedocs.io/en/latest/cli.html) covers commands, inputs, +frames, outputs, and diagnostics, and this CLI draws all of +[its examples](https://latkit.readthedocs.io/en/latest/examples/). [The 70,000-bus fault](examples/bus-fault/README.md) and -[the WECC240 fault](examples/wecc240-fault/README.md) are made with Python and this CLI alone. +[the WECC240 fault](examples/wecc240-fault/README.md) videos are made with Python and this CLI +alone. diff --git a/crates/latkit-cli/examples/branch-opening-orbit/README.md b/crates/latkit-cli/examples/branch-opening-orbit/README.md index fcbb95d1..f8367fe7 100644 --- a/crates/latkit-cli/examples/branch-opening-orbit/README.md +++ b/crates/latkit-cli/examples/branch-opening-orbit/README.md @@ -3,15 +3,15 @@ [Preview](preview.png) · The video is not in Git: [reproduce it](#reproduce) to write `animation.mp4` here. -A close 108° orbit around the network, starting farther toward the East Coast -and stopping before the far-western viewpoint. Magnification stays nearly -constant, with a higher viewing angle throughout and no late pullback or dip. -Centering and rotation change smoothly along the shorter arc. +A close orbit around the network: the bearing turns 108°, from −70° to +38°, while the center +drifts west from 84.5° W to 87° W at 37.5° N. Zoom stays nearly constant and pitch stays between +46° and 50°, so the view never pulls back or dips. Center and bearing change smoothly between +keyframes. The saved simulation and visual scales come from the [original example](../branch-opening/README.md): 70,000 buses, the Little Rock–Newark 765 kV line opening at 1 s, 0.4× playback, reduced height, no plot, 1920 × 1080 at 60 fps. The video is 23.517 seconds (1,411 frames). -The original example and its video remain separate. +Rendering the orbit leaves the original example and its video untouched. ## Reproduce @@ -22,27 +22,27 @@ python crates/latkit-cli/examples/branch-opening-orbit/render.py --cli target/cl python crates/latkit-cli/examples/branch-opening-orbit/render.py --cli target/cli/latkit.exe ``` -Python prepares the documents; Latkit draws and encodes the video. The shared -[data recipe](../branch-opening/render.py) and state boundaries are read from the -original example. Generated inputs go into `target/branch-opening-orbit/`. -No simulation rerun, duplicated frequency recording, or full image sequence is needed. +Python writes the CLI inputs; the latkit CLI draws and encodes the video. The +[data recipe](../branch-opening/render.py) and state boundaries come from the +original example. Generated inputs go to `target/branch-opening-orbit/`. +Nothing is simulated again, the frequency recording is not copied, and no image sequence is written. Edit [camera.json](camera.json) to change the orbit. `at` is simulation time in seconds, `center` is longitude/latitude, `scale` is the plane's pixels per coordinate unit, and `pitch`/`bearing` are degrees. -The recipe writes native CLI `camera` values into every frame. Monotone cubic Hermite -interpolation gives continuous camera velocity without overshooting keyframes; -zoom interpolates in logarithmic space, with gentle starts and stops. +The recipe writes a `camera` value into every line of the CLI's frames file. Keyframes are +interpolated with monotone cubic Hermite splines, so camera velocity is continuous, no value +overshoots a keyframe, and the path starts and stops gently. Zoom is interpolated in log space. -`--preview --at 5.8` shows the matching camera at that simulation time. `--prepare-only` -writes the inputs and prints the complete CLI command. `--source`, `--work`, `--output`, -and `--limit` support alternate data, scratch directories, and short previews. +`--preview --at 5.8` renders the frame at that simulation time, with its camera. `--prepare-only` +writes the inputs and prints the complete CLI command. `--source`, `--work`, and `--output` choose +other data, scratch, and output directories; `--limit N` renders only the first N frames. Each output's JSON records its camera path, source checksum, timing, and render command. -Frequency and height retain the original example's interpretation: a filtered -voltage-angle frequency estimate and an exaggerated, nonlinear height encoding. -Close framing favors the network and response over peripheral state boundaries. +Frequency and height mean the same as in the original example: frequency is estimated from +filtered voltage angles, and height exaggerates it on a nonlinear scale. The close framing shows +the network's response rather than the outer state boundaries. -The delivered video was decoded end to end (1,411 frames), with sample -brackets checked at every frame and framing inspected around the opening and -along the camera path. +The delivered video was decoded end to end (1,411 frames). The samples bracketing each frame's time +were checked at every frame, and the framing was inspected around the opening and along the camera +path. diff --git a/crates/latkit-cli/examples/branch-opening/README.md b/crates/latkit-cli/examples/branch-opening/README.md index 45b260e5..e87d1b6c 100644 --- a/crates/latkit-cli/examples/branch-opening/README.md +++ b/crates/latkit-cli/examples/branch-opening/README.md @@ -1,18 +1,18 @@ -# ACTIVSg70k line opening — Python + Latkit CLI +# ACTIVSg70k line opening — Python + latkit CLI [Preview](preview.png) · The video is not in Git: [render the saved study](#render-the-saved-study) to write `animation.mp4` here. -The synthetic 70,000-bus network responds to the Little Rock–Newark 765 kV line -(`branch_59757_59774_1`) opening at 1 s. It stays open. The video shows simulation +The synthetic 70,000-bus network responds as the Little Rock–Newark 765 kV line +(`branch_59757_59774_1`) opens at 1 s and stays open. The video shows simulation time 0.6–10 s at a constant 0.4× speed: **1,411 frames, 60 fps, 1920 × 1080, -23.517 seconds**. The map fills the frame, and a dashed gold line marks the disconnected -branch. A gentle 30° camera sweep reveals the response from changing angles. +23.517 seconds**. The map fills the frame, and a dashed gold line marks the open +branch. The camera's bearing sweeps 30° to show the response from changing angles. -[`render.py`](render.py) uses Python's standard library and the Latkit CLI. Latkit +[`render.py`](render.py) uses only Python's standard library and the latkit CLI. latkit draws the network, state boundaries, labels, and frequency legend, and encodes H.264 -through its native Windows MP4 output. There is no Rust consumer, browser renderer, -Python imaging dependency, or external encoder in this recipe. +with its native Windows MP4 output. The recipe needs no Rust code, browser, Python +imaging library, or external encoder. ## Render the saved study @@ -25,13 +25,13 @@ python crates/latkit-cli/examples/branch-opening/render.py --cli target/cli/latk ``` An installed CLI works too: `python crates/latkit-cli/examples/branch-opening/render.py`. -Use `--preview --at 2.5` for a different still, `--limit 120 --output target/preview` +Use `--preview --at 2.5` for a still at another time, `--limit 120 --output target/preview` for a short movie, or `--prepare-only` to write the inputs and print the complete -CLI command. Scene and camera JSON are generated in `target/branch-opening/render/`; -edit those and run the printed CLI command directly to experiment with the view. -A recipe rerun regenerates its inputs. +CLI command. The scene and camera JSON are written to `target/branch-opening/render/`; +to experiment with the view, edit them and run the printed CLI command directly. +Rerunning the recipe overwrites them. -The optional positional argument selects another saved-study directory. Its files are: +The optional positional argument selects another saved-study directory, which holds: ```text target/branch-opening/ @@ -42,8 +42,8 @@ target/branch-opening/ frequency.f32 ``` -The recording is local, ignored data; it is **not included in Git**. The small state -boundary asset is reused from the earlier bus-fault recording and included here. +The recording is local data, **not included in Git**. The small state-boundary file, +`states.geojson`, comes from the bus-fault recording and is included here. `animation.json` and `preview.json` record the command, source checksum, scales, dimensions, and render timing. Native MP4 output currently requires Windows. @@ -53,7 +53,7 @@ The study used `ghcr.io/lukelowry/gridkit:develop-perf`, image ID `sha256:befba9e16edce341888e73c05b9d2c35aedf1357fa18b54c56114dbe25ccd565`, and the original ACTIVSg70k case, SHA-256 `8c3d1edd9deaf409436b1f38e55f41c963d08eae7709474bcfb2c1dffadbdb15`. -The full simulation manifest remains beside the recording. +The full simulation manifest is stored beside the recording. Only bus voltage angles were monitored, at 60 Hz over 0–10 s, with both sides of the event retained. Frequency is the causal 20 ms filtered derivative of unwrapped @@ -63,22 +63,22 @@ The switching angle jump produces a brief impulse (about −1.647 to +0.683 Hz). Colors saturate at ±0.20 Hz. Signed height is exaggerated by `height_scale * asinh(df / 0.02) / asinh(0.35 / 0.02)`; it is a visual encoding of -frequency, not physical displacement. The height gain is 0.45 of the earlier view, -reducing displacement by 55% while preserving its sign and nonlinear scale. The +frequency, not physical displacement. `height_scale` includes a gain of 0.45, which +scales every height down by 55% while keeping its sign and nonlinear shape. The camera keeps a fixed center, zoom, and 48° pitch, with bearing easing from −10° to +20° over the full recording. This keeps the initial peak in view and makes the geography easier to follow. There is no bottom plot or trace legend. `HEIGHT_GAIN` and `BEARINGS` near the top of `render.py` control the exaggeration -and camera sweep. The camera path uses simulation time, so an arbitrary preview +and camera sweep. The camera path follows simulation time, so a preview at any time matches that moment in the movie. -For slow motion, Latkit interpolates between the original samples and computes the -height expression on the GPU. The recipe appends only samples needed for the next -picture and forgets older ones. Its timestamp comparison exactly matches Latkit's: -the retained lower sample must be **at or before** the requested time. Both event -sides remain distinct, so post-opening data never leaks into pre-opening pictures. -No dense interpolated cache, height cache, or full image sequence is written. +For slow motion, latkit interpolates between the recorded samples and computes the +height expression on the GPU. The recipe appends only the samples the next picture +needs and forgets older ones. It selects samples with the same comparison latkit uses: +the lower sample must be **at or before** the requested time. The samples on either +side of the event stay distinct, so no picture before the opening shows data from after it. +The recipe writes no interpolated or height cache and no image sequence. The CLI validates the generated documents before rendering. The delivered MP4 was also decoded end to end and checked around the event and sample boundaries. diff --git a/crates/latkit-cli/examples/bus-fault/README.md b/crates/latkit-cli/examples/bus-fault/README.md index 844d6bf6..6c3b1435 100644 --- a/crates/latkit-cli/examples/bus-fault/README.md +++ b/crates/latkit-cli/examples/bus-fault/README.md @@ -3,14 +3,16 @@ [Preview](preview.png) · The animation is not in Git: [`render.py`](render.py), run as below, writes `animation.mp4` here. -The saved ACTIVSg70k recording: simulation 0.6–1.4 s at 0.1×, then 1.4–10 s at 1×, 997 frames at -60 fps and 1920 × 1080. The fault is on bus 56923 at 1 s and clears at 1.15 s. Every bus keeps its -frequency-dependent color and signed height; a plot below traces six buses with a playhead. +The saved ACTIVSg70k fault recording: simulation time 0.6–1.4 s at 0.1× speed, then 1.4–10 s at +1×, as 997 frames at 60 fps and 1920 × 1080. The fault is on bus 56923 at 1 s and clears at 1.15 s. +Each bus is colored and raised or lowered by its frequency deviation; a plot below traces six buses, +with a playhead. [`render.py`](render.py) uses only Python's standard library. It writes the grid and the traced -buses as latkit JSON, whose frame columns are slices of the saved float32 caches, and a frame per -output frame that appends them. latkit reads the states' GeoJSON, draws the map with its title and -legend over the plot, and encodes the MP4: 5.5 s for the 997 frames on an RTX 2080 SUPER. +buses as latkit JSON, whose frame columns are slices of the saved float32 caches, and one line per +output frame that appends that frame's samples. latkit reads the states' GeoJSON, draws the map +with its title and legend above the plot, and encodes the MP4: 5.5 s for all 997 frames on an RTX +2080 SUPER. ```sh python crates/latkit-cli/examples/bus-fault/render.py --cli target/cli/latkit --preview @@ -18,11 +20,11 @@ python crates/latkit-cli/examples/bus-fault/render.py --cli target/cli ``` `` is the recording's directory: `buses.json`, `topology.json`, `usa-states.geojson`, -`depth-metadata.json`, and the frame-major caches `depth-frequency.f32` and `depth-height.f32` in -the metadata's bus order. They are not in this repository. Frequency comes from a causal 20 ms +`depth-metadata.json`, and the frame-major caches `depth-frequency.f32` and `depth-height.f32`, in +the metadata's bus order. These files are not in this repository. Frequency is a causal 20 ms filtered derivative of bus voltage angle; height is `asinh(df / 0.02) / asinh(0.35 / 0.02)`; colors -clip to ±0.05 Hz. +saturate at ±0.05 Hz. -The recipe writes its JSON to `target/bus-fault/` and keeps existing scenes and cameras there, so -edits survive a rerun. `--prepare-only` prints the command without rendering; `--limit N` renders the -first N frames. `animation.json` and `preview.json` record the command and timings. +The recipe writes its JSON to `target/bus-fault/` and keeps any scenes and cameras already there, so +your edits survive a rerun. `--prepare-only` prints the command without rendering; `--limit N` +renders only the first N frames. `animation.json` and `preview.json` record the command and timings. diff --git a/crates/latkit-cli/examples/wecc240-fault/README.md b/crates/latkit-cli/examples/wecc240-fault/README.md index bc4cbecd..158f51dc 100644 --- a/crates/latkit-cli/examples/wecc240-fault/README.md +++ b/crates/latkit-cli/examples/wecc240-fault/README.md @@ -1,4 +1,4 @@ -# WECC240 fault — GridKit + Latkit CLI +# WECC240 fault — GridKit + latkit CLI [Preview](preview.png) · The video is not in Git: [reproduce it](#reproduce) to write `animation.mp4` here. @@ -6,12 +6,12 @@ The reference WECC240 fault is applied at **John Day, bus 4005**, at 1.00 s and cleared at 1.15 s, with fault impedance R = 0, X = 0.01 p.u. The model contains 243 buses, 447 branches, 103 synchronous machines, and 37 inverter groups. -GridKit simulates 0–10 s; Latkit renders **1920 × 1080, 60 fps, 1,561 frames** +GridKit simulates 0–10 s; latkit renders **1920 × 1080, 60 fps, 1,561 frames** into a 26.017-second H.264 MP4. Playback slows to 0.1× around the fault and runs at 0.5× elsewhere. -The preview, case, compact recordings, and reproduction scripts are included in -Git. Intermediate CSV and generated CLI inputs stay in `target/`. +The preview, case, compact recordings, and reproduction scripts are in Git. +Intermediate CSV files and generated CLI inputs stay in `target/`. ## Reproduce @@ -25,13 +25,13 @@ python crates/latkit-cli/examples/wecc240-fault/render.py ``` Skip the first command to render the included recording without Docker. -`render.py` finds `target/cli/latkit.exe` (or `target/cli/latkit`), then falls back -to an installed `latkit`. Use `--cli PATH` to select another executable, +`render.py` uses `target/cli/latkit.exe` (or `target/cli/latkit`) if present, and otherwise +an installed `latkit`. Use `--cli PATH` to select another executable, `--preview --at 1.12` for a still, or `--limit 120 --output target/wecc-preview` -for a short movie. `--prepare-only` writes scene/data/camera/frame inputs under +for a short movie. `--prepare-only` writes the scene, data, camera, and frame inputs under `target/wecc240-fault/render/` and prints the CLI command. Native MP4 encoding currently requires Windows; the Python recipes use only -the standard library. State boundaries reuse [`../branch-opening/states.geojson`](../branch-opening/states.geojson). +the standard library. The state boundaries come from [`../branch-opening/states.geojson`](../branch-opening/states.geojson). ## Simulation and display @@ -42,24 +42,24 @@ sha256:befba9e16edce341888e73c05b9d2c35aedf1357fa18b54c56114dbe25ccd565 ``` `simulate.py` resolves the requested tag to its local image ID before running, -with two CPUs and networking disabled. To reproduce with this exact image, pass -that ID through `--image`. The saved [metadata](recording/metadata.json) includes +and runs it with two CPUs and networking disabled. To reproduce with this exact image, pass +that ID with `--image`. The saved [metadata](recording/metadata.json) includes the image labels, case hashes, solver settings, sample times, and recording hashes. -Monitors sample at 240 Hz, retaining both sides of fault application and clearing: -2,403 samples total. The map shows bus frequency deviation, estimated with a -20 ms causal filtered derivative of unwrapped voltage angle; switching angle -jumps contribute impulses. Colors saturate at ±0.1 Hz. Height exaggerates this -same quantity with an `asinh` scale; it is a visual displacement. +Monitors sample at 240 Hz and keep both sides of the fault's application and clearing: +2,403 samples in total. The map shows bus frequency deviation, estimated with a +20 ms causal filtered derivative of unwrapped voltage angle; the angle jumps at switching +cause brief impulses. Colors saturate at ±0.1 Hz. Height exaggerates the same quantity +on an `asinh` scale; it is a visual encoding, not a physical displacement. The upper plot shows actual GENROU rotor-speed deviation (`omega × 60 Hz`) for 4039_G, the machine with the largest absolute excursion. The lower plot shows faulted-bus voltage. A white playhead follows simulation time. These are distinct measurements: estimated bus frequency, rotor speed, and voltage. -Only monitor selection and monitor names change in the runtime case. +The runtime case differs from the source case only in its monitors and their names. GridKit reports initialization adjustments to small Hygov time constants and -Repca reactive limits; the complete messages are retained in [solver.txt](solver.txt). +Repca reactive limits; the complete messages are in [solver.txt](solver.txt). `animation.json` and `preview.json` record render settings, timing, and hashes. ## Source diff --git a/crates/latkit-cli/src/args.rs b/crates/latkit-cli/src/args.rs index ccf611d1..a781ff59 100644 --- a/crates/latkit-cli/src/args.rs +++ b/crates/latkit-cli/src/args.rs @@ -1,5 +1,5 @@ -//! The command line: what `render`, `validate`, `inspect`, `query`, `tile`, and `schema` take, and the -//! checks that fail as usage errors before anything is read. +//! Command-line arguments of `render`, `validate`, `inspect`, `query`, `tile`, and `schema`, and +//! the checks that report usage errors before any input is read. use std::path::{Path, PathBuf}; use std::time::Duration; @@ -10,7 +10,7 @@ use latkit::{ImageFormat, Pixels, Shot}; #[derive(Parser)] #[command(name = "latkit", version, about = "Render scenes of JSON or binary data to images and videos")] pub(crate) struct Cli { - /// Diagnostics on stderr: errors, and progress while rendering; JSON is one object a line + /// Format of errors and render progress on stderr. JSON writes one object per line #[arg(long, global = true, value_enum, default_value = "human")] pub(crate) diagnostics: Diagnostics, #[command(subcommand)] @@ -27,46 +27,46 @@ pub(crate) enum Diagnostics { pub(crate) enum Command { /// Draw scenes of datasets into an image, image sequence, raw frames, or a video Render(Job), - /// Check everything render reads, frames included, without a GPU + /// Check every input render reads, including frames, without a GPU Validate(Job), - /// Describe datasets as JSON: tables, rows, fields and their kinds, the axis and its span + /// Describe datasets as JSON: tables, row counts, fields with kinds, and the axis and its span Inspect { - /// Dataset files, each [NAME=]FILE; - reads stdin + /// Dataset files, each [NAME=]FILE (- reads stdin) #[arg(required = true, value_name = "[NAME=]FILE")] data: Vec, /// The longest a dataset may take to load, in seconds #[arg(long, default_value = "30", value_parser = deadline)] deadline: Duration, }, - /// Print the JSON Schema of what latkit reads: every input, or one of them + /// Print the JSON Schema of every input latkit reads, or of one input Schema { - /// One input's schema + /// Print only this input's schema #[arg(value_enum)] input: Option, }, - /// Read rows back as JSON Lines, an object a row: those a query keeps, sorted and paged + /// Print the rows a query selects, sorted and paged, as JSON Lines with one object per row Query { - /// The dataset, [NAME=]FILE; - reads stdin + /// The dataset, [NAME=]FILE (- reads stdin) #[arg(value_name = "[NAME=]FILE")] data: Source, - /// The query: JSON such as {"of":"Branch","filter":{"op":"gt","args":["loading",90]}}, or a - /// file of it + /// The query as JSON, such as {"of":"Branch","filter":{"op":"gt","args":["loading",90]}}, + /// or a file containing it (- reads stdin) #[arg(value_name = "QUERY")] query: String, /// The longest the dataset may take to load, in seconds #[arg(long, default_value = "30", value_parser = deadline)] deadline: Duration, }, - /// Write a dataset as time tiles: frames.json, which --tiles opens, the dataset without its - /// sampled fields' histories, and their spans at every level of detail + /// Write a dataset as time tiles: frames.json (which --tiles opens), the dataset without its + /// sampled fields' histories, and those histories in spans at every level of detail Tile { - /// The dataset, [NAME=]FILE; - reads stdin + /// The dataset, [NAME=]FILE (- reads stdin) #[arg(value_name = "[NAME=]FILE")] data: Source, - /// The directory the tiles go in + /// Directory to write the tiles to #[arg(short, long, value_name = "DIR")] output: PathBuf, - /// Entries a span holds: frames, or buckets of a level of detail; a multiple of 8 + /// Entries per span: frames at full detail, buckets at coarser levels. A multiple of 8 #[arg(long, default_value = "256")] chunk: u32, /// The longest the dataset may take to load, in seconds @@ -75,103 +75,104 @@ pub(crate) enum Command { }, } -/// What a render draws and where it goes. +/// The inputs and output of `render` and `validate`. #[derive(Args)] pub(crate) struct Job { - /// Scene JSON files, one per panel; - reads stdin + /// Scene JSON files, one per panel (- reads stdin) #[arg(required = true, value_name = "SCENE")] pub(crate) scenes: Vec, - /// A dataset: JSON document, latkit bytes, or GeoJSON (which needs a NAME); NAME= renames it + /// A dataset file: a JSON document, latkit bytes, or GeoJSON. NAME= sets its name, which + /// GeoJSON requires #[arg(short, long, value_name = "[NAME=]FILE")] pub(crate) data: Vec, - /// Tiles every view fetches as dataset NAME: time tiles' frames.json, or vector tiles' PMTiles - /// file, TileJSON, or {z}/{x}/{y} template + /// Tiles every view fetches as dataset NAME: a time tiles frames.json, or a vector tiles + /// PMTiles file, TileJSON, or {z}/{x}/{y} template #[arg(long, value_name = "NAME=FILE")] pub(crate) tiles: Vec, - /// Image file, %d pattern for an image sequence, or - for stdout + /// Output file, %d pattern for an image sequence, or - for stdout #[arg(short, long, value_name = "FILE")] pub(crate) output: Option, - /// What to write; otherwise the output's extension says, or PNG for stdout + /// Output format. Defaults to the one the output's extension names, else PNG #[arg(long, value_enum)] pub(crate) format: Option, - /// A JPEG's quality, from 1, smallest, to 100, sharpest + /// JPEG quality, from 1 (smallest) to 100 (sharpest) #[arg(long, default_value = "90", value_parser = clap::value_parser!(u8).range(1..=100))] pub(crate) quality: u8, /// The picture's width and height in pixels, such as 1920x1080 #[arg(long, default_value = "1600x900", value_parser = size)] pub(crate) size: [u32; 2], - /// Pixels per CSS pixel: views lay out size / scale CSS pixels, so 2 draws twice as sharp + /// Pixels per CSS pixel. Views lay out size / scale CSS pixels, so 2 draws twice as sharp #[arg(long, default_value = "1", value_parser = positive)] pub(crate) scale: f32, - /// A scene's panel of the picture, X,Y,WIDTH,HEIGHT as fractions; one per scene, in order + /// A scene's panel of the picture, as X,Y,WIDTH,HEIGHT fractions. Give one per scene, in order #[arg(long, value_name = "X,Y,W,H", value_parser = panel)] pub(crate) panel: Vec<[f32; 4]>, /// Keep alpha where the themes' backgrounds would be #[arg(long)] pub(crate) transparent: bool, - /// The longest a picture, or a video frame, may take, in seconds + /// The longest a picture or video frame may take, in seconds #[arg(long, default_value = "30", value_parser = deadline)] pub(crate) deadline: Duration, - /// TTF/OTF/TTC file; repeat for fallbacks; replaces system fonts + /// A TTF, OTF, or TTC font file. Repeat for fallbacks. Replaces the system fonts #[arg(long, value_name = "FILE")] pub(crate) font: Vec, - /// Camera JSON for a scene, in scene order; a view without one fits + /// A camera JSON file per scene, in scene order. Views without one use --fit #[arg(long, value_name = "FILE")] pub(crate) camera: Vec, - /// What a view without a camera frames: "all", or a fit as JSON + /// What views without a camera frame: "all", or a fit as JSON #[arg(long, default_value = "all", value_name = "FIT")] pub(crate) fit: String, - /// CSS pixels kept clear at the top, right, bottom, and left, T,R,B,L + /// CSS pixels to keep clear at the top, right, bottom, and left #[arg(long, value_name = "T,R,B,L", value_parser = insets)] pub(crate) insets: Option<[f32; 4]>, - /// The axis coordinate shown; sampled fields read their last frame at or before it + /// The axis coordinate to show. Sampled fields show their last frame at or before it #[arg(long, value_parser = finite, allow_hyphen_values = true)] pub(crate) at: Option, - /// A still's effect time in seconds, where elapsed time and flowing comets read it + /// A still's fixed effect time in seconds, for elapsed-time expressions and flowing comets #[arg(long, value_parser = seconds, conflicts_with = "frames")] pub(crate) elapsed: Option, - /// Frames as JSON Lines, one per output frame, or - for stdin + /// Frames as JSON Lines, one line per output frame (- reads stdin) #[arg(long, value_name = "FILE")] pub(crate) frames: Option, - /// Output frames a second + /// Output frames per second #[arg(long, default_value = "60", value_parser = fps)] pub(crate) fps: f64, - /// MP4 bits a second; about 0.15 bits a pixel a frame otherwise + /// MP4 bitrate in bits per second. Defaults to about 0.15 bits per pixel per frame #[arg(long, value_parser = clap::value_parser!(u32).range(1..))] pub(crate) bitrate: Option, } -/// An input `schema` describes. +/// An input whose schema `schema` prints. #[derive(Clone, Copy, PartialEq, Eq, Debug, ValueEnum)] pub(crate) enum Input { /// A scene Scene, /// A dataset's JSON document Data, - /// A patch's JSON + /// A patch Patch, - /// A query of `latkit query` + /// A query for `latkit query` Query, - /// A camera of `--camera` + /// A camera for `--camera` Camera, - /// A fit of `--fit` + /// A fit for `--fit` Fit, - /// A line of `--frames` + /// One line of a `--frames` file Frames, } -/// What an output is. +/// An output format. #[derive(Clone, Copy, PartialEq, Eq, ValueEnum)] pub(crate) enum Format { - /// PNG, lossless with alpha: one picture, or one a frame where the output has a %d pattern + /// PNG, lossless with alpha. One picture, or one per frame with a %d output pattern Png, - /// JPEG at --quality, its alpha dropped: one picture, or one a frame + /// JPEG at --quality, without alpha. One picture, or one per frame Jpeg, - /// WebP, lossless with alpha: one picture, or one a frame + /// Lossless WebP with alpha. One picture, or one per frame Webp, - /// Raw RGBA, straight alpha, frames back to back + /// Raw RGBA with straight alpha, frames back to back Rgba, - /// YUV4MPEG2: 4:2:0 BT.709 limited range, for any encoder + /// YUV4MPEG2 (4:2:0, BT.709 limited range), which any encoder reads Y4m, /// H.264 in fragmented MP4, from the platform's encoder Mp4, @@ -192,14 +193,14 @@ impl std::str::FromStr for Source { }); Ok(match named { Some((name, path)) if !path.is_empty() => Source { name: Some(name.into()), path: path.into() }, - Some(_) => return Err("NAME= names a file".into()), + Some(_) => return Err("expected a file after NAME=".into()), None => Source { name: None, path: value.into() }, }) } } impl Format { - /// The bytes its pictures compose into. + /// The pixel layout its pictures compose into. pub(crate) fn pixels(self) -> Pixels { match self { Format::Mp4 | Format::Y4m => Pixels::Nv12, @@ -207,7 +208,7 @@ impl Format { } } - /// The image file it writes, at `quality` for a JPEG; `None` for raw frames and video. + /// The image format it writes, with `quality` for JPEG. `None` for raw frames and video. pub(crate) fn image(self, quality: u8) -> Option { match self { Format::Png => Some(ImageFormat::Png), @@ -219,7 +220,7 @@ impl Format { } impl Job { - /// What the output is: `--format`, else its extension, else PNG. + /// The output format: `--format`, else the one the output's extension names, else PNG. pub(crate) fn format(&self) -> Format { self.format.unwrap_or_else(|| { let extension = self.output.as_deref().and_then(Path::extension).and_then(|e| e.to_str()); @@ -234,19 +235,20 @@ impl Job { }) } - /// The picture the job takes: its size, scale, panels, alpha, and deadline. + /// The job's shot: its size, scale, panels, transparency, and deadline. pub(crate) fn shot(&self) -> Shot { let shot = Shot::new(self.size).scale(self.scale).deadline(Some(self.deadline)).panels(self.panel.clone()); if self.transparent { shot.transparent() } else { shot } } - /// Whether the output names one file a frame: a `%d` or `%0Nd` pattern. + /// Whether the output is a `%d` or `%0Nd` pattern that names one file per frame. pub(crate) fn sequence(&self) -> bool { self.output.as_deref().is_some_and(|p| pattern(p).is_some()) } } -/// The `%d` or `%0Nd` of a path's file name, and its width. +/// The byte range of the `%d` or `%0Nd` at the last `%` in `path`, and its zero-padded width (0 +/// for `%d`). pub(crate) fn pattern(path: &Path) -> Option<(std::ops::Range, usize)> { let text = path.to_str()?; let at = text.rfind('%')?; @@ -260,8 +262,8 @@ pub(crate) fn pattern(path: &Path) -> Option<(std::ops::Range, usize)> { } impl Cli { - /// The checks clap cannot make, failing as usage errors: one reader of stdin, an output a render - /// can write, and a panel for each scene. + /// Checks clap cannot express, reported as usage errors: at most one input reads stdin, the + /// output is one a render can write, and each scene has a panel. pub(crate) fn check(&self) -> clap::error::Result<()> { let invalid = |message: &str| Self::command().error(ErrorKind::InvalidValue, message); let job = match &self.command { @@ -288,13 +290,13 @@ impl Cli { return Err(invalid("--tiles takes NAME=FILE")); } if job.scenes.len() > 1 && job.panel.len() != job.scenes.len() { - return Err(invalid("a shot of several scenes takes a --panel for each")); + return Err(invalid("several scenes need one --panel each")); } if job.scenes.len() == 1 && job.panel.len() > 1 { - return Err(invalid("one scene fills one panel")); + return Err(invalid("one scene takes at most one --panel")); } if job.camera.len() > job.scenes.len() { - return Err(invalid("--camera is one per scene")); + return Err(invalid("at most one --camera per scene")); } let Some(output) = &job.output else { if matches!(self.command, Command::Render(_)) { @@ -307,7 +309,7 @@ impl Cli { let known = matches!(extension.as_deref(), Some("png" | "jpg" | "jpeg" | "webp" | "mp4" | "y4m" | "rgba")); if job.format.is_none() && !is_stdio(output) && !known { return Err(invalid( - "--output ends in .png, .jpg, .webp, .mp4, .y4m, or .rgba, or --format says what it is", + "--output must end in .png, .jpg, .webp, .mp4, .y4m, or .rgba, unless --format is set", )); } let video = matches!(format, Format::Mp4 | Format::Y4m); @@ -315,7 +317,7 @@ impl Cli { return Err(invalid("a video or an image sequence takes --frames")); } if job.sequence() && (format.image(job.quality).is_none() || is_stdio(output)) { - return Err(invalid("a %d pattern names an image file a frame")); + return Err(invalid("a %d pattern needs an image format: png, jpeg, or webp")); } job.shot().check(format.pixels()).map_err(|e| invalid(&e.to_string())) } @@ -339,16 +341,16 @@ fn seconds(value: &str) -> Result { } fn deadline(value: &str) -> Result { - seconds(value).and_then(|d| if d.is_zero() { Err("a deadline is longer than zero".into()) } else { Ok(d) }) + seconds(value).and_then(|d| if d.is_zero() { Err("expected more than zero seconds".into()) } else { Ok(d) }) } fn fps(value: &str) -> Result { - finite(value).and_then(|f| if (1.0..=240.0).contains(&f) { Ok(f) } else { Err("1 to 240 frames a second".into()) }) + finite(value).and_then(|f| if (1.0..=240.0).contains(&f) { Ok(f) } else { Err("expected 1 to 240 fps".into()) }) } fn size(value: &str) -> Result<[u32; 2], String> { let (w, h) = value.split_once(['x', 'X']).ok_or("expected WIDTHxHEIGHT")?; - let side = |v: &str| v.trim().parse::().ok().filter(|n| *n > 0).ok_or("a side is a positive whole number"); + let side = |v: &str| v.trim().parse::().ok().filter(|n| *n > 0).ok_or("expected positive whole numbers"); Ok([side(w)?, side(h)?]) } @@ -363,7 +365,7 @@ fn panel(value: &str) -> Result<[f32; 4], String> { fn insets(value: &str) -> Result<[f32; 4], String> { numbers::<4>(value) - .and_then(|n| if n.iter().all(|v| *v >= 0.0) { Ok(n) } else { Err("insets are not negative".into()) }) + .and_then(|n| if n.iter().all(|v| *v >= 0.0) { Ok(n) } else { Err("expected nonnegative insets".into()) }) } #[cfg(test)] diff --git a/crates/latkit-cli/src/inputs.rs b/crates/latkit-cli/src/inputs.rs index 69493b1b..f2fc2ce6 100644 --- a/crates/latkit-cli/src/inputs.rs +++ b/crates/latkit-cli/src/inputs.rs @@ -1,5 +1,5 @@ -//! What a job reads: scenes, datasets, cameras, fits, and frames. Datasets and frames' patches read -//! as the library reads them, through the host, which reads the files their documents name. +//! Reading a job's inputs: scenes, datasets, cameras, fits, and frames. Datasets and frame patches +//! load through the library and its host, which reads the files their documents reference. use std::fs::File; use std::io::{self, BufRead, BufReader, Read}; @@ -15,10 +15,10 @@ use serde::de::DeserializeOwned; use crate::args::{Source, is_stdio}; use crate::{Failure, block}; -/// Frames read and loaded ahead of the one drawing. +/// Number of frames read and loaded ahead of the one being drawn. const AHEAD: usize = 2; -/// The bytes at `path`, or stdin's for `-`. +/// Reads the bytes at `path`, or stdin for `-`. pub(crate) fn read(path: &Path) -> Result, Failure> { let mut bytes = Vec::new(); let result = if is_stdio(path) { @@ -30,36 +30,36 @@ pub(crate) fn read(path: &Path) -> Result, Failure> { Ok(bytes) } -/// What paths in the document at `path` are relative to: the document, or nothing for stdin. +/// The base for relative paths in the document at `path`: the document's path, or empty for stdin. fn base(path: &Path) -> String { if is_stdio(path) { String::new() } else { path.to_string_lossy().into_owned() } } -/// What the JSON document at `path` holds, read as a `T`. +/// Reads the JSON document at `path` as a `T`. pub(crate) fn document(path: &Path) -> Result { let bytes = read(path)?; let text = std::str::from_utf8(&bytes).map_err(|_| Failure::at(Error::invalid("not UTF-8 text"), path))?; latkit::from_json(text).map_err(|e| Failure::at(e, path)) } -/// A scene from its JSON. +/// Reads a scene from its JSON file. pub(crate) fn scene(path: &Path) -> Result { document(path) } -/// A camera from its JSON. +/// Reads a camera from its JSON file. pub(crate) fn camera(path: &Path) -> Result { document(path) } -/// A fit: `all`, or one as JSON. +/// Parses a fit: a bare name such as `all`, or JSON. pub(crate) fn fit(text: &str) -> Result { let json = if text.trim_start().starts_with(['{', '"', '[']) { text.to_owned() } else { format!("\"{text}\"") }; latkit::from_json(&json).map_err(|e| Failure::from(e.at("--fit"))) } -/// The dataset `source` names: latkit's bytes, GeoJSON, or a JSON document whose files `host` -/// reads, named as it says. +/// Loads the dataset `source` names: latkit bytes, GeoJSON, or a JSON document whose referenced +/// files `host` reads. The name `source` gives, if any, names the dataset. pub(crate) fn dataset(source: &Source, host: &Native, deadline: Duration) -> Result { let path = &source.path; let bytes = read(path)?; @@ -68,28 +68,28 @@ pub(crate) fn dataset(source: &Source, host: &Native, deadline: Duration) -> Res block(deadline, Data::read(&bytes, reading)).map_err(|e| Failure::at(e, path)) } -/// One output frame's changes, as a line of the frames file says. +/// One output frame's changes, from one line of a frames file. #[derive(Deserialize, schemars::JsonSchema)] #[serde(deny_unknown_fields)] pub(crate) struct Frame { /// The axis coordinate every view shows. pub(crate) at: Option, - /// Patches by dataset name, in the order written. + /// Patches by dataset name, applied in the order written. #[serde(default)] patch: Patches, - /// Where views look at once: one camera for every view it fits, or one per view. + /// Cameras views switch to at once: one for every view whose space it fits, or one per view. pub(crate) camera: Option>, - /// Where views ease to. + /// Cameras views ease to, given like `camera`. pub(crate) fly: Option>, - /// What views frame. + /// What views fit: one fit for every view, or one per view. pub(crate) fit: Option>, /// CSS pixels every view keeps clear: top, right, bottom, left. pub(crate) insets: Option<[f32; 4]>, - /// Degrees a second every map or plane turns. + /// Degrees per second every map or plane turns. pub(crate) orbit: Option, } -/// One value for every view, or one for each in order, `null` leaving a view as it is. +/// One value for every view, or one per view in order. `null` leaves a view unchanged. #[derive(Deserialize, schemars::JsonSchema)] #[serde(untagged)] #[schemars(inline)] @@ -108,10 +108,10 @@ impl Each { } } -/// A frame's index, what it changes, and its patches by dataset, loaded. +/// A frame's index, its changes, and its loaded patches by dataset name. pub(crate) type Next = (usize, Frame, Vec<(Name, Patch)>); -/// The lines of a frames file, each a frame, its patches loaded through the host. +/// Reads a frames file, one frame per line, loading each frame's patches through the host. pub(crate) struct Frames { path: PathBuf, lines: io::Lines>, @@ -130,7 +130,7 @@ impl Frames { Ok(Frames { path: path.to_path_buf(), lines: input.lines(), index: 0, host, deadline }) } - /// The next frame, its patches loaded; `None` at the end. Blank lines are skipped. + /// The next frame, with its patches loaded. `None` at the end. Skips blank lines. pub(crate) fn next(&mut self) -> Result, Failure> { loop { let Some(line) = self.lines.next() else { return Ok(None) }; @@ -143,7 +143,7 @@ impl Frames { let at = |error: Error| Failure::at(error.at(index).at("frames"), &self.path); let mut frame: Frame = latkit::from_json(&line).map_err(at)?; if frame.at.is_some_and(|a| !a.is_finite()) { - return Err(at(Error::invalid("at is finite").at("at"))); + return Err(at(Error::invalid("expected a finite number").at("at"))); } let base = base(&self.path); let reading = Reading { base: &base, host: Some(&self.host), name: None }; @@ -153,8 +153,8 @@ impl Frames { } } - /// The frames read and loaded on a thread of their own, a few ahead of the one taken, so their - /// files are read while frames draw. + /// Reads and loads frames on a separate thread, a few ahead of the one taken, so their files + /// load while frames draw. pub(crate) fn ahead(mut self) -> Ahead { let (send, frames) = mpsc::sync_channel(AHEAD); std::thread::spawn(move || { @@ -211,19 +211,19 @@ mod tests { let geo = r#"{"type": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": [1, 2]}}"#; std::fs::write(dir.path().join("p.geojson"), geo).unwrap(); let geo_path = dir.path().join("p.geojson").to_string_lossy().into_owned(); - assert!(load(&geo_path).is_err(), "GeoJSON names its dataset"); + assert!(load(&geo_path).is_err(), "GeoJSON needs a name"); let places = load(&format!("places={geo_path}")).unwrap(); assert_eq!(rows(&places, "points"), Some(1)); } #[test] - fn frames_keep_their_patches_order_and_apply_to_every_view_or_each() { + fn frames_keep_patch_order_and_take_one_value_or_one_per_view() { let frame: Frame = latkit::from_json(r#"{"patch": {"b": [], "a": []}}"#).unwrap(); let host = Native::new(); let patches = block(deadline(), frame.patch.read(Reading { host: Some(&host), ..Reading::default() })).unwrap(); let names: Vec<&str> = patches.iter().map(|(name, _)| name.as_str()).collect(); assert_eq!(names, ["b", "a"]); - assert!(latkit::from_json::(r#"{"patch": {"a": [], "a": []}}"#).is_err(), "a dataset patched twice"); + assert!(latkit::from_json::(r#"{"patch": {"a": [], "a": []}}"#).is_err(), "a dataset named twice fails"); let error = latkit::from_json::(r#"{"patch": {"a": [[]]}}"#).map(drop).unwrap_err(); assert!(error.to_string().starts_with("patch.a[0]"), "{error}"); let one: Each = latkit::from_json("2").unwrap(); diff --git a/crates/latkit-cli/src/main.rs b/crates/latkit-cli/src/main.rs index 41ca841b..8361d090 100644 --- a/crates/latkit-cli/src/main.rs +++ b/crates/latkit-cli/src/main.rs @@ -1,5 +1,5 @@ -//! The latkit command line: scenes and datasets in the library's JSON (or its bytes), drawn by the -//! library natively into images, image sequences, raw frames, and videos. +//! The latkit command line. It renders scenes and datasets, given as the library's JSON or bytes, +//! natively into images, image sequences, raw frames, and videos. mod args; mod inputs; @@ -45,7 +45,7 @@ fn main() -> ExitCode { } } -/// Why a command failed: the library's error, and the file it came from. +/// A command's failure: the library's error, and the file it came from, if any. #[derive(Debug)] pub(crate) struct Failure { pub(crate) error: Error, @@ -53,7 +53,7 @@ pub(crate) struct Failure { } impl Failure { - /// `error`, from the file at `path`. + /// `error`, caused by the file at `path`. pub(crate) fn at(error: Error, path: &Path) -> Failure { Failure { error, source: Some(path.to_string_lossy().into_owned()) } } @@ -70,8 +70,8 @@ impl From for Failure { } } -/// Every dataset's description as JSON on stdout: its name, axis and span, and each table's rows and -/// fields. +/// Prints every dataset's description as JSON to stdout: its name, axis and span, and each table's +/// rows and fields. fn inspect(sources: &[Source], deadline: Duration) -> Result<(), Failure> { let host = latkit::host::Native::new(); let mut all = Vec::with_capacity(sources.len()); @@ -83,17 +83,17 @@ fn inspect(sources: &[Source], deadline: Duration) -> Result<(), Failure> { writeln!(out).map_err(|e| Failure::io(&e, Path::new("-"))) } -/// The JSON Schema of every input on stdout, or of `input`: its definition, with every definition -/// it is built of. +/// Prints the JSON Schema of every input to stdout. With `input`, the root `$ref` points to that +/// input's definition. fn schema(input: Option) -> Result<(), Failure> { let mut schemas = latkit::schemas(); let frames = latkit::schema::(); - let defs = schemas["$defs"].as_object_mut().expect("definitions"); + let defs = schemas["$defs"].as_object_mut().expect("the schemas have $defs"); for (name, def) in frames["$defs"].as_object().into_iter().flatten() { defs.entry(name.clone()).or_insert_with(|| def.clone()); } let mut frame = frames.clone(); - frame.as_object_mut().expect("a schema").retain(|key, _| key != "$defs" && key != "$schema"); + frame.as_object_mut().expect("a schema is an object").retain(|key, _| key != "$defs" && key != "$schema"); defs.insert("Frame".into(), frame); if let Some(input) = input { let name = match input { @@ -112,8 +112,8 @@ fn schema(input: Option) -> Result<(), Failure> { writeln!(out).map_err(|e| Failure::io(&e, Path::new("-"))) } -/// The rows `query` reads from the dataset as JSON Lines on stdout: `id`, then each field, a -/// reference as its target's id and a missing value as `null`. +/// Prints the rows `query` selects from the dataset as JSON Lines to stdout. Each line has `id`, +/// then each field. A reference prints as its target's id, and a missing value as `null`. fn query(source: &Source, query: &str, deadline: Duration) -> Result<(), Failure> { let text = if query.trim_start().starts_with('{') { query.as_bytes().to_vec() } else { inputs::read(Path::new(query))? }; @@ -121,7 +121,7 @@ fn query(source: &Source, query: &str, deadline: Duration) -> Result<(), Failure let query: Query = latkit::from_json(text).map_err(|e| Failure::from(e.at("query")))?; let rows = inputs::dataset(source, &latkit::host::Native::new(), deadline)?.snapshot().query(&query)?.table; let columns: Vec<(String, Values)> = - rows.fields().map(|(name, _)| (name.to_string(), rows.values(name).expect("a field the table has"))).collect(); + rows.fields().map(|(name, _)| (name.to_string(), rows.values(name).expect("the field exists"))).collect(); let mut out = io::BufWriter::new(io::stdout().lock()); for (row, id) in rows.ids().enumerate() { let mut line = serde_json::Map::with_capacity(columns.len() + 1); @@ -139,8 +139,8 @@ fn query(source: &Source, query: &str, deadline: Duration) -> Result<(), Failure out.flush().map_err(|e| Failure::io(&e, Path::new("-"))) } -/// The dataset as time tiles in directory `output`: its manifest, the dataset without its sampled -/// fields' histories, and their spans of `chunk` entries. +/// Writes the dataset as time tiles in directory `output`: the manifest, the dataset without its +/// sampled fields' histories, and those histories in spans of `chunk` entries. fn tile(source: &Source, output: &Path, chunk: u32, deadline: Duration) -> Result<(), Failure> { let data = inputs::dataset(source, &latkit::host::Native::new(), deadline)?.snapshot(); let mut failure = None; @@ -159,13 +159,13 @@ fn tile(source: &Source, output: &Path, chunk: u32, deadline: Duration) -> Resul } } -/// `future`'s result, driven on this thread and given up at `deadline`. +/// Drives `future` on this thread and returns its result, or [`Error::Timeout`] at `deadline`. pub(crate) fn block(deadline: Duration, future: impl Future>) -> latkit::Result { until(deadline, future)? } -/// A synchronous command needs only one parked thread to drive its future, with a deadline. The -/// future must yield; like other async timeouts this cannot interrupt a synchronous OS or driver call. +/// Drives `future` on this thread, parking between polls, until it finishes or `deadline` passes. +/// The deadline is checked only between polls, so it cannot interrupt a blocking OS or driver call. fn until(deadline: Duration, future: impl Future) -> latkit::Result { struct Thread(std::thread::Thread); impl Wake for Thread { diff --git a/crates/latkit-cli/src/render.rs b/crates/latkit-cli/src/render.rs index d8c262cf..44cd3d78 100644 --- a/crates/latkit-cli/src/render.rs +++ b/crates/latkit-cli/src/render.rs @@ -1,6 +1,7 @@ -//! Rendering a job, and validating one: views of the scenes with the datasets, then a still picture, -//! or a video of the frames handed to a sink. Validating reads, loads, and checks everything a render -//! does, frames included, without a GPU; both change frames with [`apply`]. +//! Rendering and validating a job. Rendering creates a view of each scene over the datasets, then +//! takes a still picture, or draws a video of the frames into a sink. Validating reads, loads, and +//! checks every input a render does, including frames, without a GPU. Both apply frames with +//! [`apply`]. use std::path::{Path, PathBuf}; @@ -13,11 +14,11 @@ use crate::report::Report; use crate::sinks::{Commit, Images, Output, Raw, Y4m}; use crate::{Failure, block}; -/// What a job reads before it draws, and the host that read it. +/// A job's inputs, read before drawing, and the host that read them. struct Inputs { scenes: Vec<(PathBuf, Scene)>, data: Vec, - /// What every view fetches as it draws. + /// Tiles every view fetches as it draws. tiles: Vec, cameras: Vec, fit: Fit, @@ -42,7 +43,7 @@ impl Inputs { for (i, camera) in cameras.iter().enumerate() { if !camera.fits(scenes[i].1.space()) { return Err(Failure::at( - Error::invalid("the camera does not look at its scene's space"), + Error::invalid("the camera does not match the space of its scene"), &job.camera[i], )); } @@ -54,12 +55,13 @@ impl Inputs { Ok(inputs) } - /// What every view draws: each dataset, and the tiles. + /// The sources every view draws: each dataset, and the tiles. fn sources(&self) -> Vec { self.data.iter().map(latkit::Source::from).chain(self.tiles.iter().map(latkit::Source::from)).collect() } - /// A view of each scene, drawing every dataset and the tiles, looking where the job says. + /// Creates a view of each scene over every dataset and the tiles, with the job's insets, axis + /// coordinate, and camera or fit. fn views(&self, gpu: &Gpu, job: &Job) -> Result, Failure> { let mut views = Vec::with_capacity(self.scenes.len()); for (i, (path, scene)) in self.scenes.iter().enumerate() { @@ -86,7 +88,7 @@ pub(crate) fn render(job: &Job, report: &mut Report) -> Result<(), Failure> { let gpu = block(job.deadline, Gpu::new())?; gpu.set_host(inputs.host.clone()); let mut views = inputs.views(&gpu, job)?; - let output = job.output.as_deref().expect("render takes an output"); + let output = job.output.as_deref().expect("render requires --output"); let Some(path) = &job.frames else { let mut shot = job.shot(); if let Some(elapsed) = job.elapsed { @@ -115,7 +117,7 @@ pub(crate) fn render(job: &Job, report: &mut Report) -> Result<(), Failure> { return sequence.run(frames, images); } match format { - Format::Png | Format::Jpeg | Format::Webp => unreachable!("images write above"), + Format::Png | Format::Jpeg | Format::Webp => unreachable!("image formats are handled above"), Format::Rgba => sequence.run(frames, Raw::new(Output::create(output)?)), Format::Y4m => sequence.run(frames, Y4m::new(Output::create(output)?)), Format::Mp4 => { @@ -128,7 +130,7 @@ pub(crate) fn render(job: &Job, report: &mut Report) -> Result<(), Failure> { } } -/// A video of the frames file at `path`, drawn as each line says. +/// A video of the frames file at `path`, one output frame per line. struct Sequence<'a> { job: &'a Job, path: &'a Path, @@ -154,7 +156,7 @@ impl Sequence<'_> { self.report.progress(video.frames(), video.time()); } if !any { - return Err(Failure::at(Error::invalid("the frames are empty"), path)); + return Err(Failure::at(Error::invalid("the frames file has no frames"), path)); } let frames = video.frames(); block(job.deadline, video.finish()).map_err(|e| Failure::at(e, output))?.commit()?; @@ -163,16 +165,16 @@ impl Sequence<'_> { } } -/// Applies `frame`'s patches to the datasets they name, in order, which every view draws from its -/// next frame, and checks the cameras it gives each view against that view's scene: whether it -/// changed any dataset. +/// Applies `frame`'s patches, in order, to the datasets they name. Every view draws the changes +/// from its next frame. Also checks each per-view camera against its view's scene. Returns whether +/// any dataset changed. fn apply( data: &[Data], scenes: &[(PathBuf, Scene)], frame: &Frame, patches: Vec<(Name, Patch)>, ) -> latkit::Result { - // One camera for every view looks through those it fits; one per view must fit its own. + // A single camera applies to the views it fits. A per-view camera must fit its own view. for (option, each) in [("camera", &frame.camera), ("fly", &frame.fly)] { let Some(Each::Each(cameras)) = each else { continue }; for (i, camera) in cameras.iter().enumerate() { @@ -182,7 +184,7 @@ fn apply( (Some(_), None) => false, }; if !fits { - return Err(Error::invalid("the camera does not look at its view's space").at(i).at(option)); + return Err(Error::invalid("the camera does not match the space of its view").at(i).at(option)); } } } @@ -197,7 +199,7 @@ fn apply( Ok(changed) } -/// Points every view where `frame` says. +/// Applies `frame`'s axis coordinate, insets, orbit, cameras, and fits to every view. fn steer(views: &mut [View], frame: &Frame) { for (i, view) in views.iter_mut().enumerate() { if let Some(at) = frame.at { @@ -221,8 +223,8 @@ fn steer(views: &mut [View], frame: &Frame) { } } -/// Reads, loads, and checks everything `render` would, without a GPU: scenes against the datasets, -/// cameras, fonts, and every frame applied in turn. +/// Reads, loads, and checks every input `render` would, without a GPU: scenes against the +/// datasets, cameras, fonts, and every frame applied in turn. pub(crate) fn validate(job: &Job) -> Result<(), Failure> { let inputs = Inputs::read(job)?; let Some(path) = &job.frames else { return Ok(()) }; @@ -237,5 +239,5 @@ pub(crate) fn validate(job: &Job) -> Result<(), Failure> { } } } - if any { Ok(()) } else { Err(Failure::at(Error::invalid("the frames are empty"), path)) } + if any { Ok(()) } else { Err(Failure::at(Error::invalid("the frames file has no frames"), path)) } } diff --git a/crates/latkit-cli/src/report.rs b/crates/latkit-cli/src/report.rs index 0f8beed5..a843d614 100644 --- a/crates/latkit-cli/src/report.rs +++ b/crates/latkit-cli/src/report.rs @@ -1,7 +1,7 @@ -//! What a command tells on stderr. As JSON, one object a line, `{ "type", "detail" }` as a view's -//! events cross to JavaScript: `progress` after each frame, `stats` once done, and `error` with the -//! library's `{ code, message, path }` and the file it came from. For people, an error a line, and on -//! a terminal a line of progress that rewrites itself. +//! Diagnostics on stderr. As JSON, one `{ "type", "detail" }` object per line, the shape of a +//! view's events in JavaScript: `progress` after each frame, `stats` when done, and `error` with +//! the library's `{ code, message, path }` and the source file. For people, one line per error, +//! and on a terminal a progress line that rewrites itself. use std::io::{self, IsTerminal, Write}; use std::time::Instant; @@ -14,10 +14,10 @@ use crate::args::Diagnostics; pub(crate) struct Report { json: bool, - /// Whether people read stderr on a terminal: then progress shows. + /// Whether stderr is a terminal, where progress is shown. terminal: bool, started: Instant, - /// Whether a progress line is showing. + /// Whether a progress line is on screen. showing: bool, } @@ -31,7 +31,7 @@ impl Report { } } - /// `frames` drawn, the next showing at `time` seconds. + /// Reports `frames` drawn, with the next at `time` seconds of video. pub(crate) fn progress(&mut self, frames: u64, time: f64) { if self.json { Report::line(&json!({"type": "progress", "detail": {"frames": frames, "time": time}})); @@ -42,7 +42,7 @@ impl Report { } } - /// What was drawn, and what it took. + /// Reports the frames drawn and the time, stats, and GPU work they took. pub(crate) fn done(&mut self, frames: u64, views: &[View], gpu: &Gpu) { let seconds = self.started.elapsed().as_secs_f64(); if self.json { @@ -59,7 +59,7 @@ impl Report { } } - /// Why the command failed. + /// Reports why the command failed. pub(crate) fn error(&mut self, failure: &Failure) { if self.json { let mut detail = serde_json::to_value(&failure.error).unwrap_or(Value::Null); diff --git a/crates/latkit-cli/src/sinks.rs b/crates/latkit-cli/src/sinks.rs index e790e030..e20b0c50 100644 --- a/crates/latkit-cli/src/sinks.rs +++ b/crates/latkit-cli/src/sinks.rs @@ -1,5 +1,5 @@ -//! Where pictures go: one output written whole or not at all, and the sinks a video's frames go to -//! besides the library's MP4: image files, raw RGBA, and YUV4MPEG2. +//! Output destinations: an output written whole or not at all, and the video sinks besides the +//! library's MP4: image files, raw RGBA, and YUV4MPEG2. use std::collections::VecDeque; use std::fs::File; @@ -13,14 +13,14 @@ use latkit::{Encoding, Error, Image, ImageFormat, Mp4, Pending, Pixels, Sink}; use crate::Failure; use crate::args::{is_stdio, pattern}; -/// Image files encoding at once, on the host's workers. +/// The most image files encoding at once on the host's workers. const ENCODING: usize = 4; fn written(error: &io::Error, path: &Path) -> Error { Error::Io(format!("{}: {error}", path.display()).into()) } -/// Bytes to stdout, or to a file that replaces `path` only once every byte is written. +/// Bytes to stdout, or to a temporary file that replaces `path` only once every byte is written. pub(crate) enum Output { Stdout(BufWriter), File { path: PathBuf, file: BufWriter, temporary: tempfile::TempPath }, @@ -40,7 +40,7 @@ impl Output { Ok(Output::File { path: path.to_path_buf(), file: BufWriter::new(file), temporary }) } - /// Writes what is left and, for a file, replaces its destination. + /// Flushes the remaining bytes and, for a file, moves it into place at its destination. pub(crate) fn commit(self) -> Result<(), Failure> { match self { Output::Stdout(mut out) => out.flush().map_err(|e| Failure::io(&e, Path::new("-"))), @@ -58,7 +58,7 @@ impl Output { } } - /// Writes `bytes`, naming the destination in its error. + /// Writes `bytes`. Errors name the destination. fn put(&mut self, bytes: &[u8]) -> latkit::Result<()> { self.write_all(bytes).map_err(|e| written(&e, self.path())) } @@ -80,7 +80,7 @@ impl Write for Output { } } -/// What a sink leaves once its video is finished: its output, written and in place. +/// Finalizes a sink's output once its video is finished, leaving it written and in place. pub(crate) trait Commit { fn commit(self) -> Result<(), Failure>; } @@ -91,33 +91,33 @@ impl Commit for Mp4 { } } -/// Pictures as image files, encoded on the host's workers a few at once: one a frame, numbered by -/// the pattern of the output's name, each file written whole and in order, a held frame's file the -/// frame before's; or one file, of frames that are one. +/// Pictures as image files, encoded a few at a time on the host's workers. Either one file per +/// frame, numbered by the output's pattern and each written whole and in order, or one file for a +/// single frame. A held frame (where nothing moved) repeats the previous frame's file. pub(crate) struct Images { to: To, format: ImageFormat, host: Native, size: [u32; 2], - /// Frames under way, oldest first: each frame's index, and a drawn frame's file. + /// Frames in progress, oldest first: each frame's index and, if drawn, its file being encoded. writing: VecDeque<(u64, Option>>)>, /// The last drawn frame's file. last: Vec, } -/// Where image files go. +/// Where image files are written. enum To { Numbered(PathBuf), One(Output), } impl Images { - /// A file a frame, at the paths `pattern` numbers. + /// One file per frame, at the paths `pattern` numbers. pub(crate) fn numbered(pattern: PathBuf, format: ImageFormat, host: Native) -> Images { Images::new(To::Numbered(pattern), format, host) } - /// One file, to `out`. + /// A single file, written to `out`. pub(crate) fn one(out: Output, format: ImageFormat, host: Native) -> Images { Images::new(To::One(out), format, host) } @@ -126,10 +126,10 @@ impl Images { Images { to, format, host, size: [0, 0], writing: VecDeque::new(), last: Vec::new() } } - /// The path of frame `index` where `names` numbers the frames. + /// The path of frame `index`, from the numbered pattern `names`. fn path(names: &Path, index: u64) -> PathBuf { let text = names.to_string_lossy(); - let (range, width) = pattern(names).expect("a pattern"); + let (range, width) = pattern(names).expect("the output is a %d pattern"); format!("{}{index:0width$}{}", &text[..range.start], &text[range.end..]).into() } @@ -139,7 +139,7 @@ impl Images { if let Some(file) = file { self.last = file.await?; if self.last.is_empty() { - return Err(Error::invalid(format!("the picture could not be written as {}", self.format))); + return Err(Error::invalid(format!("the picture could not be encoded as {}", self.format))); } } match &mut self.to { @@ -164,7 +164,7 @@ impl Sink for Images { async fn frame(&mut self, index: u64, pixels: Option>) -> latkit::Result<()> { if index > 0 && matches!(self.to, To::One(_)) { - return Err(Error::invalid("an image file is one frame; a %d pattern in the output writes one a frame")); + return Err(Error::invalid("an image file holds one frame; use a %d pattern to write one per frame")); } let file = match pixels { Some(rgba) => Some(self.host.spawn(Job::encode(&Image::new(self.size, rgba)?, self.format))), @@ -179,7 +179,7 @@ impl Sink for Images { async fn finish(&mut self, end: u64) -> latkit::Result<()> { if end == 0 { - return Err(Error::invalid("the frames are empty")); + return Err(Error::invalid("no frames to write")); } while !self.writing.is_empty() { self.write_oldest().await?; @@ -197,7 +197,7 @@ impl Commit for Images { } } -/// Raw RGBA, straight alpha, frames back to back; a held frame is the frame before again. +/// Raw RGBA with straight alpha, frames back to back. A held frame repeats the previous frame. pub(crate) struct Raw { out: Output, last: Vec, @@ -230,12 +230,12 @@ impl Commit for Raw { } } -/// YUV4MPEG2: a header, then each frame's planes, 4:2:0 from the GPU's NV12. A held frame is the -/// frame before again. +/// YUV4MPEG2: a header, then each frame's 4:2:0 planes, converted from the GPU's NV12. A held frame +/// repeats the previous frame. pub(crate) struct Y4m { out: Output, size: [u32; 2], - /// The last frame written, `FRAME` and planes. + /// The last frame written: its `FRAME` marker and planes. last: Vec, } @@ -251,7 +251,7 @@ impl Sink for Y4m { fn begin(&mut self, encoding: &Encoding, _: Option<&dyn Host>) -> latkit::Result<()> { self.size = encoding.size; let ([width, height], [num, den]) = (encoding.size, encoding.rate()); - // The GPU averages each 2×2 block's chroma: centered, as JPEG sites it. + // The GPU averages each 2×2 block's chroma, so samples are centered, as `C420jpeg` says. let header = format!("YUV4MPEG2 W{width} H{height} F{num}:{den} Ip A1:1 C420jpeg XCOLORRANGE=LIMITED\n"); self.out.put(header.as_bytes()) } diff --git a/crates/latkit-cli/tests/cli.rs b/crates/latkit-cli/tests/cli.rs index cd26b88d..765d0160 100644 --- a/crates/latkit-cli/tests/cli.rs +++ b/crates/latkit-cli/tests/cli.rs @@ -1,5 +1,5 @@ -//! The executable's contracts: files, pipes, exit codes, diagnostics, and every output a render -//! writes, from a still to a video. +//! Tests of the executable's behavior: files, pipes, exit codes, diagnostics, and every output a +//! render writes, from a still to a video. use std::io::Write; use std::path::Path; @@ -36,7 +36,7 @@ fn fixture() -> tempfile::TempDir { dir } -/// Whether the machine has no GPU, which only `LATKIT_REQUIRE_GPU` makes a failure. +/// Whether the run failed for lack of a GPU. With `LATKIT_REQUIRE_GPU` set, that fails the test. fn no_gpu(output: &Output) -> bool { let stderr = String::from_utf8_lossy(&output.stderr); let missing = !output.status.success() && (stderr.contains("no GPU adapter") || stderr.contains("no GPU device")); @@ -48,7 +48,7 @@ fn ok(output: &Output) { assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); } -/// Writes frames: `x` positions of the two buses, as patches of slices of one binary file. +/// Writes a frames file whose lines set the two buses' `x` from slices of one binary file. fn frames(dir: &Path, xs: &[[f32; 2]]) { let bytes: Vec = xs.iter().flatten().flat_map(|x| x.to_le_bytes()).collect(); std::fs::create_dir_all(dir.join("sequence")).unwrap(); @@ -118,7 +118,7 @@ fn validation_reads_everything_without_a_gpu_and_is_silent_on_success() { ok(&output); assert!(output.stdout.is_empty() && output.stderr.is_empty()); } - // A frame naming no dataset fails at its line, as JSON when asked. + // A frame that patches a missing dataset fails at its line, reported as JSON when requested. let bad = r#"{"at":0,"patch":{"absent":[{"forget":{"before":0}}]}}"#; std::fs::write(dir.path().join("sequence/bad.jsonl"), format!("{{\"at\":0}}\n{bad}")).unwrap(); let args = ["--diagnostics", "json", "validate", "scene.json", "-d", "grid.json", "--frames", "sequence/bad.jsonl"]; @@ -128,7 +128,7 @@ fn validation_reads_everything_without_a_gpu_and_is_silent_on_success() { assert_eq!(told["type"], "error"); assert_eq!(told["detail"]["code"], "invalid"); assert_eq!(told["detail"]["path"], "frames[1].patch.absent"); - // A camera for each view looks at its own view's space: a plot's cannot look at a plane. + // A per-view camera must fit its own view's space: a plot camera cannot look at a plane. std::fs::write(dir.path().join("sequence/bad.jsonl"), r#"{"camera": [{"x": [0, 1]}]}"#).unwrap(); let output = run(dir.path(), &args, None); let told: serde_json::Value = serde_json::from_slice(&output.stderr).unwrap(); @@ -137,7 +137,7 @@ fn validation_reads_everything_without_a_gpu_and_is_silent_on_success() { } #[test] -fn failures_name_the_part_and_the_file_and_keep_the_output() { +fn failures_report_path_and_source_and_leave_existing_output_intact() { let dir = fixture(); let output = run(dir.path(), &["--diagnostics", "json", "validate", "scene.json"], None); assert_eq!(output.status.code(), Some(1)); @@ -149,7 +149,7 @@ fn failures_name_the_part_and_the_file_and_keep_the_output() { std::fs::write(dir.path().join("out.png"), b"keep this").unwrap(); assert_eq!(run(dir.path(), &["render", "scene.json", "-o", "out.png"], None).status.code(), Some(1)); assert_eq!(std::fs::read(dir.path().join("out.png")).unwrap(), b"keep this"); - // A legend shows a color mapped through a colormap with a domain or named categories. + // A legend needs a color mapped through a colormap with a domain or named categories. let legend = r#"{"space":"plane","marks":[{"vertices":{"of":["grid","Bus"],"legend":"Buses"}}]}"#; let output = run(dir.path(), &["--diagnostics", "json", "validate", "-", "-d", "grid.json"], Some(legend.as_bytes())); @@ -158,7 +158,7 @@ fn failures_name_the_part_and_the_file_and_keep_the_output() { } #[test] -fn inspect_describes_datasets_and_geojson_takes_a_name() { +fn inspect_describes_datasets_and_geojson_needs_a_name() { let dir = fixture(); let geo = r#"{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"n":1},"geometry":{"type":"Point","coordinates":[1,2]}}]}"#; std::fs::write(dir.path().join("places.geojson"), geo).unwrap(); @@ -189,7 +189,7 @@ fn schema_describes_every_input_and_frames() { } #[test] -fn query_writes_a_json_line_a_row() { +fn query_writes_one_json_line_per_row() { let dir = fixture(); let query = r#"{"of":"Bus","sort":[["x","descending"]],"select":{"name":"name","twice":{"op":"mul","args":["x",2]}}}"#; @@ -214,7 +214,7 @@ fn query_writes_a_json_line_a_row() { } #[test] -fn a_history_tiles_and_draws_from_its_spans_as_from_itself() { +fn a_tiled_history_draws_the_same_as_the_whole_dataset() { let dir = tempfile::tempdir().unwrap(); let at: Vec = (0..2000).map(|t| t.to_string()).collect(); let v: Vec = (0..2000).map(|t| format!("{:.4}", 0.5 + 0.4 * (f64::from(t) / 40.0).sin())).collect(); @@ -246,7 +246,7 @@ fn a_history_tiles_and_draws_from_its_spans_as_from_itself() { let pixels = |o: &Output| o.stdout.as_chunks::<4>().0.to_vec(); let differing = pixels(&whole).iter().zip(pixels(&tiled)).filter(|(a, b)| a[0].abs_diff(b[0]) > 8).count(); assert!(differing < 32, "{differing} pixels differ"); - assert!(pixels(&tiled).iter().any(|p| p[0] > 200 && p[1] < 60), "the trace draws"); + assert!(pixels(&tiled).iter().any(|p| p[0] > 200 && p[1] < 60), "the trace is drawn"); } #[test] @@ -275,7 +275,7 @@ fn a_labeled_still_renders_to_a_file_and_to_stdout() { for (file, starts) in [("out.jpg", &[0xff, 0xd8, 0xff][..]), ("out.webp", &b"RIFF"[..])] { args[5] = file; ok(&run(dir.path(), &args, None)); - assert!(std::fs::read(dir.path().join(file)).unwrap().starts_with(starts), "{file} is what it says"); + assert!(std::fs::read(dir.path().join(file)).unwrap().starts_with(starts), "{file} has its format's signature"); } } @@ -310,9 +310,9 @@ fn panels_draw_a_scene_each() { } #[test] -fn a_frame_stream_writes_every_output_and_holds_still_frames() { +fn a_frame_stream_writes_every_output_and_repeats_unchanged_frames() { let dir = fixture(); - // The third frame changes nothing: it is held, and a raw stream repeats the second. + // The third frame changes nothing, so it is held and the raw stream repeats the second. frames(dir.path(), &[[-4.0, 4.0], [0.0, 8.0], [0.0, 8.0]]); let mut args = COMMON.to_vec(); args.extend(["--format", "rgba", "-o", "-"]); @@ -327,7 +327,7 @@ fn a_frame_stream_writes_every_output_and_holds_still_frames() { assert_ne!(first, second); assert_eq!(second, third); - // A sequence never truncates itself into one PNG, nor destroys an old file. + // Several frames to a single PNG fail, and the existing file stays intact. std::fs::write(dir.path().join("out.png"), b"keep").unwrap(); let mut args = COMMON.to_vec(); args.extend(["-o", "out.png"]); @@ -367,6 +367,6 @@ fn a_frame_stream_writes_every_output_and_holds_still_frames() { let absent = "{\"at\":0}\n{\"at\":0,\"patch\":{\"absent\":[]}}"; std::fs::write(dir.path().join("sequence/frames.jsonl"), absent).unwrap(); assert_eq!(run(dir.path(), &args, None).status.code(), Some(1)); - assert_eq!(std::fs::read(dir.path().join("out.mp4")).unwrap(), mp4, "a failed video keeps the file before"); + assert_eq!(std::fs::read(dir.path().join("out.mp4")).unwrap(), mp4, "a failed video leaves the file intact"); } } diff --git a/crates/latkit-js/src/canvas.rs b/crates/latkit-js/src/canvas.rs index 51aef462..209af4bd 100644 --- a/crates/latkit-js/src/canvas.rs +++ b/crates/latkit-js/src/canvas.rs @@ -1,36 +1,37 @@ -//! Canvases: a canvas shows views as its layout places them. +//! Canvases, which show views in the panels of their layout. use latkit::web::{PageView, Panel}; use wasm_bindgen::prelude::*; use crate::Error; -/// A view, for a canvas to show or a shot to draw: what `view.handle()` gives. +/// A view handle from `view.handle()`, for a canvas to show or a shot to draw. #[wasm_bindgen] pub struct Handle(pub(crate) PageView); -/// The views `handles` name. +/// The views behind `handles`. pub(crate) fn views_of(handles: Vec) -> Vec { handles.into_iter().map(|h| h.0).collect() } -/// `views`, each placed `at` four fractions of its own: `[x, y, width, height]` after another. +/// Pairs each of `views` with its panel. `at` holds four fractions per view, `[x, y, width, +/// height]`, in order. Fails unless `at` has four values per view. pub(crate) fn layout(views: Vec, at: &[f32]) -> Result, Error> { if at.len() != views.len() * 4 { - return Err(latkit::Error::invalid("a layout places each view at four fractions").at("layout").into()); + return Err(latkit::Error::invalid("expected four fractions per view").at("layout").into()); } let (ats, _) = at.as_chunks::<4>(); Ok(views.into_iter().zip(ats).map(|(view, at)| Panel { view: view.0, at: *at }).collect()) } -/// Views shown in a `` or an `OffscreenCanvas` as its layout places them; freeing it stops -/// drawing them. +/// Views shown in a `` or an `OffscreenCanvas`, in the panels of its layout. Freeing it +/// stops drawing them. #[wasm_bindgen] pub struct Canvas(pub(crate) latkit::web::Canvas); #[wasm_bindgen] impl Canvas { - /// Shows `views`, each placed `at` four fractions of its own, in place of those it showed. + /// Replaces the shown views with `views`, each at its four fractions of `at`. #[wasm_bindgen(js_name = setLayout)] pub fn set_layout(&self, views: Vec, at: &[f32]) -> Result<(), Error> { Ok(self.0.set_layout(layout(views, at)?)?) diff --git a/crates/latkit-js/src/convert.rs b/crates/latkit-js/src/convert.rs index ae82c952..380c4d2c 100644 --- a/crates/latkit-js/src/convert.rs +++ b/crates/latkit-js/src/convert.rs @@ -1,15 +1,15 @@ -//! latkit's rows and ids to JavaScript, and ids back: a query's columns cross as typed arrays and -//! arrays, built here once; everything else crosses as bytes or JSON. +//! Conversion of latkit rows and ids to JavaScript, and of ids back. A query's columns cross as +//! typed arrays and arrays, built here once. Everything else crosses as bytes or JSON. use js_sys::{Array, Float64Array, Object, Reflect}; use latkit::Error; use wasm_bindgen::{JsCast, JsValue}; -/// The largest whole number a JavaScript number holds exactly: 2^53 - 1. +/// The largest integer a JavaScript number represents exactly: 2^53 - 1. const SAFE: f64 = 9_007_199_254_740_991.0; -/// A value for JavaScript: a number, boolean, or text; a geometry as nested `[x, y]` arrays; a row a -/// reference names as its id. +/// `value` as a JavaScript value. Numbers, booleans, and text convert directly. A geometry becomes +/// nested `[x, y]` arrays, and a referenced row becomes its id. fn value(value: &latkit::Value) -> JsValue { let xy = |p: &[f64; 2]| -> JsValue { Array::of2(&p[0].into(), &p[1].into()).into() }; let path = |p: &Vec<[f64; 2]>| -> JsValue { p.iter().map(xy).collect::().into() }; @@ -29,8 +29,9 @@ fn value(value: &latkit::Value) -> JsValue { } } -/// What a query read, as `{ table, ids, fields, total }`: numbers as a `Float64Array` (missing NaN), -/// other values as arrays (missing `null`), a reference as `{ reference, ids }`. +/// A query result as `{ table, ids, fields, total }`. A number column becomes a `Float64Array` +/// (missing values are NaN), a reference column `{ reference, ids }`, and any other column an array +/// (missing values are `null`). pub(crate) fn query_result(read: &latkit::QueryResult) -> JsValue { let table = &read.table; let fields = Object::create(JsValue::NULL.unchecked_ref::()); @@ -54,12 +55,12 @@ pub(crate) fn query_result(read: &latkit::QueryResult) -> JsValue { ]) } -/// Ids held as values, missing `null`. +/// A column of ids as an array, with `null` for missing ids. fn ids_value(values: &latkit::Values) -> JsValue { (0..values.len()).map(|i| values.id(i).map_or(JsValue::NULL, |id| id_value(&id))).collect::().into() } -/// An object of `entries`. +/// A JavaScript object with `entries` as its properties. pub(crate) fn object(entries: &[(&str, JsValue)]) -> JsValue { let object = Object::new(); for (key, value) in entries { @@ -68,8 +69,8 @@ pub(crate) fn object(entries: &[(&str, JsValue)]) -> JsValue { object.into() } -/// An id as a number or a text: a number too large for JavaScript's as its decimal text, which -/// names the same row. +/// An id as a number or string. A number above 2^53 - 1 becomes its decimal string, which +/// identifies the same row. pub(crate) fn id_value(id: &latkit::Id) -> JsValue { match id { latkit::Id::Number(n) if *n as f64 <= SAFE => (*n as f64).into(), @@ -78,13 +79,13 @@ pub(crate) fn id_value(id: &latkit::Id) -> JsValue { } } -/// The id JavaScript names: a whole number from 0 to 2^53 - 1, or a text. +/// Parses a JavaScript id: an integer from 0 to 2^53 - 1, or a string. pub(crate) fn id(value: &JsValue) -> Result { if let Some(text) = value.as_string() { return Ok(latkit::Id::from(text)); } match value.as_f64() { Some(n) if n.fract() == 0.0 && (0.0..=SAFE).contains(&n) => Ok(latkit::Id::Number(n as u64)), - _ => Err(Error::invalid("an id is a whole number from 0 to 2^53 - 1, or a text").at("id")), + _ => Err(Error::invalid("expected a whole number from 0 to 2^53 - 1, or a string").at("id")), } } diff --git a/crates/latkit-js/src/data.rs b/crates/latkit-js/src/data.rs index 7bd437f7..6c8ddc09 100644 --- a/crates/latkit-js/src/data.rs +++ b/crates/latkit-js/src/data.rs @@ -1,5 +1,6 @@ -//! Datasets: a `Data` changing in place, a `Snapshot` of one version, and the sources views draw. -//! Both read alike: what they hold described, values, queries, envelopes, and their bytes. +//! Datasets: a `Data` changed in place, a `Snapshot` of one version, and the sources views draw. +//! `Data` and `Snapshot` share the same read methods: description, values, queries, envelopes, and +//! bytes. use latkit::host::Host as _; use wasm_bindgen::prelude::*; @@ -7,7 +8,7 @@ use wasm_bindgen::prelude::*; use crate::pending::{Task, task}; use crate::{Error, convert, json, to_json}; -/// A dataset changing in place; clones share it. +/// A dataset changed in place. Clones share it. #[wasm_bindgen] pub struct Data(pub(crate) latkit::Data); @@ -18,8 +19,9 @@ impl Data { Data(latkit::Data::new(name)) } - /// The dataset `bytes` hold, named `name` when given: latkit's bytes; GeoJSON, read in a worker; - /// or a document, its slices of files fetched relative to `base`. + /// Reads the dataset in `bytes`, named `name` if given. `bytes` hold latkit bytes, GeoJSON + /// (parsed in a worker, and only with a `name`), or a JSON document whose file slices are + /// fetched relative to `base`. pub fn read(bytes: Vec, name: Option, base: String) -> Task { task(async move { if let Some(name) = &name @@ -42,23 +44,23 @@ impl Data { self.0.name().to_string() } - /// Applies the patch `bytes` hold: every change, or none when one is invalid. + /// Applies the patch in `bytes`: every change, or none if any is invalid. pub fn apply(&self, bytes: &[u8]) -> Result<(), Error> { Ok(self.0.apply(latkit::Patch::from_bytes(bytes)?)?) } - /// The version it holds now: O(1), sharing every page. + /// The current version, taken in O(1) and sharing every page. #[must_use] pub fn snapshot(&self) -> Snapshot { Snapshot(self.0.snapshot()) } - /// Makes `snapshot`, a version of this dataset, the one it holds. + /// Makes `snapshot`, a version of this dataset, the current version. pub fn restore(&self, snapshot: &Snapshot) -> Result<(), Error> { Ok(self.0.restore(&snapshot.0)?) } - /// The dataset as a source views draw as it changes. + /// A source for views that follows the dataset's changes. pub fn source(&self) -> Source { Source(latkit::Source::from(&self.0)) } @@ -81,7 +83,7 @@ impl Snapshot { self.0.name().to_string() } - /// The version as a source views draw as it is. + /// A source for views that draws this version unchanged. pub fn source(&self) -> Source { Source(latkit::Source::from(&self.0)) } @@ -93,30 +95,30 @@ impl Snapshot { } } -/// What reads a version of a dataset, on each class holding one. +/// Adds the read methods to each class that holds a dataset version. macro_rules! reads { ($class:ident) => { #[wasm_bindgen] impl $class { - /// What the dataset holds, as JSON. + /// The dataset's description, as JSON. pub fn describe(&self) -> String { to_json(&self.version().describe()) } - /// The value of `field` for the row with `id` in `table`, at axis coordinate `at` when - /// given, as JSON; `null` when missing. + /// The value of `field` for the row `id` in `table`, at axis coordinate `at` if given, + /// as JSON. `null` if missing. pub fn get(&self, table: &str, id: &JsValue, field: &str, at: Option) -> Result { Ok(to_json(&self.version().get(table, convert::id(id)?, field, at))) } - /// The rows `query` (JSON) keeps, as `{ table, ids, fields, total }`: fields in the forms - /// a patch writes, `total` the rows kept before paging. + /// The rows `query` (JSON) selects, as `{ table, ids, fields, total }`. Fields are in + /// the forms a patch writes, and `total` counts the selected rows before paging. pub fn query(&self, query: &str) -> Result { Ok(convert::query_result(&self.version().query(&json("query", query)?)?)) } - /// Each row's least and greatest value of a sampled field over equal spans of the axis, as - /// `request` (JSON) asks, as `{ at, least, greatest }`. + /// Each row's least and greatest value of a sampled field over equal spans of the axis, + /// per `request` (JSON), as `{ at, least, greatest }`. pub fn envelope(&self, request: &str) -> Result { let envelope = self.version().envelope(&json("request", request)?)?; Ok(convert::object(&[ @@ -126,7 +128,7 @@ macro_rules! reads { ])) } - /// The dataset as latkit's bytes: the patch that builds it. + /// The dataset as latkit bytes: the patch that recreates it. #[wasm_bindgen(js_name = toBytes)] pub fn to_bytes(&self) -> Vec { self.version().to_bytes() @@ -138,7 +140,7 @@ macro_rules! reads { reads!(Data); reads!(Snapshot); -/// What a view draws a dataset from. +/// A source a view draws a dataset from. #[wasm_bindgen] pub struct Source(pub(crate) latkit::Source); @@ -150,8 +152,8 @@ impl Source { } } -/// The bytes of the patch `bytes` hold, latkit's or JSON, its slices of files fetched relative to -/// `base`. +/// Reads the patch in `bytes`, as latkit bytes or JSON, and returns it as latkit bytes. File slices +/// are fetched relative to `base`. #[wasm_bindgen(js_name = readPatch)] pub fn read_patch(bytes: Vec, base: String) -> Task { task(async move { diff --git a/crates/latkit-js/src/error.rs b/crates/latkit-js/src/error.rs index f9eb236b..d090800f 100644 --- a/crates/latkit-js/src/error.rs +++ b/crates/latkit-js/src/error.rs @@ -1,6 +1,6 @@ -//! Errors as JavaScript throws them: an `Error` with latkit's message, its `code` (`invalid`, -//! `unsupported`, `limit`, `io`, `device-lost`, `cancelled`, or `timeout`), and for invalid input the -//! `path` to the part. +//! latkit errors thrown as JavaScript `Error`s, with latkit's message, a `code` (`invalid`, +//! `unsupported`, `limit`, `io`, `device-lost`, `cancelled`, or `timeout`), and, for invalid input, +//! the `path` to the invalid part. use js_sys::Reflect; use wasm_bindgen::JsValue; diff --git a/crates/latkit-js/src/gpu.rs b/crates/latkit-js/src/gpu.rs index 89f9cef9..63d10a84 100644 --- a/crates/latkit-js/src/gpu.rs +++ b/crates/latkit-js/src/gpu.rs @@ -1,4 +1,4 @@ -//! The GPU: views and canvases are made of it, and its frames, budget, and work read from it. +//! The GPU: it creates views and canvases, and reports its frames, budget, and work. use wasm_bindgen::JsCast; use wasm_bindgen::prelude::*; @@ -19,8 +19,9 @@ impl Gpu { Ok(Gpu(latkit::Gpu::new().await?)) } - /// A view of `scene` (JSON), telling `tell` its events from the start: each batch a JSON array of - /// `{ type, detail }`. A canvas shows it; a shot draws it with or without one. + /// Creates a view of `scene` (JSON) that sends its events to `tell` from the start, each batch + /// as a JSON array of `{ type, detail }`. A canvas shows it. A shot draws it with or without + /// a canvas. pub fn view(&self, scene: &str, tell: js_sys::Function) -> Result { let view = latkit::web::PageView::new(latkit::View::new(&self.0, json("scene", scene)?)?); let telling = view.on(move |events| { @@ -29,8 +30,9 @@ impl Gpu { Ok(View { view, _telling: telling }) } - /// A canvas showing `views`, each placed `at` four fractions of its own, in `target`: a `` - /// element, or an `OffscreenCanvas` at `pixel_ratio` pixels a CSS pixel. + /// Creates a canvas showing `views` in `target`, each at its four fractions of `at`. `target` + /// is a `` element, or an `OffscreenCanvas` at `pixel_ratio` pixels per CSS pixel + /// (default 1). pub fn canvas( &self, target: &JsValue, @@ -44,7 +46,7 @@ impl Gpu { } else if let Some(offscreen) = target.dyn_ref::() { latkit::web::Canvas::offscreen(&self.0, offscreen, layout, pixel_ratio.unwrap_or(1.0))? } else { - return Err(latkit::Error::invalid("a canvas draws into a or an OffscreenCanvas").into()); + return Err(latkit::Error::invalid("the target must be a or an OffscreenCanvas").into()); }; Ok(Canvas(canvas)) } @@ -63,45 +65,45 @@ impl Gpu { self.0.lost() } - /// How much GPU memory data takes, as JSON `{ bytes, uploadPerFrame }`. + /// The GPU memory budget for data, as JSON `{ bytes, uploadPerFrame }`. #[wasm_bindgen(getter)] pub fn budget(&self) -> String { to_json(&self.0.budget()) } - /// Sets how much GPU memory data takes, from JSON `{ bytes, uploadPerFrame }`. + /// Sets the GPU memory budget for data from JSON `{ bytes, uploadPerFrame }`. #[wasm_bindgen(js_name = setBudget)] pub fn set_budget(&self, budget: &str) -> Result<(), Error> { self.0.set_budget(json("budget", budget)?); Ok(()) } - /// What the GPU did since it was made, counted exactly, as JSON. + /// Exact counts of the GPU's work since it was created, as JSON. #[wasm_bindgen(getter)] pub fn work(&self) -> String { to_json(&self.0.work()) } - /// Bytes on the GPU every view of it draws from. + /// Bytes resident on the GPU that its views draw from. #[wasm_bindgen(getter)] pub fn resident(&self) -> f64 { self.0.resident() as f64 } - /// Bytes waiting to upload, for every view of it. + /// Bytes waiting to upload, across all its views. #[wasm_bindgen(getter)] pub fn uploading(&self) -> f64 { self.0.uploading() as f64 } - /// Draws one frame now at `now`, milliseconds as an animation frame's timestamp; from the first, - /// no animation frames are asked for. + /// Draws one frame immediately, with `now` as its timestamp in milliseconds. After the first + /// call, no animation frames are requested. pub fn step(&self, now: f64) { latkit::web::step(&self.0, now); } - /// Done once the GPU finished what was submitted, every readback landed, and every pipeline - /// asked for compiled. + /// Resolves once the GPU has finished its submitted work, every readback has arrived, and every + /// requested pipeline has compiled. pub fn done(&self) -> Task { let gpu = self.0.clone(); task(async move { @@ -110,7 +112,7 @@ impl Gpu { }) } - /// Done when work a view waits on lands after this call, or a view asks for a frame. + /// Resolves when work a view depends on arrives after this call, or a view requests a frame. pub fn landed(&self) -> Task { let landed = latkit::web::landed(&self.0); task(async move { @@ -120,6 +122,6 @@ impl Gpu { } } -/// A listener; freeing it stops it. +/// A frame listener. Freeing it stops it. #[wasm_bindgen] pub struct Listener(#[allow(dead_code, reason = "kept until freed")] latkit::web::Listener); diff --git a/crates/latkit-js/src/lib.rs b/crates/latkit-js/src/lib.rs index cb53a3ee..17f1767b 100644 --- a/crates/latkit-js/src/lib.rs +++ b/crates/latkit-js/src/lib.rs @@ -1,10 +1,11 @@ -//! The latkit npm package: data and views for the browser, over WebGPU. +//! The WebAssembly binding behind the latkit npm package: data and views in the browser, over +//! WebGPU. //! -//! What crosses is bytes or JSON, in the forms `latkit` reads and writes. Patches cross as their byte -//! frames, which the package's TypeScript writes itself; scenes, cameras, fits, queries, and rows go -//! in as JSON, and events, hits, stats, and work come out as JSON, a view's events a batch at a time. -//! A query's columns come out as typed arrays, copied once. What the page waits for crosses as a -//! task it may cancel. +//! Values cross as bytes or JSON, in the forms `latkit` reads and writes. Patches cross as byte +//! frames, which the package's TypeScript writes. Scenes, cameras, fits, queries, and rows go in as +//! JSON. Events, hits, stats, and work come out as JSON, with a view's events in batches. A query's +//! columns come out as typed arrays, copied once. Asynchronous results cross as a cancellable +//! `Task`. #![cfg(target_arch = "wasm32")] #![allow(unsafe_code, reason = "wasm-bindgen's generated glue")] @@ -34,22 +35,22 @@ fn start() { console_error_panic_hook::set_once(); } -/// What `text` holds, in the JSON form latkit reads; or why not, at the path from `what`. +/// Deserializes `text` from latkit's JSON form. Error paths are prefixed with `what`. fn json(what: &str, text: &str) -> Result { latkit::from_json(text).map_err(|e| e.at(what).into()) } -/// `value` in the JSON form the npm package's types describe. +/// Serializes `value` to the JSON form the npm package's types describe. fn to_json(value: &impl serde::Serialize) -> String { - serde_json::to_string(value).expect("view state serializes") + serde_json::to_string(value).expect("latkit values serialize to JSON") } -/// Has views' jobs done by `run`: `(bytes: Uint8Array) => Promise`, the page's workers. +/// Runs views' jobs with `run`, the page's workers: `(bytes: Uint8Array) => Promise`. #[wasm_bindgen(js_name = setJobs)] pub fn set_jobs(run: js_sys::Function) { latkit::host::Browser::set_jobs(run); } -/// Does a job a view handed the page, in a worker: its answer's bytes. +/// Runs a job a view handed to the page, in a worker, and returns the result's bytes. #[wasm_bindgen] pub fn job(bytes: &[u8]) -> Vec { latkit::host::Job::from_bytes(bytes.to_vec()).run() @@ -61,8 +62,8 @@ pub fn module() -> JsValue { wasm_bindgen::module() } -/// A CSS gradient through `map` (JSON), a catalog name or colors of your own, toward `direction`: the -/// colors the GPU draws, a band each for a categorical map's categories. +/// A CSS `linear-gradient` toward `direction` through `map`, given as JSON (a catalog name or +/// custom colors), in the colors the GPU draws. A categorical map gets one band per category. #[wasm_bindgen(js_name = colormapCss)] pub fn colormap_css(map: &str, direction: &str) -> Result { let map: latkit::Colormap = json("colormap", map)?; diff --git a/crates/latkit-js/src/pending.rs b/crates/latkit-js/src/pending.rs index 7ff43b0a..9adcea3d 100644 --- a/crates/latkit-js/src/pending.rs +++ b/crates/latkit-js/src/pending.rs @@ -1,13 +1,13 @@ -//! What the page waits for: a promise of what latkit works out, which the page may stop waiting for. -//! Cancelling drops the work's future, so what it waited on and nothing else wants is skipped: a -//! view's query drops out of its queue, a host's job that has not begun never runs. +//! Cancellable promises of latkit results. Cancelling drops the work's future, so pending work that +//! nothing else needs is skipped: a view's query leaves its queue, and a host job that has not +//! started never runs. use futures_util::future::{AbortHandle, Abortable}; use wasm_bindgen::prelude::*; use crate::Error; -/// A promise of what latkit works out, and how to stop waiting for it. +/// A promise of a latkit result, and a way to cancel it. #[wasm_bindgen] pub struct Task { promise: js_sys::Promise, @@ -16,19 +16,19 @@ pub struct Task { #[wasm_bindgen] impl Task { - /// What it works out: rejected with a `cancelled` error once cancelled. + /// The result. Rejects with a `cancelled` error once cancelled. #[wasm_bindgen(getter)] pub fn promise(&self) -> js_sys::Promise { self.promise.clone() } - /// Stops the work, dropping what it waits on. + /// Cancels the work, dropping what it waits on. pub fn cancel(&self) { self.abort.abort(); } } -/// The task of `future`, its error thrown as latkit's. +/// Wraps `future` in a task whose promise rejects with its errors as latkit errors. pub(crate) fn task>(future: impl Future> + 'static) -> Task { let (abort, registration) = AbortHandle::new_pair(); let promise = wasm_bindgen_futures::future_to_promise(async move { diff --git a/crates/latkit-js/src/shot.rs b/crates/latkit-js/src/shot.rs index 0061890a..c47ac4a5 100644 --- a/crates/latkit-js/src/shot.rs +++ b/crates/latkit-js/src/shot.rs @@ -1,7 +1,8 @@ -//! Pictures and videos of views: a shot crosses as JSON, `{ size, scale, panels, transparent, capture, -//! elapsed }` with times in seconds, with the views of its panels, whose canvases hold their last frames -//! meanwhile; the page bounds how long it waits. An image's pixels cross once and its files are -//! written in workers; a video hands back the bytes each frame made, for the page to write. +//! Pictures and videos of views. A shot crosses as JSON, `{ size, scale, panels, transparent, +//! capture, elapsed }` with times in seconds, along with its panels' views. Canvases showing those +//! views keep their last frames meanwhile. The page decides how long to wait. An image's pixels +//! cross once, and its files are encoded in workers. A video returns the bytes each frame produced, +//! for the page to write. use std::cell::RefCell; use std::io::Write; @@ -37,31 +38,33 @@ impl Image { self.0.rgba.to_vec() } - /// The image written in a worker as a `png`, `jpeg` at `quality` from 1 to 100, or `webp` file. + /// Encodes the image in a worker as a `png`, `jpeg` (at `quality` 1 to 100, default 90), or + /// `webp` file. pub fn encode(&self, format: &str, quality: Option) -> Result { let quality = match quality { None => 90, Some(q) if (1.0..=100.0).contains(&q) => q.round() as u8, - Some(_) => return Err(latkit::Error::invalid("a quality runs from 1 to 100").at("quality").into()), + Some(_) => return Err(latkit::Error::invalid("expected 1 to 100").at("quality").into()), }; let format = match format { "png" => latkit::ImageFormat::Png, "jpeg" => latkit::ImageFormat::Jpeg { quality }, "webp" => latkit::ImageFormat::WebP, - other => return Err(latkit::Error::invalid(format!("no image format {other:?}")).at("format").into()), + other => return Err(latkit::Error::invalid(format!("unknown image format {other:?}")).at("format").into()), }; let job = latkit::host::Job::encode(&self.0, format); Ok(task(async move { let file = latkit::host::Browser.spawn(job).await?; if file.is_empty() { - return Err(latkit::Error::invalid(format!("the image could not be written as {format}"))); + return Err(latkit::Error::invalid(format!("the image could not be encoded as {format}"))); } Ok(js_sys::Uint8Array::from(&file[..])) })) } } -/// The picture `shot` (JSON) takes of `views`, one per panel, once they are complete as it captures. +/// Takes `shot`'s (JSON) picture of `views`, one per panel, once they are complete as its capture +/// says. #[wasm_bindgen] pub fn image(shot: &str, views: Vec) -> Result { let shot = shot_of(shot)?; @@ -69,7 +72,7 @@ pub fn image(shot: &str, views: Vec) -> Result { Ok(task(async move { Ok(Image(latkit::web::image(&shot, &views).await?)) })) } -/// The shot `json` describes, waiting as long as the page does. +/// The shot `json` describes, with no deadline, since the page decides how long to wait. fn shot_of(json: &str) -> Result { Ok(crate::json::("shot", json)?.deadline(None)) } @@ -89,10 +92,10 @@ impl Write for Written { } } -/// A video of a shot's pictures of its views, encoded by WebCodecs as frames are drawn. +/// A video of a shot's pictures of its views, encoded with WebCodecs as frames are drawn. #[wasm_bindgen] pub struct Video { - /// The video, or nothing while a frame draws it, and once it is finished. + /// The video. `None` while a frame is drawing it, and after it is finished or cancelled. inner: Rc>>>>, views: Vec, written: Written, @@ -103,7 +106,7 @@ pub struct Video { #[wasm_bindgen] impl Video { - /// Seconds into the video the next frame shows at. + /// The time in seconds at which the next frame shows. #[wasm_bindgen(getter)] pub fn time(&self) -> f64 { *self.frames.borrow() as f64 / self.fps @@ -115,14 +118,14 @@ impl Video { *self.frames.borrow() as f64 } - /// Draws the next frame of its views; resolves to the bytes of the file ready to write, once there - /// is room for another frame. + /// Draws the next frame of its views. Resolves to the file bytes ready to write, once there is + /// room for another frame. pub fn frame(&self) -> js_sys::Promise { let (inner, written, frames) = (self.inner.clone(), self.written.clone(), self.frames.clone()); let views = self.views.clone(); self.cancellable(async move { let taken = inner.borrow_mut().take(); - let error = || latkit::Error::invalid("a video draws one frame at a time, until it is finished"); + let error = || latkit::Error::invalid("the video is finished or cancelled"); let mut video = taken.ok_or_else(|| JsValue::from(Error::from(error())))?; let drawn = latkit::web::frame(&mut video, &views).await; *frames.borrow_mut() = video.frames(); @@ -132,7 +135,8 @@ impl Video { }) } - /// Cancels an in-flight operation, releasing its views and encoder when the future is dropped. + /// Cancels the operation in progress and ends the video. Its views and encoder are released + /// once the cancelled future is dropped. pub fn cancel(&self) { if let Some(active) = self.active.borrow_mut().take() { active.abort(); @@ -140,7 +144,7 @@ impl Video { self.inner.borrow_mut().take(); } - /// Encodes every frame under way and ends the video; resolves to the file's last bytes. + /// Encodes the remaining frames and ends the video. Resolves to the file's last bytes. pub fn finish(&self) -> js_sys::Promise { let (inner, written) = (self.inner.clone(), self.written.clone()); self.cancellable(async move { @@ -180,13 +184,13 @@ impl Drop for Video { } } -/// A video of `shot`'s (JSON) pictures of `views`, one per panel, at `fps` frames a second, as MP4 at -/// `bitrate` bits a second or about 0.15 bits a pixel a frame. +/// A video of `shot`'s (JSON) pictures of `views`, one per panel, at `fps` frames per second. It +/// encodes MP4 at `bitrate` bits per second, by default about 0.15 bits per pixel per frame. #[wasm_bindgen] pub fn video(shot: &str, views: Vec, fps: f64, bitrate: Option) -> Result { let shot = shot_of(shot)?; let views = views_of(views); - let gpu = views.first().ok_or_else(|| latkit::Error::invalid("a video draws a view or more").at("layout"))?.gpu(); + let gpu = views.first().ok_or_else(|| latkit::Error::invalid("expected at least one view").at("layout"))?.gpu(); let written = Written::default(); let mut mp4 = latkit::Mp4::new(written.clone()); if let Some(bitrate) = bitrate { @@ -203,8 +207,8 @@ pub fn video(shot: &str, views: Vec, fps: f64, bitrate: Option) -> }) } -/// Has videos encoded by what `create` makes of `(width, height, fps, bitrate)`: the npm package's -/// WebCodecs encoder. +/// Sets the factory for video encoders: `create(width, height, fps, bitrate)` returns one. The npm +/// package passes its WebCodecs encoder. #[wasm_bindgen(js_name = setEncoder)] pub fn set_encoder(create: js_sys::Function) { latkit::host::Browser::set_encoder(create); diff --git a/crates/latkit-js/src/view.rs b/crates/latkit-js/src/view.rs index 726f4dff..f019cad3 100644 --- a/crates/latkit-js/src/view.rs +++ b/crates/latkit-js/src/view.rs @@ -1,5 +1,5 @@ -//! Views: a scene drawn from the sources it is given, telling its events from the start; its state -//! read and set between frames, its questions answered by the frames that draw it. +//! Views: a scene drawn from its sources, sending its events from the start. Its state is read and +//! set between frames, and its queries are answered by the frames that draw it. use wasm_bindgen::prelude::*; @@ -8,7 +8,7 @@ use crate::data::{Snapshot, Source}; use crate::pending::{Task, task}; use crate::{Error, json, to_json}; -/// A view of a scene, telling its events from the start. +/// A view of a scene, sending its events from the start. #[wasm_bindgen] pub struct View { pub(crate) view: latkit::web::PageView, @@ -17,14 +17,14 @@ pub struct View { #[wasm_bindgen] impl View { - /// Draws `scene` (JSON) instead; an invalid scene changes nothing. + /// Replaces the scene with `scene` (JSON). An invalid scene changes nothing. #[wasm_bindgen(js_name = setScene)] pub fn set_scene(&self, scene: &str) -> Result<(), Error> { let scene = json("scene", scene)?; Ok(self.view.with(|v| v.set_scene(scene))?) } - /// Draws the datasets `sources` give, in place of those given before. + /// Replaces the view's sources with `sources`. #[wasm_bindgen(js_name = setSources)] pub fn set_sources(&self, sources: Vec) -> Result<(), Error> { Ok(self.view.with(|v| v.set_sources(sources.into_iter().map(|s| s.0)))?) @@ -51,7 +51,7 @@ impl View { self.view.read(|v| to_json(v.camera())) } - /// Looks through the camera (JSON) at once. + /// Switches to `camera` (JSON) immediately. #[wasm_bindgen(js_name = setCamera)] pub fn set_camera(&self, camera: &str) -> Result<(), Error> { let camera = json("camera", camera)?; @@ -59,7 +59,7 @@ impl View { Ok(()) } - /// Eases to the camera (JSON). + /// Eases to `camera` (JSON). pub fn fly(&self, camera: &str) -> Result<(), Error> { let camera = json("camera", camera)?; self.view.with(|v| v.fly(camera)); @@ -73,8 +73,8 @@ impl View { Ok(()) } - /// Lays every vertex row without a position out again by the scene's layout: the bytes of a patch - /// to each dataset, writing the places into the data. + /// Lays out every vertex row without a position again, using the scene's layout. Resolves to + /// one patch per dataset, as bytes, that writes the computed positions into the data. pub fn arrange(&self) -> Task { let pending = self.view.with(latkit::View::arrange); task(async move { @@ -83,7 +83,7 @@ impl View { }) } - /// Where `position` (x, y, elevation) lands on screen, CSS pixels from the top left. + /// The screen point of `position` (x, y, elevation), in CSS pixels from the top left. pub fn screen(&self, position: &[f64]) -> Option> { let position: [f64; 3] = position.try_into().ok()?; self.view.read(|v| v.screen(position)).map(Vec::from) @@ -102,7 +102,8 @@ impl View { #[wasm_bindgen(setter)] pub fn set_insets(&self, insets: &[f32]) -> Result<(), Error> { - let insets: [f32; 4] = insets.try_into().map_err(|_| latkit::Error::invalid("four insets").at("insets"))?; + let insets: [f32; 4] = + insets.try_into().map_err(|_| latkit::Error::invalid("expected four numbers").at("insets"))?; self.view.with(|v| v.set_insets(insets)); Ok(()) } @@ -117,13 +118,13 @@ impl View { self.view.with(|v| v.set_orbit(degrees_per_second)); } - /// The rows selected, as JSON sets of rows. + /// The selected rows, as JSON sets of rows. #[wasm_bindgen(getter)] pub fn selection(&self) -> String { self.view.read(|v| to_json(&v.selection())) } - /// Selects the rows (JSON sets of rows), and no others. + /// Selects exactly `rows` (JSON sets of rows). #[wasm_bindgen(js_name = setSelection)] pub fn set_selection(&self, rows: &str) -> Result<(), Error> { let rows: Vec = json("rows", rows)?; @@ -131,19 +132,19 @@ impl View { Ok(()) } - /// What the user points at, as a JSON hit or `null`. + /// The row the user points at, as a JSON hit or `null`. #[wasm_bindgen(getter)] pub fn hovered(&self) -> String { self.view.read(|v| to_json(&v.hovered())) } - /// The work under way the last frame stood in for, as JSON. + /// The pending work the last frame drew placeholders for, as JSON. #[wasm_bindgen(getter)] pub fn waiting(&self) -> String { to_json(&self.view.read(latkit::View::waiting)) } - /// Whether the last frame drew everything as the data, scene, and camera say. + /// Whether the last frame was complete, with nothing in `waiting` still pending. #[wasm_bindgen(getter)] pub fn complete(&self) -> bool { self.view.read(latkit::View::complete) @@ -155,13 +156,13 @@ impl View { self.view.read(|v| to_json(&v.failures().collect::>())) } - /// What the view does each frame, as JSON. + /// The view's per-frame statistics, as JSON. #[wasm_bindgen(getter)] pub fn stats(&self) -> String { to_json(&self.view.read(latkit::View::stats)) } - /// Whether the view wants a frame: it changed, or work it waited on landed. + /// Whether the view needs a frame: it changed, or work it waited on arrived. #[wasm_bindgen(getter, js_name = wantsFrame)] pub fn wants_frame(&self) -> bool { self.view.wants_frame() @@ -169,12 +170,13 @@ impl View { /// Every row within `radius` CSS pixels of `point`, nearest first, as JSON hits. pub fn pick(&self, point: &[f32], radius: f32) -> Result { - let point: [f32; 2] = point.try_into().map_err(|_| latkit::Error::invalid("a point is [x, y]").at("point"))?; + let point: [f32; 2] = point.try_into().map_err(|_| latkit::Error::invalid("expected [x, y]").at("point"))?; let pending = self.view.with(|v| v.pick(point, radius)); Ok(task(async move { Ok(JsValue::from_str(&to_json(&pending.await?))) })) } - /// Where the row (JSON) lies on screen, `[x, y]`, or `null`. + /// The screen point of `row` (JSON), as `[x, y]` in CSS pixels, or `null` if no mark draws it + /// in view. #[wasm_bindgen(js_name = screenOf)] pub fn screen_of(&self, row: &str) -> Result { let row: latkit::Row = json("row", row)?; @@ -185,14 +187,14 @@ impl View { })) } - /// The rows joined to the row (JSON), as JSON sets of rows: a vertex's neighbors across edges and - /// nets, or an edge's or a net's vertices. + /// The rows connected to `row` (JSON), as JSON sets of rows: a vertex's neighbors across edges + /// and nets, or an edge's or net's vertices. pub fn neighbors(&self, row: &str) -> Result { let row: latkit::Row = json("row", row)?; Ok(self.view.read(|v| to_json(&v.neighbors(&row)))) } - /// The view, for a canvas to show or a shot to draw. + /// A handle to the view, for a canvas to show or a shot to draw. pub fn handle(&self) -> Handle { Handle(self.view.clone()) } diff --git a/crates/latkit/README.md b/crates/latkit/README.md index ad9f9634..36093ef6 100644 --- a/crates/latkit/README.md +++ b/crates/latkit/README.md @@ -28,7 +28,8 @@ fn main() -> Result<(), latkit::Error> { `cargo install latkit-cli` installs the `latkit` executable. `npm install latkit` installs the browser runtime and plain-data builders. -LATK v1 uses 64-bit absolute frame indexes. Sampled values use `f32`; coordinates use `f64`. -See the [format specification](https://latkit.readthedocs.io/en/latest/formats.html). +The binary format uses 64-bit absolute frame indexes. Sampled values are `f32`; coordinates are +`f64`. +See the [format specification](https://latkit.readthedocs.io/en/latest/guide.html#bytes). -MIT licensed. Notices are included in `THIRD_PARTY.md` and `colormaps/NOTICES.md`. +MIT licensed. Third-party notices are in `THIRD_PARTY.md` and `colormaps/NOTICES.md`. diff --git a/crates/latkit/build.rs b/crates/latkit/build.rs index 43f7e303..9d21de83 100644 --- a/crates/latkit/build.rs +++ b/crates/latkit/build.rs @@ -1,4 +1,5 @@ -//! Generates the Rust side of the GPU contract from the WESL shaders, and bakes the colormap catalog. +//! Build script: generates Rust mirrors of the WESL shaders' types and bind layouts, and +//! precomputes the colormap catalog. fn main() { println!("cargo::rerun-if-changed=build.rs"); diff --git a/crates/latkit/build/colormaps.rs b/crates/latkit/build/colormaps.rs index 0bb9462b..0d0ea5a6 100644 --- a/crates/latkit/build/colormaps.rs +++ b/crates/latkit/build/colormaps.rs @@ -1,5 +1,5 @@ -//! Bakes `colormaps/catalog.txt` into `OUT_DIR/colormaps.rs`: per map its name, label, kind, color -//! count, and 256 sRGB texels, made by the crate's own `ramp`. +//! Compiles `colormaps/catalog.txt` into `OUT_DIR/colormaps.rs`. Each map gets its name, label, +//! kind, color count, and 256 sRGB texels computed by the crate's own `ramp`. use crate::ramp::{Spread, texels}; use std::env; diff --git a/crates/latkit/build/shaders.rs b/crates/latkit/build/shaders.rs index 30ba19c9..82473c8d 100644 --- a/crates/latkit/build/shaders.rs +++ b/crates/latkit/build/shaders.rs @@ -1,17 +1,17 @@ -//! Links every WESL entry, validates it with naga, and writes `OUT_DIR/shaders.rs`: +//! Links every WESL entry, validates it with naga, and writes `OUT_DIR/shaders.rs`, containing: //! -//! - every constant and struct of `lib/types.wesl`, as Rust constants and `#[repr(C)]` mirrors whose -//! size is checked against WGSL; -//! - the shared bind group layouts of `lib/bind.wesl` (group 0 per view, group 1 per mark); -//! - per shader and variant: its declarations, entry points, the layout of its pass group (group 2), -//! and whether it reads the shared groups at all. +//! - Every constant and struct of `lib/types.wesl`, as Rust constants and `#[repr(C)]` mirrors +//! whose sizes are checked against WGSL. +//! - The shared bind group layouts of `lib/bind.wesl` (group 0 per view, group 1 per mark). +//! - Per shader variant: its declarations, pipeline constant names, pass group layout (group 2), +//! and whether it uses the shared groups. //! -//! Variants share most of what they link, so each distinct top-level declaration is stored once, in -//! `OUT_DIR/shaders.wgsl`, and a variant is the list of its declarations, which join back into exactly -//! the module it linked. +//! Variants share most of their linked code, so each distinct top-level declaration is stored once +//! in `OUT_DIR/shaders.wgsl`. A variant is a list of declaration indices that join back into +//! exactly the module it linked. //! -//! It also writes each variant's whole module and `OUT_DIR/shaders.json`, every variant's file, entry -//! points, and the pipeline constants it is made with, from which the browser gate times each +//! It also writes each variant's full module, and `OUT_DIR/shaders.json` listing every variant's +//! file, entry points, and pipeline constant values. The browser gate reads it to time each //! pipeline in Chrome. use std::collections::{BTreeMap, HashMap}; @@ -23,7 +23,7 @@ use std::path::{Path, PathBuf}; use naga::valid::{Capabilities, ModuleInfo, ValidationFlags, Validator}; use naga::{AddressSpace, ArraySize, ImageClass, ImageDimension, ScalarKind, StorageAccess, TypeInner, VectorSize}; -/// A shader module and the variants it is linked in: each variant turns on its WESL features. +/// A shader module and its variants. Each variant enables its listed WESL features. struct Entry { module: &'static str, variants: &'static [(&'static str, &'static [&'static str])], @@ -60,15 +60,15 @@ const ENTRIES: &[Entry] = &[ const FEATURES: &[&str] = &["writes", "atomic", "custom", "multisampled", "lists", "ports", "focused"]; -/// What a custom marker supplies, for validating its variant. +/// A placeholder custom marker function, appended to validate the custom variants. const CUSTOM_MARKER: &str = "fn marker(f: MarkerFragment) -> MarkerColor { return MarkerColor(f.color, 0.0); }"; -/// Uniforms bound at a dynamic offset: each mark's, and each probe dispatch's step. +/// Uniforms bound at a dynamic offset: the per-mark uniform and each probe dispatch's step. const DYNAMIC: &[&str] = &["mark", "step"]; -/// The values each pipeline constant is made with, as constants of `lib/types.wesl` or booleans: the -/// primitive a probe or a label's anchor reads, the path a stroke reads, and whether channels read -/// joins, samples, and the view. +/// The values pipelines use for each pipeline constant, as `lib/types.wesl` constants or booleans. +/// `PRIMITIVE` is the primitive a probe or label anchor reads, and `PATH` the path a stroke reads. +/// The booleans say whether channels read joins, samples, and the view. const OVERRIDES: &[(&str, &[&str])] = &[ ("PRIMITIVE", &["POINTS", "STROKES", "TRIANGLES"]), ("PATH", &["PATH_ENDS", "PATH_TRACE", "PATH_GEOMETRY", "PATH_POLES"]), @@ -101,7 +101,7 @@ pub(crate) fn generate() { for entry in ENTRIES { for &(variant, features) in entry.variants { let source = link(&root, entry.module, features, false); - // A custom marker's variant lacks the marker an application supplies; a stand-in validates it. + // Custom variants lack the application's marker, so validation appends a placeholder. let custom = features.contains(&"custom"); let checked = if custom { format!("{source}\n{CUSTOM_MARKER}") } else { source.clone() }; let (module, info) = validate(&format!("{}.{variant}", entry.module), &checked); @@ -127,7 +127,7 @@ pub(crate) fn generate() { fs::write(out.join("shaders.wgsl"), &pieces.text).unwrap(); writeln!( rust, - "/// Where each declaration lies in `shaders.wgsl`.\npub(crate) static PIECES: &[(u32, u32)] = &{:?};\n", + "/// Each declaration's byte range in `shaders.wgsl`.\npub(crate) static PIECES: &[(u32, u32)] = &{:?};\n", pieces.ranges ) .unwrap(); @@ -137,7 +137,7 @@ pub(crate) fn generate() { fs::write(out.join("shaders.json"), format!("[\n{}\n]\n", manifest.join(",\n"))).unwrap(); } -/// Top-level declarations, each stored once: their text back to back, and where each lies. +/// Deduplicated top-level declarations: their concatenated text and each one's byte range. #[derive(Default)] struct Pieces { text: String, @@ -146,8 +146,8 @@ struct Pieces { } impl Pieces { - /// The declarations of `source`, as indices, storing those not stored yet; they join back into - /// `source` exactly. + /// Splits `source` into declarations and returns their indices, storing new ones. The + /// declarations join back into exactly `source`. fn add(&mut self, source: &str) -> Vec { let refs: Vec = source .split(SEPARATOR) @@ -169,11 +169,12 @@ impl Pieces { } } -/// What separates top-level declarations in a linked module, and joins them back. +/// The separator between top-level declarations in a linked module, used to split and rejoin them. const SEPARATOR: &str = "\n\n"; -/// A variant as JSON: its file, whether it draws, its compute entry points, every combination of the -/// pipeline constants it declares, and the WGSL appended to make it whole (a custom marker's stand-in). +/// A variant as JSON: its file, whether it draws, its compute entry points, every combination of +/// its pipeline constants that pipelines use, and the WGSL to append to complete it (the +/// placeholder custom marker). fn manifest_entry(shader: &str, file: &str, module: &naga::Module, types: &naga::Module, custom: bool) -> String { let compute: Vec = module .entry_points @@ -215,9 +216,9 @@ fn manifest_entry(shader: &str, file: &str, module: &naga::Module, types: &naga: ) } -/// Whether a pipeline of `module` is ever made with `constants`: only channels go without joins or -/// samples; faces read their ends and areas their geometry; a pipeline made for points reads no path, -/// so it keeps the default one, and one made for triangles reads a face's ends or an area's geometry. +/// Whether any pipeline of `module` uses `constants`. Only channel evaluation (`compute::evaluate`) +/// runs without joins or samples. Faces read their ends, and areas their geometry. Points read no +/// path, so they keep the default `PATH_ENDS`. Triangles read a face's ends or an area's geometry. fn made(module: &str, constants: &[(&str, u32)], types: &naga::Module) -> bool { let value = |name: &str| constants.iter().find(|(n, _)| *n == name).map(|&(_, v)| v); if module != "compute::evaluate" && (value("JOINED") == Some(0) || value("TIMED") == Some(0)) { @@ -281,7 +282,7 @@ fn validate(name: &str, source: &str) -> (naga::Module, ModuleInfo) { (module, info) } -/// Every `const` of a scalar type, as a Rust constant. +/// Writes every `u32`, `i32`, and `f32` constant as a Rust constant. fn mirror_consts(module: &naga::Module, rust: &mut String) { for (_, constant) in module.constants.iter() { let Some(name) = &constant.name else { continue }; @@ -296,7 +297,7 @@ fn mirror_consts(module: &naga::Module, rust: &mut String) { rust.push('\n'); } -/// Every struct as a `#[repr(C)]` mirror with explicit padding. +/// Writes every struct as a `#[repr(C)]` mirror with explicit padding and a size assertion. fn mirror_structs(module: &naga::Module, rust: &mut String) { for (_, ty) in module.types.iter() { let (Some(name), TypeInner::Struct { members, span }) = (&ty.name, &ty.inner) else { continue }; @@ -352,7 +353,8 @@ fn rust_type(module: &naga::Module, ty: naga::Handle) -> (String, u3 } } -/// The bindings of `group` as `Binding` literals; with `info`, visibility is where entry points use them. +/// The bindings of `group` as `Binding` literals. With `info`, each binding is visible to the +/// stages whose entry points use it. Without `info`, every stage sees it. fn bindings(module: &naga::Module, info: Option<&ModuleInfo>, group: u32) -> Vec { let mut found = BTreeMap::new(); for (handle, global) in module.global_variables.iter() { @@ -400,12 +402,12 @@ fn bindings(module: &naga::Module, info: Option<&ModuleInfo>, group: u32) -> Vec let dynamic = global.name.as_deref().is_some_and(|name| DYNAMIC.contains(&name)); let entry = format!("Binding {{ slot: {}, kind: {kind}, stages: {stages}, dynamic: {dynamic} }}", binding.binding); - assert!(found.insert(binding.binding, entry).is_none(), "binding {group}.{} twice", binding.binding); + assert!(found.insert(binding.binding, entry).is_none(), "binding {group}.{} declared twice", binding.binding); } found.into_values().collect() } -/// Every group 0 and 1 binding a shader declares must match the shared declaration. +/// Asserts that every group 0 and 1 binding a shader declares matches the shared declaration. fn check_shared(module: &naga::Module, shared: &naga::Module, name: &str) { for (_, global) in module.global_variables.iter() { let Some(binding) = &global.binding else { continue }; diff --git a/crates/latkit/shaders/compute/bounds.wesl b/crates/latkit/shaders/compute/bounds.wesl index e3629b7d..51a62494 100644 --- a/crates/latkit/shaders/compute/bounds.wesl +++ b/crates/latkit/shaders/compute/bounds.wesl @@ -1,11 +1,12 @@ -// What marks draw, measured for fitting: the least and greatest x, y, and z of each mark's shown -// instances in position units, as ordered words, through the same geometry the draws use: a vertex -// as its box, a stroke as its points, an area as its triangles' corners. While a fit measures chosen -// rows, only rows marked FIT count. +// Extents for fitting: the least and greatest x, y, and z of each mark's shown instances, in +// position units, as order-preserving words. It uses the same geometry as the draws: a vertex's +// box, a stroke's points, and an area's triangle corners. While a fit measures chosen rows, only +// rows marked `FIT` count. import package::lib::types::{NONE, FIT, PATH_ENDS, PATH_GEOMETRY, EXTENT_WORDS, VERTEX_POSITION, HALF_UNITS, item}; -import package::lib::bind::{frame, eye, mark}; +import package::lib::bind::{frame, mark}; import package::lib::focus::focus_of; +import package::lib::mark::px_per_length; import package::lib::reduce::ordered; import package::lib::state::{row_at, vertex_at}; import package::lib::points::{placed, vertex_half}; @@ -43,11 +44,11 @@ fn points(@builtin(global_invocation_id) id: vec3u) { if (v.flags & 1u) == 0u || !placed(v) { return; } - // A marker sized in units is part of the box. One sized in pixels is that size at any zoom, so - // it counts as its position, and the fit's padding holds its pixels: measured at the zoom shown, - // its pixels would stand for any number of units. + // A marker sized in units is part of the box. A marker sized in pixels has that size at any + // zoom, so only its position counts, and the fit's padding covers its pixels. Measured at the + // current zoom, its pixels could correspond to any number of units. let units = (v.flags & HALF_UNITS) != 0u; - let half = select(vec2f(0.0), vertex_half(v) / eye.px_per_unit.x, units); + let half = select(vec2f(0.0), vertex_half(v) / px_per_length(), units); let p = v.hi + v.lo; extend(vec3f(p - half, v.elevation)); extend(vec3f(p + half, v.elevation)); diff --git a/crates/latkit/shaders/compute/candidates.wesl b/crates/latkit/shaders/compute/candidates.wesl index 5be99521..ba501ca8 100644 --- a/crates/latkit/shaders/compute/candidates.wesl +++ b/crates/latkit/shaders/compute/candidates.wesl @@ -1,9 +1,10 @@ -// The first step of label placement: each labeled row's label measured where its anchor puts it, by -// the size of its text, as set or as measured while it is not, and keyed by its priority, -// then whether it showed before, then its id: a label that arrives later still displaces lesser -// ones, and among equals those shown keep their place. `compute/labels.wesl` places them. Made for -// one primitive, it holds only that one's anchors. A diagram's labels scale with its sheet, so they -// need no placement: only this step runs there, to ask for the texts of labels in view. +// The first step of label placement. It measures each labeled row's label box at its anchor, from +// its text's size (as set, or as measured until it is set). Each label gets a key: its priority, +// then whether it was placed in the previous frame, then its id. A label that arrives later still +// displaces lower-priority ones, and among equals, labels already shown keep their place. +// `compute/labels.wesl` places them. Each pipeline is made for one primitive and compiles only that +// primitive's anchors. A diagram's labels scale with its sheet and need no placement, so only this +// step runs there, to request the texts of labels in view. import package::lib::types::{ VISIBLE, LABEL_CODE, LABEL_PRIORITY, CANDIDATE_BOX, CANDIDATE_KEY, CANDIDATE_STATE, WANTS, PLACED_LABEL, @@ -30,7 +31,7 @@ fn candidates(@builtin(global_invocation_id) id: vec3u) { let code_bits = word(extra_at(row, mark.label + LABEL_CODE)); var next = select(0u, STICKY, sticky); let flags = word(row_at(row) + mark.stride - 1u); - // A row whose tile leaves keeps no label. + // A row whose tile is leaving gets no label. if missing_bits(code_bits) || (flags & VISIBLE) == 0u || drawn.leaving { put(at + CANDIDATE_STATE, next); return; @@ -43,8 +44,8 @@ fn candidates(@builtin(global_invocation_id) id: vec3u) { let diagram = frame.space == SPACE_DIAGRAM; let unset = (uv0 & TEXT_UNSET) != 0u; if (uv0 == 0u && uv1 == 0u) || (unset && diagram) { - // The atlas lacks this text, and the label cannot wait for placement to ask for it: ask now, - // if it could show, legible and near the view. + // The atlas lacks this text, and the label cannot wait for placement to request it. Request + // it now if the label could show: legible and near the view. let near = 0.25 * eye.size; if a.shown && legibility(size) > 0.0 && in_view(a.at - near, a.at + near) { want(code); @@ -57,7 +58,7 @@ fn candidates(@builtin(global_invocation_id) id: vec3u) { } let t = text_at(uv0 & ~TEXT_UNSET, uv1, size, a.at, a.align); if a.shown && in_view(t.lo, t.hi) { - // A diagram shows every label in view that fits; elsewhere `ask` notes those placed. + // A diagram marks every label in view as seen here; elsewhere `ask` marks placed ones. if diagram { seen(code); } diff --git a/crates/latkit/shaders/compute/compose.wesl b/crates/latkit/shaders/compute/compose.wesl index 25690aa3..70d43c37 100644 --- a/crates/latkit/shaders/compute/compose.wesl +++ b/crates/latkit/shaders/compute/compose.wesl @@ -1,6 +1,6 @@ -// A shot's panels composed into the bytes a consumer takes, so no pixel is touched on the CPU: RGBA -// rows with straight alpha for an image, or NV12 planes (BT.709, limited range) for an encoder. One -// dispatch per panel writes its panel of the picture in whole words; a fill writes the background +// Composes a shot's panels into output bytes on the GPU, so the CPU touches no pixels: RGBA rows +// with straight alpha for an image, or NV12 planes (BT.709, limited range) for a video encoder. One +// dispatch per panel writes its part of the picture in whole words. A fill writes the background // where no panel draws. import package::lib::types::Compose; @@ -14,8 +14,8 @@ const LUMA: vec3f = vec3f(0.2126, 0.7152, 0.0722); const BLUE: f32 = 1.8556; const RED: f32 = 1.5748; -// A panel's premultiplied color as the picture keeps it: straight with its alpha in a transparent -// image, else over black. +// A panel's premultiplied color as the picture stores it: straight alpha in a transparent image, +// else composited over black. fn kept(c: vec4f) -> vec4f { if compose.alpha == 0u { return vec4f(c.rgb, 1.0); @@ -26,7 +26,7 @@ fn kept(c: vec4f) -> vec4f { return vec4f(c.rgb / c.a, c.a); } -// Writes the 4×2 block of NV12 at `at` from its eight colors, rows of four. +// Writes the 4×2 NV12 block at `at` from its eight colors, in two rows of four. fn write_block(at: vec2u, rgb: array) { var chroma = vec4f(0.0); for (var row = 0u; row < 2u; row++) { @@ -35,7 +35,7 @@ fn write_block(at: vec2u, rgb: array) { let c = rgb[row * 4u + column]; let y = dot(c, LUMA); luma[column] = y; - // Cb and Cr of each 2×2, averaged. + // Cb and Cr, averaged over each 2×2. let pair = column / 2u * 2u; chroma[pair] += (c.b - y) / BLUE * 0.25; chroma[pair + 1u] += (c.r - y) / RED * 0.25; diff --git a/crates/latkit/shaders/compute/evaluate.wesl b/crates/latkit/shaders/compute/evaluate.wesl index dcdea808..a69249c8 100644 --- a/crates/latkit/shaders/compute/evaluate.wesl +++ b/crates/latkit/shaders/compute/evaluate.wesl @@ -1,15 +1,15 @@ -// The expression machine: every row of a mark runs the mark's channels, which store into the row's -// state words. All rows run the same instructions, so control flow stays uniform across a workgroup. -// A dispatch covers the pages of rows the mark lists, only those whose rows changed when only those -// did. +// The expression machine: each row of a mark runs the mark's channels, which store into the row's +// state words. All rows run the same instructions, so control flow stays uniform across a +// workgroup. A dispatch covers the row pages the mark lists: only the changed pages, when only some +// rows changed. // -// Evaluating knows nothing of transitions: while a mark eases, its channels store into its settled -// words, and each frame `ease` writes what every row shows from its frozen and settled words by the -// mark's progress: numbers and colors mix, positions move by their offset, a row whose visibility -// flipped fades, rows past the live ones left and fade out, and the rest show as evaluated. No -// channel runs to ease. Every row here is a held row, its words in pages its mark's page tables list. -// Where an edge's or a face's vertices are held is no channel's to store: a step of its own finds it -// (`lib/ends.wesl`). +// Evaluation ignores transitions. During a transition, channels store into the settled words, and +// each frame `ease` computes what every row shows from its frozen and settled words by the mark's +// progress. Numbers and colors mix, positions move by their offset, rows whose visibility flipped +// fade, rows past the live ones (which left) fade out, and other words show as evaluated. Easing +// runs no channels. Every row here is a held row, whose words lie in pages listed by its mark's +// page tables. Resolving an edge's or face's vertices to held rows is a separate step +// (`lib/ends.wesl`), not a channel. import package::lib::types::{ NONE, MISSING, MISSING_COLOR, STACK, VISIBLE, PLACED, FADE_IN, FADE_OUT, ENTERING, PAGE_ROWS, FRESH, item, @@ -22,7 +22,7 @@ import package::lib::types::{ OP_SELECT, OP_SCALE, OP_COLORMAP, OP_STORE, OP_STORE_COLOR, OP_STORE_FLAG, OP_STORE_POSITION, OP_POSITION, OP_STORE_CODE, OP_REDUCE, }; -import package::lib::bind::{frame, program, mark, pages}; +import package::lib::bind::{frame, program, mark, pages, theme_word}; import package::lib::state::{travel_words, pack_range, record_at}; import package::lib::read::{column_value, missing, missing_bits}; import package::lib::focus::{is_selected, is_hovered}; @@ -31,17 +31,19 @@ import package::lib::project::{project, to_screen}; import package::lib::reduce::{ordered, unordered}; import package::lib::ends::resolve; -// Per fitted domain: the order-preserving bits of its least and greatest value. +// Two words per fitted domain: the complement of its least value's order-preserving bits, then its +// greatest value's bits (see `lib/reduce.wesl`). @group(2) @binding(4) var domains: array>; -// Each mark's row blocks a dispatch covers: those its channels evaluate listed at `mark.block`, those -// whose state words start over marked `FRESH`; and those whose ends it finds at `mark.end_block`. +// Each mark's row blocks a dispatch covers. At `mark.block`: the blocks its channels evaluate, with +// `FRESH` on those whose state words start over. At `mark.end_block`: the blocks whose ends are +// resolved. @group(2) @binding(16) var blocks: array; -// What a dispatch of `evaluate` runs: the mark's channels over its listed blocks, the reductions -// fitting its domains over every row, or where the rows of its blocks listed for it hold their ends. +// The step a dispatch of `evaluate` runs: the mark's channels over its listed blocks, the +// reductions that fit its domains over every row, or end resolution over the blocks listed for it. @group(2) @binding(17) var step: u32; -// Whether the pipeline's marks read the view: selection, hover, the pointer, the zoom. Only those -// made for marks that do compile it. +// Whether the pipeline's marks read view state: selection, hover, the pointer, or the zoom. Only +// pipelines made for marks that do compile those reads. override VIEWED: bool = true; // The row invocation `id` evaluates: row `id.x` of the mark's `id.y`th listed page. @@ -54,7 +56,7 @@ fn fresh(id: vec3u) -> bool { return (blocks[mark.block + id.y] & FRESH) != 0u; } -// Rows of the mark that are live: those past them left and fade out. +// The mark's live row count. Rows past it left and are fading out. fn live() -> u32 { return mark.held - mark.exits; } @@ -67,7 +69,8 @@ fn constant_float(i: u32) -> f32 { return bitcast(constant(i)); } -// A domain record in constants: fitted flag, least, greatest, reduction index. +// The domain whose record starts at constant `at`: a fitted flag, least, greatest, and reduction +// index. A fitted domain reads its reduction, and is (0, 1) while no row has a value. fn domain(at: u32) -> vec2f { if constant(at) == 0u { return vec2f(constant_float(at + 1u), constant_float(at + 2u)); @@ -80,12 +83,8 @@ fn domain(at: u32) -> vec2f { return vec2f(unordered(~atomicLoad(&domains[d])), unordered(high)); } -fn theme_word(slot: u32) -> u32 { - return frame.theme[slot / 4u][slot % 4u]; -} - -// Where word `w` of `row` lies among the pages page table `table` lists: a kind word, or past the -// kind words an extra word. +// Where word `w` of `row` lies, given page table `table`. Words from `mark.stride` on are extra +// words. fn word_in(table: u32, row: u32, w: u32) -> u32 { return record_at(table, mark.stride + mark.extras, row) + w; } @@ -94,8 +93,8 @@ fn easing() -> bool { return mark.frozen != NONE; } -// Where channels store word `w` of `row`: while the mark eases, among the words it settles to, which -// `ease` reads. +// Where channels store word `w` of `row`: in the settled words during a transition (which `ease` +// reads), else in the state words. fn word_at(row: u32, w: u32) -> u32 { return word_in(select(mark.state, mark.settled, easing()), row, w); } @@ -108,22 +107,22 @@ fn store(row: u32, w: u32, value: u32) { pages[word_at(row, w)] = value; } -// Word `w` of `row` as it was drawn when the transition began. +// Word `w` of `row` as drawn when the transition began. fn was(row: u32, w: u32) -> u32 { return pages[word_in(mark.frozen, row, w)]; } -// Word `w` of `row` as its channels evaluated it, where it settles. +// Word `w` of `row` as its channels evaluated it: the value the transition settles on. fn settled(row: u32, w: u32) -> u32 { return pages[word_in(mark.settled, row, w)]; } -// Where word `w` of `row` shows. +// Where the shown value of word `w` of `row` lies. fn shown_at(row: u32, w: u32) -> u32 { return word_in(mark.state, row, w); } -// Whether `row` has nothing to ease from: it entered with this change. +// Whether `row` entered with this change, leaving nothing to ease from. fn entering(row: u32, flags: u32) -> bool { return (was(row, flags) & ENTERING) != 0u; } @@ -149,8 +148,8 @@ fn pointer_distance(row: u32) -> f32 { return distance(to_screen(project(hi, lo, z)), frame.pointer); } -// What the view knows of `row` and of itself, as op `code` reads it: whether it is selected or -// hovered, how far it is from the pointer, the zoom. +// The view state op `code` reads for `row`: whether it is selected or hovered, its distance to the +// pointer, or the zoom. fn viewed(code: u32, row: u32) -> u32 { if !VIEWED { return MISSING; @@ -205,7 +204,7 @@ fn fallback(x: u32, slot: u32) -> u32 { return x; } -// A number eased from what `row` showed; a row that entered shows its own at once, and appears as +// A number eased from the value `row` showed. A row that entered shows its new value at once, and // its draw grows it in. fn ease_number(row: u32, w: u32, now: u32, flags: u32) -> u32 { if !easing() || missing_bits(now) || entering(row, flags) { @@ -225,8 +224,8 @@ fn ease_color(row: u32, w: u32, now: u32, flags: u32) -> u32 { return pack4x8unorm(mix(unpack4x8unorm(was(row, w)), unpack4x8unorm(now), mark.motion)); } -// A position's high word `w` (x or y) eased by its offset from what the row showed, so the -// destination keeps its precision; its low word, two after it, shows as evaluated. +// A position's high word `w` (x or y), moved by the remaining offset from the old position, so the +// destination keeps its precision. Its low word, two words later, shows as evaluated. fn ease_position(row: u32, w: u32, now: u32, flags: u32) -> u32 { if !easing() || entering(row, flags) || (was(row, flags) & PLACED) == 0u { return now; @@ -236,8 +235,9 @@ fn ease_position(row: u32, w: u32, now: u32, flags: u32) -> u32 { return bits(number(now) + offset * (1.0 - mark.motion)); } -// The flags `row` shows while its mark eases: as evaluated, but a row whose visibility flipped stays -// drawn and fades, in where it became visible or entered, out where it stopped being. +// The flags `row` shows during a transition: as evaluated, except that a row whose visibility +// flipped stays drawn and fades. It fades in if it became visible or entered, and out if it stopped +// being visible. fn faded(row: u32, flags: u32) -> u32 { let now = settled(row, flags) & ~(FADE_IN | FADE_OUT); let fresh = entering(row, flags); @@ -252,14 +252,15 @@ fn faded(row: u32, flags: u32) -> u32 { return now; } -// Sets or clears `bit` of the flags at word `w`; whether the row fades is the ease's to show. +// Sets or clears `bit` of the flags at word `w`. It also clears `ENTERING` and the fade bits, which +// only transitions set. fn store_flag(row: u32, w: u32, bit: u32, on: bool) { let stored = load(row, w); store(row, w, select(stored & ~bit, stored | bit, on) & ~(ENTERING | FADE_IN | FADE_OUT)); } -// A position, from data or in a diagram from its sheet, as high and low parts of x and y; a row -// without one is not placed. +// Stores a position (from data, or in a diagram from its sheet) as the high and low parts of x and +// y, and sets `PLACED`. A missing x or y clears `PLACED` instead. fn position(row: u32, w: u32, x: vec2u, y: vec2u) { let flags = w + VERTEX_FLAGS - VERTEX_POSITION; if missing_bits(x.x) || missing_bits(y.x) { @@ -345,8 +346,8 @@ fn run(row: u32, start: u32, end: u32) { } } -// A row that left fades out with its mark's transition; its frozen state is what it showed, but for -// its comets, which travel on. +// A row that left fades out during the transition. It shows its frozen words, except its comet +// travel, which keeps advancing. fn fade_exit(row: u32, flags: u32) { let travel = travel_words().x; for (var w = 0u; w < mark.stride; w++) { @@ -359,7 +360,7 @@ fn fade_exit(row: u32, flags: u32) { @compute @workgroup_size(256) fn evaluate(@builtin(global_invocation_id) id: vec3u) { - // Ends are found over the blocks listed for them; a row that left shows the ends it had. + // Ends are resolved over the blocks listed for them. A row that left keeps the ends it had. if step == STEP_ENDS { let row = blocks[mark.end_block + id.y] * PAGE_ROWS + id.x; if row < live() { @@ -367,15 +368,15 @@ fn evaluate(@builtin(global_invocation_id) id: vec3u) { } return; } - // Reductions run over every live block, channels over the blocks listed and only those the mask - // names. One loop runs either, since a backend compiles each call of the machine whole. + // Reductions run over every live block, which a domain change lists. Channels run over the + // listed blocks, and only the channels the mask selects. One loop runs either, because a + // backend compiles each call of the machine in full. let reducing = step == STEP_REDUCE; let row = row_of(id); if row >= live() { return; } - // A row whose words start over shows nothing but what its channels write: what none writes reads - // as zero. + // A row whose words start over is zeroed first, so a word no channel writes reads as zero. if !reducing && fresh(id) { for (var w = 0u; w < mark.stride + mark.extras; w++) { pages[shown_at(row, w)] = 0u; @@ -399,9 +400,9 @@ fn evaluate(@builtin(global_invocation_id) id: vec3u) { } } -// Each frame a mark eases toward its settled state: every word a channel stores shows the frozen -// state moved toward the settled one by the mark's progress, as the program says each eases, and the -// flags show rows fading; rows that left fade out. No channel runs. +// Writes what every row shows, each frame of a transition. Each word a channel stores moves from +// its frozen value toward its settled value by the mark's progress, as its blend says, and the +// flags show fading rows. Rows that left fade out. No channel runs. @compute @workgroup_size(256) fn ease(@builtin(global_invocation_id) id: vec3u) { let row = item(id); @@ -430,9 +431,9 @@ fn ease(@builtin(global_invocation_id) id: vec3u) { pages[shown_at(row, flags)] = faded(row, flags); } -// Comets travel on by each stroke row's flow over the frame's step, so a flow that changes carries -// on from where its comets are. Wrapped at their spacing, travel keeps its precision however long -// they run. +// Advances each stroke row's comets by its flow times the frame's time step, so a changed flow +// continues from where the comets are. Travel wraps at the comet spacing, so it keeps its precision +// however long comets run. @compute @workgroup_size(256) fn travel(@builtin(global_invocation_id) id: vec3u) { let row = item(id); @@ -445,12 +446,12 @@ fn travel(@builtin(global_invocation_id) id: vec3u) { let spacing = max(frame.flow_spacing, 1e-6); var moved = number(pages[at]) + select(flow, 0.0, missing(flow)) * frame.flow_step; moved -= spacing * floor(moved / spacing); - // Words a row never held read as no travel. + // A result outside [0, spacing], such as NaN from a word never written, reads as no travel. pages[at] = bits(select(0.0, moved, moved >= 0.0 && moved <= spacing)); } -// Where a transition moved rows, their comets carry on from where they were: each row takes the -// travel its frozen state holds. +// When a transition moves rows, their comets continue from where they were: each row takes the +// travel from its frozen words. @compute @workgroup_size(256) fn carry(@builtin(global_invocation_id) id: vec3u) { let row = item(id); diff --git a/crates/latkit/shaders/compute/fields.wesl b/crates/latkit/shaders/compute/fields.wesl index 2cbb8df8..4e736587 100644 --- a/crates/latkit/shaders/compute/fields.wesl +++ b/crates/latkit/shaders/compute/fields.wesl @@ -1,25 +1,27 @@ -// Distance fields of text: the coverage a host set for a run, turned into each texel's signed distance -// to the text's edge, inside positive, as a byte of `0.5 + distance / (2 TEXT_SPREAD)`, over the run -// widened by `TEXT_SPREAD` texels on every side. Partial coverage places the edge within its texel, so -// antialiasing becomes subtexel distance. +// Text distance fields. For each run, converts the coverage a host set into each texel's signed +// distance to the text's edge (positive inside), stored as the byte +// `0.5 + distance / (2 TEXT_SPREAD)`. The field covers the run plus `TEXT_SPREAD` texels on each +// side. Partial coverage places the edge within its texel, so antialiased edges keep subtexel +// precision. // -// A texel's squared distance to the text is the least, over the field's texels, of each one's own (none -// inside the text, a partial texel's subtexel distance squared, far outside it) plus the squared -// distance to it; likewise to what lies outside the text. The least splits into one down each column, -// then one along each row, and only texels within the spread bring a texel within it, so each looks -// that far and no farther: exact wherever the field is not clamped. After the columns, one of a -// texel's two distances is its own, none, so one signed word holds both: to the outside above zero, to -// the text below. +// A texel's squared distance to the text is the minimum, over the field's texels, of each texel's +// own squared distance (0 inside the text, a partial texel's subtexel distance squared, effectively +// infinite outside) plus the squared distance to that texel. The distance to the outside works the +// same way. The minimum splits into a pass down each column, then a pass along each row. Only +// texels within the spread can bring a texel within the spread, so each pass looks only that far, +// which is exact wherever the field is not clamped. After the column pass, one of a texel's two +// distances is always 0, so one signed word holds both: the distance to the outside above zero, and +// to the text below. import package::lib::types::{ TEXT_SPREAD, FIELD_COVERAGE, FIELD_WIDTH, FIELD_HEIGHT, FIELD_COLUMNS, FIELD_AT, FIELD_ROW, FIELD_WORDS, }; -// The runs, `FIELD_WORDS` each, then their coverage, four bytes a word, their columns' least -// distances, and their fields, four texels a word. +// The runs, `FIELD_WORDS` words each, then their coverage (four bytes per word), their column +// minima, and their fields (four texels per word). @group(2) @binding(0) var fields: array; -// Texels the field reaches past the text, and the column minima a word of four texels reads. +// `SPREAD`: texels the field reaches past the text. `NEAR`: column minima read per 4-texel word. const SPREAD: i32 = i32(TEXT_SPREAD); const NEAR: i32 = 4 + 2 * SPREAD; // Farther than any texel within the spread. @@ -31,7 +33,7 @@ struct Run { size: vec2u, columns: u32, at: u32, - // Words between the starts of the field's rows. + // Words from the start of one field row to the next. row: u32, } @@ -46,7 +48,7 @@ fn run(i: u32) -> Run { ); } -// Texels across and down its field: its coverage's, and the spread on every side. +// The field's width and height in texels: the coverage plus the spread on every side. fn field_size(r: Run) -> vec2u { return r.size + vec2u(2u * u32(SPREAD)); } @@ -70,7 +72,8 @@ fn own(r: Run, p: vec2i) -> vec2f { return select(vec2f(0.0, d * d), vec2f(d * d, 0.0), d > 0.0); } -// Down each column: the least of each texel's own within the spread above and below. +// Column pass: for each texel, the least over the texels within the spread above and below it of +// their own squared distance plus the squared vertical distance. @compute @workgroup_size(16, 16) fn columns(@builtin(global_invocation_id) id: vec3u) { let r = run(id.z); @@ -88,8 +91,8 @@ fn columns(@builtin(global_invocation_id) id: vec3u) { fields[r.columns + id.y * size.x + id.x] = bitcast(least.y - least.x); } -// Along each row, four texels a word: the least of the columns' within the spread to either side, -// then each texel's byte. +// Row pass, four texels per word: for each texel, the least over the column minima within the +// spread to either side of each plus the squared horizontal distance, then the texel's byte. @compute @workgroup_size(16, 16) fn rows(@builtin(global_invocation_id) id: vec3u) { let r = run(id.z); @@ -97,7 +100,7 @@ fn rows(@builtin(global_invocation_id) id: vec3u) { if id.x * 4u >= size.x || id.y >= size.y { return; } - // Past the field's edge lies nothing. + // Nothing lies past the field's edge. var near: array; let first = i32(id.x * 4u) - SPREAD; for (var k = 0; k < NEAR; k++) { diff --git a/crates/latkit/shaders/compute/freeze.wesl b/crates/latkit/shaders/compute/freeze.wesl index 9686f031..ef7be83a 100644 --- a/crates/latkit/shaders/compute/freeze.wesl +++ b/crates/latkit/shaders/compute/freeze.wesl @@ -1,9 +1,9 @@ -// Freezing: what each row of a mark showed when its transition began, gathered into its frozen words -// at the row's new place. The remap words at `mark.remap` say where the words it showed lie (the page -// table of its words before the change, copied into them), how many rows it showed, and where each new -// row was, NONE for a row that entered, then the rows that left, which fade out past the live ones; -// without that list every row is where it was. They say too, for each kind word naming a vertex, where -// the vertices of the vertex mark it names moved, so the ends of rows easing out follow them. +// Freezing: copies what each row showed when its transition began into its frozen words, at the +// row's new held row. The remap words at `mark.remap` hold a copy of the page table of the words +// shown before the change, how many rows showed, and each new row's old row (NONE for a row that +// entered), followed by the rows that left, which fade out past the live ones. Without that list, +// every row keeps its held row. For each kind word naming a vertex, the remap words also say where +// that vertex mark's rows moved, so the ends of rows easing out follow them. import package::lib::types::{NONE, ENTERING, PAGE_BITS, PAGE_MASK, item}; import package::lib::bind::{mark, pages}; @@ -11,9 +11,9 @@ import package::lib::state::record_at; @group(2) @binding(12) var remap: array; -// The remap header's words: where the page table of the words shown starts, how many rows showed, -// where the row each new row was starts (NONE when every row is where it was), and where each kind -// word's vertex moves start (NONE when no vertex moved). +// Remap header words: where the page table of the shown words starts, how many rows showed, where +// each new row's old row is listed (NONE when no row moved), and where each kind word's vertex +// moves start (NONE when no vertex moved). const REMAP_TABLE: u32 = 0u; const REMAP_SHOWN: u32 = 1u; const REMAP_SOURCES: u32 = 2u; @@ -44,7 +44,8 @@ fn gather(@builtin(global_invocation_id) id: vec3u) { let ends = remap[header + REMAP_ENDS]; for (var w = 0u; w < words; w++) { var word = pages[shown + w]; - // Each vertex's moves: how many rows it held, then where each went, NONE where it left. + // A vertex mark's moves: its old held row count, then each old row's new held row, NONE for + // a row that left. let moves = select(NONE, remap[ends + min(w, mark.stride - 1u)], ends != NONE && w < mark.stride); if moves != NONE && word != NONE { word = select(NONE, remap[moves + 1u + word], word < remap[moves]); diff --git a/crates/latkit/shaders/compute/labels.wesl b/crates/latkit/shaders/compute/labels.wesl index 30ed4064..21eca13b 100644 --- a/crates/latkit/shaders/compute/labels.wesl +++ b/crates/latkit/shaders/compute/labels.wesl @@ -1,15 +1,18 @@ -// Label placement, once `compute/candidates.wesl` measured each label: rounds of `claim` (each -// label wanting room takes the least key into every cell it covers, or drops if one is held), -// `decide` (a label holding all its cells is placed), and `occupy` (placed labels hold their cells) -// settle, with cells not held cleared between rounds. That places labels as greedily by key as a -// sequential pass would. `ask` then asks for the texts of placed labels the atlas lacks and notes -// those it holds, so it keeps them: texts are set only for labels that show, however many rows are in -// view. `fade` eases each label in or out. Placement only decides which labels show; draws place them -// from their anchors every frame. +// Label placement, after `compute/candidates.wesl` measures each label. Placement runs rounds of +// three steps. `claim`: each label needing room writes its key into every cell it covers, and each +// cell keeps the least key; a label drops if a placed label occupies one of its cells. `decide`: a +// label is placed if every cell it covers kept its key. `occupy`: placed labels occupy their cells. +// Cells not occupied are cleared between rounds. The result matches a sequential greedy pass by +// key. +// `ask` then requests the texts of placed labels the atlas lacks, and marks the others as seen so +// the atlas keeps them. Only shown labels get their text set, however many rows are in view. `fade` +// eases each label in or out. Placement only decides which labels show; draws place them from their +// anchors every frame. // -// A mark that shows at most `label_max` labels keeps, of those wanting room, the ones of least key, -// before any claims: keys are unique, so a radix select over three digits of 11, 11, and 10 bits finds -// the greatest key kept exactly, each digit a pass over the candidates and one over its buckets. +// A mark that shows at most `label_max` labels keeps, among those needing room, the ones with the +// least keys, before any claims. Keys are unique, so a radix select over three digits of 11, 11, +// and 10 bits finds the greatest kept key exactly. Each digit takes a pass over the candidates and +// one over its buckets. import package::lib::types::{ NONE, LABEL_CODE, CANDIDATE_BOX, CANDIDATE_KEY, CANDIDATE_STATE, CANDIDATE_ALPHA, WANTS, PLACED_LABEL, DROPPED, @@ -21,11 +24,12 @@ import package::lib::read::missing_bits; import package::lib::placement::{labels, label_word, put_label, put, candidate, want, seen}; import package::lib::drawn::held_row; -// Buckets of a digit of the select capping labels. +// Buckets per digit of the select that caps labels. const SELECT_BUCKETS: u32 = 2048u; -// Word `k` of the select's state, past its buckets: the key's digits found, how many of the least -// keys are still to keep, and the digit next counted (4 once fewer want room than the cap). +// Word `k` of the select's state, after its buckets: 0, the key digits found so far; 1, how many of +// the least keys remain to keep; 2, the digit counted next (4 once fewer labels need room than the +// cap). fn select_word(k: u32) -> u32 { return mark.label_select + SELECT_BUCKETS + k; } @@ -39,7 +43,7 @@ fn digit(d: u32) -> vec2u { } } -// The held row of the drawn row invocation `id` covers, or NONE past the mark's live rows. +// The held row of the drawn row invocation `id` covers, or NONE past the mark's drawn rows. fn labeled(id: vec3u) -> u32 { let i = item(id); return select(NONE, held_row(i), i < mark.rows); @@ -74,7 +78,7 @@ fn select_count(@builtin(global_invocation_id) id: vec3u) { } let key = word(candidate(row) + CANDIDATE_KEY); let at = digit(d); - // Only keys whose higher digits are those found count. + // Only keys whose higher digits match those found so far count. let above = at.x + at.y; if above < 32u && (key >> above) != label_word(select_word(0u)) { return; @@ -82,11 +86,11 @@ fn select_count(@builtin(global_invocation_id) id: vec3u) { atomicAdd(&labels[mark.label_select + ((key >> at.x) & (SELECT_BUCKETS - 1u))], 1u); } -// The bucket holding the least key still to keep, clearing the buckets for the next digit. +// Finds the bucket holding the greatest key to keep, and clears the buckets for the next digit. @compute @workgroup_size(1) fn select_find() { let d = label_word(select_word(2u)); - // A cap of none keeps no label: the cut drops them all. + // A cap of 0 keeps no label: `select_cut` drops them all. if mark.label_max == NONE || mark.label_max == 0u || d >= 3u { return; } @@ -104,7 +108,7 @@ fn select_find() { } } if found == SELECT_BUCKETS { - // Fewer want room than the cap: each keeps its chance. + // Fewer labels need room than the cap, so none is cut. put_label(select_word(2u), 4u); return; } @@ -140,7 +144,7 @@ fn cell(x: u32, y: u32) -> u32 { return frame.cells + y * frame.cell_columns + x; } -// Whether row `row`'s label still wants room this round. +// Whether row `row`'s label still needs room this round. fn undecided(row: u32) -> bool { let s = word(candidate(row) + CANDIDATE_STATE); return (s & WANTS) != 0u && (s & (PLACED_LABEL | DROPPED)) == 0u; diff --git a/crates/latkit/shaders/compute/lists.wesl b/crates/latkit/shaders/compute/lists.wesl index 993dda18..8fa0b8e1 100644 --- a/crates/latkit/shaders/compute/lists.wesl +++ b/crates/latkit/shaders/compute/lists.wesl @@ -1,8 +1,9 @@ -// The scan of a list: each block of 256 counts scans in its workgroup and leaves its total, one -// workgroup scans the totals, and every count adds its block's offset. Counts become where each -// item's entries start, the total follows them, and the header's indirect arguments take the -// entries the list keeps. Each count is held to its share of a u32, so the total never wraps. -// Reduce-then-scan needs no forward progress between workgroups, so it is safe on every device. +// The scan of a list. Each block of 256 counts is scanned in its workgroup, which writes the +// block's total. One workgroup then scans the totals, and every count adds its block's offset. The +// counts become each item's first entry, followed by the total, and the header's indirect arguments +// get the number of entries the list keeps. Each count is capped at its share of a u32, so the +// total never wraps. Reduce-then-scan needs no forward-progress guarantee between workgroups, so it +// is safe on every device. import package::lib::bind::{mark, program}; import package::lib::types::{ @@ -16,7 +17,7 @@ import package::lib::drawn::held_row; var tile: array; var shared_items: u32; -// The list this dispatch scans: the mark's. +// The list this dispatch scans: the current mark's. fn header() -> u32 { return mark.list; } @@ -36,10 +37,11 @@ fn scan_blocks(@builtin(local_invocation_id) l: vec3u, @builtin(workgroup_id) g: let h = header(); let n = lists[h + LIST_ITEMS]; let counts = lists[h + LIST_COUNTS]; - // Past one dimension's workgroups, runs of DISPATCH_ROWS items step along the second. + // Beyond one dimension's workgroups, each step along the second covers `DISPATCH_ROWS` items. let block = g.y * (DISPATCH_ROWS / 256u) + g.x; let i = block * 256u + l.x; - // Whatever the counts, the starts climb, so every item's writes end. + // Capping each count at its share keeps the starts increasing for any counts, so every item's + // write loop ends. let share = 0xffffffffu / max(n, 1u); let own = select(0u, min(lists[counts + min(i, max(n, 1u) - 1u)], share), i < n); tile[l.x] = own; @@ -93,9 +95,9 @@ fn add_sums(@builtin(global_invocation_id) id: vec3u) { } } -// Each drawn row's word, from the runs the program lists: its held row, in the last run starting at -// or before it, and whether that run's tile fades in or out. What every pass drawing or answering for -// the mark reads in one word, written when the runs change. +// Writes each drawn row's word from the program's run list: its held row, found in the last run +// starting at or before it, and whether that run's tile fades in or out. Every pass that draws or +// probes the mark reads this one word. It runs when the runs change. @compute @workgroup_size(256) fn expand_runs(@builtin(global_invocation_id) id: vec3u) { let i = item(id); @@ -119,9 +121,9 @@ fn expand_runs(@builtin(global_invocation_id) id: vec3u) { lists[mark.drawn + i] = (program[run + 1u] + (i - program[run])) | entering | leaving; } -// A mark's segments, numbered by its list: each drawn row counts the segments of its path, then -// writes its number at each of their places, with how far along its path each starts, so a segment's instance -// finds its row, its index in it, and where dashes and comets stand at its start. +// A mark's segment list. `count_segments` counts each drawn row's segments. After the scan, +// `write_segments` writes each segment's drawn row and its distance along the path, so a segment +// instance finds its row, its index in the row, and where dashes and comets are at its start. @compute @workgroup_size(256) fn count_segments(@builtin(global_invocation_id) id: vec3u) { let i = item(id); @@ -139,7 +141,7 @@ fn write_segments(@builtin(global_invocation_id) id: vec3u) { let at = list_start(mark.list, i); let end = list_start(mark.list, i + 1u); let entries = lists[mark.list + LIST_ENTRIES]; - // An area's triangles have no distance along. + // An area's triangles and a trace's segments have no distance along. let measured = mark.primitive != TRIANGLES && PATH != PATH_TRACE; var along = 0.0; let row = held_row(i); diff --git a/crates/latkit/shaders/compute/normals.wesl b/crates/latkit/shaders/compute/normals.wesl index 131197e1..0f40593e 100644 --- a/crates/latkit/shaders/compute/normals.wesl +++ b/crates/latkit/shaders/compute/normals.wesl @@ -1,5 +1,6 @@ -// Vertex normals for faces: each face adds its unit normal to its three corners' normal words, in -// pages of the vertex mark its corners attach to, in fixed point, so the sums are exact in any order. +// Vertex normals for faces. Each face adds its unit normal, in fixed point, to the normal words of +// its three corner vertices, in pages of the vertex mark they belong to. Fixed point makes the sums +// exact in any order. import package::lib::types::{NONE, item}; import package::lib::bind::{mark, pages}; diff --git a/crates/latkit/shaders/compute/probe.wesl b/crates/latkit/shaders/compute/probe.wesl index ccb6c49a..e8c48e9c 100644 --- a/crates/latkit/shaders/compute/probe.wesl +++ b/crates/latkit/shaders/compute/probe.wesl @@ -1,19 +1,19 @@ -// Probes and queries: what a frame drew, asked about through the same geometry the draws use. One -// program, made for one primitive's geometry, answers every question a step at a time, the step a -// dispatch's uniform names; each step that reads instances puts each on screen once (`seen`) and asks -// of it what the step needs. +// Probes and queries against a drawn frame, using the same geometry as the draws. One program, +// compiled per primitive, answers every question in steps, and a dispatch's uniform selects the +// step. Each step that reads instances projects each one to the screen once (`seen`) and tests what +// the step needs. // -// The hover takes three steps over every mark it reaches: `nearest` keeps the least key over every -// instance within reach of the probe (the distance in sixty-fourths of a pixel, then the mark drawn -// last), `topmost` keeps the last instance of that mark at that key, and `describe` writes where that -// hit lies. Rows behind what the frame drew are not reached. `under` writes the nearest depth drawn -// around the probe, which gestures turn and zoom about. +// The hover takes three steps over every mark it reaches. `nearest` keeps the least key over all +// instances within reach of the probe: the distance in 1/64 pixel, then the mark drawn last. +// `topmost` keeps the last instance of that mark at that key, and `describe` writes where that hit +// lies. Rows hidden behind other drawn marks are not reached. `under` writes the nearest depth +// drawn around the probe, which gestures rotate and zoom about. // -// Queries write past the probe's words, each mark from the word its `query` names: `hits` appends every -// instance within reach of the query's point while the mark has room; `boxed` sets a bit for every row -// with an instance in the query's box, hidden or not. `locate` writes where one drawn row lies on -// screen. Every row an answer names is a held row, which the view reads back against what the frame -// drew. +// Queries write after the probe's words, each mark from the word its `query` names. `hits` appends +// every instance within reach of the query's point while the mark has room. `boxed` sets a bit for +// every row with an instance in the query's box, whether or not something drawn covers it. `locate` +// writes where one drawn row lies on screen. Every row an answer names is a held row, which the +// view maps to a table row using its record of that frame. import package::lib::types::{ NONE, FADE_OUT, POINTS, STROKES, TRIANGLES, MARK_EDGES, PATH_GEOMETRY, PROBE_KEY, PROBE_INSTANCE, PROBE_DEPTH, @@ -56,17 +56,18 @@ fn unreached() -> Reach { return Reach(1e30, 0.0, vec2f(0.0), NONE, 0u); } -// An instance as the frame drew it, on screen, depths as z: a point's center and, for a port, the -// port's center as `b`; a segment's ends; a triangle's corners, with whether each is shown, ahead of -// the eye, and in front of it. `item` is the drawn row it belongs to and `row` that row's held row; -// `part` is a port's number from 1, or a segment's index along its path. +// An instance as the frame drew it, on screen, with depth as z: a point's center (and for a port, +// the port's center as `b`), a segment's ends, or a triangle's corners with whether each is shown, +// ahead of the globe's horizon, and in front of the eye. `item` is its drawn row and `row` that +// row's held row. `part` is a port's number from 1, or a segment's index along its path. struct Seen { item: u32, row: u32, part: u32, - // The row is one of the mark's live rows, and neither it nor its tile is fading out. + // The row is a drawn row, not an exit, and neither it nor its tile is fading out. kept: bool, - // A point is shown; a stroke's style is shown and its segment on screen. + // A point is shown. A stroke's style is shown and its segment is on screen. A triangle's + // corners are all shown and in front of the eye, and one is ahead of the horizon. shown: bool, a: vec3f, b: vec3f, @@ -90,13 +91,13 @@ fn nearest_on(p: vec2f, a: vec3f, b: vec3f) -> vec3f { return mix(a, b, t); } -// Whether drawn row `item`, held as `row`, answers the pointer: one of the mark's live rows, neither -// it nor its tile, which is `leaving` or not, fading out. +// Whether drawn row `item` (held row `row`) responds to the pointer: it is not an exit, its tile is +// not `leaving`, and the row is not fading out. fn answers(item: u32, row: u32, leaving: bool) -> bool { return item < mark.rows && !leaving && (word(row_at(row) + mark.stride - 1u) & FADE_OUT) == 0u; } -// A point's marker, or past the rows one of their ports. +// A point's marker, or for an instance past the rows, one of their ports. fn seen_point(instance: u32) -> Seen { var s: Seen; let port = select(0u, instance - mark.rows, instance >= mark.rows); @@ -119,7 +120,8 @@ fn seen_point(instance: u32) -> Seen { return s; } -// A segment of a stroke: the instance's, or, locating, the middle one of drawn row `instance`'s path. +// A stroke segment: the instance's, or when locating, the middle segment of drawn row `instance`'s +// path. fn seen_segment(instance: u32, locating: bool) -> Seen { var s: Seen; var g: Segment; @@ -145,7 +147,7 @@ fn seen_segment(instance: u32, locating: bool) -> Seen { return s; } -// A triangle: a face, or one of an area's; locating, drawn row `instance`'s face. +// A triangle: a face, or one of an area's. When locating, drawn row `instance`'s face. fn seen_triangle(instance: u32, locating: bool) -> Seen { var s: Seen; s.item = select(triangle_item(instance), instance, locating); @@ -171,7 +173,7 @@ fn seen_triangle(instance: u32, locating: bool) -> Seen { return s; } -// Instance `instance` of the mark's primitive on screen; locating, drawn row `instance`'s place. +// Instance `instance` of the mark's primitive on screen; when locating, drawn row `instance`. fn seen(instance: u32, locating: bool) -> Seen { switch PRIMITIVE { case POINTS: { return seen_point(instance); } @@ -180,7 +182,8 @@ fn seen(instance: u32, locating: bool) -> Seen { } } -// How far `p` lies from what the instance drew, and the nearest of it on screen with its depth. +// How far `p` lies from the instance's drawn shape, and the shape's nearest point on screen, with +// its depth. fn reach(s: Seen, p: vec2f) -> Reach { if !s.kept || !s.shown { return unreached(); @@ -195,7 +198,7 @@ fn reach(s: Seen, p: vec2f) -> Reach { } case STROKES: { let q = nearest_on(p, s.a, s.b); - // The nearer end names the sample a trace's hit lies at; for an edge, which of its ends. + // The nearer end gives the sample a trace's hit is at; for an edge, which of its ends. var end = select(s.part, s.part + 1u, length(q.xy - s.b.xy) < length(q.xy - s.a.xy)); if mark.kind == MARK_EDGES { end = u32(2u * end >= path_count(s.item)); @@ -242,7 +245,7 @@ fn boxed(s: Seen) -> bool { if !s.shown { return false; } - // The segment crosses the box when its ends do, or it passes through it. + // Tests nine evenly spaced points along the segment, its ends included. for (var k = 0u; k <= 8u; k++) { if in_box(mix(s.a.xy, s.b.xy, f32(k) / 8.0)) { return true; @@ -257,15 +260,15 @@ fn boxed(s: Seen) -> bool { } } -// Where an area's held row `row` lies on screen, and 1 as z where it shows: where its label hangs. +// Where area held row `row` lies on screen (its label anchor), with z 1 where it shows. fn anchored(row: u32) -> vec3f { let anchor = area_anchor(row); let clip = clip_of(anchor.world); return vec3f(to_screen(clip), f32(anchor.shown && anchor.ahead && clip.w > 0.0)); } -// Where the row seen locating lies on screen, and 1 as z where it shows: a point's center, a path's -// middle segment's middle, a face's middle. +// Where the located row lies on screen, with z 1 where it shows: a point's center, the middle of a +// path's middle segment, or a face's centroid. fn located(s: Seen) -> vec3f { switch PRIMITIVE { case POINTS: { return vec3f(s.a.xy, f32(s.shown && s.front.x)); } @@ -309,8 +312,8 @@ fn instances() -> u32 { return triangle_instances(); } -// Appends hit `r` to the mark's, while it has room: they follow how many it found and how many it -// keeps, `HIT_WORDS` each. +// Appends hit `r` to the mark's hits while there is room. At `mark.query` lie the number found and +// the number kept, then the hits, `HIT_WORDS` each. fn append(r: Reach) { let at = atomicAdd(&probe[mark.query], 1u); if at >= atomicLoad(&probe[mark.query + 1u]) { @@ -325,7 +328,7 @@ fn append(r: Reach) { atomicStore(&probe[entry + 5u], bitcast(r.depth)); } -// Writes where the row located lies. +// Writes where the located row lies. fn locate(at: vec3f) { atomicStore(&probe[PROBE_LOCATE_X], bitcast(at.x)); atomicStore(&probe[PROBE_LOCATE_Y], bitcast(at.y)); @@ -335,7 +338,7 @@ fn locate(at: vec3f) { @compute @workgroup_size(256) fn answer(@builtin(global_invocation_id) id: vec3u) { let s = step.step; - // Steps that read one thing take one invocation. + // Steps that read a single item use one invocation. if s == STEP_UNDER || s == STEP_LOCATE || s == STEP_DESCRIBE { if any(id != vec3u(0u)) { return; @@ -345,8 +348,9 @@ fn answer(@builtin(global_invocation_id) id: vec3u) { atomicStore(&probe[PROBE_UNDER], bitcast(under())); return; } - // Locating reads the row the query names; describing, the topmost instance the hover found, when - // it is this mark's. Every step that reads an instance sees it through the one call below. + // Locating reads the drawn row the query names. Describing reads the topmost instance the hover + // found, if it belongs to this mark. Every step that reads an instance goes through the single + // `seen` call below. let locating = s == STEP_LOCATE; var i = item(id); if locating { @@ -355,7 +359,7 @@ fn answer(@builtin(global_invocation_id) id: vec3u) { locate(vec3f(0.0)); return; } - // An area lies where its label hangs. + // An area is located at its label anchor. if PRIMITIVE == TRIANGLES && PATH == PATH_GEOMETRY { locate(anchored(held_row(i))); return; diff --git a/crates/latkit/shaders/draw/areas.wesl b/crates/latkit/shaders/draw/areas.wesl index 2c411a8c..9f56d917 100644 --- a/crates/latkit/shaders/draw/areas.wesl +++ b/crates/latkit/shaders/draw/areas.wesl @@ -1,7 +1,7 @@ -// Areas: one instanced triangle per triangle of every area, numbered by the mark's list, flat on the -// ground in its row's color and lit by the earth's daylight. Areas lie under what draws after them: -// they test depth but write none. The focus variant draws the triangles of the rows the mark's focus -// lights again, over everything, tinted toward the theme's focus colors. +// Areas: one instanced triangle per area triangle, numbered by the mark's list, flat on the ground +// in its row's color and lit by the earth's daylight. Areas lie under anything drawn after them: +// they test depth but do not write it. The focus variant redraws the lit rows' triangles over +// everything, tinted toward the theme's focus colors. import package::lib::types::{SELECTED, HOVERED}; import package::lib::bind::frame; @@ -15,7 +15,7 @@ struct Varyings { @builtin(position) clip: vec4f, @location(0) color: vec4f, @location(1) @interpolate(flat) row: u32, - // The row's focus bits, which the focus draw tints by. + // The row's focus bits, which choose the focus draw's tint. @location(2) @interpolate(flat) focus: u32, } @@ -46,7 +46,7 @@ fn fragment(v: Varyings) -> @location(0) vec4f { var rgb = v.color.rgb; let focus = v.focus; if (focus & SELECTED) != 0u { - // A selected color with a negative red asks for each row's own, brightened. + // A selected color with a negative red means each row's own color, brightened. let tint = select(frame.selected.rgb, min(rgb * 1.6 + 0.08, vec3f(1.0)), frame.selected.r < 0.0); rgb = mix(rgb, tint, 0.5); } else if (focus & HOVERED) != 0u { diff --git a/crates/latkit/shaders/draw/backdrop.wesl b/crates/latkit/shaders/draw/backdrop.wesl index 71decf08..f84a1035 100644 --- a/crates/latkit/shaders/draw/backdrop.wesl +++ b/crates/latkit/shaders/draw/backdrop.wesl @@ -1,9 +1,9 @@ -// The backdrop under every mark: a plane's or a diagram's grid, or the earth's ground and graticule, -// lit by daylight, and on the globe ringed by its air. Lines -// are a pixel wide and antialiased per axis. Their spacing steps by tens, and the next step fades in as -// the lines crowd, so no seam shows. They fade where the view grazes the ground, and on a plane away -// from what the camera looks at. The round globe writes its depth a hair under the ground, so what -// lies behind it hides and what lies on it draws. +// The backdrop under every mark: a plane's or diagram's grid, or the earth's ground and graticule, +// lit by daylight, with the atmosphere around the globe. Lines are one pixel wide and antialiased +// per axis. Their spacing steps by powers of ten, and the next coarser spacing fades in as lines +// get dense, so no seam shows. Lines fade where the view grazes the ground, and on a plane, with +// distance from where the camera looks. The round globe writes its depth slightly below the ground, +// so marks behind it are hidden and marks on it still draw. import package::lib::types::{SPACE_EARTH, SPACE_PLANE, SPACE_DIAGRAM, GRID_OFF, GRID_DOTS}; import package::lib::bind::{frame, eye}; @@ -16,11 +16,11 @@ struct Varyings { @location(0) ndc: vec2f, } -// How far under the ground the globe's depth lies, in its radii: deeper than a chord of a few -// degrees sags, so paths and areas on the ground draw over it. +// How far below the ground the globe's depth is written, in globe radii. It exceeds the sag of a +// chord a few degrees long, so paths and areas on the ground draw over the globe. const GROUND_BIAS: f32 = 1e-3; -// What a pixel of the backdrop draws: its color, and the depth of the ground there. +// A backdrop pixel: its color, and the ground's depth there. struct Ground { color: vec4f, depth: f32, @@ -38,8 +38,8 @@ fn vertex(@builtin(vertex_index) i: u32) -> Varyings { return Varyings(vec4f(ndc, 0.0, 1.0), ndc); } -// Coverage of lines `pitch` apart through `p`, which moves `w` per pixel: a pixel wide, fading out as -// their cells shrink to a few pixels. +// Coverage of grid lines `pitch` apart at `p`, where `p` changes by `w` per pixel. Lines are a +// pixel wide and fade out as their cells shrink to a few pixels. fn lines(p: vec2f, w: vec2f, pitch: f32) -> f32 { let cells = max(w / pitch, vec2f(1e-12)); let px = abs(fract(p / pitch + 0.5) - 0.5) / cells; @@ -47,16 +47,17 @@ fn lines(p: vec2f, w: vec2f, pitch: f32) -> f32 { return max(c.x, c.y); } -// Dots `pitch` apart through `p`, which moves `w` per pixel: a CSS pixel across on any screen, fading -// out as their cells shrink to a few pixels. +// Coverage of grid dots `pitch` apart at `p`, where `p` changes by `w` per pixel. Dots are a CSS +// pixel across on any screen and fade out as their cells shrink to a few pixels. fn dots(p: vec2f, w: vec2f, pitch: f32) -> f32 { let cells = max(w / pitch, vec2f(1e-12)); let px = abs(fract(p / pitch + 0.5) - 0.5) / cells; return saturate(eye.pixel_ratio + 0.5 - length(px)) * (1.0 - smoothstep(0.08, 0.25, max(cells.x, cells.y))); } -// Lines spaced `pitch` times a power of ten, from the `lowest` power to the `highest`: the spacing -// nearest twenty pixels draws, and the next coarser one fades in as it crowds. +// Grid lines spaced `pitch` times a power of ten, with the power between `lowest` and `highest`. +// Lines draw at the spacing just under 20 pixels, and the next coarser spacing fades in as that one +// gets denser. fn grid(p: vec2f, w: vec2f, pitch: f32, lowest: f32, highest: f32, dotted: bool) -> f32 { let level = clamp(log2(max(max(w.x, w.y) * 20.0 / pitch, 1e-30)) * 0.30103, lowest, highest); let k = floor(level); @@ -72,8 +73,8 @@ fn blend(color: vec4f, over: vec4f, coverage: f32) -> vec4f { return vec4f(mix(color.rgb, over.rgb, coverage * over.a), color.a); } -// The plane: its grid where the ray meets elevation 0 at `g`, faded where the ray grazes and away -// from the center. +// The plane's grid at `g`, where the view ray meets elevation 0, faded where the ray grazes the +// plane and away from the center. fn plane(g: vec3f, d: vec3f, ahead: bool) -> vec4f { let p = g.xy + frame.center_hi + frame.center_lo; let w = fwidth(g.xy); @@ -102,10 +103,10 @@ fn globe_degrees(w: vec3f, radius: f32) -> vec2f { return vec2f(lon, degrees(asin(clamp(v.y, -1.0, 1.0)))); } -// The earth: the mercator band or the globe's disc, eased between as the projection morphs, lit by -// daylight, the graticule on it, and around the globe its air. +// The earth: the Mercator band or the globe's disc, blended as the projection morphs, lit by +// daylight, with the graticule on it and the atmosphere around the globe. fn earth(o: vec3f, d: vec3f, g: vec3f, ahead: bool) -> Ground { - // The flat map ends where web mercator does, at y = ±π. + // The flat map ends where Web Mercator does, at y = ±π. let band = PI - abs(g.y + frame.mercator_y); let flat = saturate(band / max(fwidth(band), 1e-9) + 0.5) * f32(ahead); let center = globe_center(); @@ -129,15 +130,16 @@ fn earth(o: vec3f, d: vec3f, g: vec3f, ahead: bool) -> Ground { var color = blend(frame.background, ground, covered); let facing = dot(normalize(hit - center), -d); if frame.graticule != 0u { - // Longitude jumps where it wraps; its other wrapping steadies the derivative there. + // Longitude jumps where it wraps. A second, shifted wrapping keeps the derivative steady + // there. let wrapped = fract(p.x / 360.0) * 360.0; let w = vec2f(min(fwidth(p.x), fwidth(wrapped)), fwidth(p.y)); let fade = select(smoothstep(0.02, 0.15, abs(d.z)), smoothstep(0.0, 0.3, facing), globe); color = blend(color, frame.grid_color, grid(p, w, 1.0, -2.0, 1.0, false) * fade * covered); } if frame.atmosphere.a > 0.0 && frame.globe > 0.0 { - // A thin rim of air over the globe's edge and a halo just past it, as thick as the air is - // beside the earth, lit where the sun is up over the ground the ray passes nearest. + // A thin rim of atmosphere over the globe's edge, and a halo out to 1.045 globe radii. It + // is lit where the sun is up over the ground nearest the view ray. let rim = pow(1.0 - saturate(facing), 6.0) * round; let halo = pow(1.0 - smoothstep(radius, radius * 1.045, off), 2.0) * (1.0 - round) * f32(along > 0.0); let below = daylight(globe_degrees(o + d * along, radius)); diff --git a/crates/latkit/shaders/draw/glyphs.wesl b/crates/latkit/shaders/draw/glyphs.wesl index 62ce4b87..38140fc8 100644 --- a/crates/latkit/shaders/draw/glyphs.wesl +++ b/crates/latkit/shaders/draw/glyphs.wesl @@ -1,7 +1,7 @@ -// Glyphs: every labeled row's text, placed from its anchor each frame and shown as placement decided, -// over a halo of the background so it reads over whatever lies under it. The ports variant draws -// each port's label beside it, inside its marker. Text is a distance field: edges stay crisp at any -// size, and the halo is a wider threshold of the same field. +// Glyphs: every labeled row's text, placed from its anchor each frame and shown as placement +// decided. A halo of the background color keeps text readable over anything below it. The ports +// variant draws each port's label beside it, inside its marker. Text is a distance field, so edges +// stay crisp at any size, and the halo is a wider threshold of the same field. import package::lib::types::{ CANDIDATE_WORDS, CANDIDATE_ALPHA, LABEL_CODE, LABEL_COLOR, PORT_WORDS, PORT_CODE, PORT_UV, SIDE_LEFT, @@ -32,7 +32,7 @@ struct Varyings { fn quad(t: Text, corner: u32, color: vec4f) -> Varyings { var out: Varyings; let q = vec2f(f32(corner & 1u), f32(corner >> 1u)); - // Snapped to whole target pixels while it is set at its own size, so text stays crisp. + // Outside a diagram, text has a fixed size; snapping to whole target pixels keeps it crisp. var p = mix(t.quad_lo, t.quad_hi, q); if frame.space != SPACE_DIAGRAM { p = round(p * eye.pixel_ratio) / eye.pixel_ratio; @@ -73,7 +73,7 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: let a = anchor(instance); let size = length_px(mark.label_size); let t = text_at(uv0, uv1, size, a.at, a.align); - // A diagram's labels show while they fit and read; elsewhere as placement decided. + // A diagram's labels show while they fit and are legible. Elsewhere, placement decides. var alpha = bitcast(word(record_at(mark.candidates, CANDIDATE_WORDS, row) + CANDIDATE_ALPHA)); if frame.space == SPACE_DIAGRAM { alpha = f32(a.shown && fits(t.hi - t.lo, a.room)) * legibility(size); @@ -87,8 +87,8 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: return quad(t, corner, vec4f(color.rgb, color.a * alpha)); } -// A port's label: beside the port, inside its marker, aligned away from the side it sits on. It -// shows while it reads and keeps clear of the ports across from it. +// A port's label: beside the port, inside its marker, aligned away from the port's side. It shows +// while legible and while it fits its side's share of the marker, clear of the opposite ports. @if(ports) @vertex fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: u32) -> Varyings { @@ -140,7 +140,7 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: return quad(t, corner, vec4f(text.rgb, text.a * alpha)); } -// The distance field at `uv`: texels past the text's edge, inside positive. +// The distance field at `uv`, in texels from the text's edge, positive inside. fn inside(uv: vec2f) -> f32 { let d = textureSampleLevel(atlas, ramp, uv / vec2f(textureDimensions(atlas)), 0.0).r; return (d - 0.5) * 2.0 * TEXT_SPREAD; @@ -148,7 +148,7 @@ fn inside(uv: vec2f) -> f32 { @fragment fn fragment(v: Varyings) -> @location(0) vec4f { - // Target pixels past the edge. + // Distance from the edge in target pixels. let d = inside(v.uv) * v.scale * eye.pixel_ratio; let a_text = clamp(0.5 + d, 0.0, 1.0) * v.color.a; let back = frame.background; diff --git a/crates/latkit/shaders/draw/overlay.wesl b/crates/latkit/shaders/draw/overlay.wesl index c7473e2f..e78479fa 100644 --- a/crates/latkit/shaders/draw/overlay.wesl +++ b/crates/latkit/shaders/draw/overlay.wesl @@ -1,6 +1,6 @@ -// The overlay: lines, boxes, filled rectangles, and runs of text in CSS pixels over every mark, such -// as a plot's axes, a scene's title and legends, a selection box, and a connection being drawn. Text -// is the atlas's distance field, its quad reaching over the field's spread. +// The overlay: lines, boxes, filled rectangles, and runs of text in CSS pixels, drawn over every +// mark. It draws a plot's axes, a scene's title and legends, a selection box, and a connection +// being drawn. Text samples the atlas's distance field, and its quad covers the field's spread. import package::lib::types::{Sprite, SPRITE_TEXT, SPRITE_BOX, SPRITE_FILL, TEXT_SPREAD}; import package::lib::bind::{eye, ramp}; diff --git a/crates/latkit/shaders/draw/points.wesl b/crates/latkit/shaders/draw/points.wesl index 1585a04e..e73fbd5c 100644 --- a/crates/latkit/shaders/draw/points.wesl +++ b/crates/latkit/shaders/draw/points.wesl @@ -1,9 +1,8 @@ -// Points: one instanced quad per vertex row, shaded by its marker; a group's rows draw as -// containers. A quad covers its marker and a pixel of edge, no more, and a small one a later one -// covers in a crowded cell is left out. The focus variant draws the rows the mark's focus lights -// again, over everything, reaching out to their glow; a group only glows about its border, so what -// it holds still shows. The ports variant draws one quad per port of every row, on its side of the -// row's marker. +// Points: one instanced quad per vertex row, shaded by its marker. A group's rows draw as +// containers. A quad covers its marker and any status ring or shadow, plus one pixel of antialiased +// edge. The focus variant redraws the lit rows over everything, with quads enlarged for their glow; +// a group glows only around its border, so its contents stay visible. The ports variant draws one +// quad per port of every row, on its side of the row's marker. import package::lib::types::{STATUS, MARKER_ICON, MARKER_SHAPE, MARKER_BLOCK, GROUP, SHADOW, SPACE_DIAGRAM}; import package::lib::bind::{frame, eye, mark, word, ramp, program, focused}; @@ -30,11 +29,11 @@ struct Varyings { @location(5) @interpolate(flat) shape: u32, // A group's depth; for a port, 1 when its reference names a net. @location(6) @interpolate(flat) extra: f32, - // The row's focus bits, which the focus draw glows by. + // The row's focus bits, which choose the focus draw's glow. @location(7) @interpolate(flat) focus: u32, } -// What an instance draws: a row's marker, or with `ports` one of a row's ports. +// One drawn instance: a row's marker, or in the ports variant, one of a row's ports. struct Drawn { p: Point, center: vec2f, @@ -93,8 +92,8 @@ fn inputs(row: u32) -> array { return input; } -// An icon: image `index` of the mark's images, fitted in the marker's box, faded with its row. The -// palette holds how many images there are, then each one's layer of the icon atlas. +// An icon: image `f.input[0]` of the mark's images, fitted to the marker's box and faded with its +// row. The palette holds the image count, then each image's layer in the icon atlas. fn icon(f: MarkerFragment) -> MarkerColor { let uv = vec2f(f.p.x, -f.p.y) / (2.0 * f.half) + 0.5; let count = max(program[mark.palette], 1u); @@ -113,9 +112,10 @@ fn marker(f: MarkerFragment) -> MarkerColor { return builtin_marker(f); } -// A port: its shape in its row's color mixed toward the theme's text, so it reads on the marker in -// either theme, ringed by the background so it reads apart from the marker; hollow while no net plugs -// into it. Its ring and outline are part of the drawing, as the port is. +// A port: its shape in the row's color mixed toward the theme's text color, so it stays readable on +// the marker in light and dark themes. A ring of the background color sets it apart from the +// marker. It draws hollow while no net connects to it. Its ring and outline are sized in length +// units, like the port itself. @if(ports) fn shaded(v: Varyings) -> MarkerColor { let u = frame.unit; @@ -130,8 +130,8 @@ fn shaded(v: Varyings) -> MarkerColor { return over(hollow, ring); } -// Whether the mark draws a diagram's blocks: shapes and blocks on a diagram's sheet, which the theme -// borders. +// Whether the mark draws a diagram's blocks: shape or block markers on a diagram's sheet, which get +// the theme's border. fn sheet_blocks() -> bool { return frame.space == SPACE_DIAGRAM && (mark.marker == MARKER_SHAPE || mark.marker == MARKER_BLOCK); } @@ -150,7 +150,7 @@ fn shaded(v: Varyings) -> MarkerColor { m = status_ring(m, vec4f(v.status.rgb, v.status.a * v.color.a)); } if (mark.flags & SHADOW) != 0u { - // y is up in a marker: the shadow falls below, measured from its outline lowered. + // y is up in a marker. The shadow falls below, measured from the outline shifted down. let below = v.p + vec2f(0.0, theme_lift()); let distance = select( outline_distance(below, v.half), @@ -173,7 +173,8 @@ fn fragment(v: Varyings) -> @location(0) vec4f { fn fragment(v: Varyings) -> @location(0) vec4f { let m = shaded(v); if (mark.flags & GROUP) != 0u { - // Drawn over what it holds, a group's body would hide it: it glows on both sides of its border. + // A group is drawn over its contents, so its body would hide them. It glows on both sides + // of its border instead. return output(glow(MarkerColor(vec4f(0.0), abs(m.distance)), v.focus)); } return output(glow(m, v.focus)); diff --git a/crates/latkit/shaders/draw/strokes.wesl b/crates/latkit/shaders/draw/strokes.wesl index 10161ad1..d89bdbf6 100644 --- a/crates/latkit/shaders/draw/strokes.wesl +++ b/crates/latkit/shaders/draw/strokes.wesl @@ -1,19 +1,19 @@ -// Strokes: one instanced quad per segment of every row's path, for edges and lines alike, covering -// the stroke, or its comets where they are wider, and a pixel of edge; a small segment a later one covers in a crowded cell is left out. A -// segment that ends a part of its path carries the arrowhead; in a diagram, a segment of no length is -// a junction, drawn as a dot. The focus variant draws the pieces of the rows the mark's focus lights -// again, over everything, reaching out to their glow. +// Strokes: one instanced quad per segment of every row's path, for edges and lines alike. A quad +// covers the stroke, or its comets where they are wider, any arrowhead, and one pixel of +// antialiased edge. The last segment of each part of a path draws the arrowhead. In a diagram, a +// zero-length segment is a junction, drawn as a dot. The focus variant redraws the lit rows' +// segments over everything, with quads enlarged for their glow. import package::lib::types::{ARROWS, PATH_TRACE, SPACE_DIAGRAM}; import package::lib::path::PATH; import package::lib::bind::{frame, mark, eye}; import package::lib::project::{to_clip, nearer, culled}; import package::lib::path::{ - segment, segment_screen, stroke_style, ends_colored, ends_part, path_count, path_length, px_per_length, + segment, segment_screen, stroke_style, ends_colored, ends_part, path_count, path_length, }; import package::lib::marker::{MarkerColor, glow}; import package::lib::focus::focus_of; -import package::lib::mark::fringe; +import package::lib::mark::{fringe, px_per_length}; import package::lib::stroke::{Stroke, stroke, stroke_distance}; import package::lib::daylight::daylight; import package::lib::color::output; @@ -25,17 +25,18 @@ struct Varyings { @location(1) @interpolate(flat) b: vec2f, @location(2) @interpolate(flat) row: u32, @location(3) color: vec4f, - // Width, distance along at `a` and at `b`, flow. + // Width, distance along at `a` and at `b` in CSS pixels, and flow. @location(4) @interpolate(flat) style: vec4f, - // Whether the segment ends in an arrowhead, then whether it dashes. + // Bit 0: the segment ends in an arrowhead. Bit 1: it dashes. @location(5) @interpolate(flat) arrow: u32, - // The row's focus bits, which the focus draw glows by. + // The row's focus bits, which choose the focus draw's glow. @location(6) @interpolate(flat) focus: u32, - // Distance along at `a` and at `b` in length units, and how far the row's comets traveled. + // Distance along at `a` and at `b` in position units, and how far the row's comets have + // traveled. @location(7) @interpolate(flat) comet: vec3f, } -// Whether this variant draws lit rows, its quads reaching out to their glow. +// Whether this variant draws lit rows, with quads enlarged for their glow. @if(focused) const LIT_DRAW: bool = true; @if(!focused) @@ -50,7 +51,7 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: out.clip = culled(); return out; } - // A stroke fades and grows with its row's tile. + // A stroke fades and scales with its row's tile. var style = stroke_style(s.row); style.color.a *= s.presence; style.width *= s.growth; @@ -78,7 +79,7 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: out.clip = nearer(to_clip(screen, select(ca, cb, t > 0.5))); out.a = a; out.b = b; - // How far along the whole segment this corner lies, of the part drawn. + // This corner's fraction along the whole segment, at the start or end of the drawn part. let f = mix(on.kept.x, on.kept.y, t); var color = style.color; if ends_colored() { @@ -86,9 +87,9 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: } let light = daylight(mix(ends[0].position.xy, ends[1].position.xy, f)); out.color = vec4f(color.rgb * light, color.a); - // In length units, a path runs from its start, a listed one's segment from where its list says it - // starts, its ends stopping short where the stroke does. On screen, a trace runs along x, a - // listed path as it does in length units, and else a segment from where its stroke starts. + // In position units, a segment's distance along starts where its list says (0 without a list), + // and its ends stop short where the stroke does. On screen, a trace measures along x, a listed + // path converts its distance along to pixels, and any other segment starts at 0. let along = s.along + path_length(ends[0], ends[1]) * on.kept; let pixels = length(b - a); var phase = vec2f(0.0, pixels); @@ -104,9 +105,10 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: return out; } -// The fragment's pixel in CSS pixels, a screen position as `a` and `b` are. Read from the fragment's -// own position, never interpolated: a quad whose ends lie at very different depths skews what is -// interpolated, and a renderer clipping the quad at the screen's edge, as lavapipe does, moves it. +// The fragment's position in CSS pixels, in the same screen space as `a` and `b`. It comes from the +// fragment's own position, never an interpolated value: interpolation skews across a quad whose +// ends lie at very different depths, and a renderer that clips the quad at the screen's edge, as +// lavapipe does, shifts it. fn pixel(v: Varyings) -> vec2f { return v.clip.xy / eye.pixel_ratio; } diff --git a/crates/latkit/shaders/draw/triangles.wesl b/crates/latkit/shaders/draw/triangles.wesl index 99f85578..25462a2f 100644 --- a/crates/latkit/shaders/draw/triangles.wesl +++ b/crates/latkit/shaders/draw/triangles.wesl @@ -1,5 +1,5 @@ -// Triangles: one instanced triangle per face row, lit from the camera so a flat mesh seen from above -// shows its exact colors; turned away, a face darkens by the theme's shading. +// Triangles: one instanced triangle per face row, lit from the camera so a flat mesh seen from +// above shows its exact colors. A face turned away darkens by the theme's shading. import package::lib::bind::{frame, eye, word}; import package::lib::project::culled; @@ -15,7 +15,7 @@ struct Varyings { @location(1) normal: vec3f, @location(2) color: vec4f, @location(3) @interpolate(flat) row: u32, - // The row's focus bits: a lit face tints by them. + // The row's focus bits, which tint a lit face. @location(4) @interpolate(flat) focus: u32, } @@ -30,7 +30,7 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: } out.clip = corner_clip(c); out.world = c.world; - // The normals summed at each vertex, in fixed point. + // The face normals summed at this corner's vertex, in fixed point. let n = end_normal(corner, c.vertex); if n != NONE { out.normal = vec3f(f32(bitcast(word(n))), f32(bitcast(word(n + 1u))), f32(bitcast(word(n + 2u)))); diff --git a/crates/latkit/shaders/lib/bind.wesl b/crates/latkit/shaders/lib/bind.wesl index 49b261fd..5d1b98b6 100644 --- a/crates/latkit/shaders/lib/bind.wesl +++ b/crates/latkit/shaders/lib/bind.wesl @@ -3,20 +3,25 @@ import package::lib::types::{Frame, Eye, Mark}; @group(0) @binding(0) var frame: Frame; -// Compiled marks: column descriptors, page tables, code, constants, ends. +// Every mark's compiled program: column descriptors, page tables, code, constants, ends. @group(0) @binding(1) var program: array; @group(0) @binding(2) var colormaps: texture_2d_array; @group(0) @binding(3) var ramp: sampler; -// What the pass draws through, at a dynamic offset. +// The camera the pass draws through. @group(0) @binding(4) var eye: Eye; -// The rows each mark's focus draw lights: every mark's, at its `focused`. +// The drawn rows each mark's focus draw lights, starting at the mark's `focused`. @group(0) @binding(5) var focused: array; @group(1) @binding(0) var mark: Mark; -// The pages, in blocks the arena hands out: data pages, column values as f32 or u32 words; and each -// mark's pages of words per row, which compute passes write and render passes read. A pass summing -// into them binds them as atomics. +// The theme's word in slot `slot`, one of the `THEME_*` slots. +fn theme_word(slot: u32) -> u32 { + return frame.theme[slot / 4u][slot % 4u]; +} + +// All pages, in blocks allocated by the arena. Data pages hold column values as f32 or u32 words. +// Each mark's pages hold its per-row words, which compute passes write and render passes read. A +// pass that sums into them binds them as atomics. @if(atomic) @group(2) @binding(0) var pages: array>; @elif(writes) diff --git a/crates/latkit/shaders/lib/color.wesl b/crates/latkit/shaders/lib/color.wesl index 0a068ae9..b74595ba 100644 --- a/crates/latkit/shaders/lib/color.wesl +++ b/crates/latkit/shaders/lib/color.wesl @@ -1,7 +1,7 @@ // Colors are sRGB-encoded, as in CSS, and packed RGBA8 in state. -import package::lib::types::{MISSING_COLOR, COLORMAP_CYCLIC, COLORMAP_CATEGORICAL}; -import package::lib::bind::{frame, colormaps, ramp}; +import package::lib::types::{THEME_MISSING, COLORMAP_CYCLIC, COLORMAP_CATEGORICAL}; +import package::lib::bind::{frame, colormaps, ramp, theme_word}; import package::lib::read::missing; // `x` mapped linearly from `domain` onto `range`, clamped. @@ -14,10 +14,11 @@ fn scale(x: f32, domain: vec2f, range: vec2f) -> f32 { return mix(range.x, range.y, clamp(t, 0.0, 1.0)); } -// `x` through colormap `layer` over `domain`, packed; missing for a missing `x`. +// `x` through colormap `layer` over `domain`, packed RGBA8; the theme's missing color if `x` is +// missing. fn colormap(layer: u32, kind: u32, x: f32, domain: vec2f) -> u32 { if missing(x) { - return MISSING_COLOR; + return theme_word(THEME_MISSING); } if kind == COLORMAP_CATEGORICAL { let bin = u32(abs(floor(x))) % 256u; @@ -32,7 +33,8 @@ fn to_linear(c: vec3f) -> vec3f { return select(pow((c + 0.055) / 1.055, vec3f(2.4)), c / 12.92, c <= vec3f(0.04045)); } -// A premultiplied fragment color for the target: sRGB values convert when it stores linear color. +// A premultiplied fragment color prepared for the target. sRGB values convert to linear when the +// target stores linear color. fn output(premultiplied: vec4f) -> vec4f { if frame.linear == 0u || premultiplied.a <= 0.0 { return premultiplied; diff --git a/crates/latkit/shaders/lib/daylight.wesl b/crates/latkit/shaders/lib/daylight.wesl index 07c30d5b..1b9e01d7 100644 --- a/crates/latkit/shaders/lib/daylight.wesl +++ b/crates/latkit/shaders/lib/daylight.wesl @@ -1,13 +1,13 @@ -// The earth's daylight: how lit a place is by the sun, night falling to the theme's brightness. +// The earth's daylight: how brightly the sun lights a place, down to the theme's night brightness. import package::lib::types::SPACE_EARTH; import package::lib::bind::frame; -// How far the sun stands below the horizon, in the cosine of its angle, where night has fallen. +// Night has fully fallen once the cosine of the sun's zenith angle drops to `-TWILIGHT`. const TWILIGHT: f32 = 0.1; -// How lit the earth is at longitude and latitude `p`, in degrees: 1 by day, down to the night's -// brightness, eased across the terminator; 1 everywhere without daylight. +// The daylight factor at longitude and latitude `p`, in degrees: 1 by day, easing across the +// terminator down to the night brightness. 1 everywhere when daylight is off or outside the earth. fn daylight(p: vec2f) -> f32 { let sun = frame.sun; if frame.space != SPACE_EARTH || dot(sun.xyz, sun.xyz) == 0.0 { diff --git a/crates/latkit/shaders/lib/drawn.wesl b/crates/latkit/shaders/lib/drawn.wesl index cf3940c2..8b873218 100644 --- a/crates/latkit/shaders/lib/drawn.wesl +++ b/crates/latkit/shaders/lib/drawn.wesl @@ -1,7 +1,7 @@ -// Drawn rows: which held row each row a mark draws is, and how present it is while tiles come and go. -// A table drawn whole draws each of its rows as the held row of its number. For one drawn in part, -// the program lists the runs it draws, and an expansion of them (`compute/lists.wesl`) leaves each -// drawn row's word in the lists at `mark.drawn`: its held row, and whether its tile fades in or out. +// Drawn rows: the held row behind each row a mark draws, and its fade while tiles enter and leave. +// A table drawn whole draws held row `i` as drawn row `i`. For a paged table, the program lists the +// runs the mark draws, and `expand_runs` (`compute/lists.wesl`) writes each drawn row's word to the +// lists at `mark.drawn`: its held row, and whether its tile fades in or out. import package::lib::types::{NONE, DRAWN_ROW, DRAWN_ENTERING, DRAWN_LEAVING}; import package::lib::bind::mark; @@ -9,10 +9,10 @@ import package::lib::lists::lists; struct DrawnRow { row: u32, - // How much of it shows, and how large it grows, as its tile fades in or out; 1 when steady. + // Opacity and scale factors while its tile fades in or out; 1 otherwise. presence: f32, growth: f32, - // Whether its tile is leaving: it no longer answers the pointer. + // Whether its tile is leaving. A leaving row ignores the pointer. leaving: bool, } @@ -28,7 +28,7 @@ fn drawn_row(i: u32) -> DrawnRow { return DrawnRow(word & DRAWN_ROW, presence, select(1.0, mark.reveal, entering), leaving); } -// The held row drawn row `i` of the current mark is. +// The held row of the current mark's drawn row `i`. fn held_row(i: u32) -> u32 { if mark.drawn == NONE { return i; diff --git a/crates/latkit/shaders/lib/ends.wesl b/crates/latkit/shaders/lib/ends.wesl index 648e3693..0c60344d 100644 --- a/crates/latkit/shaders/lib/ends.wesl +++ b/crates/latkit/shaders/lib/ends.wesl @@ -1,21 +1,20 @@ -// Where each end of an edge or a face row is held: the row of the vertex mark drawing the end's table -// that holds the vertex the row names. A row's ends name places in the order that table draws in, -// which stay put as the camera moves; the vertex mark holds its pages and the rows it keeps where its -// drawing lays them out, which do not. So no channel stores where an end is held: the expression -// machine's end step finds it, for the rows whose ends changed, and for every row once the vertices -// moved, and writes it into the row's kind word of the end's number, which draws, probes, and normals -// read. +// Resolves each end of an edge or face row to a held row of the vertex mark that draws the end's +// table. A row's ends name places in that table's order, and places stay fixed as the camera moves. +// Their held rows change, because the vertex mark's drawing makes pages and kept rows resident in +// its slots as the camera moves. So no channel stores an end's held row. The expression machine's +// end step resolves it for the rows whose ends changed, and for every row once the vertices moved. +// It writes end `k` into the row's kind word `k`, which draws, probes, and normals read. import package::lib::types::{NONE, PAGE_BITS, PAGE_MASK, MARK_FACES, END_WORDS, END_STATE, END_SLOTS, END_COLUMN}; import package::lib::bind::{program, mark, pages}; import package::lib::state::record_at; import package::lib::read::bits_value; -// Where place `place` of the order a table is drawn in is held, by where the program lists its held -// rows at `slots`: how many pages, then each one's slot; then the table of the rows it keeps whose -// pages it does not hold: how many buckets, then each one's place and held row, NONE where empty, a -// place in the first empty bucket from where its bits hash to, as `view/kept.rs` keeps them. NONE -// where it holds the place nowhere; a table drawn whole, with no such list, holds every row as itself. +// The held row of place `place`, from the end table at `slots`. The end table holds the page +// count, then each page's slot (NONE when not resident), then the kept-row hash table: the bucket +// count, then each bucket's place and held row (NONE where empty). A place sits in the first empty +// bucket at or after its hash, as `view/kept.rs` lays them out. Returns NONE when the place is not +// resident. Without an end table (a table drawn whole), each place is its own held row. fn held_of(slots: u32, place: u32) -> u32 { if slots == NONE || place == NONE { return place; @@ -34,7 +33,7 @@ fn held_of(slots: u32, place: u32) -> u32 { return NONE; } var b = home(place, buckets); - // At most four fifths of the buckets are full, so a probe meets an empty one within the table. + // At most four fifths of the buckets are full, so a probe always reaches an empty bucket. for (var probe = 0u; probe < buckets; probe++) { let at = kept + 1u + 2u * b; let held = program[at]; @@ -52,8 +51,7 @@ fn held_of(slots: u32, place: u32) -> u32 { return NONE; } -// The bucket a probe for place `d` starts at among `buckets`, as `view/kept.rs` finds it: its bits -// hashed. +// The bucket where a probe for place `d` starts: a hash of `d`, matching `view/kept.rs`. fn home(d: u32, buckets: u32) -> u32 { var h = d ^ (d >> 16u); h *= 0x7feb352du; @@ -62,15 +60,15 @@ fn home(d: u32, buckets: u32) -> u32 { return (h ^ (h >> 16u)) % buckets; } -// Writes where each end of live row `row` of the current mark is held: an edge's two, a face's three -// corners. +// Writes the held row of each end of the current mark's live row `row`: an edge's two ends, or a +// face's three corners. fn resolve(row: u32) { let at = record_at(mark.state, mark.stride + mark.extras, row); let ends = select(2u, 3u, mark.kind == MARK_FACES); for (var k = 0u; k < ends; k++) { let e = mark.ends + END_WORDS * k; let column = program[e + END_COLUMN]; - // A pole's end is its own row, held where its row is. + // A pole's end is the row itself, at the same held row. var held = row; if column != NONE { held = held_of(program[e + END_SLOTS], bits_value(column, row)); diff --git a/crates/latkit/shaders/lib/focus.wesl b/crates/latkit/shaders/lib/focus.wesl index 3ca23288..4c929988 100644 --- a/crates/latkit/shaders/lib/focus.wesl +++ b/crates/latkit/shaders/lib/focus.wesl @@ -1,5 +1,5 @@ -// Selection and hover: one word of bits per held row, in pages the page table at `mark.focus` lists; -// and how present a row is while its mark eases. +// Selection and hover: one word of focus bits per held row, in pages listed by the page table at +// `mark.focus`. Also how much of a row shows during a transition. import package::lib::types::{SELECTED, HOVERED, FADE_IN, FADE_OUT}; import package::lib::bind::{frame, mark, word}; @@ -21,7 +21,7 @@ fn is_hovered(row: u32) -> bool { return (focus_of(row) & HOVERED) != 0u; } -// How much of a row shows while its mark eases: rows appearing fade in, rows leaving fade out. +// A row's opacity factor during a transition: appearing rows fade in, leaving rows fade out. fn presence(flags: u32) -> f32 { if (flags & FADE_IN) != 0u { return mark.motion; @@ -32,8 +32,8 @@ fn presence(flags: u32) -> f32 { return 1.0; } -// How large a row shows while its mark eases: rows appearing grow from nothing. Draws grow them, so -// a mark whose rows only appear evaluates once. +// A row's scale factor during a transition: appearing rows grow from nothing. Draws apply it, so a +// mark whose rows only appear evaluates once. fn growth(flags: u32) -> f32 { return select(1.0, mark.motion, (flags & FADE_IN) != 0u); } diff --git a/crates/latkit/shaders/lib/label.wesl b/crates/latkit/shaders/lib/label.wesl index 034f63e9..86d53e08 100644 --- a/crates/latkit/shaders/lib/label.wesl +++ b/crates/latkit/shaders/lib/label.wesl @@ -1,8 +1,8 @@ -// Labels: where a labeled row's text hangs, how large it draws, and whether it can show. Draws place -// every label from its anchor each frame, so labels follow the camera and the rows they name exactly; -// placement measures the same box only to decide which labels give way. Text is a distance field set -// at `TEXT_BASE`, drawn at any size: CSS pixels, or units in a diagram, where text scales with the -// sheet and fades out once too small to read. +// Labels: where a labeled row's text is anchored, its size, and whether it can show. Draws place +// every label from its anchor each frame, so labels track the camera and their rows exactly. +// Placement measures the same box only to decide which labels are dropped. Text is a distance field +// set at `TEXT_BASE` and drawn at any size: in CSS pixels, or in a diagram in length units, where +// text scales with the sheet and fades out once too small to read. import package::lib::types::{ POINTS, STROKES, MARKER_BLOCK, SIZED, GROUP, PATH_TRACE, PATH_GEOMETRY, TEXT_BASE, TEXT_SPREAD, HEADER_BAND, @@ -16,8 +16,8 @@ import package::lib::path::{PATH, Segment, segment_screen, path_count}; import package::lib::triangles::{triangle_corner, area_anchor}; import package::lib::drawn::held_row; -// Where a row's label hangs, CSS pixels, how it aligns to that point (0 left or top, 0.5 centered), -// the room it must fit in (none when zero), and whether its row shows. +// A label's anchor point in CSS pixels, its alignment there (0 left or top, 0.5 centered), the room +// it must fit in (no limit when zero), and whether its row shows. struct Anchor { at: vec2f, align: vec2f, @@ -25,12 +25,12 @@ struct Anchor { shown: bool, } -// CSS pixels a size in length units spans this frame. +// A size in length units, in CSS pixels at this frame's zoom. fn length_px(size: f32) -> f32 { return size * frame.unit; } -// Where drawn row `item`'s label hangs. +// The label anchor of drawn row `item`. fn anchor(item: u32) -> Anchor { var a: Anchor; let row = held_row(item); @@ -47,7 +47,7 @@ fn anchor(item: u32) -> Anchor { a.align = vec2f(0.0, 0.5); a.room = vec2f(2.0 * (p.half.x - frame.group_padding), header); } else if mark.marker == MARKER_BLOCK || (mark.flags & SIZED) != 0u { - // A block, or any marker given a width and height, holds its label while it fits. + // A block, or a marker with a width and height, shows its label inside if it fits. a.at = center; a.align = vec2f(0.5); a.room = vec2f(2.0 * p.half.x - text, 2.0 * p.half.y); @@ -74,7 +74,7 @@ fn anchor(item: u32) -> Anchor { default: { a.align = vec2f(0.5); if PATH == PATH_GEOMETRY { - // An area's label hangs inside its largest polygon, farthest from its edges. + // An area's label sits inside its largest polygon, farthest from its edges. let c = area_anchor(row); let clip = clip_of(c.world); a.shown = c.shown && c.ahead && clip.w > 0.0; @@ -95,8 +95,8 @@ fn anchor(item: u32) -> Anchor { return a; } -// How much text `px` CSS pixels tall shows: in a diagram it fades out as the view zooms away from -// it; elsewhere it is set at a size that reads. +// Opacity of text `px` CSS pixels tall. In a diagram, text fades out as the view zooms out. +// Elsewhere text has a fixed, readable size, so it always shows. fn legibility(px: f32) -> f32 { if frame.space != SPACE_DIAGRAM { return 1.0; @@ -104,9 +104,8 @@ fn legibility(px: f32) -> f32 { return smoothstep(3.5, 6.0, px); } -// A run of text placed on screen: the text's box and the quad around it, which reaches over the -// distance field's spread, in CSS pixels; its corners in the atlas, texels; and CSS pixels per -// texel. +// A run of text placed on screen: the text's box and its quad, which extends over the distance +// field's spread (both in CSS pixels); its atlas corners in texels; and CSS pixels per texel. struct Text { lo: vec2f, hi: vec2f, @@ -117,12 +116,13 @@ struct Text { scale: f32, } -// A rectangle's corner in the atlas: 16 bits of x, then 16 of y. +// An atlas corner packed in one word: x in the low 16 bits, y in the high 16. fn corner(word: u32) -> vec2f { return vec2f(f32(word & 0xffffu), f32(word >> 16u)); } -// The run at atlas corners `uv0` and `uv1`, set `size` CSS pixels tall, hung at `at` by `align`. +// The run at atlas corners `uv0` and `uv1`, drawn at size `size` in CSS pixels, aligned by `align` +// at point `at`. fn text_at(uv0: u32, uv1: u32, size: f32, at: vec2f, align: vec2f) -> Text { var t: Text; t.uv_lo = corner(uv0); @@ -137,12 +137,12 @@ fn text_at(uv0: u32, uv1: u32, size: f32, at: vec2f, align: vec2f) -> Text { return t; } -// Whether text of extent `extent` fits `room`; no room holds any. +// Whether text of extent `extent` fits `room`. A room of zero width fits any text. fn fits(extent: vec2f, room: vec2f) -> bool { return room.x <= 0.0 || all(extent <= room); } -// Whether a box reaches into the view. +// Whether a box overlaps the view. fn in_view(lo: vec2f, hi: vec2f) -> bool { return all(hi > vec2f(0.0)) && all(lo < eye.size); } diff --git a/crates/latkit/shaders/lib/lists.wesl b/crates/latkit/shaders/lib/lists.wesl index 4766eeb7..acaa4cf8 100644 --- a/crates/latkit/shaders/lib/lists.wesl +++ b/crates/latkit/shaders/lib/lists.wesl @@ -1,4 +1,5 @@ -// Lists: per-item counts, scanned in place to where each item's entries start, then the entries. +// Lists: per-item counts, then entries. A scan turns the counts in place into each item's first +// entry. import package::lib::types::{LIST_ITEMS, LIST_COUNTS, LIST_ENTRIES, LIST_DRAW, LIST_KEPT}; @@ -17,9 +18,9 @@ fn list_entry(header: u32, i: u32) -> u32 { return lists[header + LIST_ENTRIES] + i; } -// Where item `i`'s entries start among those the list at `header` keeps: where it counted them, or, -// when the items counted more than the list keeps, as far in proportion, so every item keeps a share -// of its entries instead of the last items losing all of theirs. +// Where item `i`'s entries start among those the list at `header` keeps. When the items counted +// more entries than the list keeps, the starts scale down in proportion, so every item keeps a +// share of its entries instead of the last items losing all of theirs. fn list_start(header: u32, i: u32) -> u32 { let counts = lists[header + LIST_COUNTS]; let total = lists[counts + lists[header + LIST_ITEMS]]; diff --git a/crates/latkit/shaders/lib/mark.wesl b/crates/latkit/shaders/lib/mark.wesl index 8e671a36..b873ed50 100644 --- a/crates/latkit/shaders/lib/mark.wesl +++ b/crates/latkit/shaders/lib/mark.wesl @@ -1,19 +1,25 @@ -// What every mark's shaders share. +// Helpers shared by every mark's shaders. -import package::lib::types::POINTS; -import package::lib::bind::{eye, mark}; +import package::lib::types::{POINTS, SPACE_EARTH}; +import package::lib::bind::{frame, eye, mark}; -// The primitive a pipeline's marks draw, `POINTS`, `STROKES`, or `TRIANGLES`, for the shaders made for -// one: each pipeline holds only that one's geometry, and compiles quickly. +// The primitive a pipeline's marks draw: `POINTS`, `STROKES`, or `TRIANGLES`. Each pipeline +// compiles only that primitive's geometry, so it compiles quickly. override PRIMITIVE: u32 = POINTS; -// A size in CSS pixels: state word `word` holds position units when the mark measures it in units. +// CSS pixels per position unit at the camera's center. On the earth a position unit is a degree, +// and the eye's scale is per mercator radian. +fn px_per_length() -> f32 { + return eye.px_per_unit.x * select(1.0, radians(1.0), frame.space == SPACE_EARTH); +} + +// `size` in CSS pixels. State word `word` holds position units when the mark measures it in units. fn px(size: f32, word: u32) -> f32 { - return select(size, size * eye.px_per_unit.x, (mark.units & (1u << word)) != 0u); + return select(size, size * px_per_length(), (mark.units & (1u << word)) != 0u); } -// A marker's half, CSS pixels, scaled to keep its larger side within `range`: what a size in units -// keeps of its looks at any zoom. +// A marker's half size in CSS pixels, scaled so its larger side stays within `range`. Both sides +// scale together, so a size in units keeps its shape at any zoom. fn within(half: vec2f, range: vec2f) -> vec2f { let side = max(half.x, half.y); if side <= 0.0 { @@ -27,7 +33,7 @@ fn coverage(d: f32) -> f32 { return clamp(0.5 - d * eye.pixel_ratio, 0.0, 1.0); } -// CSS pixels a quad reaches past the shape it draws: one target pixel, over which its edge fades. +// How far a quad extends past its shape, in CSS pixels: one target pixel, for the antialiased edge. fn fringe() -> f32 { return 1.0 / eye.pixel_ratio; } diff --git a/crates/latkit/shaders/lib/marker.wesl b/crates/latkit/shaders/lib/marker.wesl index 6687bb72..516f5f8c 100644 --- a/crates/latkit/shaders/lib/marker.wesl +++ b/crates/latkit/shaders/lib/marker.wesl @@ -1,10 +1,10 @@ -// Markers are signed distance fields in CSS pixels; a custom marker supplies `fn marker`. +// Markers, drawn as signed distance fields in CSS pixels. A custom marker supplies `fn marker`. import package::lib::types::{ SELECTED, HOVERED, SHAPE_RECTANGLE, SHAPE_ROUNDED, SHAPE_DIAMOND, MARKER_GAUGE, MARKER_PIE, MARKER_BLOCK, THEME_TEXT, THEME_OUTLINE, THEME_CORNER, THEME_PORT_SIZE, THEME_LIFT, }; -import package::lib::bind::{frame, eye, mark, program}; +import package::lib::bind::{frame, eye, mark, program, theme_word}; import package::lib::mark::coverage; import package::lib::project::TAU; @@ -36,21 +36,17 @@ fn shape_distance(shape: u32, p: vec2f, half: vec2f) -> f32 { } } -fn theme_word(slot: u32) -> u32 { - return frame.theme[slot / 4u][slot % 4u]; -} - -// The theme's text color: what ports mix toward, so they read on light and dark grounds. +// The theme's text color. Ports mix toward it so they stay readable on light and dark backgrounds. fn theme_text() -> vec3f { return unpack4x8unorm(theme_word(THEME_TEXT)).rgb; } -// The border of a diagram's blocks and groups; clear for none. +// The border color of a diagram's blocks and groups; clear for none. fn theme_outline() -> vec4f { return unpack4x8unorm(theme_word(THEME_OUTLINE)); } -// A diagram's corner radius, a port's width, and how far a shadow falls: CSS pixels at this zoom. +// A diagram's corner radius, port width, and shadow offset, in CSS pixels at the current zoom. fn theme_corner() -> f32 { return bitcast(theme_word(THEME_CORNER)) * frame.unit; } @@ -63,8 +59,8 @@ fn theme_lift() -> f32 { return bitcast(theme_word(THEME_LIFT)) * frame.unit; } -// The outline a marker's distance is measured from: blocks are rounded boxes, their corners the -// theme's unless the marker sets its own, others their shape. +// Signed distance from a marker's outline. Blocks are rounded boxes with the theme's corner radius +// unless the marker sets its own. Other markers use their shape. fn outline_distance(p: vec2f, half: vec2f) -> f32 { if mark.marker == MARKER_BLOCK { let corner = select(mark.corner * frame.unit, theme_corner(), mark.corner < 0.0); @@ -78,7 +74,7 @@ fn filled(color: vec4f, distance: f32) -> MarkerColor { return MarkerColor(vec4f(color.rgb, color.a * coverage(distance)), distance); } -// `top` over `bottom`. +// `top` composited over `bottom`. fn over(top: MarkerColor, bottom: MarkerColor) -> MarkerColor { let a = top.color.a + bottom.color.a * (1.0 - top.color.a); let rgb = (top.color.rgb * top.color.a + bottom.color.rgb * bottom.color.a * (1.0 - top.color.a)) / max(a, 1e-6); @@ -124,15 +120,16 @@ fn builtin_marker(f: MarkerFragment) -> MarkerColor { } } -// A group's container: a box with the theme's corners, filled in its color, tinted a step toward the -// text color for each group holding it, and bordered; its title sits in the room atop it. +// A group's container: a bordered box with the theme's corner radius, filled with its color. Each +// enclosing group tints the fill a step toward the text color. The title sits in its header band. fn container(f: MarkerFragment, depth: f32) -> MarkerColor { let fill = mix(f.color.rgb, theme_text(), min(0.06 * depth, 0.3)); let d = box_distance(f.p, f.half, min(theme_corner(), 0.5 * min(f.half.x, f.half.y))); return border(filled(vec4f(fill, f.color.a), d), d, f.color.a); } -// `m`, its outline `d` bordered inside in the theme's outline a unit wide, never under a pixel. +// `m` with a border just inside its outline `d`, in the theme's outline color, one length unit wide +// but at least one target pixel. fn border(m: MarkerColor, d: f32, alpha: f32) -> MarkerColor { let outline = theme_outline(); if outline.a <= 0.0 { @@ -142,15 +139,16 @@ fn border(m: MarkerColor, d: f32, alpha: f32) -> MarkerColor { return MarkerColor(over(filled(vec4f(outline.rgb, outline.a * alpha), abs(d + half) - half), m).color, m.distance); } -// `m` over its shadow on the sheet: its outline `distance` lowered by the theme's lift, as soft -// again, in the theme's shadow color. +// `m` over its shadow on the sheet, in the theme's shadow color. `distance` is measured from the +// outline lowered by the theme's lift, and the shadow's edge blurs over that lift on either side. fn dropped(m: MarkerColor, distance: f32, alpha: f32) -> MarkerColor { let lift = max(theme_lift(), 0.5); let a = frame.shadow.a * alpha * (1.0 - smoothstep(-lift, lift, distance)); return MarkerColor(over(m, MarkerColor(vec4f(frame.shadow.rgb, a), distance)).color, m.distance); } -// A ring of the status color just outside the marker's outline, a part of the drawing in a diagram. +// A ring of the status color just outside the marker's outline. Its size is in length units, so in +// a diagram it scales with the drawing. fn status_ring(m: MarkerColor, status: vec4f) -> MarkerColor { let u = frame.unit; return MarkerColor(over(filled(status, abs(m.distance - 1.5 * u) - 1.25 * u), m).color, m.distance); @@ -161,8 +159,8 @@ fn premultiplied(m: MarkerColor) -> vec4f { return vec4f(m.color.rgb * m.color.a, m.color.a); } -// A premultiplied color with the glow of focus bits `focus` around its shape: the selection's, the -// hover's, or for the end of a lit edge the hover's, fainter. +// A premultiplied color with a glow around its shape for focus bits `focus`: the selection color, +// the hover color, or for the end of a lit edge, a fainter hover color. fn glow(m: MarkerColor, focus: u32) -> vec4f { let base = premultiplied(m); if focus == 0u || frame.glow <= 0.0 { diff --git a/crates/latkit/shaders/lib/path.wesl b/crates/latkit/shaders/lib/path.wesl index d903fd8f..5c0e5b5b 100644 --- a/crates/latkit/shaders/lib/path.wesl +++ b/crates/latkit/shaders/lib/path.wesl @@ -1,34 +1,35 @@ -// Paths: the segments a stroke draws, whatever mark it belongs to. A row's path runs between an -// edge's ends (straight, through the bends of a geometry column, or along the great circle on the -// earth, bowed into its lane), through a line's samples across the axis, or through a geometry -// column's points: a route a diagram's sheet worked out, or a path field. Draws, casts, probes, bounds, and labels all read a -// segment through `segment_ends` and `segment_screen`. Which path a pipeline reads is its `PATH`, so -// each compiles one kind of path. +// Paths: the segments a stroke draws, for any mark kind. A row's path runs between an edge's ends +// (straight, through the bends of a geometry column, or along a great circle on the earth, bowed +// into its lane), through a line's samples across the axis, or through a geometry column's points +// (a route computed for a diagram's sheet, or a path field). Draws, probes, bounds, and labels all +// read segments through `segment_ends` and `segment_screen`. A pipeline's `PATH` selects one kind +// of path, so each pipeline compiles only that kind. import package::lib::types::{ NONE, VISIBLE, PLACED, DASH, ENDS_COLOR, GEODESIC, STEP_BEFORE, STEP_AFTER, MARK_EDGES, PATH_ENDS, PATH_TRACE, - PATH_GEOMETRY, PATH_POLES, EDGE_WIDTH, LINE_WIDTH, GEODESIC_STEP, OWN_HEIGHT, TRIANGLES, MAX_PIECES, BOW_PIECES, SPACE_EARTH, + PATH_GEOMETRY, PATH_POLES, EDGE_WIDTH, LINE_WIDTH, GEODESIC_STEP, OWN_HEIGHT, TRIANGLES, MAX_PIECES, BOW_PIECES, MISSING, FAMILY_LANES, }; import package::lib::bind::{frame, eye, mark, focused}; import package::lib::state::{row_at, edge_at, line_at, end_vertex, same_vertex, Vertex}; -import package::lib::project::{PI, world, clip_of, shows, to_screen}; +import package::lib::project::{world, clip_of, shows, to_screen}; import package::lib::read::{missing_bits, column_value, points_of, point_bits}; import package::lib::trace::{trace_run, trace_level, trace_axis, trace_entry}; import package::lib::focus::{faded, growth}; -import package::lib::mark::px; +import package::lib::mark::{px, px_per_length}; import package::lib::points::vertex_half; import package::lib::lists::{lists, list_entry, list_start, list_total}; import package::lib::drawn::drawn_row; -// The path a pipeline's strokes read: one of the `PATH_` codes, fixed when the pipeline is made. +// The path kind a pipeline's strokes read, one of the `PATH_` codes, fixed when the pipeline is +// created. override PATH: u32 = PATH_ENDS; -// One segment of a row's path, and how far along the path it starts, in length units: position -// units, degrees on the earth. `row` is the held row, whose state and columns it reads; `item` the -// drawn row, which the mark's list numbers. A list keeps each segment's distance along; a row of one -// segment starts at 0. As its row's tile comes and goes, it shows `presence` of itself, grown by -// `growth`, and while it leaves answers no pointer. +// One segment of a row's path. `along` is the distance along the path where it starts, in position +// units (degrees on the earth). The segment list stores it; without a list it is 0. `row` is the +// held row, whose state and columns it reads. `item` is the drawn row, which the mark's list +// numbers. While its tile fades in or out, the segment shows with opacity `presence` and scale +// `growth`, and a leaving segment ignores the pointer. struct Segment { row: u32, item: u32, @@ -39,15 +40,14 @@ struct Segment { leaving: bool, } -// Segment `index` of drawn row `item`'s path, starting `along` it: the row looked up once. +// Segment `index` of drawn row `item`'s path, starting at distance `along`. fn path_segment(item: u32, index: u32, along: f32) -> Segment { let d = drawn_row(item); return Segment(d.row, item, index, along, d.presence, d.growth, d.leaving); } -// A point a path runs through: where it stands (position units as high and low parts, elevation), -// in world units and in position units, its color, and whether it is shown. Sources set where it -// stands; `path_point` places it in the world once. +// A point on a path: its position as high and low parts plus elevation, set by its source; the +// same point in world and position units, set by `placed_point`; its color; and whether it shows. struct PathPoint { hi: vec2f, lo: vec2f, @@ -55,15 +55,16 @@ struct PathPoint { world: vec3f, position: vec3f, color: vec4f, - // A vertex's half size in CSS pixels, a box when it has its own height; zero elsewhere. + // For a vertex, its half size in CSS pixels and whether it is a box (has its own height); zero + // elsewhere. half: vec2f, boxed: bool, - // Whether it has a place to stand, and whether it lies in front of the globe's horizon. + // Whether it has a position, and whether it lies in front of the globe's horizon. shown: bool, ahead: bool, } -// How a row's path is stroked: its comets' flow, and how far they traveled. +// How a row's path is stroked, including its comets' flow and how far they have traveled. struct Style { width: f32, color: vec4f, @@ -73,9 +74,9 @@ struct Style { shown: bool, } -// A segment on screen: its ends as points, in CSS pixels, and in clip space, whether it shows, and -// the part of it drawn, as fractions of it: cut at the near plane, and stopping short of the vertices -// its ends meet. +// A segment on screen: its end points, its ends in CSS pixels and in clip space, whether it shows, +// and the drawn part as fractions along it. The drawn part is cut at the near plane and stops short +// of the vertices at its ends. struct OnScreen { ends: array, a: vec2f, @@ -86,8 +87,8 @@ struct OnScreen { kept: vec2f, } -// The segment drawn as instance `instance`: a fixed number per row, or numbered by the mark's list, -// whose entries are each segment's row and distance along. +// The segment drawn as instance `instance`. Rows have a fixed number of segments, or the mark's +// list numbers them, with each entry holding a segment's drawn row and distance along. @if(!focused) fn segment(instance: u32) -> Segment { if mark.per_row != NONE { @@ -99,8 +100,8 @@ fn segment(instance: u32) -> Segment { return path_segment(item, instance - list_start(mark.list, item), bitcast(lists[entry + 1u])); } -// In the focus draw, piece `instance % mark.pieces` of the lit row `instance / mark.pieces`, as far -// along as its list keeps it; pieces past the row's own end draw nothing. +// In the focus draw: segment `instance % mark.pieces` of lit row `instance / mark.pieces`, with its +// distance along from the list. Segments past the row's last draw nothing. @if(focused) fn segment(instance: u32) -> Segment { let pieces = max(mark.pieces, 1u); @@ -113,17 +114,12 @@ fn segment(instance: u32) -> Segment { return path_segment(item, index, along); } -// Length units between two points of a path: what a segment adds to the distance along it. +// Distance between two path points in position units, which a segment adds to the distance along. fn path_length(a: PathPoint, b: PathPoint) -> f32 { return length(b.position.xy - a.position.xy); } -// CSS pixels a length unit spans where the camera looks: a degree's on the earth. -fn px_per_length() -> f32 { - return eye.px_per_unit.x * select(1.0, PI / 180.0, frame.space == SPACE_EARTH); -} - -// Segments of every live row's path. +// Segments across all drawn rows' paths. fn stroke_instances() -> u32 { if mark.per_row != NONE { return mark.rows * mark.per_row; @@ -131,7 +127,7 @@ fn stroke_instances() -> u32 { return list_total(mark.list); } -// Segments in drawn row `item`'s path, as drawn. +// Segments in drawn row `item`'s path. fn path_count(item: u32) -> u32 { if mark.per_row != NONE { return mark.per_row; @@ -139,7 +135,7 @@ fn path_count(item: u32) -> u32 { return list_start(mark.list, item + 1u) - list_start(mark.list, item); } -// Segments a geodesic from `a` to `b` takes: one per few degrees of arc. +// Segments a geodesic from `a` to `b` takes: one per `GEODESIC_STEP` degrees of arc. fn arcs(a: vec2f, b: vec2f) -> u32 { let p = radians(a); let q = radians(b); @@ -147,7 +143,8 @@ fn arcs(a: vec2f, b: vec2f) -> u32 { return clamp(u32(ceil(degrees(d) / GEODESIC_STEP)), 1u, MAX_PIECES); } -// Points the geometry column gives held row `row`: an edge's bends, or a whole path. +// The geometry column's point count for held row `row`: an edge's bends, or a whole path. Rows that +// left have none. fn geometry_count(row: u32) -> u32 { if row >= mark.held - mark.exits { return 0u; @@ -155,11 +152,11 @@ fn geometry_count(row: u32) -> u32 { return points_of(mark.geometry, row).count; } -// Whether lane `l` of edge row `row` draws: the lanes of a family draw as the pixels they cover ask, -// so its thousands of lanes take as many as the screen shows, in draws and picks alike. A lane whose -// curve lies off the screen draws nothing; and where a pixel holds many lanes, which bow apart by the -// same step, only every `step`-th lane draws, `step` a power of two, so the lanes drawn lie at least -// a pixel apart. +// Whether lane `l` of edge row `row` draws. A family draws only the lanes the screen can show +// apart, so a family of thousands of lanes costs only as many lanes as the screen resolves, in +// draws and probes alike. A lane whose curve lies off screen draws nothing. Where neighboring lanes +// lie less than a pixel apart, only every `step`-th lane draws, `step` a power of two, so drawn +// lanes are at least a pixel apart. fn lane_drawn(row: u32, l: Lane) -> bool { let e = edge_at(row_at(row)); let a = end_vertex(0u, e.a); @@ -172,8 +169,8 @@ fn lane_drawn(row: u32, l: Lane) -> bool { let sa = to_screen(ca); let sb = to_screen(cb); if (mark.flags & GEODESIC) == 0u { - // The curve keeps within the triangle of its ends and the point that bows it, twice as far - // out as its middle. + // The curve stays within the triangle of its ends and its control point, which lies twice + // as far out as the curve's middle. let c = between(a, b, 0.5, 2.0 * l.offset * frame.bow); let cc = clip_of(world(c.hi, c.lo, c.z)); if cc.w > 0.0 { @@ -196,10 +193,10 @@ fn lane_drawn(row: u32, l: Lane) -> bool { return l.index % step == 0u; } -// Segments row `row`'s path has, or for an area its triangles: the count a list over the mark's rows -// counts. A path between two ends without bends runs in one piece, or bowed into a lane in enough -// for the curve, none where its lane lies off the screen or is one of many a pixel holds, and along a -// great circle in one per few degrees of its arc. +// The segment count of held row `row`'s path, or an area's triangle count, as a segment list counts +// them. A path between two ends without bends takes one segment, or `BOW_PIECES` when bowed into a +// lane, and none when its lane is not drawn (`lane_drawn`). On a great circle it takes one segment +// per `GEODESIC_STEP` degrees of arc instead, when that is more (up to `MAX_PIECES`). fn row_segments(row: u32) -> u32 { switch PATH { case PATH_ENDS: { @@ -226,7 +223,7 @@ fn row_segments(row: u32) -> u32 { case PATH_GEOMETRY: { let count = geometry_count(row); if mark.primitive == TRIANGLES { - // An area's triangles follow the point its label hangs from, three points each. + // An area's points start with its label anchor, then three per triangle. return select(0u, (count - 1u) / 3u, count > 0u); } return max(count, 1u) - 1u; @@ -237,7 +234,7 @@ fn row_segments(row: u32) -> u32 { } } -// The vertex held row `held` names through end `end` as a point of a path, placed. +// Vertex `held` of end `end` as a placed path point. fn vertex_point(end: u32, held: u32) -> PathPoint { let v = end_vertex(end, held); var p: PathPoint; @@ -251,7 +248,7 @@ fn vertex_point(end: u32, held: u32) -> PathPoint { return placed_point(p); } -// CSS pixels from a vertex's middle to its outline along `dir`: a box's or a circle's. +// CSS pixels from a vertex's center to its outline along `dir`, for a box or a circle. fn outline(p: PathPoint, dir: vec2f) -> f32 { if p.boxed { return min(p.half.x / max(abs(dir.x), 1e-6), p.half.y / max(abs(dir.y), 1e-6)); @@ -275,7 +272,7 @@ fn position_point(xy: vec2f, z: f32, color: vec4f) -> PathPoint { return p; } -// `p` placed in the world. +// `p` with `world`, `position`, and `ahead` computed. fn placed_point(p: PathPoint) -> PathPoint { var out = p; out.world = world(p.hi, p.lo, p.z); @@ -284,14 +281,14 @@ fn placed_point(p: PathPoint) -> PathPoint { return out; } -// The point a fraction `t` of the way along the great circle from `a` to `b`, in degrees, set off to -// the left by `bow` of the arc's length. +// The point a fraction `t` along the great circle from `a` to `b` (in degrees), offset to the left +// by `bow` times the arc's length. fn along_circle(a: vec2f, b: vec2f, t: f32, bow: f32) -> vec2f { let p = radians(a); let q = radians(b); let u = vec3f(cos(p.y) * cos(p.x), cos(p.y) * sin(p.x), sin(p.y)); let v = vec3f(cos(q.y) * cos(q.x), cos(q.y) * sin(q.x), sin(q.y)); - // Half the chord's angle, which stays precise for the shortest arcs. + // The arc's angle from its chord, which stays precise for the shortest arcs. let angle = 2.0 * asin(min(1.0, 0.5 * length(v - u))); var w = mix(u, v, t); if angle > 1e-5 { @@ -304,9 +301,9 @@ fn along_circle(a: vec2f, b: vec2f, t: f32, bow: f32) -> vec2f { return vec2f(lon, degrees(asin(clamp(w.z, -1.0, 1.0)))); } -// The point a fraction `t` of the way from vertex `a` to `b`, set off to the left by `bow` of the -// length between them: along the great circle on the earth, else straight from `a`'s high and low -// parts, so it keeps their precision. +// The point a fraction `t` from vertex `a` to `b`, offset to the left by `bow` times their +// distance. A `GEODESIC` mark follows the great circle. Otherwise the point is offset from `a`'s +// high and low parts along a straight line, which keeps their precision. fn between(a: Vertex, b: Vertex, t: f32, bow: f32) -> PathPoint { var p: PathPoint; p.z = mix(a.elevation, b.elevation, t); @@ -320,10 +317,10 @@ fn between(a: Vertex, b: Vertex, t: f32, bow: f32) -> PathPoint { return p; } -// Point `i` of an edge's path, placed: its first end, its bends, then its second end. Without bends, -// it runs between its ends in as many pieces as its list kept for it, each lane bowing in world units -// so the camera never moves it. An end reads only its own vertex. Each kind of point is made in one -// place, so shaders compile small. +// Point `i` of an edge's path, placed: its first end, its bends, then its second end. Without +// bends, it runs between its ends in `count` segments, and a lane bows in world space, so its curve +// stays fixed as the camera moves. An end reads only its own vertex. Each kind of point is built in +// one place, which keeps compiled shaders small. fn end_point(row: u32, count: u32, i: u32) -> PathPoint { let e = edge_at(row_at(row)); if e.a == NONE || e.b == NONE || same_vertex(e.a, e.b) { @@ -352,9 +349,9 @@ fn end_point(row: u32, count: u32, i: u32) -> PathPoint { return placed_point(p); } -// Point `k` of a trace: in a run of buckets, each bucket's least and greatest value at its middle; in -// a run of frames, each frame's value, or with steps the corners between them. A point past its run's -// own breaks the line before the next run. +// Point `k` of a trace. In a run of buckets, each bucket gives its least and greatest value at its +// middle. In a run of frames, each frame gives its value, plus the corners between frames when the +// mark steps. A point past its run's last breaks the line before the next run. fn trace_point(row: u32, k: u32) -> PathPoint { let run = trace_run(k); let local = k - run.point; @@ -397,9 +394,9 @@ fn trace_point(row: u32, k: u32) -> PathPoint { return p; } -// Point `i` of a geometry column's path: polylines whose parts are broken by a missing point. While -// the mark eases, each point moves from the path it ran through when the transition began: point -// for point when both have as many, else from the old point at the same fraction along. +// Point `i` of a geometry column's path: polylines whose parts are separated by missing points. +// During a transition, each point moves from the path at the transition's start: from the same +// point when both paths have the same count, else from the old point at the same fraction along. fn geometry_point(row: u32, i: u32) -> PathPoint { let now = points_of(mark.geometry, row); if i >= now.count { @@ -424,7 +421,7 @@ fn geometry_point(row: u32, i: u32) -> PathPoint { return position_point(xy, 0.0, vec4f(1.0)); } -// Point `i` of a pole: the row's own vertex where it stands, then the ground beneath it. +// Point `i` of a pole: the row's own vertex, then the ground below it. fn pole_point(row: u32, i: u32) -> PathPoint { let e = edge_at(row_at(row)); if e.a == NONE { @@ -439,7 +436,7 @@ fn pole_point(row: u32, i: u32) -> PathPoint { return p; } -// Point `i` of held row `row`'s path of `count` segments, whatever it runs through, placed in the world. +// Point `i` of held row `row`'s path of `count` segments, for any path kind, placed in the world. fn path_point(row: u32, count: u32, i: u32) -> PathPoint { switch PATH { case PATH_ENDS: { return end_point(row, count, i); } @@ -450,8 +447,8 @@ fn path_point(row: u32, count: u32, i: u32) -> PathPoint { } } -// The two points segment `s` runs between: points `index` and `index + 1` of its row's path. One loop -// makes both, so the path is compiled once. +// Segment `s`'s two end points: points `index` and `index + 1` of its row's path. One loop computes +// both, so the path code is compiled once. fn segment_ends(s: Segment) -> array { var ends: array; for (var e = 0u; e < 2u; e++) { @@ -460,7 +457,8 @@ fn segment_ends(s: Segment) -> array { return ends; } -// Whether segment `s` ends a part of its row's path: the path's last, or one before a break. +// Whether segment `s` ends a part of its row's path: the path's last segment, or one before a +// break. fn ends_part(s: Segment) -> bool { if PATH == PATH_GEOMETRY { let points = points_of(mark.geometry, s.row); @@ -470,10 +468,10 @@ fn ends_part(s: Segment) -> bool { return s.index + 1u == path_count(s.item); } -// Lanes from the middle that row `row` runs in among edges joining the same two vertices. -// A row's lane among its family, the edges joining the same two vertices: how far it bows, in lanes -// of the theme's bow, its place among the family from one side, and the step between lanes. A family -// spreads as wide as `FAMILY_LANES` lanes at most: past that its lanes pack closer, a step apart. +// A row's lane in its family, the edges joining the same two vertices: its offset from the middle, +// in lanes of the theme's bow; its index in the family, counted from one side; and the step between +// lanes. A family spreads at most `FAMILY_LANES` lanes wide, so a larger family packs its lanes +// closer, `step` apart. struct Lane { offset: f32, index: u32, @@ -484,7 +482,8 @@ fn lane_of(row: u32) -> Lane { if mark.lane == NONE || row >= mark.held - mark.exits { return Lane(0.0, 0u, 1.0); } - // Twice its lane, past half a word's range, above; its family's size below. + // The high 16 bits hold twice the lane's position from the middle, plus 32768. The low 16 bits + // hold the family's size. let word = column_value(mark.lane, row).x; let size = word & 0xffffu; if word == NONE || word == MISSING || size < 2u { @@ -500,9 +499,9 @@ fn lane(row: u32) -> f32 { } // The part of the segment from clip position `ca` to `cb` in front of the near plane, as fractions -// along it. Depth is reversed, so a position lies in front where z <= w, and w - z runs linearly along -// the segment: one reaching behind the eye is cut where it crosses, not dropped. Empty where both ends -// lie behind. +// along it. Depth is reversed, so a position lies in front where z <= w, and w - z is linear along +// the segment. A segment reaching behind the eye is cut where it crosses, not dropped. The range is +// empty when both ends lie behind. fn in_front(ca: vec4f, cb: vec4f) -> vec2f { let da = ca.w - ca.z; let db = cb.w - cb.z; @@ -510,7 +509,7 @@ fn in_front(ca: vec4f, cb: vec4f) -> vec2f { return vec2f(select(0.0, t, da < 0.0), select(1.0, t, db < 0.0)); } -// Segment `s` on screen: the part in front of the near plane, `kept` saying which part of it shows. +// Segment `s` on screen: the part in front of the near plane, with `kept` giving the drawn part. fn segment_screen(s: Segment) -> OnScreen { let ends = segment_ends(s); var out: OnScreen; @@ -518,7 +517,7 @@ fn segment_screen(s: Segment) -> OnScreen { let ca = clip_of(ends[0].world); let cb = clip_of(ends[1].world); let cut = in_front(ca, cb); - // A segment reaching past the globe's edge draws, and the globe's depth cuts it there. + // A segment reaching past the globe's edge still draws; the globe's depth clips it there. let ahead = ends[0].ahead || ends[1].ahead; out.shown = ends[0].shown && ends[1].shown && ahead && cut.x < cut.y; out.ca = mix(ca, cb, cut.x); @@ -526,8 +525,8 @@ fn segment_screen(s: Segment) -> OnScreen { out.a = to_screen(out.ca); out.b = to_screen(out.cb); out.kept = cut; - // Strokes stop at the outlines of the vertices they end at, without passing each other; an end - // cut at the near plane has no vertex there. + // Strokes stop at the outlines of their end vertices, and each end gives up at most half the + // length. An end cut at the near plane has no vertex to stop at. let along = out.b - out.a; let length_along = length(along); if length_along > 1e-3 { @@ -542,14 +541,14 @@ fn segment_screen(s: Segment) -> OnScreen { return out; } -// How row `row`'s path is stroked: never thinner than a hairline. Its comets' flow and travel are in -// the lengths the theme's flow measures them in. +// How row `row`'s path is stroked, never thinner than 0.75 CSS pixels. Comet flow and travel are in +// the theme's flow unit. fn stroke_style(row: u32) -> Style { var s: Style; if mark.kind == MARK_EDGES { let e = edge_at(row_at(row)); s.width = clamp(px(e.width, EDGE_WIDTH), mark.size_min, mark.size_max) * growth(e.flags); - // An edge colored by its ends takes their colors where it draws; this only fades it. + // An edge colored by its ends gets their colors in the draw; here it only fades. s.color = select(unpack4x8unorm(e.color), vec4f(1.0), ends_colored()); s.color.a *= faded(row, e.flags); s.flow = e.flow; diff --git a/crates/latkit/shaders/lib/placement.wesl b/crates/latkit/shaders/lib/placement.wesl index 4d24cd4d..a0b96434 100644 --- a/crates/latkit/shaders/lib/placement.wesl +++ b/crates/latkit/shaders/lib/placement.wesl @@ -1,7 +1,6 @@ -// What label placement works in: each labeled row's candidate, in pages the page table at -// `mark.candidates` lists; and the labels buffer, with the texts each mark asks the host to set and -// those its labels show, its run table, the select capping its labels, and the cells of the screen -// labels claim. +// Label placement storage. Each labeled row's candidate lives in pages listed by the page table at +// `mark.candidates`. The labels buffer holds each mark's requested and shown text codes, run table, +// and the select that caps its labels, plus the grid of screen cells that labels claim. import package::lib::types::{CANDIDATE_WORDS, WANTED}; import package::lib::bind::{mark, pages}; @@ -22,7 +21,7 @@ fn candidate(row: u32) -> u32 { return record_at(mark.candidates, CANDIDATE_WORDS, row); } -// Writes word `at` of the pages: a candidate's. +// Writes word `at` of the pages, such as a candidate word. fn put(at: u32, value: u32) { pages[at] = value; } @@ -35,7 +34,7 @@ fn want(code: u32) { } } -// Notes that a label shows the text of `code`, so the atlas keeps it. +// Marks the text of `code` as shown, so the atlas keeps it. fn seen(code: u32) { atomicOr(&labels[mark.seen + code / 32u], 1u << (code % 32u)); } diff --git a/crates/latkit/shaders/lib/points.wesl b/crates/latkit/shaders/lib/points.wesl index cc898aa8..a4bee567 100644 --- a/crates/latkit/shaders/lib/points.wesl +++ b/crates/latkit/shaders/lib/points.wesl @@ -1,23 +1,23 @@ -// Points: where a vertex row's marker stands, how large, and in what color. Draws, probes, bounds, -// and labels all read a point through `point`. +// Points: a vertex row's marker position, size, and color. Draws, probes, bounds, and labels all +// read a point through `point`. import package::lib::types::{ NONE, VISIBLE, PLACED, SIZED, STATUS, SHADOW, HOVERED, VERTEX_HALF, VERTEX_WORDS, SIDE_LEFT, SIDE_RIGHT, SIDE_TOP, PORT_WORDS, PORT_CODE, PORT_SHAPE, PORT_COLUMN, HALF_UNITS, OWN_HEIGHT, }; -import package::lib::bind::{frame, eye, mark, program}; +import package::lib::bind::{frame, mark, program}; import package::lib::state::{row_at, vertex_at, Vertex}; import package::lib::project::{world, clip_of, shows, to_screen}; import package::lib::focus::{focus_of, faded, growth}; import package::lib::read::bits_value; -import package::lib::mark::{px, fringe, within}; +import package::lib::mark::{px, px_per_length, fringe, within}; import package::lib::marker::{theme_port_size, theme_lift}; import package::lib::drawn::drawn_row; struct Point { clip: vec4f, world: vec3f, - // Where it stands, in position units. + // Its position, in position units. position: vec2f, // Half the marker's width and height, CSS pixels. half: vec2f, @@ -34,8 +34,8 @@ fn placed(v: Vertex) -> bool { return (v.flags & PLACED) != 0u; } -// The marker of row `row` of the current vertex mark. A hovered marker sized in pixels grows; one -// sized in units is part of the drawing, and only glows. +// The marker of the current vertex mark's held row `row`. A hovered marker sized in pixels grows. +// One sized in units is part of the drawing, so it only glows. fn point(row: u32) -> Point { let v = vertex_at(row_at(row)); var half = vec2f(px(v.half.x, VERTEX_HALF)); @@ -61,7 +61,7 @@ fn point(row: u32) -> Point { return p; } -// The marker of drawn row `item`, faded and grown as its tile comes and goes. +// The marker of drawn row `item`, faded and scaled while its tile fades in or out. fn shown_point(item: u32) -> Point { let d = drawn_row(item); var p = point(d.row); @@ -70,18 +70,19 @@ fn shown_point(item: u32) -> Point { return p; } -// Half the marker of the vertex in any slot, CSS pixels: its flags say whether its size is in -// position units and whether it has its own height. +// Half the size of a vertex's marker in CSS pixels, for a vertex of any mark. Its flags say whether +// its size is in position units and whether it has its own height. fn vertex_half(v: Vertex) -> vec2f { var half = vec2f(v.half.x, select(v.half.x, v.half.y, (v.flags & OWN_HEIGHT) != 0u)); if (v.flags & HALF_UNITS) != 0u { - half *= eye.px_per_unit.x; + half *= px_per_length(); } return within(half, v.range); } -// Where a port sits on a marker of half size `half`, CSS pixels from its center, y down: its side in -// the code's low two bits, then its place among the ports on that side and how many share it. +// A port's offset from the center of a marker of half size `half`, in CSS pixels with y down. The +// code holds the side in its low two bits, then the port's index on that side and the number of +// ports there. fn port_place(half: vec2f, code: u32) -> vec2f { let along = (f32((code >> 2u) & 255u) + 1.0) / (f32((code >> 10u) & 255u) + 1.0) * 2.0 - 1.0; switch code & 3u { @@ -92,7 +93,7 @@ fn port_place(half: vec2f, code: u32) -> vec2f { } } -// The way into a marker from a port on its side, y down. +// The unit direction from a port into its marker, y down. fn port_inward(code: u32) -> vec2f { switch code & 3u { case SIDE_LEFT: { return vec2f(1.0, 0.0); } @@ -104,12 +105,12 @@ fn port_inward(code: u32) -> vec2f { struct Port { center: vec2f, - // Half its size, CSS pixels: never past a quarter of its marker's shorter side, so a small + // Half its size in CSS pixels, at most a quarter of the marker's shorter side, so a small // marker still shows under its ports. half: f32, shape: u32, code: u32, - // Whether its reference names a net: a free port draws hollow. + // Whether its reference names a net. An unconnected port draws hollow. wired: bool, } @@ -124,9 +125,9 @@ fn port_at(p: Point, k: u32) -> Port { return Port(center, half, program[info + PORT_SHAPE], code, wired); } -// CSS pixels a point's quad reaches past its center: the marker, its status ring when it has one, -// its shadow on a diagram's sheet when it drops one, a target pixel of edge, and when `lit`, as the -// focus draw draws it, its glow. +// How far a point's quad extends from its center, in CSS pixels. It covers the marker, its status +// ring or its shadow on a diagram's sheet, one target pixel of antialiased edge, and when `lit` (in +// the focus draw), its glow. fn reach(p: Point, lit: bool) -> vec2f { let ring = select(0.0, 3.0 * frame.unit, (mark.flags & STATUS) != 0u && p.status.a > 0.0); let shadow = select(0.0, 2.0 * theme_lift(), (mark.flags & SHADOW) != 0u); diff --git a/crates/latkit/shaders/lib/project.wesl b/crates/latkit/shaders/lib/project.wesl index 694cad66..9275a1b4 100644 --- a/crates/latkit/shaders/lib/project.wesl +++ b/crates/latkit/shaders/lib/project.wesl @@ -1,6 +1,6 @@ -// From positions to the screen. Positions are (high, low) pairs: their difference from the camera's -// center is taken in parts, so f64 data keeps its precision at any zoom. Every pass projects through -// the view's eye. +// Projection from positions to the screen. Positions are (high, low) f32 pairs, and their offset +// from the camera's center is computed part by part, so f64 data keeps its precision at any zoom. +// Every pass projects through the view's `eye`. import package::lib::types::{SPACE_EARTH}; import package::lib::bind::{frame, eye}; @@ -8,11 +8,11 @@ import package::lib::bind::{frame, eye}; const PI: f32 = 3.14159265; const TAU: f32 = 6.28318531; const MAX_LATITUDE: f32 = 85.05113; -// Depth is reversed: the near plane is at 1 and the far plane at 0, which keeps precision far away. +// Depth is reversed: the near plane is at 1 and the far plane at 0, for precision far away. const NEAR_Z: f32 = 1.0; const FAR_Z: f32 = 0.0; -// The offset of `p` from the camera's center, in position units. +// The offset of position (`hi`, `lo`) from the camera's center, in position units. fn relative(hi: vec2f, lo: vec2f) -> vec2f { return (hi - frame.center_hi) + (lo - frame.center_lo); } @@ -22,7 +22,8 @@ fn mercator_y(latitude: f32) -> f32 { return log(tan(PI / 4.0 + lat / 2.0)); } -// Web mercator radians from the center; near the center, a local expansion keeps precision. +// Web Mercator coordinates relative to the center, in radians. Near the center, a local expansion +// keeps precision. fn mercator(hi: vec2f, lo: vec2f) -> vec2f { let d = relative(hi, lo); let lat = clamp(hi.y + lo.y, -MAX_LATITUDE, MAX_LATITUDE); @@ -31,7 +32,7 @@ fn mercator(hi: vec2f, lo: vec2f) -> vec2f { return vec2f(radians(d.x), select(far, near, abs(d.y) < 0.25)); } -// `p` on the unit sphere, turned so the camera's center faces +z. +// The position on the unit sphere, rotated so the camera's center faces +z. fn on_sphere(hi: vec2f, lo: vec2f) -> vec3f { let lon = radians(relative(hi, lo).x); let lat = radians(clamp(hi.y + lo.y, -90.0, 90.0)); @@ -40,8 +41,8 @@ fn on_sphere(hi: vec2f, lo: vec2f) -> vec3f { return vec3f(v.x, v.y * cos(lat0) - v.z * sin(lat0), v.y * sin(lat0) + v.z * cos(lat0)); } -// World coordinates: plane or plot units, or mercator radians morphing onto the globe. Only the -// morph works out both. +// World coordinates: plane or plot units relative to the center, or Mercator radians morphing onto +// the globe. Only a morph in progress computes both projections. fn world(hi: vec2f, lo: vec2f, z: f32) -> vec3f { if frame.space != SPACE_EARTH { return vec3f(relative(hi, lo), z); @@ -66,15 +67,15 @@ fn project(hi: vec2f, lo: vec2f, z: f32) -> vec4f { return clip_of(world(hi, lo, z)); } -// The globe's center in world units: a sphere of radius `-center.z` whose top touches the ground at -// the camera's center. +// The globe's center in world units. The globe is a sphere of radius `-center.z` whose top touches +// the ground at the camera's center. fn globe_center() -> vec3f { return vec3f(0.0, 0.0, -1.0 / cos(radians(frame.center_hi.y + frame.center_lo.y))); } -// Whether world point `w` shows past the globe: on the round globe, whether it lies in front of the -// plane the eye sees the globe's edge on, exactly, at any pitch. On the map, or while the globe -// morphs, everything shows, and what the ground hides is left to its depth. +// Whether world point `w` is on the visible side of the globe. On the round globe, it must lie in +// front of the plane of the globe's visible edge, a test that is exact at any pitch. On the map, or +// while the globe morphs, everything shows, and the ground's depth hides what lies behind it. fn shows(w: vec3f) -> bool { if frame.space != SPACE_EARTH || frame.globe < 1.0 { return true; @@ -83,7 +84,7 @@ fn shows(w: vec3f) -> bool { return dot(w - c, eye.position.xyz - w) > 0.0; } -// CSS pixels from the top left. +// A clip position in CSS pixels from the top left. fn to_screen(clip: vec4f) -> vec2f { let ndc = clip.xy / clip.w; return (vec2f(ndc.x, -ndc.y) * 0.5 + 0.5) * eye.size; @@ -95,7 +96,7 @@ fn to_clip(screen: vec2f, at: vec4f) -> vec4f { return vec4f(vec2f(ndc.x, -ndc.y) * at.w, at.z, at.w); } -// `clip` a little toward the camera, so a mark lying on a face draws over it. +// `clip` moved slightly toward the camera, so a mark lying on a face draws over it. fn nearer(clip: vec4f) -> vec4f { return vec4f(clip.xy, min(clip.z * 1.003, clip.w), clip.w); } diff --git a/crates/latkit/shaders/lib/read.wesl b/crates/latkit/shaders/lib/read.wesl index 0442fce6..a910aa29 100644 --- a/crates/latkit/shaders/lib/read.wesl +++ b/crates/latkit/shaders/lib/read.wesl @@ -1,7 +1,7 @@ -// Reading columns: page tables in `program` point at blocks of `pages`. A column's pages are its -// data's, or for a mark drawing a table in part the pages of it the mark holds, a slot each; every -// page holds `PAGE_ROWS` rows. Values stay `u32` bits until arithmetic needs them, so -// a missing value is never an `f32` constant (WGSL cannot represent NaN constants). +// Reading columns. Page tables in `program` point at blocks of `pages`, each page holding +// `PAGE_ROWS` rows. A column's pages are its data's pages, or for a paged table the mark's resident +// pages, one per slot. Values stay `u32` bits until arithmetic needs them, because WGSL cannot +// express a missing value as an `f32` constant (NaN). import package::lib::types::{ NONE, MISSING, COLUMN_WORDS, COLUMN_FLAGS, COLUMN_ROWS, COLUMN_TABLE, COLUMN_LOW, COLUMN_JOIN, COLUMN_T, @@ -9,12 +9,13 @@ import package::lib::types::{ }; import package::lib::bind::{program, mark, word}; -// Whether a pipeline's marks read fields of other tables by row id, and fields sampled over an axis -// at the shown coordinate: set when it is made, so a scene doing neither compiles neither. +// Whether a pipeline's marks read fields of other tables through a join, and fields sampled over +// the axis. Both are set when the pipeline is created, so a scene that uses neither compiles +// neither. override JOINED: bool = true; override TIMED: bool = true; -// Whether `x` is NaN or infinite: missing. +// Whether `x` is missing: NaN or infinite. fn missing(x: f32) -> bool { return missing_bits(bitcast(x)); } @@ -39,7 +40,7 @@ fn page_of(row: u32) -> vec2u { return vec2u(row >> PAGE_BITS, row & PAGE_MASK); } -// A static column's value as (high, low) bits: the low part carries what f32 drops of an f64. +// A static column's value as (high, low) bits. The low part holds what f32 loses of an f64. fn static_value(column: u32, row: u32) -> vec2u { let at = page_of(row); let page = at.x; @@ -53,8 +54,9 @@ fn static_value(column: u32, row: u32) -> vec2u { return vec2u(hi, page_word(program[low + page], local, 0u)); } -// A sampled column's value at the shown coordinate, as bits. Each page of rows has a cursor of six -// words: block and word of this frame, block and word of the next frame, and both frames' row counts. +// A sampled column's value at the shown coordinate, as bits. Each page of rows has a six-word +// cursor: the block and word of the frame at the shown coordinate, the same for the next frame, and +// both frames' row counts. fn sample_value(column: u32, row: u32) -> u32 { let at = page_of(row); let cursor = column_word(column, COLUMN_TABLE) + at.x * 6u; @@ -75,7 +77,7 @@ fn sample_value(column: u32, row: u32) -> u32 { return bitcast(mix(bitcast(a), bitcast(b), t)); } -// A bits column's word: row indices of a reference, NONE where unwired. +// A bits column's word, such as a reference's row index; NONE where unwired. fn bits_value(column: u32, row: u32) -> u32 { if row >= column_word(column, COLUMN_ROWS) { return NONE; @@ -106,17 +108,17 @@ fn column_value(column: u32, row: u32) -> vec2u { return static_value(column, r); } -// One row's points in a geometry column: where the piece its points begin in is listed in `program`, -// its first point in that piece, and how many. Parts are broken by a missing point. +// One row's points in a geometry column: where its first piece is listed in `program`, its first +// point in that piece, and its point count. A missing point separates parts. struct Points { blocks: u32, first: u32, count: u32, } -// Row `row`'s points of geometry column `column`, as its page's rows' table says. A piece holds up to -// `1 << POINT_BITS` points as x and y bits; a row larger than a piece begins one and fills those after -// it. +// Row `row`'s points in geometry column `column`, from its page's row table. A piece holds up to +// `1 << POINT_BITS` points as x and y bits. A row larger than a piece continues into the pieces +// after its first. fn points_of(column: u32, row: u32) -> Points { if column == NONE || row >= column_word(column, COLUMN_ROWS) { return Points(0u, 0u, 0u); diff --git a/crates/latkit/shaders/lib/reduce.wesl b/crates/latkit/shaders/lib/reduce.wesl index a7d2d2c5..af5afa17 100644 --- a/crates/latkit/shaders/lib/reduce.wesl +++ b/crates/latkit/shaders/lib/reduce.wesl @@ -1,5 +1,6 @@ -// Reductions with atomics: an `f32` as `u32` bits that sort in the same order, so `atomicMax` of -// `ordered(x)` keeps the greatest value and `atomicMax` of `~ordered(x)` the least. +// Atomic reductions over `f32`. `ordered` maps an `f32` to `u32` bits that sort in the same +// order, so `atomicMax` of `ordered(x)` keeps the greatest value, and `atomicMax` of `~ordered(x)` +// keeps the least. fn ordered(x: f32) -> u32 { let b = bitcast(x); diff --git a/crates/latkit/shaders/lib/state.wesl b/crates/latkit/shaders/lib/state.wesl index 952172d1..bff2c8a5 100644 --- a/crates/latkit/shaders/lib/state.wesl +++ b/crates/latkit/shaders/lib/state.wesl @@ -1,7 +1,7 @@ -// Evaluated rows: each mark's words per row, in pages of `PAGE_ROWS` held rows. A row's words are its -// kind words, `mark.stride` of them, then its extra words (labels, marker inputs), `mark.extras`; the -// page table at `mark.state` says where each page of them lies. A mark reads the vertices its ends name -// through the page tables of the vertex marks drawing them, which its ends list. +// Reading evaluated rows. Each mark keeps its per-row words in pages of `PAGE_ROWS` held rows, +// listed by the page table at `mark.state`. A row has `mark.stride` kind words, then `mark.extras` +// extra words (labels, marker inputs). An edge or face mark reads its vertices through the page +// tables of the vertex marks that draw them, which its ends list. import package::lib::types::{ NONE, PAGE_BITS, PAGE_MASK, END_WORDS, END_STATE, END_STRIDE, END_NORMALS, VERTEX_POSITION, VERTEX_COLOR, @@ -20,7 +20,7 @@ struct Vertex { half: vec2f, elevation: f32, depth: f32, - // The CSS pixels its marker's half keeps within. + // The range of CSS pixels its marker's half size is clamped to. range: vec2f, flags: u32, } @@ -54,8 +54,8 @@ struct Area { flags: u32, } -// A range of CSS pixels in one word: each end in sixteenths of a pixel, up to 4095, the top value -// meaning no end. +// Packs a range of CSS pixels into one word: each end in 16 bits, in 1/16 pixel, up to 4095.9375 +// pixels. An upper end of 65535 means no upper bound. fn pack_range(range: vec2f) -> u32 { let ends = vec2u(round(clamp(range, vec2f(0.0), vec2f(4095.9375)) * 16.0)); let top = select(ends.y, 65535u, range.y > 4095.9375); @@ -64,7 +64,7 @@ fn pack_range(range: vec2f) -> u32 { fn unpack_range(word: u32) -> vec2f { let ends = vec2f(f32(word & 65535u), f32(word >> 16u)) / 16.0; - // The top of what a word holds is no end at all. + // The top value means no upper bound. return vec2f(ends.x, select(ends.y, 3.4e38, (word >> 16u) == 65535u)); } @@ -72,17 +72,17 @@ fn float_at(at: u32) -> f32 { return bitcast(word(at)); } -// Where the words of `row` start among the pages page table `table` lists, `stride` words a row. +// Where `row`'s words start in the pages, given page table `table` and `stride` words per row. fn record_at(table: u32, stride: u32, row: u32) -> u32 { return program[table + (row >> PAGE_BITS)] + (row & PAGE_MASK) * stride; } -// The first kind word of `row` of the current mark. +// Where the first kind word of the current mark's `row` lies in the pages. fn row_at(row: u32) -> u32 { return record_at(mark.state, mark.stride + mark.extras, row); } -// Extra word `w` of `row` of the current mark. +// Where extra word `w` of the current mark's `row` lies in the pages. fn extra_at(row: u32, w: u32) -> u32 { return row_at(row) + mark.stride + w; } @@ -102,19 +102,19 @@ fn vertex_at(at: u32) -> Vertex { ); } -// The vertex held row `held` of the vertex mark drawing the table end `end` of the current mark names. +// Vertex `held` of end `end`: a held row of the vertex mark that draws the end's table. fn end_vertex(end: u32, held: u32) -> Vertex { let e = mark.ends + END_WORDS * end; return vertex_at(record_at(program[e + END_STATE], program[e + END_STRIDE], held)); } -// Whether ends `a` and `b` of an edge, held rows of the vertex marks its two ends attach to, are one -// vertex. +// Whether an edge's ends `a` and `b`, held rows of the vertex marks its two ends attach to, are the +// same vertex. fn same_vertex(a: u32, b: u32) -> bool { return a == b && program[mark.ends + END_STATE] == program[mark.ends + END_WORDS + END_STATE]; } -// Where the normal faces sum at the vertex held row `held` names through end `end` lies, or NONE. +// Where the face normal sum for vertex `held` of end `end` lies, or NONE. fn end_normal(end: u32, held: u32) -> u32 { let table = program[mark.ends + END_WORDS * end + END_NORMALS]; if table == NONE { @@ -150,8 +150,8 @@ fn line_at(at: u32) -> Line { ); } -// The current mark's words for how far a stroke row's comets traveled and for its flow; NONE for a -// mark that draws no strokes. +// The current mark's word indices for a stroke row's comet travel and its flow; NONE for a mark +// that draws no strokes. fn travel_words() -> vec2u { switch mark.kind { case MARK_EDGES: { return vec2u(EDGE_TRAVEL, EDGE_FLOW); } diff --git a/crates/latkit/shaders/lib/stroke.wesl b/crates/latkit/shaders/lib/stroke.wesl index da860b73..56bc726c 100644 --- a/crates/latkit/shaders/lib/stroke.wesl +++ b/crates/latkit/shaders/lib/stroke.wesl @@ -7,15 +7,15 @@ import package::lib::types::{FLOW_PX, FLOW_UNITS}; struct Stroke { a: vec2f, b: vec2f, - // Distance along the whole line at `a` and at `b`, CSS pixels: dashes and comets run on across - // segments. + // Distance along the whole line at `a` and at `b`, in CSS pixels, so dashes and comets continue + // across segments. phase: vec2f, - // The same in length units, as far along as the line's own points: comets drawn in position - // units run along it. + // The same distances in position units, measured along the line's own points. Comets spaced in + // position units use them. along: vec2f, width: f32, color: vec4f, - // Comet speed, negative backward, 0 for none; and how far the row's comets traveled. + // Comet speed (negative runs backward, 0 for none), and how far the row's comets have traveled. flow: f32, travel: f32, dash: bool, @@ -23,7 +23,7 @@ struct Stroke { arrow: bool, } -// The arrowhead at `b` pointing along `dir`: a triangle `frame.arrow` long, as a signed distance. +// Signed distance to the arrowhead at `b`, a triangle `frame.arrow + width` long along `dir`. fn head_distance(p: vec2f, b: vec2f, dir: vec2f, width: f32) -> f32 { let length = frame.arrow + width; let q = p - b; @@ -36,8 +36,8 @@ fn head_distance(p: vec2f, b: vec2f, dir: vec2f, width: f32) -> f32 { return max(max(flank, along), -along - length); } -// (distance outside the stroke's edge, distance along the line, fraction of the way from `a` to `b`) -// for pixel `p`. +// For pixel `p`: (distance outside the stroke's edge, distance along the line, fraction of the way +// from `a` to `b`). fn stroke_distance(p: vec2f, s: Stroke) -> vec3f { let ab = s.b - s.a; let len = length(ab); @@ -54,7 +54,7 @@ fn stroke_distance(p: vec2f, s: Stroke) -> vec3f { return vec3f(d, mix(s.phase.x, s.phase.y, along), along); } -// How much of the pixel at `d`, from `stroke_distance`, the line of stroke `s` covers, dashed. +// Coverage of the pixel at `d` (from `stroke_distance`) by stroke `s`'s line, with its dashes. fn covered(d: vec3f, s: Stroke) -> f32 { var alpha = coverage(d.x); if s.dash { @@ -63,16 +63,15 @@ fn covered(d: vec3f, s: Stroke) -> f32 { return alpha; } -// How much of the pixel at `d` a comet covers: comets `frame.flow_spacing` apart, moved on by stroke -// `s`'s travel, each a round head `frame.flow_width` wide, never narrower than the stroke, brightest -// at its middle, trailing a tail `frame.flow_length` long that thins and fades. +// Coverage of the pixel at `d` by comets. Comets are `frame.flow_spacing` apart, offset by stroke +// `s`'s travel. Each has a round head `frame.flow_width` wide (at least the stroke's width), +// brightest at its middle, and a tail `frame.flow_length` long that thins and fades. fn comet(d: vec3f, s: Stroke) -> f32 { if s.flow == 0.0 || frame.flow.a <= 0.0 { return 0.0; } - // Where the pixel lies along the line in the comets' lengths, and CSS pixels per length: position - // units where comets are part of the drawing, CSS pixels where they keep to the screen, else the - // space's lengths. + // The pixel's distance along the line in the comet unit, and CSS pixels per comet unit: + // position units (`FLOW_UNITS`), CSS pixels (`FLOW_PX`), or else the space's length unit. var along = d.y / frame.unit; var px = frame.unit; if frame.flow_unit == FLOW_UNITS { @@ -83,7 +82,8 @@ fn comet(d: vec3f, s: Stroke) -> f32 { px = 1.0; } let spacing = max(frame.flow_spacing, 1e-6); - // How far the pixel leads the head behind it, the way comets run, and trails the head ahead. + // How far the pixel is ahead of the comet head behind it, in the direction comets run, and how + // far it is behind the head in front of it. let lead = fract(sign(s.flow) * (along - s.travel) / spacing) * spacing; let trail = spacing - lead; let across = d.x + 0.5 * s.width; @@ -94,7 +94,7 @@ fn comet(d: vec3f, s: Stroke) -> f32 { return fade * fade * (1.0 - smoothstep(0.35 * reach, reach, distance)); } -// Stroke `s` at pixel `p`, comets over its line; a comet fades with its row. +// Stroke `s`'s color at pixel `p`, with comets over its line. Comets fade with the row's opacity. fn stroke(p: vec2f, s: Stroke) -> vec4f { let d = stroke_distance(p, s); let line = s.color.a * covered(d, s); diff --git a/crates/latkit/shaders/lib/trace.wesl b/crates/latkit/shaders/lib/trace.wesl index 9903cde4..253374de 100644 --- a/crates/latkit/shaders/lib/trace.wesl +++ b/crates/latkit/shaders/lib/trace.wesl @@ -1,12 +1,19 @@ -// Traces: what a line draws of a sampled field across the axis: runs of entries left to right, each -// of one level, frames or buckets. The traced column's table holds a header (how many runs, pages of -// rows, and levels); per run its level's place among the levels, its first entry, how many, and the -// point it starts at; per level the level, how many axis pages it has, where they are listed, and -// where its pages' lists start; per level and page of rows how many pages of frames or least values -// it has and where they are listed, then of greatest values; then those lists: axis pages as (offset, -// first entry), pages of values as (offset, first entry, words per entry). Offsets within the table -// are relative to it. A line runs through each frame, and through each bucket's least and greatest -// value where it draws; between runs that do not meet, a point is left out, breaking the line. +// Traces: a line drawn through a sampled field across the axis, as runs of entries from left to +// right. Each run reads one level: frames, or buckets of frames. The line passes through each +// frame, and through each bucket's least and greatest values. Between runs that do not meet, a +// point is left out, which breaks the line. +// +// The traced column's table, with positions in it relative to its start: +// - Header (`HEADER` words): the run count, the row page count, the level count, and the total +// point count. +// - Per run (`TRACE_RUN_WORDS`): its level's index, its first entry, its entry count, and its first +// point. +// - Per level (`LEVEL_WORDS`): the level, its axis page count, where its axis pages are listed, and +// where its row pages' lists start. +// - Per level and row page, four words: the count and list position of its pages of frames (or of +// least values), then the same for its pages of greatest values. +// - The lists: axis pages as (offset in `pages`, first entry), and value pages as (offset in +// `pages`, first entry, words per entry). import package::lib::types::{NONE, MISSING, COLUMN_TABLE}; import package::lib::bind::{program, mark, word}; @@ -20,7 +27,8 @@ fn trace_table() -> u32 { return column_word(mark.trace, COLUMN_TABLE); } -// The last of `count` items of `stride` words from `at` whose word `key` is at most `value`. +// Binary search: where the last of `count` items of `stride` words at `at` whose word `key` is at +// most `value` starts. fn last_at_most(at: u32, count: u32, stride: u32, key: u32, value: u32) -> u32 { var lo = 0u; var hi = count; @@ -35,8 +43,7 @@ fn last_at_most(at: u32, count: u32, stride: u32, key: u32, value: u32) -> u32 { return at + lo * stride; } -// A run of the trace: its level's place among the levels, its first entry, how many, and the point -// it starts at. +// A run of the trace: its level's index, its first entry, its entry count, and its first point. struct TraceRun { slot: u32, first: u32, @@ -57,7 +64,7 @@ fn trace_level_at(slot: u32) -> u32 { return t + HEADER + TRACE_RUN_WORDS * program[t] + LEVEL_WORDS * slot; } -// The level a run of `slot` draws. +// The level at level index `slot`. fn trace_level(slot: u32) -> u32 { return program[trace_level_at(slot)]; } diff --git a/crates/latkit/shaders/lib/triangles.wesl b/crates/latkit/shaders/lib/triangles.wesl index 36ace24b..d4eb2d96 100644 --- a/crates/latkit/shaders/lib/triangles.wesl +++ b/crates/latkit/shaders/lib/triangles.wesl @@ -1,7 +1,8 @@ -// Triangles: a face row's corners, at the vertices its references name; or an area's triangles, three -// points at a time from its geometry column after the point its label hangs from, numbered by the -// mark's list. Draws, probes, bounds, and labels read a triangle through `triangle_corner`; which kind -// a pipeline reads is its `PATH`: ends for faces, geometry for areas. +// Triangles: a face row's corners at the vertices its references name, or an area's triangles. An +// area's geometry column holds its label anchor, then three points per triangle, and the mark's +// list numbers the triangles. Draws, probes, bounds, and labels read triangles through +// `triangle_corner`. A pipeline's `PATH` selects the kind: `PATH_ENDS` for faces, `PATH_GEOMETRY` +// for areas. import package::lib::types::{NONE, VISIBLE, PLACED, ENDS_COLOR, PATH_GEOMETRY}; import package::lib::bind::mark; @@ -14,18 +15,18 @@ import package::lib::drawn::drawn_row; struct Corner { world: vec3f, - // Where it stands, in position units. + // Its position, in position units. position: vec3f, - // A face corner's vertex, a held row of the vertex mark its end attaches to; `NONE` for an area's. + // A face corner's vertex: a held row of the vertex mark its end attaches to. NONE for an area. vertex: u32, color: vec4f, row: u32, - // Whether it has a place to stand, and whether it lies in front of the globe's horizon. + // Whether it has a position, and whether it lies in front of the globe's horizon. shown: bool, ahead: bool, } -// Triangles every live row draws: a face per row, or each of every area's, as its list numbers them. +// Triangles across all drawn rows: one per face, or every area triangle the list numbers. fn triangle_instances() -> u32 { if PATH == PATH_GEOMETRY { return stroke_instances(); @@ -33,7 +34,7 @@ fn triangle_instances() -> u32 { return mark.rows; } -// Corner `corner` of triangle `instance`, faded as its tile comes and goes. +// Corner `corner` of triangle `instance`, faded while its tile fades in or out. fn triangle_corner(instance: u32, corner: u32) -> Corner { if PATH == PATH_GEOMETRY { let s = segment(instance); @@ -47,8 +48,8 @@ fn triangle_corner(instance: u32, corner: u32) -> Corner { return c; } -// Whether triangle `instance` draws: every corner placed, and one in front of the globe's horizon; -// the globe's depth cuts a triangle reaching past its edge. +// Whether triangle `instance` draws: every corner has a position, and at least one lies in front of +// the globe's horizon. The globe's depth clips a triangle reaching past its edge. fn triangle_shown(instance: u32) -> bool { var ahead = false; for (var k = 0u; k < 3u; k++) { @@ -88,8 +89,8 @@ fn face_corner(row: u32, corner: u32) -> Corner { return c; } -// Point `offset` of the points of triangle `t.index` of area row `t.row`: 0 is where its label hangs, -// 1 to 3 its corners. +// Point `offset` of triangle `t.index` of area row `t.row`. Offset 0 is the area's label anchor; +// 1 to 3 are the triangle's corners. fn area_point(t: Segment, offset: u32) -> Corner { var c: Corner; c.row = t.row; @@ -116,7 +117,7 @@ fn area_point(t: Segment, offset: u32) -> Corner { return c; } -// Where area row `row`'s label hangs: inside its largest polygon, farthest from its edges. +// Area row `row`'s label anchor: inside its largest polygon, at the point farthest from its edges. fn area_anchor(row: u32) -> Corner { return area_point(Segment(row, NONE, 0u, 0.0, 1.0, 1.0, false), 0u); } diff --git a/crates/latkit/shaders/lib/types.wesl b/crates/latkit/shaders/lib/types.wesl index c073d69a..735649f0 100644 --- a/crates/latkit/shaders/lib/types.wesl +++ b/crates/latkit/shaders/lib/types.wesl @@ -1,13 +1,14 @@ -// The GPU contract shared with Rust: build.rs mirrors every struct and constant here. +// Constants and structs shared with Rust. The build script mirrors each one in Rust. -// No index, slot, or page. +// Sentinel for no index, slot, or page. const NONE: u32 = 0xffffffffu; -// A missing number: NaN bits. Any NaN or infinity reads as missing. +// A missing number, as NaN bits. Any NaN or infinity reads as missing. const MISSING: u32 = 0x7fc00000u; -// A missing color: transparent magenta, replaced by the theme's color on store. +// A missing color (transparent magenta), as an option given `missing()` holds. A color store +// replaces it with the option's theme color. const MISSING_COLOR: u32 = 0x00ff00ffu; -// Opcodes of the expression machine; an op is four words: code, a, b, c. +// Expression machine opcodes. Each instruction is four words: opcode, a, b, c. const OP_CONST: u32 = 0u; const OP_MISSING: u32 = 1u; const OP_FIELD: u32 = 2u; @@ -53,75 +54,87 @@ const OP_LOG: u32 = 41u; const OP_EXP: u32 = 42u; const OP_ZOOM: u32 = 43u; -// Values the expression stack holds at once. +// Depth of the expression machine's value stack. const STACK: u32 = 16u; -// Column descriptor words, six per column in `meta`. +// Column descriptors, `COLUMN_WORDS` words per column, start at `mark.columns` in the program. const COLUMN_WORDS: u32 = 6u; const COLUMN_FLAGS: u32 = 0u; const COLUMN_ROWS: u32 = 1u; +// Where the column's page table starts in the program. A sampled column has cursors here instead. const COLUMN_TABLE: u32 = 2u; +// Where the page table of an f64 column's low words starts; NONE for an f32 column. const COLUMN_LOW: u32 = 3u; +// The bits column mapping each row to a row of another table; NONE when it reads its own table. const COLUMN_JOIN: u32 = 4u; +// A sampled column's f32 interpolation fraction from the frame at the shown coordinate to the next. const COLUMN_T: u32 = 5u; -// Column flags. +// Column flags. SAMPLED: values are samples over the axis. STEP: a sampled column holds each +// frame's value instead of interpolating. BITS: values are raw u32 words, such as row indices. const SAMPLED: u32 = 1u; const STEP: u32 = 2u; const BITS: u32 = 4u; -// Rows per page, as a shift: a page of the data, of a table drawn in part as its marks hold it, a -// slot at a time, and of a mark's rows, which it evaluates a page at a time. +// Rows per page, also as a shift and a mask. Data columns, a paged table's slots, and each mark's +// rows are stored in pages, and marks evaluate their rows page by page. const PAGE_BITS: u32 = 12u; const PAGE_MASK: u32 = 4095u; const PAGE_ROWS: u32 = 4096u; -// Items a dispatch over more items than one dimension holds steps through at a time in its second -// dimension, 256 workgroups of 256 wide. +// Items per step of a dispatch's second dimension: 256 workgroups of 256. Dispatches too large for +// one dimension use it (see `item`). const DISPATCH_ROWS: u32 = 65536u; -// What marks a row block evaluation lists whose state words start over: it writes each of them -// before any channel does. +// Flag on a listed row block whose state words start over. Evaluation zeroes those rows' words +// before any channel runs. const FRESH: u32 = 0x80000000u; -// Words of a run of drawn rows in a mark's list of them: where it starts among the drawn rows, the -// held row it starts at, and whether its tile fades in or out (FADE_IN, FADE_OUT). +// Words per run in a mark's run list at `mark.row_runs`, after the run count: the run's first drawn +// row, the held row it starts at, and its tile's fade flags (FADE_IN, FADE_OUT). const RUN_WORDS: u32 = 3u; -// A drawn row's word, as the expansion of the runs writes it: its held row, and whether its tile -// fades in or out. +// A drawn row's word, written by `expand_runs`: its held row in the low 30 bits, and flags for a +// tile fading in or out. const DRAWN_ROW: u32 = 0x3fffffffu; const DRAWN_ENTERING: u32 = 0x40000000u; const DRAWN_LEAVING: u32 = 0x80000000u; -// The item invocation `id` of a dispatch over items covers: runs of DISPATCH_ROWS along the second -// dimension, when the first holds too few. +// The item index of invocation `id`. A dispatch too large for one dimension adds `DISPATCH_ROWS` +// items per step along the second. fn item(id: vec3u) -> u32 { return id.y * DISPATCH_ROWS + id.x; } -// How a word of a row eases while its mark does: kept as it is (no channel stores it), shown as -// evaluated at once, a number or a color mixed, or a position's high word moved by its offset. +// How each word of a row eases during a transition. KEEP: no channel stores it, so it is left +// unchanged. SNAP: shows the evaluated value at once. NUMBER, COLOR: mixed from the old value. +// POSITION: a position's high word, moved by its offset from the old position. const BLEND_KEEP: u32 = 0u; const BLEND_SNAP: u32 = 1u; const BLEND_NUMBER: u32 = 2u; const BLEND_COLOR: u32 = 3u; const BLEND_POSITION: u32 = 4u; -// Points a piece of a geometry's points holds at most, as a shift, as x and y words: a piece holds a -// group of a page's rows, so a row written again changes only its group's, and a row larger than a -// piece fills as many as it takes. +// Points per piece of a geometry column, as a shift and a mask. Each point is an x and a y word. A +// piece holds the points of a group of a page's rows, so rewriting a row rewrites only its group's +// piece. A row with more points than a piece spans consecutive pieces. const POINT_BITS: u32 = 12u; const POINT_MASK: u32 = 4095u; -// Words of a geometry column's table per page: the block of its rows' table, two words a row (the -// piece its points begin in, among the page's, shifted up by POINT_BITS over its first point there; -// and how many points it has), where the page's pieces are listed in the column's table, how many -// points the page has, and how many rows. +// Words per page in a geometry column's table: +// 0: the block of the page's row table, two words per row. The first word is +// `piece << POINT_BITS | first`, where `piece` indexes the page's pieces and `first` is the +// row's first point in that piece. The second word is the row's point count. +// 1: where the page's piece list starts, relative to the column's table. +// 2: the page's point count. +// 3: the page's row count. const PATH_PAGE_WORDS: u32 = 4u; -// Words of each end of a mark in its program: where the vertex mark drawing the end's table keeps -// its rows (the page table of its rows' words and how many words a row has), where it holds the rows -// of the order it draws that table in (each page's slot, then each row it keeps whose page it does -// not hold; NONE where it draws the table whole), the page table of the normals faces sum at its rows -// (NONE where none do), and the program column of the reference field naming the end's rows (NONE -// where the end is the row's own vertex, as a pole's). The end pass writes where end `k` of a row is -// held into the row's kind word `k`. +// Words per end of an edge or face mark, at `mark.ends` in the program. An end's vertices are rows +// of the vertex mark that draws the end's table. +// STATE, STRIDE: that vertex mark's state page table (NONE when no vertex mark draws the table) and +// its words per row. +// SLOTS: the end table, which maps a place to a held row (see `held_of`). NONE unless the table is +// paged. +// NORMALS: the page table of the face normals summed at its rows; NONE when no face sums them. +// COLUMN: the program column of the reference field naming the end's rows; NONE when the end is the +// row's own vertex, as for a pole. +// The end pass writes the held row of end `k` into the row's kind word `k`. const END_WORDS: u32 = 5u; const END_STATE: u32 = 0u; const END_STRIDE: u32 = 1u; @@ -129,26 +142,27 @@ const END_SLOTS: u32 = 2u; const END_NORMALS: u32 = 3u; const END_COLUMN: u32 = 4u; -// Words of the normal faces sum at a vertex row: x, y, and z in fixed point. +// Words of the face normal sum at a vertex row: x, y, and z in fixed point. const NORMAL_WORDS: u32 = 3u; -// Vertex state words. +// Vertex state words. POSITION is four words: the high parts of x and y, then their low parts. HALF +// is two words: the marker's half width and half height. const VERTEX_WORDS: u32 = 12u; const VERTEX_POSITION: u32 = 0u; const VERTEX_COLOR: u32 = 4u; const VERTEX_STATUS: u32 = 5u; const VERTEX_HALF: u32 = 6u; const VERTEX_ELEVATION: u32 = 8u; -// How many groups hold a group row: containers tint by it. +// How many groups contain a group row. Containers tint by it. const VERTEX_DEPTH: u32 = 9u; -// The CSS pixels its marker's half keeps within at any zoom, as two halves: the mark's, so every -// mark reading the vertex trims at the outline it draws. +// The range of CSS pixels the marker's half size is clamped to, packed by `pack_range`. It is the +// vertex mark's range, so edges ending at the vertex stop at the outline it draws. const VERTEX_RANGE: u32 = 10u; const VERTEX_FLAGS: u32 = 11u; -// Edge state words. Every kind's flags are its last word. Its ends are where the vertices it joins -// are held, which the end pass finds; a stroke's travel is how far its comets moved; no channel -// writes either. +// Edge state words. Every kind's flags word is its last word. FROM and TO are the held rows of the +// edge's two vertices, written by the end pass. TRAVEL is how far its comets have moved. No channel +// writes FROM, TO, or TRAVEL. const EDGE_WORDS: u32 = 7u; const EDGE_FROM: u32 = 0u; const EDGE_TO: u32 = 1u; @@ -158,9 +172,10 @@ const EDGE_FLOW: u32 = 4u; const EDGE_TRAVEL: u32 = 5u; const EDGE_FLAGS: u32 = 6u; -// A vertex mark's ports, beside its program: each port's code (its side in two bits, its place -// among the ports on that side and how many share it in eight each, and whether it is an output), -// its shape, the program column of its reference field, and its label's rectangle in the atlas. +// Words per port of a vertex mark, at `mark.port_info` in the program: its code, its shape, the +// program column of its reference field, and its label's atlas rectangle as two corner words. A +// code holds the side in bits 0-1, the port's index on that side in bits 2-9, the number of ports +// on that side in bits 10-17, and the `PORT_OUTPUT` bit. const PORT_WORDS: u32 = 5u; const PORT_CODE: u32 = 0u; const PORT_SHAPE: u32 = 1u; @@ -171,18 +186,19 @@ const SIDE_LEFT: u32 = 0u; const SIDE_RIGHT: u32 = 1u; const SIDE_TOP: u32 = 2u; const SIDE_BOTTOM: u32 = 3u; -// A group's header, in label sizes: its title sits in it. +// Height of a group's header band, in label sizes. The group's title sits in it. const HEADER_BAND: f32 = 1.8; -// Angle a geodesic segment spans at most, in degrees. +// Largest arc a geodesic segment spans, in degrees. const GEODESIC_STEP: f32 = 2.0; -// Pieces a path between two ends takes at most: along a great circle, or bowed into a lane. +// Most segments a path between two ends uses, along a great circle or bowed into a lane. const MAX_PIECES: u32 = 64u; -// Pieces a path bowed into a lane takes at least. +// Fewest segments a path bowed into a lane uses. const BOW_PIECES: u32 = 16u; -// Lanes a family of edges joining the same two vertices spreads as wide as at most: more pack closer. +// Widest spread, in lanes, of a family of edges joining the same two vertices. Larger families pack +// their lanes closer. const FAMILY_LANES: f32 = 8.0; -// Face state words: its corners, where the end pass finds their vertices held, first. +// Face state words. A, B, and C are the held rows of the corner vertices, written by the end pass. const FACE_WORDS: u32 = 5u; const FACE_A: u32 = 0u; const FACE_B: u32 = 1u; @@ -203,35 +219,35 @@ const AREA_WORDS: u32 = 2u; const AREA_COLOR: u32 = 0u; const AREA_FLAGS: u32 = 1u; -// Label words, among a labeled mark's extra words: the text's code, its priority as a level from 0 -// to 255, and its color. +// Label words among a labeled mark's extra words: text code, priority (0 to 255), and color. const LABEL_WORDS: u32 = 3u; const LABEL_CODE: u32 = 0u; const LABEL_PRIORITY: u32 = 1u; const LABEL_COLOR: u32 = 2u; -// A label's words in the labels buffer, per labeled row: the box placement measured it in, CSS -// pixels; its key; its state; and how present it is. Draws place a label from its anchor every -// frame; placement only decides whether it shows. +// Words of a labeled row's placement candidate, in pages listed by the page table at +// `mark.candidates`: the label's box in CSS pixels (four words), key, state, and opacity. Draws +// place each label from its anchor every frame; placement only decides whether it shows. const CANDIDATE_WORDS: u32 = 7u; const CANDIDATE_BOX: u32 = 0u; const CANDIDATE_KEY: u32 = 4u; const CANDIDATE_STATE: u32 = 5u; const CANDIDATE_ALPHA: u32 = 6u; -// Text is set once per run at this size, in atlas texels, as a distance field reaching this far -// past the text's edge. +// Each text run is set once, at a size of `TEXT_BASE` atlas texels, as a distance field reaching +// `TEXT_SPREAD` texels past the text's edge. const TEXT_BASE: f32 = 32.0; const TEXT_SPREAD: f32 = 8.0; -// Candidate states: in view and wanting room, placed, dropped, placed in the frame before, and -// placed while its text is not set. +// Candidate state bits. WANTS: in view and needs room. PLACED_LABEL: placed. DROPPED: a placed +// label occupies some of its cells. STICKY: placed in the previous frame. UNSET: its text is not +// set yet. const WANTS: u32 = 1u; const PLACED_LABEL: u32 = 2u; const DROPPED: u32 = 4u; const STICKY: u32 = 8u; const UNSET: u32 = 16u; -// A text run whose distance field the GPU makes, as words at the start of the fields buffer: where its -// coverage starts, its coverage's width and height, where its columns' least distances start, and -// where its field starts and the words between the starts of its rows. +// Words per text run at the start of the fields buffer, for runs whose distance field the GPU +// computes: where its coverage starts, the coverage's width and height, where its column minima +// start, where its field starts, and the field's row pitch in words. const FIELD_COVERAGE: u32 = 0u; const FIELD_WIDTH: u32 = 1u; const FIELD_HEIGHT: u32 = 2u; @@ -240,28 +256,29 @@ const FIELD_AT: u32 = 4u; const FIELD_ROW: u32 = 5u; const FIELD_WORDS: u32 = 6u; -// A run table entry whose text is not set yet holds this flag as its first corner and the run's size, -// as measured, as its second, so its label is placed where it will show before it is set. +// Flag on a run table entry's first corner word while its text is not set. The second word then +// holds the run's measured size, so its label can be placed before the text is set. const TEXT_UNSET: u32 = 0x80000000u; -// A cell a placed label holds. +// Value of a label grid cell occupied by a placed label. Other cells hold NONE or the least key +// claiming them. const OCCUPIED: u32 = 0u; -// CSS pixels a cell of the label grid covers. +// Side of a label grid cell, in CSS pixels. const CELL: f32 = 8.0; -// Codes a mark asks the host to set at most per frame. +// Most text codes a mark asks the host to set per frame. const WANTED: u32 = 64u; -// State flags. +// Row state flags, in the last word of each row's kind words. const VISIBLE: u32 = 1u; -// The row has a position: from its data, or in a diagram from its sheet. +// The row has a position, from its data or, in a diagram, from its sheet. const PLACED: u32 = 2u; const DASH: u32 = 4u; -// While a mark eases, a row that appears fades in and one that disappears or left fades out. +// During a transition, a row that appears fades in, and one that disappears or left fades out. const FADE_IN: u32 = 8u; const FADE_OUT: u32 = 16u; -// A row that entered with this change: it has no state to ease from. +// The row entered with this change, so it has no old state to ease from. const ENTERING: u32 = 32u; -// A vertex whose size is in position units, and one with its own height: what any reader of its -// slot needs to know its size in pixels. +// The vertex's size is in position units, or it has its own height. Any mark reading the vertex +// needs these to compute its size in pixels. const HALF_UNITS: u32 = 64u; const OWN_HEIGHT: u32 = 128u; @@ -277,13 +294,13 @@ const POINTS: u32 = 0u; const STROKES: u32 = 1u; const TRIANGLES: u32 = 2u; -// What a stroke's segments run between: two vertices (straight, along a great circle, or through -// bends), samples across the axis, or a geometry column's points. A triangle's corners come from -// three vertices, or from a geometry column three points at a time. +// Path kinds, the source of a stroke's points. ENDS: two vertices, joined straight, along a great +// circle, or through bends. TRACE: samples across the axis. GEOMETRY: a geometry column's points. +// Faces use ENDS for their three vertices; areas use GEOMETRY, three points per triangle. const PATH_ENDS: u32 = 0u; const PATH_TRACE: u32 = 1u; const PATH_GEOMETRY: u32 = 2u; -// A pole: from each row's own vertex down to the ground under it. +// A pole, from each row's own vertex down to the ground below it. const PATH_POLES: u32 = 3u; // Mark flags. @@ -297,23 +314,24 @@ const STEP_BEFORE: u32 = 64u; const STEP_AFTER: u32 = 128u; // A vertex mark whose blocks and groups drop shadows onto a diagram's sheet. const SHADOW: u32 = 256u; -// A vertex mark whose rows hold rows: drawn as containers. +// A vertex mark whose rows contain other rows. Its rows draw as containers. const GROUP: u32 = 512u; -// Focus bits per row: selected, hovered, and an end of a selected or hovered edge, which the focus -// draw lights over the rest; FIT marks the rows a fit measures. +// Focus bits per row. SELECTED, HOVERED, and END (an end of a selected or hovered edge) make a row +// lit, and the focus draw redraws lit rows over everything. FIT marks the rows a fit measures. const SELECTED: u32 = 1u; const HOVERED: u32 = 2u; const FIT: u32 = 4u; const END: u32 = 8u; const LIT: u32 = 11u; -// Ordered words per mark in the extents buffer: least x, y, z as complements, then greatest. +// Words per mark in the extents buffer, as order-preserving bits (`ordered`): the complements of +// the least x, y, and z, then the greatest x, y, and z. const EXTENT_WORDS: u32 = 6u; -// Probe words: the least key (distance, then the mark drawn last), the instance plus one, where the -// hit lies (its depth and screen point), the nearest depth drawn under the probe, and the instance's -// row and segment. +// Probe buffer words. KEY: the least hit key (distance, then the mark drawn last). INSTANCE: the +// hit instance plus one. DEPTH, X, Y: where the hit lies. UNDER: the nearest depth drawn around the +// probe. ROW, SEGMENT: the hit's held row and part. const PROBE_KEY: u32 = 0u; const PROBE_INSTANCE: u32 = 1u; const PROBE_DEPTH: u32 = 2u; @@ -322,14 +340,15 @@ const PROBE_Y: u32 = 4u; const PROBE_UNDER: u32 = 5u; const PROBE_ROW: u32 = 6u; const PROBE_SEGMENT: u32 = 7u; -// Where a located row lies on screen, and whether it shows. What queries find follows the probe's words. +// Where a located row lies on screen, and whether it shows. Query results follow the probe's +// `PROBE_WORDS` words. const PROBE_LOCATE_X: u32 = 8u; const PROBE_LOCATE_Y: u32 = 9u; const PROBE_LOCATED: u32 = 10u; const PROBE_WORDS: u32 = 12u; -// The steps of the probe program, as its step uniform names them: the hover's three, then the -// queries', then the depth under the probe. +// Probe program steps, chosen by its step uniform: the hover's three, the queries' three, then the +// depth under the probe. const STEP_NEAREST: u32 = 0u; const STEP_TOPMOST: u32 = 1u; const STEP_DESCRIBE: u32 = 2u; @@ -339,13 +358,12 @@ const STEP_LOCATE: u32 = 5u; const STEP_UNDER: u32 = 6u; const STEPS: u32 = 7u; -// What a dispatch of the expression machine's `evaluate` runs, as its step uniform names it: a -// mark's channels, the reductions fitting its domains, or where its rows' ends are held. +// Steps of the expression machine's `evaluate`, chosen by its step uniform: run a mark's channels, +// run the reductions that fit its domains, or resolve its rows' ends to held rows. const STEP_CHANNELS: u32 = 0u; const STEP_REDUCE: u32 = 1u; const STEP_ENDS: u32 = 2u; -// Words of a hit a query finds: its row, distance in sixty-fourths of a pixel, part, screen x and y, -// and depth. +// Words per query hit: held row, distance in 1/64 CSS pixel, part, screen x and y, and depth. const HIT_WORDS: u32 = 6u; // Overlay sprites. @@ -387,20 +405,21 @@ const THEME_LINE: u32 = 4u; const THEME_LINE_WIDTH: u32 = 5u; const THEME_AREA: u32 = 6u; const THEME_TEXT: u32 = 7u; +// The color a colormap gives a missing value. const THEME_MISSING: u32 = 8u; -// Nothing: a fully transparent color, what a missing status draws. +// A fully transparent color, which a missing status draws. const THEME_CLEAR: u32 = 9u; -// What a group's container is filled with. +// A group container's fill color. const THEME_GROUP: u32 = 10u; -// A diagram's look, in units: the border of its blocks and groups (clear for none), their corners' -// radius, its ports' width, and how far a block's shadow falls below it. +// Diagram style. OUTLINE: the border color of blocks and groups, clear for none. CORNER, PORT_SIZE, +// LIFT: the corner radius, port width, and how far a block's shadow falls, in length units. const THEME_OUTLINE: u32 = 11u; const THEME_CORNER: u32 = 12u; const THEME_PORT_SIZE: u32 = 13u; const THEME_LIFT: u32 = 14u; const THEME_SLOTS: u32 = 16u; -// The unit comets' spacing and length are in: the space's lengths, position units, or CSS pixels. +// The unit of comet spacing and length: the space's length unit, position units, or CSS pixels. const FLOW_LENGTHS: u32 = 0u; const FLOW_UNITS: u32 = 1u; const FLOW_PX: u32 = 2u; @@ -410,33 +429,34 @@ const GRID_OFF: u32 = 0u; const GRID_LINES: u32 = 1u; const GRID_DOTS: u32 = 2u; -// A list's header: its item count; where its counts (scanned in place to where each item's entries -// start, then the total), block sums, and entries start; the indirect arguments a draw and a -// dispatch over its entries read; and how many entries it keeps. +// List header words. ITEMS: the item count. COUNTS, SUMS, ENTRIES: where the per-item counts, the +// block sums, and the entries start. The scan turns the counts in place into each item's first +// entry, followed by the total. const LIST_ITEMS: u32 = 0u; const LIST_COUNTS: u32 = 1u; const LIST_SUMS: u32 = 2u; const LIST_ENTRIES: u32 = 3u; -// Draw arguments: vertices, instances (the total), first vertex, first instance. +// Indirect draw arguments: vertices, instances (the kept entries), first vertex, first instance. const LIST_DRAW: u32 = 4u; -// Dispatch arguments: workgroups over the total, 1, 1. +// Indirect dispatch arguments: workgroups over the kept entries, 1, 1. const LIST_DISPATCH: u32 = 8u; -// Entries the list keeps: past them, entries are counted but not written. +// How many entries the list keeps. Entries past it are counted but not written. const LIST_KEPT: u32 = 11u; const LIST_HEADER: u32 = 12u; -// Counts a workgroup of the scan takes. +// Counts per workgroup of the scan. const SCAN_WORKGROUP: u32 = 256u; -// What every draw and dispatch of a frame reads. +// Per-frame values every draw and dispatch reads. struct Frame { center_hi: vec2f, center_lo: vec2f, pointer: vec2f, - // Where a probe looks, CSS pixels, and the box a query covers. + // The probe's point in CSS pixels. `query` holds the running query's box (`boxed`), point and + // radius (`hits`), or drawn row (`locate`). probe: vec2f, query: vec4f, - // The direction toward the key light faces are lit by, which turns with the camera. + // Direction toward the key light that shades faces. It turns with the camera. light: vec4f, space: u32, globe: f32, @@ -446,39 +466,39 @@ struct Frame { selecting: u32, // 1 while a fit measures only the rows marked FIT. fitting: u32, - // 1 when the target stores linear color (an sRGB format): outputs convert from sRGB. + // 1 when the target stores linear color (an sRGB format), so outputs convert from sRGB. linear: u32, glow: f32, dim: f32, dash: f32, - // Comets: the distance between them and how far a tail trails its head, in the unit `flow_unit` - // says; how wide a head is, in CSS pixels; and the seconds they travel this frame. + // Comets: spacing and tail length in `flow_unit`, head width in CSS pixels, and the seconds + // they travel this frame. flow_spacing: f32, flow_length: f32, flow_width: f32, flow_unit: u32, flow_step: f32, - // How far a lane bows, in its edge's lengths at its middle. + // How far a lane bows at its middle, as a fraction of its edge's length. bow: f32, - // How much a hovered vertex sized in CSS pixels grows. + // Scale factor for a hovered vertex sized in CSS pixels. grow: f32, shading: f32, arrow: f32, halo: f32, - // CSS pixels between a group's members and its outline: its title's inset. + // CSS pixels between a group's members and its outline. It is also the title's inset. group_padding: f32, - // CSS pixels per length unit: per position unit in a diagram, where every length is a unit; 1 - // elsewhere. Lengths in the frame above are already in pixels. + // CSS pixels per length unit: per position unit in a diagram, 1 elsewhere. The theme lengths + // above are already in CSS pixels, except comet spacing and length. unit: f32, zoom: f32, - // The label grid: where its cells start in the labels buffer, and its columns and rows; and how - // far labels fade per frame. + // The label grid: where its cells start in the labels buffer, and its column and row counts. + // `label_fade` is how much a label's opacity changes per frame. cells: u32, cell_columns: u32, cell_rows: u32, label_fade: f32, - // A plane's grid: its kind, pitch in position units, and the world distance from the center it - // fades out by, 0 for none; and whether the earth draws its graticule. + // A plane's grid: kind, pitch in position units, and the world distance from the center at + // which it has faded out (0 for no fade). `graticule` is 1 if the earth draws a graticule. grid_kind: u32, grid_pitch: f32, grid_fade: f32, @@ -487,20 +507,20 @@ struct Frame { selected: vec4f, flow: vec4f, background: vec4f, - // The earth's ground. + // The earth's ground color. ground: vec4f, grid_color: vec4f, - // The direction toward the sun in the earth's frame (x toward 0° longitude on the equator, z - // toward the north pole), and how bright night stays; no direction where daylight is off. + // Direction toward the sun in earth-fixed coordinates (x toward 0° longitude on the equator, z + // toward the north pole), with night's brightness in w. All zero when daylight is off. sun: vec4f, - // The air glowing around the globe; clear where the earth has none. + // Color of the atmosphere around the globe; clear when the earth has none. atmosphere: vec4f, - // What a diagram's blocks and groups drop their shadows in. + // Shadow color of a diagram's blocks and groups. shadow: vec4f, theme: array, } -// What a frame draws through: the view's camera. +// The view's camera, which every pass projects through. struct Eye { clip_from_world: mat4x4f, world_from_clip: mat4x4f, @@ -518,33 +538,35 @@ struct Mark { index: u32, kind: u32, primitive: u32, - // Rows drawn, and rows past them that left and fade out. + // Drawn rows, and the rows after them that left and are fading out. rows: u32, exits: u32, - // Rows the mark holds evaluated, leaving rows included: of a table drawn whole its rows, of one - // drawn in part its held pages' rows. Every word a mark keeps per row is a held row's, in pages of - // `PAGE_ROWS` held rows each that page tables in the program list. + // Held rows: the rows the mark keeps evaluated on the GPU, leaving rows included. For a table + // drawn whole, these are its rows. For a paged table, they are the rows of its slots. Every + // per-row word belongs to a held row, in pages of `PAGE_ROWS` held rows listed by page tables + // in the program. held: u32, - // Where the program lists which held rows the drawn rows are, a run at a time, and where the - // lists hold each drawn row's word, expanded from them; NONE where every drawn row is the held - // row of its number, as a table drawn whole draws. + // Where the program lists the drawn rows as runs of held rows, and where the lists buffer holds + // each drawn row's word expanded from them. `drawn` is NONE when drawn row `i` is held row `i`, + // as for a table drawn whole. row_runs: u32, drawn: u32, - // How far the tiles coming and going have faded, from 0 to 1. + // Fade progress of tiles entering and leaving, from 0 to 1. reveal: f32, - // The page table of each row's words: its kind words, `stride` of them, then its extra words. + // The page table of each row's words: `stride` kind words, then `extras` extra words. state: u32, stride: u32, extras: u32, - // While the mark eases, the page tables of each row's words as it showed when the transition - // began, and as its channels evaluate them, where it settles; NONE otherwise. + // During a transition, the page tables of each row's words as shown when it began (`frozen`) + // and as its channels now evaluate them (`settled`); NONE otherwise. `motion` is its eased + // progress, from 0 to 1. frozen: u32, settled: u32, motion: f32, - // Where the program holds how each word of a row eases, a word each. + // Where the program holds each row word's blend (`BLEND_*`), one word per row word. blends: u32, - // The row blocks a dispatch over the mark's rows covers, as the blocks buffer lists them: those - // its channels evaluate at `block`, those whose ends the end pass finds at `end_block`. + // Where the blocks buffer lists the row blocks a dispatch covers: at `block` those its channels + // evaluate, and at `end_block` those whose ends the end pass resolves. block: u32, end_block: u32, // The page table of each row's focus word. @@ -564,38 +586,40 @@ struct Mark { shape: u32, corner: f32, hole: f32, - // Marker inputs: how many, the extra word of the first, and where their colors start in the constants. + // Marker inputs: how many, the extra word of the first, and where the marker's palette starts + // in the program. inputs: u32, input: u32, palette: u32, // The program column a line traces, or NONE. trace: u32, - // Segments of each row when every row has as many; NONE when the mark's list numbers them. + // Segments per row when every row has the same count; NONE when the segment list numbers them. per_row: u32, - // Where the header of the mark's list of segments starts in the lists buffer, and where what the - // running query finds of it starts among the probe's words. + // Where the mark's segment list header starts in the lists buffer, and where the running + // query's results for the mark start in the probe buffer. list: u32, query: u32, - // Where the mark's remap words start, while its transition is being frozen. + // Where the mark's remap words start, while `gather` freezes its transition. remap: u32, - // The rows its focus draw lights, at `focused` in the focused buffer, how many, and the - // instances each takes: a point one, a path as many as its longest piece count. + // The rows the focus draw lights: where they start in the focused buffer, how many, and the + // instances per row (one for a point, the most segments of any lit path). focused: u32, focused_rows: u32, pieces: u32, - // Program columns: the geometry a stroke's path runs through (a route or bends), the geometry - // it ran through when its transition began, and each row's lane among edges joining the same - // two vertices; NONE without. + // Program columns, NONE when absent: the geometry a stroke's path follows (a route or + // bends), that geometry at the start of the transition, and each edge's lane among edges + // joining the same two vertices. geometry: u32, frozen_geometry: u32, lane: u32, - // Each port of a vertex mark's rows, and how many each row has. + // Where the program lists the mark's ports (`PORT_WORDS` each), and how many each row has. port_info: u32, ports: u32, - // The extra word of the label's code, or NONE; the page table of each row's candidate; the labels - // buffer's offsets of its run table (two words per code), its wanted codes (a count, then codes), - // and its seen codes (a bit per code shown in view); the id of its first candidate, unique across - // marks; the size labels and port labels are set at, in length units. + // `label`: the extra word holding the label's code, or NONE. `candidates`: the page table of + // each row's candidate. In the labels buffer: `runs`, the run table (two corner words per + // code); `wanted`, a count followed by the codes to set; `seen`, a bit per code shown in view. + // `label_id`: the id of the mark's first candidate, unique across marks. The two sizes are in + // length units. label: u32, candidates: u32, runs: u32, @@ -604,19 +628,22 @@ struct Mark { label_id: u32, label_size: f32, port_label_size: f32, - // The most labels shown, or NONE; and where the labels buffer holds the select that caps them. + // The most labels shown, or NONE for no cap; and where the labels buffer holds the select that + // applies the cap. label_max: u32, label_select: u32, - // The CSS pixels the mark's size keeps within at any zoom: a vertex's half, a stroke's width. + // The range of CSS pixels the mark's size is clamped to at any zoom: a vertex's half size, or a + // stroke's width. size_min: f32, size_max: f32, - // A vertex mark's page table of the normals faces sum at its rows; NONE where no face reads it. + // A vertex mark's page table of the face normals summed at its rows; NONE when no face reads + // them. normals: u32, } -// A line or a run of text drawn over the marks, in CSS pixels. +// An overlay sprite in CSS pixels: a line, box, fill, or run of text, drawn over the marks. struct Sprite { - // A line's ends, or a text's top left and bottom right. + // A line's ends, or opposite corners of a rectangle (top left and bottom right for text). a: vec2f, b: vec2f, // A text's rectangle in the atlas, texels. @@ -626,18 +653,18 @@ struct Sprite { kind: u32, } -// One panel of a shot composed into the bytes a consumer takes: RGBA rows for an image, or NV12 -// planes for an encoder. Pixels are the shot's. +// One panel of a shot, composed into output bytes: RGBA rows for an image, or NV12 planes for a +// video encoder. Sizes are in the shot's pixels. struct Compose { - // Where the panel's top left lands in the picture, and its size. + // The panel's top-left position in the picture, and its size. origin: vec2u, size: vec2u, // The picture's size. picture: vec2u, // Where NV12's interleaved chroma plane starts, in words. chroma: u32, - // Whether pixels keep their alpha: an image of a transparent shot. + // Whether pixels keep their alpha, for an image of a transparent shot. alpha: u32, - // What shows where no panel draws: straight sRGB. + // The color where no panel draws, as straight-alpha sRGB. background: vec4f, } diff --git a/crates/latkit/src/data/bits.rs b/crates/latkit/src/data/bits.rs index 97cb30b8..4c95bcfa 100644 --- a/crates/latkit/src/data/bits.rs +++ b/crates/latkit/src/data/bits.rs @@ -2,7 +2,7 @@ use std::ops::Range; use super::buffer::Buffer; -/// A shared, immutable bit vector, used for booleans and validity. +/// A shared, immutable bit vector for booleans and validity. Cloning and slicing share the words. #[derive(Clone, Default)] pub(crate) struct Bits { words: Buffer, @@ -29,7 +29,7 @@ impl Bits { Bits { words: self.words.slice(first..last.max(first)), offset: bit & 63, len: range.end - range.start } } - /// `len` bits, bit `i` being `bit(i)`, packed a word at a time. + /// `len` bits where bit `i` is `bit(i)`, packed 64 to a word. pub(crate) fn from_fn(len: usize, bit: impl Fn(usize) -> bool) -> Bits { let words = (0..len.div_ceil(64)) .map(|w| { @@ -49,7 +49,8 @@ impl Bits { self.words[..full].iter().all(|&w| w == u64::MAX) && (rest == 0 || self.words[full] & mask == mask) } - /// The bits as little-endian words starting at bit 0, for byte encoding. + /// The bits as words starting at bit 0, least significant bit first, with the unused high bits + /// of the last word cleared. This is the byte encoding's layout. pub(crate) fn to_words(&self) -> Vec { if self.offset == 0 { let mut words = self.words[..self.len.div_ceil(64)].to_vec(); @@ -90,7 +91,7 @@ impl std::fmt::Debug for Bits { } } -/// Builds [`Bits`] one bit at a time, keeping the word it fills apart. +/// Builds [`Bits`] one bit at a time. The partly filled word is kept separately until it is full. pub(crate) struct BitsBuilder { words: Vec, word: u64, diff --git a/crates/latkit/src/data/buffer.rs b/crates/latkit/src/data/buffer.rs index 9ef9a4cd..1421c1f2 100644 --- a/crates/latkit/src/data/buffer.rs +++ b/crates/latkit/src/data/buffer.rs @@ -1,7 +1,7 @@ use std::ops::{Deref, Range}; use std::sync::Arc; -/// A shared, immutable slice: making one from a vector, cloning it, and slicing it never copy. +/// A shared, immutable slice. Creating one from a `Vec`, cloning, and slicing never copy elements. pub(crate) struct Buffer { data: Arc>, start: usize, diff --git a/crates/latkit/src/data/bytes.rs b/crates/latkit/src/data/bytes.rs index f27785c8..9bf05884 100644 --- a/crates/latkit/src/data/bytes.rs +++ b/crates/latkit/src/data/bytes.rs @@ -1,30 +1,36 @@ -//! One byte encoding for patches and datasets: a frame of a magic, its length, the name of the -//! dataset its patch changes (empty when it names none), and the patch's operations. Little endian -//! throughout. A frame says its length up front, so frames back to back split without decoding, and a -//! stream knows at once whether it holds a whole one. A dataset's bytes are the patch to it that -//! builds it. +//! The binary encoding shared by patches and datasets. //! -//! | Part | Layout | -//! | -------- | ------------------------------------------------------------------------------------- | -//! | frame | `b"LATK"`, `u16` version (1), `u16` flags (0), `u64` length of the whole frame, name, `u32` op count, ops | -//! | op | `u8` tag and its payload: axis, replace, set, remove, append, forget, or frames | -//! | axis | name, unit | -//! | replace | table, as `set` | -//! | remove | table name, ids | -//! | append | table name, `u8` 1 and ids or `u8` 0 for every row, `u32` frames, `f64` coordinates, | -//! | | `u32` field count; per field: name, values frame-major | -//! | forget | `f64` before | -//! | frames | table name, field name, `u32` rows, run | -//! | table | name, ids, `u32` field count; per field: name, `u8` tag, payload | -//! | field | values; a reference's table and its target ids as values; or samples | -//! | ids | `u8` 0 and `u32` count, or `u8` 1 and values: `u64`, `u32` read as `u64`, or texts | -//! | values | `u8` storage, `u32` len, data, `u8` has-validity, validity words (`u64`) | -//! | samples | `u32` rows, `u64` first frame read, `u64` frames read, `u8` highest level, `u32` run count, runs; buckets | -//! | | whose eight entries a level below are held are left out, as they follow from those | -//! | run | `u8` level, `u64` first entry, `u32` entries, `f64` coordinates (a frame's own; a | -//! | | bucket's first and last frame's), `f32` values entry-major: a frame's per row, or a | -//! | | bucket's least per row and, after every bucket's, its greatest (NaN missing) | -//! | `str` | `u32` byte length, UTF-8 | +//! A patch encodes as one frame: a header (magic and total length), the name of the dataset the +//! patch changes (empty when it names none), and the patch's operations. Everything is little +//! endian. Because the header states the frame's length, back-to-back frames split without +//! decoding, and a reader knows as soon as the header arrives whether it has a whole frame. A +//! dataset encodes as the patch that builds it from an empty dataset. +//! +//! | Part | Layout | +//! | ------------ | ---------------------------------------------------------------------------- | +//! | frame | `b"LATK"`, `u64` length of the whole frame, name, `u32` op count, ops | +//! | op | `u8` tag (0 axis, 1 replace, 2 set, 3 remove, 4 append, 5 forget, 6 frames), | +//! | | then its payload | +//! | axis | name, unit | +//! | replace, set | table | +//! | remove | table name, ids | +//! | append | table name, `u8` 1 and ids (or `u8` 0 for every row), `u32` frame count, | +//! | | `f64` coordinates, `u32` field count; per field: name, values frame-major | +//! | forget | `f64` before | +//! | frames | table name, field name, `u32` rows, run | +//! | table | name, ids, `u32` field count; per field: name, `u8` tag, payload | +//! | field | values; a reference's target table name and target ids as values; or samples | +//! | ids | `u8` 0 and `u32` count, or `u8` 1 and values: `u64`, `u32` (read as `u64`), | +//! | | or text | +//! | values | `u8` storage, `u32` length, data, `u8` has-validity, validity words (`u64`) | +//! | samples | `u32` rows, `u64` first frame, `u64` frame count, `u8` highest level, `u32` | +//! | | run count, runs. Buckets whose eight entries one level below are all present | +//! | | are omitted, since decoding derives them | +//! | run | `u8` level, `u64` first entry, `u32` entry count, `f64` coordinates (one per | +//! | | frame; two per bucket, its first and last frame's), then `f32` values | +//! | | entry-major: for frames, one per row; for buckets, every bucket's least | +//! | | value per row, then every bucket's greatest. NaN is missing | +//! | `str` | `u32` byte length, UTF-8 | use std::io::Read; use std::sync::Arc; @@ -42,17 +48,20 @@ use super::values::{Array, Values}; use crate::{Error, Result}; const MAGIC: &[u8; 4] = b"LATK"; -/// Magic, version, flags, and total frame length. -const HEADER: usize = 16; -const VERSION: u16 = 1; - -/// Limits for one binary frame. These bound encoded bytes and conservative decoded storage -/// accounting; applying a patch and retaining datasets have separate memory costs. +/// Header size in bytes: the magic and the frame's total length. +const HEADER: usize = 12; + +/// Size limits for decoding one binary frame. Exceeding either is an +/// [`Error::Limit`](crate::Error::Limit). +/// +/// The limits bound the encoded bytes and a conservative estimate of decoded storage. Memory used +/// afterwards, to apply the patch or keep the dataset, is not counted. #[derive(Clone, Copy, Debug)] pub struct DecodeLimits { - /// Maximum encoded frame size; checked as soon as the header arrives. + /// The largest encoded frame, in bytes. Checked as soon as the header arrives. Default 256 MiB. pub frame_bytes: usize, - /// Budget for decoded vectors, names, operations, pages, and derived sample buckets. + /// The budget, in bytes, for decoded vectors, names, operations, pages, and derived sample + /// buckets. Default 512 MiB. pub decoded_bytes: usize, } @@ -86,8 +95,6 @@ const STORE_GEOMETRY: u8 = 7; pub(crate) fn encode(patch: &Patch) -> Vec { let mut w = Writer(Vec::new()); w.0.extend_from_slice(MAGIC); - w.0.extend_from_slice(&VERSION.to_le_bytes()); - w.0.extend_from_slice(&0_u16.to_le_bytes()); w.0.extend_from_slice(&[0; 8]); w.str(patch.data.as_deref().unwrap_or("")); w.u32(patch.ops.len()); @@ -146,11 +153,12 @@ pub(crate) fn encode(patch: &Patch) -> Vec { } } let length = w.0.len() as u64; - w.0[8..HEADER].copy_from_slice(&length.to_le_bytes()); + w.0[MAGIC.len()..HEADER].copy_from_slice(&length.to_le_bytes()); w.0 } -/// `values` as bytes, as a field holding them writes them: how jobs hand a column over. +/// Encodes `values` alone, in the format's `values` layout. Jobs use it to pass columns to and +/// from the host. #[cfg(feature = "gpu")] pub(crate) fn values_bytes(values: &Values) -> Vec { let mut w = Writer(Vec::new()); @@ -158,7 +166,8 @@ pub(crate) fn values_bytes(values: &Values) -> Vec { w.0 } -/// The values `bytes` hold, every byte of them, as [`values_bytes`] wrote them. +/// Decodes values written by [`values_bytes`]. Errors if `bytes` are malformed or extend past +/// the values. #[cfg(feature = "gpu")] pub(crate) fn read_values(bytes: &[u8]) -> Result { let mut r = Reader { bytes, at: 0, remaining: DecodeLimits::default().decoded_bytes }; @@ -169,7 +178,8 @@ pub(crate) fn read_values(bytes: &[u8]) -> Result { Ok(values) } -/// The patch `bytes` hold, every byte of them. +/// Decodes one frame that fills `bytes` exactly, with default limits. Errors if `bytes` are +/// malformed, truncated, or extend past the frame. pub(crate) fn decode(bytes: &[u8]) -> Result { match frame(bytes, DecodeLimits::default())? { Some((patch, used)) if used == bytes.len() => Ok(patch), @@ -178,9 +188,9 @@ pub(crate) fn decode(bytes: &[u8]) -> Result { } } -/// The patches `read` carries, frames back to back, each read once whole: its header, checked -/// against `limits` as it arrives, says how many bytes follow. They end with the bytes, between -/// frames, or at the first error. +/// Decodes back-to-back frames from `read`, yielding each patch once its frame has fully arrived. +/// Each header is checked against `limits` as it arrives and gives the number of bytes to read. +/// The iterator ends when the input ends between frames, or after the first error. pub(crate) fn stream(mut read: R, limits: DecodeLimits) -> impl Iterator> { let mut ended = false; std::iter::from_fn(move || { @@ -193,7 +203,8 @@ pub(crate) fn stream(mut read: R, limits: DecodeLimits) -> impl Iterato }) } -/// The patch of the next frame `read` holds; `None` where its bytes end between frames. +/// Reads the next frame from `read`. Returns `None` if the input ends between frames; an end +/// inside a frame is a truncation error. fn next(read: &mut impl Read, limits: DecodeLimits) -> Result> { let failed = |error: std::io::Error| match error.kind() { std::io::ErrorKind::UnexpectedEof => malformed("truncated"), @@ -201,7 +212,7 @@ fn next(read: &mut impl Read, limits: DecodeLimits) -> Result> { }; let mut bytes = vec![0; HEADER]; let mut held = 0; - // The header is checked as its bytes arrive, so bytes that are not latkit's fail at once. + // Check the header as its bytes arrive, so input that is not latkit's fails immediately. let length = loop { if let Some(length) = length(&bytes[..held], limits.frame_bytes)? { break length; @@ -220,24 +231,15 @@ fn next(read: &mut impl Read, limits: DecodeLimits) -> Result> { Ok(Some(patch)) } -/// The length of the frame `bytes` begin, as its header says, once the header is checked and the -/// length found within `limit`; `None` while `bytes` hold less than the header. +/// The total length of the frame starting at `bytes`, read from its header. Validates the part +/// of the magic that has arrived, so foreign input fails early. Returns `None` until the whole +/// header is present. Errors if the length exceeds `limit` or is shorter than the header. fn length(bytes: &[u8], limit: usize) -> Result> { let held = bytes.len().min(MAGIC.len()); if bytes[..held] != MAGIC[..held] { return Err(malformed("not latkit bytes")); } - if let Some(version) = bytes.get(4..6) - && version != VERSION.to_le_bytes() - { - return Err(malformed("unsupported LATK version")); - } - if let Some(flags) = bytes.get(6..8) - && flags != [0, 0] - { - return Err(malformed("unsupported LATK flags")); - } - let Some(length) = bytes.get(8..HEADER) else { return Ok(None) }; + let Some(length) = bytes.get(MAGIC.len()..HEADER) else { return Ok(None) }; let length = u64::from_le_bytes(length.try_into().expect("eight bytes")); let length = usize::try_from(length).map_err(|_| too_long(limit))?; if length > limit { @@ -249,8 +251,8 @@ fn length(bytes: &[u8], limit: usize) -> Result> { Ok(Some(length)) } -/// The patch of the frame `bytes` begin and the frame's length; `None` while `bytes` hold only its -/// start. +/// Decodes the frame starting at `bytes`, returning its patch and length. Returns `None` while +/// `bytes` holds only part of the frame. fn frame(bytes: &[u8], limits: DecodeLimits) -> Result> { let Some(length) = length(bytes, limits.frame_bytes)? else { return Ok(None) }; let Some(frame) = bytes.get(..length) else { return Ok(None) }; @@ -280,7 +282,7 @@ impl Reader<'_> { let ids = match r.u8()? { 0 => None, 1 => Some(r.ids()?), - _ => return Err(malformed("an append names its rows or none")), + _ => return Err(malformed("an append's ids flag is neither 0 nor 1")), }; let frames = r.len(8)?; let at = r.vec(frames)?; @@ -340,7 +342,7 @@ impl Writer { #[cfg(target_endian = "little")] self.0.extend_from_slice(bytemuck::cast_slice(values)); #[cfg(target_endian = "big")] - compile_error!("latkit's byte format is written on little-endian targets"); + compile_error!("latkit's byte format requires a little-endian target"); } fn table(&mut self, table: &Table) { @@ -386,7 +388,8 @@ impl Writer { } } - /// `run`: its level, first entry, and entries, its coordinates `at`, and its `values`. + /// Writes `run`: its level, first entry, and entry count, then its coordinates `at` and its + /// `values`. fn run(&mut self, run: &Run, at: &[f64], values: &[f32]) { self.u8(run.level as u8); self.u64(run.entries.start); @@ -461,7 +464,7 @@ impl Writer { } } -/// Reads one frame, whose bytes are all there. +/// Decodes one frame whose bytes are all present. `remaining` is the decoded-storage budget left. struct Reader<'a> { bytes: &'a [u8], at: usize, @@ -478,7 +481,7 @@ impl Reader<'_> { Ok(()) } - /// Whether `n` more bytes are in the frame. + /// Errors as truncated unless `n` more bytes remain in the frame. fn reach(&self, n: usize) -> Result<()> { if self.at.saturating_add(n) > self.bytes.len() { return Err(malformed("truncated")); @@ -502,7 +505,8 @@ impl Reader<'_> { Ok(u32::from_le_bytes(self.take(4)?.try_into().expect("four bytes"))) } - /// A count of items at least `size` bytes each, checked against the bytes left. + /// Reads a `u32` count of items of at least `size` bytes each. Errors if the remaining bytes + /// cannot hold that many, so a corrupt count cannot cause a huge allocation. fn len(&mut self, size: usize) -> Result { let n = self.u32()? as usize; self.reach(n.saturating_mul(size))?; @@ -525,9 +529,10 @@ impl Reader<'_> { std::str::from_utf8(bytes).map(Name::from).map_err(|_| malformed("text is not UTF-8")) } - /// `n` values, copied whole. + /// Reads `n` plain values by copying their bytes. fn vec(&mut self, n: usize) -> Result> { - // Includes conversion into column pages and the sampled levels derived from runs. + // Charge four times the raw size, to cover conversion into column pages and the sample + // levels derived from runs. self.charge(n, 4 * size_of::())?; let bytes = self.take(n.checked_mul(size_of::()).ok_or_else(|| malformed("too large"))?)?; let mut out = vec![T::zeroed(); n]; @@ -539,7 +544,7 @@ impl Reader<'_> { match self.u8()? { 0 => Ok(Ids(IdList::Count(self.u32()?))), 1 => Ok(Ids(IdList::Listed(self.id_values()?))), - tag => Err(malformed(format!("unknown ids {tag}"))), + tag => Err(malformed(format!("unknown ids encoding {tag}"))), } } @@ -547,8 +552,8 @@ impl Reader<'_> { Ok(Column::new(&self.values()?)) } - /// Ids as values: `u32` ids, as writers without 64-bit integers send them, read as the `u64` every - /// numbered id is. + /// Reads ids as values. `u32` ids, sent by writers without 64-bit integers, are widened to + /// `u64`, the storage of every numeric id. fn id_values(&mut self) -> Result { let values = self.values()?; Ok(match values.array { @@ -578,15 +583,15 @@ impl Reader<'_> { Field::Reference(Reference::new(to, Column::new(&self.id_values()?))) } FIELD_SAMPLED => Field::Sampled(self.samples().map_err(|e| e.at(&name))?, Version::next()), - tag => return Err(malformed(format!("unknown field {tag}"))), + tag => return Err(malformed(format!("unknown field kind {tag}"))), }; table.insert(name, field); } Ok(table) } - /// A sampled field: its runs put in level by level, each level's buckets that follow from the - /// level below written before its own runs. + /// Reads a sampled field. Runs are inserted level by level. Before each level's own runs, the + /// buckets derivable from the level below are rebuilt. fn samples(&mut self) -> Result { let rows = self.u32()? as usize; let first = self.u64()?; @@ -615,7 +620,7 @@ impl Reader<'_> { Ok(samples) } - /// A run of entries of `rows` rows: what it holds, its coordinates, and its values. + /// Reads a run of entries for `rows` rows: the run, its coordinates, and its values. fn run(&mut self, rows: usize) -> Result<(Run, Vec, Vec)> { let level = u32::from(self.u8()?); if level > MAX_LEVEL { @@ -651,7 +656,7 @@ impl Reader<'_> { let n = self.len(1)?; self.charge(n, 2)?; let bytes = String::from_utf8(self.take(n)?.to_vec()).map_err(|_| malformed("text is not UTF-8"))?; - Array::Text(Text::from_parts(offsets, bytes).ok_or_else(|| malformed("text offsets"))?) + Array::Text(Text::from_parts(offsets, bytes).ok_or_else(|| malformed("invalid text offsets"))?) } STORE_GEOMETRY => { let depth = self.u8()?; @@ -664,9 +669,9 @@ impl Reader<'_> { let n = self.len(16)?; let coords: Vec<[f64; 2]> = self.vec(n)?; let geometry = - Geometry::from_parts(depth, levels, coords).ok_or_else(|| malformed("geometry offsets"))?; + Geometry::from_parts(depth, levels, coords).ok_or_else(|| malformed("invalid geometry offsets"))?; if geometry.len() != len { - return Err(malformed("geometry length")); + return Err(malformed("geometry count does not match the value count")); } Array::Geometry(geometry) } @@ -675,7 +680,7 @@ impl Reader<'_> { let valid = match self.u8()? { 0 => None, 1 => Some(Bits::from_words(self.vec(len.div_ceil(64))?, len)), - _ => return Err(malformed("validity")), + _ => return Err(malformed("the validity flag is neither 0 nor 1")), }; Ok(Values { array, valid }) } @@ -687,8 +692,8 @@ mod tests { use crate::{Data, Row, Snapshot, Value}; #[test] - fn v1_fixture_and_large_indexes_are_stable() { - let hex = include_str!("../../tests/fixtures/latk-v1.hex").trim(); + fn the_patch_fixture_and_large_indexes_are_stable() { + let hex = include_str!("../../tests/fixtures/patch.hex").trim(); let bytes: Vec = (0..hex.len()).step_by(2).map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap()).collect(); let patch = Patch::new().frames("T", "v", 0, (1_u64 << 32) + 3, [0.0, 1.0], [1.0, 2.0]); @@ -707,12 +712,6 @@ mod tests { }) .unwrap(); assert!(files.keys().any(|p| p.contains("536870912"))); - let mut version = bytes.clone(); - version[4] = 2; - assert!(Patch::from_bytes(&version).is_err()); - let mut flags = bytes.clone(); - flags[6] = 1; - assert!(Patch::from_bytes(&flags).is_err()); let tight = DecodeLimits { frame_bytes: 1024, decoded_bytes: 1 }; assert!(matches!(Patch::stream(&bytes[..], tight).next(), Some(Err(Error::Limit(_))))); } @@ -795,7 +794,7 @@ mod tests { assert!(Snapshot::from_bytes(&bytes).is_err()); } - /// Bytes handed over at most `n` a read, as a socket hands them. + /// A reader that returns at most `n` bytes per read, like a socket. struct Trickle<'a>(&'a [u8], usize); impl Read for Trickle<'_> { @@ -808,7 +807,7 @@ mod tests { } #[test] - fn patches_back_to_back_stream_one_by_one() { + fn back_to_back_patches_stream_individually() { let patches = [grid().snapshot().to_patch(), Patch::new().forget(1.5), Patch::new().remove("Bus", vec!["a"])]; let bytes: Vec = patches.iter().flat_map(Patch::to_bytes).collect(); let read: Vec> = Patch::stream(Trickle(&bytes, 5), DecodeLimits::default()) @@ -816,17 +815,17 @@ mod tests { .collect(); assert_eq!(read, patches.iter().map(Patch::to_bytes).collect::>()); let first = |bytes: &[u8], limits: DecodeLimits| Patch::stream(bytes, limits).next(); - assert!(first(&[], DecodeLimits::default()).is_none(), "no bytes, no patches"); + assert!(first(&[], DecodeLimits::default()).is_none(), "empty input yields no patch"); let foreign = first(b"LATX", DecodeLimits::default()); - assert!(foreign.is_some_and(|e| e.is_err_and(|e| e.to_string().contains("not latkit bytes"))), "at once"); - // A length past the limit fails as its header arrives rather than waiting for bytes that may - // never come. + assert!(foreign.is_some_and(|e| e.is_err_and(|e| e.to_string().contains("not latkit bytes"))), "immediately"); + // A length over the limit fails as soon as the header arrives, without waiting for bytes + // that may never come. let long = Patch::new().set(Table::new("Bus", Ids::count(1)).field("name", vec!["x".repeat(100)])).to_bytes(); let short = DecodeLimits { frame_bytes: 64, ..DecodeLimits::default() }; assert!(matches!(first(&long[..HEADER], short), Some(Err(Error::Limit(_))))); assert!(matches!(first(&long[..70], DecodeLimits::default()), Some(Err(Error::Invalid { .. }))), "truncated"); let mut twice = Patch::stream(&long[..70], DecodeLimits::default()); - assert!(twice.next().is_some_and(|patch| patch.is_err()) && twice.next().is_none(), "ends at an error"); + assert!(twice.next().is_some_and(|patch| patch.is_err()) && twice.next().is_none(), "ends after an error"); } #[test] @@ -840,24 +839,24 @@ mod tests { assert_eq!(first(&data.snapshot()), 1); assert_eq!(first(&Snapshot::from_bytes(&data.snapshot().to_bytes()).expect("decodes")), 1); data.apply(Patch::new().remove("Bus", vec!["b"])).expect("remove"); - assert_eq!(first(&data.snapshot()), 1, "rows removed keep the frames' numbers"); + assert_eq!(first(&data.snapshot()), 1, "removing rows keeps frame numbers"); } #[test] - fn time_tiles_round_trip_with_their_holes() { + fn time_tiles_round_trip_with_gaps() { let data = grid(); - // The bucket of frames 64..72, and frames 200 and 201, past a hole. + // The level-1 bucket of frames 64..72, then frames 200 and 201 after a gap. let bucket = Patch::new().frames("Bus", "vm", 1, 8, [64.0, 71.0], [0.9, 0.95, 0.97, 1.1, 1.05, 1.0]); let tile = bucket.frames("Bus", "vm", 0, 200, [200.0, 201.0], [1.0, 1.0, 1.0, 0.5, 0.5, 0.5]); let bytes = tile.to_bytes(); assert_eq!(Patch::from_bytes(&bytes).expect("decodes").to_bytes(), bytes); data.apply(tile.clone()).expect("tiles"); - assert!(data.apply(tile).is_err(), "entries held already"); + assert!(data.apply(tile).is_err(), "entries already present are refused"); let data = data.snapshot(); let back = Snapshot::from_bytes(&data.to_bytes()).expect("decodes"); assert_eq!(back.to_bytes(), data.to_bytes()); assert_eq!(back.get("Bus", "c", "vm", 201.5), Some(Value::Number(0.5))); - assert_eq!(back.get("Bus", "a", "vm", 100.0), None, "a hole holds no frame"); + assert_eq!(back.get("Bus", "a", "vm", 100.0), None, "a gap reads as missing"); assert_eq!(back.describe().span, Some([1.0, 201.0])); } diff --git a/crates/latkit/src/data/column.rs b/crates/latkit/src/data/column.rs index 99cc6f56..4c27c20a 100644 --- a/crates/latkit/src/data/column.rs +++ b/crates/latkit/src/data/column.rs @@ -4,28 +4,30 @@ use super::name::PageId; use super::values::{Kind, Picks, Value, Values}; use crate::Result; -/// Rows per page: pages are the unit of sharing between versions, of upload to the GPU, of what a -/// table draws in its order, and of what a mark keeps and evaluates per row. +/// Log2 of the rows per page (4,096). A page is the unit of sharing between versions, of GPU +/// upload, of residency when a paged table is drawn, and of a mark's per-row evaluation. pub(crate) const PAGE_BITS: u32 = 12; pub(crate) const PAGE_ROWS: usize = 1 << PAGE_BITS; pub(crate) const PAGE_MASK: usize = PAGE_ROWS - 1; -/// Writes a page remembers back: past them, it reads as written whole. +/// How many writes back a page records which rows changed. Beyond that, the page counts as +/// entirely rewritten. const REMEMBERED: u8 = 8; -/// One page of a column: up to [`PAGE_ROWS`] values, the id of exactly these contents, when it was -/// written over another page, which page and which rows, and the points its geometry takes. +/// One page of a column: up to [`PAGE_ROWS`] values and an id unique to these contents. A page +/// made by writing over another records which page and which rows (`written`). It also caches its +/// geometry's point count. #[derive(Clone, Debug)] pub(crate) struct Page { pub(crate) id: PageId, pub(crate) values: Values, pub(crate) written: Option>, - /// Counted the first time they are asked for: a page never changes. + /// Computed on first use and cached; pages are immutable. points: OnceLock, } -/// What a page was written from: the page it copied, the rows written over it there, offsets into -/// the page, and how that page was written in turn. +/// How a page was written: the page it copied (`from`), the rows written or added as offsets into +/// the page, and how `from` was written in turn (`before`), up to [`REMEMBERED`] writes back. #[derive(Debug)] pub(crate) struct Written { pub(crate) from: PageId, @@ -39,7 +41,7 @@ impl Page { Page { id: PageId::next(), values, written: None, points: OnceLock::new() } } - /// `values`: page `old` with `rows` written over it, offsets into the page. + /// A new page holding `values`, made by writing `rows` (offsets into the page) over `old`. fn over(values: Values, old: &Page, rows: Vec) -> Page { let before = old.written.clone().filter(|w| w.depth < REMEMBERED); let depth = before.as_ref().map_or(1, |w| w.depth + 1); @@ -47,13 +49,14 @@ impl Page { Page { id: PageId::next(), values, written: Some(Arc::new(written)), points: OnceLock::new() } } - /// The points its geometry takes as shaders read them, as [`Values::points`] counts them. + /// Its geometry's point count as shaders read it; see [`Values::points`]. pub(crate) fn points(&self) -> usize { *self.points.get_or_init(|| self.values.points()) } - /// The rows written since page `before`, offsets into the page, in order: `None` unless this page - /// was written over it, a few writes back at most. + /// The rows written since page `before`, as sorted offsets into the page; empty if this page is + /// `before`. `None` unless this page was written from `before` and still remembers it (see + /// [`REMEMBERED`]). pub(crate) fn rows_since(&self, before: PageId) -> Option> { if self.id == before { return Some(Vec::new()); @@ -73,12 +76,13 @@ impl Page { } } -/// A field's values in pages. Writes replace only the pages they touch; others keep their ids. +/// A field's values in pages of [`PAGE_ROWS`] rows. Writes replace only the pages they touch; +/// the others keep their ids. #[derive(Clone, Debug)] pub(crate) struct Column { len: usize, pages: Arc<[Page]>, - /// An empty slice of the column's storage, so an empty column still knows what it holds. + /// An empty slice with the column's storage type, so an empty column still knows its kind. like: Values, } @@ -91,20 +95,21 @@ impl Column { Column { len, pages, like: values.slice(0..0) } } - /// `len` missing values stored like `like`. + /// A column of `len` missing values with the storage of `like`. pub(crate) fn missing(like: &Values, len: usize) -> Column { let empty = Column { len: 0, pages: Arc::new([]), like: like.slice(0..0) }; empty.write(len, &[], &empty).expect("same storage") } - /// A column of whole pages; every page but the last holds [`PAGE_ROWS`] values. + /// A column of `pages`, each given a new id. Every page but the last must hold [`PAGE_ROWS`] + /// values. pub(crate) fn from_pages(pages: Vec, like: &Values) -> Column { debug_assert!(pages.iter().rev().skip(1).all(|p| p.len() == PAGE_ROWS)); let len = pages.iter().map(Values::len).sum(); Column { len, pages: pages.into_iter().map(Page::new).collect(), like: like.slice(0..0) } } - /// A column of `pages` as they are, their ids kept; every page but the last holds + /// A column of existing `pages`, keeping their ids. Every page but the last must hold /// [`PAGE_ROWS`] values. pub(crate) fn of_pages(pages: Vec, like: &Values) -> Column { debug_assert!(pages.iter().rev().skip(1).all(|p| p.values.len() == PAGE_ROWS)); @@ -124,7 +129,8 @@ impl Column { &self.pages } - /// Whether `other` holds the same pages: the same values, without comparing one. + /// Whether `other` has the same length and page ids, which means equal values without + /// comparing any. pub(crate) fn shares(&self, other: &Column) -> bool { self.len == other.len && self.pages.iter().zip(other.pages.iter()).all(|(a, b)| a.id == b.id) } @@ -137,25 +143,28 @@ impl Column { self.pages.get(row >> PAGE_BITS)?.values.get(row & PAGE_MASK) } - /// Where row `row` lies: its page's values and its place among them; `None` past the column's end. + /// The values of the page holding row `row` and the row's index in them; `None` past the + /// column's end. pub(crate) fn at(&self, row: usize) -> Option<(&Values, usize)> { let values = &self.pages.get(row >> PAGE_BITS)?.values; let i = row & PAGE_MASK; (i < values.len()).then_some((values, i)) } - /// The values of rows `rows`, in that order, as the GPU reads them: missing past the column's end, - /// as rows a derived table standing in for one not yet derived lacks are. + /// The values of rows `rows`, in that order, for GPU upload. Rows past the column's end come + /// back missing: a placeholder derived table, shown until its job finishes, can be shorter than + /// the rows requested. pub(crate) fn pick(&self, rows: &[u32]) -> Values { let from: Vec> = rows.iter().map(|&r| Some(r as usize).filter(|&r| r < self.len)).collect(); let sources: Vec<&Values> = std::iter::once(&self.like).chain(self.pages.iter().map(|p| &p.values)).collect(); Values::assemble(&sources, Picks::Writes { from: &from, kept: 0 }).expect("one storage") } - /// The column with `len` rows (at least the current count) and `writes` applied. + /// A copy with `len` rows (at least the current count) and `writes` applied. /// - /// `writes` are `(row, row of source)` sorted by row with no row twice. Pages that no write - /// touches and whose length does not change keep their ids; new rows not written are missing. + /// `writes` are `(row, source row)` pairs sorted by row, each row at most once. Pages that no + /// write touches and whose length is unchanged keep their ids. New rows not written are + /// missing. Errors if `source` holds another kind of value. pub(crate) fn write(&self, len: usize, writes: &[(usize, usize)], source: &Column) -> Result { debug_assert!(len >= self.len); debug_assert!(writes.windows(2).all(|w| w[0].0 < w[1].0)); @@ -189,7 +198,7 @@ impl Column { let kept = sources[0].len(); let values = Values::assemble(&sources, Picks::Writes { from: &from, kept })?; pages.push(match old { - // The page remembers what it was written over: the rows written, and those past its end. + // Record the rows written, plus the rows added past the old page's end. Some(old) => { let mut rows: Vec = page_writes.iter().map(|&(row, _)| (row - start) as u32).collect(); rows.extend(kept as u32..page_len as u32); @@ -201,9 +210,9 @@ impl Column { Ok(Column { len, pages: pages.into(), like: self.like.clone() }) } - /// The rows written since `before`, which this column was written from a few writes back at most, - /// in order: `None` where a page of it was not, or the number of rows changed. A row written the - /// value it held counts. + /// The rows written since `before`, in order, where this column was written from `before` a + /// few writes back. `None` if the row count changed or a changed page no longer remembers + /// `before`. A row rewritten with its old value still counts. pub(crate) fn rows_since(&self, before: &Column) -> Option> { if self.len != before.len { return None; @@ -217,9 +226,9 @@ impl Column { Some(rows) } - /// Whether its first `rows` rows hold what they held in `before`, which it was written from a few - /// writes back at most: written since only past them, as appending writes. Writes a page no - /// longer remembers count as written. + /// Whether the first `rows` rows are unchanged since `before`, which this column was written + /// from a few writes back: every write since is at row `rows` or later, as with appends. + /// Writes a page no longer remembers count as changes. pub(crate) fn kept_below(&self, before: &Column, rows: usize) -> bool { (0..rows.div_ceil(PAGE_ROWS)).all(|p| { let (Some(now), Some(then)) = (self.pages.get(p), before.pages.get(p)) else { return false }; @@ -229,9 +238,10 @@ impl Column { }) } - /// The rows written since `before`, or added, in order: of each page written over `before`'s, - /// the rows it remembers, else every row of it. Rows `before` held past this column's end are not - /// rows of it. + /// The rows written or added since `before`, in order. For each page that differs from + /// `before`'s, these are the rows it remembers writing since, or all its rows if it does not + /// remember `before`'s page. A page `before` lacks counts whole. Rows of `before` past this + /// column's end are not included. pub(crate) fn written_since(&self, before: &Column) -> Vec { let mut rows = Vec::new(); for (p, now) in self.pages.iter().enumerate() { @@ -248,8 +258,8 @@ impl Column { rows } - /// Every value in row order, as one run: a single page's without copying, else the pages copied - /// one after another. + /// All values in row order as one [`Values`]. A single-page column returns its page without + /// copying; otherwise the pages are concatenated. pub(crate) fn values(&self) -> Values { match &self.pages[..] { [] => self.like.clone(), @@ -258,9 +268,9 @@ impl Column { } } - /// The rows whose values differ from `before`'s, in order: of the rows written since, while the - /// pages remember them, those that now hold another value; else every row of the pages that - /// changed, compared. + /// The rows whose values differ from `before`'s, in order. When the pages remember the rows + /// written since, only those rows are compared; otherwise every row of each changed page is + /// (see [`differing`](Column::differing)). pub(crate) fn changed_since(&self, before: &Column) -> Vec { let Some(written) = self.rows_since(before) else { return self.differing(before) }; written @@ -269,13 +279,13 @@ impl Column { .collect() } - /// Whether row `row` holds what row `at` of `values` does. + /// Whether row `row` equals row `at` of `values`, as stored (see [`Values::same_row`]). pub(crate) fn holds(&self, row: usize, values: &Values, at: usize) -> bool { self.pages[row / PAGE_ROWS].values.same_row(row % PAGE_ROWS, values, at) } - /// The rows whose values differ from `before`'s, in order. A page both share holds the same values, - /// so only the pages that changed are compared, row by row; rows past the shorter column differ. + /// The rows whose values differ from `before`'s, in order. Pages with the same id are skipped; + /// the rest are compared row by row. Rows past the shorter column's end count as different. pub(crate) fn differing(&self, before: &Column) -> Vec { let mut rows = Vec::new(); for p in 0..self.pages.len().max(before.pages.len()) { @@ -292,8 +302,8 @@ impl Column { rows } - /// The column of rows `rows`, in that order. Leading pages whose rows are unchanged keep their - /// ids. + /// A column of rows `rows` of this one, in that order. Leading whole pages whose rows are + /// unchanged (`rows[i] == i`) keep their ids. pub(crate) fn gather(&self, rows: &[usize]) -> Column { let kept = rows.iter().enumerate().take_while(|&(i, &r)| i == r).count(); let kept_pages = (kept / PAGE_ROWS).min(rows.len() / PAGE_ROWS); @@ -341,13 +351,13 @@ mod tests { fn differing_rows_are_found_in_the_pages_that_changed() { let column = Column::new(&numbers(PAGE_ROWS * 2 + 10)); let source = Column::new(&Values::from(vec![-1.0_f64, (PAGE_ROWS + 4) as f64])); - // Row 3 of the second page changes; its row 4 is written the value it already held. + // Row 3 of the second page changes; its row 4 is rewritten with the value it already had. let written = column.write(column.len(), &[(PAGE_ROWS + 3, 0), (PAGE_ROWS + 4, 1)], &source); let written = written.expect("numbers"); assert_eq!(written.differing(&column), [PAGE_ROWS + 3]); - assert_eq!(column.differing(&column), Vec::::new(), "a column holds what it holds"); + assert_eq!(column.differing(&column), Vec::::new(), "a column does not differ from itself"); let longer = column.write(column.len() + 2, &[], &source).expect("numbers"); - assert_eq!(longer.differing(&column), [PAGE_ROWS * 2 + 10, PAGE_ROWS * 2 + 11], "rows past it differ"); + assert_eq!(longer.differing(&column), [PAGE_ROWS * 2 + 10, PAGE_ROWS * 2 + 11], "added rows differ"); } #[test] @@ -359,11 +369,11 @@ mod tests { assert_eq!(twice.rows_since(&column), Some(vec![3, 5, PAGE_ROWS + 4]), "every write since, in order"); assert_eq!(twice.rows_since(&once), Some(vec![5])); assert_eq!(twice.rows_since(&twice), Some(Vec::new())); - assert_eq!(column.rows_since(&twice), None, "nothing is written backward"); + assert_eq!(column.rows_since(&twice), None, "an older column is not written from a newer one"); assert_eq!(Column::new(&numbers(PAGE_ROWS + 10)).rows_since(&column), None, "nor from another column"); let grown = column.write(column.len() + 2, &[], &source).expect("numbers"); - assert_eq!(grown.rows_since(&column), None, "rows added change the rows themselves"); - // A page remembers a few writes back; past them, it reads as written whole. + assert_eq!(grown.rows_since(&column), None, "a changed row count lists no rows"); + // A page remembers a few writes back; beyond that it counts as entirely rewritten. let mut near = once.clone(); for _ in 1..REMEMBERED { near = near.write(near.len(), &[(7, 0)], &source).expect("numbers"); @@ -385,24 +395,24 @@ mod tests { "the row written, and the rows added past the end" ); let fresh = Column::new(&numbers(PAGE_ROWS + 10)); - assert_eq!(fresh.written_since(&column).len(), PAGE_ROWS + 10, "a page not written over is every row"); + assert_eq!(fresh.written_since(&column).len(), PAGE_ROWS + 10, "an unrelated page counts every row"); assert_eq!(column.written_since(&column), Vec::::new(), "nothing written since itself"); } #[test] - fn rows_appended_keep_the_rows_before_them() { + fn appends_leave_earlier_rows_unchanged() { let column = Column::new(&numbers(PAGE_ROWS + 10)); let source = Column::new(&Values::from(vec![-1.0_f64])); let appended = column.write(PAGE_ROWS + 12, &[(PAGE_ROWS + 11, 0)], &source).expect("numbers"); - assert!(appended.kept_below(&column, PAGE_ROWS + 10), "rows appended past them"); + assert!(appended.kept_below(&column, PAGE_ROWS + 10), "rows appended after them leave them unchanged"); let written = appended.write(appended.len(), &[(PAGE_ROWS + 9, 0)], &source).expect("numbers"); - assert!(written.kept_below(&appended, PAGE_ROWS + 9), "a row written past those asked about"); - assert!(!written.kept_below(&appended, PAGE_ROWS + 10), "and before them"); - assert!(!Column::new(&numbers(PAGE_ROWS + 10)).kept_below(&column, 1), "another column's rows are not"); + assert!(written.kept_below(&appended, PAGE_ROWS + 9), "a row written after the rows checked"); + assert!(!written.kept_below(&appended, PAGE_ROWS + 10), "a row written within them"); + assert!(!Column::new(&numbers(PAGE_ROWS + 10)).kept_below(&column, 1), "an unrelated column keeps no rows"); } #[test] - fn rows_written_the_value_they_held_have_not_changed() { + fn rows_rewritten_with_the_same_value_are_unchanged() { let column = Column::new(&numbers(PAGE_ROWS + 10)); // Row 4 holds 4. let source = Column::new(&Values::from(vec![-1.0_f64, 4.0])); @@ -410,21 +420,21 @@ mod tests { assert_eq!(written.rows_since(&column), Some(vec![3, 4, PAGE_ROWS + 4]), "every row written"); assert_eq!(written.changed_since(&column), vec![3, PAGE_ROWS + 4], "the rows whose values changed"); let far = (0..REMEMBERED).fold(written, |c, _| c.write(c.len(), &[(3, 0)], &source).expect("numbers")); - assert_eq!(far.changed_since(&column), vec![3, PAGE_ROWS + 4], "compared where the pages forgot"); + assert_eq!(far.changed_since(&column), vec![3, PAGE_ROWS + 4], "compared once pages forget their writes"); } #[test] - fn a_page_counts_its_points_once_and_a_page_written_over_it_its_own() { + fn a_page_counts_its_points_once_and_a_rewritten_page_recounts() { let path = |n: usize| (0..n).map(|i| [i as f64, 0.0]).collect::>(); let column = Column::new(&Values::from(vec![path(3), path(5)])); let page = &column.pages()[0]; assert_eq!(page.points.get(), None, "nothing counted until asked"); assert_eq!(page.points(), 8); - assert_eq!(page.points.get(), Some(&8), "counted once, then kept"); + assert_eq!(page.points.get(), Some(&8), "counted once, then cached"); assert_eq!(column.clone().pages()[0].points.get(), Some(&8), "a copy of the column shares the count"); let source = Column::new(&Values::from(vec![path(10)])); let written = column.write(column.len(), &[(0, 0)], &source).expect("paths"); - assert_eq!(written.pages()[0].points(), 15, "the page written over counts its own points"); + assert_eq!(written.pages()[0].points(), 15, "the rewritten page counts its own points"); } #[test] diff --git a/crates/latkit/src/data/describe.rs b/crates/latkit/src/data/describe.rs index 02806c24..d81bb70f 100644 --- a/crates/latkit/src/data/describe.rs +++ b/crates/latkit/src/data/describe.rs @@ -1,5 +1,5 @@ -//! Reading data back without moving it: what a dataset holds, and a sampled field's extremes over -//! spans of its axis. +//! Summaries of a dataset: a [`Description`] of its tables and fields, and an [`Envelope`] of each +//! row's least and greatest value of a sampled field over spans of the axis. use super::column::{PAGE_BITS, PAGE_MASK}; use super::ids::Ids; @@ -9,70 +9,73 @@ use super::table::{Field, Table}; use super::values::Kind; use crate::{Error, Result}; -/// The most values an envelope holds, least and greatest each: rows times spans. +/// The largest envelope allowed, as rows times spans. `least` and `greatest` each hold this many +/// values at most. const ENVELOPE_VALUES: usize = 1 << 26; -/// What a dataset holds: its tables, their rows and fields, and the axis its sampled fields are -/// recorded over and span. +/// A summary of a dataset: its tables with their rows and fields, the axis sampled fields are +/// recorded over, and the part of the axis they cover. #[derive(Clone, PartialEq, Debug, serde::Serialize, schemars::JsonSchema)] pub struct Description { /// The dataset's name. pub name: Name, - /// The axis's name and unit. + /// The axis name and unit, if set. pub axis: Option<[Name; 2]>, - /// The first and last coordinates its sampled fields hold. + /// The first and last axis coordinates held by any sampled field; `None` when none holds any. pub span: Option<[f64; 2]>, - /// Its tables, by name. + /// Its tables, sorted by name. pub tables: Vec, } -/// A table described: its rows, and its fields by name. +/// A summary of one table: its row count and its fields, sorted by name. #[derive(Clone, PartialEq, Debug, serde::Serialize, schemars::JsonSchema)] pub struct TableDescription { /// The table's name. pub name: Name, - /// How many rows it has. + /// The number of rows. pub rows: usize, - /// Its fields, by name. + /// Its fields, sorted by name. pub fields: Vec, } -/// A field described: what it holds, the table a reference names, the coordinates samples span. +/// A summary of one field: its kind, plus its target table or axis span where those apply. #[derive(Clone, PartialEq, Debug, serde::Serialize, schemars::JsonSchema)] pub struct FieldDescription { /// The field's name. pub name: Name, - /// What it holds. + /// The kind of values it holds. pub kind: Kind, - /// The table a reference names. + /// For a reference, the target table. #[serde(skip_serializing_if = "Option::is_none")] pub to: Option, - /// The first and last coordinates a sampled field holds. + /// For a sampled field, the first and last axis coordinates it holds. #[serde(skip_serializing_if = "Option::is_none")] pub span: Option<[f64; 2]>, } -/// Rows' least and greatest values of a sampled field over equal spans of the axis, to read back: -/// field `field` of table `table` over `spans` equal spans from `range[0]` to `range[1]`, of every -/// row or of the rows `ids` names, in its order. Plain data, like a query. +/// A request for each row's least and greatest value of a sampled field over equal spans of the +/// axis: field `field` of table `table`, over `spans` equal spans from `range[0]` to `range[1]`, +/// for every row or for the rows `ids` names, in that order. Plain data that deserializes from +/// JSON, like a query. #[derive(Clone, PartialEq, Debug, serde::Deserialize, schemars::JsonSchema)] #[serde(deny_unknown_fields)] pub struct EnvelopeRequest { - /// The table read. + /// The table to read. pub table: Name, - /// The sampled field read. + /// The sampled field to read. pub field: Name, - /// The axis coordinates the spans cover, from the first to the last. + /// The axis range the spans cover, `[first, last]`: finite, with `first <= last`. pub range: [f64; 2], - /// How many equal spans the range is cut into. + /// The number of equal spans to divide the range into; at least 1. pub spans: usize, - /// The rows read, in this order; every row unless set. + /// The rows to read, in this order; every row when unset. #[serde(default)] pub ids: Option, } impl Snapshot { - /// What the dataset holds: tables, rows, fields and their kinds, the axis and its span. + /// A summary of the dataset: its tables, rows, fields and their kinds, and the axis and its + /// span. pub fn describe(&self) -> Description { Description { name: self.name().clone(), @@ -83,10 +86,16 @@ impl Snapshot { } /// Each row's least and greatest value of a sampled field over equal spans of the axis, as - /// `request` asks: row after row, NaN where a span holds no frame. A span reads the coarsest - /// levels of detail the field holds that fit inside it, so it costs a few reads, however many - /// frames it covers; where the field holds only coarser ones, as time tiles still loading leave - /// it, those stand in. + /// `request` describes. Values are laid out row by row, with NaN where a span holds no frame. + /// + /// Each span reads the coarsest stored levels of detail that fit inside it, so it costs a few + /// reads regardless of how many frames it covers. Where only coarser levels are present, as + /// while time tiles are loading, those are used instead and may include frames just outside + /// the span. + /// + /// Errors if the table, the field, or a requested row does not exist, the field is not + /// sampled, the range is not finite and ordered, `spans` is 0, or rows times spans exceeds + /// 2^26. /// /// ``` /// # use latkit::{Data, EnvelopeRequest, Ids, Patch, Table}; @@ -106,7 +115,7 @@ impl Snapshot { } impl Table { - /// What the table holds: its rows, and its fields' kinds, targets, and spans. + /// A summary of the table: its row count and each field's kind, reference target, and span. pub fn describe(&self) -> TableDescription { let fields = self.fields.iter().map(|(name, field)| FieldDescription { name: name.clone(), @@ -124,13 +133,13 @@ impl Table { } /// Each row's least and greatest value of sampled field `field` over `spans` equal spans of the - /// axis from `range[0]` to `range[1]`: see [`Snapshot::envelope`]. + /// axis from `range[0]` to `range[1]`. See [`Snapshot::envelope`]. pub(crate) fn envelope(&self, field: &str, range: [f64; 2], spans: usize, ids: Option<&Ids>) -> Result { let Some(Field::Sampled(samples, _)) = self.field_named(field) else { return Err(Error::invalid("not a sampled field").at(field)); }; if !(range[0].is_finite() && range[1].is_finite() && range[0] <= range[1]) || spans == 0 { - return Err(Error::invalid("an envelope spans a finite range, in one span or more")); + return Err(Error::invalid("an envelope needs a finite, ordered range and at least one span")); } let rows: Vec = match ids { None => (0..self.len()).collect(), @@ -140,18 +149,19 @@ impl Table { }; if rows.len().saturating_mul(spans) > ENVELOPE_VALUES { return Err(Error::Limit( - format!("an envelope holds at most {ENVELOPE_VALUES} values: rows times spans").into(), + format!("an envelope holds at most {ENVELOPE_VALUES} values (rows times spans)").into(), )); } let width = (range[1] - range[0]) / spans as f64; let at: Vec = (0..=spans).map(|s| if s == spans { range[1] } else { range[0] + width * s as f64 }).collect(); - // A span holds the frames from its start up to the next span's; the last holds its end too. + // Each span covers frames from its start up to the next span's start; the last span also + // includes its end. let bounds: Vec = at.iter().enumerate().map(|(s, &t)| samples.bound(t, s == spans)).collect(); let pieces: Vec> = bounds.windows(2).map(|b| samples.pieces(b[0]..b[1])).collect(); let mut least = vec![f32::NAN; rows.len() * spans]; let mut greatest = least.clone(); - // Rows by block, so each piece's page is read once for all of a block's rows. + // Group rows by row block, so each piece's page is read once for all of a block's rows. let mut by_block: Vec<(usize, usize)> = rows.iter().enumerate().map(|(i, &row)| (row >> PAGE_BITS, i)).collect(); by_block.sort_unstable(); @@ -174,13 +184,16 @@ impl Table { } } -/// Rows' least and greatest values of a sampled field over spans of its axis. +/// Each row's least and greatest value of a sampled field over spans of the axis, as +/// [`Snapshot::envelope`] returns them. #[derive(Clone, PartialEq, Debug, serde::Serialize)] pub struct Envelope { - /// Where each span starts, then where the last ends: one more coordinate than spans. + /// Each span's start coordinate, then the last span's end: one more value than spans. pub at: Vec, - /// Each row's least value in each span, row after row: NaN where it has none. + /// The least value of each requested row in each span, row-major (`row * spans + span`); NaN + /// where the span has no value. pub least: Vec, - /// Each row's greatest value in each span, row after row: NaN where it has none. + /// The greatest value of each requested row in each span, row-major (`row * spans + span`); + /// NaN where the span has no value. pub greatest: Vec, } diff --git a/crates/latkit/src/data/forms.rs b/crates/latkit/src/data/forms.rs index ef1c26c0..a9850286 100644 --- a/crates/latkit/src/data/forms.rs +++ b/crates/latkit/src/data/forms.rs @@ -1,6 +1,6 @@ -//! What JSON writes for data, decoded once: a patch's changes, a table's fields, values, and ids. A -//! slice of a file is the one form read later, by [`Patch::read`] through a host; everything else is -//! decoded as it is read. JavaScript writes the byte frames instead. +//! Serde forms for data written as JSON: a patch's changes, a table's fields, values, and ids. +//! Everything decodes in one pass except a slice of a binary file, which [`Patch::read`] fetches +//! through a host afterwards. JavaScript sends the binary frames instead of JSON. use std::fmt; use std::sync::Arc; @@ -18,12 +18,12 @@ use super::table::Table; use super::values::{Value, Values}; use crate::{Error, Result}; -/// The largest whole number a number names an id by when written with a fraction: 2^53 − 1. +/// The largest float accepted as a numeric id, 2^53 − 1. Larger floats may already be rounded. const SAFE: f64 = 9_007_199_254_740_991.0; -const NUMERIC_ID: &str = "a numeric id is a whole number from 0 to 2^53 - 1"; +const NUMERIC_ID: &str = "a numeric id must be a whole number from 0 to 2^53 - 1"; -/// A dataset: its name and the patch that builds it. +/// A dataset's JSON document: its name and the patch that builds it. #[derive(Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub(crate) struct DataForm { @@ -32,23 +32,23 @@ pub(crate) struct DataForm { pub(crate) patch: Vec, } -/// One change of a patch, named by the method that makes it. +/// One change in a patch, keyed by the name of the `Patch` method that makes it. #[derive(Deserialize, JsonSchema)] #[serde(rename_all = "lowercase", deny_unknown_fields)] #[schemars(rename = "Change")] pub(crate) enum OpForm { - /// Names the axis sampled fields are recorded over, such as `time` in `s`. + /// Sets the axis that sampled fields are recorded over, such as `time` in `s`. Axis { name: String, unit: String }, /// Replaces a table with these rows and fields, or adds it. Replace(TableForm), - /// Writes fields for these rows: rows with known ids change, others are added, and fields left - /// out keep their values. + /// Writes fields for these rows. Rows with existing ids are updated, new ids add rows, and + /// fields left out keep their values. A missing table is added. Set(TableForm), - /// Removes the rows with these ids. + /// Removes the rows with these ids; ids that match no row are ignored. Remove { table: String, ids: IdsForm }, - /// Records a frame at each axis coordinate of `at`: for each field, a value per row at every - /// coordinate, frame-major; of every row, or of the rows `ids` names, the rest holding their last - /// values. + /// Records a frame at each axis coordinate in `at`. Each field gives one value per row per + /// coordinate, frame-major. Covers every row, or only the rows `ids` names; other rows keep + /// their last values. Append { at: At, table: String, @@ -58,10 +58,11 @@ pub(crate) enum OpForm { }, /// Forgets every sampled field's frames before axis coordinate `before`. Forget { before: f64 }, - /// Holds a run of sampled field `field`'s entries of level `level`, numbered from `first`: at - /// level 0 frames, each a coordinate in `at` and a value per row in `values`; at level `k` buckets - /// of `8^k` frames, each its first and last frame's coordinates, and each row's least value over - /// each bucket, then each row's greatest. Null is missing. + /// Inserts a run of entries of sampled field `field` at level `level`, numbered from `first`, + /// as time tiles store them. At level 0 each entry is a frame: one coordinate in `at` and one + /// value per row in `values`. At level `k` each entry is a bucket of `8^k` frames: its first + /// and last frame's coordinates in `at`, and in `values` every bucket's least value per row, + /// followed by every bucket's greatest. Null is missing. Frames { table: String, field: String, @@ -103,7 +104,7 @@ impl JsonSchema for super::source::Data { } } -/// A patch's JSON: its changes in order. +/// A patch's JSON form: its changes, in order. impl JsonSchema for Patch { fn schema_name() -> Cow<'static, str> { "Patch".into() @@ -132,7 +133,7 @@ impl JsonSchema for IdsForm { fn json_schema(generator: &mut SchemaGenerator) -> Schema { let id = generator.subschema_for::>(); json_schema!({ - "description": "Row ids: numbers or texts (null leaves a reference unwired), or `{count}` naming rows 0..count.", + "description": "Row ids: numbers or texts, or `{count}` for rows 0 to count - 1. In a reference, null names no row.", "anyOf": [ { "type": "array", "items": id }, { @@ -208,7 +209,7 @@ impl JsonSchema for Geometry { fn json_schema(generator: &mut SchemaGenerator) -> Schema { let geometry = generator.subschema_for::(); json_schema!({ - "description": "A point `[x, y]` (a third coordinate is read past), or a list of geometries one level shallower: a path, its parts, or polygons.", + "description": "A point `[x, y]` (a third coordinate is ignored), or a list of geometries one level down: a path is a list of points, parts a list of paths, and polygons a list of parts.", "anyOf": [ { "type": "array", "prefixItems": [{ "type": "number" }, { "type": "number" }], "items": { "type": "number" }, "minItems": 2, "maxItems": 3 }, { "type": "array", "items": geometry }, @@ -217,22 +218,22 @@ impl JsonSchema for Geometry { } } -/// A patch deserializes from its JSON: every change but a slice of a file, which [`Patch::read`] -/// reads. +/// Deserializes a patch from its JSON form. A field given as a slice of a binary file is an error +/// here; [`Patch::read`] fetches those. impl<'de> Deserialize<'de> for Patch { fn deserialize>(deserializer: D) -> std::result::Result { patch(Vec::::deserialize(deserializer)?).map_err(de::Error::custom) } } -/// Ids deserialize from a list of numbers and texts, or `{count}`. +/// Deserializes ids from a list of numbers or texts, or from `{count}`. impl<'de> Deserialize<'de> for Ids { fn deserialize>(deserializer: D) -> std::result::Result { IdsForm::deserialize(deserializer).map(|form| form.0) } } -/// An id is a whole number from 0, or a text. +/// An id's schema: a non-negative whole number, or a text. impl schemars::JsonSchema for Id { fn schema_name() -> std::borrow::Cow<'static, str> { "Id".into() @@ -246,15 +247,16 @@ impl schemars::JsonSchema for Id { } } -/// An id deserializes from a whole number from 0 to 2^53 − 1, or a text. +/// Deserializes an id from a non-negative whole number or a text. A number parsed as a float must +/// be at most 2^53 − 1. Null is an error. impl<'de> Deserialize<'de> for Id { fn deserialize>(deserializer: D) -> std::result::Result { - Key::deserialize(deserializer)?.0.ok_or_else(|| de::Error::custom("an id is a whole number or a text")) + Key::deserialize(deserializer)?.0.ok_or_else(|| de::Error::custom("an id must be a whole number or a text")) } } impl OpForm { - /// The columns its fields are written as. + /// The field columns of a replace, set, or append; empty for other changes. pub(crate) fn columns(&mut self) -> &mut [(String, Column)] { match self { OpForm::Replace(TableForm { fields, .. }) @@ -265,7 +267,7 @@ impl OpForm { } } -/// The patch `ops` make, every slice of a file read already. +/// Builds the patch for `ops`. Every slice of a file must already be replaced by its values. pub(crate) fn patch(ops: Vec) -> Result { let mut patch = Patch::new(); for (i, op) in ops.into_iter().enumerate() { @@ -320,7 +322,7 @@ impl TableForm { } } -/// A field's column as written: values, a reference, or a slice of a file. +/// A field's column as written in JSON: values, a reference, or a slice of a binary file. pub(crate) enum Column { Values(Values), Reference { to: String, ids: Ids }, @@ -336,25 +338,26 @@ pub(crate) struct Slice { } impl Slice { - /// The byte after the slice. + /// The offset just past the slice's last byte. Errors on overflow. pub(crate) fn end(&self) -> Result { let length = self.count.checked_mul(self.dtype.width()).ok_or_else(|| Error::invalid("the slice is too long"))?; - self.offset.checked_add(length).ok_or_else(|| Error::invalid("the slice ends past every file")) + self.offset.checked_add(length).ok_or_else(|| Error::invalid("the slice's end offset overflows")) } - /// The values its fetched `bytes` hold. + /// Decodes the slice's fetched `bytes`. Errors unless exactly the slice's length arrived. pub(crate) fn read(&self, bytes: &[u8]) -> Result { let length = self.end()? - self.offset; if bytes.len() as u64 != length { let file = &self.file; - return Err(Error::Io(format!("{file}: {} bytes of the slice's {length}", bytes.len()).into())); + return Err(Error::Io(format!("{file}: read {} of the slice's {length} bytes", bytes.len()).into())); } Ok(self.dtype.decode(bytes)) } } -/// How a slice's bytes hold values: little endian; `bool` a byte each, nonzero true. +/// The value type of a slice's bytes. Numbers are little endian; `bool` is one byte each, nonzero +/// meaning true. #[derive(Clone, Copy, Deserialize, JsonSchema)] enum Dtype { #[serde(rename = "f32le")] @@ -382,7 +385,7 @@ impl Dtype { } } - /// The values little-endian `bytes` hold, whole values only. + /// Decodes little-endian `bytes`, ignoring a trailing partial value. fn decode(self, bytes: &[u8]) -> Values { macro_rules! decode { ($t:ty, $n:literal) => { @@ -402,13 +405,13 @@ impl Dtype { } impl Column { - /// The values, read already: a slice of a file is read by [`Patch::read`]. + /// The column's values. Errors for a slice of a file, which only [`Patch::read`] can fetch. fn values(self) -> Result { match self { Column::Values(values) => Ok(values), - Column::Reference { .. } => unreachable!("references are fields of their own"), + Column::Reference { .. } => unreachable!("callers handle reference columns first"), Column::Slice(slice) => { - Err(Error::invalid(format!("{}: a slice of a file is read by Patch::read", slice.file))) + Err(Error::invalid(format!("{}: a slice of a file must be read with Patch::read", slice.file))) } } } @@ -430,7 +433,7 @@ impl<'de> Deserialize<'de> for Fields { let mut fields = Vec::new(); while let Some(name) = map.next_key::()? { if fields.iter().any(|(n, _)| *n == name) { - return Err(de::Error::custom(format!("field {name} is written twice"))); + return Err(de::Error::custom(format!("field {name} appears twice"))); } let column = map.next_value_seed(ColumnSeed(&name))?; fields.push((name, column)); @@ -442,7 +445,7 @@ impl<'de> Deserialize<'de> for Fields { } } -/// A column, named for its errors. +/// Deserializes a column, naming its field in errors. struct ColumnSeed<'a>(&'a str); impl<'de> DeserializeSeed<'de> for ColumnSeed<'_> { @@ -490,21 +493,21 @@ impl<'de> Visitor<'de> for ColumnVisitor<'_> { let missing = |what: &str| de::Error::custom(format!("field {}: {what}", self.0)); match (reference, file) { (Some(to), None) if dtype.is_none() && offset.is_none() && count.is_none() => { - Ok(Column::Reference { to, ids: ids.ok_or_else(|| missing("a reference names its ids"))? }) + Ok(Column::Reference { to, ids: ids.ok_or_else(|| missing("a reference needs ids"))? }) } (None, Some(file)) if ids.is_none() => Ok(Column::Slice(Slice { file, - dtype: dtype.ok_or_else(|| missing("a slice names its dtype"))?, + dtype: dtype.ok_or_else(|| missing("a slice needs a dtype"))?, offset: offset.unwrap_or(0), - count: count.ok_or_else(|| missing("a slice names its count"))?, + count: count.ok_or_else(|| missing("a slice needs a count"))?, })), - _ => Err(missing("an object is {reference, ids} or {file, dtype, offset, count}")), + _ => Err(missing("an object must be {reference, ids} or {file, dtype, offset, count}")), } } } -/// One value of an array column: a number, boolean, text, or `[x, y]` geometry; `null`, or an empty -/// geometry, missing. +/// One value of an array column: a number, boolean, text, or geometry of `[x, y]` points. `null` +/// and an empty geometry are missing. struct Element(Option); impl<'de> Deserialize<'de> for Element { @@ -551,7 +554,8 @@ enum Geometry { } impl Geometry { - /// How deep it nests below its points: 0 a point, 1 a path, 2 parts, 3 polygons. + /// Its nesting depth, following the first item at each level: 0 a point, 1 a path, 2 parts, + /// 3 polygons. fn depth(&self) -> usize { match self { Geometry::Point(_) => 0, @@ -573,7 +577,8 @@ impl Geometry { } } - /// The value it writes: `None` when it is empty. + /// The geometry as a [`Value`]; `None` when it is empty. Errors when nested deeper than + /// polygons. fn value(self) -> std::result::Result, &'static str> { if matches!(&self, Geometry::List(items) if items.is_empty()) { return Ok(None); @@ -583,7 +588,7 @@ impl Geometry { 1 => Value::Path(self.list(Geometry::point)), 2 => Value::Parts(self.list(|path| path.list(Geometry::point))), 3 => Value::Polygons(self.list(|parts| parts.list(|path| path.list(Geometry::point)))), - _ => return Err("geometries nest at most polygons of rings of [x, y] points"), + _ => return Err("geometry nests deeper than polygons of rings of [x, y] points"), })) } } @@ -600,7 +605,7 @@ impl<'de> Visitor<'de> for GeometryVisitor { match first { Part::Number(x) => { let y: f64 = seq.next_element()?.ok_or_else(|| de::Error::custom("a point is [x, y]"))?; - // A third coordinate, elevation in GeoJSON, is read past. + // Skip a third coordinate (GeoJSON elevation) and any after it. while seq.next_element::()?.is_some() {} Ok(Geometry::Point([x, y])) } @@ -655,7 +660,8 @@ impl<'de> Deserialize<'de> for Part { } } -/// Ids as written: numbers or texts with `null` for none, as [`Ids`] takes them, or `{count}`. +/// Ids as written in JSON: a list of numbers or texts with `null` for none, as [`Ids`] takes them, +/// or `{count}`. pub(crate) struct IdsForm(pub(crate) Ids); impl<'de> Deserialize<'de> for IdsForm { @@ -674,7 +680,7 @@ impl<'de> Deserialize<'de> for IdsForm { other => return Err(de::Error::custom(format!("ids: unknown key {other}"))), } } - count.map(|n| IdsForm(Ids::count(n))).ok_or_else(|| de::Error::custom("ids as a count are {count}")) + count.map(|n| IdsForm(Ids::count(n))).ok_or_else(|| de::Error::custom("an ids object must be {count}")) } fn visit_seq>(self, mut seq: A) -> std::result::Result { let mut ids = Vec::with_capacity(seq.size_hint().unwrap_or(0)); @@ -688,7 +694,7 @@ impl<'de> Deserialize<'de> for IdsForm { } } -/// One id as written. +/// One id as written in JSON; `null` is `None`. struct Key(Option); impl<'de> Deserialize<'de> for Key { @@ -732,7 +738,7 @@ mod tests { use crate::{Data, Id, Ids, Patch, Value}; #[test] - fn patches_decode_as_written() { + fn patches_decode_from_json() { let ops = serde_json::json!([ {"replace": {"table": "Bus", "ids": [7, 9], "fields": { "kv": [1.5, 2.5], @@ -753,12 +759,12 @@ mod tests { } #[test] - fn forms_refuse_what_they_cannot_read_now() { + fn forms_refuse_file_slices_bare_counts_and_invalid_ids() { let slice = serde_json::json!([{"replace": {"table": "Bus", "ids": {"count": 1}, "fields": { "kv": {"file": "kv.f64", "dtype": "f64le", "count": 1}}}}]); let error = serde_json::from_value::(slice).expect_err("a slice needs Patch::read").to_string(); assert!(error.contains("Patch::read"), "{error}"); - assert!(serde_json::from_value::(serde_json::json!(3)).is_err(), "a bare number is no count"); + assert!(serde_json::from_value::(serde_json::json!(3)).is_err(), "a bare number is not a count"); assert_eq!(serde_json::from_value::(serde_json::json!({"count": 3})).expect("a count").len(), 3); assert_eq!(serde_json::from_value::(serde_json::json!(3)).expect("an id"), Id::Number(3)); assert!(serde_json::from_value::(serde_json::json!(1.5)).is_err()); diff --git a/crates/latkit/src/data/frames.rs b/crates/latkit/src/data/frames.rs index c7e9d2d2..d106b974 100644 --- a/crates/latkit/src/data/frames.rs +++ b/crates/latkit/src/data/frames.rs @@ -1,15 +1,17 @@ -//! Time tiles: a dataset written as the files a view fetches its sampled fields' histories from, in -//! spans, as it asks for them. A span of level `k` holds `chunk` entries of the level: frames at level -//! 0, else buckets of `8^k` frames. Eight spans make the one a level up, as four map tiles make the -//! tile a zoom out, so a span still loading draws the nearest span above it that has landed. +//! Time tiles: a dataset written as files from which a view fetches its sampled fields' histories +//! in spans, on demand. A span of level `k` holds `chunk` entries of that level: frames at level 0, +//! buckets of `8^k` frames above. One span covers the same frames as eight spans of the level +//! below, as one map tile covers four tiles one zoom level in. While a span loads, the view draws +//! the nearest coarser span that has arrived. //! -//! | File | What it holds | -//! | ------------- | ------------------------------------------------------------------------------- | -//! | `frames.json` | the manifest: `data`, `chunk`, `url`, and `frames`, each sampled field's table, | -//! | | name, and the frames it reads, `[first, end]` | -//! | `base.latk` | the dataset with every sampled field reading its frames but holding none | -//! | spans | at `url`, `{field}` the field's place in `frames`: a patch holding the span's | -//! | | entries, one `frames` change for each run of them | +//! | File | Contents | +//! | ------------- | ---------------------------------------------------------------------------- | +//! | `frames.json` | the manifest: `data`, `chunk`, `url`, and `frames`, which lists each sampled | +//! | | field's `table`, `field`, `levels` (highest level stored), and `frames` | +//! | | (`[first, end]`) | +//! | `base.latk` | the dataset with each sampled field's frame range but none of its entries | +//! | spans | at `url`, with `{field}` the field's index in `frames`: a patch with one | +//! | | `frames` change per run of the span's entries | use std::ops::Range; @@ -20,14 +22,15 @@ use super::snapshot::Snapshot; use super::table::Field; use crate::{Error, Result}; -/// The manifest's name. +/// The manifest's file name. pub(crate) const MANIFEST: &str = "frames.json"; -/// The dataset without its sampled fields' entries. +/// The file name of the dataset without its sampled fields' entries. const BASE: &str = "base.latk"; -/// Where each span lies, beside the manifest. +/// The path template of each span, relative to the manifest. const URL: &str = "{field}/{level}/{index}.latk"; -/// A sampled field a manifest lists: its table and name, and the frames it reads, `[first, end]`. +/// A sampled field listed in a manifest: its table and name, its highest stored level, and the +/// frames it covers, written `[first, end]`. #[derive(Clone, PartialEq, Debug, serde::Serialize, serde::Deserialize)] pub(crate) struct Listed { pub(crate) table: Name, @@ -37,8 +40,8 @@ pub(crate) struct Listed { pub(crate) frames: Range, } -/// What a manifest says: where the dataset without its sampled fields' entries lies, the entries a -/// span holds, where each span lies, and the fields listed. +/// The contents of `frames.json`: the path of the base dataset (`data`), the entries per span +/// (`chunk`), the span path template (`url`), and the listed fields. #[derive(Clone, PartialEq, Debug, serde::Serialize, serde::Deserialize)] pub(crate) struct Manifest { pub(crate) data: String, @@ -48,7 +51,7 @@ pub(crate) struct Manifest { pub(crate) fields: Vec, } -/// Frames `first..end`, written `[first, end]`. +/// Serializes frames `first..end` as `[first, end]`. mod range { use std::ops::Range; @@ -61,15 +64,16 @@ mod range { pub(super) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { match <[u64; 2]>::deserialize(d)? { [first, end] if first <= end => Ok(first..end), - _ => Err(de::Error::custom("frames are [first, end], the first at most the end")), + _ => Err(de::Error::custom("frames must be [first, end] with first at most end")), } } } impl Manifest { - /// The manifest `bytes` hold; `None` where they hold none, being no JSON object listing `frames`. + /// Parses a manifest. Returns `None` when `bytes` are not a JSON object with a `frames` list, + /// so other tile formats can be tried, and an error when they are one but invalid. pub(crate) fn from_bytes(bytes: &[u8]) -> Option> { - /// What any manifest holds, whatever else it does. + /// The key every manifest has, used to recognize one. #[derive(serde::Deserialize)] struct Listing { #[serde(rename = "frames")] @@ -79,11 +83,11 @@ impl Manifest { crate::from_json::(text).ok()?; let manifest = crate::from_json::(text).and_then(|m| { if !chunks(m.chunk) { - return Err(Error::invalid("a span holds a multiple of 8 entries, 8 to 2^20").at("chunk")); + return Err(Error::invalid("a span must hold a multiple of 8 entries, from 8 to 2^20").at("chunk")); } for (i, field) in m.fields.iter().enumerate() { if field.levels > super::samples::MAX_LEVEL { - return Err(Error::invalid("sample levels run from 0 to 21").at("levels").at(i).at("frames")); + return Err(Error::invalid("sample levels must be from 0 to 21").at("levels").at(i).at("frames")); } } Ok(m) @@ -91,7 +95,8 @@ impl Manifest { Some(manifest.map_err(|e| e.at(MANIFEST))) } - /// Where span `index` of `level` of the field listed `field`th lies, `url` its template. + /// The path of span `index` at `level` of the field at index `field` in the manifest, from + /// template `url`. pub(crate) fn span(url: &str, field: usize, level: u32, index: u64) -> String { url.replace("{field}", &field.to_string()) .replace("{level}", &level.to_string()) @@ -99,24 +104,29 @@ impl Manifest { } } -/// Whether a span may hold `chunk` entries: a multiple of 8, so a bucket a level up never straddles two. +/// Whether `chunk` is a valid number of entries per span: from 8 to 2^20 and a multiple of 8, so a +/// bucket one level up never straddles two spans. fn chunks(chunk: u32) -> bool { (8..=1 << 20).contains(&chunk) && chunk.is_multiple_of(8) } impl Snapshot { - /// The dataset as time tiles, each file's path and bytes handed to `write`: `frames.json`, the - /// manifest a view's tiles open; `base.latk`, the dataset with its sampled fields reading their - /// frames but holding none; and a patch for each span of `chunk` entries of each level of each - /// sampled field that holds any, at `{field}/{level}/{index}.latk`. A view given the manifest as - /// [`Tiles`](crate::Tiles) fetches the spans its lines draw, at the level of detail its plot - /// window asks, and those about the coordinate it shows; until a span lands, the span above it - /// draws. + /// Writes the dataset as time tiles, passing each file's path and bytes to `write`: /// - /// The manifest is written last, so a reader finding it finds every file it names. + /// - `frames.json`, the manifest; + /// - `base.latk`, the dataset with each sampled field's frame range but none of its entries; + /// - `{field}/{level}/{index}.latk`, a patch for each span of `chunk` entries, for every level + /// of every sampled field that has entries there. + /// + /// A view given the manifest as [`Tiles`](crate::Tiles) fetches the spans its lines draw, at + /// the level of detail its plot window needs, and the frames around the coordinate it shows. + /// Until a span arrives, the coarser span above it is drawn. + /// + /// The manifest is written last, so a reader that finds it finds every file it names. Errors if + /// `chunk` is not a multiple of 8 from 8 to 2^20, or if `write` fails. pub fn to_tiles(&self, chunk: u32, mut write: impl FnMut(&str, &[u8]) -> Result<()>) -> Result<()> { if !chunks(chunk) { - return Err(Error::invalid("a span holds a multiple of 8 entries, 8 to 2^20").at("chunk")); + return Err(Error::invalid("a span must hold a multiple of 8 entries, from 8 to 2^20").at("chunk")); } let entries_per_span = u64::from(chunk); let mut base = self.clone(); @@ -132,7 +142,8 @@ impl Snapshot { levels: samples.levels(), frames: samples.start()..samples.end(), }); - // Runs come level by level, in order, so each span is whole once the next begins. + // Runs come level by level in entry order, so a span is complete once the next one + // begins. let mut span: Option<((u32, u64), Patch)> = None; for run in samples.runs() { for index in run.entries.start / entries_per_span..run.entries.end.div_ceil(entries_per_span) { @@ -177,7 +188,7 @@ mod tests { use crate::{Data, Ids, Table}; #[test] - fn a_history_writes_as_spans_a_manifest_names() { + fn a_history_writes_as_spans_listed_in_a_manifest() { let grid = Data::new("grid"); grid.apply( Patch::new().axis("time", "s").replace(Table::new("Bus", Ids::count(2)).field("kv", vec![1.0, 2.0])), @@ -195,7 +206,8 @@ mod tests { .expect("tiles"); let manifest = Manifest::from_bytes(&files[MANIFEST]).expect("a manifest").expect("valid"); assert_eq!(manifest.fields, [Listed { table: "Bus".into(), field: "vm".into(), levels: 2, frames: 0..100 }]); - // 100 frames: 7 spans of 16 at level 0; 12 buckets of 8 in 1 span at level 1; 1 of 64 at level 2. + // 100 frames: 7 spans of 16 at level 0; 12 buckets of 8 in 1 span at level 1; 1 bucket of + // 64 at level 2. let spans: Vec<&str> = files .keys() .map(String::as_str) @@ -205,10 +217,10 @@ mod tests { let back = Data::from(Snapshot::from_bytes(&files[BASE]).expect("the base")); assert_eq!(back.snapshot().describe().span, None, "the base holds no entries"); for path in &spans { - back.apply(Patch::from_bytes(&files[*path]).expect("a span")).expect("held once"); + back.apply(Patch::from_bytes(&files[*path]).expect("a span")).expect("each span applies once"); } - assert_eq!(back.snapshot().to_bytes(), data.to_bytes(), "every span put back makes the history again"); - assert!(Manifest::from_bytes(br#"{"tiles": []}"#).is_none(), "TileJSON is no manifest"); + assert_eq!(back.snapshot().to_bytes(), data.to_bytes(), "applying every span rebuilds the history"); + assert!(Manifest::from_bytes(br#"{"tiles": []}"#).is_none(), "TileJSON is not a manifest"); let backwards = br#"{"data": "b", "chunk": 16, "url": "u", "frames": [{"table": "t", "field": "f", "levels": 0, "frames": [3, 1]}]}"#; let error = Manifest::from_bytes(backwards).expect("a manifest").unwrap_err(); diff --git a/crates/latkit/src/data/geometry.rs b/crates/latkit/src/data/geometry.rs index aeba63d4..59f811ad 100644 --- a/crates/latkit/src/data/geometry.rs +++ b/crates/latkit/src/data/geometry.rs @@ -5,9 +5,10 @@ use super::values::Value; /// Shared, immutable geometries of one depth: nested offsets over `[x, y]` coordinates. /// -/// Depth 0 is a point per row, 1 a path, 2 parts (rings of a polygon, or the paths of a multi-path), -/// and 3 polygons. `levels[0]` is indexed by row; each level's offsets index the next level, and the -/// last level indexes `coords`. Offsets are absolute, so slicing rows slices only `levels[0]`. +/// Depth 0 is a point per row, 1 a path, 2 parts (rings of a polygon, or the paths of a +/// multi-path), and 3 polygons. `levels[0]` is indexed by row; each level's offsets index the next +/// level, and the last level indexes `coords`. Offsets are absolute, so slicing rows slices only +/// `levels[0]`. #[derive(Clone)] pub(crate) struct Geometry { depth: u8, @@ -42,7 +43,8 @@ impl Geometry { Geometry { depth: self.depth, levels, coords } } - /// The span `item` of `level` covers in the next level, or in `coords` from the last level. + /// The range of the next level (or of `coords`, from the last level) that `item` of `level` + /// covers. pub(crate) fn span(&self, level: usize, item: usize) -> Range { let offsets = &self.levels[level]; offsets[item] as usize..offsets[item + 1] as usize @@ -52,7 +54,7 @@ impl Geometry { &self.coords } - /// How many coordinates its rows span: of a slice, its own rows' only. + /// The number of coordinates its rows cover; for a slice, only its own rows'. pub(crate) fn points(&self) -> usize { let Some(rows) = self.levels.first() else { return self.coords.len() }; let mut span = rows[0] as usize..rows[rows.len() - 1] as usize; @@ -62,8 +64,8 @@ impl Geometry { span.len() } - /// Whether row `row` holds exactly what `other`'s row `other_row` does: the same parts of the same - /// coordinates, compared by their bits. + /// Whether row `row` equals `other`'s row `other_row`: the same part structure and + /// bit-identical coordinates. pub(crate) fn same_row(&self, row: usize, other: &Geometry, other_row: usize) -> bool { fn same(a: &[[f64; 2]], b: &[[f64; 2]]) -> bool { a.len() == b.len() && a.iter().zip(b).all(|(p, q)| p.map(f64::to_bits) == q.map(f64::to_bits)) @@ -87,7 +89,7 @@ impl Geometry { walk(self, other, 0, row, other_row) } - /// Row `row`'s coordinates, every part's in order. + /// Row `row`'s coordinates, all parts in order. pub(crate) fn row_coords(&self, row: usize) -> &[[f64; 2]] { if self.depth == 0 { return &self.coords[row..=row]; @@ -99,7 +101,7 @@ impl Geometry { &self.coords[span] } - /// A digest of row `row`: its coordinates and how they break into parts. + /// A hash of row `row`: its coordinates and how they split into parts. pub(crate) fn row_hash(&self, row: usize) -> u64 { use std::hash::Hasher; let mut hasher = rustc_hash::FxHasher::default(); @@ -119,13 +121,13 @@ impl Geometry { hasher.finish() } - /// How many points row `row`'s path takes as shaders read it: every coordinate, and a break - /// between its parts or rings. + /// The number of points shaders read for row `row`: every coordinate, plus one break between + /// consecutive parts or rings. pub(crate) fn row_points(&self, row: usize) -> usize { if self.depth == 0 { return 1; } - // The row's parts or rings: the items of the level above its coordinates. + // Find the row's parts or rings: its items at the level just above the coordinates. let mut span = row..row + 1; for offsets in &self.levels[..self.levels.len() - 1] { span = offsets[span.start] as usize..offsets[span.end] as usize; @@ -147,7 +149,8 @@ impl Geometry { } } - /// Offsets relative to each level's first entry and the coordinates the rows span. + /// Each level's offsets rebased to start at 0, and the coordinates the rows cover, for encoding + /// a slice. pub(crate) fn compact(&self) -> (Vec>, Vec<[f64; 2]>) { let mut builder = GeometryBuilder::new(self.depth); for row in 0..self.len() { @@ -202,14 +205,14 @@ impl GeometryBuilder { } } - /// Rows `rows` of `source`, in order: each level's offsets and the coordinates copied whole. + /// Appends rows `rows` of `source`, copying each level's offsets and the coordinates in bulk. pub(crate) fn push_rows(&mut self, source: &Geometry, rows: Range) { debug_assert_eq!(source.depth, self.depth); let mut span = rows; for level in 0..self.levels.len() { let offsets = &source.levels[level]; let (first, last) = (offsets[span.start], offsets[span.end]); - // Items of the next level pushed so far, which this run's items follow. + // Items already pushed at the next level; this run's offsets continue from there. let next = match self.levels.get(level + 1) { Some(below) => below.len() - 1, None => self.coords.len(), @@ -233,7 +236,7 @@ impl GeometryBuilder { self.close(level); } - /// An empty row: no coordinates, or a missing point. + /// Appends an empty row: no coordinates, or a NaN point at depth 0. pub(crate) fn push_empty(&mut self) { if self.depth == 0 { self.coords.push([f64::NAN; 2]); @@ -317,7 +320,7 @@ mod tests { } #[test] - fn runs_of_rows_copy_as_rows_do() { + fn copying_a_run_matches_copying_each_row() { let geometry = polygons(); let (mut one, mut runs) = (GeometryBuilder::new(3), GeometryBuilder::new(3)); for row in [0, 1, 2, 1, 2] { @@ -333,7 +336,7 @@ mod tests { let tail = geometry.slice(1..3); let mut sliced = GeometryBuilder::new(3); sliced.push_rows(&tail, 0..2); - assert_eq!(sliced.finish().get(1), geometry.get(2), "a slice's rows, past its first offsets"); + assert_eq!(sliced.finish().get(1), geometry.get(2), "a slice's offsets need not start at 0"); } #[test] diff --git a/crates/latkit/src/data/ids.rs b/crates/latkit/src/data/ids.rs index b27f7698..4d303a25 100644 --- a/crates/latkit/src/data/ids.rs +++ b/crates/latkit/src/data/ids.rs @@ -10,11 +10,12 @@ use super::name::{Id, IdRef}; use super::values::{Array, Values}; use crate::{Error, Result}; -/// Row ids, as given to [`Table::new`](crate::Table::new) or [`Patch::remove`](crate::Patch::remove), -/// and the target ids of a reference. +/// Row ids, as given to [`Table::new`](crate::Table::new) or +/// [`Patch::remove`](crate::Patch::remove), or the target ids of a reference. /// /// [`Ids::count`] names rows `0..n`. Lists convert from vectors, arrays, and slices of numbers or -/// texts; for a reference, `None` leaves a row unwired. A bare number is never a count. +/// texts; for a reference, `None` leaves a row unwired. A bare number is never a count: use +/// [`Ids::count`], or `{"count": n}` in JSON. /// /// ``` /// # use latkit::{Ids, Table}; @@ -30,7 +31,7 @@ pub(crate) enum IdList { Listed(Values), } -/// Ids are equal when they name the same rows in the same order. +/// Ids are equal when they list the same ids in the same order, whether counted or listed. impl PartialEq for Ids { fn eq(&self, other: &Ids) -> bool { self.len() == other.len() && (0..self.len()).all(|i| self.key(i) == other.key(i)) @@ -183,8 +184,8 @@ impl RowIds { self.key(row).map(IdRef::to_id) } - /// The ids with `added` appended; a count stays a count while the new ids continue it. Ids of - /// another kind than the table's (texts for a table of numbers) are an error. + /// The ids with `added` appended. A count stays a count when the new ids continue it. Errors + /// if `added` holds another kind of id than the table's, such as texts for a table of numbers. pub(crate) fn extend(&self, added: &[Id]) -> Result { if added.is_empty() { return Ok(self.clone()); @@ -197,7 +198,7 @@ impl RowIds { let column = self.listed(); let IdList::Listed(source) = Ids::from(added.to_vec()).0 else { unreachable!("a list of ids") }; if source.kind() != column.kind() { - return Err(Error::invalid("ids of another kind than the table's: numbers and texts do not mix")); + return Err(Error::invalid("ids must be the same kind as the table's ids (numbers or texts)")); } let len = column.len() + added.len(); let writes: Vec<(usize, usize)> = (0..added.len()).map(|k| (column.len() + k, k)).collect(); @@ -222,8 +223,8 @@ impl RowIds { } } - /// Every page's numbered ids, when every id is a number: what lookups read without matching each - /// id's kind. + /// Each page's `u64` ids, when every page stores numbers with none missing. Lookups use them to + /// read ids without matching each one's kind. pub(crate) fn numbers(&self) -> Option> { let RowIds::Listed(column) = self else { return None }; column @@ -236,7 +237,7 @@ impl RowIds { .collect() } - /// The first row without an id, which no table may have. + /// The first row with a missing id. Tables must not have one. pub(crate) fn hole(&self) -> Option { let RowIds::Listed(column) = self else { return None }; let mut start = 0; @@ -251,7 +252,8 @@ impl RowIds { None } - /// The id → row index, past holes; an error names a repeated id. + /// Builds the id-to-row index, skipping rows without ids. Errors on a repeated id, naming its + /// row. pub(crate) fn index(&self) -> Result { let RowIds::Listed(_) = self else { return Ok(Index::Identity(self.len() as u32)) }; let mut table = HashTable::with_capacity(self.len()); @@ -259,8 +261,8 @@ impl RowIds { Ok(Index::Table(table)) } - /// Indexes rows `rows` into `table`, past holes; an error names a repeated id. Numbered ids are - /// read page by page, without matching each id's kind. + /// Adds rows `rows` to `table`, skipping rows without ids. Errors on a repeated id. When every + /// id is a number, ids are read page by page without matching each one's kind. fn insert(&self, table: &mut HashTable, rows: Range) -> Result<()> { if let Some(pages) = self.numbers() { let id = |r: u32| pages[r as usize >> PAGE_BITS][r as usize & PAGE_MASK]; @@ -290,7 +292,8 @@ impl RowIds { } } -/// Finds a row by id without allocating. +/// Finds a row by id without allocating: the identity for counted ids, else a hash table of row +/// numbers. #[derive(Debug)] pub(crate) enum Index { Identity(u32), @@ -302,8 +305,9 @@ pub(crate) fn hash(key: IdRef<'_>) -> u64 { } impl Index { - /// The index of `ids`, which hold the ids this one indexes and others after them, from row - /// `from`: this one with the others added, nothing it holds hashed again. + /// The index of `ids`, which are this index's ids followed by new ones from row `from` on. + /// Copies this index and adds only the new rows, without rehashing the existing ones. Rebuilds + /// it when counted ids became a list. pub(crate) fn extended(&self, ids: &RowIds, from: usize) -> Result { match (self, ids) { (Index::Identity(_), RowIds::Count(n)) => Ok(Index::Identity(*n)), @@ -316,8 +320,9 @@ impl Index { } } - /// The index of `ids`, the ids this one indexes but those of the rows `doomed` names, the rest - /// moving up past them: this one without them, renumbered, nothing hashed again. + /// The index of `ids`, which are this index's ids without the rows flagged in `doomed`. Drops + /// those rows and renumbers the rest to close the gaps, without rehashing. Rebuilds it for + /// counted ids. pub(crate) fn without(&self, doomed: &[bool], ids: &RowIds) -> Result { let (Index::Table(table), RowIds::Listed(_)) = (self, ids) else { return ids.index() }; let mut moved = Vec::with_capacity(doomed.len()); @@ -334,8 +339,9 @@ impl Index { Ok(Index::Table(table)) } - /// The rows numbered ids `ids` name, `unwired` where one names none or is missing, and whether a - /// present id named no row: the loop chosen once for the ids' and the rows' kinds. + /// The row number each id in `ids` names, or `unwired` where the id is missing or names no row, + /// plus whether any present id named no row. Chooses one loop for the ids' and rows' storage + /// up front instead of matching per id. pub(crate) fn resolve(&self, rows: &RowIds, ids: &Values, unwired: u32) -> (Vec, bool) { let valid = |i: usize| ids.valid.as_ref().is_none_or(|v| v.get(i)); let mut dangling = false; @@ -368,8 +374,8 @@ impl Index { (resolved, dangling) } - /// The row with id `key`. A numbered row is also named by its id in decimal text, as ids too - /// large for a JSON or JavaScript number are written. + /// The row with id `key`. A numeric id can also be given as its decimal text, which is how ids + /// too large for JSON or JavaScript numbers are written. pub(crate) fn get(&self, ids: &RowIds, key: IdRef<'_>) -> Option { let number = |text: &str| text.parse::().ok().and_then(|n| self.get(ids, IdRef::Number(n))); match (self, key) { @@ -397,7 +403,7 @@ mod tests { let index = ids.index().expect("unique"); assert_eq!(index.get(&ids, IdRef::Number(9)), Some(5)); assert_eq!(index.get(&ids, IdRef::Number(2)), Some(2)); - assert!(ids.extend(&[Id::from("a")]).is_err(), "texts do not join numbers"); + assert!(ids.extend(&[Id::from("a")]).is_err(), "text ids cannot extend numeric ids"); } #[test] @@ -408,8 +414,8 @@ mod tests { let extended = index.extended(&more, 3).expect("unique"); assert_eq!(extended.get(&more, IdRef::Number(50)), Some(4)); assert_eq!(extended.get(&more, IdRef::Number(20)), Some(1)); - assert!(index.extended(&more.extend(&[Id::Number(10)]).expect("numbers"), 5).is_err(), "an id twice"); - // Rows 1 and 3 go: the others move up past them. + assert!(index.extended(&more.extend(&[Id::Number(10)]).expect("numbers"), 5).is_err(), "repeated id"); + // Remove rows 1 and 3; the others move up to close the gaps. let doomed = [false, true, false, true, false]; let kept = more.gather(&[0, 2, 4]); let fewer = extended.without(&doomed, &kept).expect("unique"); diff --git a/crates/latkit/src/data/json.rs b/crates/latkit/src/data/json.rs index e878e82d..000b850c 100644 --- a/crates/latkit/src/data/json.rs +++ b/crates/latkit/src/data/json.rs @@ -1,6 +1,6 @@ -//! Reading datasets and patches: latkit's bytes, GeoJSON, and JSON, as their first bytes say. The -//! JSON format is documented on [`Patch::read`], its forms in `forms.rs`; slices of binary files are -//! fetched through a host. +//! Reading datasets and patches from latkit's binary format, GeoJSON, or JSON, detected from the +//! first bytes. The JSON format is documented on [`Patch::read`]; its serde forms are in +//! `forms.rs`. Slices of binary files are fetched through a host. use serde::Deserialize; @@ -13,24 +13,25 @@ use super::source::Data; use crate::resource::{Fetch, Request}; use crate::{Error, Result}; -/// How [`Data::read`] and [`Patch::read`] read what they are given: the name a dataset takes, and -/// where a JSON document lies, so its slices of files are fetched through `host` relative to it. +/// Options for [`Data::read`] and [`Patch::read`]: the dataset's name, and the document's location +/// and the host that fetches the file slices it references. #[derive(Clone, Copy, Default)] pub struct Reading<'a> { - /// The name the dataset takes: GeoJSON needs one; latkit's bytes and documents name their own - /// unless given one. + /// The dataset's name. Required for GeoJSON. latkit's bytes and JSON documents carry their own + /// name, which this overrides when set. pub name: Option<&'a str>, - /// The document's own path or URL: a slice's file is relative to it, or as it is where it is - /// empty or the file's is absolute. + /// The document's own path or URL. Slice file paths resolve in its directory, unless it is + /// empty or the file path is absolute. pub base: &'a str, - /// What fetches a document's slices of files. + /// Fetches the file slices a document references. Required when it references any. pub host: Option<&'a dyn Fetch>, } impl Patch { - /// The patch `bytes` hold: latkit's, one frame as [`Patch::to_bytes`] writes it, or its JSON, - /// what any language writes without latkit. The JSON is a list of the changes [`Patch`]'s methods - /// make, each named by its method with its parameters by name. + /// Reads a patch from `bytes`: either latkit's binary format, one frame as + /// [`Patch::to_bytes`] writes it, or JSON, which any language can write without latkit. The + /// JSON is a list of changes, each an object keyed by the [`Patch`] method that makes it, with + /// that method's parameters by name. /// /// ```json /// [ @@ -41,18 +42,23 @@ impl Patch { /// ] /// ``` /// - /// - `replace` and `set` take `{table, ids, fields}`, `remove` `{table, ids}`, `append` `{table, - /// at, fields, ids?}`, `forget` `{before}`, `axis` `{name, unit}`, and `frames` `{table, field, - /// level?, first, at, values}`, as [`Patch::frames`] takes them. - /// - `ids` is a count naming rows `0..n`, or numbers or texts; `null` leaves a reference unwired. - /// - A field is an array of one kind of value, as [`Values`](crate::Values) takes them; `{"reference": table, - /// "ids": ids}`; or a slice of a binary file, `{"file", "dtype", "offset", "count"}`: `count` - /// values from byte `offset` (0 unless given) of dtype `f32le`, `f64le`, `i32le`, `u32le`, - /// `u64le`, `u8` (numbers), or `bool` (a byte each, nonzero true). - /// - `append`'s `at` is one axis coordinate or several, as [`Patch::append`] takes them. - /// - An error names the path to what could not be read, such as `[2].set.fields.x`. + /// - `replace` and `set` take `{table, ids, fields}`; `remove` takes `{table, ids}`; `append` + /// takes `{table, at, fields, ids?}`; `forget` takes `{before}`; `axis` takes + /// `{name, unit}`; and `frames` takes `{table, field, level?, first, at, values}`, as + /// [`Patch::frames`] does. + /// - `ids` is `{"count": n}` for rows `0..n`, or a list of numbers or texts; `null` leaves a + /// reference unwired. + /// - A field is an array of one kind of value, as [`Values`](crate::Values) accepts; + /// `{"reference": table, "ids": ids}`; or a slice of a binary file, + /// `{"file", "dtype", "offset", "count"}`: `count` values starting at byte `offset` + /// (default 0), with dtype `f32le`, `f64le`, `i32le`, `u32le`, `u64le`, `u8` (all read as + /// numbers), or `bool` (one byte each, nonzero meaning true). + /// - `append`'s `at` is one axis coordinate or a list, as [`Patch::append`] takes. + /// - Errors name the path to the part that could not be read, such as `[2].set.fields.x`. /// - /// Slices are fetched through the reading's host, all at once, relative to its base. + /// Slices are fetched through `reading.host`, all requests issued together, with paths + /// relative to `reading.base`. Slices with no host are an + /// [`Error::Unsupported`](crate::Error::Unsupported). /// /// ```no_run /// # async fn example(host: &impl latkit::resource::Fetch) -> latkit::Result<()> { @@ -65,23 +71,23 @@ impl Patch { if bytes.starts_with(b"LATK") { return Patch::from_bytes(bytes); } - let ops: Vec = crate::from_json(text(bytes, "a patch is latkit's bytes or JSON")?)?; + let ops: Vec = crate::from_json(text(bytes, "a patch must be latkit's bytes or JSON")?)?; build(ops, reading).await } } impl Data { - /// The dataset `bytes` hold, as their first bytes say: + /// Reads a dataset from `bytes`, detecting the format from the first bytes: /// - /// - latkit's own, as [`Snapshot::to_bytes`] writes them; - /// - GeoJSON, gzip-compressed or not: its features become rows of tables `points`, `lines`, and - /// `polygons` by their geometry, each with its geometry as field `geometry` and its properties - /// as fields; - /// - or its JSON document, `{"name": ..., "patch": [...]}`: its name and the patch that builds it, - /// read as [`Patch::read`] reads one. + /// - latkit's binary format, as [`Snapshot::to_bytes`] writes it; + /// - GeoJSON, gzip-compressed or not: features become rows of tables `points`, `lines`, and + /// `polygons` by geometry type, with the geometry in field `geometry` and each property as a + /// field; + /// - a JSON document `{"name": ..., "patch": [...]}`: the dataset's name and the patch that + /// builds it, read as [`Patch::read`] reads one. /// - /// The reading's name names the dataset, as GeoJSON needs; latkit's bytes and documents name their - /// own unless given one. + /// `reading.name` names the dataset and is required for GeoJSON. The other formats carry their + /// own name, which `reading.name` overrides when set. /// /// ``` /// # use latkit::{Data, Reading}; @@ -97,13 +103,13 @@ impl Data { let read = if bytes.starts_with(b"LATK") { Snapshot::from_bytes(bytes)? } else if geojson(bytes) { - let name = reading.name.ok_or_else(|| Error::invalid("GeoJSON names its dataset").at("name"))?; + let name = reading.name.ok_or_else(|| Error::invalid("GeoJSON needs a dataset name").at("name"))?; return crate::geo::geojson(name, bytes).map(Data::from); } else { let form: DataForm = - crate::from_json(text(bytes, "a dataset is latkit's bytes, GeoJSON, or its JSON document")?)?; + crate::from_json(text(bytes, "a dataset must be latkit's bytes, GeoJSON, or a JSON document")?)?; if form.name.is_empty() { - return Err(Error::invalid("a dataset is named").at("name")); + return Err(Error::invalid("a dataset needs a name").at("name")); } let patch = build(form.patch, reading).await.map_err(|e| e.at("patch"))?; Snapshot::new(form.name).applied(patch)? @@ -115,8 +121,8 @@ impl Data { } } -/// Whether `bytes` hold GeoJSON rather than latkit's bytes or a dataset's document: gzip-compressed, -/// or a JSON object whose first member is not one of a document's, `name` or `patch`. +/// Whether `bytes` look like GeoJSON rather than latkit's binary format or a dataset document: +/// gzip-compressed, or a JSON object whose first key is neither `name` nor `patch`. pub(crate) fn geojson(bytes: &[u8]) -> bool { if bytes.starts_with(&[0x1f, 0x8b]) { return true; @@ -127,13 +133,18 @@ pub(crate) fn geojson(bytes: &[u8]) -> bool { key != b"name" && key != b"patch" } -/// `bytes` as UTF-8 text, or an error saying what they should be. +/// `bytes` as JSON text: UTF-8 that opens an object or an array. Anything else, such as an HTML +/// page served in place of a missing file, is an error stating the `expected` format. fn text<'a>(bytes: &'a [u8], expected: &'static str) -> Result<&'a str> { - std::str::from_utf8(bytes).map_err(|_| Error::invalid(expected)) + let text = std::str::from_utf8(bytes).map_err(|_| Error::invalid(expected))?; + match text.trim_start().as_bytes().first() { + Some(b'{' | b'[') => Ok(text), + _ => Err(Error::invalid(expected)), + } } -/// Patches by dataset name, in the order written: what one step of a stream of changes carries, as -/// JSON `{"grid": [...], "weather": [...]}`, each patch in [`Patch::read`]'s form. +/// Patches keyed by dataset name, in the order written, such as one message of a stream of +/// changes: JSON `{"grid": [...], "weather": [...]}`, each patch in [`Patch::read`]'s JSON form. #[derive(Default, Deserialize)] pub struct Patches(Ordered>); @@ -150,8 +161,8 @@ impl schemars::JsonSchema for Patches { } impl Patches { - /// Each dataset's patch, in order, their slices fetched through the reading's host relative to its - /// base. + /// Builds each dataset's patch, in order, fetching file slices through `reading.host` relative + /// to `reading.base`. pub async fn read(self, reading: Reading<'_>) -> Result> { let mut patches = Vec::with_capacity(self.0.0.len()); for (name, ops) in self.0.0 { @@ -162,8 +173,8 @@ impl Patches { } } -/// The patch `ops` make: their slices of files fetched through the reading's host at once, relative -/// to its base, then each change built in order. +/// Builds the patch for `ops`. Issues every file slice request through `reading.host` before +/// awaiting any, relative to `reading.base`, then builds each change in order. async fn build(mut ops: Vec, reading: Reading<'_>) -> Result { let mut slices: Vec<&mut Column> = ops .iter_mut() @@ -180,7 +191,7 @@ async fn build(mut ops: Vec, reading: Reading<'_>) -> Result { }) .collect::>>()?, None if slices.is_empty() => Vec::new(), - None => return Err(Error::Unsupported("a document's slices of files are fetched through a host".into())), + None => return Err(Error::Unsupported("fetching a document's file slices needs a host".into())), }; for (column, bytes) in slices.iter_mut().zip(fetching) { let Column::Slice(slice) = &**column else { unreachable!("slices only") }; @@ -198,7 +209,7 @@ mod tests { use super::*; use crate::{Kind, Pending, Row, Value}; - /// A host fetching from files in memory, and counting what it fetched. + /// A host serving files from memory and recording each request. #[derive(Default)] struct Files(HashMap>, std::sync::Mutex>); @@ -243,7 +254,7 @@ mod tests { } #[test] - fn slices_fetch_at_once_relative_to_the_document_and_append_frames() { + fn slices_fetch_together_relative_to_the_document_and_append_frames() { let floats: Vec = [9_f32, 1.0, 2.0, 3.0, 4.0].into_iter().flat_map(f32::to_le_bytes).collect(); let host = Files([("docs/v.f32".to_owned(), floats)].into(), std::sync::Mutex::default()); let json = r#"{"name": "grid", "patch": [ @@ -261,18 +272,24 @@ mod tests { } #[test] - fn what_bytes_hold_says_how_they_read() { + fn the_format_is_detected_from_the_first_bytes() { let bytes = br#"{"type": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": [1, 2]}}"#; assert!(geojson(bytes) && geojson(b" {\"features\": []}") && geojson(&[0x1f, 0x8b, 8])); assert!(!geojson(br#"{"name": "grid", "patch": []}"#) && !geojson(b"[]") && !geojson(b"LATK")); let named = |name| Reading { name, ..Reading::default() }; let error = pollster::block_on(Data::read(bytes, named(None))).unwrap_err(); - assert_eq!(error.to_string(), "name: GeoJSON names its dataset"); + assert_eq!(error.to_string(), "name: GeoJSON needs a dataset name"); let read = pollster::block_on(Data::read(bytes, named(Some("city")))).unwrap(); assert_eq!(read.name().as_str(), "city"); let renamed = pollster::block_on(Data::read(&read.snapshot().to_bytes(), named(Some("town")))).unwrap(); - assert_eq!(renamed.snapshot().table("points").map(crate::Table::len), Some(1), "its tables, renamed"); + assert_eq!(renamed.snapshot().table("points").map(crate::Table::len), Some(1), "renaming keeps the tables"); assert_eq!(renamed.name().as_str(), "town"); + // A page served in place of a missing file is none of them, and the error says what is read. + let page = b""; + let error = pollster::block_on(Data::read(page, named(None))).unwrap_err().to_string(); + assert_eq!(error, "input: a dataset must be latkit's bytes, GeoJSON, or a JSON document"); + let error = pollster::block_on(Patch::read(page, Reading::default())).map(|_| ()).unwrap_err().to_string(); + assert_eq!(error, "input: a patch must be latkit's bytes or JSON"); } #[test] @@ -296,7 +313,7 @@ mod tests { .contains("dtype") ); let missing = r#"[{"replace": {"table": "Bus", "ids": {"count": 1}, "fields": {"x": {"file": "f.bin", "dtype": "f64le", "count": 1}}}}]"#; - assert_eq!(error(missing), "io: f.bin: 0 bytes of the slice's 8"); + assert_eq!(error(missing), "io: f.bin: read 0 of the slice's 8 bytes"); let hostless = pollster::block_on(Patch::read(missing.as_bytes(), Reading::default())); assert!(matches!(hostless, Err(Error::Unsupported(_))), "a slice needs a host to fetch it"); let invalid = read_data( diff --git a/crates/latkit/src/data/mod.rs b/crates/latkit/src/data/mod.rs index 6a395710..04563751 100644 --- a/crates/latkit/src/data/mod.rs +++ b/crates/latkit/src/data/mod.rs @@ -1,5 +1,5 @@ -//! Datasets: named tables of paged, immutable columns, changed only by patches, each version a -//! snapshot. +//! Datasets: named tables of immutable, paged columns. Only patches change a dataset, and each +//! version is a snapshot. mod bits; mod buffer; @@ -40,7 +40,7 @@ pub use values::{Kind, Value, Values}; pub(crate) use {geometry::Geometry, table::Field}; -// What the GPU side reads of the data. +// Data internals the GPU side uses. #[cfg(feature = "gpu")] pub(crate) use { bytes::{read_values, values_bytes}, diff --git a/crates/latkit/src/data/name.rs b/crates/latkit/src/data/name.rs index 6b59ffaf..b85cb589 100644 --- a/crates/latkit/src/data/name.rs +++ b/crates/latkit/src/data/name.rs @@ -12,7 +12,7 @@ use rustc_hash::FxHashMap; #[serde(transparent)] pub struct Name(Arc); -/// A name is a text. +/// A name's schema is a string. impl schemars::JsonSchema for Name { fn inline_schema() -> bool { true @@ -108,8 +108,8 @@ impl fmt::Debug for Name { } } -/// A row's identity within its table: a number or a text. A number above 2^53 − 1 serializes as -/// decimal text, which names the same row, since JSON readers such as JavaScript's round it. +/// A row's id within its table: a number or a text. A number above 2^53 − 1 serializes as decimal +/// text, since JSON readers such as JavaScript's would round it; the text names the same row. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum Id { /// A numeric id. @@ -127,7 +127,7 @@ impl Id { } } -/// The largest whole number every JSON reader holds exactly. +/// The largest whole number every JSON reader represents exactly. const SAFE: u64 = (1 << 53) - 1; impl serde::Serialize for Id { @@ -222,7 +222,8 @@ impl IdRef<'_> { } } -/// A row of a table in a dataset. It survives any change that keeps the names and the id. +/// One row, identified by dataset name, table name, and id. It stays valid across any change that +/// keeps those names and the id. #[derive( Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema, )] @@ -242,7 +243,8 @@ impl Row { } } -/// Rows of one table in a dataset, by id: what a view selects, fits, and tells as sets of rows. +/// Rows of one table in a dataset, by id. Views use it for selections, fits, and the rows events +/// report. #[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)] pub struct Rows { /// The dataset's name. @@ -264,7 +266,7 @@ impl Rows { self.ids.iter().map(|id| Row { data: self.data.clone(), table: self.table.clone(), id: id.clone() }) } - /// `rows` as sets of rows, a set per table in the order each first appears. + /// Groups `rows` into one set per dataset and table, in order of first appearance. pub(crate) fn group(rows: impl IntoIterator) -> Vec { let mut sets: Vec = Vec::new(); for Row { data, table, id } in rows { @@ -311,16 +313,16 @@ counter!( ); impl PageId { - /// The page whose words `digest` digests: pages alike in what they hold are one. Its top bit - /// keeps it apart from every counted id. + /// A page id made from `digest`, a hash of the page's words, so pages with equal contents + /// share one id. Its top bit is set, so it never equals a counter-assigned id. pub(crate) fn of_content(digest: u64) -> PageId { PageId(digest | 1 << 63) } } -/// Values by key: one hash table of keys for every kind of value, so a map by name compiles once -/// whatever it holds, and the values in a list, in the order inserted; a removal moves the last into -/// its place. +/// A map that keeps its entries in a `Vec` in insertion order, with a hash index from key to +/// position. The index type does not depend on `V`, so the hash map code is compiled once per key +/// type. Removing an entry moves the last entry into its position. #[derive(Clone, Debug)] pub(crate) struct Keyed { index: FxHashMap, @@ -395,12 +397,12 @@ impl Keyed { Some(value) } - /// Where `key`'s value is, or goes. + /// The entry for `key`, present or not. pub(crate) fn entry(&mut self, key: K) -> Entry<'_, K, V> { Entry { keyed: self, key } } - /// Keeps the entries `keep` holds to, in their order. + /// Keeps the entries for which `keep` returns true, in their order. pub(crate) fn retain(&mut self, mut keep: impl FnMut(&K, &mut V) -> bool) { let before = self.entries.len(); self.entries.retain_mut(|(key, value)| keep(key, value)); @@ -466,14 +468,14 @@ impl<'a, K, V> IntoIterator for &'a Keyed { } } -/// Where a key's value is in a [`Keyed`], or goes. +/// The entry of a [`Keyed`] for one key, present or not. pub(crate) struct Entry<'a, K, V> { keyed: &'a mut Keyed, key: K, } impl<'a, K: Hash + Eq + Clone, V> Entry<'a, K, V> { - /// The key's value, made by `make` where it has none. + /// The key's value, inserting `make()` if it has none. pub(crate) fn or_insert_with(self, make: impl FnOnce() -> V) -> &'a mut V { let Entry { keyed, key } = self; let i = keyed.at(&key).unwrap_or_else(|| { @@ -500,6 +502,6 @@ impl, Q: Hash + Eq + ?Sized, V> std::ops::Index type Output = V; fn index(&self, key: &Q) -> &V { - self.get(key).expect("a key the table holds") + self.get(key).expect("the key is present") } } diff --git a/crates/latkit/src/data/ordered.rs b/crates/latkit/src/data/ordered.rs index 4a87a916..0a0d8a09 100644 --- a/crates/latkit/src/data/ordered.rs +++ b/crates/latkit/src/data/ordered.rs @@ -1,5 +1,5 @@ -//! Named entries in the order written: what a JSON object means where order matters, such as a -//! patch's fields or a query's selected columns. +//! JSON objects read as named entries in the order written, for objects whose key order matters, +//! such as a query's selected fields or a message's patches by dataset. use std::fmt; use std::marker::PhantomData; @@ -18,7 +18,7 @@ impl Default for Ordered { } } -/// Named entries as an object of them. +/// The schema is an object of `V` values by name. impl schemars::JsonSchema for Ordered { fn inline_schema() -> bool { true @@ -40,14 +40,14 @@ impl<'de, V: Deserialize<'de>> Deserialize<'de> for Ordered { type Value = Ordered; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("an object, each name once") + f.write_str("an object with unique names") } fn visit_map>(self, mut map: A) -> Result, A::Error> { let mut entries: Vec<(Name, V)> = Vec::new(); while let Some(name) = map.next_key::()? { if entries.iter().any(|(n, _)| n.as_str() == name) { - return Err(de::Error::custom(format!("{name} is named twice"))); + return Err(de::Error::custom(format!("{name} appears twice"))); } entries.push((name.into(), map.next_value()?)); } diff --git a/crates/latkit/src/data/patch.rs b/crates/latkit/src/data/patch.rs index 9ceb73a0..c2bbbe2c 100644 --- a/crates/latkit/src/data/patch.rs +++ b/crates/latkit/src/data/patch.rs @@ -10,11 +10,12 @@ use super::table::{Field, Reference, Table}; use super::values::Values; use crate::{Error, Result}; -/// Changes to a dataset, applied in order and all at once by [`Data::apply`](crate::Data::apply). +/// A list of changes to a dataset, applied in order and atomically by +/// [`Data::apply`](crate::Data::apply): every change applies, or none does. /// -/// Code builds patches and sockets carry them as bytes; every change reaches a dataset the same way. -/// A patch made by [`Patch::to`] names the dataset it changes, as a stream of several datasets' patches -/// does: applying it to another dataset is an error. +/// Patches are built in code or decoded from bytes and JSON; every change reaches a dataset this +/// way. A patch made by [`Patch::to`] names its dataset, as patches in a stream of several +/// datasets do, and applying it to another dataset is an error. #[derive(Clone, Default, Debug)] pub struct Patch { pub(crate) data: Option, @@ -23,30 +24,30 @@ pub struct Patch { #[derive(Clone, Debug)] pub(crate) enum Op { - /// Names the axis sampled fields are recorded over. + /// Sets the axis sampled fields are recorded over. Axis { name: Name, unit: Name }, /// Replaces a whole table, sampled fields included. Replace(Table), - /// Writes the given fields of the given rows; unknown ids add rows, unknown tables are added. + /// Writes the given fields of the given rows. Unknown ids add rows; an unknown table is added. Set(Table), /// Removes rows by id; ids that name no row are ignored. Remove { table: Name, ids: Ids }, - /// Records frames of sampled fields, frame-major: of every row, or of the rows `ids` names, the - /// rest holding their last values. + /// Records frames of sampled fields, values frame-major. Covers every row, or the rows `ids` + /// names; other rows keep their last values. Append { table: Name, at: Vec, ids: Option, fields: Vec<(Name, Values)> }, /// Forgets every sampled field's frames before a coordinate. Forget { before: f64 }, - /// Holds a run of a sampled field's entries: their coordinates, and their values entry-major. + /// Inserts a run of a sampled field's entries: their coordinates, and their values entry-major. Frames { table: Name, field: Name, run: Run, at: Vec, values: Vec }, } impl Patch { - /// An empty patch, for whichever dataset it is applied to. + /// An empty patch that applies to any dataset. pub fn new() -> Patch { Patch::default() } - /// An empty patch to dataset `data`. + /// An empty patch for dataset `data` only; applying it to another dataset is an error. pub fn to(data: impl Into) -> Patch { Patch { data: Some(data.into()), ops: Vec::new() } } @@ -56,40 +57,42 @@ impl Patch { self.data.as_ref() } - /// Names the axis sampled fields are recorded over, such as `("time", "s")`. + /// Sets the axis that sampled fields are recorded over, such as `("time", "s")`. #[must_use] pub fn axis(mut self, name: impl Into, unit: impl Into) -> Patch { self.ops.push(Op::Axis { name: name.into(), unit: unit.into() }); self } - /// Replaces table `table.name()` with `table`, or adds it. + /// Replaces table `table.name()` with `table`, sampled fields included, or adds it. #[must_use] pub fn replace(mut self, table: Table) -> Patch { self.ops.push(Op::Replace(table)); self } - /// Writes `table`'s fields for its rows: rows with known ids change, others are added, and fields - /// it leaves out keep their values. Adds the table when there is none of its name. + /// Writes `table`'s fields for its rows. Rows with existing ids are updated, new ids add rows, + /// and fields `table` leaves out keep their values (missing for added rows). Adds the table if + /// none has its name. #[must_use] pub fn set(mut self, table: Table) -> Patch { self.ops.push(Op::Set(table)); self } - /// Removes the rows of `table` with `ids`. + /// Removes the rows of `table` with `ids`. Ids that match no row are ignored; a missing table + /// is an error. #[must_use] pub fn remove(mut self, table: impl Into, ids: impl Into) -> Patch { self.ops.push(Op::Remove { table: table.into(), ids: ids.into() }); self } - /// Records a frame of `table` at each axis coordinate of `at`: for each field, a value per row at - /// every coordinate, frame-major, every row's at the first coordinate, then at the next. + /// Records a frame of `table` at each axis coordinate in `at`. Each field gives a value per row + /// per coordinate, frame-major: every row's value at the first coordinate, then at the next. /// - /// A field's first frame makes it sampled. Coordinates never decrease; at equal coordinates the - /// last frame reads. + /// A field's first frame makes it sampled. Coordinates must be finite and must not decrease; at + /// equal coordinates, reads return the last frame. #[must_use] pub fn append( mut self, @@ -106,9 +109,9 @@ impl Patch { self } - /// Records a frame of the rows of `table` that `ids` names at each axis coordinate of `at`, as - /// [`append`](Patch::append) does for every row: a value per row named at every coordinate, - /// frame-major. Rows left out hold their last value. + /// Like [`append`](Patch::append), but records values only for the rows `ids` names: one per + /// named row per coordinate, frame-major. Other rows keep their last value. An id that names no + /// row is an error. #[must_use] pub fn append_rows( mut self, @@ -126,21 +129,24 @@ impl Patch { self } - /// Forgets every sampled field's frames before axis coordinate `before`. Whole frame pages drop, - /// so it costs O(pages); older versions of the dataset keep what they read. + /// Forgets every sampled field's frames before axis coordinate `before`. Whole pages are + /// dropped, so the cost is O(pages). Later frames keep their numbers, and older snapshots still + /// read the forgotten frames. #[must_use] pub fn forget(mut self, before: f64) -> Patch { self.ops.push(Op::Forget { before }); self } - /// Holds a run of sampled field `field`'s entries of level `level`, numbered from `first`: what a - /// time tile holds, as `latkit tile` writes them. At level 0 the entries are frames, each a - /// coordinate in `at` and a value per row of `table` in `values`. At level `k` they are buckets of - /// `8^k` frames, each its first and last frame's coordinates in `at`, and in `values` each row's - /// least value over each bucket, then each row's greatest. NaN is missing. + /// Inserts a run of entries of sampled field `field` at level `level`, numbered from `first`, + /// as time tiles (such as `latkit tile` writes) store them. At level 0 the entries are frames: + /// one coordinate each in `at` and one value per row of `table` in `values`. At level `k` they + /// are buckets of `8^k` frames: each bucket's first and last frame's coordinates in `at`, and + /// in `values` every bucket's least value per row, followed by every bucket's greatest. NaN is + /// missing. /// - /// No entry may be held already; frames past the field's widen it, and those it skips stay missing. + /// None of the entries may already be present. Entries outside the field's frame range extend + /// it, and frames in any gap stay missing. /// /// ``` /// # use latkit::{Data, Ids, Patch, Table}; @@ -166,7 +172,7 @@ impl Patch { self } - /// This patch followed by `next`'s changes, to this patch's dataset. + /// This patch followed by `next`'s changes. The result keeps this patch's dataset. #[must_use] pub fn extend(mut self, next: Patch) -> Patch { self.ops.extend(next.ops); @@ -178,22 +184,23 @@ impl Patch { self.ops.is_empty() } - /// The patch as bytes: one frame, naming its dataset when it has one. + /// Encodes the patch as one binary frame, including its dataset's name when it has one. pub fn to_bytes(&self) -> Vec { super::bytes::encode(self) } - /// A patch from [`Patch::to_bytes`] or [`Snapshot::to_bytes`](crate::Snapshot::to_bytes): one - /// whole frame. + /// Decodes one whole frame written by [`Patch::to_bytes`] or + /// [`Snapshot::to_bytes`](crate::Snapshot::to_bytes), with default [`DecodeLimits`]. Errors if + /// `bytes` are malformed, truncated, or extend past the frame. pub fn from_bytes(bytes: &[u8]) -> Result { super::bytes::decode(bytes) } - /// The patches `read` carries, frames back to back as files, pipes, and sockets carry them, each - /// once it arrived whole. A frame says its length up front, so one longer than `limits` allows is - /// [`Error::Limit`](crate::Error::Limit) before its bytes are read, and a stream of untrusted - /// bytes holds one frame at a time. The patches end with the bytes, between frames, or at the - /// first error. + /// Decodes the back-to-back frames `read` carries, as from a file, pipe, or socket, yielding + /// each patch once its frame has fully arrived. Each frame states its length up front, so one + /// longer than `limits` allows fails with [`Error::Limit`](crate::Error::Limit) before its body + /// is read, and an untrusted stream buffers only one frame at a time. The iterator ends when + /// the input ends between frames, or after the first error. /// /// ``` /// # use latkit::{DecodeLimits, Ids, Patch, Table}; @@ -217,8 +224,8 @@ impl From for Patch { } } -/// Appends to the same fields of every row of one table, one after another: recorded as one, so a -/// field takes its frames in one write however many changes carried them. +/// Consecutive appends to the same fields of every row of one table, merged into one append so +/// each field is written once, however many changes carried its frames. pub(crate) struct Appends { table: Name, at: Vec, @@ -230,9 +237,10 @@ impl Appends { Appends { table, at, fields: fields.into_iter().map(|(name, values)| (name, vec![values])).collect() } } - /// Whether an append of `fields` at `at` to `table`, which has `rows` rows, carries the run on: the - /// same fields of the same kinds, a value for every row at each coordinate, coordinates that never - /// decrease. One that does not records on its own, so its error names it. + /// Whether an append of `fields` at `at` to `table` (with `rows` rows) can join this run: the + /// same table, the same fields of the same kinds, a value for every row at each coordinate, and + /// finite coordinates that do not decrease. An append that cannot join is applied alone, so its + /// error names it. pub(crate) fn continues(&self, table: &Name, at: &[f64], fields: &[(Name, Values)], rows: Option) -> bool { let Some(rows) = rows else { return false }; let last = self.at.last().copied().unwrap_or(f64::NEG_INFINITY); @@ -272,7 +280,8 @@ impl Appends { } impl Op { - /// Holds entries of `level` from `first`, as many as `at` holds coordinates of. + /// An [`Op::Frames`] of entries of `level` from `first`; the entry count follows from the + /// number of coordinates in `at`. pub(crate) fn frames(table: Name, field: Name, level: u32, first: u64, at: Vec, values: Vec) -> Op { let entries = (at.len() / super::samples::coordinates(level)) as u64; let run = Run { level, entries: first..first.saturating_add(entries) }; @@ -298,7 +307,7 @@ impl Op { let name = table.name.clone(); table.check().map_err(|e| e.at(&name))?; if let Some((field, _)) = table.fields.iter().find(|(_, f)| matches!(f, Field::Sampled(..))) { - return Err(Error::invalid("set writes values and references; append records samples") + return Err(Error::invalid("sampled fields are written with append, not with set") .at(field) .at(&name)); } @@ -352,7 +361,7 @@ impl Op { let per_row = run.len() * super::samples::sides(run.level); let rows = values.len().checked_div(per_row).unwrap_or(0); if rows > target.len() { - return Err(Error::invalid(format!("values for {rows} rows of {}", target.len())) + return Err(Error::invalid(format!("values for {rows} rows, but the table has {}", target.len())) .at(&field) .at(&table)); } @@ -369,8 +378,8 @@ impl Op { } } -/// The row of `table` each of `ids` names; an id naming none is an error, as an append records -/// frames of rows the table holds. +/// The row of `table` for each id in `ids`. An id that names no row is an error, since an append +/// records frames only for existing rows. fn rows_named(table: &Table, ids: &Ids) -> Result> { let index = table.index()?; (0..ids.len()) diff --git a/crates/latkit/src/data/query.rs b/crates/latkit/src/data/query.rs index 9e8e961d..189a1e44 100644 --- a/crates/latkit/src/data/query.rs +++ b/crates/latkit/src/data/query.rs @@ -1,7 +1,6 @@ -//! Reading data back: the rows of a table a query keeps, in the order it sorts them, a page of them, -//! with their fields or what expressions make of them. Queries run on the CPU through the same -//! expressions marks draw with, a plain field read page by page, and read back a table, so what -//! comes back has the shape of what goes in. +//! Queries: reading a page of a table's rows, filtered and sorted, with their fields or computed +//! values. Queries evaluate on the CPU with the same expressions marks draw with (a plain field is +//! read page by page) and return a table, so the result has the same shape as the input. use std::cmp::Ordering; @@ -15,10 +14,11 @@ use crate::expr::Expr; use crate::expr::cpu::Rows; use crate::{Error, Result}; -/// The rows of a table to read back: of the rows `ids` names (every row unless set), those `filter` -/// keeps, sorted by `sort`, a page of them from `offset` of at most `limit`, with the fields `select` -/// names or computes, every field unless it is set; sampled fields read at axis coordinate `at`, their -/// last frame by default. Plain data, like a scene. +/// A request to read rows of a table. Starts from the rows `ids` names (every row by default), +/// keeps those `filter` accepts, sorts them by `sort`, and returns up to `limit` of them starting +/// at `offset`. Each row comes with the fields `select` names or computes (every field by default). +/// Sampled fields are read at axis coordinate `at`, or at their last frame by default. Plain data +/// that deserializes from JSON, like a scene. /// /// ``` /// use latkit::{Data, Ids, Order, Query, Table, field}; @@ -33,45 +33,46 @@ use crate::{Error, Result}; #[serde(deny_unknown_fields)] #[schemars(description = "The rows of a table to read back, run through the expressions marks draw with.")] pub struct Query { - /// The table read. + /// The table to read. of: Name, - /// The rows read, in this order unless sorted; every row unless set. Ids that name no row are - /// passed over. + /// The rows to read, in this order unless sorted; every row when unset. Ids that name no row + /// are skipped. #[serde(default)] ids: Option, - /// Keeps the rows it is true for. + /// Keeps only the rows for which this is true. #[serde(default)] filter: Option, - /// Keys to sort by, each after those before it; rows a key reads nothing for come last, and rows - /// all keys order alike keep their order. + /// Keys to sort by, each breaking ties left by the keys before it. Rows where a key has no + /// value sort last; rows equal on every key keep their order. #[serde(default)] sort: Vec<(Expr, Order)>, - /// The kept rows skipped first. + /// How many of the kept rows to skip. #[serde(default)] offset: usize, - /// The most rows read; a limit picks them in linear time before sorting only those. + /// The most rows to return. When sorting with a limit, the first `offset + limit` rows are + /// selected in linear time and only those are sorted. limit: Option, - /// Fields to read by name: a field as it is, anything else as numbers. Every field unless set; - /// none set reads the ids alone. + /// Fields to return, by output name. A plain field returns that field as stored; any other + /// expression returns numbers. Every field when unset; an empty object returns the ids alone. #[serde(default)] select: Option>, - /// The axis coordinate sampled fields are read at; their last frame unless set. + /// The axis coordinate at which sampled fields are read; their last frame when unset. at: Option, } -/// Which way a sort runs; rows a key reads nothing for come last either way. +/// Sort direction. Rows where the key has no value sort last in either direction. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize, schemars::JsonSchema)] #[serde(rename_all = "lowercase")] pub enum Order { - /// Least first. + /// Smallest first. #[default] Ascending, - /// Greatest first. + /// Largest first. Descending, } impl Query { - /// Every row of table `table`, every field. + /// A query for every row and field of table `table`. pub fn of(table: impl Into) -> Query { Query { of: table.into(), @@ -85,50 +86,52 @@ impl Query { } } - /// Reads only the rows `ids` names, in that order unless sorted. + /// Reads only the rows `ids` names, in that order unless sorted. Ids that name no row are + /// skipped. #[must_use] pub fn ids(mut self, ids: impl Into) -> Query { self.ids = Some(ids.into()); self } - /// Keeps the rows `keep` is true for. + /// Keeps only the rows for which `keep` is true. #[must_use] pub fn filter(mut self, keep: impl Into) -> Query { self.filter = Some(keep.into()); self } - /// Sorts by `key`, after the keys given before; rows it orders alike keep their order. + /// Sorts by `key`, breaking ties left by earlier keys. Rows equal on every key keep their + /// order; rows where `key` has no value sort last. #[must_use] pub fn sort(mut self, key: impl Into, order: Order) -> Query { self.sort.push((key.into(), order)); self } - /// Skips the first `offset` rows kept. + /// Skips the first `offset` kept rows, after sorting. #[must_use] pub fn offset(mut self, offset: usize) -> Query { self.offset = offset; self } - /// Reads at most `limit` rows. + /// Returns at most `limit` rows. #[must_use] pub fn limit(mut self, limit: usize) -> Query { self.limit = Some(limit); self } - /// Reads field `name` as `value` makes it: a field as it is, anything else as numbers. With none - /// selected, every field is read. + /// Adds output field `name`, computed by `value`. A plain field returns that field as stored; + /// any other expression returns numbers. Without any `select`, every field is returned. #[must_use] pub fn select(mut self, name: impl Into, value: impl Into) -> Query { self.select.get_or_insert_with(Ordered::default).0.push((name.into(), value.into())); self } - /// Reads the rows' ids alone, no field. + /// Returns only the rows' ids, with no fields. #[must_use] pub fn ids_alone(mut self) -> Query { self.select = Some(Ordered::default()); @@ -143,20 +146,22 @@ impl Query { } } -/// What a query read: the rows it kept, a page of them in order, as a table of their own, and how -/// many rows it kept before the page. +/// The result of [`Snapshot::query`]: the requested page of rows as a table, and how many rows +/// passed the filter. #[derive(Clone, Debug)] pub struct QueryResult { - /// The rows read, named as the table they were read from: their ids, and their fields or what - /// the query computes, sampled fields read at its coordinate. + /// The page of rows, as a table with the source table's name: their ids, and their selected + /// fields or computed values. Sampled fields come back as plain values read at the query's + /// coordinate. pub table: Table, - /// The rows the query kept before paging. + /// The number of rows that passed the filter, before `offset` and `limit`. pub total: usize, } impl Snapshot { - /// The rows of a table `query` asks for, as a table of their own, and how many rows its filter - /// kept, before the page. A limit picks its rows in linear time before sorting only them. + /// Runs `query`, returning the page of rows it describes as a table and the number of rows + /// that passed the filter. When sorting with a limit, the rows are selected in linear time and + /// only those are sorted. Errors if the table or a selected field does not exist. pub fn query(&self, query: &Query) -> Result { let table = self.table(&query.of).ok_or_else(|| Error::invalid("no such table").at(query.of.as_str()))?; let at = query.at.unwrap_or(f64::INFINITY); @@ -224,7 +229,7 @@ impl Snapshot { } } -/// How two sort keys order rows: missing last, whichever way the sort runs. +/// Orders two sort-key values. Non-finite (missing) values sort last in either direction. fn compare(a: f64, b: f64, order: Order) -> Ordering { match (a.is_finite(), b.is_finite()) { (true, true) if order == Order::Descending => b.total_cmp(&a), @@ -235,7 +240,8 @@ fn compare(a: f64, b: f64, order: Order) -> Ordering { } } -/// `field` of rows `rows` as plain values: a sampled field read at `at`, one frame, not its history. +/// `field` for rows `rows` as plain values. A sampled field becomes its values in the frame at +/// `at`, without its history. fn plain(field: &Field, at: f64, rows: &[usize]) -> Field { match field { Field::Sampled(samples, _) => { diff --git a/crates/latkit/src/data/samples.rs b/crates/latkit/src/data/samples.rs index 731ea251..beedbbb8 100644 --- a/crates/latkit/src/data/samples.rs +++ b/crates/latkit/src/data/samples.rs @@ -1,14 +1,17 @@ -//! Samples: a field's values over the data's axis. Level 0 holds frames, each row's value at one -//! coordinate; each level `k` above it holds buckets of `8^k` frames, each row's least and greatest over -//! them, so a long history draws from as few entries as there are pixels. Every level keeps its -//! entries in pages, with each entry's coordinates (a frame's own; a bucket's first and last frame's), -//! so a level draws without the levels below it. Frames are numbered from the first ever written, and -//! a version reads `start..end`. +//! Samples: a field's values over the dataset's axis, stored as levels of detail. //! -//! Any entry may be missing. Frames appended grow the newest pages in place and seal each bucket whose -//! entries below are all held; runs of entries read from bytes or fetched as time tiles fill any span, -//! whatever lies around them. An entry is held when its level's coordinates hold it: every write writes -//! an entry's coordinates and every row block's values together. +//! Level 0 holds frames: each row's value at one axis coordinate. Each level `k` above holds +//! buckets of `8^k` frames: each row's least and greatest value over the bucket. A long history can +//! then be drawn from about as many entries as there are pixels. Each level stores its entries in +//! pages together with their coordinates (a frame's own; a bucket's first and last frame's), so a +//! level can be drawn without the levels below it. Frames are numbered from the first ever +//! written, and a version reads frames `start..end`. +//! +//! Any entry may be missing. Appended frames grow the newest pages in place and write each bucket +//! whose eight entries one level below are all present. Runs of entries decoded from bytes or +//! fetched as time tiles can fill any range, regardless of what surrounds it. An entry is present +//! when its level's coordinate pages contain it: every write stores an entry's coordinates and +//! every row block's values together. use std::ops::Range; use std::sync::{Arc, PoisonError, RwLock}; @@ -18,44 +21,46 @@ use super::name::PageId; use super::values::Values; use crate::{Error, Result}; -/// Words in a frame page: entries of one row block, entry-major. An axis page holds as many words on -/// the GPU, two for each entry. +/// Words in a frame page, which holds entries of one row block, entry-major. An axis page takes at +/// most as many words on the GPU, two per entry. pub(crate) const FRAME_PAGE_WORDS: usize = 1 << 16; -/// Levels of detail kept above frames: level `k` holds each row's least and greatest value over buckets -/// of `8^k` frames. +/// Levels of detail that appends write above frames. Level `k` holds each row's least and greatest +/// value over buckets of `8^k` frames. Inserted runs can go higher, up to [`MAX_LEVEL`]. pub(crate) const LEVELS: u32 = 6; -/// Highest format level: a bucket spans 2^63 frames and still fits a u64. +/// The highest level the format allows: a bucket of `8^21 = 2^63` frames still fits a `u64`. pub(crate) const MAX_LEVEL: u32 = 21; -/// Frames an entry of level `level` spans: a frame, or a bucket's frames. +/// The number of frames an entry of `level` covers: `8^level`. pub(crate) fn bucket_frames(level: u32) -> u64 { 1 << (3 * level) } -/// The level of detail `frames` frames draw at across `columns` pixel columns: frames while there are -/// at most two a column, else the finest level whose buckets are no fewer. +/// The level of detail for drawing `frames` frames across `columns` pixel columns: frames (level +/// 0) while there are at most two per column, else the finest level with at most two buckets per +/// column. pub(crate) fn detail(frames: u64, columns: u32) -> u32 { let needed = frames.div_ceil(2 * u64::from(columns.max(1))); if needed <= 1 { 0 } else { (1..=MAX_LEVEL).find(|&k| bucket_frames(k) >= needed).unwrap_or(MAX_LEVEL) } } -/// Coordinates an entry of `level` holds: a frame its own, a bucket its first and last frame's. +/// Coordinates stored per entry of `level`: 1 for a frame, 2 for a bucket (its first and last +/// frame's). pub(crate) fn coordinates(level: u32) -> usize { if level == 0 { 1 } else { 2 } } -/// Values a row holds of an entry of `level`: a frame's, or a bucket's least and greatest. +/// Values stored per row per entry of `level`: 1 for a frame, 2 for a bucket (least and greatest). pub(crate) fn sides(level: u32) -> usize { if level == 0 { 1 } else { 2 } } -/// The log of a row block holding `level`'s values of `side`: its frames, or a level's least (0) or -/// greatest (1). +/// The index, among a row block's logs, of the log holding `level`'s `side` values: 0 for frames, +/// then each level's least (`side` 0) and greatest (`side` 1). fn slot(level: u32, side: usize) -> usize { if level == 0 { 0 } else { 2 * level as usize - 1 + side } } -/// Entries `entries` of level `level`. +/// A range of entries of one level. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub(crate) struct Run { pub(crate) level: u32, @@ -63,25 +68,26 @@ pub(crate) struct Run { } impl Run { - /// Number of entries; runs in memory always fit the address space. + /// The number of entries. A run held in memory always fits in `usize`. pub(crate) fn len(&self) -> usize { usize::try_from(self.entries.end - self.entries.start).expect("run fits in memory") } - /// The frames its entries span. + /// The frames its entries cover. pub(crate) fn frames(&self) -> Range { let span = bucket_frames(self.level); self.entries.start.saturating_mul(span)..self.entries.end.saturating_mul(span) } - /// How many coordinates it holds, and how many values for `rows` rows. + /// Its coordinate count, and its value count for `rows` rows; `None` on overflow. pub(crate) fn sizes(&self, rows: usize) -> Option<(usize, usize)> { let n = usize::try_from(self.entries.end.checked_sub(self.entries.start)?).ok()?; Some((n.checked_mul(coordinates(self.level))?, n.checked_mul(rows)?.checked_mul(sides(self.level))?)) } } -/// Appends `run` to `runs`, joining it to the last where it follows it at the same level. +/// Appends `run` to `runs`, merging it into the last run when it continues that run at the same +/// level. fn join(runs: &mut Vec, run: Run) { match runs.last_mut() { Some(last) if last.level == run.level && last.entries.end == run.entries.start => { @@ -92,10 +98,10 @@ fn join(runs: &mut Vec, run: Run) { } /// A list shared between versions. Indices are absolute: a version reads items `start..end`, so -/// forgetting the oldest never renumbers the rest. +/// forgetting the oldest items never renumbers the rest. /// -/// A version that appends while it is the newest grows the shared list in place; any other change, an -/// older version appending, an item put between others or taken out, forks a copy of what it reads. +/// The newest version appends to the shared list in place. Any other change (an older version +/// appending, or an item inserted or removed) first copies the items the version reads. pub(crate) struct Log { items: Arc>>, /// The absolute index of `items[0]`. @@ -129,7 +135,7 @@ impl Log { self.edit(|items| items.push(item)); } - /// Changes the items this version reads in a copy of its own. + /// Applies `f` to a private copy of the items this version reads. fn edit(&mut self, f: impl FnOnce(&mut Vec)) { let mut items = self.read(<[T]>::to_vec); f(&mut items); @@ -138,7 +144,7 @@ impl Log { self.base = self.start; } - /// Reads items `start..end`. + /// Calls `f` with items `start..end`. pub(crate) fn read(&self, f: impl FnOnce(&[T]) -> R) -> R { let items = self.items.read().unwrap_or_else(PoisonError::into_inner); f(&items[self.start - self.base..self.end - self.base]) @@ -148,8 +154,9 @@ impl Log { self.read(|items| items.last().cloned()) } - /// Stops reading items before absolute index `start`. Storage shared with other versions stays - /// theirs; this version copies what it keeps once the forgotten part outgrows a quarter of it. + /// Stops reading items before absolute index `start`. Shared storage is left to the other + /// versions; once the forgotten prefix exceeds a quarter of the kept items (plus 16), this + /// version copies the items it keeps. pub(crate) fn forget(&mut self, start: usize) { self.start = start.clamp(self.start, self.end); if self.start - self.base > (self.end - self.start) / 4 + 16 { @@ -160,16 +167,17 @@ impl Log { } } -/// What a page stores, shared by every version reading it, and whether appends grow it. +/// A page's storage, shared by every version that reads the page, and whether appends may grow it. struct Store { grows: bool, items: RwLock>, } -/// Consecutive entries of a level, `per` values each, entry-major: a row block's frames or a level's -/// least or greatest values, `per` its rows; or a level's coordinates, `per` an entry's. A page an -/// append opened grows in place while it is the newest version's last; a version reads at most `cap` -/// entries of it, fewer than it stores where a newer version grew it. +/// Consecutive entries of one level, `per` items each, entry-major. In a frame page the items are +/// a row block's frames or one side (least or greatest) of a level, and `per` is the block's row +/// count. In an axis page the items are a level's coordinates, and `per` is the coordinates per +/// entry. A page opened by an append grows in place while it is the newest version's last page. A +/// version reads at most `cap` entries, fewer than are stored when a newer version grew the page. #[derive(Clone)] pub(crate) struct Page { pub(crate) id: PageId, @@ -179,9 +187,9 @@ pub(crate) struct Page { store: Arc>, } -/// A page of a row block's frames, or of a level's least or greatest values: `per` rows an entry. +/// A page of a row block's frames, or of a level's least or greatest values: `per` rows per entry. pub(crate) type FramePage = Page; -/// A page of a level's coordinates: `per` an entry, a frame's own or a bucket's first and last. +/// A page of a level's coordinates, `per` per entry: a frame's own, or a bucket's first and last. pub(crate) type AxisPage = Page; impl Page { @@ -195,17 +203,18 @@ impl Page { } } - /// Reads the page's stored items. + /// Calls `f` with every stored item, which may include items past `cap`. pub(crate) fn read(&self, f: impl FnOnce(&[T]) -> R) -> R { f(&self.store.items.read().unwrap_or_else(PoisonError::into_inner)) } - /// Whether appends grow it: it takes room to grow into on the GPU. + /// Whether appends may grow it. The GPU allocates such pages at full size so they can grow in + /// place. pub(crate) fn grows(&self) -> bool { self.store.grows } - /// Entries this version reads of it. + /// The number of entries this version reads. pub(crate) fn len(&self) -> u32 { ((self.read(<[T]>::len) / self.per.max(1) as usize) as u32).min(self.cap) } @@ -214,8 +223,9 @@ impl Page { self.first + u64::from(self.len()) } - /// Appends `entry`'s `items` where this version reads the whole page, appends grow it, and the - /// entry follows its last and fits within `limit` items. + /// Appends `entry`'s `items` in place, returning whether it did. Succeeds only if the page + /// grows, this version reads all of it and nothing newer is stored past it, `entry` directly + /// follows the last entry, and the page stays within `limit` items. fn extend(&self, entry: u64, items: &[T], limit: usize) -> bool { if entry != self.end() { return false; @@ -230,14 +240,15 @@ impl Page { true } - /// The page as a version that reads only its first `len` entries sees it. + /// A handle to the same page that reads only its first `len` entries. fn capped(&self, len: u32) -> Page { Page { cap: len, ..self.clone() } } } impl AxisPage { - /// Coordinate `which` of entry `index`: a frame's own, a bucket's first (0) or last (1) frame's. + /// Coordinate `which` of entry `index`: a frame's own, or a bucket's first (0) or last (1) + /// frame's. fn coordinate(&self, index: u64, which: u32) -> Option { if index < self.first || index >= self.end() { return None; @@ -246,17 +257,17 @@ impl AxisPage { self.read(|c| c.get(i).copied()) } - /// Where entry `index` draws: a frame at its coordinate, a bucket at its middle. + /// The coordinate at which entry `index` is drawn: a frame's own, or a bucket's midpoint. fn drawn(&self, index: u64) -> Option { Some(f64::midpoint(self.coordinate(index, 0)?, self.coordinate(index, 1)?)) } - /// Words the page takes on the GPU: where each entry draws, as a high and a low `f32`. + /// The page's size in GPU words: each entry's drawn coordinate as a high and a low `f32`. pub(crate) fn words_len(&self) -> usize { 2 * (self.read(<[f64]>::len) / self.per as usize) } - /// The page's GPU words from word `from` on: a growing page uploads only its new tail. + /// The page's GPU words from word `from` on, so a growing page uploads only its new tail. pub(crate) fn words(&self, from: usize) -> Vec { let per = self.per as usize; self.read(|c| { @@ -271,7 +282,7 @@ fn split(x: f64) -> [u32; 2] { [hi.to_bits(), ((x - f64::from(hi)) as f32).to_bits()] } -/// The page of `log` holding entry `index`, where one does. +/// The page of `log` containing entry `index`, if any. fn holding(log: &Log>, index: u64) -> Option> { log.read(|pages| { let i = pages.partition_point(|p| p.first <= index).checked_sub(1)?; @@ -279,8 +290,9 @@ fn holding(log: &Log>, index: u64) -> Option> { }) } -/// The pages of `log` holding any of `entries`, in order, each with those it holds. A page that stores -/// entries past the next page's first, where a version forked, holds them no longer. +/// The pages of `log` containing any of `entries`, in order, each with the range of `entries` it +/// contains. A page that stores entries past the next page's first entry (left by a fork) is cut +/// off at that entry. fn held(log: &Log>, entries: Range) -> Vec<(Page, Range)> { if entries.is_empty() { return Vec::new(); @@ -302,7 +314,7 @@ fn held(log: &Log>, entries: Range) -> Vec<(Page, Range }) } -/// The first of `entries` a page of `log` holds. +/// The first of `entries` that a page of `log` contains. fn first_held(log: &Log>, entries: Range) -> Option { log.read(|pages| { let from = pages.partition_point(|p| p.first <= entries.start).saturating_sub(1); @@ -320,7 +332,7 @@ fn first_held(log: &Log>, entries: Range) -> Option { }) } -/// Puts `page` in its place in `log`, among pages holding none of its entries. +/// Inserts `page` into `log` in entry order. No page of `log` may contain its entries. fn place(log: &mut Log>, page: Page) { let (at, tip) = log.read(|pages| { let at = pages.partition_point(|p| p.first < page.first); @@ -329,8 +341,8 @@ fn place(log: &mut Log>, page: Page) { if tip { log.push(page) } else { log.edit(|pages| pages.insert(at, page)) } } -/// Makes the last page of `log` hold no entry from `end` on, as a version reading entries before `end` -/// sees it, before the version writes past `end`. +/// Caps the last page of `log` at entry `end`, so this version no longer reads entries a newer +/// version appended from `end` on. Called before this version writes past `end`. fn cap(log: &mut Log>, end: u64) { if log.last().is_some_and(|page| page.end() > end) { log.edit(|pages| { @@ -340,7 +352,7 @@ fn cap(log: &mut Log>, end: u64) { } } -/// Takes the pages of `log` holding only entries of `entries` out of it. +/// Removes the pages of `log` whose entries all lie within `entries`. fn take_out(log: &mut Log>, entries: &Range) { let (from, to) = log.read(|pages| { let from = pages.partition_point(|p| p.first < entries.start); @@ -358,16 +370,16 @@ fn forget_before(log: &mut Log>, index: u64) { log.forget(start + dropped); } -/// Writes entry `entry`, `items` of it, after the last of `log`: into its last page where that grows -/// and has room within `limit` items, else into a new page that grows. +/// Writes `entry`'s `items` after the last entry of `log`: into the last page if it can grow and +/// has room within `limit` items, else into a new growable page. fn grow(log: &mut Log>, entry: u64, items: &[T], limit: usize) { if !log.last().is_some_and(|page| page.extend(entry, items, limit)) { log.push(Page::new(entry, items.len(), items.to_vec(), true)); } } -/// Writes `entries` of a level, `items` of `per` each, as pages placed in order, as many entries to a -/// page as `limit` items allow. +/// Writes `entries` of a level, `per` items each, as new fixed-size pages inserted in order, each +/// holding as many entries as fit in `limit` items. fn put(log: &mut Log>, entries: &Range, items: &[T], per: usize, limit: usize) { let step = (limit / per.max(1)).max(1); for (k, chunk) in items.chunks(step * per.max(1)).enumerate() { @@ -376,30 +388,31 @@ fn put(log: &mut Log>, entries: &Range, items: &[T], per: } } -/// The logs a write writes into: every level's coordinates, and each row block's logs. +/// Writable copies of the logs: every level's coordinates, and each row block's value logs. struct Logs { axes: Vec>, blocks: Vec>>, } -/// A field's samples over the data's axis: frames, and levels of detail above them, with holes. +/// A sampled field's values over the axis: frames and the levels of detail above them, any of +/// which may have gaps. Values are stored per row block of [`PAGE_ROWS`] rows. #[derive(Clone, Default)] pub(crate) struct Samples { - /// Frames this version reads, numbered from the first ever written. + /// The frames this version reads, `start..end`, numbered from the first ever written. start: u64, end: u64, - /// Frames before this one were forgotten: pages may still store some, which this version must not - /// read again. + /// Frames before this were forgotten. Shared pages may still store some, and this version must + /// not read them again. forgotten: u64, - /// Per level from 0: its entries' coordinates; empty until anything is written. + /// Per level from 0, its entries' coordinates. Empty until something is written. axes: Arc<[Log]>, - /// Per row block: its frames, then each level's least and greatest values. + /// Per row block, its frames, then each level's least and greatest values. blocks: Arc<[Vec>]>, } impl Samples { - /// No frames yet, the first to be appended numbered `first`: what samples read back from bytes or - /// gathered from others start from, so their frames keep their numbers. + /// Empty samples whose first frame will be numbered `first`. Samples decoded from bytes or + /// gathered from others start here, so their frames keep their numbers. pub(crate) fn from_frame(first: u64) -> Samples { Samples { start: first, end: first, ..Samples::default() } } @@ -414,31 +427,35 @@ impl Samples { self.end } - /// Reads frames `frames`: what bytes say, after the runs they hold are put in. + /// Sets the frames this version reads to `frames`. Decoding calls it after inserting the runs, + /// to restore the encoded frame range. pub(crate) fn reads(&mut self, frames: Range) { (self.start, self.end) = (frames.start, frames.end); } - /// The same frames, none of them held: what a dataset written as time tiles starts from. + /// Samples with the same frame range but no entries: the base of a dataset written as time + /// tiles. pub(crate) fn emptied(&self) -> Samples { let mut empty = Samples::from_frame(self.start); empty.reads(self.start..self.end); empty } - /// The entries of `level` this version reads: those whose frames lie before its end, from the one - /// holding its first frame. + /// The entries of `level` this version reads: from the entry containing frame `start` through + /// the last entry that ends by frame `end`. fn visible(&self, level: u32) -> Range { let k = 3 * level; self.start >> k..self.end >> k } - /// Copies of the logs to write into, with every level's and at least `blocks` row blocks'. + /// Writable copies of the logs, covering at least levels 0 through [`LEVELS`] and `blocks` row + /// blocks. fn logs(&self, blocks: usize) -> Logs { self.logs_at(blocks, LEVELS) } - /// Highest level stored; the binary format records it independently of the append policy. + /// The highest level with stored pages. The binary format records it rather than assuming + /// [`LEVELS`], which only bounds the levels appends write. pub(crate) fn levels(&self) -> u32 { self.axes.iter().rposition(|log| log.read(|pages| !pages.is_empty())).unwrap_or(0) as u32 } @@ -461,12 +478,13 @@ impl Samples { self.blocks = logs.blocks.into(); } - /// The log of row block `block` holding `level`'s values of `side`. + /// The log of row block `block` holding `level`'s `side` values. fn log(&self, block: usize, level: u32, side: usize) -> Option<&Log> { self.blocks.get(block)?.get(slot(level, side)) } - /// The page of `level`'s coordinates holding entry `index`, where this version holds it. + /// The coordinate page of `level` containing entry `index`, if this version reads that entry + /// and it is present. fn axis_page(&self, level: u32, index: u64) -> Option { if !self.visible(level).contains(&index) { return None; @@ -474,32 +492,34 @@ impl Samples { holding(self.axes.get(level as usize)?, index) } - /// Whether no entry is held, of any level. + /// Whether no entry of any level is present. pub(crate) fn is_empty(&self) -> bool { self.axes.iter().enumerate().all(|(level, log)| first_held(log, self.visible(level as u32)).is_none()) } - /// Whether entry `index` of `level` is held. + /// Whether entry `index` of `level` is present. pub(crate) fn held(&self, level: u32, index: u64) -> bool { self.axis_page(level, index).is_some() } - /// Coordinate `which` of entry `index` of `level`: a frame's own, a bucket's first (0) or last (1). + /// Coordinate `which` of entry `index` of `level`: a frame's own, or a bucket's first (0) or + /// last (1) frame's. pub(crate) fn coordinate(&self, level: u32, index: u64, which: u32) -> Option { self.axis_page(level, index)?.coordinate(index, which) } - /// Where entry `index` of `level` draws: a frame at its coordinate, a bucket at its middle. + /// The coordinate at which entry `index` of `level` is drawn: a frame's own, or a bucket's + /// midpoint. pub(crate) fn drawn_at(&self, level: u32, index: u64) -> Option { self.axis_page(level, index)?.drawn(index) } - /// The coordinate of `frame`, where it is held. + /// The coordinate of `frame`, if present. pub(crate) fn at(&self, frame: u64) -> Option { self.coordinate(0, frame, 0) } - /// The runs of entries held, level by level, each as long as held entries follow one another. + /// The runs of present entries, level by level, each run as long as its entries are contiguous. pub(crate) fn runs(&self) -> Vec { let mut runs = Vec::new(); for (level, log) in self.axes.iter().enumerate() { @@ -511,8 +531,8 @@ impl Samples { runs } - /// The runs bytes hold: every run held but the buckets whose eight entries a level below are all - /// held, since those follow from them. + /// The runs the binary format stores: every present run, minus the buckets whose eight entries + /// one level below are all present, since decoding derives those. pub(crate) fn stored_runs(&self) -> Vec { let runs = self.runs(); let mut stored = Vec::new(); @@ -521,7 +541,7 @@ impl Samples { stored.push(run.clone()); continue; } - // Buckets that follow from the runs below, in order: what this run leaves out. + // Buckets derivable from the runs one level below, in order; this run omits them. let derived = runs .iter() .filter(|r| r.level + 1 == run.level) @@ -545,7 +565,7 @@ impl Samples { stored } - /// The first and last coordinates of what this version holds, at any level. + /// The first and last coordinates of the entries this version reads, across all levels. pub(crate) fn span(&self) -> Option<[f64; 2]> { let mut span: Option<[f64; 2]> = None; for (level, log) in self.axes.iter().enumerate() { @@ -557,12 +577,14 @@ impl Samples { span } - /// The coordinate of the last frame, or of the last entry of any level. + /// The coordinate of the last frame, or if that frame is missing, the end of the span across + /// all levels. fn last_coordinate(&self) -> Option { self.end.checked_sub(1).and_then(|f| self.at(f)).or_else(|| self.span().map(|s| s[1])) } - /// Appends frames: `values` holds `at.len()` frames of `rows` values each, frame-major. + /// Appends frames: `values` holds `at.len()` frames of `rows` values each, frame-major. Errors + /// on a length mismatch, a non-finite coordinate, or a coordinate below the last one. pub(crate) fn append(&mut self, at: &[f64], values: &Values, rows: usize) -> Result<()> { if at.len().checked_mul(rows) != Some(values.len()) { return Err(Error::invalid(format!("{} values for {} frames of {rows} rows", values.len(), at.len()))); @@ -599,8 +621,9 @@ impl Samples { Ok(()) } - /// Appends frames of some rows: `values` holds `at.len()` frames of a value per row of `rows`, - /// frame-major. The rest of the `total` rows hold their last frame's values, missing before any. + /// Appends frames for some rows: `values` holds `at.len()` frames of one value per row in + /// `rows`, frame-major. The other rows of `total` repeat their value from the last frame, or + /// are missing if there is none. pub(crate) fn append_rows(&mut self, at: &[f64], values: &Values, rows: &[usize], total: usize) -> Result<()> { let given = rows.len(); if at.len().checked_mul(given) != Some(values.len()) { @@ -622,36 +645,40 @@ impl Samples { self.append(at, &Values::from(dense), total) } - /// Puts `run`, held nowhere yet, in: its `coordinates` ([`coordinates`] an entry), and its values - /// for `rows` rows, entry-major, at level 0 one a row, above each row's least then, after every - /// entry's, each row's greatest. What it spans past this version's frames widens them. + /// Inserts `run`, none of whose entries may be present yet: its `coordinates` ([`coordinates`] + /// per entry) and its values for `rows` rows, entry-major. At level 0 that is one value per + /// row; above, every entry's least values come first, then every entry's greatest. Frames the + /// run covers outside this version's range extend the range. + /// + /// Errors if the level is above [`MAX_LEVEL`], the sizes do not match, the coordinates are not + /// finite and non-decreasing, the run starts before forgotten frames, or an entry is present. pub(crate) fn insert(&mut self, run: &Run, coordinates: &[f64], values: &[f32], rows: usize) -> Result<()> { let level = run.level; if level > MAX_LEVEL { - return Err(Error::invalid(format!("no level {level}: levels run to {MAX_LEVEL}"))); + return Err(Error::invalid(format!("level {level} is above the highest level, {MAX_LEVEL}"))); } let (per, sides) = (self::coordinates(level), sides(level)); if run.entries.is_empty() || run.sizes(rows) != Some((coordinates.len(), values.len())) { - return Err(Error::invalid(format!("a run of {} entries of level {level} for {rows} rows", run.len()))); + return Err(Error::invalid(format!("wrong sizes for {} level-{level} entries and {rows} rows", run.len()))); } if coordinates.iter().any(|c| !c.is_finite()) || coordinates.windows(2).any(|w| w[1] < w[0]) { return Err(Error::invalid("a run's coordinates must be finite and must not decrease")); } let frames = run.frames(); if run.entries.end.checked_mul(bucket_frames(level)).is_none() { - return Err(Error::invalid("a run past the last frame a field numbers")); + return Err(Error::invalid("the run ends past the largest frame number")); } if frames.start < self.forgotten { - return Err(Error::invalid("a run before frames the field forgot")); + return Err(Error::invalid("the run starts at a forgotten frame")); } let visible = self.visible(level); let within = run.entries.start.max(visible.start)..run.entries.end.min(visible.end); if self.axes.get(level as usize).is_some_and(|log| first_held(log, within).is_some()) { - return Err(Error::invalid(format!("entries {:?} of level {level} are held already", run.entries))); + return Err(Error::invalid(format!("entries {:?} of level {level} are already present", run.entries))); } let blocks = rows.div_ceil(PAGE_ROWS); let mut logs = self.logs_at(blocks, level); - // What a newer version grew past this one's frames stays the newer version's. + // Entries a newer version appended past this version's frames stay the newer version's. cap(&mut logs.axes[level as usize], visible.end); put(&mut logs.axes[level as usize], &run.entries, coordinates, per, FRAME_PAGE_WORDS / 2); let n = run.len(); @@ -678,7 +705,8 @@ impl Samples { Ok(()) } - /// Takes the pages holding only entries of `run` out: a span let go of. + /// Removes the pages whose entries all lie within `run`, as when a time tile span is no longer + /// needed. pub(crate) fn remove(&mut self, run: &Run) { let mut logs = self.logs(0); take_out(&mut logs.axes[run.level as usize], &run.entries); @@ -690,8 +718,8 @@ impl Samples { self.keep(logs); } - /// Writes each bucket of `level` not held whose eight entries a level below are all held: what - /// bytes leave out, as they follow from those. + /// Writes each missing bucket of `level` whose eight entries one level below are all present. + /// Decoding uses it to rebuild the buckets the binary format omits. pub(crate) fn derive(&mut self, level: u32) { let below: Vec = self.runs().into_iter().filter(|r| r.level + 1 == level).collect(); for run in below { @@ -699,12 +727,12 @@ impl Samples { for missing in self.missing(level, buckets) { let (coordinates, values, rows) = self.bucket_values(level, &missing); let run = Run { level, entries: missing }; - self.insert(&run, &coordinates, &values, rows).expect("buckets held nowhere"); + self.insert(&run, &coordinates, &values, rows).expect("only missing buckets are inserted"); } } } - /// The parts of `entries` of `level` not held. + /// The ranges of `entries` of `level` that are not present. fn missing(&self, level: u32, entries: Range) -> Vec> { let visible = self.visible(level); let held_runs = self @@ -725,8 +753,8 @@ impl Samples { out } - /// Buckets `buckets` of `level`, each from its eight entries a level below: their coordinates, and - /// their values as [`insert`](Samples::insert) takes them, with how many rows. + /// Computes buckets `buckets` of `level` from their eight entries one level below: their + /// coordinates, their values in [`insert`](Samples::insert)'s layout, and the row count. fn bucket_values(&self, level: u32, buckets: &Range) -> (Vec, Vec, usize) { let children = buckets.start * 8..buckets.end * 8; let mut coordinates = Vec::with_capacity(2 * (buckets.end - buckets.start) as usize); @@ -759,7 +787,7 @@ impl Samples { (coordinates, values, rows) } - /// Rows the row blocks' pages hold values of. + /// The number of rows the row blocks' pages store values for. fn rows(&self) -> usize { let block_rows = |b: usize| self.blocks[b].iter().filter_map(|log| log.last().map(|p| p.per as usize)).max(); (0..self.blocks.len()) @@ -768,8 +796,8 @@ impl Samples { .unwrap_or(0) } - /// `run`'s coordinates, and its values for rows `rows`, in that order, as - /// [`insert`](Samples::insert) takes them. + /// `run`'s coordinates, and its values for rows `rows` in that order, in + /// [`insert`](Samples::insert)'s layout. Missing entries read as NaN. pub(crate) fn read(&self, run: &Run, rows: &[usize]) -> (Vec, Vec) { let per = coordinates(run.level); let n = run.len(); @@ -781,7 +809,7 @@ impl Samples { at.resize(n * per, f64::NAN); let sides = sides(run.level); let mut values = vec![f32::NAN; sides * n * rows.len()]; - // Rows by block, so each page is read once for all of a block's rows. + // Group rows by row block, so each page is read once for all of a block's rows. let mut by_block: Vec<(usize, usize)> = rows.iter().enumerate().map(|(i, &row)| (row >> PAGE_BITS, i)).collect(); by_block.sort_unstable(); @@ -806,8 +834,8 @@ impl Samples { (at, values) } - /// Forgets frames before coordinate `before`; pages that hold only forgotten entries drop. True - /// when anything was forgotten. + /// Forgets frames before coordinate `before`, dropping pages that contain only forgotten + /// entries. Returns whether anything was forgotten. pub(crate) fn forget(&mut self, before: f64) -> bool { let first = self.bound(before, false).min(self.end); if first <= self.start { @@ -827,9 +855,10 @@ impl Samples { true } - /// How many entries of `level` lie before `at` (with `past`, at it too), each by its coordinate - /// `which`: by its last (1), those wholly before; by its first (0), those begun before. Where the - /// entries held of the level settle it; else where the hole holding `at` starts. + /// Counts the entries of `level` before `at` (at or before it, with `past`), comparing each + /// entry's coordinate `which`: its last (1) counts entries wholly before, its first (0) entries + /// begun before. Returns `Ok(count)` when the present entries determine it, or `Err(start)` + /// with the first entry of the gap containing `at`. fn locate(&self, level: u32, at: f64, past: bool, which: u32) -> std::result::Result { let before = |t: f64| if past { t <= at } else { t < at }; let visible = self.visible(level); @@ -859,9 +888,9 @@ impl Samples { if n > 0 || range.start == gap { Ok(range.start + n) } else { Err(gap) } } - /// The frames before `at`, or with `past` at or before it: frames where held, else the whole - /// buckets before it of the finest level held there. Where no level settles it, where the hole - /// holding it starts. + /// The number of frames before `at` (at or before it, with `past`), as an absolute frame index + /// clamped to `start..=end`. Counts frames where present, elsewhere whole buckets of the finest + /// level present. If no level determines it, returns where the gap containing `at` starts. pub(crate) fn bound(&self, at: f64, past: bool) -> u64 { let mut hole = None; for level in 0..self.axes.len() as u32 { @@ -876,9 +905,10 @@ impl Samples { hole.unwrap_or(self.start).clamp(self.start, self.end) } - /// The frames at or before `at`, as finely as the finest level settling it tells: where frames - /// are held, exactly; else the whole buckets at or before it and as far into the next as `at` lies - /// between its first and last frame's coordinates. + /// The fractional number of frames at or before `at`, as precise as the finest level that + /// determines it. Exact where frames are present; elsewhere, the whole buckets at or before + /// `at`, plus the fraction of the next bucket given by where `at` lies between its first and + /// last frame's coordinates. fn position(&self, at: f64) -> Option { (0..self.axes.len() as u32).find_map(|level| { let n = self.locate(level, at, true, 1).ok()?; @@ -890,8 +920,9 @@ impl Samples { }) } - /// The frames a trace across `across` draws: from the last frame at or before its start through - /// the first after its end, as finely as what is held tells. + /// The frames a trace over coordinates `across` draws: from the last frame at or before its + /// start through the first frame after its end, as precisely as the present levels allow. All + /// frames if either bound cannot be determined. pub(crate) fn across(&self, [x0, x1]: [f64; 2]) -> Range { let (Some(a), Some(b)) = (self.position(x0), self.position(x1)) else { return self.start..self.end }; let first = (a.floor() as u64).saturating_sub(1).max(self.start); @@ -899,7 +930,7 @@ impl Samples { first..last.max(first) } - /// The first frame held from `frame` on, of any level: where a hole ends. + /// The first frame from `frame` on that any level has present: where a gap ends. fn next_held(&self, frame: u64) -> Option { let mut next: Option = None; for (level, log) in self.axes.iter().enumerate() { @@ -912,9 +943,10 @@ impl Samples { next } - /// What covers frames `frames`, left to right: `(level, index)`, the coarsest held bucket that fits - /// at each step, or a held frame as level 0; where neither does, the finest held bucket holding the - /// frame stands in, reaching past the frames. Frames nothing holds are skipped. + /// The entries covering frames `frames`, left to right, as `(level, index)`. Each step takes + /// the coarsest present bucket that fits in the remaining frames, or a present frame (level 0). + /// If neither exists, it takes the finest present bucket containing the frame, which can extend + /// past `frames`. Frames no level has are skipped. pub(crate) fn pieces(&self, frames: Range) -> Vec<(u32, u64)> { let mut pieces = Vec::new(); let (mut f, end) = (frames.start.max(self.start), frames.end.min(self.end)); @@ -941,9 +973,9 @@ impl Samples { pieces } - /// What draws frames `frames` at `level`, left to right: runs of that level's entries where held; - /// else the nearest coarser level's entry holding them, standing in; else finer levels' entries. - /// Frames nothing holds draw nothing. + /// The runs that draw frames `frames` at `level`, left to right: that level's entries where + /// present; else, as a placeholder, the entry of the nearest coarser level that contains them; + /// else finer levels' entries. Frames no level has are not drawn. pub(crate) fn cover(&self, frames: Range, level: u32) -> Vec { let mut runs = Vec::new(); let frames = frames.start.max(self.start)..frames.end.min(self.end); @@ -983,8 +1015,8 @@ impl Samples { } } - /// Folds piece `(level, index)` of row block `block` into `extent`, a least and a greatest value per - /// row of the block, NaN until one is read. + /// Folds piece `(level, index)` of row block `block` into `extent`, which holds a least and a + /// greatest value per row of the block (NaN until a value is read). pub(crate) fn fold(&self, block: usize, (level, index): (u32, u64), extent: &mut [[f32; 2]]) { if !self.held(level, index) { return; @@ -1005,13 +1037,15 @@ impl Samples { } } - /// The last frame held at or before `at`. + /// The last frame at or before `at`. `None` if `at` lies before the first frame or in a gap in + /// the frames. pub(crate) fn frame_at(&self, at: f64) -> Option { let frame = self.locate(0, at, true, 0).ok()?.checked_sub(1)?; self.held(0, frame).then_some(frame) } - /// How far `at` lies from `frame` toward the next frame, in `0..=1`. + /// How far `at` lies from `frame` toward the next frame, in `0..=1`; 0 if the next frame is + /// missing. pub(crate) fn t(&self, frame: u64, at: f64) -> f32 { match (self.at(frame), self.at(frame + 1)) { (Some(a), Some(b)) if b > a => ((at - a) / (b - a)).clamp(0.0, 1.0) as f32, @@ -1019,7 +1053,8 @@ impl Samples { } } - /// The page holding `frame` of `block` and the word where the frame starts in it. + /// The frame page of row block `block` containing `frame`, and the word offset where that + /// frame starts in it. pub(crate) fn cursor(&self, block: usize, frame: u64) -> Option<(FramePage, u32)> { if !self.held(0, frame) { return None; @@ -1029,8 +1064,9 @@ impl Samples { Some((page, base as u32)) } - /// The pages of `block` holding any of `level`'s entries `entries`, each with those it holds: - /// frames, or a bucket's least (`side` 0) or greatest (1) values. + /// The pages of row block `block` containing any of `level`'s `entries` that this version + /// reads, each with the range it contains: frames, or buckets' least (`side` 0) or greatest (1) + /// values. pub(crate) fn pages( &self, block: usize, @@ -1043,7 +1079,8 @@ impl Samples { self.log(block, level, side).map_or_else(Vec::new, |log| held(log, entries)) } - /// The coordinate pages of `level` holding any of entries `entries`, each with those it holds. + /// The coordinate pages of `level` containing any of `entries` that this version reads, each + /// with the range it contains. pub(crate) fn axis_pages(&self, level: u32, entries: Range) -> Vec<(AxisPage, Range)> { let visible = self.visible(level); let entries = entries.start.max(visible.start)..entries.end.min(visible.end); @@ -1068,7 +1105,8 @@ impl Samples { (!value.is_nan()).then_some(f64::from(value)) } - /// `frame`'s values for the first `rows` rows into `out`; rows the frame lacks are NaN. + /// Writes `frame`'s values for the first `out.len()` rows into `out`; rows without a value are + /// NaN. fn read_frame(&self, frame: u64, out: &mut [f32]) { out.fill(f32::NAN); let rows = out.len(); @@ -1090,16 +1128,17 @@ impl Samples { let mut gathered = Samples::from_frame(frames.start); for run in runs { let (at, values) = self.read(run, rows); - gathered.insert(run, &at, &values, rows.len()).expect("runs held apart"); + gathered.insert(run, &at, &values, rows.len()).expect("runs do not overlap"); } gathered.reads(frames); gathered } } -/// After `frame` is appended to `logs`, writes each bucket it completes whose eight entries a level -/// below are all held, level by level: its first and last frame's coordinates, and per row block the -/// least of its entries' least values and the greatest of their greatest. +/// After `frame` is appended to `logs`, writes each bucket it completes, level by level, while the +/// bucket's eight entries one level below are all present. A bucket stores its first and last +/// frame's coordinates and, per row block, the least of its entries' least values and the greatest +/// of their greatest values. fn seal(logs: &mut Logs, frame: u64) { let next = frame + 1; for level in 1..=LEVELS { @@ -1150,7 +1189,7 @@ mod tests { let v = if t == 70 { -5.0 } else { f64::from(t) }; samples.append(&[f64::from(t)], &Values::from(vec![v, 2.0 * v]), 2).expect("frame"); } - assert!(samples.held(1, 15) && !samples.held(1, 16), "130 frames seal 16 buckets of 8"); + assert!(samples.held(1, 15) && !samples.held(1, 16), "130 frames complete 16 buckets of 8"); assert!(samples.held(2, 1) && !samples.held(2, 2), "and 2 of 64"); // Bucket 8 spans frames 64..72, where frame 70 dips to -5. assert_eq!(samples.entry(0, 1, 0, 8), Some(-5.0)); @@ -1193,12 +1232,13 @@ mod tests { } #[test] - fn an_older_version_writing_past_its_end_keeps_the_newer_ones_frames_out() { + fn an_older_version_writing_past_its_end_ignores_newer_frames() { let mut newest = Samples::default(); newest.append(&[0.0, 1.0], &Values::from(vec![1.0_f64, 2.0]), 1).expect("frames"); let mut older = newest.clone(); newest.append(&[2.0, 3.0], &Values::from(vec![3.0_f64, 4.0]), 1).expect("frames"); - // The older version holds frames 0 and 1; frames 4 and 5 widen it past a hole at 2 and 3. + // The older version has frames 0 and 1; inserting frames 4 and 5 extends it, leaving a gap + // at 2 and 3. older.insert(&Run { level: 0, entries: 4..6 }, &[4.0, 5.0], &[5.0, 6.0], 1).expect("a run"); assert_eq!(older.value(0, 2), None, "the newer version's frame 2 is not the older one's"); assert_eq!(older.value(0, 4), Some(5.0)); @@ -1225,7 +1265,7 @@ mod tests { samples.append(&[100.0], &Values::from(vec![1.0_f32; rows]), rows).expect("frame"); assert_eq!(samples.end(), 101); let run = Run { level: 0, entries: 10..11 }; - assert!(samples.insert(&run, &[10.0], &vec![0.0; rows], rows).is_err(), "frames forgotten stay so"); + assert!(samples.insert(&run, &[10.0], &vec![0.0; rows], rows).is_err(), "forgotten frames stay forgotten"); } #[test] @@ -1253,22 +1293,22 @@ mod tests { } #[test] - fn runs_fill_any_span_and_cover_draws_the_finest_held() { + fn runs_fill_any_span_and_cover_draws_the_finest_stored() { let (mut samples, buckets, frames) = holed(); assert_eq!((samples.start(), samples.end()), (0, 32)); let (at, values) = samples.read(&frames, &[0, 1]); - assert!(samples.insert(&frames, &at, &values, 2).is_err(), "held already"); + assert!(samples.insert(&frames, &at, &values, 2).is_err(), "already present"); assert_eq!( samples.cover(0..32, 0), vec![Run { level: 1, entries: 0..1 }, Run { level: 0, entries: 8..16 }, Run { level: 1, entries: 2..4 },], - "frames where held, else the buckets standing in" + "frames where stored, buckets elsewhere" ); assert_eq!(samples.cover(0..32, 1), vec![buckets.clone()]); assert_eq!(samples.entry(1, 1, 1, 2), Some(8.0)); assert_eq!(samples.value(0, 9), Some(9.0)); assert_eq!(samples.frame_at(12.5), Some(12)); - assert_eq!(samples.frame_at(20.0), None, "frame 20 is not held"); - assert_eq!(samples.bound(20.0, false), 16, "past the frames, whole buckets count"); + assert_eq!(samples.frame_at(20.0), None, "frame 20 is not stored"); + assert_eq!(samples.bound(20.0, false), 16, "past the stored frames, bounds fall on whole buckets"); assert_eq!(samples.pieces(0..32), vec![(1, 0), (1, 1), (1, 2), (1, 3)]); let read = samples.read(&buckets, &[0, 1]); let mut copy = Samples::default(); @@ -1278,7 +1318,7 @@ mod tests { let stored = vec![frames.clone(), Run { level: 1, entries: 0..1 }, Run { level: 1, entries: 2..4 }]; assert_eq!(samples.stored_runs(), stored); samples.remove(&frames); - assert!(!samples.held(0, 9), "a span let go of"); + assert!(!samples.held(0, 9), "a removed span is no longer stored"); assert_eq!(samples.cover(0..32, 0), vec![buckets]); } @@ -1306,7 +1346,7 @@ mod tests { } #[test] - fn appends_after_a_run_seal_only_whole_buckets() { + fn appends_after_a_run_seal_only_complete_buckets() { let mut samples = Samples::default(); let run = Run { level: 0, entries: 4..8 }; samples.insert(&run, &[4.0, 5.0, 6.0, 7.0], &[1.0, 2.0, 3.0, 4.0], 1).expect("run"); diff --git a/crates/latkit/src/data/snapshot.rs b/crates/latkit/src/data/snapshot.rs index 8ec8dc52..fe92838c 100644 --- a/crates/latkit/src/data/snapshot.rs +++ b/crates/latkit/src/data/snapshot.rs @@ -10,11 +10,12 @@ use super::table::{Field, Table}; use super::values::Value; use crate::{Error, Result}; -/// One version of a dataset: immutable, O(1) to keep, and sharing every page with the versions -/// before and after it. [`Data::snapshot`](crate::Data::snapshot) takes one; a view given one draws -/// it as it is. +/// One immutable version of a dataset. Taking one is O(1), and it shares unchanged pages with the +/// versions before and after it. [`Data::snapshot`](crate::Data::snapshot) takes one; a view given +/// one draws that version unchanged. /// -/// It reads alike whatever came of it since: what it holds described, values, queries, and envelopes. +/// Later changes to the dataset never affect it: its description, values, queries, and envelopes +/// stay the same. /// /// ``` /// # use latkit::{Data, Patch, Table, Value}; @@ -36,10 +37,12 @@ pub(crate) struct Inner { /// Sorted by name. pub(crate) tables: Vec
    , pub(crate) version: Version, - /// The version the dataset began at, which every version applied from it shares: a view eases - /// rows from one of them to the next, and replaces the rows of another dataset of the same name. + /// The version the dataset started from, shared by every version patches derive from it. A + /// view matches rows across versions with the same origin to animate them; for a same-named + /// dataset with another origin, every old row leaves and every new row enters. pub(crate) origin: Version, - /// Changes when a table, field, or kind is added or changed; marks recompile only then. + /// A hash of the table names and the fields' names, kinds, and targets. Marks recompile only + /// when it changes. pub(crate) schema: u64, } @@ -67,7 +70,7 @@ impl Snapshot { self.0.axis.as_ref().map(|(name, unit)| (name.as_str(), unit.as_str())) } - /// Every table, by name. + /// Every table, sorted by name. pub(crate) fn tables(&self) -> impl Iterator + '_ { self.0.tables.iter() } @@ -77,7 +80,7 @@ impl Snapshot { self.0.table(name) } - /// The axis coordinates the sampled fields span. + /// The first and last axis coordinates held by any sampled field. pub(crate) fn span(&self) -> Option<[f64; 2]> { let spans = self.0.tables.iter().flat_map(|t| &t.fields).filter_map(|(_, field)| match field { Field::Sampled(samples, _) => samples.span(), @@ -86,9 +89,9 @@ impl Snapshot { spans.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])]) } - /// The value of `field` for the row of `table` with `id`; `None` where it is missing. A sampled - /// field reads its last frame at or before axis coordinate `at`, its last frame unless given; a - /// reference reads the row it names. + /// The value of `field` for the row of `table` with `id`, or `None` if the table, row, field, + /// or value is missing. A sampled field reads its last frame at or before axis coordinate `at`, + /// or its last frame when `at` is `None`. A reference returns the [`Row`] it names. pub fn get(&self, table: &str, id: impl Into, field: &str, at: impl Into>) -> Option { let rows = self.table(table)?; let index = rows.row(id.into().as_ref())?; @@ -102,9 +105,9 @@ impl Snapshot { } } - /// The version `patch` makes of this one: every change in it, or none of them when one is invalid, - /// as an error whose path names the change and part, such as `patch[1].Bus.kv`. A patch to another - /// dataset is an error. + /// The version `patch` makes from this one. Applies every change or, if one is invalid, none, + /// returning an error whose path names the change and part, such as `patch[1].Bus.kv`. A patch + /// for another dataset is an error. pub(crate) fn applied(&self, patch: Patch) -> Result { if let Some(data) = &patch.data && *data != self.0.name @@ -118,7 +121,7 @@ impl Snapshot { let mut ops = patch.ops.into_iter().enumerate().peekable(); while let Some((i, op)) = ops.next() { let op = match op { - // A run of appends to the same fields of every row of a table records as one. + // Merge consecutive appends to the same fields of every row of a table into one. Op::Append { table, at, ids: None, fields } => { let rows = next.table(&table).map(Table::len); let mut run = Appends::new(table, at, fields); @@ -140,7 +143,7 @@ impl Snapshot { Ok(Snapshot(Arc::new(next))) } - /// The patch to this dataset that builds it from an empty one. + /// The patch that builds this dataset from an empty one. It names this dataset. pub fn to_patch(&self) -> Patch { let mut patch = Patch::to(self.0.name.clone()); if let Some((name, unit)) = &self.0.axis { @@ -149,27 +152,29 @@ impl Snapshot { self.0.tables.iter().fold(patch, |patch, table| patch.replace(table.clone())) } - /// The dataset as bytes: the patch to it that builds it. + /// Encodes the dataset as bytes: its [`to_patch`](Snapshot::to_patch), encoded. pub fn to_bytes(&self) -> Vec { self.to_patch().to_bytes() } - /// The dataset the bytes of a patch to a dataset build. + /// Decodes a dataset from bytes written by [`to_bytes`](Snapshot::to_bytes). Errors unless the + /// bytes hold a patch that names a dataset. pub(crate) fn from_bytes(bytes: &[u8]) -> Result { let patch = Patch::from_bytes(bytes)?; let Some(name) = patch.data.clone() else { - return Err(Error::invalid("the bytes hold a patch to no dataset").at("bytes")); + return Err(Error::invalid("the bytes hold a patch that names no dataset").at("bytes")); }; Snapshot::new(name).applied(patch) } - /// The same tables as dataset `name`: another dataset, which views draw apart from this one. + /// The same tables as a new dataset named `name`, with a new origin, so views treat it as a + /// separate dataset. pub(crate) fn renamed(&self, name: impl Into) -> Snapshot { let version = Version::next(); Snapshot(Arc::new(Inner { name: name.into(), version, origin: version, ..(*self.0).clone() })) } - /// Which version of its dataset this is: two snapshots of the same version are the same. + /// This snapshot's version. Snapshots with the same version are equal. pub(crate) fn version(&self) -> Version { self.0.version } @@ -178,14 +183,15 @@ impl Snapshot { self.0.schema } - /// The version the dataset began at; see [`Inner::origin`]. + /// The version the dataset started from; see [`Inner::origin`]. pub(crate) fn origin(&self) -> Version { self.0.origin } - /// Changes sampled field `field` of table `table` with `change`, starting it empty where the table - /// has no field of its name: what time tiles put in and let go of. Whether anything changed: not - /// where `change` says so, or where there is no such table or the field holds no samples. + /// Updates sampled field `field` of table `table` with `change`, starting from empty samples if + /// the field does not exist. Time tiles use it to insert and remove spans. Returns whether + /// anything changed: false if `change` returns false, the table does not exist, or the field is + /// not sampled. pub(crate) fn sampled(&mut self, table: &str, field: &str, change: impl FnOnce(&mut Samples) -> bool) -> bool { let Ok(i) = self.0.position(table) else { return false }; let mut samples = match self.0.tables[i].field_named(field) { @@ -245,7 +251,7 @@ impl Inner { Ok(()) } - /// A deterministic digest of every table's name and every field's name, kind, and target. + /// A deterministic hash of every table's name and every field's name, kind, and target. fn signature(&self) -> u64 { let mut hasher = FxHasher::default(); for table in &self.tables { diff --git a/crates/latkit/src/data/source.rs b/crates/latkit/src/data/source.rs index 14db03af..2e76c0f6 100644 --- a/crates/latkit/src/data/source.rs +++ b/crates/latkit/src/data/source.rs @@ -1,5 +1,5 @@ -//! What views draw from: a [`Data`] changing in place, which every view drawing it follows; a -//! [`Snapshot`], one version kept as it is; or [`Tiles`](crate::Tiles), fetched in pieces. +//! Dataset sources for views: a [`Data`] that changes in place and that views follow, a fixed +//! [`Snapshot`], or [`Tiles`](crate::Tiles) fetched in pieces. use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use std::task::Waker; @@ -10,8 +10,9 @@ use super::snapshot::Snapshot; use crate::{Error, Result}; /// A dataset that changes in place: named tables of columns, changed only by patches. Clones share -/// it, and a view drawing it draws its latest version from the frame after a patch applies. A -/// [`Snapshot`] keeps one version as it was, which costs only the pages later versions write. +/// the same dataset. A view drawing it shows the latest version from the frame after a patch +/// applies. A [`Snapshot`] keeps one version unchanged, costing only the pages that later versions +/// rewrite. /// /// ``` /// # use latkit::{Data, Ids, Patch, Table}; @@ -33,7 +34,8 @@ pub struct Data(Arc); struct Shared { name: Name, current: Mutex, - /// What wakes each view drawing the dataset, by view: a patch applied asks each for a frame. + /// A waker per view drawing the dataset, keyed by view id. Applying a patch wakes each one to + /// request a frame. watchers: Mutex>, } @@ -48,8 +50,8 @@ impl Data { &self.0.name } - /// Applies every change in `patch`, or none of them when one is invalid. A patch to another - /// dataset is an error. Every view drawing the dataset draws the new version from its next frame. + /// Applies every change in `patch`, or none if any is invalid. A patch for another dataset is + /// an error. Views drawing the dataset show the new version from their next frame. /// /// The error's path names the change and part, such as `patch[1].Bus.kv`. pub fn apply(&self, patch: impl Into) -> Result<()> { @@ -60,13 +62,14 @@ impl Data { Ok(()) } - /// The version the dataset holds now, kept as it is while patches change the dataset: O(1). + /// The current version, which later patches do not change. O(1). pub fn snapshot(&self) -> Snapshot { self.current().clone() } - /// Makes `snapshot`, a version of this dataset, the one it holds: what an undo does. Every view - /// drawing the dataset draws it from its next frame. A snapshot of another dataset is an error. + /// Makes `snapshot`, a version of this dataset, the current version, as an undo does. Views + /// drawing the dataset show it from their next frame. A snapshot of another dataset (by name) + /// is an error. pub fn restore(&self, snapshot: &Snapshot) -> Result<()> { if snapshot.name() != self.name() { return Err(Error::invalid(format!("a snapshot of '{}' restored to '{}'", snapshot.name(), self.name()))); @@ -76,7 +79,8 @@ impl Data { Ok(()) } - /// Asks view `view` for a frame through `waker` whenever a patch applies. + /// Registers `waker` to request a frame from view `view` whenever a patch applies, replacing + /// that view's previous waker. #[cfg(feature = "gpu")] pub(crate) fn watch(&self, view: u64, waker: Waker) { let mut watchers = self.0.watchers.lock().unwrap_or_else(PoisonError::into_inner); @@ -84,7 +88,7 @@ impl Data { watchers.push((view, waker)); } - /// Stops asking view `view` for frames. + /// Removes view `view`'s waker. #[cfg(feature = "gpu")] pub(crate) fn unwatch(&self, view: u64) { self.0.watchers.lock().unwrap_or_else(PoisonError::into_inner).retain(|(v, _)| *v != view); @@ -94,7 +98,7 @@ impl Data { self.0.current.lock().unwrap_or_else(PoisonError::into_inner) } - /// Asks every view drawing the dataset for a frame. + /// Wakes every view drawing the dataset to request a frame. fn wake(&self) { let wakers: Vec = self.0.watchers.lock().unwrap_or_else(PoisonError::into_inner).iter().map(|(_, w)| w.clone()).collect(); @@ -105,7 +109,7 @@ impl Data { } impl From for Data { - /// A dataset holding `snapshot`, which patches change from there. + /// A dataset whose current version is `snapshot`; patches apply from there. fn from(snapshot: Snapshot) -> Data { Data(Arc::new(Shared { name: snapshot.name().clone(), @@ -121,23 +125,23 @@ impl std::fmt::Debug for Data { } } -/// What a view draws a dataset from, by the dataset's name: a [`Data`], drawn as it changes; a -/// [`Snapshot`], drawn as it is; or [`Tiles`](crate::Tiles), fetched in pieces as the view asks for +/// Where a view gets a dataset, matched by the dataset's name: a [`Data`], drawn as it changes; a +/// [`Snapshot`], drawn unchanged; or [`Tiles`](crate::Tiles), fetched in pieces as the view needs /// them. #[cfg(feature = "gpu")] #[derive(Clone, Debug)] pub enum Source { /// A dataset drawn as it changes. Data(Data), - /// A version drawn as it is. + /// A fixed version, drawn unchanged. Snapshot(Snapshot), - /// Pieces fetched as the view asks for them. + /// Pieces fetched as the view needs them. Tiles(crate::Tiles), } #[cfg(feature = "gpu")] impl Source { - /// The name of the dataset it gives. + /// The name of the dataset it provides. pub fn name(&self) -> &Name { match self { Source::Data(data) => data.name(), diff --git a/crates/latkit/src/data/table.rs b/crates/latkit/src/data/table.rs index 90b41880..eb22731f 100644 --- a/crates/latkit/src/data/table.rs +++ b/crates/latkit/src/data/table.rs @@ -37,27 +37,28 @@ pub(crate) enum Field { Sampled(Samples, Version), } -/// A field naming rows of another table by id. Row indices derive from the ids and the target's rows. +/// A field referencing rows of another table by id. Target row numbers are resolved from the ids +/// against the target table. #[derive(Clone)] pub(crate) struct Reference { pub(crate) to: Name, pub(crate) ids: Column, - /// Target row of each row as `u32`; missing where unwired or the id names no row. Its pages align - /// with the pages of `ids`. + /// Target row of each row as `u32`; missing where unwired or the id names no row. Its pages + /// align with the pages of `ids`. pub(crate) rows: Column, - /// What each page of `rows` was resolved from. + /// The inputs each page of `rows` was resolved from, used to detect stale pages. pub(crate) resolved: Arc<[Resolved]>, pub(crate) version: Version, - /// The rows naming each target row, built on first use from `rows`, and shared by every snapshot - /// holding them. + /// The rows that reference each target row. Built from `rows` on first use and shared by every + /// snapshot with the same `rows`. pub(crate) inverse: Arc>, } -/// A reference row's target where it names no row. +/// The target row of a reference row that names no row. pub(crate) const UNWIRED: u32 = u32::MAX; -/// The rows naming each target row of a reference: target `t`'s are `rows[starts[t]..starts[t + 1]]`, -/// in row order. +/// The rows that reference each target row: target `t`'s are `rows[starts[t]..starts[t + 1]]`, in +/// row order. #[derive(Default)] pub(crate) struct Inverse { starts: Vec, @@ -65,7 +66,7 @@ pub(crate) struct Inverse { } impl Inverse { - /// The rows naming target row `target`. + /// The rows that reference target row `target`. pub(crate) fn of(&self, target: usize) -> &[u32] { match (self.starts.get(target), self.starts.get(target + 1)) { (Some(&start), Some(&end)) => &self.rows[start as usize..end as usize], @@ -101,8 +102,8 @@ impl Field { } } - /// The field of rows `rows`, in that order: a reference names the same targets by id, resolved - /// again once applied; samples keep every frame. + /// The field for rows `rows`, in that order. A reference keeps its target ids, and its target + /// rows are resolved again when the result is applied; samples keep every frame. pub(crate) fn gather(&self, rows: &[usize]) -> Field { match self { Field::Values(column, _) => Field::Values(column.gather(rows), Version::next()), @@ -123,7 +124,7 @@ impl Reference { Reference { to, ids, rows, resolved: Arc::new([]), version: Version::next(), inverse: Arc::default() } } - /// The target row row `row` names. + /// The target row that row `row` references, if it is wired. pub(crate) fn target(&self, row: usize) -> Option { match self.rows.get(row)? { Value::Number(n) => Some(n as u32), @@ -131,8 +132,8 @@ impl Reference { } } - /// The row row `row` names, [`UNWIRED`] where it names none or lies past the rows: read where its - /// page stores it, as [`targets`](Reference::targets) reads every row. + /// The target row that row `row` references, or [`UNWIRED`] if none or past the rows. Reads the + /// page's `u32` storage directly, as [`targets`](Reference::targets) does for every row. pub(crate) fn target_row(&self, row: usize) -> u32 { let Some((values, i)) = self.rows.at(row) else { return UNWIRED }; match (&values.array, &values.valid) { @@ -148,7 +149,8 @@ impl Reference { } } - /// Every row's target row, [`UNWIRED`] where it names none: read page by page, not value by value. + /// Every row's target row, or [`UNWIRED`] where it names none. Reads page by page rather than + /// value by value. pub(crate) fn targets(&self) -> Vec { let mut out = Vec::with_capacity(self.rows.len()); for page in self.rows.pages() { @@ -167,8 +169,8 @@ impl Reference { out } - /// The rows naming each target row: counted, the counts summed, then each row written at its - /// target's place. + /// The rows that reference each target row. Built once by counting the rows per target, + /// prefix-summing the counts, then writing each row into its target's range. pub(crate) fn inverse(&self) -> &Inverse { self.inverse.get_or_init(|| { let targets = self.targets(); @@ -203,7 +205,7 @@ impl Reference { .any(|(page, r)| r.ids != page.id || r.order != order || (r.dangling && r.structure != structure)) } - /// Resolves the pages whose inputs changed against `target`. + /// Resolves against `target` the pages whose inputs changed, reusing the rest. pub(crate) fn resolve(&self, target: &Table) -> Result { let index = target.index()?; let (order, structure) = (target.order, target.structure); @@ -237,7 +239,8 @@ impl Reference { } impl Table { - /// A table named `name` with rows `ids`: a count for rows `0..n`, or a list of numbers or texts. + /// A table named `name` with rows `ids`: a count for rows `0..n`, or a list of numbers or + /// texts. pub fn new(name: impl Into, ids: impl Into) -> Table { let version = Version::next(); Table { @@ -250,7 +253,8 @@ impl Table { } } - /// Adds or replaces field `name`: one value per row. + /// Adds or replaces field `name`, with one value per row. A length mismatch is an error when + /// the table is applied. #[must_use] pub fn field(mut self, name: impl Into, values: impl Into) -> Table { let column = Column::new(&values.into()); @@ -258,7 +262,9 @@ impl Table { self } - /// Adds or replaces field `name` naming rows of table `to` by id; `None` leaves a row unwired. + /// Adds or replaces field `name`, referencing rows of table `to` by id, one id per row. `None` + /// leaves a row unwired. A missing table `to` is an error when the table is applied; an id that + /// matches no row stays unwired until such a row exists. #[must_use] pub fn reference(mut self, name: impl Into, to: impl Into, ids: impl Into) -> Table { let ids = match RowIds::new(&ids.into()) { @@ -289,25 +295,25 @@ impl Table { (0..self.len()).filter_map(|row| self.ids.id(row)) } - /// Every field's name and kind, by name. + /// Every field's name and kind, sorted by name. pub fn fields(&self) -> impl Iterator + '_ { self.fields.iter().map(|(name, field)| (name, field.kind())) } - /// What field `name` holds. + /// The kind of field `name`, if it exists. pub fn kind(&self, name: &str) -> Option { self.field_named(name).map(Field::kind) } - /// The value of `field` for the row with `id`. A sampled field reads its last frame; a reference - /// reads the target's id. + /// The value of `field` for the row with `id`, or `None` if missing. A sampled field reads its + /// last frame; a reference returns the target's id. pub fn get(&self, id: impl Into, field: &str) -> Option { let row = self.row(id.into().as_ref())?; self.value(row, field, f64::INFINITY) } - /// Every row's value of `field`, in row order: a reference's as the target ids, a sampled - /// field's at its last frame. + /// Every row's value of `field`, in row order: target ids for a reference, the last frame for + /// a sampled field. pub fn values(&self, field: &str) -> Option { Some(match self.field_named(field)? { Field::Values(column, _) => column.values(), @@ -327,8 +333,7 @@ impl Table { } } - /// The table of rows `rows` of this one, in that order, with every field: what removing rows - /// keeps, and what a query reads back. + /// A table of rows `rows` of this one, in that order, with every field. Removing rows uses it. pub(crate) fn rows(&self, rows: &[usize]) -> Table { let version = Version::next(); Table { @@ -366,7 +371,8 @@ impl Table { } } - /// Checks that every field has one value per row and the ids are present and unique. + /// Checks that the table has a name, every row has a unique id, and every field except a + /// sampled one has one value per row. pub(crate) fn check(&self) -> Result<()> { if self.name.is_empty() { return Err(Error::invalid("a table needs a name")); diff --git a/crates/latkit/src/data/tests.rs b/crates/latkit/src/data/tests.rs index 4eac86f5..fda7f272 100644 --- a/crates/latkit/src/data/tests.rs +++ b/crates/latkit/src/data/tests.rs @@ -1,4 +1,5 @@ -//! Patches applied to a dataset agree with a plain model of rows, whatever the sequence. +//! Dataset tests, including a property test that any sequence of patches leaves a dataset matching +//! a plain model of its rows. use proptest::prelude::*; @@ -134,7 +135,7 @@ fn a_failed_patch_changes_nothing() { } #[test] -fn appends_one_after_another_record_as_one_and_name_their_own_errors() { +fn consecutive_appends_merge_and_report_their_own_errors() { let table = || Table::new("Bus", vec![1_u64, 2]).field("kv", vec![230.0, 115.0]); let one = Data::new("grid"); one.apply(Patch::new().replace(table()).append( @@ -150,7 +151,7 @@ fn appends_one_after_another_record_as_one_and_name_their_own_errors() { patch = patch.append("Bus", [f64::from(f)], [("vm", vec![v + 1.0, v + 2.0])]); } each.apply(patch).expect("valid"); - assert_eq!(each.snapshot().to_bytes(), one.snapshot().to_bytes(), "a frame a change records as the frames in one"); + assert_eq!(each.snapshot().to_bytes(), one.snapshot().to_bytes(), "appends of one frame each merge into one"); let error = each .apply( Patch::new() @@ -158,7 +159,7 @@ fn appends_one_after_another_record_as_one_and_name_their_own_errors() { .append("Bus", [4.0], [("vm", vec![1.0_f32])]) .append("Bus", [5.0], [("vm", vec![1.0_f32, 1.0])]), ) - .expect_err("a frame short of a row"); + .expect_err("a frame missing a row's value"); assert!(error.to_string().starts_with("patch[1].Bus.vm"), "{error}"); } @@ -185,7 +186,7 @@ fn references_to_unknown_tables_are_refused() { } #[test] -fn dangling_references_wire_up_when_the_row_arrives() { +fn dangling_references_resolve_once_the_row_exists() { let data = Data::new("grid"); data.apply(Patch::new().set(Table::new("Bus", vec![1_u64])).set(Table::new("Branch", Ids::count(1)).reference( "to", @@ -225,13 +226,13 @@ fn inverses_follow_every_change_and_snapshots_keep_theirs() { let now = data.snapshot(); assert_eq!((inverse(&now, 0), inverse(&now, 2)), (vec![0, 2], vec![1, 3])); data.apply(Patch::new().remove("Bus", vec![1_u64])).expect("valid"); - // Bus 1 left: what named it names nothing, and the rest moved up a row. + // Bus 1 is gone: the rows that referenced it are unwired, and the later buses moved up a row. let now = data.snapshot(); assert_eq!((inverse(&now, 0), inverse(&now, 1)), (vec![], vec![1, 3])); assert_eq!(inverse(&before, 0), vec![0, 2]); } -/// A dataset of `rows` rows of `Line` with a `flow` sampled over `frames` frames, one a second. +/// A dataset of `rows` rows of `Line` with a `flow` sampled over `frames` frames, one per second. fn sampled(rows: usize, frames: usize) -> Snapshot { let data = Data::new("grid"); let at: Vec = (0..frames).map(|f| f as f64).collect(); @@ -265,7 +266,7 @@ fn describe_names_tables_fields_and_spans() { } #[test] -fn envelopes_read_levels_as_frames_would() { +fn envelopes_match_a_scan_of_the_frames() { let (rows, frames) = (3, 700); let data = sampled(rows, frames); let envelope = |field: &str, range: [f64; 2], spans: usize, ids: Option>| { @@ -340,7 +341,7 @@ fn queries_filter_sort_page_and_select() { .expect("a query"); assert_eq!(names(&data.query(&json).expect("a query")), [text("b"), text("d")]); let named = data.query(&Query::of("Branch").ids([3_u64, 1, 9])).expect("by ids"); - assert_eq!(names(&named), [text("d"), text("b")], "in the order named, unknown ids passed over"); + assert_eq!(names(&named), [text("d"), text("b")], "in the order given, unknown ids skipped"); let lake = data.query(&Query::of("Branch").filter(field("name").contains("C"))).expect("contains"); assert_eq!(names(&lake), [text("c")], "ignoring case"); assert!(data.query(&Query::of("Nope")).is_err()); @@ -360,7 +361,7 @@ fn queries_read_sampled_fields_at_a_coordinate() { } #[test] -fn ids_too_large_for_json_numbers_cross_as_text() { +fn ids_too_large_for_json_numbers_are_written_as_text() { let big = (1_u64 << 53) + 1; let data = Data::new("grid"); data.apply(Table::new("Bus", vec![big, 7]).field("kv", vec![1.0, 2.0])).expect("valid"); @@ -368,7 +369,7 @@ fn ids_too_large_for_json_numbers_cross_as_text() { assert_eq!(serde_json::to_string(&Id::Number(7)).expect("an id"), "7"); let now = data.snapshot(); let text = Id::from(big.to_string()); - assert_eq!(now.get("Bus", text, "kv", None), Some(Value::Number(1.0)), "the text names the row"); + assert_eq!(now.get("Bus", text, "kv", None), Some(Value::Number(1.0)), "the decimal text finds the row"); assert_eq!(now.get("Bus", big, "kv", None), Some(Value::Number(1.0))); let counted = Table::new("Count", Ids::count(3)).field("x", vec![0.0, 1.0, 2.0]); data.apply(counted).expect("valid"); @@ -387,7 +388,7 @@ fn schema_changes_only_with_fields() { } #[test] -fn rows_appended_by_id_leave_the_rest_holding() { +fn appending_by_id_keeps_other_rows_last_values() { let data = Data::new("grid"); data.apply(Patch::new().set(Table::new("Bus", [10_u64, 20, 30])).append( "Bus", @@ -397,19 +398,19 @@ fn rows_appended_by_id_leave_the_rest_holding() { .expect("valid"); data.apply(Patch::new().append_rows("Bus", [1.0], [("vm", vec![0.9])], [30_u64])).expect("by id"); let vm = |id: u64| data.snapshot().get("Bus", id, "vm", 1.0); - assert_eq!(vm(10), Some(Value::Number(1.0)), "held"); + assert_eq!(vm(10), Some(Value::Number(1.0)), "other rows keep their last value"); assert_eq!(vm(30), Some(Value::Number(f64::from(0.9_f32))), "recorded"); let error = data.apply(Patch::new().append_rows("Bus", [2.0], [("vm", vec![1.0])], [40_u64])); - assert!(error.is_err_and(|e| e.to_string().contains("no row")), "rows appended to are rows the table holds"); + assert!(error.is_err_and(|e| e.to_string().contains("no row")), "appending to an unknown id fails"); } #[test] -fn a_patch_to_a_dataset_changes_it_alone() { +fn a_patch_applies_only_to_its_named_dataset() { let grid = Data::new("grid"); - grid.apply(Patch::to("grid").set(Table::new("Bus", Ids::count(1)))).expect("its own"); + grid.apply(Patch::to("grid").set(Table::new("Bus", Ids::count(1)))).expect("its own dataset"); let error = grid.apply(Patch::to("weather").set(Table::new("Cell", Ids::count(1)))); assert!(error.is_err_and(|e| e.to_string().contains("weather"))); let bytes = grid.snapshot().to_bytes(); assert_eq!(Patch::from_bytes(&bytes).expect("a patch").data().map(crate::Name::as_str), Some("grid")); - assert!(Snapshot::from_bytes(&Patch::new().to_bytes()).is_err(), "a dataset is a patch to one"); + assert!(Snapshot::from_bytes(&Patch::new().to_bytes()).is_err(), "dataset bytes must name the dataset"); } diff --git a/crates/latkit/src/data/text.rs b/crates/latkit/src/data/text.rs index b78958e5..59c24117 100644 --- a/crates/latkit/src/data/text.rs +++ b/crates/latkit/src/data/text.rs @@ -27,7 +27,7 @@ impl Text { (0..self.len()).map(|i| self.get(i)) } - /// The UTF-8 bytes the slice spans and offsets relative to them. + /// The offsets rebased to start at 0, and the UTF-8 text they index, for encoding a slice. pub(crate) fn compact(&self) -> (Vec, &str) { let base = self.offsets.first().copied().unwrap_or(0); let end = self.offsets.last().copied().unwrap_or(0); diff --git a/crates/latkit/src/data/values.rs b/crates/latkit/src/data/values.rs index 4d7ac964..31f9c169 100644 --- a/crates/latkit/src/data/values.rs +++ b/crates/latkit/src/data/values.rs @@ -31,7 +31,7 @@ pub(crate) enum Array { Geometry(Geometry), } -/// What a field holds. +/// The kind of values a field holds. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)] #[serde(rename_all = "lowercase")] pub enum Kind { @@ -51,12 +51,13 @@ pub enum Kind { Polygons, /// Rows of another table, by id. Reference, - /// Numbers sampled over the data's axis. + /// Numbers sampled over the dataset's axis. Sampled, } impl Kind { - /// The geometry of `depth` levels below its points: 0 a point, 1 a path, 2 parts, 3 polygons. + /// The geometry kind of nesting depth `depth`: 0 a point, 1 a path, 2 parts, 3 or more + /// polygons. pub(crate) fn geometry(depth: u8) -> Kind { match depth { 0 => Kind::Point, @@ -66,7 +67,7 @@ impl Kind { } } - /// How many levels a geometry nests below its points; `None` for any other kind. + /// A geometry kind's nesting depth; `None` for other kinds. pub(crate) fn depth(self) -> Option { match self { Kind::Point => Some(0), @@ -99,8 +100,8 @@ pub enum Value { Row(Row), } -/// A value writes as JSON: a number (`null` when not finite), boolean, or text; a geometry as nested -/// `[x, y]` points; a reference as the id of the row it names. +/// Serializes as JSON: a number (`null` when not finite), boolean, or text; a geometry as nested +/// `[x, y]` points; a reference as its target row's id. impl serde::Serialize for Value { fn serialize(&self, serializer: S) -> Result { match self { @@ -117,7 +118,7 @@ impl serde::Serialize for Value { } } -/// A value as JSON writes it. +/// The schema of a value's JSON form. impl schemars::JsonSchema for Value { fn schema_name() -> std::borrow::Cow<'static, str> { "Value".into() @@ -135,15 +136,15 @@ impl schemars::JsonSchema for Value { } } -/// Where each value an assembly makes comes from, among its sources: the first, then pages of -/// [`PAGE_ROWS`](super::column::PAGE_ROWS) rows each. A concrete type rather than a closure, so -/// [`Values::assemble`] is compiled once. +/// Where each output value of [`Values::assemble`] comes from. Source 0 is the first source; the +/// sources after it are pages of [`PAGE_ROWS`](super::column::PAGE_ROWS) rows, addressed by row. A +/// concrete type rather than a closure, so [`Values::assemble`] is compiled only once. #[derive(Clone, Copy)] pub(crate) enum Picks<'a> { /// Value `i` is row `rows[i]` of the pages. Rows(&'a [usize]), - /// Value `i` is row `from[i]` of the pages where it is written, else row `i` of the first source - /// while `i < kept`, else missing. + /// Value `i` is row `from[i]` of the pages where it is written, else row `i` of the first + /// source while `i < kept`, else missing. Writes { from: &'a [Option], kept: usize }, } @@ -168,8 +169,8 @@ impl Picks<'_> { } } - /// The values in runs, in order: each a source and the first of its rows one after another, or - /// `None` for missing values, and how many. + /// The picks grouped into runs, in order. Each run is a source and its first row (consecutive + /// rows follow), or `None` for missing values, with the run's length. fn runs(self) -> Vec<(Option<(usize, usize)>, usize)> { let mut runs: Vec<(Option<(usize, usize)>, usize)> = Vec::new(); for i in 0..self.len() { @@ -204,7 +205,7 @@ impl Values { self.len() == 0 } - /// What the values are. + /// The kind of the values. pub fn kind(&self) -> Kind { match &self.array { Array::F64(_) | Array::F32(_) | Array::I32(_) | Array::U32(_) | Array::U64(_) => Kind::Number, @@ -214,7 +215,7 @@ impl Values { } } - /// Whether value `i` is present. + /// Whether value `i` exists and is not missing. pub fn is_valid(&self, i: usize) -> bool { i < self.len() && self.valid.as_ref().is_none_or(|v| v.get(i)) } @@ -232,7 +233,8 @@ impl Values { }) } - /// Value `i` as a row id, exactly: `None` when missing, or neither a whole number nor a text. + /// Value `i` as a row id: `None` when missing, or when the values are not stored as `u64` + /// numbers or texts. pub fn id(&self, i: usize) -> Option { super::ids::key(self, i).map(super::name::IdRef::to_id) } @@ -242,7 +244,7 @@ impl Values { (self.kind() == Kind::Number).then(|| self.map_numbers(|x| x.unwrap_or(f64::NAN))) } - /// A zero-copy slice. + /// A slice of the values, without copying. Panics if `range` is out of bounds. #[must_use] pub fn slice(&self, range: Range) -> Values { let array = match &self.array { @@ -258,8 +260,8 @@ impl Values { Values { array, valid: self.valid.as_ref().map(|v| v.slice(range)) } } - /// Every value as `number` reads it, `None` where missing, mapped by `f`: a loop per storage, so - /// the storage is matched once rather than per value. + /// Maps every value, read as by `number` (`None` where missing), through `f`. One loop per + /// storage type, so the storage is matched once rather than per value. pub(crate) fn map_numbers(&self, mut f: impl FnMut(Option) -> T) -> Vec { macro_rules! each { ($v:expr, $number:expr) => { @@ -279,8 +281,8 @@ impl Values { } } - /// Calls `f` with every value as `number` reads it, `None` where missing: a loop per storage, - /// allocating nothing. + /// Calls `f` with every value, read as by `number` (`None` where missing). One loop per storage + /// type, with no allocation. pub(crate) fn each_number(&self, mut f: impl FnMut(Option)) { macro_rules! each { ($v:expr, $number:expr) => { @@ -300,7 +302,7 @@ impl Values { } } - /// Value `i` as a number; booleans are 0 or 1, others NaN. + /// Value `i` as a number; booleans are 0 or 1, texts and geometries NaN. pub(crate) fn number(&self, i: usize) -> f64 { match &self.array { Array::F64(v) => v[i], @@ -327,14 +329,14 @@ impl Values { } } - /// Points its geometries take as shaders read them: every coordinate, and a break between a row's - /// parts or rings; none where it holds no geometry. + /// The number of points shaders read for the present geometries: every coordinate, plus one + /// break between a row's parts or rings. 0 for values that are not geometries. pub(crate) fn points(&self) -> usize { let Some(geometry) = self.geometry() else { return 0 }; (0..self.len()).filter(|&row| self.is_valid(row)).map(|row| geometry.row_points(row)).sum() } - /// Geometries as values, every one present. + /// Values holding `geometry`, none missing. pub(crate) fn of_geometry(geometry: Geometry) -> Values { Values { array: Array::Geometry(geometry), valid: None } } @@ -350,7 +352,7 @@ impl Values { if let Some(other) = sources.iter().find(|s| s.kind() != kind) { return Err(Error::invalid(format!("{:?} values for a {kind:?} field", other.kind()))); } - // Rows one after another in one source copy as a run. + // Consecutive rows from one source copy as a run. let runs = picks.runs(); macro_rules! numbers { ($variant:ident, $t:ty, $store:expr) => {{ @@ -404,7 +406,8 @@ impl Values { Array::Geometry(builder.finish()) } }; - // Every value present where every source's is and none is missing. + // No validity bits are needed when no pick is missing and no picked source has missing + // values. let gaps = runs.iter().any(|(run, _)| run.is_none_or(|(s, _)| sources[s].valid.is_some())); if !gaps { return Ok(Values { array, valid: None }); @@ -421,8 +424,8 @@ impl Values { Ok(Values { array, valid: (!all).then(|| bits.finish()) }) } - /// Whether `other` holds exactly these values in the same storage, compared where they are stored: - /// floats by their bits, so a NaN equals itself, and no value is made to compare it. + /// Whether `other` holds exactly these values in the same storage. Compares the stored + /// representation without building any [`Value`]: floats by their bits, so NaN equals itself. pub(crate) fn same(&self, other: &Values) -> bool { let len = self.len(); if other.len() != len || (0..len).any(|i| self.is_valid(i) != other.is_valid(i)) { @@ -442,9 +445,8 @@ impl Values { } } - /// Whether value `i` and `other`'s value `j` are the same, compared where they are stored: floats by - /// their bits, so a NaN equals itself, and missing values alike; values in different storages - /// differ. + /// Whether value `i` equals `other`'s value `j` as stored: floats by their bits, so NaN equals + /// itself. Two missing values are equal; values in different storages differ. pub(crate) fn same_row(&self, i: usize, other: &Values, j: usize) -> bool { let valid = self.is_valid(i); if valid != other.is_valid(j) { @@ -466,8 +468,8 @@ impl Values { } } - /// `parts` one after another, in the first part's storage: what a column's pages make, a copy per - /// part with nothing picked. Every part holds the first's kind. + /// `parts` concatenated in the first part's storage, converting numbers from other storages. + /// Joins a column's pages by copying each part whole. Every part must have the first's kind. pub(crate) fn concat(parts: &[&Values]) -> Values { let first = parts.first().expect("a part"); debug_assert!(parts.iter().all(|p| p.kind() == first.kind())); @@ -704,9 +706,10 @@ impl From>>>> for Values { } } -/// A column of values of one kind, `None` missing, as JSON and JavaScript arrays give them: the first -/// present value decides what the rest are, numbers, booleans, texts, or geometries nesting alike. -/// A column of nothing present is numbers; a missing point is NaN, a missing path or polygon empty. +/// Builds a column from values of one kind, with `None` for missing, as JSON and JavaScript arrays +/// provide them. The first present value sets the kind: numbers, booleans, texts, or geometries of +/// equal nesting. A column with no present values is numbers. A missing point becomes NaN, and a +/// missing path, parts, or polygons value becomes empty. Mixed kinds and [`Value::Row`] are errors. impl TryFrom>> for Values { type Error = Error; @@ -714,7 +717,7 @@ impl TryFrom>> for Values { fn all(values: Vec>, of: impl Fn(Value) -> Option) -> Result>> { let mixed = || { Error::invalid( - "a column holds one kind of value: numbers, booleans, texts, or geometries nesting alike", + "a column must hold one kind of value: numbers, booleans, texts, or geometries of one depth", ) }; values.into_iter().map(|v| v.map(|v| of(v).ok_or_else(mixed)).transpose()).collect() @@ -743,7 +746,7 @@ impl TryFrom>> for Values { let polygons = all(values, |v| if let Value::Polygons(p) = v { Some(p) } else { None })?; polygons.into_iter().map(Option::unwrap_or_default).collect::>().into() } - Value::Row(_) => return Err(Error::invalid("a column names rows as a reference: {reference, ids}")), + Value::Row(_) => return Err(Error::invalid("rows must be written as a reference: {reference, ids}")), }) } } @@ -784,7 +787,7 @@ mod tests { } #[test] - fn same_compares_what_is_stored() { + fn same_compares_the_stored_representation() { let a = Values::from(vec![Some(1.0_f64), None, Some(f64::NAN)]); assert!(a.same(&a.slice(0..3)), "a NaN equals itself"); assert!(!a.same(&Values::from(vec![Some(1.0_f64), Some(0.0), Some(f64::NAN)])), "missing differs"); diff --git a/crates/latkit/src/derive/graph.rs b/crates/latkit/src/derive/graph.rs index 4756e6d5..565e9e00 100644 --- a/crates/latkit/src/derive/graph.rs +++ b/crates/latkit/src/derive/graph.rs @@ -1,6 +1,6 @@ -//! The graph the marks draw: one vertex mark and one edge mark per table, and where edges meet -//! vertices, by their two ends or by the ports naming a net. Layouts link vertices by it, a diagram's -//! wires run along it, and nets' stars join it. +//! The graph the scene's marks draw: one vertex mark and one edge mark per table, and how edges +//! meet vertices, either by two endpoint references or through ports that reference a net. +//! Layouts, a diagram's wires, and the stars of nets all read it. use crate::data::{Field, Inverse, Name, Reference, Row, Snapshot, Table, Value}; use crate::expr::cpu::Rows; @@ -8,7 +8,7 @@ use crate::host::Key; use crate::scene::mark::Style; use crate::{Edges, Mark, Port, Vertices}; -/// The vertex mark a table's rows take their places from, and the vertex index of its first row. +/// The vertex mark that places a table's rows, with the vertex index of the table's first row. pub(crate) struct VertexMark<'a> { pub(crate) mark: &'a Vertices, pub(crate) data: &'a Snapshot, @@ -16,15 +16,15 @@ pub(crate) struct VertexMark<'a> { pub(crate) first: usize, } -/// The edge mark a table's rows take their paths from. +/// The edge mark that draws a table's rows as paths. pub(crate) struct EdgeMark<'a> { pub(crate) mark: &'a Edges, pub(crate) data: &'a Snapshot, pub(crate) table: &'a Table, } -/// The vertex marks and edge marks of the scene, one per table: its unnamed mark of the kind, else -/// its first; each vertex mark with the vertex index of its first row. +/// The scene's vertex and edge marks, one of each kind per table: the table's unnamed mark of that +/// kind, else its first. Vertex indices number the rows of every vertex mark in turn. pub(crate) fn marks<'a>(input: &super::Input<'a>) -> (Vec>, Vec>) { let (mut vertices, mut edges): (Vec>, Vec>) = (Vec::new(), Vec::new()); for mark in &input.scene.marks { @@ -59,23 +59,25 @@ pub(crate) fn marks<'a>(input: &super::Input<'a>) -> (Vec>, Vec(mut styles: impl Iterator, style: &Style) -> Option { styles.position(|s| s.data == style.data && s.table == style.table) } -/// Whether the mark of `style` takes its table from the one of `before`: an unnamed mark from a -/// named one. +/// Whether the mark of `style` replaces the mark of `before` for their table: an unnamed mark +/// replaces a named one. fn replaces(before: &Style, style: &Style) -> bool { before.name.is_some() && style.name.is_none() } -/// The vertex mark drawing `table` of dataset `data`, which ends and members attach to. +/// The index of the vertex mark drawing `table` of dataset `data`, which edge endpoints and group +/// members attach to. pub(crate) fn drawing(vertex_marks: &[VertexMark<'_>], data: &Name, table: &Name) -> Option { vertex_marks.iter().position(|vm| vm.data.name() == data && vm.table.name() == table) } -/// The position row `row`'s data gives it, if any: its position, else its point geometry. +/// The position row `row`'s data gives it: from the mark's position fields, else its point geometry +/// (finite points only). `None` where the data gives none. pub(crate) fn position(mark: &Vertices, rows: &Rows<'_>, row: usize) -> Option<[f64; 2]> { match &mark.position { Some([x, y]) => Some([rows.number(x, row)?, rows.number(y, row)?]), @@ -91,7 +93,8 @@ pub(crate) fn key_of(row: &Row) -> Key { (row.table.clone(), row.id.clone()) } -/// What a layout links: each edge's two ends, and each net from its outputs to its inputs, by vertex. +/// The links a layout reads, by vertex index: each edge's two endpoints (self-loops dropped), and +/// each net's links from its outputs to its inputs (see `net_links`). pub(crate) fn links(marks: &[VertexMark<'_>], edge_marks: &[EdgeMark<'_>]) -> Vec<[u32; 2]> { let codes: Vec> = marks.iter().map(|vm| port_codes(vm.mark)).collect(); let mut links = Vec::new(); @@ -112,15 +115,15 @@ pub(crate) fn links(marks: &[VertexMark<'_>], edge_marks: &[EdgeMark<'_>]) -> Ve links } -/// Where an edge mark's rows meet vertices: the references naming each row's two ends, with the -/// first vertex of the table each names; or, for nets, every port reference naming its rows. +/// How an edge mark's rows meet vertices: the two endpoint references, each with the first vertex +/// index of the table it references; or, for a net table, every port reference to its rows. pub(crate) struct Joins<'a> { ends: Option<[(&'a Reference, usize); 2]>, ports: Vec>, } -/// A port reference naming a net table's rows: the vertex mark, the port's code and whether it is an -/// output, and the rows naming each net. +/// A port reference to a net table's rows: the vertex mark, the port's code, whether the port is an +/// output, and the inverse index from each net to the rows that reference it. struct Naming<'a> { mark: usize, code: u32, @@ -153,14 +156,15 @@ impl<'a> Joins<'a> { Joins { ends: None, ports } } - /// Row `row`'s two ends, by vertex. + /// Row `row`'s two endpoints, as vertex indices. `None` for a net table, or where either + /// reference is unset. pub(crate) fn ends(&self, row: usize) -> Option<[usize; 2]> { let [(a, first_a), (b, first_b)] = self.ends?; Some([first_a + a.target(row)? as usize, first_b + b.target(row)? as usize]) } - /// Net row `row`'s terminals: each vertex naming it at a port, the port's code, and whether it is - /// an output. + /// Net row `row`'s terminals: each vertex whose port references it, with the port's code and + /// whether the port is an output. pub(crate) fn terminals(&self, marks: &[VertexMark<'_>], row: usize) -> Vec<(usize, u32, bool)> { let mut terminals = Vec::new(); for naming in &self.ports { @@ -171,14 +175,15 @@ impl<'a> Joins<'a> { } } -/// Each port's code on a vertex mark's rows. +/// The code of each port of a vertex mark. pub(crate) fn port_codes(mark: &Vertices) -> Vec { Port::codes(&mark.ports.iter().map(|(_, port)| port).collect::>()) } -/// The links of a net whose terminals are `ends`, each a vertex and whether it drives the net: every -/// driver to its first reader, and its first driver to every reader, so a wide net adds as many links -/// as it has terminals. A net without both drivers and readers runs from its first terminal. +/// The links of a net whose terminals are `ends`, each a vertex and whether it drives the net. +/// Every driver links to the first reader, and the first driver to every other reader, so a net +/// adds about one link per terminal. A net lacking drivers or readers treats its first terminal as +/// the only driver. Self-links are dropped. fn net_links(ends: &[(u32, bool)]) -> Vec<[u32; 2]> { let (mut drivers, mut readers): (Vec, Vec) = (Vec::new(), Vec::new()); for &(vertex, drives) in ends { @@ -200,6 +205,6 @@ mod tests { #[test] fn nets_link_their_drivers_to_their_readers() { assert_eq!(net_links(&[(0, false), (1, true), (2, false)]), vec![[1, 0], [1, 2]]); - assert_eq!(net_links(&[(4, false), (5, false)]), vec![[4, 5]], "without a driver, the first drives"); + assert_eq!(net_links(&[(4, false), (5, false)]), vec![[4, 5]], "without a driver, the first terminal drives"); } } diff --git a/crates/latkit/src/derive/lanes.rs b/crates/latkit/src/derive/lanes.rs index 4d94152f..0f41f981 100644 --- a/crates/latkit/src/derive/lanes.rs +++ b/crates/latkit/src/derive/lanes.rs @@ -1,9 +1,13 @@ -//! Lanes: outside a diagram, each edge row's lane among the edges joining the same two vertices, its -//! family, so they bow apart instead of drawing over each other. A lane is data, made from the table's -//! ends alone: the stroke bows in world space by its lane, so it never moves as the camera does. A row's -//! lane and its family's size share one word: twice its lane, offset by `HALF`, above, and the size -//! below. Derived again only when the table's rows or ends change: on the frame while that fits its -//! budget, else in a job; a table whose edges share no ends derives nothing. +//! Lanes of parallel edges, outside diagrams. +//! +//! Edges joining the same two vertices form a family. Each edge row gets a lane within its family, +//! so the strokes bow apart instead of drawing over each other. Lanes depend only on the table's +//! endpoints, and strokes bow in world space, so lanes never change as the camera moves. +//! +//! A row's lane and its family size share one 32-bit word: twice the lane plus `HALF` in the high +//! 16 bits, the family size in the low 16. Lanes are recomputed only when the table's rows or +//! endpoints change: on the frame when that fits the budget, else in a job. A table whose edges +//! share no endpoints gets no lane field. use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; @@ -13,20 +17,20 @@ use crate::Pending; use crate::data::{Field, Keyed, Name, Reference, Table, UNWIRED, Values}; use crate::host::Job; -/// Microseconds a row takes to find its lane, by estimate. +/// Estimated microseconds to compute one row's lane. const LANE_COST: f64 = 0.06; -/// What a lane word adds to twice the lane, so it is never negative; and the most a family's size -/// says, past which it says this. +/// The offset added to twice the lane, so the high half of a lane word is never negative. const HALF: u32 = 1 << 15; +/// The largest family size a lane word stores; larger families store this. const MOST: u32 = (1 << 16) - 1; -/// What a table's lanes were made from, and a job under way. +/// The stamp a table's lanes were made from, and any pending job. type Made = Derived<(), Pending>>; #[derive(Default)] pub(crate) struct Lanes { tables: Tables, - /// Each table's lanes, made or making. + /// Each table's lane state: made, or pending in a job. made: Keyed<(Name, Name), Made>, } @@ -84,8 +88,9 @@ impl Derive for Lanes { } } -/// Keeps `lanes`, a lane word each row, beside `table` as its field `lane`, or drops its lanes when -/// there are none; whether anything changed. +/// Writes `lanes`, one lane word per row, as field `lane` of the table beside `table`. Drops that +/// table when there are no lanes or their count differs from the rows. Returns whether it wrote or +/// dropped a table. fn keep(tables: &mut Tables, key: (Name, Name), table: &Table, lanes: Option>) -> bool { match lanes.filter(|l| l.len() == table.len()) { Some(lanes) => { @@ -106,13 +111,15 @@ fn reference<'t>(table: &'t Table, name: &str) -> Option<&'t Reference> { } } -/// The lane words a lanes job answered, a little-endian `u32` each; none where it answered nothing. +/// The lane words in a lanes job's answer, each a little-endian `u32`. `None` for an empty answer, +/// which means no rows share endpoints. fn answered(bytes: &[u8]) -> Option> { (!bytes.is_empty()).then(|| bytes.as_chunks::<4>().0.iter().map(|&w| u32::from_le_bytes(w)).collect()) } -/// Each row's lane word, as shaders read it, from the rows' two ends: twice its lane offset by `HALF` -/// above, its family's size below. `None` when no two rows share their ends. +/// Each row's lane word, as shaders read it, from the rows' endpoints: twice the lane plus `HALF` +/// in the high 16 bits, the family size (at most `MOST`) in the low 16. `None` when no two rows +/// share endpoints. pub(crate) fn words(from: &[u32], to: &[u32]) -> Option> { let lanes = lanes(from, to)?; Some( @@ -126,11 +133,12 @@ pub(crate) fn words(from: &[u32], to: &[u32]) -> Option> { ) } -/// Each row's lane among the rows joining the same two vertices, from the rows' two ends -/// ([`UNWIRED`] where a reference names no row): offsets from the middle, signed so rows joining them -/// either way bow apart, with how many rows join them. `None` when no two rows share their ends. -/// Counts each pair once, then hands out lanes only to the pairs shared, so it allocates nothing per -/// pair. +/// Each row's lane among the rows joining the same two vertices, and the size of that family, from +/// the rows' endpoints ([`UNWIRED`] where a reference is unset). A lane is an offset from the +/// family's middle, negated for rows running the other way, so every row of a family bows apart. +/// A row with no parallel rows (including unset rows and self-loops) gets lane 0 in a family of 1. +/// `None` when no two rows share endpoints. It counts each pair in one map, then assigns lanes only +/// to shared pairs, so it allocates nothing per pair. pub(crate) fn lanes(from: &[u32], to: &[u32]) -> Option> { let key = |r: usize| { let (a, b) = (from[r], to[r]); @@ -164,8 +172,8 @@ pub(crate) fn lanes(from: &[u32], to: &[u32]) -> Option> { ) } -/// Each of `rows` rows' lane among the rows joining the same two ends, which `ends` names: the -/// sheet's straight wires spread by it. `None` when no two share their ends. +/// Each of `rows` rows' lane among the rows joining the same two vertices, where `ends` gives each +/// row's endpoints. The sheet spreads straight wires by lane. `None` if no rows share endpoints. pub(crate) fn spread(rows: usize, ends: impl Fn(usize) -> Option<[usize; 2]>) -> Option> { let (from, to): (Vec, Vec) = (0..rows).map(|r| ends(r).map_or((UNWIRED, UNWIRED), |[a, b]| (a as u32, b as u32))).unzip(); @@ -177,20 +185,20 @@ mod tests { use super::{HALF, UNWIRED, lanes, words}; #[test] - fn rows_joining_the_same_ends_bow_apart_either_way() { + fn edges_joining_the_same_vertices_bow_apart_in_either_direction() { // Rows 0, 1, and 3 join vertices 1 and 2, row 3 the other way; row 2 is alone. - let lanes = lanes(&[1, 1, 4, 2, UNWIRED], &[2, 2, 5, 1, 0]).expect("shared ends"); + let lanes = lanes(&[1, 1, 4, 2, UNWIRED], &[2, 2, 5, 1, 0]).expect("rows share endpoints"); assert_eq!(lanes, vec![(-1.0, 3), (0.0, 3), (0.0, 1), (-1.0, 3), (0.0, 1)]); } #[test] - fn a_lane_word_holds_twice_the_lane_and_the_family() { - let words = words(&[1, 1], &[2, 2]).expect("shared ends"); - assert_eq!(words, vec![(HALF - 1) << 16 | 2, (HALF + 1) << 16 | 2], "lanes of half either side, of two"); + fn a_lane_word_holds_twice_the_lane_and_the_family_size() { + let words = words(&[1, 1], &[2, 2]).expect("rows share endpoints"); + assert_eq!(words, vec![(HALF - 1) << 16 | 2, (HALF + 1) << 16 | 2], "lanes -0.5 and 0.5 in a family of two"); } #[test] - fn tables_sharing_no_ends_have_no_lanes() { + fn rows_sharing_no_endpoints_get_no_lanes() { assert_eq!(lanes(&[0, 1, 2], &[1, 2, 0]), None); } } diff --git a/crates/latkit/src/derive/matches.rs b/crates/latkit/src/derive/matches.rs index cc723268..bae9d9dd 100644 --- a/crates/latkit/src/derive/matches.rs +++ b/crates/latkit/src/derive/matches.rs @@ -1,23 +1,25 @@ -//! Matches: whether each row's text field holds a text a mark asks about, `field.contains(text)`, -//! ignoring case, as fields derived beside the mark's own table, which the GPU reads as booleans. -//! Texts stay on the CPU, so the view works them out here: again only when the field or the rows -//! change, and a page whose matches held keeps its column page, so only what changed uploads. +//! Text matches: for each `field.contains(text)` a mark uses, whether each row's text field +//! contains `text`, ignoring case. The results are boolean fields of a table beside the mark's own +//! table, which the GPU reads. Texts stay on the CPU, so matches are computed here, again only when +//! the field or the rows change. A page whose matches are unchanged keeps its column page, so only +//! changed pages upload. use std::sync::Arc; use super::{Derive, Input, Jobs, Tables, beside, kept}; use crate::data::{Field, Keyed, Name, Table, Values, Version}; -/// The derived field holding whether each row's text field `field` holds `text`, ignoring case. +/// The name of the derived field holding whether each row's text field `field` contains `text`, +/// ignoring case. pub(crate) fn contains_of(field: &str, text: &str) -> Name { format!("{field}.contains({text})").into() } -/// What a table's matches were made from: its rows, and each field and text asked about with the -/// field's version. +/// What a table's matches were made from: its structure version, and each field and text tested, +/// with the field's version. type Made = (Version, Vec<(Name, Arc, Option)>); -/// The fields of each table asked whether they hold a text, and the texts, in the order its marks ask. +/// For each table, the distinct field and text pairs its marks test with `contains`, in mark order. type Asked = Keyed<(Name, Name), Vec<(Name, Arc)>>; #[derive(Default)] @@ -83,8 +85,9 @@ impl Derive for Matches { } } -/// Whether each row of `table`'s text field `field` holds `text`, ignoring case; a missing text, or a -/// field that holds none, holds nothing. Read page by page, each text in place. +/// Whether each row of `table`'s text field `field` contains `text`, ignoring case. Missing +/// values, and fields that are missing or not text, match nothing. Reads page by page without +/// copying texts. fn holds(table: &Table, field: &str, text: &str) -> Values { let needle = text.to_lowercase(); let mut out = Vec::with_capacity(table.len()); @@ -112,7 +115,7 @@ mod tests { use crate::{Ids, Value}; #[test] - fn rows_hold_a_text_ignoring_case() { + fn rows_contain_a_text_ignoring_case() { let names = vec![Some("Lake Travis"), None, Some("LAKE"), Some("Café")]; let table = Table::new("Lake", Ids::count(4)).field("name", names); let rows = diff --git a/crates/latkit/src/derive/mod.rs b/crates/latkit/src/derive/mod.rs index 594fad89..4a2eaee1 100644 --- a/crates/latkit/src/derive/mod.rs +++ b/crates/latkit/src/derive/mod.rs @@ -1,15 +1,17 @@ -//! Derived data: what the view works out rather than is given. Every derivation shares one lifecycle: -//! a stamp of what it reads, so it works again only when that changes; work small enough for the -//! frame done on it, within one budget all derivations share; larger work a host job answering a -//! patch, kept only while what it was asked from still holds; and tables it writes beside the tables -//! marks draw, row for row, which marks read like data. What the GPU works out (normals, segment -//! counts, label placement) follows the same stamps, its answers read back. +//! Derived data: tables the view computes from the scene and data instead of receiving them. //! -//! The derivations: a diagram's sheet (places, sizes, boxes, wires); elsewhere the lanes of edges -//! sharing their ends, the stars of nets, and where the scene has a layout the places of vertex rows -//! without positions; the shapes of geometry fields; the triangles of every area; whether texts hold -//! what marks ask them about; and what the view draws of the tiles it is given, the pieces the camera -//! asks for. +//! Every derivation follows one lifecycle. It stamps its inputs and recomputes only when the stamp +//! changes. Small work runs on the frame, within one budget ([`FRAME_WORK`]) that all derivations +//! share. Larger work runs as a host job, and its result is kept only while its inputs are +//! unchanged. Results are tables beside the tables marks draw, row for row, which marks read like +//! data. Values the GPU computes (normals, segment counts, label placement) use the same stamps and +//! are read back. +//! +//! The derivations: a diagram's sheet (places, sizes, group boxes, wires). Outside diagrams: +//! lanes of edges sharing both endpoints, stars of nets, and, when the scene has a layout, places +//! of vertex rows without positions. Everywhere: shapes of geometry fields (every area's +//! triangles), text matches for `contains`, the pieces of given tile sets the camera needs, and +//! what each paged table draws. pub(crate) mod graph; mod lanes; @@ -39,16 +41,16 @@ use crate::tiles::TileSets; use crate::{Pending, Result, Scene, Space}; use places::Places; -/// A stamp of `inputs`: never 0, which no value is made from. +/// Hashes `inputs` into a stamp. Never 0, which means no value has been made yet. pub(crate) fn stamp_of(inputs: impl Hash) -> u64 { let mut hasher = FxHasher::default(); inputs.hash(&mut hasher); hasher.finish().max(1) } -/// Work that lands later: bytes the GPU reads back, or a job or fetch the host does. +/// Work whose bytes arrive later: a GPU readback, or a host job or fetch. pub(crate) trait Answer { - /// The bytes once they landed, taking them; `None` while they have not. + /// Takes the bytes once they arrive; `None` until then. fn take(&self) -> Option>>; } @@ -64,8 +66,7 @@ impl Answer for Pending> { } } -/// A derived product: its value, the stamp it was made from, and the work under way toward the value -/// of a newer stamp. +/// A derived value, the stamp it was made from, and any pending work toward a newer stamp. pub(crate) struct Derived { pub(crate) value: T, stamp: u64, @@ -79,36 +80,36 @@ impl Default for Derived { } impl Derived { - /// The stamp the value was made from; 0 before it was. + /// The stamp the value was made from; 0 before the first value. pub(crate) fn stamp(&self) -> u64 { self.stamp } - /// Whether `stamp` is neither what the value was made from nor what work under way is for. + /// Whether `stamp` matches neither the value nor the pending work, so new work is needed. pub(crate) fn due(&self, stamp: u64) -> bool { stamp != self.stamp && self.working.as_ref().is_none_or(|(s, _)| *s != stamp) } - /// Takes `value` as made from `stamp`, dropping any work under way. + /// Stores `value` as made from `stamp`, dropping any pending work. pub(crate) fn make(&mut self, stamp: u64, value: T) { self.value = value; self.stamp = stamp; self.working = None; } - /// Takes the value as made from `stamp` as it stands, dropping any work under way. + /// Marks the current value, unchanged, as made from `stamp`, dropping any pending work. pub(crate) fn mark(&mut self, stamp: u64) { self.stamp = stamp; self.working = None; } - /// Starts `work` toward the value of `stamp`, replacing any under way. + /// Records `work` as pending toward `stamp`, replacing any pending work. pub(crate) fn start(&mut self, stamp: u64, work: A) { self.working = Some((stamp, work)); } - /// The bytes of work that landed, and the stamp it was for. Work that failed lands as made, - /// keeping the value, so it is not tried again until what it reads changes. + /// Takes the bytes of finished work and the stamp it was for. Failed work marks its stamp as + /// made and keeps the old value, so it is not retried until the inputs change. pub(crate) fn landed(&mut self) -> Option<(u64, Vec)> { let (stamp, work) = self.working.as_ref()?; let stamp = *stamp; @@ -121,30 +122,30 @@ impl Derived { Some((stamp, bytes)) } - /// Whether work is under way. + /// Whether work is pending. pub(crate) fn busy(&self) -> bool { self.working.is_some() } } -/// Microseconds of work, by estimate, the derivations of a GPU's views do on one frame between them; -/// what does not fit goes to the host as jobs. +/// Estimated microseconds of derivation work all of a GPU's views may do on one frame together. +/// Work that does not fit goes to the host as jobs. pub(crate) const FRAME_WORK: u32 = 4000; -/// The frame's budget of inline work and the host's pool, which every derivation shares. Each job -/// and fetch wakes the view when it lands. +/// The frame's inline work budget and the host's job pool, shared by every derivation. Each job and +/// fetch wakes the view when it finishes. pub(crate) struct Jobs<'a> { errors: &'a Arc>>, host: Option<&'a dyn Host>, waker: &'a Waker, - /// What counts the jobs handed out. + /// Counts the jobs spawned. counters: &'a Counters, - /// Microseconds of inline work left this frame, by estimate. + /// Estimated microseconds of inline work left this frame. left: u32, } impl<'a> Jobs<'a> { - /// The jobs of a frame with `left` microseconds of work on it still to spend. + /// A frame's jobs, with `left` microseconds of inline work still to spend. pub(crate) fn new( host: Option<&'a dyn Host>, waker: &'a Waker, @@ -155,7 +156,7 @@ impl<'a> Jobs<'a> { Jobs { errors, host, waker, counters, left } } - /// Whether work of `cost` microseconds fits what is left of the frame's budget, spending it if so. + /// Whether `cost` microseconds fit the rest of the frame's budget; spends them if so. pub(crate) fn fits(&mut self, cost: u64) -> bool { let fits = cost <= u64::from(self.left); if fits { @@ -164,28 +165,29 @@ impl<'a> Jobs<'a> { fits } - /// Microseconds of inline work left this frame, by estimate. + /// Estimated microseconds of inline work left this frame. pub(crate) fn left(&self) -> u64 { u64::from(self.left) } - /// What its jobs and fetches wake as they land. + /// The waker its jobs and fetches wake when they finish. pub(crate) fn waker(&self) -> &Waker { self.waker } - /// The same host's jobs and fetches, waking `waker` as they land in place of this frame's view, - /// with none of the frame's budget: work done for every view drawing what it brings. + /// The same host's jobs and fetches, but waking `waker` instead of this frame's view, and with + /// no inline budget. Used for work shared by every view that draws its result. pub(crate) fn waking<'b>(&'b self, waker: &'b Waker) -> Jobs<'b> { Jobs { errors: self.errors, host: self.host, waker, counters: self.counters, left: 0 } } - /// Spends `cost` microseconds of the budget whether it fits or not: work the frame cannot defer. + /// Spends `cost` microseconds of the budget even when it does not fit, for work the frame + /// cannot defer. The budget stops at zero. pub(crate) fn spend(&mut self, cost: u64) { self.left = self.left.saturating_sub(cost.min(u64::from(u32::MAX)) as u32); } - /// Has the host do `job` off the frame; without a host, at once. + /// Runs `job` on the host, off the frame; without a host, runs it immediately. pub(crate) fn spawn(&self, job: Job) -> Pending> { self.counters.add(Count::Jobs, 1); let pending = match self.host { @@ -197,7 +199,7 @@ impl<'a> Jobs<'a> { pending } - /// Has the host fetch `request`; without a host, nothing can be fetched. + /// Fetches `request` through the host. Without a host, the fetch fails as unsupported. pub(crate) fn fetch(&self, request: &Request) -> Pending> { let pending = match self.host { Some(host) => host.fetch(request), @@ -208,22 +210,24 @@ impl<'a> Jobs<'a> { } } -/// What every derivation reads: the scene and its data, the host that measures text, whether rows -/// are being dragged, and what each table drawn in part draws. Jobs go through the frame's [`Jobs`]. +/// The inputs every derivation reads: the scene, its data, the host (which measures text), whether +/// rows are being dragged, and what each paged table draws. Jobs go through the frame's [`Jobs`]. pub(crate) struct Input<'a> { pub(crate) scene: &'a Scene, /// Bumps whenever the scene changes. pub(crate) scene_version: u64, pub(crate) data: &'a [Snapshot], pub(crate) host: Option<&'a dyn Host>, - /// Whether rows are being dragged: what they move derives on the frame, however much. + /// Whether rows are being dragged. While true, work the drag causes runs on the frame + /// regardless of budget. pub(crate) dragging: bool, - /// Each table drawn in part, by dataset and table: what row-local derivations work on alone. + /// Each paged table, by dataset and table. Row-local derivations work only on the pages and + /// kept rows listed here. pub(crate) paged: Option<&'a Keyed<(Name, Name), Paged>>, } -/// What a table drawn in part draws this frame: the order its rows draw in, the pages of it drawn, -/// and the places of the rows kept whose pages are not, each ascending. +/// What a paged table draws this frame: its order, the pages drawn, and the places of kept rows +/// outside those pages. Both lists ascend. pub(crate) struct Paged { pub(crate) order: order::Order, pub(crate) pages: Vec, @@ -236,9 +240,9 @@ impl Input<'_> { self.data.iter().find(|d| d.name() == name) } - /// Hashes what the structure of the scene's marks reads: of each table drawn, its rows and the - /// fields its places, sizes, groups, ports, wires, and nets read. A field only a look reads, such - /// as a value streaming in, changes nothing it hashes. + /// Hashes the inputs to the structure of the scene's marks: each drawn table's rows, and the + /// fields that set its places, sizes, groups, ports, wires, and nets. Fields read only for + /// appearance, such as a streaming value, do not affect the hash. pub(crate) fn structure(&self, hasher: &mut impl Hasher) { self.reads(&mut |read| match read { Read::Rows { data, table, rows } => (data, table, rows.structure, rows.order).hash(hasher), @@ -246,8 +250,8 @@ impl Input<'_> { }); } - /// Hands `read` what the structure of the scene's marks reads, in order: each table drawn, then - /// each field its places, sizes, groups, ports, wires, and nets read. + /// Calls `read` with each input to the structure of the scene's marks, in order: each drawn + /// table, then each field that sets its places, sizes, groups, ports, wires, and nets. pub(crate) fn reads<'a>(&'a self, read: &mut impl FnMut(Read<'a>)) { for mark in &self.scene.marks { let style = mark.style(); @@ -289,8 +293,9 @@ impl Input<'_> { } } - /// Hands `read` every field `expr` of a mark drawing `table` reads: a field of the table itself is - /// one it sizes or places each row by, `own`, unless a domain fits it to every row's. + /// Calls `read` with every field that `expr`, of a mark drawing `table`, reads. When `own` is + /// set, a field of `table` itself is reported as `own` (it sizes or places each row by that + /// row's value), unless a domain fits `expr` to every row's values. fn read_expr<'a>( &'a self, table: (&'a Name, &'a Name), @@ -306,38 +311,37 @@ impl Input<'_> { }); } - /// Hands `read` field `name` of `table`. + /// Calls `read` with field `name` of `table`. fn read_field<'a>(&'a self, (data, table): (&'a Name, &'a Name), name: &'a str, read: &mut impl FnMut(Read<'a>)) { let field = self.data(data).and_then(|d| d.table(table)).and_then(|t| t.field_named(name)); read(Read::Field { data, table, name, field, own: false }); } } -/// What the structure of the scene's marks reads. +/// One input to the structure of the scene's marks. pub(crate) enum Read<'a> { - /// A table a mark draws: its rows. + /// A table a mark draws. Rows { data: &'a Name, table: &'a Name, rows: &'a Table }, - /// A field: whether it exists, and as what; and whether it is `own`, a value of a vertex mark's - /// own table that sizes or places each row by that row's value alone. + /// A field, `None` where it is missing. `own` marks a field of a vertex mark's own table that + /// sizes or places each row by that row's value alone. Field { data: &'a Name, table: &'a Name, name: &'a str, field: Option<&'a Field>, own: bool }, } /// Derived tables, by the dataset and table each stands beside. pub(crate) type Tables = Keyed<(Name, Name), Table>; -/// One derivation: what it reads, stamped; what it works out, on the frame or as jobs; and the tables -/// it writes. +/// One derivation: it stamps its inputs, computes on the frame or in jobs, and writes tables. pub(crate) trait Derive { - /// Brings what it derives up to `input`, handing `jobs` what the frame cannot afford; whether its - /// tables changed. + /// Updates the derivation to `input`, sending work the frame cannot afford to `jobs`. Returns + /// whether its tables changed. fn update(&mut self, input: &Input<'_>, jobs: &mut Jobs<'_>) -> bool; - /// Whether work it asked for is under way. + /// Whether work it started is pending. fn busy(&self) -> bool; - /// Whether what it derived is final: nothing it writes stands in for work under way. Rows - /// standing where they were until a layout lands, elbows until routes land, earlier triangles - /// until shapes land, and straight edges until lanes land all stand in. + /// Whether its tables are final, with no placeholder for pending work. Placeholders include + /// rows at their old places until a layout arrives, elbows until routes arrive, rows without + /// triangles until shapes arrive, and straight edges until lanes arrive. fn complete(&self) -> bool { !self.busy() } @@ -345,42 +349,42 @@ pub(crate) trait Derive { /// The tables it derives. fn tables(&self) -> &Tables; - /// Forgets what it derived: its space no longer asks for it. + /// Drops its derived tables, once the scene's space no longer needs them. fn clear(&mut self); - /// Failures to tell since last asked, such as texts its host could not measure. + /// Takes the errors since the last call, such as texts the host could not measure. fn take_errors(&mut self) -> Vec { Vec::new() } } -/// Every derivation of a view, run in order each frame with one budget. +/// Every derivation of a view, updated in order each frame within one budget. #[derive(Default)] pub(crate) struct Derivations { /// A diagram's sheet. pub(crate) sheet: Sheet, - /// Elsewhere, the lanes of edges sharing their ends. + /// Outside diagrams, the lanes of edges that share both endpoints. lanes: lanes::Lanes, - /// Elsewhere, where the scene has a layout, the places of vertex rows without positions. + /// Outside diagrams, when the scene has a layout, the places of vertex rows without positions. places: Places, - /// Elsewhere, the stars of nets joining vertex rows. + /// Outside diagrams, the stars of nets joining vertex rows. stars: stars::Stars, shapes: shapes::Shapes, - /// Whether texts hold what marks ask them about. + /// Whether each row's text contains a string a mark asks about. matches: matches::Matches, - /// What the view draws of the tiles it is given: the pieces the camera asks for, as datasets. + /// The pieces of each given tile set the camera needs, as datasets. pub(crate) tiles: TileSets, - /// What the view draws of each dataset it draws in part. + /// The pyramids of each dataset with paged tables, and what those tables draw. pub(crate) tiled: tiled::Tiled, } -/// The derived tables beside one drawn table: the sheet's or the lanes', the shapes', then the -/// matches'. +/// The derived tables beside one drawn table: the first of the sheet's, places', stars', or lanes', +/// then the shapes', then the matches'. pub(crate) type Beside<'a> = [Option<&'a Table>; 3]; impl Derivations { - /// Brings every derivation up to `input`, spending what the tiles left of the frame's `jobs`; - /// whether any table changed. + /// Updates every derivation to `input`, spending what remains of `jobs`' frame budget after + /// the tiles. Returns whether any table changed. pub(crate) fn update(&mut self, input: &Input<'_>, jobs: &mut Jobs<'_>) -> bool { let mut changed = if matches!(input.scene.space, Space::Diagram(_)) { self.lanes.clear(); @@ -397,20 +401,20 @@ impl Derivations { changed } - /// Lays every vertex row without a position out again: a diagram's sheet, or elsewhere the - /// places. + /// Lays out every vertex row without a position again: through the sheet in a diagram, else + /// through the places. pub(crate) fn arrange(&mut self, diagram: bool) { if diagram { self.sheet.arrange() } else { self.places.arrange() } } - /// What the last arranging laid out, once it landed: each row placed, and where, in the - /// space's coordinates. + /// Takes the result of the last `arrange` once it arrives: each row laid out and its position, + /// in the space's coordinates. pub(crate) fn take_arranged(&mut self) -> Option> { self.sheet.take_arranged().or_else(|| self.places.take_arranged()) } - /// Whether everything derived is final: no layout, route, lane, or shape job stands in, no tile - /// draws its ancestor, and no table drawn in part draws its first sample. + /// Whether everything derived is final: no layout, route, lane, or shape job is pending, no + /// tile draws its ancestor as a placeholder, and no paged table draws its first sample. pub(crate) fn complete(&self) -> bool { self.sheet.complete() && self.lanes.complete() @@ -420,12 +424,12 @@ impl Derivations { && self.tiled.complete() } - /// Whether shapes are being triangulated or bent: until they land, marks draw the ones before. + /// Whether a triangulation job is pending. The rows it shapes draw nothing until it finishes. pub(crate) fn shaping(&self) -> bool { self.shapes.busy() } - /// Failures every derivation has to tell since last asked. + /// Takes the errors every derivation reported since the last call. pub(crate) fn take_errors(&mut self) -> Vec { let mut errors = self.sheet.take_errors(); errors.extend(self.places.take_errors()); @@ -442,14 +446,15 @@ impl Derivations { [first, self.shapes.tables().get(&key), self.matches.tables().get(&key)] } - /// The shapes of table `table` of dataset `data` drawn in part, a page of its order at a time, - /// by the field each is written as. + /// The shapes of paged table `table` of dataset `data`, made per page of its order, by the + /// derived field each is written as. pub(crate) fn paged(&self, data: &Name, table: &Name) -> Vec<(Name, &shapes::PagedShapes)> { self.shapes.paged(data, table) } } -/// A table beside `source`, row for row, holding `fields`. +/// A table holding `fields` that stands beside `source`, row for row: it shares `source`'s ids, +/// index, structure, and order. pub(crate) fn beside(source: &Table, fields: impl IntoIterator) -> Table { let mut table = Table { name: source.name.clone(), @@ -465,9 +470,10 @@ pub(crate) fn beside(source: &Table, fields: impl IntoIterator) -> Field { let fresh = Column::new(values); let old = before.filter(|b| b.structure == source.structure).and_then(|b| match b.field_named(name) { @@ -487,7 +493,8 @@ pub(crate) fn kept(name: &str, values: &Values, source: &Table, before: Option<& Field::Values(Column::of_pages(pages, column.like()), Version::next()) } -/// The values `fields` an answer's patch writes for each row it names, by its key. +/// Reads a job's answer patch: for each row it sets, the row's key and its values of `fields`. An +/// unreadable patch yields nothing. pub(crate) fn answers(bytes: &[u8], fields: &[&str]) -> Vec<(Key, Vec>)> { let Ok(patch) = Patch::from_bytes(bytes) else { return Vec::new() }; let mut out = Vec::new(); @@ -508,7 +515,7 @@ mod tests { use crate::data::PAGE_ROWS; #[test] - fn kept_keeps_the_pages_that_hold_the_same() { + fn kept_reuses_pages_whose_values_are_unchanged() { let rows = PAGE_ROWS + 10; let source = Table::new("T", crate::Ids::count(rows as u32)); let numbers = |changed: f64| { @@ -516,10 +523,10 @@ mod tests { }; let first = beside(&source, [(Name::from("x"), kept("x", &numbers(1.0), &source, None))]); let same = kept("x", &numbers(1.0), &source, Some(&first)); - assert_eq!(same.version(), first.field_named("x").expect("x").version(), "nothing changed"); + assert_eq!(same.version(), first.field_named("x").expect("x").version(), "unchanged values keep the version"); let Field::Values(before, _) = first.field_named("x").expect("x") else { panic!("values") }; let Field::Values(after, _) = kept("x", &numbers(2.0), &source, Some(&first)) else { panic!("values") }; - assert_eq!(after.pages()[0].id, before.pages()[0].id, "the first page held the same"); + assert_eq!(after.pages()[0].id, before.pages()[0].id, "the unchanged first page is reused"); assert_ne!(after.pages()[1].id, before.pages()[1].id, "the last page changed"); } } diff --git a/crates/latkit/src/derive/order.rs b/crates/latkit/src/derive/order.rs index 21f8142c..32518780 100644 --- a/crates/latkit/src/derive/order.rs +++ b/crates/latkit/src/derive/order.rs @@ -1,9 +1,12 @@ -//! Orders: how a table drawn in part draws its rows. Every per-row input a mark reads of such a table, -//! and every per-row word it keeps, lies in pages of its order, so the camera choosing tiles chooses -//! ranges of pages and nothing is gathered as it moves. Until its pyramid lands a table draws an even -//! sample of its rows first, after the rows the edges a sample draws end at, which land with those -//! edges as in a pyramid; a pyramid serves while the rows it was made from hold, the rows appended -//! since drawing after them in their own order. +//! Draw orders of paged tables. +//! +//! A mark lays out every per-row input and every per-row GPU word of a paged table in pages of its +//! order. Choosing tiles then chooses ranges of pages, so nothing is gathered as the camera moves. +//! +//! Until its pyramid arrives, a table draws an even sample of its rows first. The rows that sampled +//! edges end at come before the sample, as a pyramid would show them with those edges. A pyramid +//! serves while the rows it was made from are unchanged; rows appended since draw after them, in +//! table order. use std::sync::Arc; @@ -11,21 +14,20 @@ use super::pyramid::Pyramid; use super::stamp_of; use crate::gpu::shaders::PAGE_ROWS; -/// Rows a page of an order holds: a held page. - +/// A paged table's draw order: which row draws at each place. #[derive(Clone, Debug)] pub(crate) enum Order { - /// The rows `lifted` lists first; then every `step`th of the rest of a table's `rows` rows, then - /// the rest, each in their own order. + /// The rows in `lifted` first; then every `step`th row of the table's `rows` rows (those at + /// multiples of `step`) not lifted; then the remaining rows. Each group is in table order. Sampled { rows: u32, step: u32, lifted: Lifted }, - /// A pyramid's order of a table's first rows, then the rest of its `rows` rows, appended since, - /// in their own order. + /// A pyramid's order of the table's first rows, then the rows appended since, in table order, + /// up to `rows` rows. Pyramid { pyramid: Arc, rows: u32 }, } -/// The rows a sample draws before its own, as a pyramid lands a vertex with the coarsest edge ending -/// at it: those the edges a sample draws end at. Ascending, with where each draws in the sample -/// without them, ascending too. +/// The rows a sample draws before its own: the rows the sampled edges end at, just as a pyramid +/// shows a vertex at least as coarse as the coarsest edge ending at it. Holds the rows ascending, +/// each row's place in the plain sample (without lifted rows) ascending, and a stamp of the rows. #[derive(Clone, Debug, Default)] pub(crate) struct Lifted { rows: Arc<[u32]>, @@ -34,15 +36,15 @@ pub(crate) struct Lifted { } impl Lifted { - /// `rows` of a table of `len` rows lifted in a sample of `sample` rows, which they count toward; - /// and the step of the table's own even sample, which takes the rest. + /// Lifts `rows` of a table of `len` rows into a sample of `sample` rows. The lifted rows count + /// toward the sample; returns them with the step of the even sample that fills the rest. pub(crate) fn within(rows: Vec, len: u32, sample: u32) -> (Lifted, u32) { let rows = ascending(rows, len); let step = len.div_ceil(sample.saturating_sub(rows.len() as u32).max(1)).max(1); (Lifted::new(rows, len, step), step) } - /// `rows` of a table of `len` rows sampled every `step`th. + /// Lifts `rows` of a table of `len` rows into a sample taking every `step`th row. fn new(rows: Vec, len: u32, step: u32) -> Lifted { let rows = ascending(rows, len); let mut places: Vec = rows.iter().map(|&r| sample_rank(r, len, step)).collect(); @@ -56,7 +58,7 @@ impl Lifted { } } -/// `rows` of a table of `len` rows, ascending, each once. +/// The `rows` below `len`, sorted ascending without duplicates. fn ascending(mut rows: Vec, len: u32) -> Vec { rows.retain(|&r| r < len); rows.sort_unstable(); @@ -64,29 +66,30 @@ fn ascending(mut rows: Vec, len: u32) -> Vec { rows } -/// Where row `r` draws in an even sample of `rows` rows every `step`th: the sample first, then the -/// rest, each in their own order. +/// The place of row `r` in an even sample of `rows` rows taking every `step`th: the sampled rows +/// first, then the rest, each in table order. fn sample_rank(r: u32, rows: u32, step: u32) -> u32 { if r.is_multiple_of(step) { r / step } else { rows.div_ceil(step) + r - r / step - 1 } } -/// The row drawn at `d` in an even sample of `rows` rows every `step`th. +/// The row at place `d` in an even sample of `rows` rows taking every `step`th; the inverse of +/// `sample_rank`. fn sample_source(d: u32, rows: u32, step: u32) -> u32 { let first = rows.div_ceil(step); if d < first || step <= 1 { return d * step; } - // Each step's rows but its first, in their own order. + // Past the sample: each step's rows except its first, in table order. let k = d - first; (k / (step - 1)) * step + k % (step - 1) + 1 } -/// The `e`th place, from 0, that `places`, ascending, does not list. +/// The `e`th place (from 0) missing from `places`, which ascends. A binary search. fn unlisted(places: &[u32], e: u32) -> u32 { let (mut lo, mut hi) = (e, e + places.len() as u32); while lo < hi { let mid = lo + (hi - lo) / 2; - // Places up to `mid` it does not list. + // How many places up to `mid` are missing from `places`. let free = mid + 1 - places.partition_point(|&p| p <= mid) as u32; if free > e { hi = mid; @@ -98,19 +101,19 @@ fn unlisted(places: &[u32], e: u32) -> u32 { } impl Order { - /// Rows it orders: every row of its table. + /// The number of rows it orders: every row of its table. pub(crate) fn len(&self) -> u32 { match self { Order::Sampled { rows, .. } | Order::Pyramid { rows, .. } => *rows, } } - /// Pages of it. + /// The number of pages it spans. pub(crate) fn pages(&self) -> u32 { self.len().div_ceil(PAGE_ROWS) } - /// The rows a sample draws first: those lifted, then its own. + /// How many rows a sample draws first: the lifted rows plus the sample's own. 0 for a pyramid. pub(crate) fn sampled(&self) -> u32 { match self { Order::Sampled { rows, step, lifted } => { @@ -121,7 +124,7 @@ impl Order { } } - /// The row drawn at `d`. + /// The row drawn at place `d`. pub(crate) fn source(&self, d: u32) -> u32 { match self { Order::Sampled { rows, step, lifted } => match lifted.rows.get(d as usize) { @@ -132,7 +135,7 @@ impl Order { } } - /// Where row `r` draws. + /// The place where row `r` draws; the inverse of `source`. pub(crate) fn rank(&self, r: u32) -> u32 { match self { Order::Sampled { rows, step, lifted } => { @@ -146,11 +149,11 @@ impl Order { } } - /// The rows page `page` holds, in order. + /// The rows on page `page`, in order. pub(crate) fn page(&self, page: u32) -> Vec { let (start, end) = (page * PAGE_ROWS, ((page + 1) * PAGE_ROWS).min(self.len())); let Order::Sampled { rows, step, lifted } = self else { return (start..end).map(|d| self.source(d)).collect() }; - // The rows lifted it holds, then the sample's in turn, stepping past the places lifted. + // The page's lifted rows, then the sample's rows in turn, skipping lifted places. let m = lifted.len(); let mut out: Vec = lifted.rows.get(start.min(m) as usize..end.min(m) as usize).unwrap_or_default().to_vec(); @@ -169,8 +172,8 @@ impl Order { out } - /// What pages packed in it are kept by: one pyramid's whatever rows were appended since, a - /// sample's by its rows and those it lifts. + /// The key that identifies pages packed in this order. A pyramid's id ignores rows appended + /// since; a sample's depends on its row count, step, and lifted rows. pub(crate) fn id(&self) -> u64 { match self { Order::Sampled { rows, step, lifted } => stamp_of(("sampled", rows, step, lifted.stamp)), @@ -199,24 +202,24 @@ mod tests { let drawn: Vec = (0..10).map(|d| order.source(d)).collect(); assert_eq!(drawn, [0, 3, 6, 9, 1, 2, 4, 5, 7, 8]); for (d, &r) in drawn.iter().enumerate() { - assert_eq!(order.rank(r), d as u32, "row {r} draws where its rank says"); + assert_eq!(order.rank(r), d as u32, "row {r}'s rank is its place"); } assert_eq!(order.sampled(), 4); - assert_eq!(sample(4, 1).page(0), [0, 1, 2, 3], "a step of one is the rows' own order"); + assert_eq!(sample(4, 1).page(0), [0, 1, 2, 3], "a step of one keeps table order"); } #[test] - fn rows_lifted_draw_before_the_sample_and_leave_it() { + fn lifted_rows_draw_before_the_sample_and_only_once() { // Rows 4 and 6 lifted: 6 was a sample's row, 4 was not. let order = Order::Sampled { rows: 10, step: 3, lifted: Lifted::new(vec![6, 4, 6, 12], 10, 3) }; let drawn: Vec = (0..10).map(|d| order.source(d)).collect(); assert_eq!(drawn, [4, 6, 0, 3, 9, 1, 2, 5, 7, 8]); for (d, &r) in drawn.iter().enumerate() { - assert_eq!(order.rank(r), d as u32, "row {r} draws where its rank says"); + assert_eq!(order.rank(r), d as u32, "row {r}'s rank is its place"); } - assert_eq!(order.sampled(), 5, "the rows lifted, then the sample's own"); - assert_ne!(order.id(), sample(10, 3).id(), "pages packed in it are its own"); - // A page steps through the sample past the places lifted, as each row's source finds them. + assert_eq!(order.sampled(), 5, "2 lifted rows plus 3 sampled rows"); + assert_ne!(order.id(), sample(10, 3).id(), "lifted rows change the order's id"); + // Each page lists the same rows as `source` gives place by place. let rows = 3 * PAGE_ROWS + 17; let lifted = Lifted::new((0..rows).filter(|r| r % 5 == 1 || r % 7 == 0).collect(), rows, 4); let order = Order::Sampled { rows, step: 4, lifted }; @@ -228,7 +231,7 @@ mod tests { } #[test] - fn rows_appended_after_a_pyramid_draw_after_it_in_their_own_order() { + fn rows_appended_after_a_pyramid_draw_after_it_in_table_order() { let pyramid = Arc::new(Pyramid::new(vec![2, 0, 1], Vec::new())); let order = Order::Pyramid { pyramid, rows: 5 }; assert_eq!(order.page(0), [2, 0, 1, 3, 4]); diff --git a/crates/latkit/src/derive/places.rs b/crates/latkit/src/derive/places.rs index 156e0688..5f5d90e7 100644 --- a/crates/latkit/src/derive/places.rs +++ b/crates/latkit/src/derive/places.rs @@ -1,10 +1,11 @@ -//! Places outside diagrams: where a vertex row without a position stands on a plane or the earth, -//! by the scene's layout. Rows whose data positions them pin the layout, and links are the edges -//! between them, so a row without coordinates stands among its neighbors; gaps are in the space's -//! units. As on a diagram's sheet, a place persists by row, layouts place only rows without one -//! until arranging lays every row without a position out again, and the layout is a host job. The -//! places are a table beside each vertex table: its rows' positions, given or laid out, which the -//! vertex marks read. +//! Places of vertex rows without positions, outside diagrams. +//! +//! On a plane or the earth, the scene's layout places the vertex rows that have no position. Rows +//! with positions are pinned, and edges are the layout's links, so an unpositioned row ends up +//! among its neighbors. Gaps are in the space's units. As on a diagram's sheet, places persist by +//! row, and the layout (a host job) places only rows without a place until `arrange` lays out +//! every unpositioned row again. The result is a table beside each vertex table, holding its rows' +//! positions, given or laid out, which the vertex marks read. use rustc_hash::FxHashMap; @@ -18,22 +19,22 @@ use crate::{Layout, Pending}; #[derive(Default)] pub(crate) struct Places { tables: Tables, - /// Where layouts placed rows without positions, kept by row. + /// The places layouts gave rows without positions, by row. laid: FxHashMap, - /// What the tables were last made from. + /// The stamp the tables were last made from. stamp: u64, - /// Bumps when places start over, so layouts asked before are dropped. + /// Bumps when places start over, so answers to earlier layouts are dropped. generation: u64, /// Whether every row without a position is to be laid out again. arranging: bool, - /// What the last arranging laid out, until the view takes it. + /// The result of the last arrange, until the view takes it. arranged: Option>, placing: Option, - /// Bumps whenever an answer lands. + /// Bumps whenever an answer arrives. landed: u64, } -/// A layout under way: the generation it was asked in, and the row each key it answers names. +/// A pending layout job: the generation it was started in, and the row each answer key names. struct Placing { pending: Pending>, generation: u64, @@ -52,7 +53,7 @@ impl Derive for Places { return had; } let mut changed = false; - // A job its host does at once is taken before the frame draws. + // Repeat so a job the host finishes at once is taken before the frame draws. for _ in 0..4 { self.settle(); let mut hasher = rustc_hash::FxHasher::default(); @@ -77,8 +78,8 @@ impl Derive for Places { &self.tables } - /// Forgets the tables but keeps the places: a scene with a layout again finds its rows where they - /// stood. + /// Drops the tables but keeps the places, so when the scene has a layout again, rows return to + /// where they were. fn clear(&mut self) { self.tables.clear(); self.stamp = 0; @@ -86,7 +87,7 @@ impl Derive for Places { } impl Places { - /// Lays every row without a position out again. + /// Lays out every row without a position again, dropping any pending layout. pub(crate) fn arrange(&mut self) { self.arranging = true; self.placing = None; @@ -94,7 +95,7 @@ impl Places { self.stamp = 0; } - /// What the last arranging laid out, once it landed: each row placed, and where. + /// Takes the result of the last `arrange` once it arrives: each row laid out and its position. pub(crate) fn take_arranged(&mut self) -> Option> { self.arranged.take() } @@ -108,7 +109,7 @@ impl Places { if let (Some(row), [Some(Value::Number(x)), Some(Value::Number(y))]) = (placing.rows.get(&key), &values[..]) { - // The plane and the earth run y up, as layouts do. + // The plane and the earth are y up, like layouts. self.laid.insert(row.clone(), [*x, *y]); placed.push((row.clone(), [*x, *y])); } @@ -120,7 +121,7 @@ impl Places { self.landed += 1; } - /// Writes each vertex table's positions, asking the layout for the rows without one. + /// Writes each vertex table's positions, starting a layout job for rows without one. fn build(&mut self, input: &Input<'_>, jobs: &Jobs<'_>) { let (vertex_marks, edge_marks) = marks(input); let mut keys = Vec::new(); @@ -160,7 +161,7 @@ impl Places { let pending = jobs.spawn(Job::layout(&input.scene.layout, &placement)); self.placing = Some(Placing { pending, generation: self.generation, rows }); } else if self.arranging && self.placing.is_none() { - // Nothing to lay out: the arranging is done as asked. + // Nothing to lay out: the arrange finishes with no rows. self.arranging = false; self.arranged = Some(Vec::new()); } diff --git a/crates/latkit/src/derive/pyramid.rs b/crates/latkit/src/derive/pyramid.rs index ae787748..16d02324 100644 --- a/crates/latkit/src/derive/pyramid.rs +++ b/crates/latkit/src/derive/pyramid.rs @@ -1,15 +1,19 @@ -//! Pyramids: a table's rows grouped into tiles by the coarsest level each shows at, worked out in a -//! job, a dataset's tables in one. Each level splits the tiles above it four ways, and each tile -//! holds a grid of cells, a row to a cell. Rows land by priority: each at the coarsest level where its -//! cell is free, so what matters more shows from farther out, and rows crowding one cell wait for a -//! finer level, where they spread over cells of their own. A row never lands finer than its size -//! allows, so its tile grown by half its own size holds it whole. An edge lands by its own priority, -//! in the cell of its middle, and edges joining the same two vertices land together; a vertex lands -//! no finer than the coarsest edge ending at it, so the ends of every edge drawn show from as far out -//! as it does. A table drawn whole by priority is one tile, its rows in priority order. +//! Pyramids: a table's rows grouped into quadtree tiles by the coarsest level at which each shows. //! -//! A pyramid is an order: rows by tile, coarsest level first, each tile's rows together, and the -//! place of each row in it. The camera chooses tiles, each a range of the order. +//! A dataset's pyramids are computed together, in one job unless cheap enough for the frame. +//! Each level splits the tiles above it four ways, and each tile has a grid of cells, one row per +//! cell. Rows are assigned by priority, each to the coarsest level where its cell is still free, +//! so more important rows show from farther out. Rows crowding one cell wait for a finer level, +//! where they spread over separate cells. A row is never assigned finer than its size allows: its +//! tile, grown by half a tile on each side, contains it. +//! +//! An edge is assigned by its own priority, in the cell of its midpoint, and edges joining the +//! same two vertices are assigned together. A vertex is assigned no finer than the coarsest edge +//! ending at it, so the endpoints of every drawn edge show from as far out as the edge. A table +//! drawn whole by priority is one tile, its rows in priority order. +//! +//! A pyramid is an order: rows by tile, coarsest level first, each tile's rows contiguous, plus +//! each row's place in it. The camera chooses tiles, each a range of the order. use std::ops::Range; use std::sync::Arc; @@ -21,32 +25,32 @@ use crate::data::{UNWIRED, Version}; use crate::gpu::shaders::PAGE_ROWS; use crate::tiles::TileKey; -/// Cells across a tile's grid, as a shift: a tile holds at most `4^GRID_BITS` rows of its level, but -/// for the ends of edges, which land with them. +/// Log2 of the cells across a tile's grid. A tile holds at most `4^GRID_BITS` rows of its level, +/// except rows forced to that level, such as the endpoints of its edges. pub(crate) const GRID_BITS: u32 = 7; /// The finest level. pub(crate) const MAX_LEVEL: u8 = 24; -/// A level no row has landed at yet. +/// Marks a row not yet assigned a level. const NEVER: u8 = u8::MAX; -/// Tiles a camera may ask a pyramid for at most: past it, tiles stop splitting. +/// The most tiles a camera request splits into per level; past it, tiles stop splitting. const MAX_TILES: usize = 1024; -/// Cells across a tile's grid of sub-cells, as a shift: the parts of a tile the camera may ask for -/// alone, each a range of the order, so what draws of a large tile is what the camera sees of it. +/// Log2 of the sub-cells across a large tile. Each sub-cell is a range of the order that the camera +/// can request separately, so only the visible part of a large tile draws. const SUB_BITS: u32 = 3; -/// Rows past which a tile is asked for a sub-cell at a time. +/// Tiles with more rows than this are split into sub-cells. const SUB_ROWS: usize = 256; -/// What a pyramid job places: tables of rows in a square of their space. +/// A pyramid job's input: tables of rows in one square of their space. #[derive(Clone, Debug)] pub(crate) struct Leveling { pub(crate) domain: Domain, - /// The tables, every table an edge table's ends name before it. + /// The tables. A table that an edge table's endpoints reference comes before that edge table. pub(crate) tables: Vec, } -/// Where a table's rows lie, in position units: none where nothing places them, a point each, or the -/// box a geometry spans (west, south, east, north). NaN places nothing. Single precision places a -/// row within a centimeter on the earth, finer than any tile's cell. +/// Where a table's rows lie, in position units: nowhere, a point per row, or per row the box its +/// geometry spans (west, south, east, north). NaN marks an unplaced row. `f32` keeps positions on +/// the earth to within about a meter at worst (near ±180°), coarser than the finest levels' cells. #[derive(Clone, Debug, Default)] pub(crate) enum Places { #[default] @@ -59,19 +63,22 @@ pub(crate) enum Places { #[derive(Clone, Debug, Default)] pub(crate) struct Placing { pub(crate) rows: u32, - /// Whether its rows land in tiles by level; else every row is one tile, in priority order. + /// Whether its rows are assigned to tiles by level; else they form one tile in priority order. pub(crate) leveled: bool, - /// Whether the answer holds its pyramid: a table placed only for the edges ending at it is not. + /// Whether the answer includes its pyramid. A table placed only to anchor the edges ending at + /// it is not answered. pub(crate) answered: bool, pub(crate) places: Places, - /// Each row's priority, higher first; none where rows are alike. + /// Each row's priority, higher first; empty when all rows are equal. pub(crate) priority: Arc<[f32]>, - /// For a table placed by its ends, the table they name and each end's rows there. + /// For an edge table placed by its endpoints: the index of the table they reference, and each + /// endpoint reference's target rows. pub(crate) ends: Option<(u32, Vec>)>, } -/// Where each row of `places` lies across `domain`'s square, fixed point (a turn of `u32` spans it, -/// from the west and from the north), and half its size alike; nothing where it lies nowhere. +/// Each row's anchor in `domain`'s square in `u32` fixed point (the full `u32` range spans the +/// square, from the west and from the north), and for boxes, half the box's larger side in the same +/// units. Unplaced rows anchor at the origin with no extent; `Places::None` gives nothing. fn anchored(places: &Places, domain: &Domain) -> (Vec<[u32; 2]>, Vec) { let fixed = |u: f64| (u.clamp(0.0, 1.0) * 4_294_967_295.0) as u32; match places { @@ -99,8 +106,8 @@ fn anchored(places: &Places, domain: &Domain) -> (Vec<[u32; 2]>, Vec) { } } -/// Where one table's rows lie across the square, how far each reaches, the coarsest level each may -/// land at, and for an edge table each row's pair of ends. +/// One table's rows in the square: anchors, extents (half sizes), the coarsest level each may be +/// assigned (0, or `NEVER` to wait for the finest), and for an edge table each row's endpoint pair. struct Anchored { anchors: Vec<[u32; 2]>, extents: Vec, @@ -108,7 +115,7 @@ struct Anchored { pairs: Option>, } -/// The pyramid of each table a job answered for, by its place among the tables asked. +/// Computes the pyramid of each answered table, with the table's index in `leveling`. pub(crate) fn answer(leveling: &Leveling) -> Vec<(u32, Pyramid)> { let wraps = leveling.domain == Domain::Earth; let tables = &leveling.tables; @@ -118,7 +125,7 @@ pub(crate) fn answer(leveling: &Leveling) -> Vec<(u32, Pyramid)> { let anchoring = if let Some((target, ends)) = &table.ends { let Some(at) = placed.get(*target as usize).map(|a| &a.anchors) else { return Vec::new() }; let (anchors, extents) = by_ends(ends, at, wraps, n); - // A row with an end naming no row draws nothing: it lands only at the finest level. + // A row with an unset endpoint draws nothing, so it goes only to the finest level. let floor = (0..n) .map(|r| { let wired = ends.iter().all(|end| end.get(r).is_some_and(|&t| (t as usize) < at.len())); @@ -147,7 +154,7 @@ pub(crate) fn answer(leveling: &Leveling) -> Vec<(u32, Pyramid)> { let a = &placed[i]; place(&sorted[i], &a.extents, &ranked[i], &a.floor, a.pairs.as_deref(), ceiling) }; - // Edges land first: a vertex lands no finer than the coarsest edge ending at it. + // Level edges first: a vertex is assigned no finer than the coarsest edge ending at it. let mut levels: Vec>> = vec![None; tables.len()]; let mut ceilings: Vec> = vec![Vec::new(); tables.len()]; for (i, table) in tables.iter().enumerate() { @@ -186,8 +193,9 @@ pub(crate) fn answer(leveling: &Leveling) -> Vec<(u32, Pyramid)> { out } -/// Where each of `n` rows placed by `ends` lies, among rows anchored at `at`: the middle of the box -/// its ends span, and half that box's size. An end naming no row anchors nothing. +/// Anchors and extents for `n` rows placed by `ends`, among target rows anchored at `at`: the +/// middle of the box the endpoints span, and half its larger side. A row with an unset endpoint +/// gets the origin and no extent. fn by_ends(ends: &[Arc<[u32]>], at: &[[u32; 2]], wraps: bool, n: usize) -> (Vec<[u32; 2]>, Vec) { let mut anchors = Vec::with_capacity(n); let mut extents = Vec::with_capacity(n); @@ -198,8 +206,8 @@ fn by_ends(ends: &[Arc<[u32]>], at: &[[u32; 2]], wraps: bool, n: usize) -> (Vec< extents.push(0); continue; } - // The box of offsets from the end of the lowest row, the short way round where the square - // wraps: the same whichever way the row joins its ends. + // The box of offsets from the endpoint with the lowest row index, going the short way + // around where the square wraps, so the result does not depend on the edge's direction. let first = at[ends.iter().map(|end| end[r]).min().unwrap_or(0) as usize]; let (mut lo, mut hi) = ([0_i64; 2], [0_i64; 2]); for end in ends { @@ -226,8 +234,9 @@ fn by_ends(ends: &[Arc<[u32]>], at: &[[u32; 2]], wraps: bool, n: usize) -> (Vec< (anchors, extents) } -/// For each row, the next row joining the same two vertices, round to itself: a cycle per pair. Such -/// rows share their anchor, so they lie side by side along the curve. +/// For each row, the next row joining the same two vertices, forming one cycle per pair; a row +/// with no parallel rows maps to itself. Parallel rows share an anchor, so they are adjacent in +/// curve order. fn mates(sorted: &[(u64, u32)], pairs: &[u64]) -> Vec { let mut mates: Vec = (0..sorted.len() as u32).collect(); let mut i = 0; @@ -254,7 +263,7 @@ fn mates(sorted: &[(u64, u32)], pairs: &[u64]) -> Vec { mates } -/// Each row's pair of ends, the same whichever way it joins them. +/// Each row's endpoint pair as one key, independent of the edge's direction. fn pairs(from: &[u32], to: &[u32]) -> Vec { from.iter().zip(to).map(|(&a, &b)| u64::from(a.min(b)) << 32 | u64::from(a.max(b))).collect() } @@ -264,8 +273,8 @@ fn tie(row: u32) -> u32 { (u64::from(row).wrapping_mul(0x9E37_79B9_7F4A_7C15) >> 32) as u32 } -/// Each of `n` rows' rank, higher first: its priority, missing lowest, then the hash of its place, -/// so equals spread evenly. +/// Each of `n` rows' rank, higher first: its priority (missing or NaN lowest), then a hash of its +/// row index, so equal priorities spread evenly. fn ranks(priority: &[f32], n: usize) -> Vec { (0..n) .map(|r| { @@ -278,20 +287,20 @@ fn ranks(priority: &[f32], n: usize) -> Vec { .collect() } -/// Rows along the curve, by their place on it. +/// The rows sorted along the Morton curve, each with its curve key. fn along(anchors: &[[u32; 2]]) -> Vec<(u64, u32)> { let mut sorted: Vec<(u64, u32)> = anchors.iter().enumerate().map(|(r, &a)| (morton(a), r as u32)).collect(); sorted.sort_unstable(); sorted } -/// The finest level whose tile, grown by half its size, holds a row reaching `extent` from its -/// anchor. +/// The finest level whose tiles, grown by half a tile on each side, contain a row reaching `extent` +/// from its anchor. fn fit(extent: u32) -> u8 { (0..=MAX_LEVEL).rev().find(|&z| u64::from(extent) <= 1_u64 << (31 - u32::from(z))).unwrap_or(0) } -/// `x`'s bits spread to every other bit. +/// `x`'s bits spread to the even bit positions. fn spread_bits(x: u32) -> u64 { let mut x = u64::from(x); x = (x | (x << 16)) & 0x0000_FFFF_0000_FFFF; @@ -301,20 +310,20 @@ fn spread_bits(x: u32) -> u64 { (x | (x << 1)) & 0x5555_5555_5555_5555 } -/// The point's place along the curve that visits every cell of a level before the next cell of the -/// level above: cells at every level are runs of it. +/// The point's position on the Morton (Z-order) curve. The curve visits all of a cell's sub-cells +/// before leaving it, so the cells of every level are contiguous runs of it. fn morton([u, v]: [u32; 2]) -> u64 { spread_bits(u) | spread_bits(v) << 1 } -/// The tile at level `z` holding a point anchored at `anchor`. +/// The tile at level `z` containing anchor `anchor`. fn tile_of(anchor: [u32; 2], z: u8) -> TileKey { let shift = 32 - u32::from(z); let at = |c: u32| if shift >= 32 { 0 } else { c >> shift }; TileKey { z, x: at(anchor[0]), y: at(anchor[1]) } } -/// A tile as a job's answer numbers it: in 53 bits, so a number holds it exactly. +/// A tile key packed into 53 bits as a job's answer stores it, so an `f64` holds it exactly. pub(crate) fn pack(key: TileKey) -> u64 { u64::from(key.z) << 48 | u64::from(key.x) << 24 | u64::from(key.y) } @@ -323,15 +332,17 @@ fn unpack(key: u64) -> TileKey { TileKey { z: (key >> 48) as u8, x: ((key >> 24) & 0xFF_FFFF) as u32, y: (key & 0xFF_FFFF) as u32 } } -/// The level each row lands at: rows in each cell of each level, coarsest first, give way by rank; a -/// row lands at the first level where it outranks every row left in its cell, no coarser than -/// `floor`, and no finer than its size allows or `ceiling` says, where it lands when it has not by -/// then. +/// The level each row is assigned. At each level, coarsest first, each cell takes the +/// highest-ranked row left in it, so a row is assigned at the first level where it outranks every +/// other row left in its cell. Rows with a nonzero `floor` are never chosen by rank. A row is never +/// assigned finer than its size or `ceiling` allows; if no cell took it by then, it is forced +/// in at that level. Rows `pairs` marks as parallel are assigned together. Rows left over go to the +/// finest level. /// -/// Rows along the curve are taken in runs, each the rows left in one cell. While a run stays in one -/// cell level after level, those levels take its highest rows in rank order at once; where it -/// spreads over several cells, each cell's rows go on as a run of their own. So a crowd sharing a -/// cell for many levels costs its size once, not once a level. +/// Rows are processed in runs along the curve, each run the rows left in one cell. While a run +/// stays within one cell for several levels, those levels take its top rows in rank order in one +/// pass, using a heap. Where a run spreads over several cells, each cell's rows continue as a run +/// of their own. So a crowd in one cell for many levels costs its size once, not once per level. fn place( sorted: &[(u64, u32)], extents: &[u32], @@ -354,7 +365,7 @@ fn place( let mut runs: Vec<(usize, usize, u8)> = vec![(0, n, 0)]; let mut scratch: Vec<(u64, u32)> = Vec::new(); let mut forced: Vec = Vec::new(); - // Lands row `r` at level `l`, with the rows joining the same two vertices. + // Assigns row `r` level `l`, along with the rows joining the same two vertices. let land = |r: u32, l: u8, level: &mut [u8]| { level[r as usize] = l; let mut m = mates.get(r as usize).copied().unwrap_or(r); @@ -369,10 +380,10 @@ fn place( while a < b && z <= MAX_LEVEL { let (first, last) = (work[a].0, work[b - 1].0); let common = if first == last { 64 } else { (first ^ last).leading_zeros() }; - // The finest level whose cells hold the whole run. + // The finest level at which one cell holds the whole run. let single = i64::from(common / 2) - i64::from(GRID_BITS); if single < i64::from(z) { - // The run spreads over several cells of this level: each goes on alone. + // The run spans several cells at this level: split it into one run per cell. let shift = 2 * (32 - u32::from(z) - GRID_BITS); let mut i = a; while i < b { @@ -385,7 +396,7 @@ fn place( break; } let deepest = single.min(i64::from(MAX_LEVEL)) as u8; - // One row a level, in rank order: for one level the highest, for more a heap of them. + // One row per level, in rank order: the top row for a single level, else a heap. if deepest == z { let best = work[a..b] .iter() @@ -402,7 +413,7 @@ fn place( ); let mut heap = std::collections::BinaryHeap::from(std::mem::take(&mut scratch)); for l in z..=deepest { - // A row too large for this level lands where its size says, below. + // Skip rows too large for this level; the forcing step below assigns them. while let Some((_, r)) = heap.pop() { if level[r as usize] == NEVER && fits[r as usize] >= l { land(r, l, &mut level); @@ -412,7 +423,7 @@ fn place( } scratch = heap.into_vec(); } - // Rows no finer level may hold land where their size or their edges say. + // Force rows that no finer level may take to the level their size or edges allow. if forcing { forced.clear(); forced.extend(work[a..b].iter().map(|&(_, r)| r).filter(|&r| fits[r as usize] <= deepest)); @@ -422,7 +433,7 @@ fn place( } } } - // What is left goes on, along the curve, a level finer. + // The remaining rows continue, in curve order, one level finer. let mut kept = a; for i in a..b { if level[work[i].1 as usize] == NEVER { @@ -433,7 +444,7 @@ fn place( b = kept; z = deepest + 1; } - // Rows no level held, as rows naming no vertex, land at the finest. + // Rows no level took, such as edges with an unset endpoint, go to the finest level. for &(_, r) in &work[a..b] { if level[r as usize] == NEVER { level[r as usize] = MAX_LEVEL; @@ -443,9 +454,9 @@ fn place( level } -/// Rows by tile, coarsest level first, each tile's rows in priority order, highest last, and a large -/// tile's a sub-cell at a time, each in priority order; and the tiles. `sorted` holds the rows along -/// the curve. +/// Orders rows by tile, coarsest level first, and returns the order with the tiles. Each tile's +/// rows are in priority order, highest last; a large tile's rows are grouped by sub-cell, each +/// sub-cell in priority order. `sorted` holds the rows along the curve. fn tiled( sorted: &[(u64, u32)], anchors: &[[u32; 2]], @@ -454,7 +465,7 @@ fn tiled( level: &[u8], ) -> (Vec, Vec) { let n = anchors.len(); - // Rows by level, along the curve within each, so each tile's rows are a run. + // Bucket rows by level, in curve order within each, so each tile's rows are contiguous. let mut starts = vec![0_usize; usize::from(MAX_LEVEL) + 2]; for &l in level { starts[usize::from(l) + 1] += 1; @@ -484,13 +495,13 @@ fn tiled( } let start = order.len(); let members = &rows[i..j]; - // By priority alone: rows alike draw in either order. + // Sort by priority only, without the tie hash; equal rows keep their curve order. let by_priority = |order: &mut Vec, from: usize| { crate::order::by_key(&mut order[from..], |&r| ranks[r as usize] >> 32); }; let mut cells = Vec::new(); if members.len() > SUB_ROWS { - // Along the curve, each sub-cell's rows are a run. + // Along the curve, each sub-cell's rows are contiguous. let shift = 2 * (32 - u32::from(z) - SUB_BITS); let mask = (1_u64 << (2 * SUB_BITS)) - 1; let mut k = 0; @@ -515,9 +526,9 @@ fn tiled( (order, tiles) } -/// One tile of a pyramid: where its rows lie in the pyramid's order, how far they reach past it -/// across the square, and for a large tile where each sub-cell's rows end, in the curve's order of -/// the sub-cells. +/// One tile of a pyramid: its rows' range in the pyramid's order, how far its rows reach past it +/// (as a fraction of the square), and for a large tile, where each sub-cell's rows end, with +/// sub-cells in curve order. #[derive(Clone, Debug, PartialEq)] pub(crate) struct Tile { pub(crate) key: TileKey, @@ -526,9 +537,9 @@ pub(crate) struct Tile { pub(crate) cells: Vec, } -/// The pages of the order `ranges` hold past page `last`, and the last page they reach. The order lays -/// its levels out coarsest first, so a level's pages lie past those of the levels above it, but for -/// one they may share. +/// How many pages of the order `ranges` touch beyond page `last`, and the last page they reach. +/// Levels lie in the order coarsest first, so a level's pages follow those of the levels above, +/// except for one page they may share. fn pages_past(ranges: &[(u8, Range)], last: Option) -> (u32, Option) { let mut spans: Vec<(u32, u32)> = ranges .iter() @@ -545,8 +556,8 @@ fn pages_past(ranges: &[(u8, Range)], last: Option) -> (u32, Option)>, mut rows: u32) -> Vec<(u8, Range)> { let mut out = Vec::new(); for (z, range) in ranges { @@ -560,7 +571,7 @@ fn first(ranges: Vec<(u8, Range)>, mut rows: u32) -> Vec<(u8, Range)> out } -/// Sub-cell `cell` of tile `key`, numbered along the curve. +/// The tile key of sub-cell `cell` of tile `key`, with sub-cells numbered along the curve. fn sub_key(key: TileKey, cell: usize) -> TileKey { let (mut x, mut y) = (0, 0); for b in 0..SUB_BITS { @@ -570,22 +581,23 @@ fn sub_key(key: TileKey, cell: usize) -> TileKey { TileKey { z: key.z + SUB_BITS as u8, x: key.x << SUB_BITS | x, y: key.y << SUB_BITS | y } } -/// A table's rows grouped into tiles: its order, rows by tile, coarsest level first, each tile's rows -/// in priority order, highest last; each row's place in it; and its tiles. +/// A table's rows grouped into tiles: the order (rows by tile, coarsest level first, each tile's +/// rows in priority order, highest last), each row's place in it, and the tiles. #[derive(Debug)] pub(crate) struct Pyramid { pub(crate) order: Vec, pub(crate) rank: Vec, pub(crate) tiles: Vec, - /// What tells it from every other pyramid: what pages packed in its order are kept by. + /// Identifies this pyramid among all others; pages packed in its order are keyed by it. pub(crate) made: Version, + /// Each tile's index in `tiles`, by key. index: FxHashMap, - /// Every tile with tiles below it. + /// Every key that has tiles below it. above: FxHashSet, } impl Pyramid { - /// The pyramid of `order` and `tiles`: each row's place in the order worked out from it. + /// A pyramid of `order` and `tiles`, computing each row's place in the order. pub(crate) fn new(order: Vec, tiles: Vec) -> Pyramid { let mut rank = vec![UNWIRED; order.len()]; for (d, &r) in order.iter().enumerate() { @@ -605,18 +617,21 @@ impl Pyramid { Pyramid { order, rank, tiles, made: Version::next(), index, above } } - /// Rows it orders. + /// The number of rows it orders. pub(crate) fn rows(&self) -> u32 { self.order.len() as u32 } - /// The ranges of the order the camera asks for, coarsest first, each with its level: at each - /// level, every tile its rows reach into what the camera shows from, a large one only where its - /// sub-cells do, splitting while a tile draws larger than its size. A drawing holds whole pages of - /// the order, so a level whose pages would hold more than `rows` rows goes, coarser ones drawing - /// whole in its place; the coarsest, where it alone would, draws as many of its first rows as fit, - /// as one whose rows cannot land finer does, edges spanning the square. And whether `rows` held - /// back any of what the camera asks for. + /// The ranges of the order the camera needs, coarsest level first, each with its level, and + /// whether the `rows` budget dropped any of them. + /// + /// At each level it takes every tile whose rows reach into the region in sight (for a large + /// tile, only the sub-cells that do), and splits a tile into its children while it draws larger + /// than `TILE_PX` (with 25% slack) near the camera's focus. A drawing holds whole pages of the + /// order, so once a level's pages would exceed `rows` rows, that level and all finer ones are + /// dropped and the coarser levels draw instead. If the first level in sight exceeds the budget + /// alone (for example, edges spanning the square, which cannot go finer), as many of its first + /// rows as fit are drawn. pub(crate) fn wanted(&self, sight: &Sight<'_>, domain: &Domain, rows: u32) -> (Vec<(u8, Range)>, bool) { let fit = rows.div_ceil(PAGE_ROWS); if let [tile] = &self.tiles[..] @@ -646,7 +661,7 @@ impl Pyramid { if tile.cells.is_empty() { found.push((key.z, tile.rows.clone())); } - // A sub-cell's rows lie within it, each reaching no farther than the tile's. + // A sub-cell's rows lie within it and reach no farther than the tile's. let grow = tile.reach + 0.5 / f64::from(1_u32 << (u32::from(key.z) + SUB_BITS)); let mut start = tile.rows.start; for (cell, &end) in tile.cells.iter().enumerate() { @@ -667,7 +682,7 @@ impl Pyramid { next.extend(key.children()); } } - // A level whose pages would pass what fits goes: the coarser ones draw in its place. + // If this level's pages exceed the budget, drop it; coarser levels draw instead. let (pages, reached) = pages_past(&found, last); if held + pages > fit { if out.is_empty() { @@ -682,8 +697,8 @@ impl Pyramid { (out, false) } - /// The pyramids a job answered, by the place of the table among those asked, as - /// [`encode`] wrote them. + /// Decodes the pyramids a job answered, as [`encode`] wrote them, each with its table's index + /// among those asked. Stops at the first incomplete record. pub(crate) fn read(bytes: &[u8]) -> Vec<(u32, Pyramid)> { let words: Vec = bytes.as_chunks::<4>().0.iter().map(|&w| u32::from_le_bytes(w)).collect(); let mut words = words.as_slice(); @@ -712,9 +727,10 @@ impl Pyramid { } } -/// `pyramids` as a job answers them: for each, the place of its table among those asked, how many -/// rows it orders, its order, how many tiles, and each tile's key, where its rows end, how far they -/// reach, and how many sub-cells it has and where each one's rows end, little-endian words. +/// Encodes `pyramids` as a job's answer, in little-endian `u32` words. Per pyramid: its table's +/// index among those asked, its row count, its order, and its tile count; then per tile: its packed +/// key (two words), the end of its rows, its reach, its sub-cell count, and where each sub-cell's +/// rows end. pub(crate) fn encode(pyramids: &[(u32, Pyramid)]) -> Vec { let mut words: Vec = Vec::new(); for (table, pyramid) in pyramids { @@ -735,36 +751,36 @@ pub(crate) fn encode(pyramids: &[(u32, Pyramid)]) -> Vec { mod tests { use super::*; - /// Fixed point across the square. + /// The anchor `u`, `v` across the square, in fixed point. fn at(u: f64, v: f64) -> [u32; 2] { [(u * 4_294_967_296.0) as u32, (v * 4_294_967_296.0) as u32] } - /// A unit square of a plane, its rows placed `u` from the west and `v` from the north. + /// A unit square of a plane, with rows placed `u` from the west and `v` from the north. fn square(points: &[[f64; 2]]) -> (Domain, Places) { let domain = Domain::Plane { min: [0.0, 0.0], size: 1.0 }; (domain, Places::Points(points.iter().map(|&[u, v]| [u as f32, (1.0 - v) as f32]).collect())) } - /// The level row `row` of `pyramid` lands at. + /// The level row `row` of `pyramid` is assigned. fn level_of(pyramid: &Pyramid, row: u32) -> u8 { let at = pyramid.rank[row as usize]; pyramid.tiles.iter().find(|t| t.rows.contains(&at)).expect("a tile").key.z } #[test] - fn crowded_rows_wait_for_finer_levels_by_priority() { - // Two rows in one cell: the higher lands at the root, the lower a level finer. + fn crowded_rows_go_to_finer_levels_by_priority() { + // Two rows in one cell: the higher goes to the root, the lower one level finer. let anchors = [at(0.3, 0.3), at(0.3, 0.3), at(0.8, 0.8)]; let level = place(&along(&anchors), &[], &ranks(&[1.0, 2.0, 0.0], 3), &[0, 0, 0], None, &[]); - assert_eq!(level[1], 0, "the higher priority lands at the root"); - assert_eq!(level[2], 0, "a row alone in its cell lands at the root"); - assert_eq!(level[0], 1, "the lower lands once the higher has left its cell"); + assert_eq!(level[1], 0, "the higher-priority row goes to the root"); + assert_eq!(level[2], 0, "a row alone in its cell goes to the root"); + assert_eq!(level[0], 1, "the lower-priority row goes one level finer"); let alike = [at(0.3, 0.3); 4]; let level = place(&along(&alike), &[], &ranks(&[], 4), &[0; 4], None, &[]); let mut sorted = level.clone(); sorted.sort_unstable(); - assert_eq!(sorted, vec![0, 1, 2, 3], "a row a level, equals in an order of their own"); + assert_eq!(sorted, vec![0, 1, 2, 3], "one row per level, ties broken by a hash of the row"); } #[test] @@ -778,10 +794,10 @@ mod tests { let (order, tiles) = tiled(&sorted, &anchors, &[], &ranks, &level); assert_eq!(order.len(), n as usize); for tile in &tiles { - assert!(tile.rows.len() <= 1 << (2 * GRID_BITS), "a tile holds at most its grid's cells"); + assert!(tile.rows.len() <= 1 << (2 * GRID_BITS), "a tile has at most as many rows as grid cells"); } assert!(tiles.len() > 1); - // A large tile's rows lie a sub-cell at a time, each sub-cell's within it. + // A large tile's rows are grouped by sub-cell, and each group lies within its sub-cell. let large = tiles.iter().find(|t| !t.cells.is_empty()).expect("a large tile"); let mut start = large.rows.start; for (cell, &end) in large.cells.iter().enumerate() { @@ -795,13 +811,13 @@ mod tests { } #[test] - fn large_rows_stay_coarse_and_edges_land_by_their_own_priority() { + fn large_rows_stay_coarse_and_edges_get_levels_by_their_own_priority() { let anchors = [at(0.1, 0.1), at(0.9, 0.9)]; let extents = [1 << 30, 0]; let level = place(&along(&anchors), &extents, &ranks(&[0.0, 0.0], 2), &[0, 0], None, &[]); assert!(level[0] <= 1, "a row reaching a quarter of the square stays coarse"); - // Two vertices crowd one cell; the edges between them land by their own priority, parallel - // ones together, and one naming no row only at the finest level. + // Two vertices crowd one cell. The edges between them are assigned by their own priority, + // parallel ones together, and one with an unset endpoint only at the finest level. let (domain, places) = square(&[[0.3, 0.3], [0.3, 0.3], [0.31, 0.3]]); let leveling = Leveling { domain, @@ -826,11 +842,11 @@ mod tests { let pyramids: FxHashMap = answer(&leveling).into_iter().collect(); let edges = &pyramids[&1]; assert_eq!(level_of(edges, 0), 0, "an edge alone in its cell shows from the root"); - assert_eq!(level_of(edges, 1), 0, "with the edge joining the same vertices"); - assert_eq!(level_of(edges, 2), MAX_LEVEL, "an edge naming no row waits for the finest level"); - // Its ends show with it: vertices 1 and 2 land at the root though they share a cell. + assert_eq!(level_of(edges, 1), 0, "a parallel edge takes the same level"); + assert_eq!(level_of(edges, 2), MAX_LEVEL, "an edge with an unset endpoint goes to the finest level"); + // Its endpoints show with it: vertices 1 and 2 go to the root though they share a cell. let vertices = &pyramids[&0]; - assert_eq!((level_of(vertices, 1), level_of(vertices, 2)), (0, 0), "ends land with their edges"); + assert_eq!((level_of(vertices, 1), level_of(vertices, 2)), (0, 0), "endpoints take their edge's level"); } #[test] @@ -848,37 +864,37 @@ mod tests { assert_eq!(pyramid.order, answered[0].1.order); assert_eq!(pyramid.tiles, answered[0].1.tiles); for (d, &r) in pyramid.order.iter().enumerate() { - assert_eq!(pyramid.rank[r as usize], d as u32, "each row's place in the order"); + assert_eq!(pyramid.rank[r as usize], d as u32, "each row's rank is its place in the order"); } } #[test] - fn levels_count_the_pages_they_hold() { + fn levels_count_only_the_pages_past_the_level_above() { // Two sub-cells in one page, one in the next, and a level below reaching the page where the // one above ended. let above = [(0, 10..20), (0, 50..60), (0, PAGE_ROWS + 3..PAGE_ROWS + 9)]; let (pages, last) = pages_past(&above, None); assert_eq!((pages, last), (2, Some(1))); let below = [(1, PAGE_ROWS + 20..3 * PAGE_ROWS + 1)]; - assert_eq!(pages_past(&below, last), (2, Some(3)), "pages 2 and 3; page 1 is held already"); + assert_eq!(pages_past(&below, last), (2, Some(3)), "pages 2 and 3; page 1 was counted above"); } #[test] - fn a_level_alone_past_what_fits_draws_its_first_rows() { + fn a_level_over_the_budget_draws_its_first_rows() { let ranges = vec![(0, 0..3 * PAGE_ROWS), (0, 5 * PAGE_ROWS..6 * PAGE_ROWS)]; - assert_eq!(first(ranges.clone(), 2 * PAGE_ROWS), [(0, 0..2 * PAGE_ROWS)], "its first pages"); + assert_eq!(first(ranges.clone(), 2 * PAGE_ROWS), [(0, 0..2 * PAGE_ROWS)], "its first two pages"); assert_eq!( first(ranges, 4 * PAGE_ROWS), [(0, 0..3 * PAGE_ROWS), (0, 5 * PAGE_ROWS..6 * PAGE_ROWS)], - "every range, where they fit" + "every range when all fit" ); } #[test] - fn tiles_number_exactly() { + fn tile_keys_round_trip_through_an_f64() { let key = TileKey { z: MAX_LEVEL, x: (1 << 24) - 1, y: 12_345 }; assert_eq!(unpack(pack(key)), key); - assert_eq!(unpack(pack(key) as f64 as u64), key, "a number holds it exactly"); + assert_eq!(unpack(pack(key) as f64 as u64), key, "an f64 holds a packed key exactly"); } #[test] diff --git a/crates/latkit/src/derive/shapes.rs b/crates/latkit/src/derive/shapes.rs index db2f78ab..b7424516 100644 --- a/crates/latkit/src/derive/shapes.rs +++ b/crates/latkit/src/derive/shapes.rs @@ -1,12 +1,18 @@ -//! Shapes: what geometry fields become to draw. Every area's triangles, the point its label hangs from -//! first; and on the earth every line's path, unwrapped across the antimeridian and bent with the -//! globe. A table drawn whole is shaped a page of its field at a time, written beside it: a page keeps -//! the shapes made of it before, and a page written over one keeps those of its rows not written, so a -//! write shapes only the rows it wrote. A table drawn in part is shaped a page of its order at a time, -//! the first time the page is drawn: a row whose geometry did not change keeps its shape however its -//! pages come and go. Triangles that do not fit the frame's budget are made in a job, a page whole -//! where every row of it needs them, their rows drawing nothing until it lands; bending a path is -//! cheap, and done at once. Vector tiles bring their shapes, made in the job that decodes them. +//! Shapes: the drawable forms of geometry fields. +//! +//! Each area becomes triangles, preceded by its label point. On the earth, each line's path is +//! unwrapped across the antimeridian and bent to follow the globe. +//! +//! A table drawn whole is shaped page by page of its field, into a field beside it. A page keeps +//! the shapes made from it before, and a page written over another keeps the shapes of its rows +//! not written, so a write reshapes only the rows it wrote. A paged table is shaped page by page of +//! its order, the first time each page draws. A row whose geometry did not change keeps its shape +//! as its pages come and go. +//! +//! Triangulation that does not fit the frame's budget runs in a job; a page whose every row needs +//! shaping goes to the job whole. Rows waiting on a job draw nothing until it finishes. Bending a +//! path is cheap and done at once. Vector tiles bring their own shapes, made in the job that +//! decodes them. use rustc_hash::{FxHashMap, FxHashSet}; @@ -20,14 +26,14 @@ use crate::host::{Job, Shaping, shapes_answer}; use crate::tiles::TileSets; use crate::{Mark, Pending, Scene, Space}; -/// Microseconds a point takes to triangulate, by estimate. +/// Estimated microseconds to triangulate one point. const TRIANGULATE: f64 = 0.5; -/// Rows to shape one at a time: each row, its id, and the digest of its geometry. +/// Rows to shape individually: each row index, its id, and its geometry digest. type Rows = Vec<(usize, Id, u64)>; -/// What to shape: pages of the field every row of which needs shaping, by id with their values; and -/// rows one at a time. +/// Work left to shape: whole pages of the field where every row needs shaping (by page id, with +/// their values), and individual rows. #[derive(Default)] struct Missing { pages: Vec<(PageId, Values)>, @@ -39,14 +45,14 @@ impl Missing { self.pages.is_empty() && self.rows.is_empty() } - /// How many points it shapes. + /// The number of points to shape. fn points(&self, column: &Column) -> usize { let pages: usize = self.pages.iter().map(|(_, values)| values.geometry().map_or(0, Geometry::points)).sum(); pages + self.rows.iter().map(|&(row, ..)| points_of(column, row)).sum::() } - /// What it shapes as the parts of a job, each a geometry column: each page whole, then the rows; - /// and what each part is. + /// Splits the work into a job's parts, each a geometry column: each whole page, then one part + /// holding all the rows. Also returns what each part shapes. fn parts(self, column: &Column) -> (Vec, Vec) { let (mut parts, mut asked) = (Vec::new(), Vec::new()); for (id, values) in self.pages { @@ -62,16 +68,16 @@ impl Missing { } } -/// What a part of a job shapes: a page of the field whole, by its id; or rows, by id, each with the -/// digest of its geometry. +/// What one part of a job shapes: a whole page of the field, by page id; or rows, by id, each with +/// its geometry digest. enum Asked { Page(PageId), Rows(Vec<(Id, u64)>), } -/// Shapes made since the last whole set: pages whole, by the id of the page of the field each was -/// made of; and rows, by id, with the digest of the geometry each was made from and where among the -/// parts its shape lies. +/// Shapes made since the last complete set: whole pages, by the id of the field page each was made +/// from; and rows, by id, each with the geometry digest it was made from and its part and index in +/// `parts`. #[derive(Default)] struct Landed { pages: FxHashMap, @@ -80,7 +86,7 @@ struct Landed { } impl Landed { - /// Takes `shapes`, the shapes of what `asked` names, part by part. + /// Stores `shapes`, the shapes of each part `asked` names. fn land(&mut self, asked: Vec, shapes: Vec) { for (asked, shapes) in asked.into_iter().zip(shapes) { match asked { @@ -98,7 +104,7 @@ impl Landed { } } - /// Where the shape of the row with `id` lies, made from geometry whose digest is `digest`. + /// Where the shape of the row with `id` lies, if made from geometry with digest `digest`. fn row(&self, id: &Id, digest: u64) -> Option<(&Geometry, usize)> { let &(made, part, at) = self.rows.get(id)?; (made == digest).then_some(())?; @@ -106,7 +112,7 @@ impl Landed { } } -/// A field to shape: its dataset, table, and field, and how. +/// A field to shape: its dataset, table, field, and shaping. #[derive(Clone, PartialEq, Eq, Hash, Debug)] struct Want { data: Name, @@ -122,30 +128,30 @@ impl Want { } } -/// The shapes of a table drawn in part, a page of its order at a time: each page the shapes of the -/// rows of one page of the order, in its order, with an id of its own; and the shapes of the rows kept -/// whose pages are not drawn, a row at a time, by their place in the order. +/// The shapes of a paged table, per page of its order: each page holds its rows' shapes in order, +/// with a page id of its own. Kept rows whose pages are not drawn are shaped individually, by their +/// place in the order. pub(crate) struct PagedShapes { - /// The order whose pages these are. + /// The id of the order these pages belong to. pub(crate) order: u64, - /// Bumps whenever a page or a row kept is shaped. + /// Bumps whenever a page or kept row is shaped. pub(crate) version: Version, pub(crate) pages: FxHashMap, pub(crate) kept: FxHashMap, - /// How deep the shapes' geometry is. + /// The nesting depth of the shapes' geometry. pub(crate) depth: u8, } -/// One page of an order, shaped. +/// One shaped page of an order. pub(crate) struct ShapedPage { pub(crate) id: PageId, pub(crate) shapes: Values, - /// Whether every row holds its shape: none waits for a job. + /// Whether every row has its shape, with none waiting for a job. whole: bool, } -/// What a table drawn in part was shaped from: the order, the table and the field its pages were made -/// from, and the pages made. +/// What a paged table's shapes were made from: the order, the source table and field column, and +/// the shaped pages. struct Paging { order: Order, source: Table, @@ -154,8 +160,8 @@ struct Paging { } impl Paging { - /// Where a page made whole holds the shape of the row with `id`, when its geometry's digest was - /// `digest`: the page's geometry and the row's place in it. + /// Where a complete page holds the shape of the row with `id`, if the row's geometry digest is + /// still `digest`: the page's geometry and the row's index in it. fn held(&self, id: &Id, digest: u64) -> Option<(&Geometry, usize)> { let row = self.source.row(id.as_ref())?; let (values, i) = self.column.at(row).filter(|&(values, i)| values.is_valid(i))?; @@ -168,25 +174,26 @@ impl Paging { } } -/// One field's shapes: drawn whole, the last whole set made, the derived field written, and the shapes -/// made but not yet written; drawn in part, its pages, and those of the order drawn before; and a job -/// under way. +/// One field's shapes and any pending job. Drawn whole: the last complete set, the derived +/// field written, and shapes made but not yet written. Paged: its pages and the previous order's. #[derive(Default)] struct Shaped { - /// The source table, its field, and the derived column the shapes were last wholly made from. + /// The source table, its field column, and the derived column from the last time every row + /// was shaped. made: Option<(Table, Column, Column)>, - /// The derived field written last, and the stamp of what it was written from. + /// The derived field written last. field: Option, + /// The stamp of the inputs `field` was written from. stamp: u64, - /// Shapes made since the last whole set. + /// Shapes made since the last complete set. landed: Landed, - /// Drawn in part, its pages; and those of the order drawn before, whose rows alike pages made - /// afresh take their shapes from. + /// Paged: its shaped pages. paging: Option, + /// The pages of the previous order. New pages copy the shapes of unchanged rows from it. before: Option, - /// A job under way, with what each of its parts shapes. + /// A pending job, with what each of its parts shapes. working: Option<(Pending>, Vec)>, - /// Bumps whenever a job's answer lands. + /// Bumps whenever a job's answer arrives. landings: u64, } @@ -197,8 +204,8 @@ pub(crate) struct Shapes { } impl Shapes { - /// Brings the shapes up to `input`, taking those `tiles` brought as they are and handing `jobs` - /// what the frame cannot afford; whether its tables changed. + /// Updates the shapes to `input`, using as is any shapes the vector tiles in `tiles` bring, and + /// sending work the frame cannot afford to `jobs`. Returns whether its tables changed. pub(crate) fn update(&mut self, input: &Input<'_>, tiles: &TileSets, jobs: &mut Jobs<'_>) -> bool { let wants = wants(input); self.shaped.retain(|want, _| wants.contains(want)); @@ -206,7 +213,7 @@ impl Shapes { for want in &wants { let Some(source) = input.data(&want.data).and_then(|d| d.table(&want.table)) else { continue }; let shaped = self.shaped.entry(want.clone()).or_default(); - // Vector tiles bring their shapes, row for row. + // Vector tiles bring their own shapes, row for row. let brought = tiles.shaped(&want.data).and_then(|d| d.table(&want.table)?.field_named(&want.derived())); if let Some(field @ Field::Values(column, _)) = brought && column.len() == source.len() @@ -236,22 +243,22 @@ impl Shapes { changed } - /// Whether a job shapes rows: until it lands, they draw nothing. + /// Whether a shape job is pending. The rows it shapes draw nothing until it finishes. pub(crate) fn busy(&self) -> bool { self.shaped.values().any(|s| s.working.is_some()) } - /// Whether every row drawn holds its shape. + /// Whether every drawn row has its shape. pub(crate) fn complete(&self) -> bool { !self.busy() } - /// The tables beside those drawn whole, each holding the fields shaped from it. + /// The derived tables beside tables drawn whole, each holding the fields shaped from it. pub(crate) fn tables(&self) -> &Tables { &self.tables } - /// The shapes of table `table` of dataset `data` drawn in part, by the field each is written as. + /// The shapes of paged table `table` of dataset `data`, each with its derived field's name. pub(crate) fn paged(&self, data: &Name, table: &Name) -> Vec<(Name, &PagedShapes)> { self.shaped .iter() @@ -261,7 +268,7 @@ impl Shapes { } } -/// What mark `mark` shapes, and how: an area its polygons, and on the earth a line its paths. +/// The field mark `mark` shapes, and how: an area's polygons, or on the earth a line's paths. fn shaping_of(mark: &Mark, earth: bool) -> Option<(Name, Shaping)> { match mark { Mark::Areas(areas) => Some((areas.field(), Shaping::Triangles { earth })), @@ -270,7 +277,7 @@ fn shaping_of(mark: &Mark, earth: bool) -> Option<(Name, Shaping)> { } } -/// The fields of table `table` of dataset `data` the scene's marks shape, and how, each once. +/// The fields of table `table` of dataset `data` that the scene's marks shape, and how, each once. pub(crate) fn shapings(scene: &Scene, data: &Name, table: &Name) -> Vec<(Name, Shaping)> { let earth = matches!(scene.space, Space::Earth(_)); let mut shapings = Vec::new(); @@ -298,8 +305,8 @@ fn wants(input: &Input<'_>) -> Vec { } impl Shaped { - /// Brings the shapes up to `source`, drawn in part as `paged` says or else whole; whether they - /// changed. + /// Updates the shapes to `source`: as a paged table when `paged` is given, else drawn whole. + /// Returns whether they changed. fn update(&mut self, want: &Want, source: &Table, paged: Option<&Paged>, jobs: &mut Jobs<'_>) -> bool { if let Some((pending, _)) = &self.working && let Some(answer) = pending.take() @@ -316,12 +323,12 @@ impl Shaped { return self.forget(); }; if let Some(paged) = paged { - // Should it draw whole again, its field is written afresh. + // If it draws whole again later, its field is rewritten from scratch. let had = self.field.take().is_some(); self.stamp = 0; return self.update_paged(want, source, column, depth, paged, jobs) || had; } - // Drawn whole again: rows alike take their shapes from the pages drawn in part. + // Drawn whole again: unchanged rows take their shapes from the paged shapes. let had = self.paging.is_some(); if had { self.before = self.paging.take(); @@ -329,21 +336,21 @@ impl Shaped { self.update_whole(want, source, column, *version, depth, jobs) || had } - /// Takes `field` as its shapes, made elsewhere; whether they changed. + /// Uses `field`, made elsewhere (by vector tiles), as its shapes. Returns whether they changed. fn take(&mut self, field: &Field) -> bool { let changed = self.field.as_ref().is_none_or(|f| f.version() != field.version()); *self = Shaped { field: Some(field.clone()), ..Shaped::default() }; changed } - /// Forgets what it shaped, its field no longer shapes; whether it had shaped anything. + /// Drops its shapes once its field no longer needs shaping. Returns whether it had any. fn forget(&mut self) -> bool { let had = self.field.is_some() || self.paging.is_some(); (self.field, self.paging, self.before, self.stamp) = (None, None, None, 0); had } - /// Brings the shapes of a table drawn whole up to `source`; whether the derived field changed. + /// Updates the shapes of a table drawn whole to `source`; returns whether its field changed. fn update_whole( &mut self, want: &Want, @@ -359,7 +366,7 @@ impl Shaped { } self.stamp = stamp; let (mut derived, mut missing) = self.build(want, source, column, depth); - // Shapes made on the frame land at once, and the column is made again with them. + // Shapes made on the frame apply at once; rebuild the column with them. if !missing.is_empty() { match self.shape_now(want, column, missing, jobs) { Ok(()) => (derived, missing) = self.build(want, source, column, depth), @@ -374,13 +381,13 @@ impl Shaped { return true; } self.ask(want, column, missing, jobs); - // Until the job lands, its rows draw nothing and the rest draw as they were. + // Until the job finishes, its rows draw nothing and the rest draw unchanged. self.field = Some(Field::Values(derived, Version::next())); true } - /// Brings the shapes of a table drawn in part up to `source`, making each page `paged` draws the - /// first time it is drawn, and again once rows it waited on land; whether a page was made. + /// Updates the shapes of a paged table to `source`. Shapes each page `paged` draws the first + /// time it draws, and again once the rows it waited on arrive. Returns whether a page was made. fn update_paged( &mut self, want: &Want, @@ -391,7 +398,8 @@ impl Shaped { jobs: &mut Jobs<'_>, ) -> bool { let order = &paged.order; - // Another order: its pages are made afresh, rows alike taking their shapes from the last one's. + // A new order: its pages are made from scratch, copying unchanged rows' shapes from the + // previous order's pages. if self.paging.as_ref().is_none_or(|p| p.shapes.order != order.id()) { let shapes = PagedShapes { order: order.id(), @@ -405,7 +413,7 @@ impl Shaped { } let paging = self.paging.as_mut().expect("paged"); paging.order = order.clone(); - // The pages rows written since draw in are made again, and those rows kept shaped again. + // Remake the pages holding rows written since, and reshape those rows where kept. if !paging.column.shares(column) { let written = column.written_since(&paging.column); for &row in written.iter().filter(|&&row| row < order.len() as usize) { @@ -418,8 +426,8 @@ impl Shaped { (paging.source, paging.column) = (source.clone(), column.clone()); } paging.shapes.kept.retain(|d, _| paged.kept.binary_search(d).is_ok()); - // Pages drawn that were not made, or that wait on rows no job is shaping now; and rows kept - // not shaped yet, while no job is. + // Make drawn pages not made yet, or waiting on rows while no job runs; and, while no job + // runs, shape kept rows not shaped yet. let running = self.working.is_some(); let made = &paging.shapes.pages; let make: Vec = @@ -434,7 +442,7 @@ impl Shaped { return false; } let (mut made, mut kept, mut missing) = self.make_paged(want, order, &make, &keep, source, column, depth); - // Shapes made on the frame land at once, and the pages are made again with them. + // Shapes made on the frame apply at once; remake the pages with them. if !missing.rows.is_empty() { match self.shape_now(want, column, missing, jobs) { Ok(()) => (made, kept, missing) = self.make_paged(want, order, &make, &keep, source, column, depth), @@ -450,8 +458,8 @@ impl Shaped { } paging.shapes.kept.extend(kept); paging.shapes.version = Version::next(); - // Every page drawn whole and no job under way: what landed is in its pages, and the order - // before has served the pages made afresh. + // Every page complete and no job pending: the pages hold every shape that arrived, and the + // previous order is no longer needed. if self.working.is_none() && paging.shapes.pages.values().all(|page| page.whole) { self.landed = Landed::default(); self.before = None; @@ -459,8 +467,8 @@ impl Shaped { true } - /// Pages `make` of `order` shaped, each with whether it is whole; rows kept `keep`, by their place - /// in the order, each shaped; and the rows left to shape. + /// Shapes pages `make` of `order`, each with whether it is complete, and kept rows `keep` (by + /// place in the order). Also returns the rows left to shape. #[allow(clippy::too_many_arguments, clippy::type_complexity)] fn make_paged( &self, @@ -490,7 +498,7 @@ impl Shaped { (made, kept, missing) } - /// Shapes `missing` on the frame, if they fit what is left of its budget; else hands them back. + /// Shapes `missing` on the frame if it fits the rest of the budget; else returns it. fn shape_now( &mut self, want: &Want, @@ -507,7 +515,7 @@ impl Shaped { Ok(()) } - /// Asks a job to shape `missing`, unless one is under way: what it leaves waits for the next. + /// Starts a job to shape `missing`, unless one is pending; a later update then retries. fn ask(&mut self, want: &Want, column: &Column, missing: Missing, jobs: &mut Jobs<'_>) { if self.working.is_some() { return; @@ -516,12 +524,12 @@ impl Shaped { self.working = Some((jobs.spawn(Job::shapes(want.shaping, &parts)), asked)); } - /// The derived column for `column` of `source`, page by page, and what is left to shape. A page - /// keeps the shapes made of it before, or landed for it; a page written over one shaped keeps - /// those of its rows not written, as does a page at its place in the last whole set those of its - /// rows holding the same. A page none of whose rows can keep a shape is shaped whole; the rows of - /// others take a shape landed or made before while their geometry is the same, and are left to - /// shape otherwise. + /// Builds the derived column for `column` of `source`, page by page, and returns what is left + /// to shape. A page reuses the shapes made from it before, or that arrived for it. A page + /// written over a shaped page keeps the shapes of its rows not written, and a page at the same + /// place in the last complete set keeps those of its unchanged rows. A page none of whose rows + /// can reuse a shape is shaped whole. Otherwise each row reuses a shape made from the same + /// geometry, or is left to shape. fn build(&self, want: &Want, source: &Table, column: &Column, depth: u8) -> (Column, Missing) { let made_pages: FxHashMap = self .made @@ -529,10 +537,11 @@ impl Shaped { .flat_map(|(_, field, derived)| field.pages().iter().map(|p| p.id).zip(derived.pages())) .collect(); let depth = want.shaping.depth(depth); - // Rows below this many are the last whole set's, at the same places: rows since were only - // written, or added after them. + // Rows below `aligned` are at the same places as in the last complete set: since then they + // were only written over, or rows were appended after them. let aligned = self.made.as_ref().map_or(0, |(table, ..)| aligned(source, table)); - // Whether rows past them could take a shape made before only by being shaped. + // Whether row `row` may find a shape made before: it is aligned, or rows moved since the + // last complete set (`aligned` is 0), so a lookup by id may find one. let found = |row: usize| row < aligned || self.made.is_some() && aligned == 0; let fresh = self.before.is_none() && self.landed.rows.is_empty() && want.shaping != Shaping::Path; let mut pages: Vec = Vec::with_capacity(column.pages().len()); @@ -550,7 +559,7 @@ impl Shaped { pages.push(Page::new(Values::of_geometry(GeometryBuilder::new(depth).finish()))); continue; }; - // A path page that needs no bending is its own shape: shared, nothing uploads again. + // A path page needing no bending is its own shape; sharing it means no new upload. if want.shaping == Shaping::Path && !bends(geometry) { pages.push(page.clone()); continue; @@ -564,7 +573,7 @@ impl Shaped { Some((&derived.pages().get(p)?.values, same)) }); if fresh && kept.is_none() && !found(start) { - // Every row is shaped: the page whole, drawing nothing until it lands. + // Every row needs shaping: shape the page whole; it draws nothing until then. let mut empty = GeometryBuilder::new(depth); (0..len).for_each(|_| empty.push_empty()); pages.push(Page::new(Values::of_geometry(empty.finish()))); @@ -592,8 +601,8 @@ impl Shaped { (Column::of_pages(pages, &like), missing) } - /// The shapes made of a page `page` was written over, a few writes back at most, with whether - /// each of its rows keeps its shape: those not written since. + /// The shapes of the page that `page` was written over, searching back through its recorded + /// writes (a few at most), with whether each row keeps its shape: those not written since. fn over<'a>(&'a self, page: &Page, made: &FxHashMap) -> Option<(&'a Values, Vec)> { let mut written: Vec = Vec::new(); let mut at = page.written.as_deref(); @@ -614,8 +623,8 @@ impl Shaped { None } - /// Rows `rows` of `source` shaped, in that order, as [`shape`](Shaped::shape) finds them; and the - /// rows left to shape. + /// Shapes rows `rows` of `source` in that order, as [`shape`](Shaped::shape) does, and returns + /// the rows left to shape. fn make_rows(&self, want: &Want, rows: &[u32], source: &Table, column: &Column, depth: u8) -> (Values, Rows) { let mut shapes = GeometryBuilder::new(want.shaping.depth(depth)); let mut missing = Vec::new(); @@ -629,9 +638,9 @@ impl Shaped { (Values::of_geometry(shapes.finish()), missing) } - /// Writes the shape of row `row`, with `id`, whose geometry is value `at.1` of `at.0`: one landed - /// or made before while its geometry is the same, bent at once for a path, or else empty and the - /// row left to shape in `missing`. + /// Writes the shape of row `row` (with `id`), whose geometry is value `i` of `values`. Reuses a + /// shape that arrived or was made before from the same geometry; bends a path at once; else + /// writes an empty shape and adds the row to `missing`. fn shape( &self, want: &Want, @@ -649,7 +658,7 @@ impl Shaped { if let Some((landed, at)) = self.landed.row(&id, digest).or_else(|| self.kept(&id, digest)) { shapes.push_from(landed, at); } else if want.shaping == Shaping::Path { - // Bending is cheap, so done here; rows that need none keep their geometry. + // Bending is cheap, so it runs here; rows needing none keep their geometry. match values.get(i).and_then(|v| want.shaping.shape(&v)) { Some(shape) => { shapes.push_value(&shape); @@ -662,14 +671,14 @@ impl Shaped { } } - /// Where a shape made before holds the row with `id`, when its geometry's digest was `digest`: in - /// the last whole set, or in a page made whole of the order drawn in part before. + /// Where an earlier shape of the row with `id` lies, if made from geometry with digest + /// `digest`: in the last complete set, or in a complete page of the previous paged order. fn kept(&self, id: &Id, digest: u64) -> Option<(&Geometry, usize)> { self.whole(id, digest).or_else(|| self.before.as_ref()?.held(id, digest)) } - /// Where the last whole set holds the shape of the row with `id`, when its geometry's digest was - /// `digest`: the derived page's geometry and the row's place in it. + /// Where the last complete set holds the shape of the row with `id`, if made from geometry with + /// digest `digest`: the derived page's geometry and the row's index in it. fn whole(&self, id: &Id, digest: u64) -> Option<(&Geometry, usize)> { let (table, field, derived) = self.made.as_ref()?; let row = table.row(id.as_ref())?; @@ -682,8 +691,8 @@ impl Shaped { } } -/// How many leading rows of `now` are those of `then` at the same places: every row where none came -/// or went, those `then` held where rows were only added after them, else none. +/// How many leading rows of `now` are `then`'s rows at the same places: all of them if no rows came +/// or went, `then`'s rows if rows were only appended after them, else 0. fn aligned(now: &Table, then: &Table) -> usize { if now.structure == then.structure { return now.len(); @@ -695,8 +704,8 @@ fn aligned(now: &Table, then: &Table) -> usize { } } -/// Whether any two coordinates of a row of `geometry` lie farther apart than an earth path's step: a -/// page with none needs no bending. +/// Whether any two consecutive coordinates of `geometry` lie farther apart than an earth path's +/// step. A page with none needs no bending. fn bends(geometry: &Geometry) -> bool { let step = crate::geo::shapes::EARTH_STEP; let coords = geometry.coords(); @@ -733,12 +742,12 @@ mod tests { type Ring = Vec<[f64; 2]>; - /// A square a unit across at `x`. + /// A unit square at `x`. fn square(x: f64) -> Vec { vec![vec![[x, 0.0], [x + 1.0, 0.0], [x + 1.0, 1.0], [x, 1.0], [x, 0.0]]] } - /// The pages made of the shapes of `shapes`' one table drawn in part, and their ids. + /// The shaped pages of the one paged table in `shapes`, with their ids, by page. fn made(shapes: &Shapes) -> Vec<(u32, PageId)> { let paged = shapes.paged(&"map".into(), &"Zone".into()); let mut pages: Vec<(u32, PageId)> = paged[0].1.pages.iter().map(|(&page, made)| (page, made.id)).collect(); @@ -746,7 +755,7 @@ mod tests { pages } - /// The derived column of `shapes`' one table drawn whole. + /// The derived column of the one table drawn whole in `shapes`. fn derived(shapes: &Shapes) -> Column { match shapes.tables().get(&("map".into(), "Zone".into())).and_then(|t| t.field_named("geometry.triangles")) { Some(Field::Values(column, _)) => column.clone(), @@ -755,7 +764,7 @@ mod tests { } #[test] - fn a_table_drawn_in_part_is_shaped_a_page_drawn_at_a_time() { + fn a_paged_table_shapes_only_the_pages_it_draws() { let n = 3 * PAGE_ROWS as usize; let data = Data::new("map"); let squares: Vec> = (0..n).map(|i| square(i as f64)).collect(); @@ -787,22 +796,22 @@ mod tests { let page = &paged[0].1.pages[&1]; assert_eq!(page.shapes.len(), PAGE_ROWS as usize); let triangles = page.shapes.geometry().expect("triangles").row_coords(0).len(); - assert!(triangles >= 3, "its rows' triangles, in the order's order: {triangles} points"); + assert!(triangles >= 3, "its rows' triangles, in draw order: {triangles} points"); // Drawing another page shapes it alone; the first keeps its shapes. update(&mut shapes, &data.snapshot(), vec![1, 2]); let second = made(&shapes); assert_eq!((second.len(), second[0]), (2, first[0])); - // A write to a row drawn in page 2 shapes that page again, and only it. + // A write to a row on page 2 reshapes only that page. let row = u64::from(order.source(2 * PAGE_ROWS + 5)); data.apply(Table::new("Zone", vec![row]).field("geometry", vec![square(-9.0)])).expect("valid"); update(&mut shapes, &data.snapshot(), vec![1, 2]); let third = made(&shapes); - assert_eq!(third[0], second[0], "page 1 holds no row written"); + assert_eq!(third[0], second[0], "page 1 has no written rows"); assert_ne!(third[1].1, second[1].1, "page 2 is shaped again"); } #[test] - fn a_table_drawn_whole_lands_its_shapes_a_page_at_a_time() { + fn a_whole_table_shapes_in_a_job_and_reshapes_only_written_rows() { let n = 2 * PAGE_ROWS as usize + 10; let data = Data::new("map"); let squares: Vec> = (0..n).map(|i| square(i as f64)).collect(); @@ -810,7 +819,7 @@ mod tests { let scene = Scene::new(Plane::default()).draw(Areas::of("map", "Zone")); let (counters, errors) = (Counters::default(), Arc::new(Mutex::new(Vec::new()))); let mut shapes = Shapes::default(); - // No budget on the frame: every shape is made in a job, which lands by the next update. + // No frame budget: every shape is made in a job, which finishes by the next update. let update = |shapes: &mut Shapes, data: &Data| { let data = data.snapshot(); let input = Input { @@ -829,23 +838,23 @@ mod tests { }; assert!(update(&mut shapes, &data)); assert!(shapes.busy(), "a job shapes them"); - assert_eq!(points(&derived(&shapes), 3), 0, "until it lands, rows draw nothing"); + assert_eq!(points(&derived(&shapes), 3), 0, "until the job finishes, rows draw nothing"); assert!(update(&mut shapes, &data)); let whole = derived(&shapes); assert!(!shapes.busy()); assert_eq!(whole.pages().len(), 3); assert!((0..n).all(|row| points(&whole, row) >= 4), "every row holds its label point and triangles"); - // A write to one row shapes it alone: the other pages keep theirs, and its page the shapes of - // the rows not written. + // A write to one row reshapes only it: other pages keep their shapes, and its page keeps + // those of the rows not written. let row = PAGE_ROWS as usize + 7; data.apply(Table::new("Zone", vec![row as u64]).field("geometry", vec![square(-9.0)])).expect("valid"); assert!(update(&mut shapes, &data)); let written = derived(&shapes); assert_eq!(written.pages()[0].id, whole.pages()[0].id); assert_eq!(written.pages()[2].id, whole.pages()[2].id); - assert_eq!(points(&written, row), 0, "the row written waits for its job"); - assert!(points(&written, row + 1) >= 4, "the rest of its page keeps theirs"); + assert_eq!(points(&written, row), 0, "the written row draws nothing until its job finishes"); + assert!(points(&written, row + 1) >= 4, "the rest of its page keeps their shapes"); assert!(update(&mut shapes, &data)); - assert!(points(&derived(&shapes), row) >= 4, "and lands"); + assert!(points(&derived(&shapes), row) >= 4, "then its shapes arrive"); } } diff --git a/crates/latkit/src/derive/sight.rs b/crates/latkit/src/derive/sight.rs index dc57ec34..4142297a 100644 --- a/crates/latkit/src/derive/sight.rs +++ b/crates/latkit/src/derive/sight.rs @@ -1,19 +1,20 @@ -//! What the camera sees, as tiles: the one place derivations read the camera. Tiles split a -//! square of the space four ways a level, the earth's web mercator square or a square of a plane; a -//! tile is wanted where it meets what the camera shows, and splits while it draws larger than its -//! size where it lies nearest the camera's focus. +//! The camera's view as tiles; the only place derivations read the camera. +//! +//! Tiles split a square of the space four ways per level: the earth's Web Mercator square, or a +//! square of a plane. A tile is needed where it meets the region in sight, and splits while it +//! draws larger than its nominal size (`TILE_PX`) where it lies nearest the camera's focus. use glam::DVec3; use crate::tiles::TileKey; use crate::view::Projected; -/// CSS pixels a tile spans drawn at its own level. +/// CSS pixels a tile spans when drawn at its own level. pub(crate) const TILE_PX: f64 = 512.0; -/// The latitude web mercator reaches, north and south. +/// The latitude Web Mercator reaches, north and south, in degrees. const MAX_LATITUDE: f64 = 85.051_128_779_806_59; -/// Where a space's tiles lie: the earth's web mercator square, or a square of a plane. +/// The square a space's tiles divide: the earth's Web Mercator square, or a square of a plane. #[derive(Clone, Copy, PartialEq, Debug)] pub(crate) enum Domain { Earth, @@ -25,8 +26,8 @@ pub(crate) enum Domain { } impl Domain { - /// The square holding positions within `bounds` (west, south, east, north) on a plane, with a - /// little room so nothing lies on its edge. + /// The plane square containing `bounds` (west, south, east, north), centered on them and 2% + /// larger so nothing lies on its edge. Inverted bounds give the unit square. pub(crate) fn plane(bounds: [f64; 4]) -> Domain { let [w, s, e, n] = if bounds[0] <= bounds[2] { bounds } else { [0.0, 0.0, 1.0, 1.0] }; let size = (e - w).max(n - s).max(1e-9) * 1.02; @@ -34,7 +35,8 @@ impl Domain { Domain::Plane { min: [center[0] - size / 2.0, center[1] - size / 2.0], size } } - /// Where position `p` lies across the square, 0 to 1 from the west and from the north. + /// Where position `p` lies in the square, 0 to 1 from the west and from the north. On the + /// earth, latitude is clamped to Web Mercator's range and longitude wraps. pub(crate) fn unit(&self, p: [f64; 2]) -> [f64; 2] { match *self { Domain::Earth => { @@ -46,7 +48,7 @@ impl Domain { } } - /// The position at `u` across the square. + /// The position at `u` in the square; the inverse of `unit`. pub(crate) fn position(&self, u: [f64; 2]) -> [f64; 2] { match *self { Domain::Earth => { @@ -57,8 +59,8 @@ impl Domain { } } - /// West, south, east, and north of tile `key`, grown on every side by `grow` across the square, - /// within the square. + /// West, south, east, and north of tile `key` in position units, grown on every side by `grow` + /// (a fraction of the square) and clamped to the square. pub(crate) fn bounds(&self, key: TileKey, grow: f64) -> [f64; 4] { let n = f64::from(1_u32 << key.z); let (x, y) = (f64::from(key.x), f64::from(key.y)); @@ -72,20 +74,20 @@ impl Domain { } } -/// What the camera sees: where tiles are wanted and how large they draw. +/// The camera's view, for choosing tiles and measuring how large they draw. pub(crate) struct Sight<'a> { pub(crate) projected: &'a Projected, - /// The view's size, CSS pixels. + /// The view's size, in CSS pixels. pub(crate) size: [f64; 2], pub(crate) earth: bool, } -/// West, south, east, north boxes, in position units: what the camera shows, in one box, or on the -/// earth two where it wraps the antimeridian. +/// The region in sight as west, south, east, north boxes in position units: one box, or on the +/// earth two where the region crosses the antimeridian. pub(crate) type Region = Vec<[f64; 4]>; impl Sight<'_> { - /// Where position `p` lands on screen, if it shows. + /// Where position `p` appears on screen; `None` when it does not show. fn project(&self, p: [f64; 2]) -> Option<[f64; 2]> { let w = self.projected.world([p[0], p[1], 0.0]); if !self.facing(w) { @@ -101,7 +103,7 @@ impl Sight<'_> { Some([x, y]) } - /// The center of the globe the earth is, and its radius, in world units. + /// The globe's center and radius, in world units. fn sphere(&self) -> (DVec3, f64) { let radius = 1.0 / self.projected.center[1].to_radians().cos(); (DVec3::new(0.0, 0.0, -radius), radius) @@ -111,7 +113,8 @@ impl Sight<'_> { self.earth && self.projected.globe >= 0.5 } - /// Whether world point `w` faces the camera: always on a map, on the globe its near side. + /// Whether world point `w` faces the camera: always on a flat map; on the globe, only on the + /// near side. fn facing(&self, w: DVec3) -> bool { if !self.globe() { return true; @@ -120,14 +123,15 @@ impl Sight<'_> { (w - center).dot(self.projected.eye - w) > 0.0 } - /// Where tiles draw largest: where the camera looks, and tilted, the nearest ground it shows. + /// Where tiles draw largest: the camera's center, and the ground at the bottom middle of the + /// view, which is the nearest ground when the camera is tilted. pub(crate) fn focus(&self) -> Vec<[f64; 2]> { let center = [self.projected.center[0], self.projected.center[1]]; let near = self.ground([self.size[0] / 2.0, self.size[1]]); std::iter::once(center).chain(near).collect() } - /// The ground the camera shows, sampled on a grid of points across the view. + /// The ground in sight, sampled on a grid of points across the view. pub(crate) fn region(&self) -> Region { const SAMPLES: usize = 7; let globe = self.globe(); @@ -142,7 +146,7 @@ impl Sight<'_> { missed = true; continue; }; - // Longitudes kept beside the center's, so a view across the antimeridian is one span. + // Keep longitudes near the center's, so a view across the antimeridian is one span. let x = if globe { center[0] + turn(center[0], x) } else { x }; (w, e, s, n) = (w.min(x), e.max(x), s.min(y), n.max(y)); } @@ -152,7 +156,8 @@ impl Sight<'_> { return Vec::new(); } if missed { - // A tilted plane past its horizon: everything toward it. + // Some samples miss a tilted plane, which shows its horizon: grow the region by + // four times its size on each side to cover the ground toward it. let size = (e - w).max(n - s) * 4.0; return vec![[w - size, s - size, e + size, n + size]]; } @@ -177,7 +182,8 @@ impl Sight<'_> { return vec![[-180.0, -90.0, 180.0, 90.0]]; } if missed { - // A tilted map past its horizon: everything toward it. + // Some samples miss a tilted map, which shows its horizon: grow the region by four + // times its size on each side to cover the ground toward it. let size = (e - w).max(n - s) * 4.0; return boxes(w - size, e + size, s - size, n + size); } @@ -188,7 +194,7 @@ impl Sight<'_> { vec![[(w - pad_x).max(-180.0), (s - pad_y).max(-90.0), (e + pad_x).min(180.0), (n + pad_y).min(90.0)]] } - /// The cap of the globe the camera can see past its horizon. + /// The cap of the globe the camera can see, out to its horizon. fn cap(&self) -> Region { let (center, radius) = self.sphere(); let out = self.projected.eye - center; @@ -205,9 +211,10 @@ impl Sight<'_> { boxes(lon - half, lon + half, south, north) } - /// How large tile `key` of `domain`'s square draws where it lies nearest each focus, and how far - /// from the view's middle that lands. It steps across the tile in the square, where the tiles of a - /// level are alike, so on a flat map they all measure the same wherever they lie. + /// How large tile `key` of `domain`'s square draws, in CSS pixels, at its point nearest each + /// focus, and how far that point is from the view's center. It steps 1/64 of the tile in the + /// square, where all tiles of a level are the same size, so on a flat map every tile of a level + /// measures the same wherever it lies. pub(crate) fn measure(&self, focus: &[[f64; 2]], domain: &Domain, key: TileKey) -> (f64, f64) { let side = 1.0 / f64::from(1_u32 << key.z); let (west, north) = (f64::from(key.x) * side, f64::from(key.y) * side); @@ -215,7 +222,7 @@ impl Sight<'_> { let (mut size, mut distance) = (0.0_f64, f64::INFINITY); for &f in focus { let [fx, fy] = domain.unit(f); - // The earth's square wraps: the focus a turn either way, whichever lies nearer the tile. + // The earth's square wraps: shift the focus a turn either way, whichever is nearer. let fx = if *domain == Domain::Earth { fx + (west + side / 2.0 - fx).round() } else { fx }; let at = [fx.clamp(west, west + side - step), fy.clamp(north, north + side - step)]; let Some(p) = self.project(domain.position(at)) else { continue }; @@ -228,12 +235,13 @@ impl Sight<'_> { } } -/// The shortest turn from longitude `from` to `to`, in degrees. +/// The shortest signed turn from longitude `from` to `to`, in degrees. fn turn(from: f64, to: f64) -> f64 { (to - from + 540.0).rem_euclid(360.0) - 180.0 } -/// The boxes of a longitude range that may wrap, and a latitude range. +/// The boxes covering a longitude range that may wrap the antimeridian, and a latitude range +/// clamped to ±90°. fn boxes(west: f64, east: f64, south: f64, north: f64) -> Region { let (s, n) = (south.max(-90.0), north.min(90.0)); if east - west >= 360.0 { @@ -244,7 +252,7 @@ fn boxes(west: f64, east: f64, south: f64, north: f64) -> Region { if e <= 180.0 { vec![[w, s, e, n]] } else { vec![[w, s, 180.0, n], [-180.0, s, e - 360.0, n]] } } -/// Whether box `b` meets what the camera shows. +/// Whether box `b` meets the region in sight. pub(crate) fn meets(region: &Region, b: [f64; 4]) -> bool { region.iter().any(|r| b[0] <= r[2] && b[2] >= r[0] && b[1] <= r[3] && b[3] >= r[1]) } @@ -254,7 +262,7 @@ mod tests { use super::*; #[test] - fn domains_place_positions_across_their_square() { + fn domains_map_positions_into_their_unit_square() { let earth = Domain::Earth; let u = earth.unit([0.0, 0.0]); assert!((u[0] - 0.5).abs() < 1e-12 && (u[1] - 0.5).abs() < 1e-12); @@ -262,7 +270,7 @@ mod tests { assert!((back[0] + 97.5).abs() < 1e-9 && (back[1] - 30.25).abs() < 1e-9); let plane = Domain::plane([0.0, 0.0, 10.0, 5.0]); let u = plane.unit([5.0, 2.5]); - assert!((u[0] - 0.5).abs() < 1e-12 && (u[1] - 0.5).abs() < 1e-12, "the middle of the data"); + assert!((u[0] - 0.5).abs() < 1e-12 && (u[1] - 0.5).abs() < 1e-12, "the middle of the data maps to 0.5"); let b = plane.bounds(TileKey { z: 1, x: 0, y: 0 }, 0.0); assert!(b[0] < 0.0 && b[3] > 5.0, "the northwest quarter: {b:?}"); } diff --git a/crates/latkit/src/derive/stars.rs b/crates/latkit/src/derive/stars.rs index 3a1c6830..bffb112d 100644 --- a/crates/latkit/src/derive/stars.rs +++ b/crates/latkit/src/derive/stars.rs @@ -1,7 +1,10 @@ -//! Stars outside diagrams: a net's edges. Each row of an edge mark that `joins` a table is a net -//! joining the vertex rows whose reference field names it, drawn from their middle to each, as the -//! vertex mark drawing them places them. The star of each net row is a path field beside the net -//! table, which the mark draws as any geometry; it works again only when the data or scene change. +//! Stars of nets, outside diagrams. +//! +//! Each row of an edge mark with `joins` is a net joining the vertex rows whose reference field +//! names it. The net draws as a star: a segment from the members' centroid to each member, at the +//! places their vertex mark gives them. Stars are a `route` path field beside the net table, which +//! the mark draws like any geometry. They are recomputed only when the data, the scene, or the +//! layout's places change. use super::graph::{VertexMark, marks, position}; use super::{Input, Tables, beside, kept}; @@ -16,8 +19,8 @@ pub(crate) struct Stars { } impl Stars { - /// Brings the stars up to `input`, the members placed by their data or by `places`, the layout's - /// places beside their tables; whether a table changed. + /// Updates the stars to `input`. A member is at its data's position, else at its place in the + /// layout's `places` beside its table. Returns whether a table changed. pub(crate) fn update(&mut self, input: &Input<'_>, places: &Tables) -> bool { let joining = input.scene.marks.iter().any(|m| matches!(m, Mark::Edges(e) if e.joins.is_some())); if matches!(input.scene.space, Space::Diagram(_)) || !joining { @@ -82,7 +85,7 @@ impl Stars { } } -/// Where member row `row` stands: as its data positions it, else where the layout placed it. +/// The position of member row `row`: its data's position, else the layout's place for it. fn place(vm: &VertexMark<'_>, rows: &Rows<'_>, row: usize, placed: Option<&crate::Table>) -> Option<[f64; 2]> { position(vm.mark, rows, row).or_else(|| { let table = placed?; @@ -93,7 +96,7 @@ fn place(vm: &VertexMark<'_>, rows: &Rows<'_>, row: usize, placed: Option<&crate }) } -/// A net's star: a path from the middle of `members` to each; none for fewer than two. +/// A net's star: a path from the centroid of `members` to each member; empty for fewer than two. fn star(members: &[[f64; 2]]) -> Vec> { if members.len() < 2 { return Vec::new(); diff --git a/crates/latkit/src/derive/tiled.rs b/crates/latkit/src/derive/tiled.rs index 58e699e1..1a19371f 100644 --- a/crates/latkit/src/derive/tiled.rs +++ b/crates/latkit/src/derive/tiled.rs @@ -1,13 +1,17 @@ -//! Tables drawn in part. A table with a priority draws its rows in priority order, the highest on -//! top; one too large to draw whole draws the rows of the tiles the camera asks for, or where nothing -//! places its rows, those that matter most. Each draws in an [`Order`]: its pyramid's, worked out from -//! where its rows lie and their priority, on the frame while that fits its budget, else in a job, a -//! dataset's tables in one; until it lands, an even sample of its rows first, as many as a first look -//! needs, after the rows the edges sampled end at. A table and the tables its edges end at draw from -//! their pyramids together, or from their samples. A pyramid serves while the rows it was made from -//! hold; rows appended since draw after them, so appending never thins what draws, until they are an -//! eighth as many and it is made again. What the camera asks for of a table is ranges of its order: -//! nothing is gathered as it moves. +//! Tables drawn in a computed order: by priority, or by the tiles in sight. +//! +//! A table with a priority draws its rows in priority order, highest on top. A paged table (too +//! large to draw whole) draws the rows of the tiles the camera needs, or, where nothing places its +//! rows, the rows that matter most. Each draws in an [`Order`]. The order is its pyramid's, +//! computed from where its rows lie and their priority: on the frame when that fits the budget, +//! else in one job per dataset. Until the pyramid arrives, the table draws an even sample of its +//! rows first, enough for a first look, after the rows that sampled edges end at. +//! +//! A table and the table its edges end at draw from their pyramids together, or both from their +//! samples. A pyramid serves while the rows it was made from are unchanged. Rows appended since +//! draw after them, so appending never thins the drawn rows; once the appended rows reach an +//! eighth of the pyramid's rows (at least a page), the pyramid is rebuilt. What the camera needs of +//! a table is ranges of its order, so nothing is gathered as it moves. use std::ops::Range; use std::sync::Arc; @@ -23,9 +27,9 @@ use crate::expr::{Expr, Node}; use crate::gpu::shaders::PAGE_ROWS; use crate::host::Job; -/// Microseconds placing a row takes, by estimate. +/// Estimated microseconds to place one row in a pyramid. const PLACE_COST: f64 = 1.0; -/// Rows a table drawn in part draws at most until its pyramid lands: a first look at all of it. +/// The most rows a table samples before its pyramid arrives: enough for a first look at all of it. const SAMPLE: u32 = 1 << 16; /// How a table draws. @@ -33,7 +37,7 @@ const SAMPLE: u32 = 1 << 16; pub(crate) enum Draw { /// Every row. Whole, - /// The rows of the tiles the camera asks for, as many as `rows` at most. + /// The rows of the tiles the camera needs, at most `rows`. Tiles { rows: u32 }, /// The `rows` rows that matter most. First { rows: u32 }, @@ -42,47 +46,47 @@ pub(crate) enum Draw { /// Where a table's rows lie, as the mark drawing them places them. #[derive(Clone, Debug, PartialEq)] pub(crate) enum Place { - /// Nowhere a camera's tiles reach: off a map or a plane. + /// Not in a space tiles cover: neither a map nor a plane. Nowhere, - /// At a point each: x and y. + /// At one point per row: x and y. Points([Expr; 2]), /// Along a geometry field. Geometry(Name), - /// Between the rows its reference fields name. + /// Between the rows its endpoint reference fields name. Ends(Vec), } -/// How one table draws, and what that reads. +/// How one table draws, with the inputs its order reads: priority and placement. #[derive(Clone, Debug, PartialEq)] pub(crate) struct Planned { pub(crate) draw: Draw, - /// Each row's priority, as the first mark giving one has it. + /// Each row's priority, from the first mark that gives one. pub(crate) priority: Option, pub(crate) place: Place, } impl Planned { - /// Whether the table draws as it is: whole, in its own order. + /// Whether the table draws whole in table order, needing no pyramid. pub(crate) fn as_is(&self) -> bool { self.draw == Draw::Whole && self.priority.is_none() } } -/// How each table drawn draws, by dataset and table. +/// How each drawn table draws, by dataset and table. pub(crate) type Plans = Keyed<(Name, Name), Planned>; -/// What drawing in part reads this frame. +/// This frame's inputs for tables drawn in a computed order. pub(crate) struct Ask<'a> { pub(crate) data: &'a [Snapshot], pub(crate) plans: &'a Plans, - /// What the camera sees, on a map or a plane. + /// The camera's view, on a map or a plane. pub(crate) sight: Option<&'a Sight<'a>>, pub(crate) earth: bool, } -/// What a view draws of a table drawn in part: its order, the ranges of it the camera asks for, -/// finest tiles first, so what matters more draws on top, and whether the rows its plan gives it held -/// back some of what the camera asks for: only then does a plan within less draw less of it. +/// What a view draws of a paged table: its order, and the ranges of it to draw, finest tiles first +/// so more important rows draw on top. `capped` is whether the plan's row budget dropped some of +/// what the camera needs; only then does a smaller budget draw less of the table. #[derive(Clone, Debug, PartialEq)] pub(crate) struct Chosen { pub(crate) order: Order, @@ -90,17 +94,17 @@ pub(crate) struct Chosen { pub(crate) capped: bool, } -/// What each table drawn in part draws, by dataset and table. +/// What each paged table draws, by dataset and table. pub(crate) type Choices = Keyed<(Name, Name), Chosen>; -/// The pyramids of each dataset drawn in part. +/// The pyramids of each dataset with paged tables. #[derive(Default)] pub(crate) struct Tiled { sets: Vec, } impl Tiled { - /// Brings each dataset's pyramids up to `ask`, and chooses what each table drawn in part draws. + /// Updates each dataset's pyramids to `ask`, and chooses what each paged table draws. pub(crate) fn update(&mut self, ask: &Ask<'_>, jobs: &mut Jobs<'_>) -> Choices { let mut wanted: Vec<&Name> = Vec::new(); for ((data, _), planned) in ask.plans { @@ -121,22 +125,22 @@ impl Tiled { choices } - /// Whether every table drawn in part draws from its pyramid: none draws its first sample while - /// its pyramid is worked out. + /// Whether every paged table draws from its pyramid: no pyramid job is pending and no table + /// draws its sample. pub(crate) fn complete(&self) -> bool { self.sets.iter().all(|s| s.working.is_none() && !s.sampling) } - /// The bounds of every row of table `table` of dataset `data` drawn through tiles, west, south, - /// east, north in position units: what a fit of everything frames, whatever tiles are drawn. + /// The bounds of every row of paged table `table` of dataset `data` (west, south, east, north, + /// in position units), regardless of the tiles drawn. A fit to everything frames them. pub(crate) fn bounds(&self, data: &Name, table: &Name) -> Option<[f64; 4]> { let set = self.sets.iter().find(|s| s.name == *data)?; set.placed.get(table).and_then(|placed| placed.bounds) } } -/// A table's pyramid: the order of the table's rows it was made from, the square it tiles, and the -/// bounds of its rows in position units. +/// A table's pyramid, with the table's order version it was made from, the square it tiles, and +/// the bounds of its rows in position units. struct Placed { order: Version, domain: Domain, @@ -145,22 +149,24 @@ struct Placed { } impl Placed { - /// Whether it serves `table`: the rows it was made from hold, rows appended since aside. + /// Whether it still serves `table`: the rows it was made from are unchanged, apart from rows + /// appended since. fn serves(&self, table: &Table) -> bool { self.order == table.order && table.len() >= self.pyramid.rows() as usize } } -/// A dataset's pyramid job under way: the tables it answers, by their place among those asked, each -/// with the order of its rows asked from, and the square and bounds its pyramid is kept with. +/// A dataset's pending pyramid job: the tables it answers, each with its index among those asked, +/// its order version when asked, and its bounds; and the square the pyramids tile. struct Working { pending: Pending>, tables: Vec<(u32, Name, Version, Option<[f64; 4]>)>, domain: Domain, } -/// What a table's pyramid was asked for from: its rows and their order then, how it is placed, and -/// the columns placing it, as last seen; and whether a row of those it had was written since. +/// The inputs a table's pyramid was requested from, as last seen: its row count and order version, +/// a stamp of how it is placed, and the columns placing it. Also whether any of those rows was +/// written since. struct Read { rows: usize, order: Version, @@ -178,8 +184,8 @@ impl Read { Read { rows: table.len(), order: table.order, inputs: inputs_of(planned, earth), columns, written: false } } - /// Sees `table` anew: whether a row it had was written since, or its rows reordered or removed. - /// Once one was, it stays so. + /// Checks `table` again, setting `written` if a row it had was written since, or its rows were + /// reordered or removed. Once set, it stays set. fn see(&mut self, table: &Table) { if self.written { return; @@ -197,20 +203,20 @@ impl Read { } } -/// One dataset drawn in part. +/// The pyramids of one dataset with paged tables. struct Set { name: Name, /// The stamp of everything its pyramids are placed from, as last seen. stamp: u64, - /// Each table's pyramid, as last placed. + /// Each table's latest pyramid. placed: Named, working: Option, - /// What the pyramids last asked for were asked from, by table. + /// The inputs of the last pyramid request, by table. read: Named, - /// Each table's rows that the samples of the edges ending at it end at, the step of its own sample, - /// and what they were worked out from. + /// Per table: the stamp of its inputs, the rows that sampled edges ending at it end at (lifted + /// into its sample), and the step of its own sample. lifted: Named<(u64, Lifted, u32)>, - /// Whether a table draws an even sample of its rows until its pyramid lands. + /// Whether any table draws its sample because no pyramid serves it yet. sampling: bool, } @@ -227,7 +233,7 @@ impl Set { } } - /// Brings the pyramids up to `ask`, and chooses what each table drawn in part draws. + /// Updates the pyramids to `ask`, and chooses what each paged table draws. fn update(&mut self, source: &Snapshot, ask: &Ask<'_>, jobs: &mut Jobs<'_>, choices: &mut Choices) { let landed = self.land(); let placing = placed(source, ask); @@ -251,8 +257,9 @@ impl Set { (!planned.as_is()).then_some((table, planned)) }) .collect(); - // Each draws from its pyramid while that serves it, and while the pyramids of the table its - // edges end at and of the edges ending at it do: edges land with their ends. + // A table draws from its pyramid only while that pyramid serves it and so do the pyramids + // of the table its edges end at and of the edge tables ending at it, since edges and their + // endpoints are assigned levels together. let ends: Vec> = drawn .iter() .map(|(table, planned)| { @@ -277,7 +284,7 @@ impl Set { (None, Draw::Whole) => continue, (None, Draw::Tiles { .. } | Draw::First { .. }) => { self.sampling = true; - // The rows the edges sampled end at draw first, as they land with them in a pyramid. + // The rows sampled edges end at draw first, as in a pyramid. let edges: Vec = (0..drawn.len()).filter(|&e| ends[e] == Some(i)).collect(); let (rows, step) = if edges.is_empty() { (Lifted::default(), steps[i]) @@ -297,12 +304,12 @@ impl Set { let made = pyramid.rows(); let (ranges, capped) = match draw { Draw::Whole => (std::iter::once(0..len).collect(), false), - // The rows that matter most draw last; rows appended since draw too. + // The rows that matter most are last in the order; appended rows draw too. Draw::First { rows } => (std::iter::once(made - rows.min(made)..len).collect(), rows < made), Draw::Tiles { rows } => { let (mut wanted, capped) = ask.sight.map(|s| pyramid.wanted(s, &placed.domain, rows)).unwrap_or_default(); - // Finest first, so what matters more draws on top. + // Finest first, so more important rows draw last, on top. crate::order::by_key(&mut wanted, |(z, range)| { !crate::order::pair(u32::from(*z), !range.start) }); @@ -321,12 +328,11 @@ impl Set { self.lifted = lifted; } - /// Whether what was last asked for still serves `placing`: the same tables, placed alike, none of - /// whose rows were written since but those appended; while no job is under way, each table's - /// pyramid serving it, the rows appended since fewer than an eighth of those it was made from, a - /// page at least. A job under way keeps going while only rows were appended since it was asked, - /// so appending never starves the pyramids; once it lands, it serves until the rows appended - /// since are that many. + /// Whether the last request still serves `placing`: the same tables, placed the same way, with + /// no rows written since except appended ones. With no job pending, each table's pyramid must + /// also serve it, with fewer rows appended since than an eighth of the rows it was made from + /// (at least a page). A pending job keeps running while only rows were appended, so appends + /// never starve the pyramids; once it finishes, it serves until that many rows are appended. fn holds(&self, placing: &[(&Table, &Planned)], earth: bool) -> bool { placing.len() == self.read.len() && placing.iter().all(|(table, planned)| { @@ -340,8 +346,8 @@ impl Set { }) } - /// Takes the pyramids a job answered, once it landed: whether it placed any. One that failed is - /// asked again only once what it was asked from changes. + /// Takes the pyramids of a finished job. Returns true when a job succeeded. A failed job is not + /// retried until its inputs change. fn land(&mut self) -> bool { let Some(working) = &self.working else { return false }; let Some(landed) = working.pending.take() else { return false }; @@ -357,15 +363,15 @@ impl Set { true } - /// Asks for the pyramids of `placing`: at once where they fit the frame, else in one job for the - /// dataset. + /// Requests the pyramids of `placing`: computed at once when they fit the frame's budget, else + /// in one job for the dataset. fn level(&mut self, placing: &[(&Table, &Planned)], ask: &Ask<'_>, jobs: &mut Jobs<'_>) { - // What was asked before no longer serves once it lands. + // Drop any pending job; its answer would no longer serve. self.working = None; self.read = placing.iter().map(|(table, planned)| (table.name.clone(), Read::new(table, planned, ask.earth))).collect(); let (leveling, bounds) = leveling(ask, placing); - // Placing a row takes about a microsecond; ordering one by priority alone, far less. + // Placing a row takes about a microsecond; ordering one by priority alone, a twentieth. let cost: f64 = leveling .tables .iter() @@ -392,10 +398,10 @@ impl Set { } } -/// How many rows each table of `drawn` samples until its pyramid lands, as many as its plan gives it, -/// and every how many rows its sample takes one: the rows the sampled edges ending at a table, by -/// `ends`, end at count toward its rows, so edges sample few enough that their ends take half their -/// table's rows at most, the table's own sample the rest. +/// Each table's sample size until its pyramid arrives (its plan's row budget, at most `SAMPLE`), +/// and its sample step. The endpoints of sampled edges count toward their target table's sample +/// (per `ends`), so edges sample sparsely enough that their endpoints fill at most half the +/// target's sample; the target's own sample fills the rest. fn samples(drawn: &[(&Table, &Planned)], ends: &[Option]) -> (Vec, Vec) { let sample: Vec = drawn .iter() @@ -422,10 +428,10 @@ fn samples(drawn: &[(&Table, &Planned)], ends: &[Option]) -> (Vec, V (sample, steps) } -/// The rows of table `drawn[at]` that the samples of the edge tables `edges` among `drawn`, each -/// every `steps`th row, end at, lifted in its own sample of `sample` rows, and the step of its own -/// even sample of the rest: `was` where nothing it was worked out from changed, else worked out -/// again; and what they were worked out from. +/// The rows of table `drawn[at]` that the sampled rows of edge tables `edges` end at (each edge +/// table sampling every `steps[e]`th row), lifted into its sample of `sample` rows, with the step +/// of its own even sample of the rest. Returns `was` when its input stamp is unchanged, else +/// recomputes; the result starts with that stamp. fn lift( drawn: &[(&Table, &Planned)], edges: &[usize], @@ -474,8 +480,8 @@ fn lift( (from, lifted, step) } -/// The tables of `source` a pyramid places, each with its plan: every table drawn in part, and every -/// table the ends of one drawn through tiles name, which only anchors them. +/// The tables of `source` a pyramid places, each with its plan: every table not drawn as is, and +/// every table that a tiled edge table's endpoints reference, placed only to anchor those edges. fn placed<'a>(source: &'a Snapshot, ask: &'a Ask<'_>) -> Vec<(&'a Table, &'a Planned)> { let plan = |table: &Table| ask.plans.get(&(source.name().clone(), table.name.clone())); let mut out: Vec<(&Table, &Planned)> = Vec::new(); @@ -488,12 +494,12 @@ fn placed<'a>(source: &'a Snapshot, ask: &'a Ask<'_>) -> Vec<(&'a Table, &'a Pla out.push((table, planned)); } } - // Tables an edge table's ends name come before it. + // Tables an edge table's endpoints reference come before it. crate::order::by_key(&mut out, |(_, p)| u64::from(matches!(p.place, Place::Ends(_)))); out } -/// The table the ends of `table` name, as `planned` places it. +/// The table that `table`'s endpoint references point to, when `planned` places it by endpoints. pub(crate) fn ends_of<'t>(table: &'t Table, planned: &Planned) -> Option<&'t Name> { let Place::Ends(fields) = &planned.place else { return None }; match table.field_named(fields.first()?)? { @@ -515,15 +521,14 @@ fn stamp(source: &Snapshot, ask: &Ask<'_>, placed: &[(&Table, &Planned)]) -> u64 stamp_of((source.name(), tables)) } -/// What a pyramid placing `planned`'s rows is made for besides its rows: how they are placed and -/// ordered, whether they draw through tiles or as they are, and on the earth or not. +/// A stamp of what a pyramid for `planned` depends on besides its rows: how rows are placed and +/// prioritized, whether they draw by tile or as is, and whether the space is the earth. fn inputs_of(planned: &Planned, earth: bool) -> u64 { let tiles = matches!(planned.draw, Draw::Tiles { .. }); stamp_of((format!("{:?}{:?}", planned.priority, planned.place), tiles, planned.as_is(), earth)) } -/// The fields of its own table a pyramid places `planned`'s rows by: their priority, and where they -/// lie. +/// The fields of its own table a pyramid for `planned` reads: those of its priority and its place. fn fields_read(planned: &Planned) -> Vec { let mut fields = Vec::new(); if let Some(priority) = &planned.priority { @@ -538,8 +543,7 @@ fn fields_read(planned: &Planned) -> Vec { fields } -/// The column of field `name` of `table` a pyramid places its rows by: its values, or the rows a -/// reference names. +/// The column a pyramid reads for field `name` of `table`: its values, or a reference's target ids. fn column_of<'t>(table: &'t Table, name: &Name) -> Option<&'t Column> { match table.field_named(name)? { Field::Values(column, _) => Some(column), @@ -567,7 +571,7 @@ fn fields_of(expr: &Expr, out: &mut Vec) { } } -/// The box every placed row of `places` lies in, west, south, east, north; inverted when none is. +/// The box (west, south, east, north) around every placed row of `places`; inverted when none is. fn bounds_of(places: &Places) -> [f64; 4] { let mut b = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY]; let mut take = |r: [f64; 4]| { @@ -583,7 +587,7 @@ fn bounds_of(places: &Places) -> [f64; 4] { b } -/// Where `table`'s rows lie, as `place` puts them. +/// Where `table`'s rows lie, per `place`: a point or a geometry's box per row. fn locate(table: &Table, place: &Place, all: &[Snapshot]) -> Places { match place { Place::Points([x, y]) => { @@ -614,13 +618,14 @@ fn locate(table: &Table, place: &Place, all: &[Snapshot]) -> Places { } } -/// What to ask pyramid jobs for `placed`, in the square they tile, and each table's bounds. +/// The pyramid job input for `placed`, in the square they tile, and each table's bounds. fn leveling(ask: &Ask<'_>, placed: &[(&Table, &Planned)]) -> (Leveling, Named<[f64; 4]>) { let names: Vec = placed.iter().map(|(t, _)| t.name.clone()).collect(); let mut tables: Vec = placed .iter() .map(|(table, planned)| { - // Tables drawn through tiles are placed; one only an edge table's ends name anchors them. + // Tiled tables are placed, and so are tables drawn as is, which are here only to anchor + // the tiled edges ending at them. let anchors = matches!(planned.draw, Draw::Tiles { .. }) || planned.as_is(); let places = if anchors { locate(table, &planned.place, ask.data) } else { Places::None }; Placing { rows: table.len() as u32, answered: !planned.as_is(), places, ..Placing::default() } @@ -666,7 +671,7 @@ mod tests { use crate::{Data, Ids, Patch}; #[test] - fn a_sample_lifts_the_rows_its_edges_sampled_end_at() { + fn a_sample_lifts_the_endpoints_of_its_sampled_edges() { // A thousand vertices, and a thousand edges between vertices far apart in the table. let n = 1000_u64; let data = Data::new("g"); @@ -685,14 +690,14 @@ mod tests { let drawn = [(vertices, &at), (edges, &ends)]; let (from, lifted, step) = lift(&drawn, &[1], &[10, 10], 0, 300, None); let order = Order::Sampled { rows: n as u32, step, lifted: lifted.clone() }; - // Every edge the edges' sample draws ends at a row the vertices' sample draws. + // Every sampled edge ends at rows the vertices' sample draws. for e in (0..n).step_by(10) { for t in [(e * 7) % n, (e * 13 + 1) % n] { - assert!(order.rank(t as u32) < order.sampled(), "edge {e} ends at vertex {t}, drawn first"); + assert!(order.rank(t as u32) < order.sampled(), "vertex {t}, an endpoint of edge {e}, draws first"); } } - assert!(order.sampled() <= 300, "the rows lifted count toward the sample, the rest waiting"); + assert!(order.sampled() <= 300, "lifted rows count toward the sample's 300 rows"); let again = lift(&drawn, &[1], &[10, 10], 0, 300, Some(&(from, lifted, step))); - assert_eq!(again.0, from, "worked out once while its edges hold"); + assert_eq!(again.0, from, "computed once while its edges are unchanged"); } } diff --git a/crates/latkit/src/error.rs b/crates/latkit/src/error.rs index 0e3b4d3b..7388ae2e 100644 --- a/crates/latkit/src/error.rs +++ b/crates/latkit/src/error.rs @@ -7,30 +7,30 @@ use crate::Name; #[derive(Clone, Debug, PartialEq, thiserror::Error)] #[non_exhaustive] pub enum Error { - /// An input does not fit what it describes; `path` names the part, such as `marks[2].vertices.color`. + /// An input is invalid. `path` names the invalid part, such as `marks[2].vertices.color`. #[error("{path}: {reason}")] Invalid { - /// Where in the input the problem is. + /// Location of the problem in the input. path: Path, - /// What is wrong there. + /// Why that part is invalid. reason: Cow<'static, str>, }, - /// The platform or device cannot do what was asked. + /// The platform or device does not support the request. #[error("unsupported: {0}")] Unsupported(Cow<'static, str>), /// A budget or device limit would be exceeded. #[error("over limit: {0}")] Limit(Cow<'static, str>), - /// Reading or writing external bytes failed: a fetch, a file, a worker, an encoder. + /// Reading or writing external bytes failed, such as a fetch, file, worker, or encoder. #[error("io: {0}")] Io(Cow<'static, str>), /// The operation did not complete before its deadline. #[error("operation timed out")] Timeout, - /// The GPU device was lost; create a new `Gpu`. + /// The GPU device was lost. Create a new `Gpu`. #[error("GPU device lost")] DeviceLost, - /// The owner of pending work was destroyed before it could answer. + /// The owner of pending work was dropped before the work finished. #[error("operation cancelled")] Cancelled, } @@ -58,7 +58,7 @@ impl Error { } } - /// Prefixes the path of an [`Error::Invalid`] with `step`; other errors pass through. + /// Prefixes the path of an [`Error::Invalid`] with `step`. Returns other errors unchanged. #[must_use] pub fn at(self, step: impl Into) -> Error { match self { @@ -71,7 +71,7 @@ impl Error { } } -/// The error as `{ code, message, path? }`: the shape the npm package's `LatkitError` takes. +/// Serializes as `{ code, message, path? }`, the shape of the npm package's `LatkitError`. impl serde::Serialize for Error { fn serialize(&self, serializer: S) -> Result { use serde::ser::SerializeMap; @@ -85,7 +85,7 @@ impl serde::Serialize for Error { } } -/// The error as it serializes: a failure, as plain data. +/// Schema of the serialized error, named `Failure`. impl schemars::JsonSchema for Error { fn schema_name() -> Cow<'static, str> { "Failure".into() @@ -109,7 +109,7 @@ impl schemars::JsonSchema for Error { #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Path(pub Vec); -/// One step of a [`Path`]: a named part or an index. +/// One step of a [`Path`]: a named part or a list index. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Step { /// A named part, such as a table, field, or option. diff --git a/crates/latkit/src/expr/color.rs b/crates/latkit/src/expr/color.rs index 9c821f19..f930b1e3 100644 --- a/crates/latkit/src/expr/color.rs +++ b/crates/latkit/src/expr/color.rs @@ -1,6 +1,6 @@ use std::fmt; -/// An sRGB color with straight alpha, each channel in `0..=1`, as in CSS. +/// An sRGB color with straight (not premultiplied) alpha, each channel in `0..=1`, as in CSS. #[derive(Clone, Copy, PartialEq, Debug)] pub struct Color { /// Red. @@ -31,7 +31,7 @@ impl Color { Color { r, g, b, a: 1.0 } } - /// A color from `#rgb`, `#rgba`, `#rrggbb`, or `#rrggbbaa`. + /// A color from `#rgb`, `#rgba`, `#rrggbb`, or `#rrggbbaa`; `None` for any other text. pub fn hex(hex: &str) -> Option { let digits = hex.strip_prefix('#')?; if !digits.is_ascii() { @@ -50,7 +50,8 @@ impl Color { Some(Color::from_bytes([r, g, b, a])) } - /// A color from CSS: hex, `rgb(…)`, `rgba(…)`, or a named color. + /// A color from CSS: hex, `rgb(…)` or `rgba(…)` (channels 0 to 255 or percentages, alpha 0 to 1 + /// or a percentage), a named color, or `transparent`. Case-insensitive; `None` if unrecognized. pub fn css(css: &str) -> Option { let css = css.trim(); if css.starts_with('#') { @@ -92,7 +93,7 @@ impl Color { Color::rgba(red, green, blue, alpha) } - /// Red, green, blue, and alpha as bytes. + /// Red, green, blue, and alpha as bytes, clamped and rounded. pub fn to_rgba8(self) -> [u8; 4] { let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8; [q(self.r), q(self.g), q(self.b), q(self.a)] diff --git a/crates/latkit/src/expr/colormap.rs b/crates/latkit/src/expr/colormap.rs index f860b514..30f5eb4b 100644 --- a/crates/latkit/src/expr/colormap.rs +++ b/crates/latkit/src/expr/colormap.rs @@ -7,10 +7,10 @@ use super::ramp::{self, Spread}; use crate::gpu::shaders as wgsl; use crate::{Error, Result}; -/// Colors for a range of values: one of the catalog's, or one of your own. +/// Colors for a range of values: a catalog colormap or a custom one. /// /// Sequential and diverging maps clamp, cyclic maps wrap, and categorical maps bin values by their -/// integer part: category `i` takes color `i`, and may be named with [`Colormap::categories`]. +/// integer part: category `i` takes color `i`. [`Colormap::categories`] names the categories. #[derive(Clone, PartialEq, Debug)] pub struct Colormap(Map); @@ -28,13 +28,14 @@ struct Custom { categories: Vec>, } -/// How a colormap spans values. +/// How a colormap maps values: `sequential` and `diverging` maps clamp, `cyclic` maps wrap, and +/// `categorical` maps give each integer its own color. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)] #[serde(rename_all = "lowercase")] pub enum ColormapKind { /// Low to high. Sequential, - /// Below and above a middle. + /// Below and above a midpoint. Diverging, /// Around a circle: values wrap. Cyclic, @@ -51,7 +52,7 @@ impl ColormapKind { } } - /// The code shaders read it by. + /// The code shaders identify it by. #[cfg(feature = "gpu")] pub(crate) fn code(self) -> u32 { match self { @@ -63,8 +64,8 @@ impl ColormapKind { } } -/// A colormap's color, at a place from 0 to 1 or evenly between its neighbors: from a [`Color`] or -/// a `(place, Color)` pair. +/// A colormap color stop: a [`Color`], spaced evenly between its neighbors, or a `(place, Color)` +/// pair with a place from 0 to 1. #[derive(Clone, Copy, PartialEq, Debug)] pub struct Stop { at: Option, @@ -105,27 +106,29 @@ pub(crate) fn catalog() -> impl Iterator { } impl Colormap { - /// The catalog colormap named `name`, such as `"viridis"`. + /// The catalog colormap named `name`, such as `"viridis"`; `None` if the catalog has none. pub fn named(name: &str) -> Option { CATALOG.iter().position(|e| e.name == name).map(|i| Colormap(Map::Catalog(i as u8))) } - /// A colormap through `stops`, colors or `(place, color)` pairs: blended in OKLab, so lightness - /// runs evenly, a color without a place evenly between its neighbors as in a CSS gradient; or - /// for a categorical map, one color per category. Fails without a stop, for places outside - /// `0..=1` or out of order, and for a categorical map's places or more than 256 categories. + /// A colormap through `stops`, each a color or a `(place, color)` pair. Colors blend in OKLab, + /// so lightness changes evenly; a color without a place sits evenly between its neighbors, as + /// in a CSS gradient. A categorical map takes one color per category instead. + /// + /// Errors when `stops` is empty, when places lie outside `0..=1` or out of order, and when a + /// categorical map has places or more than 256 colors. pub fn new>(kind: ColormapKind, stops: impl IntoIterator) -> Result { let stops: Vec<(Option, [f32; 4])> = stops.into_iter().map(Into::into).map(|s: Stop| (s.at, s.color.to_array())).collect(); let places = || stops.iter().filter_map(|s| s.0); let wrong: Option> = match kind { _ if stops.is_empty() => Some("a colormap needs a color".into()), - ColormapKind::Categorical if places().next().is_some() => Some("a category's color has no place".into()), + ColormapKind::Categorical if places().next().is_some() => Some("categories cannot have places".into()), ColormapKind::Categorical if stops.len() > 256 => { Some(format!("a categorical map has at most 256 colors, not {}", stops.len()).into()) } - _ if places().any(|p| !(0.0..=1.0).contains(&p)) => Some("a color's place is from 0 to 1".into()), - _ if !places().is_sorted() => Some("colors' places ascend".into()), + _ if places().any(|p| !(0.0..=1.0).contains(&p)) => Some("a color's place must be from 0 to 1".into()), + _ if !places().is_sorted() => Some("colors' places must ascend".into()), _ => None, }; if let Some(reason) = wrong { @@ -135,8 +138,8 @@ impl Colormap { Ok(Colormap(Map::Custom(Arc::new(Custom { kind, count: stops.len() as u32, texels, categories: Vec::new() })))) } - /// The same categorical map with its categories named in order, from category 0: what its legend - /// lists. Panics for a continuous map. + /// This categorical map with its categories named in order from category 0, for its legend. + /// Panics if the map is not categorical. #[must_use] pub fn categories>>(&self, names: impl IntoIterator) -> Colormap { assert_eq!(self.kind(), ColormapKind::Categorical, "only a categorical map has categories"); @@ -157,7 +160,7 @@ impl Colormap { } } - /// How the colormap spans values. + /// How the colormap maps values. pub fn kind(&self) -> ColormapKind { match &self.0 { Map::Catalog(i) => CATALOG[*i as usize].kind, @@ -165,7 +168,7 @@ impl Colormap { } } - /// The catalog name, or `None` for a colormap of your own. + /// The catalog name, or `None` for a custom colormap. pub fn name(&self) -> Option<&'static str> { match &self.0 { Map::Catalog(i) => Some(CATALOG[*i as usize].name), @@ -173,7 +176,7 @@ impl Colormap { } } - /// A readable title, such as `"Red / Blue"`. + /// A human-readable title, such as `"Red / Blue"`; `"Custom"` for a custom colormap. pub fn label(&self) -> &str { match &self.0 { Map::Catalog(i) => CATALOG[*i as usize].label, @@ -199,7 +202,8 @@ impl Colormap { Colormap(Map::Custom(Arc::new(Custom { kind: self.kind(), count: self.count(), texels, categories }))) } - /// The color at `t` in `0..=1`: clamped, wrapped for cyclic maps, binned for categorical maps. + /// The color at `t`. A continuous map takes `t` in `0..=1`, clamping it, or wrapping it for a + /// cyclic map. A categorical map takes a category number, binned by its integer part. pub fn sample(&self, t: f32) -> Color { let texels = self.texels(); if self.kind() == ColormapKind::Categorical { @@ -214,7 +218,7 @@ impl Colormap { Color::rgba(red, green, blue, alpha) } - /// How many colors it was made from: a categorical map's categories. + /// The number of colors the map was made from; for a categorical map, its category count. pub fn count(&self) -> u32 { match &self.0 { Map::Catalog(i) => CATALOG[*i as usize].count, @@ -229,7 +233,7 @@ impl Colormap { } } - /// Identifies the colormap's colors, for sharing one texture layer. + /// Identifies the colormap's colors, so maps with the same colors share one texture layer. pub(crate) fn key(&self) -> ColormapKey { use std::hash::{Hash, Hasher}; match &self.0 { @@ -274,7 +278,7 @@ mod tests { #[test] fn custom_maps_blend_and_reverse() { let map = Colormap::new(ColormapKind::Sequential, [Color::BLACK, Color::WHITE]).unwrap(); - // Blended in OKLab: half lightness, a darker byte than sRGB's middle. + // Blended in OKLab, half lightness is a darker byte than sRGB's midpoint. assert!((95..105).contains(&map.sample(0.5).to_rgba8()[0])); assert_eq!(map.reversed().sample(0.0).to_rgba8(), [255, 255, 255, 255]); let placed = Colormap::new(ColormapKind::Sequential, [(0.0, Color::BLACK), (0.5, Color::WHITE)]).unwrap(); @@ -295,7 +299,7 @@ mod tests { assert!(Colormap::new(ColormapKind::Sequential, [Color::BLACK; 0]).is_err()); assert!(wrong(ColormapKind::Sequential, &[(0.5, Color::BLACK), (0.2, Color::WHITE)]).contains("ascend")); assert!(wrong(ColormapKind::Cyclic, &[(1.5, Color::BLACK)]).contains("0 to 1")); - assert!(wrong(ColormapKind::Categorical, &[(0.0, Color::BLACK)]).contains("no place")); + assert!(wrong(ColormapKind::Categorical, &[(0.0, Color::BLACK)]).contains("cannot have places")); assert!(Colormap::new(ColormapKind::Categorical, [Color::BLACK; 257]).is_err()); } } diff --git a/crates/latkit/src/expr/compile.rs b/crates/latkit/src/expr/compile.rs index 1edc52f1..ff0b8178 100644 --- a/crates/latkit/src/expr/compile.rs +++ b/crates/latkit/src/expr/compile.rs @@ -1,7 +1,7 @@ -//! Expressions to bytecode: each channel of a mark is a range of ops that ends in a store. +//! Compiles expressions to bytecode. Each channel of a mark is a range of ops ending in a store. //! -//! Compilation type-checks against the data's fields and records what each channel depends on, so -//! the view evaluates a channel only when one of its inputs changes. +//! Compilation type-checks expressions against the data's fields and records each channel's +//! inputs, so the view re-evaluates a channel only when one of its inputs changes. use std::ops::BitOr; use std::sync::Arc; @@ -10,7 +10,7 @@ use super::{Binary, Colormap, ColormapKind, Domain, Expr, FieldRef, Node, State, use crate::gpu::shaders as wgsl; use crate::{Error, Kind, Name, Result}; -/// What a channel reads, so the view knows when to evaluate it again. +/// The kinds of input a channel reads, so the view knows when to re-evaluate it. #[derive(Clone, Copy, Default, PartialEq, Eq, Hash, Debug)] pub(crate) struct Deps(u8); @@ -49,19 +49,19 @@ pub(crate) enum Words { Values, /// Text as dictionary codes. Codes, - /// Reference rows as `u32`, `NONE` where unwired. + /// Referenced rows as `u32`, `NONE` where the reference is unset. Bits, /// A point's x. X, /// A point's y. Y, - /// A geometry's points per row: a page's row count, each row's first point and the end, then - /// every point's x and y; parts are broken by a missing point. + /// A geometry's points per row: the page's row count, each row's first point index and the end + /// index, then every point's x and y. A missing point separates parts. Path, } -/// A column a program reads: of the mark's own table, of another dataset's table joined by row id, -/// or of the table the view derives beside the mark's own, row for row. +/// A column a program reads: from the mark's own table, from another dataset's table joined by row +/// id, or from the derived table the view keeps alongside the mark's table, row for row. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub(crate) struct ColumnUse { pub(crate) from: Option<(Name, Name)>, @@ -70,9 +70,9 @@ pub(crate) struct ColumnUse { pub(crate) words: Words, pub(crate) sampled: bool, pub(crate) step: bool, - /// Also read the part `f32` drops of an `f64`, for positions. + /// Also read the low part that `f32` loses from an `f64`, for full-precision positions. pub(crate) low: bool, - /// Read every frame in view, as a line tracing the field does. + /// Read at every frame in view, as a line tracing the field does. pub(crate) trace: bool, } @@ -97,15 +97,15 @@ impl ColumnUse { } } -/// An end of each row of a mark: the table whose row it names, and the program column of the -/// reference field naming it, `NONE` where the end is the row's own vertex, as a pole's. +/// One end of each row of a mark: the vertex table it references, and the program column of the +/// reference field, or `NONE` when the end is the row's own vertex, as for a pole. #[derive(Clone, PartialEq, Debug)] pub(crate) struct EndUse { pub(crate) table: Name, pub(crate) column: u32, } -/// A constant holding a text's dictionary code in a column; the view writes it. +/// A constant that holds `text`'s dictionary code in `column`; the view fills it in. #[derive(Clone, PartialEq, Debug)] pub(crate) struct TextUse { pub(crate) constant: u32, @@ -113,7 +113,7 @@ pub(crate) struct TextUse { pub(crate) text: Arc, } -/// A mark's compiled channels and what they read. +/// A mark's compiled channels and their inputs. #[derive(Clone, Default, PartialEq, Debug)] pub(crate) struct Program { pub(crate) code: Vec<[u32; 4]>, @@ -121,26 +121,28 @@ pub(crate) struct Program { /// Op ranges of every channel, then of every reduction. pub(crate) ranges: Vec<[u32; 2]>, pub(crate) channels: Vec, - /// What each fitted domain's input reads, and the input: a table drawn in part fits a domain - /// reading only its data over all its rows on the CPU. + /// The [`Reads`] of each fitted domain's input, then the inputs themselves. For a paged table, + /// a domain whose input reads only data (no view state or derived fields) is fitted on the CPU + /// over all rows. pub(crate) domains: Vec, pub(crate) fitted: Vec, pub(crate) columns: Vec, pub(crate) texts: Vec, - /// Colormap ops, whose layer the view writes. + /// Each colormap op's index in `code`, with its colormap; the view writes the op's texture + /// layer. pub(crate) colormaps: Vec<(u32, Colormap)>, - /// Each row's ends, by number: where each is held, the end pass writes into the kind word of its - /// number. + /// Each row's ends, in order. The end pass writes the GPU row holding end `i` into kind word + /// `i`. pub(crate) ends: Vec, - /// State words stored in position units. + /// Bit mask of the state words stored in position units. pub(crate) units: u32, } -/// What a channel or a fitted domain reads: kinds of input, and which of the program's columns. +/// The inputs of a channel or a fitted domain: kinds of input, and which of the program's columns. #[derive(Clone, Copy, Default, PartialEq, Eq, Debug)] pub(crate) struct Reads { pub(crate) deps: Deps, - /// Bit `i` for column `i`; the last bit stands for every column past it. + /// Bit `i` for column `i`; bit 63 also covers every column after it. pub(crate) columns: u64, } @@ -157,7 +159,7 @@ pub(crate) struct Channel { } impl Program { - /// What all channels read together. + /// The kinds of input all channels read together. pub(crate) fn reads(&self) -> Deps { self.channels.iter().fold(Deps::default(), |all, c| all | c.reads.deps) } @@ -176,7 +178,7 @@ pub(crate) enum Store { pub(crate) trait Fields { /// The kind of `name` in the mark's table, or in `from`'s table. fn kind(&self, from: Option<&(Name, Name)>, name: &str) -> Option; - /// The table a reference field of the mark's table names. + /// The table that reference field `name` of the mark's table points to. fn target(&self, name: &str) -> Option; } @@ -246,8 +248,8 @@ impl<'a> Compiler<'a> { Ok(()) } - /// Compiles a position channel reading the place the view derives for each row, at full - /// precision. + /// Compiles a position channel that reads each row's derived position (from a diagram's sheet + /// or the scene's layout), at full precision. pub(crate) fn derived_position(&mut self, word: u32) -> Result<()> { let start = self.begin(); let [x, y] = @@ -269,7 +271,7 @@ impl<'a> Compiler<'a> { Ok(()) } - /// The program column of the derived field `field`, read as words. + /// The program column of the derived field `field`, read as `u32` words. pub(crate) fn derived_bits(&mut self, field: &str) -> u32 { self.column(ColumnUse::derived(field, Words::Bits)) } @@ -284,7 +286,8 @@ impl<'a> Compiler<'a> { self.column(ColumnUse::own(field, Words::Path)) } - /// The program column of the mark's reference field `name`, read as the rows it names. + /// The program column of the mark's reference field `name`, read as the rows it references. + /// Errors if `name` is not a reference field. pub(crate) fn bits(&mut self, name: &str) -> Result { if self.fields.kind(None, name) != Some(Kind::Reference) { return Err(invalid("not a reference field")); @@ -292,27 +295,27 @@ impl<'a> Compiler<'a> { Ok(self.column(ColumnUse::own(name, Words::Bits))) } - /// Refuses `expr` for option `name` when it reads anything but static fields: the CPU works it - /// out from the data alone, as it does a row's priority. + /// Errors if `expr`, given for option `name`, reads anything but static fields. Options such as + /// a row's priority are computed on the CPU from the data alone. pub(crate) fn check_data(&self, name: &str, expr: &Expr) -> Result<()> { match self.unstructural(expr) { - Some(reason) => Err(invalid(format!("{name} reads data only, not {reason}")).at(name)), + Some(reason) => Err(invalid(format!("{name} can read only data, not {reason}")).at(name)), None => Ok(()), } } - /// Refuses `exprs` for option `name` when they read anything but static fields: where a - /// diagram's rows stand and how large they are is its structure, made from the data alone. + /// Errors if any of `exprs`, given for option `name`, reads anything but static fields. A + /// diagram's positions and sizes are its structure, which the sheet computes from data alone. pub(crate) fn check_structure<'e>(&self, name: &str, exprs: impl IntoIterator) -> Result<()> { for expr in exprs { if let Some(reason) = self.unstructural(expr) { - return Err(invalid(format!("a diagram's {name} reads data only, not {reason}")).at(name)); + return Err(invalid(format!("a diagram's {name} can read only data, not {reason}")).at(name)); } } Ok(()) } - /// What `expr` reads that structure cannot: view state or a sampled field. + /// The first input of `expr` that structure cannot depend on: view state or a sampled field. fn unstructural(&self, expr: &Expr) -> Option<&'static str> { match &*expr.0 { Node::State(State::Selected | State::Hovered) => Some("selection or hover"), @@ -332,7 +335,7 @@ impl<'a> Compiler<'a> { } } - /// Adds the next end of each row: the vertex the reference field `name` names. + /// Adds the next end of each row: the vertex that reference field `name` references. pub(crate) fn end(&mut self, name: &str) -> Result<()> { if self.fields.kind(None, name) != Some(Kind::Reference) { return Err(invalid("not a reference field").at(name)); @@ -343,16 +346,16 @@ impl<'a> Compiler<'a> { Ok(()) } - /// Adds the next end of each row: the row itself, as the vertex mark drawing the mark's own table - /// holds it, row for row, as a pole stands on. + /// Adds the next end of each row: the row's own vertex, as placed by the vertex mark that draws + /// `table`, the mark's own table. A pole stands on it. pub(crate) fn own_end(&mut self, table: &Name) { self.program.ends.push(EndUse { table: table.clone(), column: wgsl::NONE }); } - /// Compiles channel `name` storing the code of text field `expr` at `word`, and returns the - /// program column whose dictionary names the codes. + /// Compiles channel `name`, storing the dictionary code of text field `expr` at `word`. Returns + /// the program column whose dictionary decodes the codes. pub(crate) fn text(&mut self, name: &str, expr: &Expr, word: u32) -> Result { - let Node::Field(f) = &*expr.0 else { return Err(invalid("a label's text is a text field").at(name)) }; + let Node::Field(f) = &*expr.0 else { return Err(invalid("a label's text must be a text field").at(name)) }; if self.fields.kind(f.from.as_ref(), &f.name) != Some(Kind::Text) { return Err(invalid("not a text field").at(&f.name).at(name)); } @@ -364,7 +367,8 @@ impl<'a> Compiler<'a> { Ok(column) } - /// The column of sampled field `name` that a line traces over the axis. + /// The column of sampled field `name` that a line traces over the axis. Errors if `name` is not + /// a sampled field. pub(crate) fn trace(&mut self, name: &str) -> Result { if self.fields.kind(None, name) != Some(Kind::Sampled) { return Err(invalid("not a sampled field").at(name)); @@ -446,7 +450,7 @@ impl<'a> Compiler<'a> { fn coordinate(&mut self, e: &Expr) -> Result<()> { if mentions_pointer(e) { - return Err(invalid("pointer() cannot place a position")); + return Err(invalid("a position cannot use pointer()")); } self.expect(e, Ty::Number) } @@ -493,8 +497,8 @@ impl<'a> Compiler<'a> { Node::Binary(op, a, b) => self.binary(*op, a, b), Node::When(c, a, b) => { self.expect(c, Ty::Bool).map_err(|e| e.at("when"))?; - // A missing value takes the other branch's type, so it goes last: a missing first - // branch swaps the branches and negates the condition. + // A missing value takes the other branch's type, so it must compile second. A + // missing first branch swaps the branches and negates the condition. let (a, b) = match (&*a.0, &*b.0) { (Node::Missing, Node::Missing) => (a, b), (Node::Missing, _) => { @@ -512,17 +516,17 @@ impl<'a> Compiler<'a> { let ty = match (ta, self.emit(b)?) { (a, b) if a == b => a, (Ty::Bool, Ty::Number) | (Ty::Number, Ty::Bool) => Ty::Number, - (a, b) => return Err(invalid(format!("when chooses between {} and {}", ty_name(a), ty_name(b)))), + (a, b) => return Err(invalid(format!("when mixes {} and {}", ty_name(a), ty_name(b)))), }; self.op(wgsl::OP_SELECT, 0, 0, 0, 3, 1)?; Ok(ty) } Node::Contains(a, text) => { - // The view works out on the CPU whether each row's text holds the text, as a field - // derived beside the mark's own table. - let Node::Field(f) = &*a.0 else { return Err(invalid("contains reads a text field")) }; + // The view computes each row's match on the CPU, as a derived field alongside the + // mark's own table. + let Node::Field(f) = &*a.0 else { return Err(invalid("contains needs a text field")) }; if f.from.is_some() || self.fields.kind(None, &f.name) != Some(Kind::Text) { - return Err(invalid("contains reads a text field of the mark's own table").at(&f.name)); + return Err(invalid("contains needs a text field of the mark's own table").at(&f.name)); } let name = crate::derive::matches::contains_of(&f.name, text); let column = self.column(ColumnUse::derived(name, Words::Values)); @@ -531,9 +535,9 @@ impl<'a> Compiler<'a> { Ok(Ty::Bool) } Node::Is(a, text) => { - let Node::Field(f) = &*a.0 else { return Err(invalid("is compares a text field")) }; + let Node::Field(f) = &*a.0 else { return Err(invalid("is needs a text field")) }; if self.fields.kind(f.from.as_ref(), &f.name) != Some(Kind::Text) { - return Err(invalid("is compares a text field").at(&f.name)); + return Err(invalid("is needs a text field").at(&f.name)); } let column = self.field_column(f, Words::Codes, false); self.op(wgsl::OP_FIELD, column, 0, 0, 0, 1)?; @@ -582,7 +586,7 @@ impl<'a> Compiler<'a> { (Kind::Sampled, None) => (Words::Values, Ty::Number, true), (Kind::Point, Some(0)) => (Words::X, Ty::Number, false), (Kind::Point, Some(1)) => (Words::Y, Ty::Number, false), - (Kind::Reference, _) => return Err(invalid("a reference names rows; read it as an end").at(&f.name)), + (Kind::Reference, _) => return Err(invalid("a reference field holds rows, not values").at(&f.name)), _ => return Err(invalid("not a number, boolean, or text field").at(&f.name)), }; let column = self.field_column(f, words, sampled); @@ -637,7 +641,8 @@ impl<'a> Compiler<'a> { Ok(ty) } - /// A domain record in the constants; a fitted one adds a reduction of its input. + /// Adds a domain record to the constants and returns its index. A fitted domain also compiles a + /// reduction of its input. fn domain(&mut self, input: &Expr, domain: Domain) -> Result { let at = self.program.constants.len() as u32; match domain { @@ -646,7 +651,7 @@ impl<'a> Compiler<'a> { } Domain::Fit => { if self.reducing { - return Err(invalid("a fitted domain's input cannot fit another domain; set one with domain")); + return Err(invalid("nested fitted domains are not supported; give the inner one a domain")); } let index = self.program.domains.len() as u32; self.program.domains.push(Reads::default()); diff --git a/crates/latkit/src/expr/cpu.rs b/crates/latkit/src/expr/cpu.rs index 3754165e..b2721a8c 100644 --- a/crates/latkit/src/expr/cpu.rs +++ b/crates/latkit/src/expr/cpu.rs @@ -1,6 +1,7 @@ -//! The expression machine on the CPU, for what a diagram's sheet reads (where rows stand, how large -//! they are, and the texts its blocks fit) and what a query keeps, sorts, and selects. The GPU runs -//! the same expressions for everything drawn; here they read fields, sampled ones at a coordinate. +//! Evaluates expressions on the CPU, for work done from the data alone: a diagram's sheet +//! (positions, sizes, and block label texts), layouts, row priorities, fitted domains of paged +//! tables, and queries (filters, sort keys, and selections). The GPU evaluates the same expressions +//! for drawing. Here they read only fields, with sampled fields read at one axis coordinate. use std::cell::RefCell; use std::sync::Arc; @@ -15,9 +16,10 @@ use crate::data::{Field, Geometry, Snapshot, Table, Value}; pub(crate) struct Rows<'a> { pub(crate) table: &'a Table, pub(crate) all: &'a [Snapshot], - /// The axis coordinate sampled fields are read at: their last frame by default. + /// The axis coordinate at which sampled fields are read; infinity, their last frame, by + /// default. at: f64, - /// Fitted domains already reduced, by the input they reduce. + /// Fitted domains already computed, keyed by their input node. fitted: RefCell>, /// The texts `contains` looks for, lowercased once. lowered: RefCell, Arc>>, @@ -28,25 +30,26 @@ impl<'a> Rows<'a> { Rows { table, all, at: f64::INFINITY, fitted: RefCell::default(), lowered: RefCell::default() } } - /// `text` lowercased, as `contains` looks for it. + /// `text` lowercased for `contains`, cached. fn lowered(&self, text: &Arc) -> Arc { self.lowered.borrow_mut().entry(text.clone()).or_insert_with(|| text.to_lowercase().into()).clone() } - /// The rows with sampled fields read at axis coordinate `at`. + /// These rows, with sampled fields read at axis coordinate `at`. pub(crate) fn at(mut self, at: f64) -> Rows<'a> { self.at = at; self } - /// `expr` for `row` as a number; `None` where it reads missing, or holds no number. + /// `expr` evaluated for `row`; `None` where the result is missing or not a finite number. pub(crate) fn number(&self, expr: &Expr, row: usize) -> Option { let v = self.eval(expr, row); v.is_finite().then_some(v) } - /// `expr` for every row, NaN where it reads missing, each made into a `T` by `make`: a plain - /// field read page by page, anything else row by row. + /// `expr` evaluated for every row and converted by `make`, with NaN for missing values. A + /// constant fills every row, and a plain field of the table is read page by page; anything else + /// is evaluated for each row individually. pub(crate) fn numbers(&self, expr: &Expr, mut make: impl FnMut(f64) -> T) -> Vec { let n = self.table.len(); let mut out = Vec::with_capacity(n); @@ -83,7 +86,7 @@ impl<'a> Rows<'a> { out } - /// The text field `expr` names, for `row`. + /// The value of text field `expr` in `row`. pub(crate) fn text(&self, expr: &Expr, row: usize) -> Option> { let Node::Field(f) = &*expr.0 else { return None }; match self.value(f, row)? { @@ -92,7 +95,8 @@ impl<'a> Rows<'a> { } } - /// Field `f`'s value for `row`: of the table, or of a joined one by the row's id. + /// Field `f`'s value in `row`: from the table, or for a join, from the joined table's row with + /// the same id. fn value(&self, f: &FieldRef, row: usize) -> Option { let value = match &f.from { None => self.table.value(row, &f.name, self.at)?, @@ -108,7 +112,7 @@ impl<'a> Rows<'a> { }) } - /// `expr` as the GPU computes it, NaN standing for missing. + /// `expr` computed as the GPU computes it, with NaN for missing. fn eval(&self, expr: &Expr, row: usize) -> f64 { match &*expr.0 { Node::Number(v) => *v, @@ -153,7 +157,8 @@ impl<'a> Rows<'a> { } } - /// The extent of `input` over every row, as a fitted domain reduces it. + /// The extent of `input` over every row, as a fitted domain computes it; `[0, 1]` when no row + /// has a value. fn fit(&self, input: &Expr) -> [f64; 2] { let key = Arc::as_ptr(&input.0); if let Some(&extent) = self.fitted.borrow().get(&key) { @@ -216,7 +221,7 @@ mod tests { use crate::{Ids, Table, field, when}; #[test] - fn numbers_follow_the_gpu() { + fn cpu_evaluation_matches_the_gpu() { let table = Table::new("T", Ids::count(3)) .field("w", vec![Some(10.0), None, Some(30.0)]) .field("on", vec![true, false, true]); diff --git a/crates/latkit/src/expr/form.rs b/crates/latkit/src/expr/form.rs index 0654f939..9210dc66 100644 --- a/crates/latkit/src/expr/form.rs +++ b/crates/latkit/src/expr/form.rs @@ -1,6 +1,6 @@ -//! Expressions, colors, sizes, and colormaps as JSON. A number or boolean is a constant, a string a -//! field, and an object one node, named by its key; in a color option, a [`Paint`], a string is a CSS -//! color instead, and a conditional's values are colors: +//! JSON forms of expressions, colors, sizes, and colormaps. A number or boolean is a constant, a +//! string is a field name, and an object is one node whose key names its kind. In a color option (a +//! [`Paint`]), a string is a CSS color instead, and a conditional's values are colors: //! //! | Form | Builder | //! | ------------------------------------------------------------- | --------------------------------- | @@ -16,9 +16,9 @@ //! | `{ "px": a }` (sizes only) | `px(a)` | //! | `{ "units": a, "atLeast": 2, "atMost": 20 }` (sizes only) | `units(a).at_least(..)` | //! -//! Each object is its kind's form exactly: a key another kind has is refused. A map is a catalog name -//! or `{ "kind": "sequential", "colors": ["black", [0.2, "red"], "white"] }`, a color placed as a -//! `[place, color]` pair, and `categories` naming a categorical map's colors. +//! An object may have only its kind's keys; a key of another kind is an error. A colormap is a +//! catalog name or `{ "kind": "sequential", "colors": ["black", [0.2, "red"], "white"] }`, where a +//! `[place, color]` pair places a color and `categories` names a categorical map's colors. use std::borrow::Cow; use std::fmt; @@ -44,9 +44,9 @@ impl<'de> Deserialize<'de> for Paint { } } -/// What an expression, a color, a size, or a label is written as, read where it is written: a -/// constant, a text, or an object, whose keys say what it is once all are read. Where it is read says -/// what a text is: a field's name, or in a color option a CSS color. +/// The JSON of an expression, color, size, or label, before its meaning is known: a constant, a +/// string, or an object whose keys decide its kind once all are read. The caller decides what a +/// string means: a field name, or in a color option a CSS color. pub(crate) enum Written { Constant(Expr), Text(String), @@ -54,7 +54,7 @@ pub(crate) enum Written { } impl Written { - /// The expression it writes: a text names a field. + /// The expression this JSON describes; a string names a field. pub(crate) fn expr(self) -> Result { match self { Written::Constant(expr) => Ok(expr), @@ -63,14 +63,14 @@ impl Written { } } - /// The color it writes, as a color option reads it: a text is a CSS color, and a conditional's - /// values are colors. + /// The color this JSON describes, as a color option reads it: a string is a CSS color, and a + /// conditional's values are colors. pub(crate) fn paint(self) -> Result { match self { Written::Constant(expr) => Ok(expr), Written::Text(css) => Color::css(&css) .map(Expr::from) - .ok_or_else(|| format!("not a CSS color: {css:?}; a field is field({css:?})")), + .ok_or_else(|| format!("not a CSS color: {css:?}; for a field, use field({css:?})")), Written::Object(form) => form.paint(), } } @@ -116,8 +116,8 @@ impl<'de> Visitor<'de> for Reading { } } -/// Every key an expression object, a size, or a label's options hold; each kind's own alone make it. -/// A conditional's values are read as where it is read says. +/// Every key that an expression object, a size, or label options can have; each kind accepts only +/// its own keys. A conditional's values are read as the caller decides. #[derive(Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub(crate) struct Form { @@ -150,7 +150,7 @@ pub(crate) struct Form { } impl Form { - /// The keys it holds, as written. + /// The keys present, as JSON spells them. fn held(&self) -> Vec<&'static str> { [ ("field", self.field.is_some()), @@ -185,10 +185,10 @@ impl Form { .collect() } - /// Why it is not `what`, holding only `keys`: the first other key it holds. + /// Errors unless every key present is one of `keys`, naming `what` and the first other key. pub(crate) fn only(&self, what: &str, keys: &[&str]) -> Result<(), String> { match self.held().into_iter().find(|key| !keys.contains(key)) { - Some(key) => Err(format!("{what} has no {key}")), + Some(key) => Err(format!("{what} takes no {key}")), None => Ok(()), } } @@ -198,8 +198,8 @@ impl Form { self.color.take().map(Written::paint).transpose() } - /// The color it writes, as a color option reads it: a conditional's values are colors; anything - /// else is the expression it writes, which must make a color. + /// The color this object describes, as a color option reads it: a conditional's values are + /// colors, and any other object is an expression, which must produce a color. pub(crate) fn paint(self) -> Result { if self.when.is_none() { return self.expr(); @@ -207,21 +207,22 @@ impl Form { self.conditional(Written::paint) } - /// The conditional it writes, each of its values read by `value`. + /// The conditional this object describes, each value read by `value`. fn conditional(self, value: fn(Written) -> Result) -> Result { self.only("when", &["when", "otherwise"])?; let otherwise = value(self.otherwise.ok_or("when needs otherwise")?)?; - let cases = self.when.expect("held"); + let cases = self.when.expect("present"); cases.into_iter().rev().try_fold(otherwise, |rest, (c, v)| Ok(node(Node::When(c, value(v)?, rest)))) } - /// The expression it writes: the first kind it holds, and only that kind's keys. + /// The expression this object describes. Its kind is that of its first kind key, and it may + /// have only that kind's keys. pub(crate) fn expr(self) -> Result { let held = self.held(); let Some(own) = held.iter().find_map(|key| KINDS.iter().find(|keys| keys[0] == *key)) else { let mut kinds: Vec<&str> = KINDS.iter().map(|keys| keys[0]).collect(); kinds.dedup(); - return Err(format!("an expression object has one of {}", kinds.join(", "))); + return Err(format!("an expression object needs one of {}", kinds.join(", "))); }; let kind = own[0]; self.only(kind, own)?; @@ -232,7 +233,7 @@ impl Form { let needs = |key: &str| format!("{kind} needs {key}"); Ok(match kind { "field" => { - let expr = super::field(field.expect("held")); + let expr = super::field(field.expect("present")); let expr = match self.from { Some((data, table)) => expr.from_table(data, table), None => expr, @@ -241,17 +242,17 @@ impl Form { } "color" => match color { Some(Written::Text(css)) => Color::css(&css).ok_or_else(|| format!("not a CSS color: {css:?}"))?.into(), - _ => return Err("a color is a CSS color".to_owned()), + _ => return Err("color must be a CSS color".to_owned()), }, - "state" => node(Node::State(state.expect("held"))), + "state" => node(Node::State(state.expect("present"))), "missing" => { if missing != Some(true) { - return Err("missing is true".to_owned()); + return Err("missing must be true".to_owned()); } Expr::missing() } "op" => { - let op = op.expect("held"); + let op = op.expect("present"); let args = self.args.ok_or_else(|| needs("args"))?; let name = || IntoDeserializer::::into_deserializer(op.as_str()); if let Ok(unary) = Unary::deserialize(name()) { @@ -266,26 +267,26 @@ impl Form { } } "is" => { - let (a, text) = is.expect("held"); + let (a, text) = is.expect("present"); a.is(&text) } "contains" => { - let (a, text) = contains.expect("held"); + let (a, text) = contains.expect("present"); a.contains(&text) } "scale" => { - let [lo, hi] = scale.expect("held"); + let [lo, hi] = scale.expect("present"); domained(self.value.ok_or_else(|| needs("a value"))?.scale(lo..=hi), self.domain) } _ => { let value = self.value.ok_or_else(|| needs("a value"))?; - domained(value.colormap(colormap.expect("held")), self.domain) + domained(value.colormap(colormap.expect("present")), self.domain) } }) } } -/// `mapped` with `domain` when given, else fitting the values shown. +/// `mapped` with `domain` when given, else fitted to the values shown. fn domained(mapped: super::Mapped, domain: Option<[f64; 2]>) -> Expr { match domain { Some([lo, hi]) => mapped.domain(lo..=hi).into(), @@ -293,24 +294,24 @@ fn domained(mapped: super::Mapped, domain: Option<[f64; 2]>) -> Expr { } } -/// Each kind of expression object, declared once: the schema of its JSON, which reading never builds, -/// and the keys reading lets an object of that kind hold, its own key first. +/// Declares each kind of expression object once: a struct for its JSON schema, never built because +/// reading goes through `Form`, and the keys an object of that kind may have, its own key first. macro_rules! kinds { ($($(#[$meta:meta])* $node:ident { $($(#[$attr:meta])* $key:ident: $ty:ty),+ $(,)? })+) => { $( $(#[$meta])* #[derive(JsonSchema)] - #[expect(dead_code, reason = "its schema alone: expressions read through `Form`")] + #[expect(dead_code, reason = "used for its schema only; `Form` reads expressions")] #[serde(deny_unknown_fields)] struct $node { $($(#[$attr])* $key: $ty,)+ } )+ - /// Each kind's keys, its own first, in the order an object's keys name its kind. + /// Each kind's keys, its own key first. const KINDS: &[&[&str]] = &[$(&[$(stringify!($key)),+]),+]; - /// Each kind's schema, in that order. + /// Each kind's schema, in `KINDS` order. fn kinds(generator: &mut SchemaGenerator) -> Vec { vec![$(generator.subschema_for::<$node>()),+] } @@ -318,55 +319,64 @@ macro_rules! kinds { } kinds! { - /// A field of each row: of the mark's own table, or from another dataset's table joined by row id. + /// A field's value in each row: from the mark's own table, or with `from`, from another + /// dataset's table joined by row id. FieldNode { + /// The field's name. field: Name, /// The dataset and table the field is read from, joined by row id. #[serde(default)] #[schemars(extend("tsType" = "[data: string, table: string]"))] from: Option<(Name, Name)>, - /// Reads a sampled field at its last frame without interpolating. + /// For a sampled field, reads the last frame at or before the shown coordinate instead of + /// interpolating. Default `false`. #[serde(default)] step: bool, } + /// A constant color. ColorNode { color: Color, } - /// What the view knows of each row. + /// View state of each row, such as whether it is selected. StateNode { state: State, } - /// A missing value: options that read one take the theme's. + /// A missing value. An option that evaluates to missing uses the theme's value. MissingNode { #[schemars(extend("const" = true))] missing: bool, } + /// A one-argument operation, such as `{ "op": "abs", "args": ["x"] }`. UnaryNode { op: Unary, args: [Expr; 1], } + /// A two-argument operation, such as `{ "op": "add", "args": ["a", 1] }`. BinaryNode { op: Binary, args: [Expr; 2], } /// The value of the first case whose condition holds, else `otherwise`. WhenNode { + /// The cases, as `[condition, value]` pairs tried in order. #[schemars(extend("tsType" = "[condition: ExprInput, value: ExprInput][]"))] when: Vec<(Expr, Expr)>, + /// The value where no condition holds. otherwise: Expr, } - /// Whether a text field equals the text. + /// Whether a text field equals the text exactly. IsNode { #[schemars(extend("tsType" = "[field: ExprInput, text: string]"))] is: (Expr, String), } - /// Whether a text field holds the text, ignoring case: in a mark, a field of its own table. + /// Whether a text field contains the text, ignoring case. In a mark, the field must belong to + /// the mark's own table. ContainsNode { #[schemars(extend("tsType" = "[field: ExprInput, text: string]"))] contains: (Expr, String), } - /// `value` mapped linearly onto the range `scale`, clamped: from its `domain`, else from the extent of - /// the values shown. + /// `value` mapped linearly from `domain` onto the range `scale`, clamped to the range. Without + /// a `domain`, the extent of the values shown is used. ScaleNode { #[schemars(extend("tsType" = "[low: number, high: number]"))] scale: [f64; 2], @@ -375,7 +385,9 @@ kinds! { #[schemars(extend("tsType" = "[low: number, high: number]"))] domain: Option<[f64; 2]>, } - /// `value` through `colormap`: from its `domain`, else from the extent of the values shown. + /// `value` mapped through `colormap` over `domain`, or without a `domain`, over the extent of + /// the values shown. A categorical map ignores the domain and bins values by their integer + /// part. ColormapNode { colormap: Colormap, value: Expr, @@ -385,33 +397,35 @@ kinds! { } } -/// A size in CSS pixels, whatever the space's own unit. +/// A size in CSS pixels, regardless of the space's unit. #[derive(JsonSchema)] -#[expect(dead_code, reason = "its schema alone: sizes read through `Form`")] +#[expect(dead_code, reason = "used for its schema only; `Form` reads sizes")] #[serde(deny_unknown_fields)] struct Px { + /// The size, in CSS pixels. px: Expr, } -/// A size in position units, whatever the space's own unit, so it scales as the view zooms, kept -/// within CSS pixels when given. +/// A size in position units (degrees on the earth), regardless of the space's unit, so it scales +/// as the view zooms. `atLeast` and `atMost` bound it in CSS pixels. #[derive(JsonSchema)] -#[expect(dead_code, reason = "its schema alone: sizes read through `Form`")] +#[expect(dead_code, reason = "used for its schema only; `Form` reads sizes")] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[allow(clippy::struct_field_names, reason = "its JSON is `{units, atLeast, atMost}`")] struct Units { + /// The size, in position units. units: Expr, - /// CSS pixels it keeps at least, however far the view zooms out. + /// The minimum on-screen size in CSS pixels, however far the view zooms out. #[serde(default)] #[schemars(range(min = 0))] at_least: Option, - /// CSS pixels it keeps at most, however far the view zooms in. + /// The maximum on-screen size in CSS pixels, however far the view zooms in. #[serde(default)] #[schemars(range(min = 0))] at_most: Option, } -/// An expression is its JSON, `ExprNode`; TypeScript also takes the builders that write it. +/// An expression's schema is `ExprNode`; its TypeScript type also accepts the builders. impl JsonSchema for Expr { fn schema_name() -> Cow<'static, str> { "Expr".into() @@ -424,7 +438,7 @@ impl JsonSchema for Expr { } } -/// The JSON an expression is written as. +/// The JSON form of an expression. struct ExprNode; impl JsonSchema for ExprNode { @@ -441,23 +455,25 @@ impl JsonSchema for ExprNode { ]; any.extend(forms); json_schema!({ - "description": "A value for every row of a mark: a constant, a field, view state, or arithmetic of them.", + "description": "A value for each row: a number, a boolean, a field's name, or a node that computes one.", "anyOf": any, }) } } -/// A conditional's color: the first case's whose condition holds, else `otherwise`'s. +/// A conditional color: the color of the first case whose condition holds, else `otherwise`. #[derive(JsonSchema)] -#[expect(dead_code, reason = "its schema alone: colors read through `Form`")] +#[expect(dead_code, reason = "used for its schema only; `Form` reads colors")] #[serde(deny_unknown_fields)] struct WhenPaintNode { + /// The cases, as `[condition, color]` pairs tried in order. #[schemars(extend("tsType" = "[condition: ExprInput, color: PaintInput][]"))] when: Vec<(Expr, Paint)>, + /// The color where no condition holds. otherwise: Paint, } -/// A color is its JSON, `PaintNode`; TypeScript also takes the builders that write it. +/// A color option's schema is `PaintNode`; its TypeScript type also accepts the builders. impl JsonSchema for Paint { fn schema_name() -> Cow<'static, str> { "Paint".into() @@ -470,7 +486,7 @@ impl JsonSchema for Paint { } } -/// The JSON a color option is written as. +/// The JSON form of a color option. struct PaintNode; impl JsonSchema for PaintNode { @@ -487,7 +503,7 @@ impl JsonSchema for PaintNode { generator.subschema_for::(), ]; json_schema!({ - "description": "A color for every row of a mark: a CSS color, a value through a colormap, or a conditional choosing between colors.", + "description": "A color for each row: a CSS color, a value through a colormap, or a conditional choosing between colors.", "anyOf": any, }) } @@ -498,12 +514,12 @@ impl<'de> Deserialize<'de> for Size { let size = |written: Written| match written { Written::Object(form) if form.px.is_some() => { form.only("a size in CSS pixels", &["px"])?; - Ok(Size { expr: form.px.expect("held"), unit: Some(Unit::Px), px: Size::ANY }) + Ok(Size { expr: form.px.expect("present"), unit: Some(Unit::Px), px: Size::ANY }) } Written::Object(form) if form.units.is_some() => { form.only("a size in units", &["units", "atLeast", "atMost"])?; let px = [form.at_least.unwrap_or(0.0).max(0.0), form.at_most.unwrap_or(f32::INFINITY)]; - Ok(Size { expr: form.units.expect("held"), unit: Some(Unit::Units), px }) + Ok(Size { expr: form.units.expect("present"), unit: Some(Unit::Units), px }) } written => written.expr().map(Size::from), }; @@ -521,7 +537,7 @@ impl JsonSchema for Size { let px = generator.subschema_for::(); let units = generator.subschema_for::(); json_schema!({ - "description": "A size, in the space's unit unless it says its own: units in a diagram, CSS pixels elsewhere; `px` keeps CSS pixels, and `units` takes position units, which scale as the view zooms.", + "description": "A size: a value in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in CSS pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the view zooms in.", "anyOf": [expr, px, units], }) } @@ -534,7 +550,8 @@ impl<'de> Deserialize<'de> for Color { } } -/// A color writes as CSS `rgba(…)`, read back exactly: whole channels as bytes, alpha as it is. +/// Writes a color as CSS `rgba(…)` that reads back exactly: channels as byte values, fractional +/// where needed, and alpha unchanged. impl serde::Serialize for Color { fn serialize(&self, serializer: S) -> Result { let channel = |v: f32| { @@ -564,15 +581,16 @@ impl JsonSchema for Color { } } -/// Your own colormap: its kind, its colors, and for a categorical map its categories' names. +/// A custom colormap: its kind, its colors, and for a categorical map, its category names. #[derive(Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] struct CustomColormap { kind: ColormapKind, - /// Colors blended in OKLab, each at its `[place, color]` from 0 to 1 or evenly between its - /// neighbors, as in a CSS gradient; a categorical map's, one per category. + /// The colors, blended in OKLab. Each is a color, spaced evenly between its neighbors as in a + /// CSS gradient, or a `[place, color]` pair with a place from 0 to 1, in ascending order. A + /// categorical map takes one color per category, without places, at most 256. colors: Vec, - /// The categories' names, in order, for a legend: a categorical map's alone. + /// The category names in category order, shown in legends. Categorical maps only. #[serde(default)] categories: Vec, } @@ -722,7 +740,7 @@ mod tests { assert_eq!(size(r#"{"px": 3}"#), crate::px(3.0)); assert_eq!(size(r#"{"units": 3}"#), crate::units(3.0)); assert_eq!(size(r#"{"units": 3, "atLeast": 2, "atMost": 20}"#), crate::units(3.0).at_least(2.0).at_most(20.0)); - assert!(serde_json::from_str::(r#"{"px": 3, "atLeast": 2}"#).is_err(), "pixels hold no pixel range"); + assert!(serde_json::from_str::(r#"{"px": 3, "atLeast": 2}"#).is_err(), "px sizes take no bounds"); let map: Colormap = serde_json::from_str(r#"{"kind": "sequential", "colors": ["black", [0.25, "red"], "white"]}"#) .expect("a map"); @@ -736,16 +754,17 @@ mod tests { assert_eq!(paint("\"red\""), Ok(red.clone())); assert_eq!(paint("\"#ff0000\""), Ok(red.clone())); let error = paint("\"kv\"").expect_err("a field's name"); - assert!(error.starts_with(r#"not a CSS color: "kv"; a field is field("kv")"#), "{error}"); - // A conditional's values are colors, its conditions values; a colormap's input is a value. + assert!(error.starts_with(r#"not a CSS color: "kv"; for a field, use field("kv")"#), "{error}"); + // A conditional's values are colors and its conditions are values; a colormap's input is a + // value. let chosen = paint(r#"{"when": [["on", "red"]], "otherwise": {"colormap": "viridis", "value": "kv"}}"#); let mapped = field("kv").colormap(Colormap::VIRIDIS); assert_eq!(chosen, Ok(when(field("on")).then(Color::rgb(1.0, 0.0, 0.0)).otherwise(mapped))); - assert_eq!(paint(r#"{"field": "kv"}"#), Ok(field("kv")), "a field, which makes no color, fails as it compiles"); + assert_eq!(paint(r#"{"field": "kv"}"#), Ok(field("kv")), "a field parses here and must compile to a color"); } #[test] - fn errors_name_what_is_wrong() { + fn errors_name_the_offending_part() { let error = serde_json::from_str::(r#"{"field": "a", "color": "red"}"#).expect_err("two kinds"); assert!(error.to_string().contains("color"), "{error}"); let error = serde_json::from_str::(r#"{"field": "a", "map": "viridis"}"#).expect_err("a stray key"); diff --git a/crates/latkit/src/expr/mod.rs b/crates/latkit/src/expr/mod.rs index 6fce7f05..55d9e026 100644 --- a/crates/latkit/src/expr/mod.rs +++ b/crates/latkit/src/expr/mod.rs @@ -1,4 +1,5 @@ -//! Per-row values: expressions over fields and view state, compiled to bytecode the GPU runs. +//! Expressions: per-row values computed from fields and view state, compiled to bytecode the GPU +//! runs. mod color; mod colormap; @@ -18,9 +19,9 @@ pub use colormap::{Colormap, ColormapKind, Stop}; use crate::Name; -/// A value for every row of a mark: a constant, a field, view state, or arithmetic of them. +/// A per-row value of a mark: a constant, a field, view state, or arithmetic on them. /// -/// A `&str` converts to the field of that name, so options read fields by name: +/// A `&str` converts to the field of that name, so options can take field names directly: /// /// ``` /// # use latkit::{field, when, selected, Colormap, Color}; @@ -30,13 +31,13 @@ use crate::Name; /// let high_voltage = field("kv").ge(230.0) & field("kind").is("substation"); /// ``` /// -/// Expressions compile to bytecode; changing one never compiles a shader. +/// Expressions compile to bytecode, so changing one never recompiles a shader. #[derive(Clone, PartialEq, Debug)] pub struct Expr(pub(crate) Arc); -/// A color for every row of a mark: a color, or an expression making one, such as a value through a -/// colormap or a conditional choosing between colors. Color options take one; as JSON a string is a -/// CSS color, and a field reads as `{"field": name}`. +/// A per-row color of a mark: a fixed color, or an expression that yields one, such as a value +/// through a colormap or a conditional choosing between colors. Color options take a `Paint`. In +/// JSON a string is a CSS color, and a field is written `{"field": name}`. /// /// ``` /// # use latkit::{field, Color, Colormap, Paint}; @@ -63,17 +64,19 @@ pub(crate) enum Node { Colormap { input: Expr, colormap: Colormap, domain: Domain }, } -/// A field read: of the mark's own table, or from another dataset's table joined by row id. +/// A field reference: a field of the mark's own table, or of another dataset's table joined by row +/// id. #[derive(Clone, PartialEq, Debug)] pub(crate) struct FieldRef { pub(crate) name: Name, pub(crate) from: Option<(Name, Name)>, pub(crate) step: bool, - /// For a point geometry: 0 for x, 1 for y. + /// The component read from a point field: 0 for x, 1 for y. pub(crate) component: Option, } -/// What the view knows of each row. +/// View state of each row: `selected`, `hovered`, `pointer` (CSS pixels from a vertex to the +/// pointer), `elapsed` (seconds since the view was created), or `zoom` (the camera's zoom). #[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Deserialize, schemars::JsonSchema)] #[serde(rename_all = "lowercase")] pub(crate) enum State { @@ -84,7 +87,8 @@ pub(crate) enum State { Zoom, } -/// An operation of one value. +/// An operation on one value. `sin` and `cos` take radians; `not` treats values above 0.5 as +/// true. #[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Deserialize, schemars::JsonSchema)] #[serde(rename_all = "lowercase")] pub(crate) enum Unary { @@ -99,7 +103,9 @@ pub(crate) enum Unary { Exp, } -/// An operation of two values. +/// An operation on two values: arithmetic, `min` and `max`, comparisons (1 for true, 0 for +/// false), and logic. `rem` is a floored remainder, which takes the sign of the divisor. `and` and +/// `or` treat values above 0.5 as true. #[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Deserialize, schemars::JsonSchema)] #[serde(rename_all = "lowercase")] pub(crate) enum Binary { @@ -123,7 +129,7 @@ pub(crate) enum Binary { #[derive(Clone, Copy, PartialEq, Debug)] pub(crate) enum Domain { - /// The extent of the values shown, reduced on the GPU. + /// Fitted to the extent of the values shown, computed by a reduction on the GPU. Fit, Fixed([f64; 2]), } @@ -132,8 +138,8 @@ pub(crate) fn node(node: Node) -> Expr { Expr(Arc::new(node)) } -/// Whether `text` holds `needle`, ignoring case, `needle` lowercased already: what `contains` asks, -/// in a query and a mark alike. +/// Whether `text` contains `needle`, ignoring case. `needle` must already be lowercase. Queries and +/// marks share this test for `contains`. pub(crate) fn holds(text: &str, needle: &str) -> bool { if needle.is_empty() { return true; @@ -144,7 +150,7 @@ pub(crate) fn holds(text: &str, needle: &str) -> bool { text.to_lowercase().contains(needle) } -/// The field `name` of each row. +/// The value of field `name` in each row. pub fn field(name: impl Into) -> Expr { node(Node::Field(FieldRef { name: name.into(), from: None, step: false, component: None })) } @@ -159,12 +165,12 @@ pub fn selected() -> Expr { node(Node::State(State::Selected)) } -/// Whether the pointer is over the row. +/// Whether the row is hovered, by the pointer or by keyboard stepping. pub fn hovered() -> Expr { node(Node::State(State::Hovered)) } -/// CSS pixels from a vertex to the pointer; for vertices only, and not in their position. +/// CSS pixels from each vertex to the pointer. Use it only in vertex marks, and not in a position. pub fn pointer() -> Expr { node(Node::State(State::Pointer)) } @@ -174,20 +180,20 @@ pub fn elapsed() -> Expr { node(Node::State(State::Elapsed)) } -/// The camera's zoom: the web mercator level on the earth, log₂ CSS pixels per unit elsewhere. Options -/// that read it change with the view, as levels of detail do. +/// The camera's zoom: the web mercator zoom level on the earth, log₂ of CSS pixels per unit +/// elsewhere. Options that read it update as the camera zooms, as levels of detail do. pub fn zoom() -> Expr { node(Node::State(State::Zoom)) } -/// A size in position units, whatever the space's own: it scales as the view zooms, and -/// [`Size::at_least`] and [`Size::at_most`] keep it within CSS pixels. +/// A size in position units (degrees on the earth), regardless of the space's unit. It scales as +/// the view zooms; [`Size::at_least`] and [`Size::at_most`] bound it in CSS pixels. pub fn units(size: impl Into) -> Size { Size { expr: size.into(), unit: Some(Unit::Units), px: Size::ANY } } -/// A size in CSS pixels, whatever the space's own: a stroke keeps its width on screen as a diagram -/// zooms. +/// A size in CSS pixels, regardless of the space's unit, so it keeps its on-screen size as the view +/// zooms. In a diagram, this keeps a stroke's width constant on screen. pub fn px(size: impl Into) -> Size { Size { expr: size.into(), unit: Some(Unit::Px), px: Size::ANY } } @@ -213,7 +219,7 @@ macro_rules! binary { } impl Expr { - /// A missing value: options that read one take the theme's value. + /// A missing value. An option that evaluates to missing uses the theme's value. pub fn missing() -> Expr { node(Node::Missing) } @@ -223,11 +229,11 @@ impl Expr { abs => Abs, /// The square root. sqrt => Sqrt, - /// The sine of radians. + /// The sine of an angle in radians. sin => Sin, - /// The cosine of radians. + /// The cosine of an angle in radians. cos => Cos, - /// The largest integer at most the value. + /// The largest integer not greater than the value. floor => Floor, /// The natural logarithm. ln => Ln, @@ -236,9 +242,9 @@ impl Expr { ); binary!( - /// The smaller value. + /// The smaller of the value and `other`. min => Min, - /// The larger value. + /// The larger of the value and `other`. max => Max, /// The value raised to `other`. pow => Pow, @@ -270,14 +276,14 @@ impl Expr { t.clone() * t.clone() * (3.0 - 2.0 * t) } - /// Whether a text field equals `text`. + /// Whether a text field equals `text` exactly. #[must_use] pub fn is(self, text: &str) -> Expr { node(Node::Is(self, text.into())) } - /// Whether a text field holds `text`, ignoring case: of the mark's own table in a mark, which the - /// view works out on the CPU as the field changes. + /// Whether a text field contains `text`, ignoring case. In a mark, the field must belong to the + /// mark's own table; the view recomputes matches on the CPU when the field changes. #[must_use] pub fn contains(self, text: &str) -> Expr { node(Node::Contains(self, text.into())) @@ -297,14 +303,14 @@ impl Expr { self.map_fields(&|f| FieldRef { step: true, ..f.clone() }) } - /// The value through `colormap`; the domain fits the values shown unless set by - /// [`Mapped::domain`]. + /// The value mapped through `colormap`. The domain fits the values shown unless set with + /// [`Mapped::domain`]; a categorical map ignores it and bins values by their integer part. pub fn colormap(self, colormap: impl Into) -> Mapped { Mapped(node(Node::Colormap { input: self, colormap: colormap.into(), domain: Domain::Fit })) } - /// The value mapped linearly onto `range`, clamped; the domain fits the values shown unless set - /// by [`Mapped::domain`]. + /// The value mapped linearly onto `range`, clamped. The domain fits the values shown unless set + /// with [`Mapped::domain`]. pub fn scale(self, range: RangeInclusive) -> Mapped { let range = [*range.start(), *range.end()]; Mapped(node(Node::Scale { input: self, domain: Domain::Fit, range })) @@ -332,7 +338,8 @@ impl Expr { } } - /// Calls `f` with every text field the expression asks whether it holds a text, and the text. + /// Calls `f` with each field that a `contains` in the expression tests, and the text it looks + /// for. pub(crate) fn for_each_contains<'e>(&'e self, f: &mut impl FnMut(&'e FieldRef, &'e str)) { match &*self.0 { Node::Contains(a, text) => { @@ -378,7 +385,8 @@ impl Expr { } } - /// Whether a domain fits the values of every row: each row's value then reads them all. + /// Whether the expression has a fitted domain, which makes each row's value depend on every + /// row. pub(crate) fn fits(&self) -> bool { match &*self.0 { Node::Scale { domain: Domain::Fit, .. } | Node::Colormap { domain: Domain::Fit, .. } => true, @@ -393,7 +401,7 @@ impl Expr { } } - /// The field's name, when the expression is a field of its own table read as it is. + /// The field's name, when the expression is a plain field of the mark's own table. pub(crate) fn field_name(&self) -> Option<&Name> { match &*self.0 { Node::Field(f) if f.from.is_none() && f.component.is_none() => Some(&f.name), @@ -401,8 +409,8 @@ impl Expr { } } - /// The first colormap a color maps through, and its domain when set: what a legend shows. A - /// conditional's general case, its `otherwise`, comes before the cases it makes exceptions for. + /// The first colormap the expression maps through, with its domain when fixed; a legend shows + /// it. A conditional's `otherwise` is searched before its cases, since it is the general case. pub(crate) fn colormap_of(&self) -> Option<(&Colormap, Option<[f64; 2]>)> { match &*self.0 { Node::Colormap { colormap, domain, .. } => { @@ -422,7 +430,7 @@ impl Expr { pub struct Mapped(Expr); impl Mapped { - /// The input values that map to the ends of the range or colormap. + /// Sets the input values that map to the ends of the range or colormap. #[must_use] pub fn domain(self, domain: RangeInclusive) -> Mapped { let fixed = Domain::Fixed([*domain.start(), *domain.end()]); @@ -462,13 +470,15 @@ impl From for Paint { } } -/// The open cases of a [`when`]. +/// A conditional started by [`when`], waiting for [`then`](When::then) to give the value for its +/// latest condition. pub struct When { cases: Vec<(Expr, Expr)>, condition: Expr, } -/// The cases of a [`when`], waiting for another case or the value otherwise. +/// A conditional started by [`when`], waiting for another case or for +/// [`otherwise`](Then::otherwise). pub struct Then { cases: Vec<(Expr, Expr)>, } @@ -493,46 +503,48 @@ impl Then { } } -/// A size, in the space's unit unless it says its own: a bare value is in units in a diagram and in -/// CSS pixels elsewhere; [`px`] keeps it CSS pixels and [`units`] makes it position units, which -/// scale as the view zooms. It keeps within a range of CSS pixels. +/// A size. A bare value is in the space's unit: units in a diagram, CSS pixels elsewhere. [`px`] +/// makes it CSS pixels and [`units`] makes it position units, which scale as the view zooms. +/// [`Size::at_least`] and [`Size::at_most`] bound it in CSS pixels. #[derive(Clone, PartialEq, Debug)] pub struct Size { pub(crate) expr: Expr, - /// The unit it says it is in; the space's unless set. + /// Its explicit unit; `None` for the space's unit. pub(crate) unit: Option, - /// The CSS pixels it keeps within, at any zoom. + /// The CSS pixel bounds it stays within at any zoom. pub(crate) px: [f32; 2], } -/// The unit a length says it is in. +/// An explicit unit for a length. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) enum Unit { - /// CSS pixels, whatever the zoom. + /// CSS pixels, at any zoom. Px, /// Position units, which scale as the view zooms. Units, } impl Size { - /// Any number of CSS pixels. + /// No bounds: from 0 to infinitely many CSS pixels. pub(crate) const ANY: [f32; 2] = [0.0, f32::INFINITY]; - /// Whether it is in position units, in a space whose own unit is position units, as a diagram's - /// is, or CSS pixels. + /// Whether the size is in position units. `space_units` says whether the space's own unit is + /// position units, as a diagram's is. pub(crate) fn in_units(&self, space_units: bool) -> bool { self.unit.map_or(space_units, |unit| unit == Unit::Units) } - /// Keeps at least `px` CSS pixels however far the view zooms out: `units(field("r")).at_least(2.0)` - /// grows with the map and never vanishes. A marker keeps its proportions, its larger side held. + /// Sets a minimum on-screen size of `px` CSS pixels, however far the view zooms out: + /// `units(field("r")).at_least(2.0)` scales with the map but never shrinks below 2 pixels. A + /// marker is bounded on its larger side and keeps its proportions. Negative values count as 0. #[must_use] pub fn at_least(mut self, px: f32) -> Size { self.px[0] = px.max(0.0); self } - /// Keeps at most `px` CSS pixels however far the view zooms in. + /// Sets a maximum on-screen size of `px` CSS pixels, however far the view zooms in. Negative + /// values count as 0. #[must_use] pub fn at_most(mut self, px: f32) -> Size { self.px[1] = px.max(0.0); @@ -652,14 +664,14 @@ mod tests { } #[test] - fn domain_applies_to_the_mapping_whatever_the_order() { + fn domain_applies_to_the_mapping_regardless_of_order() { let a: Expr = field("vm").colormap(Colormap::BALANCE).domain(0.9..=1.1).into(); let Node::Colormap { domain, .. } = &*a.0 else { panic!("a colormap") }; assert_eq!(*domain, Domain::Fixed([0.9, 1.1])); } #[test] - fn a_join_reaches_every_field() { + fn a_join_applies_to_every_field() { let e = (field("a") + field("b")).from_table("weather", "Station"); let Node::Binary(_, a, b) = &*e.0 else { panic!("a sum") }; for side in [a, b] { diff --git a/crates/latkit/src/expr/ramp.rs b/crates/latkit/src/expr/ramp.rs index c416be0f..26f77f44 100644 --- a/crates/latkit/src/expr/ramp.rs +++ b/crates/latkit/src/expr/ramp.rs @@ -1,6 +1,7 @@ -//! A colormap's 256 texels from its stops, made one way for the catalog the build bakes and for the -//! maps made at run time: blended in OKLab, so lightness runs evenly; wrapping when cyclic; a stop a -//! texel in turn when categorical. Free of the crate, so the build script includes it as it is. +//! Builds a colormap's 256 texels from its stops, both for the catalog baked at build time and for +//! maps made at run time. Colors blend in OKLab so lightness changes evenly; cyclic ramps wrap, and +//! categorical ramps repeat their stops texel by texel. The file uses nothing from the crate, so +//! the build script includes it unchanged. /// How a ramp spans its stops. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -9,16 +10,17 @@ pub(crate) enum Spread { Continuous, /// Around a circle: past the last stop, back to the first. Cyclic, - /// A stop's color for each texel in turn. + /// Each texel takes the next stop's color, repeating. Categorical, } -/// The 256 texels through `stops`, each a place from 0 to 1, ascending, and a straight sRGB color -/// from 0 to 1. A stop without a place sits evenly between its neighbors' places, as in a CSS -/// gradient: the first at 0, the last at 1, or a turn after the first when cyclic. A categorical -/// ramp's texel `i` takes stop `i` modulo their count. +/// The 256 texels through `stops`: each an optional place from 0 to 1, ascending, and a +/// straight-alpha sRGB color with channels from 0 to 1. A stop without a place sits evenly between +/// its neighbors' places, as in a CSS gradient. An unplaced first stop sits at 0. An unplaced last +/// stop sits at 1, except in a cyclic ramp, whose unplaced stops spread evenly up to one turn past +/// the first stop. A categorical ramp's texel `i` takes stop `i` modulo the stop count. pub(crate) fn texels(spread: Spread, stops: &[(Option, [f32; 4])]) -> [[u8; 4]; 256] { - assert!(!stops.is_empty(), "a ramp has a stop"); + assert!(!stops.is_empty(), "a ramp needs at least one stop"); if spread == Spread::Categorical || stops.len() == 1 { return std::array::from_fn(|t| bytes(stops[t % stops.len()].1)); } @@ -58,10 +60,10 @@ fn blend(stops: &[(f32, [f32; 4])], x: f32, cyclic: bool) -> [f32; 4] { let n = stops.len(); let (first, last) = (stops[0], stops[n - 1]); let (a, b) = match stops.partition_point(|s| s.0 <= x) { - // Before the first stop: from the last a turn back, when cyclic. + // Before the first stop: when cyclic, blend from the last stop, one turn back. 0 if cyclic => ((last.0 - 1.0, last.1), first), 0 => (first, first), - // Past the last stop: on to the first a turn on, when cyclic. + // Past the last stop: when cyclic, blend toward the first stop, one turn on. i if i == n && cyclic => (last, (first.0 + 1.0, first.1)), i if i == n => (last, last), i => (stops[i - 1], stops[i]), @@ -82,7 +84,7 @@ fn gamma(c: f32) -> f32 { if c <= 0.003_130_8 { c * 12.92 } else { 1.055 * c.max(0.0).powf(1.0 / 2.4) - 0.055 } } -/// A straight sRGB color in OKLab, its alpha kept. +/// Converts a straight-alpha sRGB color to OKLab, keeping alpha. fn oklab([r, g, b, a]: [f32; 4]) -> [f32; 4] { let [r, g, b] = [r, g, b].map(linear); let l = (0.412_221_47 * r + 0.536_332_55 * g + 0.051_445_995 * b).cbrt(); @@ -96,7 +98,7 @@ fn oklab([r, g, b, a]: [f32; 4]) -> [f32; 4] { ] } -/// An OKLab color in straight sRGB, its alpha kept. +/// Converts an OKLab color to straight-alpha sRGB, keeping alpha. fn srgb([l, a, b, alpha]: [f32; 4]) -> [f32; 4] { let l_ = (l + 0.396_337_78 * a + 0.215_803_76 * b).powi(3); let m_ = (l - 0.105_561_346 * a - 0.063_854_17 * b).powi(3); @@ -118,14 +120,14 @@ mod tests { const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; #[test] - fn stops_land_on_their_texels_and_blend_in_oklab() { + fn stops_sit_on_their_texels_and_blend_in_oklab() { let ramp = texels(Spread::Continuous, &[(None, BLACK), (None, WHITE)]); assert_eq!((ramp[0], ramp[255]), ([0, 0, 0, 255], [255, 255, 255, 255])); - // OKLab's half lightness is a darker byte than sRGB's middle: about 100, not 128. + // OKLab's half lightness is a darker byte than sRGB's midpoint: about 100, not 128. assert!((95..105).contains(&ramp[128][0]), "{:?}", ramp[128]); let red = [1.0, 0.0, 0.0, 1.0]; let placed = texels(Spread::Continuous, &[(None, BLACK), (Some(64.0 / 255.0), red), (None, WHITE)]); - assert_eq!(placed[64], [255, 0, 0, 255], "a placed stop lands on its texel"); + assert_eq!(placed[64], [255, 0, 0, 255], "a placed stop sets its texel's color"); assert_eq!(placed[0], [0, 0, 0, 255]); } diff --git a/crates/latkit/src/geo/earcut.rs b/crates/latkit/src/geo/earcut.rs index 719dcb56..fe3cf45c 100644 --- a/crates/latkit/src/geo/earcut.rs +++ b/crates/latkit/src/geo/earcut.rs @@ -1,24 +1,26 @@ -//! Ear clipping: a polygon with holes cut into triangles, after Mapbox's earcut 3.2. Holes join the -//! outer ring through bridges; ears are clipped from a doubly linked ring, found by z-order hashing -//! on large polygons; where none clips, the ring is filtered while that removes points, cured once -//! of local self-intersections, then split in two. - -/// No node. +//! Polygon triangulation by ear clipping, ported from Mapbox's earcut 3.2. +//! +//! Each hole is joined to the outer ring by a bridge. Ears are clipped from a doubly linked ring; +//! large polygons use a z-order index to find points that block an ear. When no ear can be clipped, +//! the ring is filtered while that removes points, then cured of local self-intersections once, +//! then split in two. + +/// The null node index. const NIL: u32 = u32::MAX; #[derive(Clone, Copy)] struct Node { - /// The vertex index. + /// Index into the input points. i: u32, x: f64, y: f64, prev: u32, next: u32, - /// Z-order of the vertex, and its neighbors in z-order. + /// Z-order code of the vertex. `prev_z` and `next_z` link the ring sorted by it. z: u32, prev_z: u32, next_z: u32, - /// Whether the vertex is a lone hole point, which may stay in the ring. + /// Whether the vertex is a single-point hole (a Steiner point), which filtering never removes. steiner: bool, } @@ -30,8 +32,8 @@ struct Rings<'a> { filtered: bool, } -/// The triangles of the polygon whose points are `points`: the outer ring first, then each hole from -/// where `holes` starts it. Each triangle is three indices into `points`. +/// Triangulates a polygon. `points` holds the outer ring, then each hole; `holes` lists the index +/// where each hole starts. Returns triangles as index triples into `points`. pub(crate) fn earcut(points: &[[f64; 2]], holes: &[usize]) -> Vec<[u32; 3]> { let nodes = Vec::with_capacity(points.len() * 3 / 2); let mut rings = Rings { nodes, points, triangles: Vec::new(), filtered: false }; @@ -43,7 +45,7 @@ pub(crate) fn earcut(points: &[[f64; 2]], holes: &[usize]) -> Vec<[u32; 3]> { if !holes.is_empty() { outer = rings.eliminate_holes(holes, outer); } - // Large polygons find ears through z-order hashing. + // Polygons over 80 points index their ring in z-order to speed up ear tests. let (mut min, mut size) = ([0.0; 2], 0.0); if points.len() > 80 { let mut max = [f64::NEG_INFINITY; 2]; @@ -75,7 +77,8 @@ impl Rings<'_> { (n.x, n.y) } - /// A circular list of points `start..end`, wound as `clockwise` asks. + /// Links points `start..end` into a circular list, reversed if needed so its winding matches + /// `clockwise`. Drops a closing point equal to the first. Returns the last node, or `NIL`. fn linked(&mut self, start: usize, end: usize, clockwise: bool) -> u32 { let mut last = NIL; if clockwise == (signed_area(&self.points[start..end]) > 0.0) { @@ -128,15 +131,17 @@ impl Rings<'_> { self.xy(a) == self.xy(b) } - /// Twice the signed area of the triangle `p q r`: negative turning one way. + /// Twice the signed area of triangle `p q r`. Negative where the ring turns convexly at `q`, + /// positive at a reflex turn. fn area(&self, p: u32, q: u32, r: u32) -> f64 { let ((px, py), (qx, qy), (rx, ry)) = (self.xy(p), self.xy(q), self.xy(r)); (qy - py) * (rx - qx) - (qx - px) * (ry - qy) } - /// Removes repeated points and straight runs: around the whole ring from `start` until none is - /// left when `end` is `NIL`; else from `start` to `end`, healing the cut between them. Answers a - /// point still in the ring. + /// Removes repeated and collinear points, except Steiner points. When `end` is `NIL` or + /// `start`, it circles the whole ring until a full pass removes nothing. Otherwise it checks + /// only the nodes from `start` through `end`, as around a new diagonal. Returns a node still in + /// the ring. fn filter(&mut self, start: u32, end: u32) -> u32 { if start == NIL { return start; @@ -165,8 +170,9 @@ impl Rings<'_> { } } - /// Clips ears from the ring at `ear`. Where none clips, filters while that removes points, cures - /// local self-intersections once, then splits the ring in two. + /// Clips ears from the ring starting at `ear`, adding their triangles. When a full pass finds + /// no ear, it filters the ring and retries while that removes points, then cures local + /// self-intersections once, then splits the ring in two. fn clip(&mut self, mut ear: u32, min: [f64; 2], size: f64) { if ear == NIL { return; @@ -274,7 +280,7 @@ impl Rings<'_> { true } - /// Clips the triangles of local self-intersections, returning the ring left. + /// Clips a triangle at each local self-intersection and returns a node of the remaining ring. fn cure(&mut self, start: u32) -> u32 { let (mut p, mut start, mut cured) = (start, start, false); loop { @@ -337,12 +343,13 @@ impl Rings<'_> { } queue.push(self.leftmost(list)); } - // Holes whose leftmost points meet go around that point, so each bridges to where they meet. + // Holes whose leftmost points coincide are ordered by the slope of their first edge, so + // each one bridges to the shared point. let slope = |p: u32| { let (n, next) = (self.n(p), self.n(self.n(p).next)); (next.y - n.y) / (next.x - n.x) }; - // By x, then y, then slope: the last key first, as each sort keeps the order of ties. + // Order by x, then y, then slope. The sorts are stable, so the last key sorts first. crate::order::by_key(&mut queue, |&p| crate::order::number(slope(p))); crate::order::by_wide_key(&mut queue, |&p| { let n = self.n(p); @@ -351,7 +358,7 @@ impl Rings<'_> { for hole in queue { outer = self.eliminate_hole(hole, outer); } - // Points made collinear or repeated across the merged ring go before clipping. + // Remove points the bridges made repeated or collinear before clipping. self.filter(outer, NIL) } @@ -362,13 +369,14 @@ impl Rings<'_> { self.filter(bridge, self.n(bridge).next) } - /// The outer ring's point a hole joins it at: seen from the hole's leftmost point, nothing between. + /// Finds the outer ring node to bridge a hole to: one visible from `hole`, the hole's leftmost + /// point. `None` if no ring edge lies to its left. fn hole_bridge(&self, hole: u32, outer: u32) -> Option { let (hx, hy) = self.xy(hole); let mut qx = f64::NEG_INFINITY; let mut m = NIL; let mut p = outer; - // A hole touching the outer ring at a point joins it there. + // A hole that touches the outer ring at a point bridges there. if self.equals(hole, p) { return Some(p); } @@ -407,8 +415,8 @@ impl Rings<'_> { if hx >= px && px >= mx && hx != px && inside_triangle([ax, hy], [mx, my], [cx, hy], [px, py]) { let tan = (hy - py).abs() / (hx - px); let (bx, _) = self.xy(m); - // The hole's point on `p`'s level edge, a T touch: a bridge along that edge is valid, - // though not locally inside. + // When the hole point lies on `p`'s horizontal edge (a T junction), a bridge along + // that edge is valid although `locally_inside` rejects it. let (nx, ny) = self.xy(self.n(p).next); let along = py == hy && ny == hy && nx > hx; if (self.locally_inside(p, hole) || along) @@ -445,7 +453,7 @@ impl Rings<'_> { left } - /// Links the ring in z-order, sorted. + /// Computes each node's z-order code and links the ring in z-order. fn index(&mut self, start: u32, min: [f64; 2], size: f64) { let mut p = start; loop { @@ -464,7 +472,7 @@ impl Rings<'_> { self.sort(p); } - /// Merge-sorts the z-order list from `list`. + /// Merge-sorts the z-order list starting at `list`. fn sort(&mut self, mut list: u32) { let mut in_size = 1; loop { @@ -515,9 +523,10 @@ impl Rings<'_> { } } - /// Whether diagonal `a b` lies inside the polygon: locally visible from both ends, crossing no - /// edge, its middle inside, and making no opposite-facing sectors; or of no length between two - /// convex corners. + /// Whether diagonal `a b` lies inside the polygon. `b` must not be the node after `a`, and the + /// diagonal must be locally inside at both ends, cross no edge, have its midpoint inside, and + /// create no opposite-facing sectors. A zero-length diagonal between two reflex corners is also + /// valid. fn valid_diagonal(&self, a: u32, b: u32) -> bool { let (na, nb) = (self.n(a), self.n(b)); let zero_length = @@ -531,14 +540,14 @@ impl Rings<'_> { && (zero_length || self.middle_inside(a, b)) } - /// Whether segments `p1 q1` and `p2 q2` cross, not counting a touch. + /// Whether segments `p1 q1` and `p2 q2` cross. Touching does not count. fn crosses(&self, p1: u32, q1: u32, p2: u32, q2: u32) -> bool { let (o1, o2) = (sign(self.area(p1, q1, p2)), sign(self.area(p1, q1, q2))); let (o3, o4) = (sign(self.area(p2, q2, p1)), sign(self.area(p2, q2, q1))); o1 * o2 < 0 && o3 * o4 < 0 } - /// Whether segments `p1 q1` and `p2 q2` meet, touches included. + /// Whether segments `p1 q1` and `p2 q2` intersect, including touching. fn intersects(&self, p1: u32, q1: u32, p2: u32, q2: u32) -> bool { let o1 = sign(self.area(p1, q1, p2)); let o2 = sign(self.area(p1, q1, q2)); @@ -551,13 +560,13 @@ impl Rings<'_> { || (o4 == 0 && self.on_segment(p2, q1, q2)) } - /// Whether `q` lies within the box of segment `p r`, on which it lies. + /// For `q` collinear with segment `p r`, whether it lies on the segment, within its box. fn on_segment(&self, p: u32, q: u32, r: u32) -> bool { let ((px, py), (qx, qy), (rx, ry)) = (self.xy(p), self.xy(q), self.xy(r)); qx <= px.max(rx) && qx >= px.min(rx) && qy <= py.max(ry) && qy >= py.min(ry) } - /// Whether diagonal `a b` meets an edge of the ring, other than those at its ends. + /// Whether diagonal `a b` intersects a ring edge not incident to `a` or `b`. fn intersects_polygon(&self, a: u32, b: u32) -> bool { let (ia, ib) = (self.n(a).i, self.n(b).i); let ((ax, ay), (bx, by)) = (self.xy(a), self.xy(b)); @@ -566,7 +575,7 @@ impl Rings<'_> { loop { let next = self.n(p).next; let ((px, py), (nx, ny)) = (self.xy(p), self.xy(next)); - // An edge beside the diagonal's box cannot meet it. + // Skip edges entirely outside the diagonal's bounding box. let apart = (px > x1 && nx > x1) || (px < x0 && nx < x0) || (py > y1 && ny > y1) || (py < y0 && ny < y0); let (ip, inext) = (self.n(p).i, self.n(next).i); if !apart && ip != ia && inext != ia && ip != ib && inext != ib && self.intersects(p, next, a, b) { @@ -607,7 +616,8 @@ impl Rings<'_> { } } - /// Splits the ring along the diagonal `a b` into two; returns the new copy of `b`. + /// Splits the ring in two along diagonal `a b` by duplicating both nodes. Returns the copy of + /// `b`, which lies in the second ring. fn split_polygon(&mut self, a: u32, b: u32) -> u32 { let (na, nb) = (*self.n(a), *self.n(b)); let a2 = self.nodes.len() as u32; @@ -628,7 +638,7 @@ impl Rings<'_> { } } -/// Twice the signed area of a ring, by the shoelace sum as earcut takes it. +/// Twice the signed area of a ring by the shoelace formula, with earcut's sign convention. fn signed_area(points: &[[f64; 2]]) -> f64 { let Some(&last) = points.last() else { return 0.0 }; let mut sum = 0.0; @@ -640,7 +650,7 @@ fn signed_area(points: &[[f64; 2]]) -> f64 { sum } -/// The z-order of point `(x, y)` in a box from `min` scaled by `size` to 15 bits a side. +/// The z-order (Morton) code of `(x, y)`, offset by `min` and scaled by `size` to 15 bits per axis. fn z_order(x: f64, y: f64, min: [f64; 2], size: f64) -> u32 { let spread = |v: f64| { let mut v = (v * size) as u32 & 0x7fff; @@ -658,8 +668,8 @@ fn inside_triangle(a: [f64; 2], b: [f64; 2], c: [f64; 2], p: [f64; 2]) -> bool { && (b[0] - p[0]) * (c[1] - p[1]) >= (c[0] - p[0]) * (b[1] - p[1]) } -/// Whether `p` lies in the triangle, other than at its first corner: a point where a bridge doubled -/// that corner blocks no ear. +/// Whether `p` lies in triangle `a b c` and is not at `a`. Bridges duplicate points, and a copy of +/// `a` must not block the ear. fn inside_except_first(a: [f64; 2], b: [f64; 2], c: [f64; 2], p: [f64; 2]) -> bool { a != p && inside_triangle(a, b, c, p) } @@ -678,7 +688,7 @@ fn sign(v: f64) -> i8 { mod tests { use super::*; - /// Twice the area the triangles cover. + /// Twice the total area of `triangles`. fn covered(points: &[[f64; 2]], triangles: &[[u32; 3]]) -> f64 { triangles .iter() @@ -708,15 +718,15 @@ mod tests { #[test] fn holes_touching_the_ring_or_each_other_are_left_out() { - /// Holes, where each starts, and the area left. + /// Hole points, the index where each hole starts, and the expected area. type Case<'a> = (&'a [[f64; 2]], &'a [usize], f64); let outer = [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]]; let cases: [Case<'_>; 3] = [ // A hole touching the outer ring at its corner. (&[[0.0, 0.0], [3.0, 1.0], [1.0, 3.0]], &[4], 96.0), - // One touching its edge. + // A hole touching the outer ring's edge. (&[[0.0, 5.0], [3.0, 4.0], [3.0, 6.0]], &[4], 97.0), - // Two meeting at their leftmost point. + // Two holes sharing their leftmost point. (&[[5.0, 5.0], [7.0, 4.0], [7.0, 6.0], [5.0, 5.0], [7.0, 7.0], [6.0, 9.0]], &[4, 7], 95.0), ]; for (holes, starts, area) in cases { @@ -728,7 +738,7 @@ mod tests { #[test] fn large_rings_hash_their_ears() { - // A star of 200 points: concave everywhere, so ear finding matters. + // A 200-point star: every other corner is reflex, which exercises the hashed ear test. let points: Vec<[f64; 2]> = (0..200) .map(|k| { let a = f64::from(k) / 200.0 * std::f64::consts::TAU; @@ -743,7 +753,7 @@ mod tests { } #[test] - fn degenerate_rings_answer_nothing() { + fn degenerate_rings_give_no_triangles() { let none: Vec<[u32; 3]> = Vec::new(); assert_eq!(earcut(&[[0.0, 0.0], [1.0, 1.0]], &[]), none); assert_eq!(earcut(&[[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]], &[]), none, "collinear"); diff --git a/crates/latkit/src/geo/geojson.rs b/crates/latkit/src/geo/geojson.rs index 7bfb9446..b616b60a 100644 --- a/crates/latkit/src/geo/geojson.rs +++ b/crates/latkit/src/geo/geojson.rs @@ -1,6 +1,5 @@ -//! GeoJSON as data: a feature collection's features become rows of three tables by what their -//! geometry is, `points`, `lines`, and `polygons`, each with its geometry as field `geometry` and its -//! properties as fields. +//! Reads GeoJSON into three tables by geometry type: `points`, `lines`, and `polygons`. Each row +//! holds its geometry in field `geometry` and each property in a field of its own. use std::fmt; @@ -13,14 +12,17 @@ use crate::{Error, Result}; type Point = [f64; 2]; -/// The features of GeoJSON `bytes`, gzip-compressed or not, as dataset `name`: a feature collection, -/// one feature, or a bare geometry. +/// Reads GeoJSON `bytes`, optionally gzip-compressed, as dataset `name`. The document may be a +/// feature collection, a single feature, or a bare geometry. /// -/// Points become rows of table `points`, a point field `geometry`; line strings rows of `lines`, a -/// field of parts; polygons rows of `polygons`, a field of polygons. A multi-point is a row per point, -/// and a geometry collection a row per member. Properties become fields: numbers, booleans, and texts, -/// any other value written as its JSON. Rows take their features' ids where every row of a table has -/// one and no two share it, and are numbered in order otherwise. +/// Points become rows of table `points` with a point field `geometry`. Line strings become rows of +/// `lines` (a parts field), and polygons rows of `polygons` (a polygons field). A multi-point adds +/// a row per point, and a geometry collection a row per member. Each property becomes a field of +/// numbers, booleans, or text, with any other value written as JSON. A null counts as missing. +/// +/// A table keeps its features' ids when every row has one, all are non-negative integers or all are +/// strings, and none repeat. Otherwise its rows are numbered in order. Errors if `bytes` are not +/// GeoJSON. pub(crate) fn geojson(name: impl Into, bytes: &[u8]) -> Result { let bytes = super::gunzipped(bytes)?; let top: Top = serde_json::from_slice(&bytes).map_err(|e| Error::invalid(format!("not GeoJSON: {e}")))?; @@ -49,7 +51,8 @@ pub(crate) fn geojson(name: impl Into, bytes: &[u8]) -> Result { Snapshot::new(name).applied(patch) } -/// The top of a document: a collection, a feature, or a geometry, as one struct of each's members. +/// A document's top-level object, with the members of a feature collection, a feature, and a +/// geometry in one struct. #[derive(Deserialize)] struct Top { #[serde(rename = "type")] @@ -77,13 +80,13 @@ struct Geometry { geometries: Option>, } -/// Which table a geometry's rows go to. +/// Indices into the table array, by geometry type. const POINTS: usize = 0; const LINES: usize = 1; const POLYGONS: usize = 2; impl Geometry { - /// Adds this geometry's rows, with the feature's id and properties, to the table of each kind. + /// Adds this geometry's rows, with the feature's id and properties, to the table for its type. fn into_rows(self, id: Option<&Json>, properties: &Map, tables: &mut [Rows; 3]) { let coordinates = self.coordinates.unwrap_or(Coordinates::Empty); match (self.kind.as_str(), coordinates) { @@ -106,7 +109,7 @@ impl Geometry { member.into_rows(id, properties, tables); } } - // A geometry without coordinates, or of a type GeoJSON does not have, adds no row. + // Unknown types, missing coordinates, and coordinates of the wrong depth add no row. _ => {} } } @@ -118,7 +121,7 @@ enum Shape { Polygons(Vec>>), } -/// One table's rows as they are read: each row's id, its properties, and its shape. +/// Rows read for one table: each row's id, properties, and shape. #[derive(Default)] struct Rows { ids: Vec>, @@ -133,7 +136,7 @@ impl Rows { self.shapes.push(shape); } - /// The rows as table `name`, or `None` without any. + /// Builds table `name` from the rows, or `None` if there are none. fn table(self, name: &str) -> Option
    { if self.shapes.is_empty() { return None; @@ -170,7 +173,7 @@ impl Rows { } } for key in keys { - // A property named like the geometry would replace it. + // A property named `geometry` would replace the geometry field, so skip it. if key == "geometry" { continue; } @@ -182,7 +185,8 @@ impl Rows { } } -/// The rows' ids: every one present, of one kind, and distinct; else `None`. +/// The rows' ids, if every row has one, all are non-negative integers or all are strings, and none +/// repeat. Else `None`. fn ids(ids: &[Option]) -> Option { let mut seen = std::collections::HashSet::with_capacity(ids.len()); if ids.iter().all(|id| id.as_ref().and_then(Json::as_u64).is_some_and(|n| seen.insert(n.to_string()))) { @@ -198,8 +202,8 @@ fn ids(ids: &[Option]) -> Option { None } -/// A property's values as a field: numbers where every value present is one, booleans likewise, and -/// texts otherwise, any value not a text written as its JSON. +/// Converts a property's values into a field: numbers if every present value is a number, booleans +/// if every one is a boolean, and text otherwise, with non-string values written as JSON. fn property(values: &[Option<&Json>]) -> Values { let present = || values.iter().flatten(); if present().all(|v| v.is_number()) { @@ -220,8 +224,8 @@ fn property(values: &[Option<&Json>]) -> Values { texts.into() } -/// Coordinates of any depth: a position, a list of them, lists of those, or polygons. Positions read -/// their longitude and latitude and skip any altitude. +/// Coordinates of any nesting depth: a position, a list of positions, a list of those, or a list of +/// polygons. Positions keep longitude and latitude and drop any altitude. enum Coordinates { Number(f64), Position(Point), @@ -237,7 +241,7 @@ impl<'de> Deserialize<'de> for Coordinates { } } -/// Reads coordinates of any depth: the first element of each array says how deep the rest are. +/// Reads coordinates of any depth. The first element of each array sets the depth of the rest. struct Depth; impl<'de> Visitor<'de> for Depth { @@ -296,7 +300,7 @@ impl<'de> Visitor<'de> for Depth { } } -/// One position: longitude and latitude, any altitude skipped. +/// One position: longitude and latitude, ignoring any altitude. struct Position(Point); impl<'de> Deserialize<'de> for Position { @@ -359,7 +363,7 @@ mod tests { } #[test] - fn gzip_compressed_bytes_read_alike() { + fn gzip_compressed_geojson_reads_the_same() { use std::io::Write; let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); encoder.write_all(COLLECTION.as_bytes()).expect("written"); @@ -368,10 +372,10 @@ mod tests { } #[test] - fn bare_geometries_and_bad_documents() { + fn a_bare_geometry_reads_and_a_truncated_document_errors() { let data = geojson("g", br#"{"type": "Polygon", "coordinates": [[[0, 0], [1, 0], [0, 1], [0, 0]]]}"#) .expect("a geometry"); assert_eq!(data.table("polygons").map(Table::len), Some(1)); - assert!(geojson("g", b"{\"type\": \"FeatureCollection\"").is_err(), "cut short"); + assert!(geojson("g", b"{\"type\": \"FeatureCollection\"").is_err(), "a truncated document errors"); } } diff --git a/crates/latkit/src/geo/mod.rs b/crates/latkit/src/geo/mod.rs index b335c69e..322a990c 100644 --- a/crates/latkit/src/geo/mod.rs +++ b/crates/latkit/src/geo/mod.rs @@ -1,5 +1,5 @@ -//! Geography: GeoJSON and vector tiles as data, and what shapes become to draw: an -//! area's triangles, and lines bent along the globe. +//! Geography: GeoJSON and vector tiles read as data, polygon triangulation, and lines given extra +//! points so they follow the globe. #[cfg(feature = "gpu")] mod earcut; @@ -20,7 +20,8 @@ pub(crate) use geojson::geojson; use crate::{Error, Result}; -/// `bytes` inflated where they are gzip-compressed, else as they are. +/// Decompresses `bytes` if they start with the gzip magic number, else returns them unchanged. +/// Errors if the gzip stream is corrupt. pub(crate) fn gunzipped(bytes: &[u8]) -> Result> { if !bytes.starts_with(&[0x1f, 0x8b]) { return Ok(Cow::Borrowed(bytes)); diff --git a/crates/latkit/src/geo/mvt.rs b/crates/latkit/src/geo/mvt.rs index 81f0d699..14b5c81d 100644 --- a/crates/latkit/src/geo/mvt.rs +++ b/crates/latkit/src/geo/mvt.rs @@ -1,11 +1,11 @@ -//! Mapbox vector tiles: a tile's layers, their features' geometry in longitude and latitude, clipped -//! to the tile so neighbors never overlap, and their properties. +//! Mapbox vector tile decoding. Feature geometry is converted to longitude and latitude and clipped +//! to the tile, so neighboring tiles never overlap. use crate::tiles::TileKey; type Point = [f64; 2]; -/// What a feature's geometry is. +/// A feature's geometry type. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub(crate) enum Kind { Points, @@ -13,7 +13,7 @@ pub(crate) enum Kind { Polygons, } -/// A property value as a tile holds it. +/// A property value as stored in a tile. #[derive(Clone, PartialEq, Debug)] pub(crate) enum Property { Text(String), @@ -21,7 +21,7 @@ pub(crate) enum Property { Bool(bool), } -/// A feature's geometry, in degrees. +/// A feature's geometry, in degrees of longitude and latitude. #[derive(Clone, PartialEq, Debug)] pub(crate) enum Shape { Points(Vec), @@ -39,14 +39,14 @@ impl Shape { } } -/// One feature: its place among its layer's features, its shape, and its properties by key. +/// One feature: its shape, and its properties as pairs of key index and value. #[derive(Clone, Debug)] pub(crate) struct Feature { pub(crate) shape: Shape, pub(crate) properties: Vec<(u32, Property)>, } -/// One layer: its name, its keys, and its features. +/// One layer: its name, its property keys, and its features. #[derive(Clone, Debug, Default)] pub(crate) struct Layer { pub(crate) name: String, @@ -54,8 +54,8 @@ pub(crate) struct Layer { pub(crate) features: Vec, } -/// The layers of tile `key`'s `bytes`; a feature's geometry clipped to the tile, and one left with -/// nothing inside it dropped. Bytes that are no tile answer `None`. +/// Decodes the layers of tile `key` from `bytes`. Geometry is clipped to the tile, and features +/// with nothing inside it are dropped. `None` if the tile or one of its layers is malformed. pub(crate) fn decode(bytes: &[u8], key: TileKey) -> Option> { let mut layers = Vec::new(); let mut tile = Reader::new(bytes); @@ -114,8 +114,8 @@ fn value(bytes: &[u8]) -> Option { Some(out) } -/// A feature: `None` when nothing of it lies in the tile, or its bytes are no feature, which drops -/// it alone. +/// Decodes one feature. `None`, which drops only this feature, if it lies wholly outside the tile +/// or is malformed. fn feature(bytes: &[u8], values: &[Property], frame: &Frame) -> Option { let mut reader = Reader::new(bytes); let (mut tags, mut geometry, mut kind) = (Vec::new(), Vec::new(), 0); @@ -146,7 +146,7 @@ fn feature(bytes: &[u8], values: &[Property], frame: &Frame) -> Option Some(Feature { shape, properties }) } -/// A tile's frame: where its coordinates lie on the earth. +/// Converts a tile's coordinates to longitude and latitude. struct Frame { key: TileKey, extent: f64, @@ -157,7 +157,7 @@ impl Frame { Frame { key, extent } } - /// The longitude and latitude of tile coordinates `p`. + /// The longitude and latitude of tile coordinates `p` (web mercator, y down). fn degrees(&self, p: [f64; 2]) -> Point { let n = f64::from(1_u32 << self.key.z.min(31)); let x = (f64::from(self.key.x) + p[0] / self.extent) / n; @@ -166,7 +166,7 @@ impl Frame { [x * 360.0 - 180.0, lat] } - /// The tile's points, those outside it dropped. + /// Decodes points, dropping those outside the tile. fn points(&self, geometry: &[u32]) -> Option { let inside = |p: &[f64; 2]| p.iter().all(|&v| v >= 0.0 && v < self.extent); let points: Vec = @@ -174,7 +174,7 @@ impl Frame { (!points.is_empty()).then_some(Shape::Points(points)) } - /// The tile's lines, cut where they leave it. + /// Decodes lines, cut where they leave the tile. fn lines(&self, geometry: &[u32]) -> Option { let mut parts: Vec> = Vec::new(); for path in paths(geometry) { @@ -187,8 +187,8 @@ impl Frame { (!parts.is_empty()).then_some(Shape::Lines(parts)) } - /// The tile's polygons: each exterior ring, by its positive area, with the holes after it, - /// clipped to the tile as a whole. + /// Decodes polygons. A ring with positive area starts a polygon, and the negative-area rings + /// after it are its holes. Each polygon is clipped to the tile as a whole. fn polygons(&self, geometry: &[u32]) -> Option { let mut groups: Vec>> = Vec::new(); for ring in paths(geometry) { @@ -215,15 +215,15 @@ impl Frame { } } -/// A polygon, its exterior then its holes, clipped to the square `0..extent` as a whole: a ring -/// wholly inside it stays; the runs of the rest inside it join along its edge, where the polygon's -/// inside lies, into exteriors; and each hole inside goes to the exterior around it. Clipping each -/// ring alone would leave a hole crossing the edge running along its exterior there, which no -/// triangulation reads right. +/// Clips a polygon (exterior, then holes) to the square `0..extent` as a whole. Rings wholly inside +/// the square stay. The parts of other rings inside it are joined along the square's edge into +/// exteriors, and each hole wholly inside goes to the exterior containing it. Clipping rings +/// separately would leave a hole that crosses the edge running along its clipped exterior there, +/// which triangulation mishandles. fn clip_polygon(rings: &[Vec<[f64; 2]>], extent: f64) -> Vec>> { let inside = |p: &[f64; 2]| p.iter().all(|&v| (0.0..=extent).contains(&v)); let (mut exteriors, mut holes, mut runs) = (Vec::new(), Vec::new(), Vec::new()); - // Whether the square lies within the polygon, as the rings that miss it say. + // Whether the whole square lies inside the polygon, judged from rings that do not cross it. let mut covered = false; for (k, ring) in rings.iter().enumerate().filter(|(_, r)| r.len() >= 3) { if ring.iter().all(inside) { @@ -235,7 +235,8 @@ fn clip_polygon(rings: &[Vec<[f64; 2]>], extent: f64) -> Vec>> continue; } let crossing = runs_inside(ring, extent); - // A ring missing the square holds all of it or none: the exterior first, then a hole within. + // A ring that does not cross the square contains all of it or none. The exterior comes + // first, so a hole that contains the square clears `covered`. if crossing.is_empty() && contains(ring, [extent / 2.0; 2]) { covered = k == 0; } @@ -261,9 +262,9 @@ fn clip_polygon(rings: &[Vec<[f64; 2]>], extent: f64) -> Vec>> polygons } -/// The runs of closed `ring`, which has a point outside the square `0..extent`, inside it: each from -/// where it enters to where it leaves, on the edge. A run lying along the edge alone bounds nothing, -/// and drops. +/// The runs of closed `ring` inside the square `0..extent`, each from where it enters to where it +/// leaves, with both ends on the edge. Returns nothing if `ring` has no point outside the square. +/// A run lying entirely on the edge bounds nothing and is dropped. fn runs_inside(ring: &[[f64; 2]], extent: f64) -> Vec> { let outside = |p: &[f64; 2]| p.iter().any(|&v| !(0.0..=extent).contains(&v)); let Some(start) = ring.iter().position(outside) else { return Vec::new() }; @@ -280,7 +281,7 @@ fn runs_inside(ring: &[[f64; 2]], extent: f64) -> Vec> { run.push(q); continue; } - // The segment leaves the square: the run ends. + // The segment leaves the square, so the run ends. run.push(onto_edge(q, extent)); if run.len() >= 2 && !run.iter().all(on_edge) { runs.push(std::mem::take(&mut run)); @@ -290,7 +291,7 @@ fn runs_inside(ring: &[[f64; 2]], extent: f64) -> Vec> { runs } -/// `p`, near the square's edge, moved onto it. +/// Moves `p`, which lies near the square's edge, onto the nearest edge. fn onto_edge(p: [f64; 2], extent: f64) -> [f64; 2] { let gaps = [p[1].abs(), (p[0] - extent).abs(), (p[1] - extent).abs(), p[0].abs()]; let edge = (0..4).min_by(|&a, &b| gaps[a].total_cmp(&gaps[b])).unwrap_or(0); @@ -303,8 +304,8 @@ fn onto_edge(p: [f64; 2], extent: f64) -> [f64; 2] { } } -/// How far along the square's edge `p` lies, from the corner at 0 the way an exterior winds: along -/// y = 0, then x = extent, y = extent, and x = 0. +/// Distance of edge point `p` along the square's perimeter from the corner at the origin, in the +/// direction an exterior winds: along y = 0, then x = extent, y = extent, and x = 0. fn along(p: [f64; 2], extent: f64) -> f64 { let gaps = [p[1].abs(), (p[0] - extent).abs(), (p[1] - extent).abs(), p[0].abs()]; match (0..4).min_by(|&a, &b| gaps[a].total_cmp(&gaps[b])).unwrap_or(0) { @@ -315,8 +316,9 @@ fn along(p: [f64; 2], extent: f64) -> f64 { } } -/// The exteriors `runs` make: from where each leaves the square, along its edge the way the -/// polygon's inside lies to where the next enters, past the corners between, until the first again. +/// Joins `runs` into exterior rings. From each run's exit, a ring follows the square's edge in the +/// exterior's winding direction to the next run's entry, adding the corners it passes, until it +/// returns to its first run. fn join(runs: &[Vec<[f64; 2]>], extent: f64) -> Vec> { let around = 4.0 * extent; let entries: Vec = runs.iter().map(|r| along(r[0], extent)).collect(); @@ -362,7 +364,7 @@ fn join(runs: &[Vec<[f64; 2]>], extent: f64) -> Vec> { rings } -/// Whether closed `ring` holds `p`, by the crossings of a ray from it. +/// Whether closed `ring` contains `p`, by counting crossings of a ray from it. fn contains(ring: &[[f64; 2]], p: [f64; 2]) -> bool { let mut inside = false; let mut a = ring[ring.len() - 1]; @@ -375,8 +377,8 @@ fn contains(ring: &[[f64; 2]], p: [f64; 2]) -> bool { inside } -/// The paths a geometry's commands draw, in tile coordinates: each move starts one, a close returns -/// to its start (left implicit). +/// Decodes geometry commands into paths in tile coordinates. Each move-to starts a path, and a +/// close-path is left implicit. fn paths(geometry: &[u32]) -> Vec> { let mut paths: Vec> = Vec::new(); let (mut x, mut y) = (0_i64, 0_i64); @@ -398,7 +400,7 @@ fn paths(geometry: &[u32]) -> Vec> { if let Some(path) = paths.last_mut() { path.push([x as f64, y as f64]); } - // A move with several points is several points, each its own path. + // A move-to with several points (a multipoint) makes each point its own path. if id == 1 && count > 1 && k + 1 < count { paths.push(Vec::new()); } @@ -412,8 +414,8 @@ fn paths(geometry: &[u32]) -> Vec> { paths } -/// Twice a ring's area by the surveyor's formula in tile coordinates, y down: positive for an -/// exterior ring. +/// Twice a ring's signed area by the surveyor's formula in tile coordinates (y down). Positive for +/// an exterior ring. fn surveyor(ring: &[[f64; 2]]) -> f64 { let Some(&last) = ring.last() else { return 0.0 }; let mut sum = 0.0; @@ -425,7 +427,7 @@ fn surveyor(ring: &[[f64; 2]]) -> f64 { sum } -/// `line` cut to the square `0..extent`: the runs of it inside. +/// Clips `line` to the square `0..extent`, returning the parts inside. fn clip_line(line: &[[f64; 2]], extent: f64) -> Vec> { let mut parts: Vec> = Vec::new(); let mut current: Vec<[f64; 2]> = Vec::new(); @@ -445,7 +447,7 @@ fn clip_line(line: &[[f64; 2]], extent: f64) -> Vec> { current.push(a); } current.push(b); - // The segment left the tile: what follows starts a new run. + // The segment left the tile, so the next segment starts a new part. if b != w[1] { parts.push(std::mem::take(&mut current)); } @@ -456,7 +458,7 @@ fn clip_line(line: &[[f64; 2]], extent: f64) -> Vec> { parts } -/// Segment `a b` clipped to the square `0..extent`, by Liang and Barsky. +/// Clips segment `a b` to the square `0..extent` (Liang-Barsky). `None` if it lies outside. fn clip_segment(a: [f64; 2], b: [f64; 2], extent: f64) -> Option<([f64; 2], [f64; 2])> { let (dx, dy) = (b[0] - a[0], b[1] - a[1]); let (mut t0, mut t1) = (0.0_f64, 1.0_f64); @@ -506,7 +508,7 @@ const FIXED64: u8 = 1; const LENGTH: u8 = 2; const FIXED32: u8 = 5; -/// Protocol buffer fields, read in order; `None` past the end or on a malformed field. +/// Reads protocol buffer fields in order. Reads return `None` past the end or on a malformed field. struct Reader<'a> { bytes: &'a [u8], } @@ -567,7 +569,7 @@ impl<'a> Reader<'a> { pub(crate) mod tests { use super::*; - /// Protocol buffer fields, written. + /// Writes protocol buffer fields. #[derive(Default)] pub(crate) struct Writer(pub(crate) Vec); @@ -603,8 +605,8 @@ pub(crate) mod tests { ((v << 1) ^ (v >> 63)) as u32 } - /// A tile of one layer `name` holding one feature of `kind` drawn by `geometry`, with property - /// `name` set to `value`. + /// A tile with one layer named `layer` holding one feature of type `kind`, with geometry + /// commands `geometry` and property `name` set to `value`. pub(crate) fn tile(layer: &str, kind: u64, geometry: &[u32], value: &str) -> Vec { let mut feature = Writer::default(); feature.packed(2, &[0, 0]); @@ -627,7 +629,7 @@ pub(crate) mod tests { t.0 } - /// A square's commands, from `(x, y)` of `side`, clockwise in tile coordinates. + /// Geometry commands for a square of `side` from `(x, y)`, clockwise in tile coordinates. pub(crate) fn square(x: i64, y: i64, side: i64) -> Vec { vec![9, zig(x), zig(y), 26, zig(side), 0, 0, zig(side), zig(-side), 0, 15] } @@ -653,8 +655,8 @@ pub(crate) mod tests { let ring = vec![[x, y], [x + side, y], [x + side, y + side], [x, y + side]]; if exterior { ring } else { ring.into_iter().rev().collect() } }; - // A sea over the tile and its buffer, with islands across its left edge, across its corner, - // inside it, and outside it. + // A sea covering the tile and its buffer, with islands across its left edge, across its + // corner, inside it, and outside it. let rings = [ square(-64.0, -64.0, 4224.0, true), square(-200.0, 1800.0, 500.0, false), @@ -664,7 +666,7 @@ pub(crate) mod tests { ]; let polygons = clip_polygon(&rings, 4096.0); assert_eq!(polygons.len(), 1); - assert_eq!(polygons[0].len(), 2, "islands across the edge notch the sea; the one inside stays a hole"); + assert_eq!(polygons[0].len(), 2, "islands across the edge join the outer ring; the one inside stays a hole"); let points: Vec<[f64; 2]> = polygons[0].concat(); let triangles = super::super::earcut::earcut(&points, &[polygons[0][0].len()]); let area: f64 = triangles @@ -675,8 +677,8 @@ pub(crate) mod tests { }) .sum(); let sea = 4096.0 * 4096.0 - 300.0 * 500.0 - 196.0 * 300.0 - 200.0 * 200.0; - assert!((area - sea).abs() < 1e-6, "{area} drawn of {sea}"); - // A sea the tile lies wholly in is the tile; one beside it is nothing. + assert!((area - sea).abs() < 1e-6, "area {area}, expected {sea}"); + // A sea containing the whole tile clips to the tile. One beside it clips to nothing. assert_eq!(clip_polygon(&[square(-100.0, -100.0, 5000.0, true)], 4096.0)[0][0].len(), 4); assert_eq!(clip_polygon(&[square(5000.0, 0.0, 100.0, true)], 4096.0).len(), 0); } @@ -684,7 +686,7 @@ pub(crate) mod tests { #[test] fn shapes_are_clipped_to_the_tile() { let key = TileKey { z: 1, x: 1, y: 0 }; - // A square reaching past the tile's left edge: clipped there. + // A square reaching past the tile's left edge is clipped there. let layers = decode(&tile("land", 3, &square(-512, 1024, 1024), "x"), key).expect("a tile"); let Shape::Polygons(polygons) = &layers[0].features[0].shape else { panic!("polygons") }; assert!(polygons[0][0].iter().all(|p| p[0] >= -1e-9), "west of 0 is clipped: {:?}", polygons[0][0]); @@ -695,19 +697,20 @@ pub(crate) mod tests { } #[test] - fn a_polygon_wholly_outside_leaves_nothing() { + fn a_polygon_wholly_outside_the_tile_is_dropped() { let key = TileKey { z: 1, x: 1, y: 0 }; - // Commands move relative to where the last ring ended, (-3000, 600): the second starts at (100, 100). + // Commands are relative to where the last ring ended, (-3000, 600), so the second ring + // starts at (100, 100). let mut geometry = square(-3000, 100, 500); geometry.extend(square(3100, -500, 500)); let layers = decode(&tile("land", 3, &geometry, "x"), key).expect("a tile"); let Shape::Polygons(polygons) = &layers[0].features[0].shape else { panic!("polygons") }; - assert_eq!(polygons.len(), 1, "only the one inside: {polygons:?}"); + assert_eq!(polygons.len(), 1, "only the polygon inside remains: {polygons:?}"); assert!(polygons.iter().flatten().all(|ring| ring.len() >= 4)); } #[test] - fn lines_are_cut_where_they_leave() { + fn lines_are_cut_where_they_leave_the_tile() { let parts = clip_line(&[[-10.0, 5.0], [10.0, 5.0], [10.0, -10.0], [20.0, 5.0]], 100.0); assert_eq!(parts.len(), 2, "{parts:?}"); assert_eq!(parts[0][0], [0.0, 5.0]); diff --git a/crates/latkit/src/geo/pmtiles.rs b/crates/latkit/src/geo/pmtiles.rs index 1eeebf0f..0c3d2a9a 100644 --- a/crates/latkit/src/geo/pmtiles.rs +++ b/crates/latkit/src/geo/pmtiles.rs @@ -1,50 +1,51 @@ -//! PMTiles archives, version 3: a header, directories of tile entries along the Hilbert curve, and -//! the tiles, each found by range requests. +//! PMTiles version 3 archives: a header, directories of tile entries in Hilbert curve order, and +//! the tile data, each read with range requests. use crate::tiles::TileKey; -/// Bytes a header takes. +/// Size of the header in bytes. pub(crate) const HEADER: usize = 127; const MAGIC: &[u8] = b"PMTiles"; -/// How deep directories nest at most. +/// Maximum directory nesting depth. pub(crate) const DEPTH: usize = 4; -/// What an archive's header says. +/// An archive's header. #[derive(Clone, Copy, PartialEq, Debug)] pub(crate) struct Header { pub(crate) root: (u64, u64), pub(crate) metadata: (u64, u64), pub(crate) leaves: u64, pub(crate) data: u64, - /// Whether directories, and tiles, are gzip-compressed. + /// Whether directories are gzip-compressed. `gzip_tiles` is the same for tiles. pub(crate) gzip_directories: bool, pub(crate) gzip_tiles: bool, pub(crate) vector: bool, pub(crate) min_zoom: u8, pub(crate) max_zoom: u8, - /// West, south, east, north. + /// West, south, east, north, in degrees. pub(crate) bounds: [f64; 4], } -/// Whether `bytes` begin a PMTiles archive. +/// Whether `bytes` start with the PMTiles magic number. pub(crate) fn is_archive(bytes: &[u8]) -> bool { bytes.starts_with(MAGIC) } -/// The header at the start of `bytes`: a version 3 archive's, with compression read or none. +/// Parses the header at the start of `bytes`. Errors unless it is a version 3 archive whose +/// directories and tiles are uncompressed or gzip-compressed. pub(crate) fn header(bytes: &[u8]) -> Result { if !is_archive(bytes) || bytes.len() < HEADER { return Err("these bytes are not a PMTiles archive".into()); } if bytes[7] != 3 { - return Err(format!("PMTiles version {} is not read; version 3 is", bytes[7])); + return Err(format!("unsupported PMTiles version {}; only version 3 is supported", bytes[7])); } let u64_at = |at: usize| u64::from_le_bytes(bytes[at..at + 8].try_into().expect("eight bytes")); let degrees = |at: usize| f64::from(i32::from_le_bytes(bytes[at..at + 4].try_into().expect("four bytes"))) / 1e7; let compression = |code: u8, what: &str| match code { 0 | 1 => Ok(false), 2 => Ok(true), - other => Err(format!("the archive's {what} use compression {other}; only gzip is read")), + other => Err(format!("the archive's {what} use compression {other}; only gzip or none is supported")), }; Ok(Header { root: (u64_at(8), u64_at(16)), @@ -60,8 +61,8 @@ pub(crate) fn header(bytes: &[u8]) -> Result { }) } -/// A directory's entries, sorted by tile id: each one's id, run length (0 for a leaf directory), -/// offset, and length. +/// A directory's entries, sorted by tile id: id, run length (0 for a leaf directory), offset, and +/// length. #[derive(Clone, Default, Debug)] pub(crate) struct Directory { ids: Vec, @@ -70,17 +71,17 @@ pub(crate) struct Directory { lengths: Vec, } -/// Where a directory sends a tile. +/// The location a directory gives for a tile. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) enum Entry { - /// The tile's bytes: offset from the data section, and length. + /// The tile's data: offset from the start of the data section, and length. Tile(u64, u64), - /// A leaf directory: offset from the leaf section, and length. + /// A leaf directory to search next: offset from the start of the leaf section, and length. Leaf(u64, u64), } impl Directory { - /// The directory in `bytes`, decompressed. + /// Decodes a directory from already decompressed `bytes`. `None` if malformed. pub(crate) fn decode(bytes: &[u8]) -> Option { let mut at = 0; let mut next = || -> Option { @@ -123,7 +124,8 @@ impl Directory { Some(Directory { ids, runs, offsets, lengths }) } - /// Where the entry holding tile id `id` sends it, if any does. + /// Looks up tile `id`: the tile entry whose run covers it, or the leaf directory that may hold + /// it. `None` if neither. pub(crate) fn find(&self, id: u64) -> Option { let i = match self.ids.binary_search(&id) { Ok(i) => i, @@ -139,7 +141,8 @@ impl Directory { } } -/// Tile `key`'s id along the Hilbert curve, counting every tile of the zooms before it. +/// Tile `key`'s PMTiles id: its Hilbert curve index within its zoom, plus the number of tiles at +/// all lower zooms. pub(crate) fn tile_id(key: TileKey) -> u64 { let z = u32::from(key.z); let before: u64 = (0..z).map(|k| 1_u64 << (2 * k)).sum(); @@ -170,7 +173,7 @@ mod tests { #[test] fn tile_ids_follow_the_hilbert_curve() { assert_eq!(tile_id(TileKey { z: 0, x: 0, y: 0 }), 0); - // Zoom 1 goes around its four tiles from the top left, down, then right. + // Zoom 1 visits its four tiles from the top left: down, right, then up. let ids: Vec = [(0, 0), (0, 1), (1, 1), (1, 0)].map(|(x, y)| tile_id(TileKey { z: 1, x, y })).to_vec(); assert_eq!(ids, vec![1, 2, 3, 4]); assert_eq!(tile_id(TileKey { z: 2, x: 0, y: 0 }), 5); @@ -178,8 +181,8 @@ mod tests { #[test] fn directories_find_tiles_and_leaves() { - // Entries: ids 0 (a run of 2 tiles), 5 (a leaf); offsets written as offset + 1, then 0 for - // "right after the one before". + // Two entries: id 0 (a run of 2 tiles) and id 5 (a leaf). Offsets are stored as offset + 1, + // or 0 for "right after the previous entry". let bytes = [2, 0, 5, 2, 0, 10, 20, 1, 0]; let directory = Directory::decode(&bytes).expect("a directory"); assert_eq!(directory.find(0), Some(Entry::Tile(0, 10))); diff --git a/crates/latkit/src/geo/shapes.rs b/crates/latkit/src/geo/shapes.rs index ffade701..7fff2e8c 100644 --- a/crates/latkit/src/geo/shapes.rs +++ b/crates/latkit/src/geo/shapes.rs @@ -1,7 +1,9 @@ -//! What geometry becomes to draw: an area's triangles with the point its label hangs from, and on the -//! earth a path that bends with the globe. On the earth, longitudes unwrap across the antimeridian so -//! nothing streaks across the map, and nothing spans more than [`EARTH_STEP`] degrees: triangles -//! split along a grid of that pitch, paths gain points. +//! Converts geometry into drawable shapes: polygon triangles with a label anchor, and earth paths +//! that follow the globe. +//! +//! On the earth, longitudes are unwrapped across the antimeridian so no shape streaks across the +//! map. No piece spans more than [`EARTH_STEP`] degrees: triangles are split along a grid of that +//! pitch, and paths gain points. use std::collections::BinaryHeap; @@ -9,12 +11,14 @@ use super::earcut::earcut; type Point = [f64; 2]; -/// Degrees an earth triangle spans a side at most, and an earth path's segment: a chord this long -/// sags under a two-thousandth of the globe's radius. +/// Largest span in degrees, in longitude or latitude, of an earth triangle or path segment. A chord +/// this long sags less than 1/2000 of the globe's radius. pub(crate) const EARTH_STEP: f64 = 2.0; -/// The triangles of `polygons`, each its outer ring then its holes, as a point list: the point its -/// label hangs from first, then three points per triangle. Without triangles, no points at all. +/// Triangulates `polygons`, each an outer ring followed by holes, into a point list: the label +/// anchor first, then three points per triangle. The anchor lies in the polygon with the largest +/// outer ring. Returns an empty list when there are no triangles. With `earth`, rings are unwrapped +/// and triangles split along the [`EARTH_STEP`] grid. pub(crate) fn triangles(polygons: &[Vec>], earth: bool) -> Vec { let mut out = vec![[f64::NAN; 2]]; let mut largest: Option<(f64, Vec>)> = None; @@ -50,8 +54,8 @@ pub(crate) fn triangles(polygons: &[Vec>], earth: bool) -> Vec out } -/// `parts` as an earth path draws them: longitudes unwrapped across the antimeridian, and a point -/// every [`EARTH_STEP`] degrees along longer segments. `None` when nothing changes. +/// Prepares `parts` for an earth path: longitudes unwrapped across the antimeridian, and points +/// added every [`EARTH_STEP`] degrees along longer segments. `None` if no segment needs either. pub(crate) fn earth_parts(parts: &[Vec]) -> Option>> { let changes = parts.iter().any(|part| { part.windows(2).any(|w| (w[1][0] - w[0][0]).abs() > EARTH_STEP || (w[1][1] - w[0][1]).abs() > EARTH_STEP) @@ -59,8 +63,8 @@ pub(crate) fn earth_parts(parts: &[Vec]) -> Option>> { changes.then(|| parts.iter().map(|part| densified(&unwrap(part))).collect()) } -/// `polygon`'s rings with longitudes unwrapped: each ring continuous, and every ring on the side of -/// the antimeridian the outer ring starts on. +/// Unwraps each ring of `polygon` so its longitudes are continuous, then shifts it by whole turns +/// to start within 180 degrees of the outer ring's first point. fn unwrapped(polygon: &[Vec]) -> Vec> { let first = polygon.first().and_then(|r| r.first()).map_or(0.0, |p| p[0]); polygon @@ -80,7 +84,7 @@ fn unwrapped(polygon: &[Vec]) -> Vec> { .collect() } -/// `line` with each longitude moved by whole turns to within 180 degrees of the one before. +/// Shifts each longitude of `line` by whole turns to within 180 degrees of the previous point's. fn unwrap(line: &[Point]) -> Vec { let mut out: Vec = Vec::with_capacity(line.len()); for &p in line { @@ -93,14 +97,15 @@ fn unwrap(line: &[Point]) -> Vec { out } -/// The whole turns that bring a step of `degrees` within half a turn. A step of exactly half a turn -/// stays: a tile half the world wide has edges that long, and which way round they run is theirs. +/// Whole turns (360 degrees) that bring a longitude step of `degrees` within 180 degrees. A step of +/// exactly 180 is kept as given, because a zoom-1 tile's edges span exactly that and their +/// direction matters. fn turns(degrees: f64) -> f64 { degrees.signum() * ((degrees.abs() - 180.0) / 360.0).ceil().max(0.0) } -/// `line` with points every [`EARTH_STEP`] degrees along its longer segments, straight in longitude -/// and latitude. +/// Adds points every [`EARTH_STEP`] degrees along longer segments of `line`, interpolated linearly +/// in longitude and latitude. fn densified(line: &[Point]) -> Vec { let mut out: Vec = Vec::with_capacity(line.len()); for (k, &p) in line.iter().enumerate() { @@ -117,9 +122,9 @@ fn densified(line: &[Point]) -> Vec { out } -/// Triangle `t` cut along the grid of [`EARTH_STEP`] degrees into the pieces each cell holds, each -/// fanned into triangles. A crossing is worked out from its edge's ends in one order, so triangles -/// sharing an edge share its crossings and no crack opens. +/// Cuts triangle `t` along the [`EARTH_STEP`] grid and fans each cell's piece into triangles. A +/// triangle within one cell is kept whole. Each crossing is computed from its edge's endpoints in a +/// fixed order, so triangles sharing an edge get identical crossings and no cracks appear. fn split(t: [Point; 3], out: &mut Vec) { let cell = |v: f64| (v / EARTH_STEP).floor(); let lo = [0, 1].map(|k| cell(t[0][k].min(t[1][k]).min(t[2][k]))); @@ -157,8 +162,8 @@ fn split(t: [Point; 3], out: &mut Vec) { } } -/// The part of convex polygon `piece` on one side of the line where coordinate `axis` is `at`: at or -/// above it when `keep_above`, else at or below it. +/// Clips convex polygon `piece` to one side of the line where coordinate `axis` equals `at`: at or +/// above it when `keep_above`, else at or below it. Writes the result to `out`. fn clip(piece: &[Point], axis: usize, at: f64, keep_above: bool, out: &mut Vec) { out.clear(); let inside = |p: &Point| if keep_above { p[axis] >= at } else { p[axis] <= at }; @@ -174,8 +179,8 @@ fn clip(piece: &[Point], axis: usize, at: f64, keep_above: bool, out: &mut Vec

    Point { let (a, b) = if (p[0], p[1]) <= (q[0], q[1]) { (p, q) } else { (q, p) }; let t = (at - a[axis]) / (b[axis] - a[axis]); @@ -198,8 +203,8 @@ fn ring_area(ring: &[Point]) -> f64 { sum } -/// The point a polygon's label hangs from: the point inside it farthest from its edges, found to a -/// fiftieth of its size, as polylabel finds it. +/// A polygon's label anchor: the interior point farthest from its edges, found to within 1/50 of +/// its longer side by polylabel's method. fn label_point(rings: &[Vec]) -> Point { let Some(outer) = rings.first() else { return [f64::NAN; 2] }; let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]); @@ -213,7 +218,7 @@ fn label_point(rings: &[Vec]) -> Point { if short.is_nan() || short <= 0.0 { return [lo[0].midpoint(hi[0]), lo[1].midpoint(hi[1])]; } - // Cells start square, at most sixteen along the longer side. + // Initial cells are squares as wide as the shorter side, with at most 16 along the longer side. let size = short.max(long / 16.0); let precision = long / 50.0; let probe = |center: Point, half: f64| Cell::new(center, half, signed_distance(center, rings)); @@ -233,7 +238,7 @@ fn label_point(rings: &[Vec]) -> Point { if middle.distance > best.distance { best = middle; } - // Each cell probed costs a pass over the rings; cap them so a huge polygon stays bounded. + // Each probe is a pass over the rings, so cap the probes to bound the cost on huge polygons. let mut probes = 0; while let Some(cell) = queue.pop() { if cell.distance > best.distance { @@ -251,8 +256,9 @@ fn label_point(rings: &[Vec]) -> Point { best.center } -/// A square cell of polylabel's search: its center, half its side, the distance from its center to -/// the polygon's edges (negative outside), and the most any point of it could reach. +/// A square cell in polylabel's search: its center, half its side, the signed distance from its +/// center to the polygon's edges (negative outside), and `max`, an upper bound on that distance for +/// any point in the cell. #[derive(Clone, Copy)] struct Cell { center: Point, @@ -337,7 +343,7 @@ mod tests { vec![[x, y], [x + side, y], [x + side, y + side], [x, y + side], [x, y]] } - /// The area the triangles of a point list cover. + /// Total area of a point list's triangles, after its label anchor. fn covered(points: &[Point]) -> f64 { points[1..] .chunks(3) @@ -357,8 +363,8 @@ mod tests { } #[test] - fn a_half_world_tile_stays_where_it_lies_from_any_corner() { - // A tile at zoom 1 is half the world wide: its edges run exactly 180 degrees. + fn a_half_world_tile_does_not_wrap_regardless_of_its_first_corner() { + // A zoom-1 tile is half the world wide, so its edges span exactly 180 degrees. let corners = [[0.0, 0.0], [180.0, 0.0], [180.0, 80.0], [0.0, 80.0]]; for start in 0..4 { let mut ring: Vec = (0..4).map(|k| corners[(start + k) % 4]).collect(); @@ -388,7 +394,7 @@ mod tests { t.iter().map(|p| p[k]).fold(f64::INFINITY, f64::min), t.iter().map(|p| p[k]).fold(f64::NEG_INFINITY, f64::max), ); - assert!(hi - lo <= EARTH_STEP + 1e-9, "a piece spans {lo}..{hi}"); + assert!(hi - lo <= EARTH_STEP + 1e-9, "a triangle spans at most one grid step: {lo}..{hi}"); } } } @@ -403,8 +409,8 @@ mod tests { #[test] fn earth_parts_bend_and_unwrap() { - assert!(earth_parts(&[vec![[0.0, 0.0], [1.0, 1.0]]]).is_none(), "short segments stay"); - let parts = earth_parts(&[vec![[170.0, 10.0], [-170.0, 10.0]]]).expect("crosses"); + assert!(earth_parts(&[vec![[0.0, 0.0], [1.0, 1.0]]]).is_none(), "short segments need no bending"); + let parts = earth_parts(&[vec![[170.0, 10.0], [-170.0, 10.0]]]).expect("a long segment bends"); assert_eq!(parts[0].first(), Some(&[170.0, 10.0])); assert_eq!(parts[0].last(), Some(&[190.0, 10.0])); assert!(parts[0].windows(2).all(|w| (w[1][0] - w[0][0]).abs() <= EARTH_STEP + 1e-9)); diff --git a/crates/latkit/src/geo/tiles.rs b/crates/latkit/src/geo/tiles.rs index ac757fea..2bf13bf0 100644 --- a/crates/latkit/src/geo/tiles.rs +++ b/crates/latkit/src/geo/tiles.rs @@ -1,35 +1,35 @@ -//! Vector tiles as data: where tiles come from, and a tile decoded into the rows of the tables marks -//! draw from it, each with its shapes. +//! Vector tiles as data: tile URL templates, and decoding a tile into table rows with their shapes. use super::mvt::{self, Kind, Property, Shape}; use crate::data::{Ids, Name, Patch, Table, Values}; use crate::host::Shaping; use crate::tiles::TileKey; -/// Tiles fetched from a URL template, from a TileJSON document or given as one. +/// A tile URL template with its zoom range and bounds, read from TileJSON or given directly. #[derive(Clone, PartialEq, Debug)] pub(crate) struct Template { pub(crate) url: String, pub(crate) min_zoom: u8, pub(crate) max_zoom: u8, - /// West, south, east, north. + /// West, south, east, north, in degrees. pub(crate) bounds: [f64; 4], - /// Whether rows count from the south, as TMS has them. + /// Whether tile rows count from the south (the TMS scheme). pub(crate) tms: bool, } -/// The whole earth web mercator reaches. +/// Bounds of the web mercator world: west, south, east, north, in degrees. pub(crate) const WORLD: [f64; 4] = [-180.0, -85.051_128_78, 180.0, 85.051_128_78]; impl Template { - /// The template `url`, its zooms and bounds as most sources serve them. + /// Template `url` with common defaults: zooms 0 to 14, world bounds, and XYZ row order. pub(crate) fn of(url: &str) -> Template { Template { url: url.to_owned(), min_zoom: 0, max_zoom: 14, bounds: WORLD, tms: false } } - /// The TileJSON document `bytes`, found at `base`. + /// Reads the TileJSON document `bytes` fetched from `base`, resolving its first tile URL + /// against `base`. Errors if it is not TileJSON, names no tiles, or names image tiles. pub(crate) fn tilejson(bytes: &[u8], base: &str) -> Result { - /// What a TileJSON document says that tiles draw by. + /// The TileJSON members read. #[derive(serde::Deserialize)] struct TileJson { #[serde(default)] @@ -42,11 +42,11 @@ impl Template { } let text = std::str::from_utf8(bytes).map_err(|e| format!("{base} is not TileJSON: {e}"))?; let json = crate::from_json::(text).map_err(|e| format!("{base} is not TileJSON: {e}"))?; - let template = json.tiles.first().ok_or_else(|| format!("the TileJSON at {base} names no tiles"))?; + let template = json.tiles.first().ok_or_else(|| format!("the TileJSON at {base} lists no tile URLs"))?; if let Some(format) = json.format.as_deref() && ["png", "jpg", "jpeg", "webp", "avif"].contains(&format) { - return Err(format!("the TileJSON at {base} names image tiles; only vector tiles draw")); + return Err(format!("the TileJSON at {base} has image tiles; only vector tiles are supported")); } let zoom = |z: Option, or: u8| z.filter(|z| *z >= 0.0).map_or(or, |z| z.min(24.0) as u8); let bounds = json.bounds.as_deref().and_then(|b| <[f64; 4]>::try_from(b).ok()).unwrap_or(WORLD); @@ -66,8 +66,8 @@ impl Template { } } -/// A table a tile decodes into: its layer's features of one kind, as table `table`, and the shapes -/// its marks draw of its geometry. +/// A table decoded from each tile: the features of one `kind` in layer `layer`, as table `table`. +/// `shapes` lists the derived shape fields its marks draw. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub(crate) struct Decoded { pub(crate) layer: String, @@ -76,21 +76,23 @@ pub(crate) struct Decoded { pub(crate) shapes: Vec, } -/// Features a tile's table numbers at most; ids beyond its place in the tile never collide. +/// Most rows one tile adds to a table; further rows are dropped. Row ids are the tile's id times +/// this plus the row's index, so ids from different tiles never collide. pub(crate) const FEATURES: u64 = 1 << 20; -/// The field a tile's rows hold their geometry as. +/// Name of the field holding a tile row's geometry. const GEOMETRY: &str = "geometry"; -/// Whether field `name` of a tile's table holds its rows' shapes, which no property is read as. +/// Whether `name` is a derived shape field of a tile's table. Properties of that name are skipped. pub(crate) fn shapes_field(name: &str) -> bool { [Shaping::Triangles { earth: true }, Shaping::Path].iter().any(|s| *s.field_of(GEOMETRY) == *name) } -/// Tile `key`'s rows as a patch of every table of `tables`: each feature of the table's layer and -/// kind a row (a multi-point a row per point), its geometry field `geometry`, the fields its shapes -/// are written as, and its properties fields. A row's id is the tile's place along the Hilbert curve -/// times [`FEATURES`], plus its own place among the table's rows. `None` when the bytes are no tile. +/// Decodes tile `key` from `bytes`, optionally gzip-compressed, into a patch setting each table of +/// `tables`. Each feature of the table's layer and kind becomes a row (a multi-point, a row per +/// point) with field `geometry`, its derived shape fields, and a field per property. A row's id is +/// the tile's Hilbert curve id times [`FEATURES`], plus the row's index. `None` if `bytes` are not +/// a tile. pub(crate) fn decode(bytes: &[u8], key: TileKey, tables: &[Decoded]) -> Option { let bytes = super::gunzipped(bytes).ok()?; let layers = mvt::decode(&bytes, key)?; @@ -161,8 +163,8 @@ pub(crate) fn decode(bytes: &[u8], key: TileKey, tables: &[Decoded]) -> Option

    ]) -> Values { let present = || values.iter().flatten(); if present().all(|v| matches!(v, Property::Number(_))) { @@ -182,7 +184,7 @@ fn field(values: &[Option<&Property>]) -> Values { values.iter().map(|v| v.map(text)).collect::>>().into() } -/// A property written as text. +/// Formats a property as text. pub(crate) fn text(property: &Property) -> String { match property { Property::Text(t) => t.clone(), @@ -213,7 +215,7 @@ mod tests { let id = crate::geo::pmtiles::tile_id(key) * FEATURES; assert_eq!(water.get(id, "name"), Some(Value::Text("bay".into())), "stable ids"); assert!(matches!(water.get(id, "geometry"), Some(Value::Polygons(_)))); - // Its shapes, the triangles areas draw: a label point and a triangle at least. + // The derived triangles field: a label point and at least one triangle. let triangles = water.get(id, "geometry.triangles"); assert!(matches!(triangles, Some(Value::Path(points)) if points.len() >= 4)); let other = [Decoded { layer: "water".into(), kind: Kind::Lines, table: "water".into(), shapes: Vec::new() }]; @@ -230,7 +232,7 @@ mod tests { } #[test] - fn tile_bounds_meet_their_parents() { + fn tile_bounds_are_mercator_and_children_map_back_to_parents() { let key = TileKey { z: 1, x: 1, y: 0 }; let [w, s, e, n] = key.bounds(); assert_eq!([w, s, e], [0.0, 0.0, 180.0]); diff --git a/crates/latkit/src/gpu/arena.rs b/crates/latkit/src/gpu/arena.rs index fb8ef36a..ad728425 100644 --- a/crates/latkit/src/gpu/arena.rs +++ b/crates/latkit/src/gpu/arena.rs @@ -1,25 +1,25 @@ -//! Pages on the GPU: one pool of 128 Ki-word chunks in one storage buffer, which doubles as it fills, -//! up to the budget. It holds the data's pages, which upload, and the pages of words each view's marks -//! keep per row, which the GPU writes. A chunk is carved into blocks of one power-of-two size, and no -//! page is larger than a chunk: a geometry's points split into pieces of a group of rows each. So the -//! room any page leaves serves any other: a chunk whose blocks all come back is carved again for -//! another size. Few sizes in few chunks leave little room unused in chunks partly used, which a small -//! budget feels. +//! GPU memory for pages: one pool of 128 Ki-word chunks in a single storage buffer, which doubles +//! as it fills, up to the budget. It holds uploaded data pages and the per-row word pages each +//! view's marks keep, which the GPU writes. //! -//! A data page is identified by its id and the words of it a program reads; a geometry's pieces by -//! what they hold, so a page written again finds the pieces of its rows that kept their points -//! already on the GPU. What a view draws next reserves every page it reads at once, or none: those in -//! the pool are pinned, and those -//! it lacks take blocks then, evicting the least recently used unpinned pages, so nothing is partly +//! Each chunk is carved into blocks of one power-of-two size, and no page is larger than a chunk; a +//! geometry's points are split into pieces of one row group each. So space any page frees can serve +//! any other: a chunk whose blocks are all freed can be carved again for another size. Few sizes in +//! few chunks waste little space in partly used chunks, which matters with a small budget. +//! +//! A data page is keyed by its id and the words a program reads from it. Geometry pieces are keyed +//! by content, so a rewritten page finds the pieces of its unchanged rows already resident. A view +//! reserves all the pages its next draw reads at once, or none: resident pages are pinned, and +//! missing ones get blocks, evicting the least recently used unpinned pages. So nothing is partly //! pinned and no page waits for room. Page tables pin the pages they reference, so a page in use is -//! never evicted; unpinned pages stay cached. Uploads stream within a per-frame byte budget into the -//! blocks reserved. Views plan what they draw to fit the pool, so a reservation finding no room is -//! rare: it waits for the next frame, which grows the pool by what it lacked. +//! never evicted, and unpinned pages stay cached. Uploads stream into the reserved blocks within a +//! per-frame byte budget. Views plan their drawing to fit the pool, so a reservation rarely finds +//! no room; when it does, it waits for the next frame, which grows the pool by the shortfall. //! -//! A page of words per row is a block of its own: cleared as it is handed out where it is read before -//! anything writes it, and freed only once the frame that let it go is done with it. The pool grows only before a -//! frame records writes to the pages, since growing copies what they hold: a block a frame finds no -//! room for then waits for the next. +//! A per-row word page is a block of its own. It is cleared when handed out if it is read before +//! anything writes it, and freed only once the frame that released it is done. The pool grows only +//! before a frame records writes to the pages, since growing copies the buffer; a block that finds +//! no room after that waits for the next frame. use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque}; use std::hash::{Hash, Hasher}; @@ -32,29 +32,30 @@ use crate::expr::compile::Words; use crate::gpu::shaders::{MISSING, NONE, POINT_BITS}; use crate::gpu::work::Queue; -/// Words in a chunk, as a shift: the largest block, and the largest page. +/// Words in a chunk, as a power of two. A chunk is also the largest block and the largest page. const CHUNK_BITS: u32 = 17; pub(crate) const CHUNK_WORDS: u32 = 1 << CHUNK_BITS; /// Bytes in a chunk. pub(crate) const CHUNK_BYTES: u64 = CHUNK_WORDS as u64 * 4; const MIN_CLASS_BITS: u32 = 6; const CLASSES: usize = (CHUNK_BITS - MIN_CLASS_BITS + 1) as usize; -/// Words of a chunk's free bits: a bit for each block of the least class. +/// 64-bit words in a chunk's free bitmap, one bit per block of the smallest class. const FREE_WORDS: usize = 1 << (CHUNK_BITS - MIN_CLASS_BITS - 6); const INITIAL_CHUNKS: u32 = 8; -/// Points a piece of a geometry's points holds at most, as x and y words. +/// Most points a geometry piece holds, each stored as an x and a y word. pub(crate) const PIECE_POINTS: usize = 1 << POINT_BITS; const _: () = assert!(2 * PIECE_POINTS <= CHUNK_WORDS as usize, "a piece of points fits a chunk"); -/// Rows a piece of a geometry's points begins at, at least: a piece holds the points of a group of this -/// many rows, or fewer where it fills, so a row written again changes only its group's piece. +/// Rows per group. A geometry piece holds the points of one group of this many rows, or fewer when +/// it fills, so rewriting a row changes only its group's piece. const GROUP_ROWS: usize = 64; -/// A page as a program reads it. +/// Key of a resident page: the page and how a program reads it. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub(crate) struct PageKey { pub(crate) page: PageId, pub(crate) words: Words, - /// The part `f32` drops of an `f64`, rather than the value. + /// Whether the page holds the low part of `f64` values (what `f32` drops) instead of the + /// values. pub(crate) low: bool, } @@ -63,7 +64,7 @@ impl PageKey { PageKey { page, words, low } } - /// A piece of a geometry, `words`, keyed by what it holds. + /// The key of geometry piece `words`, from a hash of its content. pub(crate) fn piece(words: &[u32]) -> PageKey { let mut hasher = FxHasher::default(); words.hash(&mut hasher); @@ -71,27 +72,28 @@ impl PageKey { } } -/// The pieces of the geometry pages drawn lately, by each page's id: its rows' table, then its pieces -/// of points, keyed by what they hold, and how many points it has. A page drawn again finds its -/// pieces and its points without making or counting them; one written again makes them, and uploads -/// those the GPU lacks. +/// Piece keys of recently drawn geometry pages, by page id: the key of the page's row table, then +/// the content keys of its point pieces, with its point count. A page drawn again reuses them +/// without rebuilding or recounting. A rewritten page rebuilds them and uploads the pieces the GPU +/// lacks. #[derive(Default)] pub(crate) struct Pieces { keys: HashMap, usize), FxBuildHasher>, order: VecDeque, } -/// Geometry pages whose pieces are kept: a page past them makes its pieces again when it is drawn. +/// Most geometry pages whose piece keys are kept. A page forgotten past this rebuilds its pieces +/// when drawn. const PIECED_PAGES: usize = 1 << 14; impl Pieces { - /// The pieces of page `page`, and its points, when kept. + /// The piece keys and point count of page `page`, if kept. pub(crate) fn of(&self, page: PageId) -> Option<(Arc<[PageKey]>, usize)> { self.keys.get(&page).cloned() } - /// Keeps `keys` as page `page`'s pieces, of `points` points, forgetting the page kept longest when - /// too many are. + /// Keeps `keys` as page `page`'s piece keys, with `points` points. Beyond [`PIECED_PAGES`] + /// pages, forgets the oldest. pub(crate) fn keep(&mut self, page: PageId, keys: Arc<[PageKey]>, points: usize) { if self.keys.insert(page, (keys, points)).is_none() { self.order.push_back(page); @@ -103,21 +105,21 @@ impl Pieces { } } -/// A page a reservation asks for: what it is, the words it must hold, and, where the pool does not -/// hold them, what it uploads. +/// A page in a reservation: its key, the words it must hold, and the content to upload if the pool +/// lacks them. pub(crate) struct Ask { pub(crate) key: PageKey, pub(crate) need: u32, pub(crate) content: Option, } -/// What to upload for a page. +/// The content uploaded for a page. pub(crate) enum Content { - /// Static values, converted to words for `words`. + /// Static values, converted to words as `words` reads them. Values { values: Values, words: Words, low: bool }, - /// A page of samples; one an append opened grows, and only its new tail uploads. + /// A page of samples. A page an append opened grows, and only its new tail uploads. Frames(FramePage), - /// A page of where a level's entries draw along the axis; it grows like a page of samples. + /// A page of axis positions for a level's entries. It grows like a page of samples. Axis(AxisPage), /// Words computed on the CPU: text codes, join rows, a geometry's pieces. Computed(Vec), @@ -136,7 +138,7 @@ impl Content { len as u32 } - /// The page's words from word `from` on: a growing page uploads only its new tail. + /// The page's words from word `from` on, so a growing page uploads only its new tail. fn words(&self, from: usize) -> Vec { match self { Content::Values { values, words, low } => page_words(values, *words, *low).split_off(from), @@ -148,8 +150,8 @@ impl Content { } } - /// The class of the block it takes: a growing page takes a block of the most words a page of its - /// kind holds, to grow into. + /// The block class the page takes. A growing page takes a block of its kind's largest page + /// size, to grow into. fn class(&self, words: u32) -> usize { match self { Content::Frames(page) if page.grows() => class_of(FRAME_PAGE_WORDS as u32), @@ -159,12 +161,13 @@ impl Content { } } -/// Words of the block that holds `words`. +/// Words in the block that holds `words`. pub(crate) fn rounded(words: u32) -> u32 { class_words(class_of(words)) } -/// The class of a block holding `words`: the power of two at least that large. +/// The class of a block holding `words`: the smallest power of two at least that large, and at +/// least 64. fn class_of(words: u32) -> usize { let bits = words.max(1).next_power_of_two().trailing_zeros().max(MIN_CLASS_BITS); (bits - MIN_CLASS_BITS) as usize @@ -179,8 +182,9 @@ fn slots(class: usize) -> u32 { CHUNK_WORDS / class_words(class) } -/// What a page uploads as `words` reads it: numbers as `f32`, the low part of `f64`s, point -/// components, or reference rows as `u32`. Missing values are `MISSING`, or `NONE` for rows. +/// The words a page uploads for `words`: numbers as `f32` (with `low`, the low part of each `f64`), +/// point components, or reference rows as `u32`. Missing values become `MISSING`, or `NONE` for +/// rows. pub(crate) fn page_words(values: &Values, words: Words, low: bool) -> Vec { let n = values.len(); let word = |x: f64| { @@ -207,18 +211,18 @@ pub(crate) fn page_words(values: &Values, words: Words, low: bool) -> Vec { } } -/// A geometry page as `lib/read.wesl` reads it: its rows' table, two words a row (the piece its -/// points begin in, among the page's, shifted up by `POINT_BITS` over its first point there; and how -/// many points it has); then its pieces of points, every point an x and a y word. +/// A geometry page as `lib/read.wesl` reads it. The row table has two words per row: the index of +/// the piece where its points begin, shifted left by `POINT_BITS` over its first point's index in +/// that piece; and its point count. Then come the pieces, each point an x and a y word. pub(crate) struct PathPage { pub(crate) rows: Vec, pub(crate) pieces: Vec>, } -/// A geometry page's rows' table and pieces. A group of rows begins a piece, as does a row its piece -/// has no room for, so a row written again changes only its group's piece; a row larger than a piece -/// begins one and fills as many as it takes. A row's parts, or its polygons' rings, are broken by a -/// missing point. +/// Builds a geometry page's row table and pieces. Each group of rows starts a new piece, as does a +/// row that does not fit in the current one, so rewriting a row changes only its group's piece. A +/// row larger than a piece starts one and fills as many as it needs. A missing point separates a +/// row's parts, or its polygons' rings. pub(crate) fn path_pieces(values: &Values) -> PathPage { let mut rows = Vec::with_capacity(2 * values.len()); let mut pieces: Vec> = Vec::new(); @@ -262,8 +266,8 @@ pub(crate) fn path_pieces(values: &Values) -> PathPage { PathPage { rows, pieces } } -/// Bytes the arena holds a geometry page of `rows` rows and `points` points in, about: its rows' -/// table, and its points in pieces of a group of rows each, or full pieces where rows hold more. +/// Approximate arena bytes for a geometry page of `rows` rows and `points` points: its row table, +/// plus its points in one piece per row group, or in full pieces when groups hold more. pub(crate) fn path_page_bytes(rows: usize, points: usize) -> u64 { let groups = rows.div_ceil(GROUP_ROWS).max(1); let per_group = points.div_ceil(groups); @@ -282,12 +286,12 @@ struct Block { /// Words uploaded so far. written: u32, pins: u32, - /// When it was last unpinned, its place in the LRU. + /// When it was last unpinned: its position in the LRU order. tick: u64, } -/// A chunk of the pool: free, or carved into blocks of one class with a bit for each that is free; -/// the class is the one whose chunks list it. +/// A chunk of the pool: free, or carved into blocks of one class with a bit per free block. The +/// chunk does not store its class; `Arena::partial` lists it under its class while it has room. #[derive(Clone, Copy)] struct Chunk { live: u32, @@ -310,11 +314,12 @@ impl Chunk { pub(crate) struct Arena { buffer: wgpu::Buffer, chunks: Vec, - /// The most chunks the pool grows to. + /// Most chunks the pool may grow to. limit: u32, /// Changes whenever the buffer is replaced, so views rebind it. pub(crate) generation: u64, - /// Free chunks, and each class's chunks with a free block, lowest first so blocks gather low. + /// Free chunks, and each class's chunks with a free block. The lowest is used first, so blocks + /// gather at low offsets. free: BTreeSet, partial: [BTreeSet; CLASSES], blocks: HashMap, @@ -325,18 +330,18 @@ pub(crate) struct Arena { contents: HashMap, /// Bytes of allocated blocks. resident: u64, - /// Blocks of their own handed out and not yet cleared; and those let go, freed once the frame is - /// done with them. + /// Per-row blocks handed out and not yet cleared. `freed` holds released ones, freed when the + /// next frame begins. cleared: Vec<(u32, usize)>, freed: Vec<(u32, usize)>, - /// Whether this frame recorded writes to the pages, after which the pool keeps its buffer; and the - /// words of blocks it found no room for, which the next frame grows the pool by. + /// Whether this frame recorded writes to the pages, after which the pool keeps its buffer. + /// `short` counts the words of blocks that found no room; the next frame grows the pool by it. writing: bool, short: u64, } impl Arena { - /// A pool that grows to at most `bytes`, within what the device binds. + /// A pool that grows to at most `bytes`, within what the device can bind. pub(crate) fn new(device: &wgpu::Device, bytes: u64) -> Arena { let limit = limit_of(device, bytes); let chunks = INITIAL_CHUNKS.min(limit); @@ -360,8 +365,9 @@ impl Arena { } } - /// Begins a frame of every view drawing from the pool: frees the blocks the last one let go, and - /// grows the pool by what it found no room for, before anything this frame writes the pages. + /// Begins a frame for all views drawing from the pool. Frees the blocks the last frame released + /// and grows the pool to cover its shortfall, at least doubling it, before anything this frame + /// writes the pages. pub(crate) fn begin_frame(&mut self, device: &wgpu::Device, queue: &Queue) { for (offset, class) in std::mem::take(&mut self.freed) { self.release(offset, class); @@ -374,13 +380,13 @@ impl Arena { } } - /// Reserves every page `asks` names, or none: pins those the pool holds, and gives those it lacks - /// blocks of their size, evicting the least recently used unpinned pages but never these, queuing - /// what they upload, as for one the pool holds fewer words of than asked. `false` where a page - /// finds no room, or lacks what it uploads: nothing is pinned, and the next frame grows the pool by - /// what it lacked. + /// Reserves every page in `asks`, or none. Pins the resident ones and gives the missing ones + /// blocks, evicting least recently used unpinned pages but never these. Queues uploads for new + /// pages and for resident pages holding fewer words than asked. Returns `false`, pinning + /// nothing, if a missing page has no content to upload or finds no room; for no room, the next + /// frame grows the pool by the shortfall. pub(crate) fn reserve(&mut self, device: &wgpu::Device, queue: &Queue, asks: &mut [Ask]) -> bool { - // Pages held are pinned first, so placing the rest evicts none of them. + // Pin resident pages first, so placing the rest evicts none of them. let held: Vec = asks.iter().map(|ask| self.blocks.contains_key(&ask.key)).collect(); for ask in asks.iter().zip(&held).filter(|&(_, &held)| held).map(|(ask, _)| ask) { self.pin(ask.key); @@ -425,9 +431,9 @@ impl Arena { true } - /// A block of its own holding `words`, for a page of words per row, with `cleared` cleared by the - /// next [`clear`](Arena::clear); `None` where the pool has no room now, which the next frame grows - /// it by. + /// Allocates a separate block of `words` for a per-row word page. With `cleared`, the next + /// [`clear`](Arena::clear) zeroes it. `None` if the pool has no room now; the next frame grows + /// it. pub(crate) fn block(&mut self, device: &wgpu::Device, queue: &Queue, words: u32, cleared: bool) -> Option { let class = class_of(words); let offset = self.allocate(device, queue, class)?; @@ -437,13 +443,13 @@ impl Arena { Some(offset) } - /// Lets go of block `offset` of `words`, which [`block`](Arena::block) handed out: freed once - /// this frame is done with it. + /// Releases block `offset` of `words` from [`block`](Arena::block). It is freed when the next + /// frame begins, once this frame is done with it. pub(crate) fn free(&mut self, offset: u32, words: u32) { self.freed.push((offset, class_of(words))); } - /// Clears the blocks handed out since the last clearing, at once: before anything this frame + /// Clears the blocks handed out since the last clear, immediately, before anything this frame /// records or writes into them. pub(crate) fn clear(&mut self, device: &wgpu::Device, queue: &Queue) { if self.cleared.is_empty() { @@ -457,8 +463,8 @@ impl Arena { queue.submit([encoder.finish()]); } - /// Notes that this frame records writes to the pages: from now on it keeps the pool's buffer, which - /// growing would copy before they run. + /// Notes that this frame records writes to the pages. From now on the pool keeps its buffer, + /// since growing would copy it before those writes run. pub(crate) fn written(&mut self) { self.writing = true; } @@ -472,38 +478,38 @@ impl Arena { self.chunks.len() as u32 >= self.limit } - /// Lets the pool grow to at most `bytes` from now on; it never shrinks. + /// Lets the pool grow to at most `bytes` from now on. It never shrinks. pub(crate) fn set_limit(&mut self, device: &wgpu::Device, bytes: u64) { self.limit = limit_of(device, bytes).max(self.chunks.len() as u32); } - /// The block holding at least `need` words of `key`, if one does. + /// The offset of the block holding at least `need` words of `key`, if any. pub(crate) fn offset(&self, key: PageKey, need: u32) -> Option { self.blocks.get(&key).filter(|b| b.written >= need).map(|b| b.offset) } - /// Bytes of the block `key` takes, or none. + /// Bytes of the block `key` occupies, or 0. pub(crate) fn bytes_of(&self, key: PageKey) -> u64 { self.blocks.get(&key).map_or(0, |b| u64::from(class_words(b.class)) * 4) } - /// Whether the pool holds, or is uploading, at least `need` words of `key`: a reservation asks - /// for what it uploads only where not. + /// Whether the pool holds, or is uploading, at least `need` words of `key`. A reservation + /// supplies content to upload only when it does not. pub(crate) fn holds(&self, key: PageKey, need: u32) -> bool { self.offset(key, need).is_some() || (self.blocks.contains_key(&key) && self.contents.contains_key(&key)) } - /// Keeps `key`, which the pool holds, while pinned; pins nest. + /// Pins resident page `key` so it is not evicted. Pins nest. fn pin(&mut self, key: PageKey) { - let block = self.blocks.get_mut(&key).expect("a page reserved"); + let block = self.blocks.get_mut(&key).expect("a resident page"); if block.pins == 0 { self.unpinned.remove(&block.tick); } block.pins += 1; } - /// Lets go of a pin a reservation took. A page no longer pinned stays cached, but one still - /// uploading, which nothing waits for now: it goes, and its upload with it. + /// Releases a pin a reservation took. A page no longer pinned stays cached, unless it is still + /// uploading: nothing waits for it now, so it is freed and its upload dropped. pub(crate) fn unpin(&mut self, key: PageKey) { let Some(block) = self.blocks.get_mut(&key) else { return }; block.pins -= 1; @@ -511,7 +517,7 @@ impl Arena { return; } if self.contents.remove(&key).is_some() { - let block = self.blocks.remove(&key).expect("held"); + let block = self.blocks.remove(&key).expect("a resident page"); self.release(block.offset, block.class); return; } @@ -535,11 +541,11 @@ impl Arena { self.resident } - /// Uploads queued pages into the blocks reserved for them, at least one and at most `budget` bytes; - /// returns the bytes uploaded. + /// Uploads queued pages into their reserved blocks until `budget` bytes are reached, always at + /// least one page unless `budget` is 0. Returns the bytes uploaded. pub(crate) fn pump(&mut self, queue: &Queue, budget: u64) -> u64 { let mut bytes = 0; - // Views drawn earlier in the frame spent it. + // Views drawn earlier in the frame spent the budget. if budget == 0 { return 0; } @@ -557,7 +563,7 @@ impl Arena { writes.push((offset + written, tail)); } } - // Pages placed side by side, as a column's pages are, upload in one write. + // Adjacent pages, such as a column's, upload in one write. writes.sort_unstable_by_key(|&(at, _)| at); let mut run: Option<(u32, Vec, u64)> = None; for (at, words) in writes { @@ -595,10 +601,10 @@ impl Arena { self.resident -= u64::from(class_words(class)) * 4; } - /// A block of `class`: a free one of its size, a free chunk carved, the pool grown while this - /// frame has not written the pages, or room the least recently used unpinned blocks leave, - /// whatever their size; `None` when every block left is pinned or a block of its own, and the - /// next frame grows the pool by it. + /// Allocates a block of `class`. Tries, in order: a free block of that class, a newly carved + /// free chunk, growing the pool (only before this frame writes the pages), then evicting least + /// recently used unpinned blocks of any size. `None` when every remaining block is pinned or a + /// per-row block; the next frame then grows the pool by the shortfall. fn allocate(&mut self, device: &wgpu::Device, queue: &Queue, class: usize) -> Option { loop { if let Some(&k) = self.partial[class].first() { @@ -629,12 +635,13 @@ impl Arena { return None; }; self.unpinned.remove(&tick); - let block = self.blocks.remove(&key).expect("listed"); + let block = self.blocks.remove(&key).expect("an unpinned page is resident"); self.release(block.offset, block.class); } } - /// Grows the buffer to `chunks`, at most the limit, copying what it holds. + /// Grows the buffer to `chunks`, capped at the limit, copying its contents. Returns whether it + /// grew. fn grow(&mut self, device: &wgpu::Device, queue: &Queue, chunks: u32) -> bool { let before = self.chunks.len() as u32; let chunks = chunks.min(self.limit); @@ -654,7 +661,8 @@ impl Arena { } } -/// Chunks a pool of at most `bytes` holds, within what the device binds: at least the first ones. +/// Chunks in a pool of at most `bytes`, within what the device can bind, and at least +/// `INITIAL_CHUNKS`. fn limit_of(device: &wgpu::Device, bytes: u64) -> u32 { let limits = device.limits(); let bytes = bytes.min(limits.max_storage_buffer_binding_size).min(limits.max_buffer_size); @@ -696,7 +704,7 @@ mod tests { assert!((back - (1.0 + 1e-9)).abs() < 1e-15); } - /// Row `row`'s piece, first point, and count, as a page's rows' table holds them. + /// Row `row`'s piece index, first point, and point count, from a page's row table. fn entry(page: &PathPage, row: usize) -> (u32, u32, u32) { let [entry, count] = [page.rows[2 * row], page.rows[2 * row + 1]]; (entry >> POINT_BITS, entry & ((1 << POINT_BITS) - 1), count) @@ -710,27 +718,27 @@ mod tests { let page = path_pieces(&values); assert_eq!(entry(&page, 0), (0, 0, (PIECE_POINTS + 10) as u32), "the long row begins the first piece"); assert_eq!(page.pieces[0].len(), 2 * PIECE_POINTS, "and fills it"); - assert_eq!(entry(&page, 1), (1, 10, 0), "an empty row holds no point where the next begins"); - // The last row's two parts are broken by a missing point. + assert_eq!(entry(&page, 1), (1, 10, 0), "an empty row has no points and starts where the next begins"); + // A missing point separates the last row's two parts. assert_eq!(entry(&page, 2), (1, 10, 4)); assert_eq!(page.pieces[1].len(), 2 * (10 + 4)); assert_eq!(values.points(), PIECE_POINTS + 10 + 4); } #[test] - fn a_row_written_again_changes_only_its_group_s_piece() { + fn rewriting_a_row_changes_only_the_piece_of_its_group() { let rows = |nudge: f64| { let path = |r: usize| (0..5).map(|k| [r as f64, f64::from(k) + if r == 70 { nudge } else { 0.0 }]).collect(); Values::from((0..200).map(|r| vec![path(r)]).collect::>>>()) }; let (before, after) = (path_pieces(&rows(0.0)), path_pieces(&rows(0.5))); - assert_eq!(before.pieces.len(), 4, "a piece a group of 64 rows"); - assert_eq!(entry(&before, 64), (1, 0, 5), "each group begins a piece"); + assert_eq!(before.pieces.len(), 4, "one piece per group of 64 rows"); + assert_eq!(entry(&before, 64), (1, 0, 5), "each group starts a new piece"); let keys = |page: &PathPage| page.pieces.iter().map(|p| PageKey::piece(p)).collect::>(); let changed: Vec = keys(&before).iter().zip(keys(&after)).map(|(a, b)| *a != b).collect(); assert_eq!(changed, [false, true, false, false], "row 70 lies in the second group"); - assert_eq!(before.rows, after.rows, "the rows' table holds where points lie, not what they are"); - assert!(path_page_bytes(200, 1000) < 16 * 1024, "a page's estimate counts pieces as they are cut"); + assert_eq!(before.rows, after.rows, "the row table stores where points are, not their values"); + assert!(path_page_bytes(200, 1000) < 16 * 1024, "the estimate counts one piece per row group"); } } diff --git a/crates/latkit/src/gpu/atlas.rs b/crates/latkit/src/gpu/atlas.rs index 824b131e..0daebc66 100644 --- a/crates/latkit/src/gpu/atlas.rs +++ b/crates/latkit/src/gpu/atlas.rs @@ -1,13 +1,16 @@ -//! Text on the GPU: runs set once by the host at `TEXT_BASE`, turned into distance fields reaching -//! `TEXT_SPREAD` texels past the text's edge, and packed on shelves of one texture. Until a run is -//! set, its size is measured from its font's characters, each measured by the host once, so its label -//! is placed where it will show. A distance field -//! draws crisp at any size and gives halos for free, so a run is set once whatever size it shows at. -//! The host sets coverage; the GPU makes the fields of every run set in a frame at once, before the -//! frame draws (`compute/fields.wesl`). When no shelf has room, the shelf used least recently is -//! cleared for the new run; only when none fits does the packing start over. A run counts as used -//! when it is set and whenever labels in view show it, so what the view shows stays. Views follow -//! evictions to drop what they pointed at. +//! The text atlas. The host rasterizes each run once at `TEXT_BASE`, and the GPU turns it into a +//! distance field reaching `TEXT_SPREAD` texels past the text's edge, packed on shelves of one +//! texture. A distance field draws crisp at any size and also yields halos, so a run is rasterized +//! once regardless of the size it shows at. +//! +//! Until a run is rasterized, its size is estimated from its font's character advances, each +//! measured by the host once, so its label is placed where it will show. The GPU builds the fields +//! of every run rasterized in a frame in one batch before the frame draws (`compute/fields.wesl`). +//! +//! When no shelf has room, the least recently used shelf of a fitting height is cleared for the new +//! run; only when none fits does packing start over. A run counts as used when it is rasterized and +//! whenever labels in view show it, so visible text is not evicted. Views track evictions to drop +//! stale references. use std::collections::HashMap; use std::sync::Arc; @@ -26,28 +29,28 @@ const SIZE: u32 = 4096; const GAP: u32 = 1; /// Texels of field around a run's coverage. const PAD: u32 = TEXT_SPREAD as u32; -/// Runs whose fields one dispatch makes at most: one workgroup layer each. +/// Most runs whose fields one dispatch builds, one workgroup layer each. const RUNS: usize = 65_535; -/// Where a run lies in the atlas. +/// A run's location in the atlas. #[derive(Clone, Copy, PartialEq, Debug)] pub(crate) struct Glyphs { - /// Left, top, right, and bottom, texels, reaching `TEXT_SPREAD` past the text on every side. + /// Left, top, right, and bottom in texels, extending `TEXT_SPREAD` past the text on every side. pub(crate) rect: [u32; 4], /// Texels from the top of the rectangle to the baseline. pub(crate) baseline: f32, } impl Glyphs { - /// The rectangle as the two corner words shaders read: 16 bits of x, then 16 of y. + /// The rectangle as the two corner words shaders read: x in the low 16 bits, y in the high 16. pub(crate) fn corners(&self) -> [u32; 2] { let [l, t, r, b] = self.rect; [l | t << 16, r | b << 16] } } -/// What a font's text takes at the base size, as the host sets it: what a run of no characters -/// takes, what each line past the first adds, and each character's advance, measured once each. +/// A font's text metrics at the base size, as the host measures them: the size of an empty run, +/// the height each extra line adds, and each character's advance, measured once per character. struct Metrics { empty: [f32; 2], line: f32, @@ -55,7 +58,7 @@ struct Metrics { } impl Metrics { - /// `font`'s, as `host` measures it. + /// Measures `font` with `host`. fn of(host: &dyn Host, font: &Arc) -> Metrics { let measure = |text: &str, max_width: Option| { host.measure(&TextRun { text: text.into(), font: font.clone(), size_px: TEXT_BASE as u32, max_width }) @@ -66,7 +69,7 @@ impl Metrics { Metrics { empty, line, advances: FxHashMap::default() } } - /// What `c` advances a line by, measured by `host` the first time. + /// The advance of `c`, measured by `host` on first use. fn advance(&mut self, host: &dyn Host, font: &Arc, c: char) -> f32 { let empty = self.empty[0]; *self.advances.entry(c).or_insert_with(|| { @@ -77,8 +80,9 @@ impl Metrics { } } -/// The run the atlas sets for `text` shown at `size`, wrapping at `max_width`, both length units: set -/// at the base size, wrapping where `max_width` falls at it. A sheet measures the same run it draws. +/// The run the atlas rasterizes for `text` shown at `size`, wrapping at `max_width`, both in length +/// units. The run is set at the base size, with `max_width` scaled to match. A sheet measures the +/// same run it draws. pub(crate) fn run(text: Arc, font: Arc, size: f32, max_width: Option) -> TextRun { let wrap = max_width.map(|w| (w * TEXT_BASE / size.max(1e-3)).round().max(1.0) as u32); TextRun { text, font, size_px: TEXT_BASE as u32, max_width: wrap } @@ -88,11 +92,12 @@ struct Shelf { top: u32, height: u32, used: u32, - /// When a run on it was last asked for. + /// The clock tick when a run on it was last used. tick: u64, } -/// A run set whose field waits to be made: its coverage, and the shelf and corner its field fills. +/// A rasterized run whose field is not built yet: its coverage, and the shelf and corner its field +/// goes to. struct Waiting { raster: Raster, shelf: usize, @@ -104,19 +109,20 @@ pub(crate) struct Atlas { texture: wgpu::Texture, pub(crate) view: wgpu::TextureView, shelves: Vec, - /// Every run asked for, and its shelf; `None` when the host could not set it. + /// Every run rasterized: its location and shelf, or `None` when it has nothing to draw or its + /// raster failed. Either way the host is not asked for it again. runs: HashMap>, - /// Runs set whose fields the next frame makes. + /// Rasterized runs whose fields the next frame builds. waiting: Vec, - /// What making fields works in, and its bind group. + /// Scratch buffer for building fields, and its bind group. work: Option<(wgpu::Buffer, wgpu::BindGroup)>, - /// Counts the frames runs are asked for in. + /// Frame counter for shelf use. clock: u64, - /// Changes whenever runs are evicted, so what pointed at them looks again. + /// Changes whenever runs are evicted, so anything referencing them looks them up again. pub(crate) evictions: u64, - /// What counts the texts the host is asked to set. + /// Counts the texts the host is asked to rasterize. counters: Arc, - /// What each font's text takes, by font. + /// Text metrics by font. metrics: HashMap, Metrics>, } @@ -148,10 +154,11 @@ impl Atlas { } } - /// The run table entry for `run` while it is not set: the flag saying so, then the corner a - /// rectangle of its size reaches from the atlas's own corner. Its lines break at spaces as the - /// host breaks them, each as wide as its characters' advances, so its label is placed where it - /// will show while only labels that show have their texts set. Nothing without a host. + /// The run table entry for `run` before it is rasterized: the `TEXT_UNSET` flag, then its + /// estimated size as a corner word (width in the low 16 bits, height in the high). Lines break + /// at spaces as the host breaks them, each as wide as its characters' advances, so the label is + /// placed where it will show while only visible labels have their text rasterized. `[0, 0]` + /// without a host. pub(crate) fn unset(&mut self, host: Option<&dyn Host>, run: &TextRun) -> [u32; 2] { let Some(host) = host else { return [0, 0] }; let font = &run.font; @@ -178,13 +185,14 @@ impl Atlas { [crate::gpu::shaders::TEXT_UNSET, w | h << 16] } - /// Starts a frame: runs asked for from now on count as used in it. + /// Starts a frame. Runs requested from now on count as used in it. pub(crate) fn tick(&mut self) { self.clock += 1; } - /// Where `run` lies, set by `host` the first time it is asked for; its field is made before the - /// frame draws. + /// The location of `run` in the atlas, rasterized by `host` on first request. Its field is + /// built before the frame draws. `None` without a host, or when the run has nothing to draw or + /// its raster failed; a failure is reported once. pub(crate) fn text(&mut self, host: Option<&dyn Host>, run: &TextRun) -> Option { if let Some(found) = self.runs.get(run) { let (glyphs, shelf) = (*found)?; @@ -193,25 +201,36 @@ impl Atlas { } let host = host?; self.counters.add(Count::Texts, 1); - let raster = match host.rasterize(run) { - Ok(raster) => raster, + let placed = match host.rasterize(run) { + Ok(raster) => self.hold(raster), Err(error) => { self.errors.push(error); - return None; + None } }; - let placed = self.hold(raster); - if placed.is_some() { - self.runs.insert(run.clone(), placed); - } + self.runs.insert(run.clone(), placed); placed.map(|(glyphs, _)| glyphs) } - /// Places `raster`'s field, which waits to be made: where it lies, and its shelf. + /// Forgets everything the host made: runs, failures, and font metrics. A new host rasterizes + /// texts again in its own fonts. + pub(crate) fn rehost(&mut self) { + self.runs.clear(); + self.waiting.clear(); + self.shelves.clear(); + self.metrics.clear(); + self.evictions += 1; + } + + /// Places `raster`'s field, queued to be built, and returns its location and shelf. `None` if + /// the raster is empty, too large for the atlas, or malformed. fn hold(&mut self, raster: Raster) -> Option<(Glyphs, usize)> { let [w, h] = raster.size; + if w == 0 || h == 0 { + return None; + } let (width, height) = (w.saturating_add(2 * PAD), h.saturating_add(2 * PAD)); - if w == 0 || h == 0 || width > SIZE || height > SIZE { + if width > SIZE || height > SIZE { self.errors.push(crate::Error::Limit("text raster does not fit the atlas".into())); return None; } @@ -225,21 +244,21 @@ impl Atlas { Some((glyphs, shelf)) } - /// Counts `run` as used now: labels in view show it. + /// Marks `run` as used now, because labels in view show it. pub(crate) fn touch(&mut self, run: &TextRun) { if let Some(Some((_, shelf))) = self.runs.get(run) { self.shelves[*shelf].tick = self.clock; } } - /// Whether `glyphs` still lie where they were for `run`. + /// Whether `run` is still at `glyphs`. pub(crate) fn holds(&self, run: &TextRun, glyphs: Glyphs) -> bool { matches!(self.runs.get(run), Some(Some((g, _))) if *g == glyphs) } - /// Makes the fields of the runs set since last time, recorded into `encoder` before what draws - /// them: their coverage goes up, each column and then each row takes its least squared distances - /// within the spread, and each field copies into its place. + /// Builds the fields of runs rasterized since the last call, recorded into `encoder` before the + /// draws that use them. Uploads their coverage, computes each column's and then each row's + /// least squared distances within the spread, and copies each field into its place. pub(crate) fn lay( &mut self, device: &wgpu::Device, @@ -253,8 +272,8 @@ impl Atlas { let rest = self.waiting.split_off(self.waiting.len().min(RUNS)); let laid = std::mem::replace(&mut self.waiting, rest); let size = |w: &Waiting| w.raster.size.map(|n| n + 2 * PAD); - // The runs and their coverage go up; their columns and fields follow. A field starts where a - // copy out of it may, and its rows are as far apart as copies need. + // Scratch layout: run entries, coverage, column distances, then fields. Each field starts + // at a valid copy offset, and its rows use the copy row pitch. let mut runs = vec![[0_u32; FIELD_WORDS as usize]; laid.len()]; let mut coverage = Vec::new(); let mut at = laid.len() as u32 * FIELD_WORDS; @@ -317,7 +336,8 @@ impl Atlas { } } - /// The buffer making fields works in, holding at least `bytes`, and its bind group. + /// The scratch buffer for building fields, at least `bytes` long, and its bind group. A new + /// buffer gets half again as much room. fn fit_work( &mut self, device: &wgpu::Device, @@ -342,7 +362,7 @@ impl Atlas { (buffer, group) } - /// The shelf and top left of room for `width` by `height` texels. + /// Finds room for `width` by `height` texels, returning the shelf and top-left corner. fn place(&mut self, width: u32, height: u32) -> (usize, [u32; 2]) { let fits = |s: &Shelf| s.height >= height && s.height <= height + height / 2 + 4; let at = self.shelves.iter().position(|s| fits(s) && s.used + width <= SIZE); @@ -354,7 +374,7 @@ impl Atlas { }) }); let at = at.or_else(|| { - // Clear the shelf of this height used least recently, if not in this frame. + // Clear the least recently used shelf of a fitting height, unless used this frame. let clock = self.clock; let oldest = (0..self.shelves.len()) .filter(|&i| fits(&self.shelves[i]) && self.shelves[i].tick < clock) @@ -382,15 +402,15 @@ impl Atlas { } } -/// Bytes a copy out of a buffer into a texture may start at a multiple of, on every backend. +/// Alignment in bytes of a buffer-to-texture copy's start offset, valid on every backend. const COPY_ALIGN: u32 = 512; #[cfg(test)] mod tests { use super::*; - /// The field of `width` by `height` coverage bytes as it is defined, texel by texel: the least - /// squared distance to a texel inside and outside over every texel within reach. + /// The exact field of `width` by `height` coverage bytes, computed texel by texel: the least + /// squared distance to inside and to outside over every texel within reach. fn exact(coverage: &[u8], width: usize, height: usize) -> Vec { let pad = i64::from(PAD); let (w, h) = (width as i64 + 2 * pad, height as i64 + 2 * pad); @@ -429,7 +449,7 @@ mod tests { } #[test] - fn fields_made_on_the_gpu_are_the_exact_field() { + fn gpu_distance_fields_match_the_exact_field() { let gpu = match pollster::block_on(crate::Gpu::new()) { Ok(gpu) => gpu, Err(error) if std::env::var_os("LATKIT_REQUIRE_GPU").is_some() => panic!("GPU required: {error}"), @@ -492,6 +512,43 @@ mod tests { let off = made.iter().zip(&want).filter(|(a, b)| a.abs_diff(**b) > 1).count(); assert_eq!(off, 0, "every texel within a step of the exact field"); let deepest = made.iter().max().copied().unwrap_or(0); - assert!(made.contains(&0) && deepest > 224, "the field runs from nothing outside to deep inside: {deepest}"); + assert!(made.contains(&0) && deepest > 224, "the field runs from 0 outside to over 224 inside: {deepest}"); + } + + #[test] + fn each_run_is_rasterized_once_however_it_turns_out() { + /// A host that counts its rasters: none for "", a failure for "bad", else a square. + struct Counting(std::sync::atomic::AtomicU32); + impl Host for Counting { + fn rasterize(&self, run: &TextRun) -> crate::Result { + self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + match &*run.text { + "" => Ok(Raster { size: [0, 0], coverage: Vec::new(), baseline: 0.0 }), + "bad" => Err(crate::Error::Unsupported("no font".into())), + _ => Ok(Raster { size: [4, 4], coverage: vec![255; 16], baseline: 3.0 }), + } + } + } + let gpu = match pollster::block_on(crate::Gpu::new()) { + Ok(gpu) => gpu, + Err(error) if std::env::var_os("LATKIT_REQUIRE_GPU").is_some() => panic!("GPU required: {error}"), + Err(_) => return, + }; + let counting = Counting(std::sync::atomic::AtomicU32::new(0)); + let rasters = || counting.0.load(std::sync::atomic::Ordering::Relaxed); + let host: Option<&dyn Host> = Some(&counting); + let text = |text: &str| run(text.into(), "sans-serif".into(), 12.0, None); + let mut inner = gpu.0.lock(); + let atlas = &mut inner.atlas; + for _ in 0..2 { + assert!(atlas.text(host, &text("")).is_none(), "an empty raster draws nothing"); + assert!(atlas.text(host, &text("bad")).is_none(), "a failed raster draws nothing"); + assert!(atlas.text(host, &text("ok")).is_some(), "a raster is placed"); + } + assert_eq!(rasters(), 3, "each run is rasterized once"); + assert_eq!(atlas.errors.len(), 1, "only the failure is reported, once: {:?}", atlas.errors); + atlas.rehost(); + assert!(atlas.text(host, &text("ok")).is_some()); + assert_eq!(rasters(), 4, "a new host rasterizes the run again"); } } diff --git a/crates/latkit/src/gpu/colormaps.rs b/crates/latkit/src/gpu/colormaps.rs index cd78b52a..06901284 100644 --- a/crates/latkit/src/gpu/colormaps.rs +++ b/crates/latkit/src/gpu/colormaps.rs @@ -41,7 +41,8 @@ impl Colormaps { Colormaps { texture, view, layers: HashMap::new(), capacity: INITIAL_LAYERS, generation: 0 } } - /// The layer holding `colormap`, uploading it the first time. + /// The layer holding `colormap`, uploading it the first time. Errors once the device's texture + /// array layer limit is reached. pub(crate) fn layer(&mut self, device: &wgpu::Device, queue: &Queue, colormap: &Colormap) -> Result { let key = colormap.key(); if let Some(&layer) = self.layers.get(&key) { diff --git a/crates/latkit/src/gpu/dictionary.rs b/crates/latkit/src/gpu/dictionary.rs index e769997b..5e752a48 100644 --- a/crates/latkit/src/gpu/dictionary.rs +++ b/crates/latkit/src/gpu/dictionary.rs @@ -1,6 +1,6 @@ -//! Text on the GPU: each text field's distinct texts get codes in the order they are first seen. -//! Codes only ever grow, so a page's codes never change and upload once. A page is coded once; one -//! written over a page coded takes its codes and codes only the rows written. +//! Text dictionaries: each text field's distinct texts get codes in the order they are first seen. +//! Codes are never reassigned, so a page's codes never change and upload once. A page is coded +//! once, and a page written over a coded page reuses its codes and codes only the rows written. use std::collections::HashMap; use std::sync::Arc; @@ -11,7 +11,7 @@ use crate::Name; use crate::data::{Column, Keyed, Page, PageId, Values}; use crate::gpu::shaders::MISSING; -/// Pages a dictionary keeps the codes of: past them, it forgets them all. +/// Most pages a dictionary keeps codes for. Past this, it forgets all of them. const PAGES: usize = 256; #[derive(Default)] @@ -24,7 +24,7 @@ pub(crate) struct Dictionary { codes: HashMap, u32, FxBuildHasher>, /// Each code's text. texts: Vec>, - /// The codes of pages coded, by page. + /// Codes of coded pages, by page. pages: HashMap, FxBuildHasher>, } @@ -36,8 +36,8 @@ impl Dictionaries { } impl Dictionary { - /// The codes of `page`, its texts added: `f32` words, `MISSING` where missing. A page coded - /// before costs nothing; one written over a page coded costs the rows written. + /// The codes of `page` as `f32` words, `MISSING` where missing, adding its new texts. A page + /// coded before needs no work; one written over a coded page codes only the rows written. pub(crate) fn codes(&mut self, page: &Page) -> Arc<[u32]> { if let Some(codes) = self.pages.get(&page.id) { return codes.clone(); @@ -55,7 +55,7 @@ impl Dictionary { } let codes: Arc<[u32]> = match reused { Some((from, before, rows)) => { - // The page written over is usually written over no more. + // The old page is rarely written over again, so drop its codes. self.pages.remove(&from); let mut codes = before.to_vec(); for row in rows { @@ -72,13 +72,14 @@ impl Dictionary { codes } - /// The codes of `column`'s rows `rows`, in that order, their texts added: what a page packed in a - /// table's order holds. Each row costs a lookup, however its pages were coded. + /// The codes of `column`'s rows `rows`, in that order, adding new texts: the contents of a + /// packed page of a paged table. Each row costs a lookup, regardless of how its pages were + /// coded. pub(crate) fn rows(&mut self, column: &Column, rows: &[u32]) -> Vec { rows.iter().map(|&r| column.at(r as usize).map_or(MISSING, |(values, i)| self.code(values, i))).collect() } - /// The code of value `i` of `values` as an `f32` word, its text added when it is new. + /// The code of value `i` of `values` as an `f32` word, adding its text if new. fn code(&mut self, values: &Values, i: usize) -> u32 { let Some(text) = values.text().filter(|_| values.is_valid(i)) else { return MISSING }; let t = text.get(i); @@ -94,7 +95,7 @@ impl Dictionary { (code as f32).to_bits() } - /// Codes given so far. + /// Number of codes assigned so far. pub(crate) fn len(&self) -> usize { self.texts.len() } @@ -104,7 +105,7 @@ impl Dictionary { self.texts.get(code as usize) } - /// The code of `text` as an `f32` word; one no text has when it was never seen. + /// The code of `text` as an `f32` word, or NaN, which matches no text, if it was never seen. pub(crate) fn code_of(&self, text: &str) -> u32 { self.codes.get(text).map_or(f32::NAN.to_bits(), |&code| (code as f32).to_bits()) } @@ -116,7 +117,7 @@ mod tests { use crate::data::Column; #[test] - fn a_page_written_over_one_coded_codes_only_its_rows() { + fn rewriting_a_coded_page_codes_only_the_written_rows() { let column = Column::new(&Values::from(vec!["a", "b", "c"])); let mut dictionary = Dictionary::default(); let first = dictionary.codes(&column.pages()[0]); diff --git a/crates/latkit/src/gpu/icons.rs b/crates/latkit/src/gpu/icons.rs index b6b907c3..307465dc 100644 --- a/crates/latkit/src/gpu/icons.rs +++ b/crates/latkit/src/gpu/icons.rs @@ -1,5 +1,6 @@ -//! Icons on the GPU: every image an icon marker shows, as a layer of one texture array of a common -//! size, scaled to fit. Markers name their images; the same image shares its layer. +//! Icons on the GPU: each image an icon marker shows is a layer of one texture array, scaled to fit +//! a common size. Markers reference their images, and markers showing the same image share its +//! layer. use std::collections::HashMap; use std::sync::Arc; @@ -12,7 +13,8 @@ const SIZE: u32 = 64; pub(crate) struct Icons { texture: wgpu::Texture, pub(crate) view: wgpu::TextureView, - /// Each image's layer, by the address of its pixels. + /// Each image's layer, keyed by the address of its pixels, which are kept alive so the address + /// is not reused. layers: HashMap, u32)>, capacity: u32, /// Changes whenever the texture is replaced, so views rebind it. @@ -93,7 +95,7 @@ impl Icons { } } -/// `image` scaled to `SIZE` square by its nearest texels, centered with its aspect kept. +/// Scales `image` into a `SIZE` square by nearest texel, centered with its aspect ratio kept. fn fit(image: &crate::Image) -> Vec { let [w, h] = image.size.map(|v| v.max(1)); let scale = f64::from(w.max(h)) / f64::from(SIZE); diff --git a/crates/latkit/src/gpu/mod.rs b/crates/latkit/src/gpu/mod.rs index 4293fc90..d46add8b 100644 --- a/crates/latkit/src/gpu/mod.rs +++ b/crates/latkit/src/gpu/mod.rs @@ -1,5 +1,5 @@ //! The GPU: one device shared by every view, with the page arena, pipelines, colormaps, and text -//! dictionaries every view draws from. +//! dictionaries all views draw from. pub(crate) mod arena; pub(crate) mod atlas; @@ -28,8 +28,8 @@ use work::{Counters, Queue, Work}; use crate::host::Host; use crate::{Error, Result}; -/// The format views draw canvases and shots in, so both share one set of pipelines: every WebGPU -/// canvas takes it. +/// The texture format of canvases and shots, shared so both use one set of pipelines. Every WebGPU +/// canvas supports it. pub(crate) const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8Unorm; /// The GPU device that views draw with. Cheap to clone; views of one `Gpu` share uploaded data. @@ -54,63 +54,63 @@ impl DeviceEvents { self.errors .lock() .unwrap_or_else(PoisonError::into_inner) - .push(Error::Unsupported(format!("the GPU refused: {error}").into())); + .push(Error::Unsupported(format!("GPU error: {error}").into())); } } pub(crate) struct Shared { - /// The instance and adapter `Gpu::new` created the device from, for surfaces. + /// The instance and adapter `Gpu::new` created the device from, for creating surfaces. pub(crate) instance: Option, pub(crate) adapter: Option, pub(crate) device: wgpu::Device, - /// The queue, counting what is written through it. + /// The queue, which counts what is written through it. pub(crate) queue: Queue, - /// What the GPU did, counted: views count the rows they evaluate and the jobs they hand out - /// through it, and [`Gpu::work`] reads it. + /// Work counters. Views count the rows they evaluate and the jobs they start here, and + /// [`Gpu::work`] reads them. pub(crate) counters: Arc, pub(crate) sampler: wgpu::Sampler, pub(crate) lost: Arc, - /// Errors the device reported outside any scope, for views to tell. + /// Errors the device reported outside any error scope, for views to report. pub(crate) errors: Arc>>, - /// Samples per pixel every frame drawn with this GPU takes: decided once, from the screen it - /// serves, so a pixel ratio changing later (a browser's zoom, a window moved to another screen) - /// never makes a scene's pipelines again. + /// Multisample count of every frame drawn with this GPU. It is set once from the screen's pixel + /// ratio, so a later change (browser zoom, a window moved to another screen) never rebuilds a + /// scene's pipelines. samples: std::sync::OnceLock, inner: Mutex, } -/// Samples per pixel at `pixel_ratio`: shader-drawn marks antialias themselves, so dense screens skip -/// multisampling, which only triangles' edges need. +/// Multisample count at `pixel_ratio`: 1 at 1.75 and above, else 4. Shader-drawn marks antialias +/// themselves and only triangle edges need multisampling, so dense screens skip it. fn samples_at(pixel_ratio: f32) -> u32 { if pixel_ratio >= 1.75 { 1 } else { 4 } } pub(crate) struct Inner { pub(crate) arena: Arena, - /// The pieces of the geometry pages drawn lately, by what they hold. + /// Piece keys of recently drawn geometry pages. pub(crate) pieces: arena::Pieces, pub(crate) pipelines: Pipelines, pub(crate) colormaps: Colormaps, pub(crate) dictionaries: Dictionaries, - /// Pages of tables drawn in part, packed in their orders, which every view drawing them shares. + /// Pages of paged tables, packed in draw order and shared by every view drawing them. pub(crate) packed: Packed, - /// What was fetched and decoded of each source of tiles, which every view drawing it shares. + /// Fetched and decoded tiles of each tile source, shared by every view drawing it. pub(crate) tiles: crate::tiles::Cache, pub(crate) atlas: Atlas, pub(crate) icons: Icons, pub(crate) readbacks: Readbacks, pub(crate) host: Option>, pub(crate) budget: Budget, - /// What each view plans within: its share of the budget. + /// Each view's share of the budget, which it plans within. pub(crate) ledger: Ledger, - /// What the frame its views draw together may still do. + /// What the current frame, shared by views drawn together, may still upload and compute. pub(crate) frame: Allowance, } -/// What one frame of a GPU's views may still do between them: bytes to upload, and microseconds of -/// work done on the frame rather than in jobs. A frame ends when a view that drew in it draws again, -/// so views drawn together (a page's canvases, a shot's panels) share one, and a view drawn alone, -/// even at a held instant, has one each time. +/// What one frame may still do across a GPU's views: bytes to upload, and microseconds of work done +/// on the frame rather than in jobs. A frame ends when a view that drew in it draws again. So views +/// drawn together (a page's canvases, a shot's panels) share one allowance, and a view drawn alone +/// gets a new one each time, even when it redraws the same instant. #[derive(Default)] pub(crate) struct Allowance { drawn: Vec, @@ -119,8 +119,9 @@ pub(crate) struct Allowance { } impl Inner { - /// Begins view `view`'s part of a frame: a new frame when it already drew in this one, which - /// frees the pages the last one let go and grows the pool by what it found no room for. + /// Begins view `view`'s part of a frame. If no view has drawn yet, or `view` already drew in + /// the current frame, starts a new one: resets the allowance, frees the blocks the last frame + /// released, and grows the pool to cover its shortfall. pub(crate) fn begin_frame(&mut self, view: u64, device: &wgpu::Device, queue: &Queue) { if self.frame.drawn.is_empty() || self.frame.drawn.contains(&view) { self.frame = @@ -131,34 +132,35 @@ impl Inner { } } -/// The share of the budget the views of a GPU plan what they draw within, between them; the rest holds -/// pages a new version uploads while the one it replaces is still drawn, and pages cached. +/// Fraction of the budget that a GPU's views together plan their drawing within. The rest holds the +/// pages a new data version uploads while the old one is still drawn, and cached pages. const PLANNED: f64 = 0.5; -/// What each view of a GPU plans what it draws within: shares of one part of the budget, granted -/// max-min fair, so views together never plan past it however many there are. Pages several views -/// draw are on the GPU once, so they count once, split between those views. +/// Each view's share of the planned part of the budget, granted max-min fair, so views together +/// never plan past it however many there are. Pages several views draw are resident once, so they +/// count once, split between those views. #[derive(Default)] pub(crate) struct Ledger { - /// What each view would draw whole, by view. + /// Each view's request for drawing everything, by view. asks: rustc_hash::FxHashMap, } -/// What a view would draw whole: bytes of its own, and the pages it reads that any view drawing the -/// same data shares. +/// A view's memory request for drawing everything: its own bytes, plus the pieces it shares with +/// any view drawing the same data. #[derive(Clone, Default, PartialEq, Debug)] pub(crate) struct Ask { /// Bytes no other view holds: its marks' state, focus, labels, traces, and derived columns. pub(crate) own: f64, - /// Each piece any view drawing it shares, by what it holds, and its bytes. + /// Each shared piece, keyed by content, with its bytes. pub(crate) shared: Vec<(u64, f64)>, } impl Ledger { - /// Records what view `view` would draw whole, `whole` bytes as it counts them, and grants it its - /// share of what `budget` plans, as a part of `whole`. Each view asks its own bytes and a split of - /// each piece it shares between the views drawing it; each gets all it asks while an even split - /// of what is left covers it, else that split, views asking less having what they ask first. + /// Records view `view`'s request `ask` and returns its grant from the planned part of `budget`, + /// scaled to `whole` (the view's own count of its bytes). Each view asks for its own bytes plus + /// an even split of each piece it shares. Grants are max-min fair: smaller asks are granted in + /// full first, and once an ask exceeds an even split of what is left, each remaining view gets + /// that split. pub(crate) fn grant(&mut self, view: u64, whole: f64, ask: Ask, budget: u64) -> f64 { self.asks.insert(view, ask); let mut sharing: rustc_hash::FxHashMap = rustc_hash::FxHashMap::default(); @@ -192,18 +194,21 @@ impl Ledger { #[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Budget { - /// Bytes the data every view of the `Gpu` draws may take on the GPU: its pages, and each view's - /// rows. A view draws a table too large for its share through the tiles the camera asks for, so - /// what it draws always fits; pages no view draws stay cached in the room left, the least - /// recently drawn going first. By default half the device's largest storage buffer. The pool of - /// pages grows as data needs it, and never shrinks: set the budget before data draws. + /// GPU memory in bytes for the data all views of the `Gpu` draw: its pages and each view's + /// per-row state. A view draws a table too large for its share as a paged table, drawing only + /// the pages the camera needs, so it always fits. Pages no view draws stay cached in the + /// remaining room, least recently drawn evicted first. Defaults to half the device's largest + /// storage buffer. The page pool grows as data needs it and never shrinks, so set the budget + /// before drawing data. pub bytes: u64, - /// Bytes uploaded per frame at most; larger data streams in over frames. + /// Most bytes uploaded per frame; larger data streams in over several frames. Defaults to + /// 32 MiB. pub upload_per_frame: u64, } impl Budget { - /// Half the largest storage buffer `device` binds, and 32 MiB uploaded per frame. + /// The default budget: half the largest storage buffer `device` binds, and 32 MiB uploaded per + /// frame. fn of(device: &wgpu::Device) -> Budget { let limits = device.limits(); let largest = limits.max_storage_buffer_binding_size.min(limits.max_buffer_size); @@ -213,6 +218,7 @@ impl Budget { impl Gpu { /// Creates a device on the best adapter: WebGPU in browsers, Vulkan, Metal, or DX12 natively. + /// Errors if no adapter or device is available. pub async fn new() -> Result { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: wgpu::Backends::PRIMARY | wgpu::Backends::BROWSER_WEBGPU, @@ -232,7 +238,7 @@ impl Gpu { max_storage_buffers_per_shader_stage: supported.max_storage_buffers_per_shader_stage, ..wgpu::Limits::default() }; - // Timestamps, where the adapter writes them, measure what each frame costs the GPU. + // Timestamp queries, where the adapter supports them, measure each frame's GPU time. let features = adapter.features() & wgpu::Features::TIMESTAMP_QUERY; let (device, queue) = adapter .request_device(&wgpu::DeviceDescriptor { @@ -246,9 +252,10 @@ impl Gpu { Ok(Gpu::create(Some(instance), Some(adapter), device, queue)) } - /// Queues, once, what nearly every scene draws with into the page's canvases, so views opened - /// later find it compiled: in a browser, as its first view opens, compiling once that view's - /// frames have all they draw with. A GPU no view uses compiles nothing. + /// Queues, once, the pipelines nearly every scene uses to draw into the page's canvases, so + /// views opened later find them compiled. In a browser this happens as the first view opens, + /// and they compile after that view's draw pipelines finish. A GPU no view uses compiles + /// nothing. #[cfg(all(feature = "web", target_arch = "wasm32"))] pub(crate) fn warm(&self) { let pixel_ratio = web_sys::window().map_or(1.0, |w| w.device_pixel_ratio() as f32); @@ -257,20 +264,21 @@ impl Gpu { self.0.lock().pipelines.warm(&self.0.device, &waker, || crate::view::common(FORMAT, samples)); } - /// Samples per pixel its frames take, once decided. + /// The multisample count of its frames, once set. pub(crate) fn samples(&self) -> Option { self.0.samples.get().copied() } - /// Samples per pixel its frames take: decided by the first frame's `pixel_ratio` where no screen - /// decided it when the GPU was made, and kept from then on. + /// The multisample count of its frames. If no screen set it when the GPU was created, the first + /// frame's `pixel_ratio` sets it, and it stays fixed from then on. pub(crate) fn samples_for(&self, pixel_ratio: f32) -> u32 { *self.0.samples.get_or_init(|| samples_at(pixel_ratio)) } /// Draws with an application-owned device and queue without replacing their callbacks. /// Forward device loss and uncaptured errors through [`Gpu::device_events`]. Clone this - /// `Gpu` to share its caches and budget between views. + /// `Gpu` to share its caches and budget between views. Errors if the device does not meet + /// WebGPU's default limits. pub fn from_wgpu(device: wgpu::Device, queue: wgpu::Queue) -> Result { if !wgpu::Limits::default().check_limits(&device.limits()) { return Err(Error::Unsupported("the device does not meet WebGPU's default limits".into())); @@ -309,7 +317,7 @@ impl Gpu { let queue = Queue::new(queue, counters.clone()); #[cfg_attr( target_arch = "wasm32", - allow(clippy::arc_with_non_send_sync, reason = "WebGPU objects stay on the one thread a page has") + allow(clippy::arc_with_non_send_sync, reason = "WebGPU objects stay on the page's single thread") )] let shared = Arc::new_cyclic(|gpu| { let inner = Inner { @@ -341,7 +349,8 @@ impl Gpu { inner: Mutex::new(inner), } }); - // In a page, the screen decides at once: every view and canvas of the GPU draws for it. + // In a page, set the multisample count now from the screen, which every view and canvas of + // the GPU draws for. #[cfg(all(feature = "web", target_arch = "wasm32"))] if let Some(window) = web_sys::window() { let _ = shared.samples.set(samples_at(window.device_pixel_ratio() as f32)); @@ -349,9 +358,9 @@ impl Gpu { Gpu(shared) } - /// What this GPU did since it was made, counted exactly. It repeats wherever the budget, the - /// device's limits, and the frames drawn do, so tests and benchmarks hold it to what it was. - /// Subtract two for what happened between. + /// Exact counts of the work this GPU has done since it was created. The counts repeat for the + /// same budget, device limits, and frames drawn, so tests and benchmarks can compare them with + /// a baseline. Subtract two snapshots to get the work between them. pub fn work(&self) -> Work { self.0.counters.work() } @@ -366,7 +375,8 @@ impl Gpu { &self.0.queue } - /// The instance a `Gpu` from [`Gpu::new`] was created with, for creating surfaces it can draw to. + /// The instance a `Gpu` from [`Gpu::new`] was created with, for creating surfaces it can draw + /// to. pub fn instance(&self) -> Option<&wgpu::Instance> { self.0.instance.as_ref() } @@ -376,7 +386,7 @@ impl Gpu { self.0.adapter.as_ref() } - /// Sets how much GPU memory latkit uses; bytes past what the device binds are not used. + /// Sets how much GPU memory latkit uses. Bytes beyond what the device can bind are not used. pub fn set_budget(&self, budget: Budget) { let mut inner = self.0.lock(); inner.arena.set_limit(&self.0.device, budget.bytes); @@ -388,22 +398,25 @@ impl Gpu { self.0.lock().budget } - /// Bytes on the GPU every view of it draws from: data pages and what each view keeps per row. + /// Bytes resident on the GPU for all its views: data pages and each view's per-row state. pub fn resident(&self) -> u64 { self.0.lock().arena.resident() } - /// Bytes waiting to upload, for every view of it. + /// Bytes waiting to upload, across all its views. pub fn uploading(&self) -> u64 { self.0.lock().arena.pending() } - /// Gives views the platform's services: setting text, and doing jobs off the frame. + /// Gives views the platform's services: text, background jobs, fetching, and video encoding. + /// Texts the last host set are set again by this one. pub fn set_host(&self, host: impl Host) { - self.0.lock().host = Some(Arc::new(host)); + let mut inner = self.0.lock(); + inner.host = Some(Arc::new(host)); + inner.atlas.rehost(); } - /// Whether the device was lost; drawing then does nothing, and a new `Gpu` is needed. + /// Whether the device was lost. Drawing then does nothing, and a new `Gpu` is needed. pub fn lost(&self) -> bool { self.0.lost.load(Ordering::Acquire) } @@ -414,7 +427,7 @@ impl Shared { self.inner.lock().unwrap_or_else(PoisonError::into_inner) } - /// The lock, unless something holds it now. + /// The lock, or `None` if it is held now. #[cfg(all(feature = "web", target_arch = "wasm32"))] pub(crate) fn try_lock(&self) -> Option> { match self.inner.try_lock() { @@ -443,21 +456,21 @@ mod tests { fn views_share_the_planned_budget_max_min_fair() { let mut ledger = Ledger::default(); let grant = |ledger: &mut Ledger, view, bytes| ledger.grant(view, bytes, own(bytes), 1000); - assert_eq!(grant(&mut ledger, 1, 100.0), 100.0, "a view asking less than half the budget has all it asks"); - assert_eq!(grant(&mut ledger, 2, 900.0), 400.0, "the next has what is left"); - assert_eq!(grant(&mut ledger, 3, 900.0), 200.0, "two large views split what the small one leaves"); + assert_eq!(grant(&mut ledger, 1, 100.0), 100.0, "a view asking for under half the budget gets all it asks"); + assert_eq!(grant(&mut ledger, 2, 900.0), 400.0, "the next gets the rest"); + assert_eq!(grant(&mut ledger, 3, 900.0), 200.0, "two large views split the rest of the budget"); ledger.leave(2); - assert_eq!(grant(&mut ledger, 3, 900.0), 400.0, "a view that leaves gives its share back"); + assert_eq!(grant(&mut ledger, 3, 900.0), 400.0, "a view that leaves returns its share"); } #[test] - fn pages_views_share_count_once() { + fn pages_shared_by_views_count_once() { let mut ledger = Ledger::default(); - // Eight views of one dataset: 300 bytes of shared pages each reads, and 20 of its own. + // Eight views of one dataset, each reading 300 shared bytes and holding 20 of its own. let ask = Ask { own: 20.0, shared: vec![(7, 300.0)] }; let shares: Vec = (0..8).map(|view| ledger.grant(view, 320.0, ask.clone(), 1000)).collect(); - assert_eq!(shares[7], 320.0, "300 + 8 × 20 fits the 500 planned: every view draws whole"); - // Two views of other data each, too large together: each has the same part of what it asks. + assert_eq!(shares[7], 320.0, "300 + 8 × 20 fits the 500 planned, so every view gets its full ask"); + // Two views of different data, too large together, each get the same fraction of their ask. let mut ledger = Ledger::default(); ledger.grant(1, 400.0, Ask { own: 0.0, shared: vec![(1, 400.0)] }, 1000); assert_eq!(ledger.grant(2, 400.0, Ask { own: 0.0, shared: vec![(2, 400.0)] }, 1000), 250.0); diff --git a/crates/latkit/src/gpu/packed.rs b/crates/latkit/src/gpu/packed.rs index 83617ab2..c54615ad 100644 --- a/crates/latkit/src/gpu/packed.rs +++ b/crates/latkit/src/gpu/packed.rs @@ -1,9 +1,9 @@ -//! Pages packed in an order: a column of a table drawn in part, a page of its order at a time, the -//! first time a view draws that page, as the words its marks read. Each is kept by what it was -//! packed from, so views drawing a table alike share its pages, a page the camera comes back to is -//! the one it left, and a write repacks only the pages its rows draw in. A table drawn in its own -//! order reads its data's pages as they are, but a reference to a table drawn in another order, whose -//! pages are packed through that order's ranks. +//! Packed pages of paged tables. A column of a paged table is packed one page of its draw order at +//! a time, the first time a view draws that page, as the words its marks read. Packed pages are +//! keyed by their source, so views drawing a table the same way share its pages, a page the camera +//! returns to is the same page, and a write repacks only the pages its rows draw in. A table drawn +//! in its own order reads its data's pages directly, except for a reference to a table drawn in +//! another order, whose pages are packed through that order's ranks. use std::sync::Arc; @@ -17,12 +17,12 @@ use crate::gpu::shaders::PAGE_ROWS; use crate::gpu::shaders::{MISSING, NONE}; use crate::gpu::work::{Count, Counters}; -/// Columns of pages kept at most: past them, the half used least recently is forgotten. +/// Most packed columns kept. Past this, the least recently used half is forgotten. const KEPT: usize = 4096; -/// What one column of pages packed in an order is: the order, the dataset and table, the field and -/// how it is read, and what else its words read (a reference's target order, the frame of samples -/// shown, the rows a join reads). +/// The key of one packed column: the order, the dataset (`origin`) and table, the field and how it +/// is read, and anything else its words depend on (`reads`: a reference's target order, the frame +/// of samples shown, or the rows a join reads). #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub(crate) struct Packing { pub(crate) order: u64, @@ -34,8 +34,8 @@ pub(crate) struct Packing { pub(crate) reads: u64, } -/// A column's pages: what they were packed from, each page's id once packed, the samples a traced -/// page was gathered as, and when they were last used. +/// A column's packed pages: their source column, each page's id once packed, the samples gathered +/// for each traced page, and when they were last used. struct Pages { source: Option, ids: Vec>, @@ -43,13 +43,12 @@ struct Pages { used: u64, } -/// Every column of pages packed in an order, on the GPU's views' behalf, and what counts the rows -/// packed. +/// Every packed column, shared by the GPU's views, and the counters for rows packed. pub(crate) struct Packed { counters: Arc, columns: FxHashMap, - /// Pages of a table's own order whose reference rows are packed through the ranks of the order - /// their table draws in, by the page and that order. + /// Reference pages of tables drawn in their own order, packed through the ranks of the order + /// their target table draws in, keyed by page and that order. ranked: FxHashMap<(PageId, u64), (PageId, u64)>, uses: u64, } @@ -64,14 +63,14 @@ impl Packed { self.counters.add(Count::Packed, rows as u64); } - /// What counts the rows packed, for packing that may happen later. + /// The counters for rows packed, for packing done later. pub(crate) fn counters(&self) -> Arc { self.counters.clone() } - /// The pages of `packing`, packed from `source` as it is now: each kept while no row it holds was - /// written since, `page_of` saying which page holds a row. Without a source, kept while `packing` - /// is the same. + /// The pages of `packing`, packed from the current `source`. A page is kept while none of its + /// rows was written since; `page_of` maps a row to its page. Without a source, pages are kept + /// as long as `packing` matches. fn pages( &mut self, packing: &Packing, @@ -115,7 +114,8 @@ impl Packed { self.pages_of(packing, order, source, &[page])[0] } - /// The ids of pages `wanted` of `packing`, packed from `source` in `order`: the column found once. + /// The ids of pages `wanted` of `packing`, packed from `source` in `order`, looking the column + /// up once. pub(crate) fn pages_of( &mut self, packing: &Packing, @@ -136,8 +136,8 @@ impl Packed { .collect() } - /// The id of a page of rows kept, of `packing`, packed from `source`: kept while none of the rows - /// it holds, which `holds` tells, was written since. + /// The id of `packing`'s page of kept rows, packed from `source`. The page is kept while none + /// of its rows (those `holds` accepts) was written since. pub(crate) fn kept(&mut self, packing: &Packing, source: Option<&Column>, holds: impl Fn(u32) -> bool) -> PageId { let pages = self.pages(packing, source, |row| holds(row).then_some(0)); if pages.ids.is_empty() { @@ -146,7 +146,7 @@ impl Packed { *pages.ids[0].get_or_insert_with(PageId::next) } - /// Samples `samples`' rows `rows` hold, page `page` of `packing`, gathered once. + /// The samples of `samples`' rows `rows`, for page `page` of `packing`, gathered once. pub(crate) fn samples(&mut self, packing: &Packing, page: u32, rows: &[u32], samples: &Samples) -> Samples { let counters = self.counters.clone(); let pages = self.pages(packing, None, |_| None); @@ -163,8 +163,8 @@ impl Packed { .clone() } - /// The id of reference page `page` of a table drawn in its own order, packed through the ranks of - /// the order `order` its target draws in. + /// The id of reference page `page` of a table drawn in its own order, packed through the ranks + /// of order `order`, which its target draws in. pub(crate) fn ranked(&mut self, page: PageId, order: u64) -> PageId { self.uses += 1; if self.ranked.len() >= KEPT && !self.ranked.contains_key(&(page, order)) { @@ -177,7 +177,7 @@ impl Packed { } } -/// Drops the half of `kept` used least recently, by when `used` says each was. +/// Drops the least recently used half of `kept`, by the use time `used` returns. fn forget(kept: &mut FxHashMap, used: impl Fn(&V) -> u64) { let mut uses: Vec = kept.values().map(&used).collect(); let half = uses.len() / 2; @@ -185,15 +185,16 @@ fn forget(kept: &mut FxHashMap, used: kept.retain(|_, v| used(v) > cut); } -/// The words `column`'s rows `rows` are read as, a page of them, missing past the rows. +/// A page of words for `column`'s rows `rows`, padded with missing values past the rows. pub(crate) fn values(column: &Column, rows: &[u32], words: Words, low: bool) -> Vec { let mut out = page_words(&column.pick(rows), words, low); out.resize(PAGE_ROWS as usize, if words == Words::Bits { NONE } else { MISSING }); out } -/// Words of the rows `reference` names for rows `rows`, a page of them: where each draws in `rank`, -/// the order its table draws in, or as it is in a table drawn whole; `NONE` where a row names none. +/// A page of the target rows `reference` gives for rows `rows`: each target's place in `rank`, its +/// table's draw order, or the row itself when that table is drawn whole. `NONE` where a row +/// references nothing. pub(crate) fn targets(reference: &Reference, rows: &[u32], rank: Option<&Order>) -> Vec { let mut out: Vec = rows .iter() @@ -206,8 +207,8 @@ pub(crate) fn targets(reference: &Reference, rows: &[u32], rank: Option<&Order>) out } -/// The words of reference page `page` of a table drawn in its own order: the rows it names, as -/// `rank`, the order their table draws in, places them. +/// The words of reference page `page` of a table drawn in its own order: each target row's place +/// in `rank`, its table's draw order. pub(crate) fn ranked_page(reference: &Reference, page: usize, rank: &Order) -> Vec { let start = page * PAGE_ROWS as usize; let len = reference.rows.pages().get(page).map_or(0, |p| p.values.len()); @@ -219,7 +220,7 @@ pub(crate) fn ranked_page(reference: &Reference, page: usize, rank: &Order) -> V .collect() } -/// Values of `samples`' rows `rows` at frame `frame`, a page of them, missing past the rows. +/// A page of the values of `samples`' rows `rows` at frame `frame`, padded with missing values. pub(crate) fn frame(samples: &Samples, rows: &[u32], frame: Option) -> Vec { let mut out: Vec = rows .iter() @@ -229,7 +230,8 @@ pub(crate) fn frame(samples: &Samples, rows: &[u32], frame: Option) -> Vec< out } -/// The rows of `other` with the ids of `table`'s rows `rows`, a page of them; `NONE` where none. +/// A page of the rows of `other` with the ids of `table`'s rows `rows`. `NONE` where `other` has +/// no such row. pub(crate) fn joined(table: &Table, rows: &[u32], other: &Table) -> Vec { let mut out: Vec = rows .iter() @@ -273,9 +275,9 @@ mod tests { let ids: Vec = (0..3).map(|p| packed.page(&packing, &order, Some(&column), p)).collect(); let written = column.write(n, &[(5, 0)], &Column::new(&Values::from(vec![-1.0_f64]))).expect("numbers"); let after: Vec = (0..3).map(|p| packed.page(&packing, &order, Some(&written), p)).collect(); - assert_eq!((after[0], after[1]), (ids[0], ids[1]), "pages not drawing row 5 are kept"); - assert_ne!(after[2], ids[2], "row 5 draws in the last page, which packs again"); + assert_eq!((after[0], after[1]), (ids[0], ids[1]), "pages without row 5 are reused"); + assert_ne!(after[2], ids[2], "row 5 is in the last page, which is repacked"); let words = values(&written, &order.page(2), Words::Values, false); - assert_eq!(f32::from_bits(words[PAGE_ROWS as usize - 1 - 5]), -1.0, "row 5 packed where it draws"); + assert_eq!(f32::from_bits(words[PAGE_ROWS as usize - 1 - 5]), -1.0, "row 5 is packed at its place"); } } diff --git a/crates/latkit/src/gpu/pipelines.rs b/crates/latkit/src/gpu/pipelines.rs index b181e50c..14d51b75 100644 --- a/crates/latkit/src/gpu/pipelines.rs +++ b/crates/latkit/src/gpu/pipelines.rs @@ -1,11 +1,14 @@ -//! Pipelines, kept by [`Variant`]. Everything compiles through one queue per GPU -//! ([`Pipelines::ask`]), the most urgent first: what frames draw with, then their text, then what -//! changes, lit rows, and input need, then what the GPU readies for views to come, once it would -//! otherwise sit idle. In a browser what frames draw with compiles at once, their text as the -//! browser's compile workers free up, and the rest a few at a time, each off the GPU's thread, side -//! by side with the rest; what uses it waits until it landed, so no frame waits on a compile. Making -//! it afterwards finds it compiled. Natively a pipeline is made in process where first used, which -//! costs as little. Every pipeline shares the view and mark group layouts; each +//! Pipelines, cached by [`Variant`]. +//! +//! Everything compiles through one queue per GPU ([`Pipelines::ask`]), most urgent first: the +//! pipelines frames draw with, then their text, then those that changes, lit rows, and input need, +//! then pipelines warmed for future views once the queue would otherwise sit idle. +//! +//! In a browser, draw pipelines start compiling at once, text as the browser's compile workers free +//! up, and the rest a few at a time, all in the background and in parallel. Anything that uses a +//! pipeline waits until its compile finishes, so no frame waits on a compile, and creating the +//! pipeline afterwards finds it compiled. Natively a pipeline is created in process when first +//! used, which is just as cheap. Every pipeline shares the view and mark group layouts; each //! shader's pass group comes from its WGSL. use std::collections::HashMap; @@ -21,8 +24,9 @@ use super::work::{Count, Counters}; use crate::Error; use crate::view::DEPTH; -/// How soon a pipeline asked for is needed, the soonest compiling first: what frames draw with, -/// their text, what changes, lit rows, and input need, and what the GPU readies for views to come. +/// How soon a requested pipeline is needed; sooner compiles first. `Draw`: pipelines frames draw +/// with. `Text`: their text. `Answer`: pipelines that changes, lit rows, and input need. `Warm`: +/// pipelines warmed for future views. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] pub(crate) enum Urgency { Draw, @@ -31,13 +35,14 @@ pub(crate) enum Urgency { Warm, } -/// How many pipelines a browser compiles side by side. Text asked while fewer compile takes a free -/// worker, where asked sooner it would hold back what frames draw with; the rest compile at most half -/// as many at once, so frames drawing meanwhile keep the CPU, the next asked as each lands. +/// How many pipelines a browser compiles in parallel. Text starts only while fewer than this are +/// compiling, or while no draw pipeline is, since starting it sooner would delay the draw +/// pipelines. The rest compile at most half as many at once, so frames drawn meanwhile keep the +/// CPU; each finished compile starts the next. const WORKERS: u64 = 8; -/// A check of a custom marker's shader, resolving to its error if it has one: sent between threads -/// natively, kept on the page's one thread in browsers. +/// A pending check of a custom marker's shader, resolving to its error if any. It is `Send` +/// natively, and stays on the page's single thread in browsers. #[cfg(not(target_arch = "wasm32"))] type Check = Pin> + Send>>; #[cfg(target_arch = "wasm32")] @@ -50,10 +55,10 @@ struct Custom { failed: bool, } -/// One pipeline: a shader's entry point made for one primitive's geometry (its `PRIMITIVE`), one kind -/// of path (its `PATH`), and what channels read (its `JOINED`, `TIMED`, and `VIEWED`), and for a render -/// shader, the format and samples per pixel it draws into. It holds a primitive and a path only where -/// its shader reads them, so no two of its pipelines compile the same. +/// One pipeline: a shader's entry point specialized for one primitive's geometry (`PRIMITIVE`), +/// one kind of path (`PATH`), and what channels read (`JOINED`, `TIMED`, and `VIEWED`). A render +/// variant also has the format and multisample count it draws into. It sets a primitive and a path +/// only when its shader reads them, so no two variants compile to the same pipeline. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub(crate) struct Variant { shader: Shader, @@ -64,9 +69,9 @@ pub(crate) struct Variant { target: Option<(wgpu::TextureFormat, u32)>, } -/// What a mark's channels read besides its own table's static fields: fields of other tables by row -/// id, fields sampled over an axis, and the view (selection, hover, the pointer, the zoom). Every -/// pipeline but the channels' reads all of them. +/// What a mark's channels read besides its own table's static fields: other tables' fields by row +/// id (`joined`), fields sampled over an axis (`timed`), and the view's selection, hover, pointer, +/// and zoom (`viewed`). Every pipeline except the channels pipeline reads all of them. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub(crate) struct Reads { pub(crate) joined: bool, @@ -79,7 +84,7 @@ impl Reads { } impl Variant { - /// `shader` drawing into `format` with `samples` per pixel. + /// `shader` drawing into `format` with `samples` samples per pixel. pub(crate) fn draw(shader: Shader, format: wgpu::TextureFormat, samples: u32) -> Variant { Variant { shader, @@ -96,18 +101,19 @@ impl Variant { Variant { shader, entry, primitive: 0, path: PATH_ENDS, reads: Reads::ALL, target: None } } - /// The probe program made for marks of `primitive`, which answers every question a step at a time. + /// The probe program for marks of `primitive`. It answers every query in steps, each dispatch's + /// uniform selecting the step. pub(crate) fn probe(shader: Shader, primitive: u32) -> Variant { Variant::compute(shader, "answer").primitive(primitive) } - /// The channels of marks reading what `reads` says. + /// The channels pipeline for marks whose channels read `reads`. pub(crate) fn channels(reads: Reads) -> Variant { Variant { reads, ..Variant::compute(Shader::Evaluate, "evaluate") } } - /// The variant made for marks of `primitive`, a `POINTS`, `STROKES`, or `TRIANGLES` code, where its - /// shader holds one primitive's geometry. + /// This variant specialized for marks of `primitive` (a `POINTS`, `STROKES`, or `TRIANGLES` + /// code), if its shader reads `PRIMITIVE`. #[must_use] pub(crate) fn primitive(mut self, primitive: u32) -> Variant { if self.reads_constant("PRIMITIVE") { @@ -116,7 +122,7 @@ impl Variant { self } - /// The variant whose strokes read `path`, one of the `PATH_` codes, where its shader reads one. + /// This variant with strokes reading `path` (a `PATH_` code), if its shader reads `PATH`. #[must_use] pub(crate) fn path(mut self, path: u32) -> Variant { if self.reads_constant("PATH") { @@ -129,7 +135,7 @@ impl Variant { self.shader.source().overrides.contains(&name) } - /// The pipeline constants its shader declares, set. + /// Values for the pipeline constants its shader declares. fn constants(&self) -> Vec<(&'static str, f64)> { let value = |name: &str| match name { "PRIMITIVE" => f64::from(self.primitive), @@ -143,8 +149,8 @@ impl Variant { } } -/// A pipeline asked for ahead: a variant of one of latkit's shaders, or of a custom marker's, whose -/// WGSL follows its shader's. +/// A pipeline requested ahead of use: a variant of a latkit shader, or of a custom marker, whose +/// WGSL is appended to its shader's. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub(crate) enum Asked { Shader(Variant), @@ -152,7 +158,7 @@ pub(crate) enum Asked { } impl Asked { - /// Bytes of WGSL it compiles from: the more, the longer it takes. + /// Bytes of WGSL it compiles from. More bytes take longer to compile. pub(crate) fn size(&self) -> usize { match self { Asked::Shader(variant) => variant.shader.source().size(), @@ -161,8 +167,8 @@ impl Asked { } } -/// A pipeline asked for ahead: whether it compiled then, off the GPU's thread, whether that landed, -/// and what waits for it to. +/// The compile state of a pipeline requested ahead: whether it compiled in the background, whether +/// that compile finished (`landed`), and the wakers waiting for it. #[derive(Default)] struct Ahead { compiled: AtomicBool, @@ -175,12 +181,12 @@ impl Ahead { self.landed.load(Ordering::Acquire) } - /// Waits for it: `waker` wakes as it lands. + /// Registers `waker` to wake when the compile finishes. fn wait(&self, waker: &Waker) { self.waiting.lock().unwrap_or_else(PoisonError::into_inner).push(waker.clone()); } - /// Lands it, counted by `landed`, and wakes what waits. + /// Marks the compile finished, increments `landed`, and wakes the waiters. fn land(&self, landed: &AtomicU64) { self.landed.store(true, Ordering::Release); landed.fetch_add(1, Ordering::AcqRel); @@ -193,7 +199,7 @@ impl Ahead { pub(crate) struct Pipelines { pub(crate) view_layout: wgpu::BindGroupLayout, pub(crate) mark_layout: wgpu::BindGroupLayout, - /// What a shader reading neither shared group binds in their place. + /// The bind group bound in place of the shared groups for a shader that reads neither. pub(crate) empty: wgpu::BindGroup, empty_layout: wgpu::BindGroupLayout, pass_layouts: HashMap, wgpu::BindGroupLayout>, @@ -201,27 +207,28 @@ pub(crate) struct Pipelines { render: HashMap, compute: HashMap, custom: HashMap<(Arc, Variant), Custom>, - /// What views, shots, and the GPU asked for ahead, each with its compile. + /// Pipelines views, shots, and the GPU requested ahead, each with its compile state. asked: HashMap>, - /// What was asked for and waits its turn: soonest first, the largest first within an urgency, - /// since a browser compiles in the order asked. + /// Requested pipelines waiting to start compiling: most urgent first, and largest first within + /// an urgency, since a browser compiles in request order. queue: Vec<(Urgency, Asked)>, - /// How many compiles asked for ahead landed, and in a browser how many are under way. + /// How many requested compiles have finished. In a browser, `compiling` counts those in + /// progress. landed: Arc, #[cfg(all(feature = "web", target_arch = "wasm32"))] compiling: Arc, - /// Of those, what frames draw with. + /// Of the compiles in progress, how many are draw pipelines. #[cfg(all(feature = "web", target_arch = "wasm32"))] drawing: Arc, - /// Whether frames were asked to draw with anything yet, and whether what the GPU warms starts. + /// Whether any draw pipeline has started compiling, and whether warm pipelines may start. drew: bool, warming: bool, - /// Whether what views to come draw with was queued. + /// Whether the pipelines for future views were queued. #[cfg(all(feature = "web", target_arch = "wasm32"))] warmed: bool, - /// What counts the pipelines compiled. + /// Counts the pipelines compiled. counters: Arc, - /// The GPU these belong to, for a compile landing to start the next. + /// The GPU these belong to, so a finished compile can start the next. #[cfg_attr( not(all(target_arch = "wasm32", feature = "web")), expect(dead_code, reason = "only a browser compiles ahead") @@ -229,23 +236,23 @@ pub(crate) struct Pipelines { gpu: Weak, } -/// The probe a frame of `samples` per pixel reads its depth with. +/// The probe shader that reads depth in frames of `samples` samples per pixel. pub(crate) fn probe(samples: u32) -> Shader { if samples > 1 { Shader::ProbeMultisampled } else { Shader::Probe } } -/// How a render shader's vertices assemble. +/// The primitive topology of a render shader. fn topology(shader: Shader) -> wgpu::PrimitiveTopology { - // Custom markers draw as points do. + // Custom markers draw like points, as triangle strips. match shader { Shader::Backdrop | Shader::Triangles => wgpu::PrimitiveTopology::TriangleList, _ => wgpu::PrimitiveTopology::TriangleStrip, } } -/// How a render shader meets depth, which is reversed: the backdrop writes the globe's, triangles -/// write theirs, the overlay and labels ignore it, and every other mark draws where nothing nearer -/// was drawn. +/// A render shader's depth state, with reversed depth. The backdrop writes the globe's depth and +/// triangles write theirs. The overlay and glyphs ignore depth, and every other mark draws only +/// where nothing nearer was drawn. fn depth(shader: Shader) -> wgpu::DepthStencilState { let compare = match shader { Shader::Backdrop | Shader::Overlay | Shader::Glyphs => wgpu::CompareFunction::Always, @@ -299,18 +306,18 @@ impl Pipelines { } } - /// Asks for each of `needs` at `urgency`, `waker` waking as each lands: what frames draw with - /// compiles at once, their text as workers free up, the rest in turn, half the `WORKERS` at a time; - /// asking again for one still waiting its turn moves it up. Its shader module is made now: a - /// browser reads a module's WGSL on its GPU thread as it is made, so what compiles later is read - /// before anything draws. + /// Requests each of `needs` at `urgency`, waking `waker` as each compile finishes. Draw + /// pipelines start at once, text as workers free up, and the rest in turn, at most half of + /// `WORKERS` at a time. Requesting a queued pipeline again at a higher urgency moves it up. + /// Each shader module is created now, because a browser parses a module's WGSL on its GPU + /// thread at creation; this way modules that compile later are parsed before anything draws. pub(crate) fn ask(&mut self, device: &wgpu::Device, needs: &[Asked], urgency: Urgency, waker: &Waker) { self.queue(device, needs, urgency, waker); self.pump(device); } - /// Queues each of `needs` at `urgency` as [`ask`](Pipelines::ask) does, starting none: the next - /// ask, landing, or [`pump`](Pipelines::pump) does. + /// Queues each of `needs` at `urgency` like [`ask`](Pipelines::ask), without starting any. The + /// next request, finished compile, or [`pump`](Pipelines::pump) starts them. pub(crate) fn queue(&mut self, device: &wgpu::Device, needs: &[Asked], urgency: Urgency, waker: &Waker) { for need in needs { if let Some(ahead) = self.asked.get(need) { @@ -330,16 +337,16 @@ impl Pipelines { } self.queue.push((urgency, need.clone())); } - // The largest first, so the last lands soonest; what the GPU warms in the order listed. + // Largest first, so the last one finishes soonest. Warm pipelines keep their listed order. crate::order::by_key(&mut self.queue, |(urgency, need)| { let size = if *urgency == Urgency::Warm { 0 } else { !(need.size().min(u32::MAX as usize) as u32) }; crate::order::pair(*urgency as u32, size) }); } - /// Queues what `needs` lists, what views to come draw with, once per GPU: it compiles once the - /// first scene's frames have all they draw with, so it never holds them back. `waker` wakes as - /// each lands, so what waits for the GPU's work to land sees it. + /// Queues `needs`, the pipelines future views will draw with, once per GPU. They compile after + /// the first scene's draw pipelines finish, so they never delay them. `waker` wakes as each + /// finishes, so anything waiting on the GPU's compiles sees it. #[cfg(all(feature = "web", target_arch = "wasm32"))] pub(crate) fn warm(&mut self, device: &wgpu::Device, waker: &Waker, needs: impl FnOnce() -> Vec) { if !std::mem::replace(&mut self.warmed, true) { @@ -347,10 +354,10 @@ impl Pipelines { } } - /// Starts what waits its turn, in order: all that frames draw with; their text while fewer than - /// `WORKERS` compile, or none of that does; the rest while fewer than half as many compile. + /// Starts queued compiles in order: every draw pipeline; text while fewer than `WORKERS` are + /// compiling or no draw pipeline is; the rest while fewer than half of `WORKERS` are compiling. pub(crate) fn pump(&mut self, device: &wgpu::Device) { - // What the GPU warms waits for the first scene's frames to have all they draw with. + // Warm pipelines wait until the first scene's draw pipelines have finished. self.warming |= self.drew && self.drawing() == 0; while let Some(&(urgency, _)) = self.queue.first() && match urgency { @@ -369,7 +376,7 @@ impl Pipelines { } } - /// Compiles under way, off the GPU's thread. + /// Compiles in progress in the background. #[cfg_attr( not(all(target_arch = "wasm32", feature = "web")), expect(clippy::unused_self, reason = "only a browser's device compiles ahead") @@ -381,7 +388,7 @@ impl Pipelines { 0 } - /// Compiles under way of what frames draw with. + /// Draw pipeline compiles in progress. #[cfg_attr( not(all(target_arch = "wasm32", feature = "web")), expect(clippy::unused_self, reason = "only a browser's device compiles ahead") @@ -393,25 +400,27 @@ impl Pipelines { 0 } - /// Whether every one of `needs`, asked for, has landed: what uses them can be made without a - /// compile. + /// Whether every one of `needs` was requested and has finished compiling, so pipelines using + /// them can be created without a compile. pub(crate) fn ready(&self, needs: &[Asked]) -> bool { needs.iter().all(|need| self.asked.get(need).is_some_and(|ahead| ahead.landed())) } - /// How many compiles asked for ahead have landed so far: what waits on one sees it change. + /// How many requested compiles have finished so far. Anything waiting on a compile sees this + /// change. pub(crate) fn landed(&self) -> u64 { self.landed.load(Ordering::Acquire) } - /// Compiles asked for ahead still under way or waiting their turn. + /// Requested compiles still in progress or queued. #[cfg(all(feature = "web", target_arch = "wasm32"))] pub(crate) fn compiling(&self) -> u64 { self.in_flight() + self.queue.len() as u64 } - /// Starts compiling `need` off the GPU's thread, where a browser can, counted among what frames - /// draw with when it `draws`: whether it did. Its landing starts the next waiting its turn. + /// Starts compiling `need` in the background where a browser supports it, counted as a draw + /// pipeline when `draws`. Returns whether it started. When it finishes, it starts the next + /// queued compile. #[cfg_attr( not(all(target_arch = "wasm32", feature = "web")), expect(clippy::unused_self, reason = "only a browser's device compiles ahead") @@ -430,13 +439,14 @@ impl Pipelines { drawing.fetch_add(1, Ordering::AcqRel); } wasm_bindgen_futures::spawn_local(async move { - // A descriptor the browser refuses lands too: making it then tells why, as it did. + // A descriptor the browser rejects also finishes; creating the pipeline later + // reports why, as it would without compiling ahead. let _ = wasm_bindgen_futures::JsFuture::from(promise).await; compiling.fetch_sub(1, Ordering::AcqRel); if let Some(drawing) = drawing { drawing.fetch_sub(1, Ordering::AcqRel); } - // Room for the next before what waits wakes, so it finds that under way. + // Start the next compile before waking the waiters, so they find it in progress. if let Some(gpu) = gpu.upgrade() && let Some(mut inner) = gpu.try_lock() { @@ -453,8 +463,9 @@ impl Pipelines { } } - /// Makes `need` through wgpu, neither kept nor counted: what compiling it ahead captures. A - /// custom marker's WGSL fails in its own scope, so only making it for real tells why. + /// Creates `need` through wgpu without caching or counting it, for compiling ahead to capture. + /// A custom marker's WGSL errors are caught in a scope of their own here, so only creating it + /// for real reports why it fails. #[cfg(all(target_arch = "wasm32", feature = "web"))] fn create(&mut self, device: &wgpu::Device, need: &Asked) { match need { @@ -472,8 +483,8 @@ impl Pipelines { } } - /// Counts a pipeline made for `need`: compiled now unless it compiled when asked for, and late - /// unless it was asked for at all. + /// Counts a pipeline created for `need`: as compiled now unless it compiled ahead, and as late + /// if it was never requested. fn count(&self, need: &Asked) { let ahead = self.asked.get(need); if !ahead.is_some_and(|ahead| ahead.compiled.load(Ordering::Acquire)) { @@ -509,8 +520,8 @@ impl Pipelines { .clone() } - /// The layout of `shader`'s pipelines: the view and mark groups, empty where it reads neither, then - /// its pass group. + /// The layout of `shader`'s pipelines: the view and mark groups (empty if it reads neither), + /// then its pass group. fn pipeline_layout(&mut self, device: &wgpu::Device, shader: Shader) -> wgpu::PipelineLayout { let pass = (!shader.source().pass.is_empty()).then(|| self.pass_layout(device, shader)); let mut groups = if shader.source().shared { @@ -552,7 +563,7 @@ impl Pipelines { Some(pipeline) } - /// Errors custom markers' shaders turned out to have since last asked. + /// Errors found in custom markers' shaders since the last call. pub(crate) fn custom_errors(&mut self) -> Vec { let mut context = Context::from_waker(Waker::noop()); let mut errors = Vec::new(); @@ -621,7 +632,7 @@ impl Pipelines { compilation_options: options(), targets: &[Some(wgpu::ColorTargetState { format, - // Premultiplied over what is drawn. + // Blend premultiplied colors over what is already drawn. blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), write_mask: wgpu::ColorWrites::ALL, })], @@ -667,7 +678,7 @@ impl Pipelines { } } -/// The module of a custom marker of `source` drawn with `shader`: its shader's WGSL, then the marker's. +/// The shader module for a custom marker: `shader`'s WGSL followed by the marker's `source`. fn custom_module(device: &wgpu::Device, source: &str, shader: Shader) -> wgpu::ShaderModule { device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("latkit custom marker"), @@ -675,10 +686,10 @@ fn custom_module(device: &wgpu::Device, source: &str, shader: Shader) -> wgpu::S }) } -/// Compiling ahead in a browser. A browser compiles a pipeline made through `createRenderPipeline` -/// on the GPU's thread, holding back every frame and readback behind it, but one made through its -/// async twin on worker threads, side by side with others; Chrome keeps what it compiled by -/// descriptor, so making the same descriptor later finds it compiled. +/// Compiling ahead in a browser. A browser compiles a pipeline created with `createRenderPipeline` +/// on the GPU's thread, delaying every frame and readback behind it. One created with the async +/// variant compiles on worker threads, in parallel with others. Chrome caches compiled pipelines by +/// descriptor, so creating the same descriptor later finds it compiled. #[cfg(all(target_arch = "wasm32", feature = "web"))] mod compiled { use std::cell::RefCell; @@ -687,16 +698,16 @@ mod compiled { use wasm_bindgen::closure::Closure; use wasm_bindgen::{JsCast, JsValue}; - /// The device's pipeline makers, each with its async twin. + /// The device's pipeline creation methods, each with its async variant. const MAKERS: [(&str, &str); 2] = [ ("createRenderPipeline", "createRenderPipelineAsync"), ("createComputePipeline", "createComputePipelineAsync"), ]; - /// The browser compiling, off its GPU's thread, the pipeline `make` makes through wgpu: while it - /// runs, the device's makers are their async twins, handed the very descriptor wgpu builds, so - /// what compiles is what wgpu makes later, by construction. `None` where the device is no - /// browser's, or `make` made nothing. + /// Has the browser compile, in the background, the pipeline `make` creates through wgpu. While + /// `make` runs, the device's creation methods are swapped for their async variants and receive + /// the exact descriptor wgpu builds, so what compiles is exactly what wgpu creates later. + /// `None` if the device is not a browser's or `make` created nothing. pub(super) fn capture(device: &wgpu::Device, make: impl FnOnce()) -> Option { let raw: &JsValue = device.as_webgpu()?.as_ref(); let twins: Vec = MAKERS diff --git a/crates/latkit/src/gpu/readback.rs b/crates/latkit/src/gpu/readback.rs index 1b353e96..9b7950b2 100644 --- a/crates/latkit/src/gpu/readback.rs +++ b/crates/latkit/src/gpu/readback.rs @@ -1,6 +1,6 @@ -//! Bytes read back from the GPU: copied in a frame's encoder and mapped when the caller submits it, -//! so reading never stalls a frame and never submits on its own; once mapped, it wakes what waits on -//! it. Map buffers come from a pool and go back to it once read, so hovering allocates nothing. +//! Readbacks from the GPU. Bytes are copied in a frame's encoder and mapped when the caller submits +//! it, so reading never stalls a frame or submits on its own. Once mapped, a readback wakes its +//! waker. Map buffers come from a pool and return to it once read, so hovering allocates nothing. use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex, PoisonError}; @@ -9,8 +9,9 @@ use std::task::Waker; use super::work::{Count, Counters}; use crate::{Error, Result}; -/// Map buffers no readback holds, by size, what counts the readbacks made from them, and how many -/// are still mapping. Every pool says whether it counts, so none stops counting by default. +/// A pool of free map buffers, with the counters for readbacks made from it and how many are still +/// mapping. Each pool is created explicitly as counted or uncounted, so none skips counting by +/// default. #[derive(Clone)] pub(crate) struct Readbacks { free: Arc>>, @@ -19,17 +20,17 @@ pub(crate) struct Readbacks { } impl Readbacks { - /// A pool whose readbacks `counters` counts: the `Gpu`'s. + /// A pool whose readbacks `counters` counts, as the `Gpu`'s pool does. pub(crate) fn counted(counters: Arc) -> Readbacks { Readbacks { free: Arc::default(), counters: Some(counters), mapping: Arc::default() } } - /// A pool whose readbacks count as no work: the frame timer's, which only some devices have. + /// A pool whose readbacks are not counted: the frame timer's, which only some devices support. pub(crate) fn uncounted() -> Readbacks { Readbacks { free: Arc::default(), counters: None, mapping: Arc::default() } } - /// Readbacks of this pool the GPU has yet to map. + /// Readbacks from this pool the GPU has not mapped yet. #[cfg(all(feature = "web", target_arch = "wasm32"))] pub(crate) fn mapping(&self) -> u64 { self.mapping.load(Ordering::Acquire) @@ -53,7 +54,7 @@ impl Readbacks { } } -/// Where a readback's map stands. +/// The state of a readback's mapping. #[derive(Clone, Copy, PartialEq, Eq)] enum Mapping { Waiting, @@ -70,8 +71,8 @@ pub(crate) struct Readback { } impl Readback { - /// Copies `size` bytes of `source` from `offset`, to read once `encoder` is submitted; `waker` - /// wakes once they can be read. + /// Copies `size` bytes of `source` from `offset`, to read once `encoder` is submitted. `waker` + /// wakes when they can be read. pub(crate) fn new( pool: &Readbacks, device: &wgpu::Device, @@ -97,7 +98,8 @@ impl Readback { Readback { buffer: Some(buffer), size, pool: pool.clone(), mapping } } - /// The bytes once mapped, or `None` while the GPU has not finished or they were taken. + /// The bytes once mapped, or `None` while the GPU has not finished or after they were taken. + /// Errors with `DeviceLost` if mapping failed. pub(crate) fn take(&self) -> Option>> { let mut mapping = self.mapping.lock().unwrap_or_else(PoisonError::into_inner); let buffer = self.buffer.as_ref()?; @@ -120,7 +122,7 @@ impl Readback { impl Drop for Readback { fn drop(&mut self) { let Some(buffer) = self.buffer.take() else { return }; - // A buffer still mapping cannot go back yet; it is dropped instead. + // A buffer still mapping cannot return to the pool, so it is dropped instead. let mapping = *self.mapping.lock().unwrap_or_else(PoisonError::into_inner); if mapping == Mapping::Ready { buffer.unmap(); diff --git a/crates/latkit/src/gpu/shaders.rs b/crates/latkit/src/gpu/shaders.rs index cf275be8..f49d5859 100644 --- a/crates/latkit/src/gpu/shaders.rs +++ b/crates/latkit/src/gpu/shaders.rs @@ -3,7 +3,7 @@ #![allow(clippy::unreadable_literal)] -/// How a binding is read. +/// A binding's resource type. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) enum Kind { Uniform, @@ -27,7 +27,8 @@ const FRAGMENT: u32 = 2; const COMPUTE: u32 = 4; impl ShaderSource { - /// The shader's WGSL: its declarations, each stored once for every shader, joined in order. + /// The shader's WGSL: its declarations joined in order. Each declaration is stored once and + /// shared by every shader. pub(crate) fn wgsl(&self) -> String { let mut out = String::new(); for (i, &piece) in self.pieces.iter().enumerate() { @@ -40,8 +41,8 @@ impl ShaderSource { out } - /// Bytes of its WGSL: what a browser compiles for each of its pipelines, and so about how long - /// one takes. + /// Bytes of its WGSL, which a browser compiles for each of its pipelines: a rough measure of + /// compile time. pub(crate) fn size(&self) -> usize { self.pieces .iter() @@ -53,11 +54,11 @@ impl ShaderSource { } } -/// Every distinct top-level declaration of every linked shader, each once; [`PIECES`] says where each -/// lies. +/// Every distinct top-level declaration of every linked shader, each stored once. [`PIECES`] gives +/// each one's byte range. static WGSL: &str = include_str!(concat!(env!("OUT_DIR"), "/shaders.wgsl")); -/// One linked shader: its declarations, entry points, and the bindings of its pass group. +/// One linked shader: its declarations, entry points included, and the bindings of its pass group. pub(crate) struct ShaderSource { pub(crate) label: &'static str, /// Its declarations, as indices into [`PIECES`], in order. @@ -65,7 +66,8 @@ pub(crate) struct ShaderSource { pub(crate) pass: &'static [Binding], /// The pipeline constants it declares. pub(crate) overrides: &'static [&'static str], - /// Whether it reads the view and mark groups; one that does not binds its pass group alone. + /// Whether it reads the view and mark groups. A shader that does not gets empty groups in their + /// place. pub(crate) shared: bool, } @@ -114,6 +116,6 @@ impl Binding { include!(concat!(env!("OUT_DIR"), "/shaders.rs")); -// One page of rows: the data's, a table's order's, and a mark's, which it evaluates at a time. +// The data, a table's draw order, and a mark's evaluation all use the same page of rows. const _: () = assert!(1 << PAGE_BITS == PAGE_ROWS && PAGE_MASK + 1 == PAGE_ROWS && PAGE_ROWS as usize == crate::data::PAGE_ROWS); diff --git a/crates/latkit/src/gpu/work.rs b/crates/latkit/src/gpu/work.rs index dbc334d0..cbab5e5c 100644 --- a/crates/latkit/src/gpu/work.rs +++ b/crates/latkit/src/gpu/work.rs @@ -1,17 +1,21 @@ -//! What a `Gpu` did, counted exactly. Unlike time, it repeats from run to run and machine to machine -//! wherever the budget, the device's limits, and the frames drawn do, so the benchmark gates it and -//! tests assert it. Every write to the GPU goes through [`Queue`], which counts it; clippy refuses the -//! raw queue's writes anywhere else. Whatever else counts holds the counters itself: the pipelines, -//! the readback pool, the atlas, and each frame's jobs and evaluations through the `Gpu`. +//! Exact counts of the work a `Gpu` does. Unlike timings, the counts repeat across runs and +//! machines for the same budget, device limits, and frames drawn, so the benchmark gates on them +//! and tests assert them. +//! +//! Every write to the GPU goes through [`Queue`], which counts it; clippy forbids the raw queue's +//! writes anywhere else. Other counted parts hold the counters themselves: the pipelines, the +//! readback pool, the atlas, and each frame's jobs and evaluations through the `Gpu`. use std::ops::{Deref, Sub}; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering::Relaxed}; -/// [`Work`], the [`Count`] naming each of its fields, and the [`Counters`] keeping them, from one list. +/// Defines [`Work`], a [`Count`] variant for each of its fields, and the [`Counters`] that hold +/// them, from one list. macro_rules! work { ($($(#[$doc:meta])* $field:ident: $count:ident,)*) => { - /// What a [`Gpu`](crate::Gpu) did since it was made; subtract two for what happened between. + /// Counts of the work a [`Gpu`](crate::Gpu) has done since it was created. Subtract two + /// snapshots to get the work between them. #[derive(Clone, Copy, Default, PartialEq, Eq, Debug, serde::Serialize, schemars::JsonSchema)] pub struct Work { $($(#[$doc])* pub $field: u64,)* @@ -23,7 +27,7 @@ macro_rules! work { $($count,)* } - /// The counts, shared by every view of a `Gpu`, from any thread. + /// The counters, shared by every view of a `Gpu` and updated from any thread. #[derive(Default)] pub(crate) struct Counters { $($field: AtomicU64,)* @@ -55,40 +59,41 @@ macro_rules! work { work! { /// Bytes of data pages uploaded. uploaded: Uploaded, - /// Data pages, or pieces of them, uploaded: what a change uploads counts its pages here, and its - /// bytes in `uploaded`. + /// Data pages, or pieces of pages, uploaded. A change's uploads count pages here and bytes in + /// `uploaded`. uploads: Uploads, - /// Bytes written besides pages: uniforms, programs, label runs, the selection, textures. + /// Bytes written other than pages: uniforms, programs, label runs, the selection, and textures. written: Written, /// Rows whose channels the GPU evaluated. evaluated: Evaluated, - /// Rows of edges and faces whose ends the GPU found where their vertices are held: rows that - /// changed, and every row of a page of them naming a vertex now held elsewhere. + /// Edge and face rows whose endpoints the GPU resolved to their vertices' resident rows: rows + /// that changed, plus every row of a page that references a vertex now resident elsewhere. resolved: Resolved, - /// Bytes of pages of words per row the GPU cleared: those read before anything writes them, and - /// the normals faces sum afresh. + /// Bytes of per-row word pages the GPU cleared: pages read before anything writes them, and the + /// normals that faces sum from scratch. cleared: Cleared, - /// Rows packed into pages of the order a table drawn in part draws in: each page once, the first - /// time a view draws it, and again only where a write changed its rows. + /// Rows packed into pages of a paged table's draw order. Each page is packed once, the first + /// time a view draws it, and again only when a write changes its rows. packed: Packed, - /// Reads back from the GPU: probes, queries, fits, label placements, segment counts, pictures; - /// not the frame timer's, which only some devices have. + /// Readbacks from the GPU: probes, queries, fits, label placements, segment counts, and + /// pictures. Frame timer readbacks, which only some devices support, are not counted. readbacks: Readbacks, - /// Jobs handed out: layouts, routes, lanes, triangulations, pyramids, tiles; to the host's - /// workers, or done at once without a host. + /// Jobs started: layouts, routes, lanes, triangulations, pyramids, and tiles. They run on the + /// host's workers, or immediately without a host. jobs: Jobs, - /// Label texts the host was asked to set. + /// Label texts the host was asked to rasterize. texts: Texts, - /// Pipelines compiled: in a browser, each as a view or a shot asks for it ahead, off the GPU's - /// thread; natively, where it is first made. + /// Pipelines compiled. In a browser, each compiles in the background when a view or shot + /// requests it ahead of use. Natively, each compiles when first created. pipelines: Pipelines, - /// Pipelines made without being asked for ahead, each compiled where it is first used: in a - /// browser on the GPU's thread, holding frames back. None, unless a scene uses what it did not - /// ask for. + /// Pipelines not requested ahead of use, compiled when first used. In a browser they compile + /// on the GPU's thread and delay frames. Zero unless a scene uses a pipeline it did not + /// request. late: Late, } -/// The device's queue, counting what is written through it; everything else it does as the queue. +/// The device's queue, which counts what is written through it. Everything else passes through to +/// the queue. pub(crate) struct Queue { raw: wgpu::Queue, counters: Arc, @@ -106,8 +111,9 @@ impl Queue { self.raw.write_buffer(buffer, offset, bytes); } - /// Writes in command order, so a reused scratch buffer keeps earlier passes' inputs intact, and - /// what a copy recorded before puts in place is written over after it. + /// Writes `bytes` into `buffer` at `offset` in command order, through a copy recorded in + /// `encoder`, counted as written. So a reused scratch buffer keeps earlier passes' inputs + /// intact, and the write takes effect after any copy recorded before it. pub(crate) fn write_buffer_in( &self, device: &wgpu::Device, @@ -138,8 +144,8 @@ impl Queue { encoder.clear_buffer(buffer, offset, Some(size)); } - /// Writes the words of `pages` data pages, or pieces of them, side by side into `buffer` at - /// `offset`, counted as uploaded. + /// Writes `bytes`, the words of `pages` adjacent data pages or pieces of pages, into `buffer` + /// at `offset`, counted as uploaded. #[expect(clippy::disallowed_methods, reason = "the counted upload")] pub(crate) fn upload(&self, buffer: &wgpu::Buffer, offset: u64, bytes: &[u8], pages: u64) { self.counters.add(Count::Uploaded, bytes.len() as u64); diff --git a/crates/latkit/src/host/browser.rs b/crates/latkit/src/host/browser.rs index af5ded97..cc39d284 100644 --- a/crates/latkit/src/host/browser.rs +++ b/crates/latkit/src/host/browser.rs @@ -9,9 +9,10 @@ use super::{Host, Job, Raster, Request, TextRun}; use crate::Encoding; use crate::{Error, Pending}; -/// The browser: text set with a 2D canvas in the page's fonts, jobs done by the page's workers once -/// [`set_jobs`](Browser::set_jobs) names them, else at once, bytes fetched with `fetch`, and video -/// encoded by WebCodecs once [`set_encoder`](Browser::set_encoder) names how. +/// The browser host. Text is rasterized with a 2D canvas in the page's fonts, and bytes are fetched +/// with `fetch`. Jobs run on the page's workers once [`set_jobs`](Browser::set_jobs) provides them, +/// else immediately. Video is encoded with WebCodecs once [`set_encoder`](Browser::set_encoder) +/// provides an encoder. #[derive(Clone, Copy, Debug, Default)] pub struct Browser; @@ -22,8 +23,8 @@ thread_local! { static ENCODERS: RefCell> = const { RefCell::new(None) }; } -/// Jobs handed to the page's workers at once: one fewer than its cores, at most four. The page keeps -/// a worker for each and queues nothing. +/// How many jobs the page's workers run at once: one fewer than the cores, from 1 to 4. The page +/// keeps a worker for each, so it never queues. fn workers() -> usize { let cores = web_sys::window() .map(|window| window.navigator().hardware_concurrency()) @@ -37,7 +38,19 @@ fn workers() -> usize { cores.saturating_sub(1).clamp(1, 4) } -/// Hands waiting jobs to the page's workers while they have room, skipping those nothing waits for. +/// The latkit error for a value the page's script threw or rejected with. An error whose `code` is +/// `unsupported` or `timeout` keeps it; anything else is an [`Error::Io`] saying `what` failed. +fn failure(error: &JsValue, what: &str) -> Error { + let field = |key: &str| js_sys::Reflect::get(error, &key.into()).ok().and_then(|v| v.as_string()); + let message = field("message").or_else(|| error.as_string()).unwrap_or_default(); + match field("code").as_deref() { + Some("unsupported") => Error::Unsupported(message.into()), + Some("timeout") => Error::Timeout, + _ => Error::Io(format!("{what}: {message}").into()), + } +} + +/// Hands waiting jobs to the page's workers while any is free, skipping cancelled jobs. fn pump(run: &js_sys::Function) { loop { let next = QUEUE.with(|queue| queue.borrow_mut().next(workers())); @@ -50,8 +63,8 @@ fn pump(run: &js_sys::Function) { Ok(promise) => wasm_bindgen_futures::JsFuture::from(promise) .await .map(|bytes| js_sys::Uint8Array::new(&bytes).to_vec()) - .map_err(|error| Error::Unsupported(format!("a job failed: {error:?}").into())), - Err(error) => Err(Error::Unsupported(format!("jobs: {error:?}").into())), + .map_err(|error| failure(&error, "a job failed")), + Err(error) => Err(failure(&error, "a job failed")), }; resolver.resolve(answer); QUEUE.with(|queue| queue.borrow_mut().done()); @@ -61,26 +74,27 @@ fn pump(run: &js_sys::Function) { } impl Browser { - /// Has views' jobs done by `run`, which takes a job's bytes as a `Uint8Array` and returns a - /// promise of its answer's: workers, each holding the page's compiled module. Jobs queue here and - /// `run` gets one only when a worker is free for it. + /// Runs views' jobs with `run`, which takes a job's bytes as a `Uint8Array` and returns a + /// promise of its result's bytes. `run` should pass jobs to workers that each hold the page's + /// compiled module. Jobs queue here, and `run` gets one only when a worker is free. pub fn set_jobs(run: js_sys::Function) { JOBS.with(|jobs| *jobs.borrow_mut() = Some(run)); } - /// Has videos encoded by what `create` makes of `(width, height, fps, bitrate)`: an object whose - /// `close()` releases resources; `encode(nv12, time, key)` and `finish()` return promises of the units finished since, each - /// `[time, key, bytes]`, in order. The npm package makes one with WebCodecs. + /// Encodes videos with encoders made by `create(width, height, fps, bitrate)`. An encoder's + /// `encode(nv12, time, key)` and `finish()` return promises of the units finished since the + /// last call, in order, each `[time, key, bytes]`. Its `close()` releases its resources. The + /// npm package provides one built on WebCodecs. pub fn set_encoder(create: js_sys::Function) { ENCODERS.with(|encoders| *encoders.borrow_mut() = Some(create)); } } -/// An encoder the page made: its `encode` and `finish` answer promises of units. +/// An encoder made by the page. Its `encode` and `finish` return promises of units. struct Script(JsValue); impl Script { - /// Calls method `name` with `args`, resolving to the units its promise answers. + /// Calls method `name` with `args`, resolving to the units its promise returns. fn call(&self, name: &str, args: &js_sys::Array) -> Pending> { let promise = js_sys::Reflect::get(&self.0, &name.into()) .and_then(JsCast::dyn_into::) @@ -88,31 +102,27 @@ impl Script { .map(|answer| js_sys::Promise::resolve(&answer)); let promise = match promise { Ok(promise) => promise, - Err(error) => return Pending::ready(Err(Error::Unsupported(format!("the encoder: {error:?}").into()))), + Err(error) => return Pending::ready(Err(failure(&error, "the encoder failed"))), }; let (pending, resolver) = Pending::new(); wasm_bindgen_futures::spawn_local(async move { let answer = wasm_bindgen_futures::JsFuture::from(promise).await; - resolver.resolve( - answer - .map_err(|error| Error::Unsupported(format!("the encoder failed: {error:?}").into())) - .and_then(units), - ); + resolver.resolve(answer.map_err(|error| failure(&error, "the encoder failed")).and_then(units)); }); pending } } -/// The units an encoder answered: `[time, key, bytes]` each. +/// Reads the units an encoder returned, each `[time, key, bytes]`. fn units(answer: JsValue) -> crate::Result> { let units: js_sys::Array = - answer.dyn_into().map_err(|_| Error::Unsupported("the encoder answered no units".into()))?; + answer.dyn_into().map_err(|_| Error::Io("the encoder did not return an array of units".into()))?; units .iter() .map(|unit| { let unit: js_sys::Array = - unit.dyn_into().map_err(|_| Error::Unsupported("a unit is [time, key, bytes]".into()))?; - let time = unit.get(0).as_f64().ok_or_else(|| Error::Unsupported("a unit's time".into()))?; + unit.dyn_into().map_err(|_| Error::Io("an encoder unit is not an array [time, key, bytes]".into()))?; + let time = unit.get(0).as_f64().ok_or_else(|| Error::Io("an encoder unit has no numeric time".into()))?; let bytes = js_sys::Uint8Array::new(&unit.get(2)); Ok(Unit { time: time as u64, key: unit.get(1).is_truthy(), annex_b: bytes.to_vec() }) }) @@ -139,10 +149,11 @@ impl Encoder for Script { } } -/// Pixels of room around each run, so antialiasing and overhangs are kept. +/// Padding in pixels around each run, so antialiasing and overhangs are not clipped. const PAD: f64 = 2.0; -/// `text` broken at spaces into lines no wider than `max_width`, each with its width. +/// Breaks `text` at spaces into lines no wider than `max_width`, each with its width. A word wider +/// than `max_width` gets a line of its own. fn lines( context: &OffscreenCanvasRenderingContext2d, text: &str, @@ -166,13 +177,13 @@ fn lines( Some(lines) } -/// The page's 2D context for setting and measuring text, made on first use. +/// Calls `f` with the 2D context that rasterizes and measures text, created on first use. fn with_context(f: impl FnOnce(&OffscreenCanvas, &OffscreenCanvasRenderingContext2d) -> Option) -> Option { CANVAS.with(|cell| { let mut cell = cell.borrow_mut(); if cell.is_none() { let canvas = OffscreenCanvas::new(256, 64).ok()?; - // Each run is read back once it is set. + // Each run is read back after drawing, so ask for a canvas suited to readback. let options = js_sys::Object::new(); js_sys::Reflect::set(&options, &"willReadFrequently".into(), &true.into()).ok()?; let context = canvas.get_context_with_context_options("2d", &options).ok()??; @@ -184,7 +195,7 @@ fn with_context(f: impl FnOnce(&OffscreenCanvas, &OffscreenCanvasRenderingCon }) } -/// A run laid out: its lines, each with its width, and its ascent and line height. +/// A run's layout: its lines with their widths, its ascent, and its line height. struct Layout { lines: Vec<(String, f64)>, ascent: f64, @@ -192,7 +203,7 @@ struct Layout { } impl Layout { - /// `run` laid out in `context`, set to its font. + /// Lays out `run` in `context`, setting the context's font to the run's. fn of(context: &OffscreenCanvasRenderingContext2d, run: &TextRun) -> Option { context.set_font(&format!("{}px {}", run.size_px, run.font)); let lines = lines(context, &run.text, run.max_width.map(f64::from))?; @@ -201,7 +212,7 @@ impl Layout { Some(Layout { lines, ascent, line_height: ascent + metrics.font_bounding_box_descent() }) } - /// Its width and height, with room around it. + /// Its width and height, including padding. fn size(&self) -> [f64; 2] { let widest = self.lines.iter().map(|(_, w)| *w).fold(0.0, f64::max); [widest + 2.0 * PAD, self.line_height * self.lines.len() as f64 + 2.0 * PAD] @@ -239,7 +250,7 @@ impl Host for Browser { canvas.set_width(canvas.width().max(size[0].next_power_of_two())); canvas.set_height(canvas.height().max(size[1].next_power_of_two())); } - // Resizing resets the context. + // Resizing the canvas resets the context's state, so set it again. context.set_font(&format!("{}px {}", run.size_px, run.font)); context.set_text_baseline("alphabetic"); context.set_fill_style_str("#fff"); @@ -256,7 +267,7 @@ impl Host for Browser { fn encoder(&self, encoding: &Encoding) -> crate::Result> { let unsupported = - || Error::Unsupported("videos in a browser encode with WebCodecs, which the page did not give".into()); + || Error::Unsupported("browser videos need a WebCodecs encoder, and the page provided none".into()); let create = ENCODERS.with(|encoders| encoders.borrow().clone()).ok_or_else(unsupported)?; let [width, height] = encoding.size; let args = js_sys::Array::of4( @@ -265,15 +276,14 @@ impl Host for Browser { &encoding.fps.into(), &f64::from(encoding.bitrate).into(), ); - let encoder = create - .apply(&JsValue::NULL, &args) - .map_err(|error| Error::Unsupported(format!("no encoder for {width}×{height}: {error:?}").into()))?; + let encoder = + create.apply(&JsValue::NULL, &args).map_err(|error| failure(&error, "the encoder failed to start"))?; Ok(Box::new(Script(encoder))) } } -/// The bytes `request` asks for: none where there is nothing at its URL, and only the range asked for -/// where a server sends every byte. +/// Fetches the bytes of `request`. Returns no bytes on status 204 or 404. When a server ignores the +/// requested range and sends every byte, slices out the range. async fn fetched(request: &Request) -> crate::Result> { let failed = |what: &str| Error::Io(format!("{} could not be read: {what}", request.url).into()); let init = web_sys::RequestInit::new(); @@ -284,19 +294,19 @@ async fn fetched(request: &Request) -> crate::Result> { } init.set_headers(&headers); let fetch = web_sys::Request::new_with_str_and_init(&request.url, &init).map_err(|e| failed(&format!("{e:?}")))?; - // The page's fetch, or a worker's. + // Use the window's fetch, or the worker's. let global = js_sys::global(); let promise = match (global.dyn_ref::(), global.dyn_ref::()) { (Some(window), _) => window.fetch_with_request(&fetch), (None, Some(worker)) => worker.fetch_with_request(&fetch), - (None, None) => return Err(failed("no fetch here")), + (None, None) => return Err(failed("no fetch in this context")), }; let response = wasm_bindgen_futures::JsFuture::from(promise).await.map_err(|e| failed(&format!("{e:?}")))?; let response: web_sys::Response = response.dyn_into().map_err(|_| failed("not a response"))?; match response.status() { 200 | 206 => {} 204 | 404 => return Ok(Vec::new()), - status => return Err(failed(&format!("status {status}"))), + status => return Err(failed(&format!("HTTP status {status}"))), } let body = response.array_buffer().map_err(|e| failed(&format!("{e:?}")))?; let body = wasm_bindgen_futures::JsFuture::from(body).await.map_err(|e| failed(&format!("{e:?}")))?; diff --git a/crates/latkit/src/host/job.rs b/crates/latkit/src/host/job.rs index 301e5a59..a2dd2302 100644 --- a/crates/latkit/src/host/job.rs +++ b/crates/latkit/src/host/job.rs @@ -1,10 +1,11 @@ -//! Jobs: work too large for the frame (laying out in ranks or by stress, routing wires, cutting areas -//! into triangles, grouping a large table's rows into tiles, writing a picture as a file), handed to the -//! host to do off the frame. A job is bytes in and bytes out, so a thread, or a worker holding its own -//! copy of latkit, can do it with nothing shared. A view's job answers with a [`Patch`]: the rows it -//! worked out, by id, with their places, the paths their wires run along, or a vector tile's rows and -//! their shapes; or in its own words, lanes as numbers, shapes as the columns they fill, and pyramids -//! as their orders and tiles. An image job answers with the file. +//! Jobs: work too large for a frame, handed to the host to run in the background. Jobs lay out +//! graphs (layered or stress), route wires, triangulate areas, assign edge lanes, group a large +//! table's rows into tiles, decode vector tiles and GeoJSON, and encode images. +//! +//! A job is bytes in and bytes out, so a thread, or a worker with its own copy of latkit, can run +//! it with nothing shared. Layout, route, tile, and GeoJSON jobs return a [`Patch`] of rows by id. +//! Other jobs return their own format: lane words, shape columns, each table's pyramid (order and +//! tiles), or an encoded image file. use std::sync::Arc; @@ -21,14 +22,14 @@ use crate::layout::{layered::layered, levels::Graph, stress::stress}; use crate::tiles::TileKey; use crate::{Direction, Layered, Layout}; -/// Work latkit hands its [`Host`](super::Host) to do off the frame: bytes in, bytes out. +/// Work latkit hands its [`Host`](super::Host) to run in the background: bytes in, bytes out. /// -/// A host that does jobs elsewhere sends [`into_bytes`](Job::into_bytes) there, and answers with -/// what [`run`](Job::run) makes of [`from_bytes`](Job::from_bytes) of them. +/// A host that runs jobs elsewhere sends the job's [`into_bytes`](Job::into_bytes) there, rebuilds +/// it with [`from_bytes`](Job::from_bytes), and returns the result of [`run`](Job::run). #[derive(Clone, PartialEq, Eq)] pub struct Job(Vec); -/// What a job does, its first word. +/// A job's type, stored as its first word. #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Kind { Layered = 1, @@ -42,13 +43,14 @@ enum Kind { Image = 9, } -/// What a shapes job works out of each row's geometry. +/// The shapes computed from each row's geometry. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub(crate) enum Shaping { - /// An area's triangles, the point its label hangs from first; on the earth, unwrapped and split - /// along its grid. + /// An area's triangles, after its label anchor. With `earth`, rings are unwrapped and + /// triangles split along the earth grid. Triangles { earth: bool }, - /// An earth line's path: unwrapped, and bent with the globe; cheap enough that no job makes it. + /// An earth line's path, unwrapped and given extra points to follow the globe. Cheap enough + /// to compute on the frame, so it never needs a shapes job. Path, } @@ -70,7 +72,7 @@ impl Shaping { } } - /// The depth of the shapes made from geometry of `depth`. + /// The geometry depth of shapes made from geometry of `depth`. pub(crate) fn depth(self, depth: u8) -> u8 { match self { Shaping::Triangles { .. } => 1, @@ -78,8 +80,8 @@ impl Shaping { } } - /// Whether geometry of `depth` takes this shaping: triangles cut parts or polygons, and a path - /// bends anything but points. + /// Whether geometry of `depth` gets this shaping. Triangles apply to parts or polygons, and + /// paths to anything but points. pub(crate) fn takes(self, depth: u8) -> bool { match self { Shaping::Triangles { .. } => matches!(depth, 2 | 3), @@ -87,7 +89,7 @@ impl Shaping { } } - /// The derived field the shapes of geometry field `field` are written as. + /// Name of the derived field holding the shapes of geometry field `field`. pub(crate) fn field_of(self, field: &str) -> Name { match self { Shaping::Triangles { .. } => crate::scene::areas::triangles_of(field), @@ -95,8 +97,8 @@ impl Shaping { } } - /// The shapes of every row of geometry `values`, in order: a row missing shapes as nothing, and a - /// path needing no bending as itself. + /// Computes the shapes of every row of geometry `values`, in order. A missing row gets an empty + /// shape, and a path that needs no extra points is copied unchanged. pub(crate) fn shapes_of(self, values: &Values) -> Values { let depth = values.geometry().map_or(0, crate::data::Geometry::depth); let mut shapes = GeometryBuilder::new(self.depth(depth)); @@ -112,7 +114,7 @@ impl Shaping { Values::of_geometry(shapes.finish()) } - /// The shape of `value`, or `None` where it stays as it is: a path that needs no bending. + /// The shape of `value`, or `None` if it is unchanged (a path that needs no extra points). pub(crate) fn shape(self, value: &Value) -> Option { match (self, value) { (Shaping::Triangles { earth }, Value::Polygons(polygons)) => Some(Value::Path(triangles(polygons, earth))), @@ -141,12 +143,13 @@ impl Shaping { } } -/// A row a job answers for: its table and id. +/// A row in a job's result: its table and id. pub(crate) type Key = (Name, Id); -/// Vertices to place: each one's size, its center when it has one, its group and how far a group's -/// box reaches past its members (left, bottom, right, top), links between them, and the key of -/// each vertex the answer places; vertices without a key are only placed around. Plane units, y up. +/// Vertices to lay out: each one's size, pinned center if any, group, and how far a group's box +/// reaches past its members (left, bottom, right, top), plus the links between vertices. `keys` +/// names the row that receives each vertex's center. Vertices without a key are laid out but not +/// written. Plane units, y up. pub(crate) struct Placement<'a> { pub(crate) sizes: &'a [[f64; 2]], pub(crate) pinned: &'a [Option<[f64; 2]>], @@ -156,8 +159,9 @@ pub(crate) struct Placement<'a> { pub(crate) keys: &'a [Option], } -/// Wires to route: around `obstacles` and among `groups`, each net's terminals with the key of the row -/// it draws, runs beside `fixed` wires already routed, tracks `spacing` apart. Plane units, y up. +/// Wires to route around `obstacles` and among `groups`. `keys` names the row that receives each +/// net's route. Routes run beside the already routed `fixed` wires, on tracks `spacing` apart. +/// Plane units, y up. pub(crate) struct Wiring<'a> { pub(crate) obstacles: &'a [[f64; 4]], pub(crate) groups: &'a [[f64; 4]], @@ -168,7 +172,7 @@ pub(crate) struct Wiring<'a> { } impl Job { - /// A job from its bytes. + /// Rebuilds a job from its bytes. pub fn from_bytes(bytes: Vec) -> Job { Job(bytes) } @@ -178,9 +182,9 @@ impl Job { self.0 } - /// Does the job, answering in the words its derivation reads: rows by id as a patch's bytes, - /// lanes as numbers, shapes as columns, a pyramid as its order and tiles, an image as its file. - /// Bytes that are no job answer nothing. + /// Runs the job and returns its result: a patch of rows by id, lane words, shape columns, a + /// pyramid's order and tiles, or an encoded image file. Returns no bytes if the job is + /// malformed or fails. pub fn run(self) -> Vec { let mut input = Reader::new(&self.0); let kind = input.u32(); @@ -206,8 +210,8 @@ impl Job { answer.map(|patch| patch.to_bytes()).unwrap_or_default() } - /// Placing `placement` by `layout`, which is layered or stress; it answers each keyed vertex's - /// center as fields `x` and `y`, y up. + /// A job laying out `placement` with `layout` (layered or stress). Its result sets each keyed + /// vertex's center as fields `x` and `y`, y up. pub(crate) fn layout(layout: &Layout, placement: &Placement<'_>) -> Job { let mut out = match *layout { Layout::Stress(stress) => { @@ -227,7 +231,7 @@ impl Job { out.f64(layered.rank_gap); out } - Layout::None => unreachable!("a sheet asks for a layout only when its scene has one"), + Layout::None => unreachable!("a sheet requests a layout only when its scene has one"), }; out.u32(placement.sizes.len() as u32); for v in 0..placement.sizes.len() { @@ -249,8 +253,8 @@ impl Job { Job(out.0) } - /// Working out `shaping` for every row of each geometry column of `parts`; it answers each part's - /// shapes, as [`shapes_answer`] reads them. + /// A job computing `shaping` for every row of each geometry column in `parts`. Its result holds + /// each part's shapes, read by [`shapes_answer`]. pub(crate) fn shapes(shaping: Shaping, parts: &[Values]) -> Job { let mut out = Writer::new(Kind::Shapes); out.u32(shaping.code()); @@ -258,8 +262,8 @@ impl Job { Job(out.0) } - /// Decoding tile `key` from `bytes`, gzip-compressed or not, into the rows of `tables`, each with - /// the shapes its marks draw; it answers those rows. + /// A job decoding tile `key` from `bytes`, optionally gzip-compressed, into rows of `tables` + /// with their derived shapes. Its result is a patch of those rows. pub(crate) fn tile(key: TileKey, tables: &[Decoded], bytes: &[u8]) -> Job { let mut out = Writer::new(Kind::Tile); out.u32(u32::from(key.z)); @@ -283,8 +287,8 @@ impl Job { Job(out.0) } - /// The job writing `image` as a file of `format`, which it answers with; nothing where it - /// cannot be written. + /// A job encoding `image` as a file of `format`. Its result is the file's bytes, or no bytes if + /// the image cannot be encoded. pub fn encode(image: &crate::Image, format: crate::ImageFormat) -> Job { let mut out = Writer::new(Kind::Image); out.u32(match format { @@ -298,10 +302,10 @@ impl Job { Job(out.0) } - /// Reading `bytes` as dataset `name` off the thread that asks, where they hold GeoJSON, which can - /// be large, as [`Data::read`](crate::Data::read) reads it: `None` where they hold latkit's bytes - /// or a dataset's document, which read where they are. It answers the patch that builds the - /// dataset, or nothing where the GeoJSON is invalid. + /// A job reading GeoJSON `bytes` as dataset `name` in the background, as + /// [`Data::read`](crate::Data::read) does; GeoJSON can be large. `None` if `bytes` are latkit's + /// own bytes or a dataset document, which are read on the calling thread. Its result is the + /// patch that builds the dataset, or no bytes if the GeoJSON is invalid. pub fn geojson(name: &str, bytes: &[u8]) -> Option { if !crate::data::geojson(bytes) { return None; @@ -312,8 +316,9 @@ impl Job { Some(Job(out.0)) } - /// Finding every edge row's lane among the rows joining the same two vertices, from each row's two - /// ends; it answers every row's lane word as a little-endian `u32`, nothing when none share. + /// A job assigning each edge row a lane among the rows joining the same two vertices, from the + /// rows' endpoints `from` and `to`. Its result is every row's lane word as a little-endian + /// `u32`, or no bytes when no two rows share endpoints. pub(crate) fn lanes(from: &[u32], to: &[u32]) -> Job { let mut out = Writer::new(Kind::Lanes); out.u32(from.len() as u32); @@ -322,8 +327,9 @@ impl Job { Job(out.0) } - /// Grouping `leveling`'s tables into tiles, a dataset's at once; it answers each table's pyramid, - /// as [`Pyramid::read`](crate::derive::pyramid::Pyramid::read) reads it. + /// A job grouping the rows of `leveling`'s tables into tiles, all of one dataset's tables at + /// once. Its result holds each table's pyramid, read by + /// [`Pyramid::read`](crate::derive::pyramid::Pyramid::read). pub(crate) fn pyramid(leveling: &Leveling) -> Job { let mut out = Writer::new(Kind::Pyramid); match leveling.domain { @@ -361,7 +367,7 @@ impl Job { Job(out.0) } - /// Routing `wiring`; it answers each keyed row's wire as field `route`, its polylines. + /// A job routing `wiring`. Its result sets each keyed row's wire polylines as field `route`. pub(crate) fn routes(wiring: &Wiring<'_>) -> Job { let mut out = Writer::new(Kind::Routes); out.f64(wiring.spacing); @@ -398,7 +404,7 @@ impl std::fmt::Debug for Job { } } -/// A patch writing `values` for `keys`, one table at a time in the order the keys name them. +/// Builds a patch writing `values` for `keys`, grouped by table in order of first appearance. fn keyed(keys: &[Key], values: Vec, write: impl Fn(Table, Vec) -> Table) -> Patch { let mut tables: Vec<(Name, Vec, Vec)> = Vec::new(); for ((table, id), value) in keys.iter().zip(values) { @@ -491,8 +497,7 @@ fn run_routes(input: &mut Reader<'_>) -> Option { Some(keyed(&keys, routes, |table, routes| table.field("route", routes))) } -/// The shapes a shapes job answers, a column for each part it was asked: `None` where the bytes -/// are no answer. +/// Reads a shapes job's result: a column per part in the job. `None` if `bytes` are malformed. pub(crate) fn shapes_answer(bytes: &[u8]) -> Option> { Reader::new(bytes).columns() } @@ -576,7 +581,7 @@ fn run_lanes(input: &mut Reader<'_>) -> Option> { Some(words.iter().flat_map(|word| word.to_le_bytes()).collect()) } -/// Little-endian words, floats, and keys, written. +/// Writes little-endian words, floats, and keys. struct Writer(Vec); impl Writer { @@ -594,7 +599,7 @@ impl Writer { self.0.extend(v.to_le_bytes()); } - /// Words, copied whole where the machine is little-endian too. + /// Writes words, as one block copy on little-endian machines. fn words(&mut self, words: &[u32]) { if cfg!(target_endian = "little") { self.0.extend_from_slice(bytemuck::cast_slice(words)); @@ -614,7 +619,7 @@ impl Writer { self.0.extend(text.as_bytes()); } - /// Columns, each its byte length and its values' bytes. + /// Writes columns, each as its byte length followed by its values' bytes. fn columns(&mut self, columns: &[Values]) { self.u32(columns.len() as u32); for values in columns { @@ -624,7 +629,7 @@ impl Writer { } } - /// A key: 1 then a numbered id, or 2 then a texted one; the table's name after. + /// Writes a key: 1 and a numeric id, or 2 and a text id, then the table name. fn key(&mut self, (table, id): &Key) { match id { Id::Number(n) => { @@ -640,7 +645,7 @@ impl Writer { } } -/// Little-endian words, floats, and keys, read; `None` past the end. +/// Reads little-endian words, floats, and keys. Reads return `None` past the end. struct Reader<'a> { bytes: &'a [u8], } @@ -650,7 +655,7 @@ impl<'a> Reader<'a> { Reader { bytes } } - /// Every byte not read yet. + /// Takes all unread bytes. fn rest(&mut self) -> &'a [u8] { std::mem::take(&mut self.bytes) } @@ -669,7 +674,7 @@ impl<'a> Reader<'a> { self.take().map(f64::from_le_bytes) } - /// `n` words. + /// Reads `n` words. fn words(&mut self, n: usize) -> Option> { let (head, rest) = self.bytes.split_at_checked(n.checked_mul(4)?)?; self.bytes = rest; @@ -683,7 +688,7 @@ impl<'a> Reader<'a> { String::from_utf8(head.to_vec()).ok() } - /// Columns as [`Writer::columns`] wrote them. + /// Reads columns written by [`Writer::columns`]. fn columns(&mut self) -> Option> { let count = self.u32()?; (0..count) @@ -696,7 +701,7 @@ impl<'a> Reader<'a> { .collect() } - /// A key as [`Writer::key`] wrote it. + /// Reads a key written by [`Writer::key`]. fn key(&mut self) -> Option { let id = match self.u32()? { 1 => Id::Number(u64::from_le_bytes(self.take()?)), @@ -716,14 +721,14 @@ mod tests { (table.into(), Id::Number(id)) } - /// Field `field` of `table`'s row `id` in the answer. + /// Field `field` of row `id` of `table` in a job's result. fn answered(answer: &[u8], table: &str, id: u64, field: &str) -> Option { let data = crate::Snapshot::new("d").applied(Patch::from_bytes(answer).ok()?).ok()?; data.get(table, id, field, None) } #[test] - fn layout_jobs_answer_the_rows_they_place() { + fn layout_jobs_return_the_rows_they_place() { let sizes = [[10.0, 10.0]; 3]; let pinned = [None, Some([5.0, 5.0]), None]; let keys = [Some(key("V", 7)), None, Some(key("V", 9))]; @@ -749,11 +754,11 @@ mod tests { assert_eq!(answered(&answer, "V", 9, "x"), Some(Value::Number(ranked[2][0]))); assert_eq!(answered(&answer, "V", 7, "y"), Some(Value::Number(ranked[0][1]))); let stress = Job::layout(&Layout::Stress(crate::Stress::default()), &placement).run(); - assert!(matches!(answered(&stress, "V", 7, "x"), Some(Value::Number(_))), "stress answers alike"); + assert!(matches!(answered(&stress, "V", 7, "x"), Some(Value::Number(_))), "stress layouts return rows too"); } #[test] - fn route_jobs_answer_routed_wires() { + fn route_jobs_return_routed_wires() { let obstacles = [[0.0, 0.0, 10.0, 10.0], [40.0, 0.0, 50.0, 10.0]]; let terminal = |x: f64, side, owner| Terminal { at: [x, 5.0], side: Some(side), owner: Some(owner) }; let nets = [Net { terminals: vec![terminal(10.0, Side::Right, 0), terminal(40.0, Side::Left, 1)] }]; @@ -771,7 +776,7 @@ mod tests { } #[test] - fn bytes_that_are_no_job_answer_nothing() { + fn bytes_that_are_not_a_job_return_nothing() { assert_eq!(Job::from_bytes(vec![1, 0, 0, 0, 9]).run(), Vec::::new()); assert_eq!(Job::from_bytes(Vec::new()).run(), Vec::::new()); } diff --git a/crates/latkit/src/host/mod.rs b/crates/latkit/src/host/mod.rs index 2e641733..7e1fc947 100644 --- a/crates/latkit/src/host/mod.rs +++ b/crates/latkit/src/host/mod.rs @@ -1,10 +1,12 @@ -//! What the platform does for latkit: set and measure text in its fonts, do jobs off the frame, fetch -//! bytes (tiles, and the files JSON documents name), and encode video with its [`Encoder`]. A -//! [`Gpu`](crate::Gpu) is given its host with [`Gpu::set_host`](crate::Gpu::set_host); without one, -//! views draw no text, do their jobs at once, fetch nothing, and encode no video. A browser's host is -//! `Browser`, built with the `web` feature for `wasm32`. Natively, the `native` feature supplies -//! `Native`: lazy fonts, a worker pool doing jobs and reading files, and Media Foundation's H.264 on -//! Windows, without a window or network client. +//! The host: platform services latkit uses. A host rasterizes and measures text in the platform's +//! fonts, runs jobs in the background, fetches bytes (tiles, and files that JSON documents name), +//! and encodes video with an [`Encoder`]. +//! +//! A [`Gpu`](crate::Gpu) gets its host from [`Gpu::set_host`](crate::Gpu::set_host). Without one, +//! views draw no text, run jobs immediately, fetch nothing, and encode no video. In a browser the +//! host is `Browser` (the `web` feature on `wasm32`). Natively, the `native` feature supplies +//! `Native`: fonts loaded on first use, a worker pool that runs jobs and reads files, and Media +//! Foundation H.264 on Windows. It needs no window or network client. #[cfg(all(feature = "web", target_arch = "wasm32"))] mod browser; @@ -29,77 +31,79 @@ use crate::Encoding; pub use crate::resource::Request; use crate::{Error, Pending, Result}; -/// Ticks of a second an encoder's times count. +/// Ticks per second of encoder times. pub const TIMESCALE: u32 = 90_000; -/// A platform's H.264 encoder: NV12 pictures (BT.709, limited range) in, Annex B units out, no -/// B-frames, so every platform's encoder fits and the muxer never reorders. +/// A platform's H.264 encoder: NV12 pictures (BT.709, limited range) in, Annex B units out. It uses +/// no B-frames, so every platform's encoder can comply and the muxer never reorders frames. pub trait Encoder { - /// Encodes one NV12 picture at `time` ticks, a keyframe when `key`; resolves, once it has room - /// for another picture, to the units it finished since the last call, in order. + /// Encodes one NV12 picture at `time` ticks, as a keyframe when `key`. Resolves, once the + /// encoder can take another picture, to the units finished since the last call, in order. fn encode(&mut self, nv12: Vec, time: u64, key: bool) -> Pending>; - /// Resolves to the units it still holds once every picture given is encoded. + /// Resolves to the remaining units once every picture given has been encoded. fn finish(&mut self) -> Pending>; } -/// One encoded access unit of H.264: Annex B bytes, at `time` ticks of [`TIMESCALE`], decode order -/// equal to display order. +/// One encoded H.264 access unit: Annex B bytes at `time` ticks of [`TIMESCALE`]. Decode order +/// equals display order. #[derive(Clone, PartialEq, Eq, Debug)] pub struct Unit { - /// Ticks at which this frame shows. + /// Presentation time in ticks of [`TIMESCALE`]. pub time: u64, - /// Whether it is a keyframe, which carries its parameter sets in band. + /// Whether this is a keyframe. Keyframes carry their parameter sets in band. pub key: bool, - /// Its NAL units, each after a start code. + /// NAL units, each preceded by a start code. pub annex_b: Vec, } -/// The platform services a view uses. +/// Platform services for text, background jobs, fetching, and video encoding. pub trait Host: Send + Sync + 'static { - /// Sets `run` as coverage, reporting unavailable fonts or failed rasterization. + /// Rasterizes `run` to a coverage mask. Errors if its font is unavailable or rasterization + /// fails. fn rasterize(&self, run: &TextRun) -> Result; - /// The width and height `run` takes once set, in pixels; by default what setting it takes. + /// The width and height of `run` once rasterized, in pixels. By default, rasterizes it to find + /// out. fn measure(&self, run: &TextRun) -> Result<[f32; 2]> { self.rasterize(run).map(|raster| raster.size.map(|v| v as f32)) } - /// Does `job` off the frame, resolving to what [`Job::run`] answers; by default at once, on the - /// calling thread. + /// Runs `job` in the background, resolving to the result of [`Job::run`]. By default it runs + /// immediately on the calling thread. fn spawn(&self, job: Job) -> Pending> { Pending::ready(Ok(job.run())) } - /// Fetches `request`, resolving to its bytes; to no bytes where there is nothing at its URL, and - /// to an error where it cannot be read. By default a host fetches nothing. + /// Fetches `request`, resolving to its bytes. Resolves to no bytes if nothing exists at the + /// URL, and to an error if it cannot be read. By default, fails as unsupported. fn fetch(&self, request: &Request) -> Pending> { let _ = request; - Pending::ready(Err(Error::Unsupported("this host does not fetch".into()))) + Pending::ready(Err(Error::Unsupported("this host cannot fetch".into()))) } - /// An H.264 encoder for pictures as `encoding` says, hardware where the platform has one: what an - /// [`Mp4`](crate::Mp4) encodes with. By default a host encodes no video. + /// An H.264 encoder for `encoding`, in hardware where the platform has one. [`Mp4`](crate::Mp4) + /// encodes with it. By default, fails as unsupported. fn encoder(&self, encoding: &Encoding) -> crate::Result> { let _ = encoding; - Err(Error::Unsupported("this host encodes no video".into())) + Err(Error::Unsupported("this host cannot encode video".into())) } } -/// A line of text to set. +/// Text to rasterize: one line, or a paragraph wrapped at `max_width`. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub struct TextRun { /// The text. pub text: Arc, /// A CSS font family list. pub font: Arc, - /// The size in pixels. + /// Font size in pixels. pub size_px: u32, - /// Pixels a line runs to before it wraps at a space; `None` keeps one line. + /// Width in pixels at which lines wrap at a space. `None` keeps a single line. pub max_width: Option, } -/// Text set as coverage: one byte per pixel, rows top to bottom. +/// Rasterized text as a coverage mask: one byte per pixel, rows top to bottom. #[derive(Clone, PartialEq, Debug)] pub struct Raster { /// Width and height in pixels. diff --git a/crates/latkit/src/host/native.rs b/crates/latkit/src/host/native.rs index efca2c85..c0c993d4 100644 --- a/crates/latkit/src/host/native.rs +++ b/crates/latkit/src/host/native.rs @@ -1,5 +1,5 @@ -//! Native text, a small, lazily started worker pool, local files, and the platform's H.264 encoder. -//! No window or async runtime is needed. +//! The native host: text, a small worker pool started on first use, local file reads, and the +//! platform's H.264 encoder. It needs no window or async runtime. #[cfg(windows)] #[allow(unsafe_code, reason = "Media Foundation is COM: the FFI stays in this module, behind Encoder")] @@ -16,12 +16,13 @@ use super::{Host, Job, Raster, Request, TextRun}; use crate::Encoding; use crate::{Error, Pending, Resolver, Result}; -/// Native platform services: shaped text, background jobs, local files, and video. A handle: clones -/// share its fonts and workers. System fonts are discovered only on the first text request; -/// [`from_fonts`](Self::from_fonts) uses only the supplied font bytes. A pool of at most four -/// workers starts on the first job or fetch; a fetch reads a file path, or a `file://` URL, on it. -/// Video encodes with Media Foundation on Windows, hardware where the machine has it; elsewhere this -/// host encodes none. +/// Native platform services: shaped text, background jobs, local files, and video. +/// +/// `Native` is a handle; clones share its fonts and workers. System fonts are discovered on the +/// first text request, while [`from_fonts`](Self::from_fonts) uses only the supplied font bytes. A +/// pool of at most four workers starts on the first job or fetch. A fetch reads a file path or a +/// `file://` URL on a worker. Video is encoded with Media Foundation on Windows, in hardware where +/// the machine has it. Other platforms encode no video. #[derive(Clone, Default)] pub struct Native(Arc); @@ -32,13 +33,15 @@ struct Services { } impl Native { - /// A host that discovers system fonts lazily. Constructing it does no I/O and starts no threads. + /// A host that discovers system fonts on first use. Constructing it does no I/O and starts no + /// threads. pub fn new() -> Self { Self::default() } - /// A host using only these TTF/OTF/TTC bytes, in order; the first family is the generic fallback. - /// No system fonts are discovered. Each item must contain at least one usable face. + /// A host using only these TTF/OTF/TTC font bytes, in order. The first family is the fallback + /// for the generic families. No system fonts are discovered. Errors if no fonts are given or an + /// item has no usable face. pub fn from_fonts(fonts: impl IntoIterator>) -> Result { let mut db = fontdb::Database::new(); for (i, font) in fonts.into_iter().enumerate() { @@ -71,7 +74,7 @@ impl Native { f(text) } - /// Does `work` on a worker, resolving to what it answers. + /// Runs `work` on a worker, resolving to its result. fn work(&self, work: impl FnOnce() -> Result> + Send + 'static) -> Pending> { let workers = match self.0.workers.get_or_init(Workers::new) { Ok(workers) => workers, @@ -128,20 +131,21 @@ impl Host for Native { { let _ = encoding; Err(Error::Unsupported( - "native H.264 encoding is Windows' (Media Foundation); this platform has none".into(), + "native H.264 encoding is available only on Windows, through Media Foundation".into(), )) } } } -/// The bytes `request` names of a local file: none where there is no file. +/// Reads the local file, or its byte range, that `request` names by path or `file://` URL. Returns +/// no bytes if the file does not exist. Errors on other URL schemes. fn read(request: &Request) -> Result> { let url = &*request.url; let path = if url.starts_with("file:") { crate::url::file_path(url).ok_or_else(|| Error::invalid(format!("invalid file URL: {url}")))? } else { if url.contains("://") { - return Err(Error::Unsupported(format!("the native host reads files, not {url}").into())); + return Err(Error::Unsupported(format!("the native host reads only local files, not {url}").into())); } std::path::PathBuf::from(url) }; @@ -233,10 +237,10 @@ impl Text { } } -/// Points each generic family at an installed one. fontdb names a generic after fontconfig's last -/// alias for it, installed or not: stock Ubuntu's last `sans-serif` alias prefers `FreeSans`, which -/// it does not ship. A generic naming nothing installed takes the first installed of its common -/// families, else any installed family. +/// Points each generic family at an installed family. fontdb names a generic after fontconfig's +/// last alias for it, installed or not. On stock Ubuntu, the last `sans-serif` alias prefers +/// `FreeSans`, which Ubuntu does not ship. A generic naming no installed family gets the first +/// installed of its common families, else any installed family. fn installed_generics(db: &mut fontdb::Database) { fn has(db: &fontdb::Database, family: Family<'_>) -> bool { db.query(&fontdb::Query { families: &[family], ..Default::default() }).is_some() @@ -367,7 +371,7 @@ mod tests { let url = path.to_string_lossy().into_owned(); assert_eq!(fetch(Request::new(url.as_str())).unwrap(), b"0123456789"); assert_eq!(fetch(Request::range(format!("file://{url}"), 3..6)).unwrap(), b"345"); - assert_eq!(fetch(Request::range(url.as_str(), 8..20)).unwrap(), b"89", "a range past the end"); + assert_eq!(fetch(Request::range(url.as_str(), 8..20)).unwrap(), b"89", "a range past the end is cut short"); assert_eq!(fetch(Request::new(dir.join("none").to_string_lossy().into_owned())).unwrap(), b""); assert!(fetch(Request::new("https://example.com/tiles")).is_err()); std::fs::remove_dir_all(dir).unwrap(); diff --git a/crates/latkit/src/host/native/media_foundation.rs b/crates/latkit/src/host/native/media_foundation.rs index 58cce9a6..c5f5d985 100644 --- a/crates/latkit/src/host/native/media_foundation.rs +++ b/crates/latkit/src/host/native/media_foundation.rs @@ -1,6 +1,6 @@ -//! H.264 through Windows Media Foundation: the platform's encoder transform, hardware where the -//! machine has one and the software encoder otherwise, on a thread of its own so encoding overlaps -//! drawing. NV12 pictures in, Annex B units out, no B-frames, keyframes where asked. +//! H.264 encoding through Windows Media Foundation. Uses the machine's hardware encoder transform +//! if it has one, else the software encoder. The transform runs on its own thread so encoding +//! overlaps drawing. NV12 pictures in, Annex B units out, no B-frames, keyframes on request. use std::collections::VecDeque; use std::mem::ManuallyDrop; @@ -33,27 +33,27 @@ use crate::video::nals; use crate::{Encoding, Error, Pending, Resolver, Result}; use tell::Tell; -/// The longest the transform may take to start, to want a picture, or to drain. +/// Timeout for the transform to start, to accept a picture, or to drain. const STALL: Duration = Duration::from_secs(20); -/// Seconds between the keyframes the encoder places itself; the video asks for its own besides. +/// Seconds between the keyframes the encoder inserts on its own. The video also requests some. const KEYFRAMES: f64 = 2.0; -/// 100-nanosecond ticks a second, Media Foundation's clock. +/// Media Foundation clock ticks (100 ns) per second. const HNS: u64 = 10_000_000; -/// An encoder on a thread of its own, where Media Foundation lives. +/// An encoder whose Media Foundation objects live on a thread of their own. struct MediaFoundation { work: Option>, } -/// What the encoder's thread is handed: a picture, with what tells the encoder it has room for -/// another; or the end. +/// Work for the encoder's thread: a picture, whose `room` resolves once the encoder can take +/// another; or the request to finish. enum Work { Picture { nv12: Vec, time: u64, key: bool, room: Resolver> }, Finish(Resolver>), } -/// An encoder for pictures as `encoding` says: the machine's hardware encoder where one starts, else -/// Windows' software encoder. +/// An encoder for `encoding`: the machine's hardware encoder if one starts, else Windows' software +/// encoder. pub(super) fn encoder(encoding: &Encoding) -> Result> { open(*encoding, &[hardware(), software()]).map(|e| Box::new(e) as Box) } @@ -66,7 +66,8 @@ fn software() -> MFT_ENUM_FLAG { MFT_ENUM_FLAG_SYNCMFT | MFT_ENUM_FLAG_SORTANDFILTER } -/// An encoder from the first transform of `kinds`, in order, that starts. +/// Opens an encoder with the first transform of `kinds`, tried in order, that starts. Times out if +/// starting takes longer than [`STALL`]. fn open(encoding: Encoding, kinds: &[MFT_ENUM_FLAG]) -> Result { let (started, ready) = mpsc::channel(); let (work, pictures) = mpsc::channel(); @@ -104,9 +105,9 @@ impl Encoder for MediaFoundation { } } -/// The encoder's thread: starts Media Foundation and the transform, then encodes what it is handed -/// until it finishes or the encoder is dropped. Taking a picture tells there is room for another, -/// with the units finished before it; after an error, every picture is answered with it. +/// The encoder's thread. It starts Media Foundation and the transform, then encodes pictures until +/// told to finish or the encoder is dropped. Taking a picture resolves its `room` with the units +/// finished before it. After an error, every later picture resolves with that error. fn run(encoding: Encoding, kinds: &[MFT_ENUM_FLAG], started: &Sender>, work: &Receiver) { let session = match Session::new() { Ok(session) => session, @@ -155,12 +156,12 @@ fn run(encoding: Encoding, kinds: &[MFT_ENUM_FLAG], started: &Sender> drop(session); } -/// A failed Media Foundation call, named. +/// The error for a failed Media Foundation call, naming the call. fn failed(call: &str, error: &windows::core::Error) -> Error { - Error::Unsupported(format!("the Media Foundation encoder's {call}: {error}").into()) + Error::Unsupported(format!("the Media Foundation encoder's {call} failed: {error}").into()) } -/// COM and Media Foundation, started on this thread and shut down with it. +/// COM and Media Foundation, started on the current thread and shut down when the session drops. struct Session { media: bool, } @@ -179,7 +180,7 @@ impl Session { impl Drop for Session { fn drop(&mut self) { - // SAFETY: every transform, sample, and buffer of this thread was released before. + // SAFETY: every transform, sample, and buffer on this thread was already released. unsafe { if self.media { let _ = MFShutdown(); @@ -189,25 +190,27 @@ impl Drop for Session { } } -/// One H.264 encoder transform, streaming. +/// One streaming H.264 encoder transform. struct Transform { activate: IMFActivate, mft: IMFTransform, codec: Option, - /// An asynchronous transform's events: hardware encoders are asynchronous. + /// An asynchronous transform's events. Hardware encoders are asynchronous. events: Option, - /// Inputs an asynchronous transform asked for and was not given. + /// Inputs an asynchronous transform requested and has not yet received. wanted: u32, input: u32, output: u32, - /// Whether the transform makes its output samples; else, how large a buffer it takes. + /// Whether the transform allocates its output samples. If not, `output_size` is the size of + /// the buffer to allocate. provides: bool, output_size: u32, - /// The times of the pictures given and not yet encoded, in order: no B-frames reorder them. + /// Times of pictures given but not yet encoded, in order. Without B-frames, output order + /// matches input order. times: VecDeque, - /// The SPS and PPS, Annex B, for keyframes that leave them out. + /// The SPS and PPS in Annex B, added to keyframes that lack them. header: Vec, - /// Parameter sets the transform gave as units of their own, for the next picture. + /// Parameter sets the transform output as separate units, prepended to the next picture. prefix: Vec, encoding: Encoding, } @@ -260,7 +263,7 @@ impl Transform { (transform.input, transform.output) = (inputs[0], outputs[0]); } if let Some(codec) = &transform.codec { - // What an encoder cannot set it does without: these only shape its output. + // Settings an encoder rejects are skipped; they only tune its output. let gop = (encoding.fps * KEYFRAMES).round().max(1.0) as u32; let rate = eAVEncCommonRateControlMode_UnconstrainedVBR.0 as u32; for (api, value) in [ @@ -284,7 +287,7 @@ impl Transform { transform.mft.SetOutputType(transform.output, &output, 0).map_err(|e| { if e.code() == MF_E_INVALIDMEDIATYPE { Error::Unsupported( - format!("this H.264 encoder takes no {width}×{height} pictures at {num}/{den} fps").into(), + format!("this H.264 encoder rejects {width}×{height} pictures at {num}/{den} fps").into(), ) } else { failed("SetOutputType", &e) @@ -306,7 +309,7 @@ impl Transform { } } - /// The NV12 input type: the transform's own where it offers one, else a type of ours. + /// The NV12 input type: the one the transform offers, if any, else one built here. fn input_type(&self, width: u32, height: u32, num: u32, den: u32) -> Result { // SAFETY: owned COM handles only. unsafe { @@ -327,7 +330,7 @@ impl Transform { } } - /// Whether the transform makes its own output samples, and how large ours must be otherwise. + /// Reads whether the transform allocates its output samples, and the buffer size otherwise. fn output_info(&mut self) -> Result<()> { // SAFETY: owned COM handles only. let info = @@ -338,8 +341,8 @@ impl Transform { Ok(()) } - /// Encodes one NV12 picture at `time` ticks, a keyframe when `key`: the units the transform - /// finished meanwhile. + /// Encodes one NV12 picture at `time` ticks, as a keyframe when `key`. Returns the units the + /// transform finished meanwhile. fn encode(&mut self, nv12: &[u8], time: u64, key: bool) -> Result> { let sample = self.sample(nv12, time)?; if key && let Some(codec) = &self.codec { @@ -355,7 +358,7 @@ impl Transform { } self.wanted -= 1; self.input(&sample)?; - // Whatever it told meanwhile: outputs ready, and inputs wanted. + // Handle the events queued meanwhile: outputs ready and inputs requested. while self.event(false, &mut units)? {} } else { let started = Instant::now(); @@ -372,7 +375,7 @@ impl Transform { Ok(units) } - /// The units the transform still holds once every picture given is encoded. + /// Drains the transform, returning its remaining units once every picture given is encoded. fn finish(&mut self) -> Result> { let mut units = Vec::new(); // SAFETY: plain messages to an owned transform. @@ -396,14 +399,15 @@ impl Transform { Ok(units) } - /// Gives a synchronous transform `sample`, or an asynchronous one that asked for it. + /// Passes `sample` to the transform. An asynchronous transform must have requested input. fn input(&self, sample: &IMFSample) -> Result<()> { // SAFETY: an owned sample, borrowed for the call. unsafe { self.mft.ProcessInput(self.input, sample, 0) }.map_err(|e| failed("ProcessInput", &e)) } - /// Handles the asynchronous transform's next event, waiting for it, up to the stall, when `wait`: - /// an input wanted is counted, an output ready is taken into `units`. Whether there was one. + /// Handles the asynchronous transform's next event, waiting up to [`STALL`] when `wait`. An + /// input request is counted, and a ready output is taken into `units`. Returns whether there + /// was an event. fn event(&mut self, wait: bool, units: &mut Vec) -> Result { let events = self.events.as_mut().expect("an asynchronous transform"); match events.next(wait)? { @@ -421,12 +425,12 @@ impl Transform { Ok(()) } - /// Takes the output an asynchronous transform said it has. + /// Takes the output an asynchronous transform reported ready. fn drain_one(&mut self, units: &mut Vec) -> Result<()> { self.output(units).map(|_| ()) } - /// Takes one output into `units`: whether there was one, rather than a want of input. + /// Takes one output into `units`. Returns `false` if the transform needs more input instead. fn output(&mut self, units: &mut Vec) -> Result { for _ in 0..4 { let sample = if self.provides { None } else { Some(empty(self.output_size)?) }; @@ -437,7 +441,7 @@ impl Transform { pEvents: ManuallyDrop::new(None), }; let mut status = 0; - // SAFETY: the buffer's sample and events are taken back below, whatever the result. + // SAFETY: the buffer's sample and events are taken back below, on success or failure. let result = unsafe { self.mft.ProcessOutput(0, std::slice::from_mut(&mut buffer), &raw mut status) }; let sample = unsafe { ManuallyDrop::take(&mut buffer.pSample) }; drop(unsafe { ManuallyDrop::take(&mut buffer.pEvents) }); @@ -454,7 +458,8 @@ impl Transform { self.mft.SetOutputType(self.output, &changed, 0).map_err(|e| failed("SetOutputType", &e))?; self.output_info()?; } - // An asynchronous transform says when the output is ready in its new type. + // An asynchronous transform sends another event once output in the new type is + // ready. if self.events.is_some() { return Ok(true); } @@ -462,11 +467,11 @@ impl Transform { } Err(e) => return Err(failed("ProcessOutput", &e)), } - let Some(sample) = sample else { return Err(Error::Io("the encoder gave no sample".into())) }; + let Some(sample) = sample else { return Err(Error::Io("the encoder returned no sample".into())) }; let bytes = bytes(&sample)?; let types: Vec = nals(&bytes).map(|nal| nal[0] & 0x1f).collect(); if !types.iter().any(|t| (1..=5).contains(t)) { - // Parameter sets alone: they lead the next picture. + // Only parameter sets: prepend them to the next picture. self.prefix.extend_from_slice(&bytes); return Ok(true); } @@ -476,14 +481,14 @@ impl Transform { annex_b.extend_from_slice(&self.header); } annex_b.extend_from_slice(&bytes); - let time = self.times.pop_front().ok_or_else(|| Error::Io("the encoder gave more than given".into()))?; + let time = self.times.pop_front().ok_or_else(|| Error::Io("the encoder returned extra pictures".into()))?; units.push(Unit { time, key, annex_b }); return Ok(true); } - Err(Error::Io("the encoder kept changing its output".into())) + Err(Error::Io("the encoder kept changing its output type".into())) } - /// A sample of NV12 `bytes` showing at `time` ticks for a frame's duration. + /// A sample of NV12 `bytes` at `time` ticks, lasting one frame. fn sample(&self, bytes: &[u8], time: u64) -> Result { let length = u32::try_from(bytes.len()).map_err(|_| Error::invalid("the picture is too large to encode"))?; let sample = empty(length)?; @@ -517,12 +522,12 @@ fn empty(length: u32) -> Result { impl Drop for Transform { fn drop(&mut self) { - // SAFETY: an asynchronous transform is shut down through what activated it, once. + // SAFETY: the transform is shut down once, through the activation object that created it. let _ = unsafe { self.activate.ShutdownObject() }; } } -/// The transforms encoding NV12 into H.264 of `kind`, the best first. +/// The transforms of `kind` that encode NV12 into H.264, best first. fn activates(kind: MFT_ENUM_FLAG) -> Result> { let input = MFT_REGISTER_TYPE_INFO { guidMajorType: MFMediaType_Video, guidSubtype: MFVideoFormat_NV12 }; let output = MFT_REGISTER_TYPE_INFO { guidMajorType: MFMediaType_Video, guidSubtype: MFVideoFormat_H264 }; @@ -549,7 +554,8 @@ fn activates(kind: MFT_ENUM_FLAG) -> Result> { } } -/// Sets what a picture is on `kind`: its size and rate, progressive, square pixels, BT.709 limited. +/// Sets the picture format on media type `kind`: size, frame rate, progressive, square pixels, and +/// BT.709 limited range. fn picture(kind: &IMFMediaType, width: u32, height: u32, num: u32, den: u32) -> Result<()> { // SAFETY: owned COM handles, values borrowed for each call. unsafe { @@ -568,7 +574,8 @@ fn picture(kind: &IMFMediaType, width: u32, height: u32, num: u32, den: u32) -> Ok(()) } -/// The SPS and PPS the transform's output type carries, Annex B; none where it carries them in band. +/// The SPS and PPS in Annex B from the transform's output type. Empty if the type has no Annex B +/// sequence header, in which case they come in band. fn header(mft: &IMFTransform, output: u32) -> Vec { // SAFETY: owned COM handles; the blob is copied into a buffer of its size. unsafe { @@ -582,12 +589,12 @@ fn header(mft: &IMFTransform, output: u32) -> Vec { } } -/// An asynchronous transform's events, told as they come to the callback each request names. +/// Events from an asynchronous transform, delivered to the callback passed with each request. struct Events { generator: IMFMediaEventGenerator, callback: IMFAsyncCallback, told: Receiver>, - /// Whether an event was asked for and not yet told. + /// Whether an event was requested and has not yet arrived. asked: bool, } @@ -598,8 +605,8 @@ impl Events { Events { generator, callback, told, asked: false } } - /// The type of the next event: waiting for it up to the stall when `wait`, else `None` while - /// none was told. + /// The type of the next event. With `wait`, waits up to [`STALL`] and errors on timeout. + /// Otherwise returns `None` if no event has arrived. fn next(&mut self, wait: bool) -> Result> { if !self.asked { // SAFETY: owned COM handles; the callback outlives the request it answers. @@ -619,9 +626,9 @@ impl Events { } } -/// What a transform tells its next event to: a COM object of ours, as `windows::core::implement` -/// writes one. -#[allow(clippy::ref_as_ptr, clippy::inline_always, reason = "what windows::core::implement writes")] +/// The callback a transform sends its next event to: a COM object generated by +/// `windows::core::implement`. +#[allow(clippy::ref_as_ptr, clippy::inline_always, reason = "code generated by windows::core::implement")] mod tell { use std::sync::mpsc::Sender; @@ -637,7 +644,7 @@ mod tell { impl IMFAsyncCallback_Impl for Tell_Impl { fn GetParameters(&self, _: *mut u32, _: *mut u32) -> windows::core::Result<()> { - // E_NOTIMPL: the default work queue calls back. + // E_NOTIMPL: call back on the default work queue. Err(windows::core::Error::from_hresult(windows::core::HRESULT(0x8000_4001_u32 as i32))) } @@ -701,16 +708,16 @@ mod tests { assert!(units[0].key, "the first is a keyframe"); let first: Vec = nals(&units[0].annex_b).map(|nal| nal[0] & 0x1f).collect(); assert!(first.contains(&7) && first.contains(&8), "the first keyframe carries its parameter sets: {first:?}"); - assert!(units[6].key, "a keyframe where asked"); + assert!(units[6].key, "a keyframe where requested"); } #[test] - fn the_software_encoder_gives_a_unit_a_picture() { + fn the_software_encoder_returns_a_unit_per_picture() { encodes_a_short_video(&[software()]); } #[test] - fn the_hardware_encoder_gives_a_unit_a_picture() { + fn the_hardware_encoder_returns_a_unit_per_picture() { encodes_a_short_video(&[hardware()]); } } diff --git a/crates/latkit/src/host/queue.rs b/crates/latkit/src/host/queue.rs index 13e9742f..e25bf007 100644 --- a/crates/latkit/src/host/queue.rs +++ b/crates/latkit/src/host/queue.rs @@ -1,6 +1,6 @@ -//! Jobs waiting for a worker, in the order asked. A job goes to a worker only as one frees, so one -//! whose answer nothing waits for any more, as a pyramid superseded by newer data, is dropped before -//! it costs a worker anything. +//! A first-in, first-out queue of jobs waiting for a worker. A job reaches a worker only when one +//! is free, so a job whose result is no longer awaited, such as a pyramid superseded by newer data, +//! is dropped before it uses a worker. use std::collections::VecDeque; @@ -14,12 +14,12 @@ pub(crate) struct Queue { } impl Queue { - /// Queues `job`, whose answer `resolver` gives. + /// Queues `job`; `resolver` receives its result. pub(crate) fn push(&mut self, job: Job, resolver: Resolver>) { self.waiting.push_back((job, resolver)); } - /// The next job for a worker while fewer than `workers` run, dropping those nothing waits for. + /// The next job for a worker, if fewer than `workers` jobs are running. Drops cancelled jobs. pub(crate) fn next(&mut self, workers: usize) -> Option<(Job, Resolver>)> { while let Some((job, resolver)) = self.waiting.pop_front() { if resolver.is_cancelled() { @@ -35,7 +35,7 @@ impl Queue { None } - /// A worker finished its job. + /// Records that a worker finished its job. pub(crate) fn done(&mut self) { self.running = self.running.saturating_sub(1); } @@ -47,7 +47,7 @@ mod tests { use crate::Pending; #[test] - fn a_job_dropped_before_a_worker_frees_never_reaches_one() { + fn a_job_cancelled_while_queued_never_reaches_a_worker() { let mut queue = Queue::default(); let (kept, first) = Pending::>::new(); let (dropped, second) = Pending::>::new(); @@ -60,8 +60,8 @@ mod tests { assert!(queue.next(1).is_none(), "the one worker is busy"); drop(dropped); queue.done(); - let (job, _) = queue.next(1).expect("the worker freed"); - assert_eq!(job.into_bytes(), [3], "the job nothing waits for never reached it"); + let (job, _) = queue.next(1).expect("the worker is free again"); + assert_eq!(job.into_bytes(), [3], "the cancelled job was skipped"); drop((kept, later)); } } diff --git a/crates/latkit/src/image.rs b/crates/latkit/src/image.rs index 36bba992..b14bfefa 100644 --- a/crates/latkit/src/image.rs +++ b/crates/latkit/src/image.rs @@ -1,12 +1,12 @@ -//! Images: RGBA texels, what an icon marker draws and what a shot reads its picture back as, written -//! as PNG, JPEG, or lossless WebP and read from PNG. +//! RGBA images: icon marker textures and the pictures shots read back. Images encode to PNG, JPEG, +//! or lossless WebP, and decode from PNG. use std::io::{Cursor, Write}; use std::sync::Arc; use crate::{Error, Result}; -/// RGBA texels, straight alpha, sRGB, rows top to bottom. +/// An image of RGBA texels: straight alpha, sRGB, rows top to bottom. #[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize, schemars::JsonSchema)] #[serde(remote = "Self", deny_unknown_fields)] #[schemars(rename = "Bitmap")] @@ -17,7 +17,7 @@ pub struct Image { pub rgba: Arc<[u8]>, } -/// An image reads as `{ size, rgba }`, four bytes a texel. +/// Deserializes from `{ size, rgba }`, four bytes per texel, and checks the length. impl<'de> serde::Deserialize<'de> for Image { fn deserialize>(deserializer: D) -> Result { let Image { size, rgba } = Image::deserialize(deserializer)?; @@ -26,7 +26,8 @@ impl<'de> serde::Deserialize<'de> for Image { } impl Image { - /// An image of `size` texels holding `rgba`, four bytes a texel. + /// An image of `size` texels from `rgba`, four bytes per texel. Fails if a dimension is zero + /// or `rgba` has the wrong length. pub fn new(size: [u32; 2], rgba: impl Into>) -> Result { let rgba = rgba.into(); let expected = u64::from(size[0]).checked_mul(u64::from(size[1])).and_then(|pixels| pixels.checked_mul(4)); @@ -36,14 +37,15 @@ impl Image { Ok(Image { size, rgba }) } - /// The image written as a file of `format`. + /// Encodes the image as a `format` file. pub fn encode(&self, format: ImageFormat) -> Result> { let mut out = Vec::new(); self.write(format, &mut out)?; Ok(out) } - /// Writes the image as a file of `format` to a file or stream, reporting write failures. + /// Writes the image as a `format` file to a file or stream. Write failures return + /// [`Error::Io`]. pub fn write(&self, format: ImageFormat, mut output: impl Write) -> Result<()> { Self::new(self.size, self.rgba.clone())?; let [width, height] = self.size; @@ -54,7 +56,7 @@ impl Image { encoder.set_color(png::ColorType::Rgba); encoder.set_depth(png::BitDepth::Eight); encoder.set_source_srgb(png::SrgbRenderingIntent::Perceptual); - // A picture is written far more often than it is stored long. + // Favor speed over size: pictures are written often and rarely stored long. encoder.set_compression(png::Compression::Fast); let mut writer = encoder.write_header().map_err(|e| failed(&e))?; writer.write_image_data(&self.rgba).map_err(|e| failed(&e))?; @@ -62,9 +64,9 @@ impl Image { } ImageFormat::Jpeg { quality } => { if !(1..=100).contains(&quality) { - return Err(Error::invalid("a JPEG's quality runs from 1 to 100").at("quality")); + return Err(Error::invalid("JPEG quality must be from 1 to 100").at("quality")); } - let side = |v: u32| u16::try_from(v).map_err(|_| failed(&"a JPEG is at most 65535 pixels a side")); + let side = |v: u32| u16::try_from(v).map_err(|_| failed(&"each side must be at most 65535 pixels")); let mut bytes = Vec::new(); jpeg_encoder::Encoder::new(&mut bytes, quality) .encode(&self.rgba, side(width)?, side(height)?, jpeg_encoder::ColorType::Rgba) @@ -77,13 +79,13 @@ impl Image { } } - /// The image a PNG file holds, any color type and depth made eight bit RGBA. + /// Decodes a PNG file. Any color type and bit depth converts to 8-bit RGBA. pub fn from_png(bytes: &[u8]) -> Result { let failed = |e: png::DecodingError| Error::invalid(format!("not a PNG image: {e}")); let mut decoder = png::Decoder::new(Cursor::new(bytes)); decoder.set_transformations(png::Transformations::normalize_to_color8()); let mut reader = decoder.read_info().map_err(failed)?; - let mut buffer = vec![0; reader.output_buffer_size().ok_or_else(|| Error::invalid("a PNG too large"))?]; + let mut buffer = vec![0; reader.output_buffer_size().ok_or_else(|| Error::invalid("PNG too large to decode"))?]; let info = reader.next_frame(&mut buffer).map_err(failed)?; let texels = (info.width * info.height) as usize; let rgba: Vec = match info.color_type { @@ -101,23 +103,24 @@ impl Image { } } -/// How an image is written as a file. +/// An image file format. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] pub enum ImageFormat { - /// PNG: lossless, alpha kept, tagged sRGB. + /// PNG: lossless, keeps alpha, tagged sRGB. #[default] Png, - /// JPEG at `quality` from 1 to 100: small, its alpha dropped. + /// JPEG at `quality` 1 to 100: small files, alpha dropped. Jpeg { - /// From 1, smallest, to 100, sharpest; 90 is good for pictures of views. + /// 1 (smallest) to 100 (sharpest). 90 suits pictures of views. quality: u8, }, - /// WebP, lossless: smaller than PNG, alpha kept. + /// Lossless WebP: smaller than PNG, keeps alpha. WebP, } impl ImageFormat { - /// The format a file's `extension` names: `png`, `jpg` or `jpeg` at quality 90, or `webp`. + /// The format for a file `extension`, ignoring case: `png`, `jpg` or `jpeg` (quality 90), or + /// `webp`. pub fn of_extension(extension: &str) -> Option { match extension.to_ascii_lowercase().as_str() { "png" => Some(ImageFormat::Png), @@ -143,9 +146,9 @@ mod tests { use super::*; #[test] - fn png_holds_what_it_was_given() { + fn png_round_trips_losslessly() { let rgba: Vec = (0..3 * 2 * 4).map(|i| (i * 11) as u8).collect(); - let image = Image::new([3, 2], rgba).expect("whole"); + let image = Image::new([3, 2], rgba).expect("a valid image"); let back = Image::from_png(&image.encode(ImageFormat::Png).expect("written")).expect("a PNG"); assert_eq!(back, image); } @@ -153,7 +156,7 @@ mod tests { #[test] fn every_format_writes_its_file() { let rgba: Vec = (0..16 * 8 * 4).map(|i| (i * 7) as u8).collect(); - let image = Image::new([16, 8], rgba).expect("whole"); + let image = Image::new([16, 8], rgba).expect("a valid image"); let jpeg = image.encode(ImageFormat::Jpeg { quality: 90 }).expect("written"); assert_eq!(jpeg[..3], [0xff, 0xd8, 0xff], "a JPEG starts with its marker"); let webp = image.encode(ImageFormat::WebP).expect("written"); @@ -163,7 +166,7 @@ mod tests { } #[test] - fn an_image_holds_four_bytes_a_texel() { + fn images_need_a_positive_size_and_four_bytes_per_texel() { assert!(Image::new([2, 2], vec![0; 15]).is_err()); assert!(Image::from_png(b"not a png").is_err()); assert!(Image::new([0, 0], Vec::new()).is_err()); diff --git a/crates/latkit/src/json.rs b/crates/latkit/src/json.rs index 10013920..53525dc8 100644 --- a/crates/latkit/src/json.rs +++ b/crates/latkit/src/json.rs @@ -1,5 +1,5 @@ -//! JSON as latkit reads it: any input from its text, an error naming the path to what it could not -//! read, such as `marks[2].vertices.color`. +//! Reading inputs from JSON. Errors name the path to the part that could not be read, such as +//! `marks[2].vertices.color`. use std::fmt::Display; @@ -8,8 +8,8 @@ use serde_path_to_error::Segment; use crate::{Error, Path, Result, Step}; -/// What JSON `text` holds, read as `T`: a scene, a camera, a fit, a query, rows, a budget, a shot, or -/// any type that deserializes. An error names the path to what could not be read. +/// Deserializes JSON `text` as `T`, such as a scene, camera, fit, query, rows, budget, or shot. +/// Fails with [`Error::Invalid`], whose path names the part that could not be read. /// /// ``` /// # #[cfg(feature = "gpu")] { @@ -26,7 +26,7 @@ pub fn from_json(text: &str) -> Result { Ok(value) } -/// What `deserializer` holds, read as `T`, an error naming the path to what could not be read. +/// Deserializes `T` from `deserializer`. Errors name the path to the part that could not be read. pub(crate) fn read<'de, T: Deserialize<'de>, D: Deserializer<'de>>(deserializer: D) -> Result where D::Error: Display, diff --git a/crates/latkit/src/layout/boxes.rs b/crates/latkit/src/layout/boxes.rs index d7fca5f0..4dac2de7 100644 --- a/crates/latkit/src/layout/boxes.rs +++ b/crates/latkit/src/layout/boxes.rs @@ -1,23 +1,23 @@ -//! Boxes kept indexed as they change: a packed R-tree over them, whose leaves refit as their boxes -//! change, and the boxes given since it was packed, which every query reads too until there are -//! enough to pack it again. A query and a change each take about log n, and the root holds the box -//! around them all, so what keeps boxes indexed never walks them all again. +//! A spatial index of boxes that change: a packed R-tree whose leaves refit as their boxes change, +//! plus a list of items given boxes since packing, which every query also scans until there are +//! enough to repack. Queries and changes take about O(log n), and the root holds the extent of all +//! boxes, so keeping the index current never walks every box (except when repacking). use super::rect::{EMPTY, Rect, meets, union, valid}; -/// Children a node. +/// Children per node. const NODE: usize = 16; -/// Boxes given since packing past which they are packed again. +/// How many items may get boxes after packing before the index is repacked. const FRESH: usize = 64; -/// Boxes by item, `EMPTY` where an item has none, indexed. +/// Boxes by item (`EMPTY` where an item has none), with a spatial index. pub(crate) struct Boxes { rects: Vec, index: Index, - /// Each item's leaf; `usize::MAX` for one packed without a box. + /// Each item's leaf; `usize::MAX` for an item that had no box when packed. leaves: Vec, - /// Items given a box since packing, which have no leaf. + /// Items given a box since packing; they have no leaf. fresh: Vec, } @@ -28,7 +28,7 @@ impl Default for Boxes { } impl Boxes { - /// `boxes`, by item, packed. + /// Packs `boxes`, by item, into a new index. pub(crate) fn new(boxes: Vec) -> Boxes { let index = Index::new(&boxes); let mut leaves = vec![usize::MAX; boxes.len()]; @@ -38,7 +38,7 @@ impl Boxes { Boxes { rects: boxes, index, leaves, fresh: Vec::new() } } - /// Item `item`'s box is now `rect`, `EMPTY` where it has none. + /// Sets item `item`'s box to `rect` (`EMPTY` for none), adding items as needed. pub(crate) fn set(&mut self, item: usize, rect: Rect) { if item >= self.rects.len() { self.rects.resize(item + 1, EMPTY); @@ -65,25 +65,26 @@ impl Boxes { self.rects.get(item).copied().filter(valid) } - /// Visits the items whose boxes meet `region` until `visit` returns true; whether it did. + /// Visits the items whose boxes meet `region` until `visit` returns true. Returns whether it + /// did. pub(crate) fn some(&self, region: Rect, visit: &mut impl FnMut(usize) -> bool) -> bool { let mut seen = |item: usize| self.get(item).is_some_and(|b| meets(&b, ®ion)) && visit(item); self.index.some(region, &mut seen) || self.fresh.iter().any(|&item| seen(item)) } - /// The box around every box. + /// The box around all boxes. pub(crate) fn extent(&self) -> Rect { self.fresh.iter().fold(self.index.root(), |extent, &item| union(extent, self.rects[item])) } - /// Every item's box, `EMPTY` where it has none, as a job is given them. + /// Every item's box, `EMPTY` where it has none, in the form a job takes them. pub(crate) fn all(&self) -> &[Rect] { &self.rects } } -/// A packed R-tree over the valid boxes: leaves in Z-order of their middles, [`NODE`] children a -/// node. +/// A packed R-tree over the valid boxes: leaves sorted in Z-order of their centers, [`NODE`] +/// children per node. struct Index { /// The item at each leaf. items: Vec, @@ -124,7 +125,7 @@ impl Index { Index { items, nodes, starts } } - /// Visits the items meeting `region` until `visit` returns true; whether it did. + /// Visits the items meeting `region` until `visit` returns true. Returns whether it did. fn some(&self, region: Rect, visit: &mut impl FnMut(usize) -> bool) -> bool { let top = self.starts.len() - 2; !self.items.is_empty() @@ -132,7 +133,7 @@ impl Index { && self.descend(top, self.starts[top], region, visit) } - /// [`Index::some`] below `node` of `level`. + /// [`Index::some`] within the subtree of `node` at `level`. fn descend(&self, level: usize, node: usize, region: Rect, visit: &mut impl FnMut(usize) -> bool) -> bool { let first = self.starts[level - 1] + (node - self.starts[level]) * NODE; let last = (first + NODE).min(self.starts[level]); @@ -142,7 +143,7 @@ impl Index { }) } - /// Leaf `leaf`'s box is now `rect`: it, and every node above it, fit their children again. + /// Sets leaf `leaf`'s box to `rect`, and refits every node above it to its children. fn refit(&mut self, leaf: usize, rect: Rect) { self.nodes[leaf] = rect; let mut child = leaf; @@ -178,7 +179,7 @@ mod tests { use super::*; use crate::layout::rect::grow; - /// A stream of random numbers below `n`. + /// The next pseudo-random whole number below `n`, from `seed`. fn random(seed: &mut u64, n: u32) -> f64 { *seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407); f64::from(((*seed >> 33) % u64::from(n)) as u32) @@ -191,12 +192,12 @@ mod tests { } #[test] - fn boxes_moved_found_and_bounded_as_every_box_says() { + fn queries_and_extent_match_a_brute_force_scan_after_edits() { let mut seed = 3; let mut all: Vec = (0..500).map(|_| somewhere(&mut seed)).collect(); let mut boxes = Boxes::new(all.clone()); for round in 0..300 { - // A box moves, goes, or comes past the last. + // A box moves, is removed, or is added past the last item. let item = random(&mut seed, 560) as usize; let rect = if round % 7 == 0 { EMPTY } else { somewhere(&mut seed) }; if item >= all.len() { diff --git a/crates/latkit/src/layout/layered.rs b/crates/latkit/src/layout/layered.rs index 4669dbc3..c345018d 100644 --- a/crates/latkit/src/layout/layered.rs +++ b/crates/latkit/src/layout/layered.rs @@ -5,31 +5,33 @@ use super::pack; use super::rect::union; use crate::scene::{Direction, Layered}; -/// Width over height of the rows that parts nothing pins pack into. +/// The target width over height of the rows that parts with no pinned vertex pack into. const ASPECT: f64 = 16.0 / 9.0; /// Crossing-reduction sweeps, alternately down and up the ranks. const SWEEPS: usize = 8; -/// Every vertex's center: pinned vertices where they are; the others in ranks along `options.flow` -/// (`Direction::Up` toward +y), `options.gap` apart within a rank and `options.rank_gap` between -/// ranks, ordered to cross few links, each pointing from one vertex to another; cycles are broken. -/// Each connected part is laid out alone, and parts nothing pins pack into rows (aspect 16:9) beside -/// the pinned ones. Across the flow, each vertex lines up with its sources; in a part nothing pins, -/// each source then settles amid the vertices it fans out to. Groups nest as [`nested`] lays them -/// out. +/// Every vertex's center from a layered layout. Pinned vertices stay put. The others go in ranks +/// along `options.flow` (`Direction::Up` is toward +y), `options.gap` apart within a rank and +/// `options.rank_gap` between ranks, ordered to reduce crossings. Links are directed; cycles are +/// broken. +/// +/// Each connected part is laid out separately, and parts with no pinned vertex pack into rows +/// (aspect 16:9) below the pinned ones. Across the flow, each vertex lines up with its sources; in +/// a part with no pinned vertex, each source then centers among the vertices it fans out to. Groups +/// nest as [`nested`] lays them out. pub(crate) fn layered(graph: &Graph<'_>, options: &Layered) -> Vec<[f64; 2]> { nested(graph, |level| arrange(level, options)) } -/// Places one level: each part that its links join laid out alone in ranks, then the parts nothing -/// pins packed in rows below the pinned ones, tallest first. +/// Places one level: lays out each connected part in ranks, then packs the parts with no pinned +/// vertex in rows below the pinned ones, tallest first. fn arrange(level: &Level, options: &Layered) -> Vec<[f64; 2]> { let (sizes, pins) = (&level.sizes, &level.pinned); let mut centers = vec![[0.0; 2]; sizes.len()]; let (part, count) = parts(sizes.len(), &level.links); let inside = Runs::new(count, (0..sizes.len()).map(|i| (part[i] as usize, i as u32))); let joins = Runs::new(count, level.links.iter().map(|&link| (part[link[0] as usize] as usize, link))); - // Each member's place in its part, whose members run in index order. + // Each member's index within its part; a part's members are in index order. let mut slot = vec![0_u32; sizes.len()]; for p in 0..count { for (s, &i) in inside.get(p).iter().enumerate() { @@ -71,7 +73,7 @@ fn arrange(level: &Level, options: &Layered) -> Vec<[f64; 2]> { if loose.is_empty() { return centers; } - // Tallest first, then by first member, so like parts line up. + // Tallest first, ties in part order (by first member), so similar parts line up. crate::order::by_key(&mut loose, |a| !crate::order::number(a.1[3] - a.1[1])); let boxes: Vec<[f64; 2]> = loose.iter().map(|(_, b)| [b[2] - b[0], b[3] - b[1]]).collect(); let gap = options.rank_gap; @@ -87,8 +89,8 @@ fn arrange(level: &Level, options: &Layered) -> Vec<[f64; 2]> { centers } -/// Each of `count` vertices' part, the parts that `links` join numbered by their first vertex, and how many parts -/// there are. +/// Each of `count` vertices' connected part under `links`, with parts numbered in order of their +/// first vertex, and the number of parts. fn parts(count: usize, links: &[[u32; 2]]) -> (Vec, usize) { fn find(root: &mut [u32], mut vertex: u32) -> u32 { while root[vertex as usize] != vertex { @@ -122,8 +124,8 @@ fn parts(count: usize, links: &[[u32; 2]]) -> (Vec, usize) { (part, parts) } -/// Plane coordinates turned so `flow` runs along the first: `[along, across]`, across running down a sideways flow and -/// rightward across an upward or downward one. +/// Plane coordinates rotated so `flow` runs along the first axis: `[along, across]`. Across runs +/// down for a sideways flow, and right for an upward or downward one. fn to_flow(flow: Direction, [x, y]: [f64; 2]) -> [f64; 2] { match flow { Direction::Right => [x, -y], @@ -151,8 +153,9 @@ fn extent(flow: Direction, [width, height]: [f64; 2]) -> [f64; 2] { } } -/// One part's centers in flow coordinates (see [`to_flow`]). `extents` are each vertex's length along the flow and -/// span across it, `pins` the centers of those that stay, and `links` point from one vertex to another. +/// One part's centers in flow coordinates (see [`to_flow`]). `extents` are each vertex's length +/// along the flow and span across it, `pins` the centers of pinned vertices, and `links` are +/// directed. fn ranked( extents: &[[f64; 2]], pins: &[Option<[f64; 2]>], @@ -164,7 +167,8 @@ fn ranked( let rank = ranks(count, links); let rank_count = rank.iter().max().map_or(0, |&last| last as usize + 1); let mut layers = Runs::new(rank_count, (0..count).map(|i| (rank[i] as usize, i as u32))); - // Each link as its end in the earlier rank, then its end in the later; ranking leaves none within a rank. + // Each link as its end in the earlier rank, then its end in the later. Ranking leaves no link + // within a rank. let ends = |&[from, to]: &[u32; 2]| if rank[from as usize] < rank[to as usize] { [from, to] } else { [to, from] }; let above = Runs::new(count, links.iter().map(ends).map(|[early, late]| (late as usize, early))); let below = Runs::new(count, links.iter().map(ends).map(|[early, late]| (early as usize, late))); @@ -184,8 +188,9 @@ fn ranked( centers } -/// Each vertex's rank: the longest path to it from a vertex nothing points to, once the links that close cycles are -/// set aside. A walk depth first from the vertices nothing points to finds those links. +/// Each vertex's rank: the longest path to it from a source (a vertex no link points to), ignoring +/// the links that close cycles. A depth-first walk from the sources, then from any vertex left, +/// finds those links. fn ranks(count: usize, links: &[[u32; 2]]) -> Vec { const NEW: u8 = 0; const WALKING: u8 = 1; @@ -247,9 +252,9 @@ fn ranks(count: usize, links: &[[u32; 2]]) -> Vec { rank } -/// Orders each rank to cross few edges. Barycenter sweeps run alternately down the ranks, sorting each by where its -/// vertices' neighbors in earlier ranks sit, and up them by their neighbors in later ranks; the order that crosses the -/// fewest edges between adjacent ranks stays. +/// Orders each rank to reduce edge crossings. Barycenter sweeps alternate: down the ranks, sorting +/// each by the mean position of its vertices' neighbors in earlier ranks, then up, by neighbors in +/// later ranks. The order with the fewest crossings between adjacent ranks is kept. fn reorder(layers: &mut Runs, above: &Runs, below: &Runs, adjacent: &Runs<[u32; 2]>) { let mut position = vec![0_u32; above.len()]; for k in 0..layers.len() { @@ -294,18 +299,18 @@ fn settle(layer: &[u32], position: &mut [u32]) { } } -/// Counts edges crossing between adjacent ranks, keeping its buffers from one count to the next. +/// Counts edge crossings between adjacent ranks, reusing its buffers from one count to the next. #[derive(Default)] struct Crossings { - /// Where each edge between two ranks starts and ends in them. + /// Each edge's position in the earlier rank and in the later rank. ends: Vec<(u32, u32)>, - /// A Fenwick tree of how many edges so far end at each place of the later rank. + /// A Fenwick tree counting the edges seen so far that end at each position of the later rank. ended: Vec, } impl Crossings { - /// Edges between adjacent ranks that cross, given where each vertex sits in its rank: once sorted by where they - /// start, each edge crosses the earlier ones ending past it. + /// The number of crossing edge pairs between adjacent ranks, given each vertex's position in + /// its rank. With edges sorted by start, each edge crosses the earlier ones that end after it. fn count(&mut self, layers: &Runs, position: &[u32], adjacent: &Runs<[u32; 2]>) -> u64 { let mut total = 0; for k in 1..layers.len() { @@ -337,10 +342,11 @@ impl Crossings { } } -/// Each vertex's center in flow coordinates. Ranks run `rank_gap` apart along the flow, each vertex centered in its -/// rank's band; across it, each vertex sits in line with the median of its neighbors in earlier ranks unless that -/// would bring it within `gap` of the vertex before it in its rank. A rank holding pinned vertices starts where they -/// do, and vertices step past the pinned ones they would come within `gap` of. +/// Each vertex's center in flow coordinates. Ranks run `rank_gap` apart along the flow, each +/// vertex centered in its rank's band. Across the flow, each vertex lines up with the median of +/// its neighbors in earlier ranks, unless that brings it within `gap` of the previous vertex in its +/// rank. A rank holding pinned vertices starts where they do, and vertices step past pinned ones +/// they would come within `gap` of. fn coordinates( extents: &[[f64; 2]], pins: &[Option<[f64; 2]>], @@ -358,7 +364,7 @@ fn coordinates( layers.get(k).iter().filter_map(|&i| pins[i as usize].map(|pin| pin[0] - length_of(i) / 2.0)).reduce(f64::min) }; let first = (0..layers.len()).find_map(|k| lead(k).map(|at| (k, at))); - // Ranks before the first pinned one end where it starts. + // Ranks before the first rank with a pinned vertex end where that rank starts. let mut major = first.map_or(0.0, |(k, at)| at - bands[..k].iter().map(|band| band + rank_gap).sum::()); let mut obstacles = first.map(|_| Obstacles::new(extents, pins, [gap.min(rank_gap), gap])); let mut wanted: Vec = Vec::new(); @@ -403,9 +409,9 @@ fn coordinates( centers } -/// Moves each vertex across the flow toward the middle of its neighbors in later ranks, the last ranks first, as far -/// as the vertices beside it in its rank allow, `gap` apart: a source settles amid the vertices it fans out to, and a -/// chain stays in line. +/// Moves each vertex across the flow toward the median of its neighbors in later ranks, last ranks +/// first, as far as its neighbors in its rank allow at `gap` apart. A source centers among the +/// vertices it fans out to, and a chain stays in line. fn balance(centers: &mut [[f64; 2]], extents: &[[f64; 2]], layers: &Runs, below: &Runs, gap: f64) { let mut across: Vec = Vec::new(); let mut goals: Vec> = Vec::new(); @@ -455,7 +461,7 @@ fn balance(centers: &mut [[f64; 2]], extents: &[[f64; 2]], layers: &Runs, b struct Obstacles { /// A cell's side. cell: f64, - /// The room boxes keep between them along the flow and across it. + /// The clearance boxes keep between them, along the flow and across it. room: [f64; 2], /// Each box: its least along and across, then its greatest. boxes: Vec<[f64; 4]>, @@ -464,7 +470,8 @@ struct Obstacles { } impl Obstacles { - /// The pinned vertices' boxes, keeping `room` between boxes. Cells span a few vertices, so a box reaches few. + /// Obstacles holding the pinned vertices' boxes, keeping `room` between boxes. Cells span a few + /// vertices, so each box reaches few cells. fn new(extents: &[[f64; 2]], pins: &[Option<[f64; 2]>], room: [f64; 2]) -> Obstacles { let largest = extents.iter().flatten().fold(0.0, |largest: f64, &side| largest.max(side)); let cell = (room[1] * 8.0).max(largest * 2.0); @@ -491,7 +498,7 @@ impl Obstacles { } } - /// The greatest across of the boxes that `query` comes within room of, if any. + /// The greatest across edge among the boxes that `query` comes within `room` of, if any. fn reach(&self, query: [f64; 4]) -> Option { let [along, across] = self.room; let grown = [query[0] - along, query[1] - across, query[2] + along, query[3] + across]; @@ -540,8 +547,8 @@ mod tests { } impl Columns { - /// `count` vertices of [`SIZE`] joined by `edges`, none pinned or grouped; a group pads its members by - /// half a gap. + /// `count` vertices of [`SIZE`] joined by `edges`, none pinned or grouped. A group pads its + /// members by half a gap. fn new(count: usize, edges: &[[u32; 2]]) -> Columns { Columns { sizes: vec![SIZE; count], @@ -584,7 +591,7 @@ mod tests { (a - b).abs() < 1e-9 } - /// Edges that cross between vertices in the same two ranks of a rightward flow. + /// How many edge pairs cross between the same two ranks of a rightward flow. fn crossings(edges: &[[u32; 2]], centers: &[[f64; 2]]) -> usize { let mut count = 0; for (k, first) in edges.iter().enumerate() { @@ -624,7 +631,7 @@ mod tests { fn a_source_centers_on_its_fan() { // 0 fans out to 1, 2, and 3; 3 runs on to 4. let centers = Columns::new(5, &[[0, 1], [0, 2], [0, 3], [3, 4]]).layout(&options(Direction::Down)); - assert!(near(centers[0][0], centers[2][0]), "the source sits amid its fan: {centers:?}"); + assert!(near(centers[0][0], centers[2][0]), "the source is centered on its fan: {centers:?}"); assert!(near(centers[3][0], centers[4][0]), "a chain stays in line: {centers:?}"); assert!(centers[1][0] < centers[2][0] && centers[2][0] < centers[3][0], "{centers:?}"); } @@ -671,7 +678,8 @@ mod tests { #[test] fn sweeps_untangle_crossings() { - // A K2,2 short of one edge in two ranks of two, and three ranks of three; both cross in index order. + // A K2,2 minus one edge in two ranks of two, and a graph in three ranks of three. Both + // cross when laid out in index order. let small = [[0, 3], [1, 2], [1, 3]]; let large = [[0, 5], [1, 4], [2, 3], [2, 4], [3, 8], [4, 7], [5, 6]]; for (count, width, edges) in [(4, 2, &small[..]), (9, 3, &large[..])] { @@ -683,8 +691,9 @@ mod tests { } #[test] - fn groups_hold_their_members_apart_from_the_rest() { - // Group 0 holds 1 → 2 → 3 and group 6, which holds 7 → 8; 4 feeds the group and 5 drains it. + fn groups_contain_their_members_and_keep_other_vertices_out() { + // Group 0 holds 1 → 2 → 3 and group 6, which holds 7 → 8. Vertex 4 feeds group 0, and 5 + // drains it. let mut graph = Columns::new(9, &[[1, 2], [2, 3], [4, 1], [3, 5], [2, 7], [7, 8]]); graph.groups = vec![None, Some(0), Some(0), Some(0), None, None, Some(0), Some(6), Some(6)]; let options = options(Direction::Right); @@ -707,7 +716,7 @@ mod tests { } #[test] - fn a_pinned_member_holds_its_group_and_a_pinned_group_carries_its_members() { + fn a_pinned_member_fixes_its_group_and_a_pinned_group_moves_its_members() { let mut graph = Columns::new(7, &[[1, 2], [3, 0], [5, 6], [4, 5]]); graph.groups = vec![None, Some(0), Some(0), None, None, Some(4), Some(4)]; graph.pinned[1] = Some([300.0, 200.0]); @@ -723,7 +732,7 @@ mod tests { } #[test] - fn large_graphs_lay_out_apart() { + fn large_graphs_lay_out_without_overlaps() { // Fifty groups of twenty, and edges joining random vertices, many against the flow. let count = 10_000; let mut seed = 0x9e37_79b9_u32; diff --git a/crates/latkit/src/layout/levels.rs b/crates/latkit/src/layout/levels.rs index 198ed620..c5f9dd97 100644 --- a/crates/latkit/src/layout/levels.rs +++ b/crates/latkit/src/layout/levels.rs @@ -1,12 +1,14 @@ -//! Nested graphs laid out a level at a time. A level is the vertices of the top, or the members of -//! one group. A group's members are placed first, innermost groups first; the group then stands as -//! one vertex of its box's size in the level holding it, with links between members of different -//! groups lifted to the groups they meet in. Every layout places one level at a time, so every -//! layout nests alike. +//! Nested graphs, laid out one level at a time. +//! +//! A level is the top-level vertices, or the members of one group. Groups are laid out innermost +//! first: a group's members are placed, then the group acts as one vertex, sized to its box, in +//! the level holding it. Links between members of different groups are lifted to the groups that +//! share a level. Every layout places one level at a time, so all layouts nest the same way. -/// A graph to lay out, plane units with y up: each vertex's width and height, its center when it has -/// one, the group holding it, and how far a group's box reaches past its members (left, bottom, -/// right, top); and links between vertices, from one to another where a layout reads direction. +/// A graph to lay out, in plane units with y up: each vertex's width and height, its pinned center +/// if any, the group holding it, and its padding as a group (how far its box reaches past its +/// members: left, bottom, right, top); and links between vertices, directed where a layout reads +/// direction. pub(crate) struct Graph<'a> { pub(crate) sizes: &'a [[f64; 2]], pub(crate) links: &'a [[u32; 2]], @@ -22,15 +24,16 @@ pub(crate) struct Level { pub(crate) pinned: Vec>, } -/// Every vertex's center, `place` answering each level's. A group's center is the middle of its box: -/// its members' bounds grown by its pads. A pinned member holds its group where its members settled; -/// a group's own pin moves its members only when none of them is pinned. Groups out of range, of -/// themselves, or closing a cycle hold nothing. +/// Every vertex's center, with `place` laying out each level. A group's center is the middle of +/// its box: its members' bounds grown by its pads. A group with a pinned member is pinned where its +/// members settled; a group's own pin moves its members only when none of them is pinned. Group +/// references out of range, to the vertex itself, or closing a cycle are ignored. Panics unless +/// `pinned`, `groups`, and `pads` have an entry per vertex. pub(crate) fn nested(graph: &Graph<'_>, mut place: impl FnMut(&Level) -> Vec<[f64; 2]>) -> Vec<[f64; 2]> { let count = graph.sizes.len(); assert!( graph.pinned.len() == count && graph.groups.len() == count && graph.pads.len() == count, - "a pin, a group, and pads for each vertex" + "pinned, groups, and pads need an entry per vertex" ); let parent = tree(graph.groups); let depth = depths(&parent); @@ -54,7 +57,7 @@ pub(crate) fn nested(graph: &Graph<'_>, mut place: impl FnMut(&Level) -> Vec<[f6 graph.sizes.iter().map(|&[width, height]| [width.max(0.0), height.max(0.0)]).collect(); let mut pins: Vec> = graph.pinned.iter().map(|pin| pin.filter(|at| at[0].is_finite() && at[1].is_finite())).collect(); - // Centers within each vertex's level, and each group's members' middle within its own. + // Each vertex's center within its level, and each group's box middle in its members' frame. let mut centers = vec![[0.0; 2]; count]; let mut middles = vec![[0.0; 2]; count]; let mut local = vec![0_u32; count]; @@ -104,8 +107,8 @@ pub(crate) fn nested(graph: &Graph<'_>, mut place: impl FnMut(&Level) -> Vec<[f6 .collect() } -/// Each vertex's group: groups out of range or of themselves dropped, and each cycle of groups cut where a walk up it -/// closes. +/// Each vertex's group, dropping groups out of range or equal to the vertex itself, and cutting +/// each cycle of groups where a walk up it closes. fn tree(groups: &[Option]) -> Vec> { const NEW: u8 = 0; const WALKING: u8 = 1; @@ -123,7 +126,7 @@ fn tree(groups: &[Option]) -> Vec> { loop { if state[at] != NEW { if state[at] == WALKING { - parent[*walk.last().expect("a walk reaches itself only after a step")] = None; + parent[*walk.last().expect("a walk that closes a cycle is not empty")] = None; } break; } @@ -154,7 +157,7 @@ fn depths(parent: &[Option]) -> Vec { None => break, } } - // The walk ends at a vertex already counted, or at the top, which it holds. + // The walk ends at a vertex already counted, or at a top-level vertex it includes. let mut next = if depth[at] == u32::MAX { 0 } else { depth[at] + 1 }; while let Some(vertex) = walk.pop() { depth[vertex] = next; @@ -164,13 +167,14 @@ fn depths(parent: &[Option]) -> Vec { depth } -/// The link `from` → `to` between the outermost groups (or vertices) that hold each end and share a level; `None` for -/// a link within one of them or with an end out of range. +/// The link `from` → `to` lifted to the level where it is laid out: between the ancestors of its +/// ends (or the ends themselves) that share a parent. `None` for a link inside one group or vertex, +/// or with an end out of range. fn lift(from: u32, to: u32, parent: &[Option], depth: &[u32]) -> Option<[u32; 2]> { if from as usize >= parent.len() || to as usize >= parent.len() { return None; } - let up = |vertex: usize| parent[vertex].expect("a held vertex has a group") as usize; + let up = |vertex: usize| parent[vertex].expect("a nested vertex has a group") as usize; let (mut from, mut to) = (from as usize, to as usize); while depth[from] > depth[to] { from = up(from); @@ -195,15 +199,16 @@ pub(crate) fn bounds(boxes: impl Iterator) -> [f64; 4] { boxes.reduce(super::rect::union).unwrap_or([0.0; 4]) } -/// Items bucketed by key, each bucket's in the order given: bucket `k` is `items[offsets[k]..offsets[k + 1]]`. +/// Items bucketed by key, each bucket in the order given: bucket `k` is +/// `items[offsets[k]..offsets[k + 1]]`. pub(crate) struct Runs { offsets: Vec, - /// Every bucket's items, bucket after bucket; reordered within buckets, they stay bucketed. + /// Every bucket's items, bucket after bucket. Reordering within a bucket keeps them bucketed. pub(crate) items: Vec, } impl Runs { - /// `keyed` items in `buckets` buckets, each item's key below `buckets`. + /// Buckets `keyed` items into `buckets` buckets; each key must be below `buckets`. pub(crate) fn new(buckets: usize, keyed: impl Iterator + Clone) -> Runs { let mut offsets = vec![0; buckets + 1]; for (key, _) in keyed.clone() { @@ -224,7 +229,7 @@ impl Runs { } impl Runs { - /// How many buckets. + /// The number of buckets. pub(crate) fn len(&self) -> usize { self.offsets.len() - 1 } @@ -234,7 +239,7 @@ impl Runs { &self.items[self.offsets[k]..self.offsets[k + 1]] } - /// Bucket `k`'s items, to change. + /// Bucket `k`'s items, mutably. pub(crate) fn get_mut(&mut self, k: usize) -> &mut [T] { &mut self.items[self.offsets[k]..self.offsets[k + 1]] } diff --git a/crates/latkit/src/layout/mod.rs b/crates/latkit/src/layout/mod.rs index c9ecd3a7..15e1fa6a 100644 --- a/crates/latkit/src/layout/mod.rs +++ b/crates/latkit/src/layout/mod.rs @@ -1,5 +1,6 @@ -//! Layouts on the CPU: layered drawing and stress over nested graphs, packing of parts, overlap -//! removal, and a diagram's orthogonal routes. Each is pure, so it runs as well in a host's job. +//! CPU graph layouts: layered and stress layouts of nested graphs, packing of disconnected parts, +//! overlap removal, orthogonal wire routing for diagrams, and an R-tree of boxes. Each is a pure +//! function, so it runs the same on the frame or in a host job. pub(crate) mod boxes; pub(crate) mod layered; pub(crate) mod levels; diff --git a/crates/latkit/src/layout/overlap.rs b/crates/latkit/src/layout/overlap.rs index 541c80ac..a85e8ae9 100644 --- a/crates/latkit/src/layout/overlap.rs +++ b/crates/latkit/src/layout/overlap.rs @@ -1,22 +1,25 @@ -//! Overlap removal: boxes pushed apart until no two come within a clearance of each other. +//! Overlap removal: pushes boxes apart until no two are within a clearance of each other. use rustc_hash::FxHashMap; -/// Rounds of pairwise pushing, before a sweep parts what they leave. +/// Rounds of pairwise pushing before a sweep separates the overlaps that remain. const ROUNDS: usize = 40; -/// Each box's center once no two of `boxes` (center, then half width and height) come within -/// `clearance` of each other, pinned ones aside. First rounds of pushing: a pair too close parts -/// along the axis it overlaps least, each half the way, or a free box all of it when the other is -/// pinned. That moves boxes least, but a crowd jams; a sweep along x then places each box after the -/// pinned ones, in order, right of any placed box it overlaps. Pairs are found in a grid of cells as -/// wide as the widest box, so each round, and the sweep, is linear in the boxes for boxes of like -/// sizes. +/// Each box's center once no two of `boxes` (center x, center y, half width, half height) are +/// within `clearance` of each other. Pinned boxes never move, and two pinned boxes may still +/// overlap. +/// +/// First come rounds of pushing: a pair too close parts along the axis where it overlaps least, +/// each box moving half the overlap, or the free box all of it when the other is pinned. This +/// moves boxes least, but a crowd can jam. If overlaps remain after `ROUNDS`, a sweep places the +/// free boxes in order of x after the pinned ones, each pushed right past any placed box it +/// overlaps. Pairs are found in a grid of cells as wide as the widest box, so each round and the +/// sweep are linear in the number of boxes when their sizes are similar. pub(crate) fn separate(boxes: &[[f64; 4]], pinned: &[bool], clearance: f64) -> Vec<[f64; 2]> { let mut centers: Vec<[f64; 2]> = boxes.iter().map(|b| [b[0], b[1]]).collect(); let half: Vec<[f64; 2]> = boxes.iter().map(|b| [b[2], b[3]].map(|h| h.max(0.0) + clearance / 2.0)).collect(); let grid = Grid { cell: 2.0 * half.iter().flatten().copied().fold(1e-9, f64::max) }; - // Overlaps this small are rounding, not overlap. + // Overlaps this small are rounding error. let slack = grid.cell * 1e-9; let overlap = |centers: &[[f64; 2]], i: usize, j: usize| { let over = [0, 1].map(|k| half[i][k] + half[j][k] - (centers[j][k] - centers[i][k]).abs()); @@ -33,7 +36,7 @@ pub(crate) fn separate(boxes: &[[f64; 4]], pinned: &[bool], clearance: f64) -> V } let Some(over) = overlap(¢ers, i, j) else { continue }; let d = [0, 1].map(|k| centers[j][k] - centers[i][k]); - // Boxes at one point part by their order, across and along by turns. + // Coincident boxes part along x or y, alternating with the parity of `i + j`. let k = if d == [0.0; 2] { (i + j) % 2 } else { usize::from(over[1] < over[0]) }; let sign = if d[k] == 0.0 { 1.0 } else { d[k].signum() }; let share = if pinned[i] { @@ -56,7 +59,7 @@ pub(crate) fn separate(boxes: &[[f64; 4]], pinned: &[bool], clearance: f64) -> V crate::order::by_key(&mut order, |&a| crate::order::number(centers[a][0])); grid.fill(&mut cells, ¢ers, (0..centers.len()).filter(|&i| pinned[i])); for i in order { - // Right past the placed boxes it overlaps, until it overlaps none. + // Push it right past the placed boxes it overlaps until it overlaps none. while let Some(x) = grid .near(&cells, centers[i]) .filter_map(|j| overlap(¢ers, j, i).map(|_| centers[j][0] + half[j][0] + half[i][0])) @@ -69,7 +72,8 @@ pub(crate) fn separate(boxes: &[[f64; 4]], pinned: &[bool], clearance: f64) -> V centers } -/// Square cells of boxes' centers, wide enough that boxes that overlap stand in neighboring cells. +/// A grid of square cells holding box centers, wide enough that overlapping boxes are in the same +/// or neighboring cells. struct Grid { cell: f64, } @@ -79,7 +83,7 @@ impl Grid { ((c[0] / self.cell).floor() as i64, (c[1] / self.cell).floor() as i64) } - /// Fills `cells` with the boxes `which`. + /// Empties `cells`, then fills them with the boxes `which`. fn fill( &self, cells: &mut FxHashMap<(i64, i64), Vec>, @@ -92,7 +96,7 @@ impl Grid { } } - /// The boxes in the cells around `at`. + /// The boxes in the 3 × 3 cells around `at`. fn near<'a>(&self, cells: &'a FxHashMap<(i64, i64), Vec>, at: [f64; 2]) -> impl Iterator + 'a { let (x, y) = self.key(at); [-1, 0, 1] @@ -119,7 +123,7 @@ mod tests { } #[test] - fn a_pair_parts_along_its_lesser_overlap() { + fn a_pair_separates_along_its_smaller_overlap() { // Each half grows by half the clearance: 12 and 12 overlap 15 apart by 9 across, 22 along. let boxes = [[0.0, 0.0, 10.0, 10.0], [15.0, 2.0, 10.0, 10.0]]; let centers = separate(&boxes, &[false, false], 4.0); @@ -127,7 +131,7 @@ mod tests { } #[test] - fn pinned_boxes_hold() { + fn pinned_boxes_stay_put() { let boxes = [[0.0, 0.0, 10.0, 10.0], [15.0, 0.0, 10.0, 10.0]]; let centers = separate(&boxes, &[true, false], 0.0); assert_eq!(centers, vec![[0.0, 0.0], [20.0, 0.0]]); @@ -139,6 +143,6 @@ mod tests { let pinned: Vec = (0..60).map(|i| i == 7).collect(); let centers = separate(&boxes, &pinned, 2.0); assert!(clear(&boxes, ¢ers, 2.0), "{centers:?}"); - assert_eq!(centers[7], [0.0, 0.0], "the pinned box holds"); + assert_eq!(centers[7], [0.0, 0.0], "the pinned box stays put"); } } diff --git a/crates/latkit/src/layout/pack.rs b/crates/latkit/src/layout/pack.rs index 25cb7317..be0614fd 100644 --- a/crates/latkit/src/layout/pack.rs +++ b/crates/latkit/src/layout/pack.rs @@ -1,19 +1,19 @@ -/// Offsets that pack boxes (each `[width, height]`) into rows `gap` apart whose width over height approaches `aspect`, -/// in the order given; the first box's top left lands at the origin. Returns each box's top-left corner (y up: top is -/// the greater y). +/// Packs boxes (each `[width, height]`) in the given order into rows `gap` apart, so the result's +/// width over height approaches `aspect`. Returns each box's top-left corner, with y up (top is the +/// greater y); the first box's top left is at the origin. pub(crate) fn pack(boxes: &[[f64; 2]], gap: f64, aspect: f64) -> Vec<[f64; 2]> { rows(boxes, gap, span(boxes, gap, aspect)) } -/// How wide rows of `boxes` run for the rows' width over height to approach `aspect`: as wide as a rectangle of that -/// aspect holding all the boxes and their gaps. +/// The row width at which rows of `boxes` approach width over height `aspect`: the width of a +/// rectangle of that aspect whose area is the boxes' total area, gaps included. pub(super) fn span(boxes: &[[f64; 2]], gap: f64, aspect: f64) -> f64 { let area: f64 = boxes.iter().map(|[width, height]| (width + gap) * (height + gap)).sum(); (area * aspect).sqrt() } -/// Each box's top-left corner in rows `gap` apart, going down from the origin. A row breaks before a box that would -/// run it past `span`, so only a box wider than `span` makes a row wider. +/// Each box's top-left corner, in rows `gap` apart going down from the origin. A row breaks before +/// a box that would extend it past `span`, so only a box wider than `span` makes a row wider. pub(super) fn rows(boxes: &[[f64; 2]], gap: f64, span: f64) -> Vec<[f64; 2]> { let (mut x, mut top, mut tallest) = (0.0, 0.0, 0.0_f64); boxes @@ -37,7 +37,7 @@ pub(super) fn rows(boxes: &[[f64; 2]], gap: f64, span: f64) -> Vec<[f64; 2]> { mod tests { use super::*; - /// Boxes of uneven sizes, the same each run. + /// Boxes of uneven sizes, the same on every run. fn uneven(count: usize) -> Vec<[f64; 2]> { let mut seed = 0x2545_f491_u32; let mut next = move || { diff --git a/crates/latkit/src/layout/rect.rs b/crates/latkit/src/layout/rect.rs index 901ddb0f..fe5a2d54 100644 --- a/crates/latkit/src/layout/rect.rs +++ b/crates/latkit/src/layout/rect.rs @@ -1,4 +1,4 @@ -//! Boxes on the plane, least x and y then greatest, and the points they hold. +//! Axis-aligned boxes on the plane, as least x and y then greatest x and y, and points. /// A point on the plane. pub(crate) type Point = [f64; 2]; @@ -6,20 +6,20 @@ pub(crate) type Point = [f64; 2]; /// A box: least x, least y, greatest x, greatest y. pub(crate) type Rect = [f64; 4]; -/// A box holding nothing, which any union replaces. +/// The empty box. It is inverted, so a union with it yields the other box. pub(crate) const EMPTY: Rect = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY]; -/// The box holding `a` and `b`. +/// The smallest box containing `a` and `b`. pub(crate) fn union(a: Rect, b: Rect) -> Rect { [a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])] } -/// The box holding only `p`. +/// The zero-size box at `p`. pub(crate) fn dot(p: Point) -> Rect { [p[0], p[1], p[0], p[1]] } -/// The box `half` its width and height each way about `center`. +/// The box centered on `center` that reaches `half` (half width, half height) each way. pub(crate) fn around(center: Point, half: Point) -> Rect { [center[0] - half[0], center[1] - half[1], center[0] + half[0], center[1] + half[1]] } @@ -29,7 +29,7 @@ pub(crate) fn grow(r: Rect, by: f64) -> Rect { [r[0] - by, r[1] - by, r[2] + by, r[3] + by] } -/// Whether two boxes meet, touching included. +/// Whether two boxes overlap or touch. pub(crate) fn meets(a: &Rect, b: &Rect) -> bool { a[0] <= b[2] && a[2] >= b[0] && a[1] <= b[3] && a[3] >= b[1] } diff --git a/crates/latkit/src/layout/route.rs b/crates/latkit/src/layout/route.rs index cde99e4a..427be3c1 100644 --- a/crates/latkit/src/layout/route.rs +++ b/crates/latkit/src/layout/route.rs @@ -1,12 +1,12 @@ -//! Orthogonal routes: wires in horizontal and vertical runs around obstacles. +//! Orthogonal routes: wires of horizontal and vertical runs around obstacles. //! -//! Each net grows as a tree from its first terminal. Every terminal leaves its vertex along its side -//! by a stub, and each further terminal, nearest first, branches off the tree by the cheapest clear -//! elbow, or else by an A* search over a sparse visibility graph: the free stretches of the lines -//! through the nearby obstacles' corners, joined where they cross. Tracks then set apart the runs of -//! different nets that share a line. +//! Each net grows as a tree from its first terminal. Every terminal leaves its vertex by a short +//! stub along its side. Each further terminal, nearest first, branches off the tree by the +//! cheapest clear elbow, or else by an A* search over a sparse visibility graph: the free +//! stretches of the lines through nearby obstacles' corners, joined where they cross. Finally, +//! runs of different nets that share a line are spread onto separate tracks. //! -//! Sides are in plane units with y up: a top side faces +y. +//! Coordinates are plane units with y up: a top side faces +y. use std::cmp::Ordering; use std::collections::BinaryHeap; @@ -15,14 +15,14 @@ use super::boxes::Boxes; use super::rect::{Point, Rect, dot, grow, meets, union, valid}; use crate::scene::Side; -/// Where a wire meets a vertex: the point on its boundary, the side it leaves by (None: any), and the -/// obstacle it belongs to (an index into `obstacles`), which its own wire may touch. +/// Where a wire meets a vertex: the point on its boundary, the side it leaves by (`None`: any), and +/// the obstacle it belongs to (an index into `obstacles`), which its own wire may touch. #[derive(Clone, Copy, PartialEq, Debug)] pub(crate) struct Terminal { /// The point on the vertex's boundary. pub(crate) at: [f64; 2], - /// The side the wire leaves by; with none, the owner's side nearest `at`, or else the way toward - /// the rest of the net. + /// The side the wire leaves by. With none, the owner's side nearest `at`; without an owner, the + /// axis toward the rest of the net. pub(crate) side: Option, /// The obstacle the terminal belongs to. pub(crate) owner: Option, @@ -35,16 +35,17 @@ pub(crate) struct Net { pub(crate) terminals: Vec, } -/// Each net's route as polylines (a tree for nets of more than two terminals, sharing junction points), -/// running in horizontal and vertical segments around `obstacles` (each `[min x, min y, max x, max y]`), -/// leaving each terminal along its side, with parallel runs of different nets `spacing` apart. Runs of -/// `fixed` polylines, wires already routed, keep their tracks, and the new runs take tracks beside -/// them. A net whose route cannot avoid the obstacles still gets an orthogonal route through them. +/// Each net's route as polylines: a tree for nets of more than two terminals, sharing junction +/// points. Routes run in horizontal and vertical segments around `obstacles` (each +/// `[min x, min y, max x, max y]`), leave each terminal along its side, and keep parallel runs of +/// different nets `spacing` apart. Runs of `fixed` polylines (wires already routed) keep their +/// tracks, and new runs take tracks beside them. A net whose route cannot avoid the obstacles +/// still gets an orthogonal route through them. /// -/// Wires keep twice `spacing` from obstacles where they can. `groups` are soft: wires cross their -/// borders at a cost, so they enter and leave a group once, and keep off its borders. A net of fewer -/// than two terminals at finite points gets no polylines; obstacles with a non-finite or inverted box -/// are left out. +/// Wires keep twice `spacing` from obstacles where they can. `groups` are soft obstacles: crossing +/// a border costs extra, so wires enter and leave a group once and keep off its borders. A net +/// with fewer than two terminals at finite points gets no polylines. Obstacles with a non-finite +/// or inverted box are ignored. pub(crate) fn route( obstacles: &Boxes, groups: &[[f64; 4]], @@ -61,13 +62,13 @@ pub(crate) fn route( trees.iter().map(|tree| tree.as_ref().map_or_else(Vec::new, Tree::polylines)).collect() } -/// Arc points a rounded bend takes between its ends. +/// Points a rounded bend's arc has between its ends. const ARC: usize = 3; -/// A net's polylines as drawn: each bend between perpendicular runs rounded by up to `radius`, and -/// each part ending where another starts doubled at its end, so that point draws as a junction and -/// takes no arrowhead. No point repeats anywhere else, as where two bends' arcs meet on a short run: -/// it would draw as a junction too. +/// A net's polylines as drawn. Each bend between perpendicular runs is rounded with a radius of up +/// to `radius`. A part ending where another starts repeats its last point, so that point draws as +/// a junction without an arrowhead. No point repeats anywhere else (for example where two arcs +/// meet on a short run), since a repeat would draw as a junction too. pub(crate) fn finish(lines: &[Vec], radius: f64) -> Vec> { lines .iter() @@ -103,7 +104,7 @@ pub(crate) fn finish(lines: &[Vec], radius: f64) -> Vec> { .collect() } -/// Pushes `p` onto `out` unless it is the last point there again. +/// Pushes `p` onto `out` unless it repeats the last point (within `EPSILON`). fn push_new(out: &mut Vec, p: Point) { if out.last().is_none_or(|&q| manhattan(q, p) > EPSILON) { out.push(p); @@ -113,37 +114,37 @@ fn push_new(out: &mut Vec, p: Point) { /// How far wires keep from obstacles, in spacings. const CLEARANCE: f64 = 2.0; -/// Offsets below this are rounding: a point this near a box's side is on it. +/// Offsets below this are rounding error: a point this near a box's side is on it. const EPSILON: f64 = 1e-6; -/// Points nearer than this are one point. +/// Points closer than this are the same point. const SAME: f64 = 1e-9; -/// Attach points a branch tries, nearest first. +/// How many attach points a branch tries, nearest first. const ATTACHES: usize = 8; -/// Attach points a branch searches from when no elbow is clear. +/// How many attach points a search starts from when no elbow is clear. const SEARCHED: usize = 3; -/// Crossings past which a search gives up. +/// The number of visibility-graph crossings beyond which a search gives up. const MAX_CROSSINGS: usize = 1 << 20; -/// Steps a search takes before it gives up. +/// The number of states a search expands before it gives up. const MAX_VISITS: usize = 1 << 17; -/// Crossings a flood from a search's goal reaches before it takes the goal for open. +/// How many crossings a flood from the goal must reach before the goal counts as open. const POCKET: usize = 256; -/// Times a search widens its window over the obstacles that wall it in. +/// How many times a search widens its window to cover the obstacles walling it in. const WIDENINGS: usize = 1; -/// What crossing a group's border costs, in clearances of length. +/// The cost of crossing a group's border, in clearances of length. const CROSSING: f64 = 4.0; -/// No joint: the root's parent, or a joint without children. +/// No joint: the root's parent, or the end of a child list. const NONE: usize = usize::MAX; -/// No state: where a search starts. +/// No state: marks a search's starting states. const NOWHERE: u32 = u32::MAX; impl Side { @@ -158,14 +159,15 @@ impl Side { } } -/// A terminal as routing reads it: where its wire meets the vertex, the way out, and its owner's box. +/// A terminal resolved for routing: where its wire meets the vertex, its outward direction, and +/// its owner's box. struct End { at: Point, normal: Point, owner: Option, } -/// Plans nets among the obstacles, then sets their runs apart. +/// Routes nets among the obstacles, then spreads their shared runs onto tracks. struct Router<'a> { obstacles: Obstacles<'a>, groups: Groups, @@ -177,8 +179,8 @@ struct Router<'a> { } impl<'a> Router<'a> { - /// A router among `boxes` and `groups`, tracks `spacing` apart; a negative or non-finite spacing - /// is zero. + /// A router among `boxes` and `groups`, with tracks `spacing` apart. A negative or non-finite + /// spacing counts as zero. fn new(boxes: &'a Boxes, groups: &[Rect], spacing: f64) -> Router<'a> { let spacing = if spacing.is_finite() { spacing.max(0.0) } else { 0.0 }; let clearance = spacing * CLEARANCE; @@ -186,13 +188,13 @@ impl<'a> Router<'a> { Router { obstacles: Obstacles { boxes }, groups, spacing, clearance, search: Search::new(2.0 * clearance) } } - /// The shortest stub, and the least a track keeps a stub's end off its terminal. + /// The shortest stub, and the least distance a track keeps a stub's end from its terminal. fn least(&self) -> f64 { self.spacing / 8.0 } - /// A net's tree: the first terminal's stub, then each further terminal's branch, nearest first; - /// `None` for a net of fewer than two terminals at finite points. + /// A net's tree: the first terminal's stub, then each further terminal's branch, nearest + /// first. `None` for a net with fewer than two terminals at finite points. fn net(&mut self, net: &Net) -> Option { let terminals: Vec = net.terminals.iter().filter(|terminal| terminal.at.iter().all(|v| v.is_finite())).copied().collect(); @@ -224,8 +226,8 @@ impl<'a> Router<'a> { Some(tree) } - /// How routing reads a terminal. Its way out is its side; else its owner's side nearest it; else - /// the axis toward `toward`. + /// Resolves a terminal for routing. Its outward direction is its side's normal; else that of + /// its owner's side nearest it; else the axis toward `toward`. fn end(&self, terminal: Terminal, toward: Point) -> End { let owner = self.obstacles.owned(terminal.owner); let at = terminal.at; @@ -240,8 +242,8 @@ impl<'a> Router<'a> { End { at, normal, owner } } - /// Where a terminal's stub ends: clear of its owner by the clearance, or halfway to an obstacle - /// in the way. + /// Where a terminal's stub ends: past its owner by the clearance, or halfway to an obstacle in + /// the way, but at least `least` past the owner. fn stub(&self, end: &End) -> Point { let axis = usize::from(end.normal[0] == 0.0); let sign = end.normal[axis]; @@ -268,8 +270,8 @@ impl<'a> Router<'a> { point } - /// The cheapest clear way from the tree to a reader's stub end `end`: an elbow from one of the - /// nearest attach points; else a search around the obstacles at less and less clearance; else a + /// The cheapest clear path from the tree to a reader's stub end `end`: an elbow from one of the + /// nearest attach points; else a search around the obstacles at decreasing clearance; else a /// plain elbow through them. fn branch(&mut self, tree: &Tree, end: Point, reader: &End, root: &End) -> Branch { let mut attaches = tree.attaches(end); @@ -286,13 +288,13 @@ impl<'a> Router<'a> { best.consider(&[from, [middle[0], from[1]], [middle[0], end[1]], end], attach, clear); best.consider(&[from, [from[0], middle[1]], [end[0], middle[1]], end], attach, clear); } - // An elbow that keeps out of every group it need not cross is as good as any search finds. + // An elbow with no group cost is as good as anything a search would find. match best.branch.take() { Some(branch) if branch.points.windows(2).all(|w| self.groups.cost(w[0], w[1]) == 0.0) => return branch, elbow => best.branch = elbow, } - // The window frames the boxes a branch may have to go around: its terminal's, and the root's - // when the branch may leave the root's stub. + // The search window frames the boxes a branch may have to go around: its terminal's owner, + // and the root's owner when the branch may leave from the root's stub. let searched = &attaches[..attaches.len().min(SEARCHED)]; let from_root = searched.iter().any(|attach| attach.point == tree.points[1]); let owners: Vec = [reader.owner, root.owner.filter(|_| from_root)].into_iter().flatten().collect(); @@ -311,10 +313,10 @@ impl<'a> Router<'a> { Branch { points: simplify(&[attach.point, [end[0], attach.point[1]], end]), attach } } - /// Sets apart the runs of different nets that share a line: each takes its own track, `spacing` - /// apart where the corridor between obstacles has room, ordered by where its ends turn so runs - /// nest rather than cross. A run carries its joints, so branches stay joined; terminals never - /// move, and stubs only grow. + /// Spreads runs of different nets that share a line onto separate tracks, `spacing` apart where + /// the corridor between obstacles has room. Tracks are ordered by where the runs' ends turn, so + /// runs nest rather than cross. A run moves its joints, so branches stay connected; terminals + /// never move, and stubs only grow. fn separate(&self, trees: &mut [Option]) { let tolerance = self.spacing / 16.0; for axis in 0..2 { @@ -358,8 +360,8 @@ impl<'a> Router<'a> { false }); let mut order: Vec<&Run> = cluster.iter().collect(); - // By turn, then length (longest first where ends turn lower), then start: the last key - // first, as each sort keeps the order of ties. + // Order by turn, then length (longest first where ends turn lower), then start. Sort by the + // last key first, since each sort is stable. crate::order::by_key(&mut order, |a| crate::order::number(a.along[0])); crate::order::by_wide_key(&mut order, |a| { let length = crate::order::number(a.along[1] - a.along[0]); @@ -384,26 +386,26 @@ impl<'a> Router<'a> { } } -/// The axis direction an offset from a box's middle leaves it by, measured across the box's `size`. +/// The axis direction by which an offset from a box's center exits it, relative to its `size`. fn outward(offset: Point, size: Point) -> Point { let across = |k: usize| (offset[k] / size[k].max(f64::MIN_POSITIVE)).abs(); let away = |v: f64| if v < 0.0 { -1.0 } else { 1.0 }; if across(0) > across(1) { [away(offset[0]), 0.0] } else { [0.0, away(offset[1])] } } -/// What a joint of a tree is, which says how far tracks may move it. +/// The kind of a tree's joint, which limits how tracks may move it. #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Joint { /// A bend or a junction, free to move. Bend, /// A terminal, which never moves. Terminal, - /// A stub's outer end, whose stub from the terminal joint given only grows. + /// A stub's outer end. Its stub from the given terminal joint may only grow. Stub(usize), } -/// A net while it is planned: joints, each joined to its parent by an axis-aligned segment. The -/// first joint is the root's terminal. +/// A net being routed: joints, each joined to its parent by an axis-aligned segment. The first +/// joint is the root terminal. #[derive(Default)] struct Tree { points: Vec, @@ -412,7 +414,7 @@ struct Tree { } impl Tree { - /// A wire already routed, as a chain of joints none of which moves: its runs hold their tracks. + /// A wire already routed, as a chain of joints that never move, so its runs keep their tracks. fn fixed(line: &[Point]) -> Tree { let mut tree = Tree::default(); let mut parent = NONE; @@ -430,14 +432,14 @@ impl Tree { self.points.len() - 1 } - /// A joint at `point` on the segment from `child` to its parent. + /// Inserts a joint at `point` on the segment from `child` to its parent. fn split(&mut self, child: usize, point: Point) -> usize { let joint = self.add(point, self.parent[child], Joint::Bend); self.parent[child] = joint; joint } - /// Where a branch may attach, nearest `toward` first: each joint but a terminal, and each + /// Where a branch may attach, nearest to `toward` first: each joint except terminals, and each /// segment's nearest point to `toward`. Segments into terminals are stubs, which no branch /// attaches inside. fn attaches(&self, toward: Point) -> Vec { @@ -466,7 +468,8 @@ impl Tree { out } - /// The way from a joint `attach` is at along its stub into a terminal, which no branch leaves by. + /// When `attach` is at a joint with a stub into a terminal, the direction along that stub, + /// which no branch may leave by. fn stub_way(&self, attach: Attach) -> Option { if attach.interior { return None; @@ -479,8 +482,8 @@ impl Tree { .map(|terminal| direction(attach.point, self.points[terminal])) } - /// Moves a path's start along the tree while its first step runs along a segment of the tree, so - /// the path attaches where it leaves the tree. Stubs, which no branch attaches inside, stop it. + /// Moves a path's start along the tree while its first step runs along a tree segment, so the + /// path attaches where it leaves the tree. Stubs stop it, since no branch attaches inside one. fn slide(&self, path: &mut Vec, mut attach: Attach) -> Attach { for _ in 0..self.points.len() { let [start, next, ..] = path[..] else { break }; @@ -555,7 +558,7 @@ struct Attach { interior: bool, } -/// A branch from a tree to a terminal's stub end: its path, from where it attaches. +/// A branch from a tree to a terminal's stub end: its path, starting where it attaches. struct Branch { points: Vec, attach: Attach, @@ -564,9 +567,9 @@ struct Branch { /// The cheapest branch found so far. struct Best<'t> { tree: &'t Tree, - /// The way a branch enters the stub end: against the terminal's side. + /// The direction a branch enters the stub end: opposite the terminal's outward normal. into: Point, - /// What a bend costs, in length. + /// The cost of a bend, in length. bend: f64, groups: &'t Groups, cost: f64, @@ -574,8 +577,8 @@ struct Best<'t> { } impl Best<'_> { - /// Keeps the path through `points` from `attach` when it turns well, costs less than the best so - /// far, and, with `clear`, keeps out of the obstacles grown by the clearance given. + /// Keeps the path through `points` from `attach` if it turns well, costs less than the best so + /// far, and, with `clear`, avoids the obstacles grown by the given clearance. fn consider(&mut self, points: &[Point], attach: Attach, clear: Option<(&Obstacles, f64)>) { let mut path = simplify(points); let attach = self.tree.slide(&mut path, attach); @@ -595,9 +598,9 @@ impl Best<'_> { } } - /// Whether a path from the tree to the stub end turns well: it never doubles back along the - /// stub, leaves a segment it attaches inside across it, and leaves a joint along none of the - /// segments already there. + /// Whether a path from the tree to the stub end turns well: it does not double back along the + /// stub, it leaves a segment it attaches inside at a right angle, and it leaves a joint along + /// none of the joint's segments. fn fits(&self, path: &[Point], attach: Attach) -> bool { let (Some(&[start, next]), Some(&[before, last])) = (path.first_chunk(), path.last_chunk()) else { return path.len() == 1; @@ -605,7 +608,8 @@ impl Best<'_> { direction(before, last) != [-self.into[0], -self.into[1]] && self.leaves_well(start, next, attach) } - /// Whether a path's first step, from `start` toward `next`, leaves the tree well. + /// Whether a path's first step, from `start` toward `next`, leaves the tree well: at a right + /// angle to a segment it attaches inside, or along none of a joint's segments. fn leaves_well(&self, start: Point, next: Point, attach: Attach) -> bool { let tree = self.tree; let parent = tree.parent[attach.child]; @@ -623,8 +627,8 @@ impl Best<'_> { } } -/// What a search joins: the points it may leave from, each with a way it may not leave by, and the -/// point it reaches, with a way it may not arrive moving. +/// A search's ends: the points it may start from, each with a direction it may not leave by, and +/// its goal, with a direction it may not arrive moving in. struct Leg<'p> { from: &'p [(Point, Option)], to: Point, @@ -633,26 +637,27 @@ struct Leg<'p> { /// How a search over one window ended. enum Outcome { - /// The path found, from the leg's start given. + /// The path found, with the index of the leg's start it begins at. Found(usize, Vec), - /// No way inside the window, though a wider one may hold one. + /// No path inside the window, though a wider window may have one. Walled, - /// No way at all, or too much to look through. + /// No path at all, or too much to search. Unreachable, } -/// What a flood from a search's goal found before the search spreads over the window. +/// What a flood from the goal found before the full search. enum Reach { - /// A start, or more than a pocket: the search is worth running. + /// A start, or more than `POCKET` crossings: the search is worth running. Open, - /// A pocket without a start that reaches the window's edge: a wider window may join them. + /// A small region around the goal, without a start, touching the window's edge: a wider + /// window may reach a start. Apart, - /// A pocket without a start, walled in inside the window. + /// A small region around the goal, without a start, walled in inside the window. Enclosed, } -/// A state waiting in a search: its estimated total cost, its cost so far, and the state, a crossing -/// and the axis of the step that reached it. +/// A state queued in a search: its estimated total cost, its cost so far, and the state (a +/// crossing and the axis of the step that reached it). #[derive(Clone, Copy)] struct Step { estimate: f64, @@ -661,7 +666,7 @@ struct Step { } impl Ord for Step { - // The heap pops its greatest: the least estimate, then the farthest along, then the first state. + // The max-heap pops the least estimate first, then the greatest cost, then the lowest state. fn cmp(&self, other: &Step) -> Ordering { other .estimate @@ -685,7 +690,7 @@ impl PartialEq for Step { impl Eq for Step {} -/// A free stretch of a line among a search's obstacles: the line's place across, and where the +/// A free stretch of a line among a search's obstacles: the line's coordinate, and where the /// stretch starts and ends along it. #[derive(Clone, Copy, PartialEq)] struct Span { @@ -700,17 +705,17 @@ struct Crossing { point: Point, /// The horizontal span, then the vertical one. spans: [u32; 2], - /// Its place in the search's columns. + /// Its index in the search's `columns`. place: u32, } -/// Searches for orthogonal paths around obstacles over a sparse visibility graph: the free spans of -/// the lines through the obstacles' corners, the path's two ends, and the window's edges, which meet -/// where they cross. Its scratch is shared by every search, grown and never shrunk. +/// Searches for orthogonal paths around obstacles over a sparse visibility graph: the free spans +/// of the lines through the obstacles' corners, the path's ends, and the window's edges, with +/// vertices where they cross. Every search reuses its scratch buffers, which never shrink. struct Search { - /// What a bend costs, in length. + /// The cost of a bend, in length. bend: f64, - /// How far a window reaches past what it frames. + /// How far a window extends past the points and boxes it frames. margin: f64, /// The graph's vertices: horizontal span by span, each from left to right. crossings: Vec, @@ -718,18 +723,19 @@ struct Search { columns: Vec, /// Where each vertical span's crossings start in `columns`, then where the last ends. starts: Vec, - /// The cheapest cost found to each state: a crossing, and the axis of the step that reached it. + /// The cheapest cost found to each state (a crossing and the axis of the step that reached it). costs: Vec, /// The state each state was reached from. came: Vec, heap: BinaryHeap, - /// Which crossings a flood has reached, and the order it reached them in. + /// Which crossings a flood has reached. seen: Vec, + /// The crossings a flood reached, in the order it reached them. queue: Vec, } impl Search { - /// A search whose bends cost, and whose windows reach, `reach`. + /// A search where a bend costs `reach`, and windows extend `reach` past what they frame. fn new(reach: f64) -> Search { Search { bend: reach, @@ -745,10 +751,10 @@ impl Search { } } - /// The cheapest orthogonal path for `leg` around the obstacles grown by `by`, from whichever start - /// is cheapest, in a window framing the leg and the boxes `around`. The window widens over - /// obstacles that wall it in. `None` when the leg's end or every start is inside an obstacle, or no - /// way is found. + /// The cheapest orthogonal path for `leg` around the obstacles grown by `by`, from the cheapest + /// start, within a window framing the leg and the boxes `around`. The window widens to cover + /// obstacles that wall it in. `None` when the goal or every start is inside an obstacle, or no + /// path is found. fn path( &mut self, obstacles: &Obstacles, @@ -779,8 +785,8 @@ impl Search { None } - /// Builds the window's visibility graph through `ends` among `boxes`, the grown obstacles that - /// meet it; false when it holds too many crossings to search. + /// Builds the window's visibility graph through `ends` among `boxes` (the grown obstacles that + /// meet the window). Returns false when it has too many crossings to search. fn build(&mut self, boxes: &[Rect], window: Rect, ends: &[Point]) -> bool { let horizontal = spans(boxes, window, ends, 0); let vertical = spans(boxes, window, ends, 1); @@ -799,7 +805,7 @@ impl Search { return false; } } - // Each vertical span's crossings, which come bottom to top as the rows do. + // Index each vertical span's crossings; they come bottom to top, as the rows do. self.starts.clear(); self.starts.resize(vertical.len() + 1, 0); for crossing in &self.crossings { @@ -821,10 +827,10 @@ impl Search { true } - /// A* over the window's visibility graph for `leg` among `boxes`, the grown obstacles that meet - /// it, its steps costing their crossings of `groups`' borders: each state a crossing and the axis - /// of the step that reached it. The groups' lanes, just inside and outside their borders, are - /// lines of the graph too. + /// Runs A* over the window's visibility graph for `leg` among `boxes` (the grown obstacles that + /// meet the window). Each state is a crossing plus the axis of the step that reached it, and + /// each step also pays its `groups` border cost. The groups' lanes, just inside and outside + /// their borders, are lines of the graph too. fn graph(&mut self, boxes: &[Rect], groups: &Groups, window: Rect, leg: &Leg) -> Outcome { let groups = groups.within(window); let mut ends: Vec = leg.from.iter().map(|&(start, _)| start).chain([leg.to]).collect(); @@ -876,7 +882,7 @@ impl Search { } let crossing = self.crossings[vertex]; edge |= on_edge(crossing.point, &window); - // A start's first step leaves by any way but its banned one. + // A start's first step may leave in any direction but its banned one. let first = self.came[state] == NOWHERE; let banned = if first { sources.iter().find(|source| source.0 == vertex).and_then(|source| source.1) } else { None }; @@ -900,12 +906,13 @@ impl Search { self.heap.push(Step { estimate, cost: value, state: reached as u32 }); } } - // A start walled in short of the window's edge stays walled in however wide the window. + // If the search never reached the window's edge, a wider window would not help. if edge { Outcome::Walled } else { Outcome::Unreachable } } - /// The least a path can cost from `point`, reached along `axis` (`None` at a start, whose first - /// step turns for free), to `to`: its length, and a bend for the turn it still needs. + /// A lower bound on the cost from `point`, reached along `axis`, to `to`: the Manhattan + /// distance, plus a bend if a turn is still needed. `axis` is `None` at a start, whose first + /// step turns for free. fn estimate(&self, point: Point, axis: Option, to: Point) -> f64 { let turns = match ([point[0] != to[0], point[1] != to[1]], axis) { ([true, true], _) | ([true, false], Some(1)) | ([false, true], Some(0)) => 1.0, @@ -930,8 +937,8 @@ impl Search { ] } - /// Floods the graph from `goal` for a pocket's worth of crossings, to fail fast where the goal is - /// walled in apart from every start. + /// Floods the graph from `goal` for up to `POCKET` crossings, to fail fast when the goal is + /// walled off from every start. fn flood(&mut self, goal: usize, sources: &[(usize, Option)], window: Rect) -> Reach { self.seen.clear(); self.seen.resize(self.crossings.len(), false); @@ -955,7 +962,7 @@ impl Search { if edge { Reach::Apart } else { Reach::Enclosed } } - /// The path that reached state `goal`, from the start. + /// The simplified path to state `goal`, from its start. fn trace(&self, goal: usize) -> Vec { let mut path = Vec::new(); let mut state = goal; @@ -971,15 +978,16 @@ impl Search { } } -/// The free spans of lines running along `along` (0: x, 1: y) within `window`, among `boxes`, the -/// grown obstacles that meet it: through each box's corners, or where its sides' lines enter the -/// window, through each of `ends`, and every span along the window's edges. Sorted by line, then -/// start. +/// The free spans of lines running along axis `along` (0: x, 1: y) within `window`, among `boxes` +/// (the grown obstacles that meet it). Lines pass through each box's corners (clamped into the +/// window) and through each of `ends`, taking the free span at that point; lines along the +/// window's edges take every free span. Sorted by line, then start. fn spans(boxes: &[Rect], window: Rect, ends: &[Point], along: usize) -> Vec { let held = 1 - along; let (start, end) = (window[along], window[along + 2]); let (low, high) = (window[held], window[held + 2]); - // A seed is a line and a point along it its span holds; NaN takes every span of the line. + // A seed is a line and a point along it, which selects the free span holding that point; NaN + // selects every free span of the line. let mut seeds: Vec = boxes .iter() .flat_map(|r| { @@ -993,7 +1001,7 @@ fn spans(boxes: &[Rect], window: Rect, ends: &[Point], along: usize) -> Vec = (0..boxes.len()).collect(); crate::order::by_key(&mut order, |&a| crate::order::number(boxes[a][held])); let mut order = order.into_iter().peekable(); @@ -1007,7 +1015,7 @@ fn spans(boxes: &[Rect], window: Rect, ends: &[Point], along: usize) -> Vec Vec, - /// How far wires keep from obstacles: a border's lanes lie half of it inside and outside. + /// How far wires keep from obstacles. A border's lanes lie half this inside and outside it. clearance: f64, } impl Groups { - /// What a run from `a` to `b` costs past its length: [`CROSSING`] clearances for each border it - /// crosses, and its length again where it runs along a border, nearer than half a clearance. + /// The cost of a run from `a` to `b` beyond its length: [`CROSSING`] clearances per border it + /// crosses, plus the length it runs along a border closer than half a clearance. fn cost(&self, a: Point, b: Point) -> f64 { let along = usize::from(a[0] == b[0]); let held = 1 - along; @@ -1083,8 +1091,8 @@ impl Groups { Groups { boxes: self.boxes.iter().copied().filter(|r| meets(r, &window)).collect(), clearance: self.clearance } } - /// The corners of the lanes along every group's borders, half a clearance inside and outside each: - /// points the search's lines run through. + /// The corners of the lanes along every group's borders, half a clearance inside and outside, + /// which the search's lines pass through. fn lanes(&self) -> Vec { let half = self.clearance / 2.0; let mut out = Vec::new(); @@ -1100,7 +1108,7 @@ impl Groups { } } -/// The obstacles routing avoids, as boxes kept indexed give them. +/// The obstacles routing avoids, read from an index of boxes. struct Obstacles<'a> { boxes: &'a Boxes, } @@ -1111,8 +1119,8 @@ impl Obstacles<'_> { owner.and_then(|owner| self.boxes.get(owner as usize)) } - /// Visits the obstacles meeting `region`, each grown by `by`, until `visit` returns true; whether - /// it did. + /// Visits the obstacles meeting `region`, each grown by `by`, until `visit` returns true. + /// Returns whether it did. fn some(&self, region: Rect, by: f64, mut visit: impl FnMut(Rect) -> bool) -> bool { self.boxes.some(region, &mut |item| visit(grow(self.boxes.get(item).expect("a box visited"), by))) } @@ -1127,7 +1135,7 @@ impl Obstacles<'_> { out } - /// Whether the axis-aligned segment from `a` to `b` keeps out of every obstacle grown by `by`. + /// Whether the axis-aligned segment from `a` to `b` avoids every obstacle grown by `by`. fn clear(&self, a: Point, b: Point, by: f64) -> bool { let span = grow([a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])], by); !self.some(span, by, |obstacle| crosses(a, b, &obstacle)) @@ -1144,14 +1152,14 @@ fn on_edge(p: Point, r: &Rect) -> bool { p[0] == r[0] || p[0] == r[2] || p[1] == r[1] || p[1] == r[3] } -/// Whether a point is strictly inside a box. +/// Whether a point is strictly inside a box, by more than `EPSILON`. fn inside(p: Point, r: &Rect) -> bool { p[0] > r[0] + EPSILON && p[0] < r[2] - EPSILON && p[1] > r[1] + EPSILON && p[1] < r[3] - EPSILON } /// Whether the axis-aligned segment from `a` to `b` passes through a box's interior. fn crosses(a: Point, b: Point, r: &Rect) -> bool { - // A vertical segment holds its x and runs along y; a horizontal one holds its y. + // A vertical segment has a fixed x and runs along y; a horizontal one has a fixed y. let held = usize::from(a[0] != b[0]); let runs = 1 - held; a[held] > r[held] + EPSILON @@ -1165,7 +1173,7 @@ fn between(v: f64, a: f64, b: f64) -> bool { a.min(b) <= v && v <= a.max(b) } -/// The length of an orthogonal path between two points. +/// The Manhattan distance between two points: the length of an orthogonal path between them. fn manhattan(a: Point, b: Point) -> f64 { (a[0] - b[0]).abs() + (a[1] - b[1]).abs() } @@ -1181,12 +1189,12 @@ fn sign(v: f64) -> f64 { } } -/// The axis signs of the way from one point to another. +/// The sign of each axis of the direction from `from` to `to`. fn direction(from: Point, to: Point) -> Point { [sign(to[0] - from[0]), sign(to[1] - from[1])] } -/// A path's points without repeats or in-line middles. +/// A path's points with repeats and collinear middle points removed. pub(crate) fn simplify(path: &[Point]) -> Vec { let mut out: Vec = Vec::with_capacity(path.len()); for &p in path { @@ -1206,27 +1214,27 @@ pub(crate) fn simplify(path: &[Point]) -> Vec { /// A net's chain of segments along one line, which a track moves as one. struct Run { net: usize, - /// The joints it carries. + /// The joints a track moves with it. joints: Vec, /// The line's coordinate on the axis tracks move it along. line: f64, /// Where it starts and ends along the line. along: [f64; 2], - /// Where its ends turn: below zero toward lower lines, above toward higher. + /// Which way its ends turn: negative toward lower lines, positive toward higher. turn: f64, - /// The lines tracks may move it between; one line for a run holding a terminal. + /// The lines tracks may move it between; a single line for a run holding a terminal. limits: [f64; 2], } impl Run { - /// Whether it holds a terminal, so no track moves it. + /// Whether its limits allow one line only (it holds a terminal), so no track moves it. fn held(&self) -> bool { self.limits[0] == self.limits[1] } } -/// Every net's runs on lines across `axis`: maximal chains of segments along one line through -/// joints. A stub's end keeps `least` off its terminal. +/// Every net's runs whose line has a fixed `axis` coordinate: maximal chains of segments along one +/// line. A stub's end keeps at least `least` from its terminal. fn runs(trees: &[Option], axis: usize, least: f64) -> Vec { let mut runs = Vec::new(); for (net, tree) in trees.iter().enumerate() { @@ -1327,7 +1335,7 @@ mod tests { segments(wire).iter().all(|[a, b]| boxes.iter().all(|r| !crosses(*a, *b, r))) } - /// The point next to the terminal at `at` on its wire: the way the wire leaves it. + /// The point after the terminal at `at` on its wire, which shows the way the wire leaves it. fn leaving(wire: &[Vec], at: Point) -> Point { wire.iter() .find_map(|line| match line[..] { @@ -1338,8 +1346,8 @@ mod tests { .expect("the wire ends at the terminal") } - /// Whether the polylines make one tree reaching every terminal: each starts at the first terminal - /// or where another ends. + /// Whether the polylines make one tree reaching every terminal: each starts at the first + /// terminal or where another ends. fn one_tree(wire: &[Vec], terminals: &[Terminal]) -> bool { let mut reached = vec![terminals[0].at]; let mut joined = vec![false; wire.len()]; @@ -1373,7 +1381,8 @@ mod tests { #[test] fn wires_go_around_groups_they_do_not_join() { - // A thin group on the straight way between two blocks: through it is shorter, around keeps out. + // A thin group lies on the straight path between two blocks: going through is shorter, + // going around stays outside it. let boxes = [[20.0, 45.0, 40.0, 55.0], [160.0, 45.0, 180.0, 55.0]]; let group = [90.0, 44.0, 110.0, 56.0]; let nets = [Net { terminals: vec![on([40.0, 50.0], Side::Right, 0), on([160.0, 50.0], Side::Left, 1)] }]; @@ -1385,19 +1394,19 @@ mod tests { } #[test] - fn bends_meeting_on_a_short_run_repeat_no_point() { + fn bends_meeting_on_a_short_run_do_not_repeat_points() { // An S-bend whose middle run is shorter than two radii: the arcs of its bends meet halfway. let wire = finish(&[vec![[0.0, 0.0], [20.0, 0.0], [20.0, 10.0], [40.0, 10.0]]], 8.0); let line = &wire[0]; - assert!(line.windows(2).all(|w| manhattan(w[0], w[1]) > EPSILON), "no point twice: {line:?}"); + assert!(line.windows(2).all(|w| manhattan(w[0], w[1]) > EPSILON), "no repeated point: {line:?}"); assert_eq!((line[0], line[line.len() - 1]), ([0.0, 0.0], [40.0, 10.0])); - // A part ending where another starts still doubles its end: a junction. + // A part ending where another starts still repeats its last point: a junction. let tree = finish(&[vec![[0.0, 0.0], [10.0, 0.0]], vec![[10.0, 0.0], [10.0, 10.0]]], 8.0); assert_eq!(tree[0], [[0.0, 0.0], [10.0, 0.0], [10.0, 0.0]]); } #[test] - fn crossing_and_running_along_a_border_cost() { + fn crossing_or_running_along_a_group_border_adds_cost() { let groups = Groups { boxes: vec![[0.0, 0.0, 100.0, 90.0]], clearance: 2.0 * SPACING }; assert_eq!(groups.cost([-10.0, 10.0], [10.0, 10.0]), CROSSING * 2.0 * SPACING, "a crossing"); assert_eq!(groups.cost([10.0, 10.0], [20.0, 10.0]), 0.0, "inside, away from the border"); @@ -1405,7 +1414,7 @@ mod tests { } #[test] - fn a_wall_too_long_for_the_window_is_gone_around() { + fn a_wall_longer_than_the_search_window_is_routed_around() { let boxes = [ [0.0, 0.0, 20.0, 20.0], [200.0, 0.0, 220.0, 20.0], @@ -1503,7 +1512,7 @@ mod tests { } #[test] - fn a_crowded_grid_routes_alike_every_time() { + fn a_crowded_grid_routes_the_same_every_time() { let mut boxes = Vec::new(); for row in 0..30 { for column in 0..30 { @@ -1553,7 +1562,7 @@ mod tests { } #[test] - fn nets_without_two_terminals_get_nothing() { + fn nets_without_two_valid_terminals_get_no_wire() { let free = Terminal { at: [0.0, 0.0], side: None, owner: None }; let nets = [ Net::default(), diff --git a/crates/latkit/src/layout/stress.rs b/crates/latkit/src/layout/stress.rs index 81fed7b0..bbe1bb29 100644 --- a/crates/latkit/src/layout/stress.rs +++ b/crates/latkit/src/layout/stress.rs @@ -1,9 +1,10 @@ -//! Stress majorization: vertices at graph distances, then their boxes parted. Each vertex's terms are -//! its neighbors a gap away and a few pivots at their graph distance, so a round costs O(vertices × -//! pivots). Pinned vertices hold the rest; vertices start near the pinned ones they link to, or -//! around their part's most linked vertex, and parts nothing pins pack apart. Groups nest as -//! [`nested`] lays them out: a group's members settle by stress among themselves, and the group by -//! stress among the vertices and groups beside it. +//! Stress majorization: places vertices at their graph distances, then separates their boxes. +//! +//! Each vertex's terms are its neighbors, a gap away, and a few pivots at their graph distance, so +//! a round costs O(vertices × pivots). Pinned vertices stay put and anchor the rest. Vertices start +//! near the pinned vertices they connect to, or around their part's most linked vertex, and parts +//! with no pinned vertex pack apart. Groups nest as [`nested`] lays them out: a group's members +//! settle by stress among themselves, and the group among the vertices and groups beside it. use std::collections::VecDeque; @@ -11,21 +12,22 @@ use super::levels::{Graph, Level, nested}; use super::overlap::separate; use super::pack::pack; -/// Jacobi rounds stress runs. +/// Jacobi iterations per level. const ROUNDS: usize = 240; -/// Pivots stress measures graph distances from, and the distances it keeps in all. +/// The most pivots stress measures graph distances from. const PIVOTS: usize = 32; +/// The most graph distances it stores in all (pivots × vertices). const DISTANCES: usize = 8_000_000; -/// The share of the gap vertices' boxes keep clear of each other. +/// The fraction of the gap that vertices' boxes keep clear of each other. const CLEARANCE: f64 = 0.25; -/// Every vertex's center: neighbors `gap` apart, pinned vertices where they are, boxes a quarter of -/// the gap clear of each other; links' directions do not matter. +/// Every vertex's center from a stress layout: neighbors `gap` apart, pinned vertices fixed, and +/// boxes a quarter gap clear of each other. Link direction is ignored. pub(crate) fn stress(graph: &Graph<'_>, gap: f64) -> Vec<[f64; 2]> { nested(graph, |level| place(level, gap)) } -/// One level's centers. +/// One level's centers: `ROUNDS` Jacobi iterations, then overlap removal. fn place(level: &Level, gap: f64) -> Vec<[f64; 2]> { let n = level.sizes.len(); if n == 0 { @@ -47,7 +49,7 @@ fn place(level: &Level, gap: f64) -> Vec<[f64; 2]> { let there = position[other as usize]; let apart = [here[0] - there[0], here[1] - there[1]]; let length = apart[0].hypot(apart[1]); - // Vertices at one point part along a direction of their own. + // Coincident vertices separate along a direction derived from their index. let toward = if length > 1e-9 { [apart[0] / length, apart[1] / length] } else { @@ -70,11 +72,14 @@ fn place(level: &Level, gap: f64) -> Vec<[f64; 2]> { separate(&boxes, &pinned, gap * CLEARANCE) } -/// Each vertex's terms: its neighbors one `gap` away, weighted by the inverse square, and pivots at -/// their graph distance, weighted by the vertices nearer the pivot than half that distance. +/// Each vertex's terms as (other vertex, target distance, weight): its neighbors one `gap` away, +/// weighted by the inverse square of that distance; and pivots at their graph distance times +/// `gap`, weighted by the inverse square times how many vertices of the pivot's region lie within +/// half that distance of it. fn terms(n: usize, gap: f64, offsets: &[u32], targets: &[u32]) -> Vec> { let degree = |v: usize| offsets[v + 1] - offsets[v]; - // Pivots far from each other: each the vertex farthest from those before, other parts first. + // Pivots far apart: each is the vertex farthest from the earlier ones, so vertices they cannot + // reach (other parts) come first. let count = PIVOTS.min(n).min((DISTANCES / n.max(1)).max(2)); let mut pivots: Vec<(usize, Vec)> = Vec::with_capacity(count); let mut nearest = vec![u32::MAX; n]; @@ -121,9 +126,10 @@ fn terms(n: usize, gap: f64, offsets: &[u32], targets: &[u32]) -> Vec Vec<[f64; 2]> { let n = level.sizes.len(); let mut start = vec![[0.0; 2]; n]; @@ -161,7 +167,7 @@ fn start(level: &Level, gap: f64, offsets: &[u32], targets: &[u32]) -> Vec<[f64; }; for (p, inside) in parts.iter().enumerate() { let pins: Vec<[f64; 2]> = inside.iter().filter_map(|&v| level.pinned[v]).collect(); - // Hops from the pinned vertices, or from the part's first, most linked one. + // Hops from the pinned vertices, or else from the part's first vertex, its most linked. let sources: Vec = if pins.is_empty() { vec![inside[0]] } else { @@ -204,7 +210,7 @@ fn start(level: &Level, gap: f64, offsets: &[u32], targets: &[u32]) -> Vec<[f64; if loose.is_empty() { return start; } - // Parts nothing pins pack in rows below whatever is pinned. + // Parts with no pinned vertex pack in rows below the pinned vertices. let pinned: Vec<[f64; 2]> = level.pinned.iter().flatten().copied().collect(); let below = pinned.iter().map(|p| p[1]).fold(f64::INFINITY, f64::min); let left = pinned.iter().map(|p| p[0]).fold(f64::INFINITY, f64::min); @@ -219,7 +225,8 @@ fn start(level: &Level, gap: f64, offsets: &[u32], targets: &[u32]) -> Vec<[f64; start } -/// Each vertex's neighbors, once each. +/// Each vertex's neighbors, each once, ignoring direction, self-links, and links out of range: as +/// offsets into one list of targets. fn neighbors(n: usize, links: &[[u32; 2]]) -> (Vec, Vec) { let mut pairs: Vec<(u32, u32)> = links .iter() @@ -238,7 +245,7 @@ fn neighbors(n: usize, links: &[[u32; 2]]) -> (Vec, Vec) { (offsets, pairs.into_iter().map(|(_, b)| b).collect()) } -/// Hops from `from` to every vertex; `u32::MAX` where none lead. +/// Hop counts from `from` to every vertex; `u32::MAX` where unreachable. fn hops(from: usize, offsets: &[u32], targets: &[u32]) -> Vec { let mut distance = vec![u32::MAX; offsets.len() - 1]; let mut queue = VecDeque::from([from]); @@ -262,7 +269,7 @@ mod tests { (a[0] - b[0]).hypot(a[1] - b[1]) } - /// `sizes` laid out by stress with `links`, `pinned`, and `groups`, groups padding their members by 5. + /// `sizes` laid out by stress with `links`, `pinned`, and `groups` (groups pad members by 5). fn laid( sizes: &[[f64; 2]], links: &[[u32; 2]], @@ -285,7 +292,7 @@ mod tests { } #[test] - fn pins_hold_and_boxes_part() { + fn pins_stay_put_and_boxes_do_not_overlap() { let pinned = [Some([-100.0, 60.0]), None, None, None]; let p = laid(&[[60.0, 20.0]; 4], &[[0, 1], [1, 2], [1, 3]], &pinned, &[None; 4], 10.0); assert_eq!(p[0], [-100.0, 60.0]); @@ -298,7 +305,7 @@ mod tests { } #[test] - fn groups_hold_their_members_apart_from_each_other() { + fn group_boxes_do_not_overlap() { // Groups 0 and 1 hold three linked vertices each, and link through their members. let sizes = [[0.0; 2], [0.0; 2], [20.0, 10.0], [20.0, 10.0], [20.0, 10.0], [20.0, 10.0], [20.0, 10.0], [20.0, 10.0]]; @@ -312,6 +319,6 @@ mod tests { }; let (a, b) = (frame(&[2, 3, 4]), frame(&[5, 6, 7])); let overlap = a[0] < b[2] && b[0] < a[2] && a[1] < b[3] && b[1] < a[3]; - assert!(!overlap, "the groups' boxes stand apart: {a:?} {b:?}"); + assert!(!overlap, "the groups' boxes do not overlap: {a:?} {b:?}"); } } diff --git a/crates/latkit/src/lib.rs b/crates/latkit/src/lib.rs index 48253520..36d301d5 100644 --- a/crates/latkit/src/lib.rs +++ b/crates/latkit/src/lib.rs @@ -1,16 +1,16 @@ -//! Columnar data drawn as vertices, edges, lines, and areas on the GPU. +//! Columnar data drawn on the GPU as vertices, edges, lines, and areas. //! -//! A [`Data`] is a named collection of [`Table`]s, changed in place by [`Patch`]es; a [`Snapshot`] -//! keeps one version of it, O(1). A [`Scene`] describes what to draw and how it answers input: marks -//! that name tables of datasets, in a space, with a theme and controls. A [`View`] draws a scene into -//! any wgpu frame from the [`Source`]s it is given, each a dataset by name, and turns platform -//! [`Input`] into [`Event`]s. +//! A [`Data`] is a named collection of [`Table`]s, changed in place by [`Patch`]es. A [`Snapshot`] +//! is one immutable version of a `Data`, taken in O(1). A [`Scene`] describes what to draw and how +//! it responds to input: marks that reference tables of datasets, a space, a theme, and controls. A +//! [`View`] draws a scene into any wgpu frame from its [`Source`]s, one per dataset name, and turns +//! platform [`Input`] into [`Event`]s. //! -//! Every input is plain data that deserializes: [`from_json`] reads one from JSON text, an error -//! naming the path to what it could not read, and [`schema`] gives its JSON Schema. +//! Every input is plain data that deserializes. [`from_json`] reads one from JSON text and reports +//! errors with the path to the part it could not read. [`schema`] returns its JSON Schema. -// Without `gpu`, what only drawing reads of the data, expressions, and sorts has no reader; builds -// with it, which lint every feature, find what nothing reads. +// Without `gpu`, the parts of data, expressions, and sorts that only drawing uses are dead code. +// Builds with `gpu` lint every feature, so they still catch code nothing uses. #![cfg_attr(not(feature = "gpu"), allow(dead_code))] mod data; diff --git a/crates/latkit/src/order.rs b/crates/latkit/src/order.rs index 8d4161e4..6ac434a5 100644 --- a/crates/latkit/src/order.rs +++ b/crates/latkit/src/order.rs @@ -1,26 +1,28 @@ -//! The sorts latkit compiles. Each sort by a closure compiles the standard library's sort anew, so -//! every keyed sort ends in one of a few sorts of keys: items ordered by a key, least first, ties in -//! the order they came. +//! Stable sorts that share a few compiled sort functions. +//! +//! Each closure passed to a standard library sort compiles another copy of the sort. So keyed sorts +//! here build an array of keys, sort it with one of a few non-generic functions, and permute the +//! items to match. Keys sort ascending, and ties keep their input order. -/// Sorts keys packed with the index they came from: the one sort of 64-bit keys. +/// Sorts 64-bit keys packed above their 32-bit source index. Every 64-bit keyed sort uses it. #[inline(never)] fn sort_narrow(keys: &mut [u128]) { keys.sort_unstable(); } -/// Sorts 128-bit keys with the index they came from. +/// Sorts 128-bit keys paired with their source index. #[inline(never)] fn sort_wide(keys: &mut [(u128, u32)]) { keys.sort_unstable(); } -/// Sorts names with the index they came from. +/// Sorts names paired with their source index. #[inline(never)] fn sort_names(keys: &mut [(&str, u32)]) { keys.sort_unstable(); } -/// `items` ordered by `key`, least first, ties in the order they came. +/// Sorts `items` by `key`, ascending. Ties keep their input order. pub(crate) fn by_key(items: &mut [T], mut key: impl FnMut(&T) -> u64) { let mut keys: Vec = items.iter().zip(0u32..).map(|(item, i)| u128::from(key(item)) << 32 | u128::from(i)).collect(); @@ -28,14 +30,14 @@ pub(crate) fn by_key(items: &mut [T], mut key: impl FnMut(&T) -> u64) { permute(items, keys.into_iter().map(|k| k as u32 as usize).collect()); } -/// `items` ordered by a 128-bit `key`, least first, ties in the order they came. +/// Sorts `items` by a 128-bit `key`, ascending. Ties keep their input order. pub(crate) fn by_wide_key(items: &mut [T], mut key: impl FnMut(&T) -> u128) { let mut keys: Vec<(u128, u32)> = items.iter().zip(0u32..).map(|(item, i)| (key(item), i)).collect(); sort_wide(&mut keys); permute(items, keys.into_iter().map(|(_, i)| i as usize).collect()); } -/// `items` ordered by `name`, ties in the order they came. +/// Sorts `items` by `name`. Ties keep their input order. pub(crate) fn by_name(items: &mut [T], name: impl Fn(&T) -> &str) { let mut keys: Vec<(&str, u32)> = items.iter().zip(0u32..).map(|(item, i)| (name(item), i)).collect(); sort_names(&mut keys); @@ -43,29 +45,29 @@ pub(crate) fn by_name(items: &mut [T], name: impl Fn(&T) -> &str) { permute(items, from); } -/// `values` ordered as `f64::total_cmp` orders them. +/// Sorts `values` by `f64::total_cmp`. #[inline(never)] pub(crate) fn numbers(values: &mut [f64]) { values.sort_unstable_by(f64::total_cmp); } -/// A number as a key ordering as `f64::total_cmp` does. +/// Maps `x` to a key that orders as `f64::total_cmp` does. pub(crate) fn number(x: f64) -> u64 { let bits = x.to_bits(); if bits >> 63 == 1 { !bits } else { bits | 1 << 63 } } -/// Two keys as one, `high` first. +/// Packs two keys into one that orders by `high`, then `low`. pub(crate) fn pair(high: u32, low: u32) -> u64 { u64::from(high) << 32 | u64::from(low) } -/// Two keys as one, `high` first. +/// Packs two keys into one that orders by `high`, then `low`. pub(crate) fn wide(high: u64, low: u64) -> u128 { u128::from(high) << 64 | u128::from(low) } -/// Moves `items` into the order `from` lists: item `k` becomes what was at `from[k]`. +/// Reorders `items` in place so that item `k` becomes the item previously at `from[k]`. fn permute(items: &mut [T], mut from: Vec) { for k in 0..items.len() { let mut at = k; diff --git a/crates/latkit/src/panels.rs b/crates/latkit/src/panels.rs index 48b8ff59..dce19118 100644 --- a/crates/latkit/src/panels.rs +++ b/crates/latkit/src/panels.rs @@ -1,11 +1,11 @@ -//! Panels: where views draw in a target they share. A canvas showing several views and a picture or -//! video composing them lay their views out the same way. +//! Panels: the region of a shared target each view draws in. A canvas with several views and a +//! picture or video composed from them use the same layout. use crate::{Error, Result}; -/// Where each of several views draws in a target: x, y, width, and height as fractions of it, a -/// panel per view, in order; a later panel draws over an earlier one. None given is one view filling -/// the target. +/// The region of a shared target each view draws in, one panel per view, in order. A panel is +/// `[x, y, width, height]` as fractions of the target, from its top left. Later panels draw over +/// earlier ones. With no panels, one view fills the target. /// /// ``` /// use latkit::Panels; @@ -18,17 +18,18 @@ use crate::{Error, Result}; pub struct Panels(Vec<[f32; 4]>); impl Panels { - /// How many views the panels take: one when none are given. + /// Number of views the panels lay out. One when no panels are given. pub fn len(&self) -> usize { self.0.len().max(1) } - /// Whether no parts were given, so one view fills the target. + /// Whether no panels were given, so one view fills the target. pub fn is_empty(&self) -> bool { self.0.is_empty() } - /// The pixels of a target of `size` each view draws in, edges on multiples of `align`. + /// Each view's pixel rectangle in a target of `size`, with edges rounded to multiples of + /// `align`. Fails if a panel covers no pixels. pub(crate) fn rects(&self, size: [u32; 2], align: [u32; 2]) -> Result> { let whole = [[0.0, 0.0, 1.0, 1.0]]; let parts = if self.0.is_empty() { &whole[..] } else { &self.0[..] }; @@ -62,7 +63,7 @@ impl From<[[f32; 4]; N]> for Panels { } } -/// A view's panel of a target in pixels: its top left corner and its size. +/// A view's panel in target pixels: its top left corner and its size. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) struct Rect { pub(crate) origin: [u32; 2], @@ -83,7 +84,7 @@ mod tests { use super::*; #[test] - fn panels_split_a_target_on_whole_blocks() { + fn panel_edges_round_to_multiples_of_the_alignment() { let panels = Panels::from([[0.0, 0.0, 1.0, 0.7], [0.0, 0.7, 1.0, 0.3]]); let rects = panels.rects([1920, 1080], [4, 2]).expect("valid"); assert_eq!(rects, [Rect { origin: [0, 0], size: [1920, 756] }, Rect { origin: [0, 756], size: [1920, 324] }]); diff --git a/crates/latkit/src/pending.rs b/crates/latkit/src/pending.rs index 5ad3d34d..33e94d7f 100644 --- a/crates/latkit/src/pending.rs +++ b/crates/latkit/src/pending.rs @@ -1,5 +1,5 @@ -//! The one future latkit hands out: what a view will answer once the GPU has, and what a host's job -//! or fetch will bring. +//! The future latkit returns for every deferred result: a view's answer once the GPU produces +//! it, or the result of a host's job or fetch. use std::future::Future; use std::pin::Pin; @@ -8,10 +8,11 @@ use std::task::{Context, Poll, Waker}; use crate::{Error, Result}; -/// A value still to come: a future its [`Resolver`] resolves, from a view's frames, a host's worker, -/// or the network. A view's answers resolve to an error when the device is lost, and stay pending -/// while no frames are drawn. Destroying their owner cancels them. Await it, or [`take`](Pending::take) -/// it from a frame loop. +/// A future value, resolved by its [`Resolver`] from a view's frames, a host's worker, or the +/// network. Await it, or poll it with [`take`](Pending::take) from a frame loop. +/// +/// A view's answers resolve to an error when the device is lost, and stay pending while no frames +/// are drawn. Dropping their owner cancels them. #[must_use = "a pending value does nothing unless awaited or taken"] pub struct Pending(Arc>>); @@ -21,23 +22,23 @@ enum Slot { Taken, } -/// What resolves a [`Pending`]: a host keeps it until its job or fetch is done. Dropping it without -/// resolving cancels the answer and wakes its waiter. +/// The write side of a [`Pending`]. A host keeps it until its job or fetch finishes. Dropping it +/// without resolving resolves the value to [`Error::Cancelled`] and wakes its waiter. pub struct Resolver(Arc>>); impl Pending { - /// A pending value, and what resolves it. + /// Creates a pending value and its resolver. pub fn new() -> (Pending, Resolver) { let slot = Arc::new(Mutex::new(Slot::Waiting { waker: None, errors: None })); (Pending(slot.clone()), Resolver(slot)) } - /// A value already here. + /// A pending value that is already resolved. pub fn ready(result: Result) -> Pending { Pending(Arc::new(Mutex::new(Slot::Ready(result)))) } - /// Wakes `waker` once the value is resolved: at once when it already is. + /// Wakes `waker` when the value resolves, or immediately if it already has. #[cfg(feature = "gpu")] pub(crate) fn notify(&self, waker: &Waker) { let mut slot = self.0.lock().unwrap_or_else(PoisonError::into_inner); @@ -51,8 +52,9 @@ impl Pending { } } - /// Reports a host job's failure even if its derivation keeps an earlier result. Called once - /// before handing the pending value to a derivation; superseded/dropped consumers report nothing. + /// Pushes this host job's failure to `errors`, even if the derivation consuming it keeps an + /// earlier result. Call once, before handing the pending value to the derivation. Nothing is + /// reported after the pending value is dropped, such as when a newer job supersedes it. #[cfg(any(feature = "gpu", test))] pub(crate) fn report_errors(&self, errors: &Arc>>) { let mut slot = self.0.lock().unwrap_or_else(PoisonError::into_inner); @@ -63,7 +65,7 @@ impl Pending { } } - /// The value once resolved, taking it; `None` while it waits, and after it was taken. + /// Takes the value once resolved. Returns `None` while it is pending and after it was taken. pub fn take(&self) -> Option> { let mut slot = self.0.lock().unwrap_or_else(PoisonError::into_inner); match std::mem::replace(&mut *slot, Slot::Taken) { @@ -78,12 +80,12 @@ impl Pending { } impl Resolver { - /// Whether the consumer dropped its pending value, so unstarted work can be skipped. + /// Whether the consumer dropped its pending value. Work not yet started can then be skipped. pub fn is_cancelled(&self) -> bool { Arc::strong_count(&self.0) == 1 } - /// Resolves the pending value with `result`, waking whatever awaits it. + /// Resolves the pending value with `result` and wakes its waiter. pub fn resolve(self, result: Result) { let mut slot = self.0.lock().unwrap_or_else(PoisonError::into_inner); if let Slot::Waiting { errors: Some(errors), .. } = &*slot diff --git a/crates/latkit/src/resource.rs b/crates/latkit/src/resource.rs index 8ea8f821..1643b56b 100644 --- a/crates/latkit/src/resource.rs +++ b/crates/latkit/src/resource.rs @@ -3,7 +3,7 @@ use crate::Pending; use std::ops::Range; use std::sync::Arc; -/// A source of bytes used by JSON data documents and their binary slices. +/// A source of bytes for JSON data documents and their binary slices. pub trait Fetch: Send + Sync { /// Resolves to the requested bytes, or an error. A missing resource returns empty bytes. fn fetch(&self, request: &Request) -> Pending>; @@ -16,22 +16,22 @@ impl Fetch for T { } } -/// Bytes to fetch: a URL, and the range of its bytes, or all of them. +/// Bytes to fetch: a URL and an optional byte range. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub struct Request { /// The URL. pub url: Arc, - /// Bytes `start..end`; `None` fetches every byte. + /// Bytes `start..end`. `None` fetches every byte. pub range: Option>, } impl Request { - /// Every byte at `url`. + /// A request for every byte at `url`. pub fn new(url: impl Into>) -> Request { Request { url: url.into(), range: None } } - /// Bytes `range` at `url`. + /// A request for bytes `range` at `url`. pub fn range(url: impl Into>, range: Range) -> Request { Request { url: url.into(), range: Some(range) } } diff --git a/crates/latkit/src/scene/areas.rs b/crates/latkit/src/scene/areas.rs index fd91df8b..45299433 100644 --- a/crates/latkit/src/scene/areas.rs +++ b/crates/latkit/src/scene/areas.rs @@ -4,17 +4,18 @@ use crate::gpu::shaders as wgsl; use crate::{Error, Kind, Name, Result}; mark!( -/// Rows drawn as the areas a polygon field holds: parcels, states, lakes, the land and water of a -/// map. The view cuts each row's polygons into triangles once, off the frame when they are large; -/// on the earth they bend with the globe and stay whole across the antimeridian. Outline areas with -/// a [`Lines`](crate::Lines) mark of the same table, which draws their rings. +/// Rows drawn as the areas of a polygon field: parcels, states, lakes, the land and water of a map. +/// The view triangulates each row's polygons once, in the background when they are large. On the +/// earth, areas follow the globe's curve and stay whole across the antimeridian. To outline areas, +/// add a [`Lines`](crate::Lines) mark of the same table, which draws their rings. /// /// ``` /// # use latkit::{field, Areas, Colormap}; /// let states = Areas::of("map", "states").color(field("load").colormap(Colormap::MAKO)); /// ``` Areas { - /// The polygon field each row fills: one polygon's rings, or polygons. `geometry` unless set. + /// The polygon field each row fills, holding one polygon's rings or several polygons; the field + /// named `geometry` by default. geometry: Option, }); @@ -24,15 +25,15 @@ impl Areas { Areas { style: Style::new(data.into(), table.into()), geometry: None } } - /// The polygon field each row fills: one polygon's rings, or polygons. Unless set, the field - /// `geometry`. + /// Sets the polygon field each row fills, holding one polygon's rings or several polygons; the + /// field named `geometry` by default. #[must_use] pub fn geometry(mut self, field: impl Into) -> Areas { self.geometry = Some(field.into()); self } - /// The field each row fills. + /// The polygon field each row fills. pub(crate) fn field(&self) -> Name { self.geometry.clone().unwrap_or_else(|| GEOMETRY.into()) } @@ -57,8 +58,8 @@ impl Areas { } } -/// The derived field holding the triangles of polygon field `field`: per row the point its label -/// hangs from, then three points per triangle. +/// The name of the derived field holding polygon field `field`'s triangles: for each row, the point +/// its label anchors to, then three points per triangle. pub(crate) fn triangles_of(field: &str) -> Name { format!("{field}.triangles").into() } diff --git a/crates/latkit/src/scene/compile.rs b/crates/latkit/src/scene/compile.rs index f224a50f..859767b0 100644 --- a/crates/latkit/src/scene/compile.rs +++ b/crates/latkit/src/scene/compile.rs @@ -1,4 +1,4 @@ -//! One schema compiler for validation and interactive rendering. +//! Compiles marks against dataset schemas, for both validation and views. use std::sync::Arc; @@ -13,7 +13,9 @@ pub(crate) struct Compiled { pub(crate) schemas: Vec<(Name, u64)>, } -/// Missing data stays pending in a live view; validation requires it before calling this. +/// Compiles `mark` against the schemas in `all`. Returns `None` while the mark's dataset or table +/// is missing: a view keeps such a mark pending, and validation reports the absence before calling +/// this. Errors when an option fails to compile or the mark compiles to more than 32 channels. pub(crate) fn compile(mark: &Mark, all: &[Snapshot], context: Context<'_>) -> Result> { let style = mark.style(); let Some(data) = all.iter().find(|d| *d.name() == style.data) else { return Ok(None) }; diff --git a/crates/latkit/src/scene/controls.rs b/crates/latkit/src/scene/controls.rs index 014ef9e1..a739c63e 100644 --- a/crates/latkit/src/scene/controls.rs +++ b/crates/latkit/src/scene/controls.rs @@ -5,75 +5,85 @@ use serde::{Deserialize, Deserializer}; use super::form::{Presets, preset_or, presets, seconds}; -/// How a view answers the pointer and keyboard, and how much it moves on its own: a preset, or options -/// over the defaults. Each mark says whether its rows pick, select, and drag; these say what the view -/// itself does. +/// How a view responds to the pointer and keyboard, and how much it animates on its own: a preset +/// (`"none"` or `"inspect"`), or options over the defaults. Each mark controls whether its own rows +/// pick, select, and drag; these options control the view itself. #[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(remote = "Self", default, rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = presets::)] pub struct Controls { - /// Dragging the background moves the camera. + /// Whether dragging the background pans the camera. Default `true`. pub pan: bool, - /// What zooms: the wheel and pinching, or the wheel only with Control or Meta held (as a - /// trackpad's pinch sends it), leaving the plain wheel to scroll the page; or nothing. + /// Which input zooms the view: the wheel and pinching (`wheel`, the default); pinching, and the + /// wheel only with Control or Meta held, as trackpad pinches send it, so the plain wheel + /// scrolls the page (`modifier`); or nothing (`off`). pub zoom: Zoom, - /// Dragging with the secondary button, or with Shift, turns and tilts a map or plane. + /// Whether dragging with the secondary button, or with Shift, rotates and tilts a map or plane. + /// Default `true`. pub turn: bool, - /// Tab steps through the rows nearest the view's middle and arrows to the nearest row their way; - /// Enter opens the row stepped to and Space selects it. Otherwise arrows pan, `+` and `-` zoom, - /// Home fits, and Escape clears the selection. + /// Whether the keyboard controls the view. Default `true`. Tab and Shift+Tab step through the + /// rows nearest the view's center, and arrow keys step to the nearest row in their direction. + /// Enter opens the stepped-to row and Space selects it. With no row stepped to, arrow keys pan + /// (Shift+arrows turn), `+` and `-` zoom, Home fits everything, and Escape clears the + /// selection. pub keyboard: bool, - /// The row the user points at glows. It is hovered either way: events tell it, and `hovered()` - /// reads it. + /// Whether the hovered row glows, and a hovered vertex sized in CSS pixels grows. Default + /// `true`. Rows are hovered either way: [`Event::Hovered`](crate::Event) still fires and + /// `hovered()` still reads them. pub hover: bool, - /// CSS pixels around the pointer that picks reach; touch reaches 8 more. + /// How far from the pointer, in CSS pixels, a row can be and still be picked; touch adds 8. + /// Default 6. pub pick_radius: f32, - /// Dragging with Shift selects the rows in a box, instead of turning; a Shift click still toggles - /// its row. + /// Whether Shift+drag selects the rows inside a box instead of turning the camera. Shift+click + /// still toggles one row. Default `false`. pub select_box: bool, - /// Dragging from a port, from a vertex with Alt, or an edge off its end draws a connection, - /// told as [`Event::Connect`](crate::Event). + /// Whether the user can draw connections, reported as [`Event::Connect`](crate::Event): by + /// dragging from a port, from a vertex with Alt held, or an edge's end off its vertex. Default + /// `false`. pub connect: bool, - /// Delete and Backspace tell [`Event::Delete`](crate::Event) with the selection. + /// Whether Delete and Backspace report the selection as [`Event::Delete`](crate::Event). + /// Default `false`. pub delete: bool, - /// Clicking again where nothing moved selects the next row under the point, nearest first, so - /// rows drawn over one another can each be selected. + /// Whether clicking again at the same point, with the camera unmoved, selects the next row + /// under it, nearest first, so overlapping rows can each be selected. Default `false`. pub cycle: bool, - /// Seconds a touch or a pen held still asks for a menu, as the secondary button does; zero never - /// does. + /// Seconds a touch or pen must hold still to request a menu ([`Event::Menu`](crate::Event)), + /// as the secondary button does; 0 turns this off. Default 0.5. #[serde(deserialize_with = "seconds")] #[schemars(with = "f64")] pub press: Duration, - /// How much the view moves on its own: eases, flights, coasting, orbits, label fades, comets, and - /// what reads `elapsed()`. + /// How much the view animates on its own: eases, camera flights, coasting, orbits, label fades, + /// comets, and options that read `elapsed()`. `system` (the default) follows the user's + /// reduced-motion setting, `full` animates everything, and `reduced` animates only motion the + /// user drives. pub motion: Motion, } -/// What zooms a view. +/// Which input zooms a view. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub enum Zoom { /// The wheel, pinching, and the keys. #[default] Wheel, - /// Pinching and the keys, and the wheel only with Control or Meta held, as a trackpad's pinch - /// sends it: the plain wheel scrolls the page. + /// Pinching and the keys, and the wheel only with Control or Meta held (as trackpad pinches + /// send it), so the plain wheel scrolls the page. Modifier, - /// Nothing. + /// No input zooms. Off, } -/// How much a view moves on its own. +/// How much a view animates on its own. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub enum Motion { - /// As the platform asks: reduced where the user asked their system for less motion. + /// Follows the system: reduced when the user asked their system for less motion. #[default] System, - /// Every motion. + /// All motion. Full, - /// Only what the user moves: changes apply at once, flights jump, nothing coasts or orbits, - /// labels show without fading, and comets and what reads `elapsed()` hold still. + /// Only motion the user drives: changes apply at once, flights jump, nothing coasts or orbits, + /// labels appear without fading, and comets and options that read `elapsed()` stay still. Reduced, } @@ -119,7 +129,8 @@ impl Default for Controls { } impl Controls { - /// A view that answers nothing: a still picture, its camera where the application puts it. + /// Controls for a still picture: no panning, zooming, turning, keyboard, hover glow, or press + /// menus, so only the application moves the camera. Marks that select still select on click. pub fn none() -> Controls { Controls { pan: false, @@ -132,8 +143,8 @@ impl Controls { } } - /// A view to inspect rows in: they glow under the pointer, select, and step with the keys, while - /// the camera stays where the application puts it. + /// Controls for inspecting rows: rows glow under the pointer, select on click, and step with + /// the keys, while the camera moves only when the application moves it. pub fn inspect() -> Controls { Controls { pan: false, zoom: Zoom::Off, turn: false, ..Controls::default() } } diff --git a/crates/latkit/src/scene/edges.rs b/crates/latkit/src/scene/edges.rs index cc3c86a4..68b8637b 100644 --- a/crates/latkit/src/scene/edges.rs +++ b/crates/latkit/src/scene/edges.rs @@ -5,8 +5,9 @@ use crate::gpu::shaders as wgsl; use crate::{Error, Kind, Name, Result}; mark!( -/// Rows drawn as strokes between the vertices two reference fields name: branches, links, wires. In a -/// diagram, a row without ends is a net: it joins every vertex port whose reference field names it. +/// Rows drawn as strokes between the vertices named by two reference fields: branches, links, +/// wires. In a diagram, a row without ends is a net, joining every vertex port whose reference +/// field names it. /// /// ``` /// # use latkit::{field, Edges}; @@ -15,18 +16,19 @@ mark!( Edges + stroke { /// The reference fields naming each row's two vertices. ends: Option<(Name, Name)> as "[from: string, to: string]", - /// A path field of points each row runs through between its ends. + /// A path field whose points each row passes through between its ends. bends: Option, - /// How each row runs between its ends; on the earth geodesic, elsewhere straight, unless set. - /// Orthogonal routes run around blocks, bends rounded; diagrams only. + /// How each row runs between its ends: geodesic on the earth and straight elsewhere unless set. + /// `orthogonal` routes around blocks with rounded bends, in diagrams only. route: Option, - /// Whether each row ends in an arrowhead at its second end; a net's at each input it reaches. + /// Whether each row has an arrowhead at its second end; a net has one at each input it reaches. + /// Default `false`. arrows: bool, - /// Draws each row as a pole from its own vertex down to the ground beneath it, on a plane or the - /// earth; it has no ends. + /// Draws each row as a pole from its own vertex down to the ground, on a plane or the earth. + /// Poles take no `ends`, `bends`, or `joins`. Default `false`. poles: bool, - /// Draws each row as a net on a plane or the earth: it joins the vertex rows of `table` whose - /// reference field names it, as a star from their middle to each. + /// Draws each row as a net on a plane or the earth, joining the vertex rows of `table` whose + /// `reference` field names it: a star from their center to each member. joins: Option<(Name, Name)> as "[table: string, reference: string]", }); @@ -45,46 +47,48 @@ impl Edges { } } - /// Draws each row as a net on a plane or the earth: it joins the vertex rows of `table` whose - /// reference field `reference` names it, as a star from their middle to each. In a diagram nets - /// join vertex ports instead. + /// Draws each row as a net on a plane or the earth, joining the vertex rows of `table` whose + /// field `reference` names it: a star from their center to each member. In a diagram, nets join + /// vertex ports instead. #[must_use] pub fn joins(mut self, table: impl Into, reference: impl Into) -> Edges { self.joins = Some((table.into(), reference.into())); self } - /// Draws each row as a pole: a stroke from the row's own vertex, as the vertex mark drawing the - /// table places it, down to the ground beneath it, on a plane or the earth. Dashes, comets, picks, - /// and labels run along it as along any edge. + /// Draws each row as a pole: a stroke from the row's own vertex, as placed by the vertex mark + /// that draws the same table, down to the ground, on a plane or the earth. Dashes, comets, + /// picking, and labels work as on any edge. #[must_use] pub fn poles(mut self) -> Edges { self.poles = true; self } - /// A path field of points each row runs through between its ends. + /// Sets a path field whose points each row passes through between its ends. #[must_use] pub fn bends(mut self, field: impl Into) -> Edges { self.bends = Some(field.into()); self } - /// How each row runs between its ends; on the earth geodesic, elsewhere straight, unless set. + /// Sets how each row runs between its ends: geodesic on the earth and straight elsewhere unless + /// set. #[must_use] pub fn route(mut self, route: Route) -> Edges { self.route = Some(route); self } - /// Whether each row ends in an arrowhead at its second end; a net's at each input it reaches. + /// Sets whether each row has an arrowhead at its second end; a net has one at each input it + /// reaches. #[must_use] pub fn arrows(mut self, on: bool) -> Edges { self.arrows = on; self } - /// The reference fields naming each row's two vertices. + /// Sets the reference fields naming each row's two vertices. #[must_use] pub fn ends(mut self, from: impl Into, to: impl Into) -> Edges { self.ends = Some((from.into(), to.into())); @@ -94,22 +98,22 @@ impl Edges { pub(crate) fn compile(&self, c: &mut Compiler<'_>, context: Context<'_>) -> Result { if self.joins.is_some() { if self.ends.is_some() || self.bends.is_some() || self.poles { - return Err(Error::invalid("a net joins its members: it has no ends, bends, or poles").at("joins")); + return Err(Error::invalid("a net cannot have ends, bends, or poles").at("joins")); } } else if self.poles { if self.ends.is_some() || self.bends.is_some() { return Err( - Error::invalid("a pole stands on its own row's vertex: it has no ends or bends").at("poles") + Error::invalid("a pole stands on its own row's vertex and cannot have ends or bends").at("poles") ); } - // Both ends are the row's own vertex, which lights as its pole does. + // Both ends are the row's own vertex, so the vertex lights up with its pole. c.own_end(&self.style.table); c.own_end(&self.style.table); } else if let Some((from, to)) = &self.ends { c.end(from).map_err(|e| e.at("ends"))?; c.end(to).map_err(|e| e.at("ends"))?; } else if self.bends.is_some() { - return Err(Error::invalid("bends run between ends; name them").at("bends")); + return Err(Error::invalid("bends need ends").at("bends")); } if let Some(bends) = &self.bends && !matches!(c.kind(bends), Some(Kind::Path)) @@ -121,7 +125,8 @@ impl Edges { match (&self.style.color, context.theme.edge.color) { (Some(paint), _) => c.channel("color", &paint.0, color)?, (None, Some(_)) => c.theme("color", wgsl::THEME_EDGE, color)?, - // A diagram's wires, and nets without ends to blend, take the text color. + // A diagram's wires, and edges without ends to blend (nets and poles), take the text + // color. (None, None) if context.diagram() || self.ends.is_none() => { c.theme( "color", @@ -145,11 +150,11 @@ impl Edges { if self.poles { drawing.path = wgsl::PATH_POLES; } else if self.joins.is_some() { - // Each net's star runs along the path the view works out from its members. + // Each net's star follows the route the view computes from its members. drawing.path = wgsl::PATH_GEOMETRY; drawing.geometry = c.derived_path("route"); } else if context.diagram() { - // Every wire runs along the path its sheet worked out. + // Every wire follows the route its sheet computed. drawing.path = wgsl::PATH_GEOMETRY; drawing.geometry = c.derived_path("route"); } else { diff --git a/crates/latkit/src/scene/faces.rs b/crates/latkit/src/scene/faces.rs index b0f49f36..679354db 100644 --- a/crates/latkit/src/scene/faces.rs +++ b/crates/latkit/src/scene/faces.rs @@ -4,17 +4,17 @@ use crate::gpu::shaders as wgsl; use crate::{Error, Name, Result}; mark!( -/// Rows drawn as triangles between the vertices three reference fields name: meshes, surfaces, and -/// fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen from -/// above shows its exact colors. +/// Rows drawn as triangles between the vertices named by three reference fields: meshes, surfaces, +/// and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen +/// from above shows its exact colors. /// /// ``` /// # use latkit::Faces; /// let surface = Faces::of("mesh", "Face").corners("a", "b", "c"); /// ``` Faces { - /// The reference fields naming each row's three vertices. Without a color, a face blends the - /// colors of its corners. + /// The reference fields naming each row's three vertices; required. Without a color, a face + /// blends the colors of its corners. corners: Option<[Name; 3]> as "[a: string, b: string, c: string]", }); @@ -24,8 +24,8 @@ impl Faces { Faces { style: Style::new(data.into(), table.into()), corners: None } } - /// The reference fields naming each row's three vertices. Without a color, a face blends the - /// colors of its corners. + /// Sets the reference fields naming each row's three vertices; required. Without a color, a + /// face blends the colors of its corners. #[must_use] pub fn corners(mut self, a: impl Into, b: impl Into, c: impl Into) -> Faces { self.corners = Some([a.into(), b.into(), c.into()]); diff --git a/crates/latkit/src/scene/form.rs b/crates/latkit/src/scene/form.rs index 8c30b6ce..475f2874 100644 --- a/crates/latkit/src/scene/form.rs +++ b/crates/latkit/src/scene/form.rs @@ -1,6 +1,5 @@ -//! What scenes read as JSON. Each type deserializes as itself and its schema is derived from it; a -//! type a name can stand for also reads one of its presets' names, such as `"inspect"`, `"earth"`, or -//! `"stress"`. +//! JSON reading for scenes. Each type deserializes and derives its schema from its own definition; +//! a type with presets also accepts a preset's name, such as `"inspect"`, `"earth"`, or `"stress"`. use std::fmt; use std::marker::PhantomData; @@ -12,20 +11,20 @@ use serde::de::value::MapAccessDeserializer; use serde::de::{self, Deserializer, MapAccess, Visitor}; use serde_json::Value; -/// A type a name can stand for. +/// A type with named presets. pub(crate) trait Presets: Sized { - /// The names, as the schema lists them. + /// The preset names, in the order the schema lists them. const NAMES: &'static [&'static str]; - /// The value `name` stands for. + /// The preset named `name`, if any. fn preset(name: &str) -> Option; - /// The value its options, written out, make: what its derive reads. + /// Reads the type from its options object with the derived implementation. fn options<'de, D: Deserializer<'de>>(deserializer: D) -> Result; } -/// `T` read from one of its presets' names, or from its options, read where they are written so an -/// error names the path to the option it could not read. +/// Reads `T` from a preset name or from its options. Options are read in place, so an error names +/// the path to the option that failed. pub(crate) fn preset_or<'de, D: Deserializer<'de>, T: Presets>(deserializer: D) -> Result { struct Named(PhantomData); @@ -49,11 +48,12 @@ pub(crate) fn preset_or<'de, D: Deserializer<'de>, T: Presets>(deserializer: D) deserializer.deserialize_any(Named(PhantomData)) } -/// `T`'s schema as one of its presets' names or what it derived, its description kept on the whole. +/// Rewrites `T`'s derived schema to accept a preset name or the derived form, moving the +/// description to the whole. pub(crate) fn presets(schema: &mut Schema) { let mut derived = std::mem::replace(schema, json_schema!({})); let description = derived.remove("description"); - // A variant without options that a preset names already is the preset. + // Drop unit variants that a preset already names; the preset's name covers them. for key in ["oneOf", "anyOf"] { if let Some(variants) = derived.get_mut(key).and_then(Value::as_array_mut) { variants.retain(|v| !v.get("const").and_then(Value::as_str).is_some_and(|name| T::NAMES.contains(&name))); @@ -65,8 +65,8 @@ pub(crate) fn presets(schema: &mut Schema) { } } -/// A duration read as seconds. +/// Reads a duration in seconds; errors on negative or out-of-range values. pub(crate) fn seconds<'de, D: Deserializer<'de>>(deserializer: D) -> Result { let seconds = f64::deserialize(deserializer)?; - Duration::try_from_secs_f64(seconds).map_err(|_| de::Error::custom("seconds from 0, within range")) + Duration::try_from_secs_f64(seconds).map_err(|_| de::Error::custom("seconds must be finite and not negative")) } diff --git a/crates/latkit/src/scene/label.rs b/crates/latkit/src/scene/label.rs index b4b32ce9..65158074 100644 --- a/crates/latkit/src/scene/label.rs +++ b/crates/latkit/src/scene/label.rs @@ -7,10 +7,10 @@ use serde::de::{self, Deserializer}; use crate::expr::form::Written; use crate::{Expr, Paint}; -/// A row's text, placed beside, inside, or along what its mark draws, following it every frame: a -/// text field, or options. Labels that would overlap give way by priority; among equals, those -/// already shown keep their place. In a diagram a label is part of the drawing: it scales with the -/// zoom, fades out once too small to read, and shows wherever it fits its vertex. +/// Text for each row, placed beside, inside, or along the row's shape and following it every frame: +/// a text field, or options. Where labels would overlap, the higher priority shows; among equals, +/// labels already shown stay. In a diagram a label is part of the drawing: it scales with the zoom, +/// fades out when too small to read, and shows wherever it fits its vertex. /// /// ``` /// # use latkit::{field, Label, Vertices}; @@ -20,32 +20,32 @@ use crate::{Expr, Paint}; #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = text_or_options)] pub struct Label { - /// A text field. + /// The text field to show. pub(crate) text: Expr, - /// In the space's unit: units in a diagram, CSS pixels elsewhere; the theme's text size unless - /// set. + /// Font size in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's + /// `text.size` unless set. #[serde(default)] pub(crate) size: Option, /// Each row's text color; the theme's text color unless set. #[serde(default)] pub(crate) color: Option, - /// Where labels would overlap, the higher shows; the mark's priority unless set. + /// Where labels would overlap, the higher priority shows; the mark's `priority` unless set. #[serde(default)] pub(crate) priority: Option, - /// How far a line runs before it wraps, in the space's unit: units in a diagram, CSS pixels - /// elsewhere. + /// The width at which lines wrap, in the space's unit (units in a diagram, CSS pixels + /// elsewhere). #[serde(default)] pub(crate) max_width: Option, - /// The CSS font family list its texts are set in; the theme's font unless set. + /// The CSS font family list for the text; the theme's `text.font` unless set. #[serde(default)] pub(crate) font: Option>, - /// The most labels shown at once, those of highest priority among the rows in view; where they - /// would overlap, fewer. + /// The most labels shown at once: those of highest priority among the rows in view, or fewer + /// where they would overlap. No limit unless set. #[serde(default)] pub(crate) max: Option, } -/// A label reads a text field as its text alone, or an object with `text` as its options. +/// Reads a label from a text field alone, or from an options object with `text`. impl<'de> Deserialize<'de> for Label { fn deserialize>(deserializer: D) -> Result { let label = |written: Written| match written { @@ -53,7 +53,7 @@ impl<'de> Deserialize<'de> for Label { form.only("a label", &["text", "size", "color", "priority", "maxWidth", "font", "max"])?; Ok(Label { color: form.color()?.map(Paint), - text: form.text.expect("held"), + text: form.text.expect("present"), size: form.size, priority: form.priority, max_width: form.max_width, @@ -67,10 +67,14 @@ impl<'de> Deserialize<'de> for Label { } } -/// A label's schema: a text field, or its options. +/// A label's schema: a text field, or its options, with the description moved to the whole. fn text_or_options(schema: &mut schemars::Schema) { - let options = std::mem::replace(schema, schemars::json_schema!({})); + let mut options = std::mem::replace(schema, schemars::json_schema!({})); + let description = options.remove("description"); *schema = schemars::json_schema!({ "anyOf": [{ "$ref": "#/$defs/Expr" }, options] }); + if let Some(description) = description { + schema.insert("description".into(), description); + } } impl Label { @@ -79,46 +83,46 @@ impl Label { Label { text: text.into(), size: None, color: None, priority: None, max_width: None, font: None, max: None } } - /// Text size in the space's unit: units in a diagram, CSS pixels elsewhere; the theme's text size - /// unless set. + /// Sets the font size in the space's unit (units in a diagram, CSS pixels elsewhere); the + /// theme's text size unless set. #[must_use] pub fn size(mut self, size: f32) -> Label { self.size = Some(size); self } - /// Each row's text color; the theme's text color unless set. + /// Sets each row's text color; the theme's text color unless set. #[must_use] pub fn color(mut self, color: impl Into) -> Label { self.color = Some(color.into()); self } - /// Each row's priority: where labels would overlap, the higher shows. The mark's priority - /// unless set. + /// Sets each row's label priority: where labels would overlap, the higher priority shows. The + /// mark's priority unless set. #[must_use] pub fn priority(mut self, priority: impl Into) -> Label { self.priority = Some(priority.into()); self } - /// How far a line of text runs before it wraps, in the space's unit: units in a diagram, CSS - /// pixels elsewhere. + /// Sets the width at which lines wrap, in the space's unit (units in a diagram, CSS pixels + /// elsewhere). #[must_use] pub fn max_width(mut self, width: f32) -> Label { self.max_width = Some(width); self } - /// The CSS font family list its texts are set in; the theme's font unless set. + /// Sets the CSS font family list for the text; the theme's font unless set. #[must_use] pub fn font(mut self, font: impl Into>) -> Label { self.font = Some(font.into()); self } - /// Shows at most `max` labels at once, those of highest priority among the rows in view; where - /// they would overlap, fewer. + /// Shows at most `max` labels at once: those of highest priority among the rows in view, or + /// fewer where they would overlap. #[must_use] pub fn max(mut self, max: u32) -> Label { self.max = Some(max); diff --git a/crates/latkit/src/scene/layout.rs b/crates/latkit/src/scene/layout.rs index 00010459..915ba198 100644 --- a/crates/latkit/src/scene/layout.rs +++ b/crates/latkit/src/scene/layout.rs @@ -3,18 +3,21 @@ use serde::{Deserialize, Deserializer}; use super::form::{Presets, preset_or, presets}; -/// How the view places vertex rows without positions: a diagram's blocks, sized, or the points of a -/// plane or the earth, among the rows linked to them, gaps in the space's units: a name, or `{ stress -/// | layered: options }`. A place, once given, holds: rows added later are placed around the rest, -/// and [`View::arrange`](crate::View::arrange) lays every row out again. +/// How the view places vertex rows that have no position: a name, or +/// `{ stress | layered: options }`. In a diagram it places blocks; on a plane or the earth it +/// places points among the rows they link to. Gaps are in position units: diagram units, plane +/// units, or degrees on the earth. +/// +/// A row keeps its place once placed: rows added later are placed around the others, and +/// [`View::arrange`](crate::View::arrange) lays out every row again. #[derive(Clone, Copy, PartialEq, Debug, Default, Deserialize, JsonSchema)] #[serde(remote = "Self", rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = presets::)] pub enum Layout { - /// Unplaced vertices are not drawn. + /// No layout: vertices without a position are not drawn. The default. #[default] None, - /// Distances on the drawing follow distances in the graph. + /// Drawn distances follow graph distances. Stress(Stress), /// Ranks along a direction, for flows and hierarchies. Layered(Layered), @@ -43,12 +46,12 @@ impl<'de> Deserialize<'de> for Layout { } } -/// Stress majorization: each pair of vertices pulls toward its graph distance; then their boxes -/// part, keeping a quarter of the gap clear. +/// Stress majorization: vertices move until drawn distances match graph distances, then overlapping +/// boxes separate, keeping a quarter of `gap` between them. #[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct Stress { - /// Units between adjacent vertices' centers. + /// Distance between the centers of linked vertices, in position units. Default 40. #[schemars(range(min = 0))] pub gap: f64, } @@ -59,15 +62,16 @@ impl Default for Stress { } } -/// Layered drawing: vertices in ranks, edges pointing along `flow`, and nets from their outputs. +/// Layered drawing: vertices in ranks, edges pointing along `flow`, and nets ranked from their +/// outputs. #[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct Layered { - /// Where edges point. + /// The direction edges point; `right` by default. pub flow: Direction, - /// Units between vertices of a rank. + /// Distance between vertices within a rank, in position units. Default 30. pub gap: f64, - /// Units between ranks. + /// Distance between ranks, in position units. Default 80. pub rank_gap: f64, } @@ -81,12 +85,12 @@ impl Default for Layered { #[derive(Clone, Copy, PartialEq, Eq, Debug, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub enum Route { - /// Straight across the screen. + /// A straight line on screen. Straight, /// Along the great circle, on the earth. Geodesic, - /// In horizontal and vertical runs around vertices, leaving and entering at their sides, bends - /// rounded. Diagrams only. + /// Horizontal and vertical runs around vertices that leave and enter at their sides, with + /// rounded bends. Diagrams only. Orthogonal, } @@ -97,9 +101,10 @@ pub enum Interpolation { /// Straight from each sample to the next. #[default] Linear, - /// Each value holds from the sample before it: steps rise at their samples' ends. + /// Each value holds from the previous sample up to its own, so the line steps at the previous + /// sample. StepBefore, - /// Each value holds until the next sample: steps rise at the next sample. + /// Each value holds until the next sample, where the line steps. StepAfter, } diff --git a/crates/latkit/src/scene/lines.rs b/crates/latkit/src/scene/lines.rs index 03a4b34f..79492fec 100644 --- a/crates/latkit/src/scene/lines.rs +++ b/crates/latkit/src/scene/lines.rs @@ -7,9 +7,10 @@ use crate::gpu::shaders as wgsl; use crate::{Error, Kind, Name, Result, Space}; mark!( -/// Rows drawn as lines: along a path field (roads, rivers, borders) or a polygon field's rings, -/// outlining its areas; on a plot, through a sampled field's history across the data's axis, what a -/// monitor shows. On the earth paths bend with the globe and stay whole across the antimeridian. +/// Rows drawn as lines along a path field (roads, rivers, borders) or a polygon field's rings (area +/// outlines). On a plot, each row instead traces a sampled field's history across the data's axis, +/// as a monitor does. On the earth, paths follow the globe's curve and stay whole across the +/// antimeridian. /// /// ``` /// # use latkit::Lines; @@ -17,12 +18,13 @@ mark!( /// let height = Lines::of("body", "Body").trace("height"); /// ``` Lines + stroke { - /// The path or polygon field each row runs along; `geometry` unless set. Not on a plot. + /// The path or polygon field each row runs along; the field named `geometry` by default. Not + /// allowed on a plot. geometry: Option, - /// The sampled field each row traces across the data's axis, on a plot alone. A field not recorded - /// yet draws nothing until its first frame. + /// The sampled field each row traces across the data's axis; plots only, where it is required. + /// A field with no frames yet draws nothing until its first frame arrives. trace: Option, - /// How each row runs between its samples, on a plot. + /// How each row runs between its samples on a plot; `linear` by default. interpolation: Interpolation, }); @@ -38,22 +40,23 @@ impl Lines { } } - /// The path or polygon field each row runs along; the field `geometry` unless set. Not on a plot. + /// Sets the path or polygon field each row runs along; the field named `geometry` by default. + /// Not allowed on a plot. #[must_use] pub fn geometry(mut self, field: impl Into) -> Lines { self.geometry = Some(field.into()); self } - /// The sampled field each row traces across the data's axis, on a plot. Unless named, the mark - /// takes the field's name. + /// Sets the sampled field each row traces across the data's axis; plots only. Unless the mark + /// is [named](Lines::named), the field's name identifies it across scenes. #[must_use] pub fn trace(mut self, field: impl Into) -> Lines { self.trace = Some(field.into()); self } - /// How each row runs between its samples, on a plot. + /// Sets how each row runs between its samples on a plot; linear by default. #[must_use] pub fn interpolation(mut self, interpolation: Interpolation) -> Lines { self.interpolation = interpolation; @@ -70,13 +73,14 @@ impl Lines { drawing.size_px = self.stroke.px(); if matches!(context.space, Space::Plot(_)) { if self.geometry.is_some() { - return Err(Error::invalid("a plot traces sampled fields; name one with trace").at("geometry")); + return Err(Error::invalid("in a plot, use trace instead of geometry").at("geometry")); } let Some(field) = &self.trace else { return Err(Error::invalid("name the sampled field each row traces").at("trace")); }; drawing.trace = match c.kind(field) { - // Streamed data appends its first frame later; the mark compiles again then. + // Streamed data may not have its first frame yet; the mark recompiles when it + // arrives. None => wgsl::NONE, _ => c.trace(field).map_err(|e| e.at("trace"))?, }; @@ -87,7 +91,7 @@ impl Lines { }; } else { if self.trace.is_some() { - return Err(Error::invalid("only a plot traces a sampled field; name a path with geometry").at("trace")); + return Err(Error::invalid("outside a plot, use geometry instead of trace").at("trace")); } let field = self.geometry.clone().unwrap_or_else(|| GEOMETRY.into()); match c.kind(&field) { @@ -99,7 +103,7 @@ impl Lines { None => return Err(Error::invalid("name the path field each row runs along").at("geometry")), } drawing.path = wgsl::PATH_GEOMETRY; - // On the earth a line runs along its path as it bends with the globe. + // On the earth, a line follows a derived copy of its path that bends with the globe. drawing.geometry = if matches!(context.space, Space::Earth(_)) { c.derived_path(&path_of(&field)) } else { @@ -123,8 +127,8 @@ impl Lines { } } -/// The derived field holding path field `field` as an earth line draws it: unwrapped across the -/// antimeridian, with a point every few degrees along longer segments. +/// The name of the derived field holding path field `field` as drawn on the earth: unwrapped across +/// the antimeridian, with a point added every few degrees along long segments. pub(crate) fn path_of(field: &str) -> Name { format!("{field}.path").into() } diff --git a/crates/latkit/src/scene/mark.rs b/crates/latkit/src/scene/mark.rs index 0bf02fc8..30927449 100644 --- a/crates/latkit/src/scene/mark.rs +++ b/crates/latkit/src/scene/mark.rs @@ -5,7 +5,7 @@ use crate::expr::compile::{Compiler, Store}; use crate::gpu::shaders as wgsl; use crate::{Error, Expr, Name, Paint, Result, Size, Space}; -/// One drawn table of a scene: `{ vertices | edges | faces | lines | areas: options }` as JSON. +/// One table a scene draws. In JSON: `{ vertices | edges | faces | lines | areas: options }`. #[derive(Clone, PartialEq, Debug, serde::Deserialize, schemars::JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[non_exhaustive] @@ -52,7 +52,7 @@ impl From for Mark { } } -/// The field a mark reads its shape from unless told otherwise: what geography's tables hold. +/// The default name of the field a mark reads geometry from, as geographic tables name it. pub(crate) const GEOMETRY: &str = "geometry"; /// Options every mark has. @@ -85,7 +85,8 @@ impl Default for Stroke { } } -/// Where a stroke mark keeps its width, flow, and flags in each row, and the theme's width. +/// The row words where a stroke mark stores its width, flow, and flags, and the theme slot of its +/// default width. pub(crate) struct StrokeWords { pub(crate) width: u32, pub(crate) flow: u32, @@ -94,13 +95,13 @@ pub(crate) struct StrokeWords { } impl Stroke { - /// The CSS pixels the width keeps within: its size's. + /// The CSS pixel bounds of the width, from its [`Size`]. pub(crate) fn px(&self) -> [f32; 2] { self.width.as_ref().map_or(crate::Size::ANY, |size| size.px) } - /// Compiles the stroke's options: its width in the space's unit unless it says its own, and the - /// theme's in the space's. + /// Compiles the dash, flow, and width channels. A width is in the space's unit unless it names + /// its own; the theme's width is always in the space's unit. pub(crate) fn compile(&self, c: &mut Compiler<'_>, words: &StrokeWords, diagram: bool) -> Result<()> { c.channel("dash", &self.dash, Store::Flag { word: words.flags, bit: wgsl::DASH })?; c.channel("flow", &self.flow, Store::Number { word: words.flow, fallback: None })?; @@ -112,11 +113,11 @@ impl Stroke { } } -/// How a mark's rows answer the pointer. +/// How a mark's rows respond to the pointer. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) struct Interaction { - /// Whether rows are hovered, clicked, and probed; unless set, what the mark draws says: see - /// [`Mark::picks`]. + /// Whether rows respond to hover, clicks, and picks; unset, the mark's kind decides (see + /// [`Mark::picks`]). pub(crate) pick: Option, pub(crate) select: bool, pub(crate) drag: bool, @@ -144,7 +145,7 @@ impl Style { } } -/// What a scene's marks are drawn as. +/// The kind of a mark. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub(crate) enum MarkKind { Vertices, @@ -164,7 +165,7 @@ impl MarkKind { } } - /// What a mark of this kind is written under in JSON. + /// The key a mark of this kind has in JSON. pub(crate) fn name(self) -> &'static str { match self { MarkKind::Vertices => "vertices", @@ -185,7 +186,7 @@ pub(crate) struct Key { pub(crate) name: Option, } -/// The four shaders every mark draws with. +/// The primitive a mark draws its rows with: points, strokes, or triangles. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub(crate) enum Primitive { Points, @@ -203,17 +204,19 @@ impl Primitive { } } -/// How a mark's markers draw: built in or custom, their shape, and the inputs they read. +/// How a vertex mark's markers draw: the marker kind (built in or custom), its shape, and the +/// inputs it reads. #[derive(Clone, PartialEq, Debug, Default)] pub(crate) struct Markers { pub(crate) marker: u32, pub(crate) shape: u32, pub(crate) corner: f32, pub(crate) hole: f32, - /// Inputs, and the row word of the first. + /// The number of inputs, and the extra word holding the first. pub(crate) inputs: u32, pub(crate) input: u32, - /// A pie's slice colors, packed; an icon marker's layers, filled in when packed. + /// A pie's slice colors, packed; for an icon marker, its image layers, filled in when the icons + /// are packed. pub(crate) palette: Vec, /// An icon marker's images. pub(crate) icons: Vec, @@ -221,22 +224,22 @@ pub(crate) struct Markers { pub(crate) custom: Option>, } -/// How a compiled mark's labels draw: the extra word their words start at, the program column of -/// their text, their size in length units (CSS pixels, or units in a diagram), and where their lines -/// wrap. +/// How a compiled mark's labels draw: the extra word where the label words start, the program +/// column of the text, and the font size and wrap width in length units (CSS pixels, or units in a +/// diagram). #[derive(Clone, PartialEq, Debug)] pub(crate) struct Labeling { pub(crate) extra: u32, pub(crate) column: u32, pub(crate) size: f32, pub(crate) max_width: Option, - /// The font its texts are set in: the label's, else the theme's. + /// The font for the text: the label's, else the theme's. pub(crate) font: std::sync::Arc, - /// The most labels shown at once, or none. + /// The most labels shown at once; `None` for no limit. pub(crate) max: Option, } -/// What a mark compiles in: the theme it falls back on, and its space. +/// The context a mark compiles in: the theme it falls back on, the space, and the layout. #[derive(Clone, Copy)] pub(crate) struct Context<'a> { pub(crate) theme: &'a super::Theme, @@ -245,47 +248,47 @@ pub(crate) struct Context<'a> { } impl Context<'_> { - /// Whether the space is a diagram, where every length is a unit and the sheet says where vertices - /// stand and how wires run. + /// Whether the space is a diagram, where every length is in units and the sheet positions + /// vertices and routes wires. pub(crate) fn diagram(&self) -> bool { matches!(self.space, Space::Diagram(_)) } - /// Whether, outside a diagram, the view places vertex rows without positions by the scene's - /// layout: on a plane or the earth. + /// Whether the scene's layout places vertex rows without a position outside a diagram, on a + /// plane or the earth. pub(crate) fn placed(&self) -> bool { matches!(self.space, Space::Plane(_) | Space::Earth(_)) && !matches!(self.layout, super::Layout::None) } } -/// What a compiled mark draws with, besides its program. +/// Everything a compiled mark draws with besides its program. #[derive(Clone, PartialEq, Debug)] pub(crate) struct Drawing { pub(crate) kind: MarkKind, /// The primitive the mark draws its rows with. pub(crate) primitive: Primitive, - /// Where a stroke's segments run between, or where a triangle's corners come from: one of the + /// The path a stroke's segments follow, or where a triangle's corners come from: one of the /// `PATH_*` codes. pub(crate) path: u32, - /// Kind words per row, and extra words per row after them. + /// Kind words per row, then the extra words per row that follow them. pub(crate) stride: u32, pub(crate) extras: u32, pub(crate) flags: u32, pub(crate) markers: Markers, /// The program column a line traces, or `NONE`. pub(crate) trace: u32, - /// Program columns: the geometry a stroke's path runs through (a diagram's route, bends, or a - /// line's path) or an area's triangles; and each row's lane among edges joining the same two - /// vertices; `NONE` without. + /// Program columns, `NONE` when absent: the geometry a stroke's path runs through (a diagram + /// route, bends, or a line's path) or an area's triangles; and each row's lane among edges + /// joining the same two vertices. pub(crate) geometry: u32, pub(crate) lane: u32, pub(crate) label: Option, - /// A vertex mark's ports, by the reference field that names their nets, with the program column - /// reading that field. + /// A vertex mark's ports: the reference field naming each port's nets, the port, and the + /// program column that reads the field. pub(crate) ports: Vec<(Name, super::Port, u32)>, - /// The size a vertex mark's port labels are set at, in length units. + /// The font size of a vertex mark's port labels, in length units. pub(crate) port_label_size: f32, - /// The CSS pixels the mark's size keeps within: a vertex's half, a stroke's width. + /// CSS pixel bounds on the mark's size: a vertex's half-size, or a stroke's width. pub(crate) size_px: [f32; 2], } @@ -347,9 +350,9 @@ impl Mark { } } - /// Whether its rows are hovered, clicked, and probed: as its `pick` says, else whether they are - /// things, as vertices, edges, faces, and a plot's traces are; geometry (areas, and lines along - /// paths: coasts, borders, roads) is not, so hovering probes none of it. + /// Whether the mark's rows respond to hover, clicks, and picks. `pick` decides when set. + /// Otherwise vertices, edges, faces, and plot traces pick, and geometry (areas, and lines along + /// paths such as coasts, borders, and roads) does not, so hovering never probes it. pub(crate) fn picks(&self) -> bool { self.style().interaction.pick.unwrap_or(match self { Mark::Vertices(_) | Mark::Edges(_) | Mark::Faces(_) => true, @@ -358,13 +361,14 @@ impl Mark { }) } - /// `error`, met in this mark's options, as the `i`th mark of its scene: its path runs through the - /// mark as its JSON does, `marks[i].vertices`. + /// `error`, found in this mark's options, placed under the mark's JSON path as the scene's + /// `i`th mark, such as `marks[i].vertices`. pub(crate) fn within(&self, i: usize, error: Error) -> Error { error.at(self.kind().name()).at(i).at("marks") } - /// Why `data`, the dataset it names, cannot draw it: the table it names is absent. + /// The error when `data`, the dataset this mark names, lacks the mark's table; `None` when the + /// table exists. pub(crate) fn absent_from(&self, data: &crate::Snapshot) -> Option { let style = self.style(); data.table(&style.table) @@ -372,7 +376,8 @@ impl Mark { .then(|| Error::invalid(format!("dataset '{}' has no table '{}'", style.data, style.table)).at("of")) } - /// Whether its rows can carry comets: a stroke mark flowing at more than a constant 0. + /// Whether the mark's rows can carry comets: a stroke mark whose `flow` is anything but the + /// constant 0. pub(crate) fn flows(&self) -> bool { let stroke = match self { Mark::Edges(m) => &m.stroke, @@ -382,9 +387,10 @@ impl Mark { stroke.flow != Stroke::default().flow } - /// The path a stroke mark's rows run along, or an area's triangles come from: one of the `PATH_` - /// codes, which its pipelines are made for. In a diagram every wire runs through the geometry - /// its sheet worked out; on a plot a line traces its samples, elsewhere it runs along its path. + /// The path a stroke mark's rows follow, or where an area's triangles come from: one of the + /// `PATH_` codes, which its pipelines are built for; `None` for vertices and faces. In a + /// diagram every wire follows the route its sheet computed. On a plot a line traces its + /// samples, and elsewhere it follows its path field. pub(crate) fn path(&self, space: &Space) -> Option { match self { Mark::Edges(e) if e.poles => Some(wgsl::PATH_POLES), @@ -397,25 +403,26 @@ impl Mark { } } - /// The first option of this mark only a diagram has, if any: ports, groups, nets, orthogonal - /// routes, shadows. A diagram's own refusals: elevation, poles, and joins. + /// The first option of this mark that its space does not allow, with the reason. Outside a + /// diagram: ports, groups, nets (edges without ends), orthogonal routes, and shadows. In a + /// diagram: elevation, poles, and joins. pub(crate) fn misplaced(&self, diagram: bool) -> Option<(&'static str, &'static str)> { const ONLY: &str = "only a diagram has ports, groups, nets, orthogonal routes, and shadows"; if diagram { if let Mark::Edges(e) = self && e.poles { - return Some(("poles", "a diagram is flat: poles stand on planes and the earth")); + return Some(("poles", "a diagram is flat, so it cannot have poles")); } if let Mark::Edges(e) = self && e.joins.is_some() { - return Some(("joins", "a diagram's nets join vertex ports")); + return Some(("joins", "in a diagram, nets join vertex ports instead")); } if let Mark::Vertices(v) = self && v.elevation != Expr::from(0.0) { - return Some(("elevation", "a diagram is flat: vertices have no elevation")); + return Some(("elevation", "a diagram is flat, so vertices cannot have elevation")); } return None; } @@ -429,7 +436,7 @@ impl Mark { } } - /// Calls `f` with every expression the mark's options hold. + /// Calls `f` with every expression in the mark's options. pub(crate) fn for_each_expr<'a>(&'a self, f: &mut impl FnMut(&'a Expr)) { let style = self.style(); style.color.iter().map(|paint| &paint.0).chain([&style.visible]).chain(&style.priority).for_each(&mut *f); @@ -455,7 +462,8 @@ impl Mark { } } - /// Lines are keyed by the field they trace unless named. + /// The key that identifies this mark across scenes. An unnamed line mark is keyed by the field + /// it draws. pub(crate) fn key(&self) -> Key { let style = self.style(); let name = match self { @@ -465,7 +473,7 @@ impl Mark { Key { kind: self.kind(), data: style.data.clone(), table: style.table.clone(), name } } - /// Whether `other` draws the same, whatever either's interaction. + /// Whether `other` draws the same, ignoring interaction options. pub(crate) fn draws_like(&self, other: &Mark) -> bool { let mut a = self.clone(); a.style_mut().interaction = other.style().interaction; @@ -492,8 +500,8 @@ impl Mark { } } -/// Compiles `label`'s text, `priority` (its own, else the mark's), and color into extra words after -/// the mark's own. +/// Compiles `label`'s text, `priority` (the label's, else the mark's), and color into extra words +/// after the mark's own. fn compile_label( label: &Label, priority: Option<&Expr>, @@ -524,7 +532,8 @@ fn compile_label( Ok(()) } -/// A mark's documentation as its schema's description: its prose, before its examples. +/// A mark's doc lines as its schema description: the prose before the first code block, with lines +/// joined into paragraphs. pub(crate) fn described(lines: &[&str]) -> String { let prose = lines.iter().map(|line| line.strip_prefix(' ').unwrap_or(line)).take_while(|line| !line.starts_with("```")); @@ -537,9 +546,10 @@ pub(crate) fn described(lines: &[&str]) -> String { ) } -/// Declares a mark: its struct, holding the options every mark has, those strokes have where it -/// draws them, and its own; and its JSON form, flat, with `of` naming its dataset and table, from -/// which it deserializes and its schema derives. Its own options are declared once, here. +/// Declares a mark type: a struct holding the options every mark has, the stroke options when it +/// draws strokes, and its own options; and its flat JSON form, with `of` naming the dataset and +/// table, which it deserializes from and derives its schema from. Each option is declared once, +/// here. macro_rules! mark { ($(#[doc = $line:literal])* $name:ident $(+ $stroke:ident)? { $($(#[$doc:meta])* $field:ident: $ty:ty $(as $ts:literal)? $(= $default:literal)?),* $(,)? @@ -582,15 +592,15 @@ macro_rules! mark { }; (@form $name:ident + stroke { $($(#[$doc:meta])* $field:ident: $ty:ty $(as $ts:literal)? $(= $default:literal)?),* }) => { $crate::scene::mark::mark!(@struct { $($(#[$doc])* $field: $ty $(as $ts)? $(= $default)?),* } { - /// Each row's width, in the space's unit unless it says its own: units in a diagram, CSS - /// pixels elsewhere. + /// Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) + /// unless given with `px` or `units`. The theme's width unless set. width: Option<$crate::Size>, - /// Whether each row is dashed. + /// Whether each row is dashed, with the theme's `dash` period. Default `false`. #[serde(default = "crate::scene::mark::no")] dash: $crate::Expr, - /// Comets flowing along each row at this speed a second, in the lengths of the theme's - /// flow; negative flows backward. A speed that changes carries comets on from where they - /// are. + /// The speed of comets flowing along each row, per second, in the unit of the theme's + /// `flow` lengths. Negative speeds flow backward; 0, the default, draws no comets. When + /// the speed changes, comets continue from where they are. #[serde(default = "crate::scene::mark::still")] flow: $crate::Expr, }); @@ -602,7 +612,8 @@ macro_rules! mark { } }; (@struct { $($(#[$doc:meta])* $field:ident: $ty:ty $(as $ts:literal)? $(= $default:literal)?),* } { $($stroke:tt)* }) => { - /// The mark as JSON writes it: `of`, its own options, then those of strokes and of every mark. + /// The mark's JSON form: `of`, its own options, then the stroke options and the options + /// every mark has. #[derive(serde::Deserialize, schemars::JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct Form { @@ -611,32 +622,35 @@ macro_rules! mark { of: ($crate::Name, $crate::Name), $($(#[$doc])* $(#[schemars(extend("tsType" = $ts))])? #[serde(default $(= $default)?)] $field: $ty,)* $($stroke)* - /// Each row's color; the theme's unless set. + /// Each row's color. Unset, rows take the theme's default for the mark; faces instead + /// blend their corners' colors. color: Option<$crate::Paint>, - /// Whether each row is drawn and picked. + /// Whether each row is drawn and can be picked. Default `true`. #[serde(default = "crate::scene::mark::yes")] visible: $crate::Expr, - /// Each row's priority, from its data alone: what matters more draws on top, stays drawn as - /// a table too large to draw whole thins to the tiles in view, and is labeled first. + /// Each row's priority, from its data alone (no view state). Higher-priority rows draw + /// on top, are the last dropped when a table too large to draw whole is thinned to the + /// tiles in view, and are labeled first. priority: Option<$crate::Expr>, - /// Tells two marks of one table apart across scenes, and names the mark in hits. + /// Distinguishes two marks of the same table across scenes, and names the mark in hits. name: Option<$crate::Name>, - /// Whether rows are hovered, clicked, and probed. Unless set, rows that are things are - /// (vertices, edges, faces, a plot's traces) and geometry is not (areas, and lines along - /// paths: coasts, borders, roads). + /// Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and + /// plot traces do, and geometry (areas, and lines along paths such as coasts, borders, + /// and roads) does not. pick: Option, - /// Whether a click selects a row. + /// Whether clicking a row selects it. Default `true`. #[serde(default = "crate::scene::mark::on")] select: bool, - /// Whether pressing a row drags it instead of panning. + /// Whether pressing and moving a row drags it, reported as + /// [`Event::Drag`](crate::Event), instead of panning the camera. Default `false`. #[serde(default)] drag: bool, - /// Each row's text: a text field, or a label's options. + /// Each row's label: a text field, or label options. label: Option<$crate::Label>, - /// A legend of the colors at the view's bottom right, under this title: the colormap across - /// its domain, ticked; a cell for each category; or a swatch beside each name of a map's - /// `categories`. The color maps through a colormap with a domain or named categories; a - /// conditional's `otherwise` is read before its cases. + /// A legend of the mark's colors at the view's bottom right, under this title: a ticked + /// colormap across its domain, a cell for each category, or a swatch beside each name + /// of a map's `categories`. `color` must map through a colormap with a fixed domain or + /// named categories; in a conditional, `otherwise` is searched before the cases. legend: Option>, } @@ -660,7 +674,7 @@ macro_rules! mark { pub(crate) use mark; -/// Defaults of the JSON forms. +/// Defaults for options of the JSON forms. pub(crate) fn yes() -> Expr { true.into() } @@ -681,56 +695,60 @@ pub(crate) fn zero() -> Expr { 0.0.into() } -/// The options every mark has, as inherent methods. +/// Builder methods for the options every mark has. macro_rules! style_methods { ($mark:ty) => { impl $mark { - /// Each row's color. + /// Sets each row's color. Unset, rows take the theme's default for the mark; faces + /// instead blend their corners' colors. #[must_use] pub fn color(mut self, color: impl Into<$crate::Paint>) -> Self { self.style.color = Some(color.into()); self } - /// Whether each row is drawn and picked. + /// Sets whether each row is drawn and can be picked; all rows by default. #[must_use] pub fn visible(mut self, visible: impl Into<$crate::Expr>) -> Self { self.style.visible = visible.into(); self } - /// Each row's priority, from its data alone: what matters more draws on top, stays drawn - /// as a table too large to draw whole thins to the tiles in view, and is labeled first. + /// Sets each row's priority, from its data alone (no view state). Higher-priority rows + /// draw on top, are the last dropped when a table too large to draw whole is thinned to + /// the tiles in view, and are labeled first. #[must_use] pub fn priority(mut self, priority: impl Into<$crate::Expr>) -> Self { self.style.priority = Some(priority.into()); self } - /// Names the mark, so two marks of one table can be told apart across scenes. + /// Names the mark, to distinguish two marks of the same table across scenes and to + /// identify the mark in hits. #[must_use] pub fn named(mut self, name: impl Into<$crate::Name>) -> Self { self.style.name = Some(name.into()); self } - /// Whether rows are hovered, clicked, and probed. Unless set, rows that are things are - /// (vertices, edges, faces, a plot's traces) and geometry is not (areas, and lines along - /// paths: coasts, borders, roads). + /// Sets whether rows respond to hover, clicks, and picks. Unset, vertices, edges, + /// faces, and plot traces do, and geometry (areas, and lines along paths such as + /// coasts, borders, and roads) does not. #[must_use] pub fn pick(mut self, on: bool) -> Self { self.style.interaction.pick = Some(on); self } - /// Whether a click selects a row; on by default. + /// Sets whether clicking a row selects it; on by default. #[must_use] pub fn select(mut self, on: bool) -> Self { self.style.interaction.select = on; self } - /// Whether pressing a row drags it instead of panning; off by default. + /// Sets whether pressing and moving a row drags it instead of panning the camera; off + /// by default. #[must_use] pub fn drag(mut self, on: bool) -> Self { self.style.interaction.drag = on; @@ -744,11 +762,11 @@ macro_rules! style_methods { self } - /// Draws a legend of the colors at the view's bottom right, under `title`: the colormap - /// across its domain, ticked; a cell for each category; or a swatch beside each name of - /// [named categories](crate::Colormap::categories). The color maps through a colormap - /// with a domain or named categories; a conditional's `otherwise` is read before its - /// cases. Legends stack upward in the marks' order. + /// Draws a legend of the mark's colors at the view's bottom right, under `title`: a + /// ticked colormap across its domain, a cell for each category, or a swatch beside each + /// of the [named categories](crate::Colormap::categories). The color must map through a + /// colormap with a fixed domain or named categories; in a conditional, `otherwise` is + /// searched before the cases. Legends stack upward in mark order. #[must_use] pub fn legend(mut self, title: impl Into>) -> Self { self.style.legend = Some(title.into()); @@ -758,29 +776,28 @@ macro_rules! style_methods { }; } -/// The options of marks that draw strokes, as inherent methods. +/// Builder methods for the stroke options of edges and lines. macro_rules! stroke_methods { ($mark:ty) => { impl $mark { - /// Each row's width, in the space's unit unless it says its own: units in a diagram, CSS - /// pixels elsewhere; [`px`](crate::px) keeps CSS pixels, [`units`](crate::units) takes - /// position units. + /// Sets each row's width, in the space's unit (units in a diagram, CSS pixels + /// elsewhere) unless given with [`px`](crate::px) or [`units`](crate::units). #[must_use] pub fn width(mut self, width: impl Into<$crate::Size>) -> Self { self.stroke.width = Some(width.into()); self } - /// Whether each row is dashed. + /// Sets whether each row is dashed, with the theme's dash period. #[must_use] pub fn dash(mut self, dash: impl Into<$crate::Expr>) -> Self { self.stroke.dash = dash.into(); self } - /// Comets flowing along each row at this speed a second, in the lengths of the theme's - /// [`flow`](crate::FlowTheme); negative flows backward. A speed that changes carries - /// comets on from where they are. + /// Sets the speed of comets flowing along each row, per second, in the unit of the + /// theme's [`flow`](crate::FlowTheme) lengths. Negative speeds flow backward, and 0 + /// draws no comets. When the speed changes, comets continue from where they are. #[must_use] pub fn flow(mut self, speed: impl Into<$crate::Expr>) -> Self { self.stroke.flow = speed.into(); diff --git a/crates/latkit/src/scene/marker.rs b/crates/latkit/src/scene/marker.rs index 450ed8a0..222aa038 100644 --- a/crates/latkit/src/scene/marker.rs +++ b/crates/latkit/src/scene/marker.rs @@ -8,51 +8,53 @@ use super::mark::Markers; use crate::gpu::shaders as wgsl; use crate::{Color, Expr, Image}; -/// How a vertex is drawn: a shape's name, `"block"`, or `{ shape | block | gauge | pie | icon | wgsl: -/// options }`. +/// How a vertex is drawn: a shape's name, `"block"`, or +/// `{ shape | block | gauge | pie | icon | wgsl: options }`. An ellipse by default. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(remote = "Self", rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = presets::)] pub enum Marker { /// A filled shape. Shape(Shape), - /// A block: a rectangle with rounded corners, sized for diagrams. + /// A rectangle with rounded corners that holds its label inside, as a diagram's blocks do. Block { - /// Corner radius, CSS pixels, or units in a diagram; the theme's `corner` unless set. + /// Corner radius in the space's unit (units in a diagram, CSS pixels elsewhere); the + /// theme's `diagram.corner` unless set. #[serde(default)] corner: Option, }, - /// A shape's outline, filled to `fill` from 0 to 1: around a circle, up other shapes. + /// A shape's outline, filled to `fill` from 0 to 1: around a circle, or upward in other shapes. Gauge { - /// How full each row's gauge is. + /// How full each row's gauge is, from 0 to 1. fill: Expr, - /// The outline. + /// The outline's shape; an ellipse by default. #[serde(default)] shape: Shape, }, - /// A circle cut into slices, each as large as its value. + /// A circle divided into slices sized in proportion to their values. Pie { /// Each slice's value. slices: Vec, /// Each slice's color. colors: Vec, - /// The hole in the middle, as a fraction of the radius. + /// The hole in the middle, as a fraction of the radius; 0 by default. #[serde(default)] hole: f32, }, - /// One of `images`, fitted in the marker's box; `index` picks which for each row. + /// One of `images`, fitted to the marker's box; `index` picks the image for each row. Icon { /// The images. images: Vec, - /// Each row's image, from 0. + /// Each row's image index, from 0. index: Expr, }, - /// A marker in WGSL: `source` defines `fn marker(f: MarkerFragment) -> MarkerColor`, reading up to - /// eight `inputs` as `f.input[i]`. It compiles once, when the view first draws it. + /// A custom marker in WGSL. `source` defines `fn marker(f: MarkerFragment) -> MarkerColor`, + /// which can read up to eight `inputs` as `f.input[i]`. It compiles once, when the view first + /// draws it. Wgsl { - /// The function's source. + /// The WGSL source of the `marker` function. source: Arc, - /// Each row's inputs. + /// Each row's inputs, at most eight. #[serde(default)] inputs: Vec, }, @@ -111,7 +113,7 @@ impl From for Marker { } impl Marker { - /// A block with the theme's corners. + /// A block with the theme's corner radius. pub fn block() -> Marker { Marker::Block { corner: None } } @@ -127,13 +129,13 @@ impl Marker { } } - /// How the mark's markers draw; inputs start at extra word `input`. + /// How the mark's markers draw, with their inputs starting at extra word `input`. pub(crate) fn markers(&self, input: u32) -> Markers { let inputs = self.inputs().len() as u32; let base = Markers { inputs, input, ..Markers::default() }; match self { Marker::Shape(shape) => Markers { marker: wgsl::MARKER_SHAPE, shape: shape.code(), ..base }, - // A negative corner takes the theme's. + // A negative corner means the theme's corner radius. Marker::Block { corner } => Markers { marker: wgsl::MARKER_BLOCK, shape: wgsl::SHAPE_ROUNDED, diff --git a/crates/latkit/src/scene/mod.rs b/crates/latkit/src/scene/mod.rs index 5cf84f72..24bb9d06 100644 --- a/crates/latkit/src/scene/mod.rs +++ b/crates/latkit/src/scene/mod.rs @@ -1,5 +1,5 @@ -//! Scenes: what a view draws, as plain data. Marks name their datasets; the view draws them from the -//! sources it is given. +//! Scenes: plain-data descriptions of a view's content. Marks name their datasets, and the view +//! draws them from the sources it is given. pub(crate) mod areas; pub(crate) mod compile; @@ -43,9 +43,11 @@ pub use vertices::Vertices; use crate::{Error, Result, Snapshot, Source}; -/// What a view draws and how it answers input: a space, marks in drawing order, a layout, a theme, -/// controls, how long changes ease, and the title written over it. Plain data: as JSON, each field by -/// its name in camelCase, each mark `{ vertices | edges | faces | lines | areas: options }`. +/// A view's content and behavior: a space, marks drawn in order, a layout, a theme, controls, an +/// ease duration, and an optional title. +/// +/// A scene is plain data. In JSON, fields are camelCase and each mark is +/// `{ vertices | edges | faces | lines | areas: options }`. /// /// ``` /// # use latkit::{Colormap, Edges, Earth, Projection, Scene, Vertices, field}; @@ -58,31 +60,34 @@ use crate::{Error, Result, Snapshot, Source}; #[derive(Clone, PartialEq, Debug, serde::Deserialize, schemars::JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Scene { + /// The coordinate space positions are in. pub(crate) space: Space, - /// What the view draws, in drawing order. + /// The marks, in drawing order: each draws over the marks before it. #[serde(default)] pub(crate) marks: Vec, - /// How the view places vertex rows without positions: a diagram's blocks, or points on a plane - /// or the earth, gaps in the space's units. + /// How the view places vertex rows that have no position: a diagram's blocks, or points on a + /// plane or the earth. `none` by default. #[serde(default)] pub(crate) layout: Layout, + /// Default colors and sizes, and the look of text, focus, and each space; `dark` by default. #[serde(default)] pub(crate) theme: Theme, + /// How the view responds to the pointer and keyboard. #[serde(default)] pub(crate) controls: Controls, - /// Seconds that changes ease over; 0.3 unless set. + /// Seconds over which scene and data changes animate; 0.3 by default, 0 for none. #[serde(default = "ease", deserialize_with = "form::seconds")] #[schemars(with = "f64")] pub(crate) ease: Duration, - /// Written at the view's top left, larger than the theme's text. + /// A title drawn at the view's top left, larger than the theme's text. #[serde(default)] pub(crate) title: Option>, - /// Written under the title. + /// A subtitle drawn under the title. #[serde(default)] pub(crate) subtitle: Option>, } -/// How long changes ease unless set. +/// The default ease duration. fn ease() -> Duration { Duration::from_millis(300) } @@ -102,63 +107,67 @@ impl Scene { } } - /// Draws `mark` over the marks before it. + /// Adds `mark`, drawn over the marks added before it. #[must_use] pub fn draw(mut self, mark: impl Into) -> Scene { self.marks.push(mark.into()); self } - /// Places vertex rows that have no position: a diagram's blocks, or points on a plane or the earth. + /// Sets the layout that places vertex rows without a position: a diagram's blocks, or points on + /// a plane or the earth. #[must_use] pub fn layout(mut self, layout: impl Into) -> Scene { self.layout = layout.into(); self } - /// The theme. + /// Sets the theme. #[must_use] pub fn theme(mut self, theme: Theme) -> Scene { self.theme = theme; self } - /// How the view answers the pointer and keyboard. + /// Sets how the view responds to the pointer and keyboard. #[must_use] pub fn controls(mut self, controls: Controls) -> Scene { self.controls = controls; self } - /// How long changed descriptions and new data ease; zero changes at once. + /// Sets how long scene and data changes animate; zero applies them at once. Default 0.3 s. #[must_use] pub fn ease(mut self, over: Duration) -> Scene { self.ease = over; self } - /// Writes `title` at the view's top left, in the theme's text and font, larger. + /// Sets a title drawn at the view's top left, in the theme's text color and font but larger. #[must_use] pub fn title(mut self, title: impl Into>) -> Scene { self.title = Some(title.into()); self } - /// Writes `subtitle` under the title. + /// Sets a subtitle drawn under the title. #[must_use] pub fn subtitle(mut self, subtitle: impl Into>) -> Scene { self.subtitle = Some(subtitle.into()); self } - /// The space. + /// The scene's space. pub fn space(&self) -> &Space { &self.space } - /// Checks the scene against the sources a view would draw it from, without a GPU: the compiler a - /// view uses, every dataset and table drawn present, but for those tiles give, which a view - /// fetches as it draws, so their marks are checked as their pieces land. + /// Checks the scene against `sources` without a GPU, with the same compiler a view uses. + /// + /// Every drawn dataset and table must be present, except datasets that a [`Source::Tiles`] + /// gives: a view fetches those while drawing, so their marks are checked as tiles arrive. + /// Errors on duplicate source names, missing datasets or tables, options a mark cannot compile, + /// and anything else the scene itself gets wrong, such as two marks with one key. pub fn validate(&self, sources: &[Source]) -> Result<()> { self.check()?; for (i, source) in sources.iter().enumerate() { @@ -180,7 +189,7 @@ impl Scene { continue; } let dataset = snapshots.iter().find(|d| *d.name() == style.data).ok_or_else(|| { - mark.within(i, Error::invalid(format!("no source gives dataset '{}'", style.data)).at("of")) + mark.within(i, Error::invalid(format!("no source provides dataset '{}'", style.data)).at("of")) })?; if let Some(error) = mark.absent_from(dataset) { return Err(mark.within(i, error)); @@ -195,16 +204,18 @@ impl Scene { Ok(()) } - /// Two marks with one key would be indistinguishable across scenes; options a space does not have - /// are refused. + /// Checks the scene's own consistency, which needs no data. Errors when a plot has a layout, + /// when the theme's comet spacing and length use different units, when two marks share a key + /// (they would be indistinguishable across scenes), when a mark uses an option its space lacks, + /// and when a legend has no colormap with a fixed domain or named categories to show. pub(crate) fn check(&self) -> Result<()> { let diagram = matches!(self.space, Space::Diagram(_)); if matches!(self.space, Space::Plot(_)) && self.layout != Layout::None { - return Err(Error::invalid("a plot lays out no vertices").at("layout")); + return Err(Error::invalid("a plot cannot have a layout").at("layout")); } let flow = &self.theme.flow; if !flow.spacing.alike(flow.length) { - let reason = "a comet's spacing and length are in one unit, which each row's flow is in too"; + let reason = "comet spacing and length must use the same unit, which each row's flow also uses"; return Err(Error::invalid(reason).at("length").at("flow").at("theme")); } for (i, mark) in self.marks.iter().enumerate() { @@ -221,7 +232,7 @@ impl Scene { None => false, }; if style.legend.is_some() && !shown { - let reason = "a legend shows a color mapped through a colormap with a domain or named categories"; + let reason = "a legend needs a color mapped through a colormap with a domain or named categories"; return Err(mark.within(i, Error::invalid(reason).at("legend"))); } } diff --git a/crates/latkit/src/scene/port.rs b/crates/latkit/src/scene/port.rs index cf8c6103..e66d4c12 100644 --- a/crates/latkit/src/scene/port.rs +++ b/crates/latkit/src/scene/port.rs @@ -5,9 +5,9 @@ use super::Shape; use super::form::{Presets, preset_or, presets}; use crate::gpu::shaders as wgsl; -/// A vertex's port: where a net its reference field names meets it, on one side of its marker: a -/// side, or options. An output drives its net: layouts rank a net from its outputs, and its arrows -/// point away from them. +/// A port on one side of a vertex's marker, where a net connects: a side name, or options. The +/// vertex names the net in a reference field. An output drives its net: layouts rank a net from its +/// outputs, and the net's arrows point away from them. /// /// ``` /// # use latkit::{Port, Side, Vertices}; @@ -17,18 +17,19 @@ use crate::gpu::shaders as wgsl; #[serde(remote = "Self", rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = presets::)] pub struct Port { - /// The side it sits on. + /// The side of the marker it sits on. pub side: Side, - /// Its place among the ports on that side, from the top or the left; in declaration order unless set. + /// Its position among the ports on its side, counted from the top or left. Ports without an + /// `order` follow those with one, in declaration order. #[serde(default)] pub order: Option, /// Its name, drawn beside it inside the marker. #[serde(default)] pub label: Option, - /// Its marker. + /// Its shape; an ellipse by default. #[serde(default)] pub shape: Shape, - /// Whether it drives the net its reference field names; an input unless set. + /// Whether it is an output, which drives its net. Default `false`, an input. #[serde(default)] pub output: bool, } @@ -77,16 +78,16 @@ impl Port { Port { output: true, ..Port::new(side) } } - /// This port, labeled `label`. + /// Sets the port's label. #[must_use] pub fn label(mut self, label: impl Into) -> Port { self.label = Some(label.into()); self } - /// Each of `ports`' codes: its side in two bits, its place among the ports on its side (by their - /// order, then as declared) and how many share the side in eight bits each, and whether it is an - /// output. + /// Each port of `ports` packed into a code: its side in bits 0–1, its place among the ports on + /// that side (by `order`, then declaration order) in bits 2–9, the number of ports on that side + /// in bits 10–17, and the `PORT_OUTPUT` bit for an output. pub(crate) fn codes(ports: &[&Port]) -> Vec { (0..ports.len()) .map(|i| { diff --git a/crates/latkit/src/scene/space.rs b/crates/latkit/src/scene/space.rs index 5248376a..ecccec2a 100644 --- a/crates/latkit/src/scene/space.rs +++ b/crates/latkit/src/scene/space.rs @@ -3,21 +3,23 @@ use serde::{Deserialize, Deserializer}; use super::form::{Presets, preset_or, presets}; -/// The coordinate space a scene's positions are in: a name, or `{ plane | diagram | earth | plot: -/// options }`. Its own unit is what lengths are in unless they say theirs: units in a diagram, a flat -/// sheet seen from straight above with y running down, a unit a CSS pixel at zoom 0; CSS pixels -/// elsewhere. Only a diagram has ports, groups, nets, and orthogonal routes. +/// The coordinate space of a scene's positions: a name, or +/// `{ plane | diagram | earth | plot: options }`. +/// +/// The space sets the unit of lengths that do not name their own: units in a diagram, where a unit +/// is a CSS pixel at zoom 0, and CSS pixels elsewhere. Only a diagram has ports, groups, nets, and +/// orthogonal routes. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(remote = "Self", rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = presets::)] pub enum Space { /// Longitude and latitude in degrees, on a flat map or a globe. Earth(Earth), - /// Plane units with elevation: graphs and scenes the camera turns and tilts around. + /// Plane units, with elevation; the camera can turn and tilt. Plane(Plane), - /// The data's axis across, values up. + /// The data's axis horizontally and values vertically. Plot(Plot), - /// A sheet for diagrams: x right and y down in units, every length a unit. + /// A flat sheet for diagrams: x right and y down, every length in units. Diagram(Diagram), } @@ -75,24 +77,26 @@ impl From for Space { } } -/// The earth: positions are longitude and latitude in degrees, its ground in the theme's earth +/// The earth: positions are longitude and latitude in degrees. The ground takes the theme's `earth` /// colors. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct Earth { - /// A flat map or a globe; a flat map unless set. + /// A flat web mercator map (`mercator`, the default) or a globe (`globe`). pub projection: Projection, - /// Whether lines of longitude and latitude cross the ground, in the theme's grid color. + /// Whether lines of longitude and latitude cross the ground, in the theme's `grid` color. + /// Default `true`. pub graticule: bool, - /// Lit by the sun as it stands this many seconds after the Unix epoch, plus the view's axis - /// coordinate when it shows one: night falls on the ground and what stands on it, down to the - /// theme's night. Lit evenly unless set. + /// Lights the earth by the sun's position this many seconds after the Unix epoch, plus the + /// view's axis coordinate when it has one. The night side darkens the ground and everything on + /// it, down to the theme's `earth.night` brightness. Unset, the earth is lit evenly. pub daylight: Option, - /// Whether air glows around the globe, in the theme's atmosphere color, brighter where it is day. + /// Whether air glows around the globe, in the theme's `earth.atmosphere` color, brighter on the + /// day side. Default `false`. pub atmosphere: bool, } -/// How the earth is drawn. +/// How the earth is projected. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub enum Projection { @@ -104,21 +108,22 @@ pub enum Projection { } impl Earth { - /// Drawn as `projection` shows it: a flat map or a globe. + /// Sets the projection: a flat map or a globe. #[must_use] pub fn projection(mut self, projection: Projection) -> Earth { self.projection = projection; self } - /// Lit by the sun as it stands `seconds` after the Unix epoch, plus the view's axis coordinate. + /// Lights the earth by the sun's position `seconds` after the Unix epoch, plus the view's axis + /// coordinate. #[must_use] pub fn daylight(mut self, seconds: f64) -> Earth { self.daylight = Some(seconds); self } - /// Whether air glows around the globe. + /// Sets whether air glows around the globe. #[must_use] pub fn atmosphere(mut self, on: bool) -> Earth { self.atmosphere = on; @@ -126,11 +131,11 @@ impl Earth { } } -/// The direction toward the sun `seconds` after the Unix epoch, in the earth's frame: x toward 0° -/// longitude on the equator, y toward 90° east, z toward the north pole. The sun's place by the -/// low-precision formulas of the Astronomical Almanac, to a hundredth of a degree. +/// The unit direction toward the sun `seconds` after the Unix epoch, in earth-fixed coordinates: x +/// toward 0° longitude on the equator, y toward 90° east, z toward the north pole. Uses the +/// low-precision formulas of the Astronomical Almanac, accurate to about a hundredth of a degree. pub(crate) fn sun(seconds: f64) -> [f64; 3] { - // Days since noon, January 1, 2000. + // Days since noon UTC, January 1, 2000. let n = seconds / 86_400.0 - 10_957.5; let mean = (280.460 + 0.985_647_4 * n).rem_euclid(360.0); let anomaly = (357.528 + 0.985_600_3 * n).rem_euclid(360.0).to_radians(); @@ -144,17 +149,17 @@ pub(crate) fn sun(seconds: f64) -> [f64; 3] { } impl Default for Earth { - /// A web mercator map, its graticule drawn. + /// A web mercator map with a graticule, lit evenly, without atmosphere. fn default() -> Earth { Earth { projection: Projection::Mercator, graticule: true, daylight: None, atmosphere: false } } } -/// A plane: positions are plane units. +/// A plane: positions are in plane units. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct Plane { - /// The grid under the marks. + /// The grid drawn under the marks; lines 10 units apart by default. pub grid: Grid, } @@ -164,15 +169,16 @@ impl Default for Plane { } } -/// A sheet for diagrams, flat and seen from straight above: the camera pans and zooms. Positions are -/// units with y down, and lengths are units, a unit a CSS pixel at zoom 0 (sizes, padding, ports, -/// strokes, text, and the theme's), but for what says `px`, a halo, focus's glow, and the pick -/// radius. Zooming scales one fixed picture. Only a diagram has ports, groups, nets, and orthogonal -/// routes; it lays out blocks. +/// A flat sheet for diagrams, seen from straight above; the camera pans and zooms but does not +/// turn. Positions are in units with y down. Lengths are in units too, a unit being a CSS pixel at +/// zoom 0: sizes, padding, ports, strokes, text, and the theme's lengths. Sizes given with `px`, +/// the text halo, the focus glow, and the pick radius stay in CSS pixels. Zooming scales the whole +/// picture. Only a diagram has ports, groups, nets, and orthogonal routes, and the scene's layout +/// places its blocks. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct Diagram { - /// The grid under the marks. + /// The grid drawn under the marks; dots 20 units apart by default. pub grid: Grid, } @@ -188,16 +194,16 @@ impl Default for Diagram { pub enum Grid { /// No grid. Off, - /// Lines `pitch` units apart, thinned by tens as the view zooms out. + /// Lines `pitch` units apart, thinned by factors of ten as the view zooms out. Lines { /// Units between lines. pitch: f64, }, - /// Dots `pitch` units apart; drags snap to them when `snap`. + /// Dots `pitch` units apart; with `snap`, a diagram's drags snap to them. Dots { /// Units between dots. pitch: f64, - /// Whether moved vertices snap to the dots. + /// Whether drag positions snap to the dots, in a diagram. Default `false`. #[serde(default)] snap: bool, }, @@ -219,24 +225,24 @@ pub struct Plot { pub struct Axis { /// The axis title. pub label: Option, - /// Whether grid lines cross the plot at the ticks. + /// Whether grid lines cross the plot at the ticks. Default `true`. pub grid: bool, - /// Whether the axis is hidden. + /// Whether the axis is hidden. Default `false`. pub hidden: bool, - /// Ticks at these values, each a value or `[value, label]`, their values written unless labeled; - /// round values unless set. + /// Tick positions, each a value or `[value, label]`; a tick without a label shows its value. + /// Unset, ticks fall at round values. #[serde(deserialize_with = "ticks")] #[schemars(with = "Option>")] pub ticks: Ticks, - /// How tick values are written. + /// How tick values are formatted; `auto` by default. pub format: Format, - /// Decimals of fixed values, or of the significand of scientific and engineering ones; as many - /// as the ticks' step needs unless set. + /// Decimal places of fixed values, or of the significand in scientific and engineering formats, + /// at most 15. Unset, as many as the tick step needs. #[schemars(range(max = 15))] pub precision: Option, } -/// A tick as written: a value, or `[value, label]`. +/// A tick in JSON: a value, or `[value, label]`. #[derive(Deserialize, JsonSchema)] #[serde(untagged)] enum Tick { @@ -244,7 +250,8 @@ enum Tick { Labeled(f64, Option), } -/// An axis's ticks: each a value, and the label written there unless its value is. +/// An axis's ticks: each a value and an optional label, which replaces the value as the tick's +/// text. type Ticks = Option)>>; fn ticks<'de, D: Deserializer<'de>>(deserializer: D) -> Result { @@ -266,7 +273,7 @@ impl Default for Axis { } } -/// How an axis writes its tick values. +/// How an axis formats its tick values. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub enum Format { @@ -286,13 +293,14 @@ mod tests { use super::sun; #[test] - fn the_sun_stands_where_the_almanac_puts_it() { - // Noon UTC at the June solstice of 2026: over the tropic of Cancer, near the prime meridian. + fn sun_position_matches_the_almanac() { + // Noon UTC at the 2026 June solstice: the sun is over the Tropic of Cancer, near the prime + // meridian. let [x, y, z] = sun(1_782_043_200.0); let (declination, longitude) = (z.asin().to_degrees(), y.atan2(x).to_degrees()); assert!((declination - 23.44).abs() < 0.05, "{declination}"); assert!(longitude.abs() < 1.0, "{longitude}"); - // Six hours on, it stands a quarter turn west. + // Six hours later, it is a quarter turn west. let [x, y, _] = sun(1_782_043_200.0 + 6.0 * 3600.0); assert!((y.atan2(x).to_degrees() + 90.0).abs() < 1.5); } diff --git a/crates/latkit/src/scene/tests.rs b/crates/latkit/src/scene/tests.rs index 94d83e0a..652376ad 100644 --- a/crates/latkit/src/scene/tests.rs +++ b/crates/latkit/src/scene/tests.rs @@ -1,4 +1,4 @@ -//! Scenes read as JSON match the builders, field for field. +//! Tests that scenes read from JSON equal the same scenes built in Rust, field for field. use std::time::Duration; @@ -36,9 +36,9 @@ fn scenes_match_builders() { } #[test] -fn things_pick_and_geometry_does_not_unless_asked() { +fn rows_pick_by_default_and_geometry_only_when_asked() { let picks = |mark: Mark| mark.picks(); - assert!(picks(Vertices::of("d", "t").into()) && picks(Edges::of("d", "t").into()), "rows that are things"); + assert!(picks(Vertices::of("d", "t").into()) && picks(Edges::of("d", "t").into()), "vertices and edges"); assert!(picks(Lines::of("d", "t").trace("v").into()), "a plot's traces"); assert!(!picks(Lines::of("d", "t").into()) && !picks(Areas::of("d", "t").into()), "geometry"); assert!(picks(Areas::of("d", "t").pick(true).into()) && !picks(Vertices::of("d", "t").pick(false).into())); @@ -96,7 +96,7 @@ fn a_text_in_a_color_option_is_a_color_and_elsewhere_a_field() { r#"{ "space": "plane", "marks": [{ "vertices": { "of": ["grid", "Bus"], "color": "kv" } }] }"#, ) .expect_err("a field's name where a color goes"); - assert!(error.to_string().contains(r#"not a CSS color: "kv"; a field is field("kv")"#), "{error}"); + assert!(error.to_string().contains(r#"not a CSS color: "kv"; for a field, use field("kv")"#), "{error}"); } #[test] @@ -112,7 +112,7 @@ fn joins_read_from_another_table() { } #[test] -fn areas_and_the_earths_look_match_builders() { +fn areas_and_earth_options_match_builders() { let json = r##"{ "space": { "earth": { "projection": "globe", "daylight": 1782043200, "atmosphere": true } }, "marks": [ @@ -179,7 +179,7 @@ fn themes_read_a_base_and_options_over_it() { } #[test] -fn presets_stand_for_values() { +fn presets_equal_their_values() { let controls = |json: &str| serde_json::from_str::(json).expect("controls"); assert_eq!(controls(r#""inspect""#), Controls::inspect()); assert_eq!(controls(r#""none""#), Controls::none()); @@ -192,7 +192,7 @@ fn presets_stand_for_values() { ..Controls::default() }; assert_eq!(options, expected); - assert!(serde_json::from_str::(r#"{ "zoom": true }"#).is_err(), "zoom names what zooms"); + assert!(serde_json::from_str::(r#"{ "zoom": true }"#).is_err(), "zoom names an input, not a boolean"); assert_eq!(serde_json::from_str::(r#""layered""#).expect("a layout"), Layout::Layered(Layered::default())); let layered = r#"{ "layered": { "flow": "down", "rankGap": 40 } }"#; let layered = serde_json::from_str::(layered).expect("a layout"); diff --git a/crates/latkit/src/scene/theme.rs b/crates/latkit/src/scene/theme.rs index c71f643a..e14cb635 100644 --- a/crates/latkit/src/scene/theme.rs +++ b/crates/latkit/src/scene/theme.rs @@ -8,35 +8,38 @@ use serde_json::Value; use super::form::{Presets, presets}; use crate::Color; -/// Colors and sizes marks take where their options read nothing, and the look of text, focus, -/// motion, and each space, grouped by what they style: a base theme's name, or options over one -/// (`base`, dark unless set), each group's over the base's. Lengths are in the space's unit, units in a -/// diagram and CSS pixels elsewhere, but for a text's halo and focus's glow, CSS pixels everywhere. +/// Default colors and sizes for marks whose options leave them unset or missing, and the look of +/// text, focus, comets, and each space, grouped by subject. Write a base theme's name (`"dark"` or +/// `"light"`), or options over a base (`base`, dark by default); a group's options override only +/// the base values they set. +/// +/// Lengths are in the space's unit (units in a diagram, CSS pixels elsewhere), except the text halo +/// and the focus glow, which are always in CSS pixels. #[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] #[serde(remote = "Self", default, rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = with_base)] pub struct Theme { - /// Behind everything. + /// The background color. pub background: Color, - /// A plane's or a diagram's grid, and the earth's graticule. + /// The color of a plane's or diagram's grid, a plot's grid lines, and the earth's graticule. pub grid: Color, - /// What a missing color reads as. + /// The color a colormap gives a missing value, such as a row whose field has none. pub missing: Color, - /// Length of a dash. + /// The dash period: one dash and one gap of equal length. pub dash: f32, - /// How much a face turned away from the camera darkens, 0 to 1. + /// How much faces darken as they turn away from the camera, from 0 to 1. pub shading: f32, - /// Labels, axis ticks, and titles. + /// How labels, axis ticks, and titles look. pub text: TextTheme, - /// Vertices where their options say nothing. + /// Defaults for vertices. pub vertex: VertexTheme, - /// Edges where their options say nothing, their lanes, and their arrowheads. + /// Defaults for edges, and how parallel edges and arrowheads look. pub edge: EdgeTheme, - /// Lines where their options say nothing. + /// Defaults for lines. pub line: LineTheme, - /// Areas where their options say nothing. + /// Defaults for areas. pub area: AreaTheme, - /// How hovered and selected rows glow, and rows outside a selection dim. + /// How hovered and selected rows glow, and how rows outside a selection dim. pub focus: FocusTheme, /// How comets flowing along edges and lines look. pub flow: FlowTheme, @@ -48,106 +51,110 @@ pub struct Theme { pub plot: PlotTheme, } -/// Labels, axis ticks, and titles. +/// How labels, axis ticks, and titles look. #[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = merged)] pub struct TextTheme { - /// Text's color. + /// The text color. pub color: Color, - /// The CSS font family text is set in. + /// The CSS font family list. pub font: Arc, - /// Text's size. + /// The font size, in the space's unit. pub size: f32, - /// CSS pixels of background around label text. + /// Width of the background-colored halo around label text, in CSS pixels. pub halo: f32, } -/// Vertices where their options say nothing. +/// Defaults for vertices. #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = merged)] pub struct VertexTheme { - /// A vertex's color. + /// The vertex color. pub color: Color, - /// A vertex's radius. + /// The vertex radius, in the space's unit. pub radius: f32, } -/// Edges where their options say nothing, their lanes, and their arrowheads. +/// Defaults for edges, and how parallel edges and arrowheads look. #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = merged)] pub struct EdgeTheme { - /// An edge's color; `None` blends the colors of each edge's two vertices, and in a diagram takes - /// the text color. + /// The edge color. `None` blends the colors of each edge's two vertices; a diagram's wires and + /// edges without `ends` (nets and poles) then take the text color. pub color: Option, - /// An edge's width. + /// The edge width, in the space's unit. pub width: f32, - /// In a diagram, distance between the lanes of edges joining the same two vertices, and between - /// parallel orthogonal wires, which keep twice it apart. + /// In a diagram, the distance between edges joining the same two vertices and between parallel + /// orthogonal wires. Wires keep twice this distance from blocks where they can. pub spacing: f32, - /// Elsewhere, how far edges joining the same two vertices bow apart: each lane by this fraction - /// of the edge's length at its middle, as wide as eight lanes at most, more packing closer. A bow - /// is part of the drawing, so zooming never moves it. + /// Outside a diagram, how far edges joining the same two vertices bow apart: each lane by this + /// fraction of the edge's length at its middle. A family spreads at most eight lanes wide, and + /// more lanes pack closer. A bow is part of the drawing, so it scales with the zoom. pub bow: f32, - /// How far an arrowhead reaches past its edge's width. + /// How far an arrowhead extends beyond the edge's width: a head is this plus the width long. pub arrow: f32, } -/// Lines where their options say nothing. +/// Defaults for lines. #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = merged)] pub struct LineTheme { - /// A line's color. + /// The line color. pub color: Color, - /// A line's width. + /// The line width, in the space's unit. pub width: f32, } -/// Areas where their options say nothing. +/// Defaults for areas. #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = merged)] pub struct AreaTheme { - /// An area's color. + /// The area color. pub color: Color, } -/// How hovered and selected rows glow, and rows outside a selection dim. +/// How hovered and selected rows glow, and how rows outside a selection dim. #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = merged)] pub struct FocusTheme { - /// The glow around hovered rows. + /// The glow color around hovered rows. pub hover: Color, - /// The glow around selected rows; `None` glows in each row's own color. + /// The glow color around selected rows; `None` glows in each row's own color. pub selected: Option, - /// How far focus glows reach, CSS pixels. + /// How far the hover and selection glows reach, in CSS pixels. pub glow: f32, - /// The opacity of rows outside a selection. + /// The opacity of rows outside the selection while any row is selected, from 0 to 1. pub dim: f32, - /// How much a hovered vertex sized in CSS pixels grows. + /// The scale factor for a hovered vertex sized in CSS pixels; vertices sized in units only + /// glow. pub grow: f32, } -/// How comets flowing along edges and lines look: each a round head, brightest at its middle, trailing -/// a tail that thins and fades. Their spacing and length are in one unit, and each row's flow is in it -/// too: in position units, degrees on the earth, comets are part of the drawing, zooming with the -/// view and staying where they are on their lines; otherwise they keep their size and speed as it -/// zooms. +/// How comets flowing along edges and lines look: a round head, brightest at its middle, trailing a +/// tail that thins and fades. +/// +/// `spacing` and `length` must be in the same unit, which also measures each row's `flow` speed. In +/// position units (degrees on the earth), comets are part of the drawing: they scale with the zoom +/// and stay in place along their lines. In CSS pixels, they keep their on-screen size and speed. A +/// plain number is in the space's unit. #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = merged)] pub struct FlowTheme { - /// A comet's color; its alpha is how strongly a comet covers its stroke. + /// The comet color. Its alpha sets how strongly a comet covers its stroke; a fully transparent + /// color turns comets off. pub color: Color, - /// Distance between comets. + /// The distance between consecutive comets. pub spacing: Length, - /// How far a comet's tail trails its head. + /// The length of a comet's tail behind its head. pub length: Length, - /// How wide a comet's head is; never narrower than its stroke. + /// The width of a comet's head, in the space's unit; never narrower than its stroke. pub width: f32, } @@ -156,30 +163,30 @@ pub struct FlowTheme { #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = merged)] pub struct DiagramTheme { - /// What a group's container is filled with. + /// The fill color of group containers. pub group: Color, - /// The border of blocks and groups, a unit wide; `None` draws none. + /// The color of the one-unit border around blocks and groups; `None` draws no border. pub outline: Option, - /// The radius of block and group corners; a block's marker may set its own. + /// The corner radius of blocks and groups; a block marker can set its own. pub corner: f32, - /// How wide ports are. + /// The width of ports. pub port_size: f32, - /// The radius of an orthogonal wire's bends. + /// The bend radius of orthogonal wires. pub bend: f32, - /// Distance between a group's members and its outline. + /// The space between a group's members and its border. pub group_padding: f32, - /// How blocks and groups drop shadows onto the sheet. + /// How blocks and groups cast shadows on the sheet. pub shadow: ShadowTheme, } -/// How a diagram's blocks and groups drop shadows onto its sheet. +/// How a diagram's blocks and groups cast shadows on the sheet, for vertex marks with `shadow` on. #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = merged)] pub struct ShadowTheme { - /// What a shadow darkens toward; its alpha is how dark it gets under the block. + /// The color a shadow darkens toward; its alpha sets how dark the shadow is under the block. pub color: Color, - /// How far a block's or a group's shadow falls below it, and as far again it softens. + /// How far a shadow falls below its block or group; its edge softens over the same distance. pub lift: f32, } @@ -188,11 +195,11 @@ pub struct ShadowTheme { #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = merged)] pub struct EarthTheme { - /// The ground's color. + /// The ground color. pub ground: Color, - /// The air glowing around the globe, where the earth has an atmosphere. + /// The color of the air glowing around the globe, when the earth's `atmosphere` is on. pub atmosphere: Color, - /// How bright the night side stays where daylight lights the earth, 0 to 1. + /// The brightness of the night side when the earth has `daylight`, from 0 to 1. pub night: f32, } @@ -201,38 +208,38 @@ pub struct EarthTheme { #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = merged)] pub struct PlotTheme { - /// The line drawn at the shown coordinate. + /// The color of the line at the view's current axis coordinate. pub playhead: Color, } -/// A length the theme sets: a number, in the space's unit, units in a diagram and CSS pixels -/// elsewhere; `{ "px": n }`, CSS pixels however the view zooms; or `{ "units": n }`, position units, -/// which scale as it zooms. +/// A theme length: a plain number in the space's unit (units in a diagram, CSS pixels elsewhere), +/// `{ "px": n }` in CSS pixels at any zoom, or `{ "units": n }` in position units (degrees on the +/// earth), which scale as the view zooms. #[derive(Clone, Copy, PartialEq, Debug)] pub enum Length { /// In the space's unit. Space(f32), /// CSS pixels. Px(f32), - /// Position units. + /// Position units (degrees on the earth). Units(f32), } impl Length { - /// How long it is, in its unit. + /// The number, in the length's own unit. pub(crate) fn value(self) -> f32 { match self { Length::Space(v) | Length::Px(v) | Length::Units(v) => v, } } - /// Whether `other` is in its unit. + /// Whether `other` is in the same unit. pub(crate) fn alike(self, other: Length) -> bool { std::mem::discriminant(&self) == std::mem::discriminant(&other) } } -/// How a length writes: a number, or `{ "px": n }` or `{ "units": n }`, each exactly. +/// A length's JSON form: a number, `{ "px": n }`, or `{ "units": n }`, with no other keys. #[derive(Serialize, Deserialize)] #[serde(untagged)] enum LengthForm { @@ -272,7 +279,7 @@ impl<'de> Deserialize<'de> for Length { LengthForm::Px(PxLength { px }) => Length::Px(px), LengthForm::Units(UnitsLength { units }) => Length::Units(units), }) - .map_err(|_| de::Error::custom("a length: a number, { px: n }, or { units: n }")) + .map_err(|_| de::Error::custom("a length must be a number, { px: n }, or { units: n }")) } } @@ -291,18 +298,18 @@ impl JsonSchema for Length { }) }; schemars::json_schema!({ - "description": "A length: a number, in the space's unit, units in a diagram and CSS pixels elsewhere; `{ px }`, CSS pixels; or `{ units }`, position units.", + "description": "A length: a number in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in CSS pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the view zooms in.", "anyOf": [ { "type": "number" }, - exactly("px", "CSS pixels, however the view zooms."), - exactly("units", "Position units, which scale as the view zooms."), + exactly("px", "CSS pixels, at any zoom."), + exactly("units", "Position units (degrees on the earth), which grow on screen as the view zooms in."), ], }) } } impl Theme { - /// Light marks on a dark ground. + /// Light marks on a dark background; the default theme. pub fn dark() -> Theme { let hex = |hex: &str| Color::hex(hex).expect("hex"); Theme { @@ -348,7 +355,7 @@ impl Theme { } } - /// Dark marks on a light ground. + /// Dark marks on a light background. pub fn light() -> Theme { let hex = |hex: &str| Color::hex(hex).expect("hex"); let dark = Theme::dark(); @@ -401,22 +408,22 @@ impl Presets for Theme { } } -/// A theme reads a base theme's name, or options over one: written over the base as it serializes, -/// group by group, so an option left out keeps the base's and `null` clears a color the base has. +/// Reads a base theme's name, or options merged over the base's serialized form group by group: an +/// omitted option keeps the base's value, and `null` clears an optional color. impl<'de> Deserialize<'de> for Theme { fn deserialize>(deserializer: D) -> Result { let named = |name: &str| { - Theme::preset(name).ok_or_else(|| de::Error::custom(format!("no theme {name:?}: dark or light"))) + Theme::preset(name).ok_or_else(|| de::Error::custom(format!("theme {name:?} is not dark or light"))) }; let mut options = match Value::deserialize(deserializer)? { Value::String(name) => return named(&name), Value::Object(options) => options, - _ => return Err(de::Error::custom("a theme: \"dark\", \"light\", or options")), + _ => return Err(de::Error::custom("a theme must be \"dark\", \"light\", or options")), }; let base = match options.remove("base") { None => Theme::dark(), Some(Value::String(name)) => named(&name)?, - Some(_) => return Err(de::Error::custom("base names a theme: dark or light")), + Some(_) => return Err(de::Error::custom("base must be dark or light")), }; let mut theme = serde_json::to_value(base).map_err(de::Error::custom)?; over(&mut theme, Value::Object(options)); @@ -430,7 +437,8 @@ impl Serialize for Theme { } } -/// Writes `options` over `base`, object by object: a length's own object replaces the base's. +/// Merges `options` into `base` object by object, except that a length object replaces the base's +/// value whole. fn over(base: &mut Value, options: Value) { match (base, options) { (Value::Object(base), Value::Object(options)) if !is_length(&options) => { @@ -442,17 +450,17 @@ fn over(base: &mut Value, options: Value) { } } -/// Whether `options` are a length's own object, `{ px }` or `{ units }`, which replaces the base's. +/// Whether `options` is a length object, `{ px }` or `{ units }`. fn is_length(options: &serde_json::Map) -> bool { options.len() == 1 && (options.contains_key("px") || options.contains_key("units")) } -/// Options a theme merges over its base's, each of them optional. +/// Makes every option of a theme group optional in the schema, since each merges over the base. fn merged(schema: &mut schemars::Schema) { schema.remove("required"); } -/// A theme's schema: a base's name, or options over the base `base` names. +/// The theme schema: a base theme's name, or options over the base that `base` names. fn with_base(schema: &mut schemars::Schema) { merged(schema); if let Some(properties) = schema.get_mut("properties").and_then(Value::as_object_mut) { @@ -471,7 +479,7 @@ mod tests { } #[test] - fn options_go_over_their_base_group_by_group() { + fn options_merge_over_their_base_group_by_group() { let theme = theme( r#"{"base": "light", "vertex": {"radius": 3}, "edge": {"color": null}, "focus": {"selected": null}}"#, ); @@ -479,20 +487,20 @@ mod tests { assert_eq!( theme.vertex, VertexTheme { radius: 3.0, ..light.vertex }, - "the group's other options stay the base's" + "the group's other options keep the base's values" ); assert_eq!((theme.edge.color, theme.focus.selected), (None, None), "null clears a color"); assert_eq!(theme.text, light.text); } #[test] - fn a_length_says_its_unit_and_replaces_the_base_s() { + fn a_length_keeps_its_unit_and_replaces_the_base_value() { let flow = theme(r#"{"flow": {"spacing": {"units": 40}, "length": {"units": 30}}}"#).flow; assert_eq!((flow.spacing, flow.length), (Length::Units(40.0), Length::Units(30.0))); let back: Theme = theme(&serde_json::to_string(&Theme { flow, ..Theme::dark() }).expect("serializes")); - assert_eq!(back.flow, flow, "a theme reads what it writes"); + assert_eq!(back.flow, flow, "a theme round-trips through JSON"); let px = theme(r#"{"flow": {"spacing": {"px": 40}}}"#).flow; assert_eq!(px.spacing, Length::Px(40.0)); - assert!(serde_json::from_str::(r#"{"radius": 3}"#).is_err(), "a vertex's radius is in its group"); + assert!(serde_json::from_str::(r#"{"radius": 3}"#).is_err(), "radius belongs in the vertex group"); } } diff --git a/crates/latkit/src/scene/vertices.rs b/crates/latkit/src/scene/vertices.rs index 8feb7016..0838bf6b 100644 --- a/crates/latkit/src/scene/vertices.rs +++ b/crates/latkit/src/scene/vertices.rs @@ -17,32 +17,35 @@ mark!( /// .color(field("vm").colormap(Colormap::BALANCE).domain(0.95..=1.05)); /// ``` Vertices { - /// Each row's x and y. Without them, the point field `geometry` names places rows; rows without - /// either the scene's layout places. + /// Each row's x and y. Without a position, the point field that `geometry` names places rows, + /// and the scene's layout places rows that have neither. position: Option<[Expr; 2]>, - /// The point field placing each row where `position` is not given; `geometry` unless set. + /// The point field that places rows when `position` is not set; the field named `geometry` by + /// default. geometry: Option, - /// Height above the ground: on the earth, a fraction of its radius. Not in a diagram, which is - /// flat. + /// Height above the ground; on the earth, a fraction of the earth's radius. Default 0. Not + /// allowed in a diagram, which is flat. elevation: Expr = "crate::scene::mark::zero", - /// Each row's radius, in the space's unit unless it says its own: units in a diagram, CSS pixels - /// elsewhere. + /// Each row's radius, in the space's unit (units in a diagram, CSS pixels elsewhere) unless + /// given with `px` or `units`; a diagram refuses `px`. The theme's `vertex.radius` unless set. radius: Option, - /// Width and height, for markers that are not round, in the space's unit unless they say their - /// own; in a diagram a block's are units, and a block without a size fits its label and ports. + /// Each row's width and height, for markers that are not round, in the space's unit unless + /// given with `px` or `units`; a diagram refuses `px`. In a diagram, a block without a size + /// fits its label and ports. size: Option<[Size; 2]>, - /// How each row is drawn. + /// How each row is drawn; an ellipse by default. marker: Marker, /// A ring around each row's marker in this color; a missing or transparent color draws none. ring: Option, - /// A port on each row for each reference field naming a net, which an `edges` mark of the net - /// table without ends draws. Diagrams only. + /// Ports on each row, one for each reference field that names a net. An `edges` mark of the net + /// table, without `ends`, draws the nets. Diagrams only. ports: Vec<(Name, Port)> as "[reference: string, port: Port][]", - /// Makes each row a group: it holds the rows of `table` whose reference field names it, and is - /// drawn as a container around them, its label a title in its header. Diagrams only. + /// Makes each row a group containing the rows of `table` whose `reference` field names it. A + /// group draws as a container around its members, with its label as a title in its header. + /// Diagrams only. contains: Option<(Name, Name)> as "[table: string, reference: string]", - /// Whether each row, a block or a group, drops a shadow onto the sheet, as the theme's `shadow` - /// says. Diagrams only. + /// Whether each block or group casts a shadow on the sheet, styled by the theme's + /// `diagram.shadow`. Default `false`. Diagrams only. shadow: bool, }); @@ -64,8 +67,9 @@ impl Vertices { } } - /// A port on each row: where the net its reference field `reference` names meets it. An `Edges` - /// mark of the net table, without ends, draws the nets. Diagrams only. + /// Adds a port on each row for the net that reference field `reference` names, replacing any + /// earlier port for that field. An [`Edges`](crate::Edges) mark of the net table, without ends, + /// draws the nets. Diagrams only. #[must_use] pub fn port(mut self, reference: impl Into, port: impl Into) -> Vertices { let reference = reference.into(); @@ -74,75 +78,78 @@ impl Vertices { self } - /// Makes each row a group: it holds the rows of `table` whose reference field `reference` names - /// it, and draws as a container around them, its label a title in its header. Diagrams only. + /// Makes each row a group containing the rows of `table` whose `reference` field names it. A + /// group draws as a container around its members, with its label as a title in its header. + /// Diagrams only. #[must_use] pub fn contains(mut self, table: impl Into, reference: impl Into) -> Vertices { self.contains = Some((table.into(), reference.into())); self } - /// The point field placing each row where no position is given; `geometry` unless set. + /// Sets the point field that places rows without a position; `geometry` by default. #[must_use] pub fn geometry(mut self, field: impl Into) -> Vertices { self.geometry = Some(field.into()); self } - /// The point field placing rows without a position. + /// The point field that places rows without a position. pub(crate) fn point_field(&self) -> &str { self.geometry.as_deref().unwrap_or(GEOMETRY) } - /// Each row's position. Without one, a point field named `geometry` places rows; rows without - /// either the scene's layout places, in a diagram or on a plane or the earth. + /// Sets each row's x and y. Without a position, the point field named by + /// [`geometry`](Vertices::geometry) places rows, and the scene's layout places rows that have + /// neither, in a diagram or on a plane or the earth. #[must_use] pub fn position(mut self, x: impl Into, y: impl Into) -> Vertices { self.position = Some([x.into(), y.into()]); self } - /// Height above the ground: on the earth, a fraction of its radius. Not in a diagram, which is - /// flat. + /// Sets the height above the ground; on the earth, a fraction of the earth's radius. Not + /// allowed in a diagram, which is flat. #[must_use] pub fn elevation(mut self, z: impl Into) -> Vertices { self.elevation = z.into(); self } - /// Each row's radius, in the space's unit unless it says its own: units in a diagram, CSS pixels - /// elsewhere; [`px`](crate::px) keeps CSS pixels, [`units`](crate::units) takes position units. + /// Sets each row's radius, in the space's unit (units in a diagram, CSS pixels elsewhere) + /// unless given with [`px`](crate::px) or [`units`](crate::units); a diagram refuses `px`. #[must_use] pub fn radius(mut self, radius: impl Into) -> Vertices { self.radius = Some(radius.into()); self } - /// Each row's width and height, for markers that are not round, in the space's unit unless they - /// say their own; in a diagram a block's are units, and a block without a size fits its label - /// and ports. + /// Sets each row's width and height, for markers that are not round, in the space's unit + /// unless given with [`px`](crate::px) or [`units`](crate::units); a diagram refuses `px`. In a + /// diagram, a block without a size fits its label and ports. #[must_use] pub fn size(mut self, width: impl Into, height: impl Into) -> Vertices { self.size = Some([width.into(), height.into()]); self } - /// How each row is drawn. + /// Sets how each row is drawn; an ellipse by default. #[must_use] pub fn marker(mut self, marker: impl Into) -> Vertices { self.marker = marker.into(); self } - /// A ring around each row's marker in this color; a missing or transparent color draws none. + /// Draws a ring around each row's marker in this color; a missing or transparent color draws + /// none. #[must_use] pub fn ring(mut self, color: impl Into) -> Vertices { self.ring = Some(color.into()); self } - /// Whether each row, a block or a group, drops a shadow onto the sheet, as the theme's - /// [`shadow`](crate::DiagramTheme::shadow) says. Diagrams only. + /// Sets whether each block or group casts a shadow on the sheet, styled by the theme's + /// [`shadow`](crate::DiagramTheme::shadow). Diagrams only. #[must_use] pub fn shadow(mut self, on: bool) -> Vertices { self.shadow = on; @@ -183,7 +190,8 @@ impl Vertices { ports.push((reference.clone(), port.clone(), column)); } let mut drawing = Drawing::new(MarkKind::Vertices, wgsl::VERTEX_WORDS); - // A marker's half keeps within its size's pixels, halved where the size is the whole width. + // The marker's half-size stays within its size's pixel bounds; a `size` bounds the whole + // width, so its bounds are halved. drawing.size_px = match (&self.size, &self.radius) { (Some([w, _]), _) => w.px.map(|px| px / 2.0), (None, Some(radius)) => radius.px, @@ -191,7 +199,7 @@ impl Vertices { }; drawing.markers = self.marker.markers(0); if group { - // A container is a box whatever its marker. + // A group draws as a rectangle regardless of its marker. flags |= wgsl::GROUP; drawing.markers.marker = wgsl::MARKER_SHAPE; drawing.markers.shape = wgsl::SHAPE_RECTANGLE; @@ -206,8 +214,8 @@ impl Vertices { Ok(drawing) } - /// Where rows stand and how large they are in a diagram: as its sheet says, from the data alone, - /// in units. + /// Compiles positions and sizes in a diagram, which its sheet computes in units from the data + /// alone. fn sheet(&self, c: &mut Compiler<'_>, group: bool) -> Result<()> { let mut sizes = self .radius @@ -215,7 +223,7 @@ impl Vertices { .map(|size| ("radius", size)) .chain(self.size.iter().flatten().map(|size| ("size", size))); if let Some((option, _)) = sizes.find(|(_, size)| size.unit == Some(Unit::Px)) { - return Err(Error::invalid("a diagram's blocks are sized in units; px keeps strokes on screen").at(option)); + return Err(Error::invalid("a diagram's blocks are sized in units, not px").at(option)); } c.check_structure("position", self.position.iter().flatten())?; c.check_structure("size", self.size.iter().flatten().map(|s| &s.expr))?; @@ -231,9 +239,9 @@ impl Vertices { Ok(()) } - /// Where rows stand and how large they are off a diagram: at their position, or where the - /// scene's layout places them, from the data alone; sized by their size, their radius, or the - /// theme's. Its flags: `SIZED` for a width and a height. + /// Compiles positions and sizes outside a diagram. Rows stand at their position, or where the + /// scene's layout places them from the data alone, and take their size, their radius, or the + /// theme's radius. Returns the flags: `SIZED` when rows have a width and a height. fn placed(&self, c: &mut Compiler<'_>, context: Context<'_>) -> Result { let at = wgsl::VERTEX_POSITION; if context.placed() { diff --git a/crates/latkit/src/schema.rs b/crates/latkit/src/schema.rs index bdbe072c..a15cd53d 100644 --- a/crates/latkit/src/schema.rs +++ b/crates/latkit/src/schema.rs @@ -1,20 +1,21 @@ -//! What latkit reads and writes, as JSON Schema: scenes, datasets, patches, queries, cameras, fits, -//! budgets, and shots in; events, hits, stats, work, and descriptions out. Each type serializes or -//! deserializes as itself and its schema derives from the same declaration, so the code and its -//! schema never part. `latkit schema` prints them, and npm's types are written from them. +//! JSON Schemas for latkit's inputs and outputs. Inputs are scenes, datasets, patches, queries, +//! cameras, fits, budgets, and shots. Outputs are events, hits, stats, work, and descriptions. +//! +//! Each schema derives from the same declaration as its type's serde code, so the two cannot drift +//! apart. `latkit schema` prints them, and the npm package's types are generated from them. use schemars::generate::SchemaSettings; use schemars::transform::RecursiveTransform; -/// The settings every schema is made with: draft 2020-12, saying what is read or written and not -/// latkit's defaults, which descriptions say. +/// Settings for every schema: draft 2020-12 with `default` keywords removed, since descriptions +/// state latkit's defaults. fn settings() -> SchemaSettings { SchemaSettings::draft2020_12().with_transform(RecursiveTransform(|schema: &mut schemars::Schema| { schema.remove("default"); })) } -/// The JSON Schema of what `T` deserializes from, its definitions under `$defs`. +/// The JSON Schema `T` deserializes from, with its definitions under `$defs`. /// /// ``` /// let data = latkit::schema::(); @@ -24,9 +25,11 @@ pub fn schema() -> serde_json::Value { settings().into_generator().into_root_schema_for::().to_value() } -/// Every input's JSON Schema in one document, each under its name in `$defs` with what it is built -/// of: `Scene`, `Data` (a dataset's document), `Patch`, `Query`, `Camera`, `Fit`, `Budget`, `Shot`. -/// What latkit writes is under `outputs`, as it serializes: `ViewEvent`, `Stats`, `Work`, +/// Every input and output JSON Schema in one document. +/// +/// Inputs, and the types they are built from, are under `$defs` by name: `Scene`, `Data` (a +/// dataset's document), `Patch`, `Query`, `EnvelopeRequest`, `Camera`, `Fit`, `Budget`, and `Shot`. +/// Outputs, in their serialized form, are under `outputs`: `ViewEvent`, `Stats`, `Work`, /// `Description`, `Rows`, `Camera`, `Budget`, `Value`, and `Failure` (an error). /// /// ``` diff --git a/crates/latkit/src/sheet/groups.rs b/crates/latkit/src/sheet/groups.rs new file mode 100644 index 00000000..425d5da9 --- /dev/null +++ b/crates/latkit/src/sheet/groups.rs @@ -0,0 +1,125 @@ +//! A diagram's groups. A group's box surrounds its members, padded, with its title band across the +//! top; a group with no placed members is centered at its own place. Each group keeps its members' +//! boxes indexed, so an edit refits only the groups above the vertices it moved. + +use std::cmp::Reverse; +use std::collections::BTreeSet; + +use rustc_hash::FxHashMap; + +use super::Vertex; +use crate::data::Row; +use crate::layout::boxes::Boxes; +use crate::layout::rect::{EMPTY, Rect, around, valid}; + +/// Every group of a diagram with its members' boxes, and the groups' boxes for wire routing. +#[derive(Default)] +pub(crate) struct Groups { + /// Each group's vertex index and its members' boxes, y down, by member. + members: Vec<(usize, Boxes)>, + /// Each vertex's group, as an index into `members`, and its place among that group's members. + member: Vec>, + /// Each vertex's index into `members`, where it is a group. + index: Vec>, + /// The groups' boxes, y up, by index: wires cross them at a cost and keep off their borders. + boxes: Boxes, +} + +/// What a group's box needs besides its members: each vertex mark's title band height, the padding +/// around members, and the places layouts gave rows without positions. +pub(crate) struct Framing<'a> { + pub(crate) header: &'a [f64], + pub(crate) padding: f64, + pub(crate) places: &'a FxHashMap, +} + +impl Groups { + /// The groups of `vertices`, each placed around its members, innermost first. + pub(crate) fn new(vertices: &mut [Vertex], framing: &Framing<'_>) -> Groups { + let mut index = vec![None; vertices.len()]; + let mut members: Vec<(usize, Vec)> = Vec::new(); + for v in (0..vertices.len()).filter(|&v| vertices[v].group) { + index[v] = Some(members.len()); + members.push((v, Vec::new())); + } + let mut member = vec![None; vertices.len()]; + for v in 0..vertices.len() { + if let Some(g) = vertices[v].parent.and_then(|p| index[p]) { + member[v] = Some((g, members[g].1.len())); + members[g].1.push(v); + } + } + let mut order: Vec = (0..members.len()).collect(); + crate::order::by_key(&mut order, |&g| !u64::from(vertices[members[g].0].depth)); + let mut groups: Vec> = (0..members.len()).map(|_| None).collect(); + for g in order { + let v = members[g].0; + let held = Boxes::new(members[g].1.iter().map(|&m| vertices[m].bounds.unwrap_or(EMPTY)).collect()); + vertices[v].bounds = stand(&vertices[v], held.extent(), framing); + groups[g] = Some((v, held)); + } + let groups: Vec<(usize, Boxes)> = groups.into_iter().map(|g| g.expect("every group is placed")).collect(); + let boxes = Boxes::new(groups.iter().map(|&(v, _)| up(vertices[v].bounds)).collect()); + Groups { members: groups, member, index, boxes } + } + + /// Refits the groups above vertices `touched`, which an edit resized or moved. Each group + /// holding one is refit from its members, deepest first, continuing upward while a group's box + /// changes. Returns the groups whose boxes changed. + pub(crate) fn refit(&mut self, vertices: &mut [Vertex], touched: &[usize], framing: &Framing<'_>) -> Vec { + let mut due: BTreeSet<(Reverse, usize)> = BTreeSet::new(); + for &v in touched { + if let Some((g, place)) = self.member[v] { + self.members[g].1.set(place, vertices[v].bounds.unwrap_or(EMPTY)); + let group = self.members[g].0; + due.insert((Reverse(vertices[group].depth), group)); + } + if vertices[v].group { + due.insert((Reverse(vertices[v].depth), v)); + } + } + let mut changed = Vec::new(); + while let Some((_, v)) = due.pop_first() { + let g = self.index[v].expect("a group"); + let bounds = stand(&vertices[v], self.members[g].1.extent(), framing); + if bounds == vertices[v].bounds { + continue; + } + vertices[v].bounds = bounds; + self.boxes.set(g, up(bounds)); + changed.push(v); + if let Some((parent, place)) = self.member[v] { + self.members[parent].1.set(place, bounds.unwrap_or(EMPTY)); + let group = self.members[parent].0; + due.insert((Reverse(vertices[group].depth), group)); + } + } + changed + } + + /// Every group's box, y up, `EMPTY` where it has none. + pub(crate) fn boxes(&self) -> &[Rect] { + self.boxes.all() + } + + /// The box around every group, y up. + pub(crate) fn extent(&self) -> Rect { + self.boxes.extent() + } +} + +/// Group `group`'s box around `held`, its members' extent, y down; at its own place when `held` is +/// empty. `None` when it has neither. +fn stand(group: &Vertex, held: Rect, framing: &Framing<'_>) -> Option { + if valid(&held) { + let (top, padding) = (framing.header[group.mark], framing.padding); + return Some([held[0] - padding, held[1] - padding - top, held[2] + padding, held[3] + padding]); + } + let center = group.pin.or_else(|| framing.places.get(&group.key).copied()); + center.map(|c| around(c, group.half)) +} + +/// A y-down box flipped to y up for routing; `EMPTY` for none. +fn up(bounds: Option) -> Rect { + bounds.map_or(EMPTY, |b| [b[0], -b[3], b[2], -b[1]]) +} diff --git a/crates/latkit/src/sheet/mod.rs b/crates/latkit/src/sheet/mod.rs index 061ceff2..82594798 100644 --- a/crates/latkit/src/sheet/mod.rs +++ b/crates/latkit/src/sheet/mod.rs @@ -1,14 +1,18 @@ -//! A diagram's sheet: where every vertex row stands and how large it is, the boxes groups draw -//! around their members, and the paths wires run along, in units with y down. It is made from the -//! data and the scene alone, never from what was drawn, so the camera, hover, and motion never -//! change it. Places persist by row, so adding a row moves nothing else, and a row that loses its -//! position stays where it stood; layouts place only rows without one, until arranging lays every -//! row out again, and a wire routes again only when its terminals move or a box near it changes. -//! Superlinear work too large for the frame (layouts, many routes) is a host job answering with a -//! patch. The sheet is a derivation: it writes a table beside each table it draws, row for row, -//! which marks read like data. An edit of the values rows are sized and placed by derives only the -//! rows it wrote and what they reach: their boxes, the groups around them, and the wires they move. - +//! A diagram's sheet: each vertex row's place and size, group boxes around their members, and +//! wire paths, in units with y down. +//! +//! The sheet depends only on the data and the scene, never on what was drawn, so the camera, +//! hover, and motion never change it. Places persist by row: adding a row moves nothing else, and +//! a row that loses its position stays where it was. Layouts place only rows without a place until +//! `arrange` lays out every row without a position again. A wire routes again only when its +//! terminals move or a box near it changes. Superlinear work too large for the frame (layouts, +//! many routes) runs as a host job that answers with a patch. +//! +//! The sheet is a derivation: it writes a table beside each table it draws, row for row, which +//! marks read like data. An edit of the values that size and place rows recomputes only the rows +//! it wrote and what they affect: their boxes, the groups around them, and the wires they move. + +mod groups; mod wires; use std::hash::{Hash, Hasher}; @@ -26,51 +30,51 @@ use crate::layout::boxes::Boxes; use crate::layout::rect::{EMPTY, Rect, around, union, valid}; use crate::{Layout, Marker, Pending, Shape, Side, Theme, Vertices}; -/// Texts measured past which the sheet forgets them all. +/// The number of measured texts beyond which the sheet clears its measurement cache. const MEASURED: usize = 1 << 16; /// The sheet of a diagram's view. #[derive(Default)] pub(crate) struct Sheet { - /// Failures to tell: texts its host could not measure. + /// Errors to report: texts the host could not measure. errors: Vec, /// The derived table beside each table it draws. tables: Tables, - /// Places layouts gave rows without positions, kept by row, also after a row leaves, so undo + /// Places layouts gave rows without positions, by row. Kept after a row is removed, so undo /// puts it back where it was. places: FxHashMap, - /// Each orthogonal wire's route, by edge row, and those due to route again. + /// Each orthogonal wire's route, by edge row, and the rows due to route again. routes: wires::Routes, - /// The size each run of text takes, as its host measured it. + /// The size of each text run, as the host measured it. measured: FxHashMap, - /// The box of every placed vertex row the last update made: what a change moved. + /// The box of every placed vertex row as of the last update, to find the boxes a change moved. boxes: FxHashMap, - /// Every vertex's box, y up, as routing avoids it, none for a group: refit as vertices move. + /// Each vertex's routing-obstacle box, y up (`EMPTY` for a group), refit as vertices move. blocks: Boxes, - /// The groups' boxes, y up, which wires cross at a cost. - groups: Vec, - /// What the derived tables were last made from. + /// The groups, refit as their members move. + groups: groups::Groups, + /// The stamp the derived tables were last made from. stamp: u64, - /// Bumps when places start over, so layouts asked before are dropped. + /// Bumps when places start over, so answers to earlier layouts are dropped. generation: u64, - /// Whether every row without a position is to be laid out again, standing where it is until - /// the layout answers. + /// Whether every row without a position is to be laid out again. Rows stay where they are + /// until the layout answers. arranging: bool, - /// What the last arranging laid out, until the view takes it. + /// The result of the last arrange, until the view takes it. arranged: Option>, - /// The layout places were made by. + /// The layout the places were made with. layout: Option, placing: Option, routing: Option, - /// Bumps whenever an answer lands. + /// Bumps whenever an answer arrives. landed: u64, - /// What the last update drew, which an edit derives from. + /// The state of the last draw, which an edit updates incrementally. drawn: Option, } -/// What the sheet last drew: every vertex row as it stood, what wiring keeps, what the structure of -/// the marks read, and a stamp of the scene, the host, the answers landed, and the generation of -/// places it was drawn from. +/// The sheet's last drawn state: every vertex row, the wiring state, the marks' structure inputs, +/// and a stamp of the scene version, whether there is a host, the answers received, and the +/// places generation. struct Drawn { vertices: Vec, wired: wires::Wired, @@ -78,36 +82,38 @@ struct Drawn { from: u64, } -/// What the structure of the marks read when the sheet was drawn: a table's rows, or a field. +/// An input to the marks' structure as of the last draw: a table's rows, or a field. enum Seen { Rows { key: (Name, Name), structure: Version, order: Version }, Field { key: (Name, Name, Name), field: Option> }, } -/// A layout under way: the generation of places it was asked in, and the row each key it answers -/// for names. +/// A pending layout job: the places generation it was started in, and the row each answer key +/// names. struct Placing { pending: Pending>, generation: u64, rows: FxHashMap, } -// A vertex row of the diagram. +/// A vertex row of the diagram. pub(crate) struct Vertex { - /// The vertex mark drawing it. + /// The index of the vertex mark drawing it. pub(crate) mark: usize, pub(crate) key: Row, - /// Half its width and height, units; a group's while it holds nothing placed. + /// Half its width and height, in units. For a group, its size when it holds nothing placed. pub(crate) half: [f64; 2], /// The position its data gives it. pub(crate) pin: Option<[f64; 2]>, /// Its box, least x and y then greatest, once it has a place. pub(crate) bounds: Option<[f64; 4]>, - /// The group holding it, and how many groups hold it. + /// The group holding it, by vertex index. pub(crate) parent: Option, + /// How many groups hold it. pub(crate) depth: u32, - /// Whether it is a group, and how many rows it holds. + /// Whether it is a group. pub(crate) group: bool, + /// As a group, how many rows it directly holds. pub(crate) members: u32, pub(crate) shape: Shape, } @@ -119,7 +125,7 @@ impl Vertex { } impl Derive for Sheet { - /// Brings the sheet up to `input`, writing each drawn table's derived table. + /// Updates the sheet to `input`, writing each drawn table's derived table. fn update(&mut self, input: &Input<'_>, jobs: &mut Jobs<'_>) -> bool { if self.layout != Some(input.scene.layout) { if self.layout.is_some() { @@ -128,7 +134,7 @@ impl Derive for Sheet { self.layout = Some(input.scene.layout); } let mut changed = false; - // A job its host does at once is taken before the frame draws. + // Repeat so a job the host finishes at once is taken before the frame draws. for _ in 0..4 { self.settle(); let stamp = stamp(input, self.landed, self.generation); @@ -142,7 +148,7 @@ impl Derive for Sheet { changed } - /// Whether a layout or routes are under way. + /// Whether a layout or routing job is pending. fn busy(&self) -> bool { self.placing.is_some() || self.routing.is_some() } @@ -151,8 +157,8 @@ impl Derive for Sheet { &self.tables } - /// Forgets the tables but keeps places and routes: a scene that is a diagram again finds its rows - /// where they stood. + /// Drops the tables but keeps places and routes, so when the scene is a diagram again, rows + /// return to where they were. fn clear(&mut self) { self.tables.clear(); self.drawn = None; @@ -165,13 +171,13 @@ impl Derive for Sheet { } impl Sheet { - /// Whether a layout is under way, which a fit waits for. + /// Whether a layout job is pending; a fit waits for it. pub(crate) fn placing(&self) -> bool { self.placing.is_some() } - /// Lays every row without a position out again; until the layout answers, rows stand where they - /// are. + /// Lays out every row without a position again. Rows stay where they are until the layout + /// answers. pub(crate) fn arrange(&mut self) { self.arranging = true; self.placing = None; @@ -179,27 +185,28 @@ impl Sheet { self.stamp = 0; } - /// What the last arranging laid out, once it landed: each row placed, and where, units with y - /// down. + /// Takes the result of the last `arrange` once it arrives: each row laid out and its position, + /// in units with y down. pub(crate) fn take_arranged(&mut self) -> Option> { self.arranged.take() } - /// Row `row`'s box, units with y down, once it has one. + /// Row `row`'s box, in units with y down, once it has one. pub(crate) fn bounds(&self, row: &Row) -> Option<[f64; 4]> { self.boxes.get(row).copied() } - /// The box holding every placed vertex and every wire, units with y down, once anything is placed: - /// the boxes of its blocks, its groups, and its wires, as it keeps them. + /// The box around every placed vertex and every wire, in units with y down; `None` until + /// something is placed. Read from the indexes the sheet keeps of blocks, groups, and wires. pub(crate) fn extent(&self) -> Option<[f64; 4]> { let wires = self.drawn.as_ref()?.wired.extent(); - let up = self.groups.iter().fold(self.blocks.extent(), |extent, &group| union(extent, group)); + let up = union(self.blocks.extent(), self.groups.extent()); let extent = union([up[0], -up[3], up[2], -up[1]], wires); valid(&extent).then_some(extent) } - /// Takes the answers that landed: places asked in the current generation, and routes. + /// Takes the answers of finished jobs: a layout's places, if it was started in the current + /// generation, and routes. fn settle(&mut self) { if let Some(answer) = self.placing.as_ref().and_then(|p| p.pending.take()) { let placing = self.placing.take().expect("a layout under way"); @@ -209,7 +216,7 @@ impl Sheet { if let (Some(row), [Some(Value::Number(x)), Some(Value::Number(y))]) = (placing.rows.get(&key), &values[..]) { - // Layouts lay out with y up. + // Layouts are y up; the sheet is y down. self.places.insert(row.clone(), [*x, -*y]); placed.push((row.clone(), [*x, -*y])); } @@ -229,8 +236,9 @@ impl Sheet { } } - /// Makes the derived tables again: sizes, places, group boxes, then wires. After an edit of only - /// values rows are sized and placed by, each by its own row's, only what the rows it wrote reach. + /// Rebuilds the derived tables: sizes, places, group boxes, then wires. After an edit that only + /// changed values sizing or placing rows (each by its own row's value), it redraws only what + /// the written rows affect. fn build(&mut self, input: &Input<'_>, jobs: &mut Jobs<'_>) { let marks = marks(input); let from = stamp_of((input.scene_version, input.host.is_some(), self.landed, self.generation)); @@ -253,9 +261,9 @@ impl Sheet { for v in vertices.iter_mut().filter(|v| !v.group) { self.stand(v); } - stand_groups(&mut vertices, &self.places, &header, padding); + let framing = groups::Framing { header: &header, padding, places: &self.places }; + self.groups = groups::Groups::new(&mut vertices, &framing); self.blocks = Boxes::new(vertices.iter().map(obstacle).collect()); - self.groups = groups(&vertices); self.place(&vertices, vertex_marks, edge_marks, &header, padding, input, jobs); let moved = wires::moved(&self.boxes, &vertices); let mut drawn = Drawn { vertices, wired: wires::Wired::default(), reads: Vec::new(), from: 0 }; @@ -265,8 +273,8 @@ impl Sheet { drawn } - /// Draws again what an edit reaches: the rows `touched` sized and placed again, the groups around - /// them, and the wires meeting them or near what they moved. + /// Redraws what an edit affects: sizes and places rows `touched` again, refits the groups + /// around them, and recomputes the wires meeting them or near the boxes they moved. fn redraw( &mut self, mut drawn: Drawn, @@ -289,7 +297,7 @@ impl Sheet { self.stand(vertex); } } - // The boxes the edit moved, added, or took away among the blocks wires route around. + // The block boxes the edit moved, added, or removed, which wires route around. let mut moved = Vec::new(); for (&v, &was) in touched.iter().zip(&before) { let vertex = &drawn.vertices[v]; @@ -297,22 +305,18 @@ impl Sheet { moved.extend(was.into_iter().chain(vertex.bounds)); } } - // Groups stand around their members again; a group that moved is touched too. - if touched.iter().any(|&v| drawn.vertices[v].group || drawn.vertices[v].parent.is_some()) { - let groups: Vec<(usize, Option)> = - drawn.vertices.iter().enumerate().filter(|(_, v)| v.group).map(|(g, v)| (g, v.bounds)).collect(); - stand_groups(&mut drawn.vertices, &self.places, &header, padding); - touched.extend(groups.into_iter().filter(|&(g, was)| drawn.vertices[g].bounds != was).map(|(g, _)| g)); + // Refit the groups above the touched rows; a group whose box changed counts as touched. + let framing = groups::Framing { header: &header, padding, places: &self.places }; + let regrouped = self.groups.refit(&mut drawn.vertices, &touched, &framing); + if !regrouped.is_empty() { + touched.extend(regrouped); touched.sort_unstable(); touched.dedup(); } for &v in &touched { self.blocks.set(v, obstacle(&drawn.vertices[v])); } - if touched.iter().any(|&v| drawn.vertices[v].group) { - self.groups = groups(&drawn.vertices); - } - // A layout places only rows without a place: an edit asks for one only where it took one away. + // Layouts place only rows without a place, so start one only if the edit removed a place. if touched.iter().any(|&v| drawn.vertices[v].bounds.is_none()) { self.place(&drawn.vertices, vertex_marks, edge_marks, &header, padding, input, jobs); } @@ -331,8 +335,8 @@ impl Sheet { drawn } - /// Stands vertex `v`, not a group, where its data or a layout places it. A row that lost its - /// position keeps standing where it stood. + /// Places vertex `v` (not a group) where its data or a layout puts it. A row that lost its + /// position stays where it was. fn stand(&mut self, v: &mut Vertex) { if v.pin.is_none() && !self.places.contains_key(&v.key) @@ -343,7 +347,7 @@ impl Sheet { v.bounds = v.pin.or_else(|| self.places.get(&v.key).copied()).map(|center| around(center, v.half)); } - /// Works out the wires `change` reaches; the edge rows worked out, by edge mark. + /// Computes the wires `change` affects. Returns the edge rows computed, by edge mark. fn wire( &mut self, drawn: &mut Drawn, @@ -358,7 +362,7 @@ impl Sheet { marks: vertex_marks, edges: edge_marks, blocks: &self.blocks, - groups: &self.groups, + groups: self.groups.boxes(), spacing: f64::from(theme.edge.spacing), bend: f64::from(theme.diagram.bend), port: f64::from(theme.diagram.port_size), @@ -391,8 +395,9 @@ impl Sheet { vertices } - /// Half the width and height of `vm`'s row `row`, units: its size, its radius, or with neither a - /// box fitting its label and ports. A group's is what it takes holding nothing: its title. + /// Half the width and height of `vm`'s row `row`, in units: from its size, its radius, or else + /// a box fitting its label and ports. A group's is its size when it holds nothing: room for its + /// title. fn half(&mut self, vm: &VertexMark<'_>, rows: &Rows<'_>, row: usize, input: &Input<'_>) -> [f64; 2] { let theme = &input.scene.theme; let radius = f64::from(theme.vertex.radius); @@ -420,7 +425,7 @@ impl Sheet { } let size = text.map_or(f64::from(theme.text.size), |(_, size)| size); let [w, h] = text.map_or([0.0; 2], |(box_, _)| box_); - // A round or pointed outline holds its text's box inside it. + // Scale up so a round or pointed outline contains its text's box. let fit = match shape_of(vm.mark) { Shape::Ellipse => std::f64::consts::SQRT_2, Shape::Diamond => 2.0, @@ -431,7 +436,8 @@ impl Sheet { } else { (2.0 * radius, 2.0 * radius) }; - // Room for its ports: a pitch apiece along each side, and their labels across from each other. + // Room for its ports: one pitch per port along each side, and width for the left and right + // port labels on either side of the label. let port = f64::from(theme.diagram.port_size); let pitch = (2.0 * port).max(1.4 * size); let count = |side: Side| ports.iter().filter(|(_, p)| p.side == side).count() as f64; @@ -450,9 +456,8 @@ impl Sheet { if labels > 0.0 { width = width.max(w + labels + 3.0 * port + 1.6 * size); } - // A port named on the top or the bottom takes a line of its own between its side and the - // label, and as much on the other side, so the label stays in the middle; and its share of - // the side holds its name. + // A named port on the top or bottom needs a line between its side and the label, and as + // much on the other side to keep the label centered. Its share of the side fits its name. let mut stacked = false; for side in [Side::Top, Side::Bottom] { let name = widest(side); @@ -467,7 +472,7 @@ impl Sheet { [width / 2.0, height / 2.0] } - /// The width and height of row `row`'s label in units, and its size; `None` without one. + /// The width and height of row `row`'s label in units, and its font size; `None` if no label. fn label(&mut self, mark: &Vertices, rows: &Rows<'_>, row: usize, input: &Input<'_>) -> Option<([f64; 2], f64)> { let label = mark.style.label.as_ref()?; let text = rows.text(&label.text, row)?; @@ -479,8 +484,8 @@ impl Sheet { Some((measured.map(|v| v * f64::from(size) / f64::from(TEXT_BASE)), f64::from(size))) } - /// The width and height `run` takes, base pixels: as its host measures it, once, or an estimate - /// without one. + /// The width and height of `run` in base pixels: measured once by the host and cached, or + /// estimated when there is no host or measuring fails. fn measure(&mut self, run: TextRun, input: &Input<'_>) -> [f64; 2] { if let Some(&size) = self.measured.get(&run) { return size; @@ -504,8 +509,8 @@ impl Sheet { size } - /// Asks the scene's layout, once none is under way, to place every row without a place: blocks, - /// and groups holding nothing; while arranging, every row without a position. The rest hold; + /// Starts a layout job, unless one is pending, to place every block, and every group holding + /// nothing, that has no place (while arranging, no position). Other rows are pinned, and /// groups lay out around their members. #[allow(clippy::too_many_arguments, reason = "one layout request")] fn place( @@ -526,13 +531,13 @@ impl Sheet { return; } if matches!(layout, Layout::None) || !vertices.iter().any(wanted) { - // Nothing to lay out: the arranging is done as asked. + // Nothing to lay out: the arrange finishes with no rows. if std::mem::take(&mut self.arranging) { self.arranged = Some(Vec::new()); } return; } - // Layouts lay out with y up: a group's header is its top. + // Layouts are y up, so a group's header pads its top. let sizes: Vec<[f64; 2]> = vertices.iter().map(|v| v.half.map(|h| 2.0 * h)).collect(); let pinned: Vec> = vertices .iter() @@ -557,8 +562,8 @@ impl Sheet { } } -/// What the sheet reads, stamped: the scene, whether a host measures text, answers landed, the -/// generation of places, whether rows are being dragged, and what the marks' structure reads. +/// A stamp of the sheet's inputs: the scene version, whether a host measures text, the answers +/// received, the places generation, whether rows are being dragged, and the marks' structure. fn stamp(input: &Input<'_>, landed: u64, generation: u64) -> u64 { let mut hasher = FxHasher::default(); (input.scene_version, input.host.is_some(), landed, generation, input.dragging).hash(&mut hasher); @@ -569,7 +574,7 @@ fn stamp(input: &Input<'_>, landed: u64, generation: u64) -> u64 { /// The diagram's vertex marks and edge marks. type Marks<'a> = (Vec>, Vec>); -/// What the structure of the marks reads now. +/// The current inputs to the marks' structure. fn seen(input: &Input<'_>) -> Vec { let mut seen = Vec::new(); input.reads(&mut |read| { @@ -585,8 +590,9 @@ fn seen(input: &Input<'_>) -> Vec { seen } -/// The vertex rows written since `drawn`, by vertex, in order: `None` unless the same tables hold the -/// same rows and nothing changed but values rows are sized and placed by, each by its own row's. +/// The vertex rows written since `drawn`, as sorted vertex indices. `None` unless the same tables +/// hold the same rows and the only changes are to values that size or place each row by its own +/// value. fn edited(drawn: &Drawn, input: &Input<'_>, vertex_marks: &[VertexMark<'_>]) -> Option> { let mut seen = drawn.reads.iter(); let mut rows = Vec::new(); @@ -625,23 +631,18 @@ fn edited(drawn: &Drawn, input: &Input<'_>, vertex_marks: &[VertexMark<'_>]) -> }) } -/// The band across the top of each vertex mark's groups, and the padding around their members. +/// The height of each vertex mark's group header band, and the padding around group members. fn framing(vertex_marks: &[VertexMark<'_>], theme: &Theme) -> (Vec, f64) { (vertex_marks.iter().map(|vm| header(vm.mark, theme)).collect(), f64::from(theme.diagram.group_padding)) } -/// Vertex `v`'s box as routing avoids it, y up; none for a group, which holds wires rather than -/// turning them away. +/// Vertex `v`'s box as a routing obstacle, y up. `EMPTY` for a group, which wires may pass through. fn obstacle(v: &Vertex) -> Rect { v.bounds.filter(|_| !v.group).map_or(EMPTY, |b| [b[0], -b[3], b[2], -b[1]]) } -/// The groups' boxes, y up, which wires cross at a cost and keep off. -fn groups(vertices: &[Vertex]) -> Vec { - vertices.iter().filter(|v| v.group).filter_map(|v| v.bounds).map(|b| [b[0], -b[3], b[2], -b[1]]).collect() -} - -/// The outline a vertex mark's markers draw: groups and blocks are boxes, the rest their shape. +/// The outline a vertex mark's markers draw: a rectangle for groups, blocks, icons, and WGSL +/// markers, an ellipse for pies, else the marker's shape. fn shape_of(mark: &Vertices) -> Shape { if mark.contains.is_some() { return Shape::Rectangle; @@ -653,8 +654,8 @@ fn shape_of(mark: &Vertices) -> Shape { } } -/// The band across the top of a group mark's containers its titles sit in, units; none for other -/// marks, or without titles. +/// The height in units of the band across the top of a group mark's boxes, where titles sit; 0 for +/// other marks, or without labels. fn header(mark: &Vertices, theme: &Theme) -> f64 { match (&mark.contains, &mark.style.label) { (Some(_), Some(label)) => f64::from(label.size.unwrap_or(theme.text.size) * HEADER_BAND), @@ -662,9 +663,9 @@ fn header(mark: &Vertices, theme: &Theme) -> f64 { } } -/// Names each row's group: the rows of the table a group mark contains, through the reference -/// naming the group; then how many groups hold each row, cycles cut where a walk up closes; then -/// how many rows each group holds. +/// Sets each row's group: the rows of a group mark's contained table belong to the group their +/// reference names. Then sets how many groups hold each row, dropping the group of a row whose +/// walk up the groups cycles, and how many rows each group directly holds. fn hold(vertex_marks: &[VertexMark<'_>], vertices: &mut [Vertex]) { for vm in vertex_marks { let Some((table, reference)) = &vm.mark.contains else { continue }; @@ -702,40 +703,14 @@ fn hold(vertex_marks: &[VertexMark<'_>], vertices: &mut [Vertex]) { } } -/// Stands every group around its members, innermost first: their bounds grown by `padding`, with -/// its header across the top. A group holding nothing placed stands at its own place. -fn stand_groups(vertices: &mut [Vertex], places: &FxHashMap, header: &[f64], padding: f64) { - let mut order: Vec = (0..vertices.len()).filter(|&v| vertices[v].group).collect(); - crate::order::by_key(&mut order, |&v| !u64::from(vertices[v].depth)); - let mut held: Vec> = vec![None; vertices.len()]; - for v in vertices.iter().filter(|v| !v.group) { - if let (Some(p), Some(b)) = (v.parent, v.bounds) { - held[p] = Some(held[p].map_or(b, |h| union(h, b))); - } - } - for g in order { - let top = header[vertices[g].mark]; - let own = if let Some(b) = held[g] { - Some([b[0] - padding, b[1] - padding - top, b[2] + padding, b[3] + padding]) - } else { - let center = vertices[g].pin.or_else(|| places.get(&vertices[g].key).copied()); - center.map(|c| around(c, vertices[g].half)) - }; - vertices[g].bounds = own; - if let (Some(p), Some(b)) = (vertices[g].parent, own) { - held[p] = Some(held[p].map_or(b, |h| union(h, b))); - } - } -} - /// A derived table to write: its dataset and table, the table it stands beside, and its fields. type Writing<'a> = ((Name, Name), &'a Table, Vec<(&'static str, Values)>); -/// A derived field of a vertex table, and its value for a vertex. +/// A derived field of a vertex table: its name, and its value for a vertex. type VertexField = (&'static str, fn(&Vertex) -> Option); -/// A vertex table's derived fields, each a vertex's: its center and half sizes, then how many groups -/// hold it, which only groups gaining or losing members change. +/// A vertex table's derived fields: center, half sizes, then depth (how many groups hold it). +/// Depth changes only when group membership does. const VERTEX_FIELDS: [VertexField; 5] = [ ("x", |v| v.center().map(|c| c[0])), ("y", |v| v.center().map(|c| c[1])), @@ -744,9 +719,9 @@ const VERTEX_FIELDS: [VertexField; 5] = [ ("depth", |v| Some(f64::from(v.depth))), ]; -/// Writes each drawn table's derived table, dropping those of tables no longer drawn: a vertex -/// table's centers, half sizes, and depths; an edge table's paths. A field equal to the one before -/// keeps its column, so nothing uploads again. +/// Writes each drawn table's derived table, and drops those of tables no longer drawn: a vertex +/// table's centers, half sizes, and depths; an edge table's paths. A field equal to the previous +/// one keeps its column, so nothing uploads again. fn write( vertex_marks: &[VertexMark<'_>], vertices: &[Vertex], @@ -780,9 +755,9 @@ fn write( } } -/// Writes what an edit reached over the derived tables, row by row: the center and half sizes of each -/// vertex row `touched` names, and the parts of each edge row `written` names, so only the pages they -/// fall on change. False where a derived table lacks a field to write over. +/// Writes an edit's results into the derived tables row by row: the center and half sizes of each +/// vertex in `touched`, and the parts of each edge row in `written`, so only the pages they fall on +/// change. Returns false when a derived table lacks a field to write. fn write_rows( (vertex_marks, edge_marks): &Marks<'_>, drawn: &Drawn, @@ -798,7 +773,7 @@ fn write_rows( continue; } let Some(table) = derived.get_mut(&(vm.data.name().clone(), vm.table.name().clone())) else { return false }; - // An edit leaves which groups hold each row, and so its depth. + // An edit does not change group membership, so depth stays unchanged. for &(name, value) in &VERTEX_FIELDS[..4] { let values = Values::from(rows.iter().map(|&row| value(&drawn.vertices[vm.first + row])).collect::>()); @@ -820,8 +795,9 @@ fn write_rows( true } -/// Writes `values` over rows `rows` of field `name` of `table`, but where a row holds its value already, -/// so a page changes only when a value on it does; false where it holds no such column. +/// Writes `values` over rows `rows` of field `name` of `table`, skipping rows that already hold +/// their value, so a page changes only when one of its values does. Returns false when `table` has +/// no such values column or the write fails. fn write_field(table: &mut Table, name: &str, rows: &[usize], values: &Values) -> bool { let Some(Field::Values(column, _)) = table.field_named(name) else { return false }; let writes: Vec<(usize, usize)> = @@ -846,11 +822,12 @@ mod tests { const BLOCKS: u64 = 24; - /// Blocks with labels and ports, most in one of two groups, wired by orthogonal nets, straight - /// edges with lanes between the same two blocks, and orthogonal edges with ends, some leaving one - /// block by the same side. + /// Blocks with labels and ports, most in one of two groups inside one outer group. They are + /// wired by orthogonal nets, by straight edges with lanes between the same two blocks, and by + /// orthogonal edges with endpoints, some leaving one block by the same side. fn diagram() -> (Scene, Data) { let scene = Scene::new(Diagram { grid: Grid::Off }) + .draw(Vertices::of("d", "H").contains("G", "outer")) .draw(Vertices::of("d", "G").contains("B", "group")) .draw(Edges::of("d", "N").route(Route::Orthogonal)) .draw(Edges::of("d", "E").ends("from", "to")) @@ -880,7 +857,8 @@ mod tests { let data = Data::new("d"); data.apply( Patch::new() - .set(Table::new("G", vec![1_u64, 2])) + .set(Table::new("H", vec![1_u64])) + .set(Table::new("G", vec![1_u64, 2]).reference("outer", "H", vec![Some(1_u64), Some(1)])) .set(Table::new("N", (0..BLOCKS / 2).collect::>())) .set(blocks) .set(pairs(vec![0, 0, 3, 7], vec![1, 1, 9, 15], "E")) @@ -909,8 +887,8 @@ mod tests { None } - /// Brings `sheet` up to `data`, routing on the frame as while rows are dragged; whether it drew - /// only what an edit reached. + /// Updates `sheet` to `data`, routing on the frame as during a drag. Returns whether it redrew + /// only what an edit affected. fn derive(sheet: &mut Sheet, scene: &Scene, data: &Data) -> bool { let (counters, errors) = (Counters::default(), Arc::new(Mutex::new(Vec::new()))); let now = data.snapshot(); @@ -928,7 +906,7 @@ mod tests { } #[test] - fn a_wire_routes_again_around_a_block_moved_into_its_way() { + fn a_wire_reroutes_around_a_block_moved_into_its_path() { let scene = Scene::new(Diagram { grid: Grid::Off }) .draw(Edges::of("d", "F").ends("from", "to").route(Route::Orthogonal)) .draw(Vertices::of("d", "B").position("x", "y").label(Label::new("name"))); @@ -941,9 +919,10 @@ mod tests { data.apply(Patch::new().set(blocks).set(wire)).expect("valid"); let mut sheet = Sheet::default(); derive(&mut sheet, &scene, &data); - // The third block moves into the wire's way; the wire meets neither it nor anything it moved. + // The third block moves into the wire's path. The wire does not end at it, so only the + // moved box near the wire makes it route again. data.apply(Table::new("B", vec![2_u64]).field("y", vec![0.0])).expect("valid edit"); - assert!(derive(&mut sheet, &scene, &data), "an edit"); + assert!(derive(&mut sheet, &scene, &data), "only what the edit affects is redrawn"); let b = sheet.bounds(&Row { data: "d".into(), table: "B".into(), id: 2_u64.into() }).expect("placed"); let Some(Value::Parts(parts)) = sheet.tables.get(&("d".into(), "F".into())).and_then(|t| t.get(0_u64, "route")) else { @@ -958,7 +937,7 @@ mod tests { } #[test] - fn an_edit_derives_what_deriving_every_row_does() { + fn edits_derive_the_same_tables_as_a_full_rebuild() { let (scene, data) = diagram(); let (mut edits, mut whole) = (Sheet::default(), Sheet::default()); let mut seed = 0x9e37_79b9_7f4a_7c15_u64; @@ -983,7 +962,7 @@ mod tests { .field("y", rows.iter().map(|_| random(600) as f64).collect::>()), _ => Table::new("B", rows.clone()).field("x", vec![None::; rows.len()]), }; - // A row named twice in one edit takes the last value. + // Skip edits that name a row twice; the last value would win. let mut unique = rows.clone(); unique.sort_unstable(); unique.dedup(); @@ -1000,6 +979,6 @@ mod tests { } assert_eq!(edits.extent(), whole.extent(), "step {step}"); } - assert!(redrawn > 30, "edits derived only what they reached: {redrawn} of 60"); + assert!(redrawn > 30, "most steps redraw only what the edit affects: {redrawn} of 60"); } } diff --git a/crates/latkit/src/sheet/wires.rs b/crates/latkit/src/sheet/wires.rs index 571bfa95..c213775f 100644 --- a/crates/latkit/src/sheet/wires.rs +++ b/crates/latkit/src/sheet/wires.rs @@ -1,13 +1,19 @@ -//! Wires: the path each edge row of a diagram runs along, in units with y down. An edge with ends -//! runs between its two vertices; a net joins every port naming it, from its outputs. A straight wire -//! runs between outlines, set apart in lanes where edges join the same two vertices, or as a star -//! through a net's middle; bends run through the points of a path field; an orthogonal wire routes -//! around every block. A route holds until its terminals move or a box near it changes. Then it -//! routes again on the frame when few do or rows are being dragged, else in a host job, keeping its -//! old path while its terminals hold, or an elbow while they moved, until the job answers. After an -//! edit of a few vertices, only the wires meeting them, those leaving by a side it changed, and those -//! near what it moved are worked out: the sides, every route's box, and every wire's are kept as -//! edits change them, so neither finding those wires nor the box around them all walks every wire. +//! Wires: the path of each edge row of a diagram, in units with y down. +//! +//! An edge with endpoints runs between its two vertices; a net joins every port that references +//! it, from its outputs. A straight wire runs between outlines, spread into lanes where edges join +//! the same two vertices; a straight net is a star through its middle. A wire with bends runs +//! through the points of a path field. An orthogonal wire routes around every block. +//! +//! A route stays until its terminals move or a box near it changes. It then routes again on the +//! frame when few wires need it or rows are being dragged, else in a host job. Until the job +//! answers, the wire keeps its old path if its terminals are unchanged, or draws an elbow if they +//! moved. +//! +//! After an edit of a few vertices, only some wires are recomputed: those meeting the vertices, +//! those leaving by a side the edit changed, and those near the boxes it moved. The sides, every +//! route's box, and every wire's box are kept current across edits, so neither finding those wires +//! nor computing the extent of all wires walks every wire. use std::hash::{Hash, Hasher}; @@ -23,20 +29,21 @@ use crate::layout::rect::{EMPTY, Point, Rect, dot, grow, meets, union}; use crate::layout::route::{Net, Terminal, finish, route, simplify}; use crate::{Pending, Route, Shape, Side}; -/// Microseconds a wire takes to route, by estimate: what it spends of the frame's budget. A batch -/// that does not fit routes in a host job. +/// Estimated microseconds to route one wire, charged to the frame's budget. A batch that does not +/// fit routes in a host job. const ROUTE_COST: u64 = 160; -/// Wires routed on the frame at most while rows are dragged, whatever the budget; the rest route -/// once the drag ends. +/// The most wires routed on the frame per update while rows are dragged, regardless of budget. +/// The rest stay due and route in later updates; once the drag ends, those left route within the +/// frame's budget or in a host job. const DRAGGED: usize = 256; -/// How far wires keep from blocks, in tracks: as far as routes keep. +/// How far wires keep from blocks, in tracks; the same clearance the router keeps. const CLEARANCE: f64 = 2.0; -/// A wire's parts. +/// A wire's parts: its polylines. pub(crate) type Parts = Vec>; -/// A route: its polylines, y down, before bends round; a stamp of the terminals it joins; and the box -/// it spans. +/// A route: its polylines (y down, bends not yet rounded), a stamp of the terminals it joins, and +/// the box it spans. pub(crate) struct Routed { lines: Parts, terminals: u64, @@ -49,15 +56,15 @@ impl Routed { Routed { lines, terminals, reach } } - /// A wire waiting for its first route, in nothing's way. + /// A wire waiting for its first route; its empty box meets nothing. fn waiting() -> Routed { Routed { lines: Vec::new(), terminals: 0, reach: EMPTY } } } -/// Every orthogonal wire's route, by edge row, each in a slot of its own; the box each spans, by slot, -/// indexed, which finds the routes a box that moved is near; and the rows due to route again: near a -/// box that moved, or due before and not routed since. +/// Every orthogonal wire's route by edge row, each in its own slot; an index of each route's box by +/// slot, to find the routes near a box that moved; and the rows due to route again (near a box that +/// moved, or due before and not routed since). #[derive(Default)] pub(crate) struct Routes { by_row: FxHashMap, @@ -74,13 +81,14 @@ impl Routes { self.by_row.get(row).map(|(routed, _)| routed) } - /// Row `row` routes along `routed`, due no longer. + /// Stores `routed` as row `row`'s route, and clears the row's due mark. fn routed(&mut self, row: Row, routed: Routed) { self.due.remove(&row); self.put(row, routed); } - /// Row `row` routes again: until it does, it keeps its route, or waits for its first. + /// Marks row `row` due to route again. Until then it keeps its route, or a waiting placeholder + /// if it has none. fn due(&mut self, row: &Row) { if !self.by_row.contains_key(row) { self.put(row.clone(), Routed::waiting()); @@ -88,7 +96,7 @@ impl Routes { self.due.insert(row.clone()); } - /// Row `row`'s route is `routed`, in its slot, or in a free one. + /// Stores `routed` as row `row`'s route, in the row's slot or a free one. fn put(&mut self, row: Row, routed: Routed) { let reach = routed.reach; let slot = if let Some((was, slot)) = self.by_row.get_mut(&row) { @@ -106,7 +114,7 @@ impl Routes { self.reaches.set(slot, reach); } - /// Forgets the routes of the rows `kept` does not hold. + /// Drops the routes of rows for which `kept` returns false. fn retain(&mut self, kept: impl Fn(&Row) -> bool) { let gone: Vec = self.by_row.keys().filter(|row| !kept(row)).cloned().collect(); for row in gone { @@ -130,9 +138,9 @@ impl Routes { } } -/// What wiring keeps between updates, so an edit works out only the wires it reaches: each edge mark's -/// rows' parts and the boxes they run within, indexed, the edge and net rows meeting each vertex, each -/// edge mark's lanes, and the sides orthogonal edges with ends leave their vertices by. +/// Wiring state kept between updates, so an edit recomputes only the wires it affects: each edge +/// mark's rows' parts and an index of their boxes, the edge and net rows meeting each vertex, each +/// edge mark's lanes, and the sides orthogonal edges with endpoints leave their vertices by. #[derive(Default)] pub(crate) struct Wired { pub(crate) parts: Vec>, @@ -151,16 +159,16 @@ impl Wired { self.boxes.extent() } - /// The box edge mark `e`'s row `row` runs within, y down: its parts' points. + /// The box around edge mark `e`'s row `row`, y down: the bounds of its parts' points. fn bounds(&self, e: usize, row: usize) -> Rect { self.parts[e][row].iter().flatten().fold(EMPTY, |b, &p| union(b, dot(p))) } } -/// Where each orthogonal edge with ends leaves its two vertices, and by vertex and side the ends -/// leaving there, ordered along it by where their other ends lie, so wires leave a side without -/// crossing. Kept between edits, so an edit orders again only the sides it changed, and works out -/// again only the edges leaving by them. +/// Where each orthogonal edge with endpoints leaves its two vertices, and for each vertex side, the +/// ends leaving there, ordered by where their other ends lie so wires leave a side without +/// crossing. Kept between edits, so an edit reorders only the sides it changed and recomputes only +/// the edges leaving by them. #[derive(Default)] struct Sides { /// How each edge leaves its vertex at either end. @@ -180,7 +188,7 @@ type Facing = (usize, Side, f64); type Leaving = (f64, Edge, usize); impl Sides { - /// The sides each of `edges`, between the vertices it names, leaves them by. + /// The sides each of `edges` leaves its two vertices by. fn new(edges: impl Iterator)>, vertices: &[Vertex]) -> Sides { let mut sides = Sides::default(); for (edge, ends) in edges { @@ -196,16 +204,15 @@ impl Sides { sides } - /// Edge `edge`, between the vertices `ends` names, leaves them by the sides they face each other - /// by now: out of the sides it left by, into these. The sides it left and joined go into - /// `changed`. + /// Moves edge `edge` (between vertices `ends`) from the sides it left by to the sides the + /// vertices now face each other by. Adds the sides it left and joined to `changed`. fn face(&mut self, edge: Edge, ends: Option<[usize; 2]>, vertices: &[Vertex], changed: &mut Vec<(usize, Side)>) { let now = facings(ends, vertices); if self.of.get(&edge).copied() == now { return; } for (end, (v, side, along)) in self.of.remove(&edge).into_iter().flatten().enumerate() { - let leaving = self.at.get_mut(&(v, side)).expect("an end leaves by its side"); + let leaving = self.at.get_mut(&(v, side)).expect("a recorded end is listed at its side"); let it = (along, edge, end); leaving.remove(leaving.partition_point(|x| order(x) < order(&it))); if leaving.is_empty() { @@ -239,14 +246,14 @@ impl Sides { } } -/// Where an end lies along its side, then its edge: within a side, no edge leaves twice, as its ends -/// stand at two vertices. +/// The sort key of an end on a side: where its other end lies, then its edge. No edge appears +/// twice on one side, since its ends are at two different vertices. fn order(&(along, (e, row), _): &Leaving) -> u128 { crate::order::wide(crate::order::number(along), crate::order::pair(e as u32, row as u32)) } -/// How an edge between vertices `ends` leaves each: by the sides they face each other by, where the -/// other end lies along each; `None` unless both stand, apart. +/// How an edge between vertices `ends` leaves each: by the sides facing each other, with where the +/// other end lies along each. `None` unless both vertices are placed and distinct. fn facings(ends: Option<[usize; 2]>, vertices: &[Vertex]) -> Option<[Facing; 2]> { let [a, b] = ends.filter(|[a, b]| a != b)?; let (ba, bb) = (vertices[a].bounds?, vertices[b].bounds?); @@ -258,21 +265,20 @@ fn facings(ends: Option<[usize; 2]>, vertices: &[Vertex]) -> Option<[Facing; 2]> Some([(a, side_a, along(side_a, bb)), (b, side_b, along(side_b, ba))]) } -/// Whether an edge mark routes orthogonal edges with ends, which share the sides they leave by. +/// Whether an edge mark routes orthogonal edges with endpoints, whose ends share vertex sides. fn sided(em: &EdgeMark<'_>) -> bool { em.mark.route == Some(Route::Orthogonal) && em.mark.ends.is_some() } -/// What changed since wires were last worked out: the boxes that moved, added, or went, and after an -/// edit of a few rows the vertices it touched; with none, every row is worked out. +/// What changed since wires were last computed: the boxes moved, added, or removed, and after an +/// edit, the vertices it touched. With no `touched`, every row is recomputed. pub(crate) struct Change<'a> { pub(crate) moved: &'a [Rect], pub(crate) touched: Option<&'a [usize]>, } -/// Routes a host job works out: each row it answers for by its key, with the stamp of its terminals -/// when asked; the boxes that moved since, which the routes it answers may cross; and how far wires -/// keep from them. +/// A pending routing job: each row it answers, by key, with its terminals' stamp when requested; +/// the boxes that moved since, which its routes may cross; and how far wires keep from them. pub(crate) struct Routing { pub(crate) pending: Pending>, rows: FxHashMap, @@ -280,9 +286,9 @@ pub(crate) struct Routing { clearance: f64, } -/// What wires are worked out from: the diagram's vertices, vertex marks, and edge marks; the blocks -/// wires route around, by vertex, and the groups they route among, y up; and in units how far apart -/// parallel wires run, how round their bends are, and how wide ports are. +/// The inputs to wiring: the diagram's vertices, vertex marks, and edge marks; the blocks wires +/// route around (by vertex) and the groups they route among, y up; and, in units, the spacing of +/// parallel wires, the bend radius, and the port size. pub(crate) struct Context<'a> { pub(crate) vertices: &'a [Vertex], pub(crate) marks: &'a [VertexMark<'a>], @@ -294,8 +300,9 @@ pub(crate) struct Context<'a> { pub(crate) port: f64, } -/// Where a wire meets a vertex: the point, y down, the side it leaves by, the vertex, and how far out -/// the port there reaches, 0 where none is: a wire stops at a port's rim, so its arrowhead shows. +/// Where a wire meets a vertex: the point (y down), the side it leaves by, the vertex, and how far +/// the port there reaches from its center (0 for none). A wire stops at a port's rim so its +/// arrowhead shows. #[derive(Clone, Copy)] struct End { at: Point, @@ -304,8 +311,8 @@ struct End { rim: f64, } -/// An orthogonal wire to route: its edge mark and row, the row, its ends with the root first, and how -/// many of its first ends drive it. +/// An orthogonal wire to route: its edge mark index and row, the row's key, its ends with the root +/// first, and how many leading ends are outputs. struct Ask { mark: usize, row: usize, @@ -314,8 +321,8 @@ struct Ask { outputs: usize, } -/// Takes a routing job's answer: each route it worked out, as routed from the terminals it was asked -/// for; one near a box that moved since it was asked routes again. +/// Takes a routing job's answer, storing each route stamped with the terminals it was requested +/// for. A route near a box that moved since the request is marked due again. pub(crate) fn land(routes: &mut Routes, routing: &Routing, bytes: &[u8]) { for (key, values) in answers(bytes, &["route"]) { let (Some((row, terminals)), Some(Some(Value::Parts(lines)))) = @@ -332,8 +339,9 @@ pub(crate) fn land(routes: &mut Routes, routing: &Routing, bytes: &[u8]) { } } -/// The boxes a change moved, added, or took away among the blocks wires route around: where each was, -/// and where it is. +/// The block boxes a change moved, added, or removed: the old and new box of each moved block, each +/// new block's box, and the old box of each vertex removed or left without a place. Groups that +/// move are skipped. pub(crate) fn moved(before: &FxHashMap, vertices: &[Vertex]) -> Vec { let mut moved = Vec::new(); let mut now: FxHashSet<&Row> = FxHashSet::default(); @@ -353,9 +361,10 @@ pub(crate) fn moved(before: &FxHashMap, vertices: &[Vertex]) -> Vec, input: &Input<'_>, @@ -380,7 +389,7 @@ pub(crate) fn paths( } else { every_row(context, &joins, wired) }; - // Which vertices each row meets is told once, as every row is worked out. + // Record which vertices each row meets only when every row is computed. let telling = change.touched.is_none(); let mut asks: Vec = Vec::new(); for (e, em) in edge_marks.iter().enumerate() { @@ -416,7 +425,7 @@ pub(crate) fn paths( wired.meeting[v].push(meeting); } } - // A net: its ends at the ports naming it, outputs first. + // A net: its ends are the ports that reference it, outputs first. let mut ends: Vec<(End, bool)> = terminals .into_iter() .filter_map(|(v, code, output)| { @@ -444,7 +453,7 @@ pub(crate) fn paths( } } wire(context, input, jobs, &asks, change, (routes, routing), &mut wired.parts); - // The box each row worked out runs within, which the extent of every wire is kept from. + // Update the box of each computed row; the extent of all wires is kept from these boxes. if telling { let boxes = (0..edge_marks.len()).flat_map(|e| (0..edge_marks[e].table.len()).map(move |row| (e, row))); wired.boxes = Boxes::new(boxes.map(|(e, row)| wired.bounds(e, row)).collect()); @@ -459,9 +468,9 @@ pub(crate) fn paths( rows } -/// Every row of every edge mark, by edge mark, with `wired` started over for them: no parts yet, each -/// mark's lanes, where its rows start among them all, and the sides its orthogonal edges with ends -/// leave by. +/// Every row of every edge mark, by edge mark, after resetting `wired` for them: no parts yet, +/// each mark's lanes, where each mark's rows start among all rows, and the sides its orthogonal +/// edges with endpoints leave by. fn every_row(context: &Context<'_>, joins: &[Joins<'_>], wired: &mut Wired) -> Vec> { let edge_marks = context.edges; wired.parts = edge_marks.iter().map(|em| vec![Vec::new(); em.table.len()]).collect(); @@ -487,9 +496,9 @@ fn every_row(context: &Context<'_>, joins: &[Joins<'_>], wired: &mut Wired) -> V edge_marks.iter().map(|em| (0..em.table.len()).collect()).collect() } -/// The rows an edit of vertices `touched` reaches, by edge mark, in order: those meeting them; the -/// orthogonal edges with ends leaving by a side whose ends it changed; and the orthogonal wires due to -/// route again, or asked of the job under way. +/// The rows an edit of vertices `touched` affects, by edge mark, sorted: rows meeting them; +/// orthogonal edges with endpoints leaving by a side whose ends changed; and orthogonal wires due +/// to route again or requested from the pending job. fn reached( context: &Context<'_>, joins: &[Joins<'_>], @@ -503,8 +512,8 @@ fn reached( for &[e, row] in touched.iter().flat_map(|&v| &wired.meeting[v]) { rows[e as usize].push(row as usize); } - // An edge with ends meeting what the edit moved leaves by other sides, or from other places along - // them: so do the edges leaving by a side it left or joined. + // An edge meeting a vertex the edit touched may now leave by other sides, or from other places + // along them; so may the edges leaving by a side it left or joined. let mut changed = Vec::new(); for (e, rows) in rows.iter_mut().enumerate().filter(|(e, _)| sided(&edge_marks[*e])) { rows.sort_unstable(); @@ -530,14 +539,14 @@ fn reached( rows } -/// The edge mark and row drawing edge row `row`. +/// The edge mark index and row index of edge row `row`. fn drawing(context: &Context<'_>, row: &Row) -> Option<(usize, usize)> { let e = context.edges.iter().position(|em| *em.data.name() == row.data && *em.table.name() == row.table)?; Some((e, context.edges[e].table.row(row.id.as_ref())?)) } -/// Routes the orthogonal wires that are due, and writes the parts of every orthogonal wire `asks` -/// holds: its route, bends rounded; an elbow while its terminals moved and a job routes it. +/// Routes the orthogonal wires that are due, and writes the parts of every wire in `asks`: its +/// route with bends rounded, or an elbow while its terminals moved and a job routes it. fn wire( context: &Context<'_>, input: &Input<'_>, @@ -550,7 +559,7 @@ fn wire( if let Some(routing) = routing.as_mut() { routing.moved.extend_from_slice(change.moved); } - // Routes of rows no longer drawn are forgotten, once every row is worked out. + // When every row is computed, drop the routes of rows no longer drawn. if change.touched.is_none() { let drawn: FxHashSet<&Row> = asks.iter().map(|ask| &ask.key).collect(); routes.retain(|row| drawn.contains(row)); @@ -570,7 +579,7 @@ fn wire( .collect(); let asked = |k: &usize| routing.as_ref().is_some_and(|r| r.rows.contains_key(&key_of(&asks[*k].key))); if routing.is_some() && !input.dragging { - // Wires a job under way routes wait for it. + // Wires the pending job is routing wait for it. due.retain(|k| !asked(k)); } if !due.is_empty() { @@ -610,7 +619,8 @@ fn wire( let rows = keys.into_iter().zip(&due).map(|(key, &k)| (key, (asks[k].key.clone(), stamps[k]))).collect(); *routing = Some(Routing { pending, rows, moved: Vec::new(), clearance }); } - // Wires still due route later; until then they keep their routes while their terminals hold. + // Wires still due route later; until then they keep their routes if their terminals are + // unchanged. for &k in &due { routes.due(&asks[k].key); } @@ -622,7 +632,7 @@ fn wire( _ => rest.iter().map(|end| elbow(root, end, clearance)).collect(), }; let mut parts = finish(&lines, context.bend); - // A part reaching another output ends there without an arrowhead. + // A part reaching another output repeats its end there, so it draws without an arrowhead. let outputs = &rest[..(ask.outputs - 1).min(rest.len())]; for part in &mut parts { if let Some(&end) = part.last() @@ -636,8 +646,8 @@ fn wire( } } -/// Where a port of code `code` sits on box `b`, y down: on its side, spread by its place among the -/// ports there, as the port draws. +/// Where a port with code `code` sits on box `b`, y down: on its side, spaced by its place among +/// that side's ports, matching how the port draws. fn port_point(b: Rect, code: u32) -> Point { let (place, count) = (f64::from((code >> 2) & 255), f64::from((code >> 10) & 255)); let along = (place + 1.0) / (count + 1.0) * 2.0 - 1.0; @@ -650,14 +660,14 @@ fn port_point(b: Rect, code: u32) -> Point { } } -/// How far a port `size` across on box `b` reaches from its middle: at most half the box's smaller -/// side. +/// How far a port `size` across reaches from its center on box `b`: half its size, capped at a +/// quarter of the box's smaller side, so the port is at most half that side wide. fn port_reach(b: Rect, size: f64) -> f64 { (size / 2.0).min((b[2] - b[0]).min(b[3] - b[1]) / 4.0) } -/// The ends of orthogonal edge `edge`, as `sides` gives them: on the sides of its vertices facing each -/// other, spread along each by where the other ends lie. +/// The ends of orthogonal edge `edge`, from `sides`: on the facing sides of its vertices, spread +/// along each in order of where the other ends lie. fn faced(sides: &Sides, edge: Edge, vertices: &[Vertex]) -> Vec { (0..2) .filter_map(|end| { @@ -693,7 +703,7 @@ fn stamp(ends: &[End], vertices: &[Vertex]) -> u64 { hasher.finish().max(1) } -/// The net routing `ask`, y up: each end owned by its vertex's block, which a group has none of. +/// The net for routing `ask`, y up: each end is owned by its vertex's block (none for a group). fn net(ask: &Ask, vertices: &[Vertex]) -> Net { let terminal = |end: &End| Terminal { at: [end.at[0], -end.at[1]], @@ -703,8 +713,8 @@ fn net(ask: &Ask, vertices: &[Vertex]) -> Net { Net { terminals: ask.ends.iter().map(terminal).collect() } } -/// The routes of the wires near those about to route, which keep their tracks, y up: within `reach` -/// of the ends of the wires `routed`, in the order the edge marks draw them. +/// The routes, y up, of wires within `reach` of the ends of the wires `routed`, which keep their +/// tracks while those route; ordered by edge mark, then row. fn fixed(context: &Context<'_>, asks: &[Ask], routed: &[usize], routes: &Routes, reach: f64) -> Parts { let region = routed.iter().flat_map(|&k| &asks[k].ends).fold(EMPTY, |region, end| union(region, dot(end.at))); let routing: FxHashSet<&Row> = routed.iter().map(|&k| &asks[k].key).collect(); @@ -718,7 +728,8 @@ fn fixed(context: &Context<'_>, asks: &[Ask], routed: &[usize], routes: &Routes, near.into_iter().flat_map(|(_, r)| flipped(&r.lines)).collect() } -/// An orthogonal path from `a` to `b` that leaves and enters along their sides, `stub` out from each. +/// An orthogonal path from `a` to `b` that leaves and enters along their sides, with a stub of +/// length `stub` at each end. fn elbow(a: &End, b: &End, stub: f64) -> Vec { let out = |end: &End| { let n = normal(end.side); @@ -743,7 +754,7 @@ fn elbow(a: &End, b: &End, stub: f64) -> Vec { simplify(&path) } -/// The way out of a side, y down. +/// The outward direction of a side, y down; zero for no side. fn normal(side: Option) -> Point { match side { Some(Side::Left) => [-1.0, 0.0], @@ -754,8 +765,8 @@ fn normal(side: Option) -> Point { } } -/// A straight wire from `a` to `b` through the points `through`, from outline to outline; without -/// bends, shifted `shift` across into its lane. +/// A straight wire from `a` to `b` through the points `through`, from outline to outline. Without +/// bend points, it is shifted `shift` sideways into its lane. fn straight(a: &Vertex, b: &Vertex, through: &[Point], shift: f64) -> Option> { let (ca, cb) = (a.center()?, b.center()?); if let (Some(&first), Some(&last)) = (through.first(), through.last()) { @@ -772,7 +783,7 @@ fn straight(a: &Vertex, b: &Vertex, through: &[Point], shift: f64) -> Option f64 { let Some(b) = v.bounds else { return 0.0 }; let (w, h) = ((b[2] - b[0]) / 2.0, (b[3] - b[1]) / 2.0); @@ -786,9 +797,9 @@ fn outline(v: &Vertex, d: Point) -> f64 { } } -/// A straight net: two ends joined, or more joined through their middle, outputs into it and from -/// it out to inputs. Parts into the middle end doubled there, a junction without an arrowhead; parts -/// stop at the rims of the ports they join. +/// A straight net: two ends joined directly, or more joined through their centroid, with parts +/// from outputs into it and from it out to inputs. A part into the centroid repeats its end there, +/// a junction without an arrowhead. Parts stop at the rims of the ports they join. fn star(ends: &[End], outputs: usize) -> Parts { if let [a, b] = ends { let mut part = vec![rim_point(a, b.at), rim_point(b, a.at)]; @@ -805,9 +816,9 @@ fn star(ends: &[End], outputs: usize) -> Parts { ends.iter().enumerate().map(part).collect() } -/// Moves a routed part's ends at ports back to the ports' rims, as a straight net's stop: its first -/// point toward the next, and its last, doubled where it reaches an output, toward the one before. -/// The router joins the ports themselves, on their boxes' sides. +/// Moves a routed part's ends at ports back to the ports' rims, as straight nets stop: its first +/// point toward the next, and its last point (with any repeats at an output) toward the one +/// before. The router itself joins the port centers on the boxes' sides. fn stop_at_rims(part: &mut [Point], ends: &[End]) { let port = |p: Point| ends.iter().find(|end| end.rim > 0.0 && end.at == p); let last = part.len().saturating_sub(1); @@ -825,12 +836,12 @@ fn stop_at_rims(part: &mut [Point], ends: &[End]) { } } -/// `end`'s point on the way to `toward`: its port's rim, at most halfway there. +/// `end`'s point toward `toward`: on its port's rim, at most halfway there. fn rim_point(end: &End, toward: Point) -> Point { unit(end.at, toward).map_or(end.at, |(d, length)| add(end.at, d, end.rim.min(length / 2.0))) } -/// The points of row `row`'s path field `field`, y down. +/// The finite points of row `row`'s path field `field`, y down. fn bends(table: &Table, field: &str, row: usize) -> Vec { match table.value(row, field, f64::INFINITY) { Some(Value::Path(points)) => points.into_iter().filter(|p| p.iter().all(|v| v.is_finite())).collect(), @@ -838,7 +849,7 @@ fn bends(table: &Table, field: &str, row: usize) -> Vec { } } -// Lines with y up as y down, or back. +/// Lines flipped between y up and y down. fn flipped(lines: &[Vec]) -> Parts { lines.iter().map(|line| line.iter().map(|p| [p[0], -p[1]]).collect()).collect() } @@ -847,7 +858,7 @@ fn center(b: Rect) -> Point { [b[0].midpoint(b[2]), b[1].midpoint(b[3])] } -/// The unit direction from `a` to `b`, and the distance; `None` where they meet. +/// The unit direction from `a` to `b`, and the distance; `None` where they coincide. fn unit(a: Point, b: Point) -> Option<(Point, f64)> { let d = [b[0] - a[0], b[1] - a[1]]; let length = d[0].hypot(d[1]); @@ -884,7 +895,7 @@ mod tests { } #[test] - fn ports_sit_where_they_draw() { + fn port_points_are_where_ports_draw() { let b = [0.0, 0.0, 40.0, 20.0]; // The second of three on the left: halfway down, y down. assert_eq!(port_point(b, Side::Left.code() | 1 << 2 | 3 << 10), [0.0, 10.0]); @@ -896,8 +907,8 @@ mod tests { fn stars_join_outputs_to_inputs_through_their_middle() { let ends = [end([0.0, 0.0], Side::Right), end([30.0, 0.0], Side::Left), end([30.0, 30.0], Side::Left)]; let parts = star(&ends, 1); - assert_eq!(parts[0], vec![[0.0, 0.0], [20.0, 10.0], [20.0, 10.0]], "into the middle, a junction"); - assert_eq!(parts[2], vec![[20.0, 10.0], [30.0, 30.0]], "out to an input, an arrowhead"); + assert_eq!(parts[0], vec![[0.0, 0.0], [20.0, 10.0], [20.0, 10.0]], "into the middle, ending in a junction"); + assert_eq!(parts[2], vec![[20.0, 10.0], [30.0, 30.0]], "out to an input, ending in an arrowhead"); } #[test] @@ -918,7 +929,7 @@ mod tests { let mut part = vec![[0.0, 0.0], [20.0, 0.0], [20.0, 20.0], [40.0, 20.0]]; stop_at_rims(&mut part, &ends); assert_eq!(part, [[4.0, 0.0], [20.0, 0.0], [20.0, 20.0], [36.0, 20.0]], "routed between the ports"); - // A branch from a junction into an output, doubled there: the junction stays. + // A branch from a junction into an output, its end repeated there: the junction stays. let mut part = vec![[20.0, 0.0], [20.0, 20.0], [40.0, 20.0], [40.0, 20.0]]; stop_at_rims(&mut part, &ends); assert_eq!(part, [[20.0, 0.0], [20.0, 20.0], [36.0, 20.0], [36.0, 20.0]]); diff --git a/crates/latkit/src/shot.rs b/crates/latkit/src/shot.rs index 47f0d92e..222640ec 100644 --- a/crates/latkit/src/shot.rs +++ b/crates/latkit/src/shot.rs @@ -1,12 +1,13 @@ -//! Shots: pictures of views at a size of their own. A shot draws each view into its panel of the -//! picture through frames of its own, on a clock of its own, until every view is complete; then one -//! compute pass composes the parts into exactly the bytes its consumer takes, RGBA rows for an image -//! or NV12 planes for a video's encoder, and a plain buffer copy reads them back. No pixel is touched -//! on the CPU. +//! Shots: pictures of views at their own size. //! -//! A view draws at one size at a time: while a shot draws it, nothing else does. Natively the shot -//! borrows it; in a browser its canvas holds its last frame. A picture draws each view's data as it -//! was when the picture began; a video's frames follow its data. +//! A shot draws each view into its panel of the picture, with its own frames and clock, until every +//! view is complete. One compute pass then composes the panels into the exact bytes the consumer +//! needs: RGBA rows for an image, or NV12 planes for a video encoder. A buffer copy reads them +//! back, so no pixel is processed on the CPU. +//! +//! A view draws at one size at a time, so nothing else draws it during a shot. Natively the shot +//! borrows the view. In a browser, the view's canvas keeps its last frame. A picture draws each +//! view's data as of when the picture began. A video's frames follow data changes. use std::sync::Arc; use std::task::Waker; @@ -23,13 +24,14 @@ use crate::time::Notify; use crate::view::Signal; use crate::{Error, Frame, Image, Panels, Pixels, Result, View}; -/// Seconds a picture steps its views' clocks while they ease toward where they go. +/// Seconds a picture advances its views' clocks per step while they ease to their targets. pub(crate) const TICK: f64 = 1.0 / 60.0; -/// The longest a picture waits for work to land before drawing again. +/// The longest a picture waits for work to arrive before drawing again. const FRAME: Duration = Duration::from_millis(16); -/// A picture of views, each drawn into its panel of `size` pixels at `scale` pixels per CSS pixel, -/// once they are complete as its capture says: plain data, like a scene. +/// A picture of views, taken once they are complete as its [`Capture`] says. Each view draws into +/// its panel of a `size`-pixel picture at `scale` pixels per CSS pixel. A shot is plain data, like +/// a scene. /// /// ```no_run /// # async fn example(view: &mut latkit::View) -> latkit::Result<()> { @@ -43,23 +45,25 @@ const FRAME: Duration = Duration::from_millis(16); pub struct Shot { /// Width and height in pixels. size: [u32; 2], - /// Pixels per CSS pixel: views lay out `size / scale` CSS pixels; 1 unless set. + /// Pixels per CSS pixel. Views lay out `size / scale` CSS pixels. Defaults to 1. #[serde(default = "one")] scale: f32, - /// Each view's panel of the picture, `[x, y, width, height]` as fractions; the whole by default. + /// Each view's panel, `[x, y, width, height]` as fractions of the picture. Defaults to one view + /// filling it. #[serde(default)] panels: Panels, - /// Whether alpha is kept where the themes' backgrounds would be. + /// Whether the picture keeps alpha where the themes' backgrounds would be. #[serde(default)] transparent: bool, /// When the picture is taken: once its views are complete and still, or as they are now. #[serde(default)] capture: Capture, - /// The longest a picture, or a video frame, may take: 30 seconds unless set, or as long as its - /// caller waits. + /// The longest a picture or video frame may take. Defaults to 30 seconds. `None` waits as long + /// as the caller does. #[serde(skip, default = "thirty")] deadline: Option, - /// A still's effect time in seconds, where elapsed time and flowing comets read it. + /// Fixed effect time in seconds for elapsed-time expressions and flowing comets. `None` follows + /// the view's clock. #[serde(default, deserialize_with = "some_seconds")] #[schemars(with = "Option")] elapsed: Option, @@ -69,7 +73,7 @@ fn one() -> f32 { 1.0 } -#[expect(clippy::unnecessary_wraps, reason = "what a shot made from JSON waits, which its caller may lift")] +#[expect(clippy::unnecessary_wraps, reason = "serde default for an optional deadline that callers may remove")] fn thirty() -> Option { Some(Duration::from_secs(30)) } @@ -78,11 +82,11 @@ fn thirty() -> Option { #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize, schemars::JsonSchema)] #[serde(rename_all = "lowercase")] pub enum Capture { - /// Once every view is complete and still: transitions, flights, and label fades run on, a - /// sixtieth of a second a step, until they land. + /// Once every view is complete and still. Transitions, camera flights, and label fades advance + /// 1/60 s per step until they finish. #[default] Still, - /// Once every view is complete, as it stands now: what moves holds where it is. + /// Once every view is complete, as it is now. Anything in motion is captured where it is. Now, } @@ -91,7 +95,7 @@ fn some_seconds<'de, D: serde::Deserializer<'de>>(deserializer: D) -> std::resul } impl Shot { - /// A picture of `size` pixels, one view filling it at a pixel per CSS pixel. + /// A picture of `size` pixels, with one view filling it at one pixel per CSS pixel. pub fn new(size: [u32; 2]) -> Shot { Shot { size, @@ -104,29 +108,29 @@ impl Shot { } } - /// Draws `scale` pixels per CSS pixel: a view lays out `size / scale` CSS pixels, so 2 draws what - /// it shows twice as sharp. + /// Sets pixels per CSS pixel. A view lays out `size / scale` CSS pixels, so 2 draws the same + /// content twice as sharp. #[must_use] pub fn scale(mut self, scale: f32) -> Shot { self.scale = scale; self } - /// Where each view draws in the picture, one panel per view in order; by default one view fills it. + /// Sets each view's panel in the picture, one per view, in order. By default one view fills it. #[must_use] pub fn panels(mut self, panels: impl Into) -> Shot { self.panels = panels.into(); self } - /// Keeps the picture's alpha: views clear to transparent where their themes' backgrounds are. + /// Keeps the picture's alpha. Views clear to transparent instead of their themes' backgrounds. #[must_use] pub fn transparent(mut self) -> Shot { self.transparent = true; self } - /// Takes the picture as `capture` says: once its views are complete and still, by default, or as + /// Sets when the picture is taken: once its views are complete and still (the default), or as /// they are now. #[must_use] pub fn capture(mut self, capture: Capture) -> Shot { @@ -134,16 +138,16 @@ impl Shot { self } - /// The longest, in real time, a picture or a video frame may take, including GPU readback: 30 - /// seconds unless set; `None` waits for as long as its caller does, who drops the future to stop. - /// An incomplete picture returns [`Error::Timeout`]. + /// Sets the longest real time a picture or video frame may take, including GPU readback. + /// Defaults to 30 seconds. `None` waits until the caller drops the future. A picture still + /// incomplete at the deadline fails with [`Error::Timeout`]. #[must_use] pub fn deadline(mut self, deadline: Option) -> Shot { self.deadline = deadline; self } - /// Fixes elapsed-time expressions and animated drawing effects at this time while finite + /// Fixes elapsed-time expressions and animated drawing effects at `elapsed`, while finite /// transitions still settle. By default they follow the view's clock. #[must_use] pub fn elapsed(mut self, elapsed: Duration) -> Shot { @@ -156,33 +160,36 @@ impl Shot { self.size } - /// Checks, without a GPU, that its pictures compose into `pixels`: a positive scale, panels that - /// cover pixels, and for NV12 a width a multiple of 4, a height of 2, and no alpha. + /// Checks without a GPU that the shot's pictures can compose into `pixels`. Requires a positive + /// scale and panels that cover pixels. NV12 also requires a width that is a multiple of 4, a + /// height that is a multiple of 2, and no transparency. pub fn check(&self, pixels: Pixels) -> Result<()> { if !(self.scale.is_finite() && self.scale > 0.0) { - return Err(Error::invalid("a shot's scale is positive").at("scale")); + return Err(Error::invalid("a shot's scale must be positive").at("scale")); } self.panels.rects(self.size, align(pixels))?; let [width, height] = self.size; if pixels == Pixels::Nv12 && (width % 4 != 0 || height % 2 != 0) { - let reason = format!("an NV12 video's width is a multiple of 4 and its height of 2, not {width}×{height}"); + let reason = format!("NV12 video needs a width divisible by 4 and an even height, not {width}×{height}"); return Err(Error::invalid(reason).at("size")); } if pixels == Pixels::Nv12 && self.transparent { - return Err(Error::invalid("a video keeps no alpha; take it without transparent").at("transparent")); + return Err(Error::invalid("a video has no alpha channel, so it cannot be transparent").at("transparent")); } Ok(()) } - /// The picture of `views`, one per panel, once every one is complete as its [`Capture`] says, - /// each drawing its data as it was when the picture began. Fails on a render error (including - /// errors still queued in [`View::events`]) or its deadline; events remain available to the - /// caller and views are returned even on cancellation. + /// Takes the picture of `views`, one per panel, once every view is complete as the [`Capture`] + /// says. Each view draws its data as of when the picture began. + /// + /// Fails on a render error, including errors still queued in [`View::events`], or at the + /// deadline. Events stay available to the caller, and the views are given back even if the + /// future is dropped. pub async fn image(&self, views: &mut [&mut View]) -> Result { self.image_of(&mut Borrowed(views)).await } - /// The picture of the views `views` lends. + /// Takes the picture of the views `views` lends. pub(crate) async fn image_of(&self, views: &mut impl Views) -> Result { let mut drawing = Drawing::later(self.clone(), Pixels::Rgba, true); views.lend(|views| drawing.begin(views))?; @@ -209,8 +216,8 @@ impl Shot { } } -/// What lends a drawing its views for a step at a time: borrowed views natively; in a browser the -/// views of canvases, which the page may read between steps. +/// Lends a drawing its views for one step at a time. Natively these are borrowed views. In a +/// browser they are canvases' views, which the page may use between steps. pub(crate) trait Views { /// Calls `f` with the views, one per panel. fn lend(&mut self, f: impl FnOnce(&mut [&mut View]) -> R) -> R; @@ -225,76 +232,78 @@ impl Views for Borrowed<'_, '_> { } } -/// One view's panel of a picture: the texture it draws into, its size, and the bind group -/// composing it. +/// One view's panel of a picture: its target texture, its size, and its compose bind group. struct Panel { target: wgpu::TextureView, size: [u32; 2], group: wgpu::BindGroup, } -/// What a step of drawing came to. +/// The outcome of one drawing step. pub(crate) enum Step { - /// Every view was complete: the picture's bytes, once read back. + /// Every view was complete. Holds the readback of the picture's bytes. Done(Readback), - /// Views still ease toward where they go: draw again a step later. + /// Views are still easing. Draw again one step later. Easing, - /// Views wait for work under way: draw again once it lands, which wakes the drawing. + /// Views wait for pending work. Draw again when it arrives, which wakes the drawing. Waiting, } -/// Pictures under way: a shot's, composed into `pixels`, from views it draws once it has them. +/// A shot's pictures in progress, composed into `pixels`. The views come with the first picture. pub(crate) struct Drawing { shot: Shot, pixels: Pixels, - /// What the pictures draw into, once the views are known. + /// The textures and buffer the pictures draw into, created once the views are known. picture: Option, notify: Arc, waker: Waker, - /// The wakers the views had, given back once the drawing is done. + /// The views' previous wakers, given back when the drawing is dropped. lent: Vec<(Arc, Option)>, - /// Whether its views draw their data as it was when the drawing began, as a picture's do; a - /// video's follow it from frame to frame. + /// Whether views draw their data as of when the drawing began, as pictures do. A video's views + /// follow their data from frame to frame. holding: bool, - /// When the picture under way began, for its deadline. + /// When the current picture began, for its deadline. started: Option, } -/// What a drawing's pictures draw into: one texture per view, and the buffer they compose into. +/// The GPU resources a drawing's pictures draw into: one texture per view, and the buffer they +/// compose into. struct Picture { gpu: Gpu, panels: Vec, - /// The picture's bytes, and how many there are. + /// The composed picture's bytes, and their count. buffer: wgpu::Buffer, len: u64, - /// The background where no panel draws, when the panels leave gaps. + /// Fills the background where no panel draws. Present when the panels may leave gaps. fill: Option, } impl Drawing { - /// Pictures of `shot` composed into `pixels`, drawn from the views the first picture is given, - /// `holding` the data the views draw as it was when they were given. + /// A drawing of `shot`'s pictures, composed into `pixels`. The first picture is given the + /// views. With `holding`, views draw their data as of when they were given. pub(crate) fn later(shot: Shot, pixels: Pixels, holding: bool) -> Drawing { let notify = Arc::new(Notify::default()); let waker = Waker::from(notify.clone()); Drawing { shot, pixels, picture: None, notify, waker, lent: Vec::new(), holding, started: None } } - /// Begins a picture of `views`, one per panel. The first takes the views: their wakers wake the - /// drawing until it is done, and nothing else draws them. + /// Begins a picture of `views`, one per panel. The first call takes the views: until the + /// drawing is dropped, their wakers wake the drawing and nothing else draws them. Fails if + /// the view count differs from the panel count, a view is already lent, or the views use + /// different GPUs. pub(crate) fn begin(&mut self, views: &[&mut View]) -> Result<()> { self.started = Some(Instant::now()); let expected = self.shot.panels.len(); if views.len() != expected { - return Err(Error::invalid(format!("a shot of {expected} panels draws {} views", views.len()))); + return Err(Error::invalid(format!("a shot of {expected} panels was given {} views", views.len()))); } if self.picture.is_none() { for (i, view) in views.iter().enumerate() { if view.lent() || views[..i].iter().any(|other| Arc::ptr_eq(other.signal(), view.signal())) { - return Err(Error::invalid("a view is already lent to a picture")); + return Err(Error::invalid("a view is listed twice or already in use by a shot or video")); } if !Arc::ptr_eq(&views[0].gpu().0, &view.gpu().0) { - return Err(Error::invalid("all panels must use the same GPU")); + return Err(Error::invalid("all views must use the same GPU")); } } self.picture = Some(Picture::new(&self.shot, views, self.pixels)?); @@ -317,7 +326,8 @@ impl Drawing { } impl Picture { - /// What pictures of `shot` from `views`, one per panel, draw into, composed into `pixels`. + /// Creates the textures and buffer for pictures of `shot` from `views`, one per panel, composed + /// into `pixels`. Fails if the picture exceeds the GPU's limits. fn new(shot: &Shot, views: &[&mut View], pixels: Pixels) -> Result { shot.check(pixels)?; let rects = shot.panels.rects(shot.size, align(pixels))?; @@ -328,7 +338,7 @@ impl Picture { if width == 0 || height == 0 || width.max(height) > limits.max_texture_dimension_2d { return Err(Error::Limit( format!( - "a picture of {width}×{height} pixels; this GPU draws up to {0}×{0}", + "a picture of {width}×{height} pixels exceeds this GPU's limit of {0}×{0}", limits.max_texture_dimension_2d ) .into(), @@ -341,7 +351,7 @@ impl Picture { }; if len > limits.max_storage_buffer_binding_size.min(limits.max_buffer_size) { return Err(Error::Limit( - format!("a picture of {width}×{height} pixels is larger than this GPU binds").into(), + format!("a picture of {width}×{height} pixels exceeds this GPU's buffer size limit").into(), )); } let buffer = device.create_buffer(&wgpu::BufferDescriptor { @@ -350,7 +360,7 @@ impl Picture { usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC, mapped_at_creation: false, }); - // Gaps take the first view's background, or none in a transparent picture. + // Gaps get the first view's background, or stay clear in a transparent picture. let background = if shot.transparent { [0.0; 4] } else { views[0].scene().theme.background.to_array() }; let layout = gpu.0.lock().pipelines.pass_layout(device, Shader::Compose); let uniform = |origin: [u32; 2], size: [u32; 2]| { @@ -408,11 +418,11 @@ impl Picture { } impl Drawing { - /// Draws every view into its panel at `time` once. When every one is complete, and with `still` - /// none eases, composes them and starts reading the picture back. Until what the views draw with - /// compiled, it draws nothing and waits for that to land, eased or not. + /// Draws every view into its panel once, at `time`. When every view is complete, and none is + /// easing if `still`, composes the panels and starts the readback. Until the views' pipelines + /// have compiled, draws nothing and returns [`Step::Waiting`], even while views are easing. pub(crate) fn step(&mut self, views: &mut [&mut View], time: f64, still: bool) -> Result { - let picture = self.picture.as_ref().ok_or_else(|| Error::invalid("a picture begins before it draws"))?; + let picture = self.picture.as_ref().ok_or_else(|| Error::invalid("a picture must begin before it draws"))?; if picture.gpu.lost() { return Err(Error::DeviceLost); } @@ -455,7 +465,8 @@ impl Drawing { Ok(step) } - /// The bytes `readback` holds, once the GPU has them. + /// Returns `readback`'s bytes once the GPU has written them. Fails at the deadline or when the + /// device is lost. pub(crate) async fn read(&self, readback: &Readback) -> Result> { loop { self.check_deadline()?; @@ -471,7 +482,7 @@ impl Drawing { } } - /// Waits until work a view waits on lands, or a frame's time passes. + /// Waits until work a view depends on arrives, or one frame's time passes. pub(crate) async fn wait(&self) { self.finish(); self.notify.wait(FRAME).await; @@ -486,8 +497,8 @@ impl Drawing { Ok(()) } - /// Natively, polls what was submitted, so readbacks map and wake the drawing; a browser maps - /// them itself. + /// Natively, polls the device so readbacks map and wake the drawing. A browser maps them + /// itself. #[cfg_attr(target_arch = "wasm32", expect(clippy::unused_self, reason = "a browser maps readbacks itself"))] fn finish(&self) { #[cfg(not(target_arch = "wasm32"))] @@ -502,8 +513,8 @@ impl Drawing { } } -/// The compose passes of a picture of `pixels`: the background's, each panel's, and the blocks each -/// thread writes. +/// The compose entry points for `pixels`: the background fill and the per-panel pass, plus the +/// pixel block each thread writes. fn composing(pixels: Pixels) -> (&'static str, &'static str, [u32; 2]) { match pixels { Pixels::Rgba => ("fill_rgba", "rgba", [1, 1]), @@ -512,8 +523,8 @@ fn composing(pixels: Pixels) -> (&'static str, &'static str, [u32; 2]) { } impl Picture { - /// Whether what composes it into `pixels` compiled: asked for as its first picture composes, all - /// at once, `waker` waking as it lands. + /// Whether the compose pipelines for `pixels` have compiled. The first call requests them all + /// together, and `waker` wakes when they finish. fn composes(&self, pixels: Pixels, waker: &Waker) -> bool { let (fill, each, _) = composing(pixels); let needs: Vec = self @@ -529,8 +540,8 @@ impl Picture { inner.pipelines.ready(&needs) } - /// Records the passes composing every panel into the picture's `pixels`, the background first - /// where panels leave gaps. + /// Records the passes that compose every panel into the picture's `pixels`. Fills the + /// background first where panels may leave gaps. fn compose(&self, encoder: &mut wgpu::CommandEncoder, pixels: Pixels, size: [u32; 2]) { let device = self.gpu.device(); let mut inner = self.gpu.0.lock(); @@ -557,7 +568,7 @@ impl Picture { } impl Drop for Drawing { - /// Gives each view back the waker it had, so whatever drew it draws it again. + /// Gives each view back its previous waker, so its previous owner draws it again. fn drop(&mut self) { for (signal, before) in self.lent.drain(..) { signal.give_back(before); @@ -565,7 +576,7 @@ impl Drop for Drawing { } } -/// The blocks a picture composed into `pixels` is made of: a pixel, or NV12's 4×2 of luma and +/// The pixel block a picture in `pixels` is made of: one pixel, or NV12's 4×2 block of luma with /// shared chroma. fn align(pixels: Pixels) -> [u32; 2] { match pixels { diff --git a/crates/latkit/src/tiles/cache.rs b/crates/latkit/src/tiles/cache.rs index 94df80a1..f3d79b57 100644 --- a/crates/latkit/src/tiles/cache.rs +++ b/crates/latkit/src/tiles/cache.rs @@ -1,6 +1,7 @@ -//! What a GPU fetched of each source of tiles, by URL: opened once, its pieces fetched and decoded -//! once for every set drawing them, and let go of once none does. Work landing for the source wakes -//! every set drawing it, so a piece one view fetched draws in each. +//! The tile cache: each tile source a GPU's views draw, by URL. A source is opened once, its pieces +//! are fetched and decoded once for every set drawing them, and it is released once no set draws +//! it. Work finishing for a source wakes every set drawing it, so a piece one view fetched draws in +//! all of them. use std::collections::hash_map::Entry; use std::sync::{Arc, Mutex, PoisonError}; @@ -14,29 +15,29 @@ use crate::derive::Jobs; use crate::host::Request; use crate::{Error, Pending}; -/// Bytes read first of a source that is an archive, as a URL ending in `.pmtiles` names: its header, -/// and in most archives the root directory after it. Any other source is read whole. +/// Bytes first read from an archive source (a URL ending in `.pmtiles`): its header, and in most +/// archives the root directory after it. Other sources are read whole. const ARCHIVE_FIRST: u64 = 16_384; -/// Every source of tiles a GPU's views draw, by URL. +/// Every tile source a GPU's views draw, by URL. #[derive(Default)] pub(crate) struct Cache { sources: FxHashMap, Fetched>, } -/// One source of tiles: how far it opened, and what wakes the sets drawing it. +/// One tile source: its open state, and the waker for the sets drawing it. pub(crate) struct Fetched { pub(crate) state: State, sets: Arc, waker: Waker, } -/// How far a source opened. +/// A source's open state. pub(crate) enum State { - /// Its first bytes are being fetched; they say what it holds. + /// Its first bytes are being fetched; they determine what it holds. Opening(Pending>), Open(Holds), - /// It could not be opened, and why. + /// It could not be opened, with the reason. Failed(Error), } @@ -46,7 +47,7 @@ pub(crate) enum Holds { Vector(Box), } -/// The sets drawing a source, each with what wakes it: work landing for the source wakes them all. +/// The sets drawing a source, each with its waker. Work finishing for the source wakes them all. #[derive(Default)] struct Sets(Mutex>); @@ -65,8 +66,9 @@ impl Wake for Sets { } impl Cache { - /// The source at `url`, with set `set` drawing it and woken by `jobs`'s waker as its work lands: - /// opened as far as what landed allows, its first bytes asked for where no set drew it before. + /// The source at `url`, registering set `set` as drawing it, to be woken by `jobs`'s waker as + /// the source's work finishes. Starts opening the source if no set drew it before, and opens it + /// as far as finished work allows. pub(crate) fn open(&mut self, url: &Arc, set: u64, jobs: &Jobs<'_>) -> &mut Fetched { let fetched = match self.sources.entry(url.clone()) { Entry::Occupied(entry) => entry.into_mut(), @@ -87,7 +89,7 @@ impl Cache { fetched } - /// Fetches and decodes under way for the source at `url`. + /// Fetches and decodes in progress for the source at `url`. pub(crate) fn under_way(&self, url: &str) -> usize { self.sources.get(url).map_or(0, |fetched| match &fetched.state { State::Opening(_) => 1, @@ -97,7 +99,8 @@ impl Cache { }) } - /// Lets go of what set `set` drew of the source at `url`; once no set draws it, of the source. + /// Releases what set `set` drew of the source at `url`, and the source itself once no set draws + /// it. pub(crate) fn release(&mut self, url: &str, set: u64) { let Some(fetched) = self.sources.get_mut(url) else { return }; let left = { @@ -118,12 +121,12 @@ impl Cache { } impl Fetched { - /// What wakes every set drawing the source. + /// The waker for every set drawing the source. pub(crate) fn waker(&self) -> &Waker { &self.waker } - /// Opens the source once its first bytes landed: time tiles where they hold a manifest, else + /// Opens the source once its first bytes arrive: as time tiles if they hold a manifest, else as /// vector tiles. fn settle(&mut self, url: &Arc, jobs: &Jobs<'_>) { let State::Opening(pending) = &self.state else { return }; @@ -144,8 +147,8 @@ impl Fetched { } } -/// How the source at `url` starts opening: a URL template holds vector tiles at once; an archive is -/// read in ranges, from its header; a document is read whole. +/// Starts opening the source at `url`. A URL template is vector tiles, open at once. An archive is +/// read in ranges, starting with its header. A document is read whole. fn start(url: &Arc, jobs: &Jobs<'_>) -> State { if url.contains("{z}") { return State::Open(Holds::Vector(Box::new(vector::Fetched::template(url)))); diff --git a/crates/latkit/src/tiles/frames.rs b/crates/latkit/src/tiles/frames.rs index 3897f635..2e18094f 100644 --- a/crates/latkit/src/tiles/frames.rs +++ b/crates/latkit/src/tiles/frames.rs @@ -1,9 +1,9 @@ -//! Time tiles: sampled fields' histories fetched in spans as the sets drawing them ask, from the -//! files [`Snapshot::to_tiles`](crate::Snapshot::to_tiles) writes. Every field wants its coarsest -//! spans, which stand in for any span still loading; a line tracing it wants the spans of the level of -//! detail its plot window draws at, middle first; a mark showing it at the view's coordinate wants the -//! frames about that coordinate. A span drawn puts its entries in the set's dataset; one no longer -//! drawn lets them go. +//! Time tiles: sampled fields' histories fetched in spans as the sets drawing them need, from the +//! files [`Snapshot::to_tiles`](crate::Snapshot::to_tiles) writes. Every field needs its coarsest +//! spans, which are drawn as placeholders for any span still loading. A line tracing a field needs +//! the spans of the level of detail its plot window draws at, middle first. A mark showing a field +//! at the view's coordinate needs the frames around that coordinate. A drawn span adds its entries +//! to the set's dataset, and a span no longer drawn removes them. use std::ops::Range; use std::sync::Arc; @@ -20,21 +20,21 @@ use crate::derive::Jobs; use crate::host::Request; use crate::{Error, Pending}; -/// Spans a line wants at most, middle first: those its plot window draws need far fewer. +/// Most spans a line needs, middle first. A plot window normally needs far fewer. const MAX_SPANS: usize = 64; -/// Sampled fields' spans of one manifest, as every set drawing them shares them: the dataset without -/// their entries, and each field's spans fetched and decoded. +/// The spans of one manifest's sampled fields, shared by every set drawing them: the base dataset +/// without their entries, and each field's fetched and decoded spans. pub(crate) struct Fetched { manifest: Manifest, - /// Where each span lies, resolved against the manifest's URL. + /// The spans' URL template, resolved against the manifest's URL. url: String, base: Base, - /// Per field listed, its spans. + /// Each listed field's spans. fields: Vec>, } -/// The dataset without its sampled fields' entries, as it lands. +/// The base dataset, without its sampled fields' entries, as it arrives. enum Base { Fetching(Pending>), Landed(Snapshot), @@ -42,7 +42,8 @@ enum Base { } impl Fetched { - /// The fields `manifest`, found at `url`, lists; their dataset is fetched at once. + /// Opens the fields that `manifest`, fetched from `url`, lists. Their base dataset is fetched + /// immediately. pub(crate) fn open(url: &str, manifest: Manifest, jobs: &Jobs<'_>) -> Fetched { let base = jobs.fetch(&Request::new(crate::url::resolve(url, &manifest.data))); Fetched { @@ -53,19 +54,19 @@ impl Fetched { } } - /// Fetches and decodes under way. + /// Fetches and decodes in progress. pub(crate) fn under_way(&self) -> usize { usize::from(matches!(self.base, Base::Fetching(_))) + self.fields.iter().map(Tiling::under_way).sum::() } - /// Lets go of what set `set` drew. + /// Releases what set `set` drew. pub(crate) fn release(&mut self, set: u64) { for tiling in &mut self.fields { tiling.release(set); } } - /// Takes the dataset without the fields' entries, once it landed. + /// Takes the base dataset once it arrives. fn settle(&mut self) { let Base::Fetching(pending) = &self.base else { return }; let Some(landed) = pending.take() else { return }; @@ -76,8 +77,8 @@ impl Fetched { } } -/// What one set draws of time tiles: whether its dataset holds the one without the fields' entries -/// yet, and per field listed, the spans it draws with the runs each put in. +/// What one set draws of time tiles: whether its dataset holds the base dataset yet, and for each +/// listed field, the spans it draws with the runs each added. pub(crate) struct Drawing { based: bool, fields: Vec>>, @@ -93,8 +94,9 @@ impl Drawing { self.fields.iter().map(FxHashMap::len).sum() } - /// Brings `data`, set `set`'s dataset `name`, up to what `reading` reads of it, from what - /// `fetched` holds: whether it changed, whether a span stands in, and a failure to tell. + /// Updates `data`, set `set`'s dataset `name`, to what `reading` reads of it, from what + /// `fetched` holds. Returns whether it changed, whether any wanted span still shows a + /// placeholder, and a failure to report. pub(crate) fn update( &mut self, set: u64, @@ -122,8 +124,8 @@ impl Drawing { for (k, listed) in manifest.fields.iter().enumerate() { let reads = reading.reads(name, &listed.table, &listed.field); let (tiling, drawn_spans) = (&mut fields[k], &mut self.fields[k]); - // Spans drawn may tell more finely where the window lies, which may want finer spans: ask - // again until what is wanted settles, so what stands in is never taken for what draws. + // Drawn spans may locate the window more precisely, which may need finer spans. Repeat + // until the wanted spans stop changing, so a placeholder is never mistaken for data. let (mut asked, mut standing) = (Vec::new(), false); for _ in 0..=MAX_LEVEL { let wanted = @@ -143,8 +145,8 @@ impl Drawing { } } -/// Makes `data`'s field `field` of `table` hold the entries of spans `drawn` and of no other, of -/// those `tiling` decoded; `spans` holds the runs each span put in. Whether it changed. +/// Makes field `field` of `table` in `data` hold exactly the entries of spans `drawn`, from those +/// `tiling` decoded. `spans` records the runs each span added. Returns whether it changed. fn draw( spans: &mut FxHashMap>, tiling: &Tiling, @@ -177,7 +179,8 @@ fn draw( changed } -/// Puts the runs of field `field` of `table` that `tile` holds in `samples`: those it put. +/// Inserts the runs of field `field` of `table` from `tile` into `samples`, returning the runs +/// inserted. fn held(tile: &Patch, table: &Name, field: &Name, samples: &mut Samples) -> Vec { let mut runs = Vec::new(); for op in &tile.ops { @@ -194,7 +197,7 @@ fn held(tile: &Patch, table: &Name, field: &Name, samples: &mut Samples) -> Vec< runs } -/// The samples `data` holds of the field `listed` names. +/// The samples `data` holds for the field `listed` names. fn sampled<'a>(data: &'a Snapshot, listed: &Listed) -> Option<&'a Samples> { match data.table(&listed.table)?.field_named(&listed.field)? { Field::Sampled(samples, _) => Some(samples), @@ -202,10 +205,11 @@ fn sampled<'a>(data: &'a Snapshot, listed: &Listed) -> Option<&'a Samples> { } } -/// The spans of a field reading `frames` a view wants, nearest first: those of its coarsest level -/// holding any entry always; where a mark shows it, `(_, true)`, the frames about the coordinate shown; -/// where a line traces it, `(true, _)`, those of the level of detail the plot window draws at, middle -/// first. Where the window lies among the frames, what is held tells, as finely as it is held. +/// The spans a view needs of a field covering `frames`, nearest first. These are always the spans +/// of its coarsest level holding any entry; when a mark shows it (`(_, true)`), the frames around +/// the shown coordinate; and when a line traces it (`(true, _)`), the spans of the level of detail +/// the plot window draws at, middle first. The window's position among the frames comes from the +/// samples already held, as precisely as they allow. fn wanted( frames: &Range, samples: Option<&Samples>, @@ -214,10 +218,10 @@ fn wanted( chunk: u32, levels: u32, ) -> Vec { - // The entries of each level a history of `frames` holds: whole buckets only. + // The bucket indices at each level within `frames`: whole buckets only. let chunk = u64::from(chunk); let entries = |level: u32| frames.start >> (3 * level)..frames.end >> (3 * level); - // The spans of `level` holding its entries over frames `window`. + // The spans of `level` covering its entries within frames `window`. let spans = |level: u32, window: &Range| { let (held, span) = (entries(level), bucket_frames(level)); let first = (window.start / span).max(held.start); @@ -240,7 +244,8 @@ fn wanted( let (held, span) = (entries(level), bucket_frames(level)); let first = (window.start / span).max(held.start) / chunk; let end = window.end.div_ceil(span).min(held.end).div_ceil(chunk); - // Choose nearest spans before allocating: an absolute-index range may be enormous and sparse. + // Choose the nearest spans before allocating: an absolute-index range may be enormous and + // sparse. let count = end.saturating_sub(first); let middle = first + count.saturating_sub(1) / 2; wanted.extend((0..count.min(MAX_SPANS as u64)).map(|offset| { @@ -253,7 +258,7 @@ fn wanted( wanted } -/// Where a field's spans lie: the field's place in the manifest, in the spans' URL template. +/// Locates a field's spans: the spans' URL template and the field's index in the manifest. struct At<'a> { url: &'a str, field: usize, diff --git a/crates/latkit/src/tiles/mod.rs b/crates/latkit/src/tiles/mod.rs index 2c8fe6ec..3f38b1e9 100644 --- a/crates/latkit/src/tiles/mod.rs +++ b/crates/latkit/src/tiles/mod.rs @@ -1,9 +1,10 @@ -//! Tiles: data fetched in pieces, by key, as views ask for them. A key is covered by its parent up to -//! a root: a tile of the earth by the tile a zoom out, a span of a sampled field's history by the span -//! a level of detail up. A piece not fetched yet draws its nearest fetched ancestor. +//! Tiles: data fetched in pieces, by key, as views need them. Each key is covered by its parent, +//! up to a root: an earth tile by the tile one zoom out, and a span of a sampled field's history by +//! the span one level of detail up. Until a piece is fetched, its nearest fetched ancestor is drawn +//! as a placeholder. //! -//! A GPU fetches and decodes each source's pieces once for every view drawing them (`cache`, `tiling`); -//! each view draws what its camera asks for of them as a dataset of its own (`sets`). +//! A GPU fetches and decodes each source's pieces once for every view drawing them (`cache`, +//! `tiling`). Each view draws the pieces its camera needs as a dataset of its own (`sets`). mod cache; mod frames; @@ -20,23 +21,24 @@ pub(crate) use sets::{Asked, Reading, TileSets}; use crate::data::Name; -/// A dataset fetched in pieces as views ask for them: a [`Source`](crate::Source) a view draws as a -/// dataset of its own, holding the pieces its camera asks for. Every view of a GPU drawing tiles from -/// one URL shares what is fetched. What the source's first bytes hold says which tiles they are. +/// A dataset fetched in pieces as views need them: a [`Source`](crate::Source) that each view draws +/// as a dataset of its own, holding the pieces its camera needs. All views of a GPU drawing tiles +/// from one URL share what is fetched. The source's first bytes determine the kind of tiles. /// -/// **Time tiles**, a `frames.json` manifest as [`Snapshot::to_tiles`](crate::Snapshot::to_tiles) writes it: -/// a dataset whose sampled fields' histories are fetched in spans. A line tracing a field fetches the -/// level of detail its plot window draws at, a mark showing a field fetches the frames about the -/// view's coordinate, and a span still loading draws the coarser span above it. +/// **Time tiles**: a `frames.json` manifest as +/// [`Snapshot::to_tiles`](crate::Snapshot::to_tiles) writes it, for a dataset whose sampled fields' +/// histories are fetched in spans. A line tracing a field fetches the level of detail its plot +/// window draws at. A mark showing a field fetches the frames around the view's coordinate. A span +/// still loading draws the coarser span above it. /// -/// **Vector tiles**, a PMTiles archive, a TileJSON document, or a URL template with `{z}`, `{x}`, and -/// `{y}`, fetched as the camera asks for them. Each layer is a table of its -/// features of the kind marks draw it as: polygons for [`Areas`](crate::Areas), lines for -/// [`Lines`](crate::Lines), points for [`Vertices`](crate::Vertices), polygons first where marks of -/// several kinds draw one layer. A table named `layer:polygons`, `layer:lines`, or `layer:points` -/// holds that kind whatever draws it. Every feature's properties are fields, and its row's id is -/// stable, made from its tile and its place in it, so a tile fetched again finds its rows selected as -/// they were. +/// **Vector tiles**: a PMTiles archive, a TileJSON document, or a URL template with `{z}`, `{x}`, +/// and `{y}`, fetched as the camera needs them. Each layer becomes a table of its features of the +/// geometry type its marks draw: polygons for [`Areas`](crate::Areas), lines for +/// [`Lines`](crate::Lines), and points for [`Vertices`](crate::Vertices). When marks of several +/// kinds draw one layer, polygons come first. A table named `layer:polygons`, `layer:lines`, or +/// `layer:points` holds that geometry type regardless of its marks. Each feature's properties +/// become fields. Row ids are stable, derived from the tile and the feature's index in it, so a +/// refetched tile keeps its rows' selection. /// /// ``` /// # use latkit::Tiles; @@ -66,7 +68,7 @@ impl Tiles { } } -/// A key of a tiling: a piece of what a source holds, covered by its parent up to a root. +/// A tiling key: one piece of a source, covered by its parent up to a root. pub(crate) trait Key: Copy + Eq + Hash + Ord + Debug { /// The key covering this one, one level up; `None` at the root. fn parent(self) -> Option; @@ -83,9 +85,10 @@ impl Key for TileKey { } } -/// A span of a sampled field's history: at level 0, `chunk` frames from `index × chunk`; at level `k`, -/// `chunk` buckets of `8^k` frames from `index × chunk`, each row's least and greatest over each. Eight -/// spans make the one a level up, as four tiles make the tile a zoom out. +/// A span of a sampled field's history. At level 0 it is `chunk` frames starting at frame +/// `index × chunk`. At level `k` it is `chunk` buckets of `8^k` frames starting at bucket +/// `index × chunk`, each bucket holding every row's minimum and maximum over its frames. Eight +/// spans make the span one level up, as four tiles make the tile one zoom out. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] pub(crate) struct Span { pub(crate) level: u8, @@ -98,7 +101,7 @@ impl Key for Span { } } -/// A tile: its zoom, column, and row, rows counting from the north. +/// A web mercator tile: its zoom, column, and row, with rows counted from the north. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] pub(crate) struct TileKey { pub(crate) z: u8, @@ -115,7 +118,7 @@ impl TileKey { [x / n * 360.0 - 180.0, latitude((y + 1.0) / n), (x + 1.0) / n * 360.0 - 180.0, latitude(y / n)] } - /// The tile `levels` zooms up that holds this one. + /// The tile `levels` zooms out that contains this one. pub(crate) fn ancestor(self, levels: u8) -> TileKey { TileKey { z: self.z - levels, x: self.x >> levels, y: self.y >> levels } } diff --git a/crates/latkit/src/tiles/sets.rs b/crates/latkit/src/tiles/sets.rs index b627f766..4510451d 100644 --- a/crates/latkit/src/tiles/sets.rs +++ b/crates/latkit/src/tiles/sets.rs @@ -1,6 +1,6 @@ -//! What a view draws of the tiles it is given: for each source of tiles a set, whose dataset holds -//! the pieces the view's camera and plot window ask for, which marks draw like any other. The pieces -//! come from the GPU's cache, which every view drawing the same source shares. +//! Tile sets: for each tile source a view is given, a set whose dataset holds the pieces the view's +//! camera and plot window need. Marks draw it like any other dataset. The pieces come from the +//! GPU's cache, shared by every view drawing the same source. use std::sync::atomic::{AtomicU64, Ordering}; @@ -11,9 +11,9 @@ use crate::derive::sight::Sight; use crate::derive::{Input, Jobs}; use crate::{Error, Tiles}; -/// What a view reads of sampled fields: the plot window lines trace across and the pixel columns it -/// spans, the coordinate fields are shown at, and each field marks read, by dataset, table, and -/// name, with whether a line traces it. +/// What a view reads of sampled fields: the plot window lines trace across (`across`) and its width +/// in pixel columns, the coordinate fields are shown at (`at`), and each field marks read, by +/// dataset, table, and name, with whether a line traces it. #[derive(Default)] pub(crate) struct Reading { pub(crate) across: Option<[f64; 2]>, @@ -23,8 +23,8 @@ pub(crate) struct Reading { } impl Reading { - /// Whether a line traces field `field` of `table` in dataset `data`, and whether a mark shows it - /// at the coordinate. + /// Whether a line traces field `field` of `table` in dataset `data`, and whether a mark shows + /// it at the coordinate. pub(crate) fn reads(&self, data: &str, table: &str, field: &str) -> (bool, bool) { let mut reads = (false, false); for (d, t, f, traced) in &self.fields { @@ -40,23 +40,23 @@ impl Reading { } } -/// What the view asks of its tiles: the tiles the camera shows, on the earth, and what it reads of -/// sampled fields. +/// What the view needs of its tiles: what the camera shows of the earth (`sight`), and what it +/// reads of sampled fields. pub(crate) struct Asked<'a> { pub(crate) sight: Option<&'a Sight<'a>>, pub(crate) reading: &'a Reading, } -/// What a set draws, once its source opened. +/// What a set draws, once its source is open. enum Drawing { Frames(frames::Drawing), Vector(vector::Drawing), } -/// Sets made so far, which numbers each: the cache keeps what each draws apart. +/// Counter that numbers each set, so the cache can tell sets apart. static SETS: AtomicU64 = AtomicU64::new(0); -/// One source of tiles a view draws: its dataset, and what it draws of the source's pieces. +/// One tile source a view draws: its dataset, and what it draws of the source's pieces. struct TileSet { id: u64, tiles: Tiles, @@ -64,9 +64,10 @@ struct TileSet { drawing: Option, /// Whether the dataset changed since it was last taken. changed: bool, - /// Whether a failure was told: a source that went away tells it once. + /// Whether a failure was reported. A failed source reports it once. told: bool, - /// Whether a piece the view wanted at the last update drew an ancestor, or nothing, instead. + /// Whether a piece the view wanted at the last update showed a placeholder: an ancestor, or + /// nothing. standing_in: bool, } @@ -92,8 +93,8 @@ impl TileSet { } } - /// Brings the dataset up to what the view asks, from what the cache holds of the source; a - /// failure to tell, if any. + /// Updates the dataset to what the view needs, from what the cache holds of the source. Returns + /// a failure to report, if any. fn update(&mut self, input: &Input<'_>, asked: &Asked<'_>, cache: &mut Cache, jobs: &Jobs<'_>) -> Option { self.standing_in = false; let fetched = cache.open(&self.tiles.url, self.id, jobs); @@ -129,7 +130,7 @@ impl TileSet { } } -/// Every source of tiles a view was given. +/// Every tile source a view was given. #[derive(Default)] pub(crate) struct TileSets { sets: Vec, @@ -139,7 +140,7 @@ pub(crate) struct TileSets { } impl TileSets { - /// Draws `tiles`: tiles of the same name replace these, and what they drew goes. + /// Draws `tiles`, replacing any tiles of the same name and what they drew. pub(crate) fn set(&mut self, tiles: Tiles, cache: &mut Cache) { if self.sets.iter().any(|s| s.tiles == tiles) { return; @@ -148,8 +149,8 @@ impl TileSets { self.sets.push(TileSet::new(tiles)); } - /// Stops drawing the tiles drawn as dataset `name`, and what they changed that the view has yet to - /// take. + /// Stops drawing the tiles drawn as dataset `name`, and drops their changes the view has not + /// taken yet. pub(crate) fn remove(&mut self, name: &str, cache: &mut Cache) { self.changed.retain(|data| **data.name() != *name); self.sets.retain(|set| { @@ -161,7 +162,7 @@ impl TileSets { }); } - /// Stops drawing every source: the view draws no more. + /// Stops drawing every source, because the view no longer draws. pub(crate) fn release(&mut self, cache: &mut Cache) { self.changed.clear(); for set in self.sets.drain(..) { @@ -169,33 +170,32 @@ impl TileSets { } } - /// Whether every piece the view wants draws itself: none stands in by an ancestor, or by nothing - /// while its source opens, and the view took every dataset they changed. A piece that could not - /// be fetched draws nothing, and is final. + /// Whether every piece the view wants is drawn rather than a placeholder (an ancestor, or + /// nothing while its source opens), and the view took every changed dataset. A piece that could + /// not be fetched draws nothing, which counts as final. pub(crate) fn complete(&self) -> bool { self.changed.is_empty() && self.sets.iter().all(|s| !s.standing_in) } - /// Whether datasets the updates changed wait for the view to take them. + /// Whether changed datasets are waiting for the view to take them. pub(crate) fn pending(&self) -> bool { !self.changed.is_empty() } - /// The pieces every source draws, and those being fetched or decoded. + /// The number of pieces drawn across all sources, and the number being fetched or decoded. pub(crate) fn counts(&self, cache: &Cache) -> [u64; 2] { let drawn = self.sets.iter().map(|set| set.drawn() as u64).sum(); let loading = self.sets.iter().map(|set| cache.under_way(&set.tiles.url) as u64).sum(); [drawn, loading] } - /// Whether the view draws dataset `name` from tiles: pieces fetched as the view asks, drawn as - /// they are. + /// Whether the view draws dataset `name` from tiles, fetched as the view needs them. pub(crate) fn owns(&self, name: &str) -> bool { self.sets.iter().any(|s| *s.tiles.name == *name) } - /// The rows vector tiles brought of dataset `name`, each table with the fields its shapes are - /// written as too: the view takes them as the frame begins, so row for row what it draws. + /// The rows vector tiles supplied for dataset `name`, including their derived shape fields. The + /// view takes them as the frame begins, so they match what it draws row for row. pub(crate) fn shaped(&self, name: &str) -> Option<&Snapshot> { let set = self.sets.iter().find(|s| *s.tiles.name == *name)?; match &set.drawing { @@ -209,12 +209,12 @@ impl TileSets { std::mem::take(&mut self.changed) } - /// Failures since last asked. + /// Failures since the last call. pub(crate) fn take_errors(&mut self) -> Vec { std::mem::take(&mut self.errors) } - /// Brings every set up to what the view asks and the scene draws, from what `cache` holds. + /// Updates every set to what the view needs and the scene draws, from what `cache` holds. pub(crate) fn update(&mut self, input: &Input<'_>, asked: &Asked<'_>, cache: &mut Cache, jobs: &Jobs<'_>) { for set in &mut self.sets { if let Some(error) = set.update(input, asked, cache, jobs) diff --git a/crates/latkit/src/tiles/tiling.rs b/crates/latkit/src/tiles/tiling.rs index a266bf15..f713b85e 100644 --- a/crates/latkit/src/tiles/tiling.rs +++ b/crates/latkit/src/tiles/tiling.rs @@ -1,9 +1,10 @@ -//! Tilings: what a source holds, fetched in pieces by key as the sets drawing it ask for them, decoded -//! into patches, and held under a budget of bytes, the least recently drawn going first. Every set -//! drawing one source shares its tiling, so what one fetched another draws; each decodes for a stamp -//! of what its decoding reads. A key wanted draws once it is decoded for that stamp; until then its -//! nearest decoded ancestor stands in, and what is missing is fetched, nearest first. Tiles of the -//! earth and spans of a sampled field's history share it. +//! Tilings: a source's pieces, fetched by key as the sets drawing it need them, decoded into +//! patches, and kept within a byte budget, evicting the least recently drawn first. Every set +//! drawing one source shares its tiling, so a piece one set fetched draws in another. Each set +//! decodes for a stamp that identifies what its decoding reads. A wanted key draws once it is +//! decoded for that stamp; until then its nearest decoded ancestor is drawn as a placeholder, and +//! missing pieces are fetched, nearest first. Earth tiles and spans of a sampled field's history +//! both use it. use std::sync::Arc; @@ -15,66 +16,69 @@ use crate::derive::Jobs; use crate::host::Request; use crate::{Error, Pending}; -/// Fetches, and decodes, a tiling has under way at once. +/// Most fetches, and separately most decodes, a tiling runs at once. const REQUESTS: usize = 8; -/// Bytes of fetched pieces a tiling keeps, past which the least recently drawn go. +/// Bytes of fetched pieces a tiling keeps. Past this, the least recently drawn are evicted. const KEPT_BYTES: usize = 64 << 20; /// How a source fetches a piece. pub(crate) enum Requested { - /// The bytes `request` answers. + /// Fetch the bytes `request` returns. Fetch(Request), - /// Not yet: the source first fetches what finds the piece, as an archive its directories. + /// Not yet: the source must first fetch what locates the piece, such as an archive's + /// directories. Wait, - /// Nowhere: the source holds no such piece, which draws nothing. + /// The source has no such piece, which draws nothing. Missing, } -/// What a piece's bytes become, the patch it draws: at once, or the answer of a job. +/// How a piece's bytes become its patch: immediately, or as a job's result. pub(crate) enum Decoding { Now(Patch), Job(Pending>), } -/// Where a tiling's pieces come from, and what they become. +/// Fetches and decodes a tiling's pieces. pub(crate) trait Pieces { /// How to fetch `key`. fn request(&mut self, key: K, jobs: &Jobs<'_>) -> Requested; - /// What `key`'s fetched `bytes`, never empty, decode into. + /// Decodes `key`'s fetched `bytes`, which are never empty. fn decode(&self, key: K, bytes: &Arc>, jobs: &Jobs<'_>) -> Decoding; } -/// What an update draws: each wanted key decoded, else its nearest decoded ancestor, each once; and -/// whether any key stands in, by an ancestor or by nothing. +/// The result of an update: each wanted key if decoded, else its nearest decoded ancestor, each +/// listed once; and whether any wanted key shows a placeholder (an ancestor, or nothing). pub(crate) struct Drawn { pub(crate) keys: Vec, pub(crate) standing_in: bool, - /// A failure to tell: a piece that could not be fetched. + /// A failure to report: a piece that could not be fetched. pub(crate) failure: Option, } -/// What one set draws of a tiling: the stamp it decodes for, and the keys whose decodes it keeps, -/// those it draws, wants, and the ancestors of those it wants, which stand in for them. +/// One set's claim on a tiling: the stamp it decodes for, and the keys whose decodes it keeps. +/// Those are the keys it draws, the keys it wants, and their ancestors, which serve as +/// placeholders. struct Claim { stamp: u64, keys: FxHashSet, } -/// A source's pieces: their fetched bytes, and the patches they decoded into for each stamp of what -/// decoding reads. +/// A source's pieces: their fetched bytes, and the patches decoded from them for each decoding +/// stamp. pub(crate) struct Tiling { - /// Fetched bytes, as fetched, and when each last drew; empty where the source holds no piece. + /// Fetched bytes and the tick each piece last drew at. Empty bytes mean the source has no such + /// piece or the fetch failed. held: FxHashMap>, u64)>, fetching: FxHashMap>>, - /// Decodes under way and done, by key and the stamp each was asked for. + /// Decodes in progress (`decoding`) and finished (`decoded`), by key and stamp. decoding: FxHashMap<(K, u64), Pending>>, decoded: FxHashMap<(K, u64), Patch>, - /// What each set drawing the tiling draws of it, by set. + /// Each drawing set's claim, by set. claims: FxHashMap>, - /// The first piece that could not be fetched: every set drawing the tiling tells it once. + /// The first fetch failure. Every set drawing the tiling reports it once. failure: Option, - /// Counts updates, for drawn pieces to note when they last drew. + /// Update counter, recording when each piece last drew. tick: u64, } @@ -93,8 +97,8 @@ impl Default for Tiling { } impl Tiling { - /// Brings the pieces up to what set `set` wants, nearest first, decoding for `stamp`: what it - /// draws. + /// Updates the pieces for what set `set` wants, nearest first, decoding for `stamp`. Returns + /// what the set draws. pub(crate) fn update( &mut self, set: u64, @@ -105,8 +109,8 @@ impl Tiling { ) -> Drawn { self.tick += 1; self.land(); - // Keys not decoded yet decode once held, and so do the held ancestors of those still - // loading, to stand in for them; the rest fetch. A piece decoded at once draws at once. + // Decode wanted keys once fetched, and the fetched ancestors of keys still loading, as + // placeholders; fetch the rest. A piece decoded immediately draws immediately. let mut decode: Vec = Vec::new(); for &key in wanted { if self.ready(key, stamp) { @@ -143,8 +147,9 @@ impl Tiling { *used = tick; } } - // The set keeps what it draws and wants decoded, and every ancestor of what it wants, which - // stands in as it zooms; the walk up stops where another wanted key's walk passed. + // The set keeps the decodes of what it draws and wants, plus every ancestor of what it + // wants, which serves as a placeholder while zooming. Each walk up stops where an earlier + // wanted key's walk passed. let mut keys: FxHashSet = drawn.iter().copied().collect(); let mut walked: FxHashSet = FxHashSet::default(); for &key in wanted { @@ -160,22 +165,22 @@ impl Tiling { Drawn { keys: drawn, standing_in, failure: self.failure.clone() } } - /// The patch `key` decoded into for `stamp`, once it did. + /// The patch decoded from `key` for `stamp`, once ready. pub(crate) fn decoded(&self, key: &K, stamp: u64) -> Option<&Patch> { self.decoded.get(&(*key, stamp)) } - /// Fetches and decodes under way. + /// Fetches and decodes in progress. pub(crate) fn under_way(&self) -> usize { self.fetching.len() + self.decoding.len() } - /// Keeps `error` as the tiling's failure, unless it kept one already. + /// Records `error` as the tiling's failure, unless one is recorded already. pub(crate) fn fail(&mut self, error: Error) { self.failure.get_or_insert(error); } - /// Lets go of what set `set` drew: pieces no other set draws may go. + /// Releases set `set`'s claim, so pieces no other set draws may be evicted. pub(crate) fn release(&mut self, set: u64) { if self.claims.remove(&set).is_some() { self.evict(); @@ -187,7 +192,8 @@ impl Tiling { self.decoded.contains_key(&(key, stamp)) } - /// Takes fetches and decodes that landed, keeping the first failure. + /// Collects finished fetches and decodes, keeping the first failure. A decode is kept only if + /// some set still decodes for its stamp. fn land(&mut self) { let fetched: Vec<_> = self.fetching.iter().filter_map(|(key, p)| p.take().map(|r| (*key, r))).collect(); for (key, result) in fetched { @@ -208,7 +214,7 @@ impl Tiling { } } - /// Starts fetching `key`, within the requests under way at once. + /// Starts fetching `key`, unless [`REQUESTS`] fetches are already in progress. fn fetch(&mut self, key: K, pieces: &mut impl Pieces, jobs: &Jobs<'_>) { if self.fetching.contains_key(&key) || self.fetching.len() >= REQUESTS { return; @@ -224,7 +230,8 @@ impl Tiling { } } - /// Starts decoding `key` for `stamp`, within the decodes under way at once. + /// Starts decoding `key` for `stamp`, unless [`REQUESTS`] decodes are already in progress. + /// Empty bytes decode to an empty patch immediately. fn decode(&mut self, key: K, stamp: u64, pieces: &impl Pieces, jobs: &Jobs<'_>) { if self.decoding.contains_key(&(key, stamp)) || self.decoding.len() >= REQUESTS { return; @@ -244,8 +251,8 @@ impl Tiling { } } - /// Lets go of the least recently drawn pieces' bytes no set draws or wants, past the budget, and - /// of decodes no set keeps. + /// While over budget, evicts the bytes of the least recently drawn pieces no set claims. Drops + /// decodes no set keeps. fn evict(&mut self) { let claims = &self.claims; let claimed = |key: &K| claims.values().any(|claim| claim.keys.contains(key)); diff --git a/crates/latkit/src/tiles/vector.rs b/crates/latkit/src/tiles/vector.rs index c02022c5..22c32a7a 100644 --- a/crates/latkit/src/tiles/vector.rs +++ b/crates/latkit/src/tiles/vector.rs @@ -1,10 +1,10 @@ -//! Vector tiles: the tiles the camera asks for, fetched from a PMTiles archive or a URL template, -//! given or from a TileJSON document, decoded in jobs, and patched into the dataset a set draws. The -//! camera chooses the tiles that draw no larger than their own size where they lie nearest its -//! focus, nearest first. Tiles decode only the layers a set's marks draw, and again for a set whose -//! scene draws others; the job decoding a tile shapes its rows too, as their marks draw them, so a -//! tile lands with its triangles and paths. A set holds the rows with their shapes, and draws the -//! dataset of the rows as tiles wrote them. +//! Vector tiles: the tiles the camera needs, fetched from a PMTiles archive or a URL template +//! (given directly or read from a TileJSON document), decoded in jobs, and patched into the dataset +//! a set draws. The camera picks tiles that draw no larger than their own pixel size near its +//! focus, nearest first. Tiles decode only the layers a set's marks draw, and decode again for a +//! set whose scene draws others. The job decoding a tile also computes its rows' shapes for their +//! marks, so a tile arrives with its triangles and paths. A set holds the rows with their shapes, +//! and draws a dataset of the rows as the tiles wrote them. use std::sync::Arc; @@ -22,11 +22,11 @@ use crate::geo::tiles::{Decoded, Template, shapes_field}; use crate::host::{Job, Request}; use crate::{Error, Kind, Mark, Pending, Scene}; -/// Tiles drawn at once at most: past it, tiles stop splitting. +/// Most tiles drawn at once. Past this, tiles stop splitting. const MAX_TILES: usize = 256; -/// The tiles to draw within `zooms` and `bounds`: those meeting what the camera shows, each split -/// while it draws larger than its size, nearest the view's middle first. +/// The tiles to draw within `zooms` and `bounds`: those intersecting the camera's view, each split +/// while it draws larger than its pixel size, nearest the camera's focus first. fn wanted(sight: &Sight<'_>, zooms: [u8; 2], bounds: [f64; 4]) -> Vec { let limit = TILE_PX * 1.25; let region = sight.region(); @@ -54,18 +54,18 @@ fn wanted(sight: &Sight<'_>, zooms: [u8; 2], bounds: [f64; 4]) -> Vec { out.into_iter().map(|(key, _)| key).collect() } -/// Where vector tiles come from once their source opens. +/// Where a source's vector tiles come from once it opens. enum Opened { - /// An archive whose root directory lies past the first bytes. + /// An archive whose root directory lies past the first bytes and is being fetched. Root(Header, Pending>), Template(Template), Archive(Header, Directory), - /// The source could not be opened, and why. + /// The source could not be opened, with the reason. Failed(Error), } -/// Vector tiles of one source, as every set drawing them shares them: where they lie, and what was -/// fetched and decoded of them. +/// One source's vector tiles, shared by every set drawing them: where they are, and what was +/// fetched and decoded. pub(crate) struct Fetched { url: Arc, opened: Opened, @@ -76,20 +76,21 @@ pub(crate) struct Fetched { } impl Fetched { - /// The tiles a URL template names, `{z}`, `{x}`, and `{y}` in it. + /// Tiles from URL template `url`, which contains `{z}`, `{x}`, and `{y}`. pub(crate) fn template(url: &Arc) -> Fetched { Fetched::opened(url, Opened::Template(Template::of(url))) } - /// The tiles whose source's first `bytes` are a PMTiles archive's or a TileJSON document's; the - /// archive's root directory is fetched when it lies past them. + /// Opens tiles from a source whose first `bytes` are a PMTiles archive or a TileJSON document. + /// An archive's root directory is fetched if it lies past those bytes. Errors if the source + /// holds image tiles or is malformed. pub(crate) fn open(url: &Arc, bytes: &[u8], jobs: &Jobs<'_>) -> Result { if !pmtiles::is_archive(bytes) { return Template::tilejson(bytes, url).map(|template| Fetched::opened(url, Opened::Template(template))); } let header = pmtiles::header(bytes)?; if !header.vector { - return Err(format!("{url} holds image tiles; only vector tiles draw")); + return Err(format!("{url} has image tiles; only vector tiles are supported")); } let (offset, length) = header.root; let opened = match bytes.get(offset as usize..(offset + length) as usize) { @@ -109,12 +110,12 @@ impl Fetched { } } - /// Fetches and decodes under way. + /// Fetches and decodes in progress. pub(crate) fn under_way(&self) -> usize { self.tiling.under_way() } - /// Lets go of what set `set` drew. + /// Releases what set `set` drew. pub(crate) fn release(&mut self, set: u64) { self.tiling.release(set); } @@ -128,8 +129,8 @@ impl Fetched { } } - /// Opens the archive once its root directory landed, and takes leaf directories that landed; - /// why the source could not be opened, if it could not. + /// Opens the archive once its root directory arrives, and takes leaf directories that arrived. + /// Returns the reason the source could not be opened, if it could not. fn settle(&mut self) -> Option { if let Opened::Root(header, pending) = &self.opened { let header = *header; @@ -159,8 +160,8 @@ impl Fetched { } } -/// What one set draws of vector tiles: the tiles whose rows its dataset holds, with the rows of each -/// table, and those rows with the fields their shapes are written as too. +/// What one set draws of vector tiles: the tiles whose rows its dataset holds, with each tile's row +/// ids by table, and `held`, the same rows including their derived shape fields. pub(crate) struct Drawing { drawn: FxHashMap)>>, held: Snapshot, @@ -171,7 +172,7 @@ impl Drawing { Drawing { drawn: FxHashMap::default(), held: Snapshot::new("held") } } - /// The rows the dataset holds, each table with the fields its shapes are written as too. + /// The rows the dataset holds, including their derived shape fields. pub(crate) fn held(&self) -> &Snapshot { &self.held } @@ -181,9 +182,10 @@ impl Drawing { self.drawn.len() } - /// Brings `data` up to what `sight` shows and set `set`'s scene draws of dataset `name`, from - /// what `fetched` holds: whether it changed, whether a tile stands in, and a failure to tell. - #[expect(clippy::too_many_arguments, reason = "what a set draws, of what the source holds")] + /// Updates `data` to what `sight` shows and set `set`'s scene draws of dataset `name`, from + /// what `fetched` holds. Returns whether it changed, whether any wanted tile still shows a + /// placeholder, and a failure to report. + #[expect(clippy::too_many_arguments, reason = "a set's update reads the view, the frame, and the source")] pub(crate) fn update( &mut self, set: u64, @@ -211,16 +213,16 @@ impl Drawing { (changed, drawn.standing_in, failure.or(drawn.failure)) } - /// Makes `data` hold the rows of `drawn` and no other tile's, as the tiles wrote them, and the - /// rows held those with their shapes; whether it changed. + /// Makes `data` hold exactly the rows of tiles `drawn`, as the tiles wrote them, and `held` the + /// same rows with their shapes. Returns whether it changed. fn draw(&mut self, drawn: &[TileKey], stamp: u64, tiling: &Tiling, data: &mut Snapshot) -> bool { let keep: FxHashSet = drawn.iter().copied().collect(); let mut patch = Patch::new(); - // Tiles no longer drawn go, and so do those decoded for other tables, which come back - // decoded again. + // Remove tiles no longer drawn, and tiles decoded for other tables, which return decoded + // again. let gone: Vec = self.drawn.keys().filter(|k| !keep.contains(k) || tiling.decoded(k, stamp).is_none()).copied().collect(); - // One removal a table, which moves the rows after the first gone once. + // One removal per table, so rows after the first removed row move only once. let mut removed: Keyed> = Keyed::default(); for key in &gone { for (table, ids) in self.drawn.remove(key).unwrap_or_default() { @@ -250,7 +252,8 @@ impl Drawing { return false; } let Ok(held) = self.held.applied(patch) else { return false }; - // The dataset drawn holds each table held without its shapes, sharing all else. + // The drawn dataset holds each table of `held` without its shape fields, sharing + // everything else. let tables = held.tables().map(|table| { let mut table = table.clone(); table.fields.retain(|(name, _)| !shapes_field(name)); @@ -262,7 +265,7 @@ impl Drawing { } } -/// Where a source's tiles lie, and the tables a set's marks draw of them, which decoding writes. +/// Locates a source's tiles, with the tables a set's marks draw from them, which decoding writes. struct Find<'a> { url: &'a Arc, opened: &'a Opened, @@ -298,7 +301,7 @@ impl Pieces for Find<'_> { None => break, } } - // The archive has no such tile: it draws nothing. + // The archive has no such tile, so it draws nothing. Requested::Missing } Opened::Root(..) | Opened::Failed(_) => Requested::Wait, @@ -310,7 +313,7 @@ impl Pieces for Find<'_> { } } -/// The archive at `header` with root directory `bytes`, or `None` where it is corrupt. +/// The archive with `header` and root directory `bytes`, or `None` if the directory is corrupt. fn archive(header: Header, bytes: &[u8]) -> Option { let root = inflate(bytes, header.gzip_directories).and_then(|b| Directory::decode(&b))?; Some(Opened::Archive(header, root)) @@ -320,9 +323,9 @@ fn corrupt(url: &str) -> String { format!("the archive at {url} has a corrupt directory") } -/// The tables marks draw from dataset `name`: a table named after a layer holds the kind of feature -/// its marks draw, polygons first, then lines, then points; one named `layer:kind` holds that kind. -/// Each is shaped as the marks drawing its geometry shape it. +/// The tables marks draw from dataset `name`. A table named after a layer holds the geometry type +/// its marks draw, preferring polygons, then lines, then points. A table named `layer:kind` holds +/// that kind. Each table gets the shapes its marks derive from its geometry. fn decoded_tables(scene: &Scene, name: &Name) -> Vec { let mut tables: Vec = Vec::new(); for mark in &scene.marks { @@ -364,8 +367,8 @@ fn decoded_tables(scene: &Scene, name: &Name) -> Vec { tables } -/// `table`'s fields made to hold what `data`'s table of its name holds where they differ: a field a -/// tile reads as numbers but the dataset holds as texts is written as text, and so on. +/// Converts `table`'s fields to the kinds of the same-named table in `data` where they differ. For +/// example, a field a tile reads as numbers but the dataset holds as text is written as text. fn harmonized(table: &Table, data: &Snapshot) -> Table { let Some(existing) = data.table(&table.name) else { return table.clone() }; let mut out = table.clone(); @@ -408,7 +411,7 @@ fn number_of(value: &crate::Value) -> Option { } } -/// `bytes` inflated when `gzip`. +/// Decompresses `bytes` when `gzip`, else copies them. `None` if decompression fails. fn inflate(bytes: &[u8], gzip: bool) -> Option> { if !gzip { return Some(bytes.to_vec()); diff --git a/crates/latkit/src/time.rs b/crates/latkit/src/time.rs index fb8c0fa0..b3f8f227 100644 --- a/crates/latkit/src/time.rs +++ b/crates/latkit/src/time.rs @@ -1,6 +1,6 @@ -//! Bounded waits: work the views wait on wakes whatever awaits it, and a timer wakes it anyway once a -//! wait has gone on long enough, so no wait outlasts its deadline. Natively one thread keeps every -//! timer; in a browser the page's own timers do. +//! Bounded waits. Views' work wakes the task waiting on it, and a timer also wakes the task at its +//! deadline, so no wait outlasts its deadline. Native builds run every timer on one thread. In a +//! browser, the global scope's `setTimeout` runs them. use std::future::Future; use std::pin::Pin; @@ -16,8 +16,8 @@ pub(crate) fn wake_after(after: Duration, waker: Waker) { platform::wake_after(after, waker); } -/// What wakes a task waiting on views: their work landing, their readbacks mapping. Waking it marks -/// it woken until a wait takes that. +/// Wakes a task waiting on views when their work arrives or their readbacks map. A wake sets a flag +/// that the next wait clears. #[derive(Default)] pub(crate) struct Notify { woken: AtomicBool, @@ -55,7 +55,7 @@ impl Future for Wait { type Output = (); fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> { - // The waker goes in before the flag is read, so a wake between the two is never lost. + // Store the waker before reading the flag, so a wake in between is never lost. *self.notify.waker.lock().unwrap_or_else(PoisonError::into_inner) = Some(context.waker().clone()); if self.notify.woken.swap(false, Ordering::AcqRel) { return Poll::Ready(()); @@ -79,7 +79,7 @@ mod platform { use std::task::Waker; use std::time::{Duration, Instant}; - /// The one timer thread: it wakes each waker given it at its time. + /// The channel to the single timer thread, which wakes each waker at its due time. fn timers() -> &'static Mutex> { static TIMERS: OnceLock>> = OnceLock::new(); TIMERS.get_or_init(|| { @@ -130,7 +130,7 @@ mod platform { use wasm_bindgen::JsCast; use wasm_bindgen::closure::Closure; - /// Wakes `waker` from the global scope's `setTimeout`, in a page or a worker alike. + /// Wakes `waker` with the global scope's `setTimeout`, which works in a page or a worker. pub(super) fn wake_after(after: Duration, waker: Waker) { let wake = Closure::once_into_js(move || waker.wake()); let global = js_sys::global(); @@ -149,7 +149,7 @@ mod platform { use std::task::Waker; use std::time::Duration; - /// Without the browser's timers a wait is woken at once, and is polled again. + /// Without browser timers, wakes `waker` immediately so the wait is polled again. pub(super) fn wake_after(_: Duration, waker: Waker) { waker.wake(); } diff --git a/crates/latkit/src/url.rs b/crates/latkit/src/url.rs index 21fa4882..64b97119 100644 --- a/crates/latkit/src/url.rs +++ b/crates/latkit/src/url.rs @@ -1,9 +1,11 @@ -//! Where what a document names lies: a URL or a path relative to the document's own. +//! Resolving the URLs and paths a document references, relative to the document's own. -/// `url` resolved against `base`, the URL or path of the document naming it: as it is when absolute -/// (a scheme, a `data:` URL, a drive or a root of a path), from `base`'s origin when it starts with -/// `/` and `base` is a URL, else in `base`'s directory. Against a URL it resolves as RFC 3986 says, -/// its host as written and a template's braces kept. +/// Resolves `url` against `base`, the URL or path of the document that references it. +/// +/// An absolute `url` is returned unchanged: one with a scheme (including `data:`), a drive, or a +/// root path. A `url` starting with `/` resolves from `base`'s origin when `base` is a URL. Any +/// other `url` resolves in `base`'s directory. Against a URL, resolution follows RFC 3986, except +/// that the host stays as written and a template's braces are not escaped. pub(crate) fn resolve(base: &str, url: &str) -> String { let drive = |s: &str| s.as_bytes().get(1) == Some(&b':') && s.as_bytes()[0].is_ascii_alphabetic(); if drive(url) || url.starts_with('\\') || scheme(url).is_some() { @@ -38,9 +40,9 @@ pub(crate) fn resolve(base: &str, url: &str) -> String { format!("{origin}{}", escaped(&without_dots(&merged))) } -/// The path a `file:` URL names, its escapes decoded, as URLs read it: backslashes are slashes, and a -/// drive where the host goes starts the path. `None` where it names another host's file or is no -/// file URL. +/// The local path a `file:` URL names, with percent escapes decoded. Backslashes count as slashes, +/// and a drive in the host position starts the path. `None` if `url` is not a `file:` URL, names +/// another host, or has an invalid escape. #[cfg(all(feature = "native", not(target_arch = "wasm32")))] pub(crate) fn file_path(url: &str) -> Option { let rest = url.strip_prefix("file:")?.replace('\\', "/"); @@ -70,7 +72,7 @@ pub(crate) fn file_path(url: &str) -> Option { } } let path = String::from_utf8(bytes).ok()?; - // A drive's path: `/C:/data` is `C:/data`. + // Drop the slash before a drive: `/C:/data` becomes `C:/data`. let drive = path.as_bytes(); if drive.len() >= 3 && drive[0] == b'/' && drive[1].is_ascii_alphabetic() && drive[2] == b':' { return Some(path[1..].into()); @@ -78,8 +80,8 @@ pub(crate) fn file_path(url: &str) -> Option { Some(path.into()) } -/// `url`'s scheme: two or more letters, digits, `+`, `-`, or `.` from a letter up to a `:`, so a -/// drive has none. +/// `url`'s scheme: the text before the first `:`, if it is two or more letters, digits, `+`, `-`, +/// or `.` starting with a letter. A one-letter drive is not a scheme. fn scheme(url: &str) -> Option<&str> { let scheme = &url[..url.find(':')?]; (scheme.len() > 1 @@ -88,7 +90,7 @@ fn scheme(url: &str) -> Option<&str> { .then_some(scheme) } -/// `path` without its `.` and `..` segments, as RFC 3986 removes them; its query and fragment kept. +/// Removes `.` and `..` segments from `path` per RFC 3986, keeping its query and fragment. fn without_dots(path: &str) -> String { let (path, rest) = path.split_at(path.find(['?', '#']).unwrap_or(path.len())); let segments: Vec<&str> = path.split('/').collect(); @@ -103,7 +105,7 @@ fn without_dots(path: &str) -> String { } segment => kept.push(segment), } - // A last `.` or `..` names a directory. + // A trailing `.` or `..` names a directory, so the result ends with a slash. if i + 1 == segments.len() && matches!(segment, "." | "..") { kept.push(""); } @@ -111,8 +113,8 @@ fn without_dots(path: &str) -> String { format!("{}{rest}", kept.join("/")) } -/// `text` with what a URL cannot hold as written percent-encoded: spaces, quotes, angle brackets, -/// backticks, controls, and every byte past ASCII. A template's braces stay. +/// Percent-encodes the bytes of `text` a URL cannot contain literally: spaces, double quotes, angle +/// brackets, backticks, control characters, and non-ASCII bytes. Template braces are kept. fn escaped(text: &str) -> String { const HEX: &[u8; 16] = b"0123456789ABCDEF"; let mut out = String::with_capacity(text.len()); @@ -131,7 +133,7 @@ mod tests { use super::resolve; #[test] - fn names_resolve_against_their_document() { + fn relative_urls_resolve_against_their_document() { assert_eq!(resolve("https://a.b/c/d.json", "t/{z}.pbf"), "https://a.b/c/t/{z}.pbf"); assert_eq!(resolve("https://a.b/c/d.json", "t/%7Bz%7D/{z}.pbf"), "https://a.b/c/t/%7Bz%7D/{z}.pbf"); assert_eq!(resolve("https://a.b/c/d.json", "/t.pbf"), "https://a.b/t.pbf"); @@ -151,7 +153,7 @@ mod tests { #[cfg(all(feature = "native", not(target_arch = "wasm32")))] #[test] - fn file_urls_name_their_paths() { + fn file_urls_convert_to_paths() { use super::file_path; use std::path::PathBuf; assert_eq!(file_path("file:///C:/data/a%20b.f32"), Some(PathBuf::from("C:/data/a b.f32"))); diff --git a/crates/latkit/src/video/mod.rs b/crates/latkit/src/video/mod.rs index b058977c..06966a32 100644 --- a/crates/latkit/src/video/mod.rs +++ b/crates/latkit/src/video/mod.rs @@ -1,10 +1,11 @@ -//! Videos: a shot's pictures over time, handed in order to a [`Sink`] while they are drawn. Three -//! stages overlap, so throughput is the slowest of them rather than their sum: the GPU draws and -//! composes one frame while another is read back and the sink takes a third. A frame where nothing -//! moved is neither drawn nor read back: the sink is told it is held, and shows the frame before. +//! Videos: a shot's pictures over time, passed in order to a [`Sink`] as they are drawn. Three +//! stages overlap, so throughput is set by the slowest stage rather than their sum: the GPU draws +//! and composes one frame while another is read back and the sink takes a third. A frame where +//! nothing moved is neither drawn nor read back; the sink is told it is held and shows the previous +//! frame again. //! -//! [`Mp4`] is the sink most videos take: H.264 from the host's -//! [`Encoder`](crate::host::Encoder), written as fragmented MP4. +//! [`Mp4`] is the usual sink: H.264 from the host's [`Encoder`](crate::host::Encoder), written as +//! fragmented MP4. mod mp4; @@ -21,57 +22,59 @@ use crate::host::{Encoder, Host, TIMESCALE, Unit}; use crate::shot::{Borrowed, Drawing, Step, Views}; use crate::{Error, Gpu, Result, Shot, View}; -/// Frames drawn and not yet taken at once: one drawing, one reading back, one in the sink. +/// Most frames in flight at once: one drawing, one reading back, one in the sink. const IN_FLIGHT: usize = 3; -/// Seconds of video between keyframes at most. +/// Most seconds of video between keyframes. const KEYFRAMES: f64 = 2.0; -/// Bits a pixel of a frame takes by default: sharp lines and text stay sharp. +/// Default bits per pixel per frame, enough to keep lines and text sharp. const BITS_PER_PIXEL: f64 = 0.15; -/// The bytes a picture is composed into. +/// The pixel format a picture is composed into. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Pixels { - /// RGBA rows, top to bottom, four bytes a pixel with straight alpha: an [`Image`](crate::Image)'s. + /// RGBA rows, top to bottom, four bytes per pixel with straight alpha, as in an + /// [`Image`](crate::Image). Rgba, - /// NV12: a plane of luma, then one of Cb and Cr interleaved at half the resolution, BT.709 in - /// limited range. The picture's width is a multiple of 4 and its height of 2. + /// NV12: a luma plane, then a plane of interleaved Cb and Cr at half resolution, BT.709 limited + /// range. The picture's width must be a multiple of 4 and its height a multiple of 2. Nv12, } -/// What a video's frames go to, in order: an [`Mp4`] file, or whatever an application makes of the +/// Where a video's frames go, in order: an [`Mp4`] file, or the application's own handling of the /// pixels. pub trait Sink { - /// The bytes it takes. + /// The pixel format it takes. const PIXELS: Pixels; - /// Readies it for the video `encoding` describes, before the first frame, with the services of - /// the GPU's `host`: an MP4 asks it for an encoder. + /// Prepares the sink for the video `encoding` describes, before the first frame, using the + /// GPU's `host` services; an MP4 gets its encoder from the host. Does nothing by default. fn begin(&mut self, encoding: &Encoding, host: Option<&dyn Host>) -> Result<()> { let _ = (encoding, host); Ok(()) } - /// Takes frame `index`: its pixels, or `None` where nothing moved since the frame before, which it - /// shows again. Resolves once there is room for another. + /// Takes frame `index`: its pixels, or `None` if nothing moved since the previous frame, which + /// then shows again. Resolves once the sink can take another frame. fn frame(&mut self, index: u64, pixels: Option>) -> impl Future>; - /// Ends the video before frame `end`, every frame before it taken. + /// Ends the video at the time of frame `end`, once every earlier frame was taken. fn finish(&mut self, end: u64) -> impl Future>; } -/// A video: its pictures' size in pixels, frames a second, and the bits a second an encoder spends. +/// A video's encoding: picture size in pixels, frames per second, and the encoder's bitrate. #[derive(Clone, Copy, PartialEq, Debug)] pub struct Encoding { /// Width and height in pixels. pub size: [u32; 2], - /// Frames a second. + /// Frames per second. pub fps: f64, - /// Bits a second. + /// Bits per second. pub bitrate: u32, } impl Encoding { - /// A video of `size` at `fps`, at about 0.15 bits a pixel a frame. + /// A video of `size` at `fps`, at about 0.15 bits per pixel per frame, clamped to between + /// 100 kbit/s and 400 Mbit/s. pub fn new(size: [u32; 2], fps: f64) -> Encoding { let bits = f64::from(size[0]) * f64::from(size[1]) * fps * BITS_PER_PIXEL; Encoding { size, fps, bitrate: bits.clamp(1e5, 4e8) as u32 } @@ -89,8 +92,9 @@ impl Encoding { } } -/// A [`Sink`] encoding H.264 with the host's encoder and writing it to `out` as fragmented MP4 while -/// frames are drawn. A frame lasts until the next one shows, so held frames cost nothing. +/// A [`Sink`] that encodes H.264 with the host's encoder and writes it to `out` as fragmented MP4 +/// while frames are drawn. A frame lasts until the next one shows, so held frames are not encoded +/// and add nothing to the file. /// /// ```no_run /// # async fn example(gpu: &latkit::Gpu, map: &mut latkit::View, plot: &mut latkit::View) -> latkit::Result<()> { @@ -110,26 +114,26 @@ impl Encoding { pub struct Mp4 { out: W, bitrate: Option, - /// The encoder, the muxer, and the frame rate, once the video began. + /// The encoder, muxer, and frame rate, once the video has begun. began: Option<(Box, Muxer, f64)>, - /// When the last keyframe shows, in ticks. + /// The time of the last keyframe, in ticks. keyed: Option, } impl Mp4 { - /// An MP4 written to `out`, at about 0.15 bits a pixel a frame. + /// An MP4 written to `out`, at about 0.15 bits per pixel per frame. pub fn new(out: W) -> Mp4 { Mp4 { out, bitrate: None, began: None, keyed: None } } - /// Encodes at `bitrate` bits a second. + /// Encodes at `bitrate` bits per second. A bitrate of 0 is an error when the video begins. #[must_use] pub fn bitrate(mut self, bitrate: u32) -> Mp4 { self.bitrate = Some(bitrate); self } - /// What it was written to. + /// The writer the MP4 was written to. pub fn into_inner(self) -> W { self.out } @@ -151,18 +155,18 @@ impl Sink for Mp4 { let mut encoding = *encoding; if let Some(bitrate) = self.bitrate { if bitrate == 0 { - return Err(Error::invalid("an MP4 takes a positive bitrate").at("bitrate")); + return Err(Error::invalid("must be positive").at("bitrate")); } encoding.bitrate = bitrate; } - let host = host.ok_or_else(|| Error::Unsupported("an MP4 is encoded by the Gpu's host; it has none".into()))?; + let host = host.ok_or_else(|| Error::Unsupported("encoding an MP4 needs a host; the Gpu has none".into()))?; self.began = Some((host.encoder(&encoding)?, Muxer::new(encoding.size, TIMESCALE), encoding.fps)); Ok(()) } async fn frame(&mut self, index: u64, pixels: Option>) -> Result<()> { let Some(nv12) = pixels else { return Ok(()) }; - let (encoder, _, fps) = self.began.as_mut().ok_or_else(|| Error::invalid("an MP4 begins before its frames"))?; + let (encoder, _, fps) = self.began.as_mut().ok_or_else(|| Error::invalid("begin an MP4 before its frames"))?; let time = ticks(index, *fps); let key = self.keyed.is_none_or(|keyed| time - keyed >= (KEYFRAMES * f64::from(TIMESCALE)) as u64); if key { @@ -173,7 +177,7 @@ impl Sink for Mp4 { } async fn finish(&mut self, end: u64) -> Result<()> { - let (encoder, _, _) = self.began.as_mut().ok_or_else(|| Error::invalid("an MP4 begins before it ends"))?; + let (encoder, _, _) = self.began.as_mut().ok_or_else(|| Error::invalid("begin an MP4 before finishing it"))?; let units = encoder.finish().await?; self.write(units)?; let (_, muxer, fps) = self.began.as_mut().expect("begun"); @@ -183,24 +187,27 @@ impl Sink for Mp4 { } } -/// A video of a shot's pictures, handed to its sink while they are drawn. +/// A video of a shot's pictures, passed to its sink as they are drawn. pub struct Video { drawing: Drawing, sink: S, fps: f64, - /// The next frame. + /// Index of the next frame. index: u64, - /// Frames drawn or held and not yet taken, oldest first: a held frame has nothing to read back. + /// Frames drawn or held but not yet passed to the sink, oldest first. A held frame has nothing + /// to read back. flight: VecDeque<(u64, Option)>, - /// Whether a frame was drawn: after one, a frame where nothing moved is held instead. + /// Whether a frame was drawn. After the first, a frame where nothing moved is held instead. drawn: bool, } impl Shot { - /// A video of this shot's pictures at `fps` frames a second, each frame handed to `sink`. + /// A video of this shot's pictures at `fps` frames per second, each frame passed to `sink`. + /// Errors if `fps` is outside 1 to 240, if the shot does not suit the sink's pixel format (see + /// [`Shot::check`]), or if the sink cannot begin. pub fn video(&self, gpu: &Gpu, fps: f64, mut sink: S) -> Result> { if !(fps.is_finite() && (1.0..=240.0).contains(&fps)) { - return Err(Error::invalid("a video shows 1 to 240 frames a second").at("fps")); + return Err(Error::invalid("must be 1 to 240 frames a second").at("fps")); } self.check(S::PIXELS)?; let host = gpu.0.lock().host.clone(); @@ -217,7 +224,7 @@ impl Shot { } impl Video { - /// Seconds into the video the next frame shows at. + /// The time in seconds at which the next frame shows. pub fn time(&self) -> f64 { self.index as f64 / self.fps } @@ -227,9 +234,10 @@ impl Video { self.index } - /// Draws the next frame of `views`, one per panel, at its time once they are complete, and hands - /// it on to the sink. It returns once there is room for another frame, not once the sink took - /// this one. A frame where nothing moved is not drawn: the sink shows the one before again. + /// Draws the next frame of `views`, one per panel, at the frame's time once they are complete, + /// and passes it to the sink. Returns once there is room for another frame, not once the sink + /// has taken this one. A frame where nothing moved is not drawn, and the sink shows the + /// previous frame again. pub async fn frame(&mut self, views: &mut [&mut View]) -> Result<()> { self.frame_of(&mut Borrowed(views)).await } @@ -256,15 +264,15 @@ impl Video { self.drawn = true; } self.index += 1; - // Held frames first in line wait on nothing. + // Held frames at the front of the queue wait on nothing, so pass them on now. while matches!(self.flight.front(), Some((_, None))) { self.hand_on().await?; } Ok(()) } - /// Hands every frame under way to the sink, ends the video at the next frame's time, and gives - /// the sink back. + /// Passes every frame in flight to the sink, ends the video at the next frame's time, and + /// returns the sink. pub async fn finish(mut self) -> Result { while !self.flight.is_empty() { self.hand_on().await?; @@ -273,7 +281,7 @@ impl Video { Ok(self.sink) } - /// Reads the oldest frame back, when it was drawn, and hands it to the sink. + /// Reads back the oldest frame, if it was drawn, and passes it to the sink. async fn hand_on(&mut self) -> Result<()> { let Some((index, readback)) = self.flight.pop_front() else { return Ok(()) }; let pixels = match readback { diff --git a/crates/latkit/src/video/mp4.rs b/crates/latkit/src/video/mp4.rs index 85924fe3..03fd5292 100644 --- a/crates/latkit/src/video/mp4.rs +++ b/crates/latkit/src/video/mp4.rs @@ -1,12 +1,13 @@ -//! Fragmented MP4: one H.264 track written as it goes. `ftyp` and a `moov` with no samples come first, built from the -//! first keyframe's SPS and PPS. Then each run of frames from a keyframe up to the next becomes one fragment: a `moof` -//! that times, sizes, and flags its samples, and an `mdat` that holds them. Only the run being gathered is held, so a -//! video of any length streams to its output with memory for one fragment. +//! Fragmented MP4 muxing: one H.264 track written as it streams. `ftyp` and a `moov` with no +//! samples come first, built from the first keyframe's SPS and PPS. Then each run of frames from +//! one keyframe up to the next becomes a fragment: a `moof` with its samples' durations, sizes, and +//! flags, and an `mdat` holding them. Only the current run is kept in memory, so a video of any +//! length streams out using memory for one fragment. //! -//! A frame lasts until the next one shows, so a video that skips frames where nothing moved pays nothing for them -//! (variable frame rate). Each fragment's decode time is its first frame's own time, so durations never drift from the -//! caller's clock. A sample is its NAL units with 4-byte lengths in place of start codes, access unit delimiters left -//! out and SPS and PPS kept in band. +//! A frame lasts until the next one shows, so frames skipped because nothing moved take no space +//! (variable frame rate). Each fragment's decode time is its first frame's own time, so durations +//! never drift from the caller's clock. A sample is its NAL units with 4-byte lengths in place of +//! start codes, with access unit delimiters dropped and SPS and PPS kept in band. use super::Unit; @@ -20,20 +21,20 @@ const PPS: u8 = 8; const AUD: u8 = 9; /// Sample flags of a keyframe: it depends on no other. const KEY: u32 = 0x0200_0000; -/// Sample flags of a frame between keyframes: it depends on others, and is no sync sample. +/// Sample flags of a frame between keyframes: it depends on others and is not a sync sample. const DELTA: u32 = 0x0101_0000; /// The identity transform, in 16.16 and 2.30 fixed point. const MATRIX: [u32; 9] = [0x0001_0000, 0, 0, 0, 0x0001_0000, 0, 0, 0, 0x4000_0000]; -/// A fragmented MP4 muxer for one H.264 track, taking access units and handing back the bytes to write. +/// A fragmented MP4 muxer for one H.264 track: takes access units and returns the bytes to write. pub(crate) struct Muxer { size: [u32; 2], timescale: u32, - /// Whether `ftyp` and `moov` were handed out. + /// Whether `ftyp` and `moov` were written. headed: bool, - /// How many fragments were handed out. + /// How many fragments were written. sequence: u32, - /// The frames of the fragment being gathered, the first its keyframe. + /// The frames of the current fragment, starting with its keyframe. run: Vec, /// Their NAL units, length-prefixed: the fragment's `mdat` payload. data: Vec, @@ -41,28 +42,30 @@ pub(crate) struct Muxer { last: Option, } -/// A frame of the fragment being gathered. +/// A frame of the current fragment. struct Sample { - /// Ticks at which it shows. + /// Presentation time in ticks. time: u64, - /// Its bytes in the `mdat` payload. + /// Its size in bytes in the `mdat` payload. size: u32, } impl Muxer { - /// A muxer for one H.264 track of `size` pixels whose frame times are `timescale` ticks a second. + /// A muxer for one H.264 track of `size` pixels, with frame times in ticks of `timescale` per + /// second. pub(crate) fn new(size: [u32; 2], timescale: u32) -> Muxer { Muxer { size, timescale, headed: false, sequence: 0, run: Vec::new(), data: Vec::new(), last: None } } - /// Takes one access unit; returns the bytes ready to write next, in order: `ftyp` and `moov` before - /// the first keyframe's fragment, and a fragment (`moof` + `mdat`) whenever a keyframe closes the - /// run of frames before it. A frame lasts until the next frame's time, so frames a video skipped - /// because nothing moved cost nothing (variable frame rate). Units before the first keyframe are - /// dropped. Returns `Err` (a `String` reason) when the first keyframe holds no SPS and PPS. + /// Takes one access unit and returns the bytes to write next, in order: `ftyp` and `moov` + /// before the first keyframe's fragment, and a fragment (`moof` + `mdat`) whenever a keyframe + /// closes the run of frames before it. A frame lasts until the next frame's time, so frames + /// skipped because nothing moved take no space (variable frame rate). Units before the first + /// keyframe are dropped. Errors with a reason when the first keyframe lacks an SPS and PPS, or + /// its SPS is too short. /// - /// A unit whose time does not pass the last one's shows a tick after it. A unit holding no NAL units but - /// delimiters is dropped, and the frame before it lasts on. + /// A unit whose time is not after the last one's is shown one tick after it. A unit holding + /// only access unit delimiters is dropped, and the previous frame lasts longer. pub(crate) fn push(&mut self, unit: &Unit) -> Result, String> { let mut out = Vec::new(); if self.run.is_empty() && !unit.key { @@ -96,8 +99,8 @@ impl Muxer { Ok(out) } - /// Ends the video at `end` ticks: the last frame lasts until then (at least one tick). Returns - /// the last fragment's bytes, or nothing when no frame was pushed. + /// Ends the video at `end` ticks; the last frame lasts until then, at least one tick. Returns + /// the last fragment's bytes, or nothing if no frame is pending. pub(crate) fn finish(&mut self, end: u64) -> Vec { let mut out = Vec::new(); if let Some(last) = self.run.last().map(|sample| sample.time) { @@ -106,7 +109,8 @@ impl Muxer { out } - /// Writes `ftyp`, and the `moov` that describes the track by its first keyframe's `sps` and `pps`. + /// Writes `ftyp` and the `moov` describing the track, built from the first keyframe's `sps` and + /// `pps`. fn header(&self, sps: &[u8], pps: &[u8], out: &mut Vec) { boxed(out, *b"ftyp", |o| { o.extend_from_slice(b"isom"); @@ -117,7 +121,7 @@ impl Muxer { full(o, *b"mvhd", 0, 0, |o| { o.extend([0; 8]); // creation and modification times o.extend(self.timescale.to_be_bytes()); - o.extend([0; 4]); // duration: the fragments hold it + o.extend([0; 4]); // duration: set by the fragments o.extend(0x0001_0000_u32.to_be_bytes()); // rate o.extend(0x0100_u16.to_be_bytes()); // volume o.extend([0; 10]); @@ -177,15 +181,15 @@ impl Muxer { full(o, *b"tkhd", 0, 3, |o| { o.extend([0; 8]); o.extend(TRACK.to_be_bytes()); - o.extend([0; 24]); // duration, layer, group, and volume, all none + o.extend([0; 24]); // duration, layer, group, and volume, all zero matrix(o); o.extend((w << 16).to_be_bytes()); o.extend((h << 16).to_be_bytes()); }); } - /// Writes the `avc1` sample entry: the picture's size, its decoder configuration from `sps` and `pps`, and its - /// colors, BT.709 in limited range. + /// Writes the `avc1` sample entry: the picture size, the decoder configuration from `sps` and + /// `pps`, and the colors, BT.709 limited range. fn avc1(&self, sps: &[u8], pps: &[u8], o: &mut Vec) { let [w, h] = self.size; boxed(o, *b"avc1", |o| { @@ -197,7 +201,7 @@ impl Muxer { o.extend(0x0048_0000_u32.to_be_bytes()); // 72 dpi across o.extend(0x0048_0000_u32.to_be_bytes()); // and down o.extend([0; 4]); - o.extend(1_u16.to_be_bytes()); // frames a sample + o.extend(1_u16.to_be_bytes()); // frames per sample o.extend([0; 32]); // compressor name o.extend(0x18_u16.to_be_bytes()); // depth o.extend((-1_i16).to_be_bytes()); @@ -210,17 +214,18 @@ impl Muxer { o.extend((pps.len() as u16).to_be_bytes()); o.extend_from_slice(pps); if matches!(sps[1], 100 | 110 | 122 | 144) { - o.extend([0xfd, 0xf8, 0xf8, 0]); // 4:2:0, 8-bit luma and chroma, no SPS extensions + o.extend([0xfd, 0xf8, 0xf8, 0]); // 4:2:0, 8-bit, no SPS extensions } }); boxed(o, *b"colr", |o| { o.extend_from_slice(b"nclx"); - o.extend([0, 1, 0, 1, 0, 1, 0]); // primaries, transfer, and matrix BT.709; limited range + o.extend([0, 1, 0, 1, 0, 1, 0]); // BT.709 primaries/transfer/matrix, limited range }); }); } - /// Writes the fragment of the frames gathered, the last lasting until `end`, and empties the run for the next. + /// Writes a fragment of the current run, the last frame lasting until `end`, and clears the + /// run. fn fragment(&mut self, end: u64, out: &mut Vec) { self.sequence += 1; out.reserve(100 + 12 * self.run.len() + self.data.len()); @@ -253,7 +258,7 @@ impl Muxer { } } -/// Writes a box of type `kind` whose body `body` writes, its size filled in after. +/// Writes a box of type `kind` with the body `body` writes, filling in its size afterwards. fn boxed(out: &mut Vec, kind: [u8; 4], body: impl FnOnce(&mut Vec)) { let start = out.len(); out.extend([0; 4]); @@ -278,8 +283,8 @@ fn matrix(out: &mut Vec) { } } -/// The NAL units of Annex B `bytes`: what lies between start codes, less the zero bytes that lead a 4-byte start code -/// (a NAL unit never ends in one). +/// The NAL units of Annex B `bytes`: the bytes between start codes, minus trailing zero bytes, +/// which belong to a following 4-byte start code (a NAL unit never ends in a zero byte). pub(crate) fn nals(bytes: &[u8]) -> impl Iterator { let mut rest = start_code(bytes).map_or(&[][..], |at| &bytes[at + 3..]); std::iter::from_fn(move || { @@ -313,7 +318,7 @@ mod tests { const IDR: &[u8] = &[0x65, 0x88, 0x84, 0x00, 0x21]; const SLICE: &[u8] = &[0x41, 0x9a, 0x00, 0x03, 0x01, 0x7f]; - /// NAL units in Annex B form, their start codes 4 and 3 bytes by turns. + /// NAL units in Annex B form, with start codes alternating between 4 and 3 bytes. fn annex_b(nals: &[&[u8]]) -> Vec { let mut out = Vec::new(); for (i, nal) in nals.iter().enumerate() { @@ -323,12 +328,12 @@ mod tests { out } - /// A keyframe at `time`, delimited, with its SPS and PPS. + /// A keyframe at `time` with an access unit delimiter, SPS, and PPS. fn key(time: u64) -> Unit { Unit { time, key: true, annex_b: annex_b(&[AUD_NAL, SPS_NAL, PPS_NAL, IDR]) } } - /// A frame between keyframes at `time`, delimited. + /// A frame between keyframes at `time`, with an access unit delimiter. fn delta(time: u64) -> Unit { Unit { time, key: false, annex_b: annex_b(&[AUD_NAL, SLICE]) } } @@ -337,7 +342,7 @@ mod tests { u32::from_be_bytes(bytes[at..at + 4].try_into().unwrap()) } - /// The boxes `bytes` holds, each its type and its body. + /// The boxes in `bytes`, each as its type and body. fn boxes(mut bytes: &[u8]) -> Vec<([u8; 4], &[u8])> { let mut out = Vec::new(); while !bytes.is_empty() { @@ -348,7 +353,7 @@ mod tests { out } - /// The body of the box of type `kind` among those `bytes` holds past its first `skip` bytes. + /// The body of the box of type `kind` in `bytes`, after skipping its first `skip` bytes. fn child(bytes: &[u8], skip: usize, kind: [u8; 4]) -> &[u8] { let found = boxes(&bytes[skip..]).into_iter().find(|(k, _)| *k == kind); found.unwrap_or_else(|| panic!("no {}", String::from_utf8_lossy(&kind))).1 @@ -358,7 +363,7 @@ mod tests { boxes(bytes).iter().map(|(kind, _)| String::from_utf8_lossy(kind).into_owned()).collect() } - /// A fragment read back. + /// A parsed fragment. struct Fragment { sequence: u32, time: u64, @@ -412,7 +417,7 @@ mod tests { } #[test] - fn the_header_comes_with_the_first_keyframe_and_a_fragment_with_each_next() { + fn the_header_comes_with_the_first_keyframe_and_fragments_with_later_ones() { let mut mp4 = Muxer::new([640, 360], 90_000); let mut file = Vec::new(); let head = mp4.push(&key(0)).unwrap(); @@ -502,7 +507,7 @@ mod tests { } #[test] - fn late_times_take_a_tick_and_empty_units_are_dropped() { + fn late_units_move_a_tick_past_the_last_and_empty_units_are_dropped() { let mut mp4 = Muxer::new([64, 64], 1000); let mut file = mp4.push(&key(100)).unwrap(); for unit in [delta(100), delta(50), Unit { time: 300, key: false, annex_b: annex_b(&[AUD_NAL]) }, delta(400)] { diff --git a/crates/latkit/src/view/axes.rs b/crates/latkit/src/view/axes.rs index 78c40ab3..95c941e6 100644 --- a/crates/latkit/src/view/axes.rs +++ b/crates/latkit/src/view/axes.rs @@ -1,6 +1,6 @@ //! A plot's axes, drawn over its marks: grid lines at round values, tick labels and titles in the //! theme's font, and the playhead at the shown coordinate. Rebuilt each frame as overlay sprites, -//! O(ticks). The room they take around the plot's area is measured as the host sets their text. +//! in O(ticks). The room they take around the plot area is measured with the host's text metrics. use std::sync::Arc; @@ -15,10 +15,10 @@ use crate::{Axis, Camera, Color, Format, Plot, Space}; /// A tick's length, and the gap between it and its label: CSS pixels. const TICK: f64 = 4.0; const GAP: f64 = 3.0; -/// Room past an axis hidden: CSS pixels. +/// Room kept on the sides of a hidden axis, in CSS pixels. const HIDDEN: f64 = 8.0; -/// What a plot's axes take around its area, as their text is measured. +/// The room a plot's axes take around its area, from their measured text. #[derive(Clone, Copy, Default, PartialEq, Debug)] pub(crate) struct Gutters { /// Room outside the area: top, right, bottom, left, CSS pixels. @@ -27,10 +27,10 @@ pub(crate) struct Gutters { tick: f64, } -/// What `plot`'s axes take, `measure` giving the CSS pixels a run of the theme's text takes as it is -/// written, the field around it included: below the area, a tick, its labels, and the title; left of -/// it, a tick and the widest label the value axis writes; above it, the value axis's title, else half -/// its top label; right of it, half the widest label the axis across writes. +/// The room `plot`'s axes take. `measure` gives the CSS pixel size of a run of the theme's text, +/// including the distance field around it. Below the area: a tick, its labels, and the x title. +/// Left: a tick and the widest y label. Above: the y title, at least half a label's height. Right: +/// half the widest x label. A hidden axis leaves `HIDDEN` on its two sides instead. pub(crate) fn gutters(plot: &Plot, measure: impl Fn(&str) -> [f64; 2]) -> Gutters { let tick = measure("0")[1]; let widest = |axis: &Axis| written(axis).iter().map(|text| measure(text)[0]).fold(0.0, f64::max); @@ -43,8 +43,8 @@ pub(crate) fn gutters(plot: &Plot, measure: impl Fn(&str) -> [f64; 2]) -> Gutter Gutters { room: [top, right, bottom, left], tick } } -/// The labels `axis` writes as wide as its widest: those of the ticks it gives, else a value with a -/// sign, two digits, and three decimals as its format writes it. +/// Labels as wide as the widest `axis` writes: its given ticks' labels, else a sample value with a +/// sign, two digits, and three decimals, in its format. fn written(axis: &Axis) -> Vec { let Some(given) = &axis.ticks else { return vec![label(-88.888, 0.001, axis)] }; let mut values: Vec = given.iter().map(|&(v, _)| v).collect(); @@ -103,7 +103,7 @@ fn axis_ticks(axis: &Axis, range: [f64; 2], step: f64) -> Vec<(f64, String)> { } } -/// Values `step` apart within `range`. +/// Values `step` apart within `range`, at most 201. pub(super) fn ticks(range: [f64; 2], step: f64) -> impl Iterator { let first = (range[0] / step).ceil(); let last = (range[1] / step).floor().min(first + 200.0); @@ -121,14 +121,14 @@ pub(super) fn line(a: [f64; 2], b: [f64; 2], width: f32, color: Color) -> wgsl:: sprite } -/// Where a run of text goes: its anchor, and which of its edges meets the anchor. +/// Which edges of a run of text meet its anchor. #[derive(Clone, Copy)] pub(super) enum Align { - /// Centered across, top at the anchor. + /// Centered horizontally, top at the anchor. Below, - /// Right edge at the anchor, centered up and down. + /// Right edge at the anchor, centered vertically. Left, - /// Left edge at the anchor, centered up and down. + /// Left edge at the anchor, centered vertically. Right, /// Left edge at the anchor, bottom at the anchor. Above, @@ -138,7 +138,7 @@ pub(super) enum Align { BelowRight, } -/// What sets text for the overlay: the atlas, in one font, size, and color. +/// Sets overlay text through the atlas, in one font, size, and color. pub(super) struct Writer<'a> { pub(super) inner: &'a mut Inner, pub(super) font: Arc, @@ -148,7 +148,8 @@ pub(super) struct Writer<'a> { } impl Writer<'_> { - /// `text` as a sprite placed by `align` at `anchor`, CSS pixels; `None` while it cannot be set. + /// `text` as a sprite aligned by `align` at `anchor` (CSS pixels), snapped to device pixels; + /// `None` while the text cannot be set yet. pub(super) fn text(&mut self, text: &str, anchor: [f64; 2], align: Align) -> Option { let run = TextRun { text: text.into(), font: self.font.clone(), size_px: self.size_px, max_width: None }; let host = self.inner.host.clone(); @@ -176,12 +177,13 @@ impl Writer<'_> { } impl View { - /// The sprites drawn over the marks this frame: a plot's axes, the title and legends, - /// and a box being selected. + /// The sprites drawn over the marks this frame: a plot's axes, the title and legends, and a box + /// being selected or a connection being drawn. pub(super) fn overlay(&self, inner: &mut Inner) -> Vec { let evictions = inner.atlas.evictions; let mut sprites = self.written(inner); - // The atlas made room while setting this frame's text: set it again on the new packing. + // If the atlas evicted texts while setting this frame's text, set it again on the new + // packing. if inner.atlas.evictions != evictions { sprites = self.written(inner); } @@ -202,7 +204,8 @@ impl View { sprites } - /// The sprites of the overlay's text and what goes with it: a plot's axes, the title, and the legends. + /// The overlay's text sprites and the lines that go with them: a plot's axes, the title, and + /// the legends. fn written(&self, inner: &mut Inner) -> Vec { let mut sprites = self.axes_sprites(inner); sprites.extend(self.annotations(inner)); @@ -215,8 +218,8 @@ impl View { self.axes(inner, plot, self.area(), x, y) } - /// Measures what the plot's axes take as the host sets the theme's text at the view's pixel ratio, - /// once for each scene and ratio: the plot's area frames what they leave. + /// Measures the room the plot's axes take, with the host's metrics for the theme's text at the + /// view's pixel ratio, once per scene and ratio. The plot area is what they leave. pub(super) fn measure_gutters(&mut self) { let Space::Plot(plot) = &self.scene.space else { return }; let stamp = stamp_of((self.scene_version, self.pixel_ratio.to_bits())); @@ -232,7 +235,7 @@ impl View { let measure = |text: &str| { let run = TextRun { text: text.into(), font: theme.text.font.clone(), size_px: size as u32, max_width: None }; - // Without a host to set text, as wide as an average face's characters and a line tall. + // Without a host, estimate 0.6 em per character and a 1.25 em line. let guess = [0.6 * size * text.chars().count() as f64, 1.25 * size]; let [w, h] = host.and_then(|host| host.measure(&run).ok()).map_or(guess, |s| s.map(f64::from)); [(w + field) / ratio, (h + field) / ratio] @@ -289,7 +292,7 @@ impl View { } sprites.push(line([left, top], [left, bottom], 1.0, axis)); if let Some(title) = &plot.y.label { - // Above the area, from the left of the room its labels take. + // Above the area, aligned with the left edge of the room its labels take. sprites.extend(writer.text(title, [left - gutters.room[3], top], Align::Above)); } } @@ -323,25 +326,25 @@ mod tests { } #[test] - fn gutters_hold_what_the_axes_write() { - // Text as the host sets it: 7 pixels a character and a line 20 tall, its field 16 around. + fn gutters_fit_the_axis_labels_and_titles() { + // Host text metrics: 7 pixels per character and a 20-pixel line, plus 16 for the field. let measure = |text: &str| [7.0 * text.chars().count() as f64 + 16.0, 36.0]; let titled = |label: &str| Axis { label: Some(label.into()), ..Axis::default() }; let plot = Plot { x: titled("Time"), y: titled("Load") }; let room = gutters(&plot, measure).room; - // Above, the value axis's title; below, a tick, its labels, and the title across. + // Above: the y title. Below: a tick, its labels, and the x title. assert_eq!(room[0], 36.0); assert_eq!(room[2], TICK + GAP + 36.0 + 36.0); - // Left, a tick and "-88.888"; right, half of it. + // Left: a tick and "-88.888". Right: half that label. let widest = 7.0 * 7.0 + 16.0; assert_eq!(room[3], TICK + GAP + widest); assert_eq!(room[1], widest / 2.0); - // Ticks it gives are measured as they are written. + // Given ticks are measured as written. let given = Axis { ticks: Some(vec![(0.0, Some("idle".into())), (1.0, Some("overloaded".into()))]), ..Axis::default() }; let room = gutters(&Plot { x: Axis::default(), y: given }, measure).room; assert_eq!(room[3], TICK + GAP + 7.0 * 10.0 + 16.0); - assert_eq!(room[0], 18.0, "without a title, half the top label"); + assert_eq!(room[0], 18.0, "without a title, the top gutter is half a label high"); let hidden = Axis { hidden: true, ..Axis::default() }; assert_eq!(gutters(&Plot { x: hidden.clone(), y: hidden }, measure).room, [HIDDEN; 4]); } diff --git a/crates/latkit/src/view/camera.rs b/crates/latkit/src/view/camera.rs index 44cfda5d..05fc76d1 100644 --- a/crates/latkit/src/view/camera.rs +++ b/crates/latkit/src/view/camera.rs @@ -9,26 +9,26 @@ use crate::gpu::shaders as wgsl; #[derive(Clone, Copy, PartialEq, Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)] #[serde(untagged, deny_unknown_fields)] pub enum Camera { - /// On the earth, a plane, or a diagram: the point looked at and turned about, how far in, and how - /// tilted and turned. A diagram is always seen from straight above, upright. + /// A camera over the earth, a plane, or a diagram: the point it looks at and turns about, its + /// zoom, its tilt, and its rotation. A diagram is always seen from straight above, upright. Map { /// Longitude, latitude, and elevation on the earth; x, y, and elevation on a plane or a - /// diagram. In JSON, `[x, y]` looks at the ground. + /// diagram. In JSON, `[x, y]` is shorthand for elevation 0. #[serde(with = "center")] #[schemars(with = "center::Center")] center: [f64; 3], - /// On the earth the web mercator level; elsewhere, log₂ of pixels per unit. + /// The web mercator zoom level on the earth; elsewhere, log₂ of CSS pixels per unit. zoom: f64, - /// Degrees from looking straight down, up to 75; 0 in a diagram. + /// Tilt in degrees from looking straight down, up to 75. Ignored in a diagram. pitch: f64, - /// Degrees clockwise from north, or from up on a plane; 0 in a diagram. + /// Degrees clockwise from north, or from up on a plane. Ignored in a diagram. bearing: f64, }, - /// On a plot: the axis range across, and the value range up, or none to fit the values in view. + /// A plot camera: the x range shown, and the value range, or `None` to fit the values in view. Plot { - /// The axis range shown. + /// The x-axis range shown. x: [f64; 2], - /// The value range shown, or none to fit what is in view. + /// The value range shown, or `None` to fit the values in view. y: Option<[f64; 2]>, }, } @@ -36,7 +36,7 @@ pub enum Camera { mod center { use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; - /// What a center is written as: the ground's `[x, y]`, or `[x, y, elevation]`. + /// The JSON forms of a center: `[x, y]` at elevation 0, or `[x, y, elevation]`. #[derive(schemars::JsonSchema)] #[serde(untagged)] #[schemars(inline)] @@ -59,7 +59,7 @@ mod center { } } -/// Vertical field of view: the camera stands 1.5 heights from what it looks at. +/// Vertical field of view in radians, chosen so the camera stands 1.5 view heights from its center. const FOV: f64 = 0.643_501_108_793_284_4; const MAX_LATITUDE: f64 = 85.051_128_779_806_59; pub(crate) const MAX_PITCH: f64 = 75.0; @@ -68,7 +68,8 @@ const MAX_ZOOM: f64 = 24.0; /// A rectangle of the view, CSS pixels: left, top, right, bottom. pub(crate) type Area = [f64; 4]; -/// What fits framed: everything, and any rows framed since. The camera stays within reach of it. +/// The box fits have framed: everything, widened by any rows framed since. The camera stays within +/// reach of it. #[derive(Clone, Copy, PartialEq, Debug)] pub(crate) struct Bounds { pub(crate) min: [f64; 3], @@ -78,7 +79,7 @@ pub(crate) struct Bounds { } impl Bounds { - /// Reach that holds both. + /// The union of both bounds. pub(crate) fn with(self, other: Bounds) -> Bounds { Bounds { min: std::array::from_fn(|k| self.min[k].min(other.min[k])), @@ -87,7 +88,8 @@ impl Bounds { } } - /// The zooms the camera keeps within: a few levels out past what was framed, eight in. + /// The zoom range the camera keeps within: 3 levels out past the widest framing to 8 levels in + /// past the closest. pub(crate) fn zooms(&self) -> [f64; 2] { [self.zoom[0] - 3.0, self.zoom[1] + 8.0] } @@ -114,9 +116,9 @@ impl Camera { ) } - /// The camera `t` of the way from `self` to `to`, `t` from 0 to 1, as a view of `space` flies: - /// bearing, and on the earth longitude, turn the short way. A script moving the camera over time - /// reads it. + /// The camera a fraction `t` (0 to 1) of the way from `self` to `to`, as a view of `space` + /// flies between them. Bearing, and longitude on the earth, turn the short way. Cameras of + /// different kinds give `to`. Scripts that move the camera over time can use it. #[must_use] pub fn ease(&self, to: &Camera, t: f64, space: &Space) -> Camera { let earth = matches!(space, Space::Earth(_)); @@ -147,9 +149,10 @@ impl Camera { } } - /// Moved so `anchor` moves `delta` CSS pixels on screen. A map slides along the level through - /// the anchor; where that level runs off toward the horizon, it slides as far as the anchor's - /// depth across the screen would take it, still level. + /// The camera moved so `anchor` moves `delta` CSS pixels on screen. A map slides along the + /// horizontal plane through the anchor. Where that plane runs off toward the horizon, it uses + /// the point at the anchor's screen depth instead, still moving level. On a globe, the center + /// moves so the anchor appears at the new point. pub(crate) fn panned(&self, view: &Projected, anchor: [f64; 3], delta: [f64; 2]) -> Camera { match *self { Camera::Plot { x, y } => { @@ -162,7 +165,7 @@ impl Camera { let Some(s) = view.screen(a) else { return *self }; let to = [s[0] + delta[0], s[1] + delta[1]]; if view.globe >= 0.5 { - // Past the globe's edge there is nothing to hold the anchor to. + // Off the globe there is nothing to pin the anchor to. if view.ground(to).is_none() { return *self; } @@ -179,7 +182,8 @@ impl Camera { } } - /// Turned by `degrees` of bearing and pitch about `anchor`, which keeps its place on screen. + /// The camera turned by `degrees` of bearing and pitch about `anchor`, which keeps its screen + /// position. Pitch stays within `0..=MAX_PITCH`; a diagram never turns. pub(crate) fn turned(&self, view: &Projected, anchor: [f64; 3], degrees: [f64; 2]) -> Camera { let Camera::Map { center, zoom, pitch, bearing } = *self else { return *self }; if view.space == wgsl::SPACE_DIAGRAM { @@ -194,7 +198,7 @@ impl Camera { None => turned, }; } - // The camera turns rigidly about the anchor: its center goes with it. + // Rotate the camera rigidly about the anchor; its center moves with it. let rig = |p: f64, b: f64| DMat3::from_rotation_z(-b.to_radians()) * DMat3::from_rotation_x(p.to_radians()); let r = rig(to_pitch, to_bearing) * rig(pitch, bearing).transpose(); let a = view.world(anchor); @@ -204,9 +208,9 @@ impl Camera { Camera::Map { center: view.shifted(center, d, d.z), zoom, pitch: to_pitch, bearing: to_bearing } } - /// Zoomed in by `levels`, out when negative, toward `anchor`, which keeps its place on screen. A - /// map's zoom stays within `limits` (what a fit framed lets it reach) before the anchor moves the - /// center, so a zoom held at its limit moves nothing. + /// The camera zoomed in by `levels` (out when negative) toward `anchor`, which keeps its screen + /// position. A map's zoom is clamped to `limits` (the reach a fit allows) before the center + /// moves, so a zoom stopped at its limit moves nothing. pub(crate) fn zoomed(&self, view: &Projected, anchor: [f64; 3], levels: f64, limits: Option<[f64; 2]>) -> Camera { match *self { Camera::Plot { x, y } => { @@ -219,7 +223,7 @@ impl Camera { Camera::Map { center, zoom, pitch, bearing } => { let [least, most] = limits.unwrap_or([-MAX_ZOOM, MAX_ZOOM]); let (least, most) = (least.max(-MAX_ZOOM), most.min(MAX_ZOOM)); - // A zoom already past a limit is not pulled back toward it by a gesture going further. + // A zoom already past a limit stays put rather than snapping back to it. let to = (zoom + levels).clamp(least.min(zoom), most.max(zoom)); if to == zoom { return *self; @@ -239,9 +243,9 @@ impl Camera { } } - /// Within reach of what `bounds` framed: zoomed no further out than a few levels past it, no - /// further in than eight, looking across no farther than its size from it, and no higher or - /// lower than it reaches, so the camera stays above what stands on the ground. + /// The camera clamped to within reach of `bounds`: zoom within [`Bounds::zooms`], the center no + /// farther outside the box than its size, the center's elevation within the box's, and pitch + /// within `0..=MAX_PITCH`. pub(crate) fn within(&self, bounds: &Bounds) -> Camera { let Camera::Map { center, zoom, pitch, bearing } = *self else { return *self }; let span = (0..2).map(|k| bounds.max[k] - bounds.min[k]).fold(0.0, f64::max).max(1e-9); @@ -254,8 +258,8 @@ impl Camera { } } - /// The camera framing the box `min..max` of positions inside `area` with room to spare, looking - /// at its middle and keeping pitch and bearing. + /// The camera framing the box `min..max` of positions inside `area` with 24 CSS pixels of + /// padding, looking at the box's middle and keeping pitch and bearing. pub(crate) fn framing(&self, space: &Space, min: [f64; 3], max: [f64; 3], size: [f64; 2], area: Area) -> Camera { const PADDING: f64 = 24.0; let room = [(area[2] - area[0] - 2.0 * PADDING).max(1.0), (area[3] - area[1] - 2.0 * PADDING).max(1.0)]; @@ -264,8 +268,8 @@ impl Camera { Camera::Plot { .. } => (0.0, 0.0), }; let height = min[2].midpoint(max[2]); - // A box with no extent, as one point's is, has no zoom of its own: the camera looks at it at - // the zoom it shows. + // A box with no extent, such as one point's, has no zoom of its own, so the camera keeps + // its zoom. if let Camera::Map { zoom, .. } = *self && !matches!(space, Space::Plot(_)) && max[0] - min[0] <= 1e-9 @@ -302,9 +306,9 @@ impl Camera { } } - /// Refines a map camera, still looking at the box's middle, so the box's eight corners reach no - /// farther from the middle of `area` than `room` allows: the perspective changes with the zoom, - /// so a few rounds settle it. + /// Refines a map camera's zoom, still looking at the box's middle, until the box's eight + /// corners fit within `room` around the middle of `area`. Perspective changes with zoom, so + /// this iterates, up to 12 rounds. fn framed(self, space: &Space, min: [f64; 3], max: [f64; 3], size: [f64; 2], area: Area, room: [f64; 2]) -> Camera { let middle = [area[0].midpoint(area[2]), area[1].midpoint(area[3])]; let mut camera = self; @@ -326,7 +330,8 @@ impl Camera { camera } - /// Looking at `position`, keeping everything else. + /// The camera looking at `position`, otherwise unchanged. A plot centers only x, keeping its + /// x span. pub(crate) fn centered(&self, position: [f64; 3]) -> Camera { match *self { Camera::Map { zoom, pitch, bearing, .. } => Camera::Map { center: position, zoom, pitch, bearing }, @@ -352,11 +357,11 @@ pub(crate) fn latitude(mercator_y: f64) -> f64 { (2.0 * mercator_y.exp().atan() - PI / 2.0).to_degrees() } -/// A camera looking at a space through a view of some size: everything a frame and the gestures -/// need to go between positions, world coordinates, and the screen. +/// A camera's projection of a space into a view of some size, which frames and gestures use to +/// convert between positions, world coordinates, and the screen. /// -/// World coordinates are what the shaders project: position units from the camera's center across a -/// plane or plot, mercator radians or the globe on the earth, with elevation up. +/// World coordinates are what the shaders project: position units relative to the camera's center +/// on a plane or plot, mercator radians (or the globe) on the earth, with elevation up. #[derive(Clone)] pub(crate) struct Projected { pub(crate) clip_from_world: DMat4, @@ -376,7 +381,7 @@ pub(crate) struct Projected { } impl Projected { - /// How far a space's earth is morphed onto the globe. + /// How far a space's earth is morphed onto the globe: 1 for the globe projection, else 0. pub(crate) fn globe_of(space: &Space) -> f64 { match space { Space::Earth(earth) => f64::from(u8::from(earth.projection == crate::Projection::Globe)), @@ -384,8 +389,9 @@ impl Projected { } } - /// The projection of `camera` looking at `space` in a view of `size` CSS pixels, its center in the - /// middle of `area`. A plot's camera spans `area`; one without a value range shows `0..1`. + /// The projection of `camera` looking at `space` in a view of `size` CSS pixels, with its + /// center in the middle of `area`. A plot's camera spans `area`; one without a value range + /// shows `0..1`. A camera that does not fit `space` is replaced by that space's default. pub(crate) fn new(space: &Space, camera: &Camera, size: [f64; 2], area: Area, globe: f64) -> Projected { let camera = if camera.fits(space) { *camera } else { Camera::default_for(space) }; let (w, h) = (size[0].max(1.0), size[1].max(1.0)); @@ -408,7 +414,7 @@ impl Projected { let px = [room[0] / (x[1] - x[0]).abs().max(1e-12), room[1] / (y[1] - y[0]).abs().max(1e-12)]; ([x[0].midpoint(x[1]), y[0].midpoint(y[1]), 0.0], px, 0.0, 0.0, wgsl::SPACE_PLOT) } - _ => unreachable!("the camera fits the space"), + _ => unreachable!("the camera matches the space"), }; let distance = 0.5 * h / (0.5 * FOV).tan(); // Reversed depth: the near plane maps to 1 and the far plane to 0. @@ -444,9 +450,9 @@ impl Projected { } } - /// `camera` moved on the globe, its center only, so that `anchor` lands at screen point `to`: - /// Newton's method on the center's longitude and latitude from where it stands. Where no center - /// puts the anchor there in front of the globe, `camera` as it is. + /// `camera` with its center moved on the globe so that `anchor` appears at screen point `to`, + /// found by Newton's method on the center's longitude and latitude. Returns `camera` unchanged + /// if no center puts the anchor there on the globe's front side. pub(crate) fn keeping(&self, camera: Camera, anchor: [f64; 3], to: [f64; 2]) -> Camera { let Camera::Map { center, zoom, pitch, bearing } = camera else { return camera }; let earth = Space::Earth(crate::Earth::default().projection(crate::Projection::Globe)); @@ -458,7 +464,7 @@ impl Projected { let front = (w - DVec3::new(0.0, 0.0, -radius)).dot(view.eye - w) > 0.0; view.screen(w).filter(|_| front).map(|s| [s[0], s[1]]) }; - // A step a few pixels wide measures how the anchor moves with the center. + // A step of about 4 pixels measures how the anchor moves with the center. let step = 4.0 * 360.0 / (256.0 * zoom.exp2()); let mut c = center; for _ in 0..8 { @@ -519,8 +525,8 @@ impl Projected { [wrap(self.center[0] + v.x.atan2(v.z).to_degrees()), v.y.clamp(-1.0, 1.0).asin().to_degrees(), r - 1.0] } - /// Where world point `w` lands, CSS pixels from the top left, with its depth; `None` behind the - /// camera. + /// Where world point `w` appears: CSS pixels from the top left, and its depth. `None` behind + /// the camera. pub(crate) fn screen(&self, w: DVec3) -> Option<[f64; 3]> { let clip = self.clip_from_world * w.extend(1.0); (clip.w > 0.0).then(|| { @@ -542,19 +548,21 @@ impl Projected { (near, (self.unproject(p, 0.0) - near).normalize_or_zero()) } - /// How steeply the ray under `p` meets a level: 1 looking straight down, 0 along the horizon. + /// How steeply the ray under `p` meets a horizontal plane: 1 looking straight down, 0 along the + /// horizon. pub(crate) fn grazing(&self, p: [f64; 2]) -> f64 { self.ray(p).1.z.abs() } - /// Where the ray under `p` meets the level at elevation `z`, in front of the camera. + /// Where the ray under `p` meets the horizontal plane at elevation `z`, in front of the camera. pub(crate) fn on_plane(&self, p: [f64; 2], z: f64) -> Option { let (o, d) = self.ray(p); let t = (z - o.z) / d.z; (t.is_finite() && t >= 0.0).then(|| o + d * t) } - /// Where the ray under `p` meets the ground: the plane's level, the map, or the globe. + /// Where the ray under `p` meets the ground: elevation 0 on a plane or flat map, or the globe's + /// surface. pub(crate) fn ground(&self, p: [f64; 2]) -> Option { if self.globe < 0.5 { return self.on_plane(p, 0.0); @@ -602,7 +610,7 @@ fn perspective(fov: f64, aspect: f64, near: f64, far: f64) -> DMat4 { ]) } -/// `f64` split into an `f32` and what it drops. +/// `x` split into its nearest `f32` and the `f32` remainder. pub(crate) fn split(x: f64) -> (f32, f32) { let hi = x as f32; (hi, (x - f64::from(hi)) as f32) @@ -662,14 +670,14 @@ mod tests { let view = Projected::new(&plane(), &camera, SIZE, AREA, 0.0); let low = view.screen(view.world([0.0, 0.0, 0.0])).expect("in front"); let high = view.screen(view.world([0.0, 0.0, 30.0])).expect("in front"); - assert!(high[2] > low[2], "nearer is greater: {low:?} {high:?}"); + assert!(high[2] > low[2], "nearer points have greater depth: {low:?} {high:?}"); let back = view.position(view.unproject([high[0], high[1]], high[2])); assert!((back[2] - 30.0).abs() < 1e-6 && back[0].abs() < 1e-6, "{back:?}"); assert!(view.ground([low[0], low[1]]).expect("the ground").length() < 1e-6); } #[test] - fn framing_holds_a_tilted_box_and_looks_at_its_middle() { + fn framing_fits_a_tilted_box_and_looks_at_its_middle() { let camera = Camera::Map { center: [0.0; 3], zoom: 0.0, pitch: 58.0, bearing: 30.0 }; let (min, max) = ([-4.0, -4.0, 0.0], [4.0, 4.0, 2.4]); let framed = camera.framing(&plane(), min, max, SIZE, AREA); @@ -700,12 +708,12 @@ mod tests { } #[test] - fn panning_toward_the_horizon_does_not_fling() { + fn panning_toward_the_horizon_does_not_jump_far() { let camera = Camera::Map { center: [0.0; 3], zoom: 4.0, pitch: 75.0, bearing: 0.0 }; let view = Projected::new(&plane(), &camera, SIZE, AREA, 0.0); let anchor = view.position(view.ground([400.0, 200.0]).expect("ground")); let Camera::Map { center, .. } = camera.panned(&view, anchor, [0.0, -190.0]) else { panic!("a map") }; - assert!(center[1].abs() < 100.0, "a drag to the horizon stays near: {center:?}"); + assert!(center[1].abs() < 100.0, "a drag toward the horizon moves the center a short way: {center:?}"); } #[test] diff --git a/crates/latkit/src/view/clock.rs b/crates/latkit/src/view/clock.rs index 8c5d9096..4a1dbc57 100644 --- a/crates/latkit/src/view/clock.rs +++ b/crates/latkit/src/view/clock.rs @@ -1,11 +1,12 @@ -//! A view's clock: seconds of motion, moved by the steps between the times its frames give. Every -//! ease, flight, fade, coast, orbit, and `elapsed()` reads it, never the wall, so whatever draws the -//! frames decides when they are: an animation frame's timestamp, a video frame's time, a test's -//! step. A shot may fix the time read by elapsed expressions and drawing effects while this clock -//! keeps advancing finite transitions. +//! A view's clock: seconds of motion, advanced by the time between frames. +//! +//! Every ease, flight, fade, coast, orbit, and `elapsed()` reads this clock, never wall time, so +//! whatever draws the frames sets the timeline: an animation frame's timestamp, a video frame's +//! time, or a test's step. A shot may fix the time that `elapsed()` expressions and drawing effects +//! read while this clock keeps advancing finite transitions. -/// The longest step the clock takes between two frames: a driver that slept, or switched from one -/// timeline to another, never makes motion jump or run back. +/// The longest step between two frames, in seconds. A driver that slept or switched timelines +/// never makes motion jump or run backward. const STEP: f64 = 0.25; #[derive(Default)] @@ -19,7 +20,8 @@ pub(crate) struct Clock { } impl Clock { - /// Takes a frame's `time`; the seconds the view moves by. + /// Advances to a frame's `time`, returning the step in seconds. A non-finite `time` moves + /// nothing. pub(crate) fn tick(&mut self, time: f64) -> f64 { let dt = match self.last { Some(last) if time.is_finite() => (time - last).clamp(0.0, STEP), @@ -33,7 +35,7 @@ impl Clock { dt } - /// Whether the last frame moved the clock: what reads it shows anything new only then. + /// Whether the last frame moved the clock. Anything reading the clock changes only then. pub(crate) fn moved(&self) -> bool { self.step > 0.0 } @@ -43,8 +45,8 @@ impl Clock { self.now } - /// How far an ease of `seconds` begun at `start` has come, smoothed, from 0 to 1: the one ease - /// flights and transitions share. + /// Progress of an ease of `seconds` begun at `start`, from 0 to 1 with smoothstep easing. + /// Flights and transitions share it. pub(crate) fn eased(&self, start: f64, seconds: f64) -> f64 { let t = ((self.now - start) / seconds.max(1e-3)).clamp(0.0, 1.0); t * t * (3.0 - 2.0 * t) @@ -61,11 +63,11 @@ mod tests { use super::*; #[test] - fn steps_follow_the_driver_and_never_jump_or_run_back() { + fn steps_follow_the_driver_and_never_jump_or_run_backward() { let mut clock = Clock::default(); assert_eq!(clock.tick(100.0), 0.0, "the first frame starts the clock"); - assert!((clock.tick(100.5) - STEP).abs() < 1e-12, "a long gap moves it one step"); - assert_eq!(clock.tick(3.0), 0.0, "another timeline never runs it back"); + assert!((clock.tick(100.5) - STEP).abs() < 1e-12, "a long gap moves it at most one step"); + assert_eq!(clock.tick(3.0), 0.0, "an earlier time never moves it backward"); assert!((clock.tick(3.1) - 0.1).abs() < 1e-12); assert!((clock.now() - (STEP + 0.1)).abs() < 1e-12); } diff --git a/crates/latkit/src/view/drawing.rs b/crates/latkit/src/view/drawing.rs index aa00f612..adf8f55f 100644 --- a/crates/latkit/src/view/drawing.rs +++ b/crates/latkit/src/view/drawing.rs @@ -1,13 +1,15 @@ -//! Drawings: what a view draws of each table drawn in part. The marks drawing a table hold pages of -//! its order, each in a slot of rows of their own, and draw runs of those held rows: the tiles the -//! camera asks for, finest first; the rows kept whatever their tiles, selected or framed by a fit; -//! and while tiles fade, those leaving. A page keeps its slot while it is held, so its rows keep their -//! words as the camera moves; a page no longer drawn stays held while the plan leaves room, so -//! drawing it again costs nothing, and a page held again costs only its words evaluated again. The -//! vertices the edges held end at are held with them, so an edge always finds its ends: a page where -//! many of them lie whole, the rest a row at a time, so edges whose ends lie far apart hold as many -//! rows as they end at, not as many pages. Rows kept whose pages it does not hold are held in pages of -//! kept rows, each in its place there while kept. +//! How a view draws a paged table. +//! +//! A drawing assigns the pages the camera needs to GPU slots and draws them as runs of held rows: +//! tiles fading out, then the camera's tiles, finest first, then pinned rows (selected, framed by +//! a fit, or ends of kept edges) that no tile covers. A page keeps its slot while resident, so +//! panning never moves rows. Idle pages stay resident while the budget allows, so returning to them +//! is free; a page made resident again only needs its words evaluated again. +//! +//! Resident edges need their endpoints resident. A vertex page that many edges end in is made +//! resident whole; scattered endpoints become kept rows, so far-apart edges cost one row per +//! endpoint rather than one page. Kept rows live in pages of their own, each at a fixed position +//! while kept. use std::ops::Range; use std::sync::Arc; @@ -16,27 +18,27 @@ use rustc_hash::FxHashSet; use super::View; use super::kept::KeptTable; -use crate::data::{Field, Name, Reference, UNWIRED, Version}; +use crate::data::{Field, Name, Reference, Table, UNWIRED, Version}; use crate::derive::order::Order; use crate::derive::stamp_of; use crate::derive::tiled::{Chosen, Draw, Place}; use crate::gpu::shaders as wgsl; use crate::gpu::shaders::PAGE_ROWS; -/// What marks a slot holding a page of kept rows, beside the page's number. +/// Flag bit of a slot holding a page of kept rows; the low bits are that page's number. const KEPT: u32 = 1 << 31; -/// Ends of the edges held lying in a page of the order of the table they end at, from which the page -/// is held whole; fewer are held a row at a time. Ends never take more than twice the rows they name. +/// Endpoints in one vertex page at which the page is made resident whole; with fewer, they are kept +/// individually. So endpoints never take more than twice the rows they reference. const DENSE: u32 = PAGE_ROWS / 2; -/// What a slot holds. +/// The contents of a slot. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) enum Holds { Free, /// Page `k` of the order. Page(u32), - /// Page `j` of the rows kept. + /// Page `j` of the kept rows. Kept(u32), } @@ -50,8 +52,9 @@ impl Holds { } } -/// A run of held rows a mark draws: where it starts among the rows drawn, the held row it starts at, -/// how many, and whether its tile fades in or out (`FADE_IN`, `FADE_OUT`). +/// A span of consecutive held rows a mark draws: its first index among the rows drawn (`item`), its +/// first held row (`held`), its length, and whether its tile fades in or out (`FADE_IN`, +/// `FADE_OUT`, or 0). #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) struct Run { pub(crate) item: u32, @@ -60,83 +63,145 @@ pub(crate) struct Run { pub(crate) fade: u32, } -/// What a view draws of one table drawn in part. +/// How a view draws one paged table: which pages are resident in which slots, and the runs drawn +/// from them. #[derive(Debug)] pub(crate) struct Drawing { pub(crate) order: Order, - /// What each slot holds: a page of the order, a page of the rows kept (`KEPT` and its number), or + /// Each slot's contents: a page of the order, a page of kept rows (`KEPT` | its number), or /// nothing (`NONE`). slots: Arc<[u32]>, - /// Each page's slot, `NONE` where it is not held: what the ends of edges find their vertices by. + /// Each page's slot, `NONE` if not resident. The end table resolves endpoints through it. pub(crate) pages: Arc<[u32]>, - /// The rows kept whose pages it does not hold, by their place among the rows kept: each one's - /// place in the order, `NONE` where a place is free. + /// The kept rows, indexed by kept position: each one's place in the order, `NONE` for a free + /// position. kept: Arc<[u32]>, - /// The rows kept by their place in the order, with their held rows: what the ends of edges find - /// kept vertices by. + /// The kept rows keyed by order place, with their held rows. The end table resolves kept + /// endpoints through it. pub(crate) kept_table: KeptTable, - /// The rows drawn whatever their tiles, as places in the order, ascending. + /// The pinned rows (drawn regardless of tiles), as order places, ascending. pinned: Arc<[u32]>, pub(crate) runs: Vec, - /// Runs by the held row they start at, for finding where a held row draws. + /// Run indices sorted by first held row, to look up where a held row draws. by_held: Vec, - /// The ranges of the order drawn, steadily or fading in, and those fading out. + /// The ranges of the order drawn (steady or fading in), and those fading out. ranges: Vec>, leaving: Vec>, - /// Whether tiles fade in or out: until they have, it settles to a drawing without fades. + /// Whether any tile fades in or out. Once the fade finishes, its settled drawing replaces it. pub(crate) fades: bool, - /// Rows drawn, and of them those fading out. + /// Rows drawn, and how many of them fade out. pub(crate) items: u32, pub(crate) leaving_items: u32, - /// When each slot's page was last asked for, by the layout that asked: pages no longer asked for - /// stay held while there is room, those asked for latest first. + /// The layout count at which each slot's page was last needed. Idle pages stay resident while + /// there is room, most recently needed first. asked: Arc<[u64]>, - /// How many layouts led to it. + /// How many layouts led to it, its own included. lays: u64, - /// Pages it holds that it no longer draws, and what their words were made from: the scene and - /// the table's data. - pub(crate) idle: u32, - pub(crate) content: u64, - /// What it was laid out from: the order, what it draws, the rows kept, and what ends hold. + /// Idle pages, ascending. + pub(crate) idle: Arc<[u32]>, + /// The scene version its rows' words were evaluated in. + pub(crate) scene: u64, + /// The stamp of the [`Lay`] it was laid out from (see [`Lay::stamp`]). pub(crate) laid: u64, - /// What tells it from every other drawing: what lists of its rows are kept by. + /// Unique to this drawing. Lists of its rows are keyed by it. pub(crate) made: Version, - /// The places the edges held or the rows drawn whatever their tiles name that the drawing before - /// it held elsewhere, ascending: the rows naming them find their ends again. With what tells them - /// from the moves of the drawings before, the drawing's before it while none moved. + /// The endpoint or pinned places the previous drawing held elsewhere, ascending. Edge rows that + /// reference them resolve their endpoints again. The version identifies this set of moves; it + /// carries over from the previous drawing when nothing moved. pub(crate) moved: (Version, Arc<[u32]>), - /// What its rows kept were laid out from: the rows drawn whatever their tiles, what the edges held - /// ask of it, and the pages it holds. A drawing laid out from the same keeps them where they were. + /// A stamp of the kept rows' inputs: pinned rows, endpoint needs, and resident pages. A drawing + /// with the same stamp reuses its predecessor's kept rows unchanged. kept_from: u64, - /// What tells how it holds rows: see [`Drawing::layout`]. + /// A stamp of how it holds rows; see [`Drawing::layout`]. layout: u64, } -/// Rows drawn whatever their tiles, as places in the order, ascending, and what tells them from other -/// such rows without reading them. +/// Pinned rows (drawn regardless of tiles) as order places, ascending, with a stamp to compare them +/// without reading them. #[derive(Clone, Default, Debug)] pub(crate) struct Pinned { pub(crate) places: Arc<[u32]>, stamp: u64, } -/// What a drawing is laid out from. +/// The inputs a drawing is laid out from. pub(crate) struct Lay<'a> { pub(crate) chosen: &'a Chosen, - /// Rows drawn whatever their tiles: those selected or framed by a fit, and those the edges kept end - /// at. + /// Rows drawn regardless of tiles: selected rows, rows framed by a fit, and endpoints of kept + /// edges. pub(crate) pinned: &'a Pinned, - /// What the ends of the edges held hold of it, not drawn: pages whole, and rows one at a time. + /// What resident edges need resident without drawing: whole pages, and individual rows. pub(crate) ends: &'a Ending, - /// Pages it may hold in all: those it no longer draws stay held while there is room. + /// How many pages it may keep resident in all. Needed pages are always resident; idle pages + /// fill whatever room is left. pub(crate) room: u32, - /// What the words of the rows it holds are made from: the scene and the table's data. Pages no - /// longer drawn stay held only while it is as it was, so none is made again unless drawn. - pub(crate) content: u64, + /// The scene version its rows' words are evaluated in. Idle pages are released when it changes. + pub(crate) scene: u64, + /// Pages with rows written since the last layout. Idle pages among them are released, so a page + /// is only rebuilt when it draws again. + pub(crate) written: &'a Written, +} + +/// The pages of an order with rows written between two versions of its table. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) enum Written { + #[default] + Nothing, + /// Whether each page has a written row. + Pages(Vec), + Every, +} + +impl Written { + /// The pages of `order` with rows written in `now` since `before`. Adding, dropping, renaming, + /// or retyping a field, or changing a sampled field, counts as every row written. + pub(crate) fn since(before: &Table, now: &Table, order: &Order) -> Written { + if before.fields.len() != now.fields.len() { + return Written::Every; + } + let mut rows = Vec::new(); + for ((name, field), (was_name, was)) in now.fields.iter().zip(&before.fields) { + let (column, old) = match (field, was) { + _ if name != was_name => return Written::Every, + (Field::Values(column, version), Field::Values(old, old_version)) if version != old_version => { + (column, old) + } + (Field::Reference(reference), Field::Reference(old)) => (&reference.rows, &old.rows), + (Field::Values(..), Field::Values(..)) => continue, + (Field::Sampled(_, version), Field::Sampled(_, old_version)) if version == old_version => continue, + _ => return Written::Every, + }; + if !column.shares(old) { + rows.extend(column.written_since(old)); + } + } + if rows.is_empty() { + return Written::Nothing; + } + if rows.len() >= order.len() as usize { + return Written::Every; + } + let mut pages = vec![false; order.pages() as usize]; + for row in rows { + if let Some(page) = pages.get_mut((order.rank(row as u32) / PAGE_ROWS) as usize) { + *page = true; + } + } + Written::Pages(pages) + } + + fn holds(&self, page: u32) -> bool { + match self { + Written::Nothing => false, + Written::Pages(pages) => pages.get(page as usize).copied().unwrap_or(false), + Written::Every => true, + } + } } impl Lay<'_> { - /// The stamp of what it lays out. + /// A stamp of the order, ranges, pinned rows, endpoint needs, and room. Excludes `scene` and + /// `written`, which only affect idle pages. pub(crate) fn stamp(&self) -> u64 { let chosen = self.chosen; stamp_of((chosen.order.id(), chosen.order.len(), &chosen.ranges, self.pinned.stamp, self.ends.stamp, self.room)) @@ -144,16 +209,16 @@ impl Lay<'_> { } impl Drawing { - /// What draws of `lay`: pages `before` holds kept in their slots, those it no longer draws while - /// `lay` leaves room, the rows kept whose pages it does not hold (those drawn whatever their tiles, - /// and the ends' rows held one at a time) in pages of their own, each in its place there, and with - /// `fades` the tiles arriving since fading in and those gone fading out. A drawing of another order - /// keeps nothing: its rows move, and a transition moves them. + /// Lays out the drawing of `lay`, starting from `before`. Pages resident in `before` keep their + /// slots, and its idle pages stay resident while `lay.room` allows. Kept rows (pinned rows and + /// sparse endpoints whose pages are not resident) go in pages of their own, each keeping its + /// position. With `fades`, tiles new since `before` fade in and tiles gone fade out. A `before` + /// of another order carries nothing over: every row moves, and a transition animates the move. pub(crate) fn lay(before: Option<&Drawing>, lay: &Lay<'_>, fades: bool) -> Drawing { let order = lay.chosen.order.clone(); let before = before.filter(|b| b.order.id() == order.id()); let ranges = lay.chosen.ranges.clone(); - // What the drawing before settles to: its ranges drawn, those it was fading out gone. + // The ranges `before` settles to: those it draws, without those fading out. let was: &[Range] = before.map_or(&[], |b| &b.ranges); let has = |list: &[Range], r: &Range| list.iter().any(|x| x == r); let fading = fades && before.is_some(); @@ -161,16 +226,16 @@ impl Drawing { if fading { ranges.iter().filter(|r| !has(was, r)).cloned().collect() } else { Vec::new() }; let leaving: Vec> = if fading { was.iter().filter(|r| !has(&ranges, r)).cloned().collect() } else { Vec::new() }; - // Pages held: those drawn, those fading out, and those many ends lie in; while there is room - // and their words hold, those held before and no longer asked for; and the rows kept whose - // pages it does not hold, in pages of their own. + // Resident pages: those drawn or fading out, and dense endpoint pages; idle pages of + // `before` while there is room and their words are still valid; and pages of kept rows. let count = order.pages() as usize; let asked_now = asked(count, ranges.iter().chain(&leaving), &lay.ends.pages); let mut held = asked_now.clone(); - let idle_kept = - before.filter(|b| b.content == lay.content).map_or(0, |b| linger(b, &asked_now, lay.room, &mut held)); - // The rows kept stay as they were while what they are laid out from does, as rows appended or - // written leave it: every page keeps its slot, so no row the ends name moves. + let idle = before + .filter(|b| b.scene == lay.scene) + .map_or_else(Vec::new, |b| linger(b, &asked_now, lay.room, lay.written, &mut held)); + // Kept rows stay unchanged while their inputs do, and appending or writing rows leaves + // those inputs unchanged. Every page also keeps its slot, so no endpoint moves. let held_pages: Vec = (0..held.len()).filter(|&p| held[p]).collect(); let kept_from = stamp_of((lay.pinned.stamp, lay.ends.stamp, held_pages)); let same = before.filter(|b| b.kept_from == kept_from); @@ -189,7 +254,7 @@ impl Drawing { .map(|(s, &slot)| match Holds::of(slot) { Holds::Free => 0, Holds::Page(page) if !asked_now[page as usize] => { - // An idle page kept its slot, and when it was asked for. + // An idle page keeps its slot and the layout count it was last needed at. before.and_then(|b| b.asked.get(s)).copied().unwrap_or(lays) } _ => lays, @@ -211,8 +276,8 @@ impl Drawing { leaving_items: 0, asked, lays, - idle: idle_kept, - content: lay.content, + idle: idle.into(), + scene: lay.scene, laid: lay.stamp(), made: Version::next(), moved: before.map_or_else(|| (Version::next(), Arc::default()), |b| b.moved.clone()), @@ -231,14 +296,14 @@ impl Drawing { drawing } - /// The drawing this one settles to once its tiles have faded: those arriving drawn steadily, - /// those leaving gone, their pages let go. + /// The drawing this one becomes once its fades finish: entering tiles draw steadily, and + /// leaving tiles stop drawing, so their pages are no longer needed. pub(crate) fn settled(&self, lay: &Lay<'_>) -> Drawing { Drawing::lay(Some(self), lay, false) } - /// Lays out its runs: the tiles fading out first, under the rest; then what it draws, finest - /// first, `entering` fading in; then the rows kept that no tile draws. + /// Builds the runs: tiles fading out first, so they draw underneath; then the drawn ranges, + /// finest first, with `entering` fading in; then `pinned` rows no range covers. fn run(&mut self, entering: &[Range], pinned: &[u32]) { let covered = |list: &[Range], row: u32| list.iter().any(|r| r.contains(&row)); let kept: Vec = pinned @@ -267,7 +332,7 @@ impl Drawing { }; let mut leaving_items = 0; for range in &self.leaving { - // A row kept leaves no gap: it draws steadily, with the rows kept. + // Skip pinned rows: they draw steadily with the other pinned rows instead. let mut start = range.start; for &row in pinned.iter().filter(|&&row| range.contains(&row)) { span(start..row, wgsl::FADE_OUT, &mut runs); @@ -291,22 +356,22 @@ impl Drawing { self.leaving_items = leaving_items; } - /// Held rows: a page's rows in each slot. + /// The number of held rows: `PAGE_ROWS` per slot, free slots included. pub(crate) fn live(&self) -> u32 { self.slots.len() as u32 * PAGE_ROWS } - /// How many slots it has, free ones among them. + /// The number of slots, free ones included. pub(crate) fn slot_count(&self) -> usize { self.slots.len() } - /// What slot `s` holds. + /// The contents of slot `s`. pub(crate) fn holds(&self, s: usize) -> Holds { Holds::of(self.slots.get(s).copied().unwrap_or(wgsl::NONE)) } - /// The places in the order of the rows slot `s` holds, `NONE` where a place holds none. + /// The order places of the rows in slot `s`, `NONE` for empty positions. pub(crate) fn places(&self, s: usize) -> Vec { match self.holds(s) { Holds::Free => Vec::new(), @@ -315,16 +380,16 @@ impl Drawing { } } - /// The places in the order of the rows kept slot `s` holds, `NONE` where a place holds none; none - /// for a slot holding no rows kept. + /// The order places of the kept rows in slot `s`, `NONE` for empty positions. Empty unless the + /// slot holds kept rows. pub(crate) fn kept_places(&self, s: usize) -> &[u32] { let Holds::Kept(j) = self.holds(s) else { return &[] }; let start = ((j * PAGE_ROWS) as usize).min(self.kept.len()); &self.kept[start..self.kept.len().min(start + PAGE_ROWS as usize)] } - /// The rows of its table slot `s` holds, in their places: what a page of its columns packs from; - /// `NONE` where a place holds no row. + /// The table rows in slot `s`, in slot order, `NONE` for empty positions. A page of each column + /// is packed from them. pub(crate) fn rows(&self, s: usize) -> Vec { match self.holds(s) { Holds::Free => Vec::new(), @@ -336,7 +401,8 @@ impl Drawing { } } - /// The held row of place `d` in the order, if it holds it: in its page, or among the rows kept. + /// The held row of order place `d`, from its resident page or the kept rows; `None` if not + /// resident. pub(crate) fn held_of(&self, d: u32) -> Option { let slot = *self.pages.get((d / PAGE_ROWS) as usize)?; if slot != wgsl::NONE { @@ -345,12 +411,12 @@ impl Drawing { self.kept_table.held(d) } - /// The held row of row `r` of its table, if it holds it. + /// The held row of table row `r`, if resident. pub(crate) fn held_row(&self, r: u32) -> Option { (r < self.order.len()).then(|| self.held_of(self.order.rank(r))).flatten() } - /// The row of its table held row `h` holds. + /// The table row in held row `h`, if any. pub(crate) fn source(&self, h: u32) -> Option { let d = match self.holds((h / PAGE_ROWS) as usize) { Holds::Free => return None, @@ -360,14 +426,14 @@ impl Drawing { (d != wgsl::NONE && d < self.order.len()).then(|| self.order.source(d)) } - /// Where held row `h` draws among the rows drawn, if it does. + /// The index of held row `h` among the rows drawn, if it is drawn. pub(crate) fn item_of(&self, h: u32) -> Option { let i = self.by_held.partition_point(|&r| self.runs[r as usize].held <= h).checked_sub(1)?; let run = self.runs[self.by_held[i] as usize]; (h < run.held + run.len).then_some(run.item + h - run.held) } - /// The pages of its order it holds. + /// The resident pages of its order. pub(crate) fn held_pages(&self) -> impl Iterator + '_ { (0..self.slots.len()).filter_map(|s| match self.holds(s) { Holds::Page(page) => Some(page), @@ -375,26 +441,26 @@ impl Drawing { }) } - /// The rows of its table it keeps drawn whatever their tiles, whose pages it does not hold. + /// The pinned rows whose pages are not resident, as table rows. pub(crate) fn kept(&self) -> impl Iterator + '_ { let paged = |d: u32| self.pages.get((d / PAGE_ROWS) as usize).is_some_and(|&slot| slot != wgsl::NONE); self.pinned.iter().filter(move |&&d| d < self.order.len() && !paged(d)).map(|&d| self.order.source(d)) } - /// What tells how it holds rows: its order, what each slot holds, and where it keeps rows. A - /// column's pages follow it. + /// A stamp of how it holds rows: its order, each slot's contents, and the kept rows' positions. + /// Column tables are rebuilt when it changes. pub(crate) fn layout(&self) -> u64 { self.layout } - /// Whether slot `s` holds the same rows in `other` as in it. + /// Whether slot `s` holds the same rows in `other` as in this drawing. pub(crate) fn holds_alike(&self, other: &Drawing, s: usize) -> bool { self.slots.get(s) == other.slots.get(s) && (!matches!(self.holds(s), Holds::Kept(_)) || self.places(s) == other.places(s)) } } -/// The pages of `count` asked for: those `ranges` draw, and `ends`. +/// For each of `count` pages, whether it is needed: the pages `ranges` draw, and `ends`. fn asked<'r>(count: usize, ranges: impl Iterator>, ends: &[u32]) -> Vec { let mut asked = vec![false; count]; for range in ranges.filter(|r| r.start < r.end) { @@ -412,9 +478,9 @@ fn asked<'r>(count: usize, ranges: impl Iterator>, ends: & asked } -/// The table of the rows `kept`, by their places among the rows kept, held in the pages of rows kept -/// `kept_slots` says, laid out from the one `before` keeps: the rows whose places changed come and go, -/// and every other keeps its bucket. +/// The kept-row table of `kept` (order places by kept position), whose kept pages lie in the slots +/// `kept_slots` gives. It is updated from `before`'s table: only positions whose place changed are +/// removed and inserted, so every other row keeps its bucket. fn keep_table(before: Option<&Drawing>, kept: &[u32], kept_slots: &[u32]) -> KeptTable { let held = |i: usize| kept_slots[i / PAGE_ROWS as usize] * PAGE_ROWS + (i as u32) % PAGE_ROWS; let was: &[u32] = before.map_or(&[], |b| &b.kept); @@ -432,13 +498,14 @@ fn keep_table(before: Option<&Drawing>, kept: &[u32], kept_slots: &[u32]) -> Kep KeptTable::laid(before.map_or(&KeptTable::default(), |b| &b.kept_table), &rows, &gone, &came) } -/// The places `ends` or `pinned` name that `before` held and `now` holds elsewhere or not at all, -/// ascending: the rows naming them find their ends again. A page held in both keeps its slot, and a -/// row kept in both its place among the rows kept, so only rows of a page held in one alone move: -/// those named of a page `before` held, and those `before` kept of a page `now` holds. A row `before` -/// did not hold is named only by rows new to the edges, which find their ends as they arrive. +/// The places in `ends` or `pinned` that `before` held and `now` holds elsewhere or not at all, +/// ascending. Edge rows that reference them resolve their endpoints again. A page resident in both +/// keeps its slot, and a row kept in both keeps its kept position, so only rows of a page resident +/// in just one drawing move: listed rows of pages `before` held and `now` does not, and rows +/// `before` kept whose pages `now` holds. A row `before` did not hold is referenced only by edge +/// rows new to the edge drawing, which resolve their endpoints when they arrive. fn moved(before: &Drawing, now: &Drawing, ends: &Ending, pinned: &[u32]) -> Vec { - /// Those of `places`, ascending, lying in page `p`. + /// The subslice of ascending `places` that lies in page `p`. fn within(places: &[u32], p: u32) -> &[u32] { let start = places.partition_point(|&d| d < p * PAGE_ROWS); &places[start..places.partition_point(|&d| d < (p + 1) * PAGE_ROWS)] @@ -464,25 +531,31 @@ fn moved(before: &Drawing, now: &Drawing, ends: &Ending, pinned: &[u32]) -> Vec< moved } -/// Holds the pages `before` held that are no longer `asked` for while `room` leaves room, those -/// asked for latest first: how many it holds. -fn linger(before: &Drawing, asked: &[bool], room: u32, held: &mut [bool]) -> u32 { +/// Keeps idle pages resident: pages `before` held that are no longer `asked` for and have no +/// `written` row, most recently needed first, up to `room` resident pages in all. Marks them in +/// `held` and returns them ascending. +fn linger(before: &Drawing, asked: &[bool], room: u32, written: &Written, held: &mut [bool]) -> Vec { let wanted = asked.iter().filter(|&&a| a).count() as u32; let mut idle: Vec<(u64, u32)> = (0..before.slots.len()) .filter_map(|s| match before.holds(s) { - Holds::Page(page) if !asked.get(page as usize).copied().unwrap_or(true) => Some((before.asked[s], page)), + Holds::Page(page) if !asked.get(page as usize).copied().unwrap_or(true) && !written.holds(page) => { + Some((before.asked[s], page)) + } _ => None, }) .collect(); idle.sort_unstable_by(|a, b| b.cmp(a)); - let kept = idle.iter().take(room.saturating_sub(wanted) as usize); - kept.clone().for_each(|&(_, page)| held[page as usize] = true); - kept.count() as u32 + let mut kept: Vec = idle.iter().take(room.saturating_sub(wanted) as usize).map(|&(_, page)| page).collect(); + for &page in &kept { + held[page as usize] = true; + } + kept.sort_unstable(); + kept } -/// The rows of `pinned` and of `ends`, places in `order`, whose pages are not `held`, by their places -/// among the rows kept, `NONE` where a place is free: each keeps its place in `before`, and the rest -/// take the first free ones. +/// The kept rows: the places in `order` from `pinned` and `ends` whose pages are not `held`, +/// indexed by kept position, `NONE` for a free position. Rows already kept in `before` keep their +/// positions; the rest take the first free ones. fn keep(before: Option<&Drawing>, pinned: &[u32], ends: &[u32], held: &[bool], order: &Order) -> Vec { let mut out: Vec = pinned.iter().chain(ends).copied().filter(|&d| d < order.len() && !held[(d / PAGE_ROWS) as usize]).collect(); @@ -511,9 +584,9 @@ fn keep(before: Option<&Drawing>, pinned: &[u32], ends: &[u32], held: &[bool], o kept } -/// Slots for the pages `held` and `kept_pages` pages of rows kept, each keeping the slot it had in -/// `before`, the rest taking the first free ones: the slots, each page's slot, and each page of rows -/// kept's slot. +/// Assigns slots to the `held` pages and to `kept_pages` pages of kept rows. Each keeps its slot +/// from `before`; the rest take the first free slots. Returns each slot's contents, each page's +/// slot, and each kept page's slot. fn place(before: Option<&Drawing>, held: &[bool], kept_pages: usize) -> (Vec, Vec, Vec) { let mut slots: Vec = before.map_or_else(Vec::new, |b| b.slots.to_vec()); let mut pages = vec![wgsl::NONE; held.len()]; @@ -550,33 +623,33 @@ fn place(before: Option<&Drawing>, held: &[bool], kept_pages: usize) -> (Vec)>>, - /// What the edges held asked last, and the stamp of what it was worked out from. + /// The last [`Ending`] computed, with the stamp of its inputs. asked: (u64, Arc), } -/// The places a page of edges ends at, ascending, each once, and for each page of the order they lie -/// in, the run of them there: the page, and where its places start and end. +/// The distinct endpoint places of one page of edges, ascending, and for each vertex page they fall +/// in, a run `[page, start, end]` indexing into `places`. struct Places { places: Vec, runs: Vec<[u32; 3]>, } impl Ends { - /// What the edges `drawing` holds ask of the table they end at by `ends`, which draws in - /// `target`: the pages many of their ends lie in, the rest of the rows they end at, and the rows - /// the edges kept end at; worked out again only where the pages the drawing holds or what it keeps - /// changed. An edge page's ends count once each toward their page's, so a page whose rows the ends - /// of many edge pages share may be held whole for fewer rows. + /// What the edges resident in `drawing` need of the vertex table they reference through `ends`, + /// which draws in `target`: dense pages, sparse endpoint rows, and the endpoints of kept edges. + /// Recomputed only when the drawing's layout or the vertex table's length changes. Each edge + /// page counts its distinct endpoints once toward their vertex page, so a vertex page that many + /// edge pages share may be made resident whole with fewer than `DENSE` distinct rows in use. fn ending(&mut self, drawing: &Drawing, target: &Order, ends: &[&Reference]) -> Arc { let stamp = stamp_of((drawing.order.id(), target.id(), ends.iter().map(|r| r.version).collect::>())); if stamp != self.stamp { @@ -603,7 +676,7 @@ impl Ends { } rows.sort_unstable(); rows.dedup(); - // The rows the edges kept end at, drawn whatever their pages. + // Endpoints of kept edges, drawn regardless of their pages. let mut kept: Vec = Vec::new(); for row in drawing.kept() { for reference in ends { @@ -617,17 +690,18 @@ impl Ends { self.asked.1.clone() } - /// Whether the ends of edge page `page` lie at any of `places`, ascending; so where the page was - /// never worked out. + /// Whether any endpoint of edge page `page` is among `places` (ascending). True if the page was + /// never computed. fn lie_in(&self, page: u32, places: &[u32]) -> bool { let Some(Some((_, found))) = self.pages.get(page as usize) else { return true }; - // Each of the fewer looked for among the more. + // Binary-search the longer list for each item of the shorter. let (few, many) = if found.places.len() < places.len() { (&found.places[..], places) } else { (places, &found.places[..]) }; few.iter().any(|d| many.binary_search(d).is_ok()) } - /// The places in `target` that edge page `page` of `order` ends at, by `ends`. + /// The endpoint places in `target` of edge page `page` of `order`, through `ends`. Cached until + /// the page's row count changes. fn of(&mut self, order: &Order, target: &Order, ends: &[&Reference], page: u32) -> Arc { if self.pages.len() <= page as usize { self.pages.resize_with(page as usize + 1, || None); @@ -662,15 +736,15 @@ impl Ends { } } -/// What the edges held ask of the table they end at, as places in its order, ascending: the pages -/// many of their ends lie in, held whole, and the rest of the rows they end at, held one at a time, -/// neither drawn; and the rows the edges kept end at, drawn whatever their pages. +/// What resident edges need of the vertex table, as places in its order, ascending: dense `pages` +/// to make resident whole and the remaining endpoint `rows` to keep individually, neither drawn; +/// and the endpoints of `kept` edges, drawn regardless of their pages. #[derive(Default, Debug)] pub(crate) struct Ending { pages: Vec, rows: Vec, kept: Vec, - /// What tells the pages and rows held from others without reading them. + /// A stamp of `pages`, `rows`, and `kept`, to compare them without reading them. stamp: u64, } @@ -680,7 +754,7 @@ impl Ending { Ending { pages, rows, kept, stamp } } - /// What `endings` ask together. + /// The union of `endings`. fn merged(endings: &[Arc]) -> Ending { let all = |part: fn(&Ending) -> &Vec| { let mut all: Vec = endings.iter().flat_map(|e| part(e).iter().copied()).collect(); @@ -692,10 +766,10 @@ impl Ending { } } -/// What draws of a table drawn in part: the drawing drawn, the one replacing it once every page it -/// needs is on the GPU, when the one drawn was first drawn on the view's clock, what its edges ask of -/// the table they end at, what the edges ending at it ask of it together, and the rows it draws -/// whatever their tiles. +/// The drawing state of one paged table: the `current` drawing, the `next` one that replaces it +/// once every page it needs is on the GPU, and the view-clock time `since` the current one first +/// drew. Also caches its edges' needs of their vertex table (`ends`), the merged needs of edge +/// tables ending at this table (`ending`), and its pinned rows. #[derive(Default)] pub(crate) struct Drawn { pub(crate) current: Option>, @@ -704,16 +778,18 @@ pub(crate) struct Drawn { ends: Ends, ending: (u64, Arc), pinned: Pinned, + /// The table as of the last frame, to find which rows were written since. + table: Option

    , } impl Drawn { - /// The drawing marks sync to: the one replacing what draws, else what draws. + /// The drawing marks sync to: `next` if any, else `current`. pub(crate) fn latest(&self) -> Option<&Arc> { self.next.as_ref().or(self.current.as_ref()) } - /// The slots of `drawing`, of this table's edges, whose rows end at any of `places` of the table - /// they end at, ascending: each page by where its ends lie, and every page of rows kept. + /// The slots of `drawing` (this table's edge drawing) whose rows end at any of `places` in the + /// vertex table, ascending: resident pages by their cached endpoints, and every kept page. pub(crate) fn naming(&self, drawing: &Drawing, places: &[u32]) -> Vec { (0..drawing.slot_count()) .filter(|&s| match drawing.holds(s) { @@ -725,8 +801,8 @@ impl Drawn { .collect() } - /// What every edge table drawn ending at it asks of it, as one; merged again only where any - /// changed. + /// The merged needs of every edge table ending at this table, merged again only when one + /// changes. fn ending(&mut self, endings: &[Arc]) -> Arc { match endings { [] => Arc::default(), @@ -741,8 +817,8 @@ impl Drawn { } } - /// The rows drawn whatever their tiles: `own`, those selected or framed, and those `ending` keeps; - /// gathered again only where those changed. + /// The pinned rows: `own` (selected or framed by a fit) and the endpoints of kept edges in + /// `ending`. Rebuilt only when those change. fn pin(&mut self, own: Vec, ending: &Ending) -> Pinned { let stamp = stamp_of((&own, &ending.kept)); if stamp != self.pinned.stamp { @@ -757,16 +833,16 @@ impl Drawn { } impl View { - /// Lays out what draws of each table drawn in part, from what the camera chose: a drawing to - /// replace what draws where what it is laid out from changed, or where what draws has faded in. - /// A table no longer drawn in part draws as it is. + /// Lays out each paged table's drawing from the camera's choices. A new drawing replaces the + /// current one when its inputs changed, or when the current one finished fading. A table no + /// longer paged loses its drawing and draws whole, in its own order. pub(super) fn lay_out_drawings(&mut self) { let choices = std::mem::take(&mut self.choices); self.drawings.retain(|key, _| choices.contains_key(key)); let fades = !self.ease().is_zero(); let ease = self.ease().as_secs_f64(); - // Edges first: the vertices their ends name are held with them, and the rows edges kept end - // at drawn with them. + // Edge tables go first, so their vertex tables know which endpoints to make resident and + // which kept-edge endpoints to draw. let mut keys: Vec<&(Name, Name)> = choices.keys().collect(); crate::order::by_key(&mut keys, |key| { u64::from(!matches!(self.planning.plans().get(*key).map(|p| &p.place), Some(Place::Ends(_)))) @@ -776,16 +852,24 @@ impl View { let chosen = &choices[key]; let endings = ends.remove(key).unwrap_or_default(); let own = self.pinned(key, &chosen.order); - let (content, room) = (self.content(key), self.room(key)); + let (scene, room) = (self.scene_version, self.room(key)); + let table = self.snapshot(&key.0).and_then(|d| d.table(&key.1)).cloned(); let drawn = self.drawings.entry(key.clone()).or_default(); + let written = match (&drawn.table, &table) { + (Some(before), Some(now)) => Written::since(before, now, &chosen.order), + _ => Written::Every, + }; + drawn.table = table; let ending = drawn.ending(&endings); let pinned = drawn.pin(own, &ending); - let lay = Lay { chosen, pinned: &pinned, ends: &ending, room, content }; + let lay = Lay { chosen, pinned: &pinned, ends: &ending, room, scene, written: &written }; let stamp = lay.stamp(); let current = drawn.current.clone(); let latest = drawn.next.as_ref().or(current.as_ref()); - // Pages no longer drawn are let go once their words would be made again. - let stale = latest.is_some_and(|d| d.idle > 0 && d.content != content); + // Idle pages are released when the scene changes or one of their rows is written. + let stale = latest.is_some_and(|d| { + !d.idle.is_empty() && (d.scene != scene || d.idle.iter().any(|&page| written.holds(page))) + }); if latest.map(|d| d.laid) != Some(stamp) || stale { drawn.next = Some(Arc::new(Drawing::lay(current.as_deref(), &lay, fades))); } else if drawn.next.is_none() @@ -793,7 +877,7 @@ impl View { { drawn.next = Some(Arc::new(current.settled(&lay))); } - // The vertices the edges this drawing holds end at, held with their vertices. + // Record what this table's resident edges need of the vertex table they end at. let Some(place) = self.planning.plans().get(key).map(|p| &p.place) else { continue }; let Place::Ends(fields) = place else { continue }; let Some(table) = self.snapshots.iter().find(|d| *d.name() == key.0).and_then(|d| d.table(&key.1)) else { @@ -817,7 +901,8 @@ impl View { self.choices = choices; } - /// How far the tiles of each mark's table drawn in part have faded in or out this frame. + /// Sets each mark's `reveal` for this frame: how far its paged table's tiles have faded in or + /// out, from 0 to 1; 1 when they are not fading. pub(super) fn reveal(&mut self) { let ease = self.ease().as_secs_f64(); for shown in &mut self.marks { @@ -831,8 +916,8 @@ impl View { } } - /// What each table of lines or areas drawn in part draws this frame, which their shapes follow: - /// the pages of its order the camera chose, and those holding the rows drawn whatever their tiles. + /// What each paged table of lines or areas draws this frame, for shaping: the pages of its + /// order the camera chose, and the pinned rows outside those pages. pub(super) fn paged(&self) -> crate::data::Keyed<(Name, Name), crate::derive::Paged> { let shaped = |(data, table): &(Name, Name)| { self.scene.marks.iter().any(|mark| { @@ -856,15 +941,8 @@ impl View { paged } - /// What the words of the rows of `key`'s table are made from: the scene, and the table's data. - fn content(&self, key: &(Name, Name)) -> u64 { - let table = self.snapshot(&key.0).and_then(|d| d.table(&key.1)); - let data = table.map(|t| (t.structure, t.fields.iter().map(|(_, f)| f.version()).collect::>())); - stamp_of((self.scene_version, data)) - } - - /// Pages the drawing of `key`'s table may hold in all: for a table drawn through tiles, as many as - /// the rows the plan gives it fill; else none past those it draws. + /// How many pages `key`'s drawing may keep resident in all: for a table drawn by tiles, as many + /// as the plan's rows fill; otherwise 0, so no idle page stays. fn room(&self, key: &(Name, Name)) -> u32 { match self.planning.plans().get(key).map(|p| p.draw) { Some(Draw::Tiles { rows }) => rows.div_ceil(PAGE_ROWS), @@ -872,8 +950,8 @@ impl View { } } - /// The places in `order` of the rows of `key`'s table drawn whatever their tiles: those selected, - /// and those a fit frames, ascending. + /// The places in `order` of `key`'s rows drawn regardless of tiles: selected rows and rows a + /// fit frames, ascending. fn pinned(&self, key: &(Name, Name), order: &Order) -> Vec { let Some(table) = self.snapshot(&key.0).and_then(|d| d.table(&key.1)) else { return Vec::new() }; let mut rows: Vec = self @@ -904,16 +982,31 @@ mod tests { Chosen { order: order.clone(), ranges: ranges.to_vec(), capped: false } } - /// Rows drawn whatever their tiles: `places`. + /// Pinned rows at `places`. fn pinned(places: &[u32]) -> Pinned { Pinned { places: places.into(), stamp: stamp_of(places) } } - /// What draws one range of `order`. + /// A choice drawing one range of `order`. fn one(order: &Order, range: Range) -> Chosen { Chosen { order: order.clone(), ranges: vec![range], capped: false } } + #[test] + fn written_rows_mark_the_pages_that_contain_them() { + let order = order(3 * PAGE_ROWS); + let n = 3 * PAGE_ROWS as usize; + let data = crate::data::Data::new("d"); + data.apply(Table::new("T", crate::data::Ids::count(n as u32)).field("v", vec![0.0; n])).expect("valid"); + let table = || data.snapshot().table("T").expect("a table").clone(); + let before = table(); + assert_eq!(Written::since(&before, &table(), &order), Written::Nothing, "nothing written"); + data.apply(Table::new("T", vec![u64::from(PAGE_ROWS) + 5]).field("v", vec![1.0])).expect("valid"); + assert_eq!(Written::since(&before, &table(), &order), Written::Pages(vec![false, true, false])); + data.apply(Table::new("T", vec![0_u64]).field("w", vec![1.0])).expect("valid"); + assert_eq!(Written::since(&before, &table(), &order), Written::Every, "a new field marks every row written"); + } + #[test] fn pages_keep_their_slots_as_tiles_come_and_go() { let order = order(8 * PAGE_ROWS); @@ -924,13 +1017,14 @@ mod tests { pinned: &Pinned::default(), ends: &Ending::default(), room: 0, - content: 0, + scene: 0, + written: &Written::Nothing, }, false, ); assert_eq!(&*first.slots, [0, 3]); assert_eq!(first.items, 2 * PAGE_ROWS); - // Page 0 leaves, page 5 arrives: it takes the slot page 0 left, and page 3 stays where it was. + // Page 0 leaves and page 5 arrives: page 5 takes page 0's slot, and page 3 keeps its own. let next = Drawing::lay( Some(&first), &Lay { @@ -938,7 +1032,8 @@ mod tests { pinned: &Pinned::default(), ends: &Ending::default(), room: 0, - content: 0, + scene: 0, + written: &Written::Nothing, }, false, ); @@ -949,7 +1044,7 @@ mod tests { } #[test] - fn pages_no_longer_drawn_stay_held_while_there_is_room() { + fn idle_pages_stay_resident_while_there_is_room() { let order = order(8 * PAGE_ROWS); let lay = |ranges: &[Range], room: u32, before: Option<&Drawing>| { Drawing::lay( @@ -959,34 +1054,49 @@ mod tests { pinned: &Pinned::default(), ends: &Ending::default(), room, - content: 0, + scene: 0, + written: &Written::Nothing, }, false, ) }; let first = lay(&[0..PAGE_ROWS, PAGE_ROWS..2 * PAGE_ROWS], 3, None); assert_eq!(&*first.slots, [0, 1]); - // Page 1 is no longer drawn and page 4 is: with room for three pages, page 1 stays held. + // Page 1 stops drawing and page 4 starts. With room for three pages, page 1 stays resident. let next = lay(&[0..PAGE_ROWS, 4 * PAGE_ROWS..5 * PAGE_ROWS], 3, Some(&first)); - assert_eq!((&*next.slots, next.items), (&[0, 1, 4][..], 2 * PAGE_ROWS), "page 1 held, not drawn"); - // Drawn again, it draws from its slot: no slot takes a page of other rows. + assert_eq!((&*next.slots, next.items), (&[0, 1, 4][..], 2 * PAGE_ROWS), "page 1 is idle: resident, not drawn"); + // Drawn again, page 1 draws from its old slot, so no slot takes a page of other rows. let back = lay(&[0..PAGE_ROWS, PAGE_ROWS..2 * PAGE_ROWS], 3, Some(&next)); assert_eq!(&*back.slots, [0, 1, 4]); assert_eq!(crate::view::transition::bound(Some(&next), &back), Vec::::new()); - // With room for two, of the pages no longer drawn the one asked for latest stays. + // With room for two, the most recently needed idle page stays. let tight = lay(std::slice::from_ref(&(6 * PAGE_ROWS..7 * PAGE_ROWS)), 2, Some(&back)); assert_eq!(&*tight.slots, [6, 1]); - // Their words made from other data or another scene, pages no longer drawn are let go. + // In another scene, idle pages are released. let ranges = [0..PAGE_ROWS, 4 * PAGE_ROWS..5 * PAGE_ROWS]; let changed = Lay { chosen: &chosen(&order, &ranges), pinned: &Pinned::default(), ends: &Ending::default(), room: 3, - content: 1, + scene: 1, + written: &Written::Nothing, }; let changed = Drawing::lay(Some(&back), &changed, false); - assert_eq!((&*changed.slots, changed.idle), (&[0, wgsl::NONE, 4][..], 0)); + assert_eq!((&*changed.slots, &*changed.idle), (&[0, wgsl::NONE, 4][..], &[][..])); + // A row written in an idle page releases that page and no other. + let mut pages = vec![false; 8]; + pages[4] = true; + let written = Lay { + chosen: &chosen(&order, std::slice::from_ref(&(0..PAGE_ROWS))), + pinned: &Pinned::default(), + ends: &Ending::default(), + room: 3, + scene: 0, + written: &Written::Pages(pages), + }; + let written = Drawing::lay(Some(&back), &written, false); + assert_eq!((&*written.slots, &*written.idle), (&[0, 1][..], &[1][..])); } #[test] @@ -999,7 +1109,8 @@ mod tests { pinned: &Pinned::default(), ends: &Ending::default(), room: 0, - content: 0, + scene: 0, + written: &Written::Nothing, }, true, ); @@ -1009,12 +1120,13 @@ mod tests { pinned: &pinned(&[3]), ends: &Ending::default(), room: 0, - content: 0, + scene: 0, + written: &Written::Nothing, }; let next = Drawing::lay(Some(&first), &lay, true); assert!(next.fades); let fades: Vec<(u32, u32)> = next.runs.iter().map(|r| (r.len, r.fade)).collect(); - // Rows 0..10 fade out but row 3, kept, which draws steadily after the tile arriving. + // Rows 0..10 fade out except pinned row 3, which draws steadily after the entering tile. assert_eq!(fades, [(3, wgsl::FADE_OUT), (6, wgsl::FADE_OUT), (10, wgsl::FADE_IN), (1, 0)]); assert_eq!(next.leaving_items, 9); let settled = next.settled(&lay); @@ -1024,81 +1136,96 @@ mod tests { } #[test] - fn ends_lying_sparse_are_held_a_row_at_a_time() { + fn sparse_endpoints_are_kept_individually() { let order = order(4 * PAGE_ROWS); - // Many ends lie in page 3, two in page 2: page 3 is held whole, the two rows one at a time, - // and none draws. + // Many endpoints lie in page 3 and two in page 2: page 3 is resident whole, the two rows + // are kept individually, and none draws. let ends = Ending::new(vec![3], vec![2 * PAGE_ROWS + 5, 2 * PAGE_ROWS + 9], Vec::new()); - let lay = Lay { chosen: &one(&order, 0..10), pinned: &Pinned::default(), ends: &ends, room: 0, content: 0 }; + let lay = Lay { + chosen: &one(&order, 0..10), + pinned: &Pinned::default(), + ends: &ends, + room: 0, + scene: 0, + written: &Written::Nothing, + }; let drawing = Drawing::lay(None, &lay, false); - assert_eq!(&*drawing.slots, [0, 3, KEPT], "the page drawn, the ends' page, and a page of the ends' rows"); - assert_eq!(drawing.items, 10, "what ends hold is not drawn"); + assert_eq!(&*drawing.slots, [0, 3, KEPT], "the drawn page, the endpoint page, and a page of kept rows"); + assert_eq!(drawing.items, 10, "rows resident only for endpoints are not drawn"); assert_eq!(drawing.held_of(2 * PAGE_ROWS + 9), Some(2 * PAGE_ROWS + 1)); - assert_eq!(drawing.held_of(2 * PAGE_ROWS + 6), None, "rows no end names are not held"); - // Another end held: the rows held before keep their places. + assert_eq!(drawing.held_of(2 * PAGE_ROWS + 6), None, "rows no endpoint references are not resident"); + // Another endpoint is added: the rows kept before keep their positions. let more = Ending::new(vec![3], vec![PAGE_ROWS, 2 * PAGE_ROWS + 5, 2 * PAGE_ROWS + 9], Vec::new()); let next = Drawing::lay(Some(&drawing), &Lay { ends: &more, ..lay }, false); assert_eq!(next.held_of(2 * PAGE_ROWS + 9), Some(2 * PAGE_ROWS + 1)); assert_eq!(next.held_of(PAGE_ROWS), Some(2 * PAGE_ROWS + 2)); - assert_eq!(next.moved, drawing.moved, "no row ends name moved"); - // Rows appended to the order leave what the rows kept are laid out from: they stay as they were. + assert_eq!(next.moved, drawing.moved, "no endpoint moved"); + // Appending rows to the order changes no kept-row input, so the kept rows stay unchanged. let Order::Pyramid { pyramid, .. } = &order else { unreachable!("a pyramid") }; let grown = Order::Pyramid { pyramid: pyramid.clone(), rows: 4 * PAGE_ROWS + 1 }; let appended = Drawing::lay(Some(&next), &Lay { chosen: &one(&grown, 0..10), ends: &more, ..lay }, false); - assert!(Arc::ptr_eq(&appended.kept, &next.kept), "the rows kept as they were"); + assert!(Arc::ptr_eq(&appended.kept, &next.kept), "the kept rows are unchanged"); assert_eq!((&appended.kept_table, &appended.moved), (&next.kept_table, &next.moved)); - // Page 2 drawn: the rows of it held one at a time move into it, and the edges ending there find - // them again; the row kept of page 1 stays where it was. + // Page 2 is drawn: its individually kept rows move into it, and the edges ending there + // resolve their endpoints again. The kept row of page 1 stays put. let drawn = Lay { chosen: &chosen(&order, &[0..10, 2 * PAGE_ROWS..2 * PAGE_ROWS + 10]), ends: &more, ..lay }; let paged = Drawing::lay(Some(&next), &drawn, false); assert_eq!(paged.held_of(2 * PAGE_ROWS + 9), Some(paged.pages[2] * PAGE_ROWS + 9)); assert_eq!(paged.held_of(PAGE_ROWS), next.held_of(PAGE_ROWS)); assert_ne!(paged.moved.0, next.moved.0); - assert_eq!(&*paged.moved.1, [2 * PAGE_ROWS + 5, 2 * PAGE_ROWS + 9], "the rows of page 2 named moved"); + assert_eq!(&*paged.moved.1, [2 * PAGE_ROWS + 5, 2 * PAGE_ROWS + 9], "page 2's kept rows are listed as moved"); } #[test] - fn edges_find_their_ends_again_where_their_pages_name_what_moved() { + fn edge_pages_ending_at_moved_rows_resolve_their_endpoints_again() { let order = order(3 * PAGE_ROWS); let whole = one(&order, 0..3 * PAGE_ROWS); - let lay = Lay { chosen: &whole, pinned: &Pinned::default(), ends: &Ending::default(), room: 0, content: 0 }; + let lay = Lay { + chosen: &whole, + pinned: &Pinned::default(), + ends: &Ending::default(), + room: 0, + scene: 0, + written: &Written::Nothing, + }; let edges = Drawing::lay(None, &lay, false); let at = |places: &[u32]| Some((PAGE_ROWS, Arc::new(Places { places: places.to_vec(), runs: Vec::new() }))); let pages = vec![at(&[3, 9]), at(&[PAGE_ROWS + 1]), at(&[7, 2 * PAGE_ROWS])]; let drawn = Drawn { ends: Ends { pages, ..Ends::default() }, ..Drawn::default() }; - assert_eq!(drawn.naming(&edges, &[9]), [0], "the page of edges ending there"); + assert_eq!(drawn.naming(&edges, &[9]), [0], "the page of edges ending at place 9"); assert_eq!(drawn.naming(&edges, &[7, PAGE_ROWS + 1]), [1, 2]); - assert!(drawn.naming(&edges, &[5]).is_empty(), "no edge held ends there"); + assert!(drawn.naming(&edges, &[5]).is_empty(), "no resident edge ends at place 5"); } #[test] - fn rows_kept_lie_in_pages_of_their_own_and_ends_hold_their_pages() { + fn kept_rows_fill_pages_of_their_own_and_endpoint_pages_are_resident() { let order = order(4 * PAGE_ROWS); let lay = Lay { chosen: &one(&order, 0..10), pinned: &pinned(&[2 * PAGE_ROWS + 1]), ends: &Ending::new(vec![3], Vec::new(), Vec::new()), room: 0, - content: 0, + scene: 0, + written: &Written::Nothing, }; let drawing = Drawing::lay(None, &lay, false); - assert_eq!(&*drawing.slots, [0, 3, KEPT], "the page drawn, the ends', and a page of the row kept"); - assert_eq!(drawing.items, 11, "ends' pages are held, not drawn"); + assert_eq!(&*drawing.slots, [0, 3, KEPT], "the drawn page, the endpoint page, and a page of kept rows"); + assert_eq!(drawing.items, 11, "endpoint pages are resident, not drawn"); let held = drawing.held_of(2 * PAGE_ROWS + 1).expect("kept"); assert_eq!((held, drawing.source(held)), (2 * PAGE_ROWS, Some(2 * PAGE_ROWS + 1))); assert_eq!(drawing.held_pages().collect::>(), [0, 3]); - // Another row kept takes the next place among them; the first keeps its own. + // Another pinned row takes the next kept position; the first keeps its own. let more = Lay { pinned: &pinned(&[PAGE_ROWS + 7, 2 * PAGE_ROWS + 1]), ..lay }; let next = Drawing::lay(Some(&drawing), &more, false); assert_eq!( (next.held_of(2 * PAGE_ROWS + 1), next.held_of(PAGE_ROWS + 7)), (Some(2 * PAGE_ROWS), Some(2 * PAGE_ROWS + 1)) ); - assert_eq!(crate::view::transition::bound(Some(&drawing), &next), [2], "only the page of rows kept"); - // A row kept whose page comes to be drawn draws from its page. + assert_eq!(crate::view::transition::bound(Some(&drawing), &next), [2], "only the kept-row slot changed"); + // A kept row whose page starts drawing draws from that page. let drawn = Lay { chosen: &chosen(&order, &[0..10, 2 * PAGE_ROWS..2 * PAGE_ROWS + 10]), ..more }; let paged = Drawing::lay(Some(&next), &drawn, false); assert_eq!(paged.held_of(2 * PAGE_ROWS + 1), Some(paged.pages[2] * PAGE_ROWS + 1)); - assert_eq!(paged.held_of(PAGE_ROWS + 7), Some(2 * PAGE_ROWS + 1), "the other kept where it was"); + assert_eq!(paged.held_of(PAGE_ROWS + 7), Some(2 * PAGE_ROWS + 1), "the other kept row stays put"); } } diff --git a/crates/latkit/src/view/ends.rs b/crates/latkit/src/view/ends.rs index 54f8468a..ca7dffe9 100644 --- a/crates/latkit/src/view/ends.rs +++ b/crates/latkit/src/view/ends.rs @@ -1,12 +1,12 @@ -//! The end table: where each end of an edge or face row is held. A row's ends name places in the -//! order the vertices' table draws in, which stay put as the camera moves; the vertex mark drawing -//! that table holds its pages, and the rows it keeps whose pages it does not hold, where its drawing -//! lays them out, which moves as the camera does, a row at a time where the vertices the edges end at -//! lie sparse. So no channel stores where an end is held: a step of the expression machine -//! (`lib/ends.wesl`) finds it for the rows whose ends changed and the rows naming the vertices the -//! vertex mark's drawing says moved, and for every row once another mark or order holds them, and -//! writes it into the row's kind word of the end's number, where draws, probes, and normals read it -//! in one step. +//! The end table: where each endpoint of an edge or face row is held on the GPU. +//! +//! A row's ends reference places in the vertex table's order, which do not change as the camera +//! moves. The held row of a place does change: the vertex mark's drawing assigns resident pages to +//! slots and keeps sparse endpoints individually. So no channel stores held rows. Instead an +//! expression-machine step (`lib/ends.wesl`) resolves them and writes each into the row's kind +//! word for that end, where draws, probes, and normals read it directly. The step runs for rows +//! whose ends changed, for rows that reference vertices the drawing moved, and for every row when +//! another mark or another order holds the vertices. use std::sync::Arc; @@ -23,10 +23,10 @@ use crate::gpu::pipelines::Variant; use crate::gpu::shaders as wgsl; use crate::gpu::work::Count; -/// Where the rows an end of a mark's rows names are held: the vertex mark drawing their table, the -/// pages of its rows' words and how many words a row has, and the pages of the normals faces sum at -/// them; and where it holds each page of the order it draws that table in, and each row it keeps whose -/// page it does not hold, by its place in the order, and the places its drawing moved. +/// Where the vertices that one end of a mark's rows references are held: the vertex mark drawing +/// their table (`mark`), its state pages and words per row (`state`, `stride`), and its pages of +/// the normals faces sum into (`normals`). For a paged vertex table, also its order's id, each +/// page's slot (`pages`), the kept rows by order place (`kept`), and the places its drawing moved. #[derive(Clone, PartialEq, Debug)] pub(crate) struct End { pub(crate) mark: Option, @@ -40,7 +40,7 @@ pub(crate) struct End { } impl End { - /// An end no vertex mark draws the table of. + /// An end whose table no vertex mark draws. pub(crate) fn unattached() -> End { End { mark: None, @@ -54,14 +54,14 @@ impl End { } } - /// Whether every vertex it names is held elsewhere than `before` holds it: by another mark, or in - /// another order. Where only the vertex mark's pages of words, or the rows its drawing moved, - /// changed, the rest are held where they were. + /// Whether every referenced vertex may be held elsewhere than in `before`: another mark or + /// another order holds them. If only the vertex mark's word pages or its drawing's moved places + /// changed, the other vertices stay where they were. fn moved_whole(&self, before: &End) -> bool { self.mark != before.mark || self.order != before.order } - /// The places its drawing moved since `before` was attached, if any; where it moved none, none. + /// The places its drawing moved since `before` was attached; empty if none moved. fn moved_since(&self, before: &End) -> &[u32] { match (&self.moved, &before.moved) { (Some((version, places)), Some((was, _))) if version != was => places, @@ -69,8 +69,8 @@ impl End { } } - /// Words it takes in a mark's program past its `END_WORDS`: its page table, where pages and rows - /// kept are held, and its page table of normals. + /// Words its tables take in a mark's program beyond its `END_WORDS`: the state page table, the + /// page slots and kept-row table, and the normals page table. fn tables(&self) -> usize { let kept = |k: &KeptTable| 2 * k.buckets().len(); self.state.len() @@ -79,8 +79,8 @@ impl End { } } -/// The end before end `c` of `ends` that holds its rows where it does, whose tables it shares: as the -/// two ends of an edge between vertices of one mark do. +/// An earlier end of `ends` equal to end `c`, whose tables `c` shares, as both ends of an edge +/// between vertices of one mark do. fn shared(ends: &[End], c: usize) -> Option { ends[..c].iter().position(|e| *e == ends[c]) } @@ -92,11 +92,10 @@ pub(super) fn words(ends: &[End]) -> usize { .sum() } -/// Appends the words of a mark's `ends`, which the program's `uses` name, starting at word `at` of -/// the program buffer: `END_WORDS` an end, then each end's tables: the page table of its vertex mark's -/// words; its pages, how many, then each one's slot, and the table of the rows it keeps, how many -/// buckets, then each one's place and held row; and the page table of normals. Ends holding their rows -/// alike share tables. +/// Appends the words of a mark's `ends`, whose columns `uses` gives, starting at word `at` of the +/// program buffer. First `END_WORDS` per end, then each end's tables: the state page table of its +/// vertex mark; the page count, then each page's slot; the kept-row bucket count, then each +/// bucket's place and held row; and the normals page table. Equal ends share tables. pub(super) fn pack(ends: &[End], uses: &[EndUse], at: u32, out: &mut Vec) { let mut heads: Vec<[u32; wgsl::END_WORDS as usize]> = Vec::with_capacity(ends.len()); let mut table = at + wgsl::END_WORDS * ends.len() as u32; @@ -140,10 +139,10 @@ pub(super) fn pack(ends: &[End], uses: &[EndUse], at: u32, out: &mut Vec) { } impl View { - /// Points every edge and face mark's ends at the vertex marks drawing their tables. Where another - /// mark or another order holds the vertices an end names, every row finds its ends again; where - /// the vertex mark's drawing moved some, the rows naming them; where only a vertex mark's pages of - /// words moved, the ends read them where they are, and nothing runs. + /// Points every edge and face mark's ends at the vertex marks drawing their tables. If another + /// mark or order now holds an end's vertices, every row resolves its endpoints again. If the + /// vertex drawing moved some vertices, only the rows that reference them do. If only the vertex + /// mark's word pages moved, the ends read the new pages and nothing is resolved. pub(super) fn attach_ends(&mut self) { for i in 0..self.marks.len() { let ends = ends(&self.marks, i); @@ -171,9 +170,9 @@ impl View { } } - /// The row blocks of mark `i` whose rows end at any of `places` of the vertices its ends attach - /// to, ascending: its drawing's pages whose ends lie there; `None` for a table drawn whole, whose - /// rows end wherever. + /// The row blocks of mark `i` whose rows end at any of `places` in the attached vertex table, + /// ascending: the slots of its drawing whose endpoints include them. `None` for a table drawn + /// whole, which keeps no per-page endpoints, so every block resolves. fn naming(&self, i: usize, places: &[u32]) -> Option> { let shown = &self.marks[i]; let style = shown.mark.style(); @@ -181,8 +180,9 @@ impl View { Some(drawn.naming(shown.drawing()?, places)) } - /// Finds where the ends of `marks`' rows whose ends changed are held, each over the row blocks it - /// lists for them, with the expression machine each mark's channels run on; `group` is its group. + /// Resolves the held rows of the ends of `marks`' unresolved rows, over the row blocks each + /// lists, by running the end step on the pipeline each mark's channels use. `group` is the + /// expression machine's step group, left set to the channel step. pub(super) fn resolve_ends( &mut self, pass: &mut wgpu::ComputePass<'_>, @@ -206,7 +206,7 @@ impl View { let resolved = blocks.iter().map(|&b| u64::from((rows - b * wgsl::PAGE_ROWS).min(wgsl::PAGE_ROWS))); self.gpu.0.counters.add(Count::Resolved, resolved.sum()); shown.unresolved = Blocks::default(); - // Its state changed where its ends did, and what its rows' ends place with it. + // Resolving writes its state and can move where its rows stand, which places labels. shown.version += 1; shown.place_version += 1; } @@ -214,10 +214,10 @@ impl View { } } -/// Where each of mark `i`'s ends finds its vertices: the vertex mark it attaches to, the pages of -/// that mark's rows' words and of the normals faces sum at them, and where it holds the pages of the -/// order it draws its table in, if in part. An end attaches to the unnamed vertex mark drawing its -/// table, else to the first one, while that mark has pages for the rows it holds. +/// Where each of mark `i`'s ends finds its vertices: the vertex mark it attaches to, that mark's +/// state and normals pages, and for a paged table, its drawing's page slots and kept rows. An end +/// attaches to the unnamed vertex mark drawing its table, else the first one, but only while that +/// mark has word pages for all its held rows. fn ends(marks: &[Shown], i: usize) -> Vec { let Some(compiled) = &marks[i].compiled else { return Vec::new() }; let data = &marks[i].mark.style().data; @@ -226,7 +226,7 @@ fn ends(marks: &[Shown], i: usize) -> Vec { .ends .iter() .map(|end| match super::graph::end_mark(marks, data, &end.table) { - // A vertex mark without pages for its rows draws nothing, and nothing ends at it. + // A vertex mark without word pages for its rows draws nothing; nothing attaches to it. Some(m) if marks[m].pages_held() => { let (state, normals) = (marks[m].words.pages(Kept::State), marks[m].words.pages(Kept::Normals)); End { diff --git a/crates/latkit/src/view/event.rs b/crates/latkit/src/view/event.rs index 5ab15d69..ea97e24b 100644 --- a/crates/latkit/src/view/event.rs +++ b/crates/latkit/src/view/event.rs @@ -1,9 +1,10 @@ -//! What a view tells the application. An intent asks something of it: a click, an open, a menu, a -//! drag, a connection, a deletion, each told once, in the order the user acted. A change tells that -//! state the view keeps moved, named as the state is: `hovered`, `selection`, `camera`, `waiting`. -//! Only the latest change of each is told, and the state can always be read, so a listener added -//! late reads what it missed. A view tells only what the application did not do itself: its setters -//! change state silently. +//! Events a view reports to the application. +//! +//! An intent asks the application to act: a click, open, menu, drag, connection, or deletion. Each +//! is reported once, in the order the user acted. A change reports that view state moved and is +//! named after that state: `hovered`, `selection`, `camera`, `waiting`. Only the latest change of +//! each is reported, and the state can always be read, so a late listener can catch up. A view +//! reports only what the application did not do itself: its setters change state silently. use std::collections::VecDeque; @@ -11,8 +12,7 @@ use super::Camera; use super::input::{Device, Modifiers}; use crate::{Error, Name, Pending, Row, Rows}; -/// What happened in a view, for the application to act on, as [`View::events`](crate::View::events) -/// tells it. +/// Something that happened in a view, as [`View::events`](crate::View::events) reports it. #[derive(Clone, PartialEq, Debug, serde::Serialize, schemars::JsonSchema)] #[serde(tag = "type", content = "detail", rename_all = "lowercase")] #[schemars(rename = "ViewEvent")] @@ -25,57 +25,58 @@ pub enum Event { /// How the user clicked it. trigger: Trigger, }, - /// A row was double clicked, or Enter pressed on the row the keys point at. + /// A row was double-clicked, or Enter was pressed on the row the keyboard points at. Open { /// The row. hit: Hit, /// How the user opened it. trigger: Trigger, }, - /// The secondary button clicked, or the menu key pressed: what lies under the trigger's point, - /// for a menu of what to do with it. + /// A context menu was requested: by a secondary click, the menu key, or a touch or pen held + /// still. Lists the rows under the trigger's point, for a menu of actions on them. Menu { - /// What lies under the point, nearest first. + /// The rows under the point, nearest first. hits: Vec, /// How the user asked. trigger: Trigger, }, - /// A row of a draggable mark follows the pointer: the drag starts once the pointer moves a few + /// A row of a draggable mark is dragged. The drag starts once the pointer moves more than 4 CSS /// pixels from where it pressed the row. Drag { /// Whether the drag starts, moves, or ends. phase: Phase, /// The row dragged, as it was hit. hit: Hit, - /// Where the row goes, in the space's coordinates: where it was hit, moved as the pointer moved - /// since the press, at the depth the drag started; a vertex keeps where it was grabbed. On a - /// diagram's dots when its grid snaps. + /// The row's new position in the space's coordinates: the hit point moved with the pointer + /// since the press, at the depth where the drag started, so a vertex keeps its grab offset. + /// Snapped to a diagram's dots when its grid snaps. position: [f64; 3], /// The pointer. trigger: Trigger, }, - /// A connection was drawn with [`Controls::connect`](crate::Controls): from a port, a vertex - /// with Alt held, or an edge whose end was dragged off it. + /// A connection was drawn with [`Controls::connect`](crate::Controls) on: from a port, from a + /// vertex with Alt held, or by dragging an edge's end off it. Connect { /// The end it was drawn from: for an edge, the end that stayed. from: End, /// The vertex it was released over, if any. to: Option, - /// The edge whose other end was dragged, which `to` replaces. + /// The edge whose end was dragged off; `to` replaces that end. edge: Option, /// Where it was released. trigger: Trigger, }, - /// Delete or Backspace with rows selected and [`Controls::delete`](crate::Controls) on. + /// Delete or Backspace was pressed with rows selected and [`Controls::delete`](crate::Controls) + /// on. Delete { /// The rows selected. rows: Vec, /// The key. trigger: Trigger, }, - /// What the user points at changed: [`View::hovered`](crate::View::hovered). + /// The hovered row changed: [`View::hovered`](crate::View::hovered). Hovered { - /// The row now pointed at; `None` once it left every row. + /// The row now hovered; `None` once no row is. hit: Option, /// The pointer, or the keys. trigger: Trigger, @@ -87,31 +88,31 @@ pub enum Event { /// How the user changed it; `None` when the data dropped rows from it. trigger: Option, }, - /// The camera moved by a gesture, a fit, a flight, coasting, or an orbit: + /// The camera moved by a gesture, fit, flight, coast, or orbit: /// [`View::camera`](crate::View::camera). Camera(Camera), - /// What the last frame stood in for changed; nothing once it is complete: + /// The work the last frame waited on changed; empty once frames are complete: /// [`View::waiting`](crate::View::waiting). Waiting(Waiting), - /// Something failed, told once. A mark its data no longer fits also lists in + /// Something failed; reported once. A mark that no longer fits its data is also listed in /// [`View::failures`](crate::View::failures). Error(Error), } -/// Where, with what keys, and with what the user acted. For keys, the point is the row they point -/// at, else the pointer, else the view's middle. +/// Where the user acted, with which modifier keys, and with which device. For keyboard input, the +/// point is the row the keys point at, else the pointer, else the view's middle. #[derive(Clone, Copy, PartialEq, Debug, Default, serde::Serialize, schemars::JsonSchema)] pub struct Trigger { /// CSS pixels from the top left. pub point: [f32; 2], - /// The keys held. + /// The modifier keys held. pub modifiers: Modifiers, - /// What acted. + /// The device used. pub device: Device, } impl Event { - /// Whether it asks something of the application, so keeps its place among intents. + /// Whether it is an intent, which keeps its order among intents. fn intent(&self) -> bool { matches!( self, @@ -124,8 +125,8 @@ impl Event { ) } - /// Whether it leaves `older`, still untold, nothing to say: a change replaces the change of the - /// same state, a drag's move the move before it, an error the same error. + /// Whether it makes the unreported `older` redundant: a change replaces an earlier change of + /// the same state, a drag move the previous move, and an error an identical error. fn replaces(&self, older: &Event) -> bool { match (self, older) { (Event::Drag { phase: Phase::Move, .. }, Event::Drag { phase: Phase::Move, .. }) => true, @@ -138,19 +139,19 @@ impl Event { } } -/// What a view has yet to tell, in the order it happened. An intent waiting on a pick holds its -/// place, and the intents after it wait behind it; changes and errors never wait. +/// The events a view has yet to report, in order. An intent waiting on a pick holds its place, and +/// later intents wait behind it; changes and errors never wait. #[derive(Default)] pub(crate) struct Events(VecDeque); enum Slot { Ready(Event), Picking(Pending>, Awaited), - /// A pick that made nothing, dropped once settled. + /// A pick that produced no event, removed once settled. Gone, } -/// What an intent waits on a pick for. +/// An intent waiting for its pick's hits. #[derive(Clone)] pub(crate) enum Awaited { Menu(Trigger), @@ -159,8 +160,8 @@ pub(crate) enum Awaited { edge: Option, trigger: Trigger, }, - /// A click again where nothing moved: the selection moves to the row under the point after - /// `after`. + /// A repeated click where nothing moved: the selection moves to the next row under the point + /// after `after`. Cycle { after: Option, trigger: Trigger, @@ -168,18 +169,19 @@ pub(crate) enum Awaited { } impl Events { - /// Queues `event`, dropping what it leaves nothing to say. + /// Queues `event`, dropping the unreported events it makes redundant. pub(crate) fn tell(&mut self, event: Event) { self.0.retain(|slot| !matches!(slot, Slot::Ready(older) if event.replaces(older))); self.0.push_back(Slot::Ready(event)); } - /// Queues the intent `awaited` makes once `hits` answers. + /// Queues the intent `awaited` produces once `hits` resolves. pub(crate) fn tell_after(&mut self, hits: Pending>, awaited: Awaited) { self.0.push_back(Slot::Picking(hits, awaited)); } - /// Makes, in place, each event whose pick answered; one that makes nothing leaves no place. + /// Turns each intent whose pick resolved into its event, in place. One that produces no event + /// is removed. pub(crate) fn settle(&mut self, mut make: impl FnMut(Vec, Awaited) -> Option) { let mut made = false; for slot in &mut self.0 { @@ -198,7 +200,7 @@ impl Events { } } - /// The first error untold: a shot fails on it, and it stays for the view's owner. + /// The first unreported error. A shot fails on it and leaves it queued for the view's owner. pub(crate) fn error(&self) -> Option<&Error> { self.0.iter().find_map(|slot| match slot { Slot::Ready(Event::Error(error)) => Some(error), @@ -206,7 +208,8 @@ impl Events { }) } - /// Takes what can be told: all but the intents behind one still waiting on its pick. + /// Takes every event ready to report, except intents queued behind one still waiting on its + /// pick. pub(crate) fn drain(&mut self) -> std::vec::IntoIter { let (mut told, mut held) = (Vec::new(), VecDeque::new()); for slot in self.0.drain(..) { @@ -226,7 +229,7 @@ impl Extend for Events { } } -/// A vertex a connection ends at, and its port when it has the one the connection meets. +/// A vertex a connection ends at, and the port the connection meets, if any. #[derive(Clone, PartialEq, Debug, serde::Serialize, schemars::JsonSchema)] pub struct End { /// The vertex. @@ -241,14 +244,14 @@ pub struct End { pub struct Hit { /// The row. pub row: Row, - /// The name of the mark that drew it, when it has one: what tells two marks of one table apart. + /// The name of the mark that drew it, if named. It tells two marks of one table apart. #[serde(skip_serializing_if = "Option::is_none")] pub mark: Option, /// The index of the mark in the scene the frame drew. #[serde(skip)] pub(crate) index: usize, - /// The point of the row nearest the probe, CSS pixels from the top left: a vertex's or a port's - /// middle, the nearest point of a stroke or a triangle. + /// The row's point nearest the probe, in CSS pixels from the top left: a vertex's or port's + /// middle, or the nearest point of a stroke or triangle. pub point: [f32; 2], /// That point in the space's coordinates: x, y, and elevation. pub position: [f64; 3], @@ -283,13 +286,13 @@ pub enum Phase { Start, /// The pointer moved. Move, - /// The pointer let go, or the drag was cancelled. + /// The pointer was released, or the drag was cancelled. End, } /// What [`View::fit`](crate::View::fit) frames. #[derive(Clone, PartialEq, Debug, serde::Deserialize, schemars::JsonSchema)] -#[schemars(description = "What a view's fit frames.")] +#[schemars(description = "The target of a view's fit.")] #[serde(rename_all = "camelCase")] pub enum Fit { /// Everything drawn. @@ -307,7 +310,7 @@ pub enum Fit { }, } -/// The pointer a view asks for over what it shows. +/// The cursor a view wants shown over it. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] pub enum Cursor { /// The platform's default. @@ -315,15 +318,15 @@ pub enum Cursor { Default, /// Over a row a click selects. Pointer, - /// Over a row that drags. + /// Over a draggable row. Grab, /// While dragging a row or the camera. Grabbing, - /// While selecting a box. + /// While selecting a box or drawing a connection. Crosshair, } -/// What a view does each frame. +/// Counters describing a view's frames. #[derive(Clone, Copy, PartialEq, Debug, Default, serde::Serialize, schemars::JsonSchema)] #[serde(rename_all = "camelCase")] pub struct Stats { @@ -331,13 +334,13 @@ pub struct Stats { pub frames: u64, /// Rows of every mark's table. pub rows: u64, - /// Rows every mark draws: every row of a table drawn whole, the rows of the tiles in view of one - /// drawn through tiles. + /// Rows every mark draws: every row of a table drawn whole, and the rows of the tiles in view + /// of a paged table. pub drawn: u64, - /// Draw calls the last frame recorded, labels with the marks. + /// Draw calls the last frame recorded, label draws included. pub draws: u64, - /// Instances the last frame's draws drew: points, segments, triangles, glyphs; at most, for - /// segments the GPU culls. + /// Instances the last frame's draws drew: points, segments, triangles, and glyphs. An upper + /// bound for segments, which the GPU may cull. pub instances: u64, /// Fetched tiles the view draws, of every tile source. pub tiles: u64, @@ -345,21 +348,22 @@ pub struct Stats { pub tiles_loading: u64, /// Milliseconds the last frame took to record on the CPU. pub cpu_ms: f32, - /// Milliseconds the GPU takes on a frame: the median of the last frames it measured; 0 where the + /// Milliseconds the GPU takes per frame: the median of recently measured frames; 0 if the /// device cannot measure. pub gpu_ms: f32, } -/// The work under way a frame stood in for, by kind; nothing when the frame was complete. +/// The work a frame was still waiting on, by kind; empty when the frame was complete. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] pub struct Waiting { - /// Pipelines the scene draws with still compiling: until they land the view draws nothing new. + /// Pipelines the view uses are still compiling. Until those it draws with finish, the view + /// draws nothing new. pub compiling: bool, - /// Pages its marks draw still uploading, or tables not yet built from them. + /// Pages its marks draw are still uploading, or tables are not yet built from them. pub data: bool, /// Layouts, routes, lanes, shapes, tiles, or pyramids under way. pub derived: bool, - /// A label placement being read back, or texts asked for and not yet set. + /// A label placement being read back, or texts requested and not yet set. pub labels: bool, /// A fit due or being measured. pub fit: bool, @@ -373,7 +377,7 @@ impl Waiting { *self == Waiting::default() } - /// The names of what it waits on, as its fields are named. + /// The names of the fields that are set. pub fn names(&self) -> impl Iterator { let Waiting { compiling, data, derived, labels, fit, marks } = *self; [ @@ -395,7 +399,7 @@ impl std::fmt::Display for Waiting { } } -/// The names of what it waits on, as [`Waiting::names`] gives them. +/// Serializes as the list of names [`Waiting::names`] gives. impl serde::Serialize for Waiting { fn serialize(&self, serializer: S) -> Result { serializer.collect_seq(self.names()) @@ -409,7 +413,7 @@ impl schemars::JsonSchema for Waiting { fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { schemars::json_schema!({ - "description": "Work under way a frame stands in for: pipelines the scene draws with compiling, until which the view draws nothing new (`compiling`); pages uploading or tables not yet built from them (`data`); layouts, routes, lanes, shapes, tiles, or pyramids (`derived`); a label placement or texts not yet set (`labels`); a fit due or being measured (`fit`); segment counts, or a transition beginning (`marks`). Empty once a frame is complete.", + "description": "What a frame still waits on: pipelines the scene draws with, which compile before the view draws anything new (`compiling`); pages uploading, or tables not yet built from them (`data`); layouts, routes, lanes, shapes, tiles, or pyramids (`derived`); label placement, or text not yet set (`labels`); a fit due or being measured (`fit`); segment counts, or a transition starting (`marks`). Empty once the frame is complete.", "type": "array", "items": { "enum": ["compiling", "data", "derived", "labels", "fit", "marks"] }, }) @@ -461,7 +465,7 @@ mod tests { } #[test] - fn a_change_replaces_the_one_still_untold() { + fn a_change_replaces_the_undelivered_one() { let mut events = Events::default(); let mut waiting = Waiting { data: true, ..Waiting::default() }; events.tell(Event::Waiting(waiting)); diff --git a/crates/latkit/src/view/fit.rs b/crates/latkit/src/view/fit.rs index e4f15424..9fb078f7 100644 --- a/crates/latkit/src/view/fit.rs +++ b/crates/latkit/src/view/fit.rs @@ -1,8 +1,10 @@ -//! Framing what is drawn. A box frames at once, and so does what the CPU already knows: a diagram's -//! sheet holds every box and wire, and tiles hold the bounds of every row they draw. Anything else is -//! measured on the GPU, read back, and framed a frame or two later. A plot without a value range -//! measures its traces whenever they or the camera change, and grows its range with headroom so the -//! axes stay still. +//! Fits: moving the camera to frame what is drawn. +//! +//! A box fits at once, as does anything whose extent the CPU already knows: a diagram's sheet holds +//! every box and wire, and tiles hold the bounds of every row they draw. Anything else is measured +//! on the GPU, read back, and framed a frame or two later. A plot without a value range measures +//! its traces whenever they or the camera change, and grows its range with headroom so the axes +//! stay still. use super::View; use super::camera::Bounds; @@ -17,7 +19,7 @@ use crate::{Camera, Space}; pub(crate) struct Fitting { /// A fit waiting for its frame. pub(crate) wanted: Option, - /// Extents in flight, and the fit they were measured for. + /// The extents readback in flight, and the fit it measures for. reading: Option<(Readback, Option)>, /// Whether a plot's traces changed since they were last measured. pub(crate) plot_due: bool, @@ -29,7 +31,7 @@ impl Fitting { self.reading.is_some() } - /// Whether a fit waits for a frame to measure it: none while extents are being read back. + /// Whether a fit is waiting to be measured; never while extents are being read back. pub(crate) fn due(&self) -> bool { self.wanted.is_some() && self.reading.is_none() } @@ -47,7 +49,7 @@ pub(crate) struct Measure { } impl View { - /// Applies a box fit, a plot's fit, or measured extents that landed. + /// Applies a box fit, a plot's fit to its traces' span, or measured extents that arrived. pub(super) fn settle_fit(&mut self) { if self.size[0] <= 0.0 { return; @@ -75,7 +77,7 @@ impl View { self.fitted(); } - /// Lets go of the rows marked for a fit, once it framed them. + /// Clears the rows marked for a fit once it framed them. fn fitted(&mut self) { if !self.focus.fitting.is_empty() { self.focus.fitting.clear(); @@ -88,22 +90,22 @@ impl View { if self.fitting.reading.is_some() || self.size[0] <= 0.0 { return None; } - // A plot that traces frames everything by its traces' whole span, which settles once their - // data is drawn; measuring them first would frame only what shows up to the coordinate shown. + // A plot with traces fits everything to their whole span once their data is available. + // Measuring instead would frame only what is drawn up to the coordinate shown. let plot = matches!(self.scene.space, Space::Plot(_)); if plot && matches!(self.fitting.wanted, Some(Fit::All)) && self.traces() { return None; } - // A fit frames what shows once data has streamed, its shapes are triangulated and bent, and any - // layout has placed it; in a diagram, once vertices stand where their sheet puts them. + // A fit waits until data has streamed, shapes are triangulated and bent, and any layout has + // placed it; in a diagram, until vertices stand where the sheet puts them. let easing = matches!(self.scene.space, Space::Diagram(_)) && self .marks .iter() .any(|m| m.kind() == MarkKind::Vertices && (m.transition.is_some() || m.begin.is_some())); let all = matches!(self.fitting.wanted, Some(Fit::All)); - // Everything a table drawn through tiles holds frames by its rows' bounds, whatever is drawn. A - // mark with no rows has nothing to frame: it holds a fit only while no mark has rows. + // Fitting everything uses a tiled table's row bounds, regardless of what is drawn. A mark + // with no rows has nothing to frame; it only delays a fit while no mark has rows. let compiled = || self.marks.iter().filter(|m| m.compiled.is_some()); let staged = |m: &Shown| self.staged.get(&m.mark.style().data).is_some_and(|s| !s.is_empty()); let drawn = |m: &Shown| (m.drawable() && !staged(m)) || (all && self.tiled_bounds(m).is_some()); @@ -170,9 +172,9 @@ impl View { self.frame_fit(lo, hi, fit); } - /// The box `fit` frames when it is known without measuring, least x and y then greatest: in a - /// diagram once its sheet stands, its vertices and wires, or the boxes of the rows fitted; else, - /// with every mark `ready`, where only tables drawn through tiles are measured, their bounds. + /// The box `fit` frames if known without measuring, as `[min x, min y, max x, max y]`. In a + /// diagram whose sheet is placed: the sheet's extent, or the fitted rows' boxes. Otherwise, for + /// `Fit::All` once every mark is `ready` and only tiled tables are measured: their bounds. fn known(&self, fit: &Fit, ready: bool) -> Option<[f64; 4]> { let union = |a: [f64; 4], b: [f64; 4]| [a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])]; let sheet = &self.derived.sheet; @@ -190,8 +192,8 @@ impl View { measured().filter_map(|m| self.tiled_bounds(m)).reduce(union) } - /// Frames `lo..hi` as `fit` asks: without one, a plot's value range; to reveal a row, the camera - /// brought to it where it lies out of view; else the box. + /// Frames `lo..hi` as `fit` asks. With no fit, fits a plot's value range. For `Fit::Reveal`, + /// centers the camera on the row if it is out of view. Otherwise frames the box. fn frame_fit(&mut self, lo: [f64; 3], hi: [f64; 3], fit: Option<&Fit>) { if lo[0] > hi[0] { return; @@ -219,14 +221,14 @@ impl View { } } - /// The bounds of every row `shown` draws through tiles, whatever tiles are drawn. + /// The bounds of every row of `shown`'s tiled table, regardless of which tiles are drawn. fn tiled_bounds(&self, shown: &Shown) -> Option<[f64; 4]> { let style = shown.mark.style(); self.derived.tiled.bounds(&style.data, &style.table) } - /// Frames the box `min..max` at once. Framing everything sets how far the camera can go from it; - /// framing part of it widens that reach, never narrows it. + /// Frames the box `min..max` at once. Framing everything sets how far the camera can go from + /// it; framing part of it widens that reach, never narrows it. fn frame_box(&mut self, min: [f64; 3], max: [f64; 3], everything: bool) { let camera = self.camera.framing(&self.scene.space, min, max, self.size, self.area()); self.rig.hold(); @@ -242,8 +244,8 @@ impl View { self.move_camera(camera); } - /// Grows the value range a plot shows to hold `lo..hi` with headroom, and shrinks it once the - /// values fill less than a third of it. + /// Grows the value range a plot shows to hold `lo..hi` with 12% headroom on each side, and + /// shrinks it once the values fill less than a third of it. fn fit_values(&mut self, lo: f64, hi: f64) { let holds = self.plot_y.is_some_and(|[a, b]| lo >= a && hi <= b && (hi - lo) * 3.0 >= b - a); if !holds { @@ -253,12 +255,12 @@ impl View { } } - /// The coordinates a plot's traces span. /// Whether a mark traces a sampled field. fn traces(&self) -> bool { self.marks.iter().any(|shown| matches!(&shown.mark, crate::Mark::Lines(lines) if lines.trace.is_some())) } + /// The axis range a plot's traces span, widened to a unit range around a single point. fn traced_span(&self) -> Option<[f64; 2]> { let spans = self.marks.iter().filter_map(|shown| { let crate::Mark::Lines(lines) = &shown.mark else { return None }; @@ -282,7 +284,7 @@ impl super::marks::Shown { } } -/// The `f32` an ordered word stands for (see `lib/reduce.wesl`). +/// The `f32` an order-preserving word encodes; `lib/reduce.wesl`'s `unordered`. fn unordered(u: u32) -> f32 { f32::from_bits(if u & 0x8000_0000 != 0 { u & 0x7fff_ffff } else { !u }) } @@ -291,7 +293,7 @@ fn unordered(u: u32) -> f32 { mod tests { use super::unordered; - /// The inverse of `lib/reduce.wesl`'s `ordered`. + /// `lib/reduce.wesl`'s `ordered`, the inverse of `unordered`. fn ordered(x: f32) -> u32 { let b = x.to_bits(); if b & 0x8000_0000 != 0 { !b } else { b | 0x8000_0000 } diff --git a/crates/latkit/src/view/focus.rs b/crates/latkit/src/view/focus.rs index 985101ee..fd615a33 100644 --- a/crates/latkit/src/view/focus.rs +++ b/crates/latkit/src/view/focus.rs @@ -1,8 +1,10 @@ -//! Selection, hover, and the rows a fit measures, as focus words: one per held row of every mark, in -//! its pages of focus words, written only where they change. The selection keeps only rows a pickable -//! mark shows. The ends of a selected or hovered edge are lit too. Each mark's lit rows are listed, as -//! the rows it draws, for its focus draw, which draws them again over everything, glowing. Hover is -//! state either way, which expressions read; whether it glows is the controls' to say. +//! Focus words: selection, hover, and the rows a fit measures, as bits in one word per held row of +//! every mark, kept in its focus word pages and written only where they change. +//! +//! The selection keeps only rows a pickable mark shows. The endpoints of a selected or hovered edge +//! are lit too. Each mark's lit rows are listed by draw index for its focus draw, which draws them +//! again over everything with a glow. Hover is always state that expressions can read; the controls +//! decide whether it glows. use rustc_hash::FxHashMap; @@ -17,7 +19,8 @@ use crate::gpu::work::Queue; use crate::scene::mark::Primitive; use crate::{Row, Rows}; -/// Words alike between two changed ones of a page, past which they are written apart. +/// The row gap at which two changed words of a page are written separately; closer ones share one +/// write, along with the unchanged words between them. const GAP: u32 = 16; /// A focus word's mark and held row. @@ -26,19 +29,19 @@ type At = (usize, u32); #[derive(Default)] pub(crate) struct Focus { pub(crate) selection: Vec, - /// What the user points at, as it was hit. + /// The hovered row, as hit. pub(crate) hovered: Option, /// Rows a fit measures. pub(crate) fitting: Vec, - /// Words written, by mark and held row. + /// The words last written, by mark and held row. written: FxHashMap, pub(crate) dirty: bool, - /// Every mark's lit rows, laid out since they were last written. + /// Every mark's lit rows, if laid out since last taken. lit: Option>, } impl Focus { - /// The lit rows laid out since they were last taken, to write. + /// Takes the lit rows laid out since last taken, for writing to the lit buffer. pub(crate) fn take_lit(&mut self) -> Option> { self.lit.take() } @@ -53,9 +56,9 @@ pub(crate) struct Lit { } impl View { - /// Writes the focus words that changed into the pages `pages` holds; after `fresh`, pages of focus - /// words new or moved, every word. Channels reading selection or hover evaluate again, and every - /// mark's lit rows are laid out again. + /// Writes changed focus words into `pages`, or every word when `fresh` (focus word pages new or + /// moved). Channels reading focus evaluate again where words changed, and every mark's lit rows + /// are laid out again. pub(super) fn write_focus(&mut self, queue: &Queue, pages: &wgpu::Buffer, fresh: bool) { if fresh { self.focus.written.clear(); @@ -79,7 +82,7 @@ impl View { let set = words.iter().filter(|(at, b)| self.focus.written.get(at) != Some(b)).map(|(&at, _)| at); let mut changes: Vec = cleared.chain(set).collect(); changes.sort_unstable(); - // A page's changed words go in runs, the words between them as they are. + // Write each page's changed words in runs, unchanged words between them included. for run in changes.chunk_by(|a, b| a.0 == b.0 && a.1 >> wgsl::PAGE_BITS == b.1 >> wgsl::PAGE_BITS) { let (mark, page) = (run[0].0, run[0].1 >> wgsl::PAGE_BITS); let Some(&block) = self.marks[mark].words.pages(Kept::Focus).blocks.get(page as usize) else { continue }; @@ -96,22 +99,23 @@ impl View { } } if !changes.is_empty() { - // Channels reading focus evaluate the pages of the rows whose words changed; every row - // where pages of focus words are new. + // Channels reading focus evaluate the pages of rows whose words changed, or every row + // when the focus word pages are new. for (i, shown) in self.marks.iter_mut().enumerate() { let pages: Vec = changes.iter().filter(|&&(mark, _)| mark == i).map(|&(_, row)| row / wgsl::PAGE_ROWS).collect(); shown.dirty_rows(shown.channels_reading(Deps::FOCUS), (!fresh).then_some(&pages[..])); } } - // Rows lit only by hover draw no glow when it does not glow. + // When hover does not glow, rows lit only by hover are left out of the focus draw. self.lay_out_lit(&words, if glows { wgsl::LIT } else { wgsl::LIT & !wgsl::HOVERED }); self.focus.written = words; } - /// Sets `bit` in the focus words of `rows` in every mark holding them (with `picked`, every - /// pickable one), and with `ends` lights the ends of each edge among them; returns the rows some - /// mark shows. A row its data holds stays shown while a table drawn in part holds no page of it. + /// Sets `bit` in the focus words of `rows` in every mark drawing their table (only pickable + /// marks with `picked`), and with `ends` also lights the endpoints of each edge among them. + /// Returns the rows some mark shows. A row in the data stays shown even while its paged table + /// has no resident page for it. fn focus_rows(&self, rows: &[Row], bit: u32, picked: bool, ends: bool, words: &mut FxHashMap) -> Vec { let mut kept = Vec::new(); for row in rows { @@ -141,7 +145,8 @@ impl View { kept } - /// Lights the row `reference` names for row `index` in every vertex mark holding its table's. + /// Sets the `END` bit of the vertex `reference` names for row `index`, in every vertex mark + /// holding it. fn light_end(&self, data: &crate::Name, reference: &Reference, index: usize, words: &mut FxHashMap) { let Some(target) = reference.target(index) else { return }; for (i, shown) in self.marks.iter().enumerate() { @@ -156,8 +161,8 @@ impl View { } } - /// Lists every mark's drawn rows with a bit of `glowing` set for its focus draw, with the - /// instances each takes. + /// Lists each mark's drawn rows with any `glowing` bit set, by draw index, for its focus draw, + /// with the instances each takes. fn lay_out_lit(&mut self, words: &FxHashMap, glowing: u32) { let mut all = Vec::new(); for i in 0..self.marks.len() { @@ -180,9 +185,9 @@ impl View { self.focus.lit = Some(all); } - /// Instances mark `i`'s focus draw takes for each of its lit held rows `lit`: a point one; a path - /// as many as the longest of theirs has pieces, past which a row's own draw nothing; none for - /// faces. + /// Instances each lit row takes in mark `i`'s focus draw, for lit held rows `lit`: one for a + /// point; for paths and areas, enough for the longest lit row's pieces, past which a shorter + /// row's instances draw nothing; none for faces. fn pieces(&self, i: usize, lit: &[u32]) -> u32 { let shown = &self.marks[i]; let Some(compiled) = &shown.compiled else { return 0 }; @@ -196,8 +201,8 @@ impl View { } } - /// The most points any of mark `i`'s held rows `lit` has in its geometry column: its path, its - /// bends, or its triangles. + /// The most points any of mark `i`'s lit held rows `lit` has in its geometry column: path + /// points, bends, or triangle corners. fn longest(&self, i: usize, lit: &[u32]) -> u32 { let shown = &self.marks[i]; let Some(compiled) = &shown.compiled else { return 0 }; diff --git a/crates/latkit/src/view/frame.rs b/crates/latkit/src/view/frame.rs index b5208aed..8c8cb601 100644 --- a/crates/latkit/src/view/frame.rs +++ b/crates/latkit/src/view/frame.rs @@ -1,8 +1,11 @@ -//! One frame, in order: tick the clock, settle readbacks, advance the camera, derive what marks read -//! beside their data, sync columns, stream uploads, give marks pages of words for their rows, freeze -//! transitions, pack the program buffer, write focus and uniforms, then record the compute, shadow, -//! label, render, and probe passes into the caller's encoder. Nothing here submits but the clearing of -//! pages handed out, which runs before anything the frame records. +//! One frame, in order: tick the clock, settle readbacks, advance the camera, derive the tables +//! marks read beside their data, lay out drawings, sync columns, stream uploads, give marks word +//! pages for their rows, freeze transitions, pack the program buffer, write focus words and +//! uniforms, then record the compute, label, render, probe, and query passes into the caller's +//! encoder. +//! +//! The frame submits nothing itself except setup that must run before its recorded passes: +//! clearing newly handed-out pages, and copying buffers that grew. use web_time::Instant; @@ -21,9 +24,9 @@ use crate::gpu::shaders as wgsl; use crate::scene::mark::MarkKind; use crate::{Error, Name, derive}; -/// Whether the pages a frame of a view draws found room: where some found none, the next frame makes -/// it, growing the arena or planning less (`Short`), or where the arena is full and the plan draws as -/// little as it may, letting go of what draws of the data waiting for room (`Starved`). +/// Whether a frame's pages found room in the arena. If some did not, the next frame makes room: +/// `Short` grows the arena or plans less; `Starved`, when the arena is full and the plan is already +/// minimal, releases what the view draws of datasets waiting for room. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Debug)] pub(super) enum Room { #[default] @@ -32,22 +35,22 @@ pub(super) enum Room { Starved, } -/// The most seconds comets travel in one frame: after a pause they carry on, not leap. +/// The most seconds comets travel in one frame, so after a pause they resume instead of jumping. const TRAVEL: f64 = 0.1; impl View { /// Records `frame` of the scene into `encoder`; the caller submits it. Until the view is - /// [ready](View::ready) for the frame's target it records nothing, and the target keeps what it - /// holds. + /// [ready](View::ready) for the frame's target, it records nothing and the target keeps its + /// contents. /// - /// Uploads stream within the GPU's budget per frame. A mark shows a new data version once all of - /// its changed pages are on the GPU, and keeps drawing the previous one until then. + /// Uploads stream within the GPU's per-frame budget. A mark shows a new data version once all + /// of its changed pages are on the GPU, and draws the previous version until then. pub fn render(&mut self, encoder: &mut wgpu::CommandEncoder, frame: &Frame<'_>) { self.record(encoder, frame, true); } - /// Records `frame` for a picture, which takes no input: it asks for what it draws and changes - /// with alone. + /// Records `frame` for a picture. Pictures take no input, so they compile only what they draw + /// and change with, never the pipelines that probes and queries use. pub(crate) fn render_picture(&mut self, encoder: &mut wgpu::CommandEncoder, frame: &Frame<'_>) { self.record(encoder, frame, false); } @@ -60,8 +63,9 @@ impl View { } return; } - // The frame's size first: the tiles its sources bring up are those it shows. What landed is - // taken before them, so what they start and lands at once wakes the next frame. + // Set the frame's size first, so the tiles sources bring up are those this frame shows. + // Clear the arrival flag before that, so work they start that finishes at once wakes the + // next frame. self.pixel_ratio = frame.pixel_ratio.max(0.01); let ratio = f64::from(self.pixel_ratio); self.size = [f64::from(frame.size[0]) / ratio, f64::from(frame.size[1]) / ratio]; @@ -85,7 +89,7 @@ impl View { self.settle(); self.advance(dt); let mut inner = gpu.0.lock(); - // What it lets go of frees as the frame begins, before anything it records reads the pages. + // Release before `begin_frame`, which frees the pages before this frame reads any of them. if starved { self.start_over(&mut inner); } @@ -104,8 +108,8 @@ impl View { self.resources.as_mut().expect("created").write_lit(device, queue, &rows); } let measure = self.measuring(); - // A picture takes no input; frames that do probe and answer queries once what those use - // compiled. + // A picture takes no input. Other frames probe and answer queries once the pipelines those + // use have compiled. let answers = input && self.answers(); let probe = self.probe_point(answers); let query = self.lay_out_lists(answers); @@ -160,15 +164,15 @@ impl View { self.end(started, input); } - /// The time effects show this frame, and the seconds comets travel to it. What reads the clock - /// shows anything new only when it moved; a frame drawn again at the same instant, as a shot - /// draws while it waits, evaluates none of it. Under reduced motion the effect clock holds where - /// it first stood; a picture's still sets it. + /// The effect time for this frame, and the seconds comets travel to reach it. Channels reading + /// the clock re-evaluate only when it moved, so a frame redrawn at the same instant (as a shot + /// draws while it waits) evaluates none of them. Under reduced motion the effect time stays at + /// its first value. A shot's fixed time overrides both. fn effect_clock(&mut self) -> (f64, f64) { let held = !self.moves() && !self.effect_time.is_nan(); let elapsed = self.signal.elapsed().unwrap_or_else(|| if held { self.effect_time } else { self.clock.now() }); - // Comets travel the clock's step since the last frame: none at first or back in time, and - // never more than a pause's worth. + // Comets travel the clock's step since the last frame: none on the first frame or when time + // runs backward, and at most `TRAVEL`. let travel = if self.effect_time.is_nan() { 0.0 } else { (elapsed - self.effect_time).clamp(0.0, TRAVEL) }; if self.effect_time != elapsed { self.effect_time = elapsed; @@ -179,9 +183,10 @@ impl View { (elapsed, travel) } - /// Ends a frame begun at `started`: what it stood in for, what it took, and the next frame its - /// work asks for, as work landing between frames does. A frame taking no `input`, a picture's, - /// asks none for probes and queries, which only frames that take input run. + /// Ends a frame begun at `started`: reports what it still waits on, records its stats, and + /// requests the next frame if work remains, as work arriving between frames does. A frame + /// without `input` (a picture's) requests none for probes and queries, which only input frames + /// run. fn end(&mut self, started: Instant, input: bool) { let waiting = self.under_way(); if waiting != self.waiting { @@ -196,9 +201,9 @@ impl View { } } - /// Lays the frame out: every mark's pages of words for its rows, the transitions starting, the - /// ends edges attach to, fitted domains, the buffers they fit, the program, and the labels; whether - /// pages of focus words are new. + /// Lays out the frame: each mark's word pages, starting transitions, edge end attachments, + /// fitted domains and the buffers sized for them, the program buffer, and labels. Returns + /// whether any focus word pages are new. fn lay_out(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner) -> bool { let gpu = self.gpu.clone(); let device = &gpu.0.device; @@ -220,10 +225,10 @@ impl View { fresh } - /// Gives every mark pages of words for the rows it holds: its state, its focus words, its labels' - /// candidates, and at a vertex mark faces attach to, the normals they sum. Rows whose state words - /// took another shape evaluate again; a mark the arena has no room for now draws nothing until it - /// has. Whether pages of focus words are new. + /// Gives every mark word pages for its held rows: state, focus words, label candidates, and, + /// for a vertex mark that faces attach to, the normals they sum. Rows in new state pages + /// evaluate from scratch. A mark the arena has no room for draws nothing until it has. Returns + /// whether any focus word pages are new. fn fit_words(&mut self, inner: &mut Inner) -> bool { let gpu = self.gpu.clone(); let device = (&gpu.0.device, &gpu.0.queue); @@ -258,7 +263,7 @@ impl View { shown.packed = false; changed = true; fresh |= kept == Kept::Focus; - // Rows whose state starts over evaluate every channel again, every word written first. + // Rows in new state pages evaluate every channel, writing every word first. if kept == Kept::State { shown.slots_bound(&new); shown.fresh.extend(&new); @@ -274,9 +279,10 @@ impl View { fresh } - /// Pages the view draws found no room: what lacks them draws nothing until the next frame grows - /// the arena, or where it is full plans less, while it can, and else lets go of what draws of the - /// data waiting for room, unless doing so let go of nothing before. + /// Records that some pages found no room; what lacks them draws nothing until the next frame + /// makes room. If the arena can grow, or the plan can shrink (it shrinks here), that is + /// `Short`. Otherwise it is `Starved`, which releases what the view draws of datasets waiting + /// for room, unless an earlier release freed nothing (`limited`). fn found_no_room(&mut self, inner: &Inner) { let room = if !inner.arena.full() || self.planning.overflowed() { Room::Short @@ -288,10 +294,10 @@ impl View { self.room = self.room.max(room); } - /// Lets go of what the view draws of each dataset waiting for room the arena, full, cannot make - /// though the plan draws as little as it may: its marks draw nothing until what replaces it lands - /// in the room freed. Where it held nothing to let go of, what it draws does not fit the budget: - /// it tells so once, and waits for a version of its data to publish. + /// Releases what the view draws of each dataset waiting for room, when the arena is full and + /// the plan is already minimal. Its marks draw nothing until the staged replacement arrives in + /// the freed room. If nothing was released, what the view draws does not fit the budget: it + /// reports a `Limit` error once and waits for a new data version to publish. fn start_over(&mut self, inner: &mut Inner) { let mut freed = false; for shown in &mut self.marks { @@ -311,14 +317,14 @@ impl View { } if !freed { self.limited = true; - let reason = "what the view draws does not fit the GPU's budget, though it draws as little as it may"; + let reason = "the view does not fit the GPU's budget, even drawing as little as possible"; self.events.tell(Event::Error(Error::Limit(reason.into()))); } } - /// Holds what the view draws to its share: what its tables pin, each page once, and the pages of - /// words its marks keep per row, but for the pages drawings keep from before only while their room - /// lasts, which a plan within less lets go of first. + /// Keeps what the view draws within its share of the budget. It counts each page its tables pin + /// once, plus the word pages its marks keep per row, minus idle pages, which stay only while + /// there is room and go first when the plan shrinks. fn hold_to_share(&mut self, inner: &Inner) { let mut pins: Vec = self.marks.iter().flat_map(|m| m.tables.pins()).collect(); pins.sort_unstable_by_key(|key| (key.page, key.low, key.words as u8)); @@ -328,14 +334,14 @@ impl View { let idle: f64 = self .drawings .iter() - .filter_map(|(key, drawn)| Some(self.planning.page_bytes(key, drawn.current.as_ref()?.idle))) + .filter_map(|(key, drawn)| Some(self.planning.page_bytes(key, drawn.current.as_ref()?.idle.len() as u32))) .sum(); let needed = ((pages + words) as f64 - idle).max(0.0); self.planning.held(needed as u64, self.choices.values().any(|c| c.capped)); } - /// Takes what landed since the last frame: probes, fits, queries, the intents and keys waiting on - /// picks. + /// Takes the results that arrived since the last frame: GPU timing, fits, probes, queries, and + /// the intents and key steps waiting on picks. fn settle(&mut self) { if let Some(ms) = self.timer.as_mut().and_then(timer::Timer::landed) { self.stats.gpu_ms = ms; @@ -346,8 +352,9 @@ impl View { self.settle_gestures(); } - /// Tells, once, why marks the data drawn does not fit failed: once the frame took the latest - /// versions, so a dataset filled between frames tells nothing of the version before. + /// Reports, once each, why marks failed against the data drawn. Runs after the frame takes the + /// latest data versions, so a dataset filled between frames reports nothing about the version + /// before. fn tell_failures(&mut self) { for (i, shown) in self.marks.iter_mut().enumerate() { if let Some((error, told)) = &mut shown.failure @@ -359,16 +366,16 @@ impl View { } } - /// Derives what marks read beside their data, from the data and the scene alone, handing the - /// host what the frame cannot afford. First the orders tables drawn in part draw in, and the - /// ranges of them the camera asks for; then every other derivation, from the data. + /// Derives the tables marks read beside their data, from the data and scene alone, handing the + /// host whatever the frame cannot afford. First come the orders of paged tables and the ranges + /// of them the camera needs, then every other derivation. fn derive(&mut self, inner: &mut Inner) { let host = inner.host.clone(); let projected = self.projected(); let earth = matches!(self.scene.space, crate::Space::Earth(_)); let placing = earth || matches!(self.scene.space, crate::Space::Plane(_)); let sight = derive::sight::Sight { projected: &projected, size: self.size, earth }; - // One budget for the frame: what is drawn in part, then every other derivation. + // One work budget for the frame, spent on paged tables first, then on other derivations. let (waker, counters) = (self.waker.clone(), self.gpu.0.counters.clone()); let errors = self.gpu.0.errors.clone(); let mut jobs = derive::Jobs::new(host.as_deref(), &waker, &counters, &errors, inner.frame.work); @@ -394,9 +401,9 @@ impl View { inner.frame.work = jobs.left() as u32; } - /// Brings every tile set up to what the camera and the plot window ask, from what the GPU's - /// cache holds, with what landed for them since. Their pieces fetch and decode as they land, so - /// nothing here spends the frame's work. + /// Brings every tile set up to date with what the camera and plot window need, from the GPU's + /// tile cache and what arrived since. Tiles fetch and decode as they arrive, so this spends + /// none of the frame's work budget. pub(super) fn update_tiles(&mut self, inner: &mut Inner) { let host = inner.host.clone(); let projected = self.projected(); @@ -412,7 +419,7 @@ impl View { self.derived.tiles = tiles; } - /// What derivations read this frame. + /// The input derivations read this frame. fn derive_input<'a>(&'a self, host: Option<&'a dyn crate::host::Host>) -> derive::Input<'a> { derive::Input { scene: &self.scene, @@ -424,9 +431,9 @@ impl View { } } - /// Lays out the lists buffer, the marks' segment lists, and takes this frame's query when it - /// `answers` them, returning its words and whether it reads depth. A buffer replaced empty counts - /// every segment again. + /// Lays out the lists buffer and the marks' segment lists. If the frame `answers` queries, + /// takes this frame's query, returning its words and whether it reads depth. A replaced buffer + /// starts empty, so every segment is counted again. fn lay_out_lists(&mut self, answers: bool) -> Option<([f32; 4], bool)> { self.lay_out_segments(); let device = &self.gpu.0.device; @@ -452,9 +459,9 @@ impl View { } } - /// Lays out what each table drawn in part draws, syncs every mark's columns to it, streams the - /// pages they need within what the GPU's frame has left to upload, and swaps in each dataset - /// whose pages have all landed, those that just did included. + /// Lays out each paged table's drawing, syncs every mark's columns, uploads the pages they need + /// within the frame's remaining upload budget, and swaps in each dataset whose pages have all + /// arrived, including those that just did. fn stream(&mut self, inner: &mut Inner) { let gpu = self.gpu.clone(); self.lay_out_drawings(); @@ -478,7 +485,8 @@ impl View { marks } - /// Tells errors the device reported outside any scope, once each. + /// Reports errors the device raised outside any error scope, and custom pipeline errors, once + /// each. pub(super) fn tell_gpu_errors(&mut self) { let errors: Vec = std::mem::take(&mut *self.gpu.0.errors.lock().unwrap_or_else(std::sync::PoisonError::into_inner)); @@ -487,8 +495,8 @@ impl View { self.events.extend(custom.into_iter().map(Event::Error)); } - /// The plot window lines trace across, and the pixel columns it spans within what the plan - /// affords. + /// The plot's x range that traces span, and its width in device pixel columns, capped by the + /// plan. fn trace_window(&self) -> (Option<[f64; 2]>, u32) { let across = match self.camera { Camera::Plot { x, .. } => Some(x), @@ -498,8 +506,9 @@ impl View { (across, (((area[2] - area[0]) * f64::from(self.pixel_ratio)).max(1.0) as u32).min(self.trace_columns)) } - /// What marks read of sampled fields, for time tiles to fetch: the plot window, the coordinate - /// shown, and each field a mark's program reads, with whether a line traces it. + /// What marks read of sampled fields, for time tiles to fetch: the plot window and its pixel + /// columns, the coordinate shown, and each field a mark's program reads, with whether a line + /// traces it. fn reading(&self) -> crate::tiles::Reading { let (across, columns) = self.trace_window(); let mut fields = Vec::new(); @@ -514,14 +523,14 @@ impl View { crate::tiles::Reading { across, columns, at: self.at, fields } } - /// Syncs every mark's columns to its data, and for a table drawn in part to the drawing replacing - /// what draws: tables whose inputs changed are rebuilt once and staged with their dataset's, which - /// reserves every page they read at once, or waits for room. + /// Syncs every mark's columns to its data and, for a paged table, to its latest drawing. Column + /// tables whose inputs changed are rebuilt once and staged with their dataset, which reserves + /// every page they read at once or waits for room. fn sync(&mut self, inner: &mut Inner) { let at = self.at; let (across, columns) = self.trace_window(); let window = Window { across, columns, steps: 0 }; - // The order each table drawn in part draws in, by dataset. + // Each paged table's order, by dataset. let mut orders: Keyed> = Keyed::default(); for ((data, table), drawn) in &self.drawings { if let Some(drawing) = drawn.latest() { @@ -555,10 +564,10 @@ impl View { } } - /// Swaps in what each dataset draws once all of it is ready: every mark's rebuilt tables, and - /// the drawing of each of its tables drawn in part, together, so edges never end at another - /// drawing's vertices and a data version appears whole; and every mark has room for the pages of - /// rows new to it. Whether any tables were swapped in. + /// Swaps in each dataset once all of it is ready and every mark has room for word pages of its + /// new rows. A dataset's rebuilt tables and its paged tables' drawings swap in together, so + /// edges never end at another drawing's vertices and a data version appears whole. Returns + /// whether any tables were swapped in. fn publish(&mut self, inner: &mut Inner) -> bool { let mut datasets: Vec = self.marks.iter().filter(|m| m.compiled.is_some()).map(|m| m.mark.style().data.clone()).collect(); @@ -598,8 +607,8 @@ impl View { published } - /// Whether mark `i` has pages of words for the pages of rows it holds once its dataset publishes, - /// giving it those it lacks: the pages it holds stay as they are until then. + /// Whether mark `i` has word pages for the rows it will hold once its dataset publishes, + /// allocating any it lacks. Its existing pages stay unchanged until then. fn room_for(&mut self, i: usize, inner: &mut Inner) -> bool { let shown = &self.marks[i]; let style = shown.mark.style(); @@ -609,8 +618,8 @@ impl View { Some(drawing) => drawing.live(), None => self.snapshot(&key.0).and_then(|d| d.table(&key.1)).map_or(0, |t| t.len() as u32), }; - // Pages new to it hold its next rows as `page_rows` will: a slot's whole page, or the table's - // rows a page at a time. + // Size new pages as `page_rows` will: a whole page per slot of a paged table, else the + // table's own rows, a page at a time. let held = live + shown.exits; let mut rows = shown.page_rows(); let had = rows.len(); @@ -637,12 +646,11 @@ impl View { }) } - /// Takes what mark `i` draws now its dataset is published: its rebuilt tables, its table's - /// drawing, and the rows they hold. Changed columns dirty the channels reading them; rows that - /// came, went, or moved evaluate every channel, and with changed static values begin a - /// transition. + /// Updates mark `i` after its dataset publishes: its rebuilt tables, its table's drawing, and + /// the rows they hold. Changed columns dirty the channels reading them. Rows that came, went, + /// or moved evaluate every channel. Moved rows or changed static values begin a transition. fn took(&mut self, i: usize, rebuilt: Option, inner: &mut Inner) { - // A drag moves rows at once. + // During a drag, rows move without easing. let easing = !self.ease().is_zero() && !self.dragging(); let shown = &mut self.marks[i]; let Some(compiled) = &shown.compiled else { return }; @@ -659,18 +667,18 @@ impl View { let before = shown.holding.take(); let alike = before.as_ref().is_some_and(|old| holds_alike(old, &holding)); let moved = before.as_ref().and_then(|old| moved(old, &holding)); - // A path morphs from the one its row ran through only while every row keeps its place; - // rows that moved, as rows do between orders, fade instead. + // A path morphs from its previous shape only while every row keeps its place. Rows that + // moved, as they do between orders, fade instead. let kept = moved.as_ref().is_none_or(|(sources, exits)| exits.is_empty() && in_place(sources)); - // Slots given pages of other rows: their rows evaluate every channel, and their labels start - // over. + // Slots now holding pages of other rows: their rows evaluate every channel, and their + // labels restart. let bound = match &drawing { Some(new) => bound(before.as_ref().and_then(|h| h.drawing.as_deref()).filter(|_| moved.is_none()), new), None => Vec::new(), }; match &before { - // Rows that came, went, or moved evaluate every channel, constants included: where every - // row kept its place, over the row blocks rewritten; else over every row. + // Rows that came, went, or moved evaluate every channel, constants included: over the + // rewritten row blocks if every row kept its place, else over every row. Some(old) if old.drawing.is_none() && drawing.is_none() => { if moved.is_some() || shown.rows != table.len() as u32 { let rewritten = old.table.len() <= table.len() && moved.as_ref().is_none_or(|(s, _)| in_place(s)); @@ -685,7 +693,7 @@ impl View { } shown.rows = drawing.as_ref().map_or(table.len() as u32, |d| d.items); shown.holding = Some(holding); - // Without an ease, a flowing mark whose rows moved still begins, a transition of no length, + // Without an ease, a flowing mark whose rows moved still begins a zero-length transition, // so its comets follow the rows. let carries = shown.mark.flows() && moved.is_some(); let eased = published.as_ref().is_some_and(|p| shown.static_columns(p.data)); @@ -694,14 +702,14 @@ impl View { } else if moved.is_some() { shown.exits = 0; } else if shown.transition.is_some() && !bound.is_empty() { - // Rows of slots bound while the mark eases enter; the rest ease on from what they show. + // Rows in slots rebound mid-transition enter; the rest keep easing from what they show. shown.restart(&bound); } if !kept || (shown.begin.is_none() && shown.transition.is_none()) { shown.tables.thaw(inner); } - // Where rows draw changed: what follows it, shadows, normals, labels, and segment lists, follows. - // A column changed alone evaluates the channels reading it, which moves them as it must. + // If where rows draw changed, bump the versions so normals, labels, and segment lists + // follow. A changed column alone evaluates the channels reading it, which bump them. if !alike { shown.version += 1; shown.place_version += 1; @@ -722,13 +730,14 @@ impl View { if shown.kind() == MarkKind::Lines { self.fitting.plot_due = true; } - // Rows may have moved under the focus. + // Rows may have moved under the focus words. if !alike { self.focus.dirty = true; } } - /// Where each mark's fitted domains start; returns how many there are. + /// Assigns each mark the index of its first fitted domain, dirtying marks whose index moved. + /// Returns the total number of domains. fn place_domains(&mut self) -> u64 { let mut domains = 0; for shown in &mut self.marks { diff --git a/crates/latkit/src/view/gestures.rs b/crates/latkit/src/view/gestures.rs index 1034868a..71ac378b 100644 --- a/crates/latkit/src/view/gestures.rs +++ b/crates/latkit/src/view/gestures.rs @@ -1,22 +1,24 @@ -//! Gestures: what the pointer and keys mean. A press decides once the pointer moves a few pixels: with -//! Shift it selects in a box when boxes select, else turns a map or plane; on a port, an Alt-held -//! vertex, or an edge it draws a connection when connecting is on; on a row of a draggable mark it -//! drags the row; elsewhere it pans. A press that does not move clicks, Shift, Control, or Meta -//! toggling its row in the selection; with cycling on, a click again where nothing moved selects the -//! next row under the point. A secondary press turns once it moves and asks for a menu when it does -//! not, as a touch or a pen held still does. The wheel and pinches zoom, as the controls allow, and -//! keys step. A diagram never turns, and its drags snap -//! to its grid when it says so. Every camera move is about the point under the pointer and goes -//! through the rig, which carries a released drag on and keeps the camera within reach. +//! Gestures: how a view interprets pointer and key input. //! -//! The point under a press comes from the latest probe when it was taken where the press is, -//! otherwise from a probe of its own a frame or two later; the press waits for it and catches up. -//! Menus and connections tell what lies under their point once a pick answers, in their place among -//! the intents. +//! A press decides what it does once the pointer moves a few pixels. With Shift it selects a box +//! when box selection is on, else turns a map or plane. On a port, an Alt-held vertex, or an edge, +//! it draws a connection when connecting is on. On a row of a draggable mark it drags the row. +//! Elsewhere it pans. A press that does not move clicks; with Shift, Control, or Meta it toggles +//! its row in the selection. With cycling on, clicking again where nothing moved selects the next +//! row under the point. A secondary press turns once it moves and opens a menu when it does not, as +//! a touch or pen held still does. The wheel and pinches zoom, as the controls allow, and keys +//! step. A diagram never turns, and its drags snap to its grid when the grid says so. Every camera +//! move is about the point under the pointer and goes through the rig, which lets a released drag +//! coast and keeps the camera within reach. //! -//! The keys point too. Tab steps through the rows nearest the view's middle, and past the last one -//! lets focus move on; an arrow steps to the nearest row its way; Enter opens the row and Space -//! selects it. The row the keys point at is hovered until the pointer acts. +//! The point under a press comes from the latest probe if it was taken where the press is, +//! otherwise from a probe of its own a frame or two later; the press waits for it, then catches up. +//! Menus and connections report what lies under their point once a pick resolves, in their place +//! among the intents. +//! +//! Keys point too. Tab steps through the rows nearest the view's middle, and past the last one lets +//! focus move on. An arrow steps to the nearest row in its direction. Enter opens the row and Space +//! toggles it in the selection. The row the keys point at stays hovered until the pointer acts. use super::View; use super::event::{Awaited, Cursor, End, Event, Fit, Hit, Phase, Trigger}; @@ -33,9 +35,9 @@ const DOUBLE: f64 = 0.4; const DOUBLE_REACH: f32 = 6.0; /// A drag released this many seconds after its last move does not coast. const HELD: f64 = 0.06; -/// Degrees a pixel of turning drag turns. +/// Degrees turned per CSS pixel of turning drag. const TURN: f64 = 0.3; -/// What a step of the keys moves: CSS pixels, degrees, levels. +/// How far one key step moves: CSS pixels of pan, degrees of turn, and zoom levels. const STEP: f64 = 100.0; const STEP_TURN: f64 = 15.0; const STEP_ZOOM: f64 = 0.5; @@ -44,9 +46,9 @@ const STEP_ZOOM: f64 = 0.5; pub(crate) enum Gesture { #[default] Idle, - /// A pointer pressed at `from`, now at `trigger`; `answer` is `None` while its probe is in - /// flight. `earlier` is a press released here before its own answer came, which clicks first on - /// this one's. + /// A pointer pressed at `from`, now at `trigger`. `answer` is `None` while its probe is in + /// flight. `earlier` is a previous press released here before its own answer arrived; it clicks + /// first, using this press's answer. Press { pointer: u32, button: Button, @@ -58,12 +60,12 @@ pub(crate) enum Gesture { /// When it began, on the view's clock. since: f64, }, - /// A touch or pen held still until it asked for a menu: its release does nothing more. + /// A touch or pen held still long enough to open a menu; its release does nothing more. Held { pointer: u32, }, - /// A connection drawn from `from` toward the pointer at `to`; `anchor` is where it starts on - /// screen, once known. + /// A connection drawn from `from` toward the pointer at `to`. `anchor` is where it starts on + /// screen, once known; `locating` finds it when the press did not give it. Connect { pointer: u32, from: End, @@ -72,8 +74,8 @@ pub(crate) enum Gesture { locating: Option>>, to: [f32; 2], }, - /// A row dragged by `pointer`: `grab` is where the row was hit less where the press was, CSS - /// pixels, so the row keeps where it was grabbed. + /// A row dragged by `pointer`. `grab` is the hit point minus the press point, in CSS pixels, so + /// the row keeps its grab offset. Drag { pointer: u32, probed: Probed, @@ -92,7 +94,8 @@ pub(crate) enum Gesture { Pinch { pointers: [(u32, [f32; 2]); 2], }, - /// A Shift drag where boxes select: the rows in the box between the press and the pointer. + /// A Shift drag with box selection on, selecting the rows in the box between the press and the + /// pointer. Box { pointer: u32, from: [f32; 2], @@ -105,7 +108,7 @@ pub(crate) struct Gestures { pub(crate) now: Gesture, /// The rows the keys step through. pub(crate) keys: Stepping, - /// The time of the input being taken, on the clock of whatever feeds the view its input. + /// The time of the input being handled, on the clock of whatever feeds the view its input. time: f64, /// The last click: when, where, on which row, and where the camera was. clicked: Option<(f64, [f32; 2], Option, Camera)>, @@ -114,22 +117,23 @@ pub(crate) struct Gestures { moved: Option, } -/// The rows the keys step through: what a pick around the view's middle found, nearest first, as -/// the frame it was taken in drew them; the row stepped to, which holds the hover until the pointer -/// acts; and keys pressed while the rows were being picked. +/// The rows the keys step through: the hits of a pick around the view's middle, nearest first, as +/// drawn in the frame it was taken in; the index stepped to, which keeps the hover until the +/// pointer acts; and keys pressed while the pick was pending. #[derive(Default)] pub(crate) struct Stepping { /// The rows last picked; `None` until the keys first step. rows: Option>, picking: Option>>, - /// Whether the rows are where the frame now draws them. + /// Whether the rows match where the current frame draws them. fresh: bool, pub(crate) at: Option, queued: Vec<(Key, Modifiers)>, } impl Stepping { - /// The frame changed: the rows are picked again before an arrow steps. + /// Marks the rows stale after the frame changed, so they are picked again before an arrow + /// steps. pub(crate) fn stale(&mut self) { self.fresh = false; } @@ -155,7 +159,7 @@ impl Gestures { } } - /// Follows the velocity of a move of `delta` per second-scaled step. + /// Blends a move of `delta` into the velocity, smoothed over about 50 ms of input time. fn track(&mut self, delta: [f64; 2]) { let dt = self.moved.map_or(0.0, |at| self.time - at); self.moved = Some(self.time); @@ -165,7 +169,8 @@ impl Gestures { } } - /// The velocity a release coasts with: none when the pointer stopped before letting go. + /// The velocity a release coasts with: zero if the pointer never moved, or stopped for over + /// `HELD` before the release. fn release(&mut self) -> [f64; 2] { let held = self.moved.is_none_or(|at| self.time - at > HELD); let velocity = std::mem::take(&mut self.velocity); @@ -175,10 +180,10 @@ impl Gestures { } impl View { - /// Takes one pointer, wheel, or key input at `time`, seconds on the clock its frames give; - /// events it causes queue for [`events`](View::events). Double clicks and flings measure the - /// times between inputs. Returns whether the view used it, so a platform stops its default - /// handling: scrolling the page, its context menu, focus moving on. + /// Handles one pointer, wheel, or key input at `time`, in seconds on the same clock as frame + /// times. Events it causes queue for [`events`](View::events). Double clicks and flings measure + /// the time between inputs. Returns whether the view used the input, so the platform can skip + /// its default handling: scrolling the page, its context menu, or moving focus. pub fn input(&mut self, input: &Input, time: f64) -> bool { if self.size[0] <= 0.0 { return false; @@ -214,8 +219,8 @@ impl View { used } - /// The pointer acts at `trigger`: what reads it evaluates again, and it takes the hover back from - /// the keys. + /// Records the pointer at `trigger`: channels reading the pointer evaluate again, and the + /// pointer takes the hover back from the keys. fn follow_pointer(&mut self, trigger: Trigger) { self.pointer = Some(trigger); self.gestures.keys.at = None; @@ -224,12 +229,13 @@ impl View { } } - /// Where the pointer last acted, the keys it held, and what it is. + /// Where the pointer last acted, the keys it held, and its device; the default once it left. fn pointing(&self) -> Trigger { self.pointer.unwrap_or_default() } - /// The pointer left: nothing it hovered stays hovered. + /// Handles the pointer leaving. Unless a gesture is under way, its probes are dropped and the + /// hover clears, unless the keys point at a row. fn left(&mut self) { let trigger = self.pointing(); self.pointer = None; @@ -383,8 +389,8 @@ impl View { true } - /// A probe landed: a press waiting for it decides; otherwise it is what the pointer hovers, - /// unless the keys point at a row. + /// Handles a probe's answer at `point`: a press waiting for it decides; otherwise the answer + /// sets the hover, unless the keys point at a row. pub(super) fn landed(&mut self, point: [f32; 2], answer: Answer) { if let Gesture::Press { from, answer: waiting @ None, .. } = &mut self.gestures.now && near(*from, point, 0.5) @@ -401,7 +407,7 @@ impl View { } } - /// Points at `hit`, telling the application when the row changed. + /// Hovers `hit`, reporting a change when the row differs. fn hover(&mut self, hit: Option, trigger: Trigger) { if hit.as_ref().map(|h| &h.row) != self.focus.hovered.as_ref().map(|h| &h.row) { self.focus.dirty = true; @@ -411,9 +417,9 @@ impl View { self.focus.hovered = hit; } - /// Decides a press: once it moves with Shift and boxes select, a box, whatever lies under it; - /// otherwise once what lies under it is known, a release clicks or asks for a menu, and a move - /// connects, drags, pans, or turns. + /// Decides what a press does. A Shift drag with box selection on starts a box, regardless of + /// what lies under it. Otherwise, once the probe's answer is known, a release clicks or opens a + /// menu, and a move connects, drags, pans, or turns. fn pressed(&mut self) { let Gesture::Press { pointer, button, from, trigger, answer, released, earlier, .. } = &mut self.gestures.now else { @@ -486,7 +492,7 @@ impl View { } } - /// A press let go where it began: a click, or a menu for the secondary button. + /// A press released where it began: a click, or a menu for the secondary button. fn tap(&mut self, button: Button, hit: Option, trigger: Trigger) { if button == Button::Secondary { self.menu(trigger); @@ -514,7 +520,7 @@ impl View { self.events.tell(Event::Open { hit: hit.clone(), trigger }); } if again && !double && !toggle && self.scene.controls.cycle { - // The next row under the point, once a pick lists them nearest first. + // Select the next row under the point once a pick lists them, nearest first. let hits = self.pick(trigger.point, self.scene.controls.pick_radius); let after = self.focus.selection.last().cloned(); self.events.tell_after(hits, Awaited::Cycle { after, trigger }); @@ -541,8 +547,8 @@ impl View { self.reselect(selection, Some(trigger)); } - /// Selects `selection` for the user, or without a trigger for the data, telling the application - /// when it changed. + /// Sets the selection, reporting a change if it differs. `trigger` is `None` when the data, + /// not the user, changed it. pub(super) fn reselect(&mut self, selection: Vec, trigger: Option) { if selection != self.focus.selection { self.focus.selection.clone_from(&selection); @@ -551,8 +557,9 @@ impl View { } } - /// Tells where the row dragged goes with the pointer at `trigger`: where it was hit, moved as the - /// pointer moved since the press, at the depth the drag started; on a diagram's dots when it snaps. + /// Reports where the dragged row goes with the pointer at `trigger`: the hit point moved with + /// the pointer since the press, at the depth where the drag started, snapped to a diagram's + /// dots when its grid snaps. fn drag(&mut self, phase: Phase, trigger: Trigger) { let Gesture::Drag { probed, grab, .. } = &self.gestures.now else { return }; let view = self.projected(); @@ -569,7 +576,7 @@ impl View { self.events.tell(Event::Drag { phase, hit: probed.hit.clone(), position, trigger }); } - /// Whether a row is being dragged: what it moves follows at once. + /// Whether a row is being dragged. Rows it moves then update without easing. pub(super) fn dragging(&self) -> bool { matches!(self.gestures.now, Gesture::Drag { .. }) } @@ -579,13 +586,14 @@ impl View { self.scene.controls.turn && !matches!(self.scene.space, Space::Diagram(_)) } - /// Whether the rows of mark `mark` select. + /// Whether clicking a row of mark `mark` selects it. fn selects(&self, mark: usize) -> bool { self.scene.marks.get(mark).is_some_and(|m| m.style().interaction.select) } - /// Where a connection starts from what a press hit: a port, or a vertex with Alt, starts one - /// there; an edge with ends lets go of the end it was pressed nearer, from the one that stays. + /// Where a connection starts from what a press hit: at a port, or at a vertex with Alt held; + /// or, for an edge with ends, from the end that stays, detaching the end pressed nearer. Also + /// gives the edge, and the start's screen point when known. fn connect_from(&self, probed: &Probed, modifiers: Modifiers) -> Option<(End, Option, Option<[f32; 2]>)> { let hit = &probed.hit; let shown = self.marks.get(hit.index)?; @@ -604,14 +612,16 @@ impl View { } } - /// Asks what lies under the trigger's point, for a menu told in its place once the pick answers. + /// Picks what lies under the trigger's point, for a menu reported in order once the pick + /// resolves. fn menu(&mut self, trigger: Trigger) { let hits = self.pick(trigger.point, self.scene.controls.pick_radius); self.events.tell_after(hits, Awaited::Menu(trigger)); } - /// Makes the intents whose picks answered, in their places among the intents; takes the keys' - /// rows once picked; and starts a connection being drawn where the row it stays at lies. + /// Turns intents whose picks resolved into events, in order; takes the keys' rows once picked; + /// anchors a connection being drawn at its start once located; and opens a menu for a long + /// press. pub(super) fn settle_gestures(&mut self) { if let Gesture::Connect { anchor, locating: Some(locating), .. } = &mut self.gestures.now && let Some(found) = locating.take() @@ -628,8 +638,8 @@ impl View { self.held_long(); } - /// The event `awaited` makes of the hits its pick found, if any: an intent, or the selection a - /// cycling click moves to. + /// The event `awaited` produces from its pick's hits, if any: an intent, or the selection + /// change of a cycling click. fn finish(&mut self, hits: Vec, awaited: Awaited) -> Option { Some(match awaited { Awaited::Menu(trigger) => Event::Menu { hits, trigger }, @@ -659,7 +669,7 @@ impl View { }) } - /// A touch or a pen held still for the controls' press asks for a menu where it is. + /// Opens a menu where a touch or pen has been held still for the controls' `press` duration. fn held_long(&mut self) { let press = self.scene.controls.press.as_secs_f64(); let Gesture::Press { pointer, from, trigger, released: false, since, .. } = self.gestures.now else { return }; @@ -670,14 +680,16 @@ impl View { } } - /// Whether a touch or a pen is held still, toward a menu: frames go on to notice it. + /// Whether a touch or pen is pressed while long presses open menus, so frames keep running to + /// notice the hold. pub(super) fn holding(&self) -> bool { matches!(self.gestures.now, Gesture::Press { released: false, trigger, .. } if matches!(trigger.device, Device::Touch | Device::Pen)) && !self.scene.controls.press.is_zero() } - /// The wheel zooms about the point under it, as the controls allow; otherwise the page has it. + /// Zooms about the point under the wheel, as the controls allow; otherwise leaves the wheel to + /// the page. fn wheel(&mut self, point: [f32; 2], delta: [f32; 2], modifiers: Modifiers) -> bool { let zooms = match self.scene.controls.zoom { Zoom::Wheel => true, @@ -766,9 +778,9 @@ impl View { } /// Steps the row the keys point at: Tab to the next row nearest the view's middle, Shift+Tab - /// back, an arrow to the nearest row its way. Tab steps through the rows as last picked and waits - /// only for the first pick; an arrow waits for rows picked where the frame now draws them. Past - /// either end Tab is not used, so focus moves on. Whether the key was used. + /// back, an arrow to the nearest row in its direction. Tab uses the rows as last picked and + /// waits only for the first pick; an arrow waits for rows picked where the frame now draws + /// them. Past either end, Tab is not used, so focus moves on. Returns whether the key was used. fn step(&mut self, key: Key, modifiers: Modifiers) -> bool { let tab = key == Key::Tab; if !self.gestures.keys.fresh && self.gestures.keys.picking.is_none() { @@ -804,13 +816,13 @@ impl View { true } - /// Takes the keys' rows once their pick answers, and steps the keys that waited for them. + /// Takes the keys' rows once their pick resolves, and steps the keys that waited for them. fn settle_keys(&mut self) { let keys = &mut self.gestures.keys; let Some(rows) = keys.picking.as_ref().and_then(Pending::take) else { return }; let held = keys.row().map(|hit| hit.row.clone()); let rows = rows.unwrap_or_default(); - // The row stepped to keeps its place among the rows as now drawn. + // The row stepped to keeps its position among the newly picked rows. keys.at = held.as_ref().and_then(|row| rows.iter().position(|hit| hit.row == *row)); keys.rows = Some(rows); keys.picking = None; @@ -831,7 +843,7 @@ impl View { } } -/// The row of `rows` nearest row `i` within 45° of the arrow's way from it. +/// The row of `rows` nearest row `i` within 45° of the arrow's direction from it. fn toward(rows: &[Hit], i: usize, arrow: Key) -> Option { let way = match arrow { Key::Left => [-1.0, 0.0], diff --git a/crates/latkit/src/view/graph.rs b/crates/latkit/src/view/graph.rs index e512a9cf..986767b8 100644 --- a/crates/latkit/src/view/graph.rs +++ b/crates/latkit/src/view/graph.rs @@ -1,5 +1,5 @@ -//! The graph a view's data holds: the rows joined to a row across edges and nets, the row a reference -//! names, and which vertex mark an end attaches to. +//! Graph queries over a view's data: a row's neighbors across edges and nets, the row a reference +//! names, and the vertex mark an end attaches to. use rustc_hash::FxHashSet; @@ -18,9 +18,9 @@ fn reference<'t>(table: &'t Table, name: &str) -> Option<&'t Reference> { } impl View { - /// The rows joined to `row`: a vertex's neighbors across edges and nets, or an edge's or a net's - /// vertices, a set per table. It reads the references' inverses, so it costs what the row's degree - /// does. + /// The rows joined to `row`: a vertex's neighbors across edges and nets, or an edge's or net's + /// vertices, grouped by table. It reads the references' inverses, so its cost is proportional + /// to the row's degree. pub fn neighbors(&self, row: &Row) -> Vec { let mut found: Vec = Vec::new(); let Some(data) = self.snapshot(&row.data) else { return Vec::new() }; @@ -72,7 +72,8 @@ impl View { Rows::group(found) } - /// Every vertex mark's port reference field that names rows of net mark `i`'s table. + /// Each vertex mark's port reference fields that point at net mark `i`'s table, as + /// `(mark, field)`. fn ports_naming(&self, i: usize) -> Vec<(usize, Name)> { let style = self.marks[i].mark.style(); let Some(data) = self.snapshot(&style.data) else { return Vec::new() }; @@ -102,7 +103,8 @@ impl View { Some(Row { data: row.data.clone(), table: r.to.clone(), id }) } - /// Marks each edge mark on the earth whose route is not straight as following great circles. + /// Whether `shown` follows great circles: an edge mark with ends, on the earth, whose route is + /// unset or geodesic. pub(super) fn geodesic(&self, shown: &Shown) -> bool { let Space::Earth(_) = self.scene.space else { return false }; match &shown.mark { diff --git a/crates/latkit/src/view/input.rs b/crates/latkit/src/view/input.rs index a39688b3..191d3016 100644 --- a/crates/latkit/src/view/input.rs +++ b/crates/latkit/src/view/input.rs @@ -1,42 +1,42 @@ -//! Input as a view reads it, from any platform: a browser canvas translates its events into these. +//! Platform-independent input: a browser canvas translates its events into these. /// A pointer, wheel, or key event, with points in CSS pixels from the view's top left. #[derive(Clone, Copy, PartialEq, Debug)] pub enum Input { - /// A pointer pressed. + /// A pointer was pressed. Down { - /// Tells pointers apart while several are down. + /// Identifies the pointer while several are down. pointer: u32, - /// Where. + /// Where it was pressed. point: [f32; 2], /// Which button. button: Button, - /// Keys held. + /// Modifier keys held. modifiers: Modifiers, - /// What pressed: a finger's picks reach farther. + /// The device that pressed; a finger's picks reach farther. device: Device, }, /// A pointer moved. Move { /// Which pointer. pointer: u32, - /// Where to. + /// Where it moved to. point: [f32; 2], - /// Keys held. + /// Modifier keys held. modifiers: Modifiers, - /// What moved. + /// The device that moved. device: Device, }, - /// A pointer let go. + /// A pointer was released. Up { /// Which pointer. pointer: u32, - /// Where. + /// Where it was released. point: [f32; 2], - /// Keys held. + /// Modifier keys held. modifiers: Modifiers, }, - /// The platform took a pointer away. + /// The platform cancelled a pointer. Cancel { /// Which pointer. pointer: u32, @@ -47,7 +47,7 @@ pub enum Input { point: [f32; 2], /// How far. delta: [f32; 2], - /// Keys held. + /// Modifier keys held. modifiers: Modifiers, }, /// A key went down or up while the view had focus. @@ -56,7 +56,7 @@ pub enum Input { key: Key, /// Whether it went down. down: bool, - /// Keys held. + /// Modifier keys held. modifiers: Modifiers, }, /// The pointer left the view. @@ -68,13 +68,13 @@ pub enum Input { pub enum Button { /// The main button: left, or a touch. Primary, - /// The context button: right. + /// The context-menu button: right. Secondary, /// The middle button or wheel press. Middle, } -/// What the user acts with. +/// The input device. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Serialize, schemars::JsonSchema)] #[serde(rename_all = "lowercase")] pub enum Device { @@ -85,11 +85,11 @@ pub enum Device { Pen, /// A finger: picks reach 8 CSS pixels farther. Touch, - /// The keys. + /// The keyboard. Keyboard, } -/// Keys held during an input. +/// Modifier keys held during an input. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Serialize, schemars::JsonSchema)] pub struct Modifiers { /// Shift. @@ -109,7 +109,7 @@ impl Modifiers { } } -/// The keys a view answers. +/// The keys a view responds to. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Key { /// Escape. diff --git a/crates/latkit/src/view/kept.rs b/crates/latkit/src/view/kept.rs index 17c73fd1..458bea22 100644 --- a/crates/latkit/src/view/kept.rs +++ b/crates/latkit/src/view/kept.rs @@ -1,30 +1,33 @@ -//! The rows a drawing keeps whose pages it does not hold, by their place in its order: an -//! open-addressed table whose buckets hold a place and its held row, `[NONE, NONE]` where empty, laid -//! out three fifths full, any number of buckets. A table laid out from another takes out the rows that -//! went and puts in those that came while it stays between three tenths and four fifths full: only -//! their buckets, and the few after a row taken out that move back, change, so the end table's words -//! change where rows did; past those it is laid out again, a little larger or smaller, never more -//! than it must be. A probe for a place ends within a few buckets, on the GPU as here -//! (`lib/ends.wesl`). +//! The kept-row table: a drawing's kept rows, keyed by their place in its order. +//! +//! It is an open-addressed hash table with linear probing. Each bucket holds a place and its held +//! row, `[NONE, NONE]` when empty. A new table is built three fifths full, with at least `LEAST` +//! buckets and any count above that, not just powers of two. Updating from a previous table +//! removes the rows that left and inserts those that came, as long as it stays between three +//! tenths and four fifths full. Only their buckets, and the few after a removed row that shift +//! back, change, so the end table's words change only where rows did. Past those bounds the table +//! is rebuilt, slightly larger or smaller, never larger than needed. A lookup ends within a few +//! buckets, on the GPU (`lib/ends.wesl`) as here. use std::sync::Arc; use crate::gpu::shaders as wgsl; -/// Buckets a table has at least, however few rows it keeps. +/// The fewest buckets a table has once it has any. const LEAST: usize = 64; const EMPTY: [u32; 2] = [wgsl::NONE, wgsl::NONE]; +/// A drawing's kept rows: buckets of `[place, held row]`, as the module docs describe. #[derive(Clone, Default, Debug, PartialEq)] pub(crate) struct KeptTable(Arc<[[u32; 2]]>); impl KeptTable { - /// The table of `rows`, each a place and its held row, laid out from `before`: where `before` has - /// room for them, its buckets with the places `gone` taken out and the rows `came` put in; else - /// laid out afresh. + /// The table of `rows`, each `[place, held row]`, updated from `before`. If `before`'s size + /// suits `rows`, its buckets are reused with places `gone` removed and rows `came` inserted; + /// otherwise the table is rebuilt. pub(crate) fn laid(before: &KeptTable, rows: &[[u32; 2]], gone: &[u32], came: &[[u32; 2]]) -> KeptTable { let buckets = before.0.len(); - // The least table stays however few rows it keeps. + // A table of the fewest buckets is updated in place however few rows it keeps. if buckets > 0 && rows.len() * 5 <= buckets * 4 && (rows.len() * 10 >= buckets * 3 || buckets == LEAST) { let mut table = before.0.to_vec(); for &d in gone { @@ -45,7 +48,7 @@ impl KeptTable { KeptTable(table.into()) } - /// The held row of place `d`, if it keeps it. + /// The held row of place `d`, if kept. pub(crate) fn held(&self, d: u32) -> Option { if self.0.is_empty() { return None; @@ -66,8 +69,8 @@ impl KeptTable { } } -/// The bucket a probe for place `d` starts at among `buckets`, as `lib/ends.wesl` finds it: its bits -/// hashed. +/// The bucket, of `buckets`, where a probe for place `d` starts: a hash of `d`'s bits. Must match +/// `home` in `lib/ends.wesl`. fn home(d: u32, buckets: usize) -> usize { let mut h = d ^ (d >> 16); h = h.wrapping_mul(0x7feb_352d); @@ -76,7 +79,7 @@ fn home(d: u32, buckets: usize) -> usize { (h ^ (h >> 16)) as usize % buckets } -/// The bucket after `b`, the first after the last. +/// The bucket after `b`, wrapping to 0. fn next(b: usize, buckets: usize) -> usize { if b + 1 == buckets { 0 } else { b + 1 } } @@ -90,7 +93,7 @@ fn insert(table: &mut [[u32; 2]], row: [u32; 2]) { table[b] = row; } -/// Takes place `d` out, moving back each row after it whose probe passed its bucket, so every probe +/// Removes place `d`, shifting back each later row whose probe passed its bucket, so every probe /// still ends at the first empty bucket. fn remove(table: &mut [[u32; 2]], d: u32) { let mut hole = home(d, table.len()); @@ -107,7 +110,8 @@ fn remove(table: &mut [[u32; 2]], d: u32) { if place == wgsl::NONE { break; } - // A row whose probe starts after the hole, up to where it lies, stays: its probe never passed. + // A row whose probe starts after the hole and at or before its own bucket stays: its probe + // never passed the hole. let home = home(place, table.len()); let stays = if hole <= b { hole < home && home <= b } else { hole < home || home <= b }; if !stays { @@ -124,19 +128,19 @@ mod tests { use super::*; - /// A stream of random numbers below `n`. + /// The next pseudo-random number below `n`, advancing `seed`. fn random(seed: &mut u64, n: u32) -> u32 { *seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407); ((*seed >> 33) % u64::from(n)) as u32 } #[test] - fn a_table_holds_every_row_kept_and_no_other() { + fn a_table_holds_every_kept_row_and_no_other() { let mut seed = 7; let mut kept: BTreeMap = BTreeMap::new(); let mut table = KeptTable::default(); for round in 0..200 { - // Places come or go, each once a layout. + // Each round, places come or go, each at most once. let (mut gone, mut came) = (Vec::new(), Vec::new()); for _ in 0..random(&mut seed, 40) { let d = random(&mut seed, 5_000); @@ -161,7 +165,7 @@ mod tests { } #[test] - fn rows_coming_and_going_change_the_buckets_near_them_alone() { + fn rows_coming_and_going_change_only_nearby_buckets() { let rows: Vec<[u32; 2]> = (0..1_000).map(|d| [d * 7, d]).collect(); let table = KeptTable::laid(&KeptTable::default(), &rows, &[], &[]); let gone: Vec = (0..10).map(|d| d * 70).collect(); @@ -169,24 +173,24 @@ mod tests { let now: Vec<[u32; 2]> = rows.iter().filter(|r| !gone.contains(&r[0])).copied().chain(came.iter().copied()).collect(); let next = KeptTable::laid(&table, &now, &gone, &came); - assert_eq!(next.buckets().len(), table.buckets().len(), "laid in place"); + assert_eq!(next.buckets().len(), table.buckets().len(), "updated in place"); let changed = table.buckets().iter().zip(next.buckets()).filter(|(a, b)| a != b).count(); assert!(changed <= 60, "{changed} buckets changed for 20 rows that came or went"); } #[test] - fn a_table_is_laid_out_again_past_four_fifths_full_and_under_three_tenths() { + fn a_table_is_rebuilt_past_four_fifths_full_and_under_three_tenths() { let rows = |n: u32| (0..n).map(|d| [d, d]).collect::>(); let small = KeptTable::laid(&KeptTable::default(), &rows(10), &[], &[]); - assert_eq!(small.buckets().len(), LEAST, "few rows, the least buckets"); + assert_eq!(small.buckets().len(), LEAST, "few rows get the fewest buckets"); let full = KeptTable::laid(&small, &rows(100), &[], &(10..100).map(|d| [d, d]).collect::>()); - assert_eq!(full.buckets().len(), 167, "past four fifths of 64, laid out three fifths full"); + assert_eq!(full.buckets().len(), 167, "past four fifths of 64, rebuilt three fifths full"); let more = KeptTable::laid(&full, &rows(130), &[], &(100..130).map(|d| [d, d]).collect::>()); - assert_eq!(more.buckets().len(), 167, "up to four fifths full, in place"); + assert_eq!(more.buckets().len(), 167, "up to four fifths full, updated in place"); let gone: Vec = (51..130).collect(); assert_eq!(KeptTable::laid(&more, &rows(51), &gone, &[]).buckets().len(), 167, "three tenths, in place"); let gone: Vec = (20..130).collect(); - assert_eq!(KeptTable::laid(&more, &rows(20), &gone, &[]).buckets().len(), LEAST, "under three tenths"); + assert_eq!(KeptTable::laid(&more, &rows(20), &gone, &[]).buckets().len(), LEAST, "under three tenths, rebuilt"); let gone: Vec = (0..130).collect(); assert_eq!(KeptTable::laid(&more, &[], &gone, &[]), KeptTable::default(), "no rows, no buckets"); } diff --git a/crates/latkit/src/view/labels.rs b/crates/latkit/src/view/labels.rs index 52e4017b..8a79b9fb 100644 --- a/crates/latkit/src/view/labels.rs +++ b/crates/latkit/src/view/labels.rs @@ -1,12 +1,17 @@ -//! Labels: each labeled mark keeps a candidate per row, in pages of candidates, and in the labels -//! buffer a run table from its text codes to where the atlas holds each text, or, until the text is -//! set, its size, measured from its font's characters. Draws place every label from its anchor each frame. Placement decides which show: it runs -//! on the GPU when the camera, a labeled mark's state, or a run table changed, as candidates, then -//! rounds of claim and decide on a grid of cells, and labels fade in and out after it. Only placed -//! labels ask for their texts, so the host sets no more texts than fit on screen however many rows -//! are labeled. A diagram's labels scale with the sheet and show wherever they fit, so there -//! placement only gathers the texts the atlas lacks. Those are read back, with the texts of labels -//! shown, which the atlas keeps; the host sets a few each frame, and they show the frame after. +//! Labels. +//! +//! Each labeled mark keeps one candidate per row, in candidate pages. In the labels buffer it keeps +//! a run table mapping its text codes to where the atlas holds each text, or, until the text is +//! set, its size measured from its font's characters. Draws place every label from its anchor each +//! frame. +//! +//! Placement decides which labels show. It runs on the GPU when the camera, a labeled mark's state, +//! or a run table changes: candidates are measured, then rounds of claim and decide on a grid of +//! cells pick the labels shown, and labels fade in and out after it. Only placed labels request +//! their texts, so the host sets no more texts than fit on screen, however many rows are labeled. +//! A diagram's labels scale with the sheet and show wherever they fit, so there placement only +//! gathers the texts the atlas lacks. Requested texts are read back along with the texts of shown +//! labels, which the atlas keeps; the host sets a few each frame, and they show the frame after. use std::collections::HashMap; use std::ops::Range; @@ -29,14 +34,15 @@ use crate::host::TextRun; /// Rounds of claim and decide each placement runs. const ROUNDS: u32 = 6; -/// Words of the select capping a mark's labels: a digit's buckets, then its state. +/// Words of the radix select that caps a mark's labels: one digit's 2048 buckets, then 3 words of +/// state. const SELECT_WORDS: u32 = 2048 + 3; -/// Digits of a key the select finds, one pass each. +/// Digits of a key the select resolves, one pass each. const DIGITS: u32 = 3; /// Seconds labels fade in or out over. const FADE: f64 = 0.2; -/// Texts the host sets per frame at most, and the time setting them and making their distance -/// fields may take. +/// The most texts the host sets per frame, and the most time setting them and building their +/// distance fields may take. const BUDGET: usize = 48; const BUDGET_TIME: Duration = Duration::from_millis(4); @@ -48,7 +54,7 @@ pub(crate) struct Slots { pub(crate) seen: u32, /// The select capping the mark's labels, or `NONE` without a cap. pub(crate) select: u32, - /// Codes the slots hold. + /// Codes its run table and seen bits have room for. codes: u32, } @@ -60,17 +66,18 @@ pub(crate) struct Labels { /// The cell grid: where it starts, its columns, and its rows. pub(crate) cells: [u32; 3], words: u32, - /// What placement last ran from, and the readback of the texts it asked for. + /// The stamp placement last ran from, and the readback of the texts it requested. placed: Derived<()>, - /// The labeled marks' slots when placement asked. + /// The labeled marks' slots when placement requested texts. asked: Vec<(usize, Slots)>, /// Labels fade until then, on the view's clock. fading: Option, - /// Whether the last placement that landed asked for texts, which the next one shows. + /// Whether the last placement read back led to texts being set (or left to set), which the + /// next placement shows. wanting: bool, - /// Each labeled mark's written codes, with the run and where it lay. + /// Each labeled mark's codes whose texts are set, with the run and its glyphs in the atlas. written: HashMap>, - /// Each labeled mark's codes whose run table entries hold a run or an estimate. + /// How many of each labeled mark's codes have run table entries, set or estimated. estimated: HashMap, evictions: u64, /// Bumps whenever a run table changes. @@ -78,8 +85,8 @@ pub(crate) struct Labels { } impl Labels { - /// Whether every label placed shows its text: no placement is read back, and the last asked - /// for no text it lacked. + /// Whether every placed label shows its text: no placement is being read back, and the last + /// one requested no missing text. pub(crate) fn complete(&self) -> bool { !self.placed.busy() && !self.wanting } @@ -100,8 +107,8 @@ impl Labels { } impl View { - /// The run each code of mark `i`'s label text is set as: at the base size, whatever size it shows - /// at, as a diagram's sheet measures it. + /// The text run for code `code` of mark `i`'s label text, at the base size regardless of the + /// size it shows at, as a diagram's sheet measures it. fn run_of(&self, i: usize, code: u32, inner: &mut Inner) -> Option { let shown = &self.marks[i]; let compiled = shown.compiled.as_ref()?; @@ -129,8 +136,8 @@ impl View { inner.dictionaries.field(d, &t.name, &column.name).len() as u32 } - /// Sets the texts a placement asked for, within the frame's budget; placement asks again for - /// those left. + /// Sets the texts the last placement requested, within the frame's budget. The next placement + /// requests any left over. pub(super) fn settle_labels(&mut self, inner: &mut Inner) { let Some((_, bytes)) = self.labels.placed.landed() else { return }; let asked = std::mem::take(&mut self.labels.asked); @@ -142,7 +149,7 @@ impl View { continue; } let first = self.labels.slots.iter().flatten().map(|s| s.wanted).min().unwrap_or(0); - // Texts labels show stay in the atlas. + // Touch the texts shown labels use, so the atlas keeps them. let seen = (slots.seen - first) as usize; if let (Some(bits), Some(written)) = (words.get(seen..seen + (slots.codes / 32) as usize), self.labels.written.get(&i)) @@ -191,8 +198,9 @@ impl View { self.labels.written.entry(i).or_default().insert(code, (run, glyphs)); } - /// Lays each labeled mark's slots out in the labels buffer, starting it over when they moved; - /// drops run table entries the atlas evicted. + /// Lays out each labeled mark's slots in the labels buffer, replacing the buffer and starting + /// labels over when the layout changed, and fixes run table entries whose texts the atlas + /// evicted. pub(super) fn lay_out_labels(&mut self, inner: &mut Inner) { let mut slots: Vec> = vec![None; self.marks.len()]; let labeled: Vec = (0..self.marks.len()) @@ -200,8 +208,8 @@ impl View { self.marks[i].drawable() && self.marks[i].compiled.as_ref().is_some_and(|c| c.drawing.label.is_some()) }) .collect(); - // Wanted codes, then seen codes, so one copy reads them all; then the cells; then each mark's - // run table. + // Every mark's wanted codes, then every mark's seen bits, so one copy reads them all; then + // the cells; then each mark's run table, and its select if capped. let mut at = 0; let codes: Vec = labeled.iter().map(|&i| self.codes_of(i, inner).next_power_of_two().max(64)).collect(); for &i in &labeled { @@ -248,7 +256,7 @@ impl View { if inner.atlas.evictions != self.labels.evictions { self.labels.evictions = inner.atlas.evictions; self.drop_evicted(inner); - // Port labels look for their texts again where the atlas holds them now. + // Repack port labels so they find their texts where the atlas now holds them. for shown in self.marks.iter_mut().filter(|m| m.port_count() > 0) { shown.packed = false; } @@ -256,8 +264,8 @@ impl View { self.estimate_runs(inner); } - /// Writes the run table entries of each labeled mark's codes new since the last frame: where the - /// atlas holds a text, else its size as measured, so its label is placed before it is set. + /// Writes run table entries for each labeled mark's codes new since the last frame: where the + /// atlas holds the text, else its measured size, so the label is placed before its text is set. fn estimate_runs(&mut self, inner: &mut Inner) { for i in 0..self.labels.slots.len() { let Some(slots) = self.labels.slots[i] else { continue }; @@ -276,8 +284,8 @@ impl View { } } - /// The run table words of mark `i`'s codes `codes`: where the atlas holds each text set, and the - /// size of the rest, as measured. + /// The run table words for mark `i`'s `codes`: where the atlas holds each text already set, and + /// the measured size of the rest. fn run_words(&self, i: usize, codes: Range, inner: &mut Inner) -> Vec { let shown = &self.marks[i]; let written = self.labels.written.get(&i); @@ -310,8 +318,8 @@ impl View { words } - /// Puts measured sizes back in run table entries whose runs the atlas no longer holds where they - /// were. + /// Restores measured sizes in run table entries whose texts the atlas no longer holds where + /// they were. fn drop_evicted(&mut self, inner: &mut Inner) { let host = inner.host.clone(); let mut cleared = Vec::new(); @@ -336,7 +344,7 @@ impl View { self.labels.changes += 1; } - /// The labels' words of the frame uniform, the clock having moved `dt` seconds. + /// Writes the labels' fields of the frame uniform, after the clock moved `dt` seconds. pub(super) fn label_words(&mut self, frame: &mut wgsl::Frame, dt: f64) { let [cells, columns, rows] = self.labels.cells; frame.cells = cells; @@ -345,8 +353,8 @@ impl View { frame.label_fade = if self.moves() { (dt / FADE).min(1.0) as f32 } else { 1.0 }; } - /// Starts over the labels of rows whose slots took pages of other rows: their candidates hold - /// nothing placed before, and fade in from nothing. + /// Restarts the labels of rows whose slots now hold pages of other rows: their candidates are + /// cleared, so they fade in from nothing. fn clear_bound(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner) { let words = u64::from(wgsl::PAGE_ROWS * wgsl::CANDIDATE_WORDS) * 4; let queue = &self.gpu.0.queue; @@ -360,8 +368,8 @@ impl View { } } - /// Places labels when what they would show changed, and fades them; reads back the texts - /// placement asked for. Labels wait for what places them to compile. + /// Places labels when their inputs changed, and fades them; reads back the texts placement + /// requested. Labels wait until the placement pipelines have compiled. pub(super) fn place_labels(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner, eye: &wgsl::Eye) { self.clear_bound(encoder, inner); if !self.texts() { @@ -372,11 +380,11 @@ impl View { if labeled.is_empty() { return; } - // A diagram's labels follow the camera without placement; the camera only says which texts - // they ask for. + // A diagram's labels follow the camera without placement; the camera only decides which + // texts they request. let diagram = matches!(self.scene.space, Space::Diagram(_)); - // What places labels: a color changing every frame moves none, and a fit about to move the - // camera places them once it did. + // What placement depends on: a color changing every frame moves no label, and a pending fit + // delays placement until the camera has moved. let versions: Vec = labeled.iter().map(|&(i, _)| self.marks[i].place_version).collect(); let stamp = stamp_of((bytemuck::bytes_of(eye), self.labels.changes, versions)); let due = self.labels.placed.due(stamp) && !self.labels.placed.busy() && !self.fitting.framing(); @@ -408,8 +416,8 @@ impl View { pass.set_bind_group(0, &groups.view, &[]); let marks: Vec<(usize, u32, u32)> = labeled.iter().map(|&(i, _)| (i, self.marks[i].rows, self.marks[i].path())).collect(); - // Candidates are measured where their anchors put them, by a program made for each mark's - // primitive; placement reads what it wrote. + // Measure candidates at their anchors, with a pipeline for each mark's primitive; + // placement reads the result. if due { pass.set_bind_group(2, &measuring, &[]); for &(i, rows, path) in &marks { @@ -445,7 +453,7 @@ impl View { dispatch(pass, columns * rows); }; if due && let Some([clear, reset]) = &cleared { - // A capped mark's labels past its cap give up their room before any claims. + // A capped mark's labels past its cap drop out before any claims. if let Some(pipelines) = &selecting { select(&mut pass, pipelines, &groups.mark, &capped); } @@ -467,7 +475,7 @@ impl View { } } - /// Clears the texts each of `labeled` asked for and showed, before a placement asks again. + /// Clears the texts each of `labeled` requested and showed, before a placement requests again. fn forget_asked(&self, encoder: &mut wgpu::CommandEncoder, labeled: &[(usize, Slots)]) { let labels = self.resources.as_ref().expect("created before labels").labels(); for &(_, slots) in labeled { @@ -476,7 +484,7 @@ impl View { } } - /// Reads back the texts the placement of `labeled`, from `stamp`, asked for. + /// Reads back the texts the placement of `labeled` (from `stamp`) requested. fn read_placed( &mut self, encoder: &mut wgpu::CommandEncoder, @@ -495,8 +503,8 @@ impl View { } } -/// Keeps each `capped` mark's labels to its cap, those of the highest keys: a radix select over the -/// candidates' keys, a digit at a time, then a cut of those below. +/// Limits each `capped` mark's labels to its cap, keeping the highest keys: a radix select over +/// the candidates' keys, one digit per pass, then a cut of those below. fn select( pass: &mut wgpu::ComputePass<'_>, [start, count, find, cut]: &[wgpu::ComputePipeline; 4], @@ -522,9 +530,9 @@ fn select( } impl View { - /// Draws every labeled mark's labels, then every vertex mark's port labels: `glyphs` are the - /// pass groups of the labels' and the port labels' shaders. - #[allow(clippy::too_many_arguments, reason = "the main pass's state, as it draws")] + /// Draws every labeled mark's labels, then every vertex mark's port labels. `glyphs` holds the + /// shader and pass bind group for labels, then for port labels. + #[allow(clippy::too_many_arguments, reason = "the main pass's draw state")] pub(super) fn draw_labels( &self, pass: &mut wgpu::RenderPass<'_>, diff --git a/crates/latkit/src/view/legends.rs b/crates/latkit/src/view/legends.rs index ee5aef07..c866165e 100644 --- a/crates/latkit/src/view/legends.rs +++ b/crates/latkit/src/view/legends.rs @@ -1,7 +1,9 @@ -//! A scene's title and its marks' legends, written over the marks: the title and subtitle at the top -//! left, and the legends stacked up from the bottom right, each under its title: a bar of its mark's -//! colormap across the domain, ticked; a cell of color for each category; or, where categories are -//! named, a swatch beside each name. Rebuilt each frame as overlay sprites, O(legends). +//! A scene's title and its marks' legends, drawn over the marks. +//! +//! The title and subtitle sit at the top left. Legends stack upward from the bottom right, each +//! under its title: a ticked bar of the mark's colormap across its domain, a color cell per +//! category, or, for named categories, a swatch beside each name. Rebuilt each frame as overlay +//! sprites, in O(legends). use std::sync::Arc; @@ -11,7 +13,7 @@ use crate::gpu::Inner; use crate::gpu::shaders as wgsl; use crate::{Axis, Color, Colormap, ColormapKind, Theme}; -/// CSS pixels between the view's edge and what is written there. +/// CSS pixels between the view's inset edge and the title or legends. const MARGIN: f64 = 16.0; /// A legend's bar: width and height, CSS pixels. const BAR: [f64; 2] = [220.0, 8.0]; @@ -21,16 +23,17 @@ const CELLS: usize = 64; const CATEGORIES: usize = 24; /// The most names a column of a legend lists. const ROWS: usize = 8; -/// The size of what a legend writes beside its colors, ticks and names, for the theme's text size. +/// Text scale of a legend's tick labels and names, relative to the theme's text size. const SMALL: f64 = 0.9; -/// The sizes of the title and subtitle for the theme's text size, and the line each takes for its size. +/// Text scales of the title and subtitle, relative to the theme's text size, and their line height +/// relative to their own size. const TITLE: f64 = 1.75; const SUBTITLE: f64 = 1.1; const LEADING: f64 = 1.35; impl View { - /// CSS pixels the scene's title and subtitle take below the top inset, with the margin above them - /// and half of it below: none when it has neither. + /// CSS pixels the title and subtitle take below the top inset, including the margin above them + /// and half a margin below; 0 when the scene has neither. pub(super) fn title_band(&self) -> f64 { let font = f64::from(self.scene.theme.text.size); let lines: f64 = [(self.scene.title.is_some(), TITLE), (self.scene.subtitle.is_some(), SUBTITLE)] @@ -84,9 +87,9 @@ impl View { } } -/// Draws a legend of `colormap`'s named categories ending at `corner`, its right edge at its x and its -/// bottom at its y: a swatch of each category's color beside its name, in columns of up to -/// `ROWS`, under `title`, placed whole on pixels of `ratio`. How tall it is. +/// Draws a legend of `colormap`'s named categories with its bottom-right corner at `corner`: a +/// swatch of each category's color beside its name, in columns of up to `ROWS`, under `title`, +/// snapped to device pixels of `ratio`. Returns its height. fn categories( sprites: &mut Vec, theme: &Theme, @@ -103,7 +106,7 @@ fn categories( let rows = names.len().min(ROWS); let top = corner[1] - rows as f64 * row; let start = sprites.len(); - // Laid out from x = 0, each column clear of the names before it, then moved into place. + // Lay out from x = 0, each column clear of the names before it, then shift it all into place. let mut x = 0.0; for (column, names) in names.chunks(ROWS).enumerate() { let mut right = x; @@ -135,8 +138,8 @@ fn categories( height } -/// Draws a legend whose bar ends at `corner`'s x, its tick labels' bottom at its y: `colormap` across -/// `domain` under `title`. How tall it is. +/// Draws a bar legend of `colormap` across `domain`, under `title`, with the bar's right end at +/// `corner`'s x and the tick labels' bottom at its y. Returns its height. fn legend( sprites: &mut Vec, theme: &Theme, diff --git a/crates/latkit/src/view/lists.rs b/crates/latkit/src/view/lists.rs index c8038dfb..c20ac575 100644 --- a/crates/latkit/src/view/lists.rs +++ b/crates/latkit/src/view/lists.rs @@ -1,8 +1,10 @@ -//! Lists on the GPU: each item counts its entries, a scan turns the counts into where each item's -//! entries start, and each item writes its entries there. The total lands in the list's header as -//! indirect draw and dispatch arguments, so nothing waits on a readback to know it. After the -//! segment lists lie the words of the rows each mark drawing a table in part draws: which held row -//! each is, expanded from its runs when they change, so every pass reads a drawn row in one word. +//! Lists on the GPU: each item counts its entries, a scan turns the counts into start offsets, and +//! each item writes its entries there. The total goes into the list's header as indirect draw and +//! dispatch arguments, so nothing waits on a readback for it. +//! +//! After the segment lists lie the drawn-row words of each mark drawing a paged table: the held +//! row of each row drawn, expanded from the drawing's runs when they change, so every pass reads a +//! drawn row in one word. use super::View; use super::marks::Shown; @@ -13,11 +15,11 @@ use crate::gpu::readback::Readback; use crate::gpu::shaders::{self as wgsl, Shader}; use crate::scene::mark::MarkKind; -/// A stroke mark's segment list, when its rows' paths have different numbers of segments: where it -/// lies, a stamp of what it was last built from (the list, the paths, and for great circles where -/// the vertices are drawn), and its count. Each entry is a segment's row and how far along its path it -/// starts. A curved mark's count is read back, one at a time, and its list makes room for what it -/// found. +/// A listed mark's segment list (see `Shown::listed`), used when its rows have different numbers +/// of segments, or for areas, of triangles: where it lies, a stamp of its last build's inputs (the +/// list, the paths, and for great circles where the vertices are drawn), and its count. Each entry +/// is a segment's row and how far along its path it starts. A curved mark's count is read back, +/// one readback at a time, and its list grows to fit what was found. #[derive(Default)] pub(crate) struct Segments { pub(crate) list: Option, @@ -28,9 +30,8 @@ pub(crate) struct Segments { /// Words a segment list keeps per segment: its row, and its distance along. const SEGMENT_WORDS: u32 = 2; -/// Where the words of the rows a mark drawing a table in part draws lie in the lists buffer, -/// `[start, room]` in words, and the stamp of what they were last expanded from: the drawing, where -/// they lie, and the buffer. +/// Where a paged mark's drawn-row words lie in the lists buffer, `[start, room]` in words, and the +/// stamp of their last expansion's inputs: the drawing, their location, and the buffer. #[derive(Default)] pub(crate) struct DrawnRows { pub(crate) at: Option<[u32; 2]>, @@ -43,7 +44,7 @@ pub(crate) struct List { pub(crate) header: u32, pub(crate) items: u32, pub(crate) entries: u32, - /// Entries kept, and words per entry. + /// The most entries it keeps, and words per entry. pub(crate) kept: u32, pub(crate) stride: u32, sums: u32, @@ -69,7 +70,7 @@ impl List { items.div_ceil(wgsl::SCAN_WORKGROUP).max(1) + items + 1 } - /// Where the scan leaves the list's total, before it is held to what the list keeps. + /// Where the scan writes the list's total, before it is capped at `kept`. pub(crate) fn total(&self) -> u32 { self.counts + self.items } @@ -90,8 +91,8 @@ impl List { } impl View { - /// Lays out every listed stroke mark's segment list at the front of the lists buffer, taking - /// counts read back: a list too small for what its rows were found to have grows. + /// Lays out every listed mark's segment list at the front of the lists buffer, using counts + /// read back: a list smaller than its rows were found to need grows, with a quarter to spare. pub(super) fn lay_out_segments(&mut self) { let mut at = 0; for shown in &mut self.marks { @@ -117,7 +118,7 @@ impl View { at += List::front_words(list.kept, SEGMENT_WORDS) + List::back_words(items); shown.segments.list = Some(list); } - // Then a word for each row a mark drawing a table in part draws, those that left included. + // Then one word per row each paged mark draws, rows that left included. for shown in &mut self.marks { if shown.drawing().is_none() { shown.drawn_rows.at = None; @@ -134,8 +135,8 @@ impl View { self.list_words = at; } - /// The marks whose drawn rows' words are written again this frame: where their drawing, where - /// the words lie, or the lists buffer changed. + /// The marks whose drawn-row words are rewritten this frame: those whose drawing, rows, word + /// location, or lists buffer changed. pub(super) fn drawn_rows_due(&mut self) -> Vec { let generation = self.resources.as_ref().map_or(0, super::resources::Resources::lists_generation); let mut due = Vec::new(); @@ -151,14 +152,14 @@ impl View { due } - /// The marks whose segment lists build again this frame: their list, the lists buffer, or their - /// paths changed, or, on great circles, where the vertices their rows join are drawn. A color - /// changing every frame builds none. + /// The marks whose segment lists rebuild this frame: those whose list, lists buffer, paths, or + /// drawing changed, or, along great circles or lanes, where the vertices their rows join are + /// drawn. A color changing every frame rebuilds none. pub(super) fn segments_due(&mut self) -> Vec { let vertices: Vec<(bool, u64)> = self.marks.iter().filter(|m| m.kind() == MarkKind::Vertices).map(Shown::places_drawn).collect(); let generation = self.resources.as_ref().map_or(0, super::resources::Resources::lists_generation); - // Lanes draw as the screen shows them: they count again as the camera moves. + // Lanes are laid out on screen, so they count again whenever the camera moves. let projected = self.projected(); let sight = (projected.clip_from_world.to_cols_array().map(f64::to_bits), self.size.map(f64::to_bits)); let mut due = Vec::new(); @@ -177,9 +178,10 @@ impl View { due } - /// Reads back how many segments each mark along great circles holds, so a list too small grows: - /// one count at a time, never dropped for a newer list, which is counted once it lands. A mark - /// bowed into lanes alone has room for every piece of every lane, and reads back nothing. + /// Reads back how many segments each great-circle mark needs, so a list too small grows. Each + /// mark has one readback at a time: a newer list does not cancel it, and is counted after it + /// arrives. A mark only bowed into lanes always has room for every piece of every lane, so it + /// reads back nothing. pub(super) fn count_found(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &Inner) { let gpu = self.gpu.clone(); let Some(resources) = &self.resources else { return }; @@ -199,7 +201,7 @@ impl View { } impl Shown { - /// Whether how many segments its list holds is being read back, or is due to be. + /// Whether its segment count is being read back, or is due to be. pub(crate) fn counting(&self) -> bool { let found = &self.segments.found; found.busy() || (self.curved() && self.geodesic && self.drawable() && found.due(self.segments.built)) diff --git a/crates/latkit/src/view/marks.rs b/crates/latkit/src/view/marks.rs index 000ef007..928a393a 100644 --- a/crates/latkit/src/view/marks.rs +++ b/crates/latkit/src/view/marks.rs @@ -1,4 +1,4 @@ -//! Committed marks: a scene's mark, its program compiled against its data, and its GPU state. +//! Shown marks: a scene's mark, its program compiled against its data, and its GPU state. use std::sync::Arc; @@ -15,7 +15,7 @@ use crate::scene::compile::{self, Compiled}; use crate::scene::mark::{Context, Key, MarkKind, Primitive}; use crate::{Error, Mark, Port, Result, Shape, Snapshot}; -/// Where a mark's parts sit in the program buffer, as absolute word offsets. +/// Where each part of a mark's region lies in the program buffer, as absolute word offsets. #[derive(Clone, Copy, PartialEq, Debug)] pub(crate) struct Offsets { pub(crate) columns: u32, @@ -26,9 +26,11 @@ pub(crate) struct Offsets { pub(crate) palette: u32, pub(crate) port_info: u32, pub(crate) blends: u32, - /// The runs of held rows it draws, `NONE` where it draws every row as itself. + /// The runs of held rows it draws; `NONE` without a drawing, when its held rows are its table's + /// rows. pub(crate) runs: u32, - /// The page table of each kind of words it keeps per row, by `Kept`; `NONE` where it keeps none. + /// The page table of each kind of per-row words, indexed by `Kept`; `NONE` for a kind it does + /// not keep. pub(crate) pages: [u32; 6], } @@ -56,8 +58,8 @@ impl Default for Offsets { } } -/// What a mark's state holds: rows of a version of its table, as a drawing lays them out or as the -/// table orders them, and the origin of its dataset, whose rows are followed by id when they move. +/// The rows a mark's state holds: a version of its table, laid out by a drawing or in table order, +/// and the origin of its dataset. Rows that move are followed by id only within one origin. #[derive(Clone)] pub(crate) struct Holding { pub(crate) origin: Version, @@ -70,80 +72,85 @@ pub(crate) struct Shown { pub(crate) key: Key, pub(crate) compiled: Option, pub(crate) tables: Tables, - /// Rows the mark draws, as of the tables last published, and rows past them that left and fade - /// out. + /// Rows the mark draws, as of the last published tables. pub(crate) rows: u32, + /// Rows that left and are fading out, held after the live rows. pub(crate) exits: u32, - /// Rows it held when the frame began: what freezes. + /// Rows it held when the frame began; a starting transition freezes these. pub(crate) before: u32, - /// What its state holds, as the tables last published lay it out. + /// The rows its state holds, as of the last published tables. pub(crate) holding: Option, - /// How far the tiles of the table it draws in part have faded in or out, from 0 to 1. + /// How far its paged table's tiles have faded in or out, from 0 to 1. pub(crate) reveal: f32, - /// Slots of held rows given a page of other rows this frame, whose labels start over. + /// Slots that received a page of other rows this frame; their labels start over. pub(crate) bound: Vec, - /// Each fitted domain fitted over every row of a table drawn in part: the stamp of what it read, - /// and its words. + /// For a paged table, each fitted domain computed on the CPU over every row: the stamp of its + /// inputs, and its words. pub(crate) fitted: Vec<(u64, [u32; 2])>, - /// What it keeps per row, in pages of the arena. + /// Its per-row words, in pages of the arena. pub(crate) words: Words, - /// First fitted domain. + /// The index of its first fitted domain in the domains buffer. pub(crate) domains: u32, - /// Channels to evaluate, the row blocks they evaluate over, and whether fitted domains reduce, - /// next frame. Set through `dirty_rows` and `all_dirty`, so every channel made dirty names its rows. + /// The channels to evaluate next frame, as a bit mask. `blocks` holds the row blocks to + /// evaluate them over, and `reduce` whether fitted domains reduce. Set only through + /// `dirty_rows` and `all_dirty`, so every dirty channel has row blocks. pub(crate) dirty: u32, pub(crate) blocks: Blocks, pub(crate) reduce: bool, - /// Row blocks whose state words start over, in pages new to it: evaluation writes every word of - /// their rows before any channel does. + /// Row blocks whose state words start over in newly allocated pages. Evaluation writes every + /// word of their rows before any channel runs. pub(crate) fresh: Vec, - /// Where this frame's evaluation lists the row blocks it covers, and how many. + /// Where this frame's evaluation lists its row blocks in the blocks buffer, and how many. pub(crate) block: u32, pub(crate) block_count: u32, - /// The row blocks whose ends the end pass finds next frame: where rows or their ends changed, - /// and every block where the vertices they end at moved. Where this frame's end pass lists them, - /// and how many. + /// The row blocks whose endpoints the end pass resolves next frame: blocks whose rows or + /// endpoint references changed, and blocks whose rows end at vertices that moved. + /// `end_block` and `end_block_count` locate this frame's list in the blocks buffer. pub(crate) unresolved: Blocks, pub(crate) end_block: u32, pub(crate) end_block_count: u32, - /// Whether this frame's ease pass writes what the mark shows from its frozen and settled state. + /// Whether this frame's ease pass writes the mark's shown state from its frozen and settled + /// words. pub(crate) easing: bool, - /// The program's code and constants with colormap layers and text codes written in. + /// The program's code and constants, with colormap layers and text codes filled in. pub(crate) code: Vec<[u32; 4]>, pub(crate) constants: Vec, - /// Where the rows each end of its rows names are held, by end. + /// The end table of each endpoint reference: where the vertex rows it references are held. pub(crate) ends: Vec, - /// A vertex mark's ports, `PORT_WORDS` each, their labels' places in the atlas written in. + /// A vertex mark's port words, `PORT_WORDS` per port, including the atlas corners of its label. pub(crate) ports: Vec, - /// The mark's region of the program buffer, `[start, capacity]` in words, and where its parts - /// are; `packed` is false until the region holds the current words, and `written` what it holds. + /// The mark's region of the program buffer, `[start, capacity]` in words. `offsets` locates its + /// parts, `packed` is false until the region holds the current words, and `written` holds the + /// words last written there. pub(crate) region: Option<[u32; 2]>, pub(crate) offsets: Offsets, pub(crate) packed: bool, pub(crate) written: Vec, - /// A transition waiting to freeze, the one running, and where its remap words start; and whether - /// this frame's froze rows where they moved, so their comets' travel follows them. + /// A transition waiting to freeze. `transition` is the running one, `remap` where its remap + /// words start, and `carry` whether this frame froze rows at new places, so their comets' + /// travel follows them. pub(crate) begin: Option, pub(crate) transition: Option, pub(crate) remap: u32, pub(crate) carry: bool, - /// Where the header of the list this frame's query builds over its instances starts. + /// The probe-buffer word where this frame's query writes what it finds of this mark. pub(crate) query: u32, - /// The list numbering a stroke mark's segments when rows have different numbers of them. + /// The list that numbers a stroke mark's segments when rows have different segment counts. pub(crate) segments: super::lists::Segments, - /// Where the words of the rows it draws of a table drawn in part lie. + /// For a paged table, where each drawn row's held-row word lies in the lists buffer. pub(crate) drawn_rows: super::lists::DrawnRows, /// Whether an edge mark follows great circles. pub(crate) geodesic: bool, - /// Bumps whenever the mark's state is written, so what derives from it knows to follow; and - /// whenever what places its labels is: where its rows stand, how large and whether they show, - /// and their labels' texts and priorities. A color that changes every frame moves no label. + /// Incremented whenever the mark's state is written, so whatever derives from it updates. + /// `place_version` is incremented whenever the inputs of label placement are written: row + /// positions, sizes, and visibility, and label texts and priorities. A color that changes every + /// frame moves no label. pub(crate) version: u64, pub(crate) place_version: u64, /// The rows its focus draw lights. pub(crate) lit: super::focus::Lit, - /// Why it no longer compiles against the data, kept until data fits it again, and whether a frame - /// told it: the next frame does, unless a scene shown before then replaced the mark. + /// Why it no longer compiles against the data, kept until the data fits again, and whether a + /// frame has reported it. The next frame reports it unless a new scene replaces the mark first. pub(crate) failure: Option<(Error, bool)>, } @@ -200,18 +207,18 @@ impl Shown { self.key.kind } - /// Rows the mark holds evaluated whose data holds them: every per-row word is a held row's. A - /// table drawn as it is holds its rows; one drawn in part a page of rows in each slot. + /// Live held rows, not counting exits. Every per-row word belongs to a held row. An unpaged + /// table's held rows are its rows; a paged table holds a page of rows per slot. pub(crate) fn live(&self) -> u32 { self.drawing().map_or(self.rows, |d| d.live()) } - /// The drawing of the table it draws in part, as last published. + /// The drawing of its paged table, as last published. pub(crate) fn drawing(&self) -> Option<&Arc> { self.holding.as_ref().and_then(|h| h.drawing.as_ref()) } - /// The row of its table held row `h` holds. + /// The table row at held row `h`. pub(crate) fn source_row(&self, h: u32) -> Option { match self.drawing() { Some(drawing) => drawing.source(h), @@ -219,7 +226,7 @@ impl Shown { } } - /// The held row of row `r` of its table, if it holds it. + /// The held row of table row `r`, if the mark holds it. pub(crate) fn held_row(&self, r: u32) -> Option { match self.drawing() { Some(drawing) => drawing.held_row(r), @@ -227,7 +234,7 @@ impl Shown { } } - /// Where held row `h` draws among its rows, if it does. + /// The position of held row `h` among the rows the mark draws, if it draws it. pub(crate) fn item_of(&self, h: u32) -> Option { match self.drawing() { Some(drawing) => drawing.item_of(h), @@ -240,9 +247,9 @@ impl Shown { self.drawing().map_or(self.rows, |d| d.items - d.leaving_items) } - /// The runs of held rows it draws, as `lib/drawn.wesl` reads them: how many, then each one's - /// first row drawn, first held row, and fade; the rows that left last, steady. None where it - /// draws every row as itself. + /// The runs of held rows it draws, in the layout `lib/drawn.wesl` reads: the count, then each + /// run's first drawn row, first held row, and fade. Exits come last, as one steady run. Empty + /// without a drawing. fn runs(&self) -> Vec { let Some(drawing) = self.drawing() else { return Vec::new() }; let exits = (self.exits > 0).then_some([drawing.items, drawing.live(), 0]); @@ -256,32 +263,32 @@ impl Shown { words } - /// Rows the mark holds evaluated, those that left and fade out included. + /// Held rows, exits included. pub(crate) fn held(&self) -> u32 { self.live() + self.exits } - /// Whether the mark has a program, rows it holds and pages of words for them, and columns synced - /// at least once: a mark holding only the rows the ends of edges drawn name evaluates them, and - /// draws none. /// Whether the mark draws faces, which need normals. pub(crate) fn faces(&self) -> bool { self.kind() == MarkKind::Faces } + /// Whether the mark can evaluate and draw: it has a program, held rows with pages of words for + /// them, and columns synced at least once. A mark holding only the rows that drawn edges end at + /// evaluates them but draws none. pub(crate) fn drawable(&self) -> bool { self.compiled.is_some() && self.held() > 0 && self.tables.synced() && self.pages_held() } - /// Whether its pages of words per row hold every row it holds. + /// Whether its state pages have the current row width and cover every held row. pub(crate) fn pages_held(&self) -> bool { let state = self.words.pages(Kept::State); state.width == self.stride() + self.extras() && state.blocks.len() as u32 >= self.held().div_ceil(wgsl::PAGE_ROWS) } - /// Rows each page of its held rows holds: each slot's whole page of a table drawn in part, which - /// evaluates whole, or a table's own rows a page at a time; rows that left follow either. + /// The row count of each page of held rows: a full page per slot of a paged table, since slots + /// evaluate whole, or an unpaged table's rows split into pages. Exits follow in either case. pub(crate) fn page_rows(&self) -> Vec { let (live, held) = (self.live(), self.held()); let full = |start: u32| self.drawing().is_some() && start < live; @@ -293,7 +300,7 @@ impl Shown { .collect() } - /// Lets go of what it holds on the GPU, as a mark does when it goes. + /// Releases its tables and pages on the GPU, as when the mark is removed. pub(crate) fn release(&mut self, gpu: &mut Inner) { self.tables.release(gpu); self.words.release(&mut gpu.arena); @@ -309,8 +316,8 @@ impl Shown { self.compiled.as_ref().map_or(0, |c| c.drawing.extras) } - /// What its channels and fitted domains read besides its own table's static fields, which their - /// pipeline is made for: before its data says, neither joins, samples, nor the view. + /// What its channels and fitted domains read besides its own table's static fields, which picks + /// their pipeline variant. Before it compiles, neither joins, samples, nor the view. pub(crate) fn reads(&self) -> crate::gpu::pipelines::Reads { let Some(program) = self.compiled.as_ref().map(|c| &c.program) else { return crate::gpu::pipelines::Reads { joined: false, timed: false, viewed: false }; @@ -323,8 +330,8 @@ impl Shown { } } - /// The path its strokes read, or where its triangles' corners come from: one of the `PATH_` codes, - /// which its pipelines are made for. + /// The `PATH_` code its pipelines are built for: the path its strokes follow, or where its + /// triangles' corners come from. pub(crate) fn path(&self) -> u32 { match &self.compiled { Some(c) if c.drawing.primitive != Primitive::Points => c.drawing.path, @@ -332,9 +339,9 @@ impl Shown { } } - /// Whether it numbers its instances with a list: strokes whose rows' paths have different numbers - /// of segments, running through a geometry column, along great circles, or bowed into lanes; and - /// areas, whose rows have different numbers of triangles. + /// Whether a list numbers its instances: strokes whose rows have different segment counts + /// (through a geometry column, along great circles, or bowed into lanes), and areas, whose rows + /// have different triangle counts. pub(crate) fn listed(&self) -> bool { let Some(compiled) = &self.compiled else { return false }; match compiled.drawing.primitive { @@ -344,8 +351,9 @@ impl Shown { } } - /// Whether its edges curve between their ends, along great circles or bowed into lanes: how many - /// pieces each takes is found on the GPU and read back. + /// Whether its edges curve between their endpoints, along great circles or bowed into lanes. + /// The GPU counts each edge's pieces; for great circles, the total is read back to size the + /// segment list. pub(crate) fn curved(&self) -> bool { self.path() == wgsl::PATH_ENDS && (self.geodesic || self.laned()) } @@ -356,8 +364,8 @@ impl Shown { compiled.drawing.lane != wgsl::NONE && self.tables.holds(compiled.drawing.lane) } - /// Instances its list must have room for: every path's segments, each curve's pieces as last - /// found; or every area's triangles. + /// Instances its list needs room for: every path's segments plus each curve's pieces (as last + /// counted), or every area's triangles. pub(crate) fn segment_capacity(&self) -> u32 { let rows = (self.rows + self.exits) as usize; let Some(compiled) = &self.compiled else { return 1 }; @@ -375,7 +383,7 @@ impl Shown { (points + rows + pieces).max(1) as u32 } - /// Vertices each of its instances draws: a quad per point or segment, three corners per triangle. + /// Vertices per instance: 4 for the quad of a point or segment, 3 for a triangle. pub(crate) fn corners(&self) -> u32 { match self.compiled.as_ref().map(|c| c.drawing.primitive) { Some(Primitive::Triangles) => 3, @@ -383,8 +391,8 @@ impl Shown { } } - /// Its channels that say where its rows' labels go and whether they show: where rows stand, how - /// large they are and whether they show, and their labels' texts and priorities. + /// A bit mask of the channels label placement reads: row position, size, and visibility, and + /// label text and priority. pub(crate) fn placing_channels(&self) -> u32 { let Some(compiled) = &self.compiled else { return 0 }; let shaping = ["position", "radius", "width", "height", "elevation", "visible", "text", "priority"]; @@ -396,8 +404,8 @@ impl Shown { .fold(0, |mask, (k, c)| if shaping.contains(&&*c.name) { mask | 1 << k } else { mask }) } - /// A version of where its rows are drawn: their places, and while a transition runs, every frame - /// that eases them. + /// A version of where its rows are drawn: `place_version`, or while a transition runs, + /// `version`, which changes every frame that eases them. The flag says which one it is. pub(crate) fn places_drawn(&self) -> (bool, u64) { if self.transition.is_some() { (true, self.version) } else { (false, self.place_version) } } @@ -412,7 +420,7 @@ impl Shown { self.compiled.as_ref().map_or(0, |c| c.program.domains.len() as u32) } - /// Channels whose inputs include any of `deps`. + /// A bit mask of the channels whose inputs include any of `deps`. pub(crate) fn channels_reading(&self, deps: Deps) -> u32 { let Some(compiled) = &self.compiled else { return 0 }; compiled @@ -423,9 +431,8 @@ impl Shown { .fold(0, |mask, (k, c)| if c.reads.deps.intersects(deps) { mask | 1 << k } else { mask }) } - /// Channels and domains reading any column of `columns` become dirty: over the row blocks - /// `blocks` names, the pages that changed, while the channels read only their own rows; else over - /// every row. + /// Marks the channels and domains that read any of `columns` dirty. They evaluate over `blocks` + /// (the changed pages) when the channels read only their own rows, else over every row. pub(crate) fn columns_changed(&mut self, columns: u64, blocks: Option<&[u32]>) { let Some(compiled) = &self.compiled else { return }; let program = &compiled.program; @@ -439,7 +446,7 @@ impl Shown { // A fitted domain, or a table read through a join, reaches past a row's own block. let joined = program.columns.iter().enumerate().any(|(i, c)| c.from.is_some() && columns & Reads::column(i) != 0); - // Rows whose ends' references changed find them again. + // Rows whose endpoint references changed resolve their endpoints again. let ends = program .ends .iter() @@ -452,8 +459,8 @@ impl Shown { self.packed = false; } - /// Channels `channels` evaluate again on the next frame, over the row blocks `blocks` names, or - /// over every row for `None`; naming no block, none does. + /// Marks `channels` to evaluate next frame over the row blocks `blocks`, or over every row for + /// `None`. An empty block list marks nothing. pub(crate) fn dirty_rows(&mut self, channels: u32, blocks: Option<&[u32]>) { if channels != 0 && blocks.is_none_or(|b| !b.is_empty()) { self.dirty |= channels; @@ -461,9 +468,9 @@ impl Shown { } } - /// Whether any column of `columns` holds static values that show eased: values a channel reads, - /// or the path a stroke runs along. Changing one eases; a column only shaders read, such as a - /// lane, shows at once. + /// Whether any of `columns` holds static values that ease when they change: values a channel + /// reads, or the path a stroke follows. A column only shaders read, such as a lane, changes at + /// once. pub(crate) fn static_columns(&self, columns: u64) -> bool { let Some(compiled) = &self.compiled else { return false }; let program = &compiled.program; @@ -473,9 +480,9 @@ impl Shown { program.columns.iter().enumerate().any(|(i, c)| !c.sampled && columns & eased & (1 << i.min(63)) != 0) } - /// Rows rewritten in place in row blocks `blocks`, every other row where it was: those blocks - /// evaluate every channel, constants included. A fitted domain reads every row, so with one every - /// row does. + /// Rows in row blocks `blocks` were rewritten in place, and every other row is unchanged. Those + /// blocks evaluate every channel, constants included. A fitted domain reads every row, so with + /// one, every row evaluates. pub(crate) fn rows_rewritten(&mut self, blocks: &[u32]) { let Some(compiled) = &self.compiled else { return }; if !compiled.program.domains.is_empty() { @@ -484,9 +491,9 @@ impl Shown { self.slots_bound(blocks); } - /// Slots `blocks` of held rows given pages of rows held before or never: those blocks evaluate - /// every channel, constants included, and find their ends. Their rows' data is as it was, so - /// domains fitted to it hold. + /// Slots `blocks` received pages of other rows. Those blocks evaluate every channel, constants + /// included, and resolve their endpoints. The data itself is unchanged, so fitted domains stay + /// valid. pub(crate) fn slots_bound(&mut self, blocks: &[u32]) { let Some(compiled) = &self.compiled else { return }; let n = compiled.program.channels.len(); @@ -497,7 +504,7 @@ impl Shown { } } - /// Every row evaluates every channel and finds its ends. + /// Marks every channel dirty over every row, and every row's endpoints unresolved. pub(crate) fn all_dirty(&mut self) { if let Some(compiled) = &self.compiled { let n = compiled.program.channels.len(); @@ -510,8 +517,8 @@ impl Shown { } } - /// Compiles the mark against `all` in `context`; it stays uncompiled while its data or table is - /// not set. + /// Compiles the mark against the datasets `all` in `context`. It stays uncompiled while its + /// dataset or table is missing. pub(crate) fn compile(&mut self, all: &[Snapshot], context: Context<'_>, gpu: &mut Inner) -> Result<()> { self.tables.release(gpu); self.compiled = None; @@ -524,7 +531,8 @@ impl Shown { Ok(()) } - /// Whether `data` changed a schema the mark compiled against, or brought data it waited for. + /// Whether `data` changes a schema the mark compiled against, or is the dataset an uncompiled + /// mark waits for. pub(crate) fn stale(&self, data: &Snapshot) -> bool { match &self.compiled { None => *data.name() == self.mark.style().data, @@ -534,8 +542,8 @@ impl Shown { } } - /// Writes its ports' words: each one's code, shape, and reference column, and where the atlas - /// holds its label, set in `font` by the host the first time. + /// Writes its port words: each port's code, shape, reference column, and the atlas corners of + /// its label. The host sets each label in `font` the first time. pub(crate) fn write_ports(&mut self, font: &Arc, gpu: &mut Inner) { self.ports.clear(); let Some(compiled) = &self.compiled else { return }; @@ -552,8 +560,8 @@ impl Shown { } } - /// Words the mark's program region needs: descriptors and tables, ranges, code, constants, ends, - /// its markers' palette, its ports, and how each word of its rows eases. + /// Words the mark's program region needs: column descriptors and tables, ranges, code, + /// constants, end tables, the marker palette, ports, runs, page tables, and blends. pub(crate) fn packed_len(&self) -> u32 { let Some(compiled) = &self.compiled else { return 0 }; (self.tables.len() @@ -569,9 +577,9 @@ impl Shown { + self.extras() } - /// How each word of a row eases while the mark eases toward what its channels evaluate: a number - /// or a color its channels store mixes, a position's high words move by their offset, and what - /// else they store shows as evaluated; a word no channel stores keeps what it holds. + /// How each word of a row blends while the mark eases toward what its channels evaluate. Stored + /// numbers and colors interpolate, a position's high words move by their offset, and other + /// stored words snap to the new value. A word no channel stores keeps its value. fn blends(&self) -> Vec { let mut blends = vec![wgsl::BLEND_KEEP; (self.stride() + self.extras()) as usize]; let mut set = |word: u32, blend: u32| { @@ -594,7 +602,7 @@ impl Shown { _ => {} } } - // What evaluation writes of every vertex row: its flags' units, and its size's range. + // Evaluation writes these for every vertex row: its flags' units and its size range. if self.kind() == MarkKind::Vertices { set(wgsl::VERTEX_FLAGS, wgsl::BLEND_SNAP); set(wgsl::VERTEX_RANGE, wgsl::BLEND_SNAP); @@ -602,8 +610,8 @@ impl Shown { blends } - /// Appends the mark's words, which start at word `base` of the program buffer, and records where - /// each part lands. + /// Appends the mark's words, which start at program-buffer word `base`, and records each part's + /// offset. pub(crate) fn pack(&mut self, base: u32, out: &mut Vec) { let Some(compiled) = &self.compiled else { return }; let start = out.len(); @@ -635,7 +643,7 @@ impl Shown { } } -/// The pages of a mark's rows that evaluate next: every page, or those listed, the pages that +/// The row blocks (pages of held rows) to process next: all of them, or a list of those that /// changed. #[derive(Clone, PartialEq, Eq, Debug)] pub(crate) enum Blocks { @@ -659,7 +667,7 @@ impl Blocks { } } - /// The blocks of a mark of `rows` rows to evaluate, in order, each once. + /// The blocks within a mark's `rows` held rows, ascending and without repeats. pub(crate) fn of(&self, rows: u32) -> Vec { let count = rows.div_ceil(wgsl::PAGE_ROWS); match self { diff --git a/crates/latkit/src/view/mod.rs b/crates/latkit/src/view/mod.rs index fe45c8fb..9e62d70c 100644 --- a/crates/latkit/src/view/mod.rs +++ b/crates/latkit/src/view/mod.rs @@ -1,4 +1,4 @@ -//! Views: a scene drawn from the sources it is given into any wgpu frame, answering input. +//! The view: draws a scene from its sources into any wgpu frame, and handles input. mod axes; mod camera; @@ -62,28 +62,29 @@ use crate::expr::compile::Deps; use crate::scene::mark::Context; use crate::{Error, Gpu, Name, Patch, Pending, Result, Row, Rows, Scene, Snapshot, Source, Space}; -/// A frame to draw: where, a texture view of a format and size in physical pixels at a ratio of -/// physical pixels to CSS pixels, and when. +/// A frame to draw: the target texture view, its format and size, the pixel ratio, and the time. #[derive(Clone, Copy, Debug)] pub struct Frame<'a> { /// The texture view to draw into. pub target: &'a wgpu::TextureView, - /// Its format; sRGB formats are handled. + /// The target's format. Both sRGB and non-sRGB formats work. pub format: wgpu::TextureFormat, - /// Its width and height in physical pixels. + /// The target's width and height in physical pixels. pub size: [u32; 2], /// Physical pixels per CSS pixel. pub pixel_ratio: f32, - /// Seconds on the clock of whatever draws the frames: an animation frame's timestamp, a video - /// frame's time, a test's step. The view's motion moves by the steps between them, a quarter of - /// a second at most, and never back. + /// Seconds on the caller's clock: an animation frame's timestamp, a video frame's time, or a + /// test's step. The view's motion advances by the change since the previous frame, clamped to + /// between 0 and 0.25 seconds, so it never jumps or runs backward. pub time: f64, } -/// A scene drawn from the sources it is given, each a dataset by name: the application owns the data -/// and the scene; the view owns camera, coordinate, selection, and what the frame needs. It has no -/// loop: [`render`](View::render) records a frame into an encoder the application submits, and -/// [`input`](View::input) takes the platform's pointer and keys. +/// Draws a scene from its sources, which are named datasets, and handles input. +/// +/// The application owns the data and the scene. The view owns the camera, the axis coordinate, the +/// selection, and the GPU state its frames need. It has no loop of its own: +/// [`render`](View::render) records a frame into an encoder the application submits, and +/// [`input`](View::input) takes the platform's pointer and key events. /// /// ```no_run /// # async fn example() -> latkit::Result<()> { @@ -94,42 +95,45 @@ pub struct Frame<'a> { /// let mut view = View::new(&gpu, Scene::new(Plane::default()).draw(Vertices::of("graph", "Node").position("x", "y")))?; /// view.set_sources([&graph])?; /// view.fit(latkit::Fit::All); -/// // Drawn from the next frame on: a view draws the latest version of the data it is given. +/// // The next frame draws this change: a view draws the latest version of each Data it is given. /// graph.apply(Table::new("Node", [1_u64]).field("y", vec![2.0]))?; /// # Ok(()) } /// ``` pub struct View { gpu: Gpu, - /// Which of its GPU's views it is: what the GPU's ledger grants its share to. + /// This view's id among its GPU's views. The GPU's ledger grants budget shares by it. id: u64, scene: Scene, - /// Bumps whenever the scene changes. + /// Incremented whenever the scene changes. scene_version: u64, - /// What the application gave the view to draw, each a dataset by name. + /// The sources the application gave, each a named dataset. sources: Vec, - /// The version of each dataset the view draws, by name: a snapshot given, the latest version of - /// a dataset given to change in place as the view last took it, or what it holds of tiles. + /// The version of each dataset the view draws, sorted by name: a given snapshot, the latest + /// version of a `Data` as of the last refresh, or the tiles loaded so far. snapshots: Vec, - /// What the camera chose of each table drawn in part, this frame, and what draws of each. + /// What the camera chose to draw of each paged table this frame. choices: crate::derive::tiled::Choices, + /// Each paged table's drawing, by dataset and table. drawings: crate::data::Keyed<(Name, Name), drawing::Drawn>, - /// What replaces what the view draws of each dataset, its tables rebuilt, waiting for their pages. + /// Per dataset, the marks' rebuilt tables, waiting for their pages before they replace the + /// current ones. staged: crate::data::Keyed, - /// Whether the pages the view draws found room this frame, and how the next frame makes it where - /// some found none. + /// Whether this frame's pages found room in the arena, and if not, how the next frame finds it. room: frame::Room, - /// Whether the view told that what it draws does not fit the budget: until a version of its data - /// publishes, finding no room, and a fit with nothing to measure, only wait. + /// Set once the view has reported that what it draws exceeds the GPU budget. Until a new data + /// version publishes, running out of room and a due fit (with nothing drawn to measure) no + /// longer request frames. limited: bool, - /// What the view derives from the data and the scene: tables beside the tables it draws, row for - /// row, which marks read like data. + /// Tables the view derives from the data and the scene, row for row beside the tables it draws. + /// Marks read them like data. derived: Derivations, marks: Vec, camera: Camera, rig: Rig, /// CSS pixels kept clear at the top, right, bottom, and left. insets: [f32; 4], - /// What a plot's axes take around its area, as measured for a scene and pixel ratio, by their stamp. + /// The room a plot's axes take around its area, with the stamp of the scene and pixel ratio it + /// was measured for. gutters: (u64, axes::Gutters), fitting: Fitting, /// The value range a plot without one shows, fitted to its traces. @@ -138,7 +142,8 @@ pub struct View { clock: Clock, effect_time: f64, events: Events, - /// What landing work wakes, and the waker it is: readbacks, jobs, and fetches hold it. + /// The signal that arriving work wakes, and a waker for it, which readbacks, jobs, and fetches + /// hold. signal: Arc, waker: Waker, resources: Option, @@ -146,28 +151,28 @@ pub struct View { regions: Regions, /// Something visible changed since the last frame. changed: bool, - /// Whether the application was told the device was lost. + /// Whether the device loss has been reported. told_lost: bool, - /// The work under way the last frame stood in for. + /// The pending work the last frame showed placeholders for. waiting: Waiting, - /// What the scene asked the GPU to compile, by the targets its frames draw into. + /// The pipelines requested for each target the frames draw into. asked: Vec, - /// While what its frames draw with compiles, how many compiles had landed when it last looked: - /// it draws nothing new until they all have, and sleeps until the next lands. + /// While the draw pipelines compile, how many compiles had finished at the last check. The view + /// draws nothing new until they all finish, and sleeps until the next one finishes. compiling: Option, - /// Likewise while what lights rows, probes, and answers queries compiles: its frames draw without - /// lit rows, and probes and queries wait. + /// The same, while the pipelines for lit rows, probes, and queries compile. Meanwhile frames + /// draw without lit rows, and probes and queries wait. answering: Option, - /// Likewise while what its text draws with compiles: its frames draw without labels, glyphs, and - /// the overlay. + /// The same, while the text pipelines compile. Meanwhile frames draw without labels, glyphs, + /// and the overlay. texting: Option, - /// Who asked for the arranging under way. + /// The callers waiting on the arrangement under way. arranging: Vec>>, - /// Whether the platform asks for reduced motion, which `Motion::System` follows. + /// Whether the platform requests reduced motion, which `Motion::System` follows. reduced_motion: bool, - /// The face normals, made from what they read. + /// The stamp of the inputs the face normals were last summed from. normals: Derived<()>, - /// Measures what frames cost the GPU, where it can. + /// Measures the GPU time of frames, where the device supports it. timer: Option, focus: Focus, probing: Probing, @@ -176,24 +181,24 @@ pub struct View { list_words: u32, labels: labels::Labels, gestures: Gestures, - /// The pointer over the view: where, with what keys, and with what; `None` once it left. + /// The pointer over the view: its point, keys held, and device; `None` once it has left. pointer: Option, - /// The size of the last frame, CSS pixels, and its pixel ratio. + /// The size of the last frame in CSS pixels, and its pixel ratio. size: [f64; 2], pixel_ratio: f32, - /// How each table draws within the view's share of the budget, and what that weighs. + /// How each table draws within the view's share of the budget, and the costs behind it. planning: plan::Planning, - /// Pixel columns a trace spans at most: as many as the plan holds. + /// The most pixel columns a trace spans, as the plan allows. trace_columns: u32, stats: Stats, } -/// Views made so far, which numbers each. +/// Views created so far; gives each view its id. static VIEWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); impl View { - /// A view drawing `scene` with `gpu`. Its marks draw once [`set_sources`](View::set_sources) gives - /// them their datasets. + /// Creates a view that draws `scene` with `gpu`. Its marks draw nothing until + /// [`set_sources`](View::set_sources) provides their datasets. Errors if the scene is invalid. pub fn new(gpu: &Gpu, scene: Scene) -> Result { scene.check()?; let camera = Camera::default_for(&scene.space); @@ -251,28 +256,31 @@ impl View { stats: Stats::default(), }; view.set_scene(scene)?; - // Views to come find what nearly every scene draws with compiled, once this one draws. + // Queue the pipelines nearly every scene uses, so later views find them compiled. They + // compile once this view's draw pipelines are ready. #[cfg(all(feature = "web", target_arch = "wasm32"))] gpu.warm(); - // Until it draws, a view waits on everything its first frame would. + // Until its first frame, the view reports all the work that frame would wait on. view.waiting = view.under_way(); Ok(view) } - /// The scene drawn. + /// The scene the view draws. pub fn scene(&self) -> &Scene { &self.scene } - /// Draws `scene` instead, checked against the latest version of each dataset given to change in - /// place. Marks that match one of the current scene by kind, data, table, and name keep their - /// state, and ease to a changed description; an invalid scene changes nothing. + /// Replaces the scene, compiling its marks against the latest version of each + /// [`Data`](crate::Data) source. A mark that matches a current mark by kind, data, table, and + /// name keeps its state, and eases to its new description. Errors if the scene is invalid or a + /// mark fails to compile; the current scene then stays. pub fn set_scene(&mut self, scene: Scene) -> Result<()> { scene.check()?; let gpu = self.gpu.clone(); let mut inner = gpu.0.lock(); self.refresh(&mut inner); - // A mark compiles again when its theme or space changed. + // Every mark recompiles if the theme, the kind of space, or the presence of a layout + // changed. let context_changed = scene.theme != self.scene.theme || std::mem::discriminant(&scene.space) != std::mem::discriminant(&self.scene.space) || matches!(scene.layout, crate::Layout::None) != matches!(self.scene.layout, crate::Layout::None); @@ -301,11 +309,12 @@ impl View { .iter_mut() .find(|s| s.as_ref().is_some_and(|s| s.mark.draws_like(mark))) .and_then(Option::take) - .expect("kept"); + .expect("the unchanged mark's previous state"); kept.mark = mark.clone(); return kept; }; - // A changed mark takes over its predecessor's rows, and eases from them. + // A changed mark takes over the rows of the old mark with its key, and eases from + // them. let before = old.iter_mut().find(|s| s.as_ref().is_some_and(|s| s.key == shown.key)); if let Some(mut before) = before.and_then(Option::take) { before.tables.release(&mut inner); @@ -320,7 +329,7 @@ impl View { for mut dropped in old.into_iter().flatten() { dropped.release(&mut inner); } - // Marks moved: what they staged is built again for where they are now. + // Mark indices may have changed, so staged tables are dropped and rebuilt. for (_, staged) in std::mem::take(&mut self.staged) { staged.release(&mut inner); } @@ -337,14 +346,13 @@ impl View { } self.scene = scene; self.scene_version += 1; - // What the new scene uses compiles from now, while its data syncs and its sheet arranges; what - // it shares landed already. + // Request the new scene's pipelines afresh. They compile while its data syncs and its sheet + // is arranged; those it shares with the old scene are compiled already. self.asked.clear(); self.compiling = None; self.answering = None; self.texting = None; - // Where the GPU's frames have their samples per pixel already, what the scene draws with - // starts compiling now; else its first frame asks. + // If the GPU's samples per pixel are known, start compiling now; else the first frame asks. if let Some(samples) = gpu.samples() { self.ask(&mut gpu.0.lock(), (crate::gpu::FORMAT, samples), true); } @@ -354,12 +362,14 @@ impl View { Ok(()) } - /// Draws the datasets `sources` give, each by its name, in place of those given before: a - /// [`Data`](crate::Data), its latest version from the frame after each patch applies; a - /// [`Snapshot`], as it is; [`Tiles`](crate::Tiles), the pieces the view asks for. Cheap: unchanged - /// pages are shared, and only changed fields evaluate again. A mark its dataset no longer fits draws - /// nothing, and the next frame tells why, unless a scene shown before then replaces it: sources - /// and the scene reading them can be given in either order. Two sources of one name are invalid. + /// Replaces the view's sources, each a named dataset. A [`Data`](crate::Data) draws its latest + /// version, from the frame after each patch applies. A [`Snapshot`] draws as given. + /// [`Tiles`](crate::Tiles) loads the pieces the view needs. + /// + /// This is cheap: unchanged pages are shared, and only changed fields evaluate again. A mark + /// whose dataset no longer fits it draws nothing, and the next frame reports why, unless a new + /// scene replaces that mark first. So the sources and the scene can be set in either order. + /// Errors if two sources share a name. pub fn set_sources>(&mut self, sources: impl IntoIterator) -> Result<()> { let sources: Vec = sources.into_iter().map(Into::into).collect(); for (i, source) in sources.iter().enumerate() { @@ -369,8 +379,8 @@ impl View { } let gpu = self.gpu.clone(); let mut inner = gpu.0.lock(); - // A dataset no source gives any longer goes, and so does one whose source is now tiles, or - // was: the view draws what the new source gives from scratch. + // Forget datasets no source provides any more, and those whose source switched to or from + // tiles: the view draws the new source from scratch. let tiled = |source: &Source| matches!(source, Source::Tiles(_)); let gone: Vec = self .sources @@ -399,7 +409,7 @@ impl View { } self.sources = sources; drop(inner); - // Marks that lost their tables lay the program out again. + // Marks that lost their tables need the program laid out again. if forgot { self.invalidate(); } else { @@ -408,25 +418,25 @@ impl View { Ok(()) } - /// What the application gave the view to draw. + /// The sources the view draws. pub fn sources(&self) -> &[Source] { &self.sources } - /// The version of dataset `name` the view draws: the latest version of a [`Data`](crate::Data) - /// it took, a snapshot given, or what it holds of [`Tiles`](crate::Tiles), the pieces its camera - /// asked for. + /// The version of dataset `name` the view draws: the latest version it took of a + /// [`Data`](crate::Data), a given snapshot, or the pieces of [`Tiles`](crate::Tiles) loaded for + /// its camera. `None` if the view has no such dataset. pub fn snapshot(&self, name: &str) -> Option<&Snapshot> { let i = self.snapshots.binary_search_by(|s| s.name().as_str().cmp(name)).ok()?; Some(&self.snapshots[i]) } - /// Brings every source up to this frame before anything reads it: the tiles the camera and the - /// plot window ask for, with what landed for them, and the latest version of every dataset given - /// to change in place, where it changed. A frame's marks compile against, ask pipelines for, - /// derive from, and draw one version of each, so a field a version adds never reads through what - /// was not asked for. A picture draws the versions the view held when it began, while the tiles it - /// waits on land. + /// Brings every source up to date before the frame reads it: the tiles the camera and the plot + /// window need, with the pieces that arrived for them, and the latest version of each changed + /// `Data`. A frame compiles marks, requests pipelines, derives, and draws from one version of + /// each dataset, so a newly added field is never read by a pipeline not requested for it. While + /// a picture holds the view, `Data` sources stay at the versions taken when it began, but tiles + /// still arrive. fn refresh(&mut self, inner: &mut crate::gpu::Inner) { self.update_tiles(inner); for data in self.derived.tiles.take_changed() { @@ -449,9 +459,9 @@ impl View { } } - /// Takes `data` with the GPU's state locked: marks whose schema it changed compile again; one that - /// fails keeps why until data fits it again, and the frame tells it. A frame draws the data, as it - /// is or in part; what takes it between frames asks for one. + /// Takes a new version of a dataset, with the GPU's state locked. Marks whose schema it changed + /// recompile. A mark that fails keeps its error until the data fits it again, and the next + /// frame reports the error. A caller outside a frame requests a frame to draw the data. fn take_data(&mut self, data: Snapshot, inner: &mut crate::gpu::Inner) { let at = match self.snapshots.binary_search_by(|d| d.name().cmp(data.name())) { Ok(i) => { @@ -474,13 +484,14 @@ impl View { shown.failure = failed.map(|error| (error, false)); } } - // Rows may have moved under the focus and the keys. + // Rows may have moved under the focus and the keyboard-stepped hover. self.focus.dirty = true; self.gestures.keys.stale(); self.fitting.plot_due = true; } - /// Stops drawing dataset `name`: what its marks hold of it goes, until a source gives it again. + /// Stops drawing dataset `name`, releasing what its marks hold of it, until a source provides + /// it again. fn forget(&mut self, name: &str, inner: &mut crate::gpu::Inner) { self.derived.tiles.remove(name, &mut inner.tiles); self.snapshots.retain(|d| d.name() != name); @@ -502,12 +513,12 @@ impl View { } } - /// The axis coordinate shown: every sampled field reads its last frame at or before it. + /// The axis coordinate shown. Every sampled field shows its last frame at or before it. pub fn at(&self) -> f64 { self.at } - /// Shows axis coordinate `at`; only options that read sampled fields evaluate again. + /// Shows axis coordinate `at`. Only options that read sampled fields evaluate again. pub fn set_at(&mut self, at: f64) { if at.to_bits() != self.at.to_bits() { self.at = at; @@ -515,12 +526,13 @@ impl View { } } - /// Where the view looks. + /// The view's camera. pub fn camera(&self) -> &Camera { &self.camera } - /// Looks through `camera` at once. Like every setter, it tells nothing: the application knows. + /// Moves to `camera` at once, stopping any flight or coasting. Like every setter, it emits no + /// event. Ignored unless `camera` [fits](Camera::fits) the scene's space. pub fn set_camera(&mut self, camera: Camera) { if camera.fits(&self.scene.space) { self.rig.hold(); @@ -528,8 +540,8 @@ impl View { } } - /// Tells the view whether the platform asks for reduced motion, as a browser's - /// `prefers-reduced-motion` does: `Motion::System` follows it. A canvas tells it itself. + /// Sets whether the platform requests reduced motion, like a browser's + /// `prefers-reduced-motion`. `Motion::System` follows it. A canvas sets it automatically. pub fn set_reduced_motion(&mut self, reduced: bool) { if reduced != self.reduced_motion { self.reduced_motion = reduced; @@ -537,8 +549,8 @@ impl View { } } - /// Whether the view moves on its own: eases, flights, coasting, orbits, label fades, comets, and - /// what reads `elapsed()`. + /// Whether motion is enabled, so the view animates on its own: eases, flights, coasting, + /// orbits, label fades, comets, and expressions that read `elapsed()`. pub(super) fn moves(&self) -> bool { self.moves_with(&self.scene.controls) } @@ -551,12 +563,13 @@ impl View { } } - /// What changes ease over: the scene's ease, or nothing where the view does not move on its own. + /// How long changes ease: the scene's ease, or zero when motion is disabled. pub(super) fn ease(&self) -> Duration { if self.moves() { self.scene.ease } else { Duration::ZERO } } - /// Eases to `camera` over twice the scene's ease, on the view's clock. + /// Eases to `camera` over twice the scene's ease, on the view's clock. Ignored unless `camera` + /// [fits](Camera::fits) the scene's space. pub fn fly(&mut self, camera: Camera) { if camera.fits(&self.scene.space) { let seconds = (self.ease().max(Duration::from_millis(1)) * 2).as_secs_f64(); @@ -565,25 +578,29 @@ impl View { } } - /// Where `position` (x, y, and elevation) lands on screen, CSS pixels from the top left; `None` - /// behind the camera. Overlays anchor to what the view draws with it. + /// The screen point of `position` (x, y, and elevation), in CSS pixels from the top left; + /// `None` behind the camera. Use it to anchor overlays to what the view draws. pub fn screen(&self, position: [f64; 3]) -> Option<[f32; 2]> { let view = self.projected(); view.screen(view.world(position)).map(|s| [s[0] as f32, s[1] as f32]) } - /// The position on the ground under screen point `point`: on a plane at elevation 0, on the earth - /// the map or the globe, on a plot the values there; `None` where the pointer looks past it. + /// The ground position under screen point `point` (CSS pixels from the top left): on a plane, + /// the point at elevation 0; on the earth, the point on the map or globe; on a plot, the values + /// there. `None` where the point misses the ground. pub fn position(&self, point: [f32; 2]) -> Option<[f64; 3]> { let view = self.projected(); view.ground(point.map(f64::from)).map(|w| view.position(w)) } - /// Lays every vertex row without a position out again by the scene's layout: a diagram's blocks, - /// or the points of a plane or the earth. Places a layout gave otherwise hold, so rows added later - /// are placed around the rest. Once the layout lands, the answer is a patch to each dataset - /// writing each row laid out into its mark's position fields (or its point `geometry`); applied, - /// the places are the data's own. A mark whose position is not fields is left out. + /// Lays out every vertex row without a position again, by the scene's layout: a diagram's + /// blocks, or the points of a plane or the earth. Without this call, laid-out positions + /// persist, and rows added later are placed around them. + /// + /// Once the layout finishes, resolves to one patch per dataset that writes each laid-out row's + /// position into its mark's position fields (or its point `geometry`). Applying the patches + /// makes the positions part of the data. Marks whose position is not a pair of fields are + /// skipped. pub fn arrange(&mut self) -> Pending> { let (pending, resolver) = Pending::new(); self.arranging.push(resolver); @@ -592,7 +609,7 @@ impl View { pending } - /// Answers the arranging asked for, once its layout landed. + /// Resolves the pending `arrange` calls once their layout has finished. fn answer_arranging(&mut self) { let Some(placed) = self.derived.take_arranged() else { return }; let patches = arranged(&self.scene, placed); @@ -613,13 +630,13 @@ impl View { self.wake(); } - /// CSS pixels kept clear at the top, right, bottom, and left: the camera centers and fits in what - /// remains, as when panels cover part of the view. + /// CSS pixels kept clear at the top, right, bottom, and left, for panels that cover part of the + /// view. The camera centers and fits within the rest. pub fn insets(&self) -> [f32; 4] { self.insets } - /// Keeps `insets` clear. + /// Sets the [`insets`](View::insets). pub fn set_insets(&mut self, insets: [f32; 4]) { if insets != self.insets { self.insets = insets; @@ -627,25 +644,27 @@ impl View { } } - /// Degrees of bearing a map or plane turns each second about its center. + /// The orbit speed: degrees of bearing per second that a map or plane turns about its center. pub fn orbit(&self) -> f64 { self.rig.orbit } - /// Turns the camera `degrees_per_second` about its center; zero stops it. A diagram never turns. + /// Turns the camera about its center at `degrees_per_second`; zero stops it. A diagram never + /// turns. pub fn set_orbit(&mut self, degrees_per_second: f64) { let turns = degrees_per_second.is_finite() && !matches!(self.scene.space, Space::Diagram(_)); self.rig.orbit = if turns { degrees_per_second } else { 0.0 }; self.wake(); } - /// The rows selected, a set per table in the order each was first selected. + /// The selected rows, one set per table, in the order each table was first selected. pub fn selection(&self) -> Vec { Rows::group(self.focus.selection.iter().cloned()) } - /// Selects `rows`, and no others, telling nothing. Rows the data drops, or no pickable mark shows, - /// leave the selection with a [`Selection`](Event::Selection) event without a trigger. + /// Selects exactly `rows`, without emitting an event. Rows that the data drops, or that no + /// pickable mark shows, later leave the selection with a [`Selection`](Event::Selection) event + /// that has no trigger. pub fn set_selection(&mut self, rows: impl IntoIterator) { let rows = Rows::flatten(rows); if rows != self.focus.selection { @@ -655,12 +674,12 @@ impl View { } } - /// What the user points at: the row under the pointer, or the row the keys stepped to. + /// The hovered row: the row under the pointer, or the row the keyboard stepped to. pub fn hovered(&self) -> Option<&Hit> { self.focus.hovered.as_ref() } - /// Every mark that draws nothing because its data no longer fits it, and why. + /// An error for every mark that draws nothing because its data no longer fits it. pub fn failures(&self) -> impl Iterator + '_ { self.marks .iter() @@ -668,12 +687,12 @@ impl View { .filter_map(|(i, shown)| Some(shown.mark.within(i, shown.failure.as_ref()?.0.clone()))) } - /// The pointer the view asks for over what it shows. + /// The cursor the view requests for what is under the pointer. pub fn cursor(&self) -> Cursor { self.gestures.cursor(self.probing.hovered(), &self.scene) } - /// What the view did. + /// Counters of the view's work and its last frame. pub fn stats(&self) -> Stats { let rows = |m: &Shown| { let style = m.mark.style(); @@ -689,42 +708,43 @@ impl View { } } - /// Whether the last frame drew everything as the data, scene, and camera say, with nothing - /// standing in for work under way: no pipeline it draws with was compiling, no changed page - /// waited to upload, no tile drew its ancestor, no table its first sample, no row stood where it - /// was until a layout landed, no wire drew its elbows, no area its earlier triangles, no label its - /// estimate, and no fit was due. A shot or a video waits for it; [`waiting`](View::waiting) says - /// what it waits on. + /// Whether the last frame drew everything the data, scene, and camera describe, with no + /// placeholder for pending work. A frame is incomplete while a pipeline it draws with compiles, + /// a changed page waits to upload, a tile shows its ancestor, a large table shows an even + /// sample of its rows, a row keeps its old position until a layout arrives, a wire shows an + /// elbow until its route arrives, an area shows its previous triangles, a label shows its + /// estimated size, or a fit is due. Shots and videos wait for it, and + /// [`waiting`](View::waiting) lists what is pending. pub fn complete(&self) -> bool { self.waiting.is_empty() } - /// The work under way the last frame stood in for, by kind; nothing once it is complete. - /// [`Waiting`](Event::Waiting) events tell its changes. + /// The kinds of pending work the last frame showed placeholders for; empty once the view is + /// [complete](View::complete). [`Waiting`](Event::Waiting) events report its changes. pub fn waiting(&self) -> Waiting { self.waiting } - /// What happened since the last call, in order: see [`Event`]. An intent whose detail a pick has - /// yet to bring waits for it, and the intents after it wait too. + /// The events since the last call, in order: see [`Event`]. An intent whose details depend on a + /// pick is held until the pick finishes, and later intents wait behind it. pub fn events(&mut self) -> impl Iterator + use<> { self.events.drain() } - /// Woken when the view needs a frame without being asked: work it waited on lands, data or the - /// camera changes. + /// Sets the waker called when the view needs a frame on its own: when pending work arrives, or + /// the data or the camera changes. pub fn set_waker(&mut self, waker: Waker) { self.signal.set_waker(waker); } - /// What wakes the view: a shot drawing it waits on it. + /// The view's wake signal. A shot drawing the view waits on it. pub(crate) fn signal(&self) -> &Arc { &self.signal } - /// Whether a [`Shot`](crate::Shot) or a [`Video`](crate::Video) draws the view: nothing else - /// should draw it meanwhile, as a canvas or a window holds its last frame, since a view draws at - /// one size at a time. + /// Whether a [`Shot`](crate::Shot) or a [`Video`](crate::Video) is drawing the view. Nothing + /// else should draw it meanwhile, since a view draws at one size at a time; a canvas or window + /// keeps showing its last frame. pub fn lent(&self) -> bool { self.signal.lent() } @@ -734,27 +754,28 @@ impl View { &self.gpu } - /// The error a shot or a video fails on: the first one still untold, which stays for the view's - /// owner. + /// The error a shot or video fails with: the first error not yet taken from the events. It + /// stays queued for the view's owner. pub(crate) fn shot_error(&mut self) -> Option { self.tell_gpu_errors(); self.events.error().cloned() } - /// Whether the view can draw frames into `format` at `pixel_ratio` now: every pipeline its scene - /// draws with compiled. The first call for a target asks for every pipeline the scene uses, all at - /// once, what it draws with first; in a browser they compile side by side off the GPU's thread, - /// and each that lands wakes the view. Until what it draws with has, it draws nothing new, so - /// whatever shows its frames keeps the last, as a canvas does; until the rest has, it draws - /// without lit rows, and probes and queries wait. Natively it can always draw. - /// [`render`](View::render) asks for itself, drawing nothing until then. + /// Whether the view can draw frames into `format` at `pixel_ratio` now, with every pipeline its + /// scene draws with compiled. + /// + /// The first call for a target requests every pipeline the scene uses, draw pipelines first. In + /// a browser they compile in parallel off the GPU thread, and each one that finishes wakes the + /// view. Until the draw pipelines are ready, the view draws nothing new, so whatever shows its + /// frames keeps the last one, as a canvas does. Until the rest are ready, frames omit text and + /// lit rows, and probes and queries wait. Natively the view can always draw. + /// [`render`](View::render) checks this itself and records nothing until it is true. pub fn ready(&mut self, format: wgpu::TextureFormat, pixel_ratio: f32) -> bool { self.ready_for(format, pixel_ratio, true) } - /// Whether frames into `format` at `pixel_ratio`, taking `input` or not, can draw now: see - /// [`ready`](View::ready). It takes the latest version of each dataset given to change in place - /// first, so a field it adds asks for what its marks then read with before a frame reads it. + /// Like [`ready`](View::ready), for frames that take `input` or not. It refreshes the sources + /// first, so the pipelines a newly added field needs are requested before a frame reads it. pub(crate) fn ready_for(&mut self, format: wgpu::TextureFormat, pixel_ratio: f32, input: bool) -> bool { let gpu = self.gpu.clone(); let samples = gpu.samples_for(pixel_ratio); @@ -776,33 +797,34 @@ impl View { draws } - /// Whether probes and queries can run: what they and the lit rows' draws use compiled. + /// Whether probes and queries can run: the probe, query, and lit-row pipelines are compiled. fn answers(&self) -> bool { self.answering.is_none() } - /// Whether frames draw text: what labels, glyphs, and the overlay use compiled. + /// Whether frames draw text: the label, glyph, and overlay pipelines are compiled. fn texts(&self) -> bool { self.texting.is_none() } - /// Whether what its frames draw with is still compiling: it draws nothing new until all of it - /// landed. + /// Whether the draw pipelines are still compiling. The view draws nothing new until they have + /// all finished. #[cfg(all(feature = "web", target_arch = "wasm32"))] pub(crate) fn compiling(&self) -> bool { self.compiling.is_some() } - /// Whether a frame would differ from the last one: something changed, moves, or streams in, or - /// work the view waited on landed. While it only waits, it needs none: what it waits on wakes it. + /// Whether a new frame would differ from the last one: something changed, animates, or streams + /// in, or pending work arrived. While the view only waits, it needs no frame; the work it waits + /// on wakes it. pub fn needs_render(&self) -> bool { let landed = || self.gpu.0.lock().pipelines.landed(); - // While what it draws with compiles, it draws nothing new: each compile landing wakes it. + // While draw pipelines compile, the view draws nothing new; each finished compile wakes it. if let Some(seen) = self.compiling { return landed() != seen; } - // What moves on its own, only while the view moves on its own: under reduced motion an orbit, - // comets, and what reads `elapsed()` hold still. + // Self-driven motion counts only while motion is enabled: under reduced motion, orbits, + // comets, and `elapsed()` hold still. let moves = self.moves(); self.changed || self.signal.landed() @@ -810,13 +832,14 @@ impl View { || self.rig.easing() || (self.rig.orbit != 0.0 && moves) || (self.fitting.due() && !self.limited) - // Probes and queries wait for what answers them to compile, which wakes the view as it lands. + // Probes and queries wait for their pipelines, which wake the view as they compile. || (self.answers() && (self.probing.due() || self.queries.due())) || self.answering.is_some_and(|seen| landed() != seen) || self.texting.is_some_and(|seen| landed() != seen) || self.fading() || self.labels.fading(self.clock.now()) - // A mark evaluates once its columns synced: until then, the pages they wait for wake it. + // A mark evaluates once its columns are synced. Until then, the pages they wait for + // wake the view. || self.marks.iter().any(|m| (m.drawable() && (m.dirty != 0 || m.reduce)) || m.begin.is_some() || m.transition.is_some()) || (moves && self.marks.iter().any(|m| m.compiled.as_ref().is_some_and(|c| c.program.reads().intersects(Deps::TIME)))) || (moves && self.flowing()) @@ -824,22 +847,24 @@ impl View { || self.room != frame::Room::Found } - /// Whether the view wants a frame: one that would differ ([`needs_render`](View::needs_render)), - /// or one noticing a touch held long enough for a menu. A loop drawing on demand draws while this - /// holds, as a canvas does; a video holds the frames that only notice. + /// Whether the view wants a frame: one that would differ + /// ([`needs_render`](View::needs_render)), or one that only checks whether a touch has been + /// held long enough to open a menu. A loop that draws on demand draws while this is true, as a + /// canvas does. A video repeats its last frame instead of drawing frames that only check. pub fn wants_frame(&self) -> bool { self.needs_render() || (self.compiling.is_none() && self.holding()) } - /// Whether comets travel along a mark's rows: one that flows draws, and the theme shows them. + /// Whether comets travel along some mark's rows: a flowing mark is drawable, and the theme's + /// flow color is visible. fn flowing(&self) -> bool { self.scene.theme.flow.color.a > 0.0 && self.marks.iter().any(|m| m.drawable() && m.mark.flows()) } - /// Whether the next frame, taking `input` or not, does work this one left: pages to upload past - /// the frame's budget, pages that found no room this frame, a fit, probe, or query due, what tiles - /// brought for it to take, or a transition to begin. Nothing else would wake the view for it: - /// jobs, texts, fetches, and readbacks do as they land. + /// Whether the next frame, taking `input` or not, has work this one left: pages to upload past + /// the frame's budget, pages that found no room this frame, a due fit, probe, or query, tile + /// pieces to take, or a transition to begin. Nothing else would wake the view for these. Jobs, + /// texts, fetches, and readbacks wake it themselves when they arrive. fn due(&self, input: bool) -> bool { self.gpu.0.lock().arena.uploading() || self.room != frame::Room::Found @@ -854,9 +879,10 @@ impl View { self.signal.ask(); } - /// The work under way a frame drawn now stands in for: see [`complete`](View::complete). A mark - /// waits for a dataset it names until a source gives it; one that failed, or that draws tiles, - /// which may never hold its table, stands in for nothing. + /// The pending work a frame drawn now would show placeholders for: see + /// [`complete`](View::complete). A mark waits for its dataset until a source provides it. A + /// mark that failed, or whose dataset comes from tiles (which may never hold its table), waits + /// for nothing. fn under_way(&self) -> Waiting { let marks = || self.marks.iter().filter(|m| m.compiled.is_some()); let given = |m: &Shown| { @@ -881,9 +907,9 @@ impl View { } } - /// Whether something eases toward where it goes: a transition, a flight, coasting, or a label - /// fade. An orbit, a comet, and anything else that moves for as long as it is shown never settles, - /// and is not counted. + /// Whether something is easing to a rest: a transition, a flight, coasting, a tile fade, or a + /// label fade. Orbits, comets, and other motion that lasts as long as it is shown never settle, + /// and do not count. pub(crate) fn easing(&self) -> bool { self.rig.easing() || self.fading() @@ -891,7 +917,7 @@ impl View { || self.marks.iter().any(|m| m.begin.is_some() || m.transition.is_some()) } - /// Whether tiles of a table drawn in part fade in or out. + /// Whether tiles of a paged table are fading in or out. fn fading(&self) -> bool { self.drawings.values().any(|d| d.current.as_ref().is_some_and(|c| c.fades)) } @@ -902,7 +928,8 @@ impl View { self.wake(); } - /// The part of the view the camera frames: inside the insets, and on a plot inside its axes. + /// The part of the view the camera frames, in CSS pixels: inside the insets, and on a plot + /// inside its axes. At least one pixel each way. fn area(&self) -> Area { let [w, h] = self.size; let [top, right, bottom, left] = self.insets.map(f64::from); @@ -933,7 +960,8 @@ impl View { self.projected_with(&self.drawn_camera()) } - /// What `camera` would show, a plot's missing value range filled as the frame fills it. + /// The projection through `camera`, with a plot's missing value range filled in as the frame + /// fills it. fn projected_with(&self, camera: &Camera) -> Projected { let camera = match *camera { Camera::Plot { x, y: None } => Camera::Plot { x, y: Some(self.plot_y.unwrap_or([0.0, 1.0])) }, @@ -975,10 +1003,10 @@ impl std::fmt::Debug for View { } } -/// The patches writing rows laid out into the data, by dataset: each row's place into the position -/// fields of the vertex mark drawing its table, or its point geometry. +/// One patch per dataset that writes each laid-out row's position into the position fields of the +/// vertex mark drawing its table, or into its point geometry. fn arranged(scene: &Scene, placed: Vec<(Row, [f64; 2])>) -> Vec { - /// A table's rows laid out: its dataset and name, their ids, and their places. + /// A table's laid-out rows: its dataset and table names, the row ids, and their positions. type Laid = ((Name, Name), Vec, Vec<[f64; 2]>); let mut tables: Vec = Vec::new(); for (row, place) in placed { @@ -1016,8 +1044,8 @@ fn arranged(scene: &Scene, placed: Vec<(Row, [f64; 2])>) -> Vec { patches } -/// Why `shown`, whose dataset is given, draws nothing: the table it names is absent. A dataset tiles -/// give fills its tables as they land, so a table it lacks is not missing. +/// The error for a mark `shown` whose dataset is present but lacks the table it names. A dataset +/// from tiles gains tables as pieces arrive, so a table it lacks is not an error. fn absent(shown: &Shown, snapshots: &[Snapshot], tiles: &crate::tiles::TileSets) -> Option { let style = shown.mark.style(); let data = snapshots.iter().find(|d| *d.name() == style.data)?; diff --git a/crates/latkit/src/view/passes.rs b/crates/latkit/src/view/passes.rs index b8ed718d..185b6b1c 100644 --- a/crates/latkit/src/view/passes.rs +++ b/crates/latkit/src/view/passes.rs @@ -1,8 +1,11 @@ -//! A frame's passes: compute evaluates dirty channels over the row blocks that changed, eases what -//! marks show toward what changed, reduces fitted domains, numbers segments, sums -//! face normals when their corners moved, and measures extents when asked; render draws the -//! backdrop, every mark's primitive and ports in scene order with depth, the labels, and the -//! overlay; a probe, when one waits, reads what the frame drew through the same geometry. +//! A frame's GPU passes. +//! +//! The compute pass expands paged marks' drawn rows from their runs, evaluates dirty channels over +//! the changed row blocks (reducing fitted domains first), resolves endpoints, eases marks in +//! transition, moves comets, numbers segments, sums face normals when their corners moved, and +//! measures extents when asked. The render pass draws the backdrop, then every mark's primitive and +//! ports in scene order with depth, then the labels and the overlay. A pending probe then reads +//! what the frame drew, through the same geometry. use super::resources::MARK_STRIDE; use super::sync::{Source, table_of}; @@ -21,10 +24,11 @@ use crate::gpu::work::Count; use crate::scene::mark::{MarkKind, Primitive}; const WORKGROUP: u32 = 256; -/// Workgroups one dimension of a dispatch holds on every device. +/// The most workgroups per dispatch dimension that every device supports. const MAX_GROUPS: u32 = 65_535; -/// The target pixels of `area`: a plot's lines draw only inside its axes. +/// The scissor rectangle `[x, y, width, height]`, in target pixels, covering `area`. A plot's lines +/// draw only inside its axes. fn scissor(area: super::camera::Area, frame: &Frame<'_>) -> [u32; 4] { let [w, h] = frame.size.map(f64::from); let r = f64::from(frame.pixel_ratio); @@ -35,8 +39,8 @@ fn scissor(area: super::camera::Area, frame: &Frame<'_>) -> [u32; 4] { [x0 as u32, y0 as u32, (x1 - x0) as u32, (y1 - y0) as u32] } -/// The main render pass: cleared to `background`, multisampled into `view` and resolved into -/// `resolve` when there is one, its depth kept when `probing` reads it. +/// Begins the main render pass: cleared to `background`, drawn into `view` and resolved into +/// `resolve` when multisampling, with depth stored only when `probing` reads it. fn main_pass<'e>( encoder: &'e mut wgpu::CommandEncoder, view: &wgpu::TextureView, @@ -71,8 +75,8 @@ fn main_pass<'e>( }) } -/// The draws a frame recorded, and the instances they draw: at most, for a draw its segment list -/// counts on the GPU. +/// The draws a frame recorded and the instances they draw. For a draw whose segment list counts its +/// instances on the GPU, the count is an upper bound. #[derive(Clone, Copy, Default, Debug)] pub(super) struct Drawn { pub(super) draws: u64, @@ -80,7 +84,7 @@ pub(super) struct Drawn { } impl Drawn { - /// Draws `instances` of `corners` corners each, counted. + /// Draws and counts `instances` instances of `corners` vertices each. pub(super) fn draw(&mut self, pass: &mut wgpu::RenderPass<'_>, corners: u32, instances: u32) { pass.draw(0..corners, 0..instances); self.count(instances); @@ -92,8 +96,8 @@ impl Drawn { } } -/// What every mark's draw in the main pass shares: the device and target, the bind groups, the -/// lists buffer, and a plot's axes, which its lines clip to. +/// State that every mark's draw in the main pass shares: the device and target, the bind groups, +/// the lists buffer, and the plot area that lines clip to. struct Marks<'a> { device: &'a wgpu::Device, format: wgpu::TextureFormat, @@ -106,7 +110,7 @@ struct Marks<'a> { } impl Marks<'_> { - /// Draws mark `i`, `shown`: every instance, or with `lit` the rows its focus lights, glowing. + /// Draws mark `i`: every instance, or with `lit`, only the rows its focus lights, glowing. fn draw( &self, pass: &mut wgpu::RenderPass<'_>, @@ -129,7 +133,7 @@ impl Marks<'_> { (Primitive::Triangles, false) if areas => (Shader::Areas, 3), (Primitive::Triangles, false) => (Shader::Triangles, 3), (Primitive::Triangles, true) if areas => (Shader::AreasFocus, 3), - // A face lights by its color alone. + // A face shows focus by its color alone, so it has no lit draw. (Primitive::Triangles, true) => return, }; let pipeline = if let Some(source) = &compiled.drawing.markers.custom { @@ -157,8 +161,8 @@ impl Marks<'_> { } } - /// Draws the ports of every row of mark `i`, `shown`, where its rows have them: after its rows, and - /// again after the rows its focus lights, which they sit on. + /// Draws the ports of every row of mark `i`, if its rows have ports. Called after its rows, and + /// again after its lit rows, since ports sit on top of them. fn ports( &self, pass: &mut wgpu::RenderPass<'_>, @@ -179,26 +183,27 @@ impl Marks<'_> { } } -/// What a frame's compute pass does. +/// The marks each step of a frame's compute pass covers. struct Work { evaluated: Vec, - /// Marks whose rows find their ends where those changed. + /// Marks whose rows resolve changed endpoints. resolved: Vec, - /// Marks whose ease pass writes what they show from their frozen and settled state. + /// Marks whose ease pass writes their shown state from their frozen and settled words. eased: Vec, segments: Vec, faces: Vec, measured: Vec, - /// Marks whose comets carry their travel over to rows a transition moved, and that travel. + /// Marks whose comets carry their travel over to rows a transition moved; `travelled`, marks + /// whose comets move this frame. carried: Vec, travelled: Vec, - /// Marks whose drawn rows' words are expanded from their runs again. + /// Marks whose drawn rows' words are rebuilt from their runs. expanded: Vec, } -/// Dispatches a workgroup for every `WORKGROUP` of `items`, if there are any; past one dimension's -/// workgroups, runs of `DISPATCH_ROWS` items along the second, as `item` in `lib/types.wesl` reads -/// them. +/// Dispatches one workgroup per `WORKGROUP` items, if there are any. Past one dimension's limit, it +/// dispatches rows of `DISPATCH_ROWS` items along the second dimension, as `item` in +/// `lib/types.wesl` expects. pub(super) fn dispatch(pass: &mut wgpu::ComputePass<'_>, items: u32) { let groups = items.div_ceil(WORKGROUP); if groups > MAX_GROUPS { @@ -208,8 +213,8 @@ pub(super) fn dispatch(pass: &mut wgpu::ComputePass<'_>, items: u32) { } } -/// The row blocks `shown` evaluates: those its channels changed over, and those whose state words -/// start over, which `FRESH` marks. +/// The row blocks `shown` evaluates: those its dirty channels cover, and those whose state words +/// start over, flagged with `FRESH`. fn listed(shown: &super::marks::Shown) -> Vec { let count = shown.live().div_ceil(wgsl::PAGE_ROWS); let mut fresh: Vec = shown.fresh.iter().copied().filter(|&b| b < count).collect(); @@ -227,7 +232,8 @@ fn listed(shown: &super::marks::Shown) -> Vec { blocks } -/// Dispatches over `blocks` listed row blocks of a mark of `rows` held rows: a block's rows wide. +/// Dispatches over `blocks` listed row blocks of a mark with `rows` held rows: x spans one block's +/// rows, and y the blocks. pub(super) fn dispatch_blocks(pass: &mut wgpu::ComputePass<'_>, rows: u32, blocks: u32) { let wide = rows.min(wgsl::PAGE_ROWS).div_ceil(WORKGROUP); if wide > 0 && blocks > 0 { @@ -240,8 +246,9 @@ pub(super) fn offset(index: usize) -> u32 { (index as u64 * MARK_STRIDE) as u32 } -/// `input`'s least and greatest value over `rows`, as `lib/reduce.wesl` keeps them: the complement -/// of the least's ordered bits, then the greatest's; nothing where no row has one. +/// The least and greatest finite value of `input` over `rows`, encoded as `lib/reduce.wesl` keeps +/// them: the complement of the least's order-preserving bits, then the greatest's bits. `[0, 0]` +/// when no row has a finite value. fn fit(rows: &Rows<'_>, input: &crate::Expr) -> [u32; 2] { let ordered = |x: f32| { let b = x.to_bits(); @@ -257,8 +264,8 @@ fn fit(rows: &Rows<'_>, input: &crate::Expr) -> [u32; 2] { } impl super::marks::Shown { - /// Whether its fitted domains fit over every row of the table it draws in part, on the CPU: it - /// draws a table in part, and they read only its own data, nothing the view shows or derives. + /// Whether its fitted domains are computed on the CPU over every row of its paged table: it has + /// a drawing, and the domains read only its own data, nothing the view shows or derives. pub(crate) fn fits_data(&self) -> bool { let Some(compiled) = &self.compiled else { return false }; let program = &compiled.program; @@ -275,20 +282,20 @@ impl super::marks::Shown { && program.domains.iter().all(|reads| !reads.deps.intersects(viewed) && !derived(reads)) } - /// Segments of each row's path, for strokes whose rows all have as many. + /// Segments per row, for strokes whose rows all have the same count. pub(crate) fn segments_per_row(&self) -> u32 { if self.compiled.is_none() { return 0; } match self.kind() { - // A trace's points count the corners steps make. + // A trace's point count includes the corners its steps make. MarkKind::Lines => self.tables.traced().saturating_sub(1), _ => 1, } } - /// Instances its primitive draws: rows and exits, or every segment of their paths; at most what - /// its segment list keeps. + /// Instances its primitive draws: rows and exits, or every segment of their paths. With a + /// segment list, the entries the list keeps, an upper bound. pub(crate) fn instances(&self) -> u32 { if let Some(list) = &self.segments.list { return list.kept; @@ -301,7 +308,7 @@ impl super::marks::Shown { } } - /// Instances a probe or a measure reaches: live rows and their ports only. + /// Instances that probes and measures cover: live rows and their ports, not exits. pub(crate) fn live_instances(&self) -> u32 { if let Some(list) = &self.segments.list { return list.kept; @@ -319,7 +326,7 @@ impl super::marks::Shown { self.compiled.as_ref().map_or(0, |c| c.drawing.primitive.code()) } - /// Records the draw of its instances: as many as its segment list holds, when it has one. + /// Records the draw of its instances, indirect from its segment list when it has one. pub(super) fn record( &self, pass: &mut wgpu::RenderPass<'_>, @@ -338,9 +345,9 @@ impl super::marks::Shown { } impl View { - /// Evaluates the dirty channels of marks `work`, each over its dirty pages of rows, its fitted - /// domains reduced first where they changed: into its state, or while it eases into its settled - /// words. `state` is the expression machine's pass group. + /// Evaluates the dirty channels of marks `work` over their dirty row blocks, first reducing the + /// fitted domains that changed. Channels write a mark's state, or its settled words while it + /// eases. `state` is the expression machine's pass group. fn evaluate( &mut self, pass: &mut wgpu::ComputePass<'_>, @@ -360,10 +367,10 @@ impl View { pass.set_pipeline(&inner.pipelines.compute_pipeline(device, Variant::channels(shown.reads()))); pass.set_bind_group(1, group, &[offset(i)]); let rows = shown.live(); - // A domain changing evaluates every block, so the reductions run over the blocks listed; - // a table drawn in part fits domains reading only its data on the CPU, over every row. + // A changed domain dirties every block, so the reductions run over the listed blocks. A + // paged table whose domains read only its data fits them on the CPU, over every row. if shown.reduce && rows > 0 && !shown.fits_data() { - debug_assert_eq!(shown.blocks, super::marks::Blocks::All, "a domain changing changes every row"); + debug_assert_eq!(shown.blocks, super::marks::Blocks::All, "a changed domain dirties every block"); pass.set_bind_group(2, into, &[step(wgsl::STEP_REDUCE)]); dispatch_blocks(pass, rows, shown.block_count); } @@ -380,9 +387,9 @@ impl View { pass.set_bind_group(2, state, &[step(wgsl::STEP_CHANNELS)]); } - /// Evaluates dirty channels, and sums face normals when what they rest on changed; with - /// `measure`, also measures every measured mark's extents and reads them back. With `travel`, - /// comets travel on by the frame's step. + /// Records the frame's compute pass: evaluates dirty channels, and sums face normals when their + /// inputs changed. With `measure`, it also measures the extents of every measured mark and + /// returns their readback. With `travel`, comets move by the frame's step. pub(super) fn compute( &mut self, encoder: &mut wgpu::CommandEncoder, @@ -406,7 +413,7 @@ impl View { } inner.arena.written(); if !faces.is_empty() { - // Faces sum their normals afresh into the pages of the vertex marks they attach to. + // Faces sum their normals from zero into the pages of the vertex marks they attach to. let words = u64::from(wgsl::PAGE_ROWS * wgsl::NORMAL_WORDS) * 4; for shown in &self.marks { for &block in shown.words.pages(Kept::Normals).blocks.iter() { @@ -434,7 +441,7 @@ impl View { timestamp_writes: None, }); pass.set_bind_group(0, &groups.view, &[]); - // Each drawn row's word first: every pass after reads it. + // Each drawn row's word first: every later pass reads it. if let Some(group) = &lists_group { pass.set_bind_group(2, group, &[]); self.expand_runs(&mut pass, inner, &groups.mark, &expanded); @@ -442,11 +449,11 @@ impl View { pass.set_bind_group(2, &evaluate_group, &[super::resources::step(wgsl::STEP_CHANNELS)]); self.every_row(&mut pass, inner, &groups.mark, "carry", &carried); self.evaluate(&mut pass, inner, &groups.mark, &evaluate_group, &work); - // Before anything reading where rows' ends are held. + // Before anything that reads where rows' endpoints are held. self.resolve_ends(&mut pass, inner, &groups.mark, &evaluate_group, &resolved); - // After evaluating, so what a mark shows eases toward what it evaluated this frame. + // After evaluating, so marks ease toward this frame's values. self.every_row(&mut pass, inner, &groups.mark, "ease", &eased); - // After evaluating, so comets travel at the flow this frame shows. + // After evaluating, so comets travel at this frame's flow. self.every_row(&mut pass, inner, &groups.mark, "travel", &travelled); if let Some(group) = &lists_group { pass.set_bind_group(2, group, &[]); @@ -481,8 +488,8 @@ impl View { }) } - /// Starts the fitted domains marks `work` reduce: those a table drawn in part fits over all its rows - /// on the CPU written, the rest cleared for the GPU to fit. + /// Prepares the fitted domains that marks `work` reduce. Domains computed on the CPU over a + /// paged table's rows are written; the rest are cleared for the GPU to fit. fn start_domains(&mut self, encoder: &mut wgpu::CommandEncoder, work: &[usize]) { let mut fitted: Vec<(usize, Vec)> = Vec::new(); for &i in work { @@ -504,12 +511,12 @@ impl View { } } - /// What this frame's compute pass does: the marks it evaluates and whose ends it finds, the - /// segment lists it counts, the faces whose normals it sums, the marks it measures, and with - /// `travel` the marks whose comets travel. + /// Chooses the marks each compute step covers this frame: evaluation, endpoint resolution, + /// easing, segment counting, face normals, measuring, comet carry, run expansion, and with + /// `travel`, comet travel. fn work(&mut self, measure: bool, travel: bool) -> Work { for shown in &mut self.marks { - // A mark without rows has nothing to evaluate, nor ends to find. + // A mark without rows has nothing to evaluate and no endpoints to resolve. if shown.held() == 0 && shown.tables.synced() { shown.dirty = 0; shown.reduce = false; @@ -526,7 +533,7 @@ impl View { shown.place_version += 1; } } - // A mark easing toward a change writes what it shows as the clock moves, and as it evaluates. + // A mark in transition rewrites its shown state when the clock moves and when it evaluates. let eased: Vec = (0..self.marks.len()) .filter(|&i| { let shown = &self.marks[i]; @@ -555,9 +562,9 @@ impl View { Work { evaluated: work, resolved, eased, segments, faces, measured, carried, travelled, expanded } } - /// Lists the row blocks each mark evaluates this frame where its uniform finds them: every block of - /// a mark some channel of which changed for every row, else the blocks of the pages that changed; - /// and the row blocks whose ends it finds. + /// Writes the blocks buffer, which each mark's uniform points into. For each mark, it lists the + /// row blocks it evaluates this frame (every block when a channel changed for every row, else + /// the changed pages), then the row blocks whose endpoints it resolves. pub(super) fn lay_out_blocks(&mut self) { let mut words = Vec::new(); for shown in &mut self.marks { @@ -576,7 +583,7 @@ impl View { resources.write_blocks(&gpu.0.device, &gpu.0.queue, &words); } - /// Writes each drawn row's word of each of `marks`, those that left included, from its runs. + /// Writes the word of each drawn row of `marks`, exits included, from their runs. fn expand_runs( &self, pass: &mut wgpu::ComputePass<'_>, @@ -594,8 +601,8 @@ impl View { } } - /// Runs the expression machine's entry point `entry` over every held row of each of `marks`, those - /// that left included. + /// Runs the expression machine's entry point `entry` over every held row of `marks`, exits + /// included. fn every_row( &self, pass: &mut wgpu::ComputePass<'_>, @@ -614,7 +621,7 @@ impl View { } } - /// Records the count → scan → write of each of `marks`' segment lists. + /// Records the count, scan, and write steps of each of `marks`' segment lists. fn count_segments( &self, pass: &mut wgpu::ComputePass<'_>, @@ -637,9 +644,10 @@ impl View { } } - /// The words of mark `i`'s fitted domains as `lib/reduce.wesl` keeps them, each fitted over every - /// row of the table it draws in part, so a domain stays put however little of the table the - /// camera shows. Each is kept by what it read: a camera move fits none again. + /// The words of mark `i`'s fitted domains, as `lib/reduce.wesl` keeps them, each computed on + /// the CPU over every row of its paged table. So a domain stays fixed however little of the + /// table the camera shows. Each is cached by the stamp of its inputs, so a camera move refits + /// none. fn data_domains(&mut self, i: usize) -> Vec { let shown = &self.marks[i]; let Some(compiled) = &shown.compiled else { return Vec::new() }; @@ -671,8 +679,8 @@ impl View { words } - /// Whether face normals must be summed again: a face mark or a vertex mark wrote its state since - /// they were, or the globe turned under them. + /// Whether face normals need summing again: a face or vertex mark wrote its state since the + /// last sum, or the globe turned. fn normals_due(&mut self) -> bool { let faces = self.marks.iter().any(|m| m.drawable() && m.faces()); if !faces { @@ -696,8 +704,10 @@ impl View { true } - /// Draws the backdrop, every drawable mark in scene order, then `sprites` overlay sprites, counted - /// in `drawn`, once the text set this frame has its fields. Depth is kept when `probing` reads it. + /// Records the main render pass, counting draws in `drawn`: the backdrop, every drawable mark + /// in scene order, and the lit rows. Once the text pipelines are ready, it also builds the + /// distance fields of text set this frame, and draws the labels and `sprites` overlay sprites. + /// Depth is stored when `probing` reads it. pub(super) fn draw( &mut self, encoder: &mut wgpu::CommandEncoder, @@ -770,8 +780,8 @@ impl View { marks.draw(&mut pass, drawn, inner, i, shown, false); marks.ports(&mut pass, drawn, inner, i, shown); } - // The rows focus lights draw again over every mark but labels, glowing: nothing covers them but - // the ports they carry. They draw once what draws them compiled. + // Lit rows draw again, glowing, over every mark but labels; only their own ports cover + // them. They draw once their pipelines have compiled. for (i, shown) in self.marks.iter().enumerate() { if self.answers() && shown.drawable() && shown.lit.rows > 0 { marks.draw(&mut pass, drawn, inner, i, shown, true); @@ -790,13 +800,13 @@ impl View { } } - /// Finds the row nearest the probe among what the frame drew, and the nearest depth drawn there, - /// and reads them back. + /// Records the probe pass: finds the drawn row nearest the probe point and the nearest depth + /// drawn there, and returns their readback. pub(super) fn probe(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner) -> Readback { let gpu = self.gpu.clone(); let device = &gpu.0.device; let probed: Vec = (0..self.marks.len()).filter(|&i| self.marks[i].probed()).collect(); - // Every mark drawn has its probe program, which reads the depth under the probe as well as any. + // Any drawn mark's probe program can read the depth under the probe; the first one does. let under = self.marks.iter().position(super::marks::Shown::drawable); let resources = self.resources.as_mut().expect("created before passes"); let shader = crate::gpu::pipelines::probe(resources.samples()); @@ -811,8 +821,8 @@ impl View { timestamp_writes: None, }); pass.set_bind_group(0, &groups.view, &[]); - // Each step over every mark before the next: the nearest key, then its topmost instance, - // then where that lies. + // Each step runs over every mark before the next: the nearest key, then its topmost + // instance, then where that lies. for step in [wgsl::STEP_NEAREST, wgsl::STEP_TOPMOST, wgsl::STEP_DESCRIBE] { pass.set_bind_group(2, &group, &[super::resources::step(step)]); for &i in &probed { @@ -836,8 +846,9 @@ impl View { ) } - /// Records step `step` of mark `i`'s probe program, `shader`, the step's pass group bound: over - /// every instance it reaches, or once for a step that reads one thing. + /// Records step `step` of mark `i`'s probe program `shader`, with the step's pass group bound. + /// It dispatches over every instance probes cover, or a single workgroup for a step that reads + /// one result. pub(super) fn probe_step( &self, pass: &mut wgpu::ComputePass<'_>, diff --git a/crates/latkit/src/view/plan.rs b/crates/latkit/src/view/plan.rs index 2775e65b..f32ff288 100644 --- a/crates/latkit/src/view/plan.rs +++ b/crates/latkit/src/view/plan.rs @@ -1,17 +1,22 @@ -//! The plan: how each table drawn fits the budget. A table draws whole, in priority order when a mark -//! drawing it gives one, unless it is too large to: denser than a screen shows, or more than the -//! view's share of the budget holds with the rest. A table costs what the arena allocates for it: its -//! columns' pages and the pages of words its marks keep per row, in the blocks the arena hands out, -//! and its geometry's. The GPU's ledger grants each view its share, by what it would draw whole. What -//! the tables cost is worked out again only when the scene or a drawn table's rows or geometry change, -//! and the plan only when those costs or the share do. What tables drawn in part hold reaches past -//! what the costs foresee, as the vertices the edges drawn end at do: where what the view holds passes -//! its share, or finds no room in a full arena, it plans them within a smaller part of its share from -//! then on, until the share changes; so what it draws and what replaces it fit, with room to cache. Then a table a mark places on a map or a plane -//! draws through tiles, the rows of the tiles the camera asks for, as many as its share holds, and -//! edges ending at it draw through tiles too, so they show with their ends; a table a mark traces -//! draws whole, its traces spanning as few pixel columns as fit; any other table draws the rows that -//! matter most, as many as fit. What a view draws always fits. +//! The plan: how each drawn table fits the GPU budget. +//! +//! A table draws whole (in priority order when a mark gives one) unless it is too large. A table a +//! mark places on a map or a plane becomes a paged table when it is denser than a screen can show, +//! and the largest such tables are paged until the rest fit the view's budget share. A paged table +//! draws the rows of the tiles the camera needs, as many as its share holds, and edges ending at it +//! are paged too, so they show with their endpoints. If the whole tables still do not fit, a table +//! a line traces keeps drawing whole, its traces spanning only as many pixel columns as fit, and +//! any other table draws the rows that matter most, as many as fit. What a view draws always fits. +//! +//! A table costs what the arena allocates for it: its columns' pages, the arena blocks of its +//! marks' per-row words, and its geometry. The GPU's ledger grants each view a share based on what +//! it would draw whole. Costs are recomputed only when the scene, or a drawn table's rows or +//! geometry, change, and the plan only when the costs or the share change. +//! +//! Paged tables can need more than the costs predict, such as the kept rows that drawn edges end +//! at. When what the view holds exceeds its share, or finds no room in a full arena, paged tables +//! are planned within a smaller part of the share until the share changes. That way the current +//! drawing and its replacement both fit, with room left to keep idle pages. use std::hash::{Hash, Hasher}; @@ -29,20 +34,19 @@ use crate::gpu::{Ask, Ledger}; use crate::scene::mark::MarkKind; use crate::{Expr, Kind, Mark, Space}; -/// Rows past which a table a mark places draws through tiles: more than a screen shows one to a cell. +/// Rows past which a table a mark places is paged: more than a screen shows at one row per cell. pub(crate) const DENSE: usize = 1 << 18; -/// Pixel columns a view asks its traces' room for: a wide screen's. +/// Pixel columns a view requests trace room for: a wide screen's width. const ASKED_COLUMNS: f64 = 4096.0; -/// Rows a table drawn through tiles draws at least: a tile's. +/// The fewest rows a paged table draws: one tile's worth. const LEAST: u32 = 1 << 14; -/// Bytes a row's trace takes for each pixel column it spans: the least and greatest values of two -/// buckets. +/// Bytes a row's trace takes per pixel column: the least and greatest values of two buckets. const TRACE_COLUMN_BYTES: f64 = 16.0; -/// Pixel columns a trace spans at least, however little room is left. +/// The fewest pixel columns a trace spans, however little room is left. const TRACE_COLUMNS: u32 = 32; -/// What a table costs, as the arena allocates it: its rows, bytes a page of them takes, bytes its -/// geometry takes, and bytes its traces take for each pixel column they span. +/// A table's cost as the arena allocates it: its row count, bytes per page of rows, bytes of +/// geometry, and bytes its traces take per pixel column. struct Cost { rows: usize, per_page: f64, @@ -61,23 +65,23 @@ impl Cost { } } -/// Each table the scene draws, drawn whole, and what it costs. +/// Each table the scene draws, planned as drawn whole, and its cost. #[derive(Default)] struct Costs { whole: Plans, tables: Keyed<(Name, Name), Cost>, - /// The pages the costs count that any view drawing the same data shares: each field's by its - /// version and how it is read, the bytes the costs count of it, and its bytes once. + /// Pages in the costs that every view drawing the same data shares, keyed by field version and + /// read mode: the bytes the costs count for each, and its size counted once. shared: FxHashMap, } -/// The plan, and the costs it weighs, each worked out again only when what it reads changes. +/// The plan and the costs it uses, each recomputed only when its inputs change. pub(crate) struct Planning { costs: Derived, - /// How each table draws, and the pixel columns a trace spans at most. + /// How each table draws, and the most pixel columns a trace spans. plan: Derived<(Plans, u32)>, - /// The part of the room left for tables drawn in part that the plan gives them: lowered each time - /// what they held found no room in a full arena, until the share changes. + /// The fraction of the remaining room the plan gives paged tables. Lowered each time they find + /// no room in a full arena or exceed the share; reset when the share changes. fit: f64, share: f64, } @@ -99,29 +103,29 @@ impl Planning { self.costs.value.tables.get(key).map_or(0.0, |cost| f64::from(pages) * cost.per_page) } - /// The pixel columns a trace spans at most. + /// The most pixel columns a trace spans. pub(crate) fn trace_columns(&self) -> u32 { self.plan.value.1 } - /// What tables drawn in part held found no room in a full arena: they draw within less, while - /// any draws more than the least it may. Whether the plan draws less. + /// Records that paged tables found no room in a full arena, so they draw within less while any + /// draws more than its minimum. Returns whether the plan shrank. pub(crate) fn overflowed(&mut self) -> bool { self.tighten(0.75) } - /// The view holds `bytes` for what it draws: past its share, where the plan held back some of what - /// the camera asks of a table drawn in part (`capped`), tables drawn in part draw within as much - /// less as it passes the share by. Where it held back nothing, a plan within less draws no less, - /// and only lets go of the pages it keeps from before. + /// Records that the view holds `bytes` for what it draws. If that exceeds the share and the + /// plan held back some of what the camera needs of a paged table (`capped`), paged tables draw + /// within less, in proportion to the excess. If nothing was held back, a smaller plan would + /// draw no less and only release idle pages, so the plan stays. pub(crate) fn held(&mut self, bytes: u64, capped: bool) { if capped && self.share > 0.0 && bytes as f64 > self.share { self.tighten(self.share / bytes as f64); } } - /// Plans tables drawn in part within `part` of what it gave them, while any draws more than the - /// least it may: whether it does. + /// Scales the room for paged tables by `part`, if any draws more than its minimum. Returns + /// whether it did. fn tighten(&mut self, part: f64) -> bool { let shrinks = self.plans().values().any(|p| matches!(p.draw, Draw::Tiles { rows } if rows > LEAST)); if shrinks { @@ -132,8 +136,8 @@ impl Planning { } impl View { - /// Brings the plan up to the scene, the data, and the share of `budget` the GPU's `ledger` - /// grants the view by what it would draw whole. + /// Updates the plan for the scene, the data, and the share of `budget` that the GPU's `ledger` + /// grants the view based on what it would draw whole. pub(super) fn update_plan(&mut self, ledger: &mut Ledger, budget: u64) { let stamp = self.costs_stamp(); if self.planning.costs.due(stamp) { @@ -159,8 +163,8 @@ impl View { } } - /// What the costs read, stamped: the scene, and of each table a mark draws, its dataset's - /// schema, its rows, its geometry, and whether the view fetches it as tiles. + /// A stamp of the costs' inputs: the scene, and for each table a mark draws, its dataset's + /// schema, its row structure, its geometry's version, and whether it comes from tiles. fn costs_stamp(&self) -> u64 { let mut hasher = FxHasher::default(); self.scene_version.hash(&mut hasher); @@ -175,9 +179,8 @@ impl View { hasher.finish().max(1) } - /// How each table the scene draws draws, within `share` bytes, from what `costs` says each - /// costs, tables drawn in part within `fit` of the room left them; and the pixel columns a trace - /// spans at most. + /// Plans how each table the scene draws fits in `share` bytes given `costs`, giving paged + /// tables `fit` of the remaining room. Also returns the most pixel columns a trace spans. fn plan(&self, costs: &Costs, share: f64, fit: f64) -> (Plans, u32) { let (mut plans, costs) = (costs.whole.clone(), &costs.tables); let tileable = @@ -191,7 +194,7 @@ impl View { tiled.push(key.clone()); } }; - // Dense tables draw through tiles, then the largest until what draws whole fits. + // Page dense tables, then the largest ones until the whole tables fit. let keys: Vec<(Name, Name)> = plans.keys().cloned().collect(); for key in &keys { if tileable(key, &plans[key]) && costs[key].rows > DENSE { @@ -206,14 +209,14 @@ impl View { let Some(key) = largest.cloned() else { break }; tile(&key, &mut plans, &mut whole); } - // Edges ending at a table drawn through tiles draw through tiles, so they show with their ends. + // Edges ending at a paged table are paged too, so they show with their endpoints. for key in &keys { if self.ends_tiled(key, &plans) { tile(key, &mut plans, &mut whole); } } - // What still does not fit draws the rows that matter most, as many as fit; a table a mark - // traces draws whole, its traces thinning in time instead. + // If the whole tables still do not fit, each draws the rows that matter most, as many as + // fit. A table a line traces still draws whole; its traces get coarser in time instead. if whole > share { let kept: f64 = keys .iter() @@ -230,12 +233,12 @@ impl View { } whole = share.max(kept); } - // Tables drawn through tiles, and traces, share what is left. + // Paged tables and traces share the remaining room. let traced: f64 = costs.values().map(|c| c.traced).sum(); let left = (share - whole).max(share / 4.0) * fit; let each = left / (tiled.len() + usize::from(traced > 0.0)).max(1) as f64; for key in &tiled { - // A row drawn in part takes a word more: which held row it is. + // A paged table's drawn row takes one more word: its held row. let per_row = costs[key].per_row().max(1.0) + 4.0; let rows = (each / per_row).min(f64::from(u32::MAX)) as u32; plans.get_mut(key).expect("listed").draw = Draw::Tiles { rows: rows.max(LEAST) }; @@ -244,8 +247,8 @@ impl View { (plans, columns) } - /// Each table the scene draws, drawn whole, placed and ordered by the first mark drawing it to - /// give a place or a priority, with the fields its marks read; and what it costs. + /// Plans each table the scene draws as whole, with the place and priority of the first mark + /// that gives each, and computes each table's cost from the fields its marks read. fn costs(&self) -> Costs { let space = &self.scene.space; let placing = matches!(space, Space::Earth(_) | Space::Plane(_)); @@ -253,7 +256,7 @@ impl View { let mut plans = Plans::default(); let mut costs: Keyed<(Name, Name), Cost> = Keyed::default(); let mut shared: FxHashMap = FxHashMap::default(); - // The tables faces end at keep the normals faces sum at their rows. + // Tables that faces end at keep the normals the faces sum at their rows. let mut summed: Vec<(Name, Name)> = Vec::new(); for shown in self.marks.iter().filter(|m| m.kind() == MarkKind::Faces) { let Some(compiled) = &shown.compiled else { continue }; @@ -290,9 +293,9 @@ impl View { Costs { whole: plans, tables: costs, shared } } - /// The pages `shown` reads of `table`, and of the tables it joins, that every view drawing the - /// same data shares, as `page_bytes` and `geometry_bytes` count them: each field's, by its version - /// and how it is read. + /// The pages `shown` reads of `table` and its joined tables that every view drawing the same + /// data shares, sized as `page_bytes` and `geometry_bytes` count them. Each field's pages are + /// keyed by its version and read mode. fn shared_pages(&self, shown: &Shown, table: &Table) -> Vec<(u64, f64)> { let Some(compiled) = &shown.compiled else { return Vec::new() }; let mut pieces: Vec<(u64, f64)> = compiled @@ -316,7 +319,7 @@ impl View { pieces } - /// Whether the table `key` names is edges or faces whose ends lie in a table drawn through tiles. + /// Whether table `key` holds edges or faces whose endpoints lie in a paged table. fn ends_tiled(&self, key: &(Name, Name), plans: &Plans) -> bool { let Place::Ends(fields) = &plans[key].place else { return false }; let Some(table) = self.snapshot(&key.0).and_then(|d| d.table(&key.1)) else { return false }; @@ -356,7 +359,7 @@ fn geometry_of(mark: &Mark) -> Option { } } -/// Bytes of the block a page of `width` words per row takes. +/// Bytes of the arena block for a page of rows at `width` words per row. fn block_bytes(width: u32) -> f64 { f64::from(rounded(wgsl::PAGE_ROWS * width)) * 4.0 } @@ -366,10 +369,10 @@ fn column_page_bytes(low: bool) -> f64 { block_bytes(1) * if low { 2.0 } else { 1.0 } } -/// Bytes a page of `shown`'s rows takes, as the arena allocates them: a page of each column it reads, -/// and of each kind of words it keeps per row: its state, its focus words, its labels' candidates, at -/// a vertex mark faces attach to the normals they sum, and where the scene is `easing`, the frozen -/// and settled state a transition keeps. +/// Bytes a page of `shown`'s rows takes, as the arena allocates them: a page of each column it +/// reads, plus each kind of per-row words. Those are its state, focus words, and label candidates; +/// the `normals` faces sum, at a vertex mark they attach to; and when `easing`, a transition's +/// frozen and settled words. fn page_bytes(shown: &Shown, easing: bool, normals: bool) -> f64 { let Some(compiled) = &shown.compiled else { return 0.0 }; let columns: f64 = @@ -381,8 +384,8 @@ fn page_bytes(shown: &Shown, easing: bool, normals: bool) -> f64 { columns + state + block_bytes(1) + labels + normals + eased } -/// Bytes the traces `shown` draws of `table` take for each pixel column they span: a row's for each -/// field it traces. +/// Bytes the traces `shown` draws of `table` take per pixel column: `TRACE_COLUMN_BYTES` for each +/// row of each traced field. fn traced_bytes(shown: &Shown, table: &Table) -> f64 { let Some(compiled) = &shown.compiled else { return 0.0 }; let traced = compiled.program.columns.iter().filter(|c| { @@ -391,9 +394,9 @@ fn traced_bytes(shown: &Shown, table: &Table) -> f64 { traced.count() as f64 * table.len() as f64 * TRACE_COLUMN_BYTES } -/// Bytes the geometry `shown` draws of `table` takes, as the arena allocates it: for each page of -/// rows, their rows' table and their points in pieces; an area's triangles take about three times the -/// points of its outlines. +/// Bytes the geometry `shown` draws of `table` takes, as the arena allocates it: per page of rows, +/// a row table and the points in pieces. An area's triangles take about three times the points of +/// its outlines. fn geometry_bytes(shown: &Shown, table: &Table) -> f64 { let Some(field) = geometry_of(&shown.mark) else { return 0.0 }; let Some(Field::Values(column, _)) = table.field_named(&field) else { return 0.0 }; diff --git a/crates/latkit/src/view/probe.rs b/crates/latkit/src/view/probe.rs index 37bc71c9..ec15a73f 100644 --- a/crates/latkit/src/view/probe.rs +++ b/crates/latkit/src/view/probe.rs @@ -1,7 +1,8 @@ -//! Probes: what lies under a point, answered by a compute pass over what the frame drew and read back -//! a frame or two later: the nearest row, and the nearest depth drawn. One probe is in flight at a -//! time; a newer point waits for it. An answer decodes against what its frame drew, whatever changed -//! before it landed: the rows its marks held, and the camera it drew through. +//! Probes: the nearest row and the nearest drawn depth under a point. +//! +//! A compute pass runs over what the frame drew, and its result is read back a frame or two later. +//! One probe is in flight at a time; a newer point waits for it. The answer is decoded against the +//! frame that drew it (its marks' held rows and its camera), regardless of changes since. use std::sync::Arc; @@ -16,8 +17,8 @@ use crate::gpu::shaders as wgsl; use crate::scene::mark::MarkKind; use crate::{Camera, Name, Row}; -/// A hit, with the depth it lies at for dragging, and for an edge which end it is nearer: 0 its -/// first, 1 its second. +/// A hit, with its depth (for dragging) and, for an edge, the nearer endpoint: 0 for the first, +/// 1 for the second. #[derive(Clone, Debug)] pub(crate) struct Probed { pub(crate) hit: Hit, @@ -33,17 +34,17 @@ pub(crate) struct Answer { pub(crate) under: f32, } -/// What a frame drew, as an answer about it reads it: each mark's rows as it held them, and the camera -/// the frame drew through. +/// A record of what a frame drew, for decoding answers about it: each mark's held rows, and the +/// frame's camera. #[derive(Clone)] pub(crate) struct Drew { marks: Arc<[Held]>, projected: Projected, } -/// What a mark held as its frame drew it: its dataset, table, and name, whether its rows are vertices -/// and selection picks them, the rows its state held and how, its ports, and for a line tracing a -/// field, the field and what its trace drew. +/// One mark as its frame drew it: its dataset, table, and name; whether its rows are vertices and +/// whether selection picks them; the rows its state held; its port names; and for a line tracing a +/// field, the field and its trace plan. struct Held { data: Name, table: Name, @@ -56,7 +57,7 @@ struct Held { } impl Drew { - /// The row mark `i` held as held row `held`, and its place in its table. + /// The row at held row `held` of mark `i`, and its index in its table. pub(super) fn row(&self, i: usize, held: u32) -> Option<(Row, usize)> { let mark = self.marks.get(i)?; let holding = mark.holding.as_ref()?; @@ -78,7 +79,7 @@ impl Drew { self.marks.get(i).is_some_and(|m| m.selects) } - /// The port of a vertex mark `i` its probe reached: its number from 1, or 0 for the marker. + /// The name of port `number` of vertex mark `i`, numbered from 1; `None` for 0, the marker. pub(super) fn port(&self, i: usize, number: u32) -> Option { let mark = self.marks.get(i)?; if !mark.vertices || number == 0 { @@ -87,21 +88,21 @@ impl Drew { mark.ports.get(number as usize - 1).cloned() } - /// The sample point `point` of row `row`'s traced path stands for, on mark `i` tracing a field. + /// The sample that point `point` of row `row`'s traced path shows, on mark `i` tracing a field. pub(super) fn sample(&self, i: usize, row: usize, point: u32) -> Option { let mark = self.marks.get(i)?; let (field, plan) = mark.traced.as_ref()?; let (level, index, side) = plan.entry(point)?; let table = &mark.holding.as_ref()?.table; let crate::data::Field::Sampled(samples, _) = table.field_named(field)? else { return None }; - // A bucket's points stand for its middle frame, its least or greatest value. + // A bucket's two points show its middle frame, with its least or greatest value. let span = crate::data::bucket_frames(level); let frame = index * span + span / 2; let value = samples.entry(row, level, side, index)?; Some(Sample { frame, at: samples.drawn_at(level, index)?, value }) } - /// The position `point` on screen at `depth` stands for, as the frame's camera saw it. + /// The position of screen point `point` at `depth`, through the frame's camera. pub(super) fn position(&self, point: [f32; 2], depth: f64) -> [f64; 3] { self.projected.position(self.projected.unproject(point.map(f64::from), depth)) } @@ -117,7 +118,7 @@ pub(crate) struct Probing { } impl Probing { - /// Whether a point waits for a frame to probe it: none while a probe is being read back. + /// Whether a point is waiting for a frame to probe it. Never while a probe is being read back. pub(crate) fn due(&self) -> bool { self.wanted.is_some() && self.reading.is_none() } @@ -126,7 +127,8 @@ impl Probing { self.latest.as_ref().and_then(|(_, answer)| answer.hit.as_ref()) } - /// The latest answer when it was asked within a pixel or two of `point` and nothing newer waits. + /// The latest answer, if it was for a point within 2 CSS pixels of `point` and no newer probe + /// is pending. pub(crate) fn at(&self, point: [f32; 2]) -> Option { let (asked, answer) = self.latest.as_ref()?; (self.wanted.is_none() && self.reading.is_none() && near(*asked, point, 2.0)).then(|| answer.clone()) @@ -138,9 +140,9 @@ pub(crate) fn near(a: [f32; 2], b: [f32; 2], within: f32) -> bool { } impl View { - /// The point this frame probes, when it `answers` probes: one waits and none is in flight. What - /// the pointer rests on is probed again whenever the frame changed under it, so a moving frame - /// costs a probe per readback and a still one nothing. + /// The point this frame probes, if it `answers` probes, a point is waiting, and no probe is in + /// flight. The point under a resting pointer is probed again whenever the frame changes under + /// it, so a moving frame runs one probe per readback and a still frame runs none. pub(super) fn probe_point(&mut self, answers: bool) -> Option<[f32; 2]> { if (self.changed || self.easing()) && matches!(self.gestures.now, Gesture::Idle) @@ -157,7 +159,7 @@ impl View { point.filter(|_| self.marks.iter().any(Shown::drawable)) } - /// CSS pixels a probe reaches around its point: a finger reaches farther. + /// The probe radius in CSS pixels: the scene's pick radius, plus 8 for touch. pub(super) fn probe_radius(&self) -> f32 { let touch = self.pointer.is_some_and(|pointer| pointer.device == Device::Touch); self.scene.controls.pick_radius + if touch { 8.0 } else { 0.0 } @@ -167,7 +169,7 @@ impl View { self.probing.reading = Some((point, readback, self.drew())); } - /// What this frame drew, as an answer about it reads it. + /// A record of what this frame drew, for decoding answers about it. pub(super) fn drew(&self) -> Drew { let marks: Vec = self .marks @@ -194,7 +196,7 @@ impl View { Drew { marks: marks.into(), projected: self.projected() } } - /// Hands a landed probe's answer to hover and gestures. + /// Hands a finished probe's answer to hover and gestures. pub(super) fn settle_probe(&mut self) { let Some((point, readback, _)) = &self.probing.reading else { return }; let Some(bytes) = readback.take() else { return }; @@ -204,8 +206,8 @@ impl View { self.landed(point, answer); } - /// The point gestures move about under `point`: what the frame drew there, else the level the - /// camera looks at, else a point as far away as the camera's center. + /// The position gestures pivot on under screen point `point`: what the frame drew there, else + /// the ground level the camera looks at, else a point as far away as the camera's center. pub(super) fn anchor(&self, point: [f32; 2], answer: Option<&Answer>) -> [f64; 3] { let view = self.projected(); let p = point.map(f64::from); @@ -227,7 +229,7 @@ impl View { } } -/// What a probe's words say of what its frame drew. +/// Decodes a probe's words against what its frame drew. fn answer(words: &[u32], drew: &Drew) -> Answer { let word = |i: u32| words.get(i as usize).copied().unwrap_or(0); Answer { hit: hit(&word, drew), under: f32::from_bits(word(wgsl::PROBE_UNDER)) } @@ -239,7 +241,7 @@ fn hit(word: &impl Fn(u32) -> u32, drew: &Drew) -> Option { return None; } let mark = 255 - (key & 255) as usize; - // The probe names a held row: the row of its table it held. + // The probe reports a held row; map it to its table row. let (row, at) = drew.row(mark, word(wgsl::PROBE_ROW))?; let depth = f32::from_bits(word(wgsl::PROBE_DEPTH)); let point = [f32::from_bits(word(wgsl::PROBE_X)), f32::from_bits(word(wgsl::PROBE_Y))]; diff --git a/crates/latkit/src/view/program.rs b/crates/latkit/src/view/program.rs index ff9c2577..4a58dff7 100644 --- a/crates/latkit/src/view/program.rs +++ b/crates/latkit/src/view/program.rs @@ -1,9 +1,11 @@ -//! The program buffer: each mark's descriptors, tables, ranges, code, constants, ends, and ports in a -//! region of its own, rewritten alone when only that mark changed, and then only where its words -//! changed. A region that outgrows its room moves to the end of the buffer; a buffer too small for it -//! grows, what it holds copied into the new one on the GPU, so no other region is written again. Once -//! regions that moved leave more of the buffer behind than regions take, every region is laid out -//! again, each writing only its own words. +//! The program buffer: each mark's column descriptors and tables, ranges, code, constants, end +//! tables, and ports, in a region of its own. +//! +//! When a mark changes, only its region is rewritten, and only the words that differ. A region that +//! outgrows its room moves to the end of the buffer. If the buffer is too small, it grows and the +//! GPU copies its contents over, so no other region is rewritten. Once moved regions leave more of +//! the buffer unused than the regions take, every region is laid out again from the start, each +//! writing only its own words. use super::{Event, View}; use crate::gpu::Inner; @@ -15,16 +17,16 @@ pub(crate) struct Regions { pub(crate) relayout: bool, } -/// Words alike between two that changed, past which they are written apart. +/// Changed words separated by fewer unchanged words than this are written in one span. const GAP: usize = 16; -/// The room a region of `len` words gets, so it can grow a little in place. +/// The room given to a region of `len` words, with slack to grow in place. fn room(len: u32) -> u32 { len + len / 4 + 16 } -/// The spans of `words` that differ from `was`, what the same place held, as `[start, end)`: those -/// closer than `GAP` words written as one. +/// The spans of `words` that differ from `was` (the words last written there), as `[start, end)`. +/// Spans closer than `GAP` words merge. fn changed(was: &[u32], words: &[u32]) -> Vec<[usize; 2]> { let mut spans: Vec<[usize; 2]> = Vec::new(); for i in (0..words.len()).filter(|&i| was.get(i) != Some(&words[i])) { @@ -37,8 +39,8 @@ fn changed(was: &[u32], words: &[u32]) -> Vec<[usize; 2]> { } impl View { - /// Writes every mark whose words changed into its region of the program buffer; a buffer grown - /// has what it held copied over in `encoder`, and what is written after follows the copy there. + /// Writes every changed mark's words into its region of the program buffer. When the buffer + /// grows, `encoder` copies the old contents over, and later writes are ordered after the copy. pub(super) fn pack(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner) { let gpu = self.gpu.clone(); let font = self.scene.theme.text.font.clone(); @@ -59,18 +61,19 @@ impl View { } let resources = self.resources.as_mut().expect("created before packing"); let (device, queue) = (&gpu.0.device, &gpu.0.queue); - // Whether the buffer grew this frame: its words are copied in `encoder`, so writes follow there. - let mut grown = false; - let write = |encoder: &mut wgpu::CommandEncoder, grown: bool, buffer: &wgpu::Buffer, at: u32, words: &[u32]| { - let (at, bytes) = (u64::from(at) * 4, bytemuck::cast_slice(words)); - if grown { - queue.write_buffer_in(device, encoder, buffer, at, bytes); - } else { - queue.write_buffer(buffer, at, bytes); - } - }; + // Whether `encoder` copied words this frame. Writes after a copy must be ordered after it. + let mut ordered = false; + let write = + |encoder: &mut wgpu::CommandEncoder, ordered: bool, buffer: &wgpu::Buffer, at: u32, words: &[u32]| { + let (at, bytes) = (u64::from(at) * 4, bytemuck::cast_slice(words)); + if ordered { + queue.write_buffer_in(device, encoder, buffer, at, bytes); + } else { + queue.write_buffer(buffer, at, bytes); + } + }; if !self.regions.relayout { - // The room regions take; the rest of the buffer up to its end, regions that moved left. + // The room the regions take. Moved regions left the rest of the buffer, up to `end`. let mut live: u32 = self.marks.iter().filter_map(|m| m.region).map(|[_, room]| room).sum(); for shown in self.marks.iter_mut().filter(|m| !m.packed && m.compiled.is_some()) { let len = shown.packed_len(); @@ -85,10 +88,19 @@ impl View { } if u64::from(end) > resources.program_words() { let kept = u64::from(self.regions.end); - grown |= resources.grow_program(device, encoder, u64::from(end), kept); + ordered |= resources.grow_program(device, encoder, u64::from(end), kept); + } + // The GPU copies the region's words to the new place, so only changed words + // are written. + match region { + Some([from, _]) if !shown.written.is_empty() => { + let words = shown.written.len() as u32; + resources.move_program(device, encoder, from, start, words); + ordered = true; + } + _ => shown.written.clear(), } shown.region = Some([start, room(len)]); - shown.written.clear(); self.regions.end = end; start } @@ -96,7 +108,7 @@ impl View { let mut words = Vec::with_capacity(len as usize); shown.pack(start, &mut words); for [from, to] in changed(&shown.written, &words) { - write(encoder, grown, resources.program(), start + from as u32, &words[from..to]); + write(encoder, ordered, resources.program(), start + from as u32, &words[from..to]); } shown.written = words; shown.packed = true; @@ -105,14 +117,14 @@ impl View { if std::mem::take(&mut self.regions.relayout) { let total: u64 = self.marks.iter().map(|m| u64::from(room(m.packed_len()))).sum(); resources.fit_program(device, total); - // Each region's own words, at the start of its room: the room past them, which nothing reads - // until the region grows into it and writes it, is not written. + // Write each region's words at the start of its room. The slack after them is not + // written: nothing reads it until the region grows into it and writes it. let mut start = 0; for shown in self.marks.iter_mut().filter(|m| m.compiled.is_some()) { let len = shown.packed_len(); let mut words = Vec::with_capacity(len as usize); shown.pack(start, &mut words); - write(encoder, grown, resources.program(), start, &words); + write(encoder, ordered, resources.program(), start, &words); shown.written = words; shown.region = Some([start, room(len)]); shown.packed = true; diff --git a/crates/latkit/src/view/query.rs b/crates/latkit/src/view/query.rs index 879a7744..7280277f 100644 --- a/crates/latkit/src/view/query.rs +++ b/crates/latkit/src/view/query.rs @@ -1,10 +1,11 @@ -//! Queries: every row within reach of a point, every row in a box, and where a row lies on screen, -//! answered on the GPU by each mark's probe program through the geometry the frame drew, and read back -//! a frame or two later, then decoded against what that frame drew. What a query finds of each mark -//! follows the probe's words: the hits it keeps, after how many it found and how many it keeps, or a -//! bit per row. Hits land in any order; they are told in one, and a mark that found more than it kept -//! asks again with room for them all. One query runs at a time; the rest wait their turn, and one -//! nobody waits for any more is passed over. +//! Queries: every row within reach of a point, every row in a box, and where a row lies on screen. +//! +//! Each mark's probe program answers on the GPU, through the geometry the frame drew. The result is +//! read back a frame or two later and decoded against that frame. A query's results for each mark +//! follow the probe's words in the probe buffer: a hit count and capacity followed by the hits, or +//! a bit per row. Hits arrive in any order and are sorted before they are returned. A mark that +//! found more hits than it kept is queried again with room for all of them. One query runs at a +//! time; the rest wait in order, and a query nobody awaits any more is skipped. use std::collections::VecDeque; @@ -19,21 +20,23 @@ use crate::gpu::readback::Readback; use crate::gpu::shaders as wgsl; use crate::{Pending, Resolver, Row}; -/// Hits a mark keeps at first: past them, a query counts hits but keeps none, and asks again. +/// Hits kept per mark on the first try. Hits past these are counted but not kept, and the query +/// runs again with room for them. const HITS: u32 = 1024; -/// A question for the GPU. +/// A query for the GPU. pub(crate) enum Ask { /// Every row within `radius` CSS pixels of `point`, keeping up to `room` hits of each mark. Hits { point: [f32; 2], radius: f32, room: u32, reply: Resolver> }, - /// Every row in the box between two corners; they become the selection, as `trigger` chose. + /// Every row in the box between two corners. The rows become the selection, with `trigger` as + /// its cause. Boxed { corners: [[f32; 2]; 2], trigger: Trigger }, - /// Where a row lies. + /// Where a row lies on screen. Locate { row: Row, reply: Resolver> }, } impl Ask { - /// Whether nobody waits for its answer any more: a box selects, so it always runs. + /// Whether nobody awaits its answer any more. A box query selects rows, so it always runs. fn cancelled(&self) -> bool { match self { Ask::Hits { reply, .. } => reply.is_cancelled(), @@ -42,7 +45,7 @@ impl Ask { } } - /// The frame's query words for it. + /// The frame uniform's query words for it. fn words(&self, marks: &[usize], view: &View) -> [f32; 4] { match self { Ask::Hits { point, radius, .. } => [point[0], point[1], *radius, 0.0], @@ -63,8 +66,8 @@ impl Ask { } } - /// What it keeps of a mark of `instances` instances and `rows` rows, hits or rows, and the words - /// that takes. + /// How many hits or rows it keeps of a mark with `instances` instances and `rows` rows, and the + /// words that takes. fn room(&self, instances: u32, rows: u32) -> (u32, u32) { match self { Ask::Hits { room, .. } => { @@ -77,7 +80,8 @@ impl Ask { } } -/// What a query finds of mark `mark`: from word `at` of the probe's, `room` hits or rows. +/// Where a query's results for mark `mark` lie: from probe-buffer word `at`, with room for `room` +/// hits or rows. #[derive(Clone, Copy)] struct Found { mark: usize, @@ -85,8 +89,8 @@ struct Found { room: u32, } -/// A query this frame runs: what it asks, what it finds of each mark it asks, and the words that -/// takes past the probe's. +/// The query this frame runs: the query, where each mark's results go, and the words they take +/// after the probe's. struct Running { ask: Ask, found: Vec, @@ -109,14 +113,14 @@ pub(crate) struct Queries { } impl Queries { - /// Whether a query waits to run that someone still waits for. + /// Whether a query is running, or waiting to run with someone still awaiting it. pub(crate) fn due(&self) -> bool { self.running.is_some() || self.waiting.iter().any(|ask| !ask.cancelled()) } } impl View { - /// Every row of a mark that picks within `radius` CSS pixels of `point`, nearest first, as the + /// Every row of a pickable mark within `radius` CSS pixels of `point`, nearest first, as the /// next frames draw them. pub fn pick(&mut self, point: [f32; 2], radius: f32) -> Pending> { let (answer, reply) = Pending::new(); @@ -125,8 +129,8 @@ impl View { answer } - /// Where `row` lies on screen, CSS pixels from the top left, as the next frames draw it; `None` - /// when no mark draws it in view. A diagram's vertices answer at once from its sheet. + /// Where `row` lies on screen, in CSS pixels from the top left, as the next frames draw it; + /// `None` when no mark draws it in view. A diagram's vertices resolve at once from its sheet. pub fn screen_of(&mut self, row: &Row) -> Pending> { let diagram = matches!(self.scene.space, crate::Space::Diagram(_)); if let Some(b) = self.derived.sheet.bounds(row).filter(|_| diagram) { @@ -140,13 +144,13 @@ impl View { answer } - /// Selects every row in the box between two screen points, as `trigger` chose. + /// Selects every row in the box between two screen points, with `trigger` as the cause. pub(super) fn select_box(&mut self, corners: [[f32; 2]; 2], trigger: Trigger) { self.queries.waiting.push_back(Ask::Boxed { corners, trigger }); self.wake(); } - /// Where mark `i` draws `row` among the rows it draws, if it does. + /// The position of `row` among the rows mark `i` draws, if it draws it. pub(super) fn row_index(&self, i: usize, row: &Row) -> Option { let shown = self.marks.get(i)?; let style = shown.mark.style(); @@ -157,8 +161,8 @@ impl View { shown.item_of(shown.held_row(index as u32)?).filter(|&item| item < shown.rows) } - /// Takes the next query for this frame when none runs, passing over those nobody waits for, makes - /// room for what it finds past the probe's words, and returns its query words and whether it + /// Starts the next query this frame if none is in progress, skipping those nobody awaits. Makes + /// room for its results after the probe's words, and returns its query words and whether it /// reads depth. pub(super) fn next_query(&mut self) -> Option<([f32; 4], bool)> { if self.queries.running.is_some() || self.queries.reading.is_some() { @@ -195,7 +199,7 @@ impl View { Some((words, depth)) } - /// Records the running query's step over every mark it asks, and the copy of what it found. + /// Records the running query's step over every mark it covers, and the copy of its results. pub(super) fn run_query(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner) { let Some(Running { ask, found, words }) = self.queries.running.take() else { return }; let gpu = self.gpu.clone(); @@ -203,7 +207,7 @@ impl View { let resources = self.resources.as_mut().expect("created before queries"); let probe = resources.probe().clone(); let start = u64::from(wgsl::PROBE_WORDS) * 4; - // What it finds starts out empty: no hits, with room for those each mark keeps; no rows. + // Results start empty: for hits, a zero count and each mark's capacity; for a box, no rows. match &ask { Ask::Hits { .. } => { for f in &found { @@ -235,7 +239,7 @@ impl View { self.queries.reading = Some(Reading { ask, found, readback, drew: self.drew() }); } - /// Answers a query whose readback landed. + /// Answers a query whose readback has arrived. pub(super) fn settle_query(&mut self) { let Some(reading) = &self.queries.reading else { return }; let Some(bytes) = reading.readback.take() else { return }; @@ -253,7 +257,7 @@ impl View { }; match ask { Ask::Hits { point, radius, room, reply } => { - // A mark that found more than it kept asks again, with room for every hit it found. + // If a mark found more hits than it kept, run again with room for all of them. let most = found.iter().map(|f| words_at(&words, f.at, 1).first().copied().unwrap_or(0)).max(); match most.filter(|&most| most > room) { Some(room) => self.queries.waiting.push_front(Ask::Hits { point, radius, room, reply }), @@ -289,11 +293,11 @@ impl View { } } -/// The hits `words` hold of `found`, as `drew`, what their frame drew, names them. +/// The hits in `words` for `found`, decoded against `drew`, the frame they came from. fn hits(found: &[Found], words: &[u32], drew: &Drew) -> Vec { - // Nearest first, the mark drawn last first among equals, and a port before its marker; then - // by row and where it lies, so the order never depends on which hit landed first. Each row - // answers once, where it is nearest. + // Nearest first; among equals, the mark drawn last first, and a port before its marker; then + // by row and position, so the order never depends on which hit arrived first. Each row appears + // once, at its nearest hit. let mut kept: Vec<(usize, &[u32; wgsl::HIT_WORDS as usize])> = found .iter() .flat_map(|f| { @@ -302,7 +306,7 @@ fn hits(found: &[Found], words: &[u32], drew: &Drew) -> Vec { entries.as_chunks().0.iter().map(move |entry| (f.mark, entry)) }) .collect(); - // The last keys first, as each sort keeps the order of ties. + // Sort by the minor keys first: each sort is stable, so the last one keeps their order in ties. let key = |high: u32, low: u32| u128::from(crate::order::pair(high, low)); crate::order::by_wide_key(&mut kept, |&(_, entry)| key(entry[3], entry[4]) << 32 | u128::from(entry[5])); crate::order::by_wide_key(&mut kept, |&(i, entry)| key(entry[1], !(i as u32)) << 64 | key(!entry[2], entry[0])); @@ -329,7 +333,8 @@ fn hits(found: &[Found], words: &[u32], drew: &Drew) -> Vec { hits } -/// `len` words from word `at` of the probe's, in a query's answer read from past the probe's words. +/// `len` words from probe-buffer word `at`, in a query answer read back from past the probe's +/// words. fn words_at(words: &[u32], at: u32, len: u32) -> &[u32] { let start = (at - wgsl::PROBE_WORDS) as usize; words.get(start..start + len as usize).unwrap_or(&[]) diff --git a/crates/latkit/src/view/resources.rs b/crates/latkit/src/view/resources.rs index 1e8839ac..ef1333ba 100644 --- a/crates/latkit/src/view/resources.rs +++ b/crates/latkit/src/view/resources.rs @@ -1,10 +1,11 @@ -//! A view's buffers, textures, and bind groups. Grown buffers grow by half again; replacing one bumps -//! the generation, and bind groups are rebuilt when a generation they were built from changes. +//! A view's buffers, textures, and bind groups. A buffer that runs out of room is replaced by one +//! 1.5 times the size needed. Replacing a buffer increments the generation, and bind groups are +//! rebuilt when a generation they were built from changes. //! -//! Every shader's pass group (group 2) binds by one convention: 0 the pages (the data's, and what -//! marks keep per row), 2 extents, 4 domains, 5 probe words and what queries find, 6 depth, 7 sprites, -//! 8 the text atlas, 9 lists, 12 remap words, 13 labels, 15 icons, 16 the pages of rows each mark -//! evaluates, 17 the probe's step. +//! Every shader's pass group (group 2) uses the same bindings: 0 the arena pages (the data, and the +//! marks' per-row words), 2 extents, 4 domains, 5 probe words and query results, 6 depth, +//! 7 sprites, 8 the text atlas, 9 lists, 12 remap words, 13 labels, 15 icons, 16 the row blocks +//! each mark evaluates, 17 the probe step. use std::collections::HashMap; use std::num::NonZeroU64; @@ -23,7 +24,8 @@ pub(crate) fn step(step: u32) -> u32 { (u64::from(step) * MARK_STRIDE) as u32 } -/// The probe's steps, each at its own dynamic offset: a dispatch binds the one it takes. +/// A uniform buffer holding each probe step's code at its own dynamic offset. A dispatch binds the +/// offset of its step. fn steps(device: &wgpu::Device) -> wgpu::Buffer { use wgpu::util::DeviceExt as _; let mut contents = vec![0_u8; step(wgsl::STEPS) as usize]; @@ -38,11 +40,11 @@ fn steps(device: &wgpu::Device) -> wgpu::Buffer { }) } -/// What the labels buffer is used as: atomics placement works in, and a copy's source when the texts -/// placement asks for are read back. +/// The labels buffer's usages: storage for the placement atomics, and copy source for reading back +/// the texts placement requests. const LABELS: wgpu::BufferUsages = wgpu::BufferUsages::STORAGE.union(wgpu::BufferUsages::COPY_SRC); -/// A storage buffer that grows by half again when it runs out of room. +/// A buffer that is replaced by one 1.5 times the size needed when it runs out of room. struct Grown { buffer: wgpu::Buffer, label: &'static str, @@ -80,8 +82,8 @@ pub(crate) struct Groups { pub(crate) mark: wgpu::BindGroup, } -/// What a frame draws into: multisampled color resolved into the target when it multisamples, and -/// depth. +/// The textures a frame draws into: multisampled color (resolved into the target) when it +/// multisamples, and depth. struct Targets { color: Option, depth: wgpu::TextureView, @@ -97,24 +99,25 @@ pub(crate) struct Resources { program: Grown, /// The rows each mark's focus draw lights. lit: Grown, - /// The pages of rows each mark's evaluation covers this frame. + /// The row blocks each mark evaluates, and whose endpoints it resolves, this frame. blocks: Grown, remap: Grown, domains: Grown, extents: Grown, - /// The probe's words, then what the running query finds. + /// The probe's words, then the running query's results. probe: Grown, steps: wgpu::Buffer, sprites: Grown, lists: Grown, labels: wgpu::Buffer, - /// Changes whenever one of the buffers or textures above is replaced; and whenever the lists - /// buffer is, emptied. + /// Incremented whenever a buffer or texture above is replaced. `lists_generation` is also + /// incremented when the lists buffer is replaced, which empties it. generation: u64, lists_generation: u64, - /// Built from (arena, colormaps, icons, own) generations. + /// The shared bind groups, with the (arena, colormaps, icons, own) generations they were built + /// from. groups: Option<((u64, u64, u64, u64), Groups)>, - /// Pass groups, built at `passes_generation`: the arena's, the icons', and its own. + /// Pass groups by shader, built at `passes_generation`: the arena, icons, and own generations. passes: HashMap, passes_generation: (u64, u64, u64), targets: Option, @@ -128,7 +131,7 @@ impl Resources { frame: create(device, "latkit frame", uniform, size_of::() as u64), eye: create(device, "latkit eye", uniform, size_of::() as u64), marks: Grown::new(device, "latkit marks", uniform), - // A copy's source as it grows, keeping what it holds. + // A copy source, so growing keeps its contents. program: Grown::new(device, "latkit program", storage | wgpu::BufferUsages::COPY_SRC), lit: Grown::new(device, "latkit lit", storage), blocks: Grown::new(device, "latkit blocks", storage), @@ -153,7 +156,7 @@ impl Resources { } } - /// Writes the pages of rows every mark's evaluation covers this frame. + /// Writes this frame's row-block lists for every mark. pub(crate) fn write_blocks(&mut self, device: &wgpu::Device, queue: &Queue, blocks: &[u32]) { if self.blocks.ensure(device, (blocks.len() as u64 + 1) * 4) { self.generation += 1; @@ -201,8 +204,8 @@ impl Resources { replaced } - /// Makes room for `words` of program keeping its first `kept`: a buffer replaced has them copied - /// into the new one in `encoder`, so what is written into it this frame follows the copy. True when + /// Makes room for `words` of program, keeping its first `kept` words. A replaced buffer has + /// them copied over in `encoder`, so this frame's writes must follow the copy. Returns whether /// it was replaced. pub(crate) fn grow_program( &mut self, @@ -222,7 +225,24 @@ impl Resources { true } - /// Makes room for `words` probe words, what the running query finds included. + /// Copies `words` program words from word `from` to word `to` in `encoder`, through a temporary + /// buffer, since a buffer cannot be copied into itself. + pub(crate) fn move_program( + &self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + from: u32, + to: u32, + words: u32, + ) { + let size = u64::from(words) * 4; + let between = create(device, "latkit program move", wgpu::BufferUsages::COPY_SRC, size); + let program = &self.program.buffer; + encoder.copy_buffer_to_buffer(program, u64::from(from) * 4, &between, 0, size); + encoder.copy_buffer_to_buffer(&between, 0, program, u64::from(to) * 4, size); + } + + /// Makes room for `words` probe words, including the running query's results. pub(crate) fn fit_probe(&mut self, device: &wgpu::Device, words: u32) { if self.probe.ensure(device, u64::from(words.max(wgsl::PROBE_WORDS)) * 4) { self.generation += 1; @@ -236,8 +256,8 @@ impl Resources { } } - /// Makes the lists buffer hold `words`; replacing it, emptied, changes the lists generation, so - /// every list counts again. + /// Makes room for `words` in the lists buffer. Replacing it empties it and changes the lists + /// generation, so every list is counted again. pub(crate) fn fit_lists(&mut self, device: &wgpu::Device, words: u64) { if self.lists.ensure(device, (words + 1) * 4) { self.generation += 1; @@ -287,7 +307,8 @@ impl Resources { &self.sprites.buffer } - /// Changes whenever the lists buffer is replaced, emptied, so every list counts again. + /// Changes whenever the lists buffer is replaced (which empties it), so every list is counted + /// again. pub(crate) fn lists_generation(&self) -> u64 { self.lists_generation } @@ -333,7 +354,7 @@ impl Resources { groups } - /// `shader`'s pass group, binding what its WGSL declares. + /// `shader`'s pass group, cached until a generation it binds changes. pub(crate) fn pass(&mut self, device: &wgpu::Device, inner: &mut Inner, shader: Shader) -> wgpu::BindGroup { let generation = (inner.arena.generation, inner.icons.generation, self.generation); if self.passes_generation != generation { @@ -348,7 +369,7 @@ impl Resources { group } - /// `shader`'s pass group, binding what its WGSL declares. + /// Builds `shader`'s pass group from the bindings its WGSL declares. fn bind(&self, device: &wgpu::Device, inner: &mut Inner, shader: Shader) -> wgpu::BindGroup { let layout = inner.pipelines.pass_layout(device, shader); let entries: Vec> = shader @@ -388,8 +409,8 @@ impl Resources { }) } - /// The color and depth a frame of `size` and `format` draws into with `samples` per pixel: color - /// is `None` without multisampling, when marks draw straight into the target. + /// The color and depth textures for a frame of `size` and `format` at `samples` per pixel. + /// Color is `None` without multisampling, when marks draw straight into the target. pub(crate) fn targets( &mut self, device: &wgpu::Device, diff --git a/crates/latkit/src/view/rig.rs b/crates/latkit/src/view/rig.rs index 934b9640..88b6e148 100644 --- a/crates/latkit/src/view/rig.rs +++ b/crates/latkit/src/view/rig.rs @@ -1,32 +1,33 @@ -//! How the camera moves. A gesture moves it at once, about the point under the pointer. The wheel, -//! the keys, and a released drag leave motion owed about such a point, which the next frames pay out -//! exponentially, so a fling coasts and a wheel notch glides by the same rule. A flight eases over a -//! set time, an orbit turns steadily, and what a fit framed bounds how far the rest can take it. All -//! of it moves by the view's clock. +//! Camera motion. A gesture moves the camera at once, about the point under the pointer. The wheel, +//! the keys, and a released drag add pending motion about such a point, which the following frames +//! apply with exponential decay, so a fling coasts and a wheel notch glides by the same rule. A +//! flight eases over a set time, and an orbit turns steadily. The bounds of the last fit limit how +//! far gestures and pending motion can move the camera. All of it runs on the view's clock. use super::camera::{Bounds, Camera}; use super::{Event, View}; -/// Seconds over which owed motion pays out: a fling's pan and turn coast longer than a zoom glides. +/// Time constants, in seconds, for applying pending motion: a fling's pan and turn coast longer +/// than a zoom glides. const COAST: f64 = 0.3; const GLIDE: f64 = 0.1; #[derive(Default)] pub(crate) struct Rig { - /// The point owed motion moves about. + /// The point pending motion moves about. anchor: [f64; 3], - /// Motion owed: CSS pixels of pan, degrees of bearing and pitch, levels of zoom. + /// Pending motion: CSS pixels of pan, degrees of bearing and pitch, and levels of zoom. pan: [f64; 2], turn: [f64; 2], zoom: f64, flight: Option, /// Degrees of bearing per second. pub(crate) orbit: f64, - /// What the last fit framed. + /// The bounds the last fit framed. pub(crate) bounds: Option, } -/// A flight from one camera to another, begun at a time on the view's clock. +/// A flight from one camera to another, started at `start` on the view's clock. struct Flight { from: Camera, to: Camera, @@ -35,7 +36,7 @@ struct Flight { } impl Rig { - /// Whether the camera eases toward where it goes: a flight, or motion owed. An orbit never settles. + /// Whether the camera is easing to a rest: a flight or pending motion. An orbit never settles. pub(crate) fn easing(&self) -> bool { self.flight.is_some() || self.owed() } @@ -44,7 +45,7 @@ impl Rig { self.pan != [0.0; 2] || self.turn != [0.0; 2] || self.zoom != 0.0 } - /// Drops motion owed and any flight: a gesture takes the camera. + /// Drops pending motion and any flight, as when a gesture takes the camera. pub(crate) fn hold(&mut self) { self.pan = [0.0; 2]; self.turn = [0.0; 2]; @@ -52,7 +53,8 @@ impl Rig { self.flight = None; } - /// Owes `pan`, `turn`, and `zoom` more about `anchor`. + /// Adds `pan`, `turn`, and `zoom` to the pending motion, now about `anchor`, and cancels any + /// flight. pub(crate) fn owe(&mut self, anchor: [f64; 3], pan: [f64; 2], turn: [f64; 2], zoom: f64) { self.flight = None; self.anchor = anchor; @@ -61,7 +63,8 @@ impl Rig { self.zoom += zoom; } - /// Owes what a drag released at `velocity` coasts: its velocity over the coasting time. + /// Adds the motion a drag released with velocities `pan` and `turn` coasts: each velocity times + /// the coasting time constant. pub(crate) fn fling(&mut self, anchor: [f64; 3], pan: [f64; 2], turn: [f64; 2]) { self.owe(anchor, pan.map(|v| v * COAST), turn.map(|v| v * COAST), 0.0); } @@ -74,12 +77,13 @@ impl Rig { } impl View { - /// The zooms gestures and the rig keep the camera within: what the last fit framed allows. + /// The zoom range that gestures and pending motion keep the camera within, from the last fit's + /// bounds. pub(super) fn zoom_limits(&self) -> Option<[f64; 2]> { self.rig.bounds.map(|bounds| bounds.zooms()) } - /// Moves the camera by a gesture or the rig, kept within reach of what was fitted. + /// Moves the camera for a gesture or pending motion, kept within the last fit's bounds. pub(super) fn steer(&mut self, camera: Camera) { let camera = match &self.rig.bounds { Some(bounds) => camera.within(bounds), @@ -88,8 +92,8 @@ impl View { self.move_camera(camera); } - /// Pays out the share of motion owed for the `dt` seconds the clock moved, advances a flight, - /// and orbits. + /// Applies the share of pending motion due for the `dt` seconds the clock moved, advances a + /// flight, and orbits. pub(super) fn advance(&mut self, dt: f64) { if let Some(flight) = &self.rig.flight { let eased = self.clock.eased(flight.start, flight.seconds); @@ -103,7 +107,7 @@ impl View { return; } if self.rig.owed() { - // Without motion of its own, what is owed is paid at once. + // When motion is disabled, pending motion applies at once. let moves = self.moves(); let coast = if moves { 1.0 - (-dt / COAST).exp() } else { 1.0 }; let glide = if moves { 1.0 - (-dt / GLIDE).exp() } else { 1.0 }; @@ -128,14 +132,15 @@ impl View { } } - /// Moves the camera by a gesture, a fit, or a motion over frames, telling the application. + /// Moves the camera for a gesture, a fit, or an animation, and emits a `Camera` event. pub(super) fn move_camera(&mut self, camera: Camera) { if self.place_camera(camera) { self.events.tell(Event::Camera(camera)); } } - /// Moves the camera, evaluating again what reads it; whether it moved. + /// Moves the camera and dirties the channels that read the camera or the pointer. Returns + /// whether it moved. pub(super) fn place_camera(&mut self, camera: Camera) -> bool { if camera == self.camera { return false; diff --git a/crates/latkit/src/view/sync.rs b/crates/latkit/src/view/sync.rs index d5f70ac5..4947f3db 100644 --- a/crates/latkit/src/view/sync.rs +++ b/crates/latkit/src/view/sync.rs @@ -1,25 +1,25 @@ -//! A mark's columns on the GPU: for every column its program reads, a descriptor and a page table of -//! arena offsets, or for sampled columns a cursor per page of rows at the shown coordinate. +//! A mark's columns on the GPU. For every column its program reads: a descriptor and a page table +//! of arena offsets, or for a sampled column, a cursor per page of rows at the shown coordinate. //! -//! A table drawn as it is pages its rows as its data does, and its columns read the data's pages. A -//! table drawn in part pages its rows as its marks hold them, a page of its order in each slot, and -//! its columns read pages packed in that order, kept on the GPU by what they were packed from. A -//! reference names the rows of its table where they draw. +//! An unpaged table's rows are paged as its data is, and its columns read the data's pages. A paged +//! table's rows are paged by slot, one page of its order per slot, and its columns read pages +//! packed in that order, cached on the GPU by what they were packed from. A reference column to a +//! paged table holds the referenced rows' places in that table's order. //! -//! A column a line traces holds what is in view instead: runs of entries, frames while there are at -//! most two per pixel column, else the buckets of the level of detail that fit, and where those are -//! not held, the coarser ones standing in or the finer ones there are. Each level it draws lists its -//! axis pages and, per page of rows, its pages of values. +//! A column a line traces holds only what is in view: runs of entries, as raw frames while there +//! are at most two per pixel column, else as buckets of the level of detail that fits. Where the +//! samples lack those, coarser buckets are shown as placeholders, or else the finer ones available. +//! Each level drawn lists its axis pages and, per page of rows, its pages of values. //! -//! Only columns whose inputs changed are rebuilt, once each: moving the shown coordinate rebuilds -//! sampled columns' cursors and nothing else, and a table drawn in part rebuilds only where the pages -//! its marks hold changed. Rebuilt tables are staged with every other mark's of their dataset: the -//! dataset reserves every page they read at once, or none and waits for room, and once every page -//! landed the view swaps them all in, so a data version, and what a camera move draws, appear whole. -//! Staged tables are not built again while they wait, only where what they read changes again. A -//! rebuilt column that lists pages says which pages of rows hold pages that changed, so only their rows -//! evaluate again. While a mark eases, the table its geometry column had when the transition began -//! stays, packed after the rest, so paths morph from it. +//! Only columns whose inputs changed are rebuilt, once each. Moving the shown coordinate rebuilds +//! only sampled columns' cursors, and a paged table rebuilds only where its marks' resident pages +//! changed. Rebuilt tables are staged together with those of every other mark of the dataset. The +//! dataset reserves every page they read at once, or none and waits for room. Once every page has +//! arrived, the view swaps them all in, so a data version, and what a camera move draws, appear +//! complete. Staged tables are rebuilt while they wait only if their inputs change again. A rebuilt +//! column that lists pages reports which pages of rows read changed pages, so only those rows +//! evaluate again. While a mark eases, its geometry column keeps the table it had when the +//! transition began, packed after the rest, so paths morph from it. use std::ops::Range; use std::sync::Arc; @@ -40,19 +40,20 @@ use crate::{Name, Snapshot, Table}; const WORDS: usize = wgsl::COLUMN_WORDS as usize; -/// What one column's table was built from; a change in it means rebuilding the table. +/// The inputs one column's table was built from. When they change, the table is rebuilt. #[derive(Clone, PartialEq, Debug)] enum Stamp { Missing, - /// A field, and the order the table a reference of it names draws in. + /// A field's version, and the id of the order it depends on: a reference's target table's + /// order, or the order paged shapes were made in; else 0. Field(Version, u64), Sampled(Version, Option, u32), Trace(Version, Plan), Join(Version, Version), } -/// What a trace shows: the window across, the pixel columns it spans, and the steps a line draws -/// between frames (`STEP_BEFORE` or `STEP_AFTER`, or 0). +/// What a trace shows: the axis window, the pixel columns it spans, and how a line steps between +/// frames (`STEP_BEFORE`, `STEP_AFTER`, or 0). #[derive(Clone, Copy, PartialEq, Debug)] pub(crate) struct Window { pub(crate) across: Option<[f64; 2]>, @@ -60,8 +61,8 @@ pub(crate) struct Window { pub(crate) steps: u32, } -/// What a traced column draws: runs of entries left to right, each with the point it starts at; the -/// level the window asks for; and the points the trace runs through in all. +/// What a traced column draws: runs of entries left to right, each with its first point; the level +/// of detail the window needs; and the total point count. #[derive(Clone, PartialEq, Eq, Debug)] pub(crate) struct Plan { pub(crate) level: u32, @@ -71,8 +72,8 @@ pub(crate) struct Plan { } impl Plan { - /// The entry point `point` draws: its level, its index, and its side, a bucket's least (0) or - /// greatest (1); `None` for a point that breaks the line. + /// The entry that point `point` draws: its level, its index, and its side, a bucket's least (0) + /// or greatest (1). `None` for a point that breaks the line. pub(crate) fn entry(&self, point: u32) -> Option<(u32, u64, usize)> { let i = self.runs.partition_point(|(_, first)| *first <= point).checked_sub(1)?; let (run, first) = &self.runs[i]; @@ -89,8 +90,8 @@ impl Plan { } } -/// How a mark's columns of its own table page its rows: a page of the data at a time, or a page of -/// the order its drawing holds in each slot. +/// How a mark's columns of its own table page its rows: by the data's pages, or by its drawing's +/// slots, one page of the order per slot. #[derive(Clone, Copy)] pub(crate) enum Rows<'a> { Own, @@ -98,7 +99,7 @@ pub(crate) enum Rows<'a> { } impl Rows<'_> { - /// The stamp of how it pages rows: none for the data's own pages. + /// A stamp of how it pages rows: 0 for the data's own pages. fn stamp(self) -> u64 { match self { Rows::Own => 0, @@ -107,30 +108,32 @@ impl Rows<'_> { } } -/// One column on the GPU: its descriptor, whose table offsets are relative to `words`, its page table -/// or cursors, and the pages it reads: until its dataset reserves them, what it asks the arena for, -/// each with the word it lands at; then the pages it pins, and the words still waiting for a page. +/// One column on the GPU: its descriptor (whose table offsets are relative to `words`), its page +/// table or cursors in `words`, and the pages it reads. Until its dataset reserves them, `asks` +/// lists the pages it needs, each with the word that receives its offset. After that, `pins` lists +/// its pinned pages, and `missing` the words still waiting for an offset. struct ColumnTable { stamp: Stamp, - /// How it pages its rows: 0 for the data's pages, else the stamp of the pages held. + /// How it pages its rows: 0 for the data's pages, else the stamp of its drawing's slots. held: u64, descriptor: [u32; WORDS], words: Vec, asks: Vec<(usize, Ask)>, pins: Vec, - /// Words waiting for a page's offset: the word, the page, and the words it must hold. + /// Words waiting for a page's offset: the word, the page, and the words the page must hold. missing: Vec<(usize, PageKey, u32)>, /// What a traced column draws. plan: Option, - /// At most how many points a geometry column's paths take. + /// The most points a geometry column's paths take. points: u32, - /// The page of each page of rows the table lists, for a column whose rows read their own page; - /// `None` for one whose every row changes when it does (cursors, traces, joins). + /// For a column whose rows each read their own page, the page each page of rows reads. `None` + /// where any change affects every row (cursors, traces, joins). pages: Option>>, } impl ColumnTable { - /// Takes the offsets of the pages it waits for that landed; whether none waits now. + /// Fills in the offsets of waiting pages that have arrived. Returns whether none is still + /// waiting. fn resolve(&mut self, gpu: &Inner) -> bool { let words = &mut self.words; self.missing.retain(|&(at, key, need)| match gpu.arena.offset(key, need) { @@ -151,7 +154,7 @@ struct Join { pages: Vec<(PageId, Arc<[u32]>)>, } -/// A mark's tables rebuilt, by index into its current ones; and whether every table was rebuilt, +/// A mark's rebuilt tables, by index into its current ones, and whether every table was rebuilt, /// which replaces them all. pub(crate) struct Rebuilt { tables: Vec<(usize, ColumnTable)>, @@ -159,14 +162,15 @@ pub(crate) struct Rebuilt { } impl Rebuilt { - /// Lets go of the pages its tables pin. + /// Unpins its tables' pages. fn release(self, gpu: &mut Inner) { for (_, table) in self.tables { unpin(&table.pins, gpu); } } - /// Whether every table holds every page it reads, taking the offsets of those that landed. + /// Whether every page each table reads is resident, filling in the offsets of those that + /// arrived. fn ready(&mut self, gpu: &Inner) -> bool { let mut ready = true; for (_, table) in &mut self.tables { @@ -176,21 +180,21 @@ impl Rebuilt { } } -/// What replaces what a view draws of a dataset: its marks' tables rebuilt since the dataset last -/// published, by mark. They wait until every page they read landed. +/// The tables rebuilt for a dataset's marks since the dataset last published, by mark. They replace +/// the current tables once every page they read has arrived. #[derive(Default)] pub(crate) struct Staged { marks: Vec<(usize, Rebuilt)>, } impl Staged { - /// Mark `mark`'s tables staged, if any. + /// Mark `mark`'s staged tables, if any. pub(crate) fn of(&self, mark: usize) -> Option<&Rebuilt> { self.marks.iter().find(|(m, _)| *m == mark).map(|(_, r)| r) } - /// Stages mark `mark`'s tables `rebuilt`: tables rebuilt again replace those staged, whose pins - /// go; every table rebuilt replaces them all. + /// Stages mark `mark`'s `rebuilt` tables. A table rebuilt again replaces its staged version, + /// which is unpinned. A full rebuild replaces all of the mark's staged tables. pub(crate) fn put(&mut self, mark: usize, rebuilt: Rebuilt, gpu: &mut Inner) { let Some(at) = self.marks.iter().position(|(m, _)| *m == mark) else { self.marks.push((mark, rebuilt)); @@ -213,9 +217,9 @@ impl Staged { } } - /// Reserves every page the tables staged ask for, all or none: `false` where the arena has no room - /// for them now. Tables asking for pages the arena no longer holds, which they did not make, are - /// dropped, to be built again. + /// Reserves every page the staged tables need, all or none. Returns `false` when the arena has + /// no room for them now. A table that needs a page the arena has since dropped, and has no + /// content to upload for it, is discarded and rebuilt later. pub(crate) fn reserve(&mut self, gpu: &mut Inner, device: &wgpu::Device, queue: &crate::gpu::work::Queue) -> bool { let gone = |table: &ColumnTable| { table.asks.iter().any(|(_, a)| a.content.is_none() && !gpu.arena.holds(a.key, a.need)) @@ -249,25 +253,26 @@ impl Staged { reserved } - /// Whether every table staged holds every page it reads, taking the offsets of those that landed. + /// Whether every page each staged table reads is resident, filling in the offsets of those that + /// arrived. pub(crate) fn ready(&mut self, gpu: &Inner) -> bool { self.marks.iter_mut().fold(true, |ready, (_, rebuilt)| rebuilt.ready(gpu) && ready) } - /// Takes mark `mark`'s tables staged, to publish them. + /// Removes and returns mark `mark`'s staged tables, to publish them. pub(crate) fn take(&mut self, mark: usize) -> Option { let at = self.marks.iter().position(|(m, _)| *m == mark)?; Some(self.marks.swap_remove(at).1) } - /// Lets go of mark `mark`'s tables staged: it compiled again, or went. + /// Drops mark `mark`'s staged tables, after it recompiled or was removed. pub(crate) fn forget(&mut self, mark: usize, gpu: &mut Inner) { if let Some(rebuilt) = self.take(mark) { rebuilt.release(gpu); } } - /// Lets go of every table staged. + /// Unpins every staged table. pub(crate) fn release(self, gpu: &mut Inner) { for (_, rebuilt) in self.marks { rebuilt.release(gpu); @@ -279,7 +284,7 @@ impl Staged { } } -/// What a mark's program reads, column by column: the program's columns, then one per join. +/// The column tables a mark's program reads: one per program column, then one per join. #[derive(Default)] pub(crate) struct Tables { current: Vec, @@ -287,23 +292,24 @@ pub(crate) struct Tables { frozen: Option, joins: Vec, synced: bool, - /// The rows of the table last synced, which a program reading no columns follows alone. + /// The row structure of the table last synced. A program that reads no columns tracks only + /// this. structure: Option, - /// Bumps whenever a table is swapped in. + /// Incremented whenever a table is swapped in. generation: u64, } -/// What publishing rebuilt tables changed: bit `i` of `columns` for each program column whose table -/// changed, of `data` for each whose data did, not only where its rows are held; and the row blocks -/// whose pages changed, `None` when every row did. +/// What publishing rebuilt tables changed. Bit `i` of `columns` is set for each program column +/// whose table changed, and of `data` for each whose data changed, not only its paging. `blocks` +/// lists the row blocks whose pages changed; `None` when every row changed. pub(crate) struct Published { pub(crate) columns: u64, pub(crate) data: u64, pub(crate) blocks: Option>, } -/// Where a mark's own rows come from, the tables the view derives beside them, and drawn in part, the -/// shapes it makes a page of their order at a time, by the field each is written as. +/// A mark's own dataset and table, the tables the view derives beside it, and for a paged table, +/// the shapes made one page of its order at a time, by the field each is written as. pub(crate) struct Source<'a> { pub(crate) data: &'a Snapshot, pub(crate) table: &'a Table, @@ -312,14 +318,14 @@ pub(crate) struct Source<'a> { } impl<'a> Source<'a> { - /// The shapes written as field `name`, made a page of the order drawn at a time. + /// The shapes written as field `name`, made one page of the drawn order at a time. fn paged(&self, name: &Name) -> Option<&'a PagedShapes> { self.paged.iter().find(|(field, _)| field == name).map(|&(_, shapes)| shapes) } } -/// What a sync reads besides the mark's own rows: every dataset, the coordinate shown, the window a -/// trace spans, and the order each table of its dataset drawn in part draws in. +/// What a sync reads besides the mark's own rows: every dataset, the shown coordinate, the trace +/// window, and the order of each paged table in the mark's dataset. pub(crate) struct Inputs<'a> { pub(crate) all: &'a [Snapshot], pub(crate) at: f64, @@ -328,7 +334,7 @@ pub(crate) struct Inputs<'a> { } impl Inputs<'_> { - /// The order table `table` of the mark's dataset draws in, when drawn in part. + /// The order of table `table` in the mark's dataset, if that table is paged. fn order_of(&self, table: &Name) -> Option<&Order> { self.orders.iter().find(|(name, _)| name == table).map(|(_, order)| order) } @@ -341,9 +347,9 @@ enum Use<'a> { } impl Tables { - /// The tables of what `source` paged as `rows` reads, with what `inputs` reads, whose inputs changed - /// since `staged`, the tables staged to replace these, or else since the current ones: built once - /// each, asking for their pages. `None` where none changed. + /// Rebuilds the tables whose inputs changed, for `source` paged as `rows`, with `inputs`. + /// Changes are measured against the `staged` tables if any, else the current ones. Each table + /// is built once and requests its pages. `None` when nothing changed. pub(crate) fn rebuild( &mut self, program: &Program, @@ -361,8 +367,8 @@ impl Tables { Use::Field { column, join } }); let uses: Vec> = fields.chain(joins.iter().map(|from| Use::Join(from))).collect(); - // Every table rebuilds where they have no tables of this shape yet, but where a reshape is - // staged already: the tables are compared with it. + // Rebuild every table when there are no tables of this shape yet, unless a reshape is + // already staged; then compare against it. let reshaped = staged.is_some_and(|s| s.reshape && s.tables.len() == uses.len()); let reshape = !reshaped && (!self.synced @@ -381,7 +387,7 @@ impl Tables { None => (Stamp::Missing, held), }, }; - // What replaces table `i` once ready, else what draws. + // The staged replacement for table `i` if any, else the current table. let waiting = staged.and_then(|s| s.tables.iter().find(|(j, _)| *j == i)); let latest = waiting.map(|(_, t)| t).or(self.current.get(i)); if !reshape && latest.is_some_and(|t| t.stamp == stamp && t.held == paged) { @@ -407,8 +413,9 @@ impl Tables { (reshape || !rebuilt.is_empty()).then_some(Rebuilt { tables: rebuilt, reshape }) } - /// Swaps in the tables `rebuilt`, every page they read resident: the current ones' pins go, but - /// the geometry column's, `geometry`, which stays frozen while the mark eases from it. + /// Swaps in the `rebuilt` tables, whose pages are all resident, and unpins the replaced ones. + /// The replaced table of geometry column `geometry` stays frozen instead, for the mark to ease + /// from. pub(crate) fn publish(&mut self, rebuilt: Rebuilt, program: &Program, geometry: u32, gpu: &mut Inner) -> Published { let Rebuilt { tables: rebuilt, reshape } = rebuilt; self.generation += 1; @@ -463,17 +470,17 @@ impl Tables { } } - /// The program column the frozen geometry table packs as, once there is one. + /// The column index the frozen geometry table is packed at, if there is one. pub(crate) fn frozen(&self) -> Option { self.frozen.as_ref().map(|_| self.current.len() as u32) } - /// Bumps whenever a table is swapped in. + /// Incremented whenever a table is swapped in. pub(crate) fn generation(&self) -> u64 { self.generation } - /// At most how many points the paths of geometry column `column` take; 0 for other columns. + /// The most points the paths of geometry column `column` take; 0 for other columns. pub(crate) fn points(&self, column: u32) -> u32 { self.current.get(column as usize).map_or(0, |t| t.points) } @@ -508,13 +515,13 @@ impl Tables { self.current.iter().chain(&self.frozen) } - /// The pages the tables drawn pin. + /// The pages the current and frozen tables pin. pub(crate) fn pins(&self) -> impl Iterator + '_ { self.packed().flat_map(|t| t.pins.iter().copied()) } - /// Appends every descriptor, then every table; `base` is the word of the program buffer the - /// first descriptor lands at, so table offsets are absolute. + /// Appends every descriptor, then every table. `base` is the program-buffer word of the first + /// descriptor, which makes table offsets absolute. pub(crate) fn pack(&self, base: u32, out: &mut Vec) { let mut at = base + (self.packed().count() * WORDS) as u32; for table in self.packed() { @@ -531,7 +538,7 @@ impl Tables { } } - /// Releases every pin. + /// Unpins every table and marks the tables unsynced. pub(crate) fn release(&mut self, gpu: &mut Inner) { self.thaw(gpu); for table in self.current.drain(..) { @@ -566,8 +573,8 @@ impl Tables { } } -/// The pages of rows whose pages differ between table `before` and table `now`; `None` when either -/// reads no page alone, so every row changed. +/// The pages of rows whose page differs between table `before` and table `now`. `None` when either +/// table has no page list (see `ColumnTable::pages`), so every row changed. fn changed_blocks(before: &ColumnTable, now: &ColumnTable) -> Option> { let (Some(old), Some(new)) = (&before.pages, &now.pages) else { return None }; Some((0..old.len().max(new.len())).filter(|&p| old.get(p) != new.get(p)).map(|p| p as u32).collect()) @@ -626,8 +633,8 @@ fn joined<'a>((data, table): &(Name, Name), all: &'a [Snapshot]) -> Option<&'a T all.iter().find(|d| d.name() == data)?.table(table) } -/// What a trace across `window` draws: the runs covering its frames at the level they ask for, a -/// point left out between runs that do not meet. +/// What a trace across `window` draws: the runs covering its frames at the level of detail they +/// need, with one point left out between runs that do not meet, which breaks the line. fn plan(samples: &Samples, window: Window) -> Plan { let frames = window.across.map_or(samples.start()..samples.end(), |across| samples.across(across)); let level = detail(frames.end - frames.start, window.columns); @@ -671,13 +678,13 @@ fn stamp(column: &ColumnUse, source: &Source<'_>, inputs: &Inputs<'_>) -> Stamp } } -/// Builds one column's table, asking for each page it reads. +/// Builds one column's table, requesting each page it reads. struct Builder<'g> { words: Vec, asks: Vec<(usize, Ask)>, plan: Option, points: u32, - /// The page of each page of rows, while every row reads its own page's. + /// The page each page of rows reads, while every row reads only its own page. pages: Option>>, gpu: &'g mut Inner, } @@ -692,22 +699,23 @@ impl<'g> Builder<'g> { ColumnTable { stamp, held, descriptor, words, asks, pins: Vec::new(), missing: Vec::new(), plan, points, pages } } - /// Asks for page `key`, holding `need` words, to land at word `at` once it does; until then - /// `NONE`, and the table waits for it. `content` makes what it uploads, where the arena lacks it. + /// Requests page `key`, holding `need` words, whose offset goes to word `at` once it arrives. + /// Until then the word is `NONE` and the table waits. `content` builds the upload when the + /// arena lacks the page. fn place(&mut self, at: usize, key: PageKey, need: u32, content: impl FnOnce() -> Content) { let content = (!self.gpu.arena.holds(key, need)).then(content); self.asks.push((at, Ask { key, need, content })); self.words[at] = wgsl::NONE; } - /// Appends where page `key` lies, as [`place`](Builder::place) writes it. + /// Appends a word for page `key`'s offset, which [`place`](Builder::place) fills. fn push(&mut self, key: PageKey, need: u32, content: impl FnOnce() -> Content) { self.words.push(wgsl::NONE); let at = self.words.len() - 1; self.place(at, key, need, content); } - /// Lists page `page` as the one a page of rows reads. + /// Records `page` as the page the next page of rows reads. fn list(&mut self, page: Option) { if let Some(pages) = &mut self.pages { pages.push(page); @@ -762,7 +770,7 @@ impl<'g> Builder<'g> { for page in values.pages() { self.list(Some(page.id)); let key = PageKey::of(page.id, Words::Codes, false); - // Coded once, whether or not the page is on the GPU, so the dictionary holds its texts. + // Each page is coded once, resident or not, so the dictionary holds its texts. let codes = self.gpu.dictionaries.field(field.0, field.1, field.2).codes(page); self.push(key, page.values.len() as u32, || Content::Computed(codes.to_vec())); } @@ -779,7 +787,7 @@ impl<'g> Builder<'g> { Some(Field::Reference(reference)) => { reference.rows.pages().iter().for_each(|page| self.list(Some(page.id))); match inputs.order_of(&reference.to).filter(|_| column.from.is_none()) { - // The rows it names where their table draws them. + // The referenced rows' places in their table's order. Some(order) => { for (p, page) in reference.rows.pages().iter().enumerate() { let id = self.gpu.packed.ranked(page.id, order.id()); @@ -811,8 +819,8 @@ impl<'g> Builder<'g> { d } - /// A column of a table drawn in part: a page packed in its order for each page its drawing holds, - /// as `packing` keeps them. + /// A paged table's column: one page packed in the table's order per slot, cached under + /// `packing`. #[allow(clippy::too_many_arguments, reason = "one column's pages in its order")] fn held( &mut self, @@ -874,7 +882,7 @@ impl<'g> Builder<'g> { let frame = samples.frame_at(inputs.at); let t = if column.step { 0.0 } else { frame.map_or(0.0, |f| samples.t(f, inputs.at)) }; d[wgsl::COLUMN_T as usize] = t.to_bits(); - // Six words a page: this frame's block and word, the next frame's, and their rows. + // Six words per slot, laid out as `cursors` writes them. for s in 0..slots { let at = self.words.len(); self.words.extend([wgsl::NONE, 0, wgsl::NONE, 0, 0, 0]); @@ -899,9 +907,9 @@ impl<'g> Builder<'g> { } } - /// Appends the page of `packing` each slot of `drawing` holds, packed from `source`, made by `pack` - /// from its rows the first time; nothing for a free slot. The pages of the order are found in one - /// look at the column, each page of rows kept on its own. + /// Appends the page of `packing` in each slot of `drawing`, packed from `source` by `pack` the + /// first time it is needed; `NONE` for a free slot. The order's pages are looked up in one pass + /// over the column, and each page of kept rows separately. fn held_pages( &mut self, drawing: &Drawing, @@ -941,7 +949,8 @@ impl<'g> Builder<'g> { } } - /// A join's table: a bits column of the mark's rows, the rows of the other table with their ids. + /// A join's table: a bits column holding, for each of the mark's rows, the row of the other + /// table with the same id. fn join( &mut self, join: &Join, @@ -986,10 +995,9 @@ impl<'g> Builder<'g> { } } - /// A geometry column's table, as `lib/read.wesl` reads it: per page, the block of each row's - /// first point, where the page's blocks of points are listed in the table, how many points it - /// has, and how many rows; then those lists. A page's points take as many blocks of a chunk as - /// they need. + /// A geometry column's table, in the layout `lib/read.wesl` reads: per page, the block of its + /// row table (each row's first point), where its blocks of points are listed, its point count, + /// and its row count; then those lists. A page's points take as many blocks as they need. fn path(&mut self, column: &Column) { let pages = column.pages(); self.words.resize(wgsl::PATH_PAGE_WORDS as usize * pages.len(), 0); @@ -999,8 +1007,7 @@ impl<'g> Builder<'g> { } } - /// The geometry column of a table drawn in part: a page of its points per page its drawing holds, - /// as `packing` keeps them. + /// A paged table's geometry column: one page of points per slot, cached under `packing`. fn held_path(&mut self, column: &Column, drawing: &Drawing, packing: &Packing) { let per = wgsl::PATH_PAGE_WORDS as usize; self.words.resize(per * drawing.slot_count(), 0); @@ -1021,9 +1028,9 @@ impl<'g> Builder<'g> { } } - /// The shapes of a table drawn in part, made a page of its order at a time: those of each page its - /// drawing holds, and of its rows kept, a row at a time, in pages `packing` keeps by what they - /// hold. A page not shaped yet draws nothing until it is. + /// A paged table's derived shapes, made one page of its order at a time: those of each resident + /// page, and of its kept rows individually, in pages cached under `packing` by their contents. + /// A page draws nothing until its shapes are made. fn shaped(&mut self, shapes: &PagedShapes, drawing: &Drawing, packing: &Packing) { let per = wgsl::PATH_PAGE_WORDS as usize; self.words.resize(per * drawing.slot_count(), 0); @@ -1065,10 +1072,10 @@ impl<'g> Builder<'g> { } } - /// Page `p` of a geometry column's table, `id`, of `rows` rows: its rows' table, then its pieces of - /// points, each keyed by what it holds, so a page written again uploads only the pieces whose - /// rows' points changed. `pieces` makes them where the GPU lacks one, and `points` counts its - /// points where the page's pieces are not kept. + /// Page `p` (`id`, with `rows` rows) of a geometry column's table: its row table, then its + /// pieces of points, each keyed by its contents, so a rewritten page uploads only the pieces + /// whose points changed. `pieces` builds them when the GPU lacks one, and `points` counts the + /// page's points when its pieces are not cached. fn path_page( &mut self, p: usize, @@ -1104,17 +1111,21 @@ impl<'g> Builder<'g> { self.words[per * p + 3] = rows; } - /// What a line draws, laid out as `lib/trace.wesl` reads it: a header (how many runs, pages of - /// rows, and levels, and the points in all); per run its level's place, first entry, entries, - /// and first point; per level the level, how many axis pages and where they are listed, and - /// where its pages' lists start; per level and page of rows how many pages of frames or least - /// values and where they are listed, then of greatest values; then those lists, axis pages as - /// (offset, first entry) and pages of values as (offset, first entry, words per entry). Offsets - /// within the table are relative to it. `blocks` are the samples each page of rows reads, and its - /// block among theirs. + /// What a line draws, in the layout `lib/trace.wesl` reads: + /// + /// - a header: the run count, pages of rows, level count, and total points; + /// - per run: its level's index, first entry, entry count, and first point; + /// - per level: the level, its axis page count and list offset, and its page lists' start; + /// - per level and page of rows: the count and list offset of its pages of frames or least + /// values, then the same for greatest values; + /// - the lists: axis pages as (offset, first entry), and pages of values as (offset, first + /// entry, words per entry). + /// + /// Offsets within the table are relative to it. `blocks` gives each page of rows the samples it + /// reads and its block among them. fn trace(&mut self, axis: &Samples, blocks: &[(Samples, usize)], plan: Plan) { - // Each run has its own local origin. Even sparse runs billions of frames apart use - // compact GPU offsets, while CPU plans retain their absolute 64-bit indexes. + // Each run has its own origin, so runs billions of frames apart still get 32-bit GPU + // offsets. CPU plans keep absolute 64-bit indexes. self.words.extend([plan.runs.len() as u32, blocks.len() as u32, plan.runs.len() as u32, plan.points]); let origins: Vec = plan .runs @@ -1152,7 +1163,8 @@ impl<'g> Builder<'g> { self.plan = Some(plan); } - /// Appends `pages` as (offset, first entry, words per entry), each with the entries it holds. + /// Appends `pages` as (offset, first entry, words per entry), requesting each page with the + /// entries it must hold. fn sample_pages(&mut self, pages: &[(FramePage, Range)], origin: u64) { for (page, held) in pages { let key = PageKey::of(page.id, Words::Values, false); @@ -1180,8 +1192,8 @@ impl<'g> Builder<'g> { } } -/// How the registry keeps the page slot `s` of `drawing` holds of a column packed as `packing`: a page -/// of the order by its number, a page of rows kept by the places it holds. `None` for a free slot. +/// The cache key for the page in slot `s` of `drawing`, for a column packed as `packing`: a page of +/// the order by its number, or a page of kept rows by the places it holds. `None` for a free slot. fn slot_key(packing: &Packing, drawing: &Drawing, s: usize) -> Option<(Packing, u32)> { match drawing.holds(s) { Holds::Free => None, @@ -1190,15 +1202,13 @@ fn slot_key(packing: &Packing, drawing: &Drawing, s: usize) -> Option<(Packing, } } -/// How the registry keeps a page of rows kept, at `places` of the order, of a column packed as -/// `packing`. +/// The cache key for a page of kept rows at `places` of the order, in a column packed as `packing`. fn kept_packing(packing: &Packing, places: &[u32]) -> Packing { Packing { order: stamp_of((packing.order, places)), ..packing.clone() } } -/// The id of the page of `packing` slot `s` of `drawing` holds, packed from `source`: a page of its -/// order, or of its rows kept, each kept while none of its rows was written since; `None` for a free -/// slot. +/// The id of the page of `packing` in slot `s` of `drawing`, packed from `source`: a page of its +/// order, or of its kept rows, each cached until one of its rows is written; `None` if free. fn slot_page( packed: &mut crate::gpu::packed::Packed, packing: &Packing, @@ -1216,7 +1226,7 @@ fn slot_page( } } -/// Points rows `rows` of geometry column `column` take as shaders read them. +/// The points that rows `rows` of geometry column `column` take, as shaders read them. fn rows_points(column: &Column, rows: &[u32]) -> usize { rows.iter() .filter_map(|&r| column.at(r as usize)) diff --git a/crates/latkit/src/view/timer.rs b/crates/latkit/src/view/timer.rs index 5f2f3ffb..c78ab7ff 100644 --- a/crates/latkit/src/view/timer.rs +++ b/crates/latkit/src/view/timer.rs @@ -1,15 +1,16 @@ -//! The GPU's time on frames, where the device writes timestamps: an empty pass begins a frame and -//! another ends it, and what lies between is the frame's. Each measure is resolved and read back -//! without stalling, a few at a time, and the time told is the median of the last frames measured, so -//! it holds steady while a frame now and then does more. +//! GPU time per frame, on devices that support timestamp queries. An empty pass writes a timestamp +//! at the start of a frame and another at its end. Each measurement is resolved and read back +//! without stalling, a few at a time. The reported time is the median of the last frames measured, +//! so it stays steady when an occasional frame does more work. use crate::gpu::readback::{Readback, Readbacks}; -/// Measures under way at most, and frames the time told is the median of. +/// The most measurements in flight. const SLOTS: usize = 4; +/// Frames the reported median covers. const KEPT: usize = 15; -/// A query set measuring one frame, and its readback while under way. +/// A query set measuring one frame, and its readback while in flight. struct Slot { set: wgpu::QuerySet, resolved: wgpu::Buffer, @@ -24,12 +25,12 @@ pub(crate) struct Timer { period: f64, /// Milliseconds of the last frames measured, oldest first. measured: Vec, - /// Its own map buffers, whose readbacks count as no work: only some devices have a timer. + /// Its own readback buffers, left out of the work counts, since only some devices have a timer. pool: Readbacks, } impl Timer { - /// A timer, where the device writes timestamps. + /// A timer, or `None` where the device lacks timestamp queries. pub(crate) fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Option { if !device.features().contains(wgpu::Features::TIMESTAMP_QUERY) { return None; @@ -57,7 +58,7 @@ impl Timer { }) } - /// Begins measuring the frame `encoder` records, while a slot is free. + /// Starts measuring the frame `encoder` records, if a slot is free. pub(crate) fn begin(&mut self, encoder: &mut wgpu::CommandEncoder) { self.current = self.slots.iter().position(|s| s.pending.is_none()); if let Some(i) = self.current { @@ -65,7 +66,7 @@ impl Timer { } } - /// Ends the measure this frame began, reading it back once submitted. + /// Ends this frame's measurement and records its readback, which completes after submission. pub(crate) fn end(&mut self, encoder: &mut wgpu::CommandEncoder, device: &wgpu::Device) { let Some(i) = self.current.take() else { return }; let slot = &mut self.slots[i]; @@ -75,7 +76,8 @@ impl Timer { slot.pending = Some(Readback::new(&self.pool, device, encoder, &slot.resolved, 0, 16, waker)); } - /// The median milliseconds of the last frames measured, once a measure landed since last asked. + /// The median milliseconds of the last frames measured, if a measurement arrived since the last + /// call. pub(crate) fn landed(&mut self) -> Option { let mut landed = false; for slot in &mut self.slots { @@ -103,7 +105,7 @@ fn elapsed_ms(bytes: &[u8], period: f64) -> Option { Some((ticks as f64 * period / 1e6) as f32) } -/// An empty pass writing timestamp `beginning` as it begins or `end` as it ends. +/// Records an empty pass that writes timestamp `beginning` at its start, or `end` at its end. fn mark(set: &wgpu::QuerySet, encoder: &mut wgpu::CommandEncoder, beginning: Option, end: Option) { encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("latkit timer"), diff --git a/crates/latkit/src/view/transition.rs b/crates/latkit/src/view/transition.rs index 542d2e40..8329d61f 100644 --- a/crates/latkit/src/view/transition.rs +++ b/crates/latkit/src/view/transition.rs @@ -1,19 +1,22 @@ //! Transitions: a mark whose description, static data, or rows change eases from what it showed. //! -//! When one starts, what each row shows is frozen into the mark's frozen words at the row's new -//! place: where it was when the rows are the same, gathered by id when rows were added, removed, or -//! reordered. Rows that left are frozen past the new rows and fade out there. While the mark eases, -//! its channels store into its settled words once per change; each frame the clock moves, the ease -//! pass writes what every row shows from its frozen and settled words by the mark's progress, and -//! which rows fade, so draws, probes, bounds, and shadows read eased state without knowing a -//! transition runs, and no channel runs to ease. Where no row is the same as one shown before, as when -//! another dataset replaces the one drawn, every row enters, shows what it evaluated, and only fades. -//! The frozen and settled words are pages like the state's, kept only while a transition runs. A -//! stroke's geometry column keeps the table it had as the transition began, and its paths morph from -//! it point by point. +//! When a transition starts, each row's shown state is frozen into the mark's frozen words at the +//! row's new index: from the same index when no row moved, or gathered by id when rows were added, +//! removed, or reordered. Rows that left are frozen after the new rows and fade out there. //! -//! Without an ease, a flowing mark whose rows moved still begins a transition, of no length: it -//! shows the new state at once, and its comets carry their travel over to the rows' new places. +//! While the mark eases, its channels write its settled words, once per change. Each frame the +//! clock moves, the ease pass writes every row's shown state from its frozen and settled words by +//! the mark's progress, and sets which rows fade. So draws, probes, and bounds read eased state +//! without knowing a transition runs, and no channel runs just to ease. When no row matches one +//! shown before, as when another dataset replaces the drawn one, every row enters with its +//! evaluated state and only fades in. +//! +//! The frozen and settled words are paged like the state, and kept only while a transition runs. A +//! stroke's geometry column keeps its table from the start of the transition, and its paths morph +//! from it point by point. +//! +//! Without an ease, a flowing mark whose rows moved still begins a zero-length transition: it shows +//! the new state at once, and its comets carry their travel over to the rows' new places. use std::sync::Arc; @@ -28,7 +31,7 @@ use crate::gpu::shaders::PAGE_ROWS; use crate::gpu::shaders::{self as wgsl, Shader}; use crate::scene::mark::MarkKind; -/// A mark easing from what it showed, begun at a time on the view's clock. +/// A running transition: its start on the view's clock and its length in seconds. pub(crate) struct Transition { start: f64, seconds: f64, @@ -36,14 +39,15 @@ pub(crate) struct Transition { /// A transition waiting for the frame that freezes it. pub(crate) struct Begin { - /// Each new row's old row, then each exit's, when rows moved; `None` when they kept their places. + /// Each new row's old row, then each exit's old row, when rows moved; `None` when every row + /// kept its index. sources: Option>, - /// Whether no row was shown before, so rows only fade in and out. + /// Whether no new row matches one shown before, so rows only fade in and out. fades: bool, } impl Transition { - /// Eased progress on `clock`, `0..=1`; a transition of no length is done as it begins. + /// Eased progress on `clock`, from 0 to 1. A zero-length transition is at 1 from the start. pub(crate) fn motion(&self, clock: &Clock) -> f32 { if self.seconds <= 0.0 { 1.0 } else { clock.eased(self.start, self.seconds) as f32 } } @@ -54,14 +58,14 @@ impl Transition { } /// How rows moved from `old` to `new`: each new row's old row (`NONE` if it entered), and the old -/// rows that left. `None` when every row kept its place. +/// rows that left. `None` when every row kept its index. pub(crate) fn remap(old: &Table, new: &Table) -> Option<(Vec, Vec)> { if old.structure == new.structure && old.order == new.order { return None; } if old.order == new.order && new.len() >= old.len() { - // Rows kept in place, those of pages rewritten aside: there a row whose id changed enters, and - // the row it replaces leaves. + // Rows keep their index, except in rewritten pages: there a row whose id changed enters, + // and the row it replaced leaves. let mut sources: Vec = (0..new.len()).map(|r| if r < old.len() { r as u32 } else { wgsl::NONE }).collect(); let mut exits = Vec::new(); for block in rewritten(old, new).unwrap_or_default() { @@ -98,9 +102,9 @@ pub(crate) fn remap(old: &Table, new: &Table) -> Option<(Vec, Vec)> { Some((sources, exits)) } -/// The row blocks of `new` whose rows `old` did not hold where `new` does, every other row kept in -/// place: those whose ids' pages changed, and those past `old`'s rows. `None` where that does not -/// tell, as between ids counted and ids listed. +/// The row blocks of `new` holding rows that `old` did not hold at the same index, assuming every +/// other row kept its index: blocks whose id pages changed, and blocks past `old`'s rows. `None` +/// when the ids cannot tell, as between counted and listed ids. pub(crate) fn rewritten(old: &Table, new: &Table) -> Option> { let pages = |len: usize| (len as u32).div_ceil(PAGE_ROWS); match (&old.ids, &new.ids) { @@ -113,9 +117,9 @@ pub(crate) fn rewritten(old: &Table, new: &Table) -> Option> { } } -/// How rows moved from what `old` held to what `new` holds: each new held row's old held row -/// (`NONE` if it entered), and the old rows drawn that left. `None` when every row kept its place, as -/// a table drawn in part keeps its rows' places while its order serves, whatever the camera draws. +/// How held rows moved from `old` to `new`: each new held row's old held row (`NONE` if it +/// entered), and the old drawn rows that left. `None` when every row kept its held row, as in a +/// paged table whose order is unchanged, regardless of what the camera draws. pub(crate) fn moved(old: &Holding, new: &Holding) -> Option<(Vec, Vec)> { if old.origin != new.origin { // Another dataset of the same name: every row enters, and every row drawn leaves. @@ -128,12 +132,12 @@ pub(crate) fn moved(old: &Holding, new: &Holding) -> Option<(Vec, Vec) } } -/// Rows held by the table's own order or a drawing's. +/// Held rows: the table's rows, or its drawing's live held rows. fn held(holding: &Holding) -> u32 { holding.drawing.as_ref().map_or(holding.table.len() as u32, |d| d.live()) } -/// The held rows `holding` draws, but those fading out with their tiles. +/// The held rows `holding` draws, except those fading out with their tiles. fn drawn(holding: &Holding) -> Vec { match &holding.drawing { None => (0..holding.table.len() as u32).collect(), @@ -146,8 +150,8 @@ fn drawn(holding: &Holding) -> Vec { } } -/// How rows moved between holdings of different orders: each row followed to where `old` held it, -/// by id where rows came or went. +/// How rows moved between holdings with different orders: each new held row is traced to its old +/// held row, by index while the table's row order is unchanged, else by id. fn follow(old: &Holding, new: &Holding) -> (Vec, Vec) { let same = old.table.order == new.table.order; let source = |h: u32| match &new.drawing { @@ -178,13 +182,13 @@ fn follow(old: &Holding, new: &Holding) -> (Vec, Vec) { (sources, exits) } -/// Whether every new row is where it was, or entered. +/// Whether every new row kept its index or entered. pub(crate) fn in_place(sources: &[u32]) -> bool { sources.iter().enumerate().all(|(r, &s)| s == r as u32 || s == wgsl::NONE) } -/// The slots of `new` holding rows `old` did not hold there: every slot holding a page without one -/// before, those whose page took rows appended since, and those of rows kept that changed. +/// The slots of `new` holding rows that `old` did not hold there: slots whose page is new to them, +/// slots whose page gained appended rows, and slots of kept rows that changed. pub(crate) fn bound(old: Option<&Drawing>, new: &Drawing) -> Vec { let grown = old.map_or(u32::MAX, |old| old.order.len() / PAGE_ROWS); let appended = old.is_some_and(|o| o.order.len() != new.order.len()); @@ -200,7 +204,8 @@ pub(crate) fn bound(old: Option<&Drawing>, new: &Drawing) -> Vec { .collect() } -/// Whether two holdings hold the same rows the same way: the same table's rows, laid out alike. +/// Whether two holdings hold the same rows in the same layout: the same origin, row structure, row +/// order, and drawing. pub(crate) fn holds_alike(a: &Holding, b: &Holding) -> bool { let same_drawing = match (&a.drawing, &b.drawing) { (None, None) => true, @@ -211,7 +216,7 @@ pub(crate) fn holds_alike(a: &Holding, b: &Holding) -> bool { } impl Shown { - /// Takes over the rows `before` holds and their words, as a changed description of the same mark. + /// Takes over `before`'s held rows and their words, as a changed description of the same mark. pub(crate) fn inherit(&mut self, before: &mut Shown) { self.words = std::mem::take(&mut before.words); self.before = before.before; @@ -221,7 +226,8 @@ impl Shown { self.place_version = before.place_version + 1; } - /// Starts easing from what the mark shows: its rows moved as `moved` says, if they did. + /// Begins a transition from what the mark shows, with rows moved as `moved` says, if they + /// moved. Does nothing when the mark showed no rows. pub(crate) fn begin(&mut self, moved: Option<(Vec, Vec)>) { if self.before == 0 { return; @@ -239,30 +245,30 @@ impl Shown { self.all_dirty(); } - /// Starts the running transition over from what the mark shows, as slots `bound` of held rows - /// take pages of rows that enter: the rest ease on toward where they settle, which their channels - /// evaluated already, and rows that left go on fading out past the live ones. + /// Restarts the running transition from what the mark shows, as slots `bound` receive pages of + /// entering rows. Other rows keep easing toward their settled state, which their channels + /// already evaluated, and exits keep fading out after the live rows. pub(crate) fn restart(&mut self, bound: &[u32]) { let mut sources: Vec = (0..self.live()).collect(); for &slot in bound { let rows = slot * PAGE_ROWS..((slot + 1) * PAGE_ROWS).min(sources.len() as u32); sources[rows.start as usize..rows.end as usize].fill(wgsl::NONE); } - // The rows that left lay past the live rows when the frame began. + // Exits were held after the live rows when the frame began. sources.extend(self.before.saturating_sub(self.exits)..self.before); self.begin = Some(Begin { sources: Some(sources), fades: false }); } } impl View { - /// Freezes every transition starting this frame and advances the running ones; true while any - /// runs. + /// Freezes every transition starting this frame and advances the running ones. Returns whether + /// any is running. pub(super) fn transitions(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner) -> bool { // A drag moves rows at once. let seconds = if self.dragging() { 0.0 } else { self.ease().as_secs_f64() }; let mut starting: Vec = (0..self.marks.len()).filter(|&i| self.marks[i].begin.is_some()).collect(); if seconds == 0.0 { - // Without an ease only a flowing mark whose rows moved begins one, for its comets. + // Without an ease, only a flowing mark whose rows moved begins one, for its comets. starting.retain(|&i| { let shown = &mut self.marks[i]; let carries = @@ -284,9 +290,10 @@ impl View { let mut running = false; for (i, shown) in self.marks.iter_mut().enumerate() { let Some(transition) = &shown.transition else { continue }; - // One that begins this frame runs through it, as one of no length must for its comets. + // A transition beginning this frame runs through it, as a zero-length one must for its + // comets. if transition.done(&self.clock) && !starting.contains(&i) { - // One more evaluation writes every row's settled state. + // One more evaluation writes every row's final state. shown.transition = None; shown.exits = 0; shown.all_dirty(); @@ -296,21 +303,22 @@ impl View { } } else { running = true; - // Rows ease as the clock moves, and only then: their channels evaluated once. + // Rows ease only when the clock moves; their channels evaluated once already. shown.easing = self.clock.moved(); } } running } - /// Records what each starting mark shows into its frozen words: the remap words say, for each, the - /// pages its rows' words were in as the frame began, how many rows showed, where each new row was - /// where rows moved, and where the vertices its ends name moved where those did. + /// Records each starting mark's shown state into its frozen words. The remap words give, per + /// mark: its state pages as the frame began, how many rows showed, each new row's old row when + /// rows moved, and where the vertices its endpoints reference moved, if they did. fn freeze(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner, starting: &[usize]) { let gpu = self.gpu.clone(); let (device, queue) = (&gpu.0.device, &gpu.0.queue); let mut words: Vec = Vec::new(); - // Where each vertex mark's rows moved: how many it held, then where each went. + // For each starting vertex mark whose rows moved: how many it held, then each one's new + // row. let mut moves = vec![wgsl::NONE; self.marks.len()]; for &i in starting { let m = &self.marks[i]; @@ -333,7 +341,7 @@ impl View { let begin = shown.begin.take().expect("starting"); let before = shown.words.state_before().clone(); let width = shown.stride() + shown.extras(); - // Rows whose words took another shape show nothing to ease from: they enter, and fade in. + // If the row width changed, there is nothing to ease from: every row enters, fading in. let sources = if before.width == width { begin.sources } else { Some(vec![wgsl::NONE; shown.held() as usize]) }; shown.carry = sources.is_some(); @@ -350,8 +358,8 @@ impl View { } None => wgsl::NONE, }; - // Each end, held in the kind word of its number, follows its vertex where that vertex's - // mark's rows moved. + // Each endpoint, stored in the kind word of its number, follows its vertex where the + // vertex mark's rows moved. let ends: Vec<(u32, u32)> = (0..shown.ends.len()) .filter_map(|k| Some((k as u32, moves[shown.ends[k].mark?]))) .filter(|&(_, moved)| moved != wgsl::NONE) @@ -375,7 +383,7 @@ impl View { let groups = resources.groups(&gpu.0, inner); let group = resources.pass(device, inner, Shader::Freeze); let pipeline = inner.pipelines.compute(device, Shader::Freeze, "gather"); - // The marks' uniforms this frame writes before it submits hold each mark's remap offset. + // The mark uniforms this frame writes before submitting hold each mark's remap offset. let mut pass = encoder .begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("latkit freeze"), timestamp_writes: None }); pass.set_pipeline(&pipeline); @@ -387,9 +395,9 @@ impl View { } } - /// Gives each mark whose transition starts or runs frozen and settled words for every row it - /// holds, and lets them go once it ends; a transition finding no room for them does not start, and - /// its rows show what they evaluate at once. + /// Allocates frozen and settled words for every held row of each mark whose transition starts + /// or runs, and frees them once it ends. A transition without room for them is cancelled, and + /// its rows show their evaluated state at once. pub(super) fn fit_easing(&mut self, inner: &mut Inner) { let gpu = self.gpu.clone(); let device = (&gpu.0.device, &gpu.0.queue); diff --git a/crates/latkit/src/view/uniforms.rs b/crates/latkit/src/view/uniforms.rs index 5045e11f..0a32ce73 100644 --- a/crates/latkit/src/view/uniforms.rs +++ b/crates/latkit/src/view/uniforms.rs @@ -1,5 +1,4 @@ -//! What a frame tells its shaders: the frame uniform, the eyes its passes draw through, and each -//! mark's uniform. +//! The uniforms a frame writes for its shaders: the frame's, the eye's, and each mark's. use glam::{DMat4, DVec3}; @@ -18,10 +17,11 @@ fn vector(v: DVec3) -> [f32; 4] { [v.x as f32, v.y as f32, v.z as f32, 0.0] } -/// The frame uniform for `scene` seen through `projected` in a target of `size` CSS pixels, showing -/// axis coordinate `at`, `elapsed` seconds of motion into the view's clock. Pointer, probe, and -/// focus words start empty. The theme's lengths are in the space's unit, so in a diagram they scale -/// with the zoom; a halo and focus's glow are CSS pixels everywhere. +/// The frame uniform for `scene` seen through `projected` in a target of `size` CSS pixels, at axis +/// coordinate `at` and `elapsed` seconds on the view's clock. `linear` says the target is sRGB and +/// encodes color itself, so shaders write linear color. Pointer, probe, and focus words start +/// empty. Theme lengths are in the space's unit, so in a diagram they scale with the zoom. The halo +/// and the focus glow are in CSS pixels everywhere. pub(crate) fn frame( scene: &Scene, projected: &Projected, @@ -44,7 +44,7 @@ pub(crate) fn frame( }; let unit = if projected.space == wgsl::SPACE_DIAGRAM { projected.px_per_unit[0] as f32 } else { 1.0 }; let graticule = matches!(&scene.space, Space::Earth(earth) if earth.graticule); - // The sun stands where it does at the earth's daylight instant, on by the axis coordinate shown. + // The sun's position at the earth's daylight instant, advanced by the shown axis coordinate. let (sun, atmosphere) = match &scene.space { Space::Earth(earth) => ( earth.daylight.map(|t| crate::scene::sun(t + if at.is_finite() { at } else { 0.0 })), @@ -52,7 +52,7 @@ pub(crate) fn frame( ), _ => (None, None), }; - // A selected color with a negative red asks for each row's own color. + // A negative red in the selected color means each row keeps its own color. let selected = theme.focus.selected.map_or([-1.0, 0.0, 0.0, 0.9], Color::to_array); let mut frame: wgsl::Frame = bytemuck::Zeroable::zeroed(); frame.light = vector(projected.light); @@ -67,7 +67,7 @@ pub(crate) fn frame( frame.glow = theme.focus.glow; frame.dim = theme.focus.dim; frame.dash = theme.dash * unit; - // Comets travel in their own lengths, which a stroke turns into pixels where it draws them. + // Comet spacing and length keep their own unit; a stroke converts them to pixels as it draws. frame.flow_spacing = theme.flow.spacing.value(); frame.flow_length = theme.flow.length.value(); frame.flow_width = theme.flow.width * unit; @@ -77,7 +77,7 @@ pub(crate) fn frame( Length::Px(_) => wgsl::FLOW_PX, }; frame.bow = theme.edge.bow; - // Hover is state either way; the controls say only whether it glows. + // Hover is tracked either way; the controls only decide whether it glows. let glows = scene.controls.hover; frame.grow = if glows { theme.focus.grow } else { 1.0 }; frame.shading = theme.shading; @@ -104,8 +104,8 @@ pub(crate) fn frame( frame } -/// The world distance from the center a plane's grid fades out by: what the view shows looking -/// straight down, and a little more. +/// The world distance from the center at which a plane's grid has faded out: a bit more than the +/// view shows looking straight down. 0 on a plot or the globe. pub(crate) fn grid_fade(scene: &Scene, projected: &Projected, size: [f64; 2]) -> f64 { match &scene.space { Space::Plot(_) => 0.0, @@ -114,7 +114,7 @@ pub(crate) fn grid_fade(scene: &Scene, projected: &Projected, size: [f64; 2]) -> } } -/// The eye of the view: `projected` into a target of `size` CSS pixels at `pixel_ratio`. +/// The eye uniform: `projected` into a target of `size` CSS pixels at `pixel_ratio`. pub(crate) fn eye(projected: &Projected, size: [f64; 2], pixel_ratio: f32) -> wgsl::Eye { let mut eye: wgsl::Eye = bytemuck::Zeroable::zeroed(); eye.clip_from_world = matrix(projected.clip_from_world); @@ -148,9 +148,9 @@ fn theme_slots(theme: &Theme) -> [[u32; 4]; 4] { std::array::from_fn(|i| std::array::from_fn(|j| slots[i * 4 + j])) } -/// What `mark` evaluates and how it eases by `clock`: the pages of rows this frame's evaluation and -/// end pass cover, and while it eases, the page tables of its frozen and settled words, its progress, -/// and how each word eases. +/// Sets `mark`'s evaluation and easing fields: where the row-block lists of this frame's evaluation +/// and end pass start, how each word blends, and while it eases, the page tables of its frozen and +/// settled words and its progress on `clock`. fn ease(mark: &mut wgsl::Mark, shown: &Shown, clock: &super::clock::Clock) { mark.block = shown.block; mark.end_block = shown.end_block; @@ -165,7 +165,8 @@ fn ease(mark: &mut wgsl::Mark, shown: &Shown, clock: &super::clock::Clock) { } } -/// The uniform of mark `index`, labeled as `labels` says, easing by `clock`. +/// The uniform of mark `index`, with its label slots from `labels` and its easing progress from +/// `clock`. pub(crate) fn mark( index: usize, shown: &Shown, diff --git a/crates/latkit/src/view/variants.rs b/crates/latkit/src/view/variants.rs index 892147b6..b199ec1e 100644 --- a/crates/latkit/src/view/variants.rs +++ b/crates/latkit/src/view/variants.rs @@ -1,15 +1,17 @@ -//! The pipelines a scene uses, asked for as it is shown, each at its urgency: what its frames draw -//! with (for each kind of mark, its draw and the compute passes it needs, a stroke mark's for the -//! path it runs along, shadows when something casts, and each mark's channels for what they read); -//! then their text (labels, glyphs, the fields text is set in, the overlay); then what changes need -//! (the passes transitions ease with, comets carried on), what lights rows (the lit rows' draws) and, -//! for frames that take input, what the hover and the intents need (each kind of mark's probe -//! program, which answers the hover, picks, boxes, and where a row lies). The GPU compiles them -//! through its queue, the most urgent first ([`crate::gpu::pipelines`]): the view draws once what it -//! draws with landed, adds text once that did, and lights rows, probes, and answers queries once the -//! rest did, so no frame waits on a compile and nothing compiles where it is first used; what the -//! scene never uses is never asked for. A picture takes no input: it draws once all it draws, writes, -//! changes, and lights rows with landed. +//! The pipelines a scene uses, requested when the scene is shown, in three urgency tiers. +//! +//! Draw: for each kind of mark, its draw pipeline and the compute passes it needs (a stroke mark's +//! for the path it follows), and each mark's channels for what they read. Text: labels, glyphs, the +//! distance fields text is set in, and the overlay. Answer: the passes that transitions and comet +//! carry use, the lit rows' draws, and for frames that take input, each kind of mark's probe +//! program, which answers hover, picks, box selection, and row location. +//! +//! The GPU compiles them through its queue, most urgent first ([`crate::gpu::pipelines`]). The view +//! draws once its draw pipelines are ready, adds text once those are, and lights rows, probes, and +//! answers queries once the rest are. So no frame waits on a compile, and nothing compiles at first +//! use. Pipelines the scene never uses are never requested. A picture takes no input: it requests +//! everything it draws with, text, transitions, and lit rows at draw urgency, and draws once all of +//! them are ready. use super::View; use crate::gpu::Inner; @@ -17,15 +19,15 @@ use crate::gpu::pipelines::{self, Asked, Urgency, Variant}; use crate::gpu::shaders::{PATH_ENDS, PATH_GEOMETRY, Shader}; use crate::{Mark, Marker, Scene, Space}; -/// The passes of the select capping a mark's labels. +/// The passes of the selection that caps a mark's label count. const SELECT: [&str; 4] = ["select_start", "select_count", "select_find", "select_cut"]; -/// The passes placing each labeled mark's labels once its candidates are measured; a diagram's labels -/// need no placement. +/// The passes that place each labeled mark's labels once its candidates are measured. A diagram's +/// labels need no placement. pub(super) const PLACING: [&str; 5] = ["claim", "decide", "occupy", "ask", "fade"]; -/// What a view asked the GPU to compile for frames into one target, taking input or not: what they -/// draw with, their text, and what changes, lights rows, probes, and answers queries. +/// The pipelines a view requested for frames into one target, with or without input, by tier: draw, +/// text, and answer (transitions, lit rows, probes, and queries). pub(super) struct Asking { target: (wgpu::TextureFormat, u32), input: bool, @@ -34,9 +36,9 @@ pub(super) struct Asking { answering: Vec, } -/// What nearly every scene draws with, which the GPU compiles for views to come, in this order: the -/// channels, strokes between ends and through geometry, points and the backdrop, their bounds and -/// lists, ports; then text. +/// The pipelines nearly every scene draws with, which the GPU compiles ahead for later views, in +/// this order: channels, strokes between endpoints and through geometry, points and the backdrop, +/// their bounds and lists, ports, then text. #[cfg(all(feature = "web", target_arch = "wasm32"))] pub(crate) fn common(format: wgpu::TextureFormat, samples: u32) -> Vec { let draw = |shader| Variant::draw(shader, format, samples); @@ -60,9 +62,10 @@ pub(crate) fn common(format: wgpu::TextureFormat, samples: u32) -> Vec { } impl View { - /// Asks for what frames of the scene into `target`, taking `input` or not, use, each at its - /// urgency, the first time; and for the channels of every mark, which compile for what they read - /// once their data says. Each wakes the view as it lands. + /// Requests the pipelines the scene's frames into `target` use, with or without `input`, each + /// at its urgency, on the first call for that pair. Every call also requests each mark's + /// channel pipeline, whose variant depends on what the channels read, known once the data is. + /// Each finished compile wakes the view. Returns the request's index in `asked`. pub(super) fn ask(&mut self, inner: &mut Inner, target: (wgpu::TextureFormat, u32), input: bool) -> usize { let device = &self.gpu.0.device; let at = if let Some(at) = self.asked.iter().position(|a| a.target == target && a.input == input) { @@ -74,7 +77,7 @@ impl View { } else { ([draw, text, changes, lit].concat(), Vec::new(), Vec::new()) }; - // Marks alike share pipelines, each asked for once. + // Several marks can need the same pipeline; request each once. for asked in [&mut drawing, &mut texts, &mut answering] { let mut seen = rustc_hash::FxHashSet::default(); asked.retain(|need| seen.insert(need.clone())); @@ -89,8 +92,8 @@ impl View { at } - /// Each mark's channels, made for what they read: before its data says, neither joins, samples, nor - /// the view. + /// Each mark's channel pipeline, built for what its channels read. Before its data is known, + /// they read neither joins, samples, nor the view. fn channels(&self) -> Vec { let mut channels: Vec = Vec::new(); for shown in &self.marks { @@ -102,8 +105,8 @@ impl View { channels } - /// Whether frames into `format` with `samples` per pixel, taking `input` or not, can draw now: - /// whether what they draw with compiled. + /// Whether frames into `format` at `samples` per pixel, with or without `input`, can draw now: + /// whether their draw pipelines are compiled. pub(super) fn pipelines_ready( &mut self, inner: &mut Inner, @@ -115,21 +118,21 @@ impl View { inner.pipelines.ready(&self.asked[at].drawing) && inner.pipelines.ready(&self.channels()) } - /// Whether frames draw text: labels, their glyphs and fields, and the overlay. Until then they - /// draw without, and labels fade in once it lands, as after any change. + /// Whether frames can draw text: labels, their glyphs and distance fields, and the overlay. + /// Until then frames draw without text, and labels fade in once it is ready, as after any + /// change. pub(super) fn texts_ready(&self, inner: &Inner) -> bool { self.asked.iter().all(|a| inner.pipelines.ready(&a.texts)) } - /// Whether every target the view's frames taking input draw into can ease, light rows, probe, - /// and answer queries: whether what those use compiled. + /// Whether, for every target that frames taking input draw into, the pipelines for easing, lit + /// rows, probes, and queries are compiled. pub(super) fn answers_ready(&self, inner: &Inner) -> bool { self.asked.iter().filter(|a| a.input).all(|a| inner.pipelines.ready(&a.answering)) } } -/// What a scene's frames use: what they draw with, their text, what changes need, what lights rows, -/// and what input needs. +/// The pipelines a scene's frames use, by stage: drawing, text, transitions, lit rows, and input. #[derive(Default)] struct Stages { draw: Vec, @@ -139,8 +142,8 @@ struct Stages { input: Vec, } -/// What frames of `scene` into `format` with `samples` per pixel use, but for each mark's channels, -/// which compile for what they read. +/// The pipelines frames of `scene` into `format` at `samples` per pixel use, except each mark's +/// channel pipeline, which depends on what its channels read. fn stages(scene: &Scene, format: wgpu::TextureFormat, samples: u32) -> Stages { let any = |test: fn(&Mark) -> bool| scene.marks.iter().any(test); let draw = |shader| Asked::Shader(Variant::draw(shader, format, samples)); @@ -152,14 +155,14 @@ fn stages(scene: &Scene, format: wgpu::TextureFormat, samples: u32) -> Stages { }; let controls = scene.controls; let diagram = matches!(scene.space, Space::Diagram(_)); - // The overlay writes a plot's axes, the title, and the legends, and the box and the connection a - // gesture draws. + // The overlay draws a plot's axes, the title, the legends, and the selection box and connection + // line that gestures draw. let legends = any(|m| m.style().legend.is_some() && m.style().color.is_some()); let written = matches!(scene.space, Space::Plot(_)) || scene.title.is_some() || scene.subtitle.is_some() || legends; if written || controls.select_box || controls.connect { stages.text.push(draw(Shader::Overlay)); } - // The distance fields of the text frames set are made before they draw. + // The distance fields of text a frame sets are built before the frame draws. let labeled = any(|m| { m.style().label.is_some() || matches!(m, Mark::Vertices(v) if v.ports.iter().any(|(_, port)| port.label.is_some())) @@ -177,7 +180,7 @@ fn stages(scene: &Scene, format: wgpu::TextureFormat, samples: u32) -> Stages { if any(|m| matches!(m, Mark::Edges(_) | Mark::Lines(_) | Mark::Areas(_))) { stages.draw.extend(["scan_blocks", "scan_sums", "add_sums"].map(|e| compute(Shader::Lists, e))); } - // Any table may come to draw in part, as its data grows past the budget or orders it by priority. + // Any table may become paged: its data can outgrow the budget, or a mark can give a priority. if !scene.marks.is_empty() { stages.draw.push(compute(Shader::Lists, "expand_runs")); } @@ -187,9 +190,9 @@ fn stages(scene: &Scene, format: wgpu::TextureFormat, samples: u32) -> Stages { stages } -/// What `mark` adds to the stages: its draw and compute passes, for the path a stroke mark runs -/// along or an area's triangles' geometry; its lit rows, which hover, selection, and the ends of -/// edges light; its probes; its labels. +/// Adds `mark`'s pipelines to `stages`: its draw and compute passes, for the path a stroke follows +/// or an area's triangle geometry; its lit-row draw, for the rows that hover, selection, and edge +/// endpoints light; its probe; and its labels. fn mark_stages(mark: &Mark, scene: &Scene, format: wgpu::TextureFormat, samples: u32, stages: &mut Stages) { let draw = |shader| Variant::draw(shader, format, samples); let compute = |shader, entry| Asked::Shader(Variant::compute(shader, entry)); @@ -199,7 +202,7 @@ fn mark_stages(mark: &Mark, scene: &Scene, format: wgpu::TextureFormat, samples: let (shader, lit) = match mark { Mark::Vertices(v) => { if let Marker::Wgsl { source, .. } = &v.marker { - // A custom marker's WGSL follows its shader's. + // A custom marker's WGSL compiles together with its shader's. let custom = |shader, format, samples| Asked::Custom(source.clone(), Variant::draw(shader, format, samples)); stages.draw.push(custom(Shader::PointsCustom, format, samples)); @@ -221,19 +224,18 @@ fn mark_stages(mark: &Mark, scene: &Scene, format: wgpu::TextureFormat, samples: _ => "strokes", }; stages.draw.push(Asked::Shader(Variant::compute(Shader::Bounds, bounds).path(path))); - // Rows running through different numbers of segments, or areas of different numbers of - // triangles, are numbered by a list. + // A list numbers rows with differing segment counts, and areas with differing triangle counts. if matches!(mark, Mark::Edges(_)) || path == PATH_GEOMETRY { stages.draw.extend( ["count_segments", "write_segments"].map(|e| Asked::Shader(Variant::compute(Shader::Lists, e).path(path))), ); } - // One program answers the hover, the queries, and the depth under the pointer for the mark's - // geometry: any row drawn can be located, whether or not it picks. + // One probe program answers hover, queries, and the depth under the pointer for the mark's + // geometry. Every mark gets one, so any drawn row can be located, pickable or not. stages.input.push(Asked::Shader(Variant::probe(pipelines::probe(samples), primitive).path(path))); if style.label.is_some() { - // Measuring where labels hang and drawing them are made for the mark's primitive; a diagram's - // labels need no placement. + // Measuring label candidates and drawing glyphs are built for the mark's primitive. A + // diagram's labels need no placement. stages.text.push(Asked::Shader(draw(Shader::Glyphs).path(path).primitive(primitive))); stages .text diff --git a/crates/latkit/src/view/wake.rs b/crates/latkit/src/view/wake.rs index 054d56d0..c1a4afd7 100644 --- a/crates/latkit/src/view/wake.rs +++ b/crates/latkit/src/view/wake.rs @@ -1,39 +1,41 @@ -//! Sleeping while waiting: work a view waits on (a readback the GPU copies, a job a host does, bytes -//! a host fetches) lands between frames and wakes the view's signal, which marks a frame due and -//! wakes whatever draws its frames. Until something lands or changes, the view draws nothing. +//! Waking a view. Work the view waits on (a GPU readback, a host job, a host fetch) arrives between +//! frames and wakes the view's signal. The signal marks a frame due and wakes whatever draws the +//! view's frames. Until something arrives or changes, the view draws nothing. use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, PoisonError}; use std::task::{Wake, Waker}; -/// What wakes a view: work landing, or the view itself changing. +/// Wakes a view when work arrives or the view itself changes. #[derive(Default)] pub(crate) struct Signal { landed: AtomicBool, + /// The effect time a shot fixes, if any. elapsed: Mutex>, + /// Whether the shot draws a transparent background. transparent: AtomicBool, /// Whether a shot draws the view, so nothing else does. lent: AtomicBool, - /// Whether the shot draws the view's data as it was when lent, as a picture does. + /// Whether the shot keeps the view's data unchanged from when it was lent, as a picture does. holding: AtomicBool, - /// Whatever draws the view's frames, as `View::set_waker` gave it. + /// The waker of whatever draws the view's frames, as `View::set_waker` set it. waker: Mutex>, } impl Signal { - /// Whether work landed since the frame began. + /// Whether work arrived since the frame began. pub(crate) fn landed(&self) -> bool { self.landed.load(Ordering::Acquire) } - /// Takes what landed as a frame begins, before it takes anything: whether anything did. What - /// lands from then on, as work the frame starts and the host answers at once, wakes the next. + /// Clears and returns the arrival flag at the start of a frame. Work that arrives afterwards, + /// including work this frame starts that the host finishes at once, wakes the next frame. pub(crate) fn take(&self) -> bool { self.landed.swap(false, Ordering::AcqRel) } - /// Leaves what landed, `landed`, to the next frame: as a frame not ready to draw does, taking - /// none of it. + /// Restores the arrival flag `landed` for the next frame, as a frame that is not ready to draw + /// does. pub(crate) fn leave(&self, landed: bool) { if landed { self.landed.store(true, Ordering::Release); @@ -44,8 +46,9 @@ impl Signal { *self.waker.lock().unwrap_or_else(PoisonError::into_inner) = Some(waker); } - /// Lends the view to a shot: `waker` wakes the shot in place of whatever drew the view, which - /// draws it no more until it is given back, `holding` its data as it is; returns the waker it had. + /// Lends the view to a shot. `waker` wakes the shot instead of whatever drew the view, which + /// stops drawing it until it is given back. With `holding`, the view's data stays unchanged + /// meanwhile. Returns the previous waker. pub(crate) fn lend(&self, waker: Waker, elapsed: Option, transparent: bool, holding: bool) -> Option { *self.elapsed.lock().unwrap_or_else(PoisonError::into_inner) = elapsed; self.transparent.store(transparent, Ordering::Release); @@ -54,7 +57,7 @@ impl Signal { self.waker.lock().unwrap_or_else(PoisonError::into_inner).replace(waker) } - /// Gives the view back to whatever drew it, `waker` waking it again, and asks it for a frame. + /// Returns the view to whatever drew it, restoring `waker`, and requests a frame. pub(crate) fn give_back(&self, waker: Option) { *self.waker.lock().unwrap_or_else(PoisonError::into_inner) = waker; *self.elapsed.lock().unwrap_or_else(PoisonError::into_inner) = None; @@ -78,12 +81,12 @@ impl Signal { self.lent.load(Ordering::Acquire) } - /// Whether the shot drawing the view draws its data as it was when lent. + /// Whether the shot drawing the view keeps its data unchanged from when it was lent. pub(crate) fn holding(&self) -> bool { self.holding.load(Ordering::Acquire) } - /// Asks whatever draws the view for a frame. + /// Requests a frame from whatever draws the view. pub(crate) fn ask(&self) { if let Some(waker) = self.waker.lock().unwrap_or_else(PoisonError::into_inner).as_ref() { waker.wake_by_ref(); diff --git a/crates/latkit/src/view/words.rs b/crates/latkit/src/view/words.rs index 529f636c..c4be5f8d 100644 --- a/crates/latkit/src/view/words.rs +++ b/crates/latkit/src/view/words.rs @@ -1,10 +1,10 @@ -//! What a mark keeps per row, in pages of the arena: for each page of held rows, a block of each kind -//! of words it keeps, holding as many rows as the page has. A row's state is its kind words then its -//! extra words; beside it are its focus word, its label's candidate, the normals faces sum at a vertex, -//! and while the mark eases, its state as it showed when the transition began and as it settles. A -//! page keeps its blocks while the mark holds rows there, so rows keep their words as the camera moves -//! and nothing moves as a mark holds more rows; every kind is counted in the GPU's budget with the -//! data. +//! A mark's per-row words, in pages of the arena. For each page of held rows, the mark has one +//! arena block per kind of words, sized for the page's rows. A row's state is its kind words, then +//! its extra words. Beside the state are the row's focus word, its label candidate, the normals +//! faces sum at a vertex, and while the mark eases, its frozen state (as shown when the transition +//! began) and its settled state. A page keeps its blocks while the mark holds rows there, so rows +//! keep their words as the camera moves, and holding more rows moves none. Every kind counts toward +//! the GPU budget along with the data. use std::sync::Arc; @@ -25,16 +25,15 @@ pub(crate) enum Kept { const KINDS: [Kept; 6] = [Kept::State, Kept::Focus, Kept::Candidates, Kept::Normals, Kept::Frozen, Kept::Settled]; impl Kept { - /// Whether a page of it is cleared as it is handed out: where it is read before anything writes - /// it. A row's state starts over as evaluation first writes it, its frozen words as a transition - /// freezes them, and the normals faces sum as they sum them afresh. + /// Whether a new block of this kind is cleared on allocation, because something reads it before + /// anything writes it. State and frozen words need no clearing, since evaluation and freezing + /// write them first, and normals are cleared before each sum. fn cleared(self) -> bool { matches!(self, Kept::Focus | Kept::Candidates | Kept::Settled) } } -/// One kind's pages: the words each row takes, and each page's block in the arena and the words it -/// holds. +/// One kind's pages: the words per row, and each page's arena block and its size in words. #[derive(Clone, PartialEq, Eq, Debug)] pub(crate) struct Pages { pub(crate) width: u32, @@ -52,7 +51,8 @@ impl Default for Pages { #[derive(Default)] pub(crate) struct Words { pages: [Pages; KINDS.len()], - /// The state's pages before this frame changed them, while the frame is done with what they held. + /// The state pages from before this frame changed them, kept until the frame is recorded: a + /// transition starting this frame freezes rows from them. before: Option, } @@ -61,20 +61,20 @@ impl Words { &self.pages[kept as usize] } - /// The state's pages as the frame began: what a transition starting freezes rows from. + /// The state pages as the frame began, which a starting transition freezes rows from. pub(crate) fn state_before(&self) -> &Pages { self.before.as_ref().unwrap_or(&self.pages[Kept::State as usize]) } - /// Forgets the state's pages as the frame began, once the frame is recorded. + /// Drops the frame-start state pages once the frame is recorded. pub(crate) fn settle(&mut self) { self.before = None; } - /// Fits `kept`'s pages to hold `rows[p]` rows on page `p`, of `width` words each, or none at - /// `width` 0: a page whose block cannot hold its rows takes a new block, as every page does where a - /// row's words changed width; blocks it no longer has are let go. The pages given new blocks, whose - /// words start over; `None` where the arena has no room for them now, and nothing changed. + /// Fits `kept`'s pages to hold `rows[p]` rows on page `p`, at `width` words per row; `width` 0 + /// frees them all. A page whose block is too small gets a new block, as does every page when + /// the width changes. Blocks no longer used are freed. Returns the pages given new blocks, + /// whose words start over, or `None` (with nothing changed) when the arena has no room now. pub(crate) fn fit( &mut self, kept: Kept, @@ -123,7 +123,7 @@ impl Words { Some(new) } - /// Lets go of every page, as a mark does when it goes. + /// Frees every page, as when the mark is removed. pub(crate) fn release(&mut self, arena: &mut Arena) { for kept in KINDS { let pages = std::mem::take(&mut self.pages[kept as usize]); @@ -134,7 +134,7 @@ impl Words { self.before = None; } - /// The page tables the program holds: each kind it keeps, and its blocks. + /// The page tables the program buffer holds: each kind the mark keeps, with its blocks. pub(crate) fn tables(&self) -> impl Iterator { KINDS .into_iter() diff --git a/crates/latkit/src/web.rs b/crates/latkit/src/web.rs index 894107ec..bbda8263 100644 --- a/crates/latkit/src/web.rs +++ b/crates/latkit/src/web.rs @@ -1,10 +1,13 @@ -//! Views in a browser. A [`PageView`] is a view the page holds: the page reads and changes it, hears -//! what it tells, and lends it to shots, with or without a canvas showing it. A [`Canvas`] shows views -//! as its layout places them in a `` element, or in an `OffscreenCanvas` in a worker, and -//! feeds each view the pointer, wheel, and keys that land on its panel. Every canvas of one [`Gpu`] -//! draws in one animation frame, into one command buffer, after the frame listeners of [`on_frame`] -//! run: linked views always show the same moment, and an application's per-frame work lands in the -//! frame it was done for. +//! Views in a browser. +//! +//! A [`PageView`] is a view owned by the page. The page reads and changes it, listens to its +//! events, and lends it to shots, whether or not a canvas shows it. A [`Canvas`] shows views in +//! the panels of its layout, in a `` element or in an `OffscreenCanvas` in a worker. It +//! feeds each view the pointer, wheel, and key input for its panel. +//! +//! Every canvas of one [`Gpu`] draws in the same animation frame and command buffer, after the +//! [`on_frame`] listeners run. So linked views always show the same moment, and an application's +//! per-frame work appears in the frame it was done for. use std::cell::RefCell; use std::rc::{Rc, Weak}; @@ -28,7 +31,7 @@ use crate::{Image, Shot, Video}; pub struct Listener(Option>); impl Listener { - /// Keeps listening for as long as what it listens to lives. + /// Keeps the listener running for as long as its source lives, instead of stopping it on drop. pub fn forget(mut self) { self.0 = None; } @@ -45,8 +48,8 @@ impl Drop for Listener { type FrameListener = Rc>; type EventListener = Rc>; -/// The animation frames of one `Gpu`: its listeners, then every canvas that needs a frame, then what -/// each of its views told. +/// The animation frames of one `Gpu`. Each frame runs the listeners, then draws every canvas that +/// needs a frame, then dispatches each view's events. struct Frames { gpu: Gpu, canvases: Vec>>, @@ -54,15 +57,15 @@ struct Frames { listeners: Vec<(u64, FrameListener)>, next: u64, scheduled: Option, - /// Whether a harness steps the frames itself ([`step`]): no animation frame is asked for. + /// Whether a harness steps the frames itself with [`step`], so no animation frame is requested. stepped: bool, - /// What waits for work a view waits on to land ([`landed`]), woken as it asks for a frame, and - /// how many times that was. + /// Wakers of [`landed`] futures, woken whenever a frame is requested, and the number of such + /// wakes. landing: Vec, wakes: u64, tick: Option>, - /// The page's `prefers-reduced-motion`, which each view is told, and what draws again when it - /// changes; none in a worker. + /// The page's `prefers-reduced-motion` query, passed to each view, and the closure that + /// requests a frame when it changes. `None` in a worker. reduced: Option<(web_sys::MediaQueryList, Closure)>, } @@ -112,7 +115,7 @@ fn frames_of(gpu: &Gpu) -> Rc> { }) } -/// The page's or the worker's global scope, which hands out animation frames. +/// The page's or worker's global scope, which provides animation frames. enum Scope { Window(web_sys::Window), Worker(web_sys::DedicatedWorkerGlobalScope), @@ -126,8 +129,8 @@ fn scope() -> Option { global.dyn_ref::().map(|worker| Scope::Worker(worker.clone())) } -/// Asks for an animation frame, unless one is asked for or a harness steps the frames, and wakes what -/// waits for work to land. +/// Requests an animation frame, unless one is already requested or a harness steps the frames. Also +/// wakes every [`landed`] future. fn schedule(frames: &Rc>) { let landing = { let mut f = frames.borrow_mut(); @@ -170,7 +173,7 @@ fn tick(frames: &Rc>, now: f64) { let mut encoder = gpu.device().create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("latkit") }); let shown: Vec = canvases.iter().filter_map(|c| c.borrow_mut().record(&mut encoder, now / 1000.0)).collect(); - // A frame no canvas drew submits nothing: listeners alone keep frames coming. + // A frame in which no canvas drew submits nothing. Listeners alone still keep frames coming. if !shown.is_empty() { gpu.queue().submit([encoder.finish()]); } @@ -191,7 +194,8 @@ fn tick(frames: &Rc>, now: f64) { } } -/// Releases the registry's GPU and cancels its frame as soon as its final owner disappears. +/// Removes the `Gpu`'s frames from the registry and cancels its requested frame once no canvas, +/// view, or listener remains. fn release_idle(frames: &Rc>) { let mut f = frames.borrow_mut(); f.canvases.retain(|c| c.strong_count() > 0); @@ -210,14 +214,14 @@ fn release_idle(frames: &Rc>) { FRAMES.with(|all| all.borrow_mut().retain(|f| !Rc::ptr_eq(f, frames))); } -/// Asks for an animation frame of the `Gpu` whose shared state lies at `gpu`, waking what waits for -/// its work to land: what a view's waker does when work it waited on lands, always on the page's one -/// thread. +/// A view's waker. When work the view waited on arrives, it requests an animation frame of the +/// `Gpu` whose shared state is at address `gpu`, and wakes its [`landed`] futures. It always runs +/// on the page's single thread. struct Wakes { gpu: usize, } -/// What wakes `gpu`'s frames, and what waits for its work to land ([`landed`]), as work lands. +/// A waker that, when work arrives, requests a frame of `gpu` and wakes its [`landed`] futures. pub(crate) fn waker(gpu: &Gpu) -> std::task::Waker { std::task::Waker::from(Arc::new(Wakes { gpu: Arc::as_ptr(&gpu.0) as usize })) } @@ -236,8 +240,9 @@ impl std::task::Wake for Wakes { } } -/// Calls `listener` with the frame's timestamp, milliseconds as `requestAnimationFrame` gives it, -/// every animation frame before `gpu`'s canvases draw, until the returned listener is dropped. +/// Calls `listener` every animation frame, before `gpu`'s canvases draw, with the frame's timestamp +/// in milliseconds as `requestAnimationFrame` gives it. Stops when the returned [`Listener`] is +/// dropped. pub fn on_frame(gpu: &Gpu, listener: impl FnMut(f64) + 'static) -> Listener { let frames = frames_of(gpu); let id = { @@ -257,10 +262,12 @@ pub fn on_frame(gpu: &Gpu, listener: impl FnMut(f64) + 'static) -> Listener { }))) } -/// Draws one frame of `gpu` now, at `now`, milliseconds as an animation frame's timestamp: its -/// listeners, then every canvas that needs a frame, then what its views told. From the first step -/// `gpu` asks for no animation frames: a harness steps every frame itself, so a run draws the same -/// frames however fast the device is, and work landing wakes [`landed`] alone. +/// Draws one frame of `gpu` immediately, with `now` as its timestamp in milliseconds. Runs the +/// frame listeners, draws every canvas that needs a frame, then dispatches the views' events. +/// +/// After the first call, `gpu` requests no animation frames. The harness steps every frame itself, +/// so a run draws the same frames regardless of device speed, and arriving work only wakes +/// [`landed`]. pub fn step(gpu: &Gpu, now: f64) { let frames = frames_of(gpu); let scheduled = { @@ -278,9 +285,9 @@ pub fn step(gpu: &Gpu, now: f64) { tick(&frames, now); } -/// Waits until work a view of `gpu` waits on lands after this call: a readback, a compile, a job, a -/// text, a fetch; or until a view asks for a frame. What lands between the call and the first wait -/// counts, as a view a harness changes right after asking does. +/// Waits until work a view of `gpu` depends on arrives after this call, such as a readback, +/// compile, job, text, or fetch, or until a view requests a frame. Arrivals between this call and +/// the first poll count, so a harness may change a view right after calling it. pub fn landed(gpu: &Gpu) -> impl Future + 'static { let frames = frames_of(gpu); let seen = frames.borrow().wakes; @@ -294,8 +301,8 @@ pub fn landed(gpu: &Gpu) -> impl Future + 'static { }) } -/// Waits until `gpu` finished the work submitted to it, every readback of it mapped, and every -/// pipeline asked of it compiled: a frame stepped then draws all that landed. +/// Waits until `gpu` has finished its submitted work, mapped every readback, and compiled every +/// requested pipeline. A frame stepped afterwards draws all the results. pub async fn done(gpu: &Gpu) { let (submitted, finished) = crate::Pending::new(); gpu.queue().on_submitted_work_done(move || finished.resolve(Ok(()))); @@ -312,26 +319,26 @@ pub async fn done(gpu: &Gpu) { } } -/// A view the page holds: it reads and changes the view, hears what it tells, shows it in a canvas, -/// and lends it to shots. Changing it asks its canvas for a frame. +/// A view owned by the page, which reads and changes it, listens to its events, shows it in a +/// canvas, and lends it to shots. Changing it requests a frame. #[derive(Clone)] pub struct PageView(Rc>); struct Held { view: View, frames: Rc>, - /// Whether it was changed since it last drew. + /// Whether the view changed since it last drew. due: bool, listeners: Vec<(u64, EventListener)>, next: u64, } impl PageView { - /// Holds `view` for the page. Its `Gpu` must come from [`Gpu::new`]; it sets text with the - /// browser's fonts unless it was given a host. + /// Wraps `view` for the page. Its `Gpu` must come from [`Gpu::new`]. Unless the `Gpu` already + /// has a host, text is set with the browser's fonts. pub fn new(mut view: View) -> PageView { let gpu = view.gpu().clone(); - // The view sleeps while it waits; what it waits on asks for the next frame as it lands. + // The view sleeps while it waits. The work it waits on requests the next frame on arrival. view.set_waker(waker(&gpu)); if gpu.0.lock().host.is_none() { gpu.set_host(crate::host::Browser); @@ -343,7 +350,7 @@ impl PageView { PageView(held) } - /// Calls `f` with the view, then draws a frame when it needs one. + /// Calls `f` with the view, then requests a frame to draw any change. pub fn with(&self, f: impl FnOnce(&mut View) -> R) -> R { let result = f(&mut self.0.borrow_mut().view); self.0.borrow_mut().due = true; @@ -352,7 +359,7 @@ impl PageView { result } - /// Calls `f` with the view, to read it without asking for a frame. + /// Calls `f` with the view to read it, without requesting a frame. pub fn read(&self, f: impl FnOnce(&View) -> R) -> R { f(&self.0.borrow().view) } @@ -362,9 +369,9 @@ impl PageView { self.0.borrow().view.gpu().clone() } - /// Calls `listener` with what the view tells, a batch at a time and in order, until the returned - /// listener is dropped. A listener hears what is told while it listens; what lasts is state the - /// view reads out. + /// Calls `listener` with the view's events, in order, one batch per call, until the returned + /// [`Listener`] is dropped. A listener receives only the events sent while it listens. Read + /// lasting state from the view itself. pub fn on(&self, listener: impl FnMut(&[Event]) + 'static) -> Listener { let id = { let mut held = self.0.borrow_mut(); @@ -381,7 +388,7 @@ impl PageView { }))) } - /// Hands what the view told to its listeners, one batch. + /// Sends the view's pending events to its listeners as one batch. fn dispatch(&self) { let (events, listeners) = { let mut held = self.0.borrow_mut(); @@ -398,8 +405,8 @@ impl PageView { } } - /// Whether it wants a frame and no shot holds it: it changed, or its view wants one. While what - /// it draws with compiles, only a compile landing does. + /// Whether it needs a frame and is not lent to a shot: it changed, or its view wants one. While + /// its pipelines compile, a change waits until they finish. pub fn wants_frame(&self) -> bool { let held = self.0.borrow(); !held.view.lent() && (held.view.wants_frame() || (held.due && !held.view.compiling())) @@ -410,31 +417,31 @@ impl PageView { self.0.borrow_mut().view.ready(format, pixel_ratio) } - /// Whether a shot holds it. + /// Whether it is lent to a shot. fn lent(&self) -> bool { self.0.borrow().view.lent() } } -/// A view's place in a canvas: the view, and where it draws, `[x, y, width, height]` as fractions of -/// the canvas. A later panel draws over an earlier one. +/// A view's panel in a canvas. Later panels draw over earlier ones. #[derive(Clone)] pub struct Panel { /// The view shown. pub view: PageView, - /// Where it draws. + /// Where it draws: `[x, y, width, height]` as fractions of the canvas, from its top left. pub at: [f32; 4], } -/// Views shown in a `` element or an `OffscreenCanvas`, as its layout places them. An -/// element sizes itself to its CSS box at the device's pixel ratio and passes each view the pointer, -/// wheel, and keys that land on its panel; an offscreen canvas keeps the size it is given and takes -/// no input. A frame draws only the views that need one, or all of them when the canvas changes size -/// or layout. +/// Views shown in a `` element or an `OffscreenCanvas`, in the panels of its layout. +/// +/// An element canvas sizes itself to its CSS box at the device pixel ratio, and passes each view +/// the pointer, wheel, and key input for its panel. An offscreen canvas keeps the size it is given +/// and takes no input. A frame redraws only the views that need it, or all of them when the canvas +/// changes size or layout. #[derive(Clone)] pub struct Canvas(Rc>); -/// What a canvas draws into. +/// The canvas a [`Canvas`] draws into: an element, or an offscreen canvas and its pixel ratio. enum Target { Element(HtmlCanvasElement), Offscreen { canvas: OffscreenCanvas, pixel_ratio: f32 }, @@ -446,13 +453,14 @@ struct Painted { frames: Rc>, panels: Panels, views: Vec, - /// Each panel's texture while the canvas composes several views, and its size. + /// Each panel's texture and its size, used when the canvas shows several views. textures: Vec>, - /// Each panel in CSS pixels, as last drawn: what input lands on. + /// Each panel in CSS pixels as last drawn, used to route input. rects: Vec<[f32; 4]>, - /// Whether every view draws in the next frame: the canvas changed size or layout. + /// Whether every view draws in the next frame, because the canvas changed size or layout. due: bool, - /// The panel each pointer down went to, the panel under the pointer, and the panel keys go to. + /// The panel that captured each pressed pointer, the panel under the pointer, and the panel + /// that receives keys. captured: Vec<(u32, usize)>, hovered: Option, focus: usize, @@ -461,19 +469,18 @@ struct Painted { resize: Option, } -/// A listener of one kind of the element's events. +/// A listener for one kind of element event. type Handler = (&'static str, Closure); -/// What follows the element's size. +/// The observer that tracks the element's size, and its callback. type Resizing = (ResizeObserver, Closure); impl Canvas { - /// A canvas showing views in `element` as `layout` places them. The `Gpu` must come from - /// [`Gpu::new`]. + /// A canvas showing the views of `layout` in `element`. The `Gpu` must come from [`Gpu::new`]. pub fn new(gpu: &Gpu, element: &HtmlCanvasElement, layout: Vec) -> Result { let size = [element.width(), element.height()]; let surface = Surface::new(gpu, wgpu::SurfaceTarget::Canvas(element.clone()), size)?; let _ = element.style().set_property("touch-action", "none"); - // Keys reach the views without the page's help. + // Make the element focusable so keys reach the views directly. if !element.has_attribute("tabindex") { element.set_tab_index(0); } @@ -494,11 +501,12 @@ impl Canvas { Ok(canvas) } - /// A canvas showing views in `canvas` as `layout` places them, `pixel_ratio` of its pixels a CSS - /// pixel: in a worker, or wherever no page feeds it input. It keeps the size the canvas is given. + /// A canvas showing the views of `layout` in `canvas`, at `pixel_ratio` pixels per CSS pixel. + /// Use it in a worker, or wherever the page does not feed it input. It keeps the size it is + /// given. pub fn offscreen(gpu: &Gpu, canvas: &OffscreenCanvas, layout: Vec, pixel_ratio: f32) -> Result { if !(pixel_ratio.is_finite() && pixel_ratio > 0.0) { - return Err(Error::invalid("a canvas's pixel ratio is positive").at("pixelRatio")); + return Err(Error::invalid("a canvas's pixel ratio must be positive").at("pixelRatio")); } let size = [canvas.width(), canvas.height()]; let surface = Surface::new(gpu, wgpu::SurfaceTarget::OffscreenCanvas(canvas.clone()), size)?; @@ -531,8 +539,8 @@ impl Canvas { Canvas(state) } - /// Shows the views of `layout` where it places them, in place of those it showed. A view shows in - /// one panel of one canvas at a time; a panel covering no pixels is invalid. + /// Replaces the shown views with those of `layout`. A view shows in one panel of one canvas + /// at a time. Fails if a view repeats or shows in another canvas, or a panel covers no pixels. pub fn set_layout(&self, layout: Vec) -> Result<()> { let views: Vec<&PageView> = layout.iter().map(|panel| &panel.view).collect(); let twice = views.iter().enumerate().any(|(i, a)| views[i + 1..].iter().any(|b| Rc::ptr_eq(&a.0, &b.0))); @@ -542,7 +550,7 @@ impl Canvas { && other.borrow().views.iter().any(|shown| views.iter().any(|v| Rc::ptr_eq(&v.0, &shown.0))) }); if twice || elsewhere { - return Err(Error::invalid("a view shows in one panel of one canvas").at("layout")); + return Err(Error::invalid("a view can show in only one panel of one canvas").at("layout")); } let panels = Panels::from(layout.iter().map(|panel| panel.at).collect::>()); panels.rects([1 << 16, 1 << 16], [1, 1]).map_err(|e| e.at("layout"))?; @@ -569,7 +577,7 @@ impl Canvas { } impl Painted { - /// The canvas's size in pixels and pixels a CSS pixel. + /// The canvas's size in pixels, and its pixels per CSS pixel. fn measure(&self) -> ([u32; 2], f32) { match &self.target { Target::Element(element) => { @@ -583,16 +591,16 @@ impl Painted { } } - /// Whether a frame is due: the canvas changed, or a view it shows wants to draw; never while shots - /// hold every view it shows, whose last frames it keeps. + /// Whether a frame is due: the canvas changed, or a view it shows wants to draw. Never while + /// every view it shows is lent to a shot, since it keeps their last frames. fn wants_frame(&self) -> bool { self.views.iter().any(|v| !v.lent()) && (self.due || self.views.iter().any(PageView::wants_frame)) } - /// Records a frame when one is needed, sizing the canvas to its box first: each view that needs - /// to draws into its panel, straight into the canvas when one view fills it. A view a shot holds, - /// or whose scene's pipelines still compile, draws nothing new and keeps its panel; while no view - /// it shows can draw, the canvas keeps the frame it shows. + /// Records a frame if one is needed, first sizing the canvas to its CSS box. Each view that + /// needs to draw renders into its panel, or straight into the canvas when one view fills it. A + /// view lent to a shot, or whose pipelines still compile, keeps its panel's previous contents. + /// While no view can draw, the canvas keeps its current frame. fn record(&mut self, encoder: &mut wgpu::CommandEncoder, time: f64) -> Option { let (size, ratio) = self.measure(); let resized = size != self.surface.size(); @@ -627,7 +635,7 @@ impl Painted { if fresh { self.textures[i] = Some((panel_texture(self.surface.gpu.device(), format, rect.size), rect.size)); } - let (panel, _) = self.textures[i].as_ref().expect("made"); + let (panel, _) = self.textures[i].as_ref().expect("the panel texture exists"); let mut held = self.views[i].0.borrow_mut(); if ready[i] && (fresh || self.due || held.due || held.view.needs_render()) { let target = panel.create_view(&wgpu::TextureViewDescriptor::default()); @@ -651,7 +659,7 @@ impl Painted { Some(texture) } - /// Sets the element's cursor to the one the view under the pointer asks for. + /// Sets the element's cursor to the one the view under the pointer requests. fn show_cursor(&mut self) { let Target::Element(element) = &self.target else { return }; let cursor = self.hovered.and_then(|i| self.views.get(i)).map_or(Cursor::Default, |v| v.read(View::cursor)); @@ -668,9 +676,9 @@ impl Painted { } } - /// The panels `input` goes to, each with the input in its own CSS pixels: a pointer goes to the - /// panel it pressed until it lets go, else to the panel under it, which hears when it leaves; keys - /// go to the panel pressed last. + /// The panels `input` goes to, each with the input in that panel's CSS pixels. A pressed + /// pointer goes to the panel it pressed until release. Otherwise it goes to the panel under + /// it, which receives a leave when the pointer moves off. Keys go to the panel pressed last. fn route(&mut self, input: Input) -> Vec<(usize, Input)> { let rects = if self.rects.is_empty() { self.css_rects() } else { self.rects.clone() }; let under = |point: [f32; 2]| { @@ -717,7 +725,8 @@ impl Painted { routed } - /// Each panel in CSS pixels, from the canvas's size now: before its first frame records them. + /// Each panel in CSS pixels at the canvas's current size, used before the first frame records + /// them. fn css_rects(&self) -> Vec<[f32; 4]> { let (size, ratio) = self.measure(); self.panels.rects(size, [1, 1]).map(|rects| rects.iter().map(|r| css(*r, ratio)).collect()).unwrap_or_default() @@ -731,7 +740,7 @@ fn css(rect: Rect, ratio: f32) -> [f32; 4] { [x, y, w, h] } -/// `input` with its point taken from `origin`, so it reads from its panel's top left. +/// `input` with its point made relative to `origin`, its panel's top left. fn within(input: Input, origin: [f32; 2]) -> Input { let at = |point: [f32; 2]| [point[0] - origin[0], point[1] - origin[1]]; match input { @@ -747,7 +756,7 @@ fn within(input: Input, origin: [f32; 2]) -> Input { } } -/// A panel's texture: views draw into it, and the canvas copies it into place. +/// A panel's texture. A view draws into it, and the canvas copies it into place. fn panel_texture(device: &wgpu::Device, format: wgpu::TextureFormat, size: [u32; 2]) -> wgpu::Texture { device.create_texture(&wgpu::TextureDescriptor { label: Some("latkit panel"), @@ -761,9 +770,9 @@ fn panel_texture(device: &wgpu::Device, format: wgpu::TextureFormat, size: [u32; }) } -/// A canvas's surface: in the one format views draw in wherever the browser offers it, so canvases and -/// shots share their pipelines; alpha premultiplied; panels copied into it; configured again whenever -/// its size changes. +/// A canvas's surface. It uses the views' standard format when the browser offers it, so canvases +/// and shots share pipelines. Alpha is premultiplied where supported, panels are copied in, and the +/// surface is reconfigured whenever its size changes. struct Surface { gpu: Gpu, raw: wgpu::Surface<'static>, @@ -771,10 +780,10 @@ struct Surface { } impl Surface { - /// The surface of `target`, `size` pixels, drawn with `gpu`, which must come from [`Gpu::new`]. + /// Creates the surface of `target` at `size` pixels. `gpu` must come from [`Gpu::new`]. fn new(gpu: &Gpu, target: wgpu::SurfaceTarget<'static>, size: [u32; 2]) -> Result { let (Some(instance), Some(adapter)) = (gpu.instance(), gpu.adapter()) else { - return Err(Error::Unsupported("a canvas draws with a Gpu made by Gpu::new".into())); + return Err(Error::Unsupported("a canvas needs a Gpu created by Gpu::new".into())); }; let raw = instance.create_surface(target).map_err(|e| Error::Unsupported(format!("no WebGPU canvas: {e}").into()))?; @@ -786,14 +795,15 @@ impl Surface { .formats .first() .copied() - .ok_or_else(|| Error::Unsupported("the canvas offers no format".into()))? + .ok_or_else(|| Error::Unsupported("the canvas supports no texture format".into()))? }; let alpha_mode = if capabilities.alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) { wgpu::CompositeAlphaMode::PreMultiplied } else { wgpu::CompositeAlphaMode::Auto }; - // Panels are drawn apart and copied in: a WebGPU canvas takes copies, whatever wgpu reports. + // Panels are drawn separately and copied in. A WebGPU canvas accepts copies, regardless of + // what wgpu reports. let config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST, format, @@ -809,13 +819,13 @@ impl Surface { Ok(Surface { gpu: gpu.clone(), raw, config }) } - /// Its width and height in pixels, as last configured. + /// Width and height in pixels, as last configured. fn size(&self) -> [u32; 2] { [self.config.width, self.config.height] } - /// The texture to draw a frame of `size` pixels into, configuring the surface again first when - /// its size changed; `None` when the surface had none to give, which a later frame tries again. + /// The texture for a frame of `size` pixels, reconfiguring the surface first if its size + /// changed. `None` if the surface has no texture available. A later frame tries again. fn texture(&mut self, size: [u32; 2]) -> Option { let size = size.map(|v| v.max(1)); if size != self.size() { @@ -834,16 +844,16 @@ impl Surface { } } -/// Views the page holds, lent to a shot a step at a time, so the page reads them between steps and -/// hears what each step told. +/// Page views lent to a shot one step at a time, so the page can read them between steps and +/// receives each step's events. struct Lending<'a>(&'a [PageView]); impl Lending<'_> { - /// The views, each once. + /// Lends `views`. Fails if a view appears twice. fn of(views: &[PageView]) -> Result> { let twice = views.iter().enumerate().any(|(i, a)| views[i + 1..].iter().any(|b| Rc::ptr_eq(&a.0, &b.0))); if twice { - return Err(Error::invalid("a shot draws each view in one panel")); + return Err(Error::invalid("a view can appear in only one panel of a shot")); } Ok(Lending(views)) } @@ -863,14 +873,14 @@ impl Views for Lending<'_> { } } -/// The picture `shot` takes of `views`, one per panel, once they are complete and still. Canvases -/// showing them hold their last frames meanwhile. +/// Takes `shot`'s picture of `views`, one per panel, once they are complete as its +/// [`Capture`](crate::Capture) says. Canvases showing them keep their last frames meanwhile. pub async fn image(shot: &Shot, views: &[PageView]) -> Result { shot.image_of(&mut Lending::of(views)?).await } -/// Draws `video`'s next frame of `views`, one per panel. Canvases showing them hold their last frames -/// until the video is finished or dropped. +/// Draws `video`'s next frame of `views`, one per panel. Canvases showing them keep their last +/// frames until the video is finished or dropped. pub async fn frame(video: &mut Video, views: &[PageView]) -> Result<()> { video.frame_of(&mut Lending::of(views)?).await } @@ -889,7 +899,7 @@ impl Drop for Painted { } } -/// Feeds the element's pointer, wheel, and keys to the views of the panels they land on. +/// Feeds the element's pointer, wheel, and key events to the views of the panels they target. fn listen(state: &Rc>, element: &HtmlCanvasElement) -> Result<()> { let add = |kind: &'static str, handle: fn(&HtmlCanvasElement, &web_sys::Event) -> Option| { let weak = Rc::downgrade(state); @@ -907,7 +917,7 @@ fn listen(state: &Rc>, element: &HtmlCanvasElement) -> Result<( view.dispatch(); } } - // A wheel a view zooms with does not scroll the page; a key it uses does not either. + // A wheel or key event a view used must not also scroll the page. if used && matches!(kind, "wheel" | "keydown") { event.prevent_default(); } @@ -921,7 +931,7 @@ fn listen(state: &Rc>, element: &HtmlCanvasElement) -> Result<( handler.as_ref().unchecked_ref(), &options, ) - .map_err(|_| Error::Unsupported("the canvas takes no input".into()))?; + .map_err(|_| Error::Unsupported("cannot listen for input on the canvas".into()))?; state.borrow_mut().handlers.push((kind, handler)); Ok::<(), Error>(()) }; @@ -981,7 +991,7 @@ fn point(element: &HtmlCanvasElement, event: &web_sys::MouseEvent) -> [f32; 2] { ] } -/// The keys a key or mouse event says are held. +/// The modifier keys a key or mouse event reports as held. macro_rules! modifiers { ($event:expr) => { Modifiers { @@ -994,7 +1004,7 @@ macro_rules! modifiers { } use modifiers; -/// A pointer press, focusing the element and capturing the pointer. +/// Reads a pointer press, focusing the element and capturing the pointer. fn pressed(element: &HtmlCanvasElement, event: &web_sys::Event) -> Option { let event = event.dyn_ref::()?; let button = match event.button() { @@ -1014,7 +1024,7 @@ fn pressed(element: &HtmlCanvasElement, event: &web_sys::Event) -> Option }) } -/// What a pointer event's pointer is. +/// The kind of device behind a pointer event. fn device(event: &PointerEvent) -> Device { match event.pointer_type().as_str() { "touch" => Device::Touch, @@ -1023,7 +1033,7 @@ fn device(event: &PointerEvent) -> Device { } } -/// A key going down or up. +/// Reads a key press or release. fn keyed(event: &web_sys::Event) -> Option { let event = event.dyn_ref::()?; let key = match event.key().as_str() { diff --git a/crates/latkit/tests/areas.rs b/crates/latkit/tests/areas.rs index 3ec45fc9..c519266e 100644 --- a/crates/latkit/tests/areas.rs +++ b/crates/latkit/tests/areas.rs @@ -1,6 +1,6 @@ -//! Areas: polygons filled through the triangles the view derives for them, holes left open, picked -//! where they cover, framed by a fit, outlined by lines along their rings; on the earth whole across -//! the antimeridian, and shaped in a job when too large for the frame. +//! Areas: polygons filled through triangles the view derives, with holes left open. Areas pick +//! where they cover, a fit frames them, and lines trace their rings. On the earth they stay whole +//! across the antimeridian, and polygons too large for one frame are triangulated in a job. #![cfg(feature = "gpu")] @@ -29,7 +29,7 @@ fn dark(pixel: [u8; 4]) -> bool { pixel[0].max(pixel[1]).max(pixel[2]) < 30 } -/// A dataset of one table of polygons, each row its rings. +/// A dataset with one table of polygons, each row a list of rings. fn polygons(rows: Vec>) -> Data { let ids: Vec = (1..=rows.len() as u64).collect(); let data = Data::new("map"); @@ -44,7 +44,7 @@ fn view(gpu: &latkit::Gpu, scene: Scene, data: &Data) -> View { view } -/// Renders frames until `pending` resolves, within the settle deadline. +/// Renders frames until `pending` resolves, within `SETTLE` frames. fn answer(gpu: &latkit::Gpu, view: &mut View, pending: impl Future>) -> T { let mut pending = pin!(pending); let mut context = Context::from_waker(Waker::noop()); @@ -71,10 +71,10 @@ fn areas_fill_their_polygons_and_leave_their_holes() { } #[test] -fn areas_asked_to_pick_pick_their_rows_and_frame_a_fit() { +fn pickable_areas_return_their_rows_and_a_fit_frames_them() { let Some(gpu) = gpu() else { return }; let data = polygons(vec![vec![square(100.0, 100.0, 40.0)], vec![square(300.0, 100.0, 40.0)]]); - // Areas are geometry: they answer the pointer only when asked to. + // Areas respond to the pointer only when `pick` is on. let areas = Areas::of("map", "Zone").color(Color::hex("#ff0000").expect("hex")).pick(true); let scene = Scene::new(plane()).draw(areas); let mut view = view(&gpu, scene, &data); @@ -119,7 +119,7 @@ fn earth_areas_stay_whole_across_the_antimeridian() { #[test] fn large_areas_triangulate_in_a_job_and_then_draw() { let Some(gpu) = gpu() else { return }; - // A ring of 40,000 points: past what a frame triangulates. + // A ring of 40,000 points, more than one frame triangulates. let ring: Ring = (0..=40_000) .map(|k| { let a = f64::from(k) / 40_000.0 * std::f64::consts::TAU; @@ -130,12 +130,12 @@ fn large_areas_triangulate_in_a_job_and_then_draw() { let scene = Scene::new(plane()).draw(Areas::of("map", "Zone").color(Color::hex("#ff0000").expect("hex"))); let mut view = view(&gpu, scene, &data); let pixels = frame(&gpu, &mut view); - assert!(red(at(&pixels, C, C)), "drawn once the job lands: {:?}", at(&pixels, C, C)); + assert!(red(at(&pixels, C, C)), "drawn once the job finishes: {:?}", at(&pixels, C, C)); assert_eq!(errors(&mut view), Vec::new()); } #[test] -fn a_polygon_field_is_what_areas_fill() { +fn areas_require_a_polygon_field() { let Some(gpu) = gpu() else { return }; let data = Data::new("map"); data.apply( @@ -144,5 +144,5 @@ fn a_polygon_field_is_what_areas_fill() { .expect("valid"); let mut view = view(&gpu, Scene::new(plane()), &data); let refused = view.set_scene(Scene::new(plane()).draw(Areas::of("map", "Zone"))); - assert!(refused.is_err(), "a path field is no polygon field"); + assert!(refused.is_err(), "areas reject a path field"); } diff --git a/crates/latkit/tests/common/mod.rs b/crates/latkit/tests/common/mod.rs index 2bd8d7a1..ea7eb480 100644 --- a/crates/latkit/tests/common/mod.rs +++ b/crates/latkit/tests/common/mod.rs @@ -1,4 +1,4 @@ -//! What every GPU test shares: a device, frames rendered headless on a clock the test steps and +//! Shared helpers for GPU tests: a device, headless frames rendered on a test-stepped clock and //! read back, and plain scenes. #![allow(dead_code, unreachable_pub, reason = "each test file uses some")] @@ -7,20 +7,21 @@ use std::cell::Cell; use latkit::{Camera, Color, Diagram, Frame, Gpu, Grid, Plane, Theme, View}; -/// Seconds a frame steps the test's clock: frames run as a display's would, however fast they draw. +/// Seconds each frame advances the test clock, so frames run at display rate regardless of draw +/// speed. pub const TICK: f64 = 1.0 / 60.0; thread_local! { - /// The test's clock: every frame steps it, and inputs read it. + /// The test clock. Every frame advances it, and inputs read it. static NOW: Cell = const { Cell::new(0.0) }; } -/// The test's clock, for inputs. +/// The test clock's time, for inputs. pub fn now() -> f64 { NOW.with(Cell::get) } -/// Steps the test's clock `seconds` without drawing. +/// Advances the test clock by `seconds` without drawing. pub fn wait(seconds: f64) { NOW.with(|now| now.set(now.get() + seconds)); } @@ -28,8 +29,8 @@ pub fn wait(seconds: f64) { pub const SIZE: u32 = 128; pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; -/// A device, or `None` where the machine has no adapter at all, which only `LATKIT_REQUIRE_GPU` -/// makes a failure. +/// A device, or `None` if the machine has no adapter. With `LATKIT_REQUIRE_GPU` set, a missing +/// adapter panics instead. pub fn gpu() -> Option { match pollster::block_on(Gpu::new()) { Ok(gpu) => Some(gpu), @@ -43,7 +44,7 @@ pub fn gpu() -> Option { /// The longest the GPU may take to finish a frame in these tests. pub const DEADLINE: std::time::Duration = std::time::Duration::from_secs(10); -/// The most frames a view may take to settle: uploads, probes, and a transition of two seconds. +/// The most frames a view may take to settle: uploads, probes, and a two-second transition. pub const SETTLE: u32 = 600; pub fn finish(gpu: &Gpu) { @@ -51,7 +52,7 @@ pub fn finish(gpu: &Gpu) { gpu.device().poll(wait).expect("the GPU finishes within the deadline"); } -/// Renders frames until the view is settled, transitions included, then reads the target's pixels. +/// Renders frames until the view settles, including transitions, then returns the target's pixels. pub fn frame(gpu: &Gpu, view: &mut View) -> Vec<[u8; 4]> { let mut pixels = step(gpu, view); for _ in 0..SETTLE { @@ -63,7 +64,7 @@ pub fn frame(gpu: &Gpu, view: &mut View) -> Vec<[u8; 4]> { panic!("the view settles within {SETTLE} frames"); } -/// Steps the test's clock a frame, renders one frame, and reads the target's pixels. +/// Advances the test clock one tick, renders one frame, and returns the target's pixels. pub fn step(gpu: &Gpu, view: &mut View) -> Vec<[u8; 4]> { wait(TICK); let device = gpu.device(); @@ -106,7 +107,7 @@ pub fn step(gpu: &Gpu, view: &mut View) -> Vec<[u8; 4]> { bytemuck::cast_slice::(&slice.get_mapped_range().expect("mapped")).to_vec() } -/// The errors a view reported since its events were last drained, other events dropped. +/// The errors a view reported since its events were last drained. Other events are discarded. pub fn errors(view: &mut View) -> Vec { view.events().filter_map(|e| if let latkit::Event::Error(e) = e { Some(e) } else { None }).collect() } @@ -123,13 +124,13 @@ pub fn plain_theme() -> Theme { Theme { background: Color::BLACK, ..Theme::dark() } } -/// The plain theme, its edges green. +/// The plain theme with green edges. pub fn green_edges() -> Theme { let plain = plain_theme(); Theme { edge: latkit::EdgeTheme { color: Some(Color::rgb(0.0, 1.0, 0.0)), ..plain.edge }, ..plain } } -/// The plain theme, its labels without halos. +/// The plain theme with label halos off. pub fn haloless() -> Theme { let plain = plain_theme(); Theme { text: latkit::TextTheme { halo: 0.0, ..plain.text.clone() }, ..plain } @@ -139,7 +140,7 @@ pub fn plane() -> Plane { Plane { grid: Grid::Off } } -/// A diagram without a grid: y runs down, and at zoom 0 a unit is a pixel. +/// A diagram without a grid. Y points down, and at zoom 0 one unit is one pixel. pub fn diagram() -> Diagram { Diagram { grid: Grid::Off } } diff --git a/crates/latkit/tests/fixtures/latk-v1.hex b/crates/latkit/tests/fixtures/latk-v1.hex deleted file mode 100644 index a0217b3a..00000000 --- a/crates/latkit/tests/fixtures/latk-v1.hex +++ /dev/null @@ -1 +0,0 @@ -4c41544b010000004c000000000000000000000001000000060100000054010000007601000000000300000001000000020000000000000000000000000000000000f03f0000803f00000040 diff --git a/crates/latkit/tests/fixtures/patch.hex b/crates/latkit/tests/fixtures/patch.hex new file mode 100644 index 00000000..48b23cde --- /dev/null +++ b/crates/latkit/tests/fixtures/patch.hex @@ -0,0 +1 @@ +4c41544b48000000000000000000000001000000060100000054010000007601000000000300000001000000020000000000000000000000000000000000f03f0000803f00000040 diff --git a/crates/latkit/tests/gestures.rs b/crates/latkit/tests/gestures.rs index b24ea98a..b1819168 100644 --- a/crates/latkit/tests/gestures.rs +++ b/crates/latkit/tests/gestures.rs @@ -1,10 +1,12 @@ -//! Interaction events: a secondary click asks for a menu of what lies under it, as a touch held still -//! does; dragging from a port draws a connection to the vertex it is released over, an edge's end can -//! be dragged off it, a dragged row keeps where it was grabbed, a double click opens what it hits, a -//! click again can cycle through the rows under it, and Delete tells the selection. The keys point at -//! rows too; hover is state whether or not it glows and follows the frame, and a lit row keeps its -//! ports on top; setters tell nothing. A scene's first frame asks for every pipeline its input uses. -//! The wheel zooms as the controls say, and reduced motion pays every move at once. +//! Interaction events. A secondary click, or a touch held still, requests a menu of the rows under +//! it. Dragging from a port connects to the vertex it is released over, and dragging an edge +//! detaches its nearer end. A dragged row keeps its grab point under the pointer. A double click +//! opens the row it hits, clicking again cycles through the rows under the point, and Delete +//! reports the selection. Keys can step through rows too. +//! +//! Hover is reported whether or not it glows, and follows the frame. A lit row keeps its ports on +//! top. Setters emit no events. A scene's first frame requests every pipeline its input can use. +//! The wheel zooms as the controls say, and reduced motion applies every move at once. #![cfg(feature = "gpu")] @@ -18,7 +20,8 @@ use latkit::{ const C: f32 = SIZE as f32 / 2.0; -/// Three nodes, a link from node 1 to node 3, and a port on each node's right. +/// Three nodes and a link from node 1 to node 3. Each node has an empty `net` reference for its +/// port. fn graph() -> Data { let data = Data::new("graph"); data.apply( @@ -40,7 +43,7 @@ fn graph() -> Data { data } -/// The graph in a diagram, its nodes 16 units across. +/// The graph in a diagram, with nodes 16 units across and a port on each node's right. fn scene(controls: Controls) -> Scene { Scene::new(diagram()) .theme(plain_theme()) @@ -53,7 +56,7 @@ fn view(gpu: &latkit::Gpu, controls: Controls) -> View { view_of(gpu, controls, &graph()) } -/// A view of `graph`, drawn. +/// A view of `graph`, rendered until settled. fn view_of(gpu: &latkit::Gpu, controls: Controls, graph: &Data) -> View { let mut view = View::new(gpu, scene(controls)).expect("valid scene"); view.set_camera(camera()); @@ -66,7 +69,7 @@ fn editing() -> Controls { Controls { connect: true, delete: true, ..Controls::default() } } -/// Feeds `inputs`, rendering a frame after each, then renders until `wanted` finds its event. +/// Feeds `inputs`, rendering two frames after each, then renders until `wanted` matches an event. fn after(gpu: &latkit::Gpu, view: &mut View, inputs: &[Input], wanted: impl Fn(&Event) -> Option) -> T { for input in inputs { view.input(input, common::now()); @@ -103,7 +106,7 @@ fn connected(e: &Event) -> Option<(End, Option, Option)> { } #[test] -fn secondary_clicks_ask_for_a_menu() { +fn secondary_clicks_request_a_menu() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, editing()); let at = [C - 40.0, C]; @@ -122,7 +125,7 @@ fn secondary_clicks_ask_for_a_menu() { fn dragging_from_a_port_connects() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, editing()); - // Node 1's port sits on its right side, 8 units from its middle. + // Node 1's port sits on its right side, 8 units from its center. let port = [C - 32.0, C]; let target = [C + 40.0, C]; let inputs = [ @@ -140,7 +143,7 @@ fn dragging_from_a_port_connects() { } #[test] -fn a_dragged_row_keeps_where_it_was_grabbed() { +fn a_dragged_row_keeps_its_grab_point_under_the_pointer() { let Some(gpu) = gpu() else { return }; let data = Data::new("graph"); data.apply(Table::new("Node", vec![1_u64]).field("x", vec![-40.0]).field("y", vec![0.0])).expect("valid"); @@ -151,22 +154,22 @@ fn a_dragged_row_keeps_where_it_was_grabbed() { view.set_camera(camera()); view.set_sources([&data]).expect("a source"); frame(&gpu, &mut view); - // Grabbed five units right of its middle, then moved 30 right and 20 down: y runs down. + // Grabbed 5 units right of its center, then moved 30 right and 20 down (y points down). let grab = [C - 35.0, C]; let inputs = [moved(grab), down(grab, Button::Primary), moved([C - 25.0, C + 10.0]), moved([C - 5.0, C + 20.0])]; let dragged = |e: &Event| if let Event::Drag { phase: Phase::Move, position, .. } = e { Some(*position) } else { None }; let position = after(&gpu, &mut view, &inputs, dragged); let off = (position[0] + 10.0).hypot(position[1] - 20.0); - assert!(off < 0.5, "the row goes where the pointer took the point it was grabbed by: {position:?}"); + assert!(off < 0.5, "the grab point stays under the pointer: {position:?}"); } #[test] fn a_lit_block_keeps_its_ports_on_top() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, Controls::default()); - // Node 1's port is hollow, on its right side 8 units from its middle; a unit inside the block, the - // ground shows through it, lit or not. + // Node 1's port is hollow, centered on its right edge 8 units from its center. One unit inside + // the block, the pixel within the port stays the same, lit or not. let port = |pixels: &[[u8; 4]]| common::at(pixels, C as u32 - 33, C as u32); let before = port(&step(&gpu, &mut view)); view.input(&moved([C - 40.0, C]), common::now()); @@ -174,15 +177,15 @@ fn a_lit_block_keeps_its_ports_on_top() { for _ in 0..30 { pixels = step(&gpu, &mut view); } - assert_eq!(view.hovered().map(|hit| hit.row.clone()), Some(Row::new("graph", "Node", 1_u64)), "lit"); - assert!(common::close(port(&pixels), before), "{:?}, where unlit it was {before:?}", port(&pixels)); + assert_eq!(view.hovered().map(|hit| hit.row.clone()), Some(Row::new("graph", "Node", 1_u64)), "node 1 is lit"); + assert!(common::close(port(&pixels), before), "the port is unchanged: lit {:?}, unlit {before:?}", port(&pixels)); } #[test] -fn a_scene_asks_for_all_it_uses_at_its_first_frame() { +fn the_first_frame_requests_every_pipeline_the_scene_can_use() { let Some(gpu) = gpu() else { return }; - // Its first frame asks for every pipeline the scene can use: a menu, a connection, a selection, - // the keys, and a transition after them make none where it is first used. + // The first frame requests every pipeline the scene can use. A menu, a connection, a selection, + // the keys, and a later transition then need no pipeline requested late. let scene = scene(editing()).ease(std::time::Duration::from_millis(200)); let mut view = View::new(&gpu, scene).expect("valid scene"); view.set_camera(camera()); @@ -207,14 +210,14 @@ fn a_scene_asks_for_all_it_uses_at_its_first_frame() { .expect("valid"); view.set_sources([&moved_graph]).expect("a source"); frame(&gpu, &mut view); - assert_eq!(gpu.work().late, 0, "every pipeline was asked for at the first frame"); + assert_eq!(gpu.work().late, 0, "every pipeline was requested at the first frame"); } #[test] -fn dragging_an_edge_lets_go_of_its_nearer_end() { +fn dragging_an_edge_detaches_its_nearer_end() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, editing()); - // The link runs from node 1 up to node 3; pressed nearer node 3, it lets go of that end. + // The link runs from node 1 up to node 3. Pressed nearer node 3, it detaches that end. let near_three = [C - 10.0, C - 30.0]; let target = [C + 40.0, C]; let inputs = [ @@ -232,10 +235,10 @@ fn dragging_an_edge_lets_go_of_its_nearer_end() { } #[test] -fn a_double_click_faster_than_its_probe_opens() { +fn a_double_click_before_any_probe_answer_still_opens() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, editing()); - // Both clicks land before any frame answers what lies under them. + // Both clicks arrive before any frame reports the rows under them. let at = [C + 40.0, C]; for input in [down(at, Button::Primary), up(at), down(at, Button::Primary), up(at)] { view.input(&input, common::now()); @@ -246,7 +249,7 @@ fn a_double_click_faster_than_its_probe_opens() { } #[test] -fn the_selection_lets_go_of_rows_the_data_drops() { +fn the_selection_drops_rows_removed_from_the_data() { let Some(gpu) = gpu() else { return }; let data = graph(); let mut view = view_of(&gpu, editing(), &data); @@ -257,12 +260,12 @@ fn the_selection_lets_go_of_rows_the_data_drops() { _ => None, }); let kept = vec![Rows::new("graph", "Node", [3_u64])]; - assert_eq!(selected, (kept.clone(), None), "the data's doing, no trigger"); + assert_eq!(selected, (kept.clone(), None), "a data change has no trigger"); assert_eq!(view.selection(), kept); } #[test] -fn delete_tells_the_selection() { +fn delete_reports_the_selection() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, editing()); let at = [C + 40.0, C]; @@ -275,7 +278,7 @@ fn delete_tells_the_selection() { } #[test] -fn a_shift_click_toggles_where_boxes_select() { +fn a_shift_click_toggles_when_box_select_is_on() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, Controls { select_box: true, ..Controls::default() }); let (at, shift) = ([C + 40.0, C], Modifiers { shift: true, ..Modifiers::default() }); @@ -293,7 +296,7 @@ fn the_keys_step_open_and_select() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, editing()); let link = Row::new("graph", "Link", 0_u64); - // Tab waits for the rows nearest the middle, then points at the nearest: the link. + // Tab waits for the rows nearest the center, then hovers the nearest one: the link. let hovered = after(&gpu, &mut view, &[key(Key::Tab)], |e| match e { Event::Hovered { hit: Some(hit), trigger } => Some((hit.row.clone(), trigger.device)), _ => None, @@ -310,16 +313,16 @@ fn the_keys_step_open_and_select() { _ => None, }); assert_eq!(selected, vec![Rows::from(link)]); - // Before the first row, Shift+Tab is not used, so focus moves on, and nothing stays pointed at. + // Shift+Tab past the first row goes unused, so focus leaves the view and the hover clears. let back = Input::Key { key: Key::Tab, down: true, modifiers: Modifiers { shift: true, ..Modifiers::default() } }; - assert!(!view.input(&back, common::now()), "focus moves on"); + assert!(!view.input(&back, common::now()), "focus leaves the view"); let left = after(&gpu, &mut view, &[], |e| if let Event::Hovered { hit, .. } = e { Some(hit.clone()) } else { None }); assert_eq!(left, None); } #[test] -fn hover_is_told_without_its_glow_and_follows_the_frame() { +fn hover_is_reported_without_glow_and_follows_the_frame() { let Some(gpu) = gpu() else { return }; let data = graph(); let mut view = view_of(&gpu, Controls { hover: false, ..Controls::default() }, &data); @@ -329,7 +332,7 @@ fn hover_is_told_without_its_glow_and_follows_the_frame() { _ => None, }); assert_eq!(hovered, (Row::new("graph", "Node", 2_u64), Device::Mouse)); - // The node moves out from under the still pointer: the hover follows. + // The node moves out from under the still pointer, and the hover follows. data.apply(Patch::new().set(Table::new("Node", vec![2_u64]).field("x", vec![200.0]))).expect("valid"); let left = after(&gpu, &mut view, &[], |e| if let Event::Hovered { hit, .. } = e { Some(hit.clone()) } else { None }); @@ -337,7 +340,7 @@ fn hover_is_told_without_its_glow_and_follows_the_frame() { } #[test] -fn setters_tell_nothing_while_motion_tells_the_camera() { +fn setters_emit_no_events_but_a_flight_reports_the_camera() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, editing()); let set = Camera::Map { center: [10.0, 0.0, 0.0], zoom: 0.0, pitch: 0.0, bearing: 0.0 }; @@ -345,18 +348,18 @@ fn setters_tell_nothing_while_motion_tells_the_camera() { view.set_selection([Rows::new("graph", "Node", [1_u64])]); frame(&gpu, &mut view); let told: Vec = view.events().filter(|e| matches!(e, Event::Camera(_) | Event::Selection { .. })).collect(); - assert_eq!(told, Vec::new(), "setters tell nothing"); + assert_eq!(told, Vec::new(), "setters emit no events"); view.fly(Camera::Map { center: [-10.0, 0.0, 0.0], zoom: 0.0, pitch: 0.0, bearing: 0.0 }); let flown = after(&gpu, &mut view, &[], |e| if let Event::Camera(camera) = e { Some(*camera) } else { None }); - assert_ne!(flown, set, "a flight tells where it moved"); + assert_ne!(flown, set, "a flight reports where it moved"); } #[test] -fn waiting_tells_when_the_frame_is_complete() { +fn waiting_reports_when_the_frame_is_complete() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, editing()); let waiting = after(&gpu, &mut view, &[], |e| if let Event::Waiting(waiting) = e { Some(*waiting) } else { None }); - assert!(waiting.is_empty() && view.complete(), "the last change told is the complete frame: {waiting}"); + assert!(waiting.is_empty() && view.complete(), "the latest waiting event reports a complete frame: {waiting}"); } fn selected(e: &Event) -> Option> { @@ -367,13 +370,14 @@ fn selected(e: &Event) -> Option> { fn clicking_again_cycles_through_the_rows_under_the_point() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, Controls { cycle: true, ..Controls::default() }); - // Inside node 1, 6 units toward node 3, where the link leaves node 1's outline 2 units away. + // Inside node 1, 6 units toward node 3: 2 units short of where the link leaves its outline. let s = 6.0 / std::f32::consts::SQRT_2; let at = [C - 40.0 + s, C - s]; let (node, link) = (Row::new("graph", "Node", 1_u64), Row::new("graph", "Link", 0_u64)); let click = [down(at, Button::Primary), up(at)]; assert_eq!(after(&gpu, &mut view, &[&[moved(at)][..], &click].concat(), selected), vec![Rows::from(node.clone())]); - // Slower than a double click, where nothing moved: the next row under the point, then back. + // Clicks slower than a double click, without moving, select the next row under the point, then + // wrap back. common::wait(1.0); assert_eq!(after(&gpu, &mut view, &click, selected), vec![Rows::from(link)]); common::wait(1.0); @@ -381,7 +385,7 @@ fn clicking_again_cycles_through_the_rows_under_the_point() { } #[test] -fn a_touch_held_still_asks_for_a_menu_and_does_not_click() { +fn a_touch_held_still_requests_a_menu_and_does_not_click() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, Controls::default()); let at = [C - 40.0, C]; @@ -403,7 +407,7 @@ fn a_touch_held_still_asks_for_a_menu_and_does_not_click() { } assert!( !view.events().any(|e| matches!(e, Event::Click { .. } | Event::Selection { .. })), - "the release only lets go" + "the release neither clicks nor selects" ); } @@ -416,20 +420,20 @@ fn the_wheel_zooms_as_the_controls_say() { modifiers: Modifiers { control, ..Modifiers::default() }, }; let mut view = view(&gpu, Controls { zoom: Zoom::Modifier, ..Controls::default() }); - assert!(!view.input(&wheel(false), common::now()), "the plain wheel is the page's to scroll"); - assert!(view.input(&wheel(true), common::now()), "a trackpad's pinch, the wheel with Control, zooms"); + assert!(!view.input(&wheel(false), common::now()), "the plain wheel is left to scroll the page"); + assert!(view.input(&wheel(true), common::now()), "Control+wheel (a trackpad pinch) zooms"); let mut view = view_off(&gpu); assert!(!view.input(&wheel(true), common::now()), "nothing zooms"); } #[test] -fn zooming_at_the_fits_limit_moves_nothing() { +fn zooming_at_the_zoom_limit_moves_nothing() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, Controls { motion: Motion::Reduced, ..Controls::default() }); view.fit(latkit::Fit::All); frame(&gpu, &mut view); let Camera::Map { zoom: framed, .. } = *view.camera() else { panic!("a diagram's camera") }; - // Just short of the deepest zoom what was framed allows, the wheel turns toward a corner. + // Just short of the deepest zoom the framed content allows, the wheel zooms toward a corner. let near = Camera::Map { center: [5.0, -3.0, 0.0], zoom: framed + 7.95, pitch: 0.0, bearing: 0.0 }; view.set_camera(near); let wheel = Input::Wheel { point: [C + 30.0, C - 20.0], delta: [0.0, -400.0], modifiers: Modifiers::default() }; @@ -440,7 +444,7 @@ fn zooming_at_the_fits_limit_moves_nothing() { for _ in 0..3 { view.input(&wheel, common::now()); step(&gpu, &mut view); - assert_eq!(*view.camera(), at_limit, "past the limit the camera holds still, center and all"); + assert_eq!(*view.camera(), at_limit, "past the limit neither the zoom nor the center moves"); } } @@ -454,10 +458,10 @@ fn zoom(view: &View) -> f64 { } #[test] -fn reduced_motion_pays_every_move_at_once() { +fn reduced_motion_applies_every_move_at_once() { let Some(gpu) = gpu() else { return }; let wheel = Input::Wheel { point: [C, C], delta: [0.0, -120.0], modifiers: Modifiers::default() }; - // With motion, a wheel notch glides over frames; without, it lands in one. + // With motion, a wheel notch glides over several frames. Without, it applies in one. let mut moving = view(&gpu, Controls { motion: Motion::Full, ..Controls::default() }); let before = zoom(&moving); moving.input(&wheel, common::now()); @@ -467,8 +471,8 @@ fn reduced_motion_pays_every_move_at_once() { let mut still = view(&gpu, Controls { motion: Motion::Reduced, ..Controls::default() }); still.input(&wheel, common::now()); step(&gpu, &mut still); - assert!((zoom(&still) - before - 0.3).abs() < 1e-6, "lands at once: {}", zoom(&still) - before); - // The system's preference reduces it too, and a flight jumps. + assert!((zoom(&still) - before - 0.3).abs() < 1e-6, "applies at once: {}", zoom(&still) - before); + // The system preference reduces motion too, and a flight jumps to its end. let mut system = view(&gpu, Controls::default()); system.set_reduced_motion(true); let to = Camera::Map { center: [-10.0, 5.0, 0.0], zoom: 1.0, pitch: 0.0, bearing: 0.0 }; diff --git a/crates/latkit/tests/graph.rs b/crates/latkit/tests/graph.rs index f8eddc77..1311d60f 100644 --- a/crates/latkit/tests/graph.rs +++ b/crates/latkit/tests/graph.rs @@ -1,7 +1,8 @@ -//! The graph: edges joining the same two vertices bow into lanes, edges bend through the points of -//! a path field, edges end in arrowheads, and great circles settle while their vertices animate; in a -//! diagram, nets join the ports of the vertices naming them, a block makes room for its ports and -//! drops its shadow onto the sheet, and orthogonal wires route around blocks. +//! Graphs. Edges between the same two vertices bow into lanes, edges bend through the points of a +//! path field, edges end in arrowheads, and great-circle edges settle while their vertices animate. +//! In a diagram, nets join the ports of the vertices that reference them, a block grows to fit its +//! ports and drops its shadow onto the sheet, and orthogonal wires route around blocks. Ports pick +//! by name, and neighbors follow edges and nets. #![cfg(feature = "gpu")] @@ -32,7 +33,7 @@ fn nodes(x: Vec, y: Vec) -> Table { Table::new("Node", ids).field("x", x).field("y", y) } -/// Renders frames until `pending` resolves, within the settle deadline. +/// Renders frames until `pending` resolves, within `SETTLE` frames. fn answer(gpu: &latkit::Gpu, view: &mut View, pending: impl Future>) -> T { let mut pending = pin!(pending); let mut context = Context::from_waker(Waker::noop()); @@ -61,8 +62,8 @@ fn edges_joining_the_same_vertices_bow_into_lanes_in_world_units() { .draw(Edges::of("graph", "Link").ends("from", "to").width(2.0).color(latkit::Color::WHITE)) .draw(Vertices::of("graph", "Node").position("x", "y").radius(2.0)); let mut view = view(&gpu, scene, &data); - // Half a lane each way, bowing 0.12 of the edges' 80 units a lane: 4.8 units off at the middle, - // which zooming in scales as it does the rest of the drawing. + // The two edges bow half a lane each way. A lane is 0.12 of their 80-unit length, so each is + // 4.8 units off center at the middle. Zooming scales that like the rest of the drawing. for (zoom, off) in [(0.0, 5), (1.0, 10)] { view.set_camera(latkit::Camera::Map { center: [0.0; 3], zoom, pitch: 0.0, bearing: 0.0 }); let pixels = frame(&gpu, &mut view); @@ -104,7 +105,7 @@ fn edges_bend_through_their_points_and_pick_there() { } #[test] -fn nets_join_the_ports_naming_them() { +fn nets_join_the_ports_that_reference_them() { let Some(gpu) = gpu() else { return }; let data = Data::new("graph"); data.apply( @@ -145,7 +146,7 @@ fn nets_of_more_than_two_meet_at_a_junction() { .draw(Vertices::of("graph", "Node").position("x", "y").radius(2.0).port("net", Side::Left)); let mut view = view(&gpu, scene, &data); let pixels = frame(&gpu, &mut view); - // The junction sits at the middle of the three ports, 15 units down and 2 left. + // The junction sits at the center of the three ports, 15 units down and 2 left. assert!(lit(at(&pixels, C - 2, C + 15)), "at the junction"); assert!(lit(at(&pixels, C - 2, C + 30)), "to the third terminal"); assert!(!lit(at(&pixels, C, C - 20)), "nothing above"); @@ -166,7 +167,7 @@ fn an_unlabeled_block_makes_room_for_its_ports() { .fold(Vertices::of("graph", "Node").position("x", "y"), |v, port| v.port(port, Side::Left)); let mut view = view(&gpu, Scene::new(diagram()).draw(block), &data); let pixels = frame(&gpu, &mut view); - // Four ports a pitch apart down its left side, a pitch to spare: about 77 units tall. + // Four ports one pitch apart down its left side, plus a pitch of margin: about 77 units tall. assert!(lit(at(&pixels, C, C + 30)) && lit(at(&pixels, C, C - 30)), "tall enough for its ports"); assert!(!lit(at(&pixels, C, C + 45)), "and no taller"); assert_eq!(errors(&mut view), Vec::new()); @@ -193,11 +194,12 @@ fn a_block_drops_its_shadow_onto_the_sheet_below_it() { view.set_sources([&data]).expect("a source"); let pixels = frame(&gpu, &mut view); assert_eq!(errors(&mut view), Vec::new()); - // y runs down the sheet: just under the block's bottom edge, 12 units below its middle. + // Y points down the sheet. Sample just under the block's bottom edge, 12 units below its + // center. at(&pixels, C, C + 15)[0] }; let (dropped, bare) = (below(true), below(false)); - assert!(dropped + 20 < bare, "the shadow darkens the sheet below: {dropped} under {bare}"); + assert!(dropped + 20 < bare, "the shadow darkens the sheet below: {dropped} with, {bare} without"); } #[test] @@ -218,7 +220,8 @@ fn edges_along_great_circles_settle_while_their_vertices_pulse() { )), ) .expect("valid"); - // The vertices' color reads the clock, so they evaluate every frame; nothing their arcs bend by moves. + // The vertices' color reads the clock, so they evaluate every frame, but nothing the arcs + // depend on moves. let pulse = latkit::elapsed().sin().colormap(latkit::Colormap::VIRIDIS).domain(-1.0..=1.0); let scene = Scene::new(latkit::Earth { graticule: false, @@ -322,14 +325,14 @@ fn orthogonal_routes_run_around_vertices() { let mut view = view(&gpu, scene, &data); let pixels = frame(&gpu, &mut view); let green = |y: u32| at(&pixels, C, y)[1] > 120 && at(&pixels, C, y)[0] < 60; - // A straight edge would run under node 3, hidden; the route goes over or under it. + // A straight edge would run hidden under node 3. The route goes above or below it. let around = (C - 45..C - 11).chain(C + 11..C + 45).any(green); assert!(around, "around the vertex between"); assert_eq!(errors(&mut view), Vec::new()); } #[test] -fn many_routes_route_in_a_job_and_land() { +fn many_routes_route_in_a_job_and_arrive() { let Some(gpu) = gpu() else { return }; let links = |n: usize| { let data = Data::new("graph"); @@ -348,7 +351,7 @@ fn many_routes_route_in_a_job_and_land() { .draw(Vertices::of("graph", "Node").position("x", "y").size(20.0, 20.0).marker(latkit::Shape::Rectangle)); let mut view = view(&gpu, scene, &links(1)); frame(&gpu, &mut view); - // More wires than route on the frame: they draw as elbows until the job's routes land. + // More wires than one frame routes: they draw as elbows until the job's routes arrive. view.set_sources([links(64)]).expect("a source"); for _ in 0..4 { step(&gpu, &mut view); diff --git a/crates/latkit/tests/groups.rs b/crates/latkit/tests/groups.rs index 65c49408..fd8882da 100644 --- a/crates/latkit/tests/groups.rs +++ b/crates/latkit/tests/groups.rs @@ -1,6 +1,6 @@ -//! Groups: a diagram's vertex mark whose rows contain rows draws each as a container around its -//! members, padded and with its title's header on top, the same at every zoom; lit, it glows about -//! its border and hides nothing it holds. +//! Groups: a diagram vertex mark whose rows contain other rows draws each row as a padded container +//! around its members, with a title header on top. It scales only with the zoom. When lit, it glows +//! around its border and hides none of its members. #![cfg(feature = "gpu")] @@ -11,7 +11,7 @@ use latkit::{Camera, Color, Data, Edges, Ids, Patch, Scene, Table, Theme, Vertic const C: u32 = SIZE / 2; -/// Two nodes in area 1 and one outside it, linked in a chain; y runs down. +/// Two nodes in area 1 and one outside it, linked in a chain. Y points down. fn graph() -> Data { let data = Data::new("graph"); data.apply( @@ -54,11 +54,11 @@ fn blue(pixels: &[[u8; 4]], x: u32, y: u32) -> bool { } #[test] -fn groups_stand_around_their_members() { +fn groups_enclose_their_members() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, 0.0); let pixels = frame(&gpu, &mut view); - // Members span −19…19 across and −4…4 down; the group pads them by 10 units. + // Members span −19…19 across and −4…4 down. The group pads them by 10 units. assert!(blue(&pixels, C - 27, C) && blue(&pixels, C + 27, C) && blue(&pixels, C, C - 12), "around the members"); assert!(!blue(&pixels, C - 31, C) && !blue(&pixels, C + 31, C) && !blue(&pixels, C, C + 16), "and no further"); assert!(at(&pixels, C - 15, C)[0] > 120, "members draw over it"); @@ -66,11 +66,11 @@ fn groups_stand_around_their_members() { } #[test] -fn a_lit_group_glows_about_its_border_and_hides_nothing() { +fn a_lit_group_glows_around_its_border_and_hides_nothing() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, 0.0); frame(&gpu, &mut view); - // The pointer over the group's padding, away from its members: the group lights. + // Hovering the group's padding, away from its members, lights the group. let point = [C as f32 - 26.0, C as f32 + 12.0]; let device = latkit::Device::Mouse; view.input(&latkit::Input::Move { pointer: 1, point, modifiers: latkit::Modifiers::default(), device }, 0.0); @@ -85,9 +85,9 @@ fn a_lit_group_glows_about_its_border_and_hides_nothing() { } #[test] -fn groups_scale_with_the_zoom_and_nothing_else() { +fn groups_scale_only_with_the_zoom() { let Some(gpu) = gpu() else { return }; - // At twice the zoom the container is twice as wide: the sheet is one fixed picture. + // At twice the zoom the container is twice as wide, since the sheet scales as one picture. let mut view = view(&gpu, 1.0); let pixels = frame(&gpu, &mut view); assert!(blue(&pixels, C - 56, C) && blue(&pixels, C + 56, C), "twice as far out"); diff --git a/crates/latkit/tests/labels.rs b/crates/latkit/tests/labels.rs index 2b4efb64..c848594d 100644 --- a/crates/latkit/tests/labels.rs +++ b/crates/latkit/tests/labels.rs @@ -1,6 +1,7 @@ -//! Labels show beside their rows once the host sets their texts, or inside a block while they fit; -//! where two would overlap, the one of higher priority shows, and only labels that show have their -//! texts set. A diagram's labels scale with the sheet and fade out once too small to read. +//! Labels show beside their rows once the host rasterizes their text, or inside a block while they +//! fit. Where two would overlap, the higher-priority one shows, and only shown labels have their +//! text rasterized. A cap keeps only the highest priorities. A diagram's labels scale with the +//! sheet and fade out once too small to read. #![cfg(feature = "gpu")] @@ -14,7 +15,7 @@ use latkit::Ids; use latkit::host::{Host, Raster, TextRun}; use latkit::{Camera, Color, Data, Label, Marker, Scene, Table, Vertices, View, field}; -/// Sets a text as a solid box six tenths of its size wide per character, as tall as its size. +/// A host that rasterizes text as a solid box, 0.6 × size wide per character and `size` tall. struct Boxes; impl Host for Boxes { @@ -24,7 +25,7 @@ impl Host for Boxes { } } -/// Boxes, counting the texts it sets. +/// `Boxes`, counting the texts it rasterizes. struct Counted(Arc); impl Host for Counted { @@ -33,7 +34,7 @@ impl Host for Counted { Boxes.rasterize(run) } - // Measuring sets no text. + // Measuring rasterizes nothing, so it is not counted. fn measure(&self, run: &TextRun) -> latkit::Result<[f32; 2]> { Boxes.measure(run) } @@ -77,7 +78,7 @@ fn labels_show_beside_their_rows() { let mut view = view(&gpu, vec![-50.0, 10.0], vec![0.0, 0.0]); let pixels = frame(&gpu, &mut view); let c = SIZE / 2; - // A label starts four pixels past its marker's right edge, centered on it. + // A label starts four pixels past its marker's right edge, vertically centered on it. let first = at(&pixels, c - 50 + 12, c); let second = at(&pixels, c + 10 + 12, c); assert!(first[0] > 200 && first[2] < 60, "the first row's label: {first:?}"); @@ -88,7 +89,8 @@ fn labels_show_beside_their_rows() { #[test] fn a_color_changing_every_frame_leaves_labels_placed() { let Some(gpu) = gpu() else { return }; - // A pulse reads the clock, so the mark evaluates every frame; nothing that places its labels moved. + // A pulse reads the clock, so the mark evaluates every frame, but nothing that places its + // labels moves. let pulse = latkit::elapsed().sin().colormap(latkit::Colormap::VIRIDIS).domain(-1.0..=1.0); let mut view = shown(&gpu, vec![-50.0, 10.0], vec![0.0, 0.0], |v| v.radius(2.0).color(pulse.clone())); for _ in 0..30 { @@ -98,11 +100,11 @@ fn a_color_changing_every_frame_leaves_labels_placed() { } #[test] -fn a_block_holds_its_label_while_it_fits() { +fn a_block_shows_its_label_inside_while_it_fits() { let Some(gpu) = gpu() else { return }; let c = SIZE / 2; let green = Color::rgb(0.0, 1.0, 0.0); - // The label is 36 by 10 pixels: a 60 by 20 block holds it, a 20 by 20 one does not. + // The label is 36 by 10 pixels. A 60 by 20 block fits it, and a 20 by 20 block does not. let mut wide = shown(&gpu, vec![0.0], vec![0.0], |v| v.size(60.0, 20.0).color(green)); let middle = at(&frame(&gpu, &mut wide), c, c); assert!(middle[0] > 200 && middle[1] < 60, "the label inside: {middle:?}"); @@ -112,7 +114,7 @@ fn a_block_holds_its_label_while_it_fits() { } #[test] -fn overlapping_labels_give_way_by_priority() { +fn overlapping_labels_show_the_higher_priority_one() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, vec![0.0, 4.0], vec![1.0, 5.0]); let pixels = frame(&gpu, &mut view); @@ -120,15 +122,15 @@ fn overlapping_labels_give_way_by_priority() { let shared = at(&pixels, c + 20, c); assert!(shared[2] > 200 && shared[0] < 60, "the higher priority label shows: {shared:?}"); let only_first = at(&pixels, c + 7, c); - assert!(only_first[0] < 60, "the lower one gave way: {only_first:?}"); + assert!(only_first[0] < 60, "the lower one is hidden: {only_first:?}"); } #[test] -fn only_labels_that_show_have_their_texts_set() { +fn only_shown_labels_have_their_text_rasterized() { let Some(gpu) = gpu() else { return }; let set = Arc::new(AtomicUsize::new(0)); gpu.set_host(Counted(set.clone())); - // Two thousand rows in view, each named apart: far more labels than fit. + // Two thousand rows in view, each with a distinct name: far more labels than fit. let n = 2000; let x: Vec = (0..n).map(|i| f64::from((i * 37) % 120) - 60.0).collect(); let y: Vec = (0..n).map(|i| f64::from((i * 53) % 120) - 60.0).collect(); @@ -148,7 +150,7 @@ fn only_labels_that_show_have_their_texts_set() { view.set_sources([&data]).expect("a source"); let pixels = frame(&gpu, &mut view); let count = set.load(Ordering::Relaxed); - assert!(count > 0 && count < 200, "texts set: {count}"); + assert!(count > 0 && count < 200, "texts rasterized: {count}"); let red = pixels.iter().filter(|p| p[0] > 200 && p[1] < 60).count(); assert!(red > 200, "labels show: {red} pixels"); assert_eq!(errors(&mut view), Vec::new()); @@ -185,7 +187,7 @@ fn diagram_labels_scale_with_the_sheet() { let Some(gpu) = gpu() else { return }; let c = SIZE / 2; let red = |p: [u8; 4]| p[0] > 200 && p[1] < 60; - // At zoom 0 the label is 36 by 10 pixels; at zoom 1 twice that, still inside its block. + // At zoom 0 the label is 36 by 10 pixels. At zoom 1 it is twice that, still inside its block. let near = diagram_block(&gpu, 0.0); assert!(red(at(&near, c + 15, c)) && !red(at(&near, c + 21, c)), "36 wide at zoom 0"); let closer = diagram_block(&gpu, 1.0); @@ -196,7 +198,7 @@ fn diagram_labels_scale_with_the_sheet() { fn diagram_labels_fade_once_too_small_to_read() { let Some(gpu) = gpu() else { return }; let c = SIZE / 2; - // At zoom −2 the text is 2.5 pixels tall: past reading, it is gone, and the block shows. + // At zoom −2 the text is 2.5 pixels tall, too small to read, so it hides and the block shows. let far = diagram_block(&gpu, -2.0); let middle = at(&far, c, c); assert!(middle[1] > 120 && middle[0] < 60, "only the block: {middle:?}"); @@ -207,7 +209,7 @@ fn a_cap_shows_only_the_labels_of_highest_priority() { let Some(gpu) = gpu() else { return }; let set = Arc::new(AtomicUsize::new(0)); gpu.set_host(Counted(set.clone())); - // Four rows one above another, apart enough that none would give way, priorities rising upward. + // Four rows stacked vertically, far enough apart not to overlap, priority rising upward. let ys = [-45.0, -15.0, 15.0, 45.0]; let data = Data::new("d"); data.apply( @@ -226,13 +228,13 @@ fn a_cap_shows_only_the_labels_of_highest_priority() { view.set_sources([&data]).expect("a source"); let pixels = frame(&gpu, &mut view); let c = SIZE / 2; - // The plane runs y up; each label starts beside its marker. + // Y points up on the plane. Each label starts beside its marker. let labeled = |y: f64| { let row = (f64::from(c) - y) as u32; (c - 30 + 4..c - 30 + 20).any(|x| at(&pixels, x, row)[0] > 200) }; assert!(labeled(45.0) && labeled(15.0), "the two of highest priority show"); - assert!(!labeled(-15.0) && !labeled(-45.0), "the rest give way to the cap"); - assert_eq!(set.load(Ordering::Relaxed), 2, "only the labels shown have their texts set"); + assert!(!labeled(-15.0) && !labeled(-45.0), "the cap hides the rest"); + assert_eq!(set.load(Ordering::Relaxed), 2, "only the shown labels are rasterized"); assert_eq!(errors(&mut view), Vec::new()); } diff --git a/crates/latkit/tests/layouts.rs b/crates/latkit/tests/layouts.rs index 81af9528..ccb88c5b 100644 --- a/crates/latkit/tests/layouts.rs +++ b/crates/latkit/tests/layouts.rs @@ -1,8 +1,10 @@ -//! Layouts place a diagram's vertices its data gives no position: layered in ranks along the flow, -//! nets running from their outputs, stress at graph distances with boxes parted; on a plane, points -//! among their neighbors. Vertices with positions stay where they are, places hold once given, and -//! rows added later are placed around the rest until the view arranges every row again, answering -//! with a patch that writes the places into the data. +//! Layouts position the diagram vertices whose data has no position. Layered places them in ranks +//! along the flow, with nets running from their outputs. Stress places them at graph distances and +//! separates overlapping boxes. On a plane, a layout places points among their neighbors. +//! +//! Vertices with positions stay put, and computed positions stay fixed. Rows added later are +//! placed around the rest until the view arranges every row again, which resolves to a patch that +//! writes the positions into the data. The same graph always gets the same layout. #![cfg(feature = "gpu")] @@ -15,7 +17,7 @@ use std::task::{Context, Poll, Waker}; use common::{diagram, errors, frame, gpu, plain_theme, step}; use latkit::{Camera, Data, Edges, Ids, Layered, Layout, Patch, Port, Row, Scene, Side, Stress, Table, Vertices, View}; -/// Renders frames until `pending` resolves, within the settle deadline. +/// Renders frames until `pending` resolves, within `SETTLE` frames. fn answer(gpu: &latkit::Gpu, view: &mut View, pending: impl Future>) -> T { let mut pending = pin!(pending); let mut context = Context::from_waker(Waker::noop()); @@ -28,7 +30,7 @@ fn answer(gpu: &latkit::Gpu, view: &mut View, pending: impl Future) -> Data { let (x, y) = match pin { Some([x, y]) => (vec![Some(x), None, None, None], vec![Some(y), None, None, None]), @@ -62,10 +64,10 @@ fn view(gpu: &latkit::Gpu, scene: Scene, data: &Data) -> View { view } -/// Where each of nodes 1 to `count` stands, units with y down, as the view locates it on screen. +/// The positions of nodes 1 to `count`, in units with y down, from where the view draws them. fn located(gpu: &latkit::Gpu, view: &mut View, count: u64) -> Vec<[f64; 2]> { let projected = |p: [f32; 2]| { - // A quarter pixel per unit, about the middle of the view. + // A quarter pixel per unit, relative to the view's center. let c = f64::from(common::SIZE) / 2.0; [(f64::from(p[0]) - c) * 4.0, (f64::from(p[1]) - c) * 4.0] }; @@ -77,7 +79,7 @@ fn located(gpu: &latkit::Gpu, view: &mut View, count: u64) -> Vec<[f64; 2]> { .collect() } -/// Where each node settled under `layout`, units. +/// Each node's settled position under `layout`, in units. fn placed(layout: impl Into, pin: Option<[f64; 2]>) -> Vec<[f64; 2]> { let Some(gpu) = gpu() else { return Vec::new() }; let mut view = view(&gpu, scene(layout), &graph(pin)); @@ -97,8 +99,8 @@ fn layered_ranks_follow_the_flow() { return; } assert!(p.iter().all(|v| v[0].is_finite()), "every node placed and drawn: {p:?}"); - // Markers of radius 4 are 8 units across; ranks and rows of a rank stand their gaps apart edge to - // edge. + // Markers of radius 4 are 8 units across. Ranks, and rows within a rank, are their gaps apart + // edge to edge. let size = 8.0; let rank_gap = Layered::default().rank_gap; assert!((p[1][0] - p[0][0] - rank_gap - size).abs() < 1.0, "a rank apart: {p:?}"); @@ -109,7 +111,7 @@ fn layered_ranks_follow_the_flow() { #[test] fn nets_run_from_their_outputs() { - // Nodes 1 and 2 drive the net from their outputs, 3 reads it, and 4 drives 1. + // Nodes 1 and 2 drive net 1 from their outputs, node 3 reads it, and node 4 drives node 1. let Some(gpu) = gpu() else { return }; let data = Data::new("graph"); data.apply( @@ -136,7 +138,7 @@ fn nets_run_from_their_outputs() { let gap = Layered::default().rank_gap; assert!(p[0][0] - p[3][0] > gap, "1 a rank after 4, which drives it: {p:?}"); assert!((p[1][0] - p[3][0]).abs() < 1.0, "2 drives the net 3 reads, so it shares 4's rank: {p:?}"); - assert!(p[2][0] - p[0][0] > gap, "3 reads after both drivers: {p:?}"); + assert!(p[2][0] - p[0][0] > gap, "3 ranks after both drivers: {p:?}"); assert_eq!(errors(&mut view), Vec::new()); } @@ -156,7 +158,7 @@ fn stress_puts_neighbors_a_gap_apart() { } #[test] -fn stress_parts_the_boxes_it_crowds() { +fn stress_separates_overlapping_boxes() { let Some(gpu) = gpu() else { return }; let scene = Scene::new(diagram()) .theme(plain_theme()) @@ -165,7 +167,8 @@ fn stress_parts_the_boxes_it_crowds() { .draw(Vertices::of("graph", "Node").position("x", "y").size(60.0, 20.0)); let mut view = view(&gpu, scene, &graph(None)); let p = located(&gpu, &mut view, 4); - // Stress puts neighbors 40 units apart; their 60 by 20 boxes part, a quarter of the gap clear. + // Stress puts neighbors 40 units apart. Their 60 by 20 boxes separate, keeping a quarter of the + // gap clear. for i in 0..4 { for j in i + 1..4 { let (dx, dy) = ((p[i][0] - p[j][0]).abs(), (p[i][1] - p[j][1]).abs()); @@ -193,7 +196,8 @@ fn rows_added_later_move_nothing_else() { let data = graph(None); let mut view = view(&gpu, scene(Layered::default()), &data); let before = located(&gpu, &mut view, 4); - // A fifth node joins the end of the chain: the four placed hold, and it is placed after them. + // A fifth node joins the end of the chain. The four placed nodes stay put, and it goes after + // them. data.apply( Patch::new() .set(Table::new("Node", vec![5_u64]).field("x", vec![None::]).field("y", vec![None::])) @@ -207,10 +211,10 @@ fn rows_added_later_move_nothing_else() { frame(&gpu, &mut view); let after = located(&gpu, &mut view, 5); for k in 0..4 { - assert!(apart(before[k], after[k]) < 1e-3, "node {} held: {before:?} → {after:?}", k + 1); + assert!(apart(before[k], after[k]) < 1e-3, "node {} stays put: {before:?} → {after:?}", k + 1); } - assert!(after[4][0] > after[2][0], "the new node after the one it follows: {after:?}"); - // Arranged, every row is laid out again, and the answer writes the places into the data. + assert!(after[4][0] > after[2][0], "the new node is placed after node 3: {after:?}"); + // Arranging lays out every row again, and its patch writes the positions into the data. let pending = view.arrange(); let patches = answer(&gpu, &mut view, pending); frame(&gpu, &mut view); @@ -220,11 +224,11 @@ fn rows_added_later_move_nothing_else() { assert_eq!(patch.data().map(latkit::Name::as_str), Some("graph")); data.apply(patch.clone()).expect("valid"); let node = data.snapshot().query(&latkit::Query::of("Node")).expect("nodes").table; - assert!(node.ids().all(|id| node.get(id, "x").is_some()), "every node has its place as data"); + assert!(node.ids().all(|id| node.get(id, "x").is_some()), "every node's position is in the data"); frame(&gpu, &mut view); let pinned = located(&gpu, &mut view, 5); for k in 0..5 { - assert!(apart(arranged[k], pinned[k]) < 0.5, "node {} holds its place: {arranged:?} → {pinned:?}", k + 1); + assert!(apart(arranged[k], pinned[k]) < 0.5, "node {} stays put: {arranged:?} → {pinned:?}", k + 1); } assert_eq!(errors(&mut view), Vec::new()); } @@ -241,19 +245,19 @@ fn a_plane_places_points_without_positions_by_its_layout() { }; let mut view = view(&gpu, scene(Stress { gap: 40.0 }.into()), &graph(Some([-100.0, 60.0]))); let p = located(&gpu, &mut view, 4); - // The plane runs y up: on screen node 1 stands 60 units above the middle. - assert!(apart(p[0], [-100.0, -60.0]) < 1.0, "node 1 where its data says: {p:?}"); + // Y points up on the plane, so on screen node 1 is 60 units above the center. + assert!(apart(p[0], [-100.0, -60.0]) < 1.0, "node 1 at its data position: {p:?}"); assert!(p.iter().all(|v| v[0].is_finite()), "every node placed: {p:?}"); assert!((apart(p[0], p[1]) - 40.0).abs() < 15.0, "node 2 a gap from node 1: {p:?}"); // Without a layout, rows without positions are not drawn. view.set_scene(scene(Layout::None)).expect("valid scene"); frame(&gpu, &mut view); - assert!(located(&gpu, &mut view, 2)[1][0].is_nan(), "unplaced"); + assert!(located(&gpu, &mut view, 2)[1][0].is_nan(), "node 2 is not drawn"); assert_eq!(errors(&mut view), Vec::new()); } #[test] -fn layouts_answer_the_same_for_the_same_graph() { +fn layouts_place_the_same_graph_the_same_way() { for layout in [Layout::from(Stress::default()), Layout::from(Layered::default())] { let (a, b) = (placed(layout, None), placed(layout, None)); assert_eq!(a, b, "{layout:?}"); diff --git a/crates/latkit/tests/markers.rs b/crates/latkit/tests/markers.rs index ee88b9e3..9e106bc1 100644 --- a/crates/latkit/tests/markers.rs +++ b/crates/latkit/tests/markers.rs @@ -1,5 +1,6 @@ -//! Markers draw what their inputs say: a gauge filled so far, a pie's slices, an icon, a custom WGSL -//! marker, and a status ring; a custom marker that does not compile is told, not drawn. +//! Markers draw from their inputs: a gauge filled to its value, a pie's slices, an icon, a custom +//! WGSL marker, and a status ring. A custom marker that fails to compile is reported as an error, +//! not drawn. #![cfg(feature = "gpu")] @@ -56,7 +57,7 @@ fn a_pie_cuts_slices_in_their_colors() { } #[test] -fn icons_show_the_image_their_index_picks() { +fn icons_show_the_image_at_their_index() { if common::gpu().is_none() { return; } @@ -69,7 +70,7 @@ fn icons_show_the_image_their_index_picks() { } #[test] -fn custom_markers_draw_their_wgsl_and_bad_ones_are_told() { +fn custom_markers_draw_their_wgsl_and_bad_ones_are_reported() { if common::gpu().is_none() { return; } @@ -89,7 +90,7 @@ fn custom_markers_draw_their_wgsl_and_bad_ones_are_told() { frame(&gpu, &mut view); frame(&gpu, &mut view); let told = view.events().any(|e| matches!(e, Event::Error(error) if error.to_string().contains("custom marker"))); - assert!(told, "the broken marker is told"); + assert!(told, "the broken marker is reported"); } #[test] @@ -111,10 +112,10 @@ fn status_rings_surround_their_markers() { } #[test] -fn a_missing_status_draws_no_ring_whichever_branch_it_is() { +fn a_missing_status_draws_no_ring_in_either_branch() { let Some(gpu) = gpu() else { return }; let red = Color::rgb(1.0, 0.0, 0.0); - // Only the alarmed row on the left rings. + // Only the alarmed row, on the left, gets a ring. let forms: [Expr; 2] = [ when(field("alarm").gt(0.5)).then(red).otherwise(Expr::missing()), when(field("alarm").lt(0.5)).then(Expr::missing()).otherwise(red), @@ -132,7 +133,7 @@ fn a_missing_status_draws_no_ring_whichever_branch_it_is() { let pixels = frame(&gpu, &mut view); let c = SIZE / 2; let alarmed = at(&pixels, c - 30 + 11, c); - assert!(alarmed[0] > 200 && alarmed[1] < 80, "the alarmed row rings: {alarmed:?}"); + assert!(alarmed[0] > 200 && alarmed[1] < 80, "the alarmed row has a ring: {alarmed:?}"); let quiet = at(&pixels, c + 30 + 11, c); assert!(quiet[0] < 80, "the quiet row does not: {quiet:?}"); assert_eq!(errors(&mut view), Vec::new()); diff --git a/crates/latkit/tests/picks.rs b/crates/latkit/tests/picks.rs index 6f3a496c..58b49494 100644 --- a/crates/latkit/tests/picks.rs +++ b/crates/latkit/tests/picks.rs @@ -1,5 +1,5 @@ -//! Queries answer through what the frame drew: every row within reach of a point, nearest first; the -//! rows in a box a Shift drag draws; where a row lies; and the sample a traced line passes through. +//! Queries answered from what the frame drew: every row within reach of a point, nearest first, the +//! rows in a Shift-drag box, a row's screen position, and the sample a traced line passes through. #![cfg(feature = "gpu")] @@ -15,7 +15,7 @@ use latkit::{ Table, Vertices, View, }; -/// Renders frames until `pending` resolves, within the settle deadline. +/// Renders frames until `pending` resolves, within `SETTLE` frames. fn answer(gpu: &latkit::Gpu, view: &mut View, pending: impl Future>) -> T { let mut pending = pin!(pending); let mut context = Context::from_waker(Waker::noop()); @@ -77,13 +77,13 @@ fn picks_list_every_row_in_reach_nearest_first() { } #[test] -fn a_hit_names_the_row_and_place_its_frame_drew() { +fn a_hit_reports_the_row_and_position_from_its_frame() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu, Controls::default()); let c = SIZE as f32 / 2.0; let pending = view.pick([c, c], 6.0); - // The frame answering draws node 1 at the middle; before its answer lands, the nodes are given - // in another order and the camera moves away. + // The answering frame draws node 1 at the center. Before its answer arrives, the nodes are + // reordered and the camera moves away. step(&gpu, &mut view); let data = graph(); let reordered = @@ -101,8 +101,8 @@ fn a_hit_names_the_row_and_place_its_frame_drew() { #[test] fn every_hit_of_geodesic_edges_lies_in_reach() { let Some(gpu) = gpu() else { return }; - // A lattice on the globe: geodesic edges drawn through a segment list, which draws fewer segments - // than the query's list is sized for. + // A lattice on the globe. Geodesic edges draw through a segment list, which holds fewer + // segments than the query's list is sized for. let side = 24_usize; let (mut lon, mut lat, mut from, mut to) = (Vec::new(), Vec::new(), Vec::new(), Vec::new()); for i in 0..side * side { @@ -135,13 +135,14 @@ fn every_hit_of_geodesic_edges_lies_in_reach() { view.fit(latkit::Fit::All); frame(&gpu, &mut view); let c = SIZE as f32 / 2.0; - // A wide pick first leaves its counts in the lists buffer; a narrow one after must not sum them. + // A wide pick leaves its counts in the lists buffer. A later narrow pick must not add to them. for radius in [40.0, 4.0] { let pending = view.pick([c, c], radius); let hits = answer(&gpu, &mut view, pending); assert!(!hits.is_empty(), "the middle of the lattice is in reach"); for hit in &hits { - // A vertex is reached at its outline and lies at its center, a marker's radius further. + // A vertex is in reach at its outline, but its point is its center, a marker radius + // further. let reach = (hit.point[0] - c).hypot(hit.point[1] - c); let what = format!("{} {} at {:?}, {reach} away", hit.row.table, hit.row.id, hit.point); assert!(hit.distance <= radius && reach <= radius + 8.0, "{what}"); @@ -185,7 +186,7 @@ fn a_shift_drag_selects_the_rows_in_its_box() { } #[test] -fn hovering_a_trace_names_its_sample() { +fn picking_a_trace_returns_its_sample() { let Some(gpu) = gpu() else { return }; let data = Data::new("signals"); data.apply(Table::new("Signal", Ids::count(1))).expect("valid"); diff --git a/crates/latkit/tests/render.rs b/crates/latkit/tests/render.rs index 74f96549..5953797a 100644 --- a/crates/latkit/tests/render.rs +++ b/crates/latkit/tests/render.rs @@ -1,4 +1,4 @@ -//! Frames rendered headless and read back: what each mark draws lands where and how it should. +//! Frames rendered headless and read back to check where and how each mark draws. #![cfg(feature = "gpu")] @@ -44,7 +44,7 @@ fn vertices_draw_at_their_positions_in_their_colors() { } #[test] -fn marks_ask_whether_texts_hold_a_text_as_the_texts_change() { +fn contains_ignores_case_and_rechecks_changed_texts() { let Some(gpu) = gpu() else { return }; let data = Data::new("graph"); data.apply( @@ -66,9 +66,9 @@ fn marks_ask_whether_texts_hold_a_text_as_the_texts_change() { view.set_sources([&data]).expect("a source"); let center = SIZE / 2; let shown = |pixels: &[[u8; 4]]| [center - 30, center, center + 30].map(|x| at(pixels, x, center)[0] > 128); - assert_eq!(shown(&frame(&gpu, &mut view)), [false, true, false], "ignoring case, Beta alone holds ta"); + assert_eq!(shown(&frame(&gpu, &mut view)), [false, true, false], "ignoring case, only Beta contains ta"); data.apply(Table::new("Node", [0_u64]).field("name", vec!["Delta"])).expect("valid"); - assert_eq!(shown(&frame(&gpu, &mut view)), [true, true, false], "a text that changes asks again"); + assert_eq!(shown(&frame(&gpu, &mut view)), [true, true, false], "a changed text is checked again"); } #[test] @@ -97,8 +97,8 @@ fn a_stroke_in_pixels_keeps_its_width_as_a_diagram_zooms() { (0..SIZE).filter(|&y| at(&pixels, SIZE / 2, y)[1] > 128).count() }; let (px, units) = (thickness(latkit::px(2.0)), thickness(2.0.into())); - assert!((1..=3).contains(&px), "two CSS pixels at twice the size: {px}"); - assert!((4..=5).contains(&units), "two units, twice the pixels: {units}"); + assert!((1..=3).contains(&px), "a 2 px width stays 2 pixels at zoom 1: {px}"); + assert!((4..=5).contains(&units), "a 2-unit width is 4 pixels at zoom 1: {units}"); } #[test] @@ -125,7 +125,7 @@ fn channels_follow_the_selection_and_the_zoom() { "selected: {:?}", at(&pixels, center + 30, center) ); - // What the selection leaves out dims, in its own color. + // Rows outside the selection dim, keeping their own color. let [r, g, b, _] = at(&pixels, center, center); assert!(b > 60 && r.max(g) < 20, "not selected: {:?}", [r, g, b]); view.set_camera(Camera::Map { center: [0.0; 3], zoom: 1.0, pitch: 0.0, bearing: 0.0 }); @@ -182,13 +182,48 @@ fn edges_join_their_ends_and_colormaps_fit_the_values() { assert!(middle[0] > 80 && middle[0] < 130, "the middle value is grey: {middle:?}"); } +#[test] +fn a_colormap_draws_a_missing_value_in_the_themes_missing_color() { + let Some(gpu) = gpu() else { return }; + let data = Data::new("grid"); + data.apply( + Table::new("Bus", Ids::count(3)) + .field("x", vec![-40.0, 0.0, 40.0]) + .field("y", vec![0.0; 3]) + .field("v", vec![Some(0.0), None, Some(1.0)]), + ) + .expect("valid"); + let [red, blue] = [Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0)]; + let plain = plain_theme(); + let theme = Theme { missing: red, vertex: latkit::VertexTheme { color: blue, ..plain.vertex }, ..plain }; + let scene = |color: latkit::Paint| { + Scene::new(plane()) + .theme(theme.clone()) + .draw(Vertices::of("grid", "Bus").position("x", "y").radius(6.0).color(color)) + }; + let mut view = + View::new(&gpu, scene(field("v").colormap(Colormap::GRAYS).domain(0.0..=1.0).into())).expect("valid"); + view.set_camera(camera()); + view.set_sources([&data]).expect("a source"); + let pixels = frame(&gpu, &mut view); + let center = SIZE / 2; + assert!(close(at(&pixels, center - 40, center), [0, 0, 0, 255]), "0 maps to black"); + assert!(close(at(&pixels, center + 40, center), [255, 255, 255, 255]), "1 maps to white"); + let gap = at(&pixels, center, center); + assert!(close(gap, [255, 0, 0, 255]), "a missing value takes the theme's missing color: {gap:?}"); + // An option given `missing()` takes the mark's theme color instead. + view.set_scene(scene(latkit::Expr::missing().into())).expect("valid"); + let pixel = at(&frame(&gpu, &mut view), center, center); + assert!(close(pixel, [0, 0, 255, 255]), "missing() takes the vertex color: {pixel:?}"); +} + #[test] fn edges_ending_far_ahead_and_behind_the_eye_draw_whole() { let Some(gpu) = gpu() else { return }; - // A plane seen steeply from close by: from the middle one edge runs far ahead, its far end deep in - // the view, the other far back, under and past the eye. Each draws all the way across the view: - // the one ahead unbroken however differently deep its ends are, the one back cut where it leaves - // the view, not dropped. + // A plane seen steeply from close by. From the center, one edge runs far ahead, its far end + // deep in the view, and the other runs far back, under and past the eye. Each draws all the + // way across the view. The one ahead is unbroken however much its ends' depths differ. The one + // behind is clipped where it leaves the view, not dropped. let data = Data::new("grid"); data.apply( Patch::new() @@ -210,13 +245,13 @@ fn edges_ending_far_ahead_and_behind_the_eye_draw_whole() { let pixels = frame(&gpu, &mut view); let middle = SIZE / 2; let column: String = (0..SIZE).map(|y| if at(&pixels, middle, y)[1] > 128 { '#' } else { '.' }).collect(); - assert!(!column.contains('.'), "the edges down the middle, top to bottom: {column}"); + assert!(!column.contains('.'), "the edges run down the middle, top to bottom: {column}"); } #[test] fn great_circles_longer_than_their_list_coarsen_instead_of_stopping_short() { let Some(gpu) = gpu() else { return }; - // One edge a third of the way around the equator: sixty arcs, where its list first keeps six. + // One edge a third of the way around the equator: sixty arcs, where its list first holds six. let data = Data::new("grid"); data.apply( Patch::new() @@ -273,9 +308,9 @@ fn new_data_versions_show_and_fit_frames_them() { } #[test] -fn a_fit_frames_what_draws_while_another_mark_has_no_rows() { +fn a_mark_with_no_rows_does_not_delay_a_fit() { let Some(gpu) = gpu() else { return }; - // The links table is empty: it has nothing to frame, and holds no fit back. + // The links table is empty. It has nothing to frame and does not delay the fit. let data = Data::new("graph"); data.apply( Patch::new().set(Table::new("Node", Ids::count(1)).field("x", vec![1000.0]).field("y", vec![1000.0])).set( @@ -301,7 +336,7 @@ fn a_fit_frames_what_draws_while_another_mark_has_no_rows() { } #[test] -fn a_fit_frames_data_whatever_its_units_and_its_markers_pixels() { +fn a_fit_frames_data_regardless_of_units_and_marker_sizes() { let Some(gpu) = gpu() else { return }; let data = Data::new("graph"); data.apply(Table::new("Node", Ids::count(2)).field("x", vec![0.0, 1.0]).field("y", vec![0.0, 1.0])).expect("valid"); @@ -312,7 +347,7 @@ fn a_fit_frames_data_whatever_its_units_and_its_markers_pixels() { for _ in 0..4 { frame(&gpu, &mut view); } - // Markers a few pixels across leave a unit square filling the view, not lost in its middle. + // Markers a few pixels across do not shrink the fit: the unit square fills most of the view. let [a, b] = [[0.0, 0.0, 0.0], [1.0, 1.0, 0.0]].map(|p| view.screen(p).expect("in view")); let across = (b[0] - a[0]).abs().min((b[1] - a[1]).abs()); assert!(across > SIZE as f32 * 0.5, "the unit square spans {across} of {SIZE} pixels"); @@ -338,8 +373,9 @@ fn sampled_fields_read_at_the_shown_coordinate() { view.set_at(t); let pixel = at(&frame(&gpu, &mut view), center, center); if t < 0.0 { - // Before the first frame there is no value: the vertex takes the theme's color. - assert_ne!(pixel, [0, 0, 0, 255], "at {t}"); + // Before the first frame there is no value, so the vertex takes the theme's missing + // color. + assert!(close(pixel, plain_theme().missing.to_rgba8()), "at {t}: {pixel:?}"); } else { assert!(pixel[0].abs_diff(want) <= 3, "at {t}: {pixel:?}"); } @@ -356,7 +392,7 @@ fn sampled_fields_read_at_the_shown_coordinate() { } #[test] -fn faces_blend_their_corners_and_hide_what_lies_behind() { +fn faces_blend_their_corners_and_hide_vertices_behind_them() { let Some(gpu) = gpu() else { return }; let data = Data::new("mesh"); data.apply( @@ -432,7 +468,7 @@ fn presses_click_and_drag_the_rows_they_hit() { .collect(); assert_eq!(drags.iter().map(|d| d.0).collect::>(), [Phase::Start, Phase::End]); let end = drags.last().expect("an end").1; - assert!((end[0] - 10.0).abs() < 0.5 && (end[1] - 20.0).abs() < 0.5, "the pointer's position: {end:?}"); + assert!((end[0] - 10.0).abs() < 0.5 && (end[1] - 20.0).abs() < 0.5, "the drag ends at the pointer: {end:?}"); assert_eq!(view.camera(), &camera(), "dragging a row does not pan"); } @@ -476,15 +512,15 @@ fn lines_traced_in_priority_order_draw_their_whole_span() { view.set_sources([&data]).expect("a source"); let pixels = frame(&gpu, &mut view); let center = SIZE / 2; - // Ordered by priority, the table draws in part; its trace still spans every frame, not only - // those about the coordinate shown, at its start. + // Ordered by priority, the table is drawn as a paged table. Its trace still spans every frame, + // not only those near the shown coordinate at its start. for x in [center - 40, center, center + 40] { assert!(close(at(&pixels, x, center), [255, 0, 0, 255]), "at {x}: {:?}", at(&pixels, x, center)); } } #[test] -fn a_plot_frames_its_traces_whole_span_wherever_it_shows() { +fn a_plot_fit_frames_the_whole_trace_at_any_shown_coordinate() { let Some(gpu) = gpu() else { return }; let data = Data::new("signals"); data.apply(Table::new("Signal", Ids::count(1))).expect("valid"); @@ -545,29 +581,29 @@ fn new_data_and_the_scene_reading_it_change_together() { view.set_camera(camera()); view.set_sources([dataset("a")]).expect("a source"); frame(&gpu, &mut view); - // Data the mark no longer fits, then the scene that fits it, before the frame: nothing to report. + // Data the mark no longer fits, then a scene that fits it, both before the frame: no error. view.set_sources([dataset("b")]).expect("a source"); view.set_scene(reading("b")).expect("valid scene"); let pixels = frame(&gpu, &mut view); assert_eq!(common::errors(&mut view), Vec::new()); let center = SIZE / 2; assert!(at(&pixels, center, center)[0] > 200, "the new mark draws: {:?}", at(&pixels, center, center)); - // Data no mark fits by the frame is told then, once, and listed until data fits it again. + // Data that no mark fits by the frame is reported then, once, and listed until data fits again. view.set_sources([dataset("a")]).expect("a source"); frame(&gpu, &mut view); let errors = common::errors(&mut view); assert!(errors.len() == 1 && errors[0].to_string().contains("no field b"), "{errors:?}"); - assert_eq!(view.failures().collect::>(), errors, "listed as it was told"); + assert_eq!(view.failures().collect::>(), errors, "failures() lists the reported error"); view.set_camera(Camera::Map { center: [5.0, 0.0, 0.0], zoom: 0.0, pitch: 0.0, bearing: 0.0 }); frame(&gpu, &mut view); - assert_eq!(common::errors(&mut view), Vec::new(), "told once"); + assert_eq!(common::errors(&mut view), Vec::new(), "reported once"); view.set_sources([dataset("b")]).expect("a source"); frame(&gpu, &mut view); - assert_eq!(view.failures().count(), 0, "data that fits clears it"); + assert_eq!(view.failures().count(), 0, "data that fits clears the failure"); } #[test] -fn a_dataset_not_given_is_waited_on_and_a_table_it_lacks_fails() { +fn a_missing_dataset_is_waited_for_and_a_missing_table_fails() { let Some(gpu) = gpu() else { return }; let scene = Scene::new(plane()).theme(plain_theme()).draw(Vertices::of("d", "Bus").position("x", "y")); let mut view = View::new(&gpu, scene).expect("valid scene"); @@ -579,11 +615,11 @@ fn a_dataset_not_given_is_waited_on_and_a_table_it_lacks_fails() { frame(&gpu, &mut view); let errors = common::errors(&mut view); assert!(errors.len() == 1 && errors[0].to_string().contains("no table 'Bus'"), "{errors:?}"); - assert!(view.complete(), "a mark that failed stands in for nothing"); + assert!(view.complete(), "a failed mark leaves nothing to wait on"); } #[test] -fn hover_reaches_what_faces_leave_in_view() { +fn hover_skips_vertices_hidden_behind_faces() { let Some(gpu) = gpu() else { return }; let data = Data::new("mesh"); data.apply( @@ -702,7 +738,7 @@ fn long_histories_draw_from_their_levels_of_detail() { } #[test] -fn frames_tell_what_they_cost_the_gpu_where_it_measures() { +fn frames_report_gpu_time_where_timestamps_are_supported() { let Some(gpu) = gpu() else { return }; if !gpu.device().features().contains(wgpu::Features::TIMESTAMP_QUERY) { return; @@ -720,8 +756,8 @@ fn frames_tell_what_they_cost_the_gpu_where_it_measures() { common::finish(&gpu); step(&gpu, &mut view); } - // Milliseconds, measured: no frame passes the deadline `finish` holds it to, even on a CPU - // renderer under load, while a count of nanoseconds would. + // The value is in milliseconds. No frame exceeds the deadline `finish` enforces, even on a + // loaded CPU renderer, whereas a count of nanoseconds would. let ms = view.stats().gpu_ms; assert!(ms > 0.0 && ms < common::DEADLINE.as_secs_f32() * 1000.0, "{ms} ms"); } @@ -746,8 +782,8 @@ fn frames_count_their_draws_and_instances() { #[test] fn dashes_run_on_across_a_paths_segments() { let Some(gpu) = gpu() else { return }; - // A path of a hundred segments a pixel long, dashed. Were each segment to start its dashes - // afresh, each would show only the blank first half of a dash, and the path nothing. + // A dashed path of a hundred segments, each a pixel long. If each segment restarted its dashes, + // each would show only a dash's blank first half, and the path would show nothing. let points: Vec<[f64; 2]> = (0..=100).map(|i| [f64::from(i) - 50.0, 0.0]).collect(); let data = Data::new("d"); data.apply(Table::new("Path", Ids::count(1)).field("geometry", vec![points])).expect("valid"); @@ -759,11 +795,11 @@ fn dashes_run_on_across_a_paths_segments() { let pixels = frame(&gpu, &mut view); let c = SIZE / 2; let on = (c - 40..c + 40).filter(|&x| at(&pixels, x, c)[0] > 128).count(); - assert!(on > 20 && on < 60, "about half of the path dashes: {on} of 80 pixels"); + assert!(on > 20 && on < 60, "dashes cover about half the path: {on} of 80 pixels"); } -/// A view of a gray line a pixel wide from x -50 to 50, its comets red and 8 pixels wide, 30 apart -/// with tails of 8, in position units when `units`; flowing at `speed`, or not at all. +/// A view of a 1-pixel gray line from x -50 to 50, with red comets 8 pixels wide, spaced 30 apart +/// with tails of 8 (in position units if `units`). Comets flow at `speed`, or not at all if `None`. fn comets(gpu: &latkit::Gpu, units: bool, speed: Option) -> View { let data = Data::new("d"); data.apply(Table::new("Path", Ids::count(1)).field("geometry", vec![vec![[-50.0, 0.0], [50.0, 0.0]]])) @@ -786,7 +822,7 @@ fn comets_scene(units: bool, speed: Option) -> Scene { Scene::new(plane()).theme(Theme { flow, ..plain_theme() }).ease(std::time::Duration::ZERO).draw(line) } -/// Where comets' heads are along row `y`, CSS pixels: the reddest pixel of each run of red. +/// The x of each comet head along pixel row `y`: the reddest pixel of each run of red. fn heads(pixels: &[[u8; 4]], y: u32) -> Vec { let red = |x: u32| at(pixels, x, y)[0]; let mut heads = Vec::new(); @@ -811,7 +847,7 @@ fn comets_run_wider_than_their_line() { let mut view = comets(&gpu, false, Some(40.0)); let c = SIZE / 2; let red = |pixels: &[[u8; 4]], y: u32| (c - 45..c + 45).filter(|&x| at(pixels, x, y)[0] > 100).count(); - // Comets never settle: a few frames draw them. + // Comets never settle, so render a few frames instead of waiting. let pixels = (0..3).map(|_| step(&gpu, &mut view)).last().expect("frames"); assert!(view.needs_render(), "comets travel every frame"); let beside = red(&pixels, c + 2); @@ -828,7 +864,8 @@ fn comets_carry_on_from_where_they_are_when_their_flow_changes() { for _ in 0..3 { step(&gpu, &mut view); } - // Long after the view began, as a page left running is, a pause moves them a tenth of a second. + // Long after the view began, as with a page left open, a long pause moves them only a tenth + // of a second's travel. common::wait(100_000.0); let paused = heads(&step(&gpu, &mut view), SIZE / 2 + 2); let before = heads(&step(&gpu, &mut view), SIZE / 2 + 2); @@ -837,13 +874,13 @@ fn comets_carry_on_from_where_they_are_when_their_flow_changes() { view.set_scene(comets_scene(false, Some(41.0))).expect("valid scene"); let after = heads(&step(&gpu, &mut view), SIZE / 2 + 2); assert_eq!(before.len(), after.len(), "{before:?} then {after:?}"); - assert!(moved(&before, &after) <= 2, "a new speed moves comets a frame's way: {before:?} then {after:?}"); + assert!(moved(&before, &after) <= 2, "a new speed moves comets one frame's travel: {before:?} then {after:?}"); } #[test] fn comets_follow_their_rows_when_rows_move_without_easing() { let Some(gpu) = gpu() else { return }; - // Two lines whose comets travel at different speeds, so each row's travel is its own. + // Two lines whose comets travel at different speeds, so each row's travel distance differs. let data = Data::new("d"); let paths = vec![vec![[-50.0, 30.0], [50.0, 30.0]], vec![[-50.0, 0.0], [50.0, 0.0]]]; data.apply(Table::new("Path", vec!["a", "b"]).field("geometry", paths).field("speed", vec![17.0, 40.0])) @@ -863,7 +900,7 @@ fn comets_follow_their_rows_when_rows_move_without_easing() { step(&gpu, &mut view); } let before = heads(&step(&gpu, &mut view), SIZE / 2 + 2); - // Row b moves up to row 0 at once: its comets carry on from where they were, not from a's. + // Row b moves to row 0 at once. Its comets continue from where they were, not from a's. data.apply(Patch::new().remove("Path", vec!["a"])).expect("valid"); let after = heads(&step(&gpu, &mut view), SIZE / 2 + 2); let moved = before.iter().zip(&after).map(|(a, b)| a.abs_diff(*b)).max().unwrap_or(99); @@ -874,7 +911,7 @@ fn comets_follow_their_rows_when_rows_move_without_easing() { fn comets_in_units_stay_on_their_lines_as_the_view_zooms() { let Some(gpu) = gpu() else { return }; let c = f64::from(SIZE / 2); - // Where comets' heads stand in position units at zoom 0 and zoom 1, so slow they stand still. + // Comet heads in position units at zoom 0 and zoom 1, moving so slowly they stay still. let heads_at = |units: bool| { let mut view = comets(&gpu, units, Some(1e-4)); [0.0, 1.0].map(|zoom: f64| { @@ -893,11 +930,11 @@ fn comets_in_units_stay_on_their_lines_as_the_view_zooms() { } #[test] -fn sizes_in_units_keep_within_their_pixels() { +fn sizes_in_units_clamp_to_their_pixel_limits() { let Some(gpu) = gpu() else { return }; let data = Data::new("d"); data.apply(Table::new("Node", Ids::count(1)).field("x", vec![0.0]).field("y", vec![0.0])).expect("valid"); - // Pixels across a red marker through the middle, at zoom 0: a unit a pixel. + // The width in pixels of a red marker through the center, at zoom 0 (one unit per pixel). let across = |radius: latkit::Size| { let marker = Vertices::of("d", "Node").position("x", "y").radius(radius).color(Color::rgb(1.0, 0.0, 0.0)); let mut view = View::new(&gpu, Scene::new(plane()).theme(plain_theme()).draw(marker)).expect("valid scene"); @@ -906,13 +943,30 @@ fn sizes_in_units_keep_within_their_pixels() { let pixels = frame(&gpu, &mut view); (0..SIZE).filter(|&x| at(&pixels, x, SIZE / 2)[0] > 128).count() }; - assert!(across(latkit::units(0.01)) <= 1, "a hundredth of a unit is no pixel at all"); + assert!(across(latkit::units(0.01)) <= 1, "a hundredth of a unit covers at most a pixel"); let held = across(latkit::units(0.01).at_least(5.0)); assert!((9..=11).contains(&held), "a radius of at least 5 pixels: {held} across"); let capped = across(latkit::units(1000.0).at_most(6.0)); assert!((11..=13).contains(&capped), "a radius of at most 6 pixels: {capped} across"); } +#[test] +fn sizes_in_units_are_degrees_on_the_earth() { + let Some(gpu) = gpu() else { return }; + let data = Data::new("d"); + data.apply(Table::new("Node", Ids::count(1)).field("lon", vec![0.0]).field("lat", vec![0.0])).expect("valid"); + let marker = Vertices::of("d", "Node").position("lon", "lat").radius(latkit::units(0.25)).color(Color::WHITE); + let ground = Theme { earth: latkit::EarthTheme { ground: Color::BLACK, ..plain_theme().earth }, ..plain_theme() }; + let scene = Scene::new(Earth { graticule: false, ..Earth::default() }).theme(ground).draw(marker); + let mut view = View::new(&gpu, scene).expect("valid scene"); + // At zoom 6 the world is 256 · 2⁶ pixels around, so a degree spans 45.5 pixels. + view.set_camera(Camera::Map { center: [0.0; 3], zoom: 6.0, pitch: 0.0, bearing: 0.0 }); + view.set_sources([&data]).expect("a source"); + let pixels = frame(&gpu, &mut view); + let across = (0..SIZE).filter(|&x| at(&pixels, x, SIZE / 2)[0] > 128).count(); + assert!((21..=25).contains(&across), "a radius of a quarter degree is 23 pixels across: {across}"); +} + #[test] fn poles_stand_from_each_vertex_down_to_the_ground() { let Some(gpu) = gpu() else { return }; @@ -943,7 +997,7 @@ fn poles_stand_from_each_vertex_down_to_the_ground() { } #[test] -fn nets_off_a_diagram_join_their_members_as_stars() { +fn nets_outside_a_diagram_join_their_members_as_stars() { let Some(gpu) = gpu() else { return }; let data = Data::new("d"); data.apply( @@ -963,7 +1017,7 @@ fn nets_off_a_diagram_join_their_members_as_stars() { view.set_sources([&data]).expect("a source"); let pixels = frame(&gpu, &mut view); let red = |x: u32, y: u32| at(&pixels, x, y)[0] > 128; - // The members' middle is the origin: the spokes meet there, and run out to each member. + // The members' center is the origin. The spokes meet there and run out to each member. let c = SIZE / 2; assert!(red(c, c), "the star's middle"); assert!(red(c, c - 15), "the spoke up to the member at the top (y up)"); diff --git a/crates/latkit/tests/scale.rs b/crates/latkit/tests/scale.rs index ea5b6425..61b14e06 100644 --- a/crates/latkit/tests/scale.rs +++ b/crates/latkit/tests/scale.rs @@ -1,6 +1,6 @@ -//! Scale: what a view draws always fits. Tables too large to draw whole draw the tiles in view, -//! traces too large thin in time, what matters more draws on top, a page never outgrows a chunk, and -//! a view sleeps while it waits. +//! Scale: a view's drawing always fits its budget. Paged tables draw the tiles in view, traces too +//! large thin in time, higher-priority rows draw on top, a path longer than an arena chunk still +//! draws whole, and a view sleeps while it waits. #![cfg(feature = "gpu")] @@ -13,12 +13,12 @@ use latkit::{ Vertices, View, Waiting, field, when, }; -/// Jobs done on threads of their own, landing frames later, as a page's workers answer them. +/// A host that runs jobs on separate threads, finishing frames later, as a page's workers do. struct Threads; impl Host for Threads { fn rasterize(&self, _: &TextRun) -> latkit::Result { - Err(latkit::Error::Unsupported("these views set no text".into())) + Err(latkit::Error::Unsupported("these views draw no text".into())) } fn spawn(&self, job: Job) -> Pending> { @@ -28,8 +28,8 @@ impl Host for Threads { } } -/// A grid of `columns` by `rows` vertices a unit apart, from the origin, and an edge from each to -/// the next in its row. +/// A grid of `columns` by `rows` vertices one unit apart, starting at the origin, each with +/// `v` = 0, and an edge from each vertex to the next in its row. fn lattice(columns: usize, rows: usize) -> Data { let n = columns * rows; let x: Vec = (0..n).map(|i| (i % columns) as f64).collect(); @@ -38,7 +38,8 @@ fn lattice(columns: usize, rows: usize) -> Data { let from: Vec = links.iter().map(|&i| i as u64).collect(); let to: Vec = links.iter().map(|&i| i as u64 + 1).collect(); let data = Data::new("grid"); - data.apply(Patch::new().set(Table::new("Bus", Ids::count(n as u32)).field("x", x).field("y", y)).set( + let buses = Table::new("Bus", Ids::count(n as u32)).field("x", x).field("y", y).field("v", vec![0.0; n]); + data.apply(Patch::new().set(buses).set( Table::new("Line", Ids::count(links.len() as u32)).reference("from", "Bus", from).reference("to", "Bus", to), )) .expect("valid"); @@ -46,7 +47,7 @@ fn lattice(columns: usize, rows: usize) -> Data { } #[test] -fn what_matters_more_draws_on_top() { +fn higher_priority_rows_draw_on_top() { let Some(gpu) = gpu() else { return }; let data = Data::new("graph"); data.apply( @@ -94,7 +95,7 @@ fn a_table_that_starts_empty_draws_its_rows() { } #[test] -fn a_dense_table_draws_the_tiles_in_view_and_keeps_what_is_selected() { +fn a_paged_table_draws_the_tiles_in_view_and_its_selected_rows() { let Some(gpu) = gpu() else { return }; let (columns, rows) = (600, 500); let data = lattice(columns, rows); @@ -118,35 +119,36 @@ fn a_dense_table_draws_the_tiles_in_view_and_keeps_what_is_selected() { view.set_selection([far.clone().into()]); frame(&gpu, &mut view); assert_eq!(view.selection(), [latkit::Rows::from(far)]); - assert_eq!(view.stats().drawn, stats.drawn + 1, "the selected row draws whatever its tile"); + assert_eq!(view.stats().drawn, stats.drawn + 1, "the selected row draws regardless of its tile"); } #[test] -fn panning_a_dense_table_draws_every_row_in_view_as_tiles_come_and_go() { +fn panning_a_paged_table_draws_every_row_in_view_as_tiles_change() { let Some(gpu) = gpu() else { return }; let (columns, rows) = (600, 500); - // Without an ease, rows arriving draw at once. + // Without an ease, arriving rows draw at once. let scene = Scene::new(plane()) .theme(plain_theme()) .ease(std::time::Duration::ZERO) .draw(Edges::of("grid", "Line").ends("from", "to").width(2.0).color(Color::rgb(0.0, 1.0, 0.0))) .draw(Vertices::of("grid", "Bus").position("x", "y").radius(2.0).color(Color::rgb(1.0, 1.0, 1.0))); let mut view = View::new(&gpu, scene).expect("valid scene"); - view.set_sources([lattice(columns, rows)]).expect("a source"); + let data = lattice(columns, rows); + view.set_sources([&data]).expect("a source"); let center = SIZE / 2; let camera = |x: f64| Camera::Map { center: [x, 250.0, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }; let mut whole = 0; for step in 0..6 { view.set_camera(camera(100.0 + 60.0 * f64::from(step))); - // Every frame that waits on no page draws every row in view: what a camera move draws shows - // whole, the edges with their ends. + // Every frame not waiting on a page draws every row in view. After a camera move, the + // view is complete, with edges and their endpoints. for _ in 0..4 { let pixels = common::step(&gpu, &mut view); let waiting = view.waiting(); if step > 0 && !waiting.data && !waiting.marks { let pixel = |dx: u32, dy: u32| at(&pixels, center + dx, center + dy); - assert!(close(pixel(0, 0), [255, 255, 255, 255]), "step {step}, as it lands: {:?}", pixel(0, 0)); - assert!(close(pixel(4, 0), [0, 255, 0, 255]), "step {step}, as it lands: {:?}", pixel(4, 0)); + assert!(close(pixel(0, 0), [255, 255, 255, 255]), "step {step}, as pages arrive: {:?}", pixel(0, 0)); + assert!(close(pixel(4, 0), [0, 255, 0, 255]), "step {step}, as pages arrive: {:?}", pixel(4, 0)); whole += 1; } } @@ -160,26 +162,30 @@ fn panning_a_dense_table_draws_every_row_in_view_as_tiles_come_and_go() { assert!(close(pixel(4, 4), [0, 0, 0, 255]), "step {step}: none between rows: {:?}", pixel(4, 4)); assert!(close(pixel(4, 0), [0, 255, 0, 255]), "step {step}: an edge between two: {:?}", pixel(4, 0)); } - assert!(whole > 0, "frames drew as their pages landed"); - // Back where the camera began: its pages are held still, so nothing packs, uploads, or evaluates - // again. + assert!(whole > 0, "frames drew as their pages arrived"); + // A row written far from the camera's starting point leaves the pages there resident. + let last = (columns * rows - 1) as u64; + data.apply(Table::new("Bus", vec![last]).field("v", vec![1.0])).expect("valid"); + frame(&gpu, &mut view); + // Back at the starting point, its pages are still resident, so nothing packs, uploads, or + // evaluates again. let before = gpu.work(); view.set_camera(camera(100.0)); let pixels = frame(&gpu, &mut view); let work = gpu.work() - before; - assert_eq!((work.packed, work.uploaded, work.evaluated), (0, 0, 0), "the way back costs nothing"); + assert_eq!((work.packed, work.uploaded, work.evaluated), (0, 0, 0), "returning needs no work"); assert!(close(at(&pixels, center, center), [255, 255, 255, 255]), "{:?}", at(&pixels, center, center)); - // A row picked by id is found among the rows drawn. + // A row selected by id is found among the drawn rows. let selected = Row::new("grid", "Bus", (250 * columns + 400) as u64); view.set_selection([selected.clone().into()]); view.set_camera(camera(380.0)); frame(&gpu, &mut view); - assert_eq!(view.selection(), [latkit::Rows::from(selected)], "what is selected stays so"); + assert_eq!(view.selection(), [latkit::Rows::from(selected)], "the selected row stays selected"); assert_eq!(errors(&mut view), []); } #[test] -fn appending_to_a_table_drawn_through_tiles_never_thins_it() { +fn appending_to_a_paged_table_never_thins_it() { let Some(gpu) = gpu() else { return }; let (columns, rows) = (600, 500); let data = lattice(columns, rows); @@ -191,12 +197,12 @@ fn appending_to_a_table_drawn_through_tiles_never_thins_it() { view.set_camera(Camera::Map { center: [300.0, 250.0, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }); view.set_sources([&data]).expect("a source"); frame(&gpu, &mut view); - assert!(view.complete(), "its pyramid serves"); + assert!(view.complete(), "complete, drawing from its pyramid"); let drawn = view.stats().drawn; let center = SIZE / 2; let before = gpu.work(); for added in 0..4_u32 { - // A row a step, far from the view: until a pyramid holds it, it draws after the rest. + // One row per step, far from the view. Until a pyramid includes it, it draws last. let id = (columns * rows) as u64 + u64::from(added); let row = Table::new("Bus", vec![id]).field("x", vec![-50.0]).field("y", vec![f64::from(added)]); data.apply(row).expect("valid"); @@ -207,17 +213,17 @@ fn appending_to_a_table_drawn_through_tiles_never_thins_it() { assert!(close(pixel(8), [255, 255, 255, 255]), "append {added}: and its neighbor: {:?}", pixel(8)); assert!(view.stats().drawn >= drawn, "append {added}: {} drawn, {drawn} before", view.stats().drawn); } - // The pyramid serves the rows appended as they come, and only the page they land in evaluates. + // The pyramid serves appended rows as they arrive, and only the page they join evaluates. let work = gpu.work() - before; - assert_eq!(work.jobs, 0, "rows appended level nothing until they are many"); - assert!(work.evaluated <= 4 << 12, "a page each: {}", work.evaluated); + assert_eq!(work.jobs, 0, "a few appended rows start no job"); + assert!(work.evaluated <= 4 << 12, "at most a page per append evaluates: {}", work.evaluated); } #[test] fn a_change_to_one_rows_color_moves_no_row() { let Some(gpu) = gpu() else { return }; - // A grid on the globe, drawn in part: its lines are great circles, whose pieces follow where the - // buses lie. + // A grid on the globe, drawn as a paged table. Its lines are great circles whose pieces depend + // on the bus positions. let (columns, rows) = (600, 500); let n = columns * rows; let links: Vec = (0..n).filter(|i| i % columns + 1 < columns).collect(); @@ -251,16 +257,16 @@ fn a_change_to_one_rows_color_moves_no_row() { view.set_sources([&data]).expect("a source"); frame(&gpu, &mut view); assert_eq!(errors(&mut view), []); - assert!(view.stats().drawn < view.stats().rows / 4, "the grid draws in part"); - // One bus far off view, one in it: neither moves a line's pieces, so nothing is read back. + assert!(view.stats().drawn < view.stats().rows / 4, "the grid is paged"); + // One bus far out of view, one in it. Neither changes a line's pieces, so nothing is read back. for id in [0, 250 * columns + 300] { let before = gpu.work(); data.apply(Table::new("Bus", vec![id as u64]).field("v", vec![1.0])).expect("valid"); frame(&gpu, &mut view); let work = gpu.work() - before; assert_eq!(work.readbacks, 0, "bus {id}: no line counts its pieces again"); - assert!(work.evaluated <= 1 << 12, "bus {id}: a page at most evaluates: {}", work.evaluated); - assert_eq!(work.resolved, 0, "bus {id}: no line finds its ends again"); + assert!(work.evaluated <= 1 << 12, "bus {id}: at most a page evaluates: {}", work.evaluated); + assert_eq!(work.resolved, 0, "bus {id}: no line resolves its endpoints again"); } } @@ -276,28 +282,28 @@ fn a_frame_waiting_for_pages_packs_nothing_again() { view.set_sources([lattice(600, 500)]).expect("a source"); frame(&gpu, &mut view); assert_eq!(errors(&mut view), []); - // Pages trickle up a few at a time: what the camera moves to waits for them. + // Pages upload a few at a time, so the area the camera moves to waits for them. gpu.set_budget(Budget { upload_per_frame: 16 << 10, ..budget }); view.set_camera(Camera::Map { center: [80.0, 60.0, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }); let before = gpu.work(); common::step(&gpu, &mut view); let packed = (gpu.work() - before).packed; - assert!(packed > 0 && view.waiting().data, "the pages it moved to pack, and wait to upload"); + assert!(packed > 0 && view.waiting().data, "the pages at the new camera pack, then wait to upload"); let mut waited = 0; while view.waiting().data && waited < common::SETTLE { common::step(&gpu, &mut view); waited += 1; } - assert!(waited > 1, "it waited for pages over frames"); - assert_eq!((gpu.work() - before).packed, packed, "frames waiting packed nothing again"); + assert!(waited > 1, "it waited several frames for pages"); + assert_eq!((gpu.work() - before).packed, packed, "the waiting frames packed nothing again"); gpu.set_budget(budget); } #[test] -fn a_fitted_domain_holds_however_little_of_its_table_draws() { +fn a_fitted_domain_covers_every_row_of_a_paged_table() { let Some(gpu) = gpu() else { return }; - // A lattice whose rows hold v = x / 600, and one more far in a corner holding 10, the lowest - // priority, so it lands among the finest rows, which no page drawn about the middle holds. + // A lattice whose rows have v = x / 600, plus one far in a corner with v = 10 and the lowest + // priority. It falls among the finest rows, in no page drawn around the center. let (columns, rows) = (600_usize, 500_usize); let n = columns * rows; let mut x: Vec = (0..n).map(|i| (i % columns) as f64).collect(); @@ -310,7 +316,7 @@ fn a_fitted_domain_holds_however_little_of_its_table_draws() { let data = Data::new("grid"); data.apply(Table::new("Bus", Ids::count(n as u32 + 1)).field("x", x).field("y", y).field("v", v).field("p", p)) .expect("valid"); - // A blue ground, so a gray row tells from no row. + // A blue background, so a gray row is distinguishable from no row. let theme = latkit::Theme { background: Color::rgb(0.0, 0.0, 1.0), ..plain_theme() }; let scene = Scene::new(plane()).theme(theme).ease(std::time::Duration::ZERO).draw( Vertices::of("grid", "Bus") @@ -324,21 +330,21 @@ fn a_fitted_domain_holds_however_little_of_its_table_draws() { view.set_sources([&data]).expect("a source"); let pixels = frame(&gpu, &mut view); assert_eq!(errors(&mut view), []); - assert!(view.stats().drawn < view.stats().rows / 4, "the table draws in part"); - // Fitted over every row, 0 to 10: the middle row's half draws near black, not the gray it would - // over the rows held about the view. + assert!(view.stats().drawn < view.stats().rows / 4, "the table is paged"); + // The domain is fitted over every row, 0 to 10, so the center row's 0.5 draws near black, not + // the mid gray a domain fitted to the resident rows would give. let center = at(&pixels, SIZE / 2, SIZE / 2); let gray = center[0].abs_diff(center[2]) <= 3 && center[1].abs_diff(center[2]) <= 3; - assert!(gray && center[0] < 90, "the middle row draws {center:?}"); + assert!(gray && center[0] < 90, "the middle row draws near black: {center:?}"); } #[test] -fn a_join_into_a_table_drawn_in_part_reads_every_row() { +fn a_join_into_a_paged_table_reads_every_row() { let Some(gpu) = gpu() else { return }; let (columns, rows) = (600, 500); let n = columns * rows; let data = lattice(columns, rows); - // Bus 7 lies far from the view; a probe in the middle reads its value by id. + // Bus 7 lies far from the view. A probe at the center reads its value by id. let v: Vec = (0..n).map(|i| if i == 7 { 1.0 } else { 0.0 }).collect(); data.apply( Patch::new() @@ -361,14 +367,14 @@ fn a_join_into_a_table_drawn_in_part_reads_every_row() { view.set_sources([&data]).expect("a source"); let pixels = frame(&gpu, &mut view); assert_eq!(errors(&mut view), []); - assert!(view.stats().drawn < view.stats().rows / 4, "the buses draw in part"); + assert!(view.stats().drawn < view.stats().rows / 4, "the buses are paged"); let probe = view.screen([300.5, 250.5, 0.0]).expect("in view"); let pixel = at(&pixels, probe[0] as u32, probe[1] as u32); - assert!(close(pixel, [255, 255, 255, 255]), "bus 7's value, though no tile in view holds it: {pixel:?}"); + assert!(close(pixel, [255, 255, 255, 255]), "bus 7's value, though no tile in view contains it: {pixel:?}"); } #[test] -fn pages_held_while_rows_ease_enter_as_the_rest_ease_on() { +fn pages_made_resident_mid_ease_enter_as_the_rest_keep_easing() { let Some(gpu) = gpu() else { return }; let camera = |zoom: f64| Camera::Map { center: [300.0, 250.0, 0.0], zoom, pitch: 0.0, bearing: 0.0 }; let view = |zoom: f64| { @@ -382,22 +388,22 @@ fn pages_held_while_rows_ease_enter_as_the_rest_ease_on() { }; let mut pulled = view(1.0); let want = frame(&gpu, &mut pulled); - // A sample draws first; then the pyramid lands, and its rows ease in from it... + // A sample draws first. Then the pyramid arrives, and its rows ease in from the sample... let mut easing = view(3.0); common::step(&gpu, &mut easing); common::step(&gpu, &mut easing); - // ...as the camera pulls back, holding more pages than were held before. + // ...while the camera pulls back, needing more resident pages than before. easing.set_camera(camera(1.0)); let pixels = frame(&gpu, &mut easing); assert_eq!(errors(&mut easing), []); let differ = pixels.iter().zip(&want).filter(|(a, b)| !close(**a, **b)).count(); - assert_eq!(differ, 0, "settles to what the camera pulled back at once draws"); + assert_eq!(differ, 0, "settles to the frame of a view pulled back at once"); } #[test] -fn areas_drawn_in_part_fill_the_pages_drawn_as_the_camera_moves() { +fn paged_areas_fill_the_drawn_pages_as_the_camera_moves() { let Some(gpu) = gpu() else { return }; - // A lattice of cells a unit apart, each a square half a unit across: too many to draw whole. + // A lattice of cells one unit apart, each a square half a unit across: too many to draw whole. let (columns, rows) = (600_usize, 500_usize); let cells: Vec>> = (0..columns * rows) .map(|i| { @@ -419,8 +425,8 @@ fn areas_drawn_in_part_fill_the_pages_drawn_as_the_camera_moves() { let pixels = frame(&gpu, &mut view); assert_eq!(errors(&mut view), []); let stats = view.stats(); - assert!(stats.drawn < stats.rows / 4, "the cells draw in part: {} of {}", stats.drawn, stats.rows); - // The cell at the middle fills, and the gap beside it does not. + assert!(stats.drawn < stats.rows / 4, "the cells are paged: {} of {} draw", stats.drawn, stats.rows); + // The cell at the center fills, and the gap beside it does not. let center = SIZE / 2; assert!(close(at(&pixels, center, center), [255, 0, 0, 255]), "at {x}, {y}: {:?}", at(&pixels, center, center)); assert!( @@ -432,7 +438,7 @@ fn areas_drawn_in_part_fill_the_pages_drawn_as_the_camera_moves() { } #[test] -fn rows_kept_off_screen_hold_pages_of_their_own() { +fn selected_rows_off_screen_pack_into_kept_row_pages() { let Some(gpu) = gpu() else { return }; let scene = Scene::new(plane()) .theme(plain_theme()) @@ -443,21 +449,21 @@ fn rows_kept_off_screen_hold_pages_of_their_own() { view.set_sources([lattice(600, 500)]).expect("a source"); frame(&gpu, &mut view); let (drawn, before) = (view.stats().drawn, gpu.work()); - // A hundred rows along the grid's foot, far from the view, each in another page of the order. + // A hundred rows near the grid's bottom, far from the view, each in its own page of the order. let ids: Vec = (0..100_u64).map(|i| latkit::Id::from(600 * (i % 50) + i * 6)).collect(); view.set_selection([latkit::Rows { data: "grid".into(), table: "Bus".into(), ids }]); frame(&gpu, &mut view); assert_eq!(errors(&mut view), []); - assert_eq!(view.stats().drawn, drawn + 100, "every row kept draws"); + assert_eq!(view.stats().drawn, drawn + 100, "every kept row draws"); let packed = (gpu.work() - before).packed; - assert!(packed < 4096, "rows kept pack together, not a page of the order each: {packed} rows packed"); + assert!(packed < 4096, "kept rows pack together, not one order page each: {packed} rows packed"); } #[test] -fn an_edge_kept_draws_between_its_ends() { +fn a_kept_edge_draws_between_its_endpoints() { let Some(gpu) = gpu() else { return }; let budget = gpu.budget(); - // So little room that only the coarsest edges draw. + // So small a budget that only the coarsest edges draw. gpu.set_budget(Budget { bytes: 8 << 20, ..budget }); let scene = Scene::new(plane()) .theme(plain_theme()) @@ -471,7 +477,8 @@ fn an_edge_kept_draws_between_its_ends() { let green = |p: [u8; 4]| p[1] > 150 && p[0] < 100 && p[2] < 100; let before = frame(&gpu, &mut view); assert!(!green(at(&before, center, center)), "the edge is not drawn: {:?}", at(&before, center, center)); - // Line 150050 runs from bus 150300 to bus 150301, (300, 250) to (301, 250): kept, with its ends. + // Line 150050 runs from bus 150300 to bus 150301, (300, 250) to (301, 250). Selecting it keeps + // it and its endpoints. view.set_selection([Row::new("grid", "Line", 150_050_u64).into()]); let after = frame(&gpu, &mut view); gpu.set_budget(budget); @@ -480,9 +487,9 @@ fn an_edge_kept_draws_between_its_ends() { } #[test] -fn lanes_standing_in_for_a_table_drawn_in_part_read_missing_past_their_rows() { +fn placeholder_lanes_of_a_paged_table_read_missing_past_its_rows() { let Some(gpu) = gpu() else { return }; - // Two buses joined twice: their lanes derive on the frame. + // Two buses joined twice: their lanes are derived during the frame. let pair = Data::new("grid"); pair.apply( Patch::new().set(Table::new("Bus", Ids::count(2)).field("x", vec![0.0, 1.0]).field("y", vec![0.0, 0.0])).set( @@ -503,21 +510,21 @@ fn lanes_standing_in_for_a_table_drawn_in_part_read_missing_past_their_rows() { view.set_camera(Camera::Map { center: [300.0, 250.0, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }); view.set_sources([&pair]).expect("a source"); frame(&gpu, &mut view); - // A grid too large to draw whole replaces it: its lanes take a job, and until it lands the pair's - // stand in, holding none of its rows past the first two. + // A grid too large to draw whole replaces it. Its lanes take a job, and until the job finishes + // the pair's lanes are the placeholder, covering none of the grid's rows past the first two. view.set_sources([lattice(600, 500)]).expect("a source"); let pixels = frame(&gpu, &mut view); assert_eq!(errors(&mut view), []); - assert!(view.stats().drawn < view.stats().rows / 4, "the grid draws in part"); + assert!(view.stats().drawn < view.stats().rows / 4, "the grid is paged"); let pixel = at(&pixels, SIZE / 2 + 4, SIZE / 2); assert!(close(pixel, [0, 255, 0, 255]), "an edge between two buses: {pixel:?}"); } #[test] -fn a_change_to_one_row_drawn_in_part_writes_that_row_alone() { +fn a_change_to_one_row_of_a_paged_table_updates_only_its_page() { let Some(gpu) = gpu() else { return }; - // Four pages of rows drawn whole in priority order, so each is drawn far from its place in the - // table; every row lies off view but one, at the middle. + // 200,000 rows (49 pages) drawn whole in priority order, so each row draws far from its place + // in the table. Every row lies out of view except one, at the center. let (rows, middle) = (200_000_u32, 12_345_u32); let n = rows as usize; let off = |r: usize| if r == middle as usize { 0.0 } else { 1000.0 }; @@ -549,15 +556,16 @@ fn a_change_to_one_row_drawn_in_part_writes_that_row_alone() { let work = gpu.work() - before; let center = SIZE / 2; assert!(close(at(&pixels, center, center), [255, 255, 255, 255]), "{:?}", at(&pixels, center, center)); - assert!(work.evaluated <= 1 << 16, "one page of the rows drawn evaluates: {}", work.evaluated); - assert!(work.uploaded <= 8 << 16, "one page of the field drawn uploads: {} bytes", work.uploaded); + // A page is 4,096 rows; the field takes 8 bytes a row. + assert!(work.evaluated <= 4096, "only one page evaluates: {}", work.evaluated); + assert!(work.uploaded <= 8 * 4096, "only one page of the field uploads: {} bytes", work.uploaded); } #[test] -fn a_field_drawn_in_part_that_changes_kind_gathers_afresh() { +fn a_field_of_a_paged_table_that_changes_kind_is_gathered_again() { let Some(gpu) = gpu() else { return }; - // Two marks of one table drawn in part: one reads `v` as a number, the other as a text, so `v` is - // read before and after it changes kind. + // Two marks of one paged table. One reads `v` as a number and the other as text, so `v` is read + // both before and after it changes kind. let n = 1000_usize; let table = |v: latkit::Values| { Table::new("P", Ids::count(n as u32)) @@ -592,7 +600,7 @@ fn a_field_drawn_in_part_that_changes_kind_gathers_afresh() { } #[test] -fn edges_drawn_through_tiles_show_with_their_ends() { +fn paged_edges_draw_with_their_endpoints() { let Some(gpu) = gpu() else { return }; let data = lattice(600, 500); let scene = Scene::new(plane()) @@ -605,8 +613,8 @@ fn edges_drawn_through_tiles_show_with_their_ends() { let pixels = frame(&gpu, &mut view); assert_eq!(errors(&mut view), []); assert!(view.stats().drawn < view.stats().rows / 4, "edges and vertices both draw the tiles in view"); - // Half a unit up from the middle, between rows of edges; at the middle's row, an edge between - // two vertices. + // The center pixel row lies half a unit above a row of vertices, between rows of edges. Four + // pixels down, on that row, an edge runs between two vertices. let center = SIZE / 2; assert!(close(at(&pixels, center + 4, center + 4), [0, 255, 0, 255]), "{:?}", at(&pixels, center + 4, center + 4)); assert!(close(at(&pixels, center + 4, center), [0, 0, 0, 255]), "{:?}", at(&pixels, center + 4, center)); @@ -644,7 +652,7 @@ fn a_tiny_budget_never_fails() { } #[test] -fn a_first_look_draws_within_its_plan_while_pyramids_are_worked_out() { +fn a_first_look_stays_within_budget_while_pyramids_are_computed() { let Some(gpu) = gpu() else { return }; gpu.set_budget(Budget { bytes: 8 << 20, upload_per_frame: 4 << 20 }); gpu.set_host(Threads); @@ -655,8 +663,8 @@ fn a_first_look_draws_within_its_plan_while_pyramids_are_worked_out() { let mut view = View::new(&gpu, scene).expect("valid scene"); view.set_sources([lattice(1000, 500)]).expect("a source"); view.fit(Fit::All); - // Until the pyramids land, a sample draws, the ends of its edges among its vertices' rows: then - // what replaces it finds room beside it. + // Until the pyramids arrive, a sample draws, with its edges' endpoints among its vertex rows. + // Then its replacement finds room beside it. let mut settled = false; for _ in 0..600 { step(&gpu, &mut view); @@ -669,12 +677,12 @@ fn a_first_look_draws_within_its_plan_while_pyramids_are_worked_out() { } } assert!(errors(&mut view).is_empty(), "nothing fails"); - assert!(settled, "what replaces the sample lands: {:?}", view.waiting()); + assert!(settled, "the sample's replacement arrives: {:?}", view.waiting()); assert!(gpu.resident() <= 8 << 20, "{} bytes resident", gpu.resident()); } -/// A grid of 500 by 500 vertices a unit apart, each of value 0, and an edge from each to the vertex -/// `far` says: what `far` gives each from a stream of random numbers. +/// A grid of 500 by 500 vertices one unit apart, each with `v` = 0, and an edge from each vertex +/// `i` to vertex `far(i, random)`, with `random` drawn from a seeded stream. fn far_apart(far: impl Fn(u32, u64) -> u64) -> Data { let n = 250_000_u32; let mut seed = 1_u64; @@ -704,11 +712,11 @@ fn far_apart(far: impl Fn(u32, u64) -> u64) -> Data { } #[test] -fn edges_ending_far_apart_hold_the_rows_they_end_at() { +fn edges_ending_far_apart_make_their_endpoints_kept_rows() { let Some(gpu) = gpu() else { return }; gpu.set_budget(Budget { bytes: 8 << 20, upload_per_frame: 32 << 20 }); - // Each edge joins a vertex to one far off: the ends of the fewest edges drawn lie in every page of - // the vertices, more pages than the budget holds, so the rows they end at are held one at a time. + // Each edge joins a vertex to a far one. Even the fewest edges drawn end in every vertex page, + // more pages than the budget allows, so their endpoints become kept rows. let data = far_apart(|_, random| random % 250_000); let scene = Scene::new(plane()).theme(plain_theme()).draw(Edges::of("grid", "Line").ends("from", "to")).draw( Vertices::of("grid", "Bus") @@ -720,27 +728,27 @@ fn edges_ending_far_apart_hold_the_rows_they_end_at() { view.fit(Fit::All); let before = gpu.work(); frame(&gpu, &mut view); - assert_eq!(errors(&mut view), [], "what draws fits"); + assert_eq!(errors(&mut view), [], "the drawing fits the budget"); assert!(view.stats().drawn > 0, "edges and vertices draw"); - assert!((gpu.work() - before).resolved > 0, "edges found where their ends are held"); + assert!((gpu.work() - before).resolved > 0, "edges resolve their endpoints"); assert!(gpu.resident() <= 8 << 20, "{} bytes resident", gpu.resident()); - // Every vertex's value changes and none moves: the rows held stay where they are, so no edge finds - // its ends again. + // Every vertex's value changes but none moves. The kept rows stay in place, so no edge resolves + // its endpoints again. let before = gpu.work(); data.apply(Table::new("Bus", Ids::count(250_000)).field("v", vec![1.0; 250_000])).expect("valid"); frame(&gpu, &mut view); assert_eq!(errors(&mut view), []); let work = gpu.work() - before; assert!(work.uploaded > 0, "the values upload"); - assert_eq!(work.resolved, 0, "no end is found again"); + assert_eq!(work.resolved, 0, "no edge resolves its endpoints again"); } #[test] fn edges_ending_far_apart_draw_between_their_ends() { let Some(gpu) = gpu() else { return }; gpu.set_budget(Budget { bytes: 8 << 20, upload_per_frame: 32 << 20 }); - // Each edge runs along its row to a vertex of it far off, so every edge is level: one whose end - // were found at another vertex would slant across the gaps between the rows. + // Each edge runs along its row to a far vertex in the same row, so every edge is horizontal. An + // edge whose endpoint resolved to the wrong vertex would slant across the gaps between rows. let data = far_apart(|i, random| u64::from(i - i % 500) + random % 500); let green = Color::rgb(0.0, 1.0, 0.0); let scene = Scene::new(plane()) @@ -749,7 +757,7 @@ fn edges_ending_far_apart_draw_between_their_ends() { .draw(Edges::of("grid", "Line").ends("from", "to").width(2.0).color(green)) .draw(Vertices::of("grid", "Bus").position("x", "y").radius(1.0).color(Color::rgb(0.0, 0.0, 1.0))); let mut view = View::new(&gpu, scene).expect("valid scene"); - // Rows eight pixels apart, the middle row of pixels between two of them. + // Rows are eight pixels apart, and the center pixel row lies between two of them. let near = Camera::Map { center: [250.0, 250.5, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }; view.set_camera(near); view.set_sources([&data]).expect("a source"); @@ -765,8 +773,9 @@ fn edges_ending_far_apart_draw_between_their_ends() { let pixels = frame(&gpu, &mut view); assert_eq!(errors(&mut view), []); level(&pixels, "first"); - // Farther out, the edges drawn end at more rows held one at a time: the table of them grows, and - // the program holding it with it. Back where it was, every edge still finds its ends. + // Zoomed out, the drawn edges end at more kept rows, so the end table grows, and so does the + // program buffer that holds it. Back at the first camera, every edge still resolves its + // endpoints. view.set_camera(Camera::Map { center: [250.0, 250.5, 0.0], zoom: 1.0, pitch: 0.0, bearing: 0.0 }); frame(&gpu, &mut view); view.set_camera(near); @@ -779,7 +788,7 @@ fn edges_ending_far_apart_draw_between_their_ends() { fn traces_too_large_for_the_budget_thin_in_time() { let Some(gpu) = gpu() else { return }; gpu.set_budget(Budget { bytes: 8 << 20, upload_per_frame: 32 << 20 }); - // Every frame of every row would take 12 MB: past the budget, so the traces draw coarser. + // Every frame of every row would take 12 MB, over the budget, so the traces draw coarser. let (rows, frames) = (20_000_usize, 150_u32); let mut patch = Patch::new().set(Table::new("Signal", Ids::count(rows as u32))); for t in 0..frames { @@ -806,7 +815,7 @@ fn traces_too_large_for_the_budget_thin_in_time() { } #[test] -fn a_path_written_again_uploads_its_group_of_rows_alone() { +fn a_rewritten_path_uploads_only_its_group_of_rows() { let Some(gpu) = gpu() else { return }; let rows = 2000_u32; let path = |r: u32, lift: f64| -> Vec<[f64; 2]> { @@ -828,8 +837,9 @@ fn a_path_written_again_uploads_its_group_of_rows_alone() { data.apply(Table::new("Road", [700_u64]).field("geometry", vec![path(700, 0.5)])).expect("valid"); frame(&gpu, &mut view); let work = gpu.work() - before; - // The page's points take 96 KB, its rows' table 16 KB; the piece of the row's group of 64 rows, 3 KB. - assert!(work.uploaded <= 4 << 10, "the piece the row lies in uploads alone: {} bytes", work.uploaded); + // The page's points take 96 KB and its row table 16 KB. The 64-row group holding the changed + // row takes 3 KB. + assert!(work.uploaded <= 4 << 10, "only the row's 64-row group uploads: {} bytes", work.uploaded); } #[test] @@ -893,5 +903,5 @@ fn a_view_sleeps_while_it_waits() { gpu.queue().submit([encoder.finish()]); assert!(!view.needs_render(), "while the answer is read back, the view sleeps"); common::finish(&gpu); - assert!(view.needs_render(), "the answer landing wakes it"); + assert!(view.needs_render(), "the answer arriving wakes it"); } diff --git a/crates/latkit/tests/scenes.rs b/crates/latkit/tests/scenes.rs index d7393036..d3f67984 100644 --- a/crates/latkit/tests/scenes.rs +++ b/crates/latkit/tests/scenes.rs @@ -1,4 +1,5 @@ -//! The scenes of `npm/test/scenes.js` decode: the JSON form TypeScript checks is the form latkit reads. +//! The scenes in `npm/test/scenes.js` decode, so the JSON form TypeScript checks is the form latkit +//! reads. #![cfg(feature = "gpu")] diff --git a/crates/latkit/tests/schema.rs b/crates/latkit/tests/schema.rs index 92a72600..d1caceb4 100644 --- a/crates/latkit/tests/schema.rs +++ b/crates/latkit/tests/schema.rs @@ -1,14 +1,14 @@ -//! npm's input types are written from `npm/schema.json`, so it must be what the forms say, and the -//! documentation's colors from `docs/_static/theme.json`, so it must be latkit's default themes: -//! `LATKIT_BLESS=1 cargo test -p latkit --test schema` writes both, and `node npm/build.mjs schema` -//! writes the types. +//! The npm package's input types are generated from `npm/schema.json`, so it must match latkit's +//! schemas. The documentation's colors come from `docs/_static/theme.json`, so it must match +//! latkit's default themes. `LATKIT_BLESS=1 cargo test -p latkit --test schema` writes both files, +//! and `node npm/build.mjs schema` regenerates the types. #![cfg(feature = "gpu")] use std::path::Path; #[test] -fn npm_schema_is_the_forms() { +fn npm_schema_matches_the_input_schemas() { let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../npm/schema.json"); let schemas = serde_json::to_string_pretty(&latkit::schemas()).expect("schemas serialize") + "\n"; if std::env::var_os("LATKIT_BLESS").is_some() { @@ -18,13 +18,13 @@ fn npm_schema_is_the_forms() { let committed = std::fs::read_to_string(&path).unwrap_or_default().replace("\r\n", "\n"); assert!( committed == schemas, - "npm/schema.json is not what the forms say: run LATKIT_BLESS=1 cargo test -p latkit --test schema, \ + "npm/schema.json does not match latkit's schemas: run LATKIT_BLESS=1 cargo test -p latkit --test schema, \ then node npm/build.mjs schema" ); } #[test] -fn docs_palette_is_the_default_themes() { +fn docs_palette_matches_the_default_themes() { let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/_static/theme.json"); let themes = serde_json::json!({ "light": latkit::Theme::light(), "dark": latkit::Theme::dark() }); let text = serde_json::to_string_pretty(&themes).expect("themes serialize") + "\n"; @@ -40,7 +40,7 @@ fn docs_palette_is_the_default_themes() { } #[test] -fn every_input_has_a_schema_and_reads_what_it_describes() { +fn every_input_has_a_schema_matching_its_json_form() { let schemas = latkit::schemas(); let inputs = [ "Scene", @@ -62,5 +62,5 @@ fn every_input_has_a_schema_and_reads_what_it_describes() { let diagram = &schemas["$defs"]["DiagramTheme"]; assert!(diagram.to_string().contains("groupPadding"), "options are in camelCase"); let names = &schemas["$defs"]["ColormapName"]["enum"]; - assert!(names.as_array().is_some_and(|n| n.iter().any(|n| n == "viridis")), "the catalog names its maps"); + assert!(names.as_array().is_some_and(|n| n.iter().any(|n| n == "viridis")), "the schema lists the colormap names"); } diff --git a/crates/latkit/tests/shots.rs b/crates/latkit/tests/shots.rs index 2561aa1e..5447be9e 100644 --- a/crates/latkit/tests/shots.rs +++ b/crates/latkit/tests/shots.rs @@ -1,6 +1,6 @@ -//! Shots: a picture draws what a view shows at a size of its own once it is complete and still, -//! panels compose side by side, a transparent picture keeps its alpha, and a frame tells whether -//! anything stood in for work under way. +//! Shots: a picture draws what a view shows, at its own size, once the view is complete and still. +//! Panels compose side by side, a transparent picture keeps its alpha, and a frame reports whether +//! it drew placeholders for pending work. Legends draw colormaps and named categories. #![cfg(feature = "gpu")] @@ -36,7 +36,7 @@ fn pixel(image: &Image, x: u32, y: u32) -> [u8; 4] { } #[test] -fn a_picture_draws_what_the_view_shows() { +fn a_picture_draws_the_same_pixels_as_the_view() { let Some(gpu) = gpu() else { return }; let (scene, data) = dot(20.0, Color::rgb(1.0, 0.0, 0.0)); let mut view = view(&gpu, scene, &data); @@ -50,7 +50,7 @@ fn a_picture_draws_what_the_view_shows() { } #[test] -fn pictures_make_only_what_they_draw_with() { +fn pictures_compile_only_the_pipelines_they_draw_with() { let Some(gpu) = gpu() else { return }; let (scene, data) = dot(20.0, Color::rgb(1.0, 0.0, 0.0)); let mut view = view(&gpu, scene, &data); @@ -58,10 +58,10 @@ fn pictures_make_only_what_they_draw_with() { pollster::block_on(shot.image(&mut [&mut view])).expect("a picture"); let made = gpu.work().pipelines; pollster::block_on(shot.image(&mut [&mut view])).expect("a picture"); - // A picture takes no input, so it never makes the probes a pointer would need, and what it makes - // it asked for first. - assert_eq!(gpu.work().pipelines, made, "another picture makes nothing"); - assert_eq!(gpu.work().late, 0, "nothing made where it was first used"); + // A picture takes no input, so it never creates the probe pipelines a pointer needs. Every + // pipeline it creates was requested up front. + assert_eq!(gpu.work().pipelines, made, "another picture compiles no pipeline"); + assert_eq!(gpu.work().late, 0, "no pipeline compiles at first use"); } #[test] @@ -72,7 +72,7 @@ fn a_scaled_picture_draws_the_same_view_sharper() { let image = pollster::block_on(Shot::new([2 * SIZE, 2 * SIZE]).scale(2.0).image(&mut [&mut view])).expect("a picture"); assert_eq!(image.size, [2 * SIZE, 2 * SIZE]); - // The same CSS layout at twice the pixels: the dot sits twice as far out, in twice the pixels. + // The same CSS layout at twice the pixels: the dot is twice as far out and twice as large. assert!(close(pixel(&image, SIZE + 40, SIZE), RED), "{:?}", pixel(&image, SIZE + 40, SIZE)); assert!(close(pixel(&image, SIZE + 40 + 10, SIZE), RED), "a radius of 6 CSS pixels is 12 pixels"); assert!(close(pixel(&image, SIZE, SIZE), BLACK)); @@ -89,10 +89,10 @@ fn panels_compose_side_by_side() { let image = pollster::block_on(shot.image(&mut [&mut left, &mut right])).expect("a picture"); assert!(close(pixel(&image, SIZE / 2, SIZE / 2), RED), "{:?}", pixel(&image, SIZE / 2, SIZE / 2)); assert!(close(pixel(&image, SIZE + SIZE / 2, SIZE / 2), [0, 0, 255, 255])); - assert!(close(pixel(&image, SIZE, SIZE / 2), BLACK), "each panel's own background between"); - assert_eq!(gpu.work().late, 0, "what composes the panels was asked for first"); + assert!(close(pixel(&image, SIZE, SIZE / 2), BLACK), "each panel's own background shows between the dots"); + assert_eq!(gpu.work().late, 0, "the compose pipeline was requested ahead of use"); let wrong = pollster::block_on(shot.image(&mut [&mut left])); - assert!(wrong.is_err(), "a view per panel"); + assert!(wrong.is_err(), "a shot needs a view per panel"); } #[test] @@ -102,14 +102,14 @@ fn a_transparent_picture_keeps_its_alpha() { let scene = scene.theme(Theme { background: Color::TRANSPARENT, ..plain_theme() }); let mut view = view(&gpu, scene, &data); let image = pollster::block_on(Shot::new([SIZE, SIZE]).transparent().image(&mut [&mut view])).expect("a picture"); - assert_eq!(pixel(&image, 3, 3)[3], 0, "nothing drawn is clear"); + assert_eq!(pixel(&image, 3, 3)[3], 0, "undrawn pixels are clear"); assert!(close(pixel(&image, SIZE / 2, SIZE / 2), RED), "{:?}", pixel(&image, SIZE / 2, SIZE / 2)); let png = Image::from_png(&image.encode(latkit::ImageFormat::Png).expect("written")).expect("a PNG"); assert_eq!(png, image); } #[test] -fn a_picture_waits_for_what_eases_to_land() { +fn a_picture_waits_for_easing_to_finish() { let Some(gpu) = gpu() else { return }; let (scene, data) = dot(0.0, Color::rgb(1.0, 0.0, 0.0)); let mut view = view(&gpu, scene.ease(Duration::from_secs(2)), &data); @@ -118,12 +118,12 @@ fn a_picture_waits_for_what_eases_to_land() { step(&gpu, &mut view); assert!(view.needs_render(), "the dot eases over two seconds"); let image = pollster::block_on(Shot::new([SIZE, SIZE]).image(&mut [&mut view])).expect("a picture"); - assert!(close(pixel(&image, SIZE / 2 + 40, SIZE / 2), RED), "the picture shows where it lands"); + assert!(close(pixel(&image, SIZE / 2 + 40, SIZE / 2), RED), "the picture shows the dot at its final position"); assert!(close(pixel(&image, SIZE / 2, SIZE / 2), BLACK)); } #[test] -fn a_frame_tells_whether_anything_stood_in() { +fn a_frame_reports_whether_it_drew_placeholders() { let Some(gpu) = gpu() else { return }; let (scene, data) = dot(0.0, Color::rgb(1.0, 0.0, 0.0)); let mut view = View::new(&gpu, scene).expect("valid scene"); @@ -225,7 +225,7 @@ fn a_legend_draws_its_marks_colormap_across_the_domain() { .draw(Vertices::of("graph", "Node").position("x", "y").radius(4.0).color(colors).legend("")); let mut view = view(&gpu, scene, &data); let image = pollster::block_on(Shot::new([400, 200]).image(&mut [&mut view])).expect("a picture"); - // The bar ends 16 px from the right; its middle row sits above the ticks' labels. + // The bar ends 16 px from the right edge. Its middle row is above the tick labels. let y = 200 - 16 - 16 - 4 - 4; let near = |a: [u8; 4], b: [u8; 3]| a.iter().zip(b).all(|(x, y)| x.abs_diff(y) <= 24); let low = pixel(&image, 400 - 16 - 218, y); @@ -257,8 +257,8 @@ fn a_legend_lists_named_categories_by_their_colors() { let named = field("k").colormap(map.categories(["red", "blue"])); let mut view = view(&gpu, scene.draw(node.color(named).legend("")), &data); let image = pollster::block_on(Shot::new([400, 200]).image(&mut [&mut view])).expect("a picture"); - // Without a host no names are set, so the swatches end at the right margin, a row each above the - // bottom one: rows of 16.5 px up from 16 px above the bottom. + // Without a host no names are rasterized, so the swatches end at the right margin, one row each + // above the bottom margin: rows of 16.5 px, starting 16 px above the bottom. let x = 400 - 16 - 4; assert!(close(pixel(&image, x, 200 - 16 - 25), RED), "the first category: {:?}", pixel(&image, x, 159)); assert!(close(pixel(&image, x, 200 - 16 - 8), [0, 0, 255, 255]), "the second: {:?}", pixel(&image, x, 176)); diff --git a/crates/latkit/tests/tiles.rs b/crates/latkit/tests/tiles.rs index b0d200b7..04784848 100644 --- a/crates/latkit/tests/tiles.rs +++ b/crates/latkit/tests/tiles.rs @@ -1,6 +1,6 @@ -//! Tiles: the vector tiles the camera asks for and the time tiles a plot window asks for, fetched from -//! the host once for every view, decoded, and drawn as a dataset of each view's own; GeoJSON read as -//! data. +//! Tiles: the vector tiles the camera needs and the time tiles a plot window needs are fetched +//! from the host once for all views, decoded, and drawn as a dataset in each view. GeoJSON reads +//! as data. #![cfg(feature = "gpu")] @@ -19,7 +19,7 @@ use latkit::{ const C: u32 = SIZE / 2; -/// Protocol buffer fields, written. +/// Writes protocol buffer fields. #[derive(Default)] struct Writer(Vec); @@ -51,15 +51,15 @@ fn zig(v: i64) -> u32 { ((v << 1) ^ (v >> 63)) as u32 } -/// A tile whose layer `water` holds a square covering it and reaching into its buffer, named `bay`: -/// clipped, it covers the tile exactly. +/// A tile whose layer `water` has a square named `bay` that covers the tile and extends into its +/// buffer. Clipped, it covers the tile exactly. fn tile() -> Vec { let (x, side) = (-64_i64, 4224_i64); tile_of(&[9, zig(x), zig(x), 26, zig(side), 0, 0, zig(side), zig(-side), 0, 15]) } -/// A tile whose layer `water` holds a feature of 2,025 small squares named `bay`: more points than a -/// frame affords to shape. +/// A tile whose layer `water` has a feature named `bay` of 2,025 small squares: more points than a +/// frame can afford to shape. fn dense_tile() -> Vec { let (step, side) = (90_i64, 80_i64); let mut geometry = Vec::new(); @@ -75,7 +75,7 @@ fn dense_tile() -> Vec { tile_of(&geometry) } -/// A tile whose layer `water` holds one polygon feature of `geometry`, named `bay`. +/// A tile whose layer `water` has one polygon feature named `bay` with `geometry`. fn tile_of(geometry: &[u32]) -> Vec { let mut feature = Writer::default(); feature.packed(2, &[0, 0]); @@ -94,15 +94,15 @@ fn tile_of(geometry: &[u32]) -> Vec { tile.0 } -/// A host serving every tile of `tiles/{z}/{x}/{y}` as [`tile`], or with `dense` as [`dense_tile`], -/// counting the fetches. +/// A host serving every `tiles/{z}/{x}/{y}` tile as [`tile`], or as [`dense_tile`] with `dense`, +/// and recording the fetches. #[derive(Default)] struct Served { fetched: Mutex>, dense: bool, } -/// What the GPU is given as its host: the served tiles, shared with the test. +/// The host given to the GPU: the served tiles, shared with the test. struct Serving(Arc); impl Host for Serving { @@ -118,7 +118,7 @@ impl Host for Serving { } #[test] -fn tiles_the_camera_asks_for_draw_as_a_dataset() { +fn tiles_the_camera_needs_draw_as_a_dataset() { let Some(gpu) = gpu() else { return }; let served = Arc::new(Served::default()); gpu.set_host(Serving(served.clone())); @@ -144,7 +144,7 @@ fn tiles_the_camera_asks_for_draw_as_a_dataset() { assert_eq!((stats.tiles, stats.tiles_loading), (before.len() as u64, 0), "stats count the tiles drawn"); assert!(at(&pixels, C + 8, C + 8)[0] > 150, "filled: {:?}", at(&pixels, C + 8, C + 8)); assert_eq!(errors(&mut view), Vec::new()); - // A second view of the same tiles draws what the first fetched, fetching none again. + // A second view of the same tiles draws what the first fetched, without fetching again. let mut second = View::new(&gpu, scene).expect("valid scene"); second.set_camera(camera); second.set_sources([&tiles]).expect("a source"); @@ -153,8 +153,8 @@ fn tiles_the_camera_asks_for_draw_as_a_dataset() { assert_eq!(served.fetched.lock().expect("unpoisoned").len(), asked, "fetched once for every view"); assert_eq!(ids(&second), before, "the same rows"); drop(second); - // Zooming in asks for finer tiles, which replace their ancestors in the frame the last of them - // lands in. + // Zooming in needs finer tiles, which replace their ancestors in the frame where the last of + // them arrives. view.set_camera(Camera::Map { center: [10.0, 10.0, 0.0], zoom: 6.0, pitch: 0.0, bearing: 0.0 }); let mut loading = false; for _ in 0..SETTLE { @@ -165,13 +165,13 @@ fn tiles_the_camera_asks_for_draw_as_a_dataset() { } loading |= now; } - assert!(ids(&view).iter().all(|id| !before.contains(id)), "finer tiles' rows, the frame they landed in"); + assert!(ids(&view).iter().all(|id| !before.contains(id)), "finer tiles replace their ancestors on arrival"); view.set_sources::([]).expect("no sources"); - assert!(view.snapshot("base").is_none(), "what no source gives goes"); + assert!(view.snapshot("base").is_none(), "a dataset no source provides is removed"); } #[test] -fn tiles_land_shaped_by_the_jobs_decoding_them() { +fn tiles_arrive_shaped_by_the_jobs_that_decode_them() { let Some(gpu) = gpu() else { return }; let served = Arc::new(Served { dense: true, ..Served::default() }); gpu.set_host(Serving(served.clone())); @@ -182,26 +182,26 @@ fn tiles_land_shaped_by_the_jobs_decoding_them() { view.set_camera(camera(2.0)); view.set_sources([&Tiles::new("base", "tiles/{z}/{x}/{y}.pbf")]).expect("a source"); frame(&gpu, &mut view); - // As the camera zooms in and out, tiles come and go: a job decodes each, shaping its rows as it - // does, and none shapes them again, though a frame draws the rows the one before took. + // As the camera zooms in and out, tiles come and go. A job decodes each tile and shapes its + // rows, and no frame shapes them again, even when it draws rows an earlier frame loaded. let fetches = || served.fetched.lock().expect("unpoisoned").len() as u64; let (jobs, asked) = (gpu.work().jobs, fetches()); for zoom in [6.0, 0.0] { view.set_camera(camera(zoom)); frame(&gpu, &mut view); } - assert!(fetches() > asked, "tiles came"); - assert_eq!(gpu.work().jobs - jobs, fetches() - asked, "a job a tile"); + assert!(fetches() > asked, "tiles were fetched"); + assert_eq!(gpu.work().jobs - jobs, fetches() - asked, "one job per tile"); assert_eq!(errors(&mut view), Vec::new()); } -/// A host serving files by path, keeping the paths asked for. +/// A host serving files by path, recording the requested paths. struct Files { files: BTreeMap>, asked: Mutex>, } -/// What the GPU is given as its host: the files, shared with the test. +/// The host given to the GPU: the files, shared with the test. struct ServingFiles(Arc); impl Host for ServingFiles { @@ -216,7 +216,7 @@ impl Host for ServingFiles { } #[test] -fn time_tiles_draw_the_spans_a_plot_window_asks_for() { +fn time_tiles_draw_the_spans_a_plot_window_needs() { let Some(gpu) = gpu() else { return }; let data = Data::new("signals"); data.apply(Patch::new().axis("time", "s").set(Table::new("Signal", Ids::count(1)))).expect("valid"); @@ -249,7 +249,7 @@ fn time_tiles_draw_the_spans_a_plot_window_asks_for() { let pixels = frame(&gpu, &mut tiled); assert_eq!(errors(&mut tiled), Vec::new()); let stats = tiled.stats(); - assert_eq!(stats.tiles_loading, 0, "every span asked for landed"); + assert_eq!(stats.tiles_loading, 0, "every requested span arrived"); let differing = pixels.iter().zip(&expected).filter(|(a, b)| !common::close(**a, **b)).count(); if differing > SIZE as usize { let ascii = |p: &[[u8; 4]]| { @@ -266,7 +266,7 @@ fn time_tiles_draw_the_spans_a_plot_window_asks_for() { assert!(differing <= SIZE as usize, "the window draws as the whole history does: {differing} pixels differ"); assert!(pixels.iter().any(|p| p[0] > 200), "the trace draws"); let asked = served.asked.lock().expect("unpoisoned").clone(); - assert!(asked.len() * 8 < written, "only the spans the window draws: {asked:?} of {written}"); + assert!(asked.len() * 8 < written, "only the spans the window draws are fetched: {asked:?} of {written}"); } #[test] diff --git a/crates/latkit/tests/transitions.rs b/crates/latkit/tests/transitions.rs index 0be36bf0..08cd35ed 100644 --- a/crates/latkit/tests/transitions.rs +++ b/crates/latkit/tests/transitions.rs @@ -1,6 +1,7 @@ -//! Changes ease: a moved vertex starts where it was, a removed one fades where it stood, and edges -//! follow their vertices; another dataset of the same name replaces rows rather than moving them; -//! sampled fields and new frames never ease. +//! Changes ease: a moved vertex starts from its old position, a removed one fades in place, and +//! edges follow their vertices. Another dataset with the same name replaces rows instead of moving +//! them. Sampled fields and new frames never ease. A transition evaluates its channels once, and a +//! change to one page evaluates only that page's rows. #![cfg(feature = "gpu")] @@ -53,7 +54,7 @@ fn moved_vertices_start_where_they_were_and_their_edges_follow() { assert!(close(at(&pixels, c - 30, c - 40), BLACK), "and has not jumped"); assert!(view.needs_render(), "the transition runs"); let pixels = frame(&gpu, &mut view); - assert!(close(at(&pixels, c - 30, c - 40), WHITE), "it ends where its data says"); + assert!(close(at(&pixels, c - 30, c - 40), WHITE), "it ends at its new position"); assert!(close(at(&pixels, c - 30, c), BLACK)); let middle = at(&pixels, c, c - 20); assert!(middle[1] > 200 && middle[0] < 60, "the edge joins the moved vertex: {middle:?}"); @@ -78,7 +79,7 @@ fn removed_rows_fade_where_they_stood_and_new_rows_grow() { .expect("valid"); let pixels = step(&gpu, &mut view); assert!(close(at(&pixels, c - 30, c), WHITE), "the removed vertex still shows: {:?}", at(&pixels, c - 30, c)); - assert!(close(at(&pixels, c + 30, c), WHITE), "the kept one stays"); + assert!(close(at(&pixels, c + 30, c), WHITE), "the remaining one stays"); assert!(at(&pixels, c, c + 40)[0] < 60, "the new vertex starts from nothing: {:?}", at(&pixels, c, c + 40)); let pixels = frame(&gpu, &mut view); assert!(close(at(&pixels, c - 30, c), BLACK), "the removed vertex is gone"); @@ -96,7 +97,7 @@ fn another_dataset_of_the_same_name_replaces_rows_rather_than_moving_them() { view.set_sources([graph()]).expect("a source"); let c = SIZE / 2; frame(&gpu, &mut view); - // The same ids in a dataset of its own: its rows are not the drawn ones moved. + // The same ids in a separate dataset: its rows are new rows, not the drawn ones moved. let other = graph(); other.apply(Table::new("Node", vec![1_u64]).field("y", vec![40.0])).expect("valid"); view.set_sources([&other]).expect("a source"); @@ -110,7 +111,7 @@ fn another_dataset_of_the_same_name_replaces_rows_rather_than_moving_them() { } #[test] -fn sampled_fields_and_zero_ease_change_at_once() { +fn zero_ease_moves_a_vertex_at_once() { let Some(gpu) = gpu() else { return }; let mut view = View::new(&gpu, scene().ease(Duration::ZERO)).expect("valid scene"); view.set_camera(camera()); @@ -123,6 +124,39 @@ fn sampled_fields_and_zero_ease_change_at_once() { assert!(close(at(&pixels, c - 30, c - 40), WHITE), "without ease the vertex moves at once"); } +#[test] +fn sampled_fields_and_new_frames_change_at_once() { + let Some(gpu) = gpu() else { return }; + let mut view = View::new(&gpu, scene()).expect("valid scene"); + view.set_camera(camera()); + let data = Data::new("graph"); + data.apply( + Patch::new() + .set(Table::new("Node", vec![1_u64, 2]).field("x", vec![-30.0, 30.0])) + .set(Table::new("Link", Ids::count(1)).reference("from", "Node", vec![1_u64]).reference( + "to", + "Node", + vec![2_u64], + )) + .append("Node", [0.0], [("y", vec![0.0, 0.0])]) + .append("Node", [1.0], [("y", vec![40.0, 0.0])]), + ) + .expect("valid"); + view.set_sources([&data]).expect("a source"); + view.set_at(0.0); + let c = SIZE / 2; + frame(&gpu, &mut view); + // The scene eases over two seconds, but a sampled field shows its value at `at` at once. + view.set_at(1.0); + let pixels = step(&gpu, &mut view); + assert!(close(at(&pixels, c - 30, c - 40), WHITE), "the vertex shows the value at the new coordinate"); + data.apply(Patch::new().append("Node", [2.0], [("y", vec![-40.0, 0.0])])).expect("valid"); + view.set_at(2.0); + let pixels = step(&gpu, &mut view); + assert!(close(at(&pixels, c - 30, c + 40), WHITE), "a new frame shows at once"); + assert_eq!(errors(&mut view), Vec::new()); +} + #[test] fn a_transition_evaluates_once_and_eases_without_evaluating() { let Some(gpu) = gpu() else { return }; @@ -135,7 +169,7 @@ fn a_transition_evaluates_once_and_eases_without_evaluating() { data.apply(Table::new("Node", vec![1_u64]).field("y", vec![40.0])).expect("valid"); step(&gpu, &mut view); let evaluated = gpu.work().evaluated; - // A second into a transition of two. + // One second into a two-second transition. let mut pixels = Vec::new(); for _ in 0..60 { pixels = step(&gpu, &mut view); @@ -147,7 +181,7 @@ fn a_transition_evaluates_once_and_eases_without_evaluating() { } #[test] -fn a_change_to_one_page_evaluates_its_rows_alone() { +fn a_change_to_one_page_evaluates_only_its_rows() { let Some(gpu) = gpu() else { return }; let rows = (1 << 16) + 16; let scene = Scene::new(plane()) diff --git a/crates/latkit/tests/validation.rs b/crates/latkit/tests/validation.rs index 5785940a..e28db073 100644 --- a/crates/latkit/tests/validation.rs +++ b/crates/latkit/tests/validation.rs @@ -23,7 +23,7 @@ fn validation_requires_unique_datasets_and_drawn_tables() { assert!(scene.validate(&[data(), data()]).unwrap_err().to_string().contains("two sources named")); assert!(scene.validate(&[Data::new("grid").into()]).unwrap_err().to_string().contains("Bus")); let tiles = [Tiles::new("grid", "history/frames.json").into()]; - assert!(scene.validate(&tiles).is_ok(), "tiles' marks are checked once they land"); + assert!(scene.validate(&tiles).is_ok(), "tiles' marks are checked once the tiles arrive"); } #[test] @@ -42,7 +42,7 @@ fn diagrams_allow_positions_to_be_derived_by_layout() { } #[test] -fn shadows_drop_onto_a_diagram_sheet_alone() { +fn shadows_are_valid_only_on_a_diagram() { let block = Vertices::of("grid", "Bus").position("x", "y").shadow(true); let on_a_plane = Scene::new(Plane::default()).draw(block.clone()).validate(&[data()]); assert!(on_a_plane.unwrap_err().to_string().contains("only a diagram has"), "refused on a plane"); diff --git a/crates/latkit/tests/videos.rs b/crates/latkit/tests/videos.rs index 797dc10d..cbe02acd 100644 --- a/crates/latkit/tests/videos.rs +++ b/crates/latkit/tests/videos.rs @@ -1,5 +1,5 @@ -//! Videos: frames compose into NV12 on the GPU, a frame where nothing moved is held rather than drawn -//! or encoded, keyframes fall at their times, and what the encoder gives is written as MP4. +//! Videos: frames compose into NV12 on the GPU, a frame where nothing moved is held instead of +//! drawn or encoded, keyframes fall at their times, and the encoder's output is written as MP4. #![cfg(feature = "gpu")] @@ -14,11 +14,11 @@ use latkit::Ids; use latkit::host::{Encoder, Host, Raster, TextRun, Unit}; use latkit::{Color, Data, Encoding, Label, Mp4, Pending, Pixels, Scene, Shot, Sink, Table, Vertices, View}; -/// The pictures an encoder was given: time, whether a keyframe, and the NV12 bytes. +/// The pictures an encoder received: time, whether it is a keyframe, and the NV12 bytes. type Pictures = Arc)>>>; -/// A host whose encoder keeps the pictures it is given and answers each with a stand-in unit: a -/// parameter set pair and a slice, which the muxer takes as H.264. +/// A host whose encoder keeps the pictures it receives and returns a placeholder unit for each: a +/// parameter set pair and a slice, which the muxer accepts as H.264. struct Keeping(Pictures); impl Host for Keeping { @@ -66,7 +66,7 @@ fn view(gpu: &latkit::Gpu) -> View { view } -/// Luma and chroma of NV12 `bytes` of a square picture of `SIZE` at pixel `x`, `y`. +/// The luma and chroma at pixel (`x`, `y`) of NV12 `bytes` for a `SIZE` by `SIZE` picture. fn sample(bytes: &[u8], x: u32, y: u32) -> [u8; 3] { let luma = bytes[(y * SIZE + x) as usize]; let chroma = (SIZE * SIZE + (y / 2) * SIZE + (x / 2) * 2) as usize; @@ -94,7 +94,7 @@ fn frames_compose_into_nv12_and_hold_where_nothing_moved() { } }); assert_eq!(video.frames(), 9); - assert!(!view.lent() || video.frames() > 0, "the video holds the view while it lasts"); + assert!(!view.lent() || video.frames() > 0, "the view is lent to the video while it lasts"); let file = pollster::block_on(video.finish()).expect("a file").into_inner(); assert!(!view.lent(), "a finished video gives the view back"); let pictures = pictures.lock().unwrap_or_else(PoisonError::into_inner); @@ -103,7 +103,7 @@ fn frames_compose_into_nv12_and_hold_where_nothing_moved() { let [first, moved] = [&pictures[0].2, &pictures[1].2]; assert_eq!(first.len(), (SIZE * SIZE * 3 / 2) as usize); let c = SIZE / 2; - // BT.709 limited range: red is 63, 102, 240; black is 16, 128, 128. + // BT.709 limited range: red is (63, 102, 240) and black is (16, 128, 128). assert!(near(sample(first, c, c), [63, 102, 240]), "{:?}", sample(first, c, c)); assert!(near(sample(first, 2, 2), [16, 128, 128]), "{:?}", sample(first, 2, 2)); assert!(near(sample(moved, c + 30, c), [63, 102, 240]), "the moved dot"); @@ -143,11 +143,11 @@ fn labeled_marks_easing_between_frames_complete_every_frame() { } #[test] -fn a_video_is_whole_blocks_and_needs_an_encoder() { +fn a_video_needs_an_encoder_and_a_size_in_whole_blocks() { let Some(gpu) = gpu() else { return }; assert!(Shot::new([SIZE, SIZE]).video(&gpu, 30.0, Mp4::new(Vec::new())).is_err(), "a host without an encoder"); gpu.set_host(Keeping(Pictures::default())); - assert!(Shot::new([SIZE, SIZE]).video(&gpu, 0.0, Mp4::new(Vec::new())).is_err(), "a frame rate"); + assert!(Shot::new([SIZE, SIZE]).video(&gpu, 0.0, Mp4::new(Vec::new())).is_err(), "a frame rate above zero"); assert!( Shot::new([SIZE + 2, SIZE]).video(&gpu, 30.0, Mp4::new(Vec::new())).is_err(), "a width of whole 4-pixel blocks" @@ -163,7 +163,7 @@ fn a_video_is_whole_blocks_and_needs_an_encoder() { assert!(clear.check(latkit::Pixels::Rgba).is_ok() && clear.check(latkit::Pixels::Nv12).is_err()); } -/// A sink keeping what it is given: each frame's index and whether it was drawn, and the RGBA bytes. +/// A sink that records each frame's index and RGBA bytes (`None` if held), and the end index. #[derive(Default)] struct Frames { taken: Vec<(u64, Option>)>, @@ -188,7 +188,7 @@ impl Sink for Frames { fn any_sink_takes_every_frame_in_order_and_held_ones_without_pixels() { let Some(gpu) = gpu() else { return }; let mut view = view(&gpu); - // RGBA takes any width: no encoder, no 4-pixel blocks. + // RGBA allows any width: there is no encoder, so no 4-pixel blocks. let mut video = Shot::new([SIZE + 2, SIZE]).video(&gpu, 30.0, Frames::default()).expect("a video"); pollster::block_on(async { for _ in 0..4 { diff --git a/docs/_static/performance.csv b/docs/_static/performance.csv index 81e1d007..ae18321c 100644 --- a/docs/_static/performance.csv +++ b/docs/_static/performance.csv @@ -1,15 +1,15 @@ Scenario,Largest,Open (ms),Frame (ms),GPU (ms),Pick (ms) -"A first visit: latkit, the GPU, and pipelines made without a shader cache","10,000 rows",504,,, -"Vertices on a plane, colored through a colormap","2,000,000 rows",769,0.1,0.5,12.3 -"A lattice grid on the globe: buses, and the geodesic lines between them","2,000,000 buses","1,619",0.4,1.3,15.8 -"Every vertex labeled: placement, collisions, and the texts set","1,000,000 rows",685,1.5,0.9, -Edges by the thousand between the same sixteen pairs of vertices,"100,000 edges",530,0.1,0.2,6.3 -"Polygons of 33 points on the mercator map, triangulated in jobs","30,000 polygons",673,0.1,1.2,6.1 -"Vector tiles fetched as the camera asks for them, from a local server","4,096 squares a tile",507,0.2,0.2, -"Blocks with ports in trees of 64, laid out in layers, wires routed around them","10,000 blocks",575,0.1,0.2, -"A plot of traces 1,024 frames long, fed a frame every animation frame","10,000 traces",592,0.2,23.6,20.5 -One trace with a long history: levels of detail,"1,000,000 frames",578,0.2,0.4, -"Every row moving to a new place at once, easing over a second","1,000,000 rows",730,0.2,0.2, -"Views of one GPU drawing one dataset: pages shared, not copied",8 views,621,0.4,2.8, -"A 16 MiB budget: what draws fits, through tiles, and nothing fails","2,000,000 buses","1,654",0.2,0.2, -"Pictures and video of two views of a 100,000-bus grid","8,294,400 pixels",560,,, +"A first visit: latkit, the GPU, and pipelines made without a shader cache","10,000 rows",604,,, +"Vertices on a plane, colored through a colormap","2,000,000 rows",839,0.2,0.5,13.0 +"A lattice grid on the globe: buses, and the geodesic lines between them","2,000,000 buses","1,598",0.3,1.3,11.8 +"Every vertex labeled: placement, collisions, and text layout","1,000,000 rows",688,1.5,0.9, +Edges by the thousand between the same sixteen pairs of vertices,"100,000 edges",530,0.1,0.2,6.2 +"Polygons of 33 points on the mercator map, triangulated in jobs","30,000 polygons",668,0.1,1.1,6.5 +Vector tiles fetched from a local server as the camera needs them,"4,096 squares a tile",504,0.1,0.1, +"Blocks with ports in trees of 64, laid out in layers, wires routed around them","10,000 blocks",600,0.1,0.2, +"A plot of traces 1,024 frames long, fed a frame every animation frame","10,000 traces",590,0.3,23.3,20.9 +One trace with a long history: levels of detail,"1,000,000 frames",599,0.1,0.4, +"Every row moving to a new place at once, easing over a second","1,000,000 rows",688,0.1,0.2, +"Views of one GPU drawing one dataset: pages shared, not copied",8 views,549,0.3,2.8, +"A 16 MiB budget: paged tables fit, and nothing fails","2,000,000 buses","1,493",0.1,0.2, +"Pictures and video of two views of a 100,000-bus grid","8,294,400 pixels",579,,, diff --git a/docs/cli.md b/docs/cli.md index 1700b9ec..dbdd0e96 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,8 +1,8 @@ # Command line -The `latkit` executable draws scenes of datasets into images, image sequences, raw frames, and -video on the machine's GPU, with the browser's renderer. Its inputs are latkit's JSON, so any -language can write them. +The `latkit` executable renders scenes to images, image sequences, raw frames, and video on the +machine's GPU, with the same renderer as the browser. Its inputs are latkit's JSON, so any language +can write them. ```sh cargo install latkit-cli # or, from the repository: cargo build --locked -p latkit-cli --profile cli @@ -23,26 +23,29 @@ latkit schema scene > scene.schema.json | `render` | draws the scenes, one per `--panel`, and writes `--output` | | `validate` | reads and checks everything `render` would, frames included, without a GPU | | `inspect` | prints datasets as JSON: tables, rows, fields and their kinds, the axis and its span | -| `query` | prints the rows a query keeps, sorted and paged, as JSON Lines | +| `query` | prints the rows a query matches, sorted and paged, as JSON Lines | | `tile` | writes a dataset as [time tiles](guide.md#fetch-long-histories-in-spans) in `--output` | | `schema` | prints the JSON Schema of every input, or of `scene`, `data`, `patch`, `query`, `camera`, `fit`, `frames` | ## Inputs -- **Scenes** are the [scene](guide.md#scenes) JSON, with a `title` and `subtitle`, and on any mark a - `legend`. `--camera FILE` gives scenes cameras, in order; a view without one frames `--fit` - (`all`, or a fit as JSON). `--insets T,R,B,L` keeps room clear. +- **Scenes** are [scene](guide.md#scenes) JSON files, one per panel; their titles, subtitles, and + mark legends are drawn into the picture. `--camera FILE` sets a scene's camera, in scene order; a + view without one frames `--fit` (`all`, or a fit as JSON). `--insets T,R,B,L` keeps CSS pixels + clear at each edge. - **Datasets** (`-d [NAME=]FILE`) are a [JSON document](guide.md#json), latkit's - [bytes](guide.md#bytes), or GeoJSON, which takes a `NAME`. A `NAME` renames any dataset. - Slices of binary files load relative to the document. -- **Tiles** (`--tiles NAME=FILE`) are time tiles' `frames.json`, as `tile` writes it, or a PMTiles - file, TileJSON, or a `{z}/{x}/{y}` template on disk. -- **Fonts** (`--font FILE`, repeated) replace system fonts; the first is the fallback. -- One input at most reads stdin (`-`). + [bytes](guide.md#bytes), or GeoJSON, which needs a `NAME`. A `NAME` renames any dataset. + Binary file slices load relative to the document. +- **Tiles** (`--tiles NAME=FILE`) are a time-tile `frames.json` written by `tile`, or vector tiles + on disk: a PMTiles file, TileJSON, or a `{z}/{x}/{y}` template. +- **Fonts** (`--font FILE`, repeatable) replace the system fonts; the first is used for generic + families such as `sans-serif`. +- At most one input can read stdin (`-`). ## Frames -`--frames` is JSON Lines, a line an output frame at `n / --fps` seconds. Every key is optional: +`--frames` is a JSON Lines file with one line per output frame; line `n` shows at `n / --fps` +seconds. Every key is optional: ```json { @@ -65,36 +68,39 @@ latkit schema scene > scene.schema.json } ``` -| Key | Changes | -| -------- | ------------------------------------------------------------------------- | -| `at` | the axis coordinate every view shows | -| `patch` | datasets by name, in order, each a patch in latkit's JSON | -| `camera` | where views look at once; `fly` eases there over twice the scene's `ease` | -| `fit` | what views frame | -| `insets` | CSS pixels each view keeps clear | -| `orbit` | degrees a second a map or plane turns | +| Key | Changes | +| -------- | ------------------------------------------------------------------------------ | +| `at` | the axis coordinate every view shows | +| `patch` | patches by dataset name, applied in order, in latkit's JSON | +| `camera` | where views look, set at once; `fly` eases there over twice the scene's `ease` | +| `fit` | what views frame | +| `insets` | CSS pixels each view keeps clear | +| `orbit` | degrees per second a map or plane turns | `camera`, `fly`, and `fit` take one value for every view, or a list with one per view (`null` -leaves one). Frames load a couple ahead of the one drawing. Scenes ease as they say; `"ease": 0` -shows each frame's data as it is. +leaves that view unchanged). The CLI reads and loads up to two frames ahead of the one drawing. +Changes ease over each scene's `ease`; set `"ease": 0` to show each frame's data without easing. ## Outputs | Output | Writes | | ------------------------ | ---------------------------------------------------------------- | | `.png`, `.jpg`, `.webp` | one picture; JPEG at `--quality` | -| `frame-%04d.png` | an image a frame (`--frames`) | +| `frame-%04d.png` | one image per frame (`--frames`) | | `.rgba`, `--format rgba` | straight-alpha RGBA, frames back to back | | `.y4m` | YUV4MPEG2, 4:2:0 BT.709, for any encoder (`--frames`) | | `.mp4` | H.264 in fragmented MP4 from the platform's encoder (`--frames`) | -A still waits until every view is complete and settled; `--elapsed` fixes its effect time. A frame -where nothing moved is held. A file is replaced only once it is whole. MP4 needs Windows (Media -Foundation); elsewhere, pipe Y4M to an encoder. `--deadline` bounds each picture or frame. +A still waits until every view is complete and motion has settled; `--elapsed` sets the time its +animated effects show. A video frame where nothing moved repeats the frame before. An output file is +replaced only once it is completely written. MP4 needs Windows (Media Foundation); elsewhere, pipe +Y4M to an encoder. `--deadline` limits how long each picture or video frame may take (30 seconds by +default). ## Diagnostics -Exit codes: 0 success, 1 failure, 2 usage. Errors print `latkit: SOURCE: message` with the part's -path, such as `frames[3].patch.grid[0].Bus.x`. `--diagnostics json` writes one object a line to -stderr, `{ type, detail }`: `progress` after each frame, `stats` when done, and `error` with its +Exit codes: 0 success, 1 failure, 2 usage error. Errors print as `latkit: SOURCE: message`, where +`SOURCE` is the input file; for invalid input, the message starts with the path to the invalid +part, such as `frames[3].patch.grid[0].Bus.x`. `--diagnostics json` writes one JSON object per line +to stderr, `{ type, detail }`: `progress` after each frame, `stats` when done, and `error` with its `code`, `message`, `path`, and `source`. diff --git a/docs/examples/bloom.md b/docs/examples/bloom.md index e67706a3..8bbb045f 100644 --- a/docs/examples/bloom.md +++ b/docs/examples/bloom.md @@ -5,10 +5,10 @@ :class: picture ``` -Thirty-two thousand seeds a golden angle apart, sized and colored by a wave winding through them, through a colormap of their own. +Thirty-two thousand seeds placed a golden angle apart, sized and colored by a spiral wave through a custom colormap. -The picture is drawn by the `latkit` command line from this module, which writes its data with -`Patch` and its scene as a typed `Scene`; `node docs/gallery/render.ts bloom` draws it again. +The `latkit` command line draws this picture from the module below, which writes its data with +`Patch` and its scene as a typed `Scene`. `node docs/gallery/render.ts bloom` draws it again. ```{literalinclude} ../gallery/bloom.ts :language: typescript diff --git a/docs/examples/clifford.md b/docs/examples/clifford.md index ec3b5dcd..58f66260 100644 --- a/docs/examples/clifford.md +++ b/docs/examples/clifford.md @@ -5,10 +5,10 @@ :class: picture ``` -A strange attractor of 260,000 translucent points, colored through a cyclic colormap by where each step heads, so density and flow both show. +A strange attractor of 260,000 translucent points, colored by the direction of each step through a cyclic colormap, so both density and flow show. -The picture is drawn by the `latkit` command line from this module, which writes its data with -`Patch` and its scene as a typed `Scene`; `node docs/gallery/render.ts clifford` draws it again. +The `latkit` command line draws this picture from the module below, which writes its data with +`Patch` and its scene as a typed `Scene`. `node docs/gallery/render.ts clifford` draws it again. ```{literalinclude} ../gallery/clifford.ts :language: typescript diff --git a/docs/examples/currents.md b/docs/examples/currents.md index a699d81b..856b795e 100644 --- a/docs/examples/currents.md +++ b/docs/examples/currents.md @@ -5,10 +5,10 @@ :class: picture ``` -Streamlines traced through a field of seven vortices, as a wind map draws them: each a path of geometry, colored by its speed. +Streamlines traced through a field of seven vortices, as on a wind map. Each streamline is a path geometry, colored by its speed. -The picture is drawn by the `latkit` command line from this module, which writes its data with -`Patch` and its scene as a typed `Scene`; `node docs/gallery/render.ts currents` draws it again. +The `latkit` command line draws this picture from the module below, which writes its data with +`Patch` and its scene as a typed `Scene`. `node docs/gallery/render.ts currents` draws it again. ```{literalinclude} ../gallery/currents.ts :language: typescript diff --git a/docs/examples/diagram.md b/docs/examples/diagram.md index 4b201941..497f0410 100644 --- a/docs/examples/diagram.md +++ b/docs/examples/diagram.md @@ -5,10 +5,10 @@ :class: picture ``` -Blocks in nested groups, placed in layers by latkit's own layout, wires routed at right angles between their ports, a feedback loop, and a status ring. +Blocks in nested groups, placed in layers by latkit's layered layout, with wires routed at right angles between their ports, a feedback loop, and a status ring. -The picture is drawn by the `latkit` command line from this module, which writes its data with -`Patch` and its scene as a typed `Scene`; `node docs/gallery/render.ts diagram` draws it again. +The `latkit` command line draws this picture from the module below, which writes its data with +`Patch` and its scene as a typed `Scene`. `node docs/gallery/render.ts diagram` draws it again. ```{literalinclude} ../gallery/diagram.ts :language: typescript diff --git a/docs/examples/frequency.md b/docs/examples/frequency.md index b9bf3240..0e302920 100644 --- a/docs/examples/frequency.md +++ b/docs/examples/frequency.md @@ -5,10 +5,10 @@ :class: picture ``` -A simulated fault on the ACTIVSg10k case: every machine's speed traced over ten seconds, read straight from the recording's sampled field, and colored by distance from the fault. +A simulated fault on the ACTIVSg10k case: every machine's speed over ten seconds, traced directly from the recording's sampled field and colored by distance from the fault. -The picture is drawn by the `latkit` command line from this module, which writes its data with -`Patch` and its scene as a typed `Scene`; `node docs/gallery/render.ts frequency` draws it again. +The `latkit` command line draws this picture from the module below, which writes its data with +`Patch` and its scene as a typed `Scene`. `node docs/gallery/render.ts frequency` draws it again. ```{literalinclude} ../gallery/frequency.ts :language: typescript diff --git a/docs/examples/globe.md b/docs/examples/globe.md index bb1a350f..9d118603 100644 --- a/docs/examples/globe.md +++ b/docs/examples/globe.md @@ -5,10 +5,10 @@ :class: picture ``` -Routes between forty cities along great circles, comets flowing on them, over coastlines and borders read from GeoJSON, on a globe with its atmosphere. +Routes between forty cities along great circles, with comets flowing along them, over coastlines and borders read from GeoJSON, on a globe with an atmosphere. -The picture is drawn by the `latkit` command line from this module, which writes its data with -`Patch` and its scene as a typed `Scene`; `node docs/gallery/render.ts globe` draws it again. +The `latkit` command line draws this picture from the module below, which writes its data with +`Patch` and its scene as a typed `Scene`. `node docs/gallery/render.ts globe` draws it again. ```{literalinclude} ../gallery/globe.ts :language: typescript diff --git a/docs/examples/grid.md b/docs/examples/grid.md index ea862f7f..cc5e26b2 100644 --- a/docs/examples/grid.md +++ b/docs/examples/grid.md @@ -5,10 +5,10 @@ :class: picture ``` -The example suite's ACTIVSg70k case, 70,000 buses and 88,207 branches, over the states on a Mercator map tilted toward the Great Lakes. Each line takes its color and width from its voltage level, and the backbone draws on top by priority. +The example suite's ACTIVSg70k case, with 70,000 buses and 88,207 branches, over the US states on a Mercator map tilted toward the Great Lakes. Each line's color and width follow its voltage level, and `priority` draws the high-voltage backbone on top. -The picture is drawn by the `latkit` command line from this module, which writes its data with -`Patch` and its scene as a typed `Scene`; `node docs/gallery/render.ts grid` draws it again. +The `latkit` command line draws this picture from the module below, which writes its data with +`Patch` and its scene as a typed `Scene`. `node docs/gallery/render.ts grid` draws it again. ```{literalinclude} ../gallery/grid.ts :language: typescript diff --git a/docs/examples/helios.md b/docs/examples/helios.md index 40f75d8b..0a959c70 100644 --- a/docs/examples/helios.md +++ b/docs/examples/helios.md @@ -5,10 +5,10 @@ :class: picture ``` -A network in five rings: vertices colored through a colormap, labeled hubs, and comets flowing along every branch. +A network in five rings, with vertices colored through a colormap, labeled hubs, and comets flowing along every branch. -The picture is drawn by the `latkit` command line from this module, which writes its data with -`Patch` and its scene as a typed `Scene`; `node docs/gallery/render.ts helios` draws it again. +The `latkit` command line draws this picture from the module below, which writes its data with +`Patch` and its scene as a typed `Scene`. `node docs/gallery/render.ts helios` draws it again. ```{literalinclude} ../gallery/helios.ts :language: typescript diff --git a/docs/examples/index.md b/docs/examples/index.md index 3866f8c9..bfb891f3 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -1,8 +1,8 @@ # Examples -Every picture here is drawn by latkit from the module on its page: data written with `Patch`, a -typed `Scene`, and the `latkit` command line. The -[example suite](https://github.com/lukelowry/latkit/tree/main/npm/examples) runs the same features +The `latkit` command line draws every picture here from the module on its page, which writes its +data with `Patch` and its scene as a typed `Scene`. The +[example suite](https://github.com/lukelowry/latkit/tree/main/npm/examples) shows the same features live in the browser. :::{container} cards diff --git a/docs/examples/opening.md b/docs/examples/opening.md index f7ad413c..1c53aae3 100644 --- a/docs/examples/opening.md +++ b/docs/examples/opening.md @@ -5,27 +5,27 @@ :class: picture banner ``` -The ACTIVSg70k case on the globe as its Little Rock to Newark 765 kV line opens, in red. A DC -power flow of all 70,000 buses finds the angles before the opening and how far each one shifts -after it, and each of the 88,207 branches is colored by the flow it picks up, in amber, or sheds, in -cyan; changes under 10 MW stay grey, the distribution web under 115 kV dim beneath the backbone. -Each bus stands as high as its frequency 2.5 s after the opening, recorded from a simulation of the -same event, and poles drop from the high-voltage buses to the ground. +The ACTIVSg70k case on the globe as its Little Rock to Newark 765 kV line, in red, opens. A DC +power flow of all 70,000 buses computes the bus angles before the opening and how far each shifts +after it. Each of the 88,207 branches is colored by the flow it picks up (amber) or sheds (cyan). +Changes under 10 MW stay grey, and branches below 230 kV with such small changes form a dim web +beneath the backbone. Each bus's height is its frequency 2.5 s after the opening, recorded from a +simulation of the same event, and poles drop to the ground from a sample of buses at 345 kV and up. -The picture is drawn by the `latkit` command line from this module, which writes its data with -`Patch` and its scene as a typed `Scene`; `node docs/gallery/render.ts opening` draws it again. +The `latkit` command line draws this picture from the module below, which writes its data with +`Patch` and its scene as a typed `Scene`. `node docs/gallery/render.ts opening` draws it again. ```{literalinclude} ../gallery/opening.ts :language: typescript ``` -The power flow solves the grid's susceptance matrix by conjugate gradients, in a few seconds: +The power flow solves the grid's susceptance matrix with conjugate gradients in a few seconds: ```{literalinclude} ../gallery/flow.ts :language: typescript ``` -The recording is too large for the repository; `record.ts` keeps the one instant drawn: +The recording is too large for the repository, so `record.ts` saves only the instant drawn: ```{literalinclude} ../gallery/record.ts :language: typescript diff --git a/docs/examples/terrain.md b/docs/examples/terrain.md index 2006db37..b51d913a 100644 --- a/docs/examples/terrain.md +++ b/docs/examples/terrain.md @@ -5,10 +5,10 @@ :class: picture ``` -Ridged mountains as faces between a grid of vertices: each face blends its corners' colors, lit by the normals the GPU works out, in three dimensions. +Ridged mountains in three dimensions, as faces over a grid of vertices. Each face blends its corners' colors and is lit by normals the GPU computes. -The picture is drawn by the `latkit` command line from this module, which writes its data with -`Patch` and its scene as a typed `Scene`; `node docs/gallery/render.ts terrain` draws it again. +The `latkit` command line draws this picture from the module below, which writes its data with +`Patch` and its scene as a typed `Scene`. `node docs/gallery/render.ts terrain` draws it again. ```{literalinclude} ../gallery/terrain.ts :language: typescript diff --git a/docs/examples/texas.md b/docs/examples/texas.md index 0459fe85..0e97fd2e 100644 --- a/docs/examples/texas.md +++ b/docs/examples/texas.md @@ -5,10 +5,10 @@ :class: picture ``` -A synthetic Texas grid: generators sized by the square root of their output and labeled by priority, the most a frame can fit, with comets flowing along its backbone in map units, so they keep their place as the view zooms. +A synthetic Texas grid. Generators are sized by the square root of their output, and up to ten of the largest are labeled. Comets flow along the lines of 345 kV and up, spaced in map units so they keep their place as the view zooms. -The picture is drawn by the `latkit` command line from this module, which writes its data with -`Patch` and its scene as a typed `Scene`; `node docs/gallery/render.ts texas` draws it again. +The `latkit` command line draws this picture from the module below, which writes its data with +`Patch` and its scene as a typed `Scene`. `node docs/gallery/render.ts texas` draws it again. ```{literalinclude} ../gallery/texas.ts :language: typescript diff --git a/docs/examples/tides.md b/docs/examples/tides.md index ea57ba8e..4f8d4cf2 100644 --- a/docs/examples/tides.md +++ b/docs/examples/tides.md @@ -5,10 +5,10 @@ :class: picture ``` -Closed contours swelling outward, each deformed by harmonics that drift with its level, colored from the center out. +Closed contours swelling outward, each rippled by harmonics whose phases shift with its level, colored from the center out. -The picture is drawn by the `latkit` command line from this module, which writes its data with -`Patch` and its scene as a typed `Scene`; `node docs/gallery/render.ts tides` draws it again. +The `latkit` command line draws this picture from the module below, which writes its data with +`Patch` and its scene as a typed `Scene`. `node docs/gallery/render.ts tides` draws it again. ```{literalinclude} ../gallery/tides.ts :language: typescript diff --git a/docs/examples/torus.md b/docs/examples/torus.md index 393ab1e3..3dfc266b 100644 --- a/docs/examples/torus.md +++ b/docs/examples/torus.md @@ -7,8 +7,8 @@ Sixty-four filaments around a (2, 3) torus knot, drawn as edges between points with elevation and seen from above at an angle. -The picture is drawn by the `latkit` command line from this module, which writes its data with -`Patch` and its scene as a typed `Scene`; `node docs/gallery/render.ts torus` draws it again. +The `latkit` command line draws this picture from the module below, which writes its data with +`Patch` and its scene as a typed `Scene`. `node docs/gallery/render.ts torus` draws it again. ```{literalinclude} ../gallery/torus.ts :language: typescript diff --git a/docs/gallery/bloom.ts b/docs/gallery/bloom.ts index 7fb7e22c..db366f14 100644 --- a/docs/gallery/bloom.ts +++ b/docs/gallery/bloom.ts @@ -1,4 +1,4 @@ -/** Golden bloom: seeds a golden angle apart, colored by a wave turning through them. */ +/** Golden bloom: seeds placed a golden angle apart, colored by an eight-armed spiral wave. */ import { Patch } from 'latkit'; import { THEME, floats, framed, type Piece } from './look.ts'; @@ -7,7 +7,7 @@ export default function bloom(): Piece { const golden = Math.PI * (3 - Math.sqrt(5)); const r = (i: number) => 2.2 * Math.sqrt(i); const angle = (i: number) => i * golden; - // Where a seed lies around the center, unwound from its golden angle. + // The seed's polar angle around the center, in [0, 2π). const polar = (i: number) => angle(i) % (2 * Math.PI); const data = new Patch('bloom').replace( 'Seed', @@ -15,7 +15,7 @@ export default function bloom(): Piece { { x: floats(seeds, (i) => r(i) * Math.cos(angle(i))), y: floats(seeds, (i) => r(i) * Math.sin(angle(i))), - // Eight arms winding outward through the seeds, dark between them. + // Eight spiral arms winding outward, with dark gaps between them. wave: floats(seeds, (i) => (0.5 + 0.5 * Math.sin(r(i) * 0.05 - 8 * polar(i))) ** 1.6), size: floats(seeds, (i) => 0.5 + 0.85 * Math.sqrt(i / seeds)), }, diff --git a/docs/gallery/clifford.ts b/docs/gallery/clifford.ts index c1282dc5..cd2237f7 100644 --- a/docs/gallery/clifford.ts +++ b/docs/gallery/clifford.ts @@ -1,9 +1,9 @@ -/** Clifford: a strange attractor of a quarter million points, translucent, so density shows. */ +/** Clifford: a strange attractor of 260,000 translucent points, so density shows. */ import { Patch } from 'latkit'; import { THEME, framed, type Piece } from './look.ts'; export default function clifford(): Piece { - // As many points as a view draws whole, past which it draws a dense table through tiles. + // Just under 2^18 = 262,144 rows, past which a view pages a table instead of drawing it whole. const n = 260_000; const [a, b, c, d] = [-1.4, 1.6, 1.0, 0.7]; const x = new Float32Array(n); @@ -16,7 +16,7 @@ export default function clifford(): Piece { if (i >= 0) { x[i] = nx * 300; y[i] = ny * 300; - // Where each step heads, around the circle: the attractor's flow, as a cyclic color. + // The step's direction as a fraction of a turn, for a cyclic colormap. turn[i] = (Math.atan2(ny - py, nx - px) / (2 * Math.PI) + 1) % 1; } [px, py] = [nx, ny]; diff --git a/docs/gallery/currents.ts b/docs/gallery/currents.ts index 4965a7ae..795b7e67 100644 --- a/docs/gallery/currents.ts +++ b/docs/gallery/currents.ts @@ -1,8 +1,8 @@ -/** Currents: streamlines through a field of vortices, as a wind map draws them, colored by speed. */ +/** Currents: streamlines through a field of vortices, as on a wind map, colored by speed. */ import { Patch } from 'latkit'; import { THEME, framed, type Piece } from './look.ts'; -/** Vortices: where each turns, how strongly, and which way. */ +/** Vortices as `[x, y, strength]`. The sign of the strength sets the direction of spin. */ const VORTICES = [ [-380, 120, 1.0], [-120, -170, -0.8], @@ -13,7 +13,8 @@ const VORTICES = [ [560, 250, -0.5], ] as const; -/** The flow at `x`, `y`: a steady drift east, and each vortex's swirl falling off from its center. */ +/** The flow velocity at `x`, `y`: a steady eastward drift plus each vortex's swirl, which falls off + * with distance from its center. */ function flow(x: number, y: number): [number, number] { let [u, v] = [0.35, 0.05]; for (const [cx, cy, s] of VORTICES) { @@ -29,9 +30,10 @@ export default function currents(): Piece { const [w, h] = [1300, 720]; const geometry: [number, number][][] = []; const speed: number[] = []; - // Seeds on a grid, jittered by a fixed hash, so the field is covered evenly every time. + // Streamlines start on a grid, jittered by a fixed hash, so they cover the field evenly and the + // same way on every run. const jitter = (i: number) => (Math.sin(i * 12.9898) * 43758.5453) % 1; - // Inside an ellipse, so the field ends softly rather than at a frame. + // Streamlines stay inside an ellipse, so the field has a soft edge rather than a rectangular one. const inside = (x: number, y: number) => (x / (w / 2)) ** 2 + (y / (h / 2)) ** 2 < 1; for (let i = 0; i < 52 * 30; i++) { let x = (((i % 52) + 0.5 + 0.8 * jitter(i)) / 52 - 0.5) * w; @@ -40,7 +42,7 @@ export default function currents(): Piece { const path: [number, number][] = [[x, y]]; let travelled = 0; for (let s = 0; s < 70; s++) { - // A midpoint step, so curves stay curves. + // A midpoint step, which follows curves more closely than an Euler step. const [u0, v0] = flow(x, y); const [u, v] = flow(x + 2.5 * u0, y + 2.5 * v0); const length = Math.hypot(u, v); diff --git a/docs/gallery/diagram.ts b/docs/gallery/diagram.ts index b9180e4e..66ec159b 100644 --- a/docs/gallery/diagram.ts +++ b/docs/gallery/diagram.ts @@ -1,8 +1,8 @@ -/** Diagram: a microgrid's controller as blocks in groups, wired port to port and laid out by latkit. */ +/** Diagram: a microgrid controller as blocks in groups, wired port to port, laid out by latkit. */ import { Patch, reference, type Marker, type Mark, type Port, type Side } from 'latkit'; import { THEME, type Piece } from './look.ts'; -/** Kinds of block: the marker each draws as, its fill, and its ports. */ +/** Block kinds: each kind's marker, fill color, and ports as `[name, side, output]`. */ const KINDS = { Source: { marker: 'rounded', color: '#173836', ports: [['out', 'right', true]] }, Process: { @@ -54,7 +54,8 @@ export default function diagram(): Piece { blocks.push(b); return b; }; - // A signal driven by `from`'s output and read by each of `to`, through its input or the port named. + // Adds a signal driven by the `out` port of each block in `from`. Each block in `to` reads it + // through its `in` port, or through the port named with it. const wire = (name: string, from: Block | Block[], ...to: (Block | [Block, string])[]) => { const id = next++; signals.push({ id, name }); @@ -77,7 +78,7 @@ export default function diagram(): Piece { const filter = block('Process', 'Filter', sensing); const estimator = block('Process', 'State estimator', sensing); const dispatch = block('Process', 'Dispatch', control); - // The controller closing the loop sits between the groups it joins. + // The controller that closes the loop is in no group, so it sits between the groups it joins. const droop = block('Control', 'Droop'); const limiter = block('Process', 'Limiter', control); const inverter = block('Process', 'Inverter', power, true); diff --git a/docs/gallery/flow.ts b/docs/gallery/flow.ts index b2f38615..707d3e19 100644 --- a/docs/gallery/flow.ts +++ b/docs/gallery/flow.ts @@ -1,9 +1,9 @@ /** - * A DC power flow: each bus's voltage angle from what it injects, through branches weighted by - * their susceptance, and how the angles shift when one branch opens. + * A DC power flow. It computes each bus's voltage angle from its injection, with branches weighted + * by their susceptance, and how the angles shift when one branch opens. */ -/** Buses and the branches between them, each branch's reactance in per unit. */ +/** A network of buses and the branches between them. Reactance is in per unit. */ export interface Network { readonly buses: number; readonly from: Uint32Array; @@ -11,10 +11,11 @@ export interface Network { readonly reactance: Float32Array; } -/** The smallest reactance a branch is taken to have, so none is infinitely stiff. */ +/** The smallest reactance magnitude a branch is given, so no susceptance is infinite. */ const STIFFEST = 1e-4; -/** The network's susceptance matrix, row by row, and each bus's island. */ +/** The network's susceptance matrix in compressed sparse rows, with the diagonal stored separately, + * and each bus's island. */ class Susceptance { readonly start: Uint32Array; readonly column: Uint32Array; @@ -46,7 +47,7 @@ class Susceptance { this.diagonal[a]! += w; this.diagonal[c]! += w; } - // Islands, so each balances on its own. + // Find the islands with union-find, so each one is balanced separately. const root = Uint32Array.from({ length: buses }, (_, b) => b); const find = (b: number): number => { while (root[b] !== b) b = root[b] = root[root[b]!]!; @@ -56,12 +57,13 @@ class Susceptance { this.island = Uint32Array.from({ length: buses }, (_, b) => find(b)); } - /** A branch's susceptance: its reactance's inverse, series capacitors taken at their size. */ + /** A branch's susceptance: the inverse of its reactance's magnitude, so series capacitors + * (negative reactance) count by their size. */ susceptance(e: number): number { return 1 / Math.max(Math.abs(this.net.reactance[e]!), STIFFEST); } - /** `B·v` into `out`. */ + /** Writes `B·v` into `out`. */ times(v: Float64Array, out: Float64Array): void { for (let b = 0; b < v.length; b++) { let sum = this.diagonal[b]! * v[b]!; @@ -71,7 +73,7 @@ class Susceptance { } } - /** `p` less its island's mean, so every island's injections sum to zero. */ + /** Returns `p` minus its island's mean, so every island's injections sum to zero. */ balanced(p: Float64Array): Float64Array { const sum = new Float64Array(p.length); const count = new Float64Array(p.length); @@ -83,7 +85,8 @@ class Susceptance { return p.map((v, b) => v - mean[this.island[b]!]!); } - /** The angles `θ` with `B·θ = p`, by conjugate gradients with each bus's own scale. */ + /** Solves `B·θ = p` for the angles `θ`, after balancing `p` per island, by conjugate gradients + * with a diagonal (Jacobi) preconditioner. */ solve(injection: Float64Array, tolerance = 1e-10): Float64Array { const n = injection.length; const p = this.balanced(injection); @@ -116,14 +119,15 @@ class Susceptance { } } -/** Each bus's angle in radians, from its injection in per unit. */ +/** Returns each bus's angle in radians, given its injection in per unit. */ export function angles(net: Network, injection: Float64Array): Float64Array { return new Susceptance(net).solve(injection); } /** - * How each bus's angle shifts when `branch` opens, from the angles before: the flow the branch - * carried moves onto the rest of the network as if injected at one end and drawn at the other. + * Returns how each bus's angle shifts when `branch` opens, given the angles `theta` before. The + * branch's flow moves onto the rest of the network, as if injected at one end and withdrawn at the + * other. */ export function shifts(net: Network, theta: Float64Array, branch: number): Float64Array { const b = new Susceptance(net); diff --git a/docs/gallery/frequency.ts b/docs/gallery/frequency.ts index 3b07740f..c34e562b 100644 --- a/docs/gallery/frequency.ts +++ b/docs/gallery/frequency.ts @@ -1,4 +1,4 @@ -/** Frequency: every machine of ACTIVSg10k swinging after a fault, traced over time on a plot. */ +/** Frequency: the speed of every machine in ACTIVSg10k after a fault, traced over time. */ import { THEME, type Piece } from './look.ts'; import { readCase } from './grids.ts'; @@ -6,7 +6,7 @@ export default function frequency(): Piece { const c = readCase('activsg10k'); const fault = c.header.fault!; const [fx, fy] = [c.positions[2 * fault.bus]!, c.positions[2 * fault.bus + 1]!]; - // Each machine's distance from the fault, in degrees: the disturbance reaches the near ones first. + // Each machine's distance from the fault in degrees. The disturbance reaches near machines first. const distance = Float32Array.from(c.machineBus, (b) => Math.hypot(c.positions[2 * b]! - fx, c.positions[2 * b + 1]! - fy), ); @@ -16,7 +16,7 @@ export default function frequency(): Piece { name: 'frequency', alt: `The speed of ${machines} machines over ten seconds after a fault, near ones bright and swinging hardest, far ones dark`, data: [c.patch], - // The whole recording across; speeds fitted up the side. + // x spans the whole recording. With `y` null, the speed range fits what is in view. camera: { x: [0, (c.header.frames! - 1) * c.header.dt!], y: null }, scene: { space: { plot: { x: { label: 'Time (s)' }, y: { label: 'Speed (mHz from 60 Hz)' } } }, @@ -27,7 +27,7 @@ export default function frequency(): Piece { lines: { of: ['grid', 'Machine'], trace: 'speed', - // The nearest machines, swinging hardest, on top. + // The nearest machines, which swing hardest, draw on top. priority: { op: 'neg', args: ['distance'] }, width: 0.7, color: { diff --git a/docs/gallery/globe.ts b/docs/gallery/globe.ts index 5b8dc091..30143abb 100644 --- a/docs/gallery/globe.ts +++ b/docs/gallery/globe.ts @@ -1,8 +1,8 @@ -/** Globe: routes between the world's great cities along great circles, comets flowing on them. */ +/** Globe: routes between world cities along great circles, with comets flowing along them. */ import { Patch, reference, units } from 'latkit'; import { THEME, worldMap, type Piece } from './look.ts'; -/** Cities: name, longitude, latitude, and people in millions, about. */ +/** Cities as `[name, longitude, latitude, population in millions]`. Populations are approximate. */ const CITIES: readonly (readonly [string, number, number, number])[] = [ ['Tokyo', 139.69, 35.69, 37], ['Delhi', 77.21, 28.61, 32], @@ -46,7 +46,7 @@ const CITIES: readonly (readonly [string, number, number, number])[] = [ ['Reykjavík', -21.94, 64.15, 0.2], ]; -/** Long hauls between hubs, besides each city's nearest neighbors. */ +/** Long-haul routes between hubs, added to the routes to each city's three nearest neighbors. */ const HAULS = [ ['London', 'New York'], ['New York', 'Los Angeles'], @@ -71,7 +71,7 @@ const HAULS = [ ['Santiago', 'Auckland'], ] as const; -/** Degrees of the great circle between two cities. */ +/** The great-circle distance between two cities, in degrees, by the haversine formula. */ function arc( a: readonly [string, number, number, number], b: readonly [string, number, number, number], diff --git a/docs/gallery/grid.ts b/docs/gallery/grid.ts index 46823054..1f51a6de 100644 --- a/docs/gallery/grid.ts +++ b/docs/gallery/grid.ts @@ -1,28 +1,30 @@ -/** The grid: ACTIVSg70k's buses and branches over the states they stand in, drawn by voltage. */ +/** The grid: ACTIVSg70k's buses and branches over the US states, colored and sized by voltage. */ import { fileURLToPath } from 'node:url'; import type { CustomColormap, Mark, Size } from 'latkit'; import { THEME, type Piece } from './look.ts'; import { readCase } from './grids.ts'; -/** The states of the United States, which the CLI reads as GeoJSON. */ +/** Path of the US states as GeoJSON, which the CLI reads directly. */ export const STATES = fileURLToPath( new URL('../../crates/latkit-cli/examples/branch-opening/states.geojson', import.meta.url), ); -/** Colors of the voltage levels, from below 100 kV to 765 kV: the higher, the hotter. */ +/** Colormap of the voltage levels, from below 100 kV to 765 kV. Higher levels are hotter colors. */ export const LEVELS: CustomColormap = { kind: 'sequential', colors: ['#1c2e44', '#22507a', '#2d80b3', '#58bde6', '#ffcf73', '#fff4dc'], }; -/** Widths by voltage level, growing with its square: the backbone stands out of the rest. */ +/** Line width by voltage level, growing with the level's square so the high-voltage backbone + * stands out. */ export const WIDTHS: Size = { scale: [0.3, 2.3], value: { op: 'mul', args: ['level', 'level'] }, domain: [0, 25], }; -/** The states drawn beneath a grid: filled a shade above the ground, outlined faintly. */ +/** Marks that draw the states beneath a grid: filled slightly lighter than the ground, with faint + * outlines. */ export function states(outline = '#26364a'): Mark[] { return [ { areas: { of: ['states', 'polygons'], color: '#0f1720', pick: false } }, @@ -36,7 +38,7 @@ export default function grid(): Piece { name: 'grid', alt: 'Seventy thousand buses of a synthetic grid on the United States, its lines colored and widened by voltage', data: [c.patch, { name: 'states', file: STATES }], - // Over the grid from the south, tilted, so the backbone recedes toward the Great Lakes. + // A tilted view from the south, so the backbone recedes toward the Great Lakes. camera: { center: [-84.2, 37.2], zoom: 4.38, pitch: 42, bearing: -8 }, scene: { space: { earth: { graticule: false } }, diff --git a/docs/gallery/grids.ts b/docs/gallery/grids.ts index 14312a48..020f9d9d 100644 --- a/docs/gallery/grids.ts +++ b/docs/gallery/grids.ts @@ -1,8 +1,9 @@ /** - * The synthetic grid cases the example suite fetches from `npm/examples/public/grids`, read into one - * dataset named `grid`: buses with their voltage level, branches between them, machines, and where - * a case simulated a fault, each machine's speed frame by frame. The format is the suite's own: a - * "LKG1" magic, a JSON header, and typed arrays it places. + * Reads the synthetic grid cases that the example suite fetches from `npm/examples/public/grids` + * into one dataset named `grid`. It holds buses with their voltage level, the branches between + * them, machines, and, for a case that simulated a fault, each machine's speed per frame. The file + * format is the suite's own: an "LKG1" magic, a JSON header, and typed arrays at the offsets the + * header gives. */ import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; @@ -27,15 +28,17 @@ export interface Case { readonly name: string; readonly header: Header; readonly patch: Patch; - /** Each bus's longitude and latitude, back to back. */ + /** Each bus's longitude and latitude, interleaved. */ readonly positions: Float32Array; /** Each machine's bus row. */ readonly machineBus: Uint32Array; - /** One of the case's arrays by name, such as `branch.reactance`. */ + /** Returns one of the case's arrays by name, such as `branch.reactance`. Throws if it is + * missing. */ readonly array: (name: string, of: new (b: ArrayBuffer, o: number, l: number) => T) => T; } -/** A voltage level: 0 below 100 kV, then 115–161, 230, 345, 500, and 765 kV. */ +/** The voltage level of `kv`: 0 below 100 kV, then 1 to 5 for 115–161, 230, 345, 500, and + * 765 kV. */ export function levelOf(kv: number): number { return kv >= 700 ? 5 : kv >= 450 ? 4 : kv >= 300 ? 3 : kv >= 200 ? 2 : kv >= 100 ? 1 : 0; } @@ -116,7 +119,7 @@ export function readCase(id: string): Case { }, ); if (header.frames !== undefined && header.dt !== undefined) { - // Per unit of 60 Hz, in mHz. + // Speeds are stored in per unit of 60 Hz. This scale converts them to mHz. const scale = (header.speedScale ?? 1) * 60_000; const speeds = Float32Array.from(array('machine.speed', Int16Array), (v) => v * scale); patch.axis('time', 's'); diff --git a/docs/gallery/helios.ts b/docs/gallery/helios.ts index 657cdaf5..845b721b 100644 --- a/docs/gallery/helios.ts +++ b/docs/gallery/helios.ts @@ -1,4 +1,4 @@ -/** Helios: flow through a synthetic network of five rings, its edges carrying comets. */ +/** Helios: a synthetic network of five rings, with comets flowing along its edges. */ import { Patch, reference } from 'latkit'; import { TAU, THEME, framed, type Piece } from './look.ts'; @@ -23,7 +23,7 @@ export default function helios(): Piece { radius.push(hub ? 5 : 2.3); name.push(hub && ring === rings - 1 ? `S${String(node / 12 + 1).padStart(2, '0')}` : ''); const here = ring * around + node; - // Around each ring, inward across rings, and chords across the core. + // Branches run around each ring, inward to the ring inside it, and as chords across the core. from.push(here); to.push(ring * around + ((node + 1) % around)); if (ring > 0) { diff --git a/docs/gallery/look.ts b/docs/gallery/look.ts index 5424f264..a4b26847 100644 --- a/docs/gallery/look.ts +++ b/docs/gallery/look.ts @@ -1,16 +1,17 @@ /** - * What every piece of the gallery shares: its frame, its ground and type, and what a piece is. + * Settings and helpers shared by the gallery's pieces: picture size, theme, framing, and the + * `Piece` type. */ import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { gunzipSync } from 'node:zlib'; import type { Camera, Fit, Patch, Scene } from 'latkit'; -/** Pixels written. Views lay out half as many CSS pixels, drawn twice as sharp. */ +/** Picture size in pixels. Views lay out at half this size in CSS pixels and draw at `SCALE`. */ export const SIZE = [1600, 1000] as const; export const SCALE = 2; -/** Every piece's ground and type: latkit's dark theme, a deeper ground, no label halos. */ +/** The theme of every piece: latkit's dark theme with a darker background and no label halos. */ export const THEME = { base: 'dark', background: '#0b0f14', @@ -18,8 +19,8 @@ export const THEME = { } as const; /** - * A fit of `box`, a drawing's least and greatest x and y with y up, into the picture: centered, - * with an even margin around. + * Fits a box into the picture, centered with a margin of at least 7% on each side. The box is a + * drawing's `[minX, minY, maxX, maxY]`, with y up. */ export function framed([x0, y0, x1, y1]: readonly [number, number, number, number]): Fit { const aspect = SIZE[0] / SIZE[1]; @@ -35,29 +36,29 @@ export function framed([x0, y0, x1, y1]: readonly [number, number, number, numbe }; } -/** A dataset as a file the CLI reads as it is: GeoJSON names its dataset. */ +/** A dataset file the CLI reads directly, such as GeoJSON, loaded as the dataset `name`. */ export interface File { readonly name: string; readonly file: string; } -/** One picture of the gallery: its datasets, its scene, and how the view looks at it. */ +/** One picture of the gallery: its datasets, its scene, and its view settings. */ export interface Piece { - /** The picture's name: `docs/images/NAME.png`. */ + /** The picture's name. It is written to `docs/images/NAME.png`. */ readonly name: string; - /** What it shows, for its alt text. */ + /** The picture's alt text. */ readonly alt: string; readonly data: readonly (Patch | File)[]; readonly scene: Scene; - /** Pixels written, when not `SIZE`: the hero is a banner. */ + /** Picture size in pixels, if not `SIZE`. The hero is a wide banner. */ readonly size?: readonly [number, number]; - /** The camera; without one the view frames everything drawn. */ + /** The camera. Without one, the view frames `fit`. */ readonly camera?: Camera; - /** What a view without a camera frames: everything drawn unless set. */ + /** What a view without a camera frames. Defaults to everything drawn. */ readonly fit?: Fit; - /** Seconds of effects shown: where flowing comets are. */ + /** Elapsed seconds of effects, which sets where flowing comets are. */ readonly elapsed?: number; - /** The axis coordinate shown, where sampled fields are read. */ + /** The axis coordinate shown, at which sampled fields are read. */ readonly at?: number; } @@ -68,7 +69,7 @@ export function floats(n: number, f: (i: number) => number): Float32Array { return values; } -/** Row ids `0..n` named by a function of the row, for a reference field. */ +/** `n` row ids, `f(i)` for each row `i`, as a reference field's values. */ export function rows(n: number, f: (i: number) => number): Uint32Array { const ids = new Uint32Array(n); for (let i = 0; i < n; i++) ids[i] = f(i); @@ -77,7 +78,8 @@ export function rows(n: number, f: (i: number) => number): Uint32Array { export const TAU = 2 * Math.PI; -/** A map the example suite ships, inflated to `target/gallery` for the CLI to read as GeoJSON. */ +/** Decompresses a map the example suite ships into `target/gallery` as GeoJSON for the CLI, and + * returns its path. */ export function worldMap(name: string): string { const out = fileURLToPath(new URL(`../../target/gallery/${name}.geojson`, import.meta.url)); const source = fileURLToPath( diff --git a/docs/gallery/opening.ts b/docs/gallery/opening.ts index a8a833af..6f742632 100644 --- a/docs/gallery/opening.ts +++ b/docs/gallery/opening.ts @@ -1,8 +1,8 @@ /** * A line opening: ACTIVSg70k as its Little Rock to Newark 765 kV line opens. A DC power flow of the - * whole grid finds where the line's power goes: the branches that pick it up glow amber, those it - * leaves glow blue, and the rest of the grid stays a quiet web. Each bus stands as high as its - * frequency, recorded 2.5 s into a simulation of the same opening. + * whole grid shows where the line's power goes. Branches that pick it up are amber, branches that + * lose flow are cyan, and the rest of the grid is a dim web. Each bus's height is its frequency + * 2.5 s into a simulation of the same opening. */ import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; @@ -13,7 +13,7 @@ import { levelOf, readCase } from './grids.ts'; import { THEME, worldMap, type Piece } from './look.ts'; import { STATES } from './grid.ts'; -/** Each bus's frequency deviation in hertz, as `record.ts` wrote it. */ +/** Reads each bus's frequency deviation in hertz from the file `record.ts` writes. */ function frequency(): Float32Array { const file = fileURLToPath(new URL('opening.frequency.gz', import.meta.url)); const bytes = gunzipSync(fs.readFileSync(file)); @@ -48,7 +48,8 @@ export default function opening(): Piece { const mw = (e: number, angle: Float64Array) => ((angle[net.from[e]!]! - angle[net.to[e]!]!) * 100) / Math.max(Math.abs(net.reactance[e]!), 1e-4); - // What each branch picks up once the line is open, in MW, compressed so a few MW still shows. + // Each branch's change in flow magnitude once the line opens, as asinh(ΔMW / 4), so a change of a + // few MW still shows. const gain = Float32Array.from({ length: m }, (_, e) => { const before = mw(e, theta); return Math.asinh((Math.abs(before + mw(e, shift)) - Math.abs(before)) / 4); @@ -57,8 +58,8 @@ export default function opening(): Piece { levelOf(Math.max(kv[net.from[e]!]!, kv[net.to[e]!]!)), ); const hz = frequency(); - // A pole down to the ground from one bus of 345 kV and up in each cell of 3/4 degree, so the - // height reads without a curtain of them. + // A pole to the ground from one bus of 345 kV or more in each 0.75° cell, so the heights are + // readable without crowding the view with poles. const cells = new Set(); const pole = Float32Array.from({ length: n }, (_, b) => { const cell = `${Math.floor(c.positions[2 * b]! / 0.75)} ${Math.floor(c.positions[2 * b + 1]! / 0.75)}`; @@ -75,8 +76,8 @@ export default function opening(): Piece { lat: Float64Array.from({ length: n }, (_, b) => c.positions[2 * b + 1]!), kv, hz, - // A fraction of the earth's radius: on the ground at the lowest frequencies, rising with - // the frequency, the highest peaks compressed. + // Height as a fraction of the earth's radius: zero at deviations of -16 mHz and below, + // rising with frequency, with the highest peaks compressed by asinh. height: Float32Array.from(hz, (f) => 0.022 * Math.asinh(Math.max(0, f + 0.016) / 0.03)), pole, }, @@ -90,7 +91,7 @@ export default function opening(): Piece { gain, change: Float32Array.from(gain, Math.abs), level, - // Wider by voltage, and by how far past a quiet 10 MW the branch's flow changed. + // Width grows with voltage level, and with how far the flow change exceeds about 10 MW. width: Float32Array.from( gain, (g, e) => 0.45 + 0.12 * level[e]! + 0.3 * Math.max(0, Math.min(Math.abs(g), 6) - 1.7), @@ -109,9 +110,10 @@ export default function opening(): Piece { pick: false, }, }); - // Map lines are a light steel at low alpha, so they lighten whatever lies under them. + // Map lines are a light steel blue at low alpha, so they lighten whatever is beneath them. const map = (alpha: string) => `#7fa6cc${alpha}`; - // The distribution web under 115 kV that the opening leaves be, dim and thin under the rest. + // Branches below 230 kV whose flow changes by less than about 10 MW, drawn dim and thin beneath + // the rest. const quiet: ExprInput = { op: 'and', args: [ @@ -137,13 +139,14 @@ export default function opening(): Piece { projection: 'globe', graticule: true, atmosphere: true, - // Dusk at the equinox: the sun sets over the grid's west, night falls on the Atlantic. + // Dusk at the September equinox. The sun is setting over the western grid, and it is + // night over the Atlantic. daylight: Date.UTC(2026, 8, 22, 22, 20) / 1000, }, }, ease: 0, - // The docs page's own ground, air a muted steel rather than the event's cyan, a faint - // graticule, and a night that keeps the grid readable. + // The docs page's background, a muted steel atmosphere rather than the cyan of the flow + // changes, a faint graticule, and a night shade light enough to keep the grid readable. theme: { ...THEME, background: '#0f141a', @@ -192,7 +195,8 @@ export default function opening(): Piece { priority: 'change', width: 'width', color: { - // The backbone a quiet grey within 10 MW either way; past it, cyan and amber of equal brightness. + // Grey for changes within about 10 MW either way. Beyond that, cyan for less flow + // and amber for more, at equal brightness. colormap: { kind: 'diverging', colors: [ @@ -219,7 +223,8 @@ export default function opening(): Piece { pick: false, }, }, - // The opened line: a faint red halo, red, and a bright core, the brightest thing drawn. + // The opened line as a faint red halo, a red line, and a pale core. It is the brightest + // thing drawn. opened('halo', 10, '#ff2e4d2e'), opened('opened', 4.4, '#ff2e4d'), opened('core', 1.3, '#ffd6dd'), diff --git a/docs/gallery/record.ts b/docs/gallery/record.ts index eb13277a..ac489871 100644 --- a/docs/gallery/record.ts +++ b/docs/gallery/record.ts @@ -1,8 +1,8 @@ /** - * Writes `opening.frequency.gz`, what the line opening's picture lifts each bus by: its frequency - * deviation from 60 Hz at one instant of the branch-opening study's recording - * (`crates/latkit-cli/examples/branch-opening`), in ACTIVSg70k's bus order, as 16-bit integers of - * 10 µHz. The recording is 168 MB and stays out of the repository; this instant is 140 kB. + * Writes `opening.frequency.gz`, which sets each bus's height in the line opening picture. It holds + * each bus's frequency deviation from 60 Hz at one instant of the branch-opening study's recording + * (`crates/latkit-cli/examples/branch-opening`), in ACTIVSg70k's bus order, as 16-bit integers in + * units of 10 µHz. The recording is 168 MB, too large for the repository. One instant is 140 kB. * * node docs/gallery/record.ts [seconds] [study] */ diff --git a/docs/gallery/render.ts b/docs/gallery/render.ts index c6fb9508..2d1dba96 100644 --- a/docs/gallery/render.ts +++ b/docs/gallery/render.ts @@ -1,10 +1,10 @@ /** - * The documentation's gallery, drawn by latkit. Each piece writes its datasets with `Patch`, as - * latkit's bytes, and its scene as a typed `Scene`; the `latkit` command line draws it into - * `docs/images/NAME.png`, with nothing done to the picture after. + * Draws the documentation's gallery with latkit. Each piece builds its datasets with `Patch`, saved + * in latkit's binary format, and its scene as a typed `Scene`. The `latkit` CLI draws each piece to + * `docs/images/NAME.png`, with no editing of the picture afterwards. * * cargo build --locked -p latkit-cli --profile cli - * cargo xtask wasm # the package `latkit` comes from + * cargo xtask wasm # builds the `latkit` package the pieces import * node docs/gallery/render.ts [name...] */ import { execFileSync } from 'node:child_process'; diff --git a/docs/gallery/terrain.ts b/docs/gallery/terrain.ts index ff67a8d5..d715f383 100644 --- a/docs/gallery/terrain.ts +++ b/docs/gallery/terrain.ts @@ -1,8 +1,8 @@ -/** Terrain: ridged mountains as lit faces between a grid of vertices, colored by height. */ +/** Terrain: ridged mountains as lit triangles over a grid of vertices, colored by height. */ import { Patch, reference } from 'latkit'; import { THEME, floats, rows, type Piece } from './look.ts'; -/** Height at `x`, `y`: ridges of several directions and scales, sharpened, then summed. */ +/** The height at `x`, `y`: sharpened ridges at six directions and scales, summed. */ function height(x: number, y: number): number { let h = 0; let amplitude = 1; @@ -16,7 +16,7 @@ function height(x: number, y: number): number { h += amplitude * ridge * ridge; amplitude *= 0.48; } - // A valley across the middle, and the land falling away toward the edges. + // Cut a valley across the middle, and lower the land toward the edges. return ( h * (0.35 + 0.65 * Math.min(1, Math.abs(y + 0.25 * x) / 260)) * @@ -32,7 +32,8 @@ export default function terrain(): Piece { const py = (i: number) => (Math.floor(i / columns) / (lines - 1) - 0.5) * h; const z = floats(n, (i) => 150 * height(px(i), py(i))); const cells = (columns - 1) * (lines - 1); - // Two triangles a cell: its corners as rows of the grid. + // Two triangles per grid cell. Returns corner `k` (0 to 5) of the cell's triangles as a vertex + // row. const corner = (cell: number, k: number) => { const c = cell % (columns - 1); const r = Math.floor(cell / (columns - 1)); diff --git a/docs/gallery/texas.ts b/docs/gallery/texas.ts index 2cf24ac4..b9af0237 100644 --- a/docs/gallery/texas.ts +++ b/docs/gallery/texas.ts @@ -1,4 +1,4 @@ -/** Texas: ACTIVSg2000's generators sized by output, its highest-voltage lines carrying comets. */ +/** Texas: ACTIVSg2000's generators sized by output, with comets on its lines of 345 kV and up. */ import { units } from 'latkit'; import { THEME, type Piece } from './look.ts'; import { readCase } from './grids.ts'; diff --git a/docs/gallery/tides.ts b/docs/gallery/tides.ts index aeb2259f..4f10696e 100644 --- a/docs/gallery/tides.ts +++ b/docs/gallery/tides.ts @@ -1,4 +1,4 @@ -/** Tides: closed contours swelling outward, each deformed by harmonics that drift with its level. */ +/** Tides: nested closed contours, each rippled by harmonics whose phases shift with its level. */ import { Patch } from 'latkit'; import { TAU, THEME, framed, type Piece } from './look.ts'; diff --git a/docs/gallery/torus.ts b/docs/gallery/torus.ts index b4bc3fd2..84cc208c 100644 --- a/docs/gallery/torus.ts +++ b/docs/gallery/torus.ts @@ -4,7 +4,8 @@ import { TAU, THEME, floats, rows, type Piece } from './look.ts'; type Vec = [number, number, number]; -/** The knot's centerline at `t`: around the torus twice while it winds through three times. */ +/** The knot's centerline at `t`. It goes around the torus twice while winding through the hole + * three times. */ function knot(t: number): Vec { const r = 100 + 42 * Math.cos(3 * t); return [r * Math.cos(2 * t), r * Math.sin(2 * t), 42 * Math.sin(3 * t)]; @@ -33,7 +34,7 @@ export default function torus(): Piece { const h = 1e-3; for (let s = 0; s < samples; s++) { const t = (s / samples) * TAU; - // The centerline's frame: its tangent, the way it bends, and their cross. + // The centerline's Frenet frame: tangent, normal (the direction it bends), and binormal. const tangent = unit(sub(knot(t + h), knot(t - h))); const ahead = unit(sub(knot(t + 2 * h), knot(t))); const behind = unit(sub(knot(t), knot(t - 2 * h))); diff --git a/docs/guide.md b/docs/guide.md index b3e39d23..b85e5d87 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -1,12 +1,12 @@ # Guide -How latkit works: datasets and the patches that change them, scenes, views, and pictures, and -what it takes to ship them. +How latkit works: datasets and the patches that change them, scenes, views, pictures and video, +the data formats, and deployment. ## Data -A dataset is a name and its tables. A table is rows, each with an id, and fields, a value per row. -Patches change datasets all at once: every change of a patch, or none. +A dataset is a name and a set of tables. A table has rows, each with an id, and fields, each with +one value per row. A patch applies atomically: all of its changes, or none. ### Write a dataset @@ -34,28 +34,32 @@ const grid = new Data('grid').apply( ); ``` -- **Ids** are nonnegative safe integers (`0..2^53 - 1`) or texts, listed, or `{ count: n }` for rows `0` to `n - 1`. -- **Values** are a column per field: typed arrays of numbers (a `Uint8Array` holds booleans), or - arrays of numbers, booleans, texts, or geometries. `null` is missing. +- **Ids** are a list of nonnegative safe integers (`0..2^53 - 1`) or strings, or `{ count: n }` + for rows `0` to `n - 1`. +- **Values** are one column per field: typed arrays of numbers, or arrays of numbers, booleans, + strings, or geometries. `null` is missing. A `Uint8Array` holds booleans, so use `Uint32Array` + for small unsigned integers. - **Geometries** are `[x, y]` points, paths of points, parts (a polygon's rings, or a path's pieces), or polygons. - **References** name rows of another table by id: `reference(table, ids)`. -| Change | Does | -| ---------------------------------------------------- | -------------------------------------------------------------------- | -| `replace(table, ids, fields)` | replaces the table, or adds it | -| `set(table, ids, fields)` | writes fields for those rows: known ids change, others are added | -| `remove(table, ids)` | removes the rows | -| `axis(name, unit)` | names the axis sampled fields are recorded over | -| `append(table, at, fields, { ids })` | records a frame at each coordinate of `at`, of every row or of `ids` | -| `forget(before)` | forgets the frames before a coordinate | -| `frames(table, field, { level, first, at, values })` | holds a run of a sampled field's entries, as a time tile does | +| Change | Does | +| ---------------------------------------------------- | ----------------------------------------------------------------------- | +| `replace(table, ids, fields)` | replaces the table, or adds it | +| `set(table, ids, fields)` | updates those rows and adds unknown ids; other fields keep their values | +| `remove(table, ids)` | removes the rows | +| `axis(name, unit)` | names the axis that sampled fields are recorded over | +| `append(table, at, fields, { ids })` | records a frame at each coordinate of `at`, for every row or for `ids` | +| `forget(before)` | drops the frames before a coordinate | +| `frames(table, field, { level, first, at, values })` | adds a run of a sampled field's stored entries, as a time tile does | + +`patch.extend(next)` appends another patch's changes. ### Change it -`apply` changes a dataset in place, and every view drawing it draws the change from the next frame. -A snapshot keeps one version in O(1), sharing its pages, so an undo history costs only what -changed: +`apply` changes a dataset in place; every view drawing it shows the change on the next frame. +`snapshot()` keeps the current version in O(1) by sharing pages with the dataset, so an undo history +costs only the pages that changed: ```ts const before = grid.snapshot(); @@ -65,10 +69,13 @@ grid.restore(before); // and undone ### Record over time -Sampled values are stored as `f32`, including values supplied as `Float64Array`; coordinates -remain `f64`. NaN means missing in sampled values and numeric query results. Static columns retain -their original numeric storage. A field's first frame makes it sampled. A view shows the axis -coordinate `view.at`: each sampled field reads its last frame at or before it. +A field becomes sampled with its first frame. Sampled values are stored as `f32`, even when given +as a `Float64Array`, so a history is for visualization, not a lossless `f64` archive. Axis +coordinates stay `f64`, and static columns keep their original numeric type. NaN marks a missing +value in sampled fields and in numeric query results. A view shows the axis coordinate `view.at`: +marks interpolate each sampled field linearly between the frames on either side of it, or show the +last frame at or before it when read with `field(name).step()`. `get` and `query` read the last +frame at or before the `at` they are given, or the last frame without one. ```ts grid.apply(new Patch().axis('time', 's')); @@ -79,13 +86,13 @@ grid.apply(new Patch().forget(1)); // a rolling window view.at = 1.2; ``` -Each sampled field keeps levels of detail as it records: at level `k`, each row's least and -greatest value over buckets of `8^k` frames. A line tracing a field across a plot reads the level -that gives each pixel column a bucket or two, so a long history draws as fast as a short one. +As it records, each sampled field keeps levels of detail: level `k` holds each row's minimum and +maximum over buckets of `8^k` frames. A line tracing a field on a plot reads the level that gives +each pixel column one or two buckets, so a long history draws as fast as a short one. ### Fetch long histories in spans -A history too large to load whole is written as time tiles, and a view fetches only what it draws: +Write a history too large to load at once as time tiles; a view then fetches only what it draws: ```sh latkit tile history.latk -o history/ @@ -95,25 +102,26 @@ latkit tile history.latk -o history/ view.sources = [new Tiles('grid', 'history/frames.json')]; ``` -`tile` writes `frames.json`, `base.latk` (the dataset with every sampled field's frames left out), -and each field's history in spans of `--chunk` entries, at every level of detail -(`Snapshot::to_tiles` in Rust). A line tracing a field fetches the spans its plot window draws, at -the detail it draws them, and a mark showing a field fetches the frames about `view.at`. A span -still loading draws the coarser span above it, as a map tile still loading draws the tile a zoom -out. +`tile` writes `frames.json`, `base.latk` (the dataset without its sampled fields' frames), and each +sampled field's history in spans of `--chunk` entries (256 by default) at every level of detail. In +Rust, `Snapshot::to_tiles` writes the same files. A line tracing a field fetches the spans its plot +window shows, at the detail it draws. A mark showing a field fetches the frames around `view.at`. +While a span loads, the view draws the coarser span above it, as a map shows the parent tile while a +tile loads. ### Stream -Patches are bytes: `patch.toBytes()` writes one frame, and frames back to back stream. Each frame -says its length up front. The parser holds at most one frame of `limit` bytes (256 MiB by default), -in addition to the input chunk and patches you retain. Breaking iteration cancels the reader. A -server in any language writes the same frames ([format](#bytes)). +`patch.toBytes()` encodes a patch as one frame, and frames written back to back form a stream. Each +frame's header gives its length, so `Patch.stream` refuses a frame longer than `limit` (256 MiB by +default) before reading its body. It buffers at most one frame, besides the current input chunk and +the patches you keep, and cancels the reader on an error or when you break out of the loop. A server +in any language can write the same frames ([format](#bytes)). ```ts for await (const patch of Patch.stream((await fetch('/feed')).body!)) grid.apply(patch); ``` -A patch made with `new Patch('grid')` names its dataset, and `apply` refuses it for another. +`new Patch('grid')` names its dataset; applying it to any other dataset throws. ### Read files @@ -123,14 +131,14 @@ const saved = await Data.read(bytes); // latkit's bytes, as data.toBytes() wrote const states = await Data.read(geojson, { name: 'states' }); // GeoJSON, gzip-compressed or not ``` -GeoJSON becomes tables `points`, `lines`, and `polygons`, each with its geometry as field -`geometry` and its properties as fields; it is read in a worker. The JSON document's form is in -[Formats](#json). `Patch.read(source)` reads one patch the same ways, its bytes without +GeoJSON becomes the tables `points`, `lines`, and `polygons`, each with a `geometry` field and one +field per property; it is parsed in a worker. [Formats](#json) describes the JSON document. +`Patch.read(source)` reads one patch from the same kinds of source; reading bytes needs no WebAssembly. ### Read back -Reading needs no GPU. Queries run the expressions marks draw with: +Reading needs no GPU. Queries use the same expressions as marks: ```ts grid.get('Bus', 101, 'name'); // 'Northgate' @@ -144,22 +152,20 @@ const hot = grid.query({ // { table, ids, fields: { name: string[], loading: Float64Array }, total } ``` -`total` counts the rows kept before `limit`. `field('name').contains('north')` matches texts in -queries. `describe()` lists tables, rows, fields and their kinds, and the axis and its span. -`envelope({ table, field, range, spans, ids })` gives each row's least and greatest value of a -sampled field over equal spans of the axis, read from levels of detail, so a span costs a few reads -however many frames it covers. A `Snapshot` reads the same way, as of its version. +`total` is the number of rows the query matched before `limit`. `field('name').contains('north')` +matches text, ignoring case. `describe()` lists the tables, their rows, their fields and field +kinds, and the axis with its span. `envelope({ table, field, range, spans, ids })` returns each +row's minimum and maximum of a sampled field over equal spans of the axis. It reads the levels of +detail, so each span costs a few reads however many frames it covers. A `Snapshot` has the same +read methods and reads its own version. -`patch.extend(next)` appends another patch's operations. `Uint8Array` means booleans in -JavaScript patches; JSON binary slices with dtype `u8` mean numbers. Use `Uint32Array` for unsigned -numeric bytes in a JavaScript patch. For all method signatures, see the -[API reference](reference/api.md). +The [API reference](reference/api.md) lists every method's signature. ## Scenes -A scene is plain data: a `space`, `marks` drawn in order, and optionally a `layout`, `theme`, -`controls`, `ease` (seconds changes ease over), and a `title` and `subtitle`. A view draws a scene; -`view.scene = next` replaces it, keeping every row's place in the transition. +A scene is plain data: a `space`, a list of `marks` drawn in order, and optional `layout`, `theme`, +`controls`, `ease` (the seconds a change eases over, 0.3 by default), `title`, and `subtitle`. +Setting `view.scene = next` replaces the scene, and each row eases from where it was drawn. ```ts const scene: Scene = { @@ -184,47 +190,44 @@ const scene: Scene = { ### Spaces -| Space | Positions | -| ----------- | ----------------------------------------------------------------- | -| `'plane'` | plane units with elevation; the camera turns and tilts | -| `'earth'` | longitude and latitude in degrees, on a map or a globe | -| `'plot'` | the axis coordinate across, values up | -| `'diagram'` | a flat sheet, y down, every length a unit; ports, groups, layouts | +| Space | Positions | +| ----------- | --------------------------------------------------------------------- | +| `'plane'` | plane units with elevation; the camera turns and tilts | +| `'earth'` | longitude and latitude in degrees, on a map or a globe | +| `'plot'` | the axis coordinate across, values up | +| `'diagram'` | a flat sheet, y down, sizes in position units; ports, groups, layouts | -Each takes options as `{ plane: { grid } }`, `{ diagram: { grid } }`, `{ earth: { projection, -graticule, daylight, atmosphere } }`, or `{ plot: { x, y } }`. The earth's `projection` is -`'mercator'` (the default) or `'globe'`. A grid is `'off'`, `{ lines: { pitch } }`, or -`{ dots: { pitch, snap } }`. A plot's axes measure their labels and titles and leave room for them. +To set options, write the space as an object: `{ plane: { grid } }`, `{ diagram: { grid } }`, +`{ earth: { projection, graticule, daylight, atmosphere } }`, or `{ plot: { x, y } }`. The earth's +`projection` is `'mercator'` (the default) or `'globe'`. A grid is `'off'`, `{ lines: { pitch } }`, +or `{ dots: { pitch, snap } }`. A plot's axes reserve room for their labels and titles. ### Marks -Every mark names its table, `of: [dataset, table]`, and takes `color`, `visible`, `priority`, -`label`, `legend`, `name`, and `pick`, `select`, and `drag`. Unless `pick` says, rows -that are things are hovered, clicked, and probed (vertices, edges, faces, a plot's traces) and -geometry is not (areas, and lines along paths: coasts, borders, roads), so hovering probes none of -it; `pick: true` makes a map's states or cells answer the pointer. - -| Mark | Draws each row as | Its own options | -| ---------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------ | -| `vertices` | a marker at `position` (or the point field `geometry`) | `elevation`, `radius`, `size`, `marker`, `ring`, `ports`, `contains`, `shadow` | -| `edges` | a stroke between the rows two reference fields name | `ends`, `bends`, `route`, `arrows`, `width`, `dash`, `flow` | -| `lines` | a path field's stroke; on a plot, a sampled field traced over time | `geometry`, `trace`, `interpolation`, `width`, `dash`, `flow` | -| `areas` | the area a polygon field covers | `geometry` | -| `faces` | a triangle between three vertices | `corners` | - -A `flow` sends comets along a stroke. In a diagram, vertices are blocks with `ports`, a vertex mark -with `contains` draws groups around the rows that name it, and edges of a table without `ends` are -nets joining the ports that name them. The theme's `diagram` group (`outline`, `corner`, -`portSize`, `bend`) sets how blocks, groups, ports, and wires look, and `shadow: true` lifts blocks -and groups off the sheet. - -Edges whose ends lie far apart draw within the budget too: the vertices they end at are held a row -at a time where few of a table's pages hold them. +Every mark names its table with `of: [dataset, table]` and takes `color`, `visible`, `priority`, +`label`, `legend`, `name`, `pick`, `select`, and `drag`. By default, the pointer hovers, clicks, +and probes vertices, edges, faces, and a plot's traces, but not geometry such as areas and lines +along paths (coasts, borders, roads). Set `pick: true` to make, for example, a map's states or cells +respond to the pointer. + +| Mark | Draws each row as | Its own options | +| ---------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | +| `vertices` | a marker at `position` (or at the point field `geometry`) | `elevation`, `radius`, `size`, `marker`, `ring`, `ports`, `contains`, `shadow` | +| `edges` | a stroke between the two rows its `ends` fields reference | `ends`, `bends`, `route`, `arrows`, `width`, `dash`, `flow` | +| `lines` | a stroke along a path field; on a plot, a sampled field traced over time | `geometry`, `trace`, `interpolation`, `width`, `dash`, `flow` | +| `areas` | the area a polygon field covers | `geometry` | +| `faces` | a triangle between three vertices | `corners` | + +`flow` animates comets along a stroke. In a diagram, vertices are blocks with `ports`. A vertex mark +with `contains` draws each row as a group around the rows that reference it. An edges mark of a +table without `ends` draws nets, each joining the ports that reference it. The theme's `diagram` +group (`outline`, `corner`, `portSize`, `bend`) styles blocks, groups, ports, and wires, and +`shadow: true` gives blocks and groups a drop shadow. ### Expressions -Options that vary by row take expressions: a number, a field name, or a builder. Colors are values -of their own: a string is a CSS color, and a field colors rows through a colormap or a condition. +Options that vary by row take an expression: a number, a field name, or a builder. Color options +differ: a string is a CSS color, and a field sets colors only through a colormap or a conditional. ```ts import { field, when, selected, units, px } from 'latkit'; @@ -238,27 +241,33 @@ px(3); // a size in CSS pixels, however the view zooms field('vm').from('weather', 'Station'); // read from another dataset, joined by id ``` -A size is CSS pixels on a plane, the earth, and a plot, and position units in a diagram, unless -`px()` or `units()` says. State expressions read the view: `selected()`, `hovered()`, `pointer()`, -`elapsed()`, `zoom()`. Colormaps are a catalog name or your own colors, `{ kind, colors }`; a -categorical map can name its `categories` for its legend. `colormapCss(map)` gives the same colors -as a CSS gradient. +Sizes are in CSS pixels on a plane, the earth, and a plot, and in position units in a diagram, +unless wrapped in `px()` or `units()`; on the earth a position unit is a degree. State expressions +read the view: `selected()`, `hovered()`, `pointer()`, `elapsed()`, and `zoom()`. A colormap is a +catalog name or your own `{ kind, colors }`; a categorical colormap can name its `categories` for +the legend. A colormap draws a missing value in the theme's `missing` color, while an option given +`missing()` takes its theme value. `colormapCss(map)` returns the same colors as a CSS gradient. ### Themes, controls, and layouts -- **Theme**: `'dark'`, `'light'`, or options over a base, a group for each kind of thing drawn: - `{ base: 'light', text: { size: 12 }, edge: { width: 2 } }`. `edge: { color: null }` blends each - edge's ends' colors, and `focus: { selected: null }` glows in each row's own color. A comet's - spacing and length take `{ px: n }` or `{ units: n }`. -- **Controls**: `'inspect'` (the default), `'none'`, or options: `pan`, `zoom`, `turn`, - `keyboard`, `hover`, `pickRadius`, `selectBox`, `connect`, `delete`, `cycle`, `press`, `motion`. -- **Layout** places vertex rows without positions: `'stress'`, `'layered'`, or their options. A - place, once given, holds; `view.arrange()` lays every row out again. +- **Theme**: `'dark'`, `'light'`, or overrides of a base theme, grouped by what they style: + `{ base: 'light', text: { size: 12 }, edge: { width: 2 } }`. `edge: { color: null }` colors each + edge by blending its two vertices' colors, and `focus: { selected: null }` draws the selection + glow in each row's own color. The `flow` group's comet `spacing` and `length` take `{ px: n }` or + `{ units: n }`. +- **Controls**: options `pan`, `zoom`, `turn`, `keyboard`, `hover`, `pickRadius`, `selectBox`, + `connect`, `delete`, `cycle`, `press`, and `motion`; by default the pointer pans, zooms, turns, + and hovers. The presets are `'inspect'`, which leaves the camera to the application, and + `'none'`, for a still picture. +- **Layout** places vertex rows that have no position, in any space but a plot: `'stress'`, + `'layered'`, or their options. Without a layout, such rows are not drawn. A row keeps its place + once laid out; `view.arrange()` lays out every row again. ## Views -A `Gpu` makes views and canvases. A view draws one scene from its sources, each a dataset by its -name; a canvas shows views, one per panel, and passes each the input on its panel. +A `Gpu` creates views and canvases. A view draws one scene from its sources, the datasets its marks +name. A canvas shows views, one per panel, and passes each view the pointer and key input on its +panel. ```ts const gpu = await Gpu.create(); @@ -274,13 +283,18 @@ gpu.canvas(element, { ### Sources -`view.sources` lists what a view draws, each by its name: a `Data`, its latest version, from the -frame after each patch applies; a `Snapshot`, as it is; or `Tiles`, fetched in pieces as the view -asks for them, every view of a GPU sharing what is fetched: [time tiles](#fetch-long-histories-in-spans), -a `frames.json` manifest, or vector tiles as the camera asks for them (a PMTiles archive, a TileJSON -document, or a `{z}/{x}/{y}` template), each layer a table of the kind marks draw it as, or named -`layer:polygons`, `layer:lines`, or `layer:points`. `view.snapshot(name)` is the version of a -source the view draws now, tiles fetched so far included. +`view.sources` lists the datasets a view draws, each matched by its name. A source is one of: + +- `Data`: the view draws its latest version, from the frame after each patch applies. +- `Snapshot`: the view draws that version, unchanged. +- `Tiles`: pieces fetched as the view needs them, shared by every view of the GPU. The URL is a + [time tiles](#fetch-long-histories-in-spans) `frames.json` manifest, or vector tiles fetched as + the camera moves: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` template. Each vector + layer becomes a table of the features that match the mark drawing it (polygons, lines, or points); + name the table `layer:polygons`, `layer:lines`, or `layer:points` to choose explicitly. + +`view.snapshot(name)` returns the version of a source the view draws now, including the tiles +fetched so far. ### Camera @@ -295,11 +309,11 @@ view.orbit = 6; // degrees a second ### Events -`view.on(type, listener)` returns a function that stops listening. Intents are told once each, in -the order the user acted: `click`, `open`, `menu`, `drag`, `connect`, `delete`. Changes tell only -their latest, and their state can always be read: `hovered` (`view.hovered`), `selection` -(`view.selection`), `camera`, and `waiting` (`view.waiting`). A view tells only what the -application did not do: its setters change state silently. +`view.on(type, listener)` returns a function that removes the listener. Intent events (`click`, +`open`, `menu`, `drag`, `connect`, `delete`) fire once each, in the order the user acted. Change +events (`hovered`, `selection`, `camera`, `waiting`) report only the latest state, which the +property of the same name always holds (`view.hovered`, `view.selection`, and so on). Setters fire +no events, so events report only what the application did not do itself. ```ts view.on('click', ({ hit }) => console.log(hit.row, hit.point)); @@ -310,8 +324,8 @@ view.on('error', (error) => console.error(error.code, error.path, error.message) ### Rows -A row is `{ data, table, id }`; a set of rows of one table is `{ data, table, ids }`. The selection, -`selection` events, `delete` events, fits, and neighbors are sets: +A row is `{ data, table, id }`. A set of rows in one table is `{ data, table, ids }`; the +selection, `selection` and `delete` events, fits, and `neighbors` use sets: ```ts view.selection = [{ data: 'grid', table: 'Bus', ids: [101, 102] }]; @@ -320,15 +334,15 @@ const hits = await view.pick([x, y], { radius: 8 }); // rows within 8 CSS pixels const at = await view.screenOf({ data: 'grid', table: 'Bus', id: 101 }); // [x, y] on screen, or null ``` -Picks and locations answer once a frame draws the view. Every wait takes `{ signal }` and ends at -it, 30 seconds unless given: it rejects with `timeout` when it timed out, and with `cancelled` when -it was aborted or the view was destroyed. +`pick` and `screenOf` resolve once a frame draws the view. Calls that wait take `{ signal }` and +stop waiting when it aborts; without one, they time out after 30 seconds. They reject with code +`timeout` on a timeout, and `cancelled` when aborted or when the view is destroyed. ### Layouts into data -In a diagram, or on a plane with a layout, rows without positions are placed by it. -`view.arrange()` lays every row out again and answers a patch to each dataset writing the places -into its position fields: +A scene's `layout` places vertex rows that have no position. `view.arrange()` lays them all out +again and resolves to one patch per dataset that writes their places into the mark's position +fields. Applied, the places become part of the data: ```ts for (const patch of await view.arrange()) if (patch.data === grid.name) grid.apply(patch); @@ -336,38 +350,50 @@ for (const patch of await view.arrange()) if (patch.data === grid.name) grid.app ### Stats and memory -`view.stats` tells rows drawn, draw calls, tiles, and CPU and GPU milliseconds a frame; -`gpu.resident` and `gpu.uploading` the bytes of data on the GPU and waiting to upload, for every -view of it. `gpu.work` counts exactly what the GPU did: bytes uploaded, written, and cleared, rows -evaluated, whose ends it found, and packed, readbacks, jobs, texts, and pipelines. -`gpu.budget = { bytes, uploadPerFrame }` sets how much memory data takes, and how much of it a frame -uploads. The GPU's views share it: a view gets what it asks while an even split covers it, and the -rest split what is left; pages several views draw count once. A table too large for its view's -share draws through the tiles in view, a page of its order at a time; a page it drew stays on the -GPU while its share leaves room, so the camera coming back draws it again for nothing. +`view.stats` reports, per frame, the rows drawn, draw calls, tiles, and CPU and GPU milliseconds. +`gpu.resident` and `gpu.uploading` are the bytes of data on the GPU and waiting to upload, across all +views of that GPU. `gpu.work` counts exactly what the GPU did: bytes uploaded, written, and cleared; +rows evaluated, resolved (edge and face endpoints found), and packed; readbacks, jobs, texts, and +pipelines compiled. -A frame does the work of what changed: a patch evaluates only the pages it wrote, unless a fitted -domain or a join reads them; a transition evaluates once and blends each frame; and rows drawn -through tiles keep their places while the camera moves, so only arriving rows upload. +`gpu.budget = { bytes, uploadPerFrame }` caps the GPU memory data may take (by default half the +device's largest storage buffer) and the bytes uploaded per frame (32 MiB by default). Set it before +data draws: the memory pool grows as needed and never shrinks. The views of one GPU share the +budget: a view that needs less than an even split gets all it needs, and the others divide the +rest. Pages that several views draw count once. -`Data`, `Snapshot`, `Gpu`, `View`, `Canvas`, and `Image` have an idempotent `destroy()` and -`Symbol.dispose`; destroying a `Gpu` destroys its canvases, views, and frame listeners. +A table too large for its view's share is paged: the view draws only the pages the camera needs. A +page the camera leaves stays resident while the budget allows, so returning to it needs no upload. +Drawn edges need their endpoints resident: a vertex page that many drawn edges end in is made +resident whole, and scattered endpoints are made resident individually, so edges between far-apart +vertices still fit the budget. -Videos are asynchronous resources: finish them with `await video.finish()` or cancel with -`await video.abort(reason)`. `record` cleans up on errors automatically. +Each frame does only the work its changes need. A patch re-evaluates only the pages it wrote, unless +a fitted domain or a join reads them. A transition evaluates once, then blends each frame. Rows of a +paged table keep their GPU slots while the camera moves, so only newly needed rows upload. -`gpu.lost` tells whether the device is gone. Create a new GPU, views, and canvases to recover, -then supply retained scenes and datasets. `view.failures` contains input/loading failures; -`view.wantsFrame` is useful for custom scheduling. For manual rendering use `gpu.step(now)`; -`gpu.done()` waits for drawing to settle, and `gpu.landed()` waits for pending work to arrive. -`view.position([x, y])` is the position under a canvas point, and `view.screen(position)` the -point a position draws at. +`Data`, `Snapshot`, `Gpu`, `View`, `Canvas`, and `Image` have an idempotent `destroy()` and +`Symbol.dispose`, so they work with `using`. Destroying a `Gpu` also destroys its canvases, views, +and frame listeners. + +Finish a video with `await video.finish()` or cancel it with `await video.abort(reason)`. `record` +finishes the video itself, and aborts it on an error. + +`gpu.lost` tells whether the device is gone. To recover, create a new `Gpu`, views, and canvases, +and give them your scenes and datasets again. `view.failures` lists the marks that draw nothing +because their data no longer fits them, with the reason. To drive frames yourself, call +`gpu.step(now)`: after the first call, latkit stops requesting animation frames, and +`view.wantsFrame` tells whether a view needs one. `gpu.done()` resolves once submitted GPU work, +readbacks, and requested pipeline compiles have finished; `gpu.landed()` resolves when work a view +waits on (a readback, compile, job, text, or fetch) arrives, or a view needs a frame. +`view.position([x, y])` is the position under a canvas point, and `view.screen(position)` is the +screen point where a position draws. ## Pictures and video -A `Shot` is plain data: a size in pixels, and how views are drawn into it. It draws views whether or -not a canvas shows them; a canvas showing one holds its last frame meanwhile. `canvas.shot()` is a -picture of exactly what a canvas shows. +A `Shot` is plain data: a size in pixels and how views are drawn into it. It draws views whether or +not a canvas shows them; a canvas showing one keeps its last frame while the shot draws. +`canvas.shot()` makes a shot of exactly what the canvas shows. ```ts const shot = new Shot([3840, 2160], { scale: 2, layout: [{ view }] }); // 1920 by 1080 CSS pixels @@ -376,21 +402,23 @@ const png = await image.encode('png'); // or 'jpeg' with { quality }, or 'webp' image.destroy(); ``` -| Option | Does | -| ------------- | ----------------------------------------------------------------------- | -| `scale` | draws `scale` pixels per CSS pixel | -| `layout` | the views and where each draws, `at: [x, y, width, height]` fractions | -| `transparent` | keeps alpha where the themes' backgrounds are | -| `capture` | once views are complete and still (`'still'`), or as they are (`'now'`) | -| `elapsed` | fixes time-reading expressions and effects at `elapsed` seconds | +| Option | Does | +| ------------- | --------------------------------------------------------------------------------------------- | +| `scale` | draws `scale` pixels per CSS pixel (1 by default) | +| `layout` | the views and where each draws, `at: [x, y, width, height]` as fractions | +| `transparent` | keeps alpha where the themes' backgrounds would be | +| `capture` | waits until views are complete and still (`'still'`, the default), or only complete (`'now'`) | +| `elapsed` | fixes `elapsed()` and animated effects, such as comets, at `elapsed` seconds | -Files are encoded in a worker when workers are available; otherwise encoding runs on the calling thread. +Images are encoded in a worker where workers are available, and on the calling thread otherwise. ### Video -`shot.video({ fps, output, bitrate })` encodes H.264 in MP4 with WebCodecs as frames are drawn, and -writes it to a `WritableStream`: a file handle's, or a buffer's. `record` steps the views to each -frame's time; returning `false` finishes the video. +`shot.video({ fps, output, bitrate })` encodes H.264 MP4 with WebCodecs as frames are drawn, and +writes it to a `WritableStream`, such as a file's or an in-memory buffer's. `bitrate` defaults to +about 0.15 bits per pixel per frame. Each frame draws the views at its own time, `frames / fps` +seconds, so motion follows the video rather than the wall clock. `record(step)` calls `step(time)` +before each frame and finishes the video when it returns `false`. ```ts const file = await (await showSaveFilePicker()).createWritable(); @@ -405,32 +433,33 @@ await video.record((time) => { }); ``` -Width and height are multiples of 4 and 2. A frame where nothing moved is held rather than drawn. -`{ signal }` aborts a video: its output is aborted and its views are given back. +The width is rounded down to a multiple of 4 and the height to a multiple of 2, as H.264 needs. A +frame where nothing moved is not drawn; the previous frame is shown again. A `signal` passed to +`shot.video` aborts the video: its output is aborted and its views are released. -For manual control, call `await video.frame()` one frame at a time, then `await video.finish()`. -Finish and abort are idempotent; both release the output's writer lock, encoder, and abort -listener. Frame/time counters remain readable after completion. A failed step, frame, write, or -progress callback aborts `record`. Cancellation interrupts pending rendering and encoding; an -arbitrary writable sink must still settle its own in-flight write before its lock can be released. -Browser output uses Constrained Baseline H.264, with output timestamps checked against input order. +For manual control, call `await video.frame()` for each frame, then `await video.finish()`. +`finish` and `abort` are idempotent; both release the output's writer lock, the encoder, and the +abort listener. `frames` and `time` stay readable afterwards. An error in a step, frame, write, or +progress callback aborts `record`. Aborting cancels pending drawing and encoding at once, but the +writer lock is released only after the stream settles its in-flight write. In the browser, the +output is Constrained Baseline H.264, with output timestamps checked against input order. ## Formats ### Schema -Everything latkit reads and writes as JSON has a JSON Schema, derived from the same declarations -that read and write it: `latkit schema` prints it, and the npm package's types are written from it. -Its `$defs` are the inputs (`Scene`, `Data`, `Patch`, `Query`, `Camera`, `Fit`, `Budget`, `Shot`) -and its `outputs` what latkit writes back (`ViewEvent`, `Stats`, `Work`, `Description`, `Rows`, -`Camera`, `Budget`, `Value`, `Failure`). +All JSON latkit reads and writes has a JSON Schema, generated from the same declarations that read +and write it. `latkit schema` prints it, and the npm package's types are generated from it. Its +`$defs` are the inputs (`Scene`, `Data`, `Patch`, `Query`, `Camera`, `Fit`, `Budget`, `Shot`), and +its `outputs` are what latkit returns (`ViewEvent`, `Stats`, `Work`, `Description`, `Rows`, +`Camera`, `Budget`, `Value`, `Failure`, and the types they use). (json)= ### JSON A dataset's document is its name and the patch that builds it. A patch is a list of changes, each -named by its method with its parameters by name: +an object keyed by its method name, with its parameters by name: ```json { @@ -467,23 +496,26 @@ named by its method with its parameters by name: - `replace` and `set` take `{ table, ids, fields }`, `remove` `{ table, ids }`, `append` `{ at, table, ids?, fields }`, `forget` `{ before }`, `axis` `{ name, unit }`, and `frames` - `{ table, field, level?, first, at, values }`, a run of a sampled field's entries as a - [time tile](#fetch-long-histories-in-spans) holds them. -- `ids` are numbers or texts, or `{ "count": n }`; `null` leaves a reference unwired. + `{ table, field, level?, first, at, values }`, a run of a sampled field's entries as stored in a + [time tile](#fetch-long-histories-in-spans). +- `ids` are numbers or strings, or `{ "count": n }`; a `null` id in a reference leaves that row + unconnected. - A field is an array of one kind of value, a reference `{ reference, ids }`, or a slice of a binary - file beside the document, `{ file, dtype, offset, count }`: `count` values from byte `offset`, of - dtype `f32le`, `f64le`, `i32le`, `u32le`, `u64le`, `u8`, or `bool` (a byte each, nonzero true). + file beside the document, `{ file, dtype, offset, count }`: `count` values starting at byte + `offset`, of dtype `f32le`, `f64le`, `i32le`, `u32le`, `u64le`, `u8` (numbers), or `bool` (one + byte each, nonzero is true). (bytes)= ### Bytes -LATK v1 encodes a patch as one frame; a dataset's bytes are the patch that builds it. A frame says its -length up front, so frames back to back split without decoding. Little endian throughout. +A patch encodes as one frame; a dataset's bytes are the patch that builds it. A frame's header +gives its length, so frames written back to back split without decoding. All values are little +endian. | Part | Layout | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| frame | `LATK`, `u16` version (1), `u16` flags (0), `u64` length of the whole frame, dataset name (empty for none), `u32` op count, ops | +| frame | `LATK`, `u64` length of the whole frame, dataset name (empty for none), `u32` op count, ops | | op | `u8` tag: 0 axis, 1 replace, 2 set, 3 remove, 4 append, 5 forget, 6 frames; then its payload | | axis | name, unit | | replace, set | table name, ids, `u32` field count; per field: name, `u8` 0 values, 1 reference (target table name, ids as values), or 2 samples | @@ -498,36 +530,32 @@ length up front, so frames back to back split without decoding. Little endian th | storage | 0 `f64`, 1 `f32`, 2 `i32`, 3 `u32`, 4 `u64`, 5 bits, 6 texts (`u32` offsets, then `u32` byte length and UTF-8), 7 geometry (`u8` depth, each level's `u32` count and offsets, `u32` point count, `f64` x y pairs) | | text | `u32` byte length, UTF-8 | -Bits are `u64` words, low bit first. A sampled field's entries are frames at level 0, and at level -`k` buckets of `8^k` frames; a bucket whose eight entries a level below are held is left out, as it -follows from them. A writer outside latkit writes frames as `append` changes. - -The header is 16 bytes. The version is independent of the crate/npm version. Readers reject -unknown versions, nonzero flags, unknown operation/storage tags, truncated frames, and trailing -bytes. The checked-in `latk-v1.hex` fixture is tested by Rust and JavaScript. +Bits are packed into `u64` words, low bit first. A sampled field's entries are frames at level 0, +and buckets of `8^k` frames at level `k`. A bucket is left out when all eight entries one level +below it are stored, since it can be computed from them. Writers outside latkit should record +frames as `append` changes. -Absolute frame indexes are `u64`; counts within an encoded run remain `u32`. A run's end and its -span in frames must fit in `u64`. JavaScript builders accept only safe integers, including the -run's end (`2^53 - 1` at most). Levels 0 through 21 are legal; the writer records the highest -stored level so changing the renderer's detail policy does not change the file's interpretation. -Normal appends currently derive up to six levels. Tile manifests record each field's `levels`. -Sample indexes in query/pick JSON outputs use decimal strings above JavaScript's safe-integer range. +The header is 12 bytes: the magic `LATK` and the frame's length. Readers reject other bytes, +unknown operation or storage tags, truncated frames, and trailing bytes. The format carries no +version: a release that changes it says so, and files written before it no longer open. Rust and +JavaScript tests both check the committed `patch.hex` fixture. -Sample values and extrema are `f32`, with NaN representing missing values. Axis coordinates and -geometry coordinates are `f64`. An appended `Float64Array` is normalized to sampled `f32` storage; -this is a visualization history, not a lossless float64 archive. Static float64 columns stay float64. +Absolute frame indexes are `u64`; counts within an encoded run are `u32`. A run's end and its span +in frames must fit in `u64`. JavaScript builders accept only safe integers, including the run's end +(`2^53 - 1` at most). Levels 0 through 21 are valid. The writer records the highest stored level, so +a change to the renderer's detail policy does not change how a file reads. Appends currently derive +up to six levels. Tile manifests record each field's `levels`. In query and pick JSON output, sample +indexes above JavaScript's safe-integer range are written as decimal strings. -`Patch.stream` limits one encoded frame (256 MiB by default), in addition to the supplied input -chunk and patches retained by the caller. It validates the header before allocating the frame body -and cancels its reader on failure or early termination. Rust decoding defaults to a 256 MiB frame -limit and a 512 MiB conservative decoded-storage budget; `Patch::decode_with_limits` accepts -`DecodeLimits` for applications with different needs. Applying patches and retaining snapshots -have their own memory costs, separate from decoding. +[Stream](#stream) describes the limits `Patch.stream` applies. In Rust, `Patch::from_bytes` decodes +with the default `DecodeLimits`: a 256 MiB frame and 512 MiB of decoded storage (a conservative +estimate). `Patch::stream(read, limits)` takes other limits. Applying patches and keeping snapshots +cost memory beyond decoding. ## Deploying -Install `latkit` from npm for JavaScript and `latkit-cli` from Cargo for the `latkit` command. -The Rust library is the `latkit` crate. The WASM binding crate is an implementation detail. +Install `latkit` from npm for JavaScript, and `latkit-cli` from Cargo for the `latkit` command. +The Rust library is the `latkit` crate; the crate that binds it to WebAssembly is internal. | Capability | Browser | Node | Native CLI | | ----------------------------------- | ------------------------------ | -------------------------------- | ---------------------------- | @@ -538,13 +566,14 @@ The Rust library is the `latkit` crate. The WASM binding crate is an implementat | H.264 MP4 | WebCodecs Constrained Baseline | Use the CLI | Windows Media Foundation | Serve WASM with `Content-Type: application/wasm`. WebGPU requires HTTPS or localhost and a -supported device. Keep the JavaScript, WASM, and worker from the same package version together. -Images and raw frame output are available where native MP4 encoding is unavailable. +supported device. Serve the JavaScript, WASM, and worker from the same package version. Where native +MP4 encoding is unavailable, the CLI still writes images and raw frames. ### Assets and workers -Default loading resolves assets beside the installed modules. With Vite or another bundler -supporting URL imports, explicit assets also work under nested deployment paths: +By default, `load()` finds the WASM and worker files next to the installed modules. With Vite or +another bundler that supports URL imports, you can pass their URLs explicitly, which also works under +nested deployment paths: ```ts import { load } from 'latkit'; @@ -554,14 +583,15 @@ import workerUrl from 'latkit/worker?url'; await load({ wasm: wasmUrl, worker: () => new Worker(workerUrl) }); ``` -`latkit/worker` is a standalone classic worker. A custom factory may use a Blob URL; retain the -URL while the factory can be called and revoke it on application teardown. Workers are created -on demand and retire when idle. Where workers are unavailable, jobs run on the calling thread. +`latkit/worker` is a standalone classic worker. A custom factory may use a Blob URL; keep the URL +valid while the factory can be called, and revoke it when the application shuts down. Workers start +on demand and stop after 30 seconds idle. Where workers are unavailable, jobs run on the calling +thread. Your Content Security Policy must allow the WASM asset and data origins in `connect-src`, WebAssembly compilation (typically `'wasm-unsafe-eval'` in `script-src`), and the worker URL in -`worker-src`. Add `blob:` only when using Blob workers. Cross-origin resources need appropriate -CORS headers. These requirements are deployment choices; latkit does not change your policy. +`worker-src`. Add `blob:` only when using Blob workers. Cross-origin resources need CORS headers. +latkit never changes your policy. ### Server-side builders and data @@ -572,10 +602,10 @@ const bytes = new Patch().replace('T', [1], { value: [42] }).toBytes(); const expression = field('value').gt(0).toJSON(); ``` -Patches and expressions need no WebAssembly. For Node data/query operations, load the package's -matching WASM bytes as a `WebAssembly.Module` and pass it to `load({ wasm })`. For worker -rendering, provide an `OffscreenCanvas` and drive `gpu.step(now)` if the environment has no -animation-frame loop. +Patches and expressions need no WebAssembly. To use datasets and queries in Node, compile the +package's WASM bytes into a `WebAssembly.Module` and pass it to `load({ wasm })`. To render in a +worker, pass an `OffscreenCanvas` to `gpu.canvas`, and call `gpu.step(now)` yourself if the +environment has no animation-frame loop. See [resource lifetimes](#stats-and-memory) and [video cleanup](#video). @@ -598,16 +628,16 @@ let saved = grid.snapshot().to_bytes(); let restored = Data::read(&saved, Reading::default()).await?; ``` -A `Data` changes in place and its clones share it; a view drawing it draws its latest version, and -a `Snapshot` keeps one version. `Data::read` and `Patch::read` take a `Reading`: the name a dataset -takes, the base its slices are relative to, and the `resource::Fetch` that fetches them. Implement -`fetch(&Request) -> Pending>`; `Pending::ready` wraps immediate results, and `Pending::new` -returns a resolver for asynchronous work. A rendering `Host` implements this smaller interface, so -the native and browser hosts read slices as they are. +A `Data` changes in place, and its clones share it; a view drawing it draws its latest version, and +a `Snapshot` keeps one version. `Data::read` and `Patch::read` take a `Reading`: the dataset's name, +the base its slices are relative to, and the `resource::Fetch` that fetches them. A `Fetch` +implements `fetch(&Request) -> Pending>`; `Pending::ready` wraps an immediate result, and +`Pending::new` returns a pending value with a resolver for asynchronous work. Every rendering +`Host` is also a `Fetch`, so the native and browser hosts read slices directly. Enable `gpu` for scenes, views, and wgpu rendering; `native` adds system fonts, jobs, files, and -Windows H.264. `web` enables browser integration for the WASM target. Native file loading does -not include an HTTP client; applications can provide one through `Fetch` or `Host`. +Windows H.264. `web` enables browser integration for the WASM target. Native file loading includes +no HTTP client; applications can provide one through `Fetch` or `Host`. ### Share an existing device @@ -619,10 +649,10 @@ device.set_device_lost_callback(move |_, _| loss.lost()); device.on_uncaptured_error(std::sync::Arc::new(move |error| events.error(&error))); ``` -Forward these events from your application's existing callbacks. `from_wgpu` validates device -limits and preserves callback ownership. Clone one `Gpu` for views that should share caches and -budgets. `Gpu::new` owns its device and installs its own callbacks. Device loss requires a new -device and views; datasets and scene descriptions can be retained. +Forward device loss and uncaptured errors from your application's own callbacks, as above. +`from_wgpu` checks the device's limits and leaves its callbacks to you. Clone one `Gpu` for views +that should share caches and a budget. `Gpu::new` instead creates its own device and installs its +own callbacks. After device loss, create a new device and views; datasets and scenes can be reused. The [Rust API reference](https://docs.rs/latkit) documents each type, method, feature, and example. The [format specification](#bytes) describes cross-language LATK storage. diff --git a/docs/index.md b/docs/index.md index 70562ea4..95bbbdce 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,9 +15,9 @@ renders interactive views in the browser with WebGPU, and images and video from ::: :::{container} caption -The 70,000-bus ACTIVSg70k grid as one 765 kV line opens: a DC power flow finds where its power goes, -amber on the branches that pick it up and cyan on those it leaves, and each bus stands as high as its -frequency. Drawn by the `latkit` command line in the {doc}`examples/opening` example. +The 70,000-bus ACTIVSg70k grid as one 765 kV line opens. A DC power flow computes where the line's +power goes: branches that pick it up glow amber, and those that lose flow glow cyan. Each bus is +raised by its frequency. Drawn by the `latkit` command line in the {doc}`examples/opening` example. ::: ## Install @@ -30,17 +30,18 @@ cargo add latkit # in Rust ## Quickstart -`quickstart(canvas)` draws a dataset on a canvas; destroy the `gpu` and `data` it returns when the -page is done with them. The package's consumer test type-checks and renders this file. +`quickstart(canvas)` draws a dataset on a canvas. When the page is done with them, destroy the +`gpu` and `grid` it returns. The package's consumer test type-checks and renders this file. ```{literalinclude} ../npm/test/consumer/quickstart.ts :language: typescript ``` -A **dataset** is named tables of columns, changed by **patches**. A **scene** says what to draw: -marks naming tables, in a space, with a theme and controls. A **view** draws a scene from its -**sources**, the datasets it draws, on a canvas or into pictures. Scenes, expressions, and patches -are plain data that need no WebAssembly; `load()` loads it for data and drawing. +A **dataset** is a set of named tables of columns, changed by **patches**. A **scene** describes +what to draw: marks that name tables, in a space, with a theme and controls. A **view** draws a +scene from its **sources** (the datasets it reads) on a canvas or into pictures. Scenes, +expressions, and patches are plain data that need no WebAssembly. `load()` loads the WebAssembly +that datasets and drawing need; `Gpu.create()`, `Data.read()`, and `Patch.read()` call it for you. ## Examples @@ -49,7 +50,7 @@ are plain data that need no WebAssembly; `load()` loads it for data and drawing. : 1,456 machines swinging after a fault, traced from a recording on a plot. {doc}`examples/globe` -: Routes along great circles on a globe, comets flowing on them. +: Routes along great circles on a globe, with comets flowing along them. {doc}`examples/diagram` : Blocks in nested groups, laid out and wired port to port. @@ -61,9 +62,9 @@ are plain data that need no WebAssembly; `load()` loads it for data and drawing. : 260,000 translucent points of a strange attractor. ::: -Every picture is drawn by latkit; the {doc}`examples/index` hold them all with the code that draws -them. The {doc}`guide` covers data, scenes, views, and pictures, the {doc}`cli` draws them from -JSON, {doc}`performance` measures what each costs, and the reference lists every option. +latkit draws every picture on this site; the {doc}`examples/index` show each one with its code. The +{doc}`guide` covers data, scenes, views, and pictures; the {doc}`cli` page covers drawing from JSON; +{doc}`performance` reports benchmark results; and the reference lists every option. ```{toctree} :hidden: diff --git a/docs/performance.md b/docs/performance.md index a37d6072..8697ecfe 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -1,10 +1,10 @@ # Performance -Each benchmark scenario at the largest size it ran, in Chrome on one GPU: what opening took, and the -most a frame took on the CPU and the GPU while the view moved, and to answer a pick or the hover. A -frame's CPU time stays the same however many rows draw; the work grows on the GPU. `cargo xtask -bench` measures every scenario up its sizes and holds it to the committed baselines, and -`node benchmark/docs.ts` writes this table from them. +Each row is a benchmark scenario at the largest size it ran, in Chrome on one GPU. **Open** is the +time from creating the view until it has drawn everything. **Frame** and **GPU** are the median CPU +and GPU milliseconds per frame while the view moved, in its slowest moving phase. **Pick** is the +time to answer a pick or hover. `cargo xtask bench` runs every scenario at increasing sizes and +checks it against the committed baselines; `node benchmark/docs.ts` writes this table from them. ```{csv-table} :file: _static/performance.csv @@ -14,26 +14,40 @@ bench` measures every scenario up its sizes and holds it to the committed baseli ## What a page is held to -The browser gate opens every example page cold and moves the pointer over it. It fails when: +The browser gate first compiles every pipeline cold in Chrome; each must compile within 800 ms. +It then opens each example page in a fresh Chrome without a shader cache, moves the pointer over it +(hovering, zooming, panning, clicking, and stepping through rows with the keys), and exports a +picture. -- a pipeline takes over 800 ms to compile, or fails to; -- a page draws no frame within a second; -- a page misses two display frames in a row while it opens or under the pointer; -- a pipeline compiles where it is first used, holding back the frames that use it. +A page fails at once on: + +- a console or page error; +- a pipeline compiled where it is first used, which holds back the frames that use it; +- no picture exported. + +A page is late if any view has drawn no frame 1,000 ms after the page opens, or if the page misses +more than one display frame in a row while opening or under the pointer. A late page is opened again, +cold, and fails only if it is late both times: a stall of the machine slows one open, while a slow +page slows both. + +On SwiftShader, which draws on the CPU, the gate only checks that every pipeline compiles; page +timings need a GPU. ## What makes it fast -- Every row's drawing is computed on the GPU, so a frame's CPU work stays the same however many - rows draw. -- Pipelines compile off the GPU's thread before any frame uses them: what frames draw with at once, - their text as compile workers free up, and the rest a few at a time. -- A pipeline compiles only what its mark reads and is: the primitive it draws, its joins, sampled - fields, and view state. -- A table too large to draw whole draws the tiles the camera asks for, within the GPU's budget, a - page of its order at a time. A page keeps its place on the GPU while the camera moves, and stays - there while the budget leaves room once the camera moves on, so coming back costs nothing. -- What the view derives from the data, as lanes, triangles, and fitted domains, follows the data, - never the camera: a page's triangles are made the first time it draws. -- Map tiles are decoded and shaped in workers, so a tile landing costs the frame a copy of its rows. -- A write costs the pages holding the rows it changed, and appending the page the rows land in: - tiles are made again only once the rows appended since are an eighth as many as they hold. +- Every row's drawing is computed on the GPU, so a frame's CPU time stays the same however many rows + draw; only the GPU's work grows. +- Pipelines compile in the background, off the GPU's thread, before any frame needs them: first those + the next frame draws with, then text, then the rest, a few at a time. +- Each pipeline contains only the code its mark uses: the primitive it draws, and whether it reads + joins, sampled fields, or view state. +- A table too large to draw whole is paged: the view draws only the pages the camera needs, within + the GPU memory budget. A page keeps its GPU slot while the camera moves, and stays resident after + the camera leaves while the budget allows, so returning to it needs no upload. +- Data derived for drawing, such as edge lanes, triangles, and fitted domains, is recomputed when the + data changes, never when the camera moves. A page's triangles are built the first time it draws. +- Map tiles are decoded and shaped in workers, so a tile arriving costs the frame only a copy of its + rows. +- A write updates only the pages holding the rows it changed, and an append only the page the new + rows go into. A paged table's spatial tiles are rebuilt only once the rows appended since the last + build reach an eighth of the rows it was built from. diff --git a/docs/reference/api.md b/docs/reference/api.md index b79f777f..a044df7f 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -1,22 +1,26 @@ # JavaScript API Generated from the shipped declarations. `load()` loads the WebAssembly that datasets, views, -and pictures are made with; scenes, expressions, and patches need none. Options take the [scene -and result types](types.md). +and pictures need. Scenes, expressions, and patches do not need it. Options use the [scene and +result types](types.md). ## Loading and waiting -Load the WebAssembly once a page; every wait ends at its signal. +Load the WebAssembly once per page. A wait ends when its signal aborts (30 s by default). ### load ```ts /** - * Loads latkit's WebAssembly once a page: data, views, and pictures can be made once it resolves. - * Concurrent calls share one load, and a failed one can be tried again; options given after the first - * call must match it. Importing the package neither loads WebAssembly nor asks for a GPU; loading asks - * for the page's GPU adapter while the WebAssembly compiles. `Gpu.create`, `Data.read`, and - * `Patch.read` load it themselves. + * Loads latkit's WebAssembly, once per page. Datasets, views, and pictures can be created once it + * resolves. Later and concurrent calls share the first call's load. Options they pass must equal + * the first call's, or they reject with an `io` error. A failed load rejects with an `io` error and + * can be retried. + * + * Importing the package loads nothing and requests no GPU. `load()` requests a GPU adapter while + * the WebAssembly compiles, so the browser starts its GPU process early, then requests the first + * GPU device for `Gpu.create()`. `Gpu.create()` and `Data.read()` call `load()` themselves, as does + * `Patch.read()` for JSON. */ function load(options?: LoadOptions): Promise; ``` @@ -24,12 +28,17 @@ function load(options?: LoadOptions): Promise; ### LoadOptions ```ts -/** Loading latkit's WebAssembly: once a page, before data, views, and pictures can be made. */ -/** How latkit loads. Options left out keep what loading began with. */ +/** Options for {@link load}. Only the first call's options take effect. */ interface LoadOptions { - /** The matching package's WASM asset, or an already compiled module. Defaults to the bundled asset. */ + /** + * The URL of this package version's `latkit/wasm` asset, or that asset already compiled. + * Defaults to the bundled asset. + */ wasm?: string | URL | WebAssembly.Module; - /** Makes a worker running the standalone `latkit/worker` bundle. Defaults to the bundled worker. */ + /** + * Creates a worker that runs the standalone `latkit/worker` bundle. Defaults to the bundled + * worker. + */ worker?: () => Worker; } ``` @@ -37,8 +46,13 @@ interface LoadOptions { ### Wait ```ts -/** How long a promise may wait: until `signal` aborts, by default 30 seconds. */ +/** Options that bound how long a promise waits. */ interface Wait { + /** + * Aborts the wait. A timeout rejects with a `timeout` error; any other abort rejects with a + * `cancelled` error whose `cause` is the abort reason. Either way, latkit cancels the call's + * pending work where it can. Without a signal, each wait times out after 30 seconds. + */ signal?: AbortSignal; } ``` @@ -47,27 +61,37 @@ interface Wait { ```ts /** - * What latkit throws: an `Error` with a `code`, and for invalid input the `path` to the part, such - * as `marks[2].vertices.color`. `unloaded` is a call made before `await load()`. + * The error latkit throws: an `Error` with a `code`, plus a `path` to the bad part of invalid + * input, such as `marks[2].vertices.color`. The codes: + * + * - `invalid`: the input is malformed or does not fit; `path` says where. + * - `unsupported`: the browser or device cannot do what was asked. + * - `limit`: a budget or device limit would be exceeded. + * - `io`: reading or writing external bytes failed, such as loading latkit or fetching a file. + * - `device-lost`: the GPU device was lost; create a new `Gpu`. + * - `cancelled`: the wait's signal aborted, or the work was cancelled. + * - `timeout`: the wait's signal timed out. + * - `unloaded`: the call needs WebAssembly, and `load()` has not finished. + * - `destroyed`: the object was destroyed, so it can no longer be used. */ interface LatkitError extends Error { - code: Failure['code'] | 'unloaded'; + code: Failure['code'] | 'unloaded' | 'destroyed'; path?: string; } ``` ## Data and patches -Datasets of named tables, changed by patches written without WebAssembly, and what views draw them from. +Datasets of named tables, the patches that change them (written without WebAssembly), and the sources views draw. ### Data ````ts /** - * A dataset that changes in place: named tables of columns, changed only by patches, every change - * of one or none. A view given it draws its latest version, from the frame after a patch applies. A - * snapshot keeps one version, sharing every page with it, so earlier versions kept, as an undo - * history does, cost only the pages they hold apart. + * A dataset that changes in place: named tables of columns, changed only by patches. A patch + * applies completely or not at all. Views given a `Data` draw its latest version, from the frame + * after each patch. A {@link Snapshot} keeps one version and shares its pages with the dataset, so + * keeping earlier versions, as an undo history does, costs only the pages that differ. * * ```ts * const grid = new Data('grid').apply(new Patch().replace('Bus', [1, 2], { kv: [230, 115] })); @@ -76,16 +100,22 @@ Datasets of named tables, changed by patches written without WebAssembly, and wh * ``` */ class Data { - /** An empty dataset named `name`. */ + /** Creates an empty dataset named `name`. Throws an `unloaded` error before `load()` finishes. */ constructor(name: string); /** - * The dataset `source` holds, as its first bytes say: latkit's bytes; GeoJSON, gzip-compressed or - * not, read in a worker, its points, line strings, and polygons rows of tables `points`, `lines`, - * and `polygons`, each with its geometry as field `geometry` and its properties as fields; or its - * JSON document, `{"name": ..., "patch": [...]}`, its patch read as `Patch.read` reads one. `source` - * is bytes, a `Blob`, a `Response`, a stream, a URL to fetch, or a document as a JSON value. `name` - * names it, as GeoJSON needs; `base` is what a document's slices of files are relative to, by - * default the URL it was fetched from. + * Reads a dataset from `source`, calling `load()` first. Its first bytes decide the format: + * + * - latkit's bytes, as `toBytes()` writes them. + * - GeoJSON, plain or gzip-compressed, read in a worker. Points, line strings, and polygons + * become rows of tables `points`, `lines`, and `polygons`, with the geometry in field + * `geometry` and each property as a field. GeoJSON requires `options.name`. + * - A dataset document, `{"name": ..., "patch": [...]}`, whose patch is read as + * {@link Patch.read} reads one. + * + * `options.name` names the dataset, replacing the name in latkit's bytes or a document. + * `options.base` is the URL that a document's file slices are relative to; it defaults to the URL + * the source was fetched from. Rejects with an `invalid` error for bytes that are not a valid + * dataset, and with an `io` error when a fetch fails or answers an error status. */ static read( source: DataSource, @@ -95,15 +125,24 @@ class Data { } & Wait, ): Promise; get name(): string; - /** Applies every change of `patch`, or none when one is invalid. */ + /** + * Applies every change in `patch` (a {@link Patch} or its bytes), or none of them when one is + * invalid, and returns this dataset. Views draw the new version from their next frame. Throws an + * `invalid` error whose `path` names the failing change, such as `patch[1].Bus.kv`, or when the + * patch names another dataset. + */ apply(patch: Patch | Uint8Array): this; - /** The version it holds now, kept as it is while patches change it: O(1). */ + /** The current version as a {@link Snapshot}, which later patches leave unchanged. O(1). */ snapshot(): Snapshot; - /** Makes `snapshot`, a version of this dataset, the one it holds: what an undo does. */ + /** + * Makes `snapshot`, a version of this dataset, the current version, as an undo does. Views draw + * it from their next frame. Throws an `invalid` error for a snapshot of a dataset with another + * name. + */ restore(snapshot: Snapshot): this; - /** What it holds now: see {@link Snapshot.describe}. */ + /** Describes the current version; see {@link Snapshot.describe}. */ describe(): Description; - /** A value it holds now: see {@link Snapshot.get}. */ + /** Reads one value of the current version; see {@link Snapshot.get}. */ get( table: string, id: Id, @@ -112,13 +151,16 @@ class Data { at?: number; }, ): Value; - /** The rows a query keeps of what it holds now: see {@link Snapshot.query}. */ + /** Queries the current version; see {@link Snapshot.query}. */ query(query: Query): QueryResult; - /** Rows' extremes over spans of what it holds now: see {@link Snapshot.envelope}. */ + /** Per-row minimums and maximums of the current version; see {@link Snapshot.envelope}. */ envelope(request: EnvelopeRequest): Envelope; - /** What it holds now as latkit's bytes: the patch that builds it. */ + /** The current version as latkit's bytes: a patch that builds it from an empty dataset. */ toBytes(): Uint8Array; - /** Frees this handle; views drawing the dataset and its snapshots keep it. */ + /** + * Frees this handle. Views drawing the dataset keep drawing it, and its snapshots stay usable. + * Other methods throw afterwards. + */ destroy(): void; [Symbol.dispose](): void; } @@ -127,12 +169,22 @@ class Data { ### Snapshot ```ts -/** One version of a dataset, as it was when taken: O(1) to keep. A view given one draws it as it is. */ +/** + * One immutable version of a dataset, taken in O(1) by {@link Data.snapshot}. A view given a + * snapshot draws that version unchanged. + */ class Snapshot { get name(): string; - /** What the dataset holds: tables, rows, fields and their kinds, the axis and its span. */ + /** + * Describes the dataset: its tables and their row counts, each field and its kind, and the axis + * and the span its sampled fields cover. + */ describe(): Description; - /** The value of `field` for the row with `id` in `table`, at axis coordinate `at` when given. */ + /** + * Reads `field` of the row with `id` in `table`. A sampled field reads its last frame at or + * before `options.at`, or its last frame when `at` is omitted. A reference reads the referenced + * row's id. Returns `null` when the table, row, field, or value is missing. + */ get( table: string, id: Id, @@ -142,18 +194,24 @@ class Snapshot { }, ): Value; /** - * The rows of a table `query` keeps, filtered, sorted, and paged, sampled fields read at its - * coordinate, on the CPU through the expressions marks draw with. + * Runs `query` on the CPU: reads a table's rows, filtered, sorted, and paged, through the same + * expressions marks use. Sampled fields are read at `query.at`. Throws an `invalid` error for an + * invalid query. */ query(query: Query): QueryResult; /** - * Each row's least and greatest value of a sampled field over equal spans of a range, of every row - * or of the rows named: row after row, NaN where a span holds no frame. It reads the coarsest - * levels of detail that fit, so a span costs a few reads however many frames it covers. + * Computes each row's minimum and maximum of a sampled field over equal spans of an axis range, + * for every row or the rows `request.ids` lists. Each span reads the coarsest stored levels of + * detail that fit in it, so it costs a few reads regardless of how many frames it covers. Throws + * an `invalid` error for an unknown table or row, or a field that is not sampled, and a `limit` + * error when rows times spans exceeds 2^26. */ envelope(request: EnvelopeRequest): Envelope; - /** The version as latkit's bytes: the patch that builds it. */ + /** This version as latkit's bytes: a patch that builds it from an empty dataset. */ toBytes(): Uint8Array; + /** + * Frees this handle. Views given the snapshot keep drawing it; other methods throw afterwards. + */ destroy(): void; [Symbol.dispose](): void; } @@ -163,23 +221,23 @@ class Snapshot { ```ts /** - * A dataset fetched in pieces as views ask for them: plain data naming where they lie. Each view - * draws the pieces its camera and plot window ask for as a dataset of its own, and every view of a - * GPU shares what is fetched. The source's first bytes say what it holds. + * A dataset fetched in pieces as views need them. A `Tiles` is plain data: a dataset name and a + * URL. Each view draws the pieces its camera and plot window need as a dataset of its own, and all + * views of a GPU share the fetched pieces. The first bytes at `url` decide the kind of tiles: * - * - Time tiles, a `frames.json` manifest as `latkit tile` writes it: a dataset whose sampled - * fields' histories are fetched in spans. A line tracing a field fetches the detail its plot - * window draws, a mark showing one the frames about the view's coordinate, and a span still - * loading draws the coarser span above it. - * - Vector tiles: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` template, fetched as - * the camera asks. Each layer is a table of its features of the kind marks draw it as (areas - * polygons, lines lines, vertices points), or named `layer:polygons`, `layer:lines`, or - * `layer:points`. + * - Time tiles: a `frames.json` manifest as `latkit tile` writes it, for a dataset whose sampled + * fields' histories are fetched in spans. A line tracing a field fetches the level of detail its + * plot window shows; other marks fetch the frames around the view's `at`. A span still loading + * shows the coarser span that contains it. + * - Vector tiles: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` URL template, fetched + * as the camera needs them. Each layer becomes a table of features of the kind its marks draw: + * polygons for areas, lines for lines, points for vertices. A table named `layer:polygons`, + * `layer:lines`, or `layer:points` holds that kind regardless of the marks. */ class Tiles { readonly name: string; readonly url: string; - /** The tiles at `url`, drawn as dataset `name`. */ + /** Tiles at `url`, drawn as the dataset named `name`. */ constructor(name: string, url: string); } ``` @@ -187,14 +245,17 @@ class Tiles { ### Source ```ts -/** What a view draws a dataset from, by its name. */ +/** A dataset a view draws; views match it to the scene's marks by the dataset's name. */ type Source = Data | Snapshot | Tiles; ``` ### DataSource ```ts -/** What `Data.read` reads: latkit's bytes, GeoJSON, or a dataset's JSON document. */ +/** + * What {@link Data.read} accepts: bytes, a `Blob`, a `Response`, a byte stream, a URL to fetch as + * a string, or a parsed JSON value. The content is latkit's bytes, GeoJSON, or a dataset document. + */ type DataSource = Uint8Array | ArrayBuffer | Blob | Response | ReadableStream | string | object; ``` @@ -203,8 +264,9 @@ type DataSource = ```ts /** - * The rows a query keeps, in the forms a patch writes: numbers as a `Float64Array` (missing NaN), - * other values as arrays (missing `null`), a reference as its target's ids. + * The rows a query returns, as columns in the forms a patch takes: numbers as a `Float64Array` + * (missing values NaN), other values as arrays (missing `null`), and a reference as the ids of the + * rows it references. */ interface QueryResult { table: string; @@ -218,7 +280,7 @@ interface QueryResult { ids: (Id | null)[]; } >; - /** The rows the query kept before paging. */ + /** How many rows matched the query before `offset` and `limit` applied. */ total: number; } ``` @@ -226,10 +288,16 @@ interface QueryResult { ### Envelope ```ts -/** Rows' least and greatest values over spans of the axis: `at` holds one more edge than spans. */ +/** Each row's minimum and maximum of a sampled field over equal spans of the axis. */ interface Envelope { + /** The span boundaries on the axis: one more than the number of spans. */ at: Float64Array; + /** + * Each row's minimum in each span: `spans` values per row, row after row. NaN where a span has + * no frame. + */ least: Float32Array; + /** Each row's maximum in each span, laid out like `least`. */ greatest: Float32Array; } ``` @@ -238,8 +306,10 @@ interface Envelope { ````ts /** - * Changes to a dataset, applied in order and all at once by `Data.apply`: plain data, written as - * latkit's bytes as it is built. + * A list of changes to a dataset, which `Data.apply` applies in order, all or none. The patch is + * encoded as latkit's bytes as it is built, without WebAssembly. Each method adds a change and + * returns the patch. For values it cannot encode, a method throws an `invalid` error with a `path` + * such as `Bus.kv`, and leaves the patch unchanged. * * ```ts * const patch = new Patch('grid') @@ -248,18 +318,24 @@ interface Envelope { * ``` */ class Patch { - /** An empty patch: to dataset `data`, which it names, or to whichever it is applied to. */ + /** Creates an empty patch for the dataset named `data`, or for any dataset when omitted. */ constructor(data?: string); - /** The dataset the patch changes, when it names one; `Data.apply` refuses it for another. */ + /** + * The name of the dataset this patch is for, if any. `Data.apply` rejects it for a dataset with + * another name. + */ get data(): string | undefined; /** - * The patch `source` holds: latkit's bytes, one frame as `toBytes` writes it, or its JSON, what - * any language writes without latkit: a list of changes, each named by its method with its - * parameters by name, such as `{"set": {"table": "Bus", "ids": [1], "fields": {"kv": [345]}}}`. - * Slices of binary files, `{file, dtype, offset, count}`, are fetched at once relative to `base`, - * by default the URL the JSON was fetched from. `source` is bytes, a `Blob`, a `Response`, a URL - * to fetch, or the JSON as a value. Bytes read without WebAssembly; JSON loads it, as `load()` - * does. A frame longer than `limit` bytes (256 MiB unless given) is refused. + * Reads a patch from `source`: latkit's bytes, one frame as `toBytes()` writes it, or JSON that + * any language can write without latkit. The JSON is a list of changes. Each change is keyed by + * its method name and holds the method's parameters by name, such as + * `{"set": {"table": "Bus", "ids": [1], "fields": {"kv": [345]}}}`. A field may be a slice of a + * binary file, `{file, dtype, offset, count}`. Slices are fetched at once, relative to + * `options.base`, which defaults to the URL the JSON was fetched from. + * + * Bytes are read without WebAssembly; JSON calls `load()` first. Rejects with a `limit` error for + * a frame longer than `options.limit` bytes (256 MiB by default), with an `invalid` error for + * malformed input, and with an `io` error when a fetch fails or answers an error status. */ static read( source: PatchSource, @@ -269,10 +345,13 @@ class Patch { } & Wait, ): Promise; /** - * The patches arriving back to back in `stream`, each as soon as its last byte does: a response - * body, a file, or a socket's messages. Each frame says its length up front, so a patch longer - * than `limit` bytes (256 MiB unless given) throws before allocating its body. The parser holds at most one frame, in addition to - * the input chunk and patches the caller retains. Breaking iteration cancels the reader. + * Yields each patch in `stream` as soon as its last byte arrives. The stream carries frames back + * to back, such as a response body, a file, or a socket's messages. Each frame states its length + * up front, so a frame longer than `options.limit` bytes (256 MiB by default) throws a `limit` + * error before its body is allocated. Besides the current chunk and the patches the caller keeps, + * at most one frame is buffered. Breaking out of the loop cancels the stream. Throws an `invalid` + * error for a `limit` that is not a whole number of at least 12 bytes, for bytes that are not + * latkit's, or for a stream that ends inside a frame. * * ```ts * for await (const patch of Patch.stream((await fetch('/feed')).body!)) data.apply(patch); @@ -286,22 +365,27 @@ class Patch { limit?: number; }, ): AsyncGenerator; - /** Names the axis sampled fields are recorded over, such as `('time', 's')`. */ + /** + * Sets the name and unit of the axis that sampled fields are recorded over, such as + * `axis('time', 's')`. + */ axis(name: string, unit: string): this; - /** Replaces table `table` with these rows and fields, or adds it. */ + /** Replaces table `table` with rows `ids` and `fields`, adding the table if it does not exist. */ replace(table: string, ids: RowIds, fields?: Fields): this; /** - * Writes `fields` for rows `ids` of `table`: rows with known ids change, others are added, and - * fields left out keep their values. Adds the table when there is none of its name. + * Writes `fields` for rows `ids` of `table`. Rows with existing ids change, new ids add rows, and + * omitted fields keep their values. Adds the table if it does not exist. */ set(table: string, ids: RowIds, fields?: Fields): this; /** Removes the rows of `table` with `ids`. */ remove(table: string, ids: RowIds): this; /** - * Records a frame of `table` at each axis coordinate of `at`: for each field, a value per row at - * every coordinate, frame-major; of every row, or of rows `ids`, the rest holding their last - * values. A field's first frame makes it sampled. Appends of float arrays to the same fields of - * every row, one after another, write as one change, so a frame a call costs no more than one call. + * Records a frame of `table` at each axis coordinate in `at`. Each field gives a value per row + * per frame, frame by frame: every row of the first frame, then of the next. Frames cover every + * row, or only rows `options.ids`; other rows keep their last values. A field's first frame makes + * it a sampled field. Consecutive appends of `Float32Array` or `Float64Array` values to the same + * fields of every row are written as one change, so one frame per call costs no more than one + * call with every frame. */ append( table: string, @@ -313,15 +397,20 @@ class Patch { ids?: RowIds; }, ): this; - /** Forgets every sampled field's frames before axis coordinate `before`. */ + /** Drops every sampled field's frames before axis coordinate `before`. */ forget(before: number): this; /** - * Holds a run of sampled field `field`'s entries of level `level` (0 unless given), numbered from - * `first`: what a time tile holds, as `latkit tile` writes them. At level 0 the entries are frames, - * each a coordinate in `at` and a value per row of `table` in `values`. At level `k` they are - * buckets of `8^k` frames, each its first and last frame's coordinates in `at`, and in `values` - * each row's least value over each bucket, then each row's greatest. NaN is missing. No entry may - * be held already; frames past the field's widen it. + * Stores a run of entries of sampled field `field` of `table`, at level of detail `level` (0 by + * default), numbered from `first`. This is what a time tile holds, as `latkit tile` writes it. + * + * At level 0 each entry is a frame: a coordinate in `at`, and a value per row in `values`, entry + * by entry. At level `k` each entry is a bucket of `8^k` frames: its first and last frames' + * coordinates in `at`. `values` then holds every bucket's per-row minimums, entry by entry, + * followed by every bucket's per-row maximums. NaN marks a missing value. + * + * None of the entries may be stored already. Entries past the field's last frame extend it. + * Throws an `invalid` error for a level outside 0 to 21, lengths of `at` and `values` that do not + * divide into whole entries and rows, or frame numbers that are not safe nonnegative integers. */ frames( table: string, @@ -338,9 +427,9 @@ class Patch { values: Float32Array | readonly number[]; }, ): this; - /** Appends `next`'s changes, to this patch's dataset. */ + /** Appends `next`'s changes to this patch, which keeps its own dataset name. */ extend(next: Patch): this; - /** The patch as bytes: one frame, naming its dataset when it has one. */ + /** A copy of the patch as latkit's bytes: one frame, naming the dataset if the patch does. */ toBytes(): Uint8Array; } ```` @@ -348,14 +437,21 @@ class Patch { ### PatchSource ```ts -/** What `Patch.read` reads: latkit's bytes or a patch's JSON. */ -type PatchSource = Uint8Array | ArrayBuffer | Blob | Response | string | object; +/** + * What {@link Patch.read} accepts: bytes, a `Blob`, a `Response`, a byte stream, a URL to fetch as a + * string, or a parsed JSON value. The content is latkit's bytes or a patch's JSON. + */ +type PatchSource = + Uint8Array | ArrayBuffer | Blob | Response | ReadableStream | string | object; ``` ### RowIds ```ts -/** Row ids as a patch takes them: listed, `{ count }` for rows `0..count`, or a typed array. */ +/** + * Row ids: a list of numbers or strings, `{ count }` for ids `0` to `count - 1`, or a `Uint32Array` + * or `Float64Array` of numbers. + */ type RowIds = Ids | readonly (Id | null)[] | Uint32Array | Float64Array; ``` @@ -363,8 +459,9 @@ type RowIds = Ids | readonly (Id | null)[] | Uint32Array | Float64Array; ```ts /** - * One field's values, a value per row; `null` is missing. Typed arrays copy at once: numbers, or - * in a `Uint8Array` booleans, nonzero true. Booleans, texts, and geometries also as arrays. + * One field's values, one per row, with `null` for a missing value. Numbers come as a typed array + * or an array. A `Uint8Array` holds booleans, nonzero being true. Booleans, strings, and geometries + * also come as arrays. Values are copied when passed, so the arrays can be reused at once. */ type Values = | Float64Array @@ -381,14 +478,20 @@ type Values = ### Geometry ```ts -/** A point `[x, y]`, or a list of geometries one level shallower: a path, its parts, or polygons. */ +/** + * A point `[x, y]`, or a nested list of points: a path (a list of points), parts (a list of paths, + * such as a polygon's rings), or polygons (a list of parts). + */ type Geometry = readonly number[] | readonly Geometry[]; ``` ### Reference ```ts -/** A field naming rows of another table by id: `null` leaves a row unwired. */ +/** + * A reference field: for each row, the id of a row in table `reference`, or `null` to reference + * none. + */ interface Reference { reference: string; ids: RowIds; @@ -398,21 +501,21 @@ interface Reference { ### reference ```ts -/** Values of a field naming rows of `table` by id. */ +/** Builds a {@link Reference} field: for each row, the id of the row of `table` it references. */ function reference(table: string, ids: RowIds): Reference; ``` ### Fields ```ts -/** A table's fields by name. */ +/** A table's fields by name: values, or a {@link Reference}. */ type Fields = Readonly>; ``` ### ReadableBytes ```ts -/** A byte stream, structurally compatible with the browser and Node ReadableStream. */ +/** A byte stream: anything shaped like a browser or Node.js `ReadableStream` of `Uint8Array`. */ interface ReadableBytes { getReader(): { read(): Promise<{ @@ -427,20 +530,23 @@ interface ReadableBytes { ## Expressions -What varies by row: builders writing the expressions and colors scenes take. +Builders for the per-row expressions and colors that scene options take. ### Expr ```ts -/** A value for every row of a mark: a constant, a field, view state, or arithmetic of them. */ +/** A value for every row of a mark: a constant, a field, view state, or arithmetic on them. */ class Expr { readonly node: ExprNode; - /** An expression from its JSON form; use the builders for common operations. */ + /** Wraps an expression's JSON form. The builders cover common cases. */ constructor(node: ExprNode); toJSON(): ExprNode; - /** Reads this expression's fields from `table` of dataset `data`, joined by row id. */ + /** Reads this expression's fields from table `table` of dataset `data`, joined by row id. */ from(data: string, table: string): Expr; - /** Reads sampled fields at their last frame without interpolating. */ + /** + * Reads this expression's sampled fields at the last frame at or before the view's `at`, instead + * of interpolating toward the next frame. + */ step(): Expr; add(other: ExprInput): Expr; sub(other: ExprInput): Expr; @@ -463,18 +569,29 @@ class Expr { neg(): Expr; abs(): Expr; sqrt(): Expr; + /** The sine of the value, an angle in radians. */ sin(): Expr; + /** The cosine of the value, an angle in radians. */ cos(): Expr; floor(): Expr; ln(): Expr; exp(): Expr; /** Whether a text field equals `text`. */ is(text: string): Expr; - /** Whether a text field holds `text`, ignoring case: in a mark, a field of its own table. */ + /** + * Whether a text field contains `text`, ignoring case. In a mark, the field must be in the mark's + * own table. + */ contains(text: string): Expr; - /** The value through `map`, a color for every row: a catalog name, or your own colors. */ + /** + * Maps the value to a color for every row through `map`, a catalog name or custom colors. The + * domain fits the values shown unless set with {@link Colormapped.domain}. + */ colormap(map: Colormap): Colormapped; - /** The value mapped linearly onto `[low, high]`, clamped. */ + /** + * Maps the value linearly onto `[low, high]`, clamped. The domain fits the values shown unless + * set with {@link Mapped.domain}. + */ scale(low: number, high: number): Mapped; } ``` @@ -482,16 +599,16 @@ class Expr { ### ExprInput ```ts -/** What a value is written as: an {@link Expr}, or its JSON form, in which a string names a field. */ +/** A value option: an {@link Expr}, or its JSON form, in which a string is a field name. */ type ExprInput = Expr | ExprNode; ``` ### Mapped ```ts -/** A value mapped onto a range, whose domain can be set. */ +/** A value mapped linearly onto a range, with an input domain that can be set. */ class Mapped extends Expr { - /** The input values that map to the ends; it fits the values shown unless set. */ + /** Sets the input values that map to the range's ends, instead of fitting the values shown. */ domain(low: number, high: number): Mapped; } ``` @@ -499,7 +616,7 @@ class Mapped extends Expr { ### Missing ```ts -/** A missing value, which options take the theme's for: a value and a color alike. */ +/** A missing value, valid as a value or a color. An option given one uses the theme's value. */ class Missing extends Expr { readonly node: MissingNode; constructor(); @@ -511,12 +628,12 @@ class Missing extends Expr { ```ts /** - * A color for every row of a mark: a CSS color, a value through a colormap, or a conditional choosing - * between colors. Color options take one, or a CSS color as it is. + * A color for every row of a mark: a constant, a value mapped through a colormap, or a conditional + * choosing between colors. Color options take a `Paint` or a CSS color string. */ class Paint { readonly node: PaintNode; - /** A color from its JSON form; use the builders for common colors. */ + /** Wraps a color's JSON form. The builders cover common cases. */ constructor(node: PaintNode); toJSON(): PaintNode; } @@ -526,8 +643,8 @@ class Paint { ```ts /** - * What a color is written as: a CSS color, a {@link Paint}, or its JSON form. A string is a CSS - * color here; a field reads with {@link field}. + * A color option: a CSS color, a {@link Paint}, or its JSON form. A string here is always a CSS + * color, never a field name; map a field to colors with `field(name).colormap(map)`. */ type PaintInput = Color | Paint | PaintNode; ``` @@ -535,9 +652,9 @@ type PaintInput = Color | Paint | PaintNode; ### Colormapped ```ts -/** A value through a colormap, whose domain can be set. */ +/** A value mapped through a colormap, with an input domain that can be set. */ class Colormapped extends Paint { - /** The input values that map to the ends; it fits the values shown unless set. */ + /** Sets the input values that map to the colormap's ends, instead of fitting the values shown. */ domain(low: number, high: number): Colormapped; } ``` @@ -545,28 +662,30 @@ class Colormapped extends Paint { ### Color ```ts -/** A CSS color, such as `'#ffb000'` or `'rebeccapurple'`: what a string is in a color option. */ +/** + * A CSS color, such as `'#ffb000'` or `'rebeccapurple'`. In a color option, a string is a color. + */ type Color = string; ``` ### field ```ts -/** The field `name` of each row. */ +/** Reads field `name` of each row. */ const field: (name: string) => Expr; ``` ### color ```ts -/** A color constant. */ +/** A constant color, from a CSS color string. */ const color: (css: Color) => Paint; ``` ### missing ```ts -/** A missing value: options that read one take the theme's. */ +/** A missing value: an option given one uses the theme's value. */ const missing: () => Missing; ``` @@ -574,8 +693,10 @@ const missing: () => Missing; ```ts /** - * Starts a conditional: `when(c).then(a).when(d).then(b).otherwise(e)`. Its values are colors, a - * string a CSS color, where its first is one; else values. + * Starts a conditional. `when(c).then(a).when(d).then(b).otherwise(e)` takes the value of the first + * case whose condition holds, else `e`. If the first `then` gets a color (a CSS string, a + * {@link Paint}, a missing value, or a color's JSON form), every value of the conditional is a + * color, and strings are CSS colors. Otherwise every value is an expression. */ function when(condition: ExprInput): When; ``` @@ -583,11 +704,11 @@ function when(condition: ExprInput): When; ### When ```ts -/** A conditional's first case, waiting for its value where the condition holds. */ +/** A conditional's first condition, awaiting its value from `then`. */ class When { - /** The color where the condition holds: every value of the conditional is a color. */ + /** Sets the color where the condition holds; every value of the conditional is then a color. */ then(value: PaintInput): Then; - /** The value where the condition holds. */ + /** Sets the value where the condition holds. */ then(value: ExprInput): Then; } ``` @@ -595,11 +716,11 @@ class When { ### Then ```ts -/** A conditional's cases so far, waiting for another or the value where none holds. */ +/** A conditional's cases so far, awaiting another `when` case or the `otherwise` fallback. */ class Then { - /** Another case, tried when those before it do not hold. */ + /** Adds a case, tried when no earlier condition holds. */ when(condition: ExprInput): Next; - /** The value where no case holds. */ + /** Sets the value where no condition holds, and returns the finished conditional. */ otherwise(value: InputOf): T; } ``` @@ -607,9 +728,9 @@ class Then { ### Next ```ts -/** A conditional's next case, waiting for its value where the condition holds. */ +/** A conditional's next condition, awaiting its value from `then`. */ class Next { - /** The value where the condition holds. */ + /** Sets the value where this condition holds. */ then(value: InputOf): Then; } ``` @@ -617,7 +738,7 @@ class Next { ### InputOf ```ts -/** What a conditional's values are written as, a value's or a color's. */ +/** A conditional's value input: {@link PaintInput} for colors, else {@link ExprInput}. */ type InputOf = T extends Paint ? PaintInput : ExprInput; ``` @@ -625,8 +746,9 @@ type InputOf = T extends Paint ? PaintInput : ExprInput; ```ts /** - * A size in CSS pixels, whatever the space's own unit: a stroke keeps its width on screen as a - * diagram zooms. A number makes a theme's length too. + * A size in CSS pixels regardless of the space's unit, so it stays the same on screen as the view + * zooms: `px(1)` keeps a diagram's stroke 1 pixel wide at any zoom. Given a number, the result also + * works as a theme length. */ function px(size: number): { px: number; @@ -638,10 +760,10 @@ function px(size: ExprInput): Px; ```ts /** - * A size in position units, whatever the space's own unit, so it scales as the view zooms, kept - * within `atLeast` and `atMost` CSS pixels when given: `units('r', { atLeast: 2 })` grows with the map - * and never vanishes. A marker keeps its proportions, its larger side held. A number alone makes a - * theme's length too. + * A size in position units (degrees on the earth) regardless of the space's unit, so it scales as + * the view zooms. `within` limits its on-screen size in CSS pixels: `units('r', { atLeast: 2 })` + * grows with the map and never shrinks below 2 pixels. A marker keeps its proportions, with the + * limits applied to its larger side. Given only a number, the result also works as a theme length. */ function units(size: number): { units: number; @@ -652,11 +774,11 @@ function units(size: ExprInput, within?: Within): Units; ### Within ```ts -/** The CSS pixels a size in position units keeps within. */ +/** On-screen limits, in CSS pixels, for a size given in position units. */ interface Within { - /** CSS pixels it keeps at least, however far the view zooms out. */ + /** The smallest size in CSS pixels, however far the view zooms out. */ atLeast?: number; - /** CSS pixels it keeps at most, however far the view zooms in. */ + /** The largest size in CSS pixels, however far the view zooms in. */ atMost?: number; } ``` @@ -678,7 +800,7 @@ const hovered: () => Expr; ### pointer ```ts -/** CSS pixels from a vertex to the pointer. */ +/** CSS pixels from a vertex to the pointer. Vertex marks only, and not in a vertex's position. */ const pointer: () => Expr; ``` @@ -692,21 +814,24 @@ const zoom: () => Expr; ### elapsed ```ts -/** Seconds since the view was created. */ +/** Seconds since the view was created. A mark that reads it redraws every frame. */ const elapsed: () => Expr; ``` ### icon ```ts -/** An icon marker showing one of `images` for each row, picked by `index`. */ +/** + * An icon marker: each row shows `images[index]`, fitted in the marker's box, with `index` counting + * from 0. The pixels are copied into the marker. + */ function icon(images: readonly RgbaImage[], index: ExprInput): Marker; ``` ### RgbaImage ```ts -/** Straight-alpha RGBA pixels, compatible with browser ImageData and server-produced pixels. */ +/** RGBA pixels with straight alpha, rows top to bottom. A browser `ImageData` has this shape. */ interface RgbaImage { width: number; height: number; @@ -718,8 +843,10 @@ interface RgbaImage { ```ts /** - * A CSS gradient through `map`, a catalog name or colors of your own, toward `direction`: the colors - * the GPU draws, a band each for a categorical map's categories. + * Returns a CSS `linear-gradient(...)` with the colors the GPU draws for `map`, a catalog name or a + * custom colormap. `direction` is the gradient's CSS direction, `'to right'` by default. A + * categorical map gives one solid band per category. Needs `await load()` first, and throws an + * `invalid` error for an invalid `map`. */ function colormapCss(map: Colormap, direction?: string): string; ``` @@ -731,55 +858,77 @@ A GPU makes views, and canvases that show them. ### Gpu ```ts -/** The GPU device views draw with. Views of one GPU draw in one animation frame, sharing its memory. */ +/** + * A WebGPU device that views draw with. The views and canvases of one `Gpu` share its memory and + * draw together in each animation frame, so linked views always show the same moment. + */ class Gpu { - /** Asks for a WebGPU device, loading latkit first: the first call takes the one made as it loaded. */ + /** + * Requests a WebGPU device, calling `load()` first. The first call reuses the device that + * `load()` requested. Rejects with an `unsupported` error when the browser has no WebGPU device. + */ static create(): Promise; /** - * A view of `scene`, drawing the datasets its `sources` give. A {@link Canvas} shows it, and a - * {@link Shot} draws it with or without one. + * Creates a view that draws `scene` from the datasets in `options.sources`. Show it in a + * {@link Canvas}, or draw it into pictures with a `Shot`, with or without a canvas. Throws an + * `invalid` error for an invalid scene. */ view(scene: Scene, options?: ViewOptions): View; /** - * A canvas showing views as `layout` places them in `target`: a `` element, which sizes - * itself to its CSS box and passes each view the input landing on it, or an `OffscreenCanvas` in a - * worker, which keeps the size it is given at `pixelRatio` pixels a CSS pixel. + * Creates a canvas that shows views in `target`, placed by `options.layout`. A `` element + * sizes itself to its CSS box at the device pixel ratio, and passes pointer, wheel, and key input + * to the views in its panels. An `OffscreenCanvas`, as in a worker, keeps the size it is given at + * `options.pixelRatio` pixels per CSS pixel, and takes no input. Throws an `invalid` error for an + * invalid layout, such as a view already shown in another panel. */ canvas(target: HTMLCanvasElement | OffscreenCanvas, options?: CanvasOptions): Canvas; /** - * Calls `listener` with the frame's timestamp every animation frame, before the views draw, so - * what it changes shows in that frame. Returns a function that stops it. + * Calls `listener` every animation frame with the frame's timestamp in milliseconds, before the + * views draw, so changes it makes show in that frame. While any listener is registered, a frame + * runs every animation frame. Returns a function that removes the listener. */ on(type: 'frame', listener: (now: number) => void): () => void; - /** Whether the device was lost; a new `Gpu` is needed. */ + /** Whether the device was lost. Drawing then does nothing; create a new `Gpu`. */ get lost(): boolean; - /** How much GPU memory data takes. */ + /** + * The GPU memory budget for data, shared by every view of this GPU; see {@link Budget}. Defaults + * to half the device's largest storage buffer, and 32 MiB uploaded per frame. + */ get budget(): Budget; set budget(budget: Budget); - /** What this GPU did since it was created, counted exactly; see {@link Work}. */ + /** + * Exact counts of the work this GPU has done since it was created; see {@link Work}. The counts + * repeat for the same budget, device limits, and frames, so tests and benchmarks can assert them. + */ get work(): Work; - /** Bytes on the GPU every view of it draws from: data, and what each view keeps per row. */ + /** + * Bytes of GPU memory the views of this GPU draw from: data pages, and each view's per-row data. + */ get resident(): number; - /** Bytes waiting to upload, for every view of this GPU. */ + /** Bytes waiting to upload to the GPU, across every view of this GPU. */ get uploading(): number; /** - * Draws one frame now at `now`, milliseconds on the clock animation frames give: the frame - * listeners, then every canvas that needs a frame. From the first step no animation frames are - * asked for: a harness, as tests and benchmarks are, steps every frame itself, so a run draws the - * same frames however fast the device is. + * Draws one frame immediately at time `now`, in milliseconds on the `requestAnimationFrame` + * clock: it runs the frame listeners, then draws every canvas that needs a frame. After the first + * call, this GPU stops requesting animation frames. A test or benchmark that steps every frame + * itself then draws the same frames on any device. */ step(now: number): void; /** - * Resolves once the GPU finished the work submitted to it, every readback landed, and every - * pipeline a view or shot asked for compiled: a frame stepped then draws all of it. + * Resolves once the GPU has finished all submitted work, every readback has arrived, and every + * pipeline a view or shot requested has compiled. A frame stepped after that uses all of it. */ done(wait?: Wait): Promise; /** - * Resolves when work a view waits on lands after this call (a readback, a compile, a job, a text, - * a fetch), or a view asks for a frame. + * Resolves when, after this call, work a view is waiting on arrives (a readback, a pipeline + * compile, a job, a label text, or a fetch), or a view requests a frame. A harness that steps + * frames awaits it to know when to step again. */ landed(wait?: Wait): Promise; - /** Releases this GPU's canvases, views, and frame listeners. Other GPUs and data stay usable. */ + /** + * Destroys this GPU's canvases, views, and frame listeners, then frees the device. Other GPUs and + * datasets stay usable. Calling it again does nothing; other methods throw afterwards. + */ destroy(): void; [Symbol.dispose](): void; } @@ -788,62 +937,100 @@ class Gpu { ### View ````ts -/** A scene drawn from the sources it is given: shown by a canvas, which passes it input, or drawn by shots. */ +/** + * A scene drawn from its sources; created by `Gpu.view()`. A canvas shows it and passes it input, + * and a `Shot` draws it into pictures. + */ class View { - /** The scene drawn; setting an invalid one throws and keeps the scene drawn. */ + /** + * The scene drawn. Marks that match a mark of the previous scene by kind, dataset, table, and + * name keep their state and ease to their new options. Setting an invalid scene throws an + * `invalid` error and keeps the current one. + */ get scene(): Scene; set scene(scene: Scene); /** - * What the view draws, each a dataset by name: a `Data`, its latest version from the frame after - * each patch applies; a `Snapshot`, as it is; `Tiles`, the pieces the view asks for. A mark its - * dataset no longer fits draws nothing, and the next frame tells why. Two sources of one name are - * invalid. + * The datasets the view draws, matched to marks by dataset name. The view draws a `Data` at its + * latest version, from the frame after each patch; a `Snapshot` unchanged; and `Tiles` as the + * pieces it needs. A mark whose dataset no longer fits it draws nothing; it is listed in + * `failures`, and the next frame emits an `error` event. Setting two sources with the same name + * throws an `invalid` error. */ get sources(): readonly Source[]; set sources(sources: readonly Source[]); - /** The axis coordinate shown: sampled fields read their last frame at or before it. */ + /** + * The axis coordinate shown, such as a time. Marks read sampled fields at it, interpolating + * between the frame at or before it and the next, unless an expression uses `step()`. + */ get at(): number; set at(at: number); - /** Where the view looks; setting it looks there at once. */ + /** + * Where the view looks. Setting it moves the camera at once; use {@link View.fly} to ease. A + * camera of the wrong kind for the space, such as a plot camera on a map, is ignored. + */ get camera(): Camera; set camera(camera: Camera); - /** CSS pixels kept clear at the top, right, bottom, and left, as when panels cover the view. */ + /** + * CSS pixels kept clear at the top, right, bottom, and left, such as where page panels cover the + * view. The camera centers and fits within the rest. + */ get insets(): Insets; set insets(insets: Insets); - /** Degrees per second a map or plane turns around its center; 0 stops it. A diagram never turns. */ + /** + * Degrees per second the camera turns about its center on a map or plane; 0 stops it. A diagram + * never turns. + */ get orbit(): number; set orbit(degreesPerSecond: number); - /** The rows selected, a set per table; setting it selects those and no others. */ + /** The selected rows, one set per table. Setting it selects exactly those rows. */ get selection(): readonly Rows[]; set selection(rows: readonly Rows[]); - /** What the user points at: the row under the pointer, or the row the keys stepped to. */ + /** The row under the pointer, or the row keyboard navigation moved to; `null` if none. */ get hovered(): Hit | null; - /** The work under way the last frame stood in for; empty once it is complete. */ - get waiting(): Waiting; /** - * Whether the last frame drew everything as the data, scene, and camera say, with nothing standing - * in for work under way: what `waiting` lists. A shot waits for it. + * The kinds of pending work that kept the last frame from being complete; empty when it was + * complete. See {@link Waiting}. */ + get waiting(): Waiting; + /** Whether the last frame was complete, with nothing in `waiting` pending. Shots wait for it. */ get complete(): boolean; - /** Every mark that draws nothing because its data no longer fits it, and why. */ + /** + * An error for each mark that draws nothing because its data no longer fits it, such as a missing + * table. Each error's `path` names the mark. + */ get failures(): readonly LatkitError[]; - /** What the view does each frame. */ + /** Counts and timings of the view's frames; see {@link Stats}. */ get stats(): Stats; - /** Whether the view wants a frame: it changed, or work it waited on landed. */ + /** Whether the view needs a frame, because it changed or work it was waiting on arrived. */ get wantsFrame(): boolean; - /** Frames what `fit` names on the next frame, keeping pitch and bearing. */ + /** + * Frames what `fit` names, every row by default, keeping pitch and bearing. It takes effect on + * the next frame, or a frame or two later when the extent must be measured on the GPU. + */ fit(fit?: Fit): void; - /** Eases to `camera`. */ + /** + * Eases the camera to `camera` over twice the scene's ease time, or at once when motion is + * reduced. A camera of the wrong kind for the space is ignored. + */ fly(camera: Camera): void; - /** Where `position` lands on screen; `null` behind the camera. */ + /** + * Projects `position` to a point on screen with the current camera, for anchoring HTML overlays; + * `null` when it is behind the camera. + */ screen(position: Position): Point | null; - /** The position on the ground under screen point `point`; `null` where the view shows none there. */ + /** + * The position on the ground under screen point `point`: at elevation 0 on a plane, on the map or + * globe on the earth, the values there on a plot. `null` where the point looks past the ground. + */ position(point: Point): Position | null; - /** Where `row` lies on screen, or `null` when no mark draws it in view, once a frame draws the view. */ + /** + * Resolves to where `row` is on screen once a frame draws the view, or `null` when no mark draws + * it in view. A diagram's vertices resolve at once. + */ screenOf(row: Row, wait?: Wait): Promise; /** - * Every row of a mark that picks within `radius` CSS pixels (0 unless given) of `point`, nearest - * first, once a frame draws the view. + * Resolves, once a frame draws the view, to every row of a pickable mark within `options.radius` + * CSS pixels (0 by default) of `point`, nearest first. */ pick( point: Point, @@ -852,15 +1039,16 @@ class View { } & Wait, ): Promise; /** - * The rows joined to `row`, a set per table: a vertex's neighbors across edges and nets, or an - * edge's or a net's vertices. + * The rows connected to `row`, one set per table: a vertex's neighbors across edges and nets, or + * an edge's or a net's vertices. The cost is proportional to the row's degree. */ neighbors(row: Row): Rows[]; /** - * Lays every vertex row without a position out again by the scene's layout: a diagram's blocks, or - * points on a plane or the earth. Places a layout gave otherwise hold, so rows added later are - * placed around the rest. Once the layout lands, answers a patch to each dataset writing each row - * laid out into its mark's position fields; applied, the places are the data's own. + * Runs the scene's layout again for every vertex row without a position: a diagram's blocks, or + * points on a plane or the earth. Otherwise a layout keeps the places it gave, so rows added + * later are placed around the rest. Resolves, once the layout finishes, to one patch per dataset + * that writes each laid-out row's place into its mark's position fields. Applying the patches + * makes the places part of the data. Marks whose position is not read from fields are left out. * * ```ts * for (const patch of await view.arrange()) if (patch.data === grid.name) grid.apply(patch); @@ -868,16 +1056,21 @@ class View { */ arrange(wait?: Wait): Promise; /** - * The version of dataset `name` the view draws: the latest version of a `Data` it took, a snapshot - * given, or what it holds of `Tiles`, the pieces its camera asked for. + * The version of dataset `name` the view draws, or `undefined` if it draws none of that name: the + * latest version of a `Data` the view has taken, a given snapshot, or the pieces of `Tiles` + * fetched for its camera. */ snapshot(name: string): Snapshot | undefined; /** - * Calls `listener` with each event of `type` told while it listens; returns a function that stops - * it. What lasts is state to read, as the property each change is named for. + * Calls `listener` with each event of `type` emitted while it is registered, and returns a + * function that removes it. Past events are not replayed; read the current state from the + * property a change event is named after. */ on(type: T, listener: (event: ViewEvents[T]) => void): () => void; - /** Frees the view, rejecting what it was asked and has not answered; a canvas showing it keeps its last frame. */ + /** + * Frees the view and rejects its unanswered queries. A canvas showing it keeps its last frame. + * Calling it again does nothing; other methods throw afterwards. + */ destroy(): void; [Symbol.dispose](): void; } @@ -886,8 +1079,9 @@ class View { ### ViewOptions ```ts -/** What a view is made with: the sources of the datasets its scene names. */ +/** Options for `Gpu.view()`. */ interface ViewOptions { + /** The datasets the scene draws; see {@link View.sources}. */ sources?: readonly Source[]; } ``` @@ -896,10 +1090,11 @@ interface ViewOptions { ```ts /** - * What a view tells its listeners, by type. Intents (`click`, `open`, `menu`, `drag`, `connect`, - * `delete`) are told once each, in the order the user acted. Changes (`hovered`, `selection`, - * `camera`, `waiting`) are named like the property they changed, and tell only their latest. A view - * tells only what the application did not do itself: its setters change state silently. + * The events a view emits, by type, mapped to each event's detail. Intents (`click`, `open`, + * `menu`, `drag`, `connect`, `delete`) are emitted once each, in the order the user acted. Changes + * (`hovered`, `selection`, `camera`, `waiting`) are named after the property that changed and carry + * only its latest value. An `error` event's detail is a {@link LatkitError}. Setters emit nothing, + * so events report only what the application did not do itself. */ type ViewEvents = { [E in ViewEvent as E['type']]: E extends { @@ -920,14 +1115,17 @@ type Insets = [top: number, right: number, bottom: number, left: number]; ### Point ```ts -/** A point on screen: CSS pixels from the top left. */ +/** A point on screen, in CSS pixels from the view's top left corner. */ type Point = [x: number, y: number]; ``` ### Position ```ts -/** A position in the space's coordinates: x, y, and elevation. */ +/** + * A position in the space's coordinates: x, y, and elevation. On the earth, x and y are longitude + * and latitude; on a plot, the axis coordinate and the value. + */ type Position = [x: number, y: number, elevation: number]; ``` @@ -935,24 +1133,29 @@ type Position = [x: number, y: number, elevation: number]; ```ts /** - * Views shown in a `` element, which sizes itself to its CSS box and passes each view the - * input landing on its panel, or an `OffscreenCanvas` in a worker, which keeps the size it is given. + * Shows views in a `` element or an `OffscreenCanvas`, each in its layout panel; created by + * `Gpu.canvas()`. A frame redraws only the views that need it, or every view after a resize or a + * layout change. */ class Canvas { - /** What it draws into. */ + /** The `` element or `OffscreenCanvas` it draws into. */ get element(): HTMLCanvasElement | OffscreenCanvas; /** - * The views shown and where, a later panel over an earlier one; setting it shows those in place of - * the ones shown. A view shows in one panel of one canvas at a time. + * The views shown and their regions; later panels draw over earlier ones. Setting it replaces the + * shown views. A view can show in only one panel of one canvas at a time. Setting a layout that + * breaks this, or that has a panel covering no pixels, throws an `invalid` error. */ get layout(): readonly Panel[]; set layout(layout: readonly Panel[]); /** - * A picture of exactly what the canvas shows: its views as its layout places them, at its size in - * CSS pixels times `scale`. + * Returns a {@link Shot} of exactly what the canvas shows: its views in its layout, at the + * canvas's CSS size times `options.scale` (1 by default) in pixels. */ shot(options?: ShotOptions): Shot; - /** Stops drawing; the views remain, and the element keeps its last frame. */ + /** + * Stops drawing and frees the canvas. The views stay usable, and the element keeps its last + * frame. Calling it again does nothing. + */ destroy(): void; [Symbol.dispose](): void; } @@ -961,11 +1164,14 @@ class Canvas { ### CanvasOptions ```ts -/** How a canvas shows its views. */ +/** Options for `Gpu.canvas()`. */ interface CanvasOptions { - /** The views shown and where; a later panel draws over an earlier one. */ + /** The views shown and their regions; later panels draw over earlier ones. None by default. */ layout?: readonly Panel[]; - /** Pixels a CSS pixel of an `OffscreenCanvas`; an element follows its device. */ + /** + * Pixels per CSS pixel of an `OffscreenCanvas`, 1 by default; it must be positive. A `` + * element uses the device pixel ratio instead. + */ pixelRatio?: number; } ``` @@ -973,9 +1179,10 @@ interface CanvasOptions { ### Panel ```ts -/** A view's place in a canvas or a picture: where it draws, all of it unless `at` says. */ +/** A view and the region of a canvas or picture it draws in. */ interface Panel { view: View; + /** Where the view draws; the whole canvas or picture by default. */ at?: Rect; } ``` @@ -983,7 +1190,7 @@ interface Panel { ### Rect ```ts -/** `[x, y, width, height]` as fractions of a canvas or a picture. */ +/** A region `[x, y, width, height]` in fractions of a canvas or picture, from its top left. */ type Rect = readonly [x: number, y: number, width: number, height: number]; ``` @@ -995,9 +1202,9 @@ Shots draw views into images and video. ````ts /** - * A picture of views, each drawn into its panel of `size` pixels as `layout` places it: drawn while a - * canvas showing it holds its last frame, each view's data as it was when the picture began. A - * canvas's `shot()` takes one of what it shows. + * A picture of views, `size` pixels in all, each view drawn into its layout panel. While a shot + * draws a view, a canvas showing that view keeps its last frame. An image draws each view's data as + * it was when the picture began. A canvas's `shot()` makes a shot of what the canvas shows. * * ```ts * const png = await (await canvas.shot({ scale: 2 }).image()).encode('png'); @@ -1008,20 +1215,25 @@ class Shot { readonly options: ShotOptions & { layout?: readonly Panel[]; }; - /** A picture `size` pixels wide and high, of the views of `options.layout`. */ + /** Creates a shot of `size`, `[width, height]` in pixels, of the views in `options.layout`. */ constructor( size: readonly [number, number], options?: ShotOptions & { layout?: readonly Panel[]; }, ); - /** The picture, once every view is complete as `capture` says. */ + /** + * Draws the picture once every view is complete, as `capture` says, and resolves to the + * {@link Image}. Rejects with an `invalid` error for invalid options or a view in two panels, and + * with the first error a view reports while drawing. + */ image(wait?: Wait): Promise; /** - * A video of these pictures at `fps` frames a second, H.264 in MP4, written to `output` as it is - * drawn, at `bitrate` bits a second or about 0.15 bits a pixel a frame. Its width rounds down to a - * multiple of 4 and its height of 2, the blocks H.264 takes. `signal` aborts it: the output is - * aborted and the views are given back. + * Starts a video of this shot: H.264 in MP4 at `fps` frames per second, written to `output` as + * frames are drawn. `bitrate` is in bits per second, about 0.15 bits per pixel per frame by + * default. The width is rounded down to a multiple of 4 and the height to a multiple of 2, as the + * encoder requires. Canvases showing the views keep their last frame until the video finishes or + * aborts. Aborting `signal` aborts the video and `output`; frames have no other time limit. */ video({ fps, @@ -1039,19 +1251,25 @@ class Shot { ### ShotOptions ```ts -/** How a picture is taken. */ +/** Options for a {@link Shot}. */ interface ShotOptions { - /** Pixels per CSS pixel: a view lays out `size / scale` CSS pixels, so 2 draws twice as sharp. 1 unless given. */ + /** + * Pixels per CSS pixel, 1 by default. Views lay out at `size / scale` CSS pixels, so 2 draws the + * same layout twice as sharp. + */ scale?: number; - /** Keeps alpha where the themes' backgrounds would be. */ + /** Leaves the background transparent where the theme's background color would be. */ transparent?: boolean; /** - * When the picture is taken: once its views are complete and still, transitions, flights, and - * label fades run on until they land (`still`, the default); or once they are complete, as they - * stand now, what moves held where it is (`now`). + * When the picture is taken. `'still'`, the default, waits until every view is complete and + * still: transitions, camera flights, and label fades run until they finish. `'now'` waits only + * until every view is complete, and freezes anything moving where it is. */ capture?: 'still' | 'now'; - /** A still's effect time in seconds, where elapsed time and flowing comets read it. */ + /** + * The time in seconds that `elapsed()` expressions and flowing comets show in a still picture. By + * default they follow the view's clock. + */ elapsed?: number; } ``` @@ -1059,15 +1277,19 @@ interface ShotOptions { ### Image ```ts -/** RGBA pixels, straight alpha, rows top to bottom. */ +/** A picture's RGBA pixels, with straight alpha, rows top to bottom. */ class Image { + /** Width in pixels. */ get width(): number; + /** Height in pixels. */ get height(): number; - /** The pixels, copied. */ + /** A copy of the pixels: 4 bytes per pixel, `width * height * 4` bytes in all. */ get rgba(): Uint8Array; /** - * The image as a file, written in a worker: PNG, lossless with alpha; JPEG at `quality` from 1 to - * 100 (90 by default), small, its alpha dropped; or WebP, lossless with alpha, smaller than PNG. + * Encodes the image as a file, in a worker. PNG is lossless with alpha. JPEG uses + * `options.quality` from 1 to 100 (90 by default) and drops alpha. WebP is lossless with alpha + * and smaller than PNG. Rejects with an `invalid` error for a quality outside 1 to 100 or an + * unknown format. */ encode( format: ImageFormat, @@ -1075,6 +1297,7 @@ class Image { quality?: number; } & Wait, ): Promise; + /** Frees the pixels. Other methods throw afterwards. */ destroy(): void; [Symbol.dispose](): void; } @@ -1083,29 +1306,53 @@ class Image { ### ImageFormat ```ts -/** How an image is written as a file. */ +/** An image file format for {@link Image.encode}. */ type ImageFormat = 'png' | 'jpeg' | 'webp'; ``` ### Video ```ts -/** A video being drawn, encoded, and written. */ +/** + * A video being drawn, encoded, and written; created by {@link Shot.video}. Draw frames with + * {@link Video.frame} or {@link Video.record}, then call {@link Video.finish}. + */ class Video { - /** Seconds into the video the next frame shows at; kept once it is done. */ + /** The time in seconds at which the next frame shows. Still readable after the video ends. */ get time(): number; - /** Frames drawn or held so far; kept once it is done. */ + /** + * Frames recorded so far, including frames repeated because nothing moved. Still readable after + * the video ends. + */ get frames(): number; - /** Draws and writes one frame, waiting for the output's backpressure. One frame draws at a time. */ + /** + * Draws the next frame once its views are complete, and writes its bytes, waiting on the + * output's backpressure. Calls must not overlap: a call made before the previous one resolves, + * or after `finish()`, rejects with an `invalid` error, and a call after `abort()` with a + * `cancelled` error. A failure aborts the video. + */ frame(): Promise; - /** Records until `step(time)` returns false; any step, drawing, output, or progress error aborts it. */ + /** + * Records frames until `step(time)` returns false, then finishes the video. `step` gets the next + * frame's time in seconds and can change the views before that frame is drawn. `onProgress` is + * called after each frame. An error from `step`, drawing, the output, or `onProgress` aborts the + * video and is rethrown. + */ record( step: (time: number) => boolean | Promise, onProgress?: (progress: { frames: number; time: number }) => void, ): Promise; - /** Finishes pending work, writes the end of the file, and closes and unlocks the output. Idempotent. */ + /** + * Waits for the frame in progress, encodes the remaining frames, writes the end of the file, and + * closes and unlocks the output. Later calls return the same promise. Rejects with a `cancelled` + * error if the video was aborted. + */ finish(): Promise; - /** Cancels drawing and encoding, aborts the output, and unlocks it once pending writes settle. Idempotent. */ + /** + * Cancels drawing and encoding, aborts the output with `reason`, and unlocks it once pending + * writes settle. Later calls return the same promise. After `finish()` completes, it does + * nothing. + */ abort(reason?: unknown): Promise; } ``` diff --git a/docs/reference/cli.md b/docs/reference/cli.md index ee0ab95d..afc203b5 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -11,16 +11,16 @@ Usage: latkit [OPTIONS] Commands: render Draw scenes of datasets into an image, image sequence, raw frames, or a video - validate Check everything render reads, frames included, without a GPU - inspect Describe datasets as JSON: tables, rows, fields and their kinds, the axis and its span - schema Print the JSON Schema of what latkit reads: every input, or one of them - query Read rows back as JSON Lines, an object a row: those a query keeps, sorted and paged - tile Write a dataset as time tiles: frames.json, which --tiles opens, the dataset without its sampled fields' histories, and their spans at every level of detail + validate Check every input render reads, including frames, without a GPU + inspect Describe datasets as JSON: tables, row counts, fields with kinds, and the axis and its span + schema Print the JSON Schema of every input latkit reads, or of one input + query Print the rows a query selects, sorted and paged, as JSON Lines with one object per row + tile Write a dataset as time tiles: frames.json (which --tiles opens), the dataset without its sampled fields' histories, and those histories in spans at every level of detail help Print this message or the help of the given subcommand(s) Options: --diagnostics - Diagnostics on stderr: errors, and progress while rendering; JSON is one object a line + Format of errors and render progress on stderr. JSON writes one object per line [default: human] [possible values: human, json] @@ -41,37 +41,37 @@ Usage: latkit render [OPTIONS] ... Arguments: ... - Scene JSON files, one per panel; - reads stdin + Scene JSON files, one per panel (- reads stdin) Options: -d, --data <[NAME=]FILE> - A dataset: JSON document, latkit bytes, or GeoJSON (which needs a NAME); NAME= renames it + A dataset file: a JSON document, latkit bytes, or GeoJSON. NAME= sets its name, which GeoJSON requires --diagnostics - Diagnostics on stderr: errors, and progress while rendering; JSON is one object a line + Format of errors and render progress on stderr. JSON writes one object per line [default: human] [possible values: human, json] --tiles - Tiles every view fetches as dataset NAME: time tiles' frames.json, or vector tiles' PMTiles file, TileJSON, or {z}/{x}/{y} template + Tiles every view fetches as dataset NAME: a time tiles frames.json, or a vector tiles PMTiles file, TileJSON, or {z}/{x}/{y} template -o, --output - Image file, %d pattern for an image sequence, or - for stdout + Output file, %d pattern for an image sequence, or - for stdout --format - What to write; otherwise the output's extension says, or PNG for stdout + Output format. Defaults to the one the output's extension names, else PNG Possible values: - - png: PNG, lossless with alpha: one picture, or one a frame where the output has a %d pattern - - jpeg: JPEG at --quality, its alpha dropped: one picture, or one a frame - - webp: WebP, lossless with alpha: one picture, or one a frame - - rgba: Raw RGBA, straight alpha, frames back to back - - y4m: YUV4MPEG2: 4:2:0 BT.709 limited range, for any encoder + - png: PNG, lossless with alpha. One picture, or one per frame with a %d output pattern + - jpeg: JPEG at --quality, without alpha. One picture, or one per frame + - webp: Lossless WebP with alpha. One picture, or one per frame + - rgba: Raw RGBA with straight alpha, frames back to back + - y4m: YUV4MPEG2 (4:2:0, BT.709 limited range), which any encoder reads - mp4: H.264 in fragmented MP4, from the platform's encoder --quality - A JPEG's quality, from 1, smallest, to 100, sharpest + JPEG quality, from 1 (smallest) to 100 (sharpest) [default: 90] @@ -81,51 +81,51 @@ Options: [default: 1600x900] --scale - Pixels per CSS pixel: views lay out size / scale CSS pixels, so 2 draws twice as sharp + Pixels per CSS pixel. Views lay out size / scale CSS pixels, so 2 draws twice as sharp [default: 1] --panel - A scene's panel of the picture, X,Y,WIDTH,HEIGHT as fractions; one per scene, in order + A scene's panel of the picture, as X,Y,WIDTH,HEIGHT fractions. Give one per scene, in order --transparent Keep alpha where the themes' backgrounds would be --deadline - The longest a picture, or a video frame, may take, in seconds + The longest a picture or video frame may take, in seconds [default: 30] --font - TTF/OTF/TTC file; repeat for fallbacks; replaces system fonts + A TTF, OTF, or TTC font file. Repeat for fallbacks. Replaces the system fonts --camera - Camera JSON for a scene, in scene order; a view without one fits + A camera JSON file per scene, in scene order. Views without one use --fit --fit - What a view without a camera frames: "all", or a fit as JSON + What views without a camera frame: "all", or a fit as JSON [default: all] --insets - CSS pixels kept clear at the top, right, bottom, and left, T,R,B,L + CSS pixels to keep clear at the top, right, bottom, and left --at - The axis coordinate shown; sampled fields read their last frame at or before it + The axis coordinate to show. Sampled fields show their last frame at or before it --elapsed - A still's effect time in seconds, where elapsed time and flowing comets read it + A still's fixed effect time in seconds, for elapsed-time expressions and flowing comets --frames - Frames as JSON Lines, one per output frame, or - for stdin + Frames as JSON Lines, one line per output frame (- reads stdin) --fps - Output frames a second + Output frames per second [default: 60] --bitrate - MP4 bits a second; about 0.15 bits a pixel a frame otherwise + MP4 bitrate in bits per second. Defaults to about 0.15 bits per pixel per frame -h, --help Print help (see a summary with '-h') @@ -134,43 +134,43 @@ Options: ## validate ```text -Check everything render reads, frames included, without a GPU +Check every input render reads, including frames, without a GPU Usage: latkit validate [OPTIONS] ... Arguments: ... - Scene JSON files, one per panel; - reads stdin + Scene JSON files, one per panel (- reads stdin) Options: -d, --data <[NAME=]FILE> - A dataset: JSON document, latkit bytes, or GeoJSON (which needs a NAME); NAME= renames it + A dataset file: a JSON document, latkit bytes, or GeoJSON. NAME= sets its name, which GeoJSON requires --diagnostics - Diagnostics on stderr: errors, and progress while rendering; JSON is one object a line + Format of errors and render progress on stderr. JSON writes one object per line [default: human] [possible values: human, json] --tiles - Tiles every view fetches as dataset NAME: time tiles' frames.json, or vector tiles' PMTiles file, TileJSON, or {z}/{x}/{y} template + Tiles every view fetches as dataset NAME: a time tiles frames.json, or a vector tiles PMTiles file, TileJSON, or {z}/{x}/{y} template -o, --output - Image file, %d pattern for an image sequence, or - for stdout + Output file, %d pattern for an image sequence, or - for stdout --format - What to write; otherwise the output's extension says, or PNG for stdout + Output format. Defaults to the one the output's extension names, else PNG Possible values: - - png: PNG, lossless with alpha: one picture, or one a frame where the output has a %d pattern - - jpeg: JPEG at --quality, its alpha dropped: one picture, or one a frame - - webp: WebP, lossless with alpha: one picture, or one a frame - - rgba: Raw RGBA, straight alpha, frames back to back - - y4m: YUV4MPEG2: 4:2:0 BT.709 limited range, for any encoder + - png: PNG, lossless with alpha. One picture, or one per frame with a %d output pattern + - jpeg: JPEG at --quality, without alpha. One picture, or one per frame + - webp: Lossless WebP with alpha. One picture, or one per frame + - rgba: Raw RGBA with straight alpha, frames back to back + - y4m: YUV4MPEG2 (4:2:0, BT.709 limited range), which any encoder reads - mp4: H.264 in fragmented MP4, from the platform's encoder --quality - A JPEG's quality, from 1, smallest, to 100, sharpest + JPEG quality, from 1 (smallest) to 100 (sharpest) [default: 90] @@ -180,51 +180,51 @@ Options: [default: 1600x900] --scale - Pixels per CSS pixel: views lay out size / scale CSS pixels, so 2 draws twice as sharp + Pixels per CSS pixel. Views lay out size / scale CSS pixels, so 2 draws twice as sharp [default: 1] --panel - A scene's panel of the picture, X,Y,WIDTH,HEIGHT as fractions; one per scene, in order + A scene's panel of the picture, as X,Y,WIDTH,HEIGHT fractions. Give one per scene, in order --transparent Keep alpha where the themes' backgrounds would be --deadline - The longest a picture, or a video frame, may take, in seconds + The longest a picture or video frame may take, in seconds [default: 30] --font - TTF/OTF/TTC file; repeat for fallbacks; replaces system fonts + A TTF, OTF, or TTC font file. Repeat for fallbacks. Replaces the system fonts --camera - Camera JSON for a scene, in scene order; a view without one fits + A camera JSON file per scene, in scene order. Views without one use --fit --fit - What a view without a camera frames: "all", or a fit as JSON + What views without a camera frame: "all", or a fit as JSON [default: all] --insets - CSS pixels kept clear at the top, right, bottom, and left, T,R,B,L + CSS pixels to keep clear at the top, right, bottom, and left --at - The axis coordinate shown; sampled fields read their last frame at or before it + The axis coordinate to show. Sampled fields show their last frame at or before it --elapsed - A still's effect time in seconds, where elapsed time and flowing comets read it + A still's fixed effect time in seconds, for elapsed-time expressions and flowing comets --frames - Frames as JSON Lines, one per output frame, or - for stdin + Frames as JSON Lines, one line per output frame (- reads stdin) --fps - Output frames a second + Output frames per second [default: 60] --bitrate - MP4 bits a second; about 0.15 bits a pixel a frame otherwise + MP4 bitrate in bits per second. Defaults to about 0.15 bits per pixel per frame -h, --help Print help (see a summary with '-h') @@ -233,13 +233,13 @@ Options: ## inspect ```text -Describe datasets as JSON: tables, rows, fields and their kinds, the axis and its span +Describe datasets as JSON: tables, row counts, fields with kinds, and the axis and its span Usage: latkit inspect [OPTIONS] <[NAME=]FILE>... Arguments: <[NAME=]FILE>... - Dataset files, each [NAME=]FILE; - reads stdin + Dataset files, each [NAME=]FILE (- reads stdin) Options: --deadline @@ -248,7 +248,7 @@ Options: [default: 30] --diagnostics - Diagnostics on stderr: errors, and progress while rendering; JSON is one object a line + Format of errors and render progress on stderr. JSON writes one object per line [default: human] [possible values: human, json] @@ -260,26 +260,26 @@ Options: ## schema ```text -Print the JSON Schema of what latkit reads: every input, or one of them +Print the JSON Schema of every input latkit reads, or of one input Usage: latkit schema [OPTIONS] [INPUT] Arguments: [INPUT] - One input's schema + Print only this input's schema Possible values: - scene: A scene - data: A dataset's JSON document - - patch: A patch's JSON - - query: A query of `latkit query` - - camera: A camera of `--camera` - - fit: A fit of `--fit` - - frames: A line of `--frames` + - patch: A patch + - query: A query for `latkit query` + - camera: A camera for `--camera` + - fit: A fit for `--fit` + - frames: One line of a `--frames` file Options: --diagnostics - Diagnostics on stderr: errors, and progress while rendering; JSON is one object a line + Format of errors and render progress on stderr. JSON writes one object per line [default: human] [possible values: human, json] @@ -291,16 +291,16 @@ Options: ## query ```text -Read rows back as JSON Lines, an object a row: those a query keeps, sorted and paged +Print the rows a query selects, sorted and paged, as JSON Lines with one object per row Usage: latkit query [OPTIONS] <[NAME=]FILE> Arguments: <[NAME=]FILE> - The dataset, [NAME=]FILE; - reads stdin + The dataset, [NAME=]FILE (- reads stdin) - The query: JSON such as {"of":"Branch","filter":{"op":"gt","args":["loading",90]}}, or a file of it + The query as JSON, such as {"of":"Branch","filter":{"op":"gt","args":["loading",90]}}, or a file containing it (- reads stdin) Options: --deadline @@ -309,7 +309,7 @@ Options: [default: 30] --diagnostics - Diagnostics on stderr: errors, and progress while rendering; JSON is one object a line + Format of errors and render progress on stderr. JSON writes one object per line [default: human] [possible values: human, json] @@ -321,26 +321,26 @@ Options: ## tile ```text -Write a dataset as time tiles: frames.json, which --tiles opens, the dataset without its sampled fields' histories, and their spans at every level of detail +Write a dataset as time tiles: frames.json (which --tiles opens), the dataset without its sampled fields' histories, and those histories in spans at every level of detail Usage: latkit tile [OPTIONS] --output <[NAME=]FILE> Arguments: <[NAME=]FILE> - The dataset, [NAME=]FILE; - reads stdin + The dataset, [NAME=]FILE (- reads stdin) Options: --diagnostics - Diagnostics on stderr: errors, and progress while rendering; JSON is one object a line + Format of errors and render progress on stderr. JSON writes one object per line [default: human] [possible values: human, json] -o, --output - The directory the tiles go in + Directory to write the tiles to --chunk - Entries a span holds: frames, or buckets of a level of detail; a multiple of 8 + Entries per span: frames at full detail, buckets at coarser levels. A multiple of 8 [default: 256] diff --git a/docs/reference/types.md b/docs/reference/types.md index 29ad560b..0db590c7 100644 --- a/docs/reference/types.md +++ b/docs/reference/types.md @@ -4,32 +4,37 @@ Generated from the same schema as the package types. Each field documents its me ## Scenes -A scene: its space, how it answers input, and how it is laid out. +A scene: its space, how it responds to input, and how it is laid out. ### Scene ```ts /** - * What a view draws and how it answers input: a space, marks in drawing order, a layout, a theme, - * controls, how long changes ease, and the title written over it. Plain data: as JSON, each field - * by its name in camelCase, each mark `{ vertices | edges | faces | lines | areas: options }`. + * A view's content and behavior: a space, marks drawn in order, a layout, a theme, controls, an + * ease duration, and an optional title. + * + * A scene is plain data. In JSON, fields are camelCase and each mark is + * `{ vertices | edges | faces | lines | areas: options }`. */ interface Scene { + /** The coordinate space positions are in. */ space: Space; - /** What the view draws, in drawing order. */ + /** The marks, in drawing order: each draws over the marks before it. */ marks?: Mark[]; /** - * How the view places vertex rows without positions: a diagram's blocks, or points on a plane or - * the earth, gaps in the space's units. + * How the view places vertex rows that have no position: a diagram's blocks, or points on a plane + * or the earth. `none` by default. */ layout?: Layout; + /** Default colors and sizes, and the look of text, focus, and each space; `dark` by default. */ theme?: Theme; + /** How the view responds to the pointer and keyboard. */ controls?: Controls; - /** Seconds that changes ease over; 0.3 unless set. */ + /** Seconds over which scene and data changes animate; 0.3 by default, 0 for none. */ ease?: number; - /** Written at the view's top left, larger than the theme's text. */ + /** A title drawn at the view's top left, larger than the theme's text. */ title?: string | null; - /** Written under the title. */ + /** A subtitle drawn under the title. */ subtitle?: string | null; } ``` @@ -38,10 +43,11 @@ interface Scene { ```ts /** - * The coordinate space a scene's positions are in: a name, or - * `{ plane | diagram | earth | plot: options }`. Its own unit is what lengths are in unless they - * say theirs: units in a diagram, a flat sheet seen from straight above with y running down, a - * unit a CSS pixel at zoom 0; CSS pixels elsewhere. Only a diagram has ports, groups, nets, and + * The coordinate space of a scene's positions: a name, or + * `{ plane | diagram | earth | plot: options }`. + * + * The space sets the unit of lengths that do not name their own: units in a diagram, where a unit + * is a CSS pixel at zoom 0, and CSS pixels elsewhere. Only a diagram has ports, groups, nets, and * orthogonal routes. */ type Space = @@ -54,15 +60,15 @@ type Space = earth: Earth; } | { - /** Plane units with elevation: graphs and scenes the camera turns and tilts around. */ + /** Plane units, with elevation; the camera can turn and tilt. */ plane: Plane; } | { - /** The data's axis across, values up. */ + /** The data's axis horizontally and values vertically. */ plot: Plot; } | { - /** A sheet for diagrams: x right and y down in units, every length a unit. */ + /** A flat sheet for diagrams: x right and y down, every length in units. */ diagram: Diagram; }; ``` @@ -70,9 +76,9 @@ type Space = ### Plane ```ts -/** A plane: positions are plane units. */ +/** A plane: positions are in plane units. */ interface Plane { - /** The grid under the marks. */ + /** The grid drawn under the marks; lines 10 units apart by default. */ grid?: Grid; } ``` @@ -81,21 +87,27 @@ interface Plane { ```ts /** - * The earth: positions are longitude and latitude in degrees, its ground in the theme's earth + * The earth: positions are longitude and latitude in degrees. The ground takes the theme's `earth` * colors. */ interface Earth { - /** A flat map or a globe; a flat map unless set. */ + /** A flat web mercator map (`mercator`, the default) or a globe (`globe`). */ projection?: Projection; - /** Whether lines of longitude and latitude cross the ground, in the theme's grid color. */ + /** + * Whether lines of longitude and latitude cross the ground, in the theme's `grid` color. Default + * `true`. + */ graticule?: boolean; /** - * Lit by the sun as it stands this many seconds after the Unix epoch, plus the view's axis - * coordinate when it shows one: night falls on the ground and what stands on it, down to the - * theme's night. Lit evenly unless set. + * Lights the earth by the sun's position this many seconds after the Unix epoch, plus the view's + * axis coordinate when it has one. The night side darkens the ground and everything on it, down to + * the theme's `earth.night` brightness. Unset, the earth is lit evenly. */ daylight?: number | null; - /** Whether air glows around the globe, in the theme's atmosphere color, brighter where it is day. */ + /** + * Whether air glows around the globe, in the theme's `earth.atmosphere` color, brighter on the day + * side. Default `false`. + */ atmosphere?: boolean; } ``` @@ -103,7 +115,7 @@ interface Earth { ### Projection ```ts -/** How the earth is drawn. */ +/** How the earth is projected. */ type Projection = 'mercator' | 'globe'; ``` @@ -111,14 +123,15 @@ type Projection = 'mercator' | 'globe'; ```ts /** - * A sheet for diagrams, flat and seen from straight above: the camera pans and zooms. Positions - * are units with y down, and lengths are units, a unit a CSS pixel at zoom 0 (sizes, padding, - * ports, strokes, text, and the theme's), but for what says `px`, a halo, focus's glow, and the - * pick radius. Zooming scales one fixed picture. Only a diagram has ports, groups, nets, and - * orthogonal routes; it lays out blocks. + * A flat sheet for diagrams, seen from straight above; the camera pans and zooms but does not + * turn. Positions are in units with y down. Lengths are in units too, a unit being a CSS pixel at + * zoom 0: sizes, padding, ports, strokes, text, and the theme's lengths. Sizes given with `px`, + * the text halo, the focus glow, and the pick radius stay in CSS pixels. Zooming scales the whole + * picture. Only a diagram has ports, groups, nets, and orthogonal routes, and the scene's layout + * places its blocks. */ interface Diagram { - /** The grid under the marks. */ + /** The grid drawn under the marks; dots 20 units apart by default. */ grid?: Grid; } ``` @@ -142,20 +155,20 @@ interface Plot { interface Axis { /** The axis title. */ label?: string | null; - /** Whether grid lines cross the plot at the ticks. */ + /** Whether grid lines cross the plot at the ticks. Default `true`. */ grid?: boolean; - /** Whether the axis is hidden. */ + /** Whether the axis is hidden. Default `false`. */ hidden?: boolean; /** - * Ticks at these values, each a value or `[value, label]`, their values written unless labeled; - * round values unless set. + * Tick positions, each a value or `[value, label]`; a tick without a label shows its value. Unset, + * ticks fall at round values. */ ticks?: Tick[] | null; - /** How tick values are written. */ + /** How tick values are formatted; `auto` by default. */ format?: Format; /** - * Decimals of fixed values, or of the significand of scientific and engineering ones; as many as - * the ticks' step needs unless set. + * Decimal places of fixed values, or of the significand in scientific and engineering formats, at + * most 15. Unset, as many as the tick step needs. */ precision?: number | null; } @@ -164,14 +177,14 @@ interface Axis { ### Tick ```ts -/** A tick as written: a value, or `[value, label]`. */ +/** A tick in JSON: a value, or `[value, label]`. */ type Tick = number | [number, string | null]; ``` ### Format ```ts -/** How an axis writes its tick values. */ +/** How an axis formats its tick values. */ type Format = 'auto' | 'fixed' | 'scientific' | 'engineering'; ``` @@ -182,18 +195,18 @@ type Format = 'auto' | 'fixed' | 'scientific' | 'engineering'; type Grid = | 'off' | { - /** Lines `pitch` units apart, thinned by tens as the view zooms out. */ + /** Lines `pitch` units apart, thinned by factors of ten as the view zooms out. */ lines: { /** Units between lines. */ pitch: number; }; } | { - /** Dots `pitch` units apart; drags snap to them when `snap`. */ + /** Dots `pitch` units apart; with `snap`, a diagram's drags snap to them. */ dots: { /** Units between dots. */ pitch: number; - /** Whether moved vertices snap to the dots. */ + /** Whether drag positions snap to the dots, in a diagram. Default `false`. */ snap?: boolean; }; }; @@ -203,61 +216,71 @@ type Grid = ```ts /** - * How a view answers the pointer and keyboard, and how much it moves on its own: a preset, or - * options over the defaults. Each mark says whether its rows pick, select, and drag; these say - * what the view itself does. + * How a view responds to the pointer and keyboard, and how much it animates on its own: a preset + * (`"none"` or `"inspect"`), or options over the defaults. Each mark controls whether its own rows + * pick, select, and drag; these options control the view itself. */ type Controls = | 'none' | 'inspect' | { - /** Dragging the background moves the camera. */ + /** Whether dragging the background pans the camera. Default `true`. */ pan?: boolean; /** - * What zooms: the wheel and pinching, or the wheel only with Control or Meta held (as a trackpad's - * pinch sends it), leaving the plain wheel to scroll the page; or nothing. + * Which input zooms the view: the wheel and pinching (`wheel`, the default); pinching, and the + * wheel only with Control or Meta held, as trackpad pinches send it, so the plain wheel scrolls + * the page (`modifier`); or nothing (`off`). */ zoom?: Zoom; - /** Dragging with the secondary button, or with Shift, turns and tilts a map or plane. */ + /** + * Whether dragging with the secondary button, or with Shift, rotates and tilts a map or plane. + * Default `true`. + */ turn?: boolean; /** - * Tab steps through the rows nearest the view's middle and arrows to the nearest row their way; - * Enter opens the row stepped to and Space selects it. Otherwise arrows pan, `+` and `-` zoom, - * Home fits, and Escape clears the selection. + * Whether the keyboard controls the view. Default `true`. Tab and Shift+Tab step through the rows + * nearest the view's center, and arrow keys step to the nearest row in their direction. Enter + * opens the stepped-to row and Space selects it. With no row stepped to, arrow keys pan + * (Shift+arrows turn), `+` and `-` zoom, Home fits everything, and Escape clears the selection. */ keyboard?: boolean; /** - * The row the user points at glows. It is hovered either way: events tell it, and `hovered` reads - * it. + * Whether the hovered row glows, and a hovered vertex sized in CSS pixels grows. Default `true`. + * Rows are hovered either way: a `'hovered'` event still fires and `hovered` still reads them. */ hover?: boolean; - /** CSS pixels around the pointer that picks reach; touch reaches 8 more. */ + /** + * How far from the pointer, in CSS pixels, a row can be and still be picked; touch adds 8. Default + * 6. + */ pickRadius?: number; /** - * Dragging with Shift selects the rows in a box, instead of turning; a Shift click still toggles - * its row. + * Whether Shift+drag selects the rows inside a box instead of turning the camera. Shift+click + * still toggles one row. Default `false`. */ selectBox?: boolean; /** - * Dragging from a port, from a vertex with Alt, or an edge off its end draws a connection, told as - * a `'connect'` event. + * Whether the user can draw connections, reported as a `'connect'` event: by dragging from a port, + * from a vertex with Alt held, or an edge's end off its vertex. Default `false`. */ connect?: boolean; - /** Delete and Backspace tell a `'delete'` event with the selection. */ + /** Whether Delete and Backspace report the selection as a `'delete'` event. Default `false`. */ delete?: boolean; /** - * Clicking again where nothing moved selects the next row under the point, nearest first, so rows - * drawn over one another can each be selected. + * Whether clicking again at the same point, with the camera unmoved, selects the next row under + * it, nearest first, so overlapping rows can each be selected. Default `false`. */ cycle?: boolean; /** - * Seconds a touch or a pen held still asks for a menu, as the secondary button does; zero never - * does. + * Seconds a touch or pen must hold still to request a menu (a `'menu'` event), as the secondary + * button does; 0 turns this off. Default 0.5. */ press?: number; /** - * How much the view moves on its own: eases, flights, coasting, orbits, label fades, comets, and - * what reads `elapsed`. + * How much the view animates on its own: eases, camera flights, coasting, orbits, label fades, + * comets, and options that read `elapsed`. `system` (the default) follows the user's + * reduced-motion setting, `full` animates everything, and `reduced` animates only motion the user + * drives. */ motion?: Motion; }; @@ -266,14 +289,14 @@ type Controls = ### Motion ```ts -/** How much a view moves on its own. */ +/** How much a view animates on its own. */ type Motion = 'system' | 'full' | 'reduced'; ``` ### Zoom ```ts -/** What zooms a view. */ +/** Which input zooms a view. */ type Zoom = 'wheel' | 'modifier' | 'off'; ``` @@ -281,17 +304,20 @@ type Zoom = 'wheel' | 'modifier' | 'off'; ```ts /** - * How the view places vertex rows without positions: a diagram's blocks, sized, or the points of a - * plane or the earth, among the rows linked to them, gaps in the space's units: a name, or - * `{ stress | layered: options }`. A place, once given, holds: rows added later are placed around - * the rest, and `view.arrange` lays every row out again. + * How the view places vertex rows that have no position: a name, or + * `{ stress | layered: options }`. In a diagram it places blocks; on a plane or the earth it + * places points among the rows they link to. Gaps are in position units: diagram units, plane + * units, or degrees on the earth. + * + * A row keeps its place once placed: rows added later are placed around the others, and + * `view.arrange` lays out every row again. */ type Layout = | 'none' | 'stress' | 'layered' | { - /** Distances on the drawing follow distances in the graph. */ + /** Drawn distances follow graph distances. */ stress: Stress; } | { @@ -303,13 +329,16 @@ type Layout = ### Layered ```ts -/** Layered drawing: vertices in ranks, edges pointing along `flow`, and nets from their outputs. */ +/** + * Layered drawing: vertices in ranks, edges pointing along `flow`, and nets ranked from their + * outputs. + */ interface Layered { - /** Where edges point. */ + /** The direction edges point; `right` by default. */ flow?: Direction; - /** Units between vertices of a rank. */ + /** Distance between vertices within a rank, in position units. Default 30. */ gap?: number; - /** Units between ranks. */ + /** Distance between ranks, in position units. Default 80. */ rankGap?: number; } ``` @@ -318,11 +347,11 @@ interface Layered { ```ts /** - * Stress majorization: each pair of vertices pulls toward its graph distance; then their boxes - * part, keeping a quarter of the gap clear. + * Stress majorization: vertices move until drawn distances match graph distances, then overlapping + * boxes separate, keeping a quarter of `gap` between them. */ interface Stress { - /** Units between adjacent vertices' centers. */ + /** Distance between the centers of linked vertices, in position units. Default 40. */ gap?: number; } ``` @@ -337,7 +366,7 @@ type Direction = 'right' | 'left' | 'down' | 'up'; ### Order ```ts -/** Which way a sort runs; rows a key reads nothing for come last either way. */ +/** Sort direction. Rows where the key has no value sort last in either direction. */ type Order = 'ascending' | 'descending'; ``` @@ -347,14 +376,14 @@ type Order = 'ascending' | 'descending'; /** How much GPU memory latkit uses. */ interface Budget { /** - * Bytes the data every view of the `Gpu` draws may take on the GPU: its pages, and each view's - * rows. A view draws a table too large for its share through the tiles the camera asks for, so - * what it draws always fits; pages no view draws stay cached in the room left, the least recently - * drawn going first. By default half the device's largest storage buffer. The pool of pages grows - * as data needs it, and never shrinks: set the budget before data draws. + * GPU memory in bytes for the data all views of the `Gpu` draw: its pages and each view's per-row + * state. A view draws a table too large for its share as a paged table, drawing only the pages the + * camera needs, so it always fits. Pages no view draws stay cached in the remaining room, least + * recently drawn evicted first. Defaults to half the device's largest storage buffer. The page + * pool grows as data needs it and never shrinks, so set the budget before drawing data. */ bytes: number; - /** Bytes uploaded per frame at most; larger data streams in over frames. */ + /** Most bytes uploaded per frame; larger data streams in over several frames. Defaults to 32 MiB. */ uploadPerFrame: number; } ``` @@ -367,37 +396,39 @@ How a scene looks, a group for each kind of thing it draws, and the lengths its ```ts /** - * Colors and sizes marks take where their options read nothing, and the look of text, focus, - * motion, and each space, grouped by what they style: a base theme's name, or options over one - * (`base`, dark unless set), each group's over the base's. Lengths are in the space's unit, units - * in a diagram and CSS pixels elsewhere, but for a text's halo and focus's glow, CSS pixels - * everywhere. + * Default colors and sizes for marks whose options leave them unset or missing, and the look of + * text, focus, comets, and each space, grouped by subject. Write a base theme's name (`"dark"` or + * `"light"`), or options over a base (`base`, dark by default); a group's options override only + * the base values they set. + * + * Lengths are in the space's unit (units in a diagram, CSS pixels elsewhere), except the text halo + * and the focus glow, which are always in CSS pixels. */ type Theme = | 'dark' | 'light' | { - /** Behind everything. */ + /** The background color. */ background?: Color; - /** A plane's or a diagram's grid, and the earth's graticule. */ + /** The color of a plane's or diagram's grid, a plot's grid lines, and the earth's graticule. */ grid?: Color; - /** What a missing color reads as. */ + /** The color a colormap gives a missing value, such as a row whose field has none. */ missing?: Color; - /** Length of a dash. */ + /** The dash period: one dash and one gap of equal length. */ dash?: number; - /** How much a face turned away from the camera darkens, 0 to 1. */ + /** How much faces darken as they turn away from the camera, from 0 to 1. */ shading?: number; - /** Labels, axis ticks, and titles. */ + /** How labels, axis ticks, and titles look. */ text?: TextTheme; - /** Vertices where their options say nothing. */ + /** Defaults for vertices. */ vertex?: VertexTheme; - /** Edges where their options say nothing, their lanes, and their arrowheads. */ + /** Defaults for edges, and how parallel edges and arrowheads look. */ edge?: EdgeTheme; - /** Lines where their options say nothing. */ + /** Defaults for lines. */ line?: LineTheme; - /** Areas where their options say nothing. */ + /** Defaults for areas. */ area?: AreaTheme; - /** How hovered and selected rows glow, and rows outside a selection dim. */ + /** How hovered and selected rows glow, and how rows outside a selection dim. */ focus?: FocusTheme; /** How comets flowing along edges and lines look. */ flow?: FlowTheme; @@ -415,15 +446,15 @@ type Theme = ### TextTheme ```ts -/** Labels, axis ticks, and titles. */ +/** How labels, axis ticks, and titles look. */ interface TextTheme { - /** Text's color. */ + /** The text color. */ color?: Color; - /** The CSS font family text is set in. */ + /** The CSS font family list. */ font?: string; - /** Text's size. */ + /** The font size, in the space's unit. */ size?: number; - /** CSS pixels of background around label text. */ + /** Width of the background-colored halo around label text, in CSS pixels. */ halo?: number; } ``` @@ -431,11 +462,11 @@ interface TextTheme { ### VertexTheme ```ts -/** Vertices where their options say nothing. */ +/** Defaults for vertices. */ interface VertexTheme { - /** A vertex's color. */ + /** The vertex color. */ color?: Color; - /** A vertex's radius. */ + /** The vertex radius, in the space's unit. */ radius?: number; } ``` @@ -443,27 +474,27 @@ interface VertexTheme { ### EdgeTheme ```ts -/** Edges where their options say nothing, their lanes, and their arrowheads. */ +/** Defaults for edges, and how parallel edges and arrowheads look. */ interface EdgeTheme { /** - * An edge's color; `null` blends the colors of each edge's two vertices, and in a diagram takes - * the text color. + * The edge color. `null` blends the colors of each edge's two vertices; a diagram's wires and + * edges without `ends` (nets and poles) then take the text color. */ color?: Color | null; - /** An edge's width. */ + /** The edge width, in the space's unit. */ width?: number; /** - * In a diagram, distance between the lanes of edges joining the same two vertices, and between - * parallel orthogonal wires, which keep twice it apart. + * In a diagram, the distance between edges joining the same two vertices and between parallel + * orthogonal wires. Wires keep twice this distance from blocks where they can. */ spacing?: number; /** - * Elsewhere, how far edges joining the same two vertices bow apart: each lane by this fraction of - * the edge's length at its middle, as wide as eight lanes at most, more packing closer. A bow is - * part of the drawing, so zooming never moves it. + * Outside a diagram, how far edges joining the same two vertices bow apart: each lane by this + * fraction of the edge's length at its middle. A family spreads at most eight lanes wide, and more + * lanes pack closer. A bow is part of the drawing, so it scales with the zoom. */ bow?: number; - /** How far an arrowhead reaches past its edge's width. */ + /** How far an arrowhead extends beyond the edge's width: a head is this plus the width long. */ arrow?: number; } ``` @@ -471,11 +502,11 @@ interface EdgeTheme { ### LineTheme ```ts -/** Lines where their options say nothing. */ +/** Defaults for lines. */ interface LineTheme { - /** A line's color. */ + /** The line color. */ color?: Color; - /** A line's width. */ + /** The line width, in the space's unit. */ width?: number; } ``` @@ -483,9 +514,9 @@ interface LineTheme { ### AreaTheme ```ts -/** Areas where their options say nothing. */ +/** Defaults for areas. */ interface AreaTheme { - /** An area's color. */ + /** The area color. */ color?: Color; } ``` @@ -494,20 +525,25 @@ interface AreaTheme { ```ts /** - * How comets flowing along edges and lines look: each a round head, brightest at its middle, - * trailing a tail that thins and fades. Their spacing and length are in one unit, and each row's - * flow is in it too: in position units, degrees on the earth, comets are part of the drawing, - * zooming with the view and staying where they are on their lines; otherwise they keep their size - * and speed as it zooms. + * How comets flowing along edges and lines look: a round head, brightest at its middle, trailing a + * tail that thins and fades. + * + * `spacing` and `length` must be in the same unit, which also measures each row's `flow` speed. In + * position units (degrees on the earth), comets are part of the drawing: they scale with the zoom + * and stay in place along their lines. In CSS pixels, they keep their on-screen size and speed. A + * plain number is in the space's unit. */ interface FlowTheme { - /** A comet's color; its alpha is how strongly a comet covers its stroke. */ + /** + * The comet color. Its alpha sets how strongly a comet covers its stroke; a fully transparent + * color turns comets off. + */ color?: Color; - /** Distance between comets. */ + /** The distance between consecutive comets. */ spacing?: Length; - /** How far a comet's tail trails its head. */ + /** The length of a comet's tail behind its head. */ length?: Length; - /** How wide a comet's head is; never narrower than its stroke. */ + /** The width of a comet's head, in the space's unit; never narrower than its stroke. */ width?: number; } ``` @@ -515,17 +551,17 @@ interface FlowTheme { ### FocusTheme ```ts -/** How hovered and selected rows glow, and rows outside a selection dim. */ +/** How hovered and selected rows glow, and how rows outside a selection dim. */ interface FocusTheme { - /** The glow around hovered rows. */ + /** The glow color around hovered rows. */ hover?: Color; - /** The glow around selected rows; `null` glows in each row's own color. */ + /** The glow color around selected rows; `null` glows in each row's own color. */ selected?: Color | null; - /** How far focus glows reach, CSS pixels. */ + /** How far the hover and selection glows reach, in CSS pixels. */ glow?: number; - /** The opacity of rows outside a selection. */ + /** The opacity of rows outside the selection while any row is selected, from 0 to 1. */ dim?: number; - /** How much a hovered vertex sized in CSS pixels grows. */ + /** The scale factor for a hovered vertex sized in CSS pixels; vertices sized in units only glow. */ grow?: number; } ``` @@ -535,19 +571,19 @@ interface FocusTheme { ```ts /** How a diagram's blocks, groups, ports, and wires look. */ interface DiagramTheme { - /** What a group's container is filled with. */ + /** The fill color of group containers. */ group?: Color; - /** The border of blocks and groups, a unit wide; `null` draws none. */ + /** The color of the one-unit border around blocks and groups; `null` draws no border. */ outline?: Color | null; - /** The radius of block and group corners; a block's marker may set its own. */ + /** The corner radius of blocks and groups; a block marker can set its own. */ corner?: number; - /** How wide ports are. */ + /** The width of ports. */ portSize?: number; - /** The radius of an orthogonal wire's bends. */ + /** The bend radius of orthogonal wires. */ bend?: number; - /** Distance between a group's members and its outline. */ + /** The space between a group's members and its border. */ groupPadding?: number; - /** How blocks and groups drop shadows onto the sheet. */ + /** How blocks and groups cast shadows on the sheet. */ shadow?: ShadowTheme; } ``` @@ -555,11 +591,11 @@ interface DiagramTheme { ### ShadowTheme ```ts -/** How a diagram's blocks and groups drop shadows onto its sheet. */ +/** How a diagram's blocks and groups cast shadows on the sheet, for vertex marks with `shadow` on. */ interface ShadowTheme { - /** What a shadow darkens toward; its alpha is how dark it gets under the block. */ + /** The color a shadow darkens toward; its alpha sets how dark the shadow is under the block. */ color?: Color; - /** How far a block's or a group's shadow falls below it, and as far again it softens. */ + /** How far a shadow falls below its block or group; its edge softens over the same distance. */ lift?: number; } ``` @@ -569,11 +605,11 @@ interface ShadowTheme { ```ts /** How the earth looks. */ interface EarthTheme { - /** The ground's color. */ + /** The ground color. */ ground?: Color; - /** The air glowing around the globe, where the earth has an atmosphere. */ + /** The color of the air glowing around the globe, when the earth's `atmosphere` is on. */ atmosphere?: Color; - /** How bright the night side stays where daylight lights the earth, 0 to 1. */ + /** The brightness of the night side when the earth has `daylight`, from 0 to 1. */ night?: number; } ``` @@ -583,7 +619,7 @@ interface EarthTheme { ```ts /** How a plot looks. */ interface PlotTheme { - /** The line drawn at the shown coordinate. */ + /** The color of the line at the view's current axis coordinate. */ playhead?: Color; } ``` @@ -592,29 +628,30 @@ interface PlotTheme { ```ts /** - * A length: a number, in the space's unit, units in a diagram and CSS pixels elsewhere; `{ px }`, - * CSS pixels; or `{ units }`, position units. + * A length: a number in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in + * CSS pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the + * view zooms in. */ type Length = | number | { - /** CSS pixels, however the view zooms. */ + /** CSS pixels, at any zoom. */ px: number; } | { - /** Position units, which scale as the view zooms. */ + /** Position units (degrees on the earth), which grow on screen as the view zooms in. */ units: number; }; ``` ## Marks -What a scene draws each table as. +The marks that draw the tables of a scene. ### Mark ```ts -/** One drawn table of a scene: `{ vertices | edges | faces | lines | areas: options }` as JSON. */ +/** One table a scene draws. In JSON: `{ vertices | edges | faces | lines | areas: options }`. */ type Mark = | { /** Rows drawn as markers. */ @@ -646,74 +683,84 @@ interface Vertices { /** The dataset and table drawn. */ of: [data: string, table: string]; /** - * Each row's x and y. Without them, the point field `geometry` names places rows; rows without - * either the scene's layout places. + * Each row's x and y. Without a position, the point field that `geometry` names places rows, and + * the scene's layout places rows that have neither. */ position?: [ExprInput, ExprInput] | null; - /** The point field placing each row where `position` is not given; `geometry` unless set. */ + /** + * The point field that places rows when `position` is not set; the field named `geometry` by + * default. + */ geometry?: string | null; /** - * Height above the ground: on the earth, a fraction of its radius. Not in a diagram, which is - * flat. + * Height above the ground; on the earth, a fraction of the earth's radius. Default 0. Not allowed + * in a diagram, which is flat. */ elevation?: ExprInput; /** - * Each row's radius, in the space's unit unless it says its own: units in a diagram, CSS pixels - * elsewhere. + * Each row's radius, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given + * with `px` or `units`; a diagram refuses `px`. The theme's `vertex.radius` unless set. */ radius?: Size | null; /** - * Width and height, for markers that are not round, in the space's unit unless they say their own; - * in a diagram a block's are units, and a block without a size fits its label and ports. + * Each row's width and height, for markers that are not round, in the space's unit unless given + * with `px` or `units`; a diagram refuses `px`. In a diagram, a block without a size fits its + * label and ports. */ size?: [Size, Size] | null; - /** How each row is drawn. */ + /** How each row is drawn; an ellipse by default. */ marker?: Marker; /** A ring around each row's marker in this color; a missing or transparent color draws none. */ ring?: PaintInput | null; /** - * A port on each row for each reference field naming a net, which an `edges` mark of the net table - * without ends draws. Diagrams only. + * Ports on each row, one for each reference field that names a net. An `edges` mark of the net + * table, without `ends`, draws the nets. Diagrams only. */ ports?: [reference: string, port: Port][]; /** - * Makes each row a group: it holds the rows of `table` whose reference field names it, and is - * drawn as a container around them, its label a title in its header. Diagrams only. + * Makes each row a group containing the rows of `table` whose `reference` field names it. A group + * draws as a container around its members, with its label as a title in its header. Diagrams only. */ contains?: [table: string, reference: string] | null; /** - * Whether each row, a block or a group, drops a shadow onto the sheet, as the theme's `shadow` - * says. Diagrams only. + * Whether each block or group casts a shadow on the sheet, styled by the theme's `diagram.shadow`. + * Default `false`. Diagrams only. */ shadow?: boolean; - /** Each row's color; the theme's unless set. */ + /** + * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their + * corners' colors. + */ color?: PaintInput | null; - /** Whether each row is drawn and picked. */ + /** Whether each row is drawn and can be picked. Default `true`. */ visible?: ExprInput; /** - * Each row's priority, from its data alone: what matters more draws on top, stays drawn as a table - * too large to draw whole thins to the tiles in view, and is labeled first. + * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are + * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are + * labeled first. */ priority?: ExprInput | null; - /** Tells two marks of one table apart across scenes, and names the mark in hits. */ + /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ name?: string | null; /** - * Whether rows are hovered, clicked, and probed. Unless set, rows that are things are (vertices, - * edges, faces, a plot's traces) and geometry is not (areas, and lines along paths: coasts, - * borders, roads). + * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces + * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. */ pick?: boolean | null; - /** Whether a click selects a row. */ + /** Whether clicking a row selects it. Default `true`. */ select?: boolean; - /** Whether pressing a row drags it instead of panning. */ + /** + * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the + * camera. Default `false`. + */ drag?: boolean; - /** Each row's text: a text field, or a label's options. */ + /** Each row's label: a text field, or label options. */ label?: Label | null; /** - * A legend of the colors at the view's bottom right, under this title: the colormap across its - * domain, ticked; a cell for each category; or a swatch beside each name of a map's `categories`. - * The color maps through a colormap with a domain or named categories; a conditional's `otherwise` - * is read before its cases. + * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap + * across its domain, a cell for each category, or a swatch beside each name of a map's + * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a + * conditional, `otherwise` is searched before the cases. */ legend?: string | null; } @@ -723,74 +770,84 @@ interface Vertices { ```ts /** - * Rows drawn as strokes between the vertices two reference fields name: branches, links, wires. In - * a diagram, a row without ends is a net: it joins every vertex port whose reference field names - * it. + * Rows drawn as strokes between the vertices named by two reference fields: branches, links, + * wires. In a diagram, a row without ends is a net, joining every vertex port whose reference + * field names it. */ interface Edges { /** The dataset and table drawn. */ of: [data: string, table: string]; /** The reference fields naming each row's two vertices. */ ends?: [from: string, to: string] | null; - /** A path field of points each row runs through between its ends. */ + /** A path field whose points each row passes through between its ends. */ bends?: string | null; /** - * How each row runs between its ends; on the earth geodesic, elsewhere straight, unless set. - * Orthogonal routes run around blocks, bends rounded; diagrams only. + * How each row runs between its ends: geodesic on the earth and straight elsewhere unless set. + * `orthogonal` routes around blocks with rounded bends, in diagrams only. */ route?: Route | null; - /** Whether each row ends in an arrowhead at its second end; a net's at each input it reaches. */ + /** + * Whether each row has an arrowhead at its second end; a net has one at each input it reaches. + * Default `false`. + */ arrows?: boolean; /** - * Draws each row as a pole from its own vertex down to the ground beneath it, on a plane or the - * earth; it has no ends. + * Draws each row as a pole from its own vertex down to the ground, on a plane or the earth. Poles + * take no `ends`, `bends`, or `joins`. Default `false`. */ poles?: boolean; /** - * Draws each row as a net on a plane or the earth: it joins the vertex rows of `table` whose - * reference field names it, as a star from their middle to each. + * Draws each row as a net on a plane or the earth, joining the vertex rows of `table` whose + * `reference` field names it: a star from their center to each member. */ joins?: [table: string, reference: string] | null; /** - * Each row's width, in the space's unit unless it says its own: units in a diagram, CSS pixels - * elsewhere. + * Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given + * with `px` or `units`. The theme's width unless set. */ width?: Size | null; - /** Whether each row is dashed. */ + /** Whether each row is dashed, with the theme's `dash` period. Default `false`. */ dash?: ExprInput; /** - * Comets flowing along each row at this speed a second, in the lengths of the theme's flow; - * negative flows backward. A speed that changes carries comets on from where they are. + * The speed of comets flowing along each row, per second, in the unit of the theme's `flow` + * lengths. Negative speeds flow backward; 0, the default, draws no comets. When the speed changes, + * comets continue from where they are. */ flow?: ExprInput; - /** Each row's color; the theme's unless set. */ + /** + * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their + * corners' colors. + */ color?: PaintInput | null; - /** Whether each row is drawn and picked. */ + /** Whether each row is drawn and can be picked. Default `true`. */ visible?: ExprInput; /** - * Each row's priority, from its data alone: what matters more draws on top, stays drawn as a table - * too large to draw whole thins to the tiles in view, and is labeled first. + * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are + * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are + * labeled first. */ priority?: ExprInput | null; - /** Tells two marks of one table apart across scenes, and names the mark in hits. */ + /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ name?: string | null; /** - * Whether rows are hovered, clicked, and probed. Unless set, rows that are things are (vertices, - * edges, faces, a plot's traces) and geometry is not (areas, and lines along paths: coasts, - * borders, roads). + * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces + * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. */ pick?: boolean | null; - /** Whether a click selects a row. */ + /** Whether clicking a row selects it. Default `true`. */ select?: boolean; - /** Whether pressing a row drags it instead of panning. */ + /** + * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the + * camera. Default `false`. + */ drag?: boolean; - /** Each row's text: a text field, or a label's options. */ + /** Each row's label: a text field, or label options. */ label?: Label | null; /** - * A legend of the colors at the view's bottom right, under this title: the colormap across its - * domain, ticked; a cell for each category; or a swatch beside each name of a map's `categories`. - * The color maps through a colormap with a domain or named categories; a conditional's `otherwise` - * is read before its cases. + * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap + * across its domain, a cell for each category, or a swatch beside each name of a map's + * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a + * conditional, `otherwise` is searched before the cases. */ legend?: string | null; } @@ -800,62 +857,73 @@ interface Edges { ```ts /** - * Rows drawn as lines: along a path field (roads, rivers, borders) or a polygon field's rings, - * outlining its areas; on a plot, through a sampled field's history across the data's axis, what a - * monitor shows. On the earth paths bend with the globe and stay whole across the antimeridian. + * Rows drawn as lines along a path field (roads, rivers, borders) or a polygon field's rings (area + * outlines). On a plot, each row instead traces a sampled field's history across the data's axis, + * as a monitor does. On the earth, paths follow the globe's curve and stay whole across the + * antimeridian. */ interface Lines { /** The dataset and table drawn. */ of: [data: string, table: string]; - /** The path or polygon field each row runs along; `geometry` unless set. Not on a plot. */ + /** + * The path or polygon field each row runs along; the field named `geometry` by default. Not + * allowed on a plot. + */ geometry?: string | null; /** - * The sampled field each row traces across the data's axis, on a plot alone. A field not recorded - * yet draws nothing until its first frame. + * The sampled field each row traces across the data's axis; plots only, where it is required. A + * field with no frames yet draws nothing until its first frame arrives. */ trace?: string | null; - /** How each row runs between its samples, on a plot. */ + /** How each row runs between its samples on a plot; `linear` by default. */ interpolation?: Interpolation; /** - * Each row's width, in the space's unit unless it says its own: units in a diagram, CSS pixels - * elsewhere. + * Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given + * with `px` or `units`. The theme's width unless set. */ width?: Size | null; - /** Whether each row is dashed. */ + /** Whether each row is dashed, with the theme's `dash` period. Default `false`. */ dash?: ExprInput; /** - * Comets flowing along each row at this speed a second, in the lengths of the theme's flow; - * negative flows backward. A speed that changes carries comets on from where they are. + * The speed of comets flowing along each row, per second, in the unit of the theme's `flow` + * lengths. Negative speeds flow backward; 0, the default, draws no comets. When the speed changes, + * comets continue from where they are. */ flow?: ExprInput; - /** Each row's color; the theme's unless set. */ + /** + * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their + * corners' colors. + */ color?: PaintInput | null; - /** Whether each row is drawn and picked. */ + /** Whether each row is drawn and can be picked. Default `true`. */ visible?: ExprInput; /** - * Each row's priority, from its data alone: what matters more draws on top, stays drawn as a table - * too large to draw whole thins to the tiles in view, and is labeled first. + * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are + * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are + * labeled first. */ priority?: ExprInput | null; - /** Tells two marks of one table apart across scenes, and names the mark in hits. */ + /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ name?: string | null; /** - * Whether rows are hovered, clicked, and probed. Unless set, rows that are things are (vertices, - * edges, faces, a plot's traces) and geometry is not (areas, and lines along paths: coasts, - * borders, roads). + * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces + * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. */ pick?: boolean | null; - /** Whether a click selects a row. */ + /** Whether clicking a row selects it. Default `true`. */ select?: boolean; - /** Whether pressing a row drags it instead of panning. */ + /** + * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the + * camera. Default `false`. + */ drag?: boolean; - /** Each row's text: a text field, or a label's options. */ + /** Each row's label: a text field, or label options. */ label?: Label | null; /** - * A legend of the colors at the view's bottom right, under this title: the colormap across its - * domain, ticked; a cell for each category; or a swatch beside each name of a map's `categories`. - * The color maps through a colormap with a domain or named categories; a conditional's `otherwise` - * is read before its cases. + * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap + * across its domain, a cell for each category, or a swatch beside each name of a map's + * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a + * conditional, `otherwise` is searched before the cases. */ legend?: string | null; } @@ -865,44 +933,53 @@ interface Lines { ```ts /** - * Rows drawn as the areas a polygon field holds: parcels, states, lakes, the land and water of a - * map. The view cuts each row's polygons into triangles once, off the frame when they are large; - * on the earth they bend with the globe and stay whole across the antimeridian. Outline areas with - * a `Lines` mark of the same table, which draws their rings. + * Rows drawn as the areas of a polygon field: parcels, states, lakes, the land and water of a map. + * The view triangulates each row's polygons once, in the background when they are large. On the + * earth, areas follow the globe's curve and stay whole across the antimeridian. To outline areas, + * add a `Lines` mark of the same table, which draws their rings. */ interface Areas { /** The dataset and table drawn. */ of: [data: string, table: string]; - /** The polygon field each row fills: one polygon's rings, or polygons. `geometry` unless set. */ + /** + * The polygon field each row fills, holding one polygon's rings or several polygons; the field + * named `geometry` by default. + */ geometry?: string | null; - /** Each row's color; the theme's unless set. */ + /** + * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their + * corners' colors. + */ color?: PaintInput | null; - /** Whether each row is drawn and picked. */ + /** Whether each row is drawn and can be picked. Default `true`. */ visible?: ExprInput; /** - * Each row's priority, from its data alone: what matters more draws on top, stays drawn as a table - * too large to draw whole thins to the tiles in view, and is labeled first. + * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are + * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are + * labeled first. */ priority?: ExprInput | null; - /** Tells two marks of one table apart across scenes, and names the mark in hits. */ + /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ name?: string | null; /** - * Whether rows are hovered, clicked, and probed. Unless set, rows that are things are (vertices, - * edges, faces, a plot's traces) and geometry is not (areas, and lines along paths: coasts, - * borders, roads). + * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces + * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. */ pick?: boolean | null; - /** Whether a click selects a row. */ + /** Whether clicking a row selects it. Default `true`. */ select?: boolean; - /** Whether pressing a row drags it instead of panning. */ + /** + * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the + * camera. Default `false`. + */ drag?: boolean; - /** Each row's text: a text field, or a label's options. */ + /** Each row's label: a text field, or label options. */ label?: Label | null; /** - * A legend of the colors at the view's bottom right, under this title: the colormap across its - * domain, ticked; a cell for each category; or a swatch beside each name of a map's `categories`. - * The color maps through a colormap with a domain or named categories; a conditional's `otherwise` - * is read before its cases. + * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap + * across its domain, a cell for each category, or a swatch beside each name of a map's + * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a + * conditional, `otherwise` is searched before the cases. */ legend?: string | null; } @@ -912,46 +989,52 @@ interface Areas { ```ts /** - * Rows drawn as triangles between the vertices three reference fields name: meshes, surfaces, and - * fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen from - * above shows its exact colors. + * Rows drawn as triangles between the vertices named by three reference fields: meshes, surfaces, + * and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen + * from above shows its exact colors. */ interface Faces { /** The dataset and table drawn. */ of: [data: string, table: string]; /** - * The reference fields naming each row's three vertices. Without a color, a face blends the colors - * of its corners. + * The reference fields naming each row's three vertices; required. Without a color, a face blends + * the colors of its corners. */ corners?: [a: string, b: string, c: string] | null; - /** Each row's color; the theme's unless set. */ + /** + * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their + * corners' colors. + */ color?: PaintInput | null; - /** Whether each row is drawn and picked. */ + /** Whether each row is drawn and can be picked. Default `true`. */ visible?: ExprInput; /** - * Each row's priority, from its data alone: what matters more draws on top, stays drawn as a table - * too large to draw whole thins to the tiles in view, and is labeled first. + * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are + * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are + * labeled first. */ priority?: ExprInput | null; - /** Tells two marks of one table apart across scenes, and names the mark in hits. */ + /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ name?: string | null; /** - * Whether rows are hovered, clicked, and probed. Unless set, rows that are things are (vertices, - * edges, faces, a plot's traces) and geometry is not (areas, and lines along paths: coasts, - * borders, roads). + * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces + * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. */ pick?: boolean | null; - /** Whether a click selects a row. */ + /** Whether clicking a row selects it. Default `true`. */ select?: boolean; - /** Whether pressing a row drags it instead of panning. */ + /** + * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the + * camera. Default `false`. + */ drag?: boolean; - /** Each row's text: a text field, or a label's options. */ + /** Each row's label: a text field, or label options. */ label?: Label | null; /** - * A legend of the colors at the view's bottom right, under this title: the colormap across its - * domain, ticked; a cell for each category; or a swatch beside each name of a map's `categories`. - * The color maps through a colormap with a domain or named categories; a conditional's `otherwise` - * is read before its cases. + * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap + * across its domain, a cell for each category, or a swatch beside each name of a map's + * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a + * conditional, `otherwise` is searched before the cases. */ legend?: string | null; } @@ -962,7 +1045,7 @@ interface Faces { ```ts /** * How a vertex is drawn: a shape's name, `"block"`, or - * `{ shape | block | gauge | pie | icon | wgsl: options }`. + * `{ shape | block | gauge | pie | icon | wgsl: options }`. An ellipse by default. */ type Marker = | 'ellipse' @@ -975,50 +1058,53 @@ type Marker = shape: Shape; } | { - /** A block: a rectangle with rounded corners, sized for diagrams. */ + /** A rectangle with rounded corners that holds its label inside, as a diagram's blocks do. */ block: { - /** Corner radius, CSS pixels, or units in a diagram; the theme's `corner` unless set. */ + /** + * Corner radius in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's + * `diagram.corner` unless set. + */ corner?: number | null; }; } | { - /** A shape's outline, filled to `fill` from 0 to 1: around a circle, up other shapes. */ + /** A shape's outline, filled to `fill` from 0 to 1: around a circle, or upward in other shapes. */ gauge: { - /** How full each row's gauge is. */ + /** How full each row's gauge is, from 0 to 1. */ fill: ExprInput; - /** The outline. */ + /** The outline's shape; an ellipse by default. */ shape?: Shape; }; } | { - /** A circle cut into slices, each as large as its value. */ + /** A circle divided into slices sized in proportion to their values. */ pie: { /** Each slice's value. */ slices: ExprInput[]; /** Each slice's color. */ colors: Color[]; - /** The hole in the middle, as a fraction of the radius. */ + /** The hole in the middle, as a fraction of the radius; 0 by default. */ hole?: number; }; } | { - /** One of `images`, fitted in the marker's box; `index` picks which for each row. */ + /** One of `images`, fitted to the marker's box; `index` picks the image for each row. */ icon: { /** The images. */ images: Bitmap[]; - /** Each row's image, from 0. */ + /** Each row's image index, from 0. */ index: ExprInput; }; } | { /** - * A marker in WGSL: `source` defines `fn marker(f: MarkerFragment) -> MarkerColor`, reading up to - * eight `inputs` as `f.input[i]`. It compiles once, when the view first draws it. + * A custom marker in WGSL. `source` defines `fn marker(f: MarkerFragment) -> MarkerColor`, which + * can read up to eight `inputs` as `f.input[i]`. It compiles once, when the view first draws it. */ wgsl: { - /** The function's source. */ + /** The WGSL source of the `marker` function. */ source: string; - /** Each row's inputs. */ + /** Each row's inputs, at most eight. */ inputs?: ExprInput[]; }; }; @@ -1034,7 +1120,7 @@ type Shape = 'ellipse' | 'rectangle' | 'rounded' | 'diamond'; ### Bitmap ```ts -/** RGBA texels, straight alpha, sRGB, rows top to bottom. */ +/** An image of RGBA texels: straight alpha, sRGB, rows top to bottom. */ interface Bitmap { /** Width and height in texels. */ size: [number, number]; @@ -1047,9 +1133,9 @@ interface Bitmap { ```ts /** - * A vertex's port: where a net its reference field names meets it, on one side of its marker: a - * side, or options. An output drives its net: layouts rank a net from its outputs, and its arrows - * point away from them. + * A port on one side of a vertex's marker, where a net connects: a side name, or options. The + * vertex names the net in a reference field. An output drives its net: layouts rank a net from its + * outputs, and the net's arrows point away from them. */ type Port = | 'left' @@ -1057,18 +1143,18 @@ type Port = | 'top' | 'bottom' | { - /** The side it sits on. */ + /** The side of the marker it sits on. */ side: Side; /** - * Its place among the ports on that side, from the top or the left; in declaration order unless - * set. + * Its position among the ports on its side, counted from the top or left. Ports without an `order` + * follow those with one, in declaration order. */ order?: number | null; /** Its name, drawn beside it inside the marker. */ label?: string | null; - /** Its marker. */ + /** Its shape; an ellipse by default. */ shape?: Shape; - /** Whether it drives the net its reference field names; an input unless set. */ + /** Whether it is an output, which drives its net. Default `false`, an input. */ output?: boolean; }; ``` @@ -1097,27 +1183,33 @@ type Interpolation = 'linear' | 'stepBefore' | 'stepAfter'; ### Label ```ts +/** + * Text for each row, placed beside, inside, or along the row's shape and following it every frame: + * a text field, or options. Where labels would overlap, the higher priority shows; among equals, + * labels already shown stay. In a diagram a label is part of the drawing: it scales with the zoom, + * fades out when too small to read, and shows wherever it fits its vertex. + */ type Label = | ExprInput | { - /** A text field. */ + /** The text field to show. */ text: ExprInput; - /** In the space's unit: units in a diagram, CSS pixels elsewhere; the theme's text size unless set. */ + /** + * Font size in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's + * `text.size` unless set. + */ size?: number | null; /** Each row's text color; the theme's text color unless set. */ color?: PaintInput | null; - /** Where labels would overlap, the higher shows; the mark's priority unless set. */ + /** Where labels would overlap, the higher priority shows; the mark's `priority` unless set. */ priority?: ExprInput | null; - /** - * How far a line runs before it wraps, in the space's unit: units in a diagram, CSS pixels - * elsewhere. - */ + /** The width at which lines wrap, in the space's unit (units in a diagram, CSS pixels elsewhere). */ maxWidth?: number | null; - /** The CSS font family list its texts are set in; the theme's font unless set. */ + /** The CSS font family list for the text; the theme's `text.font` unless set. */ font?: string | null; /** - * The most labels shown at once, those of highest priority among the rows in view; where they - * would overlap, fewer. + * The most labels shown at once: those of highest priority among the rows in view, or fewer where + * they would overlap. No limit unless set. */ max?: number | null; }; @@ -1127,8 +1219,9 @@ type Label = ```ts /** - * A size, in the space's unit unless it says its own: units in a diagram, CSS pixels elsewhere; - * `px` keeps CSS pixels, and `units` takes position units, which scale as the view zooms. + * A size: a value in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in CSS + * pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the + * view zooms in. */ type Size = ExprInput | Px | Units; ``` @@ -1136,8 +1229,9 @@ type Size = ExprInput | Px | Units; ### Px ```ts -/** A size in CSS pixels, whatever the space's own unit. */ +/** A size in CSS pixels, regardless of the space's unit. */ interface Px { + /** The size, in CSS pixels. */ px: ExprInput; } ``` @@ -1146,26 +1240,27 @@ interface Px { ```ts /** - * A size in position units, whatever the space's own unit, so it scales as the view zooms, kept - * within CSS pixels when given. + * A size in position units (degrees on the earth), regardless of the space's unit, so it scales as + * the view zooms. `atLeast` and `atMost` bound it in CSS pixels. */ interface Units { + /** The size, in position units. */ units: ExprInput; - /** CSS pixels it keeps at least, however far the view zooms out. */ + /** The minimum on-screen size in CSS pixels, however far the view zooms out. */ atLeast?: number | null; - /** CSS pixels it keeps at most, however far the view zooms in. */ + /** The maximum on-screen size in CSS pixels, however far the view zooms in. */ atMost?: number | null; } ``` ## Expressions -Expressions as JSON: a constant, a field, or a node of these kinds; and colors, which take CSS strings where values take field names. +Expressions as JSON: a constant, a field, or one of these nodes. Colors take CSS strings where values take field names. ### ExprNode ```ts -/** A value for every row of a mark: a constant, a field, view state, or arithmetic of them. */ +/** A value for each row: a number, a boolean, a field's name, or a node that computes one. */ type ExprNode = | number | boolean @@ -1187,8 +1282,8 @@ type ExprNode = ```ts /** - * A color for every row of a mark: a CSS color, a value through a colormap, or a conditional - * choosing between colors. + * A color for each row: a CSS color, a value through a colormap, or a conditional choosing between + * colors. */ type PaintNode = Color | ColorNode | ColormapNode | WhenPaintNode | MissingNode; ``` @@ -1196,12 +1291,19 @@ type PaintNode = Color | ColorNode | ColormapNode | WhenPaintNode | MissingNode; ### FieldNode ```ts -/** A field of each row: of the mark's own table, or from another dataset's table joined by row id. */ +/** + * A field's value in each row: from the mark's own table, or with `from`, from another dataset's + * table joined by row id. + */ interface FieldNode { + /** The field's name. */ field: string; /** The dataset and table the field is read from, joined by row id. */ from?: [data: string, table: string] | null; - /** Reads a sampled field at its last frame without interpolating. */ + /** + * For a sampled field, reads the last frame at or before the shown coordinate instead of + * interpolating. Default `false`. + */ step?: boolean; } ``` @@ -1209,6 +1311,7 @@ interface FieldNode { ### ColorNode ```ts +/** A constant color. */ interface ColorNode { /** A CSS color, such as `#ffb000` or `rebeccapurple`. */ color: Color; @@ -1218,7 +1321,7 @@ interface ColorNode { ### StateNode ```ts -/** What the view knows of each row. */ +/** View state of each row, such as whether it is selected. */ interface StateNode { state: State; } @@ -1227,7 +1330,7 @@ interface StateNode { ### MissingNode ```ts -/** A missing value: options that read one take the theme's. */ +/** A missing value. An option that evaluates to missing uses the theme's value. */ interface MissingNode { missing: true; } @@ -1236,6 +1339,7 @@ interface MissingNode { ### UnaryNode ```ts +/** A one-argument operation, such as `{ "op": "abs", "args": ["x"] }`. */ interface UnaryNode { op: Unary; args: [ExprInput]; @@ -1245,6 +1349,7 @@ interface UnaryNode { ### BinaryNode ```ts +/** A two-argument operation, such as `{ "op": "add", "args": ["a", 1] }`. */ interface BinaryNode { op: Binary; args: [ExprInput, ExprInput]; @@ -1256,7 +1361,9 @@ interface BinaryNode { ```ts /** The value of the first case whose condition holds, else `otherwise`. */ interface WhenNode { + /** The cases, as `[condition, value]` pairs tried in order. */ when: [condition: ExprInput, value: ExprInput][]; + /** The value where no condition holds. */ otherwise: ExprInput; } ``` @@ -1264,9 +1371,11 @@ interface WhenNode { ### WhenPaintNode ```ts -/** A conditional's color: the first case's whose condition holds, else `otherwise`'s. */ +/** A conditional color: the color of the first case whose condition holds, else `otherwise`. */ interface WhenPaintNode { + /** The cases, as `[condition, color]` pairs tried in order. */ when: [condition: ExprInput, color: PaintInput][]; + /** The color where no condition holds. */ otherwise: PaintInput; } ``` @@ -1274,7 +1383,7 @@ interface WhenPaintNode { ### IsNode ```ts -/** Whether a text field equals the text. */ +/** Whether a text field equals the text exactly. */ interface IsNode { is: [field: ExprInput, text: string]; } @@ -1283,7 +1392,10 @@ interface IsNode { ### ContainsNode ```ts -/** Whether a text field holds the text, ignoring case: in a mark, a field of its own table. */ +/** + * Whether a text field contains the text, ignoring case. In a mark, the field must belong to the + * mark's own table. + */ interface ContainsNode { contains: [field: ExprInput, text: string]; } @@ -1293,8 +1405,8 @@ interface ContainsNode { ```ts /** - * `value` mapped linearly onto the range `scale`, clamped: from its `domain`, else from the extent - * of the values shown. + * `value` mapped linearly from `domain` onto the range `scale`, clamped to the range. Without a + * `domain`, the extent of the values shown is used. */ interface ScaleNode { scale: [low: number, high: number]; @@ -1306,7 +1418,10 @@ interface ScaleNode { ### ColormapNode ```ts -/** `value` through `colormap`: from its `domain`, else from the extent of the values shown. */ +/** + * `value` mapped through `colormap` over `domain`, or without a `domain`, over the extent of the + * values shown. A categorical map ignores the domain and bins values by their integer part. + */ interface ColormapNode { colormap: Colormap; value: ExprInput; @@ -1317,14 +1432,18 @@ interface ColormapNode { ### Unary ```ts -/** An operation of one value. */ +/** An operation on one value. `sin` and `cos` take radians; `not` treats values above 0.5 as true. */ type Unary = 'neg' | 'not' | 'abs' | 'sqrt' | 'sin' | 'cos' | 'floor' | 'ln' | 'exp'; ``` ### Binary ```ts -/** An operation of two values. */ +/** + * An operation on two values: arithmetic, `min` and `max`, comparisons (1 for true, 0 for false), + * and logic. `rem` is a floored remainder, which takes the sign of the divisor. `and` and `or` + * treat values above 0.5 as true. + */ type Binary = | 'add' | 'sub' @@ -1347,7 +1466,10 @@ type Binary = ### State ```ts -/** What the view knows of each row. */ +/** + * View state of each row: `selected`, `hovered`, `pointer` (CSS pixels from a vertex to the + * pointer), `elapsed` (seconds since the view was created), or `zoom` (the camera's zoom). + */ type State = 'selected' | 'hovered' | 'pointer' | 'elapsed' | 'zoom'; ``` @@ -1414,29 +1536,33 @@ type ColormapName = ### ColormapKind ```ts -/** How a colormap spans values. */ +/** + * How a colormap maps values: `sequential` and `diverging` maps clamp, `cyclic` maps wrap, and + * `categorical` maps give each integer its own color. + */ type ColormapKind = 'sequential' | 'diverging' | 'cyclic' | 'categorical'; ``` ### CustomColormap ```ts -/** Your own colormap: its kind, its colors, and for a categorical map its categories' names. */ +/** A custom colormap: its kind, its colors, and for a categorical map, its category names. */ interface CustomColormap { kind: ColormapKind; /** - * Colors blended in OKLab, each at its `[place, color]` from 0 to 1 or evenly between its - * neighbors, as in a CSS gradient; a categorical map's, one per category. + * The colors, blended in OKLab. Each is a color, spaced evenly between its neighbors as in a CSS + * gradient, or a `[place, color]` pair with a place from 0 to 1, in ascending order. A categorical + * map takes one color per category, without places, at most 256. */ colors: (Color | [number, Color])[]; - /** The categories' names, in order, for a legend: a categorical map's alone. */ + /** The category names in category order, shown in legends. Categorical maps only. */ categories?: string[]; } ``` ## Rows, cameras, and queries -Naming rows, framing them, and asking for them. +Identifying rows, framing them, and querying them. ### Id @@ -1451,7 +1577,10 @@ type Id = number | string; ### Ids ```ts -/** Row ids: numbers or texts (null leaves a reference unwired), or `{count}` naming rows 0..count. */ +/** + * Row ids: numbers or texts, or `{count}` for rows 0 to count - 1. In a reference, null names no + * row. + */ type Ids = | (Id | null)[] | { @@ -1462,7 +1591,10 @@ type Ids = ### Row ```ts -/** A row of a table in a dataset. It survives any change that keeps the names and the id. */ +/** + * One row, identified by dataset name, table name, and id. It stays valid across any change that + * keeps those names and the id. + */ interface Row { /** The dataset's name. */ data: string; @@ -1476,7 +1608,10 @@ interface Row { ### Rows ```ts -/** Rows of one table in a dataset, by id: what a view selects, fits, and tells as sets of rows. */ +/** + * Rows of one table in a dataset, by id. Views use it for selections, fits, and the rows events + * report. + */ interface Rows { /** The dataset's name. */ data: string; @@ -1495,20 +1630,20 @@ type Camera = | { /** * Longitude, latitude, and elevation on the earth; x, y, and elevation on a plane or a diagram. In - * JSON, `[x, y]` looks at the ground. + * JSON, `[x, y]` is shorthand for elevation 0. */ center: [number, number] | [number, number, number]; - /** On the earth the web mercator level; elsewhere, log₂ of pixels per unit. */ + /** The web mercator zoom level on the earth; elsewhere, log₂ of CSS pixels per unit. */ zoom: number; - /** Degrees from looking straight down, up to 75; 0 in a diagram. */ + /** Tilt in degrees from looking straight down, up to 75. Ignored in a diagram. */ pitch: number; - /** Degrees clockwise from north, or from up on a plane; 0 in a diagram. */ + /** Degrees clockwise from north, or from up on a plane. Ignored in a diagram. */ bearing: number; } | { - /** The axis range shown. */ + /** The x-axis range shown. */ x: [number, number]; - /** The value range shown, or none to fit what is in view. */ + /** The value range shown, or `null` to fit the values in view. */ y?: [number, number] | null; }; ``` @@ -1516,7 +1651,7 @@ type Camera = ### Fit ```ts -/** What a view's fit frames. */ +/** The target of a view's fit. */ type Fit = | 'all' | { @@ -1543,32 +1678,35 @@ type Fit = ```ts /** The rows of a table to read back, run through the expressions marks draw with. */ interface Query { - /** The table read. */ + /** The table to read. */ of: string; /** - * The rows read, in this order unless sorted; every row unless set. Ids that name no row are - * passed over. + * The rows to read, in this order unless sorted; every row when unset. Ids that name no row are + * skipped. */ ids?: Ids | null; - /** Keeps the rows it is true for. */ + /** Keeps only the rows for which this is true. */ filter?: ExprInput | null; /** - * Keys to sort by, each after those before it; rows a key reads nothing for come last, and rows - * all keys order alike keep their order. + * Keys to sort by, each breaking ties left by the keys before it. Rows where a key has no value + * sort last; rows equal on every key keep their order. */ sort?: [ExprInput, Order][]; - /** The kept rows skipped first. */ + /** How many of the kept rows to skip. */ offset?: number; - /** The most rows read; a limit picks them in linear time before sorting only those. */ + /** + * The most rows to return. When sorting with a limit, the first `offset + limit` rows are selected + * in linear time and only those are sorted. + */ limit?: number | null; /** - * Fields to read by name: a field as it is, anything else as numbers. Every field unless set; none - * set reads the ids alone. + * Fields to return, by output name. A plain field returns that field as stored; any other + * expression returns numbers. Every field when unset; an empty object returns the ids alone. */ select?: { [name: string]: ExprInput; } | null; - /** The axis coordinate sampled fields are read at; their last frame unless set. */ + /** The axis coordinate at which sampled fields are read; their last frame when unset. */ at?: number | null; } ``` @@ -1577,32 +1715,33 @@ interface Query { ```ts /** - * Rows' least and greatest values of a sampled field over equal spans of the axis, to read back: - * field `field` of table `table` over `spans` equal spans from `range[0]` to `range[1]`, of every - * row or of the rows `ids` names, in its order. Plain data, like a query. + * A request for each row's least and greatest value of a sampled field over equal spans of the + * axis: field `field` of table `table`, over `spans` equal spans from `range[0]` to `range[1]`, + * for every row or for the rows `ids` names, in that order. Plain data that deserializes from + * JSON, like a query. */ interface EnvelopeRequest { - /** The table read. */ + /** The table to read. */ table: string; - /** The sampled field read. */ + /** The sampled field to read. */ field: string; - /** The axis coordinates the spans cover, from the first to the last. */ + /** The axis range the spans cover, `[first, last]`: finite, with `first <= last`. */ range: [number, number]; - /** How many equal spans the range is cut into. */ + /** The number of equal spans to divide the range into; at least 1. */ spans: number; - /** The rows read, in this order; every row unless set. */ + /** The rows to read, in this order; every row when unset. */ ids?: Ids | null; } ``` ## Results -What views tell: events, hits, descriptions, and what frames cost. +View events and hits, dataset descriptions and values, and frame statistics. ### ViewEvent ```ts -/** What happened in a view, for the application to act on, as `view.on` tells it. */ +/** Something that happened in a view, as `view.on` reports it. */ type ViewEvent = | { type: 'click'; @@ -1625,7 +1764,7 @@ type ViewEvent = | { type: 'menu'; detail: { - /** What lies under the point, nearest first. */ + /** The rows under the point, nearest first. */ hits: Hit[]; /** How the user asked. */ trigger: Trigger; @@ -1639,8 +1778,8 @@ type ViewEvent = /** The row dragged, as it was hit. */ hit: Hit; /** - * Where the row goes, in the space's coordinates: where it was hit, moved as the pointer moved - * since the press, at the depth the drag started; a vertex keeps where it was grabbed. On a + * The row's new position in the space's coordinates: the hit point moved with the pointer since + * the press, at the depth where the drag started, so a vertex keeps its grab offset. Snapped to a * diagram's dots when its grid snaps. */ position: [number, number, number]; @@ -1655,7 +1794,7 @@ type ViewEvent = from: End; /** The vertex it was released over, if any. */ to: End | null; - /** The edge whose other end was dragged, which `to` replaces. */ + /** The edge whose end was dragged off; `to` replaces that end. */ edge: Row | null; /** Where it was released. */ trigger: Trigger; @@ -1673,7 +1812,7 @@ type ViewEvent = | { type: 'hovered'; detail: { - /** The row now pointed at; `null` once it left every row. */ + /** The row now hovered; `null` once no row is. */ hit: Hit | null; /** The pointer, or the keys. */ trigger: Trigger; @@ -1709,11 +1848,11 @@ type ViewEvent = interface Hit { /** The row. */ row: Row; - /** The name of the mark that drew it, when it has one: what tells two marks of one table apart. */ + /** The name of the mark that drew it, if named. It tells two marks of one table apart. */ mark?: string; /** - * The point of the row nearest the probe, CSS pixels from the top left: a vertex's or a port's - * middle, the nearest point of a stroke or a triangle. + * The row's point nearest the probe, in CSS pixels from the top left: a vertex's or port's middle, + * or the nearest point of a stroke or triangle. */ point: [number, number]; /** That point in the space's coordinates: x, y, and elevation. */ @@ -1730,7 +1869,7 @@ interface Hit { ### End ```ts -/** A vertex a connection ends at, and its port when it has the one the connection meets. */ +/** A vertex a connection ends at, and the port the connection meets, if any. */ interface End { /** The vertex. */ row: Row; @@ -1764,15 +1903,15 @@ type Phase = 'start' | 'move' | 'end'; ```ts /** - * Where, with what keys, and with what the user acted. For keys, the point is the row they point - * at, else the pointer, else the view's middle. + * Where the user acted, with which modifier keys, and with which device. For keyboard input, the + * point is the row the keys point at, else the pointer, else the view's middle. */ interface Trigger { /** CSS pixels from the top left. */ point: [number, number]; - /** The keys held. */ + /** The modifier keys held. */ modifiers: Modifiers; - /** What acted. */ + /** The device used. */ device: Device; } ``` @@ -1780,14 +1919,14 @@ interface Trigger { ### Device ```ts -/** What the user acts with. */ +/** The input device. */ type Device = 'mouse' | 'pen' | 'touch' | 'keyboard'; ``` ### Modifiers ```ts -/** Keys held during an input. */ +/** Modifier keys held during an input. */ interface Modifiers { /** Shift. */ shift: boolean; @@ -1804,17 +1943,17 @@ interface Modifiers { ```ts /** - * What a dataset holds: its tables, their rows and fields, and the axis its sampled fields are - * recorded over and span. + * A summary of a dataset: its tables with their rows and fields, the axis sampled fields are + * recorded over, and the part of the axis they cover. */ interface Description { /** The dataset's name. */ name: string; - /** The axis's name and unit. */ + /** The axis name and unit, if set. */ axis: [string, string] | null; - /** The first and last coordinates its sampled fields hold. */ + /** The first and last axis coordinates held by any sampled field; `null` when none holds any. */ span: [number, number] | null; - /** Its tables, by name. */ + /** Its tables, sorted by name. */ tables: TableDescription[]; } ``` @@ -1822,13 +1961,13 @@ interface Description { ### TableDescription ```ts -/** A table described: its rows, and its fields by name. */ +/** A summary of one table: its row count and its fields, sorted by name. */ interface TableDescription { /** The table's name. */ name: string; - /** How many rows it has. */ + /** The number of rows. */ rows: number; - /** Its fields, by name. */ + /** Its fields, sorted by name. */ fields: FieldDescription[]; } ``` @@ -1836,15 +1975,15 @@ interface TableDescription { ### FieldDescription ```ts -/** A field described: what it holds, the table a reference names, the coordinates samples span. */ +/** A summary of one field: its kind, plus its target table or axis span where those apply. */ interface FieldDescription { /** The field's name. */ name: string; - /** What it holds. */ + /** The kind of values it holds. */ kind: Kind; - /** The table a reference names. */ + /** For a reference, the target table. */ to?: string; - /** The first and last coordinates a sampled field holds. */ + /** For a sampled field, the first and last axis coordinates it holds. */ span?: [number, number]; } ``` @@ -1852,7 +1991,7 @@ interface FieldDescription { ### Kind ```ts -/** What a field holds. */ +/** The kind of values a field holds. */ type Kind = 'number' | 'bool' | 'text' | 'point' | 'path' | 'parts' | 'polygons' | 'reference' | 'sampled'; ``` @@ -1884,22 +2023,22 @@ interface Sample { ### Stats ```ts -/** What a view does each frame. */ +/** Counters describing a view's frames. */ interface Stats { /** Frames rendered. */ frames: number; /** Rows of every mark's table. */ rows: number; /** - * Rows every mark draws: every row of a table drawn whole, the rows of the tiles in view of one - * drawn through tiles. + * Rows every mark draws: every row of a table drawn whole, and the rows of the tiles in view of a + * paged table. */ drawn: number; - /** Draw calls the last frame recorded, labels with the marks. */ + /** Draw calls the last frame recorded, label draws included. */ draws: number; /** - * Instances the last frame's draws drew: points, segments, triangles, glyphs; at most, for - * segments the GPU culls. + * Instances the last frame's draws drew: points, segments, triangles, and glyphs. An upper bound + * for segments, which the GPU may cull. */ instances: number; /** Fetched tiles the view draws, of every tile source. */ @@ -1909,8 +2048,8 @@ interface Stats { /** Milliseconds the last frame took to record on the CPU. */ cpuMs: number; /** - * Milliseconds the GPU takes on a frame: the median of the last frames it measured; 0 where the - * device cannot measure. + * Milliseconds the GPU takes per frame: the median of recently measured frames; 0 if the device + * cannot measure. */ gpuMs: number; } @@ -1920,11 +2059,11 @@ interface Stats { ```ts /** - * Work under way a frame stands in for: pipelines the scene draws with compiling, until which the - * view draws nothing new (`compiling`); pages uploading or tables not yet built from them - * (`data`); layouts, routes, lanes, shapes, tiles, or pyramids (`derived`); a label placement or - * texts not yet set (`labels`); a fit due or being measured (`fit`); segment counts, or a - * transition beginning (`marks`). Empty once a frame is complete. + * What a frame still waits on: pipelines the scene draws with, which compile before the view draws + * anything new (`compiling`); pages uploading, or tables not yet built from them (`data`); + * layouts, routes, lanes, shapes, tiles, or pyramids (`derived`); label placement, or text not yet + * set (`labels`); a fit due or being measured (`fit`); segment counts, or a transition starting + * (`marks`). Empty once the frame is complete. */ type Waiting = ('compiling' | 'data' | 'derived' | 'labels' | 'fit' | 'marks')[]; ``` @@ -1932,54 +2071,57 @@ type Waiting = ('compiling' | 'data' | 'derived' | 'labels' | 'fit' | 'marks')[] ### Work ```ts -/** What a `Gpu` did since it was made; subtract two for what happened between. */ +/** + * Counts of the work a `Gpu` has done since it was created. Subtract two snapshots to get the work + * between them. + */ interface Work { /** Bytes of data pages uploaded. */ uploaded: number; /** - * Data pages, or pieces of them, uploaded: what a change uploads counts its pages here, and its - * bytes in `uploaded`. + * Data pages, or pieces of pages, uploaded. A change's uploads count pages here and bytes in + * `uploaded`. */ uploads: number; - /** Bytes written besides pages: uniforms, programs, label runs, the selection, textures. */ + /** Bytes written other than pages: uniforms, programs, label runs, the selection, and textures. */ written: number; /** Rows whose channels the GPU evaluated. */ evaluated: number; /** - * Rows of edges and faces whose ends the GPU found where their vertices are held: rows that - * changed, and every row of a page of them naming a vertex now held elsewhere. + * Edge and face rows whose endpoints the GPU resolved to their vertices' resident rows: rows that + * changed, plus every row of a page that references a vertex now resident elsewhere. */ resolved: number; /** - * Bytes of pages of words per row the GPU cleared: those read before anything writes them, and the - * normals faces sum afresh. + * Bytes of per-row word pages the GPU cleared: pages read before anything writes them, and the + * normals that faces sum from scratch. */ cleared: number; /** - * Rows packed into pages of the order a table drawn in part draws in: each page once, the first - * time a view draws it, and again only where a write changed its rows. + * Rows packed into pages of a paged table's draw order. Each page is packed once, the first time a + * view draws it, and again only when a write changes its rows. */ packed: number; /** - * Reads back from the GPU: probes, queries, fits, label placements, segment counts, pictures; not - * the frame timer's, which only some devices have. + * Readbacks from the GPU: probes, queries, fits, label placements, segment counts, and pictures. + * Frame timer readbacks, which only some devices support, are not counted. */ readbacks: number; /** - * Jobs handed out: layouts, routes, lanes, triangulations, pyramids, tiles; to the host's workers, - * or done at once without a host. + * Jobs started: layouts, routes, lanes, triangulations, pyramids, and tiles. They run on the + * host's workers, or immediately without a host. */ jobs: number; - /** Label texts the host was asked to set. */ + /** Label texts the host was asked to rasterize. */ texts: number; /** - * Pipelines compiled: in a browser, each as a view or a shot asks for it ahead, off the GPU's - * thread; natively, where it is first made. + * Pipelines compiled. In a browser, each compiles in the background when a view or shot requests + * it ahead of use. Natively, each compiles when first created. */ pipelines: number; /** - * Pipelines made without being asked for ahead, each compiled where it is first used: in a browser - * on the GPU's thread, holding frames back. None, unless a scene uses what it did not ask for. + * Pipelines not requested ahead of use, compiled when first used. In a browser they compile on the + * GPU's thread and delay frames. Zero unless a scene uses a pipeline it did not request. */ late: number; } diff --git a/npm/README.md b/npm/README.md index 39196c02..597e0b9d 100644 --- a/npm/README.md +++ b/npm/README.md @@ -71,6 +71,6 @@ Serve WebAssembly as `application/wasm`, in a secure context for WebGPU. From the repository: `cargo xtask wasm --dev` builds the package for development, and `cargo xtask package` builds an optimized tarball under `target/npm`. -LATK v1 uses 64-bit frame indexes. Sample values use `f32` and axis coordinates use `f64`. -`Video.record()` cleans up on failure; manual recordings must finish or abort. -See [deployment and platform support](https://latkit.readthedocs.io/en/latest/deployment.html). +The binary format uses 64-bit frame indexes. Sample values use `f32` and axis coordinates use +`f64`. `Video.record()` cleans up on failure; manual recordings must finish or abort. +See [deploying](https://latkit.readthedocs.io/en/latest/guide.html#deploying). diff --git a/npm/build.mjs b/npm/build.mjs index be55cca8..31d16ae8 100644 --- a/npm/build.mjs +++ b/npm/build.mjs @@ -1,4 +1,5 @@ -/** Builds from pinned workspace tools; all release and consumer outputs stay below target/npm. */ +/** Build steps of the npm package, run with the workspace's pinned tools. Release and consumer + * outputs all stay under target/npm. */ import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { createHash } from 'node:crypto'; @@ -55,7 +56,7 @@ if (mode === 'prepare') { assert.equal( fs.readFileSync(path.join(stage, `src/${name}.ts`), 'utf8').replaceAll('\r\n', '\n'), source, - `src/${name}.ts is not what npm/schema.json says: run node npm/build.mjs schema`, + `src/${name}.ts is out of date with npm/schema.json: run node npm/build.mjs schema`, ); } execFileSync( @@ -85,7 +86,7 @@ if (mode === 'prepare') { format: 'iife', platform: 'browser', target: 'es2022', - // wasm-bindgen's unused async initializer mentions import.meta; initSync doesn't need it. + // wasm-bindgen's unused async initializer mentions import.meta, which initSync does not need. logOverride: { 'empty-import-meta': 'silent' }, logLevel: 'warning', }); diff --git a/npm/docs.mjs b/npm/docs.mjs index 028b08ab..69934a60 100644 --- a/npm/docs.mjs +++ b/npm/docs.mjs @@ -1,4 +1,8 @@ -/** Checked-in reference snapshots let Read the Docs build with Python alone. */ +/** + * Writes the JavaScript reference pages in `docs/reference` from the package's declarations, or + * with `--check` asserts that the checked-in pages are current. The pages are checked in so Read + * the Docs can build with Python alone. + */ import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; @@ -9,9 +13,20 @@ const root = path.resolve(import.meta.dirname, '..'); const check = process.argv.includes('--check'); const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); -/** Each declaration of `file` by name, as its signature. */ +/** + * `text` with its first comment blanked out. Every module opens with a comment about itself, and + * where the declarations drop the imports below it, it would read as the first declaration's doc. + */ +function withoutModuleDoc(text) { + const comments = ts.getLeadingCommentRanges(text, 0) ?? []; + const first = comments.find((range) => !text.startsWith('///', range.pos)); + if (!first) return text; + return text.slice(0, first.pos) + ' '.repeat(first.end - first.pos) + text.slice(first.end); +} + +/** Maps each top-level declaration of `file` by name to its printed signature. */ function declarations(file) { - const text = fs.readFileSync(path.join(root, file), 'utf8'); + const text = withoutModuleDoc(fs.readFileSync(path.join(root, file), 'utf8')); const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true); const found = new Map(); for (const node of source.statements) { @@ -30,15 +45,16 @@ function declarations(file) { .replace(/^declare /gm, '') .replace(/^\s*(?:#private;|private .*;)\s*$/gm, '') .trim(); - // An overloaded function's signatures, each in turn. + // An overloaded function keeps every signature, in order. found.set(name, found.has(name) ? `${found.get(name)}\n${signature}` : signature); } return found; } /** - * A page of `files`' declarations under `head`, in `sections`: each a heading, what it holds, and - * its declarations in reading order. Every declaration is in one section, so none goes unlisted. + * Builds a reference page of the declarations in `files`: `head`, then each section of `sections` + * as `[title, description, names]`, with its declarations in the order listed. Throws if a + * declaration is in no section or a section names a declaration that does not exist. */ function page(head, files, sections) { const all = new Map(files.flatMap((file) => [...declarations(file)])); @@ -46,7 +62,7 @@ function page(head, files, sections) { const unlisted = [...all.keys()].filter((name) => !listed.includes(name)); const missing = listed.filter((name) => !all.has(name)); assert.deepEqual(unlisted, [], `npm/docs.mjs lists no section for ${unlisted.join(', ')}`); - assert.deepEqual(missing, [], `npm/docs.mjs lists what the package lacks: ${missing.join(', ')}`); + assert.deepEqual(missing, [], `npm/docs.mjs lists undeclared names: ${missing.join(', ')}`); return ( head + sections @@ -61,19 +77,19 @@ function page(head, files, sections) { const pages = { 'api.md': page( - '# JavaScript API\n\nGenerated from the shipped declarations. `load()` loads the WebAssembly that datasets, views,\nand pictures are made with; scenes, expressions, and patches need none. Options take the [scene\nand result types](types.md).\n\n', + '# JavaScript API\n\nGenerated from the shipped declarations. `load()` loads the WebAssembly that datasets, views,\nand pictures need. Scenes, expressions, and patches do not need it. Options use the [scene and\nresult types](types.md).\n\n', ['load', 'internal', 'data', 'patch', 'scene', 'colormap', 'gpu', 'view', 'canvas', 'shot'].map( (name) => `npm/dist/${name}.d.ts`, ), [ [ 'Loading and waiting', - 'Load the WebAssembly once a page; every wait ends at its signal.', + 'Load the WebAssembly once per page. A wait ends when its signal aborts (30 s by default).', ['load', 'LoadOptions', 'Wait', 'LatkitError'], ], [ 'Data and patches', - 'Datasets of named tables, changed by patches written without WebAssembly, and what views draw them from.', + 'Datasets of named tables, the patches that change them (written without WebAssembly), and the sources views draw.', [ 'Data', 'Snapshot', @@ -95,7 +111,7 @@ const pages = { ], [ 'Expressions', - 'What varies by row: builders writing the expressions and colors scenes take.', + 'Builders for the per-row expressions and colors that scene options take.', [ 'Expr', 'ExprInput', @@ -156,7 +172,7 @@ const pages = { [ [ 'Scenes', - 'A scene: its space, how it answers input, and how it is laid out.', + 'A scene: its space, how it responds to input, and how it is laid out.', [ 'Scene', 'Space', @@ -201,7 +217,7 @@ const pages = { ], [ 'Marks', - 'What a scene draws each table as.', + 'The marks that draw the tables of a scene.', [ 'Mark', 'Vertices', @@ -224,7 +240,7 @@ const pages = { ], [ 'Expressions', - 'Expressions as JSON: a constant, a field, or a node of these kinds; and colors, which take CSS strings where values take field names.', + 'Expressions as JSON: a constant, a field, or one of these nodes. Colors take CSS strings where values take field names.', [ 'ExprNode', 'PaintNode', @@ -251,12 +267,12 @@ const pages = { ], [ 'Rows, cameras, and queries', - 'Naming rows, framing them, and asking for them.', + 'Identifying rows, framing them, and querying them.', ['Id', 'Ids', 'Row', 'Rows', 'Camera', 'Fit', 'Query', 'EnvelopeRequest'], ], [ 'Results', - 'What views tell: events, hits, descriptions, and what frames cost.', + 'View events and hits, dataset descriptions and values, and frame statistics.', [ 'ViewEvent', 'Hit', @@ -287,8 +303,8 @@ for (const [name, content] of Object.entries(pages)) { ...(await prettier.resolveConfig(file)), parser: 'markdown', }); - // The reference names things as npm does: no path of Rust's, and in the types the schema writes - // from Rust's documentation, no call of Rust's. + // The reference must use npm's names. No line may contain a Rust path (`::`), and types.md, + // which the schema writes from Rust's documentation, may not contain a Rust call (`name()`). const rust = name === 'types.md' ? /::|\w\(\)/ : /::/; const leftover = formatted.split('\n').filter((line) => rust.test(line)); assert.deepEqual(leftover, [], `${file}: Rust's names in the TypeScript reference`); diff --git a/npm/examples/gate/browser.ts b/npm/examples/gate/browser.ts index 19966c58..ec1e7ec0 100644 --- a/npm/examples/gate/browser.ts +++ b/npm/examples/gate/browser.ts @@ -1,5 +1,5 @@ /** - * What the gate and the benchmark share: Chrome to drive, and deadlines every wait races. + * Helpers shared by the gate and the benchmark: launching Chrome, and deadlines for every wait. */ import fs from 'node:fs'; @@ -10,7 +10,7 @@ import { chromium, type Browser } from 'playwright-core'; /** Milliseconds any one step may take before giving up on it. */ export const STEP = 60_000; -/** Rejects after `ms` with what timed out. */ +/** Races `work` against a timer that rejects after `ms` with an error naming `step`. */ export function deadline(work: Promise, ms: number, step: string): Promise { let timer: NodeJS.Timeout | undefined; const late = new Promise((_, reject) => { @@ -19,7 +19,8 @@ export function deadline(work: Promise, ms: number, step: string): Promise return Promise.race([work, late]).finally(() => clearTimeout(timer)); } -/** Chrome to drive: `$LATKIT_CHROME`, or the newest Chromium in Playwright's browser cache. */ +/** Returns the path of the Chrome to drive: `$LATKIT_CHROME`, or else the newest Chromium in + * Playwright's browser cache. Throws if there is none. */ export function chrome(): string { const given = process.env.LATKIT_CHROME; if (given) return given; @@ -55,20 +56,20 @@ export function chrome(): string { ); } -/** A Chrome being driven, and how it ends. */ +/** A launched Chrome. */ export interface Chrome { browser: Browser; /** - * Ends Chrome: politely within seconds, else by killing its processes. A page stuck on its GPU - * never closes politely, and a Chrome left behind keeps the GPU busy under the next one. + * Closes Chrome, or kills its processes if it has not closed within 5 s. A page stuck on the GPU + * never closes normally, and a leftover Chrome would keep the GPU busy during the next one. */ close(): Promise; } /** - * WebGPU on SwiftShader, which draws on the CPU, so no shader can hang the machine's GPU. These - * override Chrome's own GPU checks, so a real GPU never gets them. ANGLE draws on SwiftShader too on - * Linux, as its GPU-less CI needs; on Windows that leaves WebGPU no adapter at all. + * Flags that run WebGPU on SwiftShader, which draws on the CPU, so no shader can hang the machine's + * GPU. They override Chrome's own GPU checks, so they are never passed with a real GPU. On Linux, + * ANGLE uses SwiftShader too, as GPU-less CI needs. On Windows that flag leaves WebGPU no adapter. */ const SOFTWARE = [ '--enable-unsafe-webgpu', @@ -81,13 +82,14 @@ const SOFTWARE = [ ]; /** - * A machine with two GPUs draws on its fast one, every launch: Chrome ignores WebGPU's - * `powerPreference` on Windows, and picks either otherwise, so times and limit-bound work would - * compare two devices. + * Makes a machine with two GPUs draw on the fast one on every launch. Chrome ignores WebGPU's + * `powerPreference` on Windows and may pick either GPU elsewhere, so times and limit-bound work + * would otherwise compare two devices. */ const HARDWARE = ['--force_high_performance_gpu']; -/** Chrome, headless, with `args` besides: on SwiftShader when `software`, as Linux always is. */ +/** Launches headless Chrome with the extra `args`. It draws on SwiftShader when `software`, which + * defaults to true on Linux. */ export async function launch( args: readonly string[] = [], software = process.platform === 'linux', diff --git a/npm/examples/gate/gate.ts b/npm/examples/gate/gate.ts index 440cee06..70a7eca7 100644 --- a/npm/examples/gate/gate.ts +++ b/npm/examples/gate/gate.ts @@ -1,18 +1,19 @@ /** - * The browser gate. Times every shader variant's pipelines in Chrome against a budget, then opens - * every page of the suite in a fresh browser and moves the pointer over it. It fails on a pipeline - * over budget or failing to compile, a console error, a page that draws no frame in time, a page - * missing frames in a row while it opens or under the pointer, or a pipeline compiled where first - * used. A page late, drawing no frame in time or missing frames, opens again, cold: it fails late - * only on every open, as a stall of the machine slows one open and a slow page all. Every wait races - * a deadline. + * The browser gate. It times the pipelines of every shader variant in Chrome against a budget, then + * opens each page of the suite in a fresh browser and moves the pointer over it. * - * On SwiftShader (`--software`, as Linux always is), the CPU draws, slower than any budget, so the - * gate only checks that every pipeline compiles: pages and times are judged on a GPU. + * A pipeline fails if it does not compile or is over budget. A page fails on a console or page + * error, a pipeline compiled at first use, or no exported picture. A page is slow if it draws no + * frame in time, or misses frames in a row while it opens or under the pointer. A slow page is + * opened again, cold, and fails only if every open is slow: a machine stall slows one open, but a + * slow page slows them all. Every wait races a deadline. * - * `node gate/gate.ts [--software] [page...]`; `cargo xtask browser` runs - * it. Naming pages visits only those, without timing the shaders. Chrome is `$LATKIT_CHROME`, or the - * newest Chromium Playwright installed. + * On SwiftShader (`--software`, and always on Linux) the CPU draws, slower than any budget, so the + * gate only checks that every pipeline compiles. Pages and times are judged only on a GPU. + * + * Usage: `node gate/gate.ts [--software] [page...]`, which + * `cargo xtask browser` runs. Naming pages visits only those and skips timing the shaders. Chrome + * is `$LATKIT_CHROME`, or else the newest Chromium that Playwright installed. */ import fs from 'node:fs'; @@ -22,31 +23,31 @@ import type { Page } from 'playwright-core'; import { createServer } from 'vite'; import { STEP, deadline, launch, type Chrome } from './browser.ts'; -/** Milliseconds Chrome may take to compile any one pipeline, cold: the slowest takes about 0.35 s, - * and a view opens no sooner than the slowest it uses, the rest compiling beside it. */ +/** Maximum milliseconds Chrome may take to compile one pipeline cold. The slowest takes about + * 0.35 s. A view opens no sooner than its slowest pipeline; the others compile in parallel. */ const COMPILE_BUDGET = 800; -/** Milliseconds from opening a page, cold, to its views' first frames, which draw everything their - * scenes use: every pipeline compiles side by side, off Chrome's GPU thread. */ +/** Maximum milliseconds from opening a page cold to its views' first frames. A first frame draws + * everything its scene uses. Every pipeline compiles in parallel, off Chrome's GPU thread. */ const FIRST_FRAME_BUDGET = 1000; -/** Display frames in a row a page may miss while it opens or the pointer moves over it. Frames come - * at the display's pace, which the machine sets: 60 Hz on a panel, 32 Hz over Remote Desktop, whose - * vblanks slip one at a time. Nothing compiles on Chrome's GPU thread, so no frame waits on latkit; - * what it compiles where first used is counted apart, as `work.late`. */ +/** Maximum consecutive display frames a page may miss while it opens or under the pointer. The + * machine sets the display rate: 60 Hz on a panel, 32 Hz over Remote Desktop, where single vblanks + * slip. Nothing compiles on Chrome's GPU thread, so no frame waits on latkit. Pipelines compiled at + * first use are counted separately, as `work.late`. */ const MISSED = 1; -/** Milliseconds between display frames past which the gate tells what the views were doing. */ +/** Milliseconds between display frames beyond which the gate logs what the views were doing. */ const LONG = 50; -/** Cold opens a page may take to draw its first frame in time and keep its frames. What any open - * does wrong fails at once. */ +/** Maximum cold opens per page to draw its first frame in time without missing frames. An error on + * any open fails the page at once. */ const OPENS = 2; -/** What an open of a page did wrong, and how it was late. */ +/** The failures of one open of a page: its errors, and the ways it was slow. */ interface Opened { wrong: string[]; slow: string[]; } -/** A gap between display frames past `LONG`: how long, each view's last frame on the CPU and the - * GPU and what it waited on, and the work the GPU did since the frame before. */ +/** A gap between display frames longer than `LONG`. It records each view's last CPU and GPU frame + * times and `waiting` list, and the GPU work done since the previous frame. */ interface Long { gap: number; cpuMs: number[]; @@ -60,7 +61,7 @@ const examples = path.dirname(here); const args = process.argv.slice(2); const software = args.includes('--software') || process.platform === 'linux'; -/** A variant of shaders.json: its file, whether it draws, its compute entries, and its constants. */ +/** A shader variant listed in shaders.json. `append` is source added after the file's own. */ interface Variant { file: string; draws: boolean; @@ -84,12 +85,14 @@ interface Timing { error?: string; } -/** Chrome compiling cold on every launch, as a first visit does. */ +/** Launches Chrome without its shader disk cache, so every pipeline compiles cold, as on a first + * visit. */ function cold(): Promise { return launch(['--disable-gpu-shader-disk-cache'], software); } -/** Every pipeline the variants in `dir` make: each compute entry, or the draw, per constants. */ +/** Lists the pipelines of the variants in `dir`: one per compute entry point, or one draw, for each + * set of constants. */ function pipelines(dir: string): Pipeline[] { const variants = JSON.parse(fs.readFileSync(path.join(dir, 'shaders.json'), 'utf8')) as Variant[]; return variants.flatMap((variant) => { @@ -109,7 +112,8 @@ function pipelines(dir: string): Pipeline[] { }); } -/** In the page: makes each pipeline cold, one at a time, and times it. Runs from its source alone. */ +/** Runs in the page. Creates each pipeline cold, sequentially, and times it. Only its source + * reaches the page, so it must be self-contained. */ async function time({ pipelines, step, @@ -146,7 +150,7 @@ async function time({ if (!module) modules.set(pipeline.code, (module = device.createShaderModule({ code: pipeline.code }))); const constants = pipeline.constants; - // Draws test against depth, as latkit's do; the backdrop writes it. + // Like latkit's draw pipelines, these test against depth, which the backdrop writes. const made = pipeline.draws ? device.createRenderPipelineAsync({ layout: 'auto', @@ -188,7 +192,8 @@ async function time({ return timings; } -/** Times every pipeline against the budget; returns the failures. */ +/** Times every pipeline and returns the failures: compile errors and, on a GPU, pipelines over + * `COMPILE_BUDGET`. */ async function compileBudget(dir: string): Promise { const blank = http.createServer((_, response) => response.end('gate'), @@ -229,10 +234,10 @@ function frames(page: Page): Promise { return deadline(two, STEP, 'animation frames'); } -/** Waits, for at most `budget` ms, until every view of `page` has opened: it was complete, or it drew - * 30 frames waiting on nothing but its fit, as a plot of streaming data measuring its value range - * again does every frame. A view draws once every pipeline its scene uses compiled, so then nothing - * is left to compile. */ +/** Waits up to `budget` ms until every view of `page` has opened. A view has opened once nothing is + * waiting, or once it has drawn 30 frames waiting only on `fit`, since a plot of streaming data + * refits its value range every frame. A view draws only after every pipeline its scene uses has + * compiled, so nothing is left to compile by then. */ async function settled(page: Page, budget: number): Promise { await page.waitForFunction( () => { @@ -257,8 +262,8 @@ async function settled(page: Page, budget: number): Promise { ); } -/** Opens page `id` until an open is in time, at most `OPENS` times: what an open did wrong, else - * how the last was late. */ +/** Opens page `id` until an open is in time or has errors, at most `OPENS` times. Returns the + * failures of the last open. */ async function visit(url: string, id: string): Promise { for (let n = 1; ; n++) { const { wrong, slow } = await open(url, id); @@ -267,8 +272,9 @@ async function visit(url: string, id: string): Promise { } } -/** Opens page `id` in a fresh browser, waits for its first frames, then moves the pointer over - * every view: hovering a grid of points, zooming, panning, and clicking. */ +/** Opens page `id` in a fresh browser and waits for its first frames. Then it works every view with + * the pointer and keys (hovering a grid of points, zooming, dragging, clicking, right-clicking, and + * stepping with Tab and the arrows) and exports a picture. */ async function open(url: string, id: string): Promise { const opened: Opened = { wrong: [], slow: [] }; const { wrong, slow } = opened; @@ -283,8 +289,9 @@ async function open(url: string, id: string): Promise { if (message.type() === 'error') wrong.push(`console: ${message.text().slice(0, 300)}`); }); page.on('pageerror', (error) => wrong.push(`page: ${error.message.slice(0, 300)}`)); - // The longest gap between animation frames since the last reset, and the display's frame - // interval: the median of the latest gaps; and each gap past `LONG`, with what the views did. + // Tracks the longest gap between animation frames since the last reset, the display's frame + // interval (the median of the last 120 gaps), and each gap longer than `LONG` with what the + // views were doing. await page.addInitScript((long: number) => { interface Stage { views: { stats: { cpuMs: number; gpuMs: number }; waiting: string[] }[]; @@ -335,14 +342,14 @@ async function open(url: string, id: string): Promise { }; requestAnimationFrame(tick); }, LONG); - /** Tells the frames past `LONG` since the last reset, while `doing`. */ + /** Logs each frame longer than `LONG` since the last reset, labeled with `doing`. */ const tell = async (doing: string) => { const long = await page.evaluate(() => (window as unknown as { long: Long[] }).long); for (const frame of long) console.log(` ${id}: ${describe(frame)} ${doing}`); }; const started = performance.now(); await deadline(page.goto(`${url}#${id}`), STEP, `open ${id}`); - /** Waits until every view has drawn more than `frames` frames, for at most `budget` ms. */ + /** Waits up to `budget` ms until every view has drawn more than `frames` frames. */ const drawn = (frames: number, budget: number) => page.waitForFunction( (frames) => { @@ -365,16 +372,16 @@ async function open(url: string, id: string): Promise { return opened; } const first = performance.now() - started; - // What stalls while the views open, from their first frames: what the page did loading before - // them shows nothing to stall. + // Stalls count from the views' first frames, since the page shows nothing that could stall + // while it loads. await page.evaluate(() => (window as unknown as { resetGap(): void }).resetGap()); await deadline(settled(page, STEP), STEP + 1000, `${id}'s opening`); await deadline(page.waitForTimeout(500), STEP, 'settle'); const opening = await page.evaluate(() => (window as unknown as { gap: number }).gap); await tell('opening'); let longest = { gap: 0, step: '' }; - /** Does `step` and waits two frames; keeps the longest gap between frames and its step, and - * tells each frame past `LONG`. */ + /** Performs `step` and waits two frames. Records the longest gap between frames with its step, + * and logs each frame longer than `LONG`. */ const act = async (step: string, action: () => Promise) => { await page.evaluate(() => (window as unknown as { resetGap(): void }).resetGap()); await deadline(action(), STEP, step); @@ -405,15 +412,16 @@ async function open(url: string, id: string): Promise { await page.mouse.up(); }); await act(`click canvas ${c}`, () => page.mouse.click(cx, cy)); - // The intents whose picks list what lies under a point, and the keys stepping through rows. + // Right-click raises a menu intent listing what lies under the point. Tab and the arrow keys + // step through rows. await act(`menu on canvas ${c}`, () => page.mouse.click(cx, cy, { button: 'right' })); await act(`step with Tab on canvas ${c}`, () => page.keyboard.press('Tab')); await act(`step with an arrow on canvas ${c}`, () => page.keyboard.press('ArrowRight')); await act(`stop stepping on canvas ${c}`, () => page.keyboard.press('Escape')); } - // Every page exports a picture of its views: a shot draws them and a worker writes the PNG. + // Every page exports a picture of its views. A shot draws them and a worker writes the PNG. await page.locator('main canvas').first().focus(); - await deadline(page.keyboard.press('x'), STEP, 'ask for a picture'); + await deadline(page.keyboard.press('x'), STEP, 'press x to export a picture'); const saved = page.waitForFunction( () => document.body.textContent?.includes('Saved views.png'), undefined, @@ -425,7 +433,7 @@ async function open(url: string, id: string): Promise { try { await deadline(saved, STEP + 1000, `${id}'s picture`); } catch { - // What the views waited on, and what the page said, name why. + // Explain the failure with the views' `waiting` lists and the page's status text. const said = await page.evaluate(() => { const views = (window as unknown as { stage?: { views: { waiting: string[] }[] } }).stage ?.views; @@ -433,7 +441,7 @@ async function open(url: string, id: string): Promise { const status = [...document.querySelectorAll('.status, [role=status], output')] .map((element) => element.textContent?.trim()) .filter(Boolean); - return `views waiting on ${waiting.join(', ')}; the page said ${status.join(' | ') || 'nothing'}`; + return `views waiting on ${waiting.join(', ')}; page status: ${status.join(' | ') || 'nothing'}`; }); wrong.push(`no picture exported: ${said}`); } @@ -445,7 +453,8 @@ async function open(url: string, id: string): Promise { }; return { interval: w.interval(), late: w.stage?.gpu.work.late ?? 0 }; }); - // A gap of n intervals missed n - 1 frames; past MISSED + 1.5 intervals, more than MISSED did. + // A gap of n intervals missed n - 1 frames, so a gap over MISSED + 1.5 intervals missed more + // than MISSED. const budget = (MISSED + 1.5) * interval; console.log( `${id}: first frame ${first.toFixed(0)} ms, frames every ${interval.toFixed(0)} ms, longest while ` + @@ -457,15 +466,15 @@ async function open(url: string, id: string): Promise { if (gap > budget) slow.push(`missed ${missed(gap)} frames in a row on ${step} (${gap.toFixed(0)} ms)`); if (late > 0) - wrong.push(`${late} pipelines compiled where first used, holding back the frames using them`); + wrong.push(`${late} pipelines compiled at first use, delaying the frames that use them`); } finally { await chrome.close(); } return opened; } -/** A frame past `LONG` as the gate tells it: what each view's last frame took on the CPU and the GPU - * and waited on, and the work the GPU did, bytes in MB. */ +/** Formats a frame longer than `LONG` for the log: each view's last CPU and GPU times and `waiting` + * list, and the GPU work done, with byte counts in MB. */ function describe({ gap, cpuMs, gpuMs, waiting, work }: Long): string { const ms = (list: number[]) => list.map((n) => n.toFixed(1)).join('/'); const waited = waiting.map((list) => list.join('+') || 'nothing').join(', '); @@ -476,7 +485,8 @@ function describe({ gap, cpuMs, gpuMs, waiting, work }: Long): string { return `frame of ${gap.toFixed(0)} ms (cpu ${ms(cpuMs)} ms, gpu ${ms(gpuMs)} ms, waiting on ${waited}; ${done || 'no work'})`; } -/** Serves the suite and visits `only` its pages, or every page its bar lists. */ +/** Serves the suite and visits the pages named in `only`, or, if it is empty, every page in the + * example picker. */ async function suite(only: string[]): Promise { const server = await createServer({ root: examples, @@ -517,7 +527,7 @@ if (!dir) { process.exit(2); } if (software) - console.log('SwiftShader draws on the CPU: pipelines must compile, untimed; no pages'); + console.log('SwiftShader draws on the CPU, so the gate only checks that pipelines compile'); const shaders = only.length && !software ? [] : (await compileBudget(dir)).map((f) => `shaders: ${f}`); const failures = [...shaders, ...(software ? [] : await suite(only))]; diff --git a/npm/examples/public/history/0/0/0.latk b/npm/examples/public/history/0/0/0.latk index 5fc4d2408e28129ac01f66f7ed59b126c4b07b3c..2f14e0121f6f6f15ed6dc4506f2982e028332a38 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/1.latk b/npm/examples/public/history/0/0/1.latk index cb0e05755be80c4f4846f6703104f0ddf7928d0e..5c5b092b0b0c4fac76ffff9b30f755ccc2fa6b58 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/10.latk b/npm/examples/public/history/0/0/10.latk index cf031e6ea6c386f5a5857d52e888648415879484..75314ca04a563bf4e6f5664b4c5d8401813af6b3 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/11.latk b/npm/examples/public/history/0/0/11.latk index 8b12d9fad8b2f050af9b1d5d23e7ec89d53ecf01..f593b0f4ef34c76bf14ad7579d832321eede1c54 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/12.latk b/npm/examples/public/history/0/0/12.latk index 4d3dbbe8d44370780a3609c7541aeb2e46939ce9..23d7cefe64da3a0a33fee0f34b37ee29a4e1fda6 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/13.latk b/npm/examples/public/history/0/0/13.latk index 38ca0acc678bd0073304d8fbbe719f50d1d0a78f..c7c5ad994b841b0a77d0fb7cc85eff68e567dbe1 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/14.latk b/npm/examples/public/history/0/0/14.latk index f4e9a9191715c0bbb0ca65ed912928ce81439c71..5f63fdda145213e2a116de53d6d2c51e59caafcf 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/15.latk b/npm/examples/public/history/0/0/15.latk index e906eb1747b2591895f685bef45a5fdc969244bb..5a33799ca6c5d207adbcec9688b0fb5eefb0834e 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/16.latk b/npm/examples/public/history/0/0/16.latk index c381a7bb0f8393789f3ac9d76e8a320f627fd7ea..caae08ac0248a9a54acf4e7f1dcc61549c08321a 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/17.latk b/npm/examples/public/history/0/0/17.latk index 65acd61c8c1a8d50ca0fce5a8921c0f19a1f4f27..3ec93f68bc5efb217428dbcab5a9f020f913515f 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/18.latk b/npm/examples/public/history/0/0/18.latk index 94be7eb6f878593a0ef83585b8001456514da3a3..65a2737d6b201dbe14b2ee04dc25b2ede82b70e3 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/19.latk b/npm/examples/public/history/0/0/19.latk index c1f97dae330c4676cc205b3cf7ad2b6b218e489d..d77381eaa7c776426b5dca658f0644512566d23d 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/2.latk b/npm/examples/public/history/0/0/2.latk index bc899b42241b1a698189ec1c8c186fa76815913c..52bae85c5925922350765879587293603476e6ed 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/20.latk b/npm/examples/public/history/0/0/20.latk index 77b2d43ea248d9237da6866843a95917fb644a33..bdf2ee393418806c02d404b37e4813ee225c5e27 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/21.latk b/npm/examples/public/history/0/0/21.latk index 9e2fa2d902356d06c884bd1af1a8f35f69b744b6..22895c40fb16cc28e30dc77a1661a7dccefd6d06 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/22.latk b/npm/examples/public/history/0/0/22.latk index 98990f260afbbfa7bf73cebe1b16f671643a5bb1..a8b339019956fe37538f271519f3df30d9e00500 100644 GIT binary patch delta 16 XcmX@q!L+Y~iPgt3#M^Eor)3KOGG7II delta 20 bcmdnj!E~sDiPOh1#G8?Ufx&4bt7QuSL9_*c diff --git a/npm/examples/public/history/0/0/3.latk b/npm/examples/public/history/0/0/3.latk index 50781c674bc0f93747fd6a0acaace46c5b13c339..fd09fe0fa1e6d31eb845bd5af2345fea60a1d4e1 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/4.latk b/npm/examples/public/history/0/0/4.latk index 859cf0e65ca269b7e4cd99e7fe12ffda447821ff..ceee2a6d4124258b8a87f1b10e87b1a79a65c09c 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/5.latk b/npm/examples/public/history/0/0/5.latk index da22022ca0ca4cfaafb75261dba6c68daddab80d..b9f43154fef69a9ed8787beaaa8a0bf3684bd847 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/6.latk b/npm/examples/public/history/0/0/6.latk index 57a17054d267aa379877dd746168760892166aa5..c317e1d56ef961ea45334ba3ceaaed9631aa03c2 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/7.latk b/npm/examples/public/history/0/0/7.latk index fbd6ad737f605cd45a5eb98268eb6ee23a06bbe3..05f0f973b25d03a59454ee4f69f564619966cefa 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/8.latk b/npm/examples/public/history/0/0/8.latk index 81a541f9f128541c3da412017162dcba0a60e428..e8c2aa87400c9d6c8ba1c323ada20ba970c6a474 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/0/9.latk b/npm/examples/public/history/0/0/9.latk index aabd88c0f8942e9eb37baaaf9e61a850a6b58df7..08aada514a923ccaf5b8c6e64dfb4f1eadc2c55f 100644 GIT binary patch delta 19 acmX>!fo0zW7FHj}5O2Fi&aIq`mK*>>!v&%M delta 23 ecmdltf#uKy7ET|>5N}2X1_q}_)~&3JmK*?6X9c4G diff --git a/npm/examples/public/history/0/1/0.latk b/npm/examples/public/history/0/1/0.latk index 3fefca912530094126c5f5a2575da1249066355e..d90888b30e9aac7fcfcab338a4107127e707f6dd 100644 GIT binary patch delta 22 dcmX@KfMeeR4ptw>5O2Fi&Q?ywt(;7j0svVh24VmJ delta 26 hcmdnDfaA~t4o)A(5N}2X1_q}_)>c-=t*lIz0sv`T244UG diff --git a/npm/examples/public/history/0/1/1.latk b/npm/examples/public/history/0/1/1.latk index 1d4eba4a419c12aedc19a577e61b0da04df16e5c..729c75fb1acb055205e757051e66ab4df279fda2 100644 GIT binary patch delta 22 dcmX@KfMeeR4ptw>5O2Fi&Q?ywt(;7j0svVh24VmJ delta 26 hcmdnDfaA~t4o)A(5N}2X1_q}_)>c-=t*lIz0sv`T244UG diff --git a/npm/examples/public/history/0/1/2.latk b/npm/examples/public/history/0/1/2.latk index 8c2e8cad8548867d17747a910cf368d9b7a2e44b..f0ed070a7365ee583b01187edb36b6ff0dc3a9a5 100644 GIT binary patch delta 19 acmX?fi*4U6HdY_U5O2Fi&aIq`mTLh{Fb3ZM delta 23 ecmdmYi|x=YHclVM5N}2X1_q}_)~&3JmTLiEGzQ=R diff --git a/npm/examples/public/history/0/2/0.latk b/npm/examples/public/history/0/2/0.latk index fa565fba06b7a9684c730267e51edcb0a885e83b..b4f4e724ff0e81f9744c31467d5b44dcf62833a3 100644 GIT binary patch delta 16 YcmaF#nd#nVCRQKE5bt{%IdAR+07cyh1^@s6 delta 20 ccmccjnd#AICQcv65N}2X28Kr)S#RzH09lF%3jhEB diff --git a/npm/examples/public/history/0/3/0.latk b/npm/examples/public/history/0/3/0.latk index b4142238a58289e4e025e3c57fcb5e4daecd2f2b..efc4061499ede997a7db8a1b36d6a85b1f1fa9de 100644 GIT binary patch delta 14 VcmeyQ`%ahD$1%kF-A2xrVgN4-1`_}P delta 18 ZcmaE-`$?D6$1%j4k%58X(?-^pVgNk+1{eSU diff --git a/npm/examples/public/history/0/4/0.latk b/npm/examples/public/history/0/4/0.latk index 7cf635ecea3a29d53de92ad079d1a9243b3cc7ca..344785e507a7f5574afe862ed5b20e511f3d315a 100644 GIT binary patch delta 14 Vcmcb_a*l=7$1%j)ZzHEC696NB1T6pn delta 18 ZcmX@da*2i0$1%j4k%56BXd|m9696wL1Tp{s diff --git a/npm/examples/public/history/base.latk b/npm/examples/public/history/base.latk index dd827ea7c88a12205928348b73335693d8774635..14ea9dc9e3f5b7029889a30d84d48356b8f08bcc 100644 GIT binary patch delta 14 VcmX@cb%=}A$1%j)X(OjCD*z&f1PlNG delta 18 ZcmX@ab&QMC$1%j4k%581Z6m8KD*!G31Q7rL diff --git a/npm/examples/scripts/history.ts b/npm/examples/scripts/history.ts index 46fe3a37..707e08df 100644 --- a/npm/examples/scripts/history.ts +++ b/npm/examples/scripts/history.ts @@ -1,8 +1,9 @@ /** - * Writes the recorded day the Live data page reads as time tiles, into `public/history/`: the voltages - * of 64 buses in six areas, every 15 s for a day, as a load curve swings them and three disturbances - * pull areas down and recover. Deterministic: run it again and the same bytes come out. Run it from - * the examples with `node scripts/history.ts`; it tiles the day with the repository's `latkit tile`. + * Writes the recorded day that the Live data page reads as time tiles, into `public/history/`. It + * contains the voltages of 64 buses in six areas, every 15 s for a day. A load curve swings them, + * and three disturbances pull areas down before they recover. Every run writes the same bytes. Run + * it from the examples with `node scripts/history.ts`. It tiles the day with the repository's + * `latkit tile`. */ import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; @@ -10,19 +11,20 @@ import os from 'node:os'; import path from 'node:path'; import { Patch } from 'latkit'; -/** Buses, the areas they lie in, and seconds between samples, over a day. */ +/** The bus count, the area names, the seconds between samples, and the seconds in a day. */ const BUSES = 64; const AREAS = ['North', 'South', 'East', 'West', 'Coast', 'Valley']; const EVERY = 15; const DAY = 24 * 60 * 60; -/** When each disturbance strikes, the area it strikes, and how deep it pulls. */ +/** Each disturbance's start in seconds of the day, its area as an index into `AREAS`, and how far + * it pulls voltages down, in per unit. */ const DISTURBANCES = [ { at: 9.2 * 3600, area: 2, depth: 0.05 }, { at: 13.8 * 3600, area: 4, depth: 0.07 }, { at: 18.5 * 3600, area: 0, depth: 0.06 }, ]; -/** Deterministic numbers in [0, 1), as the examples' `random` makes them. */ +/** A seeded random generator of numbers in [0, 1), the same as the examples' `random`. */ function random(seed: number): () => number { return () => { seed = (seed + 0x6d2b79f5) | 0; @@ -32,7 +34,8 @@ function random(seed: number): () => number { }; } -/** The system's load at second `t` of the day, from 0 to 1: low at night, peaking morning and evening. */ +/** The system load at second `t` of the day, between 0 and 1. It is low at night and peaks in the + * morning and evening. */ function load(t: number): number { const hour = t / 3600; const peak = (at: number, width: number) => Math.exp(-(((hour - at) / width) ** 2)); @@ -41,7 +44,7 @@ function load(t: number): number { ); } -/** How far a disturbance pulls an area down, `since` seconds after it struck. */ +/** The voltage drop, in per unit, of a disturbance of `depth`, `since` seconds after it starts. */ function dip(since: number, depth: number): number { return since < 0 ? 0 : depth * Math.exp(-since / 240) * Math.min(1, since / 20); } diff --git a/npm/examples/src/examples/colormaps/index.ts b/npm/examples/src/examples/colormaps/index.ts index eb4ec629..6516b69b 100644 --- a/npm/examples/src/examples/colormaps/index.ts +++ b/npm/examples/src/examples/colormaps/index.ts @@ -1,8 +1,8 @@ /** - * The colormap catalog on a field of hexagons: each cell an area colored on the GPU through the - * chosen colormap, beside the same colormap as a CSS ramp, so the two can be compared. Choose a - * colormap, a field to show and how many cells, how the value is transformed on its way to the map, - * and which cells show; hover a cell to read it, or brush over the field to read what lies under it. + * The colormap catalog on a field of hexagons. Each cell is an area colored on the GPU through the + * chosen colormap, shown beside the same colormap as a CSS ramp for comparison. Choose a colormap, + * the field and number of cells, a transform applied to values before the colormap, and which cells + * show. Hover a cell to read its value, or brush over the field to read the cells under the brush. */ import { Data, @@ -20,21 +20,21 @@ import type { Stage } from '../../ui/stage.js'; import { theme } from '../../ui/theme.js'; /** - * Maps of our own: clear to a placed, half-clear blue, then an opaque amber, whose alpha shows the - * plane's grid through the cells; a diverging one from teal through a light middle to rust; and a - * cyclic one, whose ends meet. + * Custom colormaps. `custom` runs from transparent to a semi-transparent blue at a placed stop, + * then to opaque amber, so the plane's grid shows through the cells. `diverging` runs from teal + * through a light middle to rust. `cyclic` ends where it starts. */ const CUSTOM = { custom: { kind: 'sequential', colors: ['#2a6fdb00', [0.55, '#2a6fdb99'], '#ffb000'] }, diverging: { kind: 'diverging', colors: ['#00727a', '#e8e4d8', '#b5462a'] }, cyclic: { kind: 'cyclic', colors: ['#3b5bdb', '#e64980', '#fab005', '#3b5bdb'] }, } as const satisfies Record; -/** A colormap of the catalog, or one of ours. */ +/** A catalog colormap's name, or a key of `CUSTOM`. */ type Choice = ColormapName | keyof typeof CUSTOM; const mapOf = (choice: Choice): Colormap => choice in CUSTOM ? CUSTOM[choice as keyof typeof CUSTOM] : (choice as ColormapName); -/** How a cell's value, 0 to 1, is carried to the colormap, each again 0 to 1. */ +/** Transforms from a cell's value, 0 to 1, to the colormap's input, also 0 to 1, with labels. */ const TRANSFORMS: Record Expr]> = { none: ['As is', (v) => v], sqrt: ['Square root', (v) => v.sqrt()], @@ -66,12 +66,12 @@ const TRANSFORMS: Record Expr]> = { peaks: ['Peaks only', (v) => v.sub(0.6).max(0).div(0.4)], }; type Transform = keyof typeof TRANSFORMS; -/** Which cells show: a band of values, or of classes. */ +/** Which cells show: all of them, or those selected by a band of values or by class. */ type Show = 'all' | 'inside' | 'outside' | 'not' | 'class' | 'others'; -/** A checkerboard under a ramp, so what is clear in it shows. */ +/** A checkerboard behind each ramp, so a colormap's transparent parts are visible. */ const CHECKS = 'repeating-conic-gradient(#888 0 25%, #ccc 0 50%) 0 0 / 8px 8px'; -/** Every catalog colormap by kind, then ours. */ +/** Every catalog colormap grouped by kind, then the custom ones. */ const CATALOG: readonly (readonly [string, readonly Choice[]])[] = [ [ 'Sequential', @@ -126,7 +126,8 @@ const CATALOG: readonly (readonly [string, readonly Choice[]])[] = [ const CATEGORICAL = new Set(CATALOG.find(([kind]) => kind === 'Categorical')![1]); const KINDS = ['All', ...CATALOG.map(([kind]) => kind)] as const; -/** What the cells show, each a value from 0 to 1 at a point; categorical maps read classes. */ +/** Fields the cells can show, each a value from 0 to 1 at a point. Categorical colormaps show the + * value's class instead. */ const FIELDS = { waves: (x: number, y: number) => 0.5 + 0.3 * Math.sin(x * 0.11) * Math.cos(y * 0.09) + 0.2 * Math.sin((x + y) * 0.035), @@ -141,8 +142,8 @@ const FIELDS = { } as const; type FieldName = keyof typeof FIELDS; -/** A field of `columns` by `rows` hexagons of side 1, pointy side up, as a patch replacing table - * `Cell`: each cell's polygon, its value, its class of ten, and its value written. */ +/** A patch that replaces table `Cell` with `columns` by `rows` pointy-top hexagons of side 1. Each + * cell has its polygon, its value, its class from 0 to 9, and its value as label text. */ function hexagons(columns: number, rows: number, value: FieldName): Patch { const count = columns * rows; const width = columns * Math.sqrt(3); @@ -182,11 +183,12 @@ export default function page(stage: Stage): void { let classed = 5; let labels = false; let ownLegend = false; - /** One dataset, replaced field by field: the view eases each cell from its color to the next. */ + /** One dataset, replaced whenever the field or cell count changes. The view eases each cell from + * its old color to its new one. */ const data = new Data('colors'); stage.signal.addEventListener('abort', () => data.destroy()); - /** Whether a cell shows, as the panel says. */ + /** Whether each cell shows, from the Show setting. */ const visible = (): Expr | boolean => { const v = field('value'); const c = field('class'); @@ -222,20 +224,20 @@ export default function page(stage: Stage): void { ] : []; return { - // Lines under the cells show through what a map leaves clear. + // Grid lines under the cells show through a colormap's transparent parts. space: { plane: { grid: colormap === 'custom' ? { lines: { pitch: 4 } } : 'off' } }, - // The base themes by name. + // A built-in theme, by name. theme: light ? 'light' : 'dark', ease: 0.25, marks: [ { - // The cells are what the page reads: hovering one reads its value. + // The cells are pickable, so hovering one reads its value. areas: { of: ['colors', 'Cell'], color: categorical ? mapped : reversed ? mapped.domain(1, 0) : mapped.domain(0, 1), visible: visible(), pick: true, - // Where values matter most, their cells are labeled first. + // Cells with higher values take priority, so they are labeled first. priority: 'value', ...(labels ? { label: { text: 'label', size: 10, max: 60 } } : {}), ...(ownLegend ? { legend: TRANSFORMS[transform]![0] } : {}), @@ -331,7 +333,7 @@ export default function page(stage: Stage): void { rebuild(); }, ); - // By columns: a field three rows for every two columns wide. + // Sized by column count, with two rows for every three columns. shown.choice( 'Cells', [ @@ -389,7 +391,8 @@ export default function page(stage: Stage): void { reading.textContent = `Cell ${hit.row.id}: ${v?.toFixed(3) ?? '–'}`; }); - // A brush: the cells within its reach of the pointer, read by a pick, and their mean value. + // A brush: a pick finds the cells within `reach` CSS pixels of the pointer, and the note shows + // their mean value. let reach = 24; shown.range( 'Brush', @@ -418,8 +421,8 @@ export default function page(stage: Stage): void { { signal: stage.signal }, ); - // The catalog as CSS ramps: the colors the GPU draws, drawn by the browser over checks, so - // what a map leaves clear shows. + // The catalog as CSS ramps: the same colors the GPU draws, drawn by the browser over a + // checkerboard so transparent parts show. const catalog = stage.section('Catalog'); const groups: [string, HTMLElement[]][] = []; catalog.choice( diff --git a/npm/examples/src/examples/diagram/index.ts b/npm/examples/src/examples/diagram/index.ts index 949eef99..6144565b 100644 --- a/npm/examples/src/examples/diagram/index.ts +++ b/npm/examples/src/examples/diagram/index.ts @@ -1,7 +1,7 @@ /** * A block diagram on a sheet: blocks with ports, wires routed around them, and groups drawn around - * their members. Add, drag, connect, arrange, and undo; open a preset or a JSON file; restyle the - * sheet, its blocks, and its wires. + * their members. You can add, drag, connect, arrange, and undo, open a preset or a saved `.latk` + * file, and restyle the sheet, its blocks, and its wires. */ import { Data, @@ -29,8 +29,8 @@ import type { Stage } from '../../ui/stage.js'; import { theme } from '../../ui/theme.js'; /** - * Kinds of block: the marker each draws as, its fill on the dark and the light sheet, and its ports, - * each naming the signal it is plugged into. + * The block kinds. Each has a marker, a fill for the dark and the light sheet, and ports given as + * name, side, and direction. Each port is a reference field naming the signal plugged into it. */ const KINDS = { Input: { marker: 'rounded', fill: ['#173836', '#e2f3ef'], ports: [['out', 'right', 'out']] }, @@ -62,8 +62,8 @@ const KINDS = { >; type Kind = keyof typeof KINDS; const kinds = Object.keys(KINDS) as Kind[]; -/** Each kind as a shape of its own: inputs round, processes square, controls diamonds, outputs a - * block with deeply rounded corners. */ +/** A distinct shape per kind, used when Shapes is on: inputs are ellipses, processes rectangles, + * controls diamonds, and outputs blocks with deeply rounded corners. */ const SHAPES = { Input: 'ellipse', Process: 'rectangle', @@ -71,7 +71,7 @@ const SHAPES = { Output: { block: { corner: 18 } }, } as const satisfies Record; -/** A port of a block, by the reference field naming its signal. */ +/** A block's port. `port` names the reference field that holds the port's signal. */ interface Plug { kind: Kind; id: Id; @@ -81,8 +81,9 @@ interface Plug { const direction = (kind: Kind, port: string) => KINDS[kind].ports.find(([name]) => name === port)?.[2]; -/** A kind's ports as the scene declares them: outputs drive their signals, and a feedback port is - * named. As dots, a plain input is its side alone; as diamonds, each port is ordered on its side. */ +/** Declares a kind's ports for the scene. Outputs drive their signals, and the feedback port gets + * a label. With dot ports, a plain input is declared by its side alone. With diamond ports, every + * port also gets a shape and an order on its side. */ const ports = (kind: Kind, diamonds: boolean): [string, Port][] => KINDS[kind].ports.map(([name, side, way], order): [string, Port] => [ name, @@ -97,9 +98,9 @@ const ports = (kind: Kind, diamonds: boolean): [string, Port][] => ]); /** - * A diagram drawn up as rows: blocks of each kind, the signals their ports plug into, and the groups - * holding Process blocks, themselves held by stations. Positions, x right and y down, are optional: - * blocks without one are placed by the layout. + * Builds a diagram's rows: blocks of each kind, the signals their ports plug into, groups of + * Process blocks, and stations that contain groups. Positions (x right, y down) are optional. The + * layout places blocks without one. */ class Sketch { readonly blocks = new Map< @@ -110,7 +111,7 @@ class Sketch { readonly signals: { id: number; name: string }[] = []; readonly groups: { id: number; name: string; station?: number }[] = []; readonly stations: { id: number; name: string }[] = []; - /** Blocks whose status ring shows. */ + /** Blocks whose status ring is shown. */ readonly alarms = new Set(); next = 1; @@ -122,7 +123,8 @@ class Sketch { return { kind, id, port: 'out' }; } - /** A signal driven by `from`'s output (or the port it names) and read by each of `to`'s inputs. */ + /** Adds a signal driven by the `out` port of each block in `from` and read by each of `to`, on + * its `in` port or on the port paired with it. */ wire(name: string, from: Plug | Plug[], ...to: (Plug | [Plug, string])[]): void { const id = this.next++; this.signals.push({ id, name }); @@ -145,7 +147,7 @@ class Sketch { return id; } - /** The dataset. */ + /** Builds the dataset from the rows. */ data(): Data { const patch = new Patch().set( 'Signal', @@ -205,15 +207,15 @@ class Sketch { } } -/** The colors groups may take, on the dark and the light sheet. */ +/** Group colors, for the dark and the light sheet. */ const GROUP_COLORS = { slate: ['#1a2230', '#e6ebf2'], teal: ['#14302f', '#dcefec'], plum: ['#2a1f33', '#efe6f4'], } as const; -/** A pipeline written as a JSON document, as `Data.read` reads one: a change per method, with its - * parameters by name. */ +/** A small pipeline written as a JSON document for `Data.read`. Each change is an object keyed by + * its method, with the method's parameters by name. */ const PIPELINE = { name: 'studio', patch: [ @@ -261,8 +263,8 @@ const PIPELINE = { ], }; -/** The presets: a water plant laid out in ranks, then four drawn where they stand, and one written - * as a JSON document. */ +/** The presets: a water plant placed in ranks by the layout, four with fixed positions, and one + * written as a JSON document. */ const PRESETS: readonly (readonly [string, () => Data | Promise])[] = [ [ 'Water plant', @@ -364,14 +366,14 @@ const PRESETS: readonly (readonly [string, () => Data | Promise])[] = [ ]; /** - * Earlier versions of the data, to step back to and forward again: snapshots, freed once - * forgotten. + * Undo and redo history, kept as snapshots of the data. It keeps up to 100 undo steps and destroys + * each snapshot once it can no longer be restored. */ class History { readonly #undo: Snapshot[] = []; #redo: Snapshot[] = []; - /** Remembers `before`, a snapshot of the version an edit started from. */ + /** Records `before`, a snapshot taken before an edit, and clears the redo steps. */ commit(before: Snapshot): void { this.#undo.push(before); if (this.#undo.length > 100) this.#undo.shift()!.destroy(); @@ -379,7 +381,8 @@ class History { this.#redo = []; } - /** The version before `current`, which redo then steps forward to. */ + /** Returns the snapshot before `current` and keeps `current` for redo. When there is nothing to + * undo, it destroys `current` and returns undefined. */ undo(current: Snapshot): Snapshot | undefined { const before = this.#undo.pop(); if (before) this.#redo.push(current); @@ -410,7 +413,8 @@ export default function page(stage: Stage): void { type Sheet = 'dots' | 'snap' | 'lines' | 'off'; let grid: Sheet = 'dots'; let light = false; - /** How the sheet, its blocks, ports, and wires look, and how the view answers keys and presses. */ + /** Settings for how the sheet, blocks, ports, and wires look, and how the view responds to keys + * and presses. */ const style = { shapes: false, diamonds: false, @@ -439,7 +443,7 @@ export default function page(stage: Stage): void { off: 'off', }; - /** The tables the diagram holds now. */ + /** The names of the tables the data has now. */ const held = () => new Set(data.describe().tables.map((t) => t.name)); /** Every id of `table`. */ const ids = (table: string) => data.query({ of: table, select: {} }).ids; @@ -447,8 +451,8 @@ export default function page(stage: Stage): void { const scene = (): Scene => { const tables = held(); const marks: Mark[] = []; - // Containers take the theme's group color, tinted by how deep they sit; their titles head them, and - // they and the blocks they hold lift off the sheet. + // Containers take the theme's group color, tinted by nesting depth. Their titles sit at the + // top, and they and the blocks inside them cast shadows on the sheet. const title = { text: 'name', size: 11, color: light ? '#5b6b7d' : '#7f93a8' }; if (tables.has('Station')) { marks.push({ @@ -470,7 +474,8 @@ export default function page(stage: Stage): void { }, }); } - // Comets run from each signal's driver to its readers; a feedback signal is dashed. + // With Flow on, comets run from each signal's driver to its readers. A signal named Feedback + // is dashed. marks.push({ edges: { of: ['studio', 'Signal'], @@ -484,7 +489,7 @@ export default function page(stage: Stage): void { : {}), }, }); - // A mark for each kind of block the data holds; a table the data lacks would fail. + // One mark per block kind in the data. A mark of a table the data lacks would fail. for (const kind of kinds.filter((k) => tables.has(k))) { // Without a size, a block fits its label and ports. marks.push({ @@ -530,14 +535,15 @@ export default function page(stage: Stage): void { marks, }; }; - /** An id no row of the data has yet. */ + /** An id that no row in the data uses yet. */ const fresh = () => 1 + Math.max(0, ...[...held()].flatMap((t) => ids(t).map((id) => Number(id) || 0))); const view = stage.view(scene(), { label: 'A diagram: drag blocks, drag from a port to wire it, Delete removes', }); - /** Takes what the data holds now: a mark for each kind of block it holds, and its description. */ + /** Refreshes after a data change. The scene is rebuilt only if the set of tables changed, so each + * block kind in the data has a mark. The description is always updated. */ let kindsShown = [...held()].join(); const changed = () => { const kindsHeld = [...held()].join(); @@ -547,14 +553,14 @@ export default function page(stage: Stage): void { } describe(); }; - /** Applies `patch` as one edit that undo steps back over. */ + /** Applies `patch` as one undoable edit and shows `said`. */ const edit = (patch: Patch, said: string) => { history.commit(data.snapshot()); data.apply(patch); changed(); note(said); }; - /** Shows `next`, a diagram of its own: nothing to undo back to. */ + /** Replaces the diagram with `next`, clears the undo history, and fits the view to it. */ const open = (next: Data, said: string) => { history.clear(); data.destroy(); @@ -580,7 +586,7 @@ export default function page(stage: Stage): void { (data.get(row.table, row.id, 'name') as string | null) ?? `${row.table} ${row.id}`; const isBlock = (table: string): table is Kind => table in KINDS; - // Moving: a block goes where the drag says, moves at once, and lands as one edit. + // Dragging moves a block immediately, and the whole drag is recorded as one undoable edit. let before: Snapshot | undefined; view.on('drag', ({ phase, hit, position }) => { if (!isBlock(hit.row.table)) return; @@ -601,8 +607,9 @@ export default function page(stage: Stage): void { } }); - // Wiring: an output and an input join on the output's signal, made when it has none; an input - // wired elsewhere leaves the signal it was on. A plugged port let go over nothing is unplugged. + // Wiring joins an output and an input on the output's signal, creating the signal if the output + // has none. An input wired elsewhere leaves its old signal. Dropping a plugged port over empty + // space unplugs it. view.on('connect', ({ from, to }) => { if (!to) { const plugged = @@ -638,7 +645,7 @@ export default function page(stage: Stage): void { edit(patch, `Wired ${name(from.row)} and ${name(to.row)}.`); }); - /** Removes `rows` as one edit. */ + /** Removes `rows` as one undoable edit. */ const remove = (rows: readonly Rows[]) => { const patch = new Patch(); for (const { table, ids } of rows) patch.remove(table, ids); @@ -646,7 +653,7 @@ export default function page(stage: Stage): void { }; view.on('delete', ({ rows }) => remove(rows)); - /** Selects `row` and what it joins, and frames them. */ + /** Selects `row` and its neighbors, and fits the view to them. */ const reveal = (row: Row) => { const joined = view.neighbors(row); const near = [one(row), ...joined]; @@ -655,7 +662,7 @@ export default function page(stage: Stage): void { describe(); note(`${name(row)} and ${each(joined).length} joined to it.`); }; - /** Selects `row` and puts its name up for editing. */ + /** Selects `row` and focuses its name for editing. */ const rename = (row: Row) => { view.selection = [one(row)]; describe(); @@ -665,7 +672,7 @@ export default function page(stage: Stage): void { const named = (row: Row) => isBlock(row.table) || row.table === 'Group' || row.table === 'Station'; - // Opening a row renames it; the menu names what can be done with the row under it. + // Opening a row renames it. The context menu lists actions for the row under the pointer. view.on('open', ({ hit }) => named(hit.row) && rename(hit.row)); const menu = stage.menu(); view.on('menu', ({ hits, trigger }) => { @@ -720,8 +727,8 @@ export default function page(stage: Stage): void { { label: 'Arrange', key: 'a', - // Every block lets go of its position, the layout places them all again, and the places it - // answers become the blocks' positions: one edit undo steps back over. + // Clears every block's position so the layout places them all again, then writes the + // computed places back as positions. One undo reverts the whole arrangement. run: () => { const patch = new Patch(data.name); const tables = held(); @@ -735,7 +742,7 @@ export default function page(stage: Stage): void { changed(); view.fit(); void view.arrange().then((patches) => { - // The places of every table, written as one change. + // Merges the computed places of every table into one patch. const placed = patches .filter((p) => p.data === data.name) .reduce((all, p) => all.extend(p), new Patch(data.name)); @@ -749,7 +756,8 @@ export default function page(stage: Stage): void { { label: 'Zoom in', key: '=', run: () => zoom(0.5) }, { label: 'Zoom out', key: '-', run: () => zoom(-0.5) }, ]); - /** Eases the camera in by `by` steps of doubling. */ + /** Flies the camera in by `by` zoom levels, where each level doubles the scale. Negative zooms + * out. */ const zoom = (by: number) => { const camera = view.camera; if ('center' in camera) view.fly({ ...camera, zoom: camera.zoom + by }); @@ -760,7 +768,7 @@ export default function page(stage: Stage): void { label: kind, key: String(i + 1), run: () => { - // New blocks land where the camera looks; nothing else moves. + // New blocks appear at the camera's center, and nothing else moves. const camera = view.camera; const [x, y] = 'center' in camera ? camera.center : [0, 0]; const id = fresh(); @@ -835,7 +843,7 @@ export default function page(stage: Stage): void { (next) => ((grid = next), restyle()), ); - // The sheet's theme: how blocks, groups, ports, wires, and shadows look. + // Theme settings for blocks, groups, ports, wires, and shadows. const sheet = stage.section('Sheet', false); const knob = ( label: string, @@ -883,7 +891,7 @@ export default function page(stage: Stage): void { sheet.toggle('Shadows', undefined, style.shadows, (on) => ((style.shadows = on), restyle())); sheet.toggle('Outlines', undefined, style.outline, (on) => ((style.outline = on), restyle())); - // How the view answers keys and presses. + // How the view responds to keys and presses. const answers = stage.section('Interaction', false); answers.toggle('Keys', undefined, style.keyboard, (on) => ((style.keyboard = on), restyle())); answers.toggle('Cycle picks', undefined, style.cycle, (on) => ((style.cycle = on), restyle())); @@ -899,7 +907,8 @@ export default function page(stage: Stage): void { ); answers.note('Keys: Tab steps through blocks, Enter renames, Space selects, Delete removes.'); - // The diagram as a file: latkit's bytes, the patch that builds it, read back as they were. + // Saves the diagram as a `.latk` file, the patch that builds it in latkit's binary format. + // Opening the file restores the diagram unchanged. const file = stage.section('File', false); const picker = h('input', { type: 'file', accept: '.latk', hidden: true }); file.content.append(picker); @@ -932,7 +941,7 @@ export default function page(stage: Stage): void { ]); const described = file.note(); - // The selected block's name, editable. + // An editable name field for the selected row. const selected = stage.section('Selection'); const input = selected.text('Name', (value) => { const [row] = each(view.selection); @@ -946,7 +955,7 @@ export default function page(stage: Stage): void { input.disabled = true; const told = selected.note(); const note = (text: string) => (told.textContent = text); - // The selected block wears a tag where the view located it; it goes when the camera moves. + // A tag labels the selected block at its screen position. It hides when the camera moves. const tag = h('p', { class: 'callout', hidden: true }); stage.regions.main.append(tag); view.on('camera', () => (tag.hidden = true)); @@ -963,7 +972,7 @@ export default function page(stage: Stage): void { tag.style.translate = `${point[0]}px ${point[1] - 18}px`; tag.hidden = false; }); - // What the data holds, as it describes itself. + // Row counts per table, from `data.describe()`. described.textContent = data .describe() .tables.map((t) => `${t.name} ${t.rows.toLocaleString()}`) diff --git a/npm/examples/src/examples/index.ts b/npm/examples/src/examples/index.ts index 0771d35d..09df10e3 100644 --- a/npm/examples/src/examples/index.ts +++ b/npm/examples/src/examples/index.ts @@ -1,6 +1,6 @@ import type { Page } from '../ui/shell.js'; -/** Names and modules for the example picker. IDs keep existing links working. */ +/** The example picker's pages. Their IDs keep older names so existing links still work. */ export const examples: readonly Page[] = [ { id: 'bunny', title: 'Soft body', load: () => import('./soft-body/index.js') }, { id: 'network', title: 'Networks', load: () => import('./networks/index.js') }, diff --git a/npm/examples/src/examples/linked-views/index.ts b/npm/examples/src/examples/linked-views/index.ts index b08d16fe..c09cf86c 100644 --- a/npm/examples/src/examples/linked-views/index.ts +++ b/npm/examples/src/examples/linked-views/index.ts @@ -1,8 +1,8 @@ /** - * One record in two views on one canvas: a lattice of nodes on a tilted plane, each standing on a - * pole as high as its signal, and the signals traced on a plot. One playhead moves both; play at a - * speed or scrub, and read where each view stands. Select nodes in either view and the other selects - * them too, their traces standing out. + * One recording in two views on one canvas: a lattice of nodes on a tilted plane, each on a pole as + * high as its signal, and a plot tracing the signals over time. One playhead moves both views. Play + * at a chosen speed or scrub, and read each view's time and stats. Nodes selected in either view + * are selected in the other too, and their traces stand out. */ import { Data, @@ -21,17 +21,18 @@ import { import type { Stage } from '../../ui/stage.js'; import { theme } from '../../ui/theme.js'; -/** Nodes a side of the lattice, frames recorded, seconds between them, and a full signal's height. */ +/** Nodes per side of the lattice, frames recorded, seconds between frames, and the height of a + * full signal in plane units. */ const SIDE = 20; const FRAMES = 1201; const STEP = 0.05; const HEIGHT = 3; -/** Every `TRACED`th node, from the middle of the first run, is traced when few are. */ +/** When only a few traces show, every `TRACED`th node is traced, starting at node 10. */ const TRACED = 25; /** - * The record: nodes at whole x and y, linked to their neighbors right and down, each a signal - * rippling across the lattice, `0.5 + 0.45 sin(0.37 x + 1.3 t) cos(0.31 y - 0.7 t)`. + * Builds the recording: nodes at integer x and y, linked to their neighbors at +x and +y, each with + * a signal rippling across the lattice, `0.5 + 0.45 sin(0.37 x + 1.3 t) cos(0.31 y - 0.7 t)`. */ function record(): Data { const n = SIDE * SIDE; @@ -55,7 +56,7 @@ function record(): Data { { count: from.length }, { from: reference('Node', from), to: reference('Node', to) }, ); - // Every frame in one append, frame by frame. + // One append holds every frame, with the samples laid out frame after frame. const times = Array.from({ length: FRAMES }, (_, f) => f * STEP); const signal = new Float32Array(FRAMES * n); times.forEach((t, f) => { @@ -72,14 +73,15 @@ export default function page(stage: Stage): void { const look = { height: HEIGHT, poles: true, - /** Whether signals read their last sample as it was, rather than eased between samples. */ + /** Whether signals hold their last sample instead of easing between samples. */ held: false, size: 'pixels' as 'pixels' | 'zoom' | 'units', }; - /** Each node's signal: eased between samples, or held at the last one. */ + /** Each node's signal, eased between samples or held at the last one. */ const reading = () => (look.held ? field('signal').step() : field('signal')); const signal = () => reading().colormap('viridis').domain(0, 1); - /** A node's radius: CSS pixels, grown with the zoom, or units of the plane kept within pixels. */ + /** A node's radius: fixed CSS pixels, growing with the zoom, or plane units clamped to a range of + * pixels. */ const radius = (): Size => look.size === 'zoom' ? zoom().mul(0.9).clamp(1.5, 9) @@ -126,7 +128,7 @@ export default function page(stage: Stage): void { of: ['signals', 'Node'], trace: 'signal', color: signal(), - // Selected nodes' traces stand out, the rest dim. + // Selected nodes' traces are drawn wider, and the rest dim. width: when(selected()) .then(3) .otherwise(few ? 1.5 : 0.8), @@ -136,7 +138,7 @@ export default function page(stage: Stage): void { ], }); const [plane, plot] = stage.panels([lattice(), traces()], { - label: 'A lattice on poles beside its signals traced: one playhead moves both', + label: 'A lattice on poles beside a plot of its signals: one playhead moves both', at: [ [0, 0, 0.55, 1], [0.55, 0, 0.45, 1], @@ -152,7 +154,7 @@ export default function page(stage: Stage): void { plot.scene = traces(); }; - // What either view selects, the other selects too. + // Copy each view's selection to the other. const same = (a: readonly Rows[], b: readonly Rows[]) => JSON.stringify(a) === JSON.stringify(b); for (const [from, to] of [ [plane, plot], @@ -174,7 +176,8 @@ export default function page(stage: Stage): void { }); playback.record([0, duration]); - // Where each view stands: both draw in the canvas's one frame, so they never part. + // Each view's time and stats. Both views draw in the canvas's single frame, so their times always + // match. const where = stage.section('View stats', false); const lines = views.map(() => where.note()); let read = 0; diff --git a/npm/examples/src/examples/live-data/index.ts b/npm/examples/src/examples/live-data/index.ts index e12124f8..65b77c1b 100644 --- a/npm/examples/src/examples/live-data/index.ts +++ b/npm/examples/src/examples/live-data/index.ts @@ -1,9 +1,9 @@ /** - * Thousands of signals streaming in, traced over a scrolling window: frames append as they arrive - * and the oldest are forgotten. Read the trace under the pointer, rank the largest changes with a - * query, color traces by area or by their value now, and strike a disturbance to watch it spread. - * Or draw a recorded day instead, fetched as time tiles a span at a time, at the detail the window - * draws. + * Thousands of streaming signals traced over a scrolling window. Frames are appended as they + * arrive, and the oldest are forgotten. Read the trace under the pointer, rank the largest changes + * with a query, color traces by area or by their current value, and strike a disturbance to watch + * it spread. Or draw a recorded day instead, fetched span by span as time tiles, at the detail the + * window needs. */ import { Data, @@ -26,7 +26,7 @@ import { each } from '../../ui/rows.js'; import type { Stage } from '../../ui/stage.js'; import { theme } from '../../ui/theme.js'; -/** The areas signals belong to, in the colors tab10 gives them. */ +/** The signal areas, with the colors tab10 gives them. */ const AREAS = [ ['North', '#1f77b4'], ['South', '#ff7f0e'], @@ -35,22 +35,23 @@ const AREAS = [ ['Coast', '#9467bd'], ['Valley', '#8c564b'], ] as const; -/** Seconds of history kept; older frames are forgotten. */ +/** Seconds of history kept. Older frames are forgotten. */ const HISTORY = 120; -/** Seconds between disturbances, and how long one lasts: long enough to have died away. */ +/** Seconds between automatic disturbances, and how long each lasts (long enough to die away). */ const EVERY = 25; const LASTS = 6; -/** Signals the hot list names, and how often it reads them again. */ +/** Signals per page of the hot list, and how often it refreshes, in milliseconds. */ const HOT = 12; const HOT_EVERY = 250; -/** The recorded day: time tiles `scripts/history.ts` writes, of 64 buses' voltages every 15 s. */ +/** The recorded day, as time tiles of 64 buses' voltages every 15 s. `scripts/history.ts` writes + * them. */ const RECORDED = '/history/frames.json'; -/** Seconds of the recorded day a second of playing shows. */ +/** Seconds of the recorded day shown per second of playback. */ const DAY_RATE = 600; /** - * What the signals measure: each its unit, the range it keeps to, the digits it reads to, and - * whether its low values are the ones to watch. + * The quantities the signals can measure. Each has a unit, a normal range, the decimal places to + * show, and whether its low values (rather than high) are the ones to watch. */ const QUANTITIES = { voltage: { label: 'Voltage', unit: 'pu', range: [0.92, 1.04], digits: 4, low: true }, @@ -59,14 +60,15 @@ const QUANTITIES = { temperature: { label: 'Temperature', unit: '°C', range: [20, 110], digits: 1, low: false }, } as const; type Quantity = keyof typeof QUANTITIES; -/** The colormaps a signal's value now may take. */ +/** The colormaps offered for coloring traces by current value. */ const MAPS: readonly ColormapName[] = ['coolwarm', 'viridis', 'turbo', 'magma', 'icefire']; -/** How often a signal reports nothing, when signals drop out. */ +/** The chance per sample that a signal reports nothing, when dropouts are on. */ const DROPOUT = 0.002; /** - * Patches sent as bytes through a stream, as a socket or a response body brings them, each in two - * pieces as the network splits it, and read back with `Patch.stream` as each one's last byte arrives. + * Sends patches as bytes through a stream, as a socket or a response body would deliver them. Each + * patch is written in two pieces, as a network may split it. `Patch.stream` reads each patch back + * once its last byte arrives. */ class Feed { readonly #writer: WritableStreamDefaultWriter; @@ -92,10 +94,9 @@ class Feed { } /** - * Synthetic SCADA signals: each sits at its own offset, its area swings slowly, every signal - * carries its own noise, and every `EVERY` seconds one area is disturbed and recovers, as is an - * area struck by hand. A disturbance pulls voltages and the system's frequency down, and pushes - * loading and temperature up. + * Synthetic SCADA signals. Each signal has its own offset and noise, and each area swings slowly. + * Every `EVERY` seconds one area is disturbed and recovers, and so does an area struck by hand. A + * disturbance pulls voltages and the system frequency down, and pushes loading and temperature up. */ class Signals { readonly area: Float64Array; @@ -106,7 +107,7 @@ class Signals { /** The area struck by hand, and when. */ #struck?: { area: number; at: number }; - /** Whether signals now and then report nothing. */ + /** Whether signals occasionally report nothing. */ dropouts = false; constructor( @@ -124,17 +125,17 @@ class Signals { } } - /** Strikes area `area` at `t`: it is disturbed and recovers as the timed disturbances do. */ + /** Strikes area `area` at time `t`. It is disturbed and recovers like a timed disturbance. */ strike(area: number, t: number): void { this.#struck = { area, at: t }; } - /** Every signal's value at `t`, into `out`: NaN where one reports nothing. */ + /** Writes every signal's value at `t` into `out`, with NaN for a signal that reports nothing. */ sample(t: number, out: Float32Array): void { const struck = Math.floor(t / EVERY) % AREAS.length; const dip = depth(t - Math.floor(t / EVERY) * EVERY); const byHand = this.#struck && t >= this.#struck.at ? depth(t - this.#struck.at) : 0; - // The system's frequency falls with any disturbance, wherever it strikes. + // The system frequency falls with a disturbance in any area. const system = 60 + 0.01 * Math.sin(t * 0.21) - 2 * Math.max(dip, byHand); for (let i = 0; i < this.count; i++) { const a = this.area[i]!; @@ -158,14 +159,15 @@ class Signals { } } -/** How far a disturbance has pulled values, `since` seconds after it struck. */ +/** How far a disturbance has pulled values, `since` seconds after it started. It is zero once + * `LASTS` seconds have passed. */ function depth(since: number): number { return since < LASTS ? 0.06 * Math.exp(-since * 1.4) * Math.sin(Math.min(since, 0.5) * Math.PI) : 0; } -/** Second `at` of a day as a clock reads it. */ +/** Formats second `at` of a day as a clock time, hh:mm:ss. */ function clock(at: number): string { const s = Math.max(0, Math.round(at)); const two = (n: number) => String(n).padStart(2, '0'); @@ -192,7 +194,7 @@ export default function page(stage: Stage): void { let signals = new Signals(count, quantity); let values = new Float32Array(count); let data = new Data('scada'); - /** The span of the recorded day that landed. */ + /** The span of the recorded day that has arrived so far. */ let day: [number, number] | undefined; let t = 0; let carry = 0; @@ -210,7 +212,7 @@ export default function page(stage: Stage): void { format: axes.format, precision: axes.format === 'auto' ? null : digits - 1, grid: axes.grid, - // The range a signal keeps to, ticked at its ends and its middle. + // Limits: ticks at the ends and the middle of the quantity's normal range. ...(axes.limits ? { ticks: [ @@ -232,8 +234,8 @@ export default function page(stage: Stage): void { : '64 buses · a sample every 15 s', } : {}), - // Traces draw the theme's line width, and plain, its line color; what a signal does not report - // reads as missing. + // Traces use the theme's line width, and its line color when plain. Samples a signal did not + // report are drawn in the `missing` color. theme: { ...theme, focus: { dim }, @@ -247,7 +249,7 @@ export default function page(stage: Stage): void { lines: { of: ['scada', 'Signal'], trace: 'value', - // By value, each trace takes the color of its value at the time shown. + // Colored by value, each trace takes the color of its value at the time shown. ...(colorBy === 'plain' ? {} : { @@ -267,8 +269,8 @@ export default function page(stage: Stage): void { }); /** - * `f` of what the view draws of the signals now: the live record, or what it fetched of the - * recorded day; `undefined` before it has any. + * Applies `f` to the signal data the view draws now: the live record, or the fetched part of the + * recorded day. Returns `undefined` before any of the day has arrived. */ const read = (f: (held: Data | Snapshot) => T): T | undefined => { if (source === 'live') return f(data); @@ -281,8 +283,9 @@ export default function page(stage: Stage): void { } }; - /** Frames what `at` shows: the window before it, filling from the record's start until the - * record is as long; or all that is kept. */ + /** Points the camera at time `at`: the window ending at `at`, or everything kept when the window + * is unlimited. Until the record is as long as the window, the window starts at the record's + * start. With Auto range off, y spans the quantity's normal range. */ const frame = (at: number) => { const first = read((held) => held.describe().span?.[0]) ?? at; const span = source === 'live' ? window : dayWindow; @@ -294,20 +297,21 @@ export default function page(stage: Stage): void { view.camera = { x, y: auto ? null : [range[0], range[1]] }; }; - /** Takes a patch the feed brought: the record grows, and the view draws it the next frame. */ + /** Applies a patch from the feed. The record grows, and the view draws it the next frame. */ const take = (patch: Patch) => { data.apply(patch); playback.record(data.describe().span!); }; - /** The stream patches come through when they come as bytes, made again with each record. */ + /** The byte stream that patches go through when they are streamed. It is rebuilt with each + * record. */ let feed: Feed | undefined; stage.signal.addEventListener('abort', () => feed?.close()); - /** Draws the source chosen: a live record started again, of `n` signals measuring the quantity - * shown; or the recorded day, drawn as the tiles of it the window asks for land. */ + /** Draws the chosen source: a new live record of `n` signals measuring the current quantity, or + * the recorded day, drawn as the tiles the window needs arrive. */ const restart = (n = count) => { count = n; - // Patches of the record before arrive no more. + // Stop the previous record's feed. feed?.close(); feed = undefined; data.destroy(); @@ -334,7 +338,7 @@ export default function page(stage: Stage): void { values = new Float32Array(n); t = 0; carry = 0; - // The record starts with its first frame, so the value field is there from the start. + // The record starts with its first frame, so the value field exists from the start. signals.sample(0, values); data = new Data('scada').apply( new Patch() @@ -370,8 +374,8 @@ export default function page(stage: Stage): void { let spanned = 0; stage.frame((dt) => { if (performance.now() - listed > HOT_EVERY) list(); - // The recorded day spans what of it landed, growing as its spans do: until the transport is - // moved, it shows to the end. + // The recorded day's span grows as more of it arrives. A playhead at the end follows the new + // end. if (source === 'recorded' && performance.now() - spanned > HOT_EVERY) { spanned = performance.now(); const span = read((held) => held.describe().span); @@ -410,7 +414,7 @@ export default function page(stage: Stage): void { return `${value.toFixed(digits)} ${unit}`; }; const when = (at: number) => (source === 'live' ? `${at.toFixed(2)} s` : clock(at)); - /** Signal `id`'s least and greatest values over the window shown, read from its levels of detail. */ + /** Signal `id`'s least and greatest values in the window, from its levels of detail. */ const spread = (id: Id) => { const camera = view.camera; if (!('x' in camera)) return ''; @@ -439,8 +443,9 @@ export default function page(stage: Stage): void { list(); }); - // The signals furthest out at the time shown, read by a query: a pick, of every signal or of an - // area's, in linear time, a page of them at a time. + // The signals with the most extreme values at the time shown (lowest or highest, depending on + // the quantity), among all signals or one area's. The query selects them in linear time and + // returns them page by page. const hottest = stage.section('Largest changes', false); let inArea = -1; let page = 0; @@ -479,7 +484,7 @@ export default function page(stage: Stage): void { const found = read((held) => { if (!held.describe().span) return undefined; let rows = held.query({ ...query, offset: page * HOT }); - // Past the last page, the last. + // Past the last page, show the last page instead. if (!rows.ids.length && page > 0) { page = Math.max(0, Math.ceil(rows.total / HOT) - 1); rows = held.query({ ...query, offset: page * HOT }); @@ -746,7 +751,7 @@ export default function page(stage: Stage): void { stage.detail( source === 'live' ? `${count.toLocaleString()} signals · ${shown().label.toLowerCase()} · ${HISTORY} s kept` - : '64 buses · voltage · a recorded day, fetched as the window draws it', + : '64 buses · voltage · a recorded day, fetched as the window needs it', ); restart(); } diff --git a/npm/examples/src/examples/networks/cases.test.ts b/npm/examples/src/examples/networks/cases.test.ts index 4a10ac2d..ff56521a 100644 --- a/npm/examples/src/examples/networks/cases.test.ts +++ b/npm/examples/src/examples/networks/cases.test.ts @@ -5,7 +5,7 @@ import { readCase } from './cases.js'; const CASES = ['wecc240', 'activsg2000', 'activsg10k', 'activsg25k', 'activsg70k'] as const; -/** Case `id` as the page reads it, from the file it fetches. */ +/** Reads case `id` from the file the page fetches, inflating it if it is gzip-compressed. */ function read(id: string) { const raw = readFileSync(`npm/examples/public/grids/${id}.bin`); const bytes = raw[0] === 0x1f && raw[1] === 0x8b ? gunzipSync(raw) : raw; @@ -13,7 +13,7 @@ function read(id: string) { } describe('grid cases', () => { - test.each(CASES)('%s reads into buses, lines, and machines that stand on the earth', (id) => { + test.each(CASES)('%s reads into buses, lines, and machines positioned on the earth', (id) => { const found = read(id); const rows = Object.fromEntries( found.data.describe().tables.map((table) => [table.name, table.rows]), @@ -24,7 +24,7 @@ describe('grid cases', () => { expect(Math.abs(found.positions[i]!)).toBeLessThanOrEqual(180); expect(Math.abs(found.positions[i + 1]!)).toBeLessThanOrEqual(90); } - // Every line's ends name buses of the case. + // Every line's `from` and `to` reference a bus of the case, and every machine's bus row exists. const lines = found.data.query({ of: 'Line', select: { from: 'from', to: 'to' } }); const ends = lines.fields as Record; expect(ends.from!.ids.every((id) => id !== null)).toBe(true); @@ -40,7 +40,7 @@ describe('grid cases', () => { expect(speeds.some((mhz) => Math.abs(mhz) > 0)).toBe(true); }); - test('bytes that are no case are refused', () => { + test('bytes that are not a grid case are rejected', () => { expect(() => readCase('nothing', new ArrayBuffer(16))).toThrow('not a grid case'); }); }); diff --git a/npm/examples/src/examples/networks/cases.ts b/npm/examples/src/examples/networks/cases.ts index 71abd11b..bf4f4ac7 100644 --- a/npm/examples/src/examples/networks/cases.ts +++ b/npm/examples/src/examples/networks/cases.ts @@ -1,13 +1,16 @@ /** - * Synthetic grid cases, as the page fetches them from `public/grids`, each gzip-compressed. + * Synthetic grid cases. The page fetches them gzip-compressed from `public/grids`. * - * A case file holds a "LKG1" magic and a u32 header length, then a JSON header naming the case and - * its counts, then typed arrays on eight-byte boundaries past the header, where its `arrays` place - * them as [type, offset, length]: each bus's number, name, kV, longitude and latitude, load and - * generation in MW; each branch's two bus rows, reactance, and whether it transforms; each machine's - * bus row, name, rating, and output. Simulated cases also hold each machine's speed deviation, frame - * by frame as int16 of `speedScale` per unit, `dt` seconds apart, and the fault the simulation - * applied. + * A case file starts with the magic "LKG1" and a u32 header length, followed by a JSON header with + * the case's name and counts. Typed arrays follow on eight-byte boundaries. The header's `arrays` + * gives each as [type, offset, length], with the offset in bytes from the first eight-byte boundary + * after the header and the length in elements. The arrays hold: + * - per bus: number, name, kV, longitude and latitude, and load and generation in MW; + * - per branch: its two bus rows, reactance, and whether it is a transformer; + * - per machine: bus row, name, rating, and output. + * + * Simulated cases also hold each machine's speed deviation for every frame, `dt` seconds apart, as + * int16 in steps of `speedScale` per unit, and the fault the simulation applied. */ import { Data, Patch, reference } from 'latkit'; @@ -25,14 +28,14 @@ interface Header { >; } -/** A case's simulation: each machine's speed deviation over the seconds simulated. */ +/** A case's simulation: each machine's speed deviation over the simulated time. */ export interface Simulation { readonly frames: number; readonly dt: number; readonly duration: number; - /** Speed deviation in mHz, frame by frame, a value per machine. */ + /** Speed deviation in mHz, one value per machine for each frame, frame-major. */ readonly speeds: Float32Array; - /** The largest deviation, in mHz, either way. */ + /** The largest deviation either way, in mHz: the int16 maximum at the file's `speedScale`. */ readonly largest: number; readonly fault?: { readonly bus: string; readonly on: number; readonly off: number }; } @@ -45,19 +48,20 @@ export interface Case { readonly machines: number; /** Bytes downloaded, compressed. */ readonly bytes: number; - /** Whether its buses have names, which labels show. */ + /** Whether its buses have names, which labels and search use. */ readonly named: boolean; - /** Each bus's longitude and latitude, and its load and generation in MW. */ + /** Each bus's longitude and latitude, interleaved. `load` and `generation` are per bus, in MW. */ readonly positions: Float32Array; readonly load: Float32Array; readonly generation: Float32Array; - /** Each machine's bus row, and its output in MW. */ + /** Each machine's bus row. `machinePower` is each machine's output in MW. */ readonly machineBus: Uint32Array; readonly machinePower: Float32Array; readonly simulation?: Simulation; } -/** A bus's or line's voltage level: 0 below 100 kV, then 115–161, 230, 345, 500, and 765 kV. */ +/** The voltage level of a bus or line, an index into `LEVELS`: 0 below 100 kV, then 115–161, 230, + * 345, 500, and 765 kV. */ export function levelOf(kv: number): number { return kv >= 700 ? 5 : kv >= 450 ? 4 : kv >= 300 ? 3 : kv >= 200 ? 2 : kv >= 100 ? 1 : 0; } @@ -66,7 +70,8 @@ export const LEVELS = ['< 100 kV', '115–161 kV', '230 kV', '345 kV', '500 kV', const MAGIC = 0x31474b4c; const text = new TextDecoder(); -/** A case file's bytes, read into one dataset named `grid`: buses, lines, machines, and the fault. */ +/** Reads a case file into a dataset named `grid` with tables `Bus`, `Line`, `Machine`, and, for a + * simulated fault, `Fault`. Throws if the bytes are not a case. */ export function readCase(id: string, buffer: ArrayBuffer, downloaded = buffer.byteLength): Case { const view = new DataView(buffer); if (buffer.byteLength < 8 || view.getUint32(0, true) !== MAGIC) @@ -162,7 +167,7 @@ export function readCase(id: string, buffer: ArrayBuffer, downloaded = buffer.by if (simulated) { const frames = header.frames!; const dt = header.dt!; - // Per unit of 60 Hz, in mHz. + // Converts int16 steps to mHz: a step is `speedScale` per unit, and 1 per unit is 60 Hz. const scale = (header.speedScale ?? 1) * 60_000; const speeds = Float32Array.from(array('machine.speed', Int16Array), (value) => value * scale); patch.axis('time', 's'); @@ -198,17 +203,17 @@ export function readCase(id: string, buffer: ArrayBuffer, downloaded = buffer.by }; } -/** How far a download has come: bytes received, of how many when the response says. */ +/** Reports download progress: the bytes received so far, and the total if the response gives it. */ export type Progress = (received: number, total: number | undefined) => void; -/** Megabytes received, of how many when known, for a status line. */ +/** Formats download progress in megabytes for a status line, with the total when known. */ export const megabytes = (received: number, total: number | undefined) => `${(received / 1e6).toFixed(1)}${total ? ` of ${(total / 1e6).toFixed(1)}` : ''} MB`; /** - * A file's bytes, inflated where they arrive gzip-compressed, and how many arrived; `progress` - * hears of each chunk. A total the response gives that counts fewer bytes than arrive, as a - * compressed transfer's does, goes untold. + * Downloads a file. Returns its bytes, inflated if they arrive gzip-compressed, and the number of + * bytes received before inflating. `progress` is called for each chunk. It gets no total once the + * bytes received exceed the response's length, as they do when the transfer is compressed. */ export async function download( url: string, @@ -218,7 +223,7 @@ export async function download( const response = await fetch(url, { signal: AbortSignal.any([signal, AbortSignal.timeout(180_000)]), }); - if (!response.ok || !response.body) throw new Error(`${url} answered ${response.status}`); + if (!response.ok || !response.body) throw new Error(`${url}: HTTP ${response.status}`); const total = Number(response.headers.get('content-length')) || undefined; const chunks: Uint8Array[] = []; let received = 0; diff --git a/npm/examples/src/examples/networks/continent.ts b/npm/examples/src/examples/networks/continent.ts index 3c2aa923..a9940f73 100644 --- a/npm/examples/src/examples/networks/continent.ts +++ b/npm/examples/src/examples/networks/continent.ts @@ -1,10 +1,11 @@ import { random } from '../../random.js'; /** - * A synthetic grid the size of a continent's. Buses cluster around North America's cities as a real - * grid's do: every substation feeds radial distribution feeders, which hold most of the buses; each - * city's substations mesh at 115 and 230 kV; and cities join along 345 to 765 kV corridors through - * switching stations, the busiest by double or triple circuits, with rural substations and power - * plants along the way. Seeded, so every build draws the same grid, in time linear in its size. + * A synthetic grid the size of a continent's. Its buses cluster around North America's cities, as a + * real grid's do. Each substation feeds radial distribution feeders, which hold most of the buses. + * Each city's substations form a mesh at 115 and 230 kV. Cities connect along 230 to 765 kV + * corridors through switching stations, with double or triple circuits on the busiest, and rural + * substations and power plants along the way. The generator is seeded, so every build produces the + * same grid, in time linear in its size. */ import { Data, Patch, reference } from 'latkit'; import { levelOf, type Case } from './cases.js'; @@ -186,13 +187,14 @@ const STATION_SPACING = 90; /** The share of buses fed along corridors rather than in cities. */ const RURAL = 0.12; -/** Kilometres between two points, near enough for a continent. */ +/** Approximate kilometres between two points, accurate enough at the scale of a continent. */ function km(lon0: number, lat0: number, lon1: number, lat1: number): number { const across = (lon1 - lon0) * Math.cos(((lat0 + lat1) * Math.PI) / 360); return Math.hypot(across, lat1 - lat0) * KM; } -/** Buses and lines as they are made, in typed arrays that grow by half again. */ +/** Collects buses and lines in typed arrays that grow by half when full, and machines in plain + * arrays. */ class Builder { lon = new Float64Array(1024); lat = new Float64Array(1024); @@ -268,8 +270,8 @@ class Builder { } } -/** The cheapest tree joining `n` points, by Prim's algorithm on squared distance: each point's parent - * in it, the first point's being -1. */ +/** The minimum spanning tree of `n` points under `distance`, by Prim's algorithm in O(n²). Returns + * each point's parent, with -1 for point 0, the root. */ function spanning(n: number, distance: (a: number, b: number) => number): Int32Array { const parent = new Int32Array(n).fill(-1); const best = new Float64Array(n).fill(Infinity); @@ -291,8 +293,8 @@ function spanning(n: number, distance: (a: number, b: number) => number): Int32A return parent; } -/** Feeds `count` distribution buses from substation bus `root`: radial feeders that run outward from - * it and branch, steps of `step` degrees apart. */ +/** Adds `count` 13.8 kV distribution buses fed from substation bus `root`, on radial feeders that + * run outward and branch, with buses about `step` degrees apart. */ function feed(grid: Builder, rng: () => number, root: number, count: number, step: number): void { const feeders = Math.max(3, Math.round((count / 50) * (0.8 + 0.4 * rng()))); const lat0 = grid.lat[root]!; @@ -304,13 +306,13 @@ function feed(grid: Builder, rng: () => number, root: number, count: number, ste ? left : Math.min(left, Math.round((count / feeders) * (0.6 + 0.8 * rng()))); left -= size; - // The feeder's buses, and the way each one's line ran. + // The feeder's first bus, and the heading of the line to each of its buses. const first = grid.buses; const headings = new Float32Array(size); let heading = (2 * Math.PI * (f + rng())) / feeders; let parent = root; for (let k = 0; k < size; k++) { - // Mostly the trunk runs on; now and then a lateral branches off a bus already fed. + // Usually the trunk continues. Sometimes (18%) a lateral branches off an earlier bus. if (k > 0 && rng() < 0.18) { const from = Math.floor(rng() * k); parent = first + from; @@ -329,12 +331,13 @@ function feed(grid: Builder, rng: () => number, root: number, count: number, ste } } -/** The synthetic grids, by id, and the buses each is built to. */ +/** The target bus count of each synthetic grid, by id. */ export const CONTINENTS: Readonly> = { 'continent-2m': 2_000_000, }; -/** The synthetic grid `id` as a case named `grid`, as the case files are read. */ +/** Builds synthetic grid `id` as a `Case` with a dataset named `grid`, in the form `readCase` + * returns. Throws for an unknown id. */ export function continent(id: string): Case { const target = CONTINENTS[id]; if (!target) throw new Error(`No synthetic grid ${id}`); @@ -343,8 +346,8 @@ export function continent(id: string): Case { const weights = CITIES.map(([, , , people]) => people ** 0.85); const total = weights.reduce((a, b) => a + b, 0); - // Corridors first, as a graph of cities: the cheapest tree joining them all, and each city's two - // nearest neighbours within 700 km. + // Plan the corridors first, as a graph of cities: a minimum spanning tree joining them all, plus + // links from each city to its two nearest neighbours within 700 km. const n = CITIES.length; const distance = (a: number, b: number) => km(CITIES[a]![1], CITIES[a]![2], CITIES[b]![1], CITIES[b]![2]); @@ -360,7 +363,8 @@ export function continent(id: string): Case { for (const b of near) if (distance(a, b) < 700) corridors.add(pair(a, b)); } - // Each city: substations meshing at 115 and 230 kV, each feeding its share of the city's buses. + // In each city, substations form a mesh at 115 and 230 kV, and each feeds its share of the + // city's buses. const hubs = new Map(); const substations: number[][] = []; for (let c = 0; c < n; c++) { @@ -388,7 +392,7 @@ export function continent(id: string): Case { const [a, b] = [subs[p]!, subs[s]!]; grid.line(a, b, Math.min(grid.kv[a]!, grid.kv[b]!), grid.kv[a] !== grid.kv[b]); }); - // A mesh as well as a tree: each substation also joins one more near it. + // Links beyond the tree make a mesh: each substation also joins a random other one nearby. for (let s = 1; s < count; s++) { const other = Math.floor(rng() * count); if (other !== s && other !== parent[s]) { @@ -399,13 +403,14 @@ export function continent(id: string): Case { grid.line(a, b, Math.min(grid.kv[a]!, grid.kv[b]!), grid.kv[a] !== grid.kv[b]); } } - // Feeders run shorter where people crowd closer. + // Feeder spans are shorter in bigger cities, where people live closer together. const step = 0.0012 + 0.002 / Math.sqrt(1 + people); for (let s = 0; s < count; s++) feed(grid, rng, subs[s]!, Math.max(0, Math.floor(buses / count) - 1), step); } - /** City `c`'s bus at `kv`, made on first use and joined to its central substation. */ + /** City `c`'s bus at `kv`, created on first use and connected through a transformer to the + * city's central substation. */ const hub = (c: number, kv: number) => { const key = c * 1000 + kv; const found = hubs.get(key); @@ -419,8 +424,8 @@ export function continent(id: string): Case { return bus; }; - // Corridors: each a chain of switching stations, its circuits as many as its cities need, with a - // rural substation and its feeders off every station, and a power plant at some. + // Each corridor is a chain of switching stations, with one to three circuits by the population + // of its two cities. Every station feeds a rural substation, and some have a power plant. let stations = 0; const links = [...corridors].map((p) => [Math.floor(p / n), p % n] as const); for (const [a, b] of links) stations += Math.floor(distance(a, b) / STATION_SPACING); @@ -441,7 +446,7 @@ export function continent(id: string): Case { const circuits = people > 14 ? 3 : people > 6 ? 2 : 1; const length = distance(a, b); const hops = Math.floor(length / STATION_SPACING) + 1; - // The corridor bends a little off the straight line, the same way along its whole length. + // The corridor bows slightly to one side of the straight line along its whole length. const bend = (rng() - 0.5) * 0.12; let previous = hub(a, kv); for (let h = 1; h <= hops; h++) { diff --git a/npm/examples/src/examples/networks/index.ts b/npm/examples/src/examples/networks/index.ts index aca0d860..2cba3a68 100644 --- a/npm/examples/src/examples/networks/index.ts +++ b/npm/examples/src/examples/networks/index.ts @@ -1,11 +1,13 @@ /** - * A grid at scale over the earth. Choose a synthetic grid, from WECC 240-bus to ACTIVSg70k, a - * continent's grid of two million buses built in the page, or a lattice of up to a million buses, - * and the map beneath it: coasts and borders, Natural Earth's states colored by the grid's figures, - * or OpenStreetMap's planet as vector tiles. Play a simulated fault and watch the machines' speeds - * swing; turn the globe at noon or now; find a bus by name; hover or click anything to inspect it. - * The panels below change how buses, lines, and labels draw, how focus and comets look, and how the - * view answers the pointer and the keys. + * A large power grid drawn over the earth. Choose a grid: a synthetic case from WECC 240-bus to + * ACTIVSg70k, a two-million-bus continental grid built in the page, or a lattice of up to a million + * buses. Choose the map beneath it: coasts and borders, Natural Earth's states colored by the + * grid's statistics, or OpenStreetMap's planet as vector tiles. + * + * Play a simulated fault and watch the machine speeds swing. Turn the globe, with daylight at noon + * or now. Find a bus by name, and hover or click anything to inspect it. The other panels change + * how buses, lines, and labels draw, how focus and comets look, and how the view responds to the + * pointer and keys. */ import { Patch, @@ -41,7 +43,7 @@ import { lattice } from './lattice.js'; import { LOOK, MAPS, type Loaded, type MapChoice } from './maps.js'; import { STATE_FIELDS, joinStates, type Joined, type StateField } from './states.js'; -/** The grids the page draws, synthetic cases first. */ +/** The grid choices as [label, id], case files first. */ const GRIDS = [ ['WECC 240', 'wecc240'], ['ACTIVSg2000', 'activsg2000'], @@ -64,7 +66,7 @@ const BUS_COLORS = { generation: { label: 'generation', colormap: 'amber' }, } as const satisfies Record; type BusColor = keyof typeof BUS_COLORS; -/** A label as an option shows it, its first letter a capital. */ +/** Capitalizes a label's first letter for display as an option. */ const option = (label: string) => label[0]!.toUpperCase() + label.slice(1); const STATE_COLORMAPS: Readonly> = { buses: 'viridis', @@ -72,45 +74,45 @@ const STATE_COLORMAPS: Readonly> = { generation: 'amber', swing: 'inferno', }; -/** How a bus's marker is drawn: a shape by name, or the shape option. */ +/** Bus marker choices, each a shape name or the `{ shape }` form of `Marker`. */ const MARKERS = { circle: { shape: 'ellipse' }, square: 'rectangle', rounded: 'rounded', diamond: 'diamond', } as const satisfies Record; -/** The fonts labels may be set in. */ +/** Label font choices, as CSS font families. */ const FONTS = { sans: 'system-ui, sans-serif', serif: 'Georgia, serif', mono: 'ui-monospace, monospace', } as const; -/** Degrees on a side of the squares buses are zoned by. */ +/** The side, in degrees, of the squares that group buses into zones. */ const ZONE = 4; -/** Fields the inspector leaves out: where a row is, which the view shows, and translations. */ +/** Fields the inspector hides: location fields, which the view already shows, and translations. */ const HIDDEN = /^(lon|lat|geometry|position|zone|name_.*|name:.*)$/; -/** Noon over the western US at midsummer, where the first grid lies: seconds after the epoch. */ +/** Noon at midsummer over the western US, where the first grid lies, in seconds since the epoch. */ const NOON = Date.UTC(2026, 5, 21, 19) / 1000; const fixed = (value: number) => (value >= 100 ? value.toFixed(0) : value.toPrecision(2)); -/** A grid drawn: its dataset and counts, and the case it came from when it is one. */ +/** The grid being drawn: its dataset, its counts, and its case if it came from one. */ interface Grid { readonly id: GridId; readonly label: string; - /** Its dataset; none for the map alone. */ + /** Its dataset, absent when only the map is drawn. */ readonly data?: Data; readonly buses: number; readonly lines: number; readonly summary: string; readonly case?: Case; readonly peaks: { readonly load: number; readonly generation: number; readonly power: number }; - /** The box its buses stand in, which framing the grid shows; none for the map alone. */ + /** The bounding box of its buses, which framing shows. Absent when only the map is drawn. */ readonly bounds?: Fit; - /** Whether its buses are zoned: grouped by the square of the map they stand in. */ + /** Whether its buses are grouped into zones by the map square they lie in. */ readonly zoned: boolean; } -/** The box a case's buses stand in. */ +/** The bounding box of a case's buses. */ function extent(positions: Float32Array): Fit { const min: [number, number, number] = [Infinity, Infinity, 0]; const max: [number, number, number] = [-Infinity, -Infinity, 0]; @@ -123,8 +125,9 @@ function extent(positions: Float32Array): Fit { return { bounds: { min, max } }; } -/** A case's buses zoned by the `ZONE`-degree square each stands in: a zone for each square, and a - * reference from each bus to its zone, which an edges mark draws as a star joining its buses. */ +/** Groups a case's buses into zones by the `ZONE`-degree square each lies in. The patch adds a + * `Zone` row per square and a `zone` reference from each bus to its zone. An edges mark draws each + * zone as a star joining its buses. */ function zones(found: Case): Patch { const ids = found.data.query({ of: 'Bus', select: {} }).ids; const squares = new Map(); @@ -180,10 +183,10 @@ export default async function page(stage: Stage): Promise { night: 0.28, comets: 'screen' as 'screen' | 'map', cometScale: 1, - /** The bus a search found, ringed. */ + /** The name of the bus the last search found, which is ringed. */ found: '', }; - /** How the view answers the pointer and the keys. */ + /** How the view responds to the pointer and keys. */ const interaction = { preset: 'explore' as 'explore' | 'inspect' | 'none', zoom: 'wheel' as Zoom, @@ -233,12 +236,12 @@ export default async function page(stage: Stage): Promise { earth: { ground: LOOK.ground, atmosphere: '#5aa9ff99', night: look.night }, text: { halo: look.halo }, focus: { glow: look.glow, dim: look.dim, grow: look.grow }, - // Marks without a color of their own take the theme's: plain buses and lines; lines set to - // blend take the colors of the buses they join. + // Marks without their own color use the theme's: plain buses and lines. A null edge color + // makes lines blend the colors of the buses they join. vertex: { color: '#d9e2ec' }, edge: { color: look.lineColor === 'ends' ? null : '#8fa3b8', bow: look.bow }, - // Comets kept soft, as rivers carry them across the map too: on screen, pixels apart; on the - // map, degrees apart, so they zoom with it. + // Comets stay soft, since rivers carry them across the map too. Their length and spacing are + // in pixels on screen, or in degrees on the map, so they scale with the zoom. flow: look.comets === 'map' ? { @@ -258,7 +261,7 @@ export default async function page(stage: Stage): Promise { marks: [...mapMarks(), ...gridMarks()], }); - /** The map's marks, its states colored as chosen. */ + /** The map's marks, with its states colored by the chosen field. */ const mapMarks = (): Mark[] => { if (!map) return []; const by = state.statesBy; @@ -269,27 +272,29 @@ export default async function page(stage: Stage): Promise { return map.loaded.marks({ labels: state.mapLabels, ...(states ? { states } : {}) }); }; - /** A bus's radius, `base` CSS pixels, as the panel says: grown where it is hovered, selected, or - * near the pointer; grown with the zoom, or sized on the map and kept within pixels. */ + /** A bus's radius from `base` CSS pixels, per the Buses panel. Emphasis enlarges hovered, + * selected, or nearby buses. The size mode keeps pixels, grows with the zoom, or sets the size in + * degrees on the map, kept within 1–16 CSS pixels: 0.06° per pixel of `base` matches the pixel + * size at the opening view. */ const busRadius = (base: Expr | number): Size => { const emphasis = { none: 1, hovered: when(hovered()).then(2.2).otherwise(1), selected: when(selected()).then(2.2).otherwise(1), - // A lens: up to 2.6 times as large at the pointer, back to 1 eighty pixels out. + // A lens: 2.6 times as large at the pointer, easing back to normal size 80 pixels away. pointer: pointer().div(80).neg().add(1).max(0).mul(1.6).add(1), }[look.emphasis]; const radius = typeof emphasis === 'number' ? base : emphasis.mul(base); if (look.size === 'zoom') return zoom().sub(3).mul(0.5).clamp(0.6, 3).mul(radius); if (look.size === 'units') - return units(typeof radius === 'number' ? radius * 0.03 : radius.mul(0.03), { + return units(typeof radius === 'number' ? radius * 0.06 : radius.mul(0.06), { atLeast: 1, atMost: 16, }); return radius; }; - /** What the grid draws, as the panel says. */ + /** The grid's marks, per the panel settings. */ const gridMarks = (): Mark[] => { if (!grid?.buses) return []; const marks: Mark[] = []; @@ -334,7 +339,7 @@ export default async function page(stage: Stage): Promise { edges: { of: ['grid', 'Line'], ends: ['from', 'to'], - // Lines of higher voltage draw on top, and stay drawn as the camera pulls back. + // Higher-voltage lines draw on top and stay visible as the camera zooms out. priority: 'level', ...(look.lineColor === 'level' ? { color: level } : {}), width: field('level') @@ -360,7 +365,7 @@ export default async function page(stage: Stage): Promise { of: ['grid', 'Bus'], position: ['lon', 'lat'], ...(busColor ? { color: busColor } : {}), - // Where buses crowd, those of higher voltage stand out, on top and from farther out. + // Where buses crowd, higher-voltage buses draw on top and stay visible from farther out. priority: 'level', radius: busRadius( big ? field('level').scale(0.8, 2.6).domain(0, 5) : grid.buses > 5000 ? 2 : 3, @@ -420,15 +425,15 @@ export default async function page(stage: Stage): Promise { const view = stage.view(scene(), { label: 'A grid over the earth: drag to pan, Shift-drag to turn, hover or click to inspect', }); - // Fits keep clear of the readout over the view's top and the credit and legend over its foot. + // Fits leave room for the readout at the top and the credit and legend at the bottom. view.insets = [32, 16, 32, 16]; const restyle = () => (view.scene = scene()); - /** Frames the grid, whatever the map beneath it spans; the map alone, all of it. */ + /** Frames the grid regardless of the map's extent, or the whole map when there is no grid. */ const frame = () => view.fit(grid?.bounds ?? 'all'); const legend = stage.legend(); const credit = stage.credit(); const canvas = stage.regions.main.querySelector('canvas')!; - // A search's first bus wears a tag on the map, which follows it as the camera moves. + // A tag labels the first bus a search finds and follows it as the camera moves. const tag = h('p', { class: 'callout', hidden: true }); stage.regions.main.append(tag); let tagged: [number, number, number] | undefined; @@ -509,7 +514,7 @@ export default async function page(stage: Stage): Promise { const flat = state.projection === 'plane' || projection === 'plane'; state.projection = projection; restyle(); - // Between the earth and a plane, positions mean other units: frame the grid again. + // Positions have different units on the earth and on a plane, so switching reframes the grid. if (flat) return frame(); const camera = view.camera; if ('center' in camera) view.fly({ ...camera, pitch: projection === 'tilt' ? 50 : 0 }); @@ -538,7 +543,7 @@ export default async function page(stage: Stage): Promise { }); viewSection.toggle('Orbit', 't', false, (on) => (view.orbit = on ? 4 : 0)); viewSection.actions([{ label: 'Fit', key: 'f', run: frame }]); - // Where the pointer meets the ground, and where the camera stands, as the view tells it. + // Notes showing where the pointer meets the ground, and the camera's zoom, pitch, and bearing. const where = viewSection.note('Point at the map.'); const camera = viewSection.note(); const degrees = (value: number, [plus, minus]: string) => @@ -804,7 +809,7 @@ export default async function page(stage: Stage): Promise { const fields = (table?.fields ?? []).map((f) => f.name).filter((name) => !HIDDEN.test(name)); const name = fields.find((f) => /^name$/i.test(f)); described.textContent = `${row.table} · ${name ? format(value(name)) : `#${row.id}`}${hoveredRow ? '' : ' · selected'}`; - // The grid's figures first, then the rest, leaving out what this row has no value for. + // The grid's statistics first, then the other fields, skipping fields without a value. const first = fields.filter((f) => f in STATE_FIELDS); let shown = 0; for (const f of [...first, ...fields.filter((f) => f !== name && !(f in STATE_FIELDS))]) { @@ -825,13 +830,15 @@ export default async function page(stage: Stage): Promise { }); view.on('selection', inspect); - /** Puts the tag where its bus now stands on screen, or hides it where none shows. */ + /** Moves the tag to its bus's screen position, or hides it when nothing is tagged or the bus is + * behind the camera. */ function pin(): void { const point = tagged && view.screen(tagged); tag.hidden = !point; if (point) tag.style.translate = `${point[0]}px ${point[1]}px`; } - /** Selects every bus whose name holds `text`, frames them, and tags the first. */ + /** Selects the buses whose names contain `text`, up to 200 of the highest voltage, frames them, + * and tags and rings the first. */ function find(text: string): void { const data = grid?.data; tagged = undefined; @@ -862,7 +869,7 @@ export default async function page(stage: Stage): Promise { look.found = names[0]!; restyle(); view.selection = rows; - // One found is brought into view as the camera stands; more are framed together. + // A single match is brought into view at the current zoom. Several are framed together. view.fit(found.ids.length === 1 ? { reveal: first } : { rows }); tagged = [lon[0]!, lat[0]!, 0]; tag.textContent = names[0]!; @@ -892,8 +899,8 @@ export default async function page(stage: Stage): Promise { legend.ramp(colormap, `${label} 0 – ${fixed(grid.peaks[state.color])} MW`); } - /** The grid's figures in the map's states, when it has states, and what they color by; and the - * view given what the map and the grid hold. */ + /** Rewrites the grid's statistics into the map's states, when the map has states and a state + * color is chosen. Then updates the view's sources, marks, and legend. */ function join(): void { if (joined) map?.loaded.states?.restore(joined.before); joined?.before.destroy(); @@ -946,7 +953,7 @@ export default async function page(stage: Stage): Promise { stage.detail(`${label}: downloading ${megabytes(received, total)}`), ); if (load.signal.aborted) return; - // Cases' buses are zoned by where they stand; a continent's are too many to zone in the page. + // Zone case files' buses by location. The continent has too many buses to zone here. const zoned = found.named; if (zoned) found.data.apply(zones(found)); const peak = (values: Float32Array) => values.reduce((top, v) => Math.max(top, v), 0); @@ -1028,7 +1035,7 @@ export default async function page(stage: Stage): Promise { await Promise.all([loadGrid(GRIDS[0][1]), loadMap(MAPS[0])]); } -/** A value read from a dataset, written for the inspector. */ +/** Formats a dataset value for the inspector. */ function format(value: unknown): string { if (value === null || value === undefined) return '–'; if (typeof value === 'number') diff --git a/npm/examples/src/examples/networks/lattice.ts b/npm/examples/src/examples/networks/lattice.ts index 92daa540..2bf4c174 100644 --- a/npm/examples/src/examples/networks/lattice.ts +++ b/npm/examples/src/examples/networks/lattice.ts @@ -1,7 +1,7 @@ /** - * Square lattices of buses over most of the globe: the stress cases, past any synthetic case in - * size. Each bus joins its right and lower neighbours, and a few lines span the whole box as great - * circles. Built in time linear in the bus count. + * Square lattices of buses over most of the globe, used as stress cases larger than any synthetic + * case. Each bus connects to its east and north neighbours, and a few lines span the whole box, + * which the geodesic route draws as great circles. Built in time linear in the bus count. */ import { Data, Patch, reference, type Fit } from 'latkit'; @@ -39,8 +39,8 @@ export const LATTICES: Readonly> = { }, }; -/** The lattice `id` as dataset `grid`, buses colored by their distance from the box's middle, and - * the box. */ +/** Builds lattice `id` as dataset `grid`, with its counts and bounding box. Each bus's `load` is + * its distance from the box's center, scaled to 0–1, for coloring. Throws for an unknown id. */ export function lattice(id: string): { data: Data; buses: number; lines: number; bounds: Fit } { const spec = LATTICES[id]; if (!spec) throw new Error(`No lattice ${id}`); diff --git a/npm/examples/src/examples/networks/maps.ts b/npm/examples/src/examples/networks/maps.ts index 5c6a91e2..4c4aeb22 100644 --- a/npm/examples/src/examples/networks/maps.ts +++ b/npm/examples/src/examples/networks/maps.ts @@ -1,8 +1,11 @@ /** - * What the map beneath the grid draws: Natural Earth's coasts and borders as lines, shipped with the - * page; Natural Earth's states and provinces as areas the grid's figures color, from Natural Earth's - * repository; or OpenStreetMap's planet as OpenFreeMap's vector tiles. GeoJSON downloads as bytes - * and is read in a worker; tiles are fetched as the camera asks for them. + * The maps drawn beneath the grid: + * - Natural Earth's coasts and borders as lines, shipped with the page; + * - Natural Earth's states and provinces as areas colored by the grid's statistics, downloaded from + * Natural Earth's repository; + * - OpenStreetMap's planet as OpenFreeMap's vector tiles. + * + * GeoJSON downloads as bytes and is read in a worker. Tiles are fetched as the camera needs them. */ import { Data, Tiles, field, type ColormapName, type Mark, type Source } from 'latkit'; import { download, megabytes } from './cases.js'; @@ -22,19 +25,20 @@ export const LOOK = { place: '#c3ccd8', } as const; -/** How the map draws: whether its areas are labeled, and what its states color by. */ +/** Map style options: whether areas are labeled, and which field colors the states. */ export interface Styling { readonly labels: boolean; readonly states?: { readonly by: string; readonly peak: number; readonly colormap: ColormapName }; } -/** A map, loaded: its marks, drawn under the grid, what they draw, and its states when it has them. */ +/** A loaded map: a summary for the status line, its marks (drawn under the grid), its sources, and + * its states dataset if it has one. */ export interface Loaded { readonly summary: string; marks(styling: Styling): Mark[]; /** The datasets its marks draw. */ readonly sources: readonly Source[]; - /** The dataset of states the grid's figures color, its rows in table `polygons`. */ + /** The states dataset, colored by the grid's statistics. Its rows are in table `polygons`. */ readonly states?: Data; } @@ -42,7 +46,7 @@ export interface MapChoice { readonly id: string; readonly label: string; readonly attribution: string; - /** Loads the map, telling `progress` how it goes. */ + /** Loads the map, reporting status text to `progress`. */ load(signal: AbortSignal, progress: (status: string) => void): Promise; } @@ -50,7 +54,7 @@ const NE = 'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/maste const NATURAL_EARTH = 'Natural Earth · public domain'; const seconds = (since: number) => `${((performance.now() - since) / 1000).toFixed(1)} s`; -/** Natural Earth's coasts, land borders, and province borders at 1:50M: datasets of lines. */ +/** Natural Earth's coastlines, country borders, and province borders at 1:50M, drawn as lines. */ const borders: MapChoice = { id: 'borders', label: 'Coasts and borders', @@ -81,7 +85,7 @@ const borders: MapChoice = { }, }; -/** Natural Earth's states and provinces at `scale`: areas the grid's figures color. */ +/** Natural Earth's states and provinces at `scale`, as areas colored by the grid's statistics. */ function states(scale: '50m' | '10m', label: string): MapChoice { return { id: `states-${scale}`, @@ -102,7 +106,7 @@ function states(scale: '50m' | '10m', label: string): MapChoice { summary: `${(received / 1e6).toFixed(1)} MB in ${seconds(started)}, read in ${seconds(fetched)}`, marks: ({ labels, states }) => [ { - // Each state holds the grid's figures: hovering one inspects them. + // Each state carries the grid's statistics, which hovering it shows in the inspector. areas: { of: ['states', 'polygons'], color: states @@ -127,16 +131,17 @@ const planet: MapChoice = { load() { return Promise.resolve({ sources: [new Tiles('base', 'https://tiles.openfreemap.org/planet')], - summary: 'tiles fetch as the camera asks for them', + summary: 'tiles fetched as the camera needs them', marks: ({ labels }) => [ { areas: { of: ['base', 'landcover'], color: LOOK.landcover } }, { areas: { of: ['base', 'park'], color: LOOK.park } }, { areas: { of: ['base', 'water'], color: LOOK.water } }, - // Rivers carry comets downstream, as their ways run. + // Comets run along rivers in the direction of their OpenStreetMap ways: downstream. { lines: { of: ['base', 'waterway'], color: LOOK.waterway, width: 0.8, flow: 10 } }, { lines: { of: ['base', 'transportation'], color: LOOK.road, width: 0.7 } }, { lines: { of: ['base', 'boundary'], color: LOOK.line, width: 1, dash: true } }, - // Places by rank, 1 the most important; a tile holds only the points inside it, so none repeat. + // Place labels by rank, where 1 is the most important. A tile holds only the points inside + // it, so no label repeats. ...(labels ? [ { diff --git a/npm/examples/src/examples/networks/states.ts b/npm/examples/src/examples/networks/states.ts index 6e3b8fd9..a34cb1c8 100644 --- a/npm/examples/src/examples/networks/states.ts +++ b/npm/examples/src/examples/networks/states.ts @@ -1,12 +1,12 @@ /** - * A grid's figures for each state it reaches, written into the states dataset as fields its areas - * color by: buses, load, and generation counted where each bus stands, and where the grid was - * simulated, how far its machines' speeds swing in each state, frame by frame. + * Per-state statistics of a grid, written into the states dataset as fields the state areas can be + * colored by. Each bus adds to the bus count, load, and generation of the state it lies in. For a + * simulated grid, each state also gets its machines' largest speed swing, frame by frame. */ import { Patch, type Data, type Id, type Snapshot } from 'latkit'; import type { Case } from './cases.js'; -/** What each state's figures hold, and in what unit. */ +/** The per-state fields, with their labels and units. */ export const STATE_FIELDS = { buses: { label: 'buses', unit: '' }, load: { label: 'load', unit: ' MW' }, @@ -16,11 +16,11 @@ export const STATE_FIELDS = { export type StateField = keyof typeof STATE_FIELDS; export interface Joined { - /** The states as they were before the grid's figures were written into them. */ + /** A snapshot of the states from before the grid's statistics were written, for restoring. */ readonly before: Snapshot; - /** How many states the grid reaches. */ + /** The number of states that contain a bus of the grid. */ readonly states: number; - /** Each figure's largest value in any state. */ + /** Each field's largest value in any state. */ readonly peaks: Readonly>; } @@ -33,7 +33,8 @@ interface State { readonly rings: readonly Ring[]; } -/** Whether a point lies inside a state: crossing its rings an odd number of times, holes included. */ +/** Whether a point lies inside a state, by the even-odd rule: a ray from the point crosses the + * state's rings, holes included, an odd number of times. */ function inside(state: State, x: number, y: number): boolean { const [w, s, e, n] = state.bounds; if (x < w || x > e || y < s || y > n) return false; @@ -47,7 +48,8 @@ function inside(state: State, x: number, y: number): boolean { return crossings % 2 === 1; } -/** Writes the grid's figures into `states` for every state it reaches, keeping what they were. */ +/** Writes the grid's statistics into `states` for every state the grid reaches. The result keeps a + * snapshot of the states from before, for restoring them. */ export function joinStates(states: Data, grid: Case): Joined { const { positions, simulation } = grid; const extent = [Infinity, Infinity, -Infinity, -Infinity]; @@ -77,7 +79,7 @@ export function joinStates(states: Data, grid: Case): Joined { bounds[3] >= extent[1]!; if (meets) found.push({ row, bounds, rings }); }); - // Each degree cell's states, so a bus tests only those near it. + // Index the states by one-degree cell, so each bus tests only the states near it. const cells = new Map(); const cell = (x: number, y: number) => Math.floor(x + 180) * 180 + Math.floor(y + 90); for (const state of found) { @@ -115,7 +117,7 @@ export function joinStates(states: Data, grid: Case): Joined { const patch = new Patch().set('polygons', ids as number[], { buses, load, generation }); const peaks = { buses: peak(buses), load: peak(load), generation: peak(generation), swing: 0 }; if (simulation) { - // Each frame, the largest swing of a state's machines either way. + // For each frame, the largest absolute speed deviation among each state's machines. const g = grid.machines; const swing = new Float32Array(rows); for (let f = 0; f < simulation.frames; f++) { diff --git a/npm/examples/src/examples/power-flow/grid.test.ts b/npm/examples/src/examples/power-flow/grid.test.ts index 9dfdd946..ca068c9e 100644 --- a/npm/examples/src/examples/power-flow/grid.test.ts +++ b/npm/examples/src/examples/power-flow/grid.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from 'vitest'; import { Grid } from './grid.js'; import texas from './texas.json'; -/** MW each substation sends out along its branches in service, by the flows solved. */ +/** The net MW each substation sends out along its branches, from the solved flows. */ function sent(grid: Grid): Float64Array { const out = new Float64Array(grid.n); texas.branches.forEach((branch, j) => { @@ -12,14 +12,14 @@ function sent(grid: Grid): Float64Array { return out; } -/** The largest gap between what each substation injects and what its branches carry away. */ +/** The largest difference between a substation's injection and the net flow out on its branches. */ function imbalance(grid: Grid): number { const out = sent(grid); return Math.max(...Array.from(out, (mw, i) => Math.abs(mw - grid.injection[i]!))); } describe('the DC power flow', () => { - test('balances every island: plants make what loads are served, and each bus sends on its injection', () => { + test('generation matches served load and each bus sends out its injection', () => { const grid = new Grid(); const total = grid.injection.reduce((sum, mw) => sum + mw, 0); expect(Math.abs(total)).toBeLessThan(1e-6); diff --git a/npm/examples/src/examples/power-flow/grid.ts b/npm/examples/src/examples/power-flow/grid.ts index 260873ae..da2d0158 100644 --- a/npm/examples/src/examples/power-flow/grid.ts +++ b/npm/examples/src/examples/power-flow/grid.ts @@ -1,10 +1,10 @@ /** - * The grid a control room watches, as a model: plants dispatch to meet demand, a DC power flow sets - * every branch's flow, protection trips what runs past its rating, and islands balance alone. + * A model of the grid a control room watches. Plants dispatch to meet demand, a DC power flow sets + * every branch's flow, protection trips overloaded branches, and each island balances on its own. */ import texas from './texas.json'; -/** What each substation is, in the order its color index counts them. */ +/** The kinds of substation, in color index order. `fuels` lists the categories of each kind. */ export const KINDS = [ { label: 'nuclear', color: '#a181ef', fuels: ['Nuclear Steam'] }, { @@ -16,7 +16,8 @@ export const KINDS = [ { label: 'solar', color: '#ffca29', fuels: ['Solar PV'] }, { label: 'load', color: '#8a93a0', fuels: ['Load'] }, ] as const; -/** How far past its rating a branch runs before its protection trips it, and for how many ticks. */ +/** Protection trips a branch loaded over `TRIP_AT` times its rating for `TRIP_AFTER` ticks in a + * row. */ export const TRIP_AT = 1.15; export const TRIP_AFTER = 3; /** Seconds a tripped branch stays out before it recloses. */ @@ -26,19 +27,19 @@ export const REACTANCE = 0.0004; type Substation = (typeof texas.substations)[number]; -/** Each substation's kind, by index into `KINDS`. */ +/** A substation's kind, as an index into `KINDS`. */ const kindOf = (s: Substation) => KINDS.findIndex((k) => (k.fuels as readonly string[]).includes(s.category)); /** - * The grid's state and its physics: plants dispatch to meet demand, a DC power flow sets every - * branch's flow from the injections, protection trips branches held past their rating, and each - * island balances on its own plants, its loads dark where it has none. + * The grid's state and physics. Plants dispatch to meet demand, and a DC power flow sets every + * branch's flow from the injections. Protection trips branches that stay over their rating. Each + * island balances on its own plants, and its loads go dark if it has none. */ export class Grid { readonly n = texas.substations.length; readonly m = texas.branches.length; - /** What each plant can make and each load asks for, MW. */ + /** Each plant's capacity, or each load's size, in MW. */ readonly capacity = Float64Array.from(texas.substations, (s) => s.units.reduce((a, b) => a + b, 0), ); @@ -46,19 +47,22 @@ export class Grid { readonly kind = Float32Array.from(texas.substations, kindOf); readonly rating = Float64Array.from(texas.branches, (b) => b.pmax); readonly #x: Float64Array; - /** Each substation's output, or each load's demand served, as a share; MW injected. */ + /** Each plant's output as a share of its capacity, or each load's share of demand served. + * `injection` holds the MW each substation injects. */ readonly fill = new Float32Array(this.n); readonly injection = new Float64Array(this.n); - /** Each branch's flow in MW, from its first end to its second; its loading; whether it is out. */ + /** Each branch's flow in MW from its `from` end to its `to` end, its loading as a share of its + * rating, and whether it is out. */ readonly flow = new Float64Array(this.m); readonly loading = new Float64Array(this.m); readonly out = new Uint8Array(this.m); - /** Ticks each branch has run past its trip setting, and when an out branch recloses. */ + /** Consecutive ticks each branch has been over its trip setting, and when an out branch + * recloses. */ readonly #over = new Uint8Array(this.m); readonly #reclose = new Float64Array(this.m); - /** Plants out of service until a time. */ + /** For each plant, the time until which it is out of service. */ readonly #plantOut = new Float64Array(this.n); - /** Demand as a share of the loads' size, and the time. */ + /** Demand as a share of the loads' size, and the simulated time in seconds. */ demand = 0.78; t = 0; /** Branches tripped since the last restore, and loads dark now. */ @@ -76,7 +80,7 @@ export class Grid { this.step(0); } - /** Trips the branch loaded nearest its rating, as a fault would. */ + /** Trips the most heavily loaded branch in service, as a fault would. */ tripWorst(): void { let worst = -1; for (let j = 0; j < this.m; j++) @@ -100,13 +104,14 @@ export class Grid { this.trips++; } - /** Advances `dt` seconds: dispatch, flow, protection. */ + /** Advances `dt` seconds, then dispatches, solves the flows, and runs protection. A `dt` of 0 + * solves again without tripping anything. */ step(dt: number): void { this.t += dt; const { n, m, t } = this; for (let j = 0; j < m; j++) if (this.out[j] && t >= this.#reclose[j]! && this.#reclose[j]! > 0) this.out[j] = 0; - // What each load asks for, swinging through the day, and what each plant could give. + // Each load's demand, which swings through the day, and each plant's available output. const want = new Float64Array(n); const can = new Float64Array(n); for (let i = 0; i < n; i++) { @@ -114,7 +119,7 @@ export class Grid { if (this.load[i]) want[i] = this.capacity[i]! * Math.min(1.4, Math.max(0, this.demand + wave)); else if (t >= this.#plantOut[i]!) { - // Wind and sun come and go; the rest follow dispatch. + // Wind and solar output vary with the weather. Other plants follow dispatch. const k = this.kind[i]!; const weather = k === 2 @@ -125,7 +130,8 @@ export class Grid { can[i] = this.capacity[i]! * weather; } } - // Islands of the branches in service, each balanced on its own plants. + // Split the grid into islands joined by branches in service, and balance each on its own + // plants. const island = this.#islands(); this.dark = 0; for (const members of island) { @@ -145,7 +151,7 @@ export class Grid { } this.#solve(members); } - // Protection: a branch held past its trip setting trips; a plant now and then does too. + // Protection trips a branch that stays over its trip setting. Now and then a plant trips too. for (let j = 0; j < m; j++) { this.loading[j] = this.out[j] ? 0 : Math.abs(this.flow[j]!) / this.rating[j]!; this.#over[j] = this.loading[j]! > TRIP_AT ? this.#over[j]! + 1 : 0; @@ -163,7 +169,7 @@ export class Grid { return !this.load[i] && this.t < this.#plantOut[i]!; } - /** Substations joined by branches in service, island by island. */ + /** Groups the substations into islands joined by branches in service. */ #islands(): number[][] { const seen = new Uint8Array(this.n); const near: number[][] = Array.from({ length: this.n }, () => []); @@ -189,8 +195,9 @@ export class Grid { return islands; } - /** The DC power flow of one island: angles from its injections, its first member the reference, - * then each branch's flow. */ + /** Solves the DC power flow of one island. Bus angles come from the injections (per unit on a + * 100 MVA base), with the first member as the reference bus. Branch flows follow from the + * angles. */ #solve(members: number[]): void { const size = members.length - 1; const at = new Map(members.slice(1).map((bus, k) => [bus, k])); @@ -208,7 +215,7 @@ export class Grid { } }); members.slice(1).forEach((bus, k) => (b[k]![size] = this.injection[bus]! / 100)); - // Gaussian elimination with partial pivoting; the island is small. + // Gaussian elimination with partial pivoting, which is fast enough for an island this small. for (let c = 0; c < size; c++) { let pivot = c; for (let r = c + 1; r < size; r++) diff --git a/npm/examples/src/examples/power-flow/index.ts b/npm/examples/src/examples/power-flow/index.ts index bbaab9b8..ede8986b 100644 --- a/npm/examples/src/examples/power-flow/index.ts +++ b/npm/examples/src/examples/power-flow/index.ts @@ -1,11 +1,14 @@ /** - * Grid operations: a state's transmission grid as a control room watches it. Each substation is a - * gauge of its output sized by its capacity, a plant's round and a load's square, or a pie of it, or - * an icon of its kind; branches are as wide as their rating, color by loading, carry comets along - * their flow, bow apart where they run in parallel, and bend along their corridors. The flows come - * from a DC power flow solved every tick: push demand up or trip a line, and overloaded branches trip - * in turn, flows swing onto what is left, and loads cut off from every plant go dark. Each tick eases - * into place; what trips pulses until it is restored. + * Grid operations: a state's transmission grid as a control room watches it. + * + * Each substation is a gauge of its output sized by its capacity (round for a plant, square for a + * load), a pie, or an icon of its kind. A branch's width shows its rating and its color shows its + * loading. Comets travel along each branch's flow, parallel branches bow apart, and branches can + * bend along their corridors. + * + * A DC power flow is solved every tick. Push demand up or trip a line, and overloaded branches trip + * in turn, flows shift onto the remaining branches, and loads cut off from every plant go dark. + * Each tick eases into place, and tripped equipment pulses until it is restored. */ import { Data, @@ -28,7 +31,7 @@ import { theme } from '../../ui/theme.js'; import { Grid, KINDS } from './grid.js'; import texas from './texas.json'; -/** The control room's dark theme: a quiet network, its alarms and flows bright. */ +/** The control room's dark palette: a muted network with bright alarms and flows. */ const LOOK = { background: '#07090c', ground: '#10161d', @@ -41,8 +44,8 @@ const LOOK = { tripped: '#ff1915', } as const; -/** A branch's color by its loading, % of its rating: calm blues to 80%, then amber at 90% through - * red where its protection trips it. */ +/** A branch's color by its loading in % of its rating: blues up to 80%, then amber at 90%, turning + * red around the trip setting. */ const LOADING = field('loading') .colormap({ kind: 'sequential', @@ -67,7 +70,8 @@ const LOADING = field('loading') /** Seconds between ticks of the simulation. */ const TICK = 0.5; -/** An icon for each kind of substation, a tripped one's last: a disc of its color, its initial on it. */ +/** Draws an icon per substation kind: a disc in the kind's color with its initial. The last icon, + * a red disc marked ×, is for substations that are tripped or dark. */ function icons(): RgbaImage[] { const size = 48; return [ @@ -89,8 +93,8 @@ function icons(): RgbaImage[] { }); } -/** Where each branch bends: a point beside its middle, a tenth of its length out, sides alternating, - * as a corridor runs around what lies between its substations. */ +/** Each branch's corridor bend: one point beside its midpoint, a tenth of its length out, on + * alternating sides, like a corridor routed around obstacles between its substations. */ function corridors(): number[][][] { const s = texas.substations; return texas.branches.map((b, j) => { @@ -100,7 +104,7 @@ function corridors(): number[][][] { return [[(a.lon + c.lon) / 2 - dy * side, (a.lat + c.lat) / 2 + dx * side]]; }); } -/** The grid's state as a patch of the `grid` dataset's changing fields. */ +/** A patch of the `grid` dataset's changing fields, from the grid's current state. */ function patchOf(grid: Grid): Patch { const kind = Float32Array.from(grid.kind, (k, i) => grid.tripped(i) || (grid.load[i] && grid.fill[i]! < 0.05) ? KINDS.length : k, @@ -133,7 +137,7 @@ function patchOf(grid: Grid): Patch { } export default async function page(stage: Stage): Promise { - // Labels set in Jura once it has loaded. + // Labels use the Jura font, so load it first. const jura = new FontFace( 'Jura', `url(${import.meta.env.BASE_URL}fonts/jura-latin-400-normal.woff2)`, @@ -196,7 +200,7 @@ export default async function page(stage: Stage): Promise { }; let motion: Motion = 'system'; const ICONS = icons(); - /** What trips pulses: red to dark red and back, twice a second. */ + /** The pulse of tripped equipment: red to dark red and back, twice a second. */ const pulse = elapsed() .mul(4 * Math.PI) .sin() @@ -207,14 +211,15 @@ export default async function page(stage: Stage): Promise { colors: [...KINDS.map((k) => k.color), LOOK.tripped], categories: [...KINDS.map((k) => k.label), 'tripped or dark'], }); - // Comets run with the flow, in degrees a second, faster the nearer its rating, and never on a - // branch out. + // Comet speed in degrees per second. Comets move in the flow's direction, faster as the loading + // rises. A speed of 0 draws none, so an out branch has no comets. const comets = when(field('out').gt(0.5)) .then(0) .when(field('flow').lt(0)) .then(field('flow').mul(1.8).sub(0.45)) .otherwise(field('flow').mul(1.8).add(0.45)); - /** How a substation draws: a gauge of its output, a pie of output and headroom, or its kind's icon. */ + /** A substation's marker: a gauge of its output, a pie of output and headroom, or its kind's + * icon. */ const marker = (shape: 'ellipse' | 'rounded'): Marker => state.markers === 'pies' ? { @@ -227,14 +232,16 @@ export default async function page(stage: Stage): Promise { : state.markers === 'icons' ? icon(ICONS, field('kind')) : { gauge: { fill: field('fill'), shape } }; - /** A substation mark: plants as round gauges, loads as square ones, in one table. */ + /** A mark for the plants (round gauges) or the loads (square ones). Both marks draw the one + * `Substation` table. */ const substations = (name: string, load: number, shape: 'ellipse' | 'rounded') => ({ vertices: { of: ['grid', 'Substation'] as [string, string], name, position: ['lon', 'lat'] as [string, string], visible: field('load').eq(load), - // Both marks size every row alike, by its capacity, so branches stop at the outline that shows. + // Both marks give every row the same radius from its capacity, so branches stop at the + // outline that is shown. radius: field('capacity').sqrt().scale(8, 17).domain(11, 46), marker: marker(shape), color: kinds, @@ -260,12 +267,14 @@ export default async function page(stage: Stage): Promise { text: { color: LOOK.text, font: 'Jura, sans-serif', size: 12, halo: 3 }, // Selected rows glow blue, or each in its own color. focus: { hover: LOOK.hover, selected: state.ownGlow ? null : LOOK.hover, glow: 6 }, - // Comets are part of the map, in degrees: they zoom with it and stay on their branches. + // Comet spacing and length are in map units (degrees), so comets zoom with the map and stay + // on their branches. flow: { color: LOOK.comet, spacing: units(1.35), length: units(0.6), width: 6 }, edge: { bow: 0.22 }, }, ease: state.ease, - // Easing, pulses, and comets hold still where the system asks for reduced motion, unless set. + // With `motion` at 'system', easing, pulses, and comets stop when the system asks for reduced + // motion. controls: { motion }, marks: [ { lines: { of: ['state', 'Border'], width: 1.25, color: LOOK.border } }, @@ -400,7 +409,7 @@ export default async function page(stage: Stage): Promise { const meter = stage.meter(view); meter.note(() => `${grid.trips} trips`); - /** Shows the grid as it stands. */ + /** Shows the grid's current state in the view, the totals, and the inspector. */ function show(): void { data.apply(patchOf(grid)); const served = grid.injection.reduce((sum, p, i) => sum + (grid.load[i] ? -p : 0), 0); diff --git a/npm/examples/src/examples/soft-body/body.test.ts b/npm/examples/src/examples/soft-body/body.test.ts index c505b1b3..17ec278f 100644 --- a/npm/examples/src/examples/soft-body/body.test.ts +++ b/npm/examples/src/examples/soft-body/body.test.ts @@ -1,4 +1,4 @@ -/** The body's numerical checks, run by vitest; no wasm build. */ +/** Numerical checks of the soft body, run by vitest without a wasm build. */ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { test } from 'vitest'; @@ -101,7 +101,7 @@ test('body rests where it lands, hops at once, and ignores presses in the air', }); test('stands at any firmness, stays down once toppled, and wakes when poked', () => { - // Creeping as fast as before, a body would never count as still, and never rest. + // A body that kept creeping would never count as still, so it would never rest. for (const firmness of [0, 0.45, 1]) { const body = new Body(mesh); body.drop(true); @@ -179,7 +179,7 @@ test('pop lifts every piece and visibly separates before gathering', () => { ); assert.ok(separated.length >= 20, `${separated.length}/24 pieces left the original silhouette`); run(body, 0.6); - assert.ok(!body.burst.gathering, 'the full scatter has time to read before automatic return'); + assert.ok(!body.burst.gathering, 'the pieces stay scattered long enough to see'); run(body, 4); assert.ok(!body.burst.active, 'automatic assembly still completes'); diff --git a/npm/examples/src/examples/soft-body/body.ts b/npm/examples/src/examples/soft-body/body.ts index ee49508b..e83c5a75 100644 --- a/npm/examples/src/examples/soft-body/body.ts +++ b/npm/examples/src/examples/soft-body/body.ts @@ -1,47 +1,48 @@ import type { Mesh } from './mesh.js'; import { Burst } from './burst.js'; -/** What the controls set; the body reads them every step. */ +/** Settings from the page's controls. The body reads them every step. */ export interface Controls { /** Meters per second squared. */ gravity: number; - /** 0 is jelly, 1 is rubber: how hard the body pulls back to its shape. */ + /** How strongly the body pulls back to its shape, from 0 (jelly) to 1 (rubber). */ firmness: number; - /** The volume the body's pressure holds, as a share of its rest volume. */ + /** The volume the body's pressure keeps, as a fraction of its rest volume. */ inflate: number; wind: boolean; } -/** One frame of every vertex, a column per field, filled in place by {@link Body.sample}. */ +/** Per-vertex state at one step, one column per field, filled in place by {@link Body.sample}. */ export interface Frame { readonly x: Float32Array; readonly y: Float32Array; readonly z: Float32Array; /** Meters per second. */ readonly speed: Float32Array; - /** Meters from where the rest shape would put the vertex. */ + /** Meters between the vertex and its place in the rest shape the body is pulled toward. */ readonly strain: Float32Array; } -/** One step, in seconds. */ +/** Length of one simulation step, in seconds. */ export const STEP = 1 / 120; -/** The walls stand this far from the center. */ +/** Distance from the center to each wall, in meters. */ export const ROOM = 3.6; const CEILING = 6.5; -/** Mean speed, in meters per second, that counts as still, and the seconds it lasts before the - * body rests. */ +/** The body counts as still below this mean speed, in meters per second, and rests after staying + * still for `REST_AFTER` seconds. */ const STILL = 0.02; const REST_AFTER = 0.5; -/** Floor friction against how far a vertex sank this step: static holds it, kinetic slows it. */ +/** Floor friction, scaled by how far a vertex sank into the floor this step. Static friction stops + * a short slide, and kinetic friction slows a longer one. */ const STATIC_FRICTION = 0.8; const KINETIC_FRICTION = 0.5; -/** Bends resist folding at this multiple of the springs' compliance. */ +/** Compliance of the bend constraints, as a multiple of the springs' compliance. */ const BEND = 12; -/** How far a full squash lowers the rest shape, which keeps its volume. */ +/** Fraction by which a full squash lowers the rest shape. The shape widens to keep its volume. */ const SQUASH = 0.29; -/** Past this tilt from upright, as the cosine of its angle, the bunny topples and stays down. */ +/** Cosine of the tilt from upright past which the bunny topples and stays down. */ const TIPPING = 0.6; -/** How fast, per second, a standing bunny's turn returns upright. */ +/** Rate, per second, at which a standing bunny's orientation returns to upright. */ const BALANCE = 18; type Vec3 = [number, number, number]; @@ -51,10 +52,10 @@ const cross = (a: Vec3, b: Vec3): Vec3 => [ a[0] * b[1] - a[1] * b[0], ]; const dot = (a: Vec3, b: Vec3) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; -/** A vector's length: what `Math.hypot` gives, at a fraction of its cost in the inner loops. */ +/** A vector's length. Same as `Math.hypot`, but much cheaper in the inner loops. */ const norm = (x: number, y: number, z: number) => Math.sqrt(x * x + y * y + z * z); const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v)); -/** Column-major rotation of a unit quaternion [x, y, z, w]. */ +/** The rotation matrix of a unit quaternion [x, y, z, w], as its three columns. */ function rotation([x, y, z, w]: number[]): [Vec3, Vec3, Vec3] { return [ [1 - 2 * (y! * y! + z! * z!), 2 * (x! * y! + w! * z!), 2 * (x! * z! - w! * y!)], @@ -64,22 +65,23 @@ function rotation([x, y, z, w]: number[]): [Vec3, Vec3, Vec3] { } /** - * A soft bunny: Verlet integration with compliant stretch, bend, and volume constraints, pulled - * toward its rest shape. It moves only when something acts on it, and rests once still, so a - * resting bunny costs nothing until it is touched. + * A soft bunny, simulated with Verlet integration, compliant stretch, bend, and volume constraints, + * and a pull toward its rest shape. It moves only when something acts on it and rests once still, + * so a resting bunny needs no simulation steps until it is touched. */ export class Body { readonly count: number; - /** Positions now and one step ago, x, y, z per vertex. */ + /** Current positions, x, y, z per vertex. `previous` holds the positions one step earlier. */ readonly x: Float64Array; private readonly previous: Float64Array; - /** Rest positions about the rest center. */ + /** Rest positions relative to the rest shape's center. */ private readonly rest: Float64Array; - /** How closely each vertex follows the rest shape: the ears, softer, lag behind. */ + /** How closely each vertex follows the rest shape. The ears are softer and lag behind. */ private readonly softness: Float64Array; /** - * Distance constraints, a spring along every edge and then a bend across it: their two ends, - * rest lengths, compliance as a multiple of the springs', and multipliers. + * Distance constraints. Springs along every edge come first, then bends joining the two corners + * opposite each edge. Each has two end vertices, a rest length, a compliance as a multiple of the + * springs', and a Lagrange multiplier. */ private readonly ends: Uint32Array; private readonly lengths: Float64Array; @@ -107,7 +109,7 @@ export class Body { /** Seconds it has been still with nothing acting on it. */ private still = 0; private scale = 1; - /** How far squashed, 0 to 1, and until when the squash holds. */ + /** How far squashed, from 0 to 1, and the simulated time until which the squash is held. */ private squashing = 0; private squashUntil = -Infinity; private grabbed?: { @@ -125,7 +127,7 @@ export class Body { this.rest = Float64Array.from(mesh.positions); const center = this.center(this.rest); for (let i = 0; i < count * 3; i++) this.rest[i] = this.rest[i]! - center[i % 3]!; - // The ears, above 1.5 m, follow the rest shape loosely, their tips most. + // The ears, above 1.5 m, follow the rest shape loosely, and their tips most loosely. this.softness = Float64Array.from( { length: count }, (_, i) => 1 - 0.8 * clamp((mesh.positions[i * 3 + 2]! - 1.5) / 0.75, 0, 1), @@ -143,8 +145,8 @@ export class Body { this.neighbors[a[e]!]!.push({ row: b[e]!, length }); this.neighbors[b[e]!]!.push({ row: a[e]!, length }); } - // Area-weighted mass avoids making dense parts of the scan artificially heavy. Each edge's two - // opposite corners make a bend. + // Area-weighted mass avoids making dense parts of the scan artificially heavy. Each edge shared + // by two faces adds a bend between its two opposite corners. const mass = new Float64Array(count); const opposite = new Map(); for (let t = 0; t < mesh.faces.a.length; t++) { @@ -181,23 +183,24 @@ export class Body { this.burst = new Burst(mesh, ROOM, CEILING); } - /** A frame to {@link sample} into, reused every step: appending copies it. */ + /** Creates a frame for {@link sample} to fill. Reuse it every step, since appending copies it. */ frame(): Frame { const column = () => new Float32Array(this.count); return { x: column(), y: column(), z: column(), speed: column(), strain: column() }; } - /** Resting: still, with nothing acting on it. Nothing steps until something wakes it. */ + /** Whether the body is resting: still, with nothing acting on it. It takes no steps until + * woken. */ get asleep(): boolean { return this.still >= REST_AFTER; } - /** Anything that acts on the body wakes it, as do changed controls. */ + /** Wakes the body. Anything that acts on the body wakes it, and so should changed controls. */ wake(): void { this.still = 0; } - /** Advance by whole steps, none while resting; returns how many ran. */ + /** Advances by whole steps, at most 16, stopping once the body rests. Returns the steps taken. */ advance(seconds: number, controls: Controls): number { const most = Math.min(16, Math.floor(seconds / STEP)); let steps = 0; @@ -205,7 +208,7 @@ export class Body { return steps; } - /** Every vertex's position, speed, and strain now. */ + /** Writes every vertex's current position, speed, and strain into `frame`. */ sample(frame: Frame): void { const [c, r0, r1, r2] = this.goal; const [lateral, vertical] = this.shape(); @@ -222,7 +225,7 @@ export class Body { y - this.previous[i * 3 + 1]!, z - this.previous[i * 3 + 2]!, ) / STEP; - // Strain is the distance from the shape the body is pulled toward: inflated and squashed. + // Strain is the distance from the inflated, squashed rest shape the body is pulled toward. const rx = this.rest[i * 3]! * lateral; const ry = this.rest[i * 3 + 1]! * lateral; const rz = this.rest[i * 3 + 2]! * vertical; @@ -234,7 +237,8 @@ export class Body { } } - /** Center height, mean speed, and intact volume. Volume is missing while in pieces. */ + /** Returns the center height, the mean speed, and the volume as a fraction of the rest volume. + * The volume is NaN while the body is in pieces. */ metrics(): [number, number, number] { if (this.burst.active) { const chunks = this.burst.chunks; @@ -258,13 +262,14 @@ export class Body { ]; } - // Impulses change velocity, which Verlet keeps as the step from previous to now. + // Verlet keeps velocity as the step from the previous position to the current one, so an + // impulse moves the previous position. private push(row: number, v: Vec3): void { for (let a = 0; a < 3; a++) this.previous[row * 3 + a] = this.previous[row * 3 + a]! - v[a]! * STEP; } - /** A hop: an upward push, and a little forward, while on the floor; `gentle` hops lower. */ + /** Hops up and slightly forward if the body is on the floor. A `gentle` hop is lower. */ hop(gentle = false): void { if (this.contacts === 0 || this.grabbed || this.burst.active) return; const facing = this.goal[1]; @@ -273,12 +278,13 @@ export class Body { const vz = (this.x[i * 3 + 2]! - this.previous[i * 3 + 2]!) / STEP; this.push(i, [facing[0] * 0.45, facing[1] * 0.45, Math.max(0, speed - vz)]); } - // It has left the floor: presses before it lands do nothing. + // The body has left the floor, so presses before it lands do nothing. this.contacts = 0; this.wake(); } - /** Squash now: held, until {@link endSquash}; else for a moment. The material does the rebound. */ + /** Squashes the body until {@link endSquash} if `held`, else for 0.3 s. The material makes the + * rebound. */ squash(held = false): void { if (this.burst.active || this.grabbed) return; this.squashUntil = held ? Infinity : this.time + 0.3; @@ -289,7 +295,8 @@ export class Body { if (this.squashUntil === Infinity) this.squashUntil = this.time; } - /** Pop into capped pieces now, `gentle` ones flying less; pressed again, call them home. */ + /** Pops the body into capped pieces, which fly less if `gentle`. Called while the body is in + * pieces, it gathers them back. */ pop(gentle = false): void { if (this.burst.active) return this.burst.gather(); this.release(); @@ -300,7 +307,7 @@ export class Body { this.wake(); } - /** Dent the surface around a vertex. */ + /** Dents the surface around vertex `row`. */ poke(row: number, speed = 6): void { if (this.burst.active) return; const near = this.near(row, 0.7); @@ -316,7 +323,7 @@ export class Body { this.wake(); } - /** Lift it high, turned at random unless `upright`, and let go. */ + /** Lifts the body high, turned at random unless `upright`, and lets go. */ drop(upright = false): void { const yaw = upright ? 0.6 : Math.random() * Math.PI * 2; const tilt = upright ? 0 : (Math.random() - 0.5) * 0.9; @@ -341,7 +348,7 @@ export class Body { this.wake(); } - /** Hold a vertex and the surface around it toward `target` until released. */ + /** Pulls vertex `row` and the surface around it toward `target` until released. */ grab(row: number, target: readonly number[]): void { if (this.burst.active) return; this.squashUntil = -Infinity; @@ -405,7 +412,7 @@ export class Body { this.still = !acting && speed < STILL ? this.still + STEP : 0; } - /** Gravity, wind, and damping move every vertex on from where its last step took it. */ + /** Moves every vertex on by its damped velocity from the last step, plus gravity and wind. */ private integrate(controls: Controls): void { const { x, previous, count, normals } = this; const t = this.time; @@ -438,7 +445,8 @@ export class Body { } } - /** Three rounds of every distance constraint and the pressure, then the normals once. */ + /** Runs three iterations of every distance constraint and the pressure, then updates the + * normals. */ private solve(controls: Controls): void { const compliance = 1e-7 * 10 ** (3 * (1 - controls.firmness)); this.lambdas.fill(0); @@ -450,7 +458,7 @@ export class Body { this.normalize(); } - /** Every spring and bend, each pulled toward its rest length, inflated. */ + /** Pulls every spring and bend toward its rest length, scaled by the inflation. */ private distances(compliance: number): void { const { x, ends, lengths, compliances, lambdas, inverseMass, scale } = this; const alpha = compliance / (STEP * STEP); @@ -480,14 +488,14 @@ export class Body { } } - /** The enclosed volume and its gradient per vertex, with the normals they imply. */ + /** Computes the enclosed volume, its gradient per vertex, and the normals from that gradient. */ private measure(): number { const volume = this.enclose(); this.normalize(); return volume; } - /** The enclosed volume, and its gradient per vertex. */ + /** Computes the enclosed volume and its gradient per vertex. */ private enclose(): number { const { x, gradient } = this; const { a: fa, b: fb, c: fc } = this.mesh.faces; @@ -518,7 +526,7 @@ export class Body { return (this.volume = volume / 6); } - /** The unit outward normals the last volume gradient implies. */ + /** Sets the unit outward normals from the last volume gradient. */ private normalize(): void { const { gradient, normals } = this; for (let i = 0; i < gradient.length; i += 3) { @@ -529,7 +537,7 @@ export class Body { } } - /** Pull the enclosed volume toward `inflate` of the rest volume. */ + /** Pulls the enclosed volume toward `inflate` times the rest volume. */ private pressure(compliance: number, inflate: number): void { const volume = this.enclose(); const { x, gradient, inverseMass, count } = this; @@ -550,13 +558,14 @@ export class Body { } } - /** The rest shape's sideways and upward scale: inflated, and squashed keeping its volume. */ + /** The rest shape's sideways and vertical scale, with the inflation and a squash that keeps the + * volume. */ private shape(): [number, number] { const squashed = 1 - SQUASH * this.squashing; return [this.scale / Math.sqrt(squashed), this.scale * squashed]; } - /** Pull toward the rest shape, turned and moved to fit where the body is now. */ + /** Pulls the body toward its rest shape, rotated and moved to best fit the current positions. */ private match(stiffness: number): void { const { x, rest, count, softness } = this; const c = this.center(x); @@ -578,7 +587,7 @@ export class Body { column[2] += pz * r; } } - // Müller et al. 2016: the rotation nearest A, warm started from the last step. + // Find the rotation nearest A (Müller et al. 2016), warm started from the last step. let q = this.quaternion; for (let iteration = 0; iteration < 8; iteration++) { const R = rotation(q); @@ -605,9 +614,9 @@ export class Body { const n = Math.hypot(...q); q = q.map((v) => v / n); } - // Matching reads part of gravity's sag as a turn, and on its own would roll the bunny over: - // standing short of its tipping point, the turn keeps upright at the same heading. Toppled - // past it, the bunny lies where it fell. + // Shape matching reads part of gravity's sag as a rotation, which alone would roll the bunny + // over. Short of the tipping point, the rotation eases back upright at the same heading. Past + // it, the bunny lies where it fell. if (stiffness > 0 && this.contacts > 8 && !this.grabbed && rotation(q)[2][2] > TIPPING) { const heading = rotation(q)[0]; const yaw = Math.atan2(heading[1], heading[0]); @@ -652,20 +661,22 @@ export class Body { } } - /** The floor with its friction, the ceiling, then the walls; returns the mean speed this step. */ + /** Collides with the floor (with friction), the ceiling, then the walls. Returns the mean speed + * this step. */ private collide(): number { const { x, previous } = this; let contacts = 0; let travel = 0; for (let i = 0; i < x.length; i += 3) { - // The floor first: friction needs where the vertex was, before a wall's bounce rewrites it. + // The floor comes first, since friction needs the vertex's previous position before a wall + // bounce rewrites it. if (x[i + 2]! < 0) { const depth = -x[i + 2]!; const vz = x[i + 2]! - previous[i + 2]!; x[i + 2] = 0; previous[i + 2] = vz < -0.5 * STEP ? vz * 0.18 : 0; - // Friction acts on what this step moved it along the floor, so no push from the springs - // or the rest shape can ratchet the body across it. + // Friction acts on this step's movement along the floor, so pushes from the springs or + // the rest shape cannot ratchet the body across it. const tx = x[i]! - previous[i]!; const ty = x[i + 1]! - previous[i + 1]!; const slide = Math.sqrt(tx * tx + ty * ty); @@ -701,7 +712,8 @@ export class Body { } private near(row: number, radius: number) { - // Walk the rest surface: adjacent folded ears must not share a grab neighborhood. + // Search along the rest surface's edges, not through space, so ears folded against each other + // do not share a grab neighborhood. const distance = new Float64Array(this.count).fill(Infinity); distance[row] = 0; const queue = [row]; diff --git a/npm/examples/src/examples/soft-body/burst.ts b/npm/examples/src/examples/soft-body/burst.ts index dbafc6f3..df5ea674 100644 --- a/npm/examples/src/examples/soft-body/burst.ts +++ b/npm/examples/src/examples/soft-body/burst.ts @@ -18,7 +18,8 @@ interface Chunk { const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v)); -/** Independent capped pieces. No original triangle or spring connects them during a pop. */ +/** The body popped into independent capped pieces, which no original triangle or spring + * connects. */ export class Burst { readonly chunks: Chunk[]; readonly count: number; @@ -94,7 +95,7 @@ export class Burst { this.targetBody[i + 1] = cy + rx * sin + ry * cos; this.targetBody[i + 2] = this.mesh.positions[i + 2]! * scale + 0.025; } - // Stable variation, sampled only at launch. Replay records the resulting positions. + // Seeded random variation, sampled only at launch. Replay records the resulting positions. let seed = 0x12b9; const random = () => { seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0; @@ -157,7 +158,7 @@ export class Burst { } gather(): void { - // Queue early requests until the pieces have visibly separated, including key repeats. + // Defer early requests, including key repeats, until the pieces have visibly separated. if (this.active) this.returnAt = Math.min(this.returnAt, Math.max(this.age, this.reduced ? 0.5 : 0.9)); } @@ -170,7 +171,7 @@ export class Burst { this.active = false; } - /** Returns true on the one step that finishes assembly. */ + /** Advances one step. Returns true on the step that finishes reassembly. */ step(dt: number, gravity: number): boolean { if (!this.active) return false; this.previous.set(this.x); @@ -213,7 +214,8 @@ export class Burst { } this.rotate(chunk, dt); } - // Soft sphere proxies separate tumbling pieces; stop before seams close on return. + // Soft sphere proxies push tumbling pieces apart. They stop once the return starts, so the + // seams can close. if (this.age < this.returnAt) this.separate(); this.writeVertices(dt); if (settled && this.age > this.returnAt + 1.1) { diff --git a/npm/examples/src/examples/soft-body/fracture.ts b/npm/examples/src/examples/soft-body/fracture.ts index 36d78bbd..a0002f67 100644 --- a/npm/examples/src/examples/soft-body/fracture.ts +++ b/npm/examples/src/examples/soft-body/fracture.ts @@ -15,7 +15,7 @@ export interface Piece { const EPS = 1e-7; -/** Split the closed mesh into independently capped solids, once at page load. */ +/** Splits the closed mesh into `count` separately capped solids. Runs once, at page load. */ export function fracture(mesh: Mesh, count = 24): Piece[] { const first: Piece = { vertices: Array.from({ length: mesh.count }, (_, i) => ({ @@ -26,7 +26,7 @@ export function fracture(mesh: Mesh, count = 24): Piece[] { }; const pieces = [first]; while (pieces.length < count) { - // Cut the largest bounding volume, with stable ordering for repeatable pieces. + // Cut the piece with the largest bounding box. Stable ordering makes the pieces repeatable. const bounds = pieces.map((piece) => { const lo = [Infinity, Infinity, Infinity]; const hi = [-Infinity, -Infinity, -Infinity]; @@ -104,7 +104,8 @@ function split(piece: Piece, axis: number, at: number): [Piece, Piece] { if (cut.size === 2) segments.push([...cut] as [number, number]); } - // Each cut boundary is a loop; triangulate concave loops instead of a center fan. + // Each cut boundary is a loop. Cap it by ear clipping, which handles concave loops that a + // center fan would not. const neighbors = new Map(); for (const [a, b] of segments) { neighbors.set(a, [...(neighbors.get(a) ?? []), b]); @@ -136,7 +137,8 @@ function split(piece: Piece, axis: number, at: number): [Piece, Piece] { return sides.map(compact) as [Piece, Piece]; } -/** Counterclockwise triangles viewed along the positive cut axis. */ +/** Caps a cut loop by ear clipping. The triangles are counterclockwise viewed along the positive + * cut axis. */ function cap(loop: number[], vertices: CutVertex[], axis: number): [number, number, number][] { const u = (axis + 1) % 3; const v = (axis + 2) % 3; diff --git a/npm/examples/src/examples/soft-body/index.ts b/npm/examples/src/examples/soft-body/index.ts index 44f0bda4..e9b8b5af 100644 --- a/npm/examples/src/examples/soft-body/index.ts +++ b/npm/examples/src/examples/soft-body/index.ts @@ -1,8 +1,8 @@ /** - * A soft bunny of springs in a room: hop, squash, poke, fling, or pop it, and replay or scrub what - * it did. Its faces darken turning from the light over soft footprints on the floor; its vertices, - * their dots hidden until the mesh shows, are what the pointer drags; and a plot traces its height, - * speed, and volume. + * A soft bunny made of springs, in a room. Hop, squash, poke, fling, or pop it, then replay or + * scrub its history. Faces darken as they turn away from the light, above soft contact shadows on + * the floor. The pointer drags the vertices, whose dots stay hidden until the mesh is shown. A plot + * traces the bunny's height, speed, and volume. */ import { Data, @@ -19,13 +19,14 @@ import { theme } from '../../ui/theme.js'; import { Body, ROOM, STEP, type Controls } from './body.js'; import { loadMesh } from './mesh.js'; -/** Seconds of frames kept to replay and scrub. */ +/** Seconds of frames kept for replay and scrubbing. */ const HISTORY = 20; -/** Seconds of simulation between recorded frames, whatever the display's refresh rate. */ +/** Simulated seconds between recorded frames, regardless of the display's refresh rate. */ const RECORD = 1 / 60; /** Seconds the plot shows. */ const WINDOW = 12; -/** What the bunny can be painted by. Each domain starts below rest, so a bunny at rest draws bright. */ +/** The bunny's color schemes. Each domain starts below the value at rest, so a resting bunny stays + * clear of the colormap's darkest end. */ const PAINTS: Record< string, { label: string; field: string; colormap: ColormapName; domain: [number, number]; legend: string } @@ -69,8 +70,8 @@ const THEME = { shading: 0.7, } as const; -// A soft footprint on the floor under the bunny and each of its pieces, darker the nearer they sit: -// a marker of our own, in WGSL, its darkness read from each row's `opacity`. +// A soft contact shadow on the floor under the bunny and under each of its pieces, darker the +// lower they are. It is a custom marker in WGSL that reads its darkness from each row's `opacity`. const CONTACT_SHADOW = `fn marker(f: MarkerFragment) -> MarkerColor { let p = f.p / max(f.half, vec2f(0.001)); let d = length(p); @@ -82,7 +83,7 @@ export default async function bunny(stage: Stage): Promise { const mesh = await loadMesh(new URL('./stanford-bunny.bin', import.meta.url)); if (stage.signal.aborted) return; const body = new Body(mesh); - // Reduced motion hops lower, pops gentler, and never orbits. + // With reduced motion, the bunny hops lower and pops more gently, and the camera never orbits. const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)'); reducedMotion.addEventListener( 'change', @@ -92,7 +93,8 @@ export default async function bunny(stage: Stage): Promise { { signal: stage.signal }, ); const controls: Controls = { gravity: 9.81, firmness: 0.45, inflate: 1, wind: false }; - /** Sets a control, waking the body: a resting body answers new gravity or a new material. */ + /** Wraps a control's setter to also wake the body, so a resting body responds to new gravity or + * a new material. */ const waking = (set: (value: T) => void) => (value: T) => { @@ -101,7 +103,8 @@ export default async function bunny(stage: Stage): Promise { }; let paint = 'solid'; let inspect = false; - /** How faces turn from the light, the floor's grid, and the mesh's dots. */ + /** Light settings: how much faces turned from the camera darken, the floor's grid, and the + * radius of the mesh's dots in pixels. */ const lit: { shading: number; grid: 'lines' | 'dots' | 'off'; dots: number } = { shading: THEME.shading, grid: 'lines', @@ -117,7 +120,7 @@ export default async function bunny(stage: Stage): Promise { let carry = 0; let recordAt = 0; - // The mesh once; then every step, a frame of every vertex and of the body as a whole. + // The mesh is set once. Each recorded frame then appends every vertex and the body as a whole. const data = new Data('bunny').apply( new Patch() .axis('time', 's') @@ -163,12 +166,13 @@ export default async function bunny(stage: Stage): Promise { data.apply(new Patch().set('Shadow', { count: body.burst.chunks.length + 1 }, {})); - // Faces lit from the camera over soft footprints; the vertices stay pickable, their dots hidden. + // Faces lit from the camera, above soft contact shadows. The vertices stay pickable while their + // dots are hidden. const room = (live: boolean): Scene => { const p = PAINTS[paint]!; return { space: { plane: { grid: GRIDS[lit.grid] } }, - // The mesh's dots and springs draw the theme's sizes. + // The mesh's dots and springs take their sizes from the theme. theme: { ...THEME, shading: lit.shading, @@ -396,7 +400,7 @@ export default async function bunny(stage: Stage): Promise { plot.camera = { x: [x0, x0 + WINDOW], y: null }; }, change(live) { - // Paused, a press pans instead of grabbing. + // Outside live mode, a press pans instead of grabbing. space.scene = room(live); if (!live) { body.release(true); @@ -461,8 +465,8 @@ export default async function bunny(stage: Stage): Promise { space.on('click', ({ hit }) => act(() => body.poke(hit.row.id as number))()); const canvas = stage.regions.main.querySelector('canvas')!; - // The JS drag event intentionally combines release and cancellation. Capture cancellation - // before the view's bubbling listener, so it cannot leave a throw behind. + // The view's drag event reports release and cancellation alike. Catch cancellation before the + // view's bubbling listener, so a cancelled drag does not fling the bunny. canvas.addEventListener('pointercancel', () => body.release(true), { capture: true, signal: stage.signal, @@ -488,7 +492,8 @@ export default async function bunny(stage: Stage): Promise { }); stage.frame((dt) => { if (playback.mode !== 'live') return; - // A resting bunny steps and records nothing, so both views sleep until something acts on it. + // A resting bunny takes no steps and records nothing, so both views sleep until something + // acts on it. if (body.asleep) { carry = 0; return; @@ -497,7 +502,7 @@ export default async function bunny(stage: Stage): Promise { carry = Math.min(carry + dt * timeScale, 16 * STEP); const steps = body.advance(carry, controls); carry -= steps * STEP; - // Recorded at a fixed rate of simulated time, and once more as it comes to rest. + // Record at a fixed rate of simulated time, and once more when the body comes to rest. if (steps && (body.time + STEP / 2 >= recordAt || body.asleep)) { publish(); recordAt = body.time + RECORD; diff --git a/npm/examples/src/examples/soft-body/mesh.ts b/npm/examples/src/examples/soft-body/mesh.ts index 3397be4d..37179713 100644 --- a/npm/examples/src/examples/soft-body/mesh.ts +++ b/npm/examples/src/examples/soft-body/mesh.ts @@ -1,16 +1,18 @@ /** - * The Stanford bunny, from the Stanford 3D Scanning Repository (graphics.stanford.edu/data/3Dscanrep), - * as the public-domain `bunny` package ships it: a closed low-resolution reconstruction. The file - * holds its vertex and triangle counts as two uint32s, then float32 x, y, z per vertex with y up, then - * uint16 triangles wound outward. + * The Stanford bunny, from the Stanford 3D Scanning Repository + * (graphics.stanford.edu/data/3Dscanrep), as shipped by the public-domain `bunny` package: a + * closed, low-resolution reconstruction. + * + * The file holds the vertex and triangle counts as two uint32s, then float32 x, y, z per vertex + * with y up, then uint16 triangles wound outward. */ export interface Mesh { readonly count: number; /** x, y, z per vertex in meters, z up, resting on z = 0 and centered over the origin. */ readonly positions: Float64Array; - /** Each outward triangle's three corners, a column each. */ + /** The three corners of each outward-wound triangle, one column per corner. */ readonly faces: { readonly a: Uint32Array; readonly b: Uint32Array; readonly c: Uint32Array }; - /** Each triangle side once: the springs, and the wireframe. */ + /** Every triangle edge, listed once. They are the springs and the wireframe. */ readonly springs: { readonly a: Uint32Array; readonly b: Uint32Array }; } @@ -21,7 +23,7 @@ export async function loadMesh(url: URL, height = 2.4): Promise { const [count, faces] = new Uint32Array(bytes, 0, 2) as unknown as [number, number]; const scan = new Float32Array(bytes, 8, count * 3); const triangles = new Uint16Array(bytes, 8 + count * 12, faces * 3); - // The scan is y up; turn it z up, which keeps its triangles wound outward. + // The scan is y up. Rotate it to z up, which keeps its triangles wound outward. const positions = new Float64Array(count * 3); const lo = [Infinity, Infinity, Infinity]; const hi = [-Infinity, -Infinity, -Infinity]; diff --git a/npm/examples/src/random.ts b/npm/examples/src/random.ts index 452785bf..d25b2d27 100644 --- a/npm/examples/src/random.ts +++ b/npm/examples/src/random.ts @@ -1,4 +1,5 @@ -/** Deterministic sample data, shared by the synthetic examples. */ +/** Returns a seeded random generator of numbers in [0, 1). The synthetic examples use it so their + * sample data is the same on every load. */ export function random(seed: number): () => number { return () => { seed = (seed + 0x6d2b79f5) | 0; diff --git a/npm/examples/src/ui/controls.ts b/npm/examples/src/ui/controls.ts index e9a18f1b..feb1ffcd 100644 --- a/npm/examples/src/ui/controls.ts +++ b/npm/examples/src/ui/controls.ts @@ -2,14 +2,14 @@ import type { Scene, View } from 'latkit'; import { h } from './dom.js'; import type { Stage } from './stage.js'; -/** A button that runs `run`, and the key that runs it too, if any. */ +/** A button that calls `run`, with an optional `key` that also calls it. */ export interface Action { label: string; key?: string; run: () => void; } -/** A key as its button shows it, if any. */ +/** The key label shown on a button, if there is a key. */ const kbd = (key: string | undefined) => key ? [h('kbd', {}, key === ' ' ? 'Space' : key.toUpperCase())] : []; @@ -17,7 +17,7 @@ const kbd = (key: string | undefined) => export class Section { readonly element: HTMLElement; readonly content = h('div', { class: 'section-body' }); - /** The row toggles share, made on first use. */ + /** The row that holds the toggles, created on first use. */ #toggles?: HTMLElement; constructor( @@ -28,7 +28,7 @@ export class Section { this.element = h('details', { class: 'section', open }, h('summary', {}, title), this.content); } - /** Buttons in a grid, each also run by its key. */ + /** Adds a grid of buttons. Each button's key, if any, also runs it. */ actions(actions: readonly Action[]): void { const row = h('div', { class: 'actions' }); for (const { label, key, run } of actions) { @@ -40,8 +40,8 @@ export class Section { this.content.append(row); } - /** A button that stays pressed, also flipped by its key, if any. Returns a setter for outside - * changes. */ + /** Adds a toggle button, which its key, if any, also flips. Returns a setter for changes made + * elsewhere. */ toggle( label: string, key: string | undefined, @@ -62,8 +62,8 @@ export class Section { return set; } - /** A slider with its value shown through `format`, calling `change` as it moves; returns the - * input, for outside changes. */ + /** Adds a slider that shows its value through `format` and calls `change` as it moves. Returns + * the input so the page can change it. */ range( label: string, o: { min: number; max: number; step: number; value: number; format: (value: number) => string }, @@ -89,7 +89,7 @@ export class Section { return input; } - /** One of several options; returns the select, for outside changes. */ + /** Adds a dropdown of labeled `options`. Returns the select so the page can change it. */ choice( label: string, options: readonly (readonly [string, T])[], @@ -108,7 +108,7 @@ export class Section { return select; } - /** Swatches naming the series a view draws. */ + /** Adds color swatches that label the series a view draws. */ series(items: readonly { label: string; color: string }[]): HTMLElement { const swatches = items.map(({ label, color }) => h('span', { style: `--swatch: ${color}` }, label), @@ -118,19 +118,19 @@ export class Section { return line; } - /** A line of text the page keeps up to date. */ + /** Adds a line of text for the page to keep up to date. */ note(text = ''): HTMLParagraphElement { const line = h('p', { class: 'note' }, text); this.content.append(line); return line; } - /** A view inside this section. */ + /** Creates a view inside this section. */ view(scene: Scene, options: { label: string; height?: number }): View { return this.stage.view(scene, { ...options, into: this.content }); } - /** Standard playback speed control. */ + /** Adds the standard playback speed control. */ speed(rates: readonly number[], value: number, change: (value: number) => void): void { this.choice( 'Speed', @@ -140,7 +140,7 @@ export class Section { ); } - /** A labeled text field, such as a selected item's name. */ + /** Adds a labeled text field, such as for a selected item's name. */ text(label: string, change: (value: string) => void): HTMLInputElement { const input = h('input', { type: 'text' }); input.addEventListener('change', () => change(input.value), { signal: this.stage.signal }); diff --git a/npm/examples/src/ui/dom.ts b/npm/examples/src/ui/dom.ts index c80f7a76..6f233d41 100644 --- a/npm/examples/src/ui/dom.ts +++ b/npm/examples/src/ui/dom.ts @@ -1,7 +1,8 @@ type Child = Node | string | null | undefined | false; type Attributes = Record; -/** An element with attributes and children; `false` and `undefined` attributes are left off. */ +/** Creates an element with `attributes` and `children`. Attributes that are `false` or `undefined` + * are omitted, `true` sets an empty attribute, and falsy children are skipped. */ export function h( tag: K, attributes: Attributes = {}, diff --git a/npm/examples/src/ui/export.ts b/npm/examples/src/ui/export.ts index 3870aa2e..d071b872 100644 --- a/npm/examples/src/ui/export.ts +++ b/npm/examples/src/ui/export.ts @@ -2,11 +2,11 @@ import type { ImageFormat } from 'latkit'; import { h } from './dom.js'; import type { Stage } from './stage.js'; -/** Seconds of video a recording takes. */ +/** Length of a recorded video, in seconds. */ const SECONDS = 6; -/** Frames a second of video. */ +/** Video frame rate, in frames per second. */ const FPS = 30; -/** Each picture format: its file's extension and type, and whether it keeps alpha. */ +/** Each picture format's file extension, MIME type, and whether it keeps alpha. */ const FORMATS: Record = { png: { extension: 'png', type: 'image/png', alpha: true }, jpeg: { extension: 'jpg', type: 'image/jpeg', alpha: false }, @@ -14,27 +14,29 @@ const FORMATS: Record Math.min(2, devicePixelRatio); const picture = async () => { status.textContent = 'Taking a picture…'; const { extension, type, alpha } = FORMATS[format]; - // The page runs on as it is taken: a picture of the moment, not of where motion settles. + // `capture: 'now'` pictures this moment while the page keeps running, instead of waiting for + // motion to settle. const shot = canvas.shot({ scale: scale(), transparent: transparent && alpha, capture: 'now' }); const image = await shot.image(); try { @@ -63,7 +65,8 @@ export function exportSection(stage: Stage): void { signal: recording.signal, ...(bitrate ? { bitrate } : {}), }); - // Each frame waits for the page to reach its time, so the video runs as the page does. + // Each frame waits until the page reaches that frame's time, so the video plays at the + // page's real speed. await recorder.record( async (time) => { while (performance.now() - started < time * 1000) await nextFrame(); @@ -110,7 +113,7 @@ export function exportSection(stage: Stage): void { format, (next) => { format = next; - // PNG and WebP are lossless; JPEG trades detail for size, and has no alpha. + // PNG and WebP are lossless. JPEG trades detail for size and has no alpha. quality_.disabled = next !== 'jpeg'; if (!FORMATS[next].alpha && transparent) clear(false); }, diff --git a/npm/examples/src/ui/legend.ts b/npm/examples/src/ui/legend.ts index 9ce93b17..5e694ebc 100644 --- a/npm/examples/src/ui/legend.ts +++ b/npm/examples/src/ui/legend.ts @@ -2,7 +2,8 @@ import { colormapCss, type Colormap } from 'latkit'; import { h } from './dom.js'; import type { Stage } from './stage.js'; -/** A colormap ramp with its label, over the main view: at its foot, or its top over a plot's axes. */ +/** A colormap ramp and label over the main view. It sits at the bottom, or at the top (`'top'`) so + * it does not cover a plot's axes. */ export class Legend { readonly #ramp = h('span', { class: 'ramp', 'aria-hidden': 'true' }); readonly #label = h('span'); @@ -18,7 +19,7 @@ export class Legend { this.#label.textContent = label; } - /** Hides the legend, as while nothing it describes shows. */ + /** Hides the legend, for example while nothing it describes is shown. */ set hidden(hidden: boolean) { this.#element.hidden = hidden; } diff --git a/npm/examples/src/ui/menu.ts b/npm/examples/src/ui/menu.ts index 355e3abc..feedce3e 100644 --- a/npm/examples/src/ui/menu.ts +++ b/npm/examples/src/ui/menu.ts @@ -2,15 +2,15 @@ import type { View } from 'latkit'; import { h } from './dom.js'; import type { Stage } from './stage.js'; -/** An item of a menu: what it says, and what it does; a disabled one shows but does nothing. */ +/** A menu item. A disabled item is shown but cannot be chosen. */ export interface Item { label: string; run: () => void; disabled?: boolean; } -/** A menu over the main view where a view asked for one; gone once an item runs, on Escape, or on a - * press elsewhere. */ +/** A context menu shown over the main view where a view requested one. It closes when an item runs, + * on Escape, or on a press outside it. */ export class Menu { readonly #element = h('div', { class: 'menu', role: 'menu', hidden: true }); @@ -27,7 +27,8 @@ export class Menu { addEventListener('keydown', (event) => event.key === 'Escape' && this.close(), { signal }); } - /** Opens `items` at `point`, CSS pixels over `view`, kept inside the main view. */ + /** Opens `items` at `point`, in CSS pixels from the top left of `view`, kept at least 8 px inside + * the main view. */ open(view: View, point: [number, number], items: readonly Item[]): void { const canvas = this.stage.canvases.find((c) => c.layout.some((p) => p.view === view)); const panel = canvas?.layout.find((p) => p.view === view)?.at ?? [0, 0, 1, 1]; diff --git a/npm/examples/src/ui/meter.ts b/npm/examples/src/ui/meter.ts index 468e25f1..cea71df2 100644 --- a/npm/examples/src/ui/meter.ts +++ b/npm/examples/src/ui/meter.ts @@ -5,9 +5,10 @@ import type { Stage } from './stage.js'; const MB = 1 << 20; /** - * A readout over a view: frames per second, the view's frame time on the CPU and the GPU, draws, - * instances, and tiles, what it waits on, the GPU's memory, and the page's own measures; and how - * much memory and upload the GPU may take, which the page gives back when it closes. + * A performance readout for a view, in the side panel's Performance section. It shows frames per + * second, the view's CPU and GPU frame times, draws, instances, tiles, what the view is waiting on, + * GPU memory, marks that failed, and the page's own measurements. It also sets the GPU's memory and + * upload budgets, which are restored when the page closes. */ export class Meter { readonly #line = h('p', { class: 'meter', 'aria-live': 'off' }); @@ -15,7 +16,7 @@ export class Meter { readonly #notes: (() => string)[] = []; #frames = 0; #since = performance.now(); - /** What the view waits on, as it last told. */ + /** What the view is waiting on, from its latest `waiting` event. */ #waiting: string[] = []; constructor(stage: Stage, view: View) { @@ -85,7 +86,7 @@ export class Meter { return result; } - /** A readout computed at each refresh. */ + /** Adds a line that `read` computes at each refresh, about twice a second. */ note(read: () => string): void { this.#notes.push(read); } diff --git a/npm/examples/src/ui/playback.ts b/npm/examples/src/ui/playback.ts index 5f6d1deb..8e93c80e 100644 --- a/npm/examples/src/ui/playback.ts +++ b/npm/examples/src/ui/playback.ts @@ -4,19 +4,21 @@ import type { Stage } from './stage.js'; export interface PlaybackOptions { /** Shows coordinate `at` in every view. */ show(at: number): void; - /** Whether the data records as it plays: live follows its end. Otherwise playing loops. */ + /** Whether the data is recorded while it plays (default true). Live playback follows the end of + * the data. Otherwise playing loops. */ live?: boolean; - /** Seconds of data a second of playing shows. */ + /** Seconds of data shown per second of playback (default 1). */ rate?: number; - /** Instant replay: how many seconds back from the end, and at what rate. */ + /** Instant replay: how many seconds back from the end to start, and the playback rate. */ replay?: { seconds: number; rate: number }; - /** Called as playback goes live or leaves it. */ + /** Called when playback goes live or stops being live. */ change?(live: boolean): void; - /** Coordinate `at` as the transport writes it; seconds unless given. */ + /** Formats coordinate `at` for the transport. Defaults to seconds. */ format?(at: number): string; } -/** Live, paused, or playing a recorded span, with the transport that drives it. */ +/** Playback state (live, paused, or playing a recorded span) and the transport controls that drive + * it. */ export class Playback { mode: 'live' | 'playing' | 'paused'; start = 0; @@ -80,32 +82,33 @@ export class Playback { return this.#at; } - /** Hides the transport, as while there is nothing to play. */ + /** Hides the transport, for example while there is nothing to play. */ set hidden(hidden: boolean) { this.#transport.hidden = hidden; this.#slider.hidden = hidden; } - /** Seconds of data a second of playing shows, from now on. */ + /** Sets the seconds of data shown per second of playback, from now on. */ set rate(rate: number) { this.options.rate = rate; if (this.mode === 'playing') this.#rate = rate; } - /** Whether the data records as it plays, from now on: live follows its end; otherwise it pauses, - * and playing loops. */ + /** Sets whether the data is recorded while it plays. Going live follows the end of the data. + * Otherwise playback pauses, and playing then loops. */ set live(live: boolean) { this.#live = live; this.#set(live ? 'live' : 'paused'); } - /** Shows coordinate `at`, paused. */ + /** Pauses and shows coordinate `at`. */ seek(at: number): void { this.#set('paused'); this.#show(at); } - /** The recorded span changed; live playback shows its end. */ + /** Updates the recorded span. Live playback jumps to its end, and a position before its start + * moves to the start. */ record([start, end]: readonly [number, number]): void { this.start = start; this.end = end; @@ -115,7 +118,7 @@ export class Playback { else if (this.#at < start) this.#show(start); } - /** Goes live, or for recorded data plays on from where it is. */ + /** Goes live, or, for recorded data, plays on from the current position. */ play(): void { this.#rate = this.options.rate ?? 1; this.#set(this.#live ? 'live' : 'playing'); @@ -126,7 +129,7 @@ export class Playback { this.#set('paused'); } - /** Plays the last seconds again, slowly, then goes live. */ + /** Plays the last `replay.seconds` again at `replay.rate`, then goes live if the data is live. */ replay(): void { const replay = this.options.replay; if (!replay) return; diff --git a/npm/examples/src/ui/rows.ts b/npm/examples/src/ui/rows.ts index 5c550b8a..31aff7f6 100644 --- a/npm/examples/src/ui/rows.ts +++ b/npm/examples/src/ui/rows.ts @@ -1,9 +1,9 @@ -/** Sets of rows, as a view selects and tells them, and the rows in them. */ +/** Conversions between single rows and the row sets that views select and report. */ import type { Row, Rows } from 'latkit'; -/** Each row of `sets`, in order. */ +/** Flattens `sets` into single rows, in order. */ export const each = (sets: readonly Rows[]): Row[] => sets.flatMap(({ data, table, ids }) => ids.map((id) => ({ data, table, id }))); -/** `row` as a set of rows. */ +/** Wraps `row` in a set of one row. */ export const one = ({ data, table, id }: Row): Rows => ({ data, table, ids: [id] }); diff --git a/npm/examples/src/ui/shell.ts b/npm/examples/src/ui/shell.ts index b6a59a4f..4d350c15 100644 --- a/npm/examples/src/ui/shell.ts +++ b/npm/examples/src/ui/shell.ts @@ -5,7 +5,7 @@ import { Stage } from './stage.js'; declare global { interface Window { - /** Active example, exposed for the browser gate. */ + /** The active example's stage, exposed for the browser gate. */ stage?: Stage; } } @@ -16,7 +16,8 @@ export interface Page { load(): Promise<{ default: (stage: Stage) => Promise | void }>; } -/** Navigation, controls, and a full-window canvas shared by every example. */ +/** The page layout every example shares: a header with the example picker, the main view, the + * controls panel, and the timeline. It shows the example named in the URL hash. */ export class Shell { readonly #picker = h('select', { 'aria-label': 'Example' }); readonly #detail = h('p', { class: 'detail' }); diff --git a/npm/examples/src/ui/stage.ts b/npm/examples/src/ui/stage.ts index 68324328..d3b9b730 100644 --- a/npm/examples/src/ui/stage.ts +++ b/npm/examples/src/ui/stage.ts @@ -6,17 +6,18 @@ import { Menu } from './menu.js'; import { Meter } from './meter.js'; import { Playback, type PlaybackOptions } from './playback.js'; -/** Where a page draws: the main view, the side panel, and the timeline. */ +/** The page regions an example fills: the main view, the side panel, and the timeline. */ export interface Regions { main: HTMLElement; side: HTMLElement; timeline: HTMLElement; } -/** What a page draws with. Everything made through it goes when the page closes. */ +/** Creates an example page's views, canvases, and controls. Everything made through it is + * destroyed when the page closes. */ export class Stage { readonly #abort = new AbortController(); - /** Aborted when the page closes: pass it to listeners the page adds itself. */ + /** Aborted when the page closes. Pass it to any listener the page adds itself. */ readonly signal = this.#abort.signal; readonly #views = new Set(); readonly #canvases: Canvas[] = []; @@ -48,20 +49,21 @@ export class Stage { return [...this.#views]; } - /** Every canvas the page shows its views in, in the order they were made. */ + /** The page's canvases, in creation order. */ get canvases(): readonly Canvas[] { return this.#canvases; } - /** A view in a canvas of its own, in `into`, the main region unless given. */ + /** Creates a view in its own canvas, appended to `into` (the main region by default). */ view(scene: Scene, options: { label: string; into?: HTMLElement; height?: number }): View { return this.panels([scene], options)[0]!; } /** - * Views sharing one canvas in `into`, the main region unless given: each `at` its x, y, width, and - * height as fractions of the canvas, side by side unless given, and on a narrow screen one above - * the other. Input goes to the view under it. + * Creates views that share one canvas, appended to `into` (the main region by default). Each + * view fills its `at` rectangle: x, y, width, and height as fractions of the canvas. By default + * the views sit side by side. On screens 700 px wide or less, each rectangle's axes swap, so the + * views stack. Input goes to the view under the pointer. */ panels( scenes: readonly Scene[], @@ -90,7 +92,8 @@ export class Stage { if (views.length > 1) narrow.addEventListener('change', () => (canvas.layout = layout()), { signal: this.signal }); views.forEach((view) => { - // What could not be fetched only goes undrawn; anything else stops the page. + // A failed fetch (`io`) only leaves that data undrawn and is reported in the header. Any + // other error stops the page. view.on('error', (error) => error.code === 'io' ? this.detail(error.message) : this.fail(error), ); @@ -99,7 +102,8 @@ export class Stage { return views; } - /** Calls `step` every frame before the views draw, with the seconds since the last, at most 0.1. */ + /** Calls `step` every frame before the views draw, with the seconds since the previous frame, + * capped at 0.1. */ frame(step: (dt: number) => void): void { let last: number | undefined; const stop = this.gpu.on('frame', (now) => { @@ -114,46 +118,48 @@ export class Stage { this.signal.addEventListener('abort', stop); } - /** Runs `action` when `key` is pressed outside a text field. */ + /** Runs `action` when `key` is pressed without modifiers, unless focus is in a form control. */ key(key: string, action: () => void): void { this.#keys.set(key.toLowerCase(), action); } - /** A titled group of controls in the side panel. */ + /** Adds a titled group of controls to the side panel. */ section(title: string, open = true): Section { const section = new Section(this, title, open); this.regions.side.append(section.element); return section; } - /** The transport: live, paused, or playing a recorded span. */ + /** Adds the playback controls: live, paused, or playing a recorded span. */ playback(options: PlaybackOptions): Playback { return new Playback(this, options); } - /** A readout over `view`: frames per second, its frame time, and the page's own measures. */ + /** Adds a performance readout for `view`, with frame rate, frame times, and the page's own + * measurements. */ meter(view: View): Meter { return new Meter(this, view); } - /** A menu over the main view, opened where a view asks for one. */ + /** Creates a context menu over the main view, to open where a view requests one. */ menu(): Menu { return new Menu(this); } - /** A colormap ramp over the main view: at its foot, or its top over a plot's axes. */ + /** Adds a colormap legend over the main view, at the bottom, or at the top (`'top'`) so it does + * not cover a plot's axes. */ legend(place: 'foot' | 'top' = 'foot'): Legend { return new Legend(this, place); } - /** A line under the main view crediting where its data comes from. */ + /** Adds a line under the main view that credits the data source. */ credit(): HTMLElement { const line = h('p', { class: 'credit' }); this.regions.main.append(line); return line; } - /** Compact dataset information in the shared header. */ + /** Shows short dataset information in the shared header. */ detail(text: string): void { this.line.textContent = text; } diff --git a/npm/examples/src/ui/theme.ts b/npm/examples/src/ui/theme.ts index 536b5fc0..df5497c2 100644 --- a/npm/examples/src/ui/theme.ts +++ b/npm/examples/src/ui/theme.ts @@ -1,6 +1,6 @@ import type { Scene } from 'latkit'; -/** Default canvas colors; examples only override what they demonstrate. */ +/** Default canvas colors. Examples override only the colors they demonstrate. */ export const theme = { base: 'dark', background: '#101214', diff --git a/npm/examples/test/setup.ts b/npm/examples/test/setup.ts index 0dbb989e..b607c0c1 100644 --- a/npm/examples/test/setup.ts +++ b/npm/examples/test/setup.ts @@ -1,6 +1,7 @@ /** - * The page models' tests run in node, where fetch reads no files: latkit's WebAssembly, which it - * fetches from beside its module, is read from disk instead, and loaded before the tests make data. + * Setup for the examples' tests, which run in node. Node's fetch cannot read files, so `file:` + * URLs, such as latkit's WebAssembly beside its module, are read from disk instead. latkit is + * loaded before any test makes data. */ import { readFile } from 'node:fs/promises'; import { load } from 'latkit'; diff --git a/npm/examples/vite.config.ts b/npm/examples/vite.config.ts index 8b885965..2d3891bf 100644 --- a/npm/examples/vite.config.ts +++ b/npm/examples/vite.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from 'vite'; export default defineConfig({ server: { host: '127.0.0.1', port: 5200, strictPort: true }, preview: { host: '127.0.0.1', port: 5200 }, - // The package loads its wasm next to itself; prebundling would move it. + // latkit loads its wasm from beside its own module, which prebundling would move. optimizeDeps: { exclude: ['latkit'] }, build: { target: 'es2022' }, }); diff --git a/npm/release.mjs b/npm/release.mjs index b7adb9f4..af1adecc 100644 --- a/npm/release.mjs +++ b/npm/release.mjs @@ -1,4 +1,5 @@ -/** One retryable release: verify or publish each artifact, then create the Changesets tags. */ +/** Runs a release that can be retried. It verifies or publishes each artifact, then creates the + * Changesets tags. */ import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { createHash } from 'node:crypto'; @@ -14,7 +15,8 @@ const run = (command, args, options = {}) => const hash = (file, algorithm, encoding = 'hex') => createHash(algorithm).update(fs.readFileSync(file)).digest(encoding); -/** Only an explicit 404 means absent. Authentication, rate limits, and network failures stop release. */ +/** Fetches registry metadata from `url`. Returns undefined only on a 404. Any other failure, such + * as authentication, rate limits, or the network, throws and stops the release. */ export async function registry(url) { const response = await fetch(url, { headers: { 'User-Agent': 'latkit-release (github.com/lukelowry/latkit)' }, @@ -24,7 +26,8 @@ export async function registry(url) { return response.json(); } -/** Successful registry responses must contain a digest, never count as absent. */ +/** Returns the digest that `select` reads from the registry metadata at `url`, or undefined if the + * version is absent. A successful response without a digest throws rather than count as absent. */ async function digest(url, select) { const metadata = await registry(url); if (metadata === undefined) return undefined; @@ -33,7 +36,11 @@ async function digest(url, select) { return value; } -/** Existing versions must contain the expected artifact; retries resume after the last completed publish. */ +/** + * Publishes each artifact its registry does not have yet, waiting up to 2 minutes for it to appear, + * then checks that the registry's digest matches the verified build. A retry resumes after the last + * completed publish. + */ export async function ensureArtifacts(artifacts) { for (const artifact of artifacts) { let seen = await artifact.lookup(); @@ -58,7 +65,8 @@ async function release() { const report = read('target/npm/package.json'); const { name, version } = read('npm/package.json'); const git = (args) => run('git', args, { stdio: 'pipe', encoding: 'utf8' }).trim(); - // Tags follow a complete release, so later commits at the same version have nothing to publish. + // Tags are created only after a complete release, so later commits at the same version publish + // nothing. if (git(['tag', '--list', `${name}@${version}`]) !== '') { console.log(`${name}@${version}: released`); return; diff --git a/npm/schema.json b/npm/schema.json index 502d6891..26a2725c 100644 --- a/npm/schema.json +++ b/npm/schema.json @@ -6,49 +6,52 @@ "additionalProperties": false, "properties": { "space": { - "$ref": "#/$defs/Space" + "$ref": "#/$defs/Space", + "description": "The coordinate space positions are in." }, "marks": { "type": "array", "items": { "$ref": "#/$defs/Mark" }, - "description": "What the view draws, in drawing order." + "description": "The marks, in drawing order: each draws over the marks before it." }, "layout": { "$ref": "#/$defs/Layout", - "description": "How the view places vertex rows without positions: a diagram's blocks, or points on a plane\nor the earth, gaps in the space's units." + "description": "How the view places vertex rows that have no position: a diagram's blocks, or points on a\nplane or the earth. `none` by default." }, "theme": { - "$ref": "#/$defs/Theme" + "$ref": "#/$defs/Theme", + "description": "Default colors and sizes, and the look of text, focus, and each space; `dark` by default." }, "controls": { - "$ref": "#/$defs/Controls" + "$ref": "#/$defs/Controls", + "description": "How the view responds to the pointer and keyboard." }, "ease": { "type": "number", "format": "double", - "description": "Seconds that changes ease over; 0.3 unless set." + "description": "Seconds over which scene and data changes animate; 0.3 by default, 0 for none." }, "title": { "type": [ "string", "null" ], - "description": "Written at the view's top left, larger than the theme's text." + "description": "A title drawn at the view's top left, larger than the theme's text." }, "subtitle": { "type": [ "string", "null" ], - "description": "Written under the title." + "description": "A subtitle drawn under the title." } }, "required": [ "space" ], - "description": "What a view draws and how it answers input: a space, marks in drawing order, a layout, a theme,\ncontrols, how long changes ease, and the title written over it. Plain data: as JSON, each field by\nits name in camelCase, each mark `{ vertices | edges | faces | lines | areas: options }`.\n\n```\n# use latkit::{Colormap, Edges, Earth, Projection, Scene, Vertices, field};\nlet voltage = field(\"vm\").colormap(Colormap::BALANCE).domain(0.95..=1.05);\nlet scene = Scene::new(Earth::default().projection(Projection::Globe))\n .draw(Edges::of(\"grid\", \"Branch\").ends(\"from\", \"to\"))\n .draw(Vertices::of(\"grid\", \"Bus\").position(\"lon\", \"lat\").color(voltage).legend(\"Bus voltage · pu\"))\n .title(\"Voltage\");\n```" + "description": "A view's content and behavior: a space, marks drawn in order, a layout, a theme, controls, an\nease duration, and an optional title.\n\nA scene is plain data. In JSON, fields are camelCase and each mark is\n`{ vertices | edges | faces | lines | areas: options }`.\n\n```\n# use latkit::{Colormap, Edges, Earth, Projection, Scene, Vertices, field};\nlet voltage = field(\"vm\").colormap(Colormap::BALANCE).domain(0.95..=1.05);\nlet scene = Scene::new(Earth::default().projection(Projection::Globe))\n .draw(Edges::of(\"grid\", \"Branch\").ends(\"from\", \"to\"))\n .draw(Vertices::of(\"grid\", \"Bus\").position(\"lon\", \"lat\").color(voltage).legend(\"Bus voltage · pu\"))\n .title(\"Voltage\");\n```" }, "Space": { "anyOf": [ @@ -77,7 +80,7 @@ "additionalProperties": false }, { - "description": "Plane units with elevation: graphs and scenes the camera turns and tilts around.", + "description": "Plane units, with elevation; the camera can turn and tilt.", "type": "object", "properties": { "plane": { @@ -90,7 +93,7 @@ "additionalProperties": false }, { - "description": "The data's axis across, values up.", + "description": "The data's axis horizontally and values vertically.", "type": "object", "properties": { "plot": { @@ -103,7 +106,7 @@ "additionalProperties": false }, { - "description": "A sheet for diagrams: x right and y down in units, every length a unit.", + "description": "A flat sheet for diagrams: x right and y down, every length in units.", "type": "object", "properties": { "diagram": { @@ -118,7 +121,7 @@ ] } ], - "description": "The coordinate space a scene's positions are in: a name, or `{ plane | diagram | earth | plot:\noptions }`. Its own unit is what lengths are in unless they say theirs: units in a diagram, a flat\nsheet seen from straight above with y running down, a unit a CSS pixel at zoom 0; CSS pixels\nelsewhere. Only a diagram has ports, groups, nets, and orthogonal routes." + "description": "The coordinate space of a scene's positions: a name, or\n`{ plane | diagram | earth | plot: options }`.\n\nThe space sets the unit of lengths that do not name their own: units in a diagram, where a unit\nis a CSS pixel at zoom 0, and CSS pixels elsewhere. Only a diagram has ports, groups, nets, and\northogonal routes." }, "Earth": { "type": "object", @@ -126,11 +129,11 @@ "properties": { "projection": { "$ref": "#/$defs/Projection", - "description": "A flat map or a globe; a flat map unless set." + "description": "A flat web mercator map (`mercator`, the default) or a globe (`globe`)." }, "graticule": { "type": "boolean", - "description": "Whether lines of longitude and latitude cross the ground, in the theme's grid color." + "description": "Whether lines of longitude and latitude cross the ground, in the theme's `grid` color.\nDefault `true`." }, "daylight": { "type": [ @@ -138,14 +141,14 @@ "null" ], "format": "double", - "description": "Lit by the sun as it stands this many seconds after the Unix epoch, plus the view's axis\ncoordinate when it shows one: night falls on the ground and what stands on it, down to the\ntheme's night. Lit evenly unless set." + "description": "Lights the earth by the sun's position this many seconds after the Unix epoch, plus the\nview's axis coordinate when it has one. The night side darkens the ground and everything on\nit, down to the theme's `earth.night` brightness. Unset, the earth is lit evenly." }, "atmosphere": { "type": "boolean", - "description": "Whether air glows around the globe, in the theme's atmosphere color, brighter where it is day." + "description": "Whether air glows around the globe, in the theme's `earth.atmosphere` color, brighter on the\nday side. Default `false`." } }, - "description": "The earth: positions are longitude and latitude in degrees, its ground in the theme's earth\ncolors." + "description": "The earth: positions are longitude and latitude in degrees. The ground takes the theme's `earth`\ncolors." }, "Projection": { "oneOf": [ @@ -160,7 +163,7 @@ "description": "A sphere." } ], - "description": "How the earth is drawn." + "description": "How the earth is projected." }, "Plane": { "type": "object", @@ -168,10 +171,10 @@ "properties": { "grid": { "$ref": "#/$defs/Grid", - "description": "The grid under the marks." + "description": "The grid drawn under the marks; lines 10 units apart by default." } }, - "description": "A plane: positions are plane units." + "description": "A plane: positions are in plane units." }, "Grid": { "oneOf": [ @@ -202,7 +205,7 @@ "lines" ], "additionalProperties": false, - "description": "Lines `pitch` units apart, thinned by tens as the view zooms out." + "description": "Lines `pitch` units apart, thinned by factors of ten as the view zooms out." }, { "type": "object", @@ -216,7 +219,7 @@ "format": "double" }, "snap": { - "description": "Whether moved vertices snap to the dots.", + "description": "Whether drag positions snap to the dots, in a diagram. Default `false`.", "type": "boolean" } }, @@ -230,7 +233,7 @@ "dots" ], "additionalProperties": false, - "description": "Dots `pitch` units apart; drags snap to them when `snap`." + "description": "Dots `pitch` units apart; with `snap`, a diagram's drags snap to them." } ], "description": "A plane's or a diagram's grid: `\"off\"`, `{ lines: { pitch } }`, or `{ dots: { pitch, snap } }`." @@ -263,11 +266,11 @@ }, "grid": { "type": "boolean", - "description": "Whether grid lines cross the plot at the ticks." + "description": "Whether grid lines cross the plot at the ticks. Default `true`." }, "hidden": { "type": "boolean", - "description": "Whether the axis is hidden." + "description": "Whether the axis is hidden. Default `false`." }, "ticks": { "type": [ @@ -277,11 +280,11 @@ "items": { "$ref": "#/$defs/Tick" }, - "description": "Ticks at these values, each a value or `[value, label]`, their values written unless labeled;\nround values unless set." + "description": "Tick positions, each a value or `[value, label]`; a tick without a label shows its value.\nUnset, ticks fall at round values." }, "format": { "$ref": "#/$defs/Format", - "description": "How tick values are written." + "description": "How tick values are formatted; `auto` by default." }, "precision": { "type": [ @@ -291,7 +294,7 @@ "format": "uint8", "minimum": 0, "maximum": 15, - "description": "Decimals of fixed values, or of the significand of scientific and engineering ones; as many\nas the ticks' step needs unless set." + "description": "Decimal places of fixed values, or of the significand in scientific and engineering formats,\nat most 15. Unset, as many as the tick step needs." } }, "description": "A plot axis." @@ -320,7 +323,7 @@ "maxItems": 2 } ], - "description": "A tick as written: a value, or `[value, label]`." + "description": "A tick in JSON: a value, or `[value, label]`." }, "Format": { "oneOf": [ @@ -345,7 +348,7 @@ "description": "A significand and a power of ten that is a multiple of three: `1.5e3`, `150e-6`." } ], - "description": "How an axis writes its tick values." + "description": "How an axis formats its tick values." }, "Diagram": { "type": "object", @@ -353,10 +356,10 @@ "properties": { "grid": { "$ref": "#/$defs/Grid", - "description": "The grid under the marks." + "description": "The grid drawn under the marks; dots 20 units apart by default." } }, - "description": "A sheet for diagrams, flat and seen from straight above: the camera pans and zooms. Positions are\nunits with y down, and lengths are units, a unit a CSS pixel at zoom 0 (sizes, padding, ports,\nstrokes, text, and the theme's), but for what says `px`, a halo, focus's glow, and the pick\nradius. Zooming scales one fixed picture. Only a diagram has ports, groups, nets, and orthogonal\nroutes; it lays out blocks." + "description": "A flat sheet for diagrams, seen from straight above; the camera pans and zooms but does not\nturn. Positions are in units with y down. Lengths are in units too, a unit being a CSS pixel at\nzoom 0: sizes, padding, ports, strokes, text, and the theme's lengths. Sizes given with `px`,\nthe text halo, the focus glow, and the pick radius stay in CSS pixels. Zooming scales the whole\npicture. Only a diagram has ports, groups, nets, and orthogonal routes, and the scene's layout\nplaces its blocks." }, "Mark": { "oneOf": [ @@ -426,7 +429,7 @@ "description": "Rows drawn as the areas their polygons cover." } ], - "description": "One drawn table of a scene: `{ vertices | edges | faces | lines | areas: options }` as JSON." + "description": "One table a scene draws. In JSON: `{ vertices | edges | faces | lines | areas: options }`." }, "Vertices": { "type": "object", @@ -457,18 +460,18 @@ }, "minItems": 2, "maxItems": 2, - "description": "Each row's x and y. Without them, the point field `geometry` names places rows; rows without\neither the scene's layout places." + "description": "Each row's x and y. Without a position, the point field that `geometry` names places rows,\nand the scene's layout places rows that have neither." }, "geometry": { "type": [ "string", "null" ], - "description": "The point field placing each row where `position` is not given; `geometry` unless set." + "description": "The point field that places rows when `position` is not set; the field named `geometry` by\ndefault." }, "elevation": { "$ref": "#/$defs/Expr", - "description": "Height above the ground: on the earth, a fraction of its radius. Not in a diagram, which is\nflat." + "description": "Height above the ground; on the earth, a fraction of the earth's radius. Default 0. Not\nallowed in a diagram, which is flat." }, "radius": { "anyOf": [ @@ -479,7 +482,7 @@ "type": "null" } ], - "description": "Each row's radius, in the space's unit unless it says its own: units in a diagram, CSS pixels\nelsewhere." + "description": "Each row's radius, in the space's unit (units in a diagram, CSS pixels elsewhere) unless\ngiven with `px` or `units`; a diagram refuses `px`. The theme's `vertex.radius` unless set." }, "size": { "type": [ @@ -491,11 +494,11 @@ }, "minItems": 2, "maxItems": 2, - "description": "Width and height, for markers that are not round, in the space's unit unless they say their\nown; in a diagram a block's are units, and a block without a size fits its label and ports." + "description": "Each row's width and height, for markers that are not round, in the space's unit unless\ngiven with `px` or `units`; a diagram refuses `px`. In a diagram, a block without a size\nfits its label and ports." }, "marker": { "$ref": "#/$defs/Marker", - "description": "How each row is drawn." + "description": "How each row is drawn; an ellipse by default." }, "ring": { "anyOf": [ @@ -523,7 +526,7 @@ "minItems": 2, "maxItems": 2 }, - "description": "A port on each row for each reference field naming a net, which an `edges` mark of the net\ntable without ends draws. Diagrams only.", + "description": "Ports on each row, one for each reference field that names a net. An `edges` mark of the net\ntable, without `ends`, draws the nets. Diagrams only.", "tsType": "[reference: string, port: Port][]" }, "contains": { @@ -541,12 +544,12 @@ ], "minItems": 2, "maxItems": 2, - "description": "Makes each row a group: it holds the rows of `table` whose reference field names it, and is\ndrawn as a container around them, its label a title in its header. Diagrams only.", + "description": "Makes each row a group containing the rows of `table` whose `reference` field names it. A\ngroup draws as a container around its members, with its label as a title in its header.\nDiagrams only.", "tsType": "[table: string, reference: string]" }, "shadow": { "type": "boolean", - "description": "Whether each row, a block or a group, drops a shadow onto the sheet, as the theme's `shadow`\nsays. Diagrams only." + "description": "Whether each block or group casts a shadow on the sheet, styled by the theme's\n`diagram.shadow`. Default `false`. Diagrams only." }, "color": { "anyOf": [ @@ -557,11 +560,11 @@ "type": "null" } ], - "description": "Each row's color; the theme's unless set." + "description": "Each row's color. Unset, rows take the theme's default for the mark; faces instead\nblend their corners' colors." }, "visible": { "$ref": "#/$defs/Expr", - "description": "Whether each row is drawn and picked." + "description": "Whether each row is drawn and can be picked. Default `true`." }, "priority": { "anyOf": [ @@ -572,29 +575,29 @@ "type": "null" } ], - "description": "Each row's priority, from its data alone: what matters more draws on top, stays drawn as\na table too large to draw whole thins to the tiles in view, and is labeled first." + "description": "Each row's priority, from its data alone (no view state). Higher-priority rows draw\non top, are the last dropped when a table too large to draw whole is thinned to the\ntiles in view, and are labeled first." }, "name": { "type": [ "string", "null" ], - "description": "Tells two marks of one table apart across scenes, and names the mark in hits." + "description": "Distinguishes two marks of the same table across scenes, and names the mark in hits." }, "pick": { "type": [ "boolean", "null" ], - "description": "Whether rows are hovered, clicked, and probed. Unless set, rows that are things are\n(vertices, edges, faces, a plot's traces) and geometry is not (areas, and lines along\npaths: coasts, borders, roads)." + "description": "Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and\nplot traces do, and geometry (areas, and lines along paths such as coasts, borders,\nand roads) does not." }, "select": { "type": "boolean", - "description": "Whether a click selects a row." + "description": "Whether clicking a row selects it. Default `true`." }, "drag": { "type": "boolean", - "description": "Whether pressing a row drags it instead of panning." + "description": "Whether pressing and moving a row drags it, reported as\n[`Event::Drag`](crate::Event), instead of panning the camera. Default `false`." }, "label": { "anyOf": [ @@ -605,14 +608,14 @@ "type": "null" } ], - "description": "Each row's text: a text field, or a label's options." + "description": "Each row's label: a text field, or label options." }, "legend": { "type": [ "string", "null" ], - "description": "A legend of the colors at the view's bottom right, under this title: the colormap across\nits domain, ticked; a cell for each category; or a swatch beside each name of a map's\n`categories`. The color maps through a colormap with a domain or named categories; a\nconditional's `otherwise` is read before its cases." + "description": "A legend of the mark's colors at the view's bottom right, under this title: a ticked\ncolormap across its domain, a cell for each category, or a swatch beside each name\nof a map's `categories`. `color` must map through a colormap with a fixed domain or\nnamed categories; in a conditional, `otherwise` is searched before the cases." } }, "required": [ @@ -625,7 +628,7 @@ "tsType": "ExprInput" }, "ExprNode": { - "description": "A value for every row of a mark: a constant, a field, view state, or arithmetic of them.", + "description": "A value for each row: a number, a boolean, a field's name, or a node that computes one.", "anyOf": [ { "type": "number" @@ -677,7 +680,8 @@ "additionalProperties": false, "properties": { "field": { - "type": "string" + "type": "string", + "description": "The field's name." }, "from": { "type": [ @@ -699,13 +703,13 @@ }, "step": { "type": "boolean", - "description": "Reads a sampled field at its last frame without interpolating." + "description": "For a sampled field, reads the last frame at or before the shown coordinate instead of\ninterpolating. Default `false`." } }, "required": [ "field" ], - "description": "A field of each row: of the mark's own table, or from another dataset's table joined by row id." + "description": "A field's value in each row: from the mark's own table, or with `from`, from another\ndataset's table joined by row id." }, "ColorNode": { "type": "object", @@ -719,7 +723,8 @@ }, "required": [ "color" - ] + ], + "description": "A constant color." }, "StateNode": { "type": "object", @@ -732,7 +737,7 @@ "required": [ "state" ], - "description": "What the view knows of each row." + "description": "View state of each row, such as whether it is selected." }, "State": { "type": "string", @@ -743,7 +748,7 @@ "elapsed", "zoom" ], - "description": "What the view knows of each row." + "description": "View state of each row: `selected`, `hovered`, `pointer` (CSS pixels from a vertex to the\npointer), `elapsed` (seconds since the view was created), or `zoom` (the camera's zoom)." }, "MissingNode": { "type": "object", @@ -757,7 +762,7 @@ "required": [ "missing" ], - "description": "A missing value: options that read one take the theme's." + "description": "A missing value. An option that evaluates to missing uses the theme's value." }, "UnaryNode": { "type": "object", @@ -778,7 +783,8 @@ "required": [ "op", "args" - ] + ], + "description": "A one-argument operation, such as `{ \"op\": \"abs\", \"args\": [\"x\"] }`." }, "Unary": { "type": "string", @@ -793,7 +799,7 @@ "ln", "exp" ], - "description": "An operation of one value." + "description": "An operation on one value. `sin` and `cos` take radians; `not` treats values above 0.5 as\ntrue." }, "BinaryNode": { "type": "object", @@ -814,7 +820,8 @@ "required": [ "op", "args" - ] + ], + "description": "A two-argument operation, such as `{ \"op\": \"add\", \"args\": [\"a\", 1] }`." }, "Binary": { "type": "string", @@ -836,7 +843,7 @@ "and", "or" ], - "description": "An operation of two values." + "description": "An operation on two values: arithmetic, `min` and `max`, comparisons (1 for true, 0 for\nfalse), and logic. `rem` is a floored remainder, which takes the sign of the divisor. `and` and\n`or` treat values above 0.5 as true." }, "WhenNode": { "type": "object", @@ -857,10 +864,12 @@ "minItems": 2, "maxItems": 2 }, + "description": "The cases, as `[condition, value]` pairs tried in order.", "tsType": "[condition: ExprInput, value: ExprInput][]" }, "otherwise": { - "$ref": "#/$defs/Expr" + "$ref": "#/$defs/Expr", + "description": "The value where no condition holds." } }, "required": [ @@ -891,7 +900,7 @@ "required": [ "is" ], - "description": "Whether a text field equals the text." + "description": "Whether a text field equals the text exactly." }, "ContainsNode": { "type": "object", @@ -915,7 +924,7 @@ "required": [ "contains" ], - "description": "Whether a text field holds the text, ignoring case: in a mark, a field of its own table." + "description": "Whether a text field contains the text, ignoring case. In a mark, the field must belong to\nthe mark's own table." }, "ScaleNode": { "type": "object", @@ -952,7 +961,7 @@ "scale", "value" ], - "description": "`value` mapped linearly onto the range `scale`, clamped: from its `domain`, else from the extent of\nthe values shown." + "description": "`value` mapped linearly from `domain` onto the range `scale`, clamped to the range. Without\na `domain`, the extent of the values shown is used." }, "ColormapNode": { "type": "object", @@ -982,7 +991,7 @@ "colormap", "value" ], - "description": "`value` through `colormap`: from its `domain`, else from the extent of the values shown." + "description": "`value` mapped through `colormap` over `domain`, or without a `domain`, over the extent of\nthe values shown. A categorical map ignores the domain and bins values by their integer\npart." }, "Colormap": { "description": "Colors for a range of values: a catalog name, or your own colors.", @@ -1082,21 +1091,21 @@ } ] }, - "description": "Colors blended in OKLab, each at its `[place, color]` from 0 to 1 or evenly between its\nneighbors, as in a CSS gradient; a categorical map's, one per category." + "description": "The colors, blended in OKLab. Each is a color, spaced evenly between its neighbors as in a\nCSS gradient, or a `[place, color]` pair with a place from 0 to 1, in ascending order. A\ncategorical map takes one color per category, without places, at most 256." }, "categories": { "type": "array", "items": { "type": "string" }, - "description": "The categories' names, in order, for a legend: a categorical map's alone." + "description": "The category names in category order, shown in legends. Categorical maps only." } }, "required": [ "kind", "colors" ], - "description": "Your own colormap: its kind, its colors, and for a categorical map its categories' names." + "description": "A custom colormap: its kind, its colors, and for a categorical map, its category names." }, "ColormapKind": { "oneOf": [ @@ -1108,7 +1117,7 @@ { "type": "string", "const": "diverging", - "description": "Below and above a middle." + "description": "Below and above a midpoint." }, { "type": "string", @@ -1121,10 +1130,10 @@ "description": "Distinct colors for distinct integers." } ], - "description": "How a colormap spans values." + "description": "How a colormap maps values: `sequential` and `diverging` maps clamp, `cyclic` maps wrap, and\n`categorical` maps give each integer its own color." }, "Size": { - "description": "A size, in the space's unit unless it says its own: units in a diagram, CSS pixels elsewhere; `px` keeps CSS pixels, and `units` takes position units, which scale as the view zooms.", + "description": "A size: a value in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in CSS pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the view zooms in.", "anyOf": [ { "$ref": "#/$defs/Expr" @@ -1142,20 +1151,22 @@ "additionalProperties": false, "properties": { "px": { - "$ref": "#/$defs/Expr" + "$ref": "#/$defs/Expr", + "description": "The size, in CSS pixels." } }, "required": [ "px" ], - "description": "A size in CSS pixels, whatever the space's own unit." + "description": "A size in CSS pixels, regardless of the space's unit." }, "Units": { "type": "object", "additionalProperties": false, "properties": { "units": { - "$ref": "#/$defs/Expr" + "$ref": "#/$defs/Expr", + "description": "The size, in position units." }, "atLeast": { "type": [ @@ -1163,7 +1174,7 @@ "null" ], "format": "float", - "description": "CSS pixels it keeps at least, however far the view zooms out.", + "description": "The minimum on-screen size in CSS pixels, however far the view zooms out.", "minimum": 0 }, "atMost": { @@ -1172,14 +1183,14 @@ "null" ], "format": "float", - "description": "CSS pixels it keeps at most, however far the view zooms in.", + "description": "The maximum on-screen size in CSS pixels, however far the view zooms in.", "minimum": 0 } }, "required": [ "units" ], - "description": "A size in position units, whatever the space's own unit, so it scales as the view zooms, kept\nwithin CSS pixels when given." + "description": "A size in position units (degrees on the earth), regardless of the space's unit, so it scales\nas the view zooms. `atLeast` and `atMost` bound it in CSS pixels." }, "Marker": { "anyOf": [ @@ -1209,14 +1220,14 @@ "additionalProperties": false }, { - "description": "A block: a rectangle with rounded corners, sized for diagrams.", + "description": "A rectangle with rounded corners that holds its label inside, as a diagram's blocks do.", "type": "object", "properties": { "block": { "type": "object", "properties": { "corner": { - "description": "Corner radius, CSS pixels, or units in a diagram; the theme's `corner` unless set.", + "description": "Corner radius in the space's unit (units in a diagram, CSS pixels elsewhere); the\ntheme's `diagram.corner` unless set.", "type": [ "number", "null" @@ -1233,18 +1244,18 @@ "additionalProperties": false }, { - "description": "A shape's outline, filled to `fill` from 0 to 1: around a circle, up other shapes.", + "description": "A shape's outline, filled to `fill` from 0 to 1: around a circle, or upward in other shapes.", "type": "object", "properties": { "gauge": { "type": "object", "properties": { "fill": { - "description": "How full each row's gauge is.", + "description": "How full each row's gauge is, from 0 to 1.", "$ref": "#/$defs/Expr" }, "shape": { - "description": "The outline.", + "description": "The outline's shape; an ellipse by default.", "$ref": "#/$defs/Shape" } }, @@ -1260,7 +1271,7 @@ "additionalProperties": false }, { - "description": "A circle cut into slices, each as large as its value.", + "description": "A circle divided into slices sized in proportion to their values.", "type": "object", "properties": { "pie": { @@ -1283,7 +1294,7 @@ } }, "hole": { - "description": "The hole in the middle, as a fraction of the radius.", + "description": "The hole in the middle, as a fraction of the radius; 0 by default.", "type": "number", "format": "float" } @@ -1301,7 +1312,7 @@ "additionalProperties": false }, { - "description": "One of `images`, fitted in the marker's box; `index` picks which for each row.", + "description": "One of `images`, fitted to the marker's box; `index` picks the image for each row.", "type": "object", "properties": { "icon": { @@ -1315,7 +1326,7 @@ } }, "index": { - "description": "Each row's image, from 0.", + "description": "Each row's image index, from 0.", "$ref": "#/$defs/Expr" } }, @@ -1332,18 +1343,18 @@ "additionalProperties": false }, { - "description": "A marker in WGSL: `source` defines `fn marker(f: MarkerFragment) -> MarkerColor`, reading up to\neight `inputs` as `f.input[i]`. It compiles once, when the view first draws it.", + "description": "A custom marker in WGSL. `source` defines `fn marker(f: MarkerFragment) -> MarkerColor`,\nwhich can read up to eight `inputs` as `f.input[i]`. It compiles once, when the view first\ndraws it.", "type": "object", "properties": { "wgsl": { "type": "object", "properties": { "source": { - "description": "The function's source.", + "description": "The WGSL source of the `marker` function.", "type": "string" }, "inputs": { - "description": "Each row's inputs.", + "description": "Each row's inputs, at most eight.", "type": "array", "items": { "$ref": "#/$defs/Expr" @@ -1364,7 +1375,7 @@ ] } ], - "description": "How a vertex is drawn: a shape's name, `\"block\"`, or `{ shape | block | gauge | pie | icon | wgsl:\noptions }`." + "description": "How a vertex is drawn: a shape's name, `\"block\"`, or\n`{ shape | block | gauge | pie | icon | wgsl: options }`. An ellipse by default." }, "Shape": { "oneOf": [ @@ -1421,14 +1432,14 @@ "size", "rgba" ], - "description": "RGBA texels, straight alpha, sRGB, rows top to bottom." + "description": "An image of RGBA texels: straight alpha, sRGB, rows top to bottom." }, "Paint": { "$ref": "#/$defs/PaintNode", "tsType": "PaintInput" }, "PaintNode": { - "description": "A color for every row of a mark: a CSS color, a value through a colormap, or a conditional choosing between colors.", + "description": "A color for each row: a CSS color, a value through a colormap, or a conditional choosing between colors.", "anyOf": [ { "description": "A CSS color, such as `#ffb000` or `rebeccapurple`.", @@ -1468,17 +1479,19 @@ "minItems": 2, "maxItems": 2 }, + "description": "The cases, as `[condition, color]` pairs tried in order.", "tsType": "[condition: ExprInput, color: PaintInput][]" }, "otherwise": { - "$ref": "#/$defs/Paint" + "$ref": "#/$defs/Paint", + "description": "The color where no condition holds." } }, "required": [ "when", "otherwise" ], - "description": "A conditional's color: the first case's whose condition holds, else `otherwise`'s." + "description": "A conditional color: the color of the first case whose condition holds, else `otherwise`." }, "Port": { "anyOf": [ @@ -1495,11 +1508,11 @@ "type": "object", "properties": { "side": { - "description": "The side it sits on.", + "description": "The side of the marker it sits on.", "$ref": "#/$defs/Side" }, "order": { - "description": "Its place among the ports on that side, from the top or the left; in declaration order unless set.", + "description": "Its position among the ports on its side, counted from the top or left. Ports without an\n`order` follow those with one, in declaration order.", "type": [ "integer", "null" @@ -1515,11 +1528,11 @@ ] }, "shape": { - "description": "Its marker.", + "description": "Its shape; an ellipse by default.", "$ref": "#/$defs/Shape" }, "output": { - "description": "Whether it drives the net its reference field names; an input unless set.", + "description": "Whether it is an output, which drives its net. Default `false`, an input.", "type": "boolean" } }, @@ -1529,7 +1542,7 @@ ] } ], - "description": "A vertex's port: where a net its reference field names meets it, on one side of its marker: a\nside, or options. An output drives its net: layouts rank a net from its outputs, and its arrows\npoint away from them.\n\n```\n# use latkit::{Port, Side, Vertices};\nlet pumps = Vertices::of(\"plant\", \"Pump\").port(\"in\", Side::Left).port(\"out\", Port::output(Side::Right));\n```" + "description": "A port on one side of a vertex's marker, where a net connects: a side name, or options. The\nvertex names the net in a reference field. An output drives its net: layouts rank a net from its\noutputs, and the net's arrows point away from them.\n\n```\n# use latkit::{Port, Side, Vertices};\nlet pumps = Vertices::of(\"plant\", \"Pump\").port(\"in\", Side::Left).port(\"out\", Port::output(Side::Right));\n```" }, "Side": { "oneOf": [ @@ -1562,15 +1575,14 @@ "$ref": "#/$defs/Expr" }, { - "description": "A row's text, placed beside, inside, or along what its mark draws, following it every frame: a\ntext field, or options. Labels that would overlap give way by priority; among equals, those\nalready shown keep their place. In a diagram a label is part of the drawing: it scales with the\nzoom, fades out once too small to read, and shows wherever it fits its vertex.\n\n```\n# use latkit::{field, Label, Vertices};\nlet buses = Vertices::of(\"grid\", \"Bus\").position(\"lon\", \"lat\").label(Label::new(\"name\").priority(field(\"kv\")));\n```", "type": "object", "properties": { "text": { - "description": "A text field.", + "description": "The text field to show.", "$ref": "#/$defs/Expr" }, "size": { - "description": "In the space's unit: units in a diagram, CSS pixels elsewhere; the theme's text size unless\nset.", + "description": "Font size in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's\n`text.size` unless set.", "type": [ "number", "null" @@ -1589,7 +1601,7 @@ ] }, "priority": { - "description": "Where labels would overlap, the higher shows; the mark's priority unless set.", + "description": "Where labels would overlap, the higher priority shows; the mark's `priority` unless set.", "anyOf": [ { "$ref": "#/$defs/Expr" @@ -1600,7 +1612,7 @@ ] }, "maxWidth": { - "description": "How far a line runs before it wraps, in the space's unit: units in a diagram, CSS pixels\nelsewhere.", + "description": "The width at which lines wrap, in the space's unit (units in a diagram, CSS pixels\nelsewhere).", "type": [ "number", "null" @@ -1608,14 +1620,14 @@ "format": "float" }, "font": { - "description": "The CSS font family list its texts are set in; the theme's font unless set.", + "description": "The CSS font family list for the text; the theme's `text.font` unless set.", "type": [ "string", "null" ] }, "max": { - "description": "The most labels shown at once, those of highest priority among the rows in view; where they\nwould overlap, fewer.", + "description": "The most labels shown at once: those of highest priority among the rows in view, or fewer\nwhere they would overlap. No limit unless set.", "type": [ "integer", "null" @@ -1629,7 +1641,8 @@ "text" ] } - ] + ], + "description": "Text for each row, placed beside, inside, or along the row's shape and following it every frame:\na text field, or options. Where labels would overlap, the higher priority shows; among equals,\nlabels already shown stay. In a diagram a label is part of the drawing: it scales with the zoom,\nfades out when too small to read, and shows wherever it fits its vertex.\n\n```\n# use latkit::{field, Label, Vertices};\nlet buses = Vertices::of(\"grid\", \"Bus\").position(\"lon\", \"lat\").label(Label::new(\"name\").priority(field(\"kv\")));\n```" }, "Edges": { "type": "object", @@ -1673,7 +1686,7 @@ "string", "null" ], - "description": "A path field of points each row runs through between its ends." + "description": "A path field whose points each row passes through between its ends." }, "route": { "anyOf": [ @@ -1684,15 +1697,15 @@ "type": "null" } ], - "description": "How each row runs between its ends; on the earth geodesic, elsewhere straight, unless set.\nOrthogonal routes run around blocks, bends rounded; diagrams only." + "description": "How each row runs between its ends: geodesic on the earth and straight elsewhere unless set.\n`orthogonal` routes around blocks with rounded bends, in diagrams only." }, "arrows": { "type": "boolean", - "description": "Whether each row ends in an arrowhead at its second end; a net's at each input it reaches." + "description": "Whether each row has an arrowhead at its second end; a net has one at each input it reaches.\nDefault `false`." }, "poles": { "type": "boolean", - "description": "Draws each row as a pole from its own vertex down to the ground beneath it, on a plane or the\nearth; it has no ends." + "description": "Draws each row as a pole from its own vertex down to the ground, on a plane or the earth.\nPoles take no `ends`, `bends`, or `joins`. Default `false`." }, "joins": { "type": [ @@ -1709,7 +1722,7 @@ ], "minItems": 2, "maxItems": 2, - "description": "Draws each row as a net on a plane or the earth: it joins the vertex rows of `table` whose\nreference field names it, as a star from their middle to each.", + "description": "Draws each row as a net on a plane or the earth, joining the vertex rows of `table` whose\n`reference` field names it: a star from their center to each member.", "tsType": "[table: string, reference: string]" }, "width": { @@ -1721,15 +1734,15 @@ "type": "null" } ], - "description": "Each row's width, in the space's unit unless it says its own: units in a diagram, CSS\npixels elsewhere." + "description": "Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere)\nunless given with `px` or `units`. The theme's width unless set." }, "dash": { "$ref": "#/$defs/Expr", - "description": "Whether each row is dashed." + "description": "Whether each row is dashed, with the theme's `dash` period. Default `false`." }, "flow": { "$ref": "#/$defs/Expr", - "description": "Comets flowing along each row at this speed a second, in the lengths of the theme's\nflow; negative flows backward. A speed that changes carries comets on from where they\nare." + "description": "The speed of comets flowing along each row, per second, in the unit of the theme's\n`flow` lengths. Negative speeds flow backward; 0, the default, draws no comets. When\nthe speed changes, comets continue from where they are." }, "color": { "anyOf": [ @@ -1740,11 +1753,11 @@ "type": "null" } ], - "description": "Each row's color; the theme's unless set." + "description": "Each row's color. Unset, rows take the theme's default for the mark; faces instead\nblend their corners' colors." }, "visible": { "$ref": "#/$defs/Expr", - "description": "Whether each row is drawn and picked." + "description": "Whether each row is drawn and can be picked. Default `true`." }, "priority": { "anyOf": [ @@ -1755,29 +1768,29 @@ "type": "null" } ], - "description": "Each row's priority, from its data alone: what matters more draws on top, stays drawn as\na table too large to draw whole thins to the tiles in view, and is labeled first." + "description": "Each row's priority, from its data alone (no view state). Higher-priority rows draw\non top, are the last dropped when a table too large to draw whole is thinned to the\ntiles in view, and are labeled first." }, "name": { "type": [ "string", "null" ], - "description": "Tells two marks of one table apart across scenes, and names the mark in hits." + "description": "Distinguishes two marks of the same table across scenes, and names the mark in hits." }, "pick": { "type": [ "boolean", "null" ], - "description": "Whether rows are hovered, clicked, and probed. Unless set, rows that are things are\n(vertices, edges, faces, a plot's traces) and geometry is not (areas, and lines along\npaths: coasts, borders, roads)." + "description": "Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and\nplot traces do, and geometry (areas, and lines along paths such as coasts, borders,\nand roads) does not." }, "select": { "type": "boolean", - "description": "Whether a click selects a row." + "description": "Whether clicking a row selects it. Default `true`." }, "drag": { "type": "boolean", - "description": "Whether pressing a row drags it instead of panning." + "description": "Whether pressing and moving a row drags it, reported as\n[`Event::Drag`](crate::Event), instead of panning the camera. Default `false`." }, "label": { "anyOf": [ @@ -1788,27 +1801,27 @@ "type": "null" } ], - "description": "Each row's text: a text field, or a label's options." + "description": "Each row's label: a text field, or label options." }, "legend": { "type": [ "string", "null" ], - "description": "A legend of the colors at the view's bottom right, under this title: the colormap across\nits domain, ticked; a cell for each category; or a swatch beside each name of a map's\n`categories`. The color maps through a colormap with a domain or named categories; a\nconditional's `otherwise` is read before its cases." + "description": "A legend of the mark's colors at the view's bottom right, under this title: a ticked\ncolormap across its domain, a cell for each category, or a swatch beside each name\nof a map's `categories`. `color` must map through a colormap with a fixed domain or\nnamed categories; in a conditional, `otherwise` is searched before the cases." } }, "required": [ "of" ], - "description": "Rows drawn as strokes between the vertices two reference fields name: branches, links, wires. In a diagram, a row without ends is a net: it joins every vertex port whose reference field names it." + "description": "Rows drawn as strokes between the vertices named by two reference fields: branches, links, wires. In a diagram, a row without ends is a net, joining every vertex port whose reference field names it." }, "Route": { "oneOf": [ { "type": "string", "const": "straight", - "description": "Straight across the screen." + "description": "A straight line on screen." }, { "type": "string", @@ -1818,7 +1831,7 @@ { "type": "string", "const": "orthogonal", - "description": "In horizontal and vertical runs around vertices, leaving and entering at their sides, bends\nrounded. Diagrams only." + "description": "Horizontal and vertical runs around vertices that leave and enter at their sides, with\nrounded bends. Diagrams only." } ], "description": "How an edge runs between its ends." @@ -1852,7 +1865,7 @@ }, "minItems": 3, "maxItems": 3, - "description": "The reference fields naming each row's three vertices. Without a color, a face blends the\ncolors of its corners.", + "description": "The reference fields naming each row's three vertices; required. Without a color, a face\nblends the colors of its corners.", "tsType": "[a: string, b: string, c: string]" }, "color": { @@ -1864,11 +1877,11 @@ "type": "null" } ], - "description": "Each row's color; the theme's unless set." + "description": "Each row's color. Unset, rows take the theme's default for the mark; faces instead\nblend their corners' colors." }, "visible": { "$ref": "#/$defs/Expr", - "description": "Whether each row is drawn and picked." + "description": "Whether each row is drawn and can be picked. Default `true`." }, "priority": { "anyOf": [ @@ -1879,29 +1892,29 @@ "type": "null" } ], - "description": "Each row's priority, from its data alone: what matters more draws on top, stays drawn as\na table too large to draw whole thins to the tiles in view, and is labeled first." + "description": "Each row's priority, from its data alone (no view state). Higher-priority rows draw\non top, are the last dropped when a table too large to draw whole is thinned to the\ntiles in view, and are labeled first." }, "name": { "type": [ "string", "null" ], - "description": "Tells two marks of one table apart across scenes, and names the mark in hits." + "description": "Distinguishes two marks of the same table across scenes, and names the mark in hits." }, "pick": { "type": [ "boolean", "null" ], - "description": "Whether rows are hovered, clicked, and probed. Unless set, rows that are things are\n(vertices, edges, faces, a plot's traces) and geometry is not (areas, and lines along\npaths: coasts, borders, roads)." + "description": "Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and\nplot traces do, and geometry (areas, and lines along paths such as coasts, borders,\nand roads) does not." }, "select": { "type": "boolean", - "description": "Whether a click selects a row." + "description": "Whether clicking a row selects it. Default `true`." }, "drag": { "type": "boolean", - "description": "Whether pressing a row drags it instead of panning." + "description": "Whether pressing and moving a row drags it, reported as\n[`Event::Drag`](crate::Event), instead of panning the camera. Default `false`." }, "label": { "anyOf": [ @@ -1912,20 +1925,20 @@ "type": "null" } ], - "description": "Each row's text: a text field, or a label's options." + "description": "Each row's label: a text field, or label options." }, "legend": { "type": [ "string", "null" ], - "description": "A legend of the colors at the view's bottom right, under this title: the colormap across\nits domain, ticked; a cell for each category; or a swatch beside each name of a map's\n`categories`. The color maps through a colormap with a domain or named categories; a\nconditional's `otherwise` is read before its cases." + "description": "A legend of the mark's colors at the view's bottom right, under this title: a ticked\ncolormap across its domain, a cell for each category, or a swatch beside each name\nof a map's `categories`. `color` must map through a colormap with a fixed domain or\nnamed categories; in a conditional, `otherwise` is searched before the cases." } }, "required": [ "of" ], - "description": "Rows drawn as triangles between the vertices three reference fields name: meshes, surfaces, and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen from above shows its exact colors." + "description": "Rows drawn as triangles between the vertices named by three reference fields: meshes, surfaces, and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen from above shows its exact colors." }, "Lines": { "type": "object", @@ -1951,18 +1964,18 @@ "string", "null" ], - "description": "The path or polygon field each row runs along; `geometry` unless set. Not on a plot." + "description": "The path or polygon field each row runs along; the field named `geometry` by default. Not\nallowed on a plot." }, "trace": { "type": [ "string", "null" ], - "description": "The sampled field each row traces across the data's axis, on a plot alone. A field not recorded\nyet draws nothing until its first frame." + "description": "The sampled field each row traces across the data's axis; plots only, where it is required.\nA field with no frames yet draws nothing until its first frame arrives." }, "interpolation": { "$ref": "#/$defs/Interpolation", - "description": "How each row runs between its samples, on a plot." + "description": "How each row runs between its samples on a plot; `linear` by default." }, "width": { "anyOf": [ @@ -1973,15 +1986,15 @@ "type": "null" } ], - "description": "Each row's width, in the space's unit unless it says its own: units in a diagram, CSS\npixels elsewhere." + "description": "Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere)\nunless given with `px` or `units`. The theme's width unless set." }, "dash": { "$ref": "#/$defs/Expr", - "description": "Whether each row is dashed." + "description": "Whether each row is dashed, with the theme's `dash` period. Default `false`." }, "flow": { "$ref": "#/$defs/Expr", - "description": "Comets flowing along each row at this speed a second, in the lengths of the theme's\nflow; negative flows backward. A speed that changes carries comets on from where they\nare." + "description": "The speed of comets flowing along each row, per second, in the unit of the theme's\n`flow` lengths. Negative speeds flow backward; 0, the default, draws no comets. When\nthe speed changes, comets continue from where they are." }, "color": { "anyOf": [ @@ -1992,11 +2005,11 @@ "type": "null" } ], - "description": "Each row's color; the theme's unless set." + "description": "Each row's color. Unset, rows take the theme's default for the mark; faces instead\nblend their corners' colors." }, "visible": { "$ref": "#/$defs/Expr", - "description": "Whether each row is drawn and picked." + "description": "Whether each row is drawn and can be picked. Default `true`." }, "priority": { "anyOf": [ @@ -2007,29 +2020,29 @@ "type": "null" } ], - "description": "Each row's priority, from its data alone: what matters more draws on top, stays drawn as\na table too large to draw whole thins to the tiles in view, and is labeled first." + "description": "Each row's priority, from its data alone (no view state). Higher-priority rows draw\non top, are the last dropped when a table too large to draw whole is thinned to the\ntiles in view, and are labeled first." }, "name": { "type": [ "string", "null" ], - "description": "Tells two marks of one table apart across scenes, and names the mark in hits." + "description": "Distinguishes two marks of the same table across scenes, and names the mark in hits." }, "pick": { "type": [ "boolean", "null" ], - "description": "Whether rows are hovered, clicked, and probed. Unless set, rows that are things are\n(vertices, edges, faces, a plot's traces) and geometry is not (areas, and lines along\npaths: coasts, borders, roads)." + "description": "Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and\nplot traces do, and geometry (areas, and lines along paths such as coasts, borders,\nand roads) does not." }, "select": { "type": "boolean", - "description": "Whether a click selects a row." + "description": "Whether clicking a row selects it. Default `true`." }, "drag": { "type": "boolean", - "description": "Whether pressing a row drags it instead of panning." + "description": "Whether pressing and moving a row drags it, reported as\n[`Event::Drag`](crate::Event), instead of panning the camera. Default `false`." }, "label": { "anyOf": [ @@ -2040,20 +2053,20 @@ "type": "null" } ], - "description": "Each row's text: a text field, or a label's options." + "description": "Each row's label: a text field, or label options." }, "legend": { "type": [ "string", "null" ], - "description": "A legend of the colors at the view's bottom right, under this title: the colormap across\nits domain, ticked; a cell for each category; or a swatch beside each name of a map's\n`categories`. The color maps through a colormap with a domain or named categories; a\nconditional's `otherwise` is read before its cases." + "description": "A legend of the mark's colors at the view's bottom right, under this title: a ticked\ncolormap across its domain, a cell for each category, or a swatch beside each name\nof a map's `categories`. `color` must map through a colormap with a fixed domain or\nnamed categories; in a conditional, `otherwise` is searched before the cases." } }, "required": [ "of" ], - "description": "Rows drawn as lines: along a path field (roads, rivers, borders) or a polygon field's rings, outlining its areas; on a plot, through a sampled field's history across the data's axis, what a monitor shows. On the earth paths bend with the globe and stay whole across the antimeridian." + "description": "Rows drawn as lines along a path field (roads, rivers, borders) or a polygon field's rings (area outlines). On a plot, each row instead traces a sampled field's history across the data's axis, as a monitor does. On the earth, paths follow the globe's curve and stay whole across the antimeridian." }, "Interpolation": { "oneOf": [ @@ -2065,12 +2078,12 @@ { "type": "string", "const": "stepBefore", - "description": "Each value holds from the sample before it: steps rise at their samples' ends." + "description": "Each value holds from the previous sample up to its own, so the line steps at the previous\nsample." }, { "type": "string", "const": "stepAfter", - "description": "Each value holds until the next sample: steps rise at the next sample." + "description": "Each value holds until the next sample, where the line steps." } ], "description": "How a line runs between its samples." @@ -2099,7 +2112,7 @@ "string", "null" ], - "description": "The polygon field each row fills: one polygon's rings, or polygons. `geometry` unless set." + "description": "The polygon field each row fills, holding one polygon's rings or several polygons; the field\nnamed `geometry` by default." }, "color": { "anyOf": [ @@ -2110,11 +2123,11 @@ "type": "null" } ], - "description": "Each row's color; the theme's unless set." + "description": "Each row's color. Unset, rows take the theme's default for the mark; faces instead\nblend their corners' colors." }, "visible": { "$ref": "#/$defs/Expr", - "description": "Whether each row is drawn and picked." + "description": "Whether each row is drawn and can be picked. Default `true`." }, "priority": { "anyOf": [ @@ -2125,29 +2138,29 @@ "type": "null" } ], - "description": "Each row's priority, from its data alone: what matters more draws on top, stays drawn as\na table too large to draw whole thins to the tiles in view, and is labeled first." + "description": "Each row's priority, from its data alone (no view state). Higher-priority rows draw\non top, are the last dropped when a table too large to draw whole is thinned to the\ntiles in view, and are labeled first." }, "name": { "type": [ "string", "null" ], - "description": "Tells two marks of one table apart across scenes, and names the mark in hits." + "description": "Distinguishes two marks of the same table across scenes, and names the mark in hits." }, "pick": { "type": [ "boolean", "null" ], - "description": "Whether rows are hovered, clicked, and probed. Unless set, rows that are things are\n(vertices, edges, faces, a plot's traces) and geometry is not (areas, and lines along\npaths: coasts, borders, roads)." + "description": "Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and\nplot traces do, and geometry (areas, and lines along paths such as coasts, borders,\nand roads) does not." }, "select": { "type": "boolean", - "description": "Whether a click selects a row." + "description": "Whether clicking a row selects it. Default `true`." }, "drag": { "type": "boolean", - "description": "Whether pressing a row drags it instead of panning." + "description": "Whether pressing and moving a row drags it, reported as\n[`Event::Drag`](crate::Event), instead of panning the camera. Default `false`." }, "label": { "anyOf": [ @@ -2158,20 +2171,20 @@ "type": "null" } ], - "description": "Each row's text: a text field, or a label's options." + "description": "Each row's label: a text field, or label options." }, "legend": { "type": [ "string", "null" ], - "description": "A legend of the colors at the view's bottom right, under this title: the colormap across\nits domain, ticked; a cell for each category; or a swatch beside each name of a map's\n`categories`. The color maps through a colormap with a domain or named categories; a\nconditional's `otherwise` is read before its cases." + "description": "A legend of the mark's colors at the view's bottom right, under this title: a ticked\ncolormap across its domain, a cell for each category, or a swatch beside each name\nof a map's `categories`. `color` must map through a colormap with a fixed domain or\nnamed categories; in a conditional, `otherwise` is searched before the cases." } }, "required": [ "of" ], - "description": "Rows drawn as the areas a polygon field holds: parcels, states, lakes, the land and water of a map. The view cuts each row's polygons into triangles once, off the frame when they are large; on the earth they bend with the globe and stay whole across the antimeridian. Outline areas with a [`Lines`](crate::Lines) mark of the same table, which draws their rings." + "description": "Rows drawn as the areas of a polygon field: parcels, states, lakes, the land and water of a map. The view triangulates each row's polygons once, in the background when they are large. On the earth, areas follow the globe's curve and stay whole across the antimeridian. To outline areas, add a [`Lines`](crate::Lines) mark of the same table, which draws their rings." }, "Layout": { "anyOf": [ @@ -2186,7 +2199,7 @@ { "oneOf": [ { - "description": "Distances on the drawing follow distances in the graph.", + "description": "Drawn distances follow graph distances.", "type": "object", "properties": { "stress": { @@ -2214,7 +2227,7 @@ ] } ], - "description": "How the view places vertex rows without positions: a diagram's blocks, sized, or the points of a\nplane or the earth, among the rows linked to them, gaps in the space's units: a name, or `{ stress\n| layered: options }`. A place, once given, holds: rows added later are placed around the rest,\nand [`View::arrange`](crate::View::arrange) lays every row out again." + "description": "How the view places vertex rows that have no position: a name, or\n`{ stress | layered: options }`. In a diagram it places blocks; on a plane or the earth it\nplaces points among the rows they link to. Gaps are in position units: diagram units, plane\nunits, or degrees on the earth.\n\nA row keeps its place once placed: rows added later are placed around the others, and\n[`View::arrange`](crate::View::arrange) lays out every row again." }, "Stress": { "type": "object", @@ -2223,11 +2236,11 @@ "gap": { "type": "number", "format": "double", - "description": "Units between adjacent vertices' centers.", + "description": "Distance between the centers of linked vertices, in position units. Default 40.", "minimum": 0 } }, - "description": "Stress majorization: each pair of vertices pulls toward its graph distance; then their boxes\npart, keeping a quarter of the gap clear." + "description": "Stress majorization: vertices move until drawn distances match graph distances, then overlapping\nboxes separate, keeping a quarter of `gap` between them." }, "Layered": { "type": "object", @@ -2235,20 +2248,20 @@ "properties": { "flow": { "$ref": "#/$defs/Direction", - "description": "Where edges point." + "description": "The direction edges point; `right` by default." }, "gap": { "type": "number", "format": "double", - "description": "Units between vertices of a rank." + "description": "Distance between vertices within a rank, in position units. Default 30." }, "rankGap": { "type": "number", "format": "double", - "description": "Units between ranks." + "description": "Distance between ranks, in position units. Default 80." } }, - "description": "Layered drawing: vertices in ranks, edges pointing along `flow`, and nets from their outputs." + "description": "Layered drawing: vertices in ranks, edges pointing along `flow`, and nets ranked from their\noutputs." }, "Direction": { "oneOf": [ @@ -2288,52 +2301,52 @@ "type": "object", "properties": { "background": { - "description": "Behind everything.", + "description": "The background color.", "type": "string", "tsType": "Color" }, "grid": { - "description": "A plane's or a diagram's grid, and the earth's graticule.", + "description": "The color of a plane's or diagram's grid, a plot's grid lines, and the earth's graticule.", "type": "string", "tsType": "Color" }, "missing": { - "description": "What a missing color reads as.", + "description": "The color a colormap gives a missing value, such as a row whose field has none.", "type": "string", "tsType": "Color" }, "dash": { - "description": "Length of a dash.", + "description": "The dash period: one dash and one gap of equal length.", "type": "number", "format": "float" }, "shading": { - "description": "How much a face turned away from the camera darkens, 0 to 1.", + "description": "How much faces darken as they turn away from the camera, from 0 to 1.", "type": "number", "format": "float" }, "text": { - "description": "Labels, axis ticks, and titles.", + "description": "How labels, axis ticks, and titles look.", "$ref": "#/$defs/TextTheme" }, "vertex": { - "description": "Vertices where their options say nothing.", + "description": "Defaults for vertices.", "$ref": "#/$defs/VertexTheme" }, "edge": { - "description": "Edges where their options say nothing, their lanes, and their arrowheads.", + "description": "Defaults for edges, and how parallel edges and arrowheads look.", "$ref": "#/$defs/EdgeTheme" }, "line": { - "description": "Lines where their options say nothing.", + "description": "Defaults for lines.", "$ref": "#/$defs/LineTheme" }, "area": { - "description": "Areas where their options say nothing.", + "description": "Defaults for areas.", "$ref": "#/$defs/AreaTheme" }, "focus": { - "description": "How hovered and selected rows glow, and rows outside a selection dim.", + "description": "How hovered and selected rows glow, and how rows outside a selection dim.", "$ref": "#/$defs/FocusTheme" }, "flow": { @@ -2363,57 +2376,57 @@ "additionalProperties": false } ], - "description": "Colors and sizes marks take where their options read nothing, and the look of text, focus,\nmotion, and each space, grouped by what they style: a base theme's name, or options over one\n(`base`, dark unless set), each group's over the base's. Lengths are in the space's unit, units in a\ndiagram and CSS pixels elsewhere, but for a text's halo and focus's glow, CSS pixels everywhere." + "description": "Default colors and sizes for marks whose options leave them unset or missing, and the look of\ntext, focus, comets, and each space, grouped by subject. Write a base theme's name (`\"dark\"` or\n`\"light\"`), or options over a base (`base`, dark by default); a group's options override only\nthe base values they set.\n\nLengths are in the space's unit (units in a diagram, CSS pixels elsewhere), except the text halo\nand the focus glow, which are always in CSS pixels." }, "TextTheme": { "type": "object", "additionalProperties": false, "properties": { "color": { - "description": "Text's color.", + "description": "The text color.", "tsType": "Color", "type": "string" }, "font": { "type": "string", - "description": "The CSS font family text is set in." + "description": "The CSS font family list." }, "size": { "type": "number", "format": "float", - "description": "Text's size." + "description": "The font size, in the space's unit." }, "halo": { "type": "number", "format": "float", - "description": "CSS pixels of background around label text." + "description": "Width of the background-colored halo around label text, in CSS pixels." } }, - "description": "Labels, axis ticks, and titles." + "description": "How labels, axis ticks, and titles look." }, "VertexTheme": { "type": "object", "additionalProperties": false, "properties": { "color": { - "description": "A vertex's color.", + "description": "The vertex color.", "tsType": "Color", "type": "string" }, "radius": { "type": "number", "format": "float", - "description": "A vertex's radius." + "description": "The vertex radius, in the space's unit." } }, - "description": "Vertices where their options say nothing." + "description": "Defaults for vertices." }, "EdgeTheme": { "type": "object", "additionalProperties": false, "properties": { "color": { - "description": "An edge's color; `None` blends the colors of each edge's two vertices, and in a diagram takes\nthe text color.", + "description": "The edge color. `None` blends the colors of each edge's two vertices; a diagram's wires and\nedges without `ends` (nets and poles) then take the text color.", "tsType": "Color", "type": [ "string", @@ -2423,66 +2436,66 @@ "width": { "type": "number", "format": "float", - "description": "An edge's width." + "description": "The edge width, in the space's unit." }, "spacing": { "type": "number", "format": "float", - "description": "In a diagram, distance between the lanes of edges joining the same two vertices, and between\nparallel orthogonal wires, which keep twice it apart." + "description": "In a diagram, the distance between edges joining the same two vertices and between parallel\northogonal wires. Wires keep twice this distance from blocks where they can." }, "bow": { "type": "number", "format": "float", - "description": "Elsewhere, how far edges joining the same two vertices bow apart: each lane by this fraction\nof the edge's length at its middle, as wide as eight lanes at most, more packing closer. A bow\nis part of the drawing, so zooming never moves it." + "description": "Outside a diagram, how far edges joining the same two vertices bow apart: each lane by this\nfraction of the edge's length at its middle. A family spreads at most eight lanes wide, and\nmore lanes pack closer. A bow is part of the drawing, so it scales with the zoom." }, "arrow": { "type": "number", "format": "float", - "description": "How far an arrowhead reaches past its edge's width." + "description": "How far an arrowhead extends beyond the edge's width: a head is this plus the width long." } }, - "description": "Edges where their options say nothing, their lanes, and their arrowheads." + "description": "Defaults for edges, and how parallel edges and arrowheads look." }, "LineTheme": { "type": "object", "additionalProperties": false, "properties": { "color": { - "description": "A line's color.", + "description": "The line color.", "tsType": "Color", "type": "string" }, "width": { "type": "number", "format": "float", - "description": "A line's width." + "description": "The line width, in the space's unit." } }, - "description": "Lines where their options say nothing." + "description": "Defaults for lines." }, "AreaTheme": { "type": "object", "additionalProperties": false, "properties": { "color": { - "description": "An area's color.", + "description": "The area color.", "tsType": "Color", "type": "string" } }, - "description": "Areas where their options say nothing." + "description": "Defaults for areas." }, "FocusTheme": { "type": "object", "additionalProperties": false, "properties": { "hover": { - "description": "The glow around hovered rows.", + "description": "The glow color around hovered rows.", "tsType": "Color", "type": "string" }, "selected": { - "description": "The glow around selected rows; `None` glows in each row's own color.", + "description": "The glow color around selected rows; `None` glows in each row's own color.", "tsType": "Color", "type": [ "string", @@ -2492,48 +2505,48 @@ "glow": { "type": "number", "format": "float", - "description": "How far focus glows reach, CSS pixels." + "description": "How far the hover and selection glows reach, in CSS pixels." }, "dim": { "type": "number", "format": "float", - "description": "The opacity of rows outside a selection." + "description": "The opacity of rows outside the selection while any row is selected, from 0 to 1." }, "grow": { "type": "number", "format": "float", - "description": "How much a hovered vertex sized in CSS pixels grows." + "description": "The scale factor for a hovered vertex sized in CSS pixels; vertices sized in units only\nglow." } }, - "description": "How hovered and selected rows glow, and rows outside a selection dim." + "description": "How hovered and selected rows glow, and how rows outside a selection dim." }, "FlowTheme": { "type": "object", "additionalProperties": false, "properties": { "color": { - "description": "A comet's color; its alpha is how strongly a comet covers its stroke.", + "description": "The comet color. Its alpha sets how strongly a comet covers its stroke; a fully transparent\ncolor turns comets off.", "tsType": "Color", "type": "string" }, "spacing": { "$ref": "#/$defs/Length", - "description": "Distance between comets." + "description": "The distance between consecutive comets." }, "length": { "$ref": "#/$defs/Length", - "description": "How far a comet's tail trails its head." + "description": "The length of a comet's tail behind its head." }, "width": { "type": "number", "format": "float", - "description": "How wide a comet's head is; never narrower than its stroke." + "description": "The width of a comet's head, in the space's unit; never narrower than its stroke." } }, - "description": "How comets flowing along edges and lines look: each a round head, brightest at its middle, trailing\na tail that thins and fades. Their spacing and length are in one unit, and each row's flow is in it\ntoo: in position units, degrees on the earth, comets are part of the drawing, zooming with the\nview and staying where they are on their lines; otherwise they keep their size and speed as it\nzooms." + "description": "How comets flowing along edges and lines look: a round head, brightest at its middle, trailing a\ntail that thins and fades.\n\n`spacing` and `length` must be in the same unit, which also measures each row's `flow` speed. In\nposition units (degrees on the earth), comets are part of the drawing: they scale with the zoom\nand stay in place along their lines. In CSS pixels, they keep their on-screen size and speed. A\nplain number is in the space's unit." }, "Length": { - "description": "A length: a number, in the space's unit, units in a diagram and CSS pixels elsewhere; `{ px }`, CSS pixels; or `{ units }`, position units.", + "description": "A length: a number in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in CSS pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the view zooms in.", "anyOf": [ { "type": "number" @@ -2543,7 +2556,7 @@ "properties": { "px": { "type": "number", - "description": "CSS pixels, however the view zooms." + "description": "CSS pixels, at any zoom." } }, "required": [ @@ -2556,7 +2569,7 @@ "properties": { "units": { "type": "number", - "description": "Position units, which scale as the view zooms." + "description": "Position units (degrees on the earth), which grow on screen as the view zooms in." } }, "required": [ @@ -2571,12 +2584,12 @@ "additionalProperties": false, "properties": { "group": { - "description": "What a group's container is filled with.", + "description": "The fill color of group containers.", "tsType": "Color", "type": "string" }, "outline": { - "description": "The border of blocks and groups, a unit wide; `None` draws none.", + "description": "The color of the one-unit border around blocks and groups; `None` draws no border.", "tsType": "Color", "type": [ "string", @@ -2586,26 +2599,26 @@ "corner": { "type": "number", "format": "float", - "description": "The radius of block and group corners; a block's marker may set its own." + "description": "The corner radius of blocks and groups; a block marker can set its own." }, "portSize": { "type": "number", "format": "float", - "description": "How wide ports are." + "description": "The width of ports." }, "bend": { "type": "number", "format": "float", - "description": "The radius of an orthogonal wire's bends." + "description": "The bend radius of orthogonal wires." }, "groupPadding": { "type": "number", "format": "float", - "description": "Distance between a group's members and its outline." + "description": "The space between a group's members and its border." }, "shadow": { "$ref": "#/$defs/ShadowTheme", - "description": "How blocks and groups drop shadows onto the sheet." + "description": "How blocks and groups cast shadows on the sheet." } }, "description": "How a diagram's blocks, groups, ports, and wires look." @@ -2615,36 +2628,36 @@ "additionalProperties": false, "properties": { "color": { - "description": "What a shadow darkens toward; its alpha is how dark it gets under the block.", + "description": "The color a shadow darkens toward; its alpha sets how dark the shadow is under the block.", "tsType": "Color", "type": "string" }, "lift": { "type": "number", "format": "float", - "description": "How far a block's or a group's shadow falls below it, and as far again it softens." + "description": "How far a shadow falls below its block or group; its edge softens over the same distance." } }, - "description": "How a diagram's blocks and groups drop shadows onto its sheet." + "description": "How a diagram's blocks and groups cast shadows on the sheet, for vertex marks with `shadow` on." }, "EarthTheme": { "type": "object", "additionalProperties": false, "properties": { "ground": { - "description": "The ground's color.", + "description": "The ground color.", "tsType": "Color", "type": "string" }, "atmosphere": { - "description": "The air glowing around the globe, where the earth has an atmosphere.", + "description": "The color of the air glowing around the globe, when the earth's `atmosphere` is on.", "tsType": "Color", "type": "string" }, "night": { "type": "number", "format": "float", - "description": "How bright the night side stays where daylight lights the earth, 0 to 1." + "description": "The brightness of the night side when the earth has `daylight`, from 0 to 1." } }, "description": "How the earth looks." @@ -2654,7 +2667,7 @@ "additionalProperties": false, "properties": { "playhead": { - "description": "The line drawn at the shown coordinate.", + "description": "The color of the line at the view's current axis coordinate.", "tsType": "Color", "type": "string" } @@ -2674,60 +2687,60 @@ "type": "object", "properties": { "pan": { - "description": "Dragging the background moves the camera.", + "description": "Whether dragging the background pans the camera. Default `true`.", "type": "boolean" }, "zoom": { - "description": "What zooms: the wheel and pinching, or the wheel only with Control or Meta held (as a\ntrackpad's pinch sends it), leaving the plain wheel to scroll the page; or nothing.", + "description": "Which input zooms the view: the wheel and pinching (`wheel`, the default); pinching, and the\nwheel only with Control or Meta held, as trackpad pinches send it, so the plain wheel\nscrolls the page (`modifier`); or nothing (`off`).", "$ref": "#/$defs/Zoom" }, "turn": { - "description": "Dragging with the secondary button, or with Shift, turns and tilts a map or plane.", + "description": "Whether dragging with the secondary button, or with Shift, rotates and tilts a map or plane.\nDefault `true`.", "type": "boolean" }, "keyboard": { - "description": "Tab steps through the rows nearest the view's middle and arrows to the nearest row their way;\nEnter opens the row stepped to and Space selects it. Otherwise arrows pan, `+` and `-` zoom,\nHome fits, and Escape clears the selection.", + "description": "Whether the keyboard controls the view. Default `true`. Tab and Shift+Tab step through the\nrows nearest the view's center, and arrow keys step to the nearest row in their direction.\nEnter opens the stepped-to row and Space selects it. With no row stepped to, arrow keys pan\n(Shift+arrows turn), `+` and `-` zoom, Home fits everything, and Escape clears the\nselection.", "type": "boolean" }, "hover": { - "description": "The row the user points at glows. It is hovered either way: events tell it, and `hovered()`\nreads it.", + "description": "Whether the hovered row glows, and a hovered vertex sized in CSS pixels grows. Default\n`true`. Rows are hovered either way: [`Event::Hovered`](crate::Event) still fires and\n`hovered()` still reads them.", "type": "boolean" }, "pickRadius": { - "description": "CSS pixels around the pointer that picks reach; touch reaches 8 more.", + "description": "How far from the pointer, in CSS pixels, a row can be and still be picked; touch adds 8.\nDefault 6.", "type": "number", "format": "float" }, "selectBox": { - "description": "Dragging with Shift selects the rows in a box, instead of turning; a Shift click still toggles\nits row.", + "description": "Whether Shift+drag selects the rows inside a box instead of turning the camera. Shift+click\nstill toggles one row. Default `false`.", "type": "boolean" }, "connect": { - "description": "Dragging from a port, from a vertex with Alt, or an edge off its end draws a connection,\ntold as [`Event::Connect`](crate::Event).", + "description": "Whether the user can draw connections, reported as [`Event::Connect`](crate::Event): by\ndragging from a port, from a vertex with Alt held, or an edge's end off its vertex. Default\n`false`.", "type": "boolean" }, "delete": { - "description": "Delete and Backspace tell [`Event::Delete`](crate::Event) with the selection.", + "description": "Whether Delete and Backspace report the selection as [`Event::Delete`](crate::Event).\nDefault `false`.", "type": "boolean" }, "cycle": { - "description": "Clicking again where nothing moved selects the next row under the point, nearest first, so\nrows drawn over one another can each be selected.", + "description": "Whether clicking again at the same point, with the camera unmoved, selects the next row\nunder it, nearest first, so overlapping rows can each be selected. Default `false`.", "type": "boolean" }, "press": { - "description": "Seconds a touch or a pen held still asks for a menu, as the secondary button does; zero never\ndoes.", + "description": "Seconds a touch or pen must hold still to request a menu ([`Event::Menu`](crate::Event)),\nas the secondary button does; 0 turns this off. Default 0.5.", "type": "number", "format": "double" }, "motion": { - "description": "How much the view moves on its own: eases, flights, coasting, orbits, label fades, comets, and\nwhat reads `elapsed()`.", + "description": "How much the view animates on its own: eases, camera flights, coasting, orbits, label fades,\ncomets, and options that read `elapsed()`. `system` (the default) follows the user's\nreduced-motion setting, `full` animates everything, and `reduced` animates only motion the\nuser drives.", "$ref": "#/$defs/Motion" } }, "additionalProperties": false } ], - "description": "How a view answers the pointer and keyboard, and how much it moves on its own: a preset, or options\nover the defaults. Each mark says whether its rows pick, select, and drag; these say what the view\nitself does." + "description": "How a view responds to the pointer and keyboard, and how much it animates on its own: a preset\n(`\"none\"` or `\"inspect\"`), or options over the defaults. Each mark controls whether its own rows\npick, select, and drag; these options control the view itself." }, "Zoom": { "oneOf": [ @@ -2739,35 +2752,35 @@ { "type": "string", "const": "modifier", - "description": "Pinching and the keys, and the wheel only with Control or Meta held, as a trackpad's pinch\nsends it: the plain wheel scrolls the page." + "description": "Pinching and the keys, and the wheel only with Control or Meta held (as trackpad pinches\nsend it), so the plain wheel scrolls the page." }, { "type": "string", "const": "off", - "description": "Nothing." + "description": "No input zooms." } ], - "description": "What zooms a view." + "description": "Which input zooms a view." }, "Motion": { "oneOf": [ { "type": "string", "const": "system", - "description": "As the platform asks: reduced where the user asked their system for less motion." + "description": "Follows the system: reduced when the user asked their system for less motion." }, { "type": "string", "const": "full", - "description": "Every motion." + "description": "All motion." }, { "type": "string", "const": "reduced", - "description": "Only what the user moves: changes apply at once, flights jump, nothing coasts or orbits,\nlabels show without fading, and comets and what reads `elapsed()` hold still." + "description": "Only motion the user drives: changes apply at once, flights jump, nothing coasts or orbits,\nlabels appear without fading, and comets and options that read `elapsed()` stay still." } ], - "description": "How much a view moves on its own." + "description": "How much a view animates on its own." }, "Data": { "type": "object", @@ -2786,7 +2799,7 @@ "required": [ "name" ], - "description": "A dataset: its name and the patch that builds it." + "description": "A dataset's JSON document: its name and the patch that builds it." }, "Change": { "oneOf": [ @@ -2814,7 +2827,7 @@ "axis" ], "additionalProperties": false, - "description": "Names the axis sampled fields are recorded over, such as `time` in `s`." + "description": "Sets the axis that sampled fields are recorded over, such as `time` in `s`." }, { "type": "object", @@ -2978,7 +2991,7 @@ "set" ], "additionalProperties": false, - "description": "Writes fields for these rows: rows with known ids change, others are added, and fields left\nout keep their values." + "description": "Writes fields for these rows. Rows with existing ids are updated, new ids add rows, and\nfields left out keep their values. A missing table is added." }, { "type": "object", @@ -3004,7 +3017,7 @@ "remove" ], "additionalProperties": false, - "description": "Removes the rows with these ids." + "description": "Removes the rows with these ids; ids that match no row are ignored." }, { "type": "object", @@ -3110,7 +3123,7 @@ "append" ], "additionalProperties": false, - "description": "Records a frame at each axis coordinate of `at`: for each field, a value per row at every\ncoordinate, frame-major; of every row, or of the rows `ids` names, the rest holding their last\nvalues." + "description": "Records a frame at each axis coordinate in `at`. Each field gives one value per row per\ncoordinate, frame-major. Covers every row, or only the rows `ids` names; other rows keep\ntheir last values." }, { "type": "object", @@ -3189,13 +3202,13 @@ "frames" ], "additionalProperties": false, - "description": "Holds a run of sampled field `field`'s entries of level `level`, numbered from `first`: at\nlevel 0 frames, each a coordinate in `at` and a value per row in `values`; at level `k` buckets\nof `8^k` frames, each its first and last frame's coordinates, and each row's least value over\neach bucket, then each row's greatest. Null is missing." + "description": "Inserts a run of entries of sampled field `field` at level `level`, numbered from `first`,\nas time tiles store them. At level 0 each entry is a frame: one coordinate in `at` and one\nvalue per row in `values`. At level `k` each entry is a bucket of `8^k` frames: its first\nand last frame's coordinates in `at`, and in `values` every bucket's least value per row,\nfollowed by every bucket's greatest. Null is missing." } ], - "description": "One change of a patch, named by the method that makes it." + "description": "One change in a patch, keyed by the name of the `Patch` method that makes it." }, "Ids": { - "description": "Row ids: numbers or texts (null leaves a reference unwired), or `{count}` naming rows 0..count.", + "description": "Row ids: numbers or texts, or `{count}` for rows 0 to count - 1. In a reference, null names no row.", "anyOf": [ { "type": "array", @@ -3259,7 +3272,7 @@ ] }, "Geometry": { - "description": "A point `[x, y]` (a third coordinate is read past), or a list of geometries one level shallower: a path, its parts, or polygons.", + "description": "A point `[x, y]` (a third coordinate is ignored), or a list of geometries one level down: a path is a list of points, parts a list of paths, and polygons a list of parts.", "anyOf": [ { "type": "array", @@ -3296,7 +3309,7 @@ "u8", "bool" ], - "description": "How a slice's bytes hold values: little endian; `bool` a byte each, nonzero true." + "description": "The value type of a slice's bytes. Numbers are little endian; `bool` is one byte each, nonzero\nmeaning true." }, "Patch": { "type": "array", @@ -3310,7 +3323,7 @@ "properties": { "of": { "type": "string", - "description": "The table read." + "description": "The table to read." }, "ids": { "anyOf": [ @@ -3321,7 +3334,7 @@ "type": "null" } ], - "description": "The rows read, in this order unless sorted; every row unless set. Ids that name no row are\npassed over." + "description": "The rows to read, in this order unless sorted; every row when unset. Ids that name no row\nare skipped." }, "filter": { "anyOf": [ @@ -3332,7 +3345,7 @@ "type": "null" } ], - "description": "Keeps the rows it is true for." + "description": "Keeps only the rows for which this is true." }, "sort": { "type": "array", @@ -3349,13 +3362,13 @@ "minItems": 2, "maxItems": 2 }, - "description": "Keys to sort by, each after those before it; rows a key reads nothing for come last, and rows\nall keys order alike keep their order." + "description": "Keys to sort by, each breaking ties left by the keys before it. Rows where a key has no\nvalue sort last; rows equal on every key keep their order." }, "offset": { "type": "integer", "format": "uint", "minimum": 0, - "description": "The kept rows skipped first." + "description": "How many of the kept rows to skip." }, "limit": { "type": [ @@ -3364,7 +3377,7 @@ ], "format": "uint", "minimum": 0, - "description": "The most rows read; a limit picks them in linear time before sorting only those." + "description": "The most rows to return. When sorting with a limit, the first `offset + limit` rows are\nselected in linear time and only those are sorted." }, "select": { "type": [ @@ -3374,7 +3387,7 @@ "additionalProperties": { "$ref": "#/$defs/Expr" }, - "description": "Fields to read by name: a field as it is, anything else as numbers. Every field unless set;\nnone set reads the ids alone." + "description": "Fields to return, by output name. A plain field returns that field as stored; any other\nexpression returns numbers. Every field when unset; an empty object returns the ids alone." }, "at": { "type": [ @@ -3382,7 +3395,7 @@ "null" ], "format": "double", - "description": "The axis coordinate sampled fields are read at; their last frame unless set." + "description": "The axis coordinate at which sampled fields are read; their last frame when unset." } }, "required": [ @@ -3395,15 +3408,15 @@ { "type": "string", "const": "ascending", - "description": "Least first." + "description": "Smallest first." }, { "type": "string", "const": "descending", - "description": "Greatest first." + "description": "Largest first." } ], - "description": "Which way a sort runs; rows a key reads nothing for come last either way." + "description": "Sort direction. Rows where the key has no value sort last in either direction." }, "EnvelopeRequest": { "type": "object", @@ -3411,11 +3424,11 @@ "properties": { "table": { "type": "string", - "description": "The table read." + "description": "The table to read." }, "field": { "type": "string", - "description": "The sampled field read." + "description": "The sampled field to read." }, "range": { "type": "array", @@ -3425,13 +3438,13 @@ }, "minItems": 2, "maxItems": 2, - "description": "The axis coordinates the spans cover, from the first to the last." + "description": "The axis range the spans cover, `[first, last]`: finite, with `first <= last`." }, "spans": { "type": "integer", "format": "uint", "minimum": 0, - "description": "How many equal spans the range is cut into." + "description": "The number of equal spans to divide the range into; at least 1." }, "ids": { "anyOf": [ @@ -3442,7 +3455,7 @@ "type": "null" } ], - "description": "The rows read, in this order; every row unless set." + "description": "The rows to read, in this order; every row when unset." } }, "required": [ @@ -3451,7 +3464,7 @@ "range", "spans" ], - "description": "Rows' least and greatest values of a sampled field over equal spans of the axis, to read back:\nfield `field` of table `table` over `spans` equal spans from `range[0]` to `range[1]`, of every\nrow or of the rows `ids` names, in its order. Plain data, like a query." + "description": "A request for each row's least and greatest value of a sampled field over equal spans of the\naxis: field `field` of table `table`, over `spans` equal spans from `range[0]` to `range[1]`,\nfor every row or for the rows `ids` names, in that order. Plain data that deserializes from\nJSON, like a query." }, "Camera": { "anyOf": [ @@ -3480,22 +3493,22 @@ "maxItems": 3 } ], - "description": "Longitude, latitude, and elevation on the earth; x, y, and elevation on a plane or a\ndiagram. In JSON, `[x, y]` looks at the ground." + "description": "Longitude, latitude, and elevation on the earth; x, y, and elevation on a plane or a\ndiagram. In JSON, `[x, y]` is shorthand for elevation 0." }, "zoom": { "type": "number", "format": "double", - "description": "On the earth the web mercator level; elsewhere, log₂ of pixels per unit." + "description": "The web mercator zoom level on the earth; elsewhere, log₂ of CSS pixels per unit." }, "pitch": { "type": "number", "format": "double", - "description": "Degrees from looking straight down, up to 75; 0 in a diagram." + "description": "Tilt in degrees from looking straight down, up to 75. Ignored in a diagram." }, "bearing": { "type": "number", "format": "double", - "description": "Degrees clockwise from north, or from up on a plane; 0 in a diagram." + "description": "Degrees clockwise from north, or from up on a plane. Ignored in a diagram." } }, "required": [ @@ -3504,7 +3517,7 @@ "pitch", "bearing" ], - "description": "On the earth, a plane, or a diagram: the point looked at and turned about, how far in, and how\ntilted and turned. A diagram is always seen from straight above, upright." + "description": "A camera over the earth, a plane, or a diagram: the point it looks at and turns about, its\nzoom, its tilt, and its rotation. A diagram is always seen from straight above, upright." }, { "type": "object", @@ -3518,7 +3531,7 @@ }, "minItems": 2, "maxItems": 2, - "description": "The axis range shown." + "description": "The x-axis range shown." }, "y": { "type": [ @@ -3531,13 +3544,13 @@ }, "minItems": 2, "maxItems": 2, - "description": "The value range shown, or none to fit what is in view." + "description": "The value range shown, or `None` to fit the values in view." } }, "required": [ "x" ], - "description": "On a plot: the axis range across, and the value range up, or none to fit the values in view." + "description": "A plot camera: the x range shown, and the value range, or `None` to fit the values in view." } ], "description": "Where a view looks." @@ -3618,7 +3631,7 @@ "description": "The box from `min` to `max`: x, y, and elevation." } ], - "description": "What a view's fit frames." + "description": "The target of a view's fit." }, "Rows": { "type": "object", @@ -3644,7 +3657,7 @@ "table", "ids" ], - "description": "Rows of one table in a dataset, by id: what a view selects, fits, and tells as sets of rows." + "description": "Rows of one table in a dataset, by id. Views use it for selections, fits, and the rows events\nreport." }, "Row": { "type": "object", @@ -3667,7 +3680,7 @@ "table", "id" ], - "description": "A row of a table in a dataset. It survives any change that keeps the names and the id." + "description": "One row, identified by dataset name, table name, and id. It stays valid across any change that\nkeeps those names and the id." }, "Budget": { "type": "object", @@ -3677,13 +3690,13 @@ "type": "integer", "format": "uint64", "minimum": 0, - "description": "Bytes the data every view of the `Gpu` draws may take on the GPU: its pages, and each view's\nrows. A view draws a table too large for its share through the tiles the camera asks for, so\nwhat it draws always fits; pages no view draws stay cached in the room left, the least\nrecently drawn going first. By default half the device's largest storage buffer. The pool of\npages grows as data needs it, and never shrinks: set the budget before data draws." + "description": "GPU memory in bytes for the data all views of the `Gpu` draw: its pages and each view's\nper-row state. A view draws a table too large for its share as a paged table, drawing only\nthe pages the camera needs, so it always fits. Pages no view draws stay cached in the\nremaining room, least recently drawn evicted first. Defaults to half the device's largest\nstorage buffer. The page pool grows as data needs it and never shrinks, so set the budget\nbefore drawing data." }, "uploadPerFrame": { "type": "integer", "format": "uint64", "minimum": 0, - "description": "Bytes uploaded per frame at most; larger data streams in over frames." + "description": "Most bytes uploaded per frame; larger data streams in over several frames. Defaults to\n32 MiB." } }, "required": [ @@ -3710,15 +3723,15 @@ "scale": { "type": "number", "format": "float", - "description": "Pixels per CSS pixel: views lay out `size / scale` CSS pixels; 1 unless set." + "description": "Pixels per CSS pixel. Views lay out `size / scale` CSS pixels. Defaults to 1." }, "panels": { "$ref": "#/$defs/Panels", - "description": "Each view's panel of the picture, `[x, y, width, height]` as fractions; the whole by default." + "description": "Each view's panel, `[x, y, width, height]` as fractions of the picture. Defaults to one view\nfilling it." }, "transparent": { "type": "boolean", - "description": "Whether alpha is kept where the themes' backgrounds would be." + "description": "Whether the picture keeps alpha where the themes' backgrounds would be." }, "capture": { "$ref": "#/$defs/Capture", @@ -3730,13 +3743,13 @@ "null" ], "format": "double", - "description": "A still's effect time in seconds, where elapsed time and flowing comets read it." + "description": "Fixed effect time in seconds for elapsed-time expressions and flowing comets. `None` follows\nthe view's clock." } }, "required": [ "size" ], - "description": "A picture of views, each drawn into its panel of `size` pixels at `scale` pixels per CSS pixel,\nonce they are complete as its capture says: plain data, like a scene.\n\n```no_run\n# async fn example(view: &mut latkit::View) -> latkit::Result<()> {\nuse latkit::Shot;\nlet image = Shot::new([3840, 2160]).scale(2.0).image(&mut [view]).await?;\nstd::fs::write(\"view.png\", image.encode(latkit::ImageFormat::Png)?).expect(\"written\");\n# Ok(()) }\n```" + "description": "A picture of views, taken once they are complete as its [`Capture`] says. Each view draws into\nits panel of a `size`-pixel picture at `scale` pixels per CSS pixel. A shot is plain data, like\na scene.\n\n```no_run\n# async fn example(view: &mut latkit::View) -> latkit::Result<()> {\nuse latkit::Shot;\nlet image = Shot::new([3840, 2160]).scale(2.0).image(&mut [view]).await?;\nstd::fs::write(\"view.png\", image.encode(latkit::ImageFormat::Png)?).expect(\"written\");\n# Ok(()) }\n```" }, "Panels": { "type": "array", @@ -3749,19 +3762,19 @@ "minItems": 4, "maxItems": 4 }, - "description": "Where each of several views draws in a target: x, y, width, and height as fractions of it, a\npanel per view, in order; a later panel draws over an earlier one. None given is one view filling\nthe target.\n\n```\nuse latkit::Panels;\nlet map_over_plot = Panels::from([[0.0, 0.0, 1.0, 0.8], [0.0, 0.8, 1.0, 0.2]]);\nassert_eq!(map_over_plot.len(), 2);\nassert_eq!(Panels::default().len(), 1);\n```" + "description": "The region of a shared target each view draws in, one panel per view, in order. A panel is\n`[x, y, width, height]` as fractions of the target, from its top left. Later panels draw over\nearlier ones. With no panels, one view fills the target.\n\n```\nuse latkit::Panels;\nlet map_over_plot = Panels::from([[0.0, 0.0, 1.0, 0.8], [0.0, 0.8, 1.0, 0.2]]);\nassert_eq!(map_over_plot.len(), 2);\nassert_eq!(Panels::default().len(), 1);\n```" }, "Capture": { "oneOf": [ { "type": "string", "const": "still", - "description": "Once every view is complete and still: transitions, flights, and label fades run on, a\nsixtieth of a second a step, until they land." + "description": "Once every view is complete and still. Transitions, camera flights, and label fades advance\n1/60 s per step until they finish." }, { "type": "string", "const": "now", - "description": "Once every view is complete, as it stands now: what moves holds where it is." + "description": "Once every view is complete, as it is now. Anything in motion is captured where it is." } ], "description": "When a picture is taken." @@ -3830,7 +3843,7 @@ "type", "detail" ], - "description": "A row was double clicked, or Enter pressed on the row the keys point at." + "description": "A row was double-clicked, or Enter was pressed on the row the keyboard points at." }, { "type": "object", @@ -3843,7 +3856,7 @@ "type": "object", "properties": { "hits": { - "description": "What lies under the point, nearest first.", + "description": "The rows under the point, nearest first.", "type": "array", "items": { "$ref": "#/outputs/Hit" @@ -3864,7 +3877,7 @@ "type", "detail" ], - "description": "The secondary button clicked, or the menu key pressed: what lies under the trigger's point,\nfor a menu of what to do with it." + "description": "A context menu was requested: by a secondary click, the menu key, or a touch or pen held\nstill. Lists the rows under the trigger's point, for a menu of actions on them." }, { "type": "object", @@ -3885,7 +3898,7 @@ "$ref": "#/outputs/Hit" }, "position": { - "description": "Where the row goes, in the space's coordinates: where it was hit, moved as the pointer moved\nsince the press, at the depth the drag started; a vertex keeps where it was grabbed. On a\ndiagram's dots when its grid snaps.", + "description": "The row's new position in the space's coordinates: the hit point moved with the pointer\nsince the press, at the depth where the drag started, so a vertex keeps its grab offset.\nSnapped to a diagram's dots when its grid snaps.", "type": "array", "items": { "type": "number", @@ -3911,7 +3924,7 @@ "type", "detail" ], - "description": "A row of a draggable mark follows the pointer: the drag starts once the pointer moves a few\npixels from where it pressed the row." + "description": "A row of a draggable mark is dragged. The drag starts once the pointer moves more than 4 CSS\npixels from where it pressed the row." }, { "type": "object", @@ -3939,7 +3952,7 @@ ] }, "edge": { - "description": "The edge whose other end was dragged, which `to` replaces.", + "description": "The edge whose end was dragged off; `to` replaces that end.", "anyOf": [ { "$ref": "#/outputs/Row" @@ -3966,7 +3979,7 @@ "type", "detail" ], - "description": "A connection was drawn with [`Controls::connect`](crate::Controls): from a port, a vertex\nwith Alt held, or an edge whose end was dragged off it." + "description": "A connection was drawn with [`Controls::connect`](crate::Controls) on: from a port, from a\nvertex with Alt held, or by dragging an edge's end off it." }, { "type": "object", @@ -4000,7 +4013,7 @@ "type", "detail" ], - "description": "Delete or Backspace with rows selected and [`Controls::delete`](crate::Controls) on." + "description": "Delete or Backspace was pressed with rows selected and [`Controls::delete`](crate::Controls)\non." }, { "type": "object", @@ -4013,7 +4026,7 @@ "type": "object", "properties": { "hit": { - "description": "The row now pointed at; `None` once it left every row.", + "description": "The row now hovered; `None` once no row is.", "anyOf": [ { "$ref": "#/outputs/Hit" @@ -4038,7 +4051,7 @@ "type", "detail" ], - "description": "What the user points at changed: [`View::hovered`](crate::View::hovered)." + "description": "The hovered row changed: [`View::hovered`](crate::View::hovered)." }, { "type": "object", @@ -4096,7 +4109,7 @@ "type", "detail" ], - "description": "The camera moved by a gesture, a fit, a flight, coasting, or an orbit:\n[`View::camera`](crate::View::camera)." + "description": "The camera moved by a gesture, fit, flight, coast, or orbit:\n[`View::camera`](crate::View::camera)." }, { "type": "object", @@ -4113,7 +4126,7 @@ "type", "detail" ], - "description": "What the last frame stood in for changed; nothing once it is complete:\n[`View::waiting`](crate::View::waiting)." + "description": "The work the last frame waited on changed; empty once frames are complete:\n[`View::waiting`](crate::View::waiting)." }, { "type": "object", @@ -4130,10 +4143,10 @@ "type", "detail" ], - "description": "Something failed, told once. A mark its data no longer fits also lists in\n[`View::failures`](crate::View::failures)." + "description": "Something failed; reported once. A mark that no longer fits its data is also listed in\n[`View::failures`](crate::View::failures)." } ], - "description": "What happened in a view, for the application to act on, as [`View::events`](crate::View::events)\ntells it." + "description": "Something that happened in a view, as [`View::events`](crate::View::events) reports it." }, "Hit": { "type": "object", @@ -4147,7 +4160,7 @@ "string", "null" ], - "description": "The name of the mark that drew it, when it has one: what tells two marks of one table apart." + "description": "The name of the mark that drew it, if named. It tells two marks of one table apart." }, "point": { "type": "array", @@ -4157,7 +4170,7 @@ }, "minItems": 2, "maxItems": 2, - "description": "The point of the row nearest the probe, CSS pixels from the top left: a vertex's or a port's\nmiddle, the nearest point of a stroke or a triangle." + "description": "The row's point nearest the probe, in CSS pixels from the top left: a vertex's or port's\nmiddle, or the nearest point of a stroke or triangle." }, "position": { "type": "array", @@ -4222,7 +4235,7 @@ "table", "id" ], - "description": "A row of a table in a dataset. It survives any change that keeps the names and the id." + "description": "One row, identified by dataset name, table name, and id. It stays valid across any change that\nkeeps those names and the id." }, "Id": { "description": "A row's id: a whole number from 0, or a text. A number above 2^53 - 1 is written as its decimal text, which names the same row.", @@ -4276,11 +4289,11 @@ }, "modifiers": { "$ref": "#/outputs/Modifiers", - "description": "The keys held." + "description": "The modifier keys held." }, "device": { "$ref": "#/outputs/Device", - "description": "What acted." + "description": "The device used." } }, "required": [ @@ -4288,7 +4301,7 @@ "modifiers", "device" ], - "description": "Where, with what keys, and with what the user acted. For keys, the point is the row they point\nat, else the pointer, else the view's middle." + "description": "Where the user acted, with which modifier keys, and with which device. For keyboard input, the\npoint is the row the keys point at, else the pointer, else the view's middle." }, "Modifiers": { "type": "object", @@ -4316,7 +4329,7 @@ "alt", "meta" ], - "description": "Keys held during an input." + "description": "Modifier keys held during an input." }, "Device": { "oneOf": [ @@ -4338,10 +4351,10 @@ { "type": "string", "const": "keyboard", - "description": "The keys." + "description": "The keyboard." } ], - "description": "What the user acts with." + "description": "The input device." }, "Phase": { "oneOf": [ @@ -4358,7 +4371,7 @@ { "type": "string", "const": "end", - "description": "The pointer let go, or the drag was cancelled." + "description": "The pointer was released, or the drag was cancelled." } ], "description": "The phase of a [`Drag`](Event::Drag)." @@ -4381,7 +4394,7 @@ "required": [ "row" ], - "description": "A vertex a connection ends at, and its port when it has the one the connection meets." + "description": "A vertex a connection ends at, and the port the connection meets, if any." }, "Rows": { "type": "object", @@ -4407,7 +4420,7 @@ "table", "ids" ], - "description": "Rows of one table in a dataset, by id: what a view selects, fits, and tells as sets of rows." + "description": "Rows of one table in a dataset, by id. Views use it for selections, fits, and the rows events\nreport." }, "Camera": { "anyOf": [ @@ -4436,22 +4449,22 @@ "maxItems": 3 } ], - "description": "Longitude, latitude, and elevation on the earth; x, y, and elevation on a plane or a\ndiagram. In JSON, `[x, y]` looks at the ground." + "description": "Longitude, latitude, and elevation on the earth; x, y, and elevation on a plane or a\ndiagram. In JSON, `[x, y]` is shorthand for elevation 0." }, "zoom": { "type": "number", "format": "double", - "description": "On the earth the web mercator level; elsewhere, log₂ of pixels per unit." + "description": "The web mercator zoom level on the earth; elsewhere, log₂ of CSS pixels per unit." }, "pitch": { "type": "number", "format": "double", - "description": "Degrees from looking straight down, up to 75; 0 in a diagram." + "description": "Tilt in degrees from looking straight down, up to 75. Ignored in a diagram." }, "bearing": { "type": "number", "format": "double", - "description": "Degrees clockwise from north, or from up on a plane; 0 in a diagram." + "description": "Degrees clockwise from north, or from up on a plane. Ignored in a diagram." } }, "required": [ @@ -4460,7 +4473,7 @@ "pitch", "bearing" ], - "description": "On the earth, a plane, or a diagram: the point looked at and turned about, how far in, and how\ntilted and turned. A diagram is always seen from straight above, upright." + "description": "A camera over the earth, a plane, or a diagram: the point it looks at and turns about, its\nzoom, its tilt, and its rotation. A diagram is always seen from straight above, upright." }, { "type": "object", @@ -4474,7 +4487,7 @@ }, "minItems": 2, "maxItems": 2, - "description": "The axis range shown." + "description": "The x-axis range shown." }, "y": { "type": [ @@ -4487,20 +4500,20 @@ }, "minItems": 2, "maxItems": 2, - "description": "The value range shown, or none to fit what is in view." + "description": "The value range shown, or `None` to fit the values in view." } }, "required": [ "x", "y" ], - "description": "On a plot: the axis range across, and the value range up, or none to fit the values in view." + "description": "A plot camera: the x range shown, and the value range, or `None` to fit the values in view." } ], "description": "Where a view looks." }, "Waiting": { - "description": "Work under way a frame stands in for: pipelines the scene draws with compiling, until which the view draws nothing new (`compiling`); pages uploading or tables not yet built from them (`data`); layouts, routes, lanes, shapes, tiles, or pyramids (`derived`); a label placement or texts not yet set (`labels`); a fit due or being measured (`fit`); segment counts, or a transition beginning (`marks`). Empty once a frame is complete.", + "description": "What a frame still waits on: pipelines the scene draws with, which compile before the view draws anything new (`compiling`); pages uploading, or tables not yet built from them (`data`); layouts, routes, lanes, shapes, tiles, or pyramids (`derived`); label placement, or text not yet set (`labels`); a fit due or being measured (`fit`); segment counts, or a transition starting (`marks`). Empty once the frame is complete.", "type": "array", "items": { "enum": [ @@ -4559,19 +4572,19 @@ "type": "integer", "format": "uint64", "minimum": 0, - "description": "Rows every mark draws: every row of a table drawn whole, the rows of the tiles in view of one\ndrawn through tiles." + "description": "Rows every mark draws: every row of a table drawn whole, and the rows of the tiles in view\nof a paged table." }, "draws": { "type": "integer", "format": "uint64", "minimum": 0, - "description": "Draw calls the last frame recorded, labels with the marks." + "description": "Draw calls the last frame recorded, label draws included." }, "instances": { "type": "integer", "format": "uint64", "minimum": 0, - "description": "Instances the last frame's draws drew: points, segments, triangles, glyphs; at most, for\nsegments the GPU culls." + "description": "Instances the last frame's draws drew: points, segments, triangles, and glyphs. An upper\nbound for segments, which the GPU may cull." }, "tiles": { "type": "integer", @@ -4593,7 +4606,7 @@ "gpuMs": { "type": "number", "format": "float", - "description": "Milliseconds the GPU takes on a frame: the median of the last frames it measured; 0 where the\ndevice cannot measure." + "description": "Milliseconds the GPU takes per frame: the median of recently measured frames; 0 if the\ndevice cannot measure." } }, "required": [ @@ -4607,7 +4620,7 @@ "cpuMs", "gpuMs" ], - "description": "What a view does each frame." + "description": "Counters describing a view's frames." }, "Work": { "type": "object", @@ -4622,13 +4635,13 @@ "type": "integer", "format": "uint64", "minimum": 0, - "description": "Data pages, or pieces of them, uploaded: what a change uploads counts its pages here, and its\nbytes in `uploaded`." + "description": "Data pages, or pieces of pages, uploaded. A change's uploads count pages here and bytes in\n`uploaded`." }, "written": { "type": "integer", "format": "uint64", "minimum": 0, - "description": "Bytes written besides pages: uniforms, programs, label runs, the selection, textures." + "description": "Bytes written other than pages: uniforms, programs, label runs, the selection, and textures." }, "evaluated": { "type": "integer", @@ -4640,49 +4653,49 @@ "type": "integer", "format": "uint64", "minimum": 0, - "description": "Rows of edges and faces whose ends the GPU found where their vertices are held: rows that\nchanged, and every row of a page of them naming a vertex now held elsewhere." + "description": "Edge and face rows whose endpoints the GPU resolved to their vertices' resident rows: rows\nthat changed, plus every row of a page that references a vertex now resident elsewhere." }, "cleared": { "type": "integer", "format": "uint64", "minimum": 0, - "description": "Bytes of pages of words per row the GPU cleared: those read before anything writes them, and\nthe normals faces sum afresh." + "description": "Bytes of per-row word pages the GPU cleared: pages read before anything writes them, and the\nnormals that faces sum from scratch." }, "packed": { "type": "integer", "format": "uint64", "minimum": 0, - "description": "Rows packed into pages of the order a table drawn in part draws in: each page once, the first\ntime a view draws it, and again only where a write changed its rows." + "description": "Rows packed into pages of a paged table's draw order. Each page is packed once, the first\ntime a view draws it, and again only when a write changes its rows." }, "readbacks": { "type": "integer", "format": "uint64", "minimum": 0, - "description": "Reads back from the GPU: probes, queries, fits, label placements, segment counts, pictures;\nnot the frame timer's, which only some devices have." + "description": "Readbacks from the GPU: probes, queries, fits, label placements, segment counts, and\npictures. Frame timer readbacks, which only some devices support, are not counted." }, "jobs": { "type": "integer", "format": "uint64", "minimum": 0, - "description": "Jobs handed out: layouts, routes, lanes, triangulations, pyramids, tiles; to the host's\nworkers, or done at once without a host." + "description": "Jobs started: layouts, routes, lanes, triangulations, pyramids, and tiles. They run on the\nhost's workers, or immediately without a host." }, "texts": { "type": "integer", "format": "uint64", "minimum": 0, - "description": "Label texts the host was asked to set." + "description": "Label texts the host was asked to rasterize." }, "pipelines": { "type": "integer", "format": "uint64", "minimum": 0, - "description": "Pipelines compiled: in a browser, each as a view or a shot asks for it ahead, off the GPU's\nthread; natively, where it is first made." + "description": "Pipelines compiled. In a browser, each compiles in the background when a view or shot\nrequests it ahead of use. Natively, each compiles when first created." }, "late": { "type": "integer", "format": "uint64", "minimum": 0, - "description": "Pipelines made without being asked for ahead, each compiled where it is first used: in a\nbrowser on the GPU's thread, holding frames back. None, unless a scene uses what it did not\nask for." + "description": "Pipelines not requested ahead of use, compiled when first used. In a browser they compile\non the GPU's thread and delay frames. Zero unless a scene uses a pipeline it did not\nrequest." } }, "required": [ @@ -4699,7 +4712,7 @@ "pipelines", "late" ], - "description": "What a [`Gpu`](crate::Gpu) did since it was made; subtract two for what happened between." + "description": "Counts of the work a [`Gpu`](crate::Gpu) has done since it was created. Subtract two\nsnapshots to get the work between them." }, "Description": { "type": "object", @@ -4718,7 +4731,7 @@ }, "minItems": 2, "maxItems": 2, - "description": "The axis's name and unit." + "description": "The axis name and unit, if set." }, "span": { "type": [ @@ -4731,14 +4744,14 @@ }, "minItems": 2, "maxItems": 2, - "description": "The first and last coordinates its sampled fields hold." + "description": "The first and last axis coordinates held by any sampled field; `None` when none holds any." }, "tables": { "type": "array", "items": { "$ref": "#/outputs/TableDescription" }, - "description": "Its tables, by name." + "description": "Its tables, sorted by name." } }, "required": [ @@ -4747,7 +4760,7 @@ "span", "tables" ], - "description": "What a dataset holds: its tables, their rows and fields, and the axis its sampled fields are\nrecorded over and span." + "description": "A summary of a dataset: its tables with their rows and fields, the axis sampled fields are\nrecorded over, and the part of the axis they cover." }, "TableDescription": { "type": "object", @@ -4760,14 +4773,14 @@ "type": "integer", "format": "uint", "minimum": 0, - "description": "How many rows it has." + "description": "The number of rows." }, "fields": { "type": "array", "items": { "$ref": "#/outputs/FieldDescription" }, - "description": "Its fields, by name." + "description": "Its fields, sorted by name." } }, "required": [ @@ -4775,7 +4788,7 @@ "rows", "fields" ], - "description": "A table described: its rows, and its fields by name." + "description": "A summary of one table: its row count and its fields, sorted by name." }, "FieldDescription": { "type": "object", @@ -4786,14 +4799,14 @@ }, "kind": { "$ref": "#/outputs/Kind", - "description": "What it holds." + "description": "The kind of values it holds." }, "to": { "type": [ "string", "null" ], - "description": "The table a reference names." + "description": "For a reference, the target table." }, "span": { "type": [ @@ -4806,14 +4819,14 @@ }, "minItems": 2, "maxItems": 2, - "description": "The first and last coordinates a sampled field holds." + "description": "For a sampled field, the first and last axis coordinates it holds." } }, "required": [ "name", "kind" ], - "description": "A field described: what it holds, the table a reference names, the coordinates samples span." + "description": "A summary of one field: its kind, plus its target table or axis span where those apply." }, "Kind": { "oneOf": [ @@ -4860,10 +4873,10 @@ { "type": "string", "const": "sampled", - "description": "Numbers sampled over the data's axis." + "description": "Numbers sampled over the dataset's axis." } ], - "description": "What a field holds." + "description": "The kind of values a field holds." }, "Budget": { "type": "object", @@ -4873,13 +4886,13 @@ "type": "integer", "format": "uint64", "minimum": 0, - "description": "Bytes the data every view of the `Gpu` draws may take on the GPU: its pages, and each view's\nrows. A view draws a table too large for its share through the tiles the camera asks for, so\nwhat it draws always fits; pages no view draws stay cached in the room left, the least\nrecently drawn going first. By default half the device's largest storage buffer. The pool of\npages grows as data needs it, and never shrinks: set the budget before data draws." + "description": "GPU memory in bytes for the data all views of the `Gpu` draw: its pages and each view's\nper-row state. A view draws a table too large for its share as a paged table, drawing only\nthe pages the camera needs, so it always fits. Pages no view draws stay cached in the\nremaining room, least recently drawn evicted first. Defaults to half the device's largest\nstorage buffer. The page pool grows as data needs it and never shrinks, so set the budget\nbefore drawing data." }, "uploadPerFrame": { "type": "integer", "format": "uint64", "minimum": 0, - "description": "Bytes uploaded per frame at most; larger data streams in over frames." + "description": "Most bytes uploaded per frame; larger data streams in over several frames. Defaults to\n32 MiB." } }, "required": [ diff --git a/npm/schema.mjs b/npm/schema.mjs index 3c75edf2..6877d09c 100644 --- a/npm/schema.mjs +++ b/npm/schema.mjs @@ -1,17 +1,18 @@ /** - * npm's types, written from `schema.json`: what latkit reads (`$defs`) becomes `inputs.ts`, and what - * it writes (`outputs`) becomes `outputs.ts`. Each definition reachable from the roots becomes a type, - * documented as its form is; an output an input already declares is that input's type. A schema's - * `tsType` names the hand-written type that stands for it, such as `ExprInput`. + * Generates npm's types from `schema.json`. The types latkit reads (`$defs`) become `inputs.ts`, + * and the types it writes (`outputs`) become `outputs.ts`. Each definition reachable from the roots + * becomes a type, documented with its schema description. An output that an input already declares + * uses that input's type. A schema's `tsType` names a hand-written type to use instead, such as + * `ExprInput`. */ import fs from 'node:fs'; import path from 'node:path'; import prettier from 'prettier'; -/** The types npm takes as plain data; everything they are built of comes with them. */ +/** Root types npm takes as plain data. Every type they are built from is generated too. */ const INPUTS = ['Scene', 'Query', 'EnvelopeRequest', 'Camera', 'Fit', 'Budget', 'ColormapKind']; -/** The types npm reads back. */ +/** Root types npm reads back from latkit. */ const OUTPUTS = [ 'ViewEvent', 'Stats', @@ -24,29 +25,30 @@ const OUTPUTS = [ 'Failure', ]; -/** The hand-written types a `tsType` names, and the module that declares them. */ +/** Hand-written types that a `tsType` names, keyed by the module that declares them. */ const IMPORTS = { './scene.js': ['Color', 'ExprInput', 'PaintInput'] }; -/** `inputs.ts` and `outputs.ts` of `schema.json` at `file`, formatted as the repository's sources are. */ +/** Returns the sources of `inputs.ts` and `outputs.ts` for the schema at `file`, formatted with the + * repository's Prettier config. */ export async function typescript(file) { const schema = JSON.parse(fs.readFileSync(file, 'utf8')); const inputs = declare(schema.$defs, '#/$defs/', INPUTS, new Set()); const outputs = declare(schema.outputs, '#/outputs/', OUTPUTS, new Set(inputs.names)); const config = await prettier.resolveConfig(path.join(path.dirname(file), 'src/inputs.ts')); const format = (source) => prettier.format(source, { ...config, parser: 'typescript' }); - const header = (from) => - `/** Written by \`node npm/build.mjs schema\` from \`npm/schema.json\`, ${from}: do not edit. */`; + const header = (types) => + `/** ${types}, written by \`node npm/build.mjs schema\` from \`npm/schema.json\`. Do not edit. */`; const imports = (modules) => Object.entries(modules) .filter(([, names]) => names.length) .map(([module, names]) => `import type { ${names.sort().join(', ')} } from '${module}';`); return { inputs: await format( - [header('what latkit reads'), '', ...imports(IMPORTS), '', inputs.source, ''].join('\n'), + [header('The types latkit reads'), '', ...imports(IMPORTS), '', inputs.source, ''].join('\n'), ), outputs: await format( [ - header('what latkit writes'), + header('The types latkit writes'), '', ...imports({ './inputs.js': outputs.borrowed }), '', @@ -58,8 +60,9 @@ export async function typescript(file) { } /** - * The declarations of the definitions `defs` that `roots` reach, refs to them starting `prefix`; - * `declared` names those another module declares, which these borrow. + * Declares the definitions in `defs` that `roots` reach, where refs to them start with `prefix`. + * Names in `declared` are declared by another module. They are not declared again, and those used + * are returned as `borrowed` for import. */ function declare(defs, prefix, roots, declared) { const output = prefix === '#/outputs/'; @@ -80,7 +83,7 @@ function declare(defs, prefix, roots, declared) { const type = (schema) => { if (schema === true || schema === undefined || Object.keys(schema).length === 0) return 'unknown'; - // An optional value that `null` also sets takes the null its schema allows. + // A `tsType` whose schema also allows null accepts null too. if (schema.tsType) return [schema.type].flat().includes('null') ? `${schema.tsType} | null` : schema.tsType; const name = ref(schema); @@ -110,8 +113,8 @@ function declare(defs, prefix, roots, declared) { return 'unknown'; } }; - // A member's documentation moves onto the one key of a tagged variant; elsewhere TypeScript would - // show it nowhere. + // A union member's description moves onto the single key of a tagged variant, because TypeScript + // shows no documentation on union members. const members = (schemas) => { const parts = schemas.map((s) => { const keys = Object.keys(s.properties ?? {}); @@ -140,7 +143,8 @@ function declare(defs, prefix, roots, declared) { } return /[|&]/.test(item) ? `(${item})[]` : `${item}[]`; }; - // What latkit writes leaves an absent value out rather than writing null. + // latkit omits absent values from its output rather than writing null, so optional output + // fields drop their null. const absent = (schema) => { if (Array.isArray(schema.type)) return { ...schema, type: schema.type.filter((t) => t !== 'null') }; @@ -181,14 +185,15 @@ function declare(defs, prefix, roots, declared) { return { names, source, borrowed: [...reached].filter((n) => declared.has(n) && used(n)) }; } -/** Rust's names npm calls otherwise. */ +/** Rust names that npm spells differently. */ const RENAMED = { 'View::events': 'view.on' }; /** - * A code span of Rust's documentation as npm names what it names: an event as the event npm tells - * (`Event::Connect` a `'connect'` event), a member as one of an instance where it starts lowercase - * (`View::complete` `view.complete`, `Controls::connect` `controls.connect`) and of its type else, a - * call as what it calls (`hovered()` `hovered`). + * Rewrites a code span from Rust's documentation with npm's names. An event becomes the event npm + * emits (`Event::Connect` becomes a `'connect'` event). A lowercase member becomes an instance + * member in camelCase (`View::complete` becomes `view.complete`), and any other member stays on its + * type (`Space::Diagram` becomes `Space.Diagram`). A call drops its parentheses (`hovered()` + * becomes `hovered`). */ function named(code) { const camel = (name) => name.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); @@ -204,9 +209,9 @@ function named(code) { } /** - * Documentation as the forms write it, wrapped anew: paragraphs kept, lines at most 96 wide, Rust's - * links to its own items as plain code, its names as npm's, its `None` as the `null` JSON writes, - * and its examples, which are Rust, left out. + * Converts a schema description into a doc comment. It keeps paragraphs, rewraps lines to at most + * 96 columns, turns Rust's intra-doc links into plain code spans, renames Rust names to npm's, + * writes `None` as `null` (as JSON does), and drops code blocks, which are Rust examples. */ function doc(text) { const lines = []; @@ -219,7 +224,7 @@ function doc(text) { for (const paragraph of plain.split('\n\n').filter((p) => p.trim())) { if (lines.length) lines.push(''); let line = ''; - // A code span stays on one line, as TSDoc reads them. + // Each code span stays on one line, as TSDoc requires. const words = (paragraph.match(/(?:`[^`]*`|[^\s`])+/g) ?? []).map((w) => w.replace(/\s+/g, ' '), ); diff --git a/npm/src/canvas.ts b/npm/src/canvas.ts index d929d818..3fa29a63 100644 --- a/npm/src/canvas.ts +++ b/npm/src/canvas.ts @@ -1,33 +1,38 @@ -/** Canvases: views shown in a `` or an `OffscreenCanvas`, as a layout places them. */ +/** Canvases that show views in a `` element or an `OffscreenCanvas`. */ import type * as bindings from '../pkg/latkit.js'; import { hold, inner, release } from './internal.js'; import { Shot, type ShotOptions } from './shot.js'; import { handleOf, type View } from './view.js'; -/** `[x, y, width, height]` as fractions of a canvas or a picture. */ +/** A region `[x, y, width, height]` in fractions of a canvas or picture, from its top left. */ export type Rect = readonly [x: number, y: number, width: number, height: number]; -/** A view's place in a canvas or a picture: where it draws, all of it unless `at` says. */ +/** A view and the region of a canvas or picture it draws in. */ export interface Panel { view: View; + /** Where the view draws; the whole canvas or picture by default. */ at?: Rect; } -/** How a canvas shows its views. */ +/** Options for `Gpu.canvas()`. */ export interface CanvasOptions { - /** The views shown and where; a later panel draws over an earlier one. */ + /** The views shown and their regions; later panels draw over earlier ones. None by default. */ layout?: readonly Panel[]; - /** Pixels a CSS pixel of an `OffscreenCanvas`; an element follows its device. */ + /** + * Pixels per CSS pixel of an `OffscreenCanvas`, 1 by default; it must be positive. A `` + * element uses the device pixel ratio instead. + */ pixelRatio?: number; } -/** The whole of a canvas or a picture. */ +/** The rect covering a whole canvas or picture. */ const WHOLE: Rect = [0, 0, 1, 1]; /** - * Views shown in a `` element, which sizes itself to its CSS box and passes each view the - * input landing on its panel, or an `OffscreenCanvas` in a worker, which keeps the size it is given. + * Shows views in a `` element or an `OffscreenCanvas`, each in its layout panel; created by + * `Gpu.canvas()`. A frame redraws only the views that need it, or every view after a resize or a + * layout change. */ export class Canvas { #target: HTMLCanvasElement | OffscreenCanvas; @@ -57,7 +62,7 @@ export class Canvas { this.#released = released; } - /** @internal A canvas of `device` in `target`, which calls `released` once destroyed. */ + /** @internal Creates a canvas of `device` in `target`; `released` runs once it is destroyed. */ static made( device: bindings.Gpu, target: HTMLCanvasElement | OffscreenCanvas, @@ -67,14 +72,15 @@ export class Canvas { return new Canvas(device, target, options, released); } - /** What it draws into. */ + /** The `` element or `OffscreenCanvas` it draws into. */ get element(): HTMLCanvasElement | OffscreenCanvas { return this.#target; } /** - * The views shown and where, a later panel over an earlier one; setting it shows those in place of - * the ones shown. A view shows in one panel of one canvas at a time. + * The views shown and their regions; later panels draw over earlier ones. Setting it replaces the + * shown views. A view can show in only one panel of one canvas at a time. Setting a layout that + * breaks this, or that has a panel covering no pixels, throws an `invalid` error. */ get layout(): readonly Panel[] { return this.#layout; @@ -89,8 +95,8 @@ export class Canvas { } /** - * A picture of exactly what the canvas shows: its views as its layout places them, at its size in - * CSS pixels times `scale`. + * Returns a {@link Shot} of exactly what the canvas shows: its views in its layout, at the + * canvas's CSS size times `options.scale` (1 by default) in pixels. */ shot(options: ShotOptions = {}): Shot { const scale = options.scale ?? 1; @@ -106,7 +112,10 @@ export class Canvas { return new Shot(size, { ...options, layout: this.#layout }); } - /** Stops drawing; the views remain, and the element keeps its last frame. */ + /** + * Stops drawing and frees the canvas. The views stay usable, and the element keeps its last + * frame. Calling it again does nothing. + */ destroy(): void { const freed = release(this); if (!freed) return; @@ -123,7 +132,7 @@ function canvas(owner: Canvas): bindings.Canvas { return inner(owner, 'Canvas'); } -/** @internal Where each panel of `layout` draws, four fractions after another. */ +/** @internal Flattens each panel's rect into four floats; the whole target where `at` is unset. */ export function placed(layout: readonly Panel[]): Float32Array { return Float32Array.from(layout.flatMap(({ at }) => at ?? WHOLE)); } diff --git a/npm/src/colormap.ts b/npm/src/colormap.ts index b1b4156f..b40d5ee2 100644 --- a/npm/src/colormap.ts +++ b/npm/src/colormap.ts @@ -1,11 +1,13 @@ -/** Colormaps in the page: the colors the GPU draws, as CSS, for legends. */ +/** Colormaps as CSS gradients, for legends drawn in HTML. */ import type { Colormap } from './inputs.js'; import { wasm } from './internal.js'; /** - * A CSS gradient through `map`, a catalog name or colors of your own, toward `direction`: the colors - * the GPU draws, a band each for a categorical map's categories. + * Returns a CSS `linear-gradient(...)` with the colors the GPU draws for `map`, a catalog name or a + * custom colormap. `direction` is the gradient's CSS direction, `'to right'` by default. A + * categorical map gives one solid band per category. Needs `await load()` first, and throws an + * `invalid` error for an invalid `map`. */ export function colormapCss(map: Colormap, direction = 'to right'): string { return wasm().colormapCss(JSON.stringify(map), direction); diff --git a/npm/src/data.ts b/npm/src/data.ts index f381ddbb..e4620a90 100644 --- a/npm/src/data.ts +++ b/npm/src/data.ts @@ -1,43 +1,53 @@ -/** Datasets: a `Data` changing in place, a `Snapshot` of one version, and `Tiles` fetched in pieces. */ +/** Datasets: `Data` changes in place, a `Snapshot` is one version, and `Tiles` load in pieces. */ import type * as bindings from '../pkg/latkit.js'; import type { EnvelopeRequest, Id, Query } from './inputs.js'; -import { hold, inner, release, wasm, waited, wrap, type Wait } from './internal.js'; +import { bytesOf, hold, inner, release, wasm, waited, wrap, type Wait } from './internal.js'; import { load } from './load.js'; import type { Description, Value } from './outputs.js'; import { framed, type Patch } from './patch.js'; /** - * The rows a query keeps, in the forms a patch writes: numbers as a `Float64Array` (missing NaN), - * other values as arrays (missing `null`), a reference as its target's ids. + * The rows a query returns, as columns in the forms a patch takes: numbers as a `Float64Array` + * (missing values NaN), other values as arrays (missing `null`), and a reference as the ids of the + * rows it references. */ export interface QueryResult { table: string; ids: Id[]; fields: Record; - /** The rows the query kept before paging. */ + /** How many rows matched the query before `offset` and `limit` applied. */ total: number; } -/** Rows' least and greatest values over spans of the axis: `at` holds one more edge than spans. */ +/** Each row's minimum and maximum of a sampled field over equal spans of the axis. */ export interface Envelope { + /** The span boundaries on the axis: one more than the number of spans. */ at: Float64Array; + /** + * Each row's minimum in each span: `spans` values per row, row after row. NaN where a span has + * no frame. + */ least: Float32Array; + /** Each row's maximum in each span, laid out like `least`. */ greatest: Float32Array; } -/** What `Data.read` reads: latkit's bytes, GeoJSON, or a dataset's JSON document. */ +/** + * What {@link Data.read} accepts: bytes, a `Blob`, a `Response`, a byte stream, a URL to fetch as + * a string, or a parsed JSON value. The content is latkit's bytes, GeoJSON, or a dataset document. + */ export type DataSource = Uint8Array | ArrayBuffer | Blob | Response | ReadableStream | string | object; -/** What reads a version of a dataset, as the bindings' `Data` and `Snapshot` both do. */ +/** A bindings object that reads one version of a dataset: a `Data` or a `Snapshot`. */ type Reads = bindings.Data | bindings.Snapshot; /** - * A dataset that changes in place: named tables of columns, changed only by patches, every change - * of one or none. A view given it draws its latest version, from the frame after a patch applies. A - * snapshot keeps one version, sharing every page with it, so earlier versions kept, as an undo - * history does, cost only the pages they hold apart. + * A dataset that changes in place: named tables of columns, changed only by patches. A patch + * applies completely or not at all. Views given a `Data` draw its latest version, from the frame + * after each patch. A {@link Snapshot} keeps one version and shares its pages with the dataset, so + * keeping earlier versions, as an undo history does, costs only the pages that differ. * * ```ts * const grid = new Data('grid').apply(new Patch().replace('Bus', [1, 2], { kv: [230, 115] })); @@ -46,19 +56,25 @@ type Reads = bindings.Data | bindings.Snapshot; * ``` */ export class Data { - /** An empty dataset named `name`. */ + /** Creates an empty dataset named `name`. Throws an `unloaded` error before `load()` finishes. */ constructor(name: string) { hold(this, new (wasm().Data)(name)); } /** - * The dataset `source` holds, as its first bytes say: latkit's bytes; GeoJSON, gzip-compressed or - * not, read in a worker, its points, line strings, and polygons rows of tables `points`, `lines`, - * and `polygons`, each with its geometry as field `geometry` and its properties as fields; or its - * JSON document, `{"name": ..., "patch": [...]}`, its patch read as `Patch.read` reads one. `source` - * is bytes, a `Blob`, a `Response`, a stream, a URL to fetch, or a document as a JSON value. `name` - * names it, as GeoJSON needs; `base` is what a document's slices of files are relative to, by - * default the URL it was fetched from. + * Reads a dataset from `source`, calling `load()` first. Its first bytes decide the format: + * + * - latkit's bytes, as `toBytes()` writes them. + * - GeoJSON, plain or gzip-compressed, read in a worker. Points, line strings, and polygons + * become rows of tables `points`, `lines`, and `polygons`, with the geometry in field + * `geometry` and each property as a field. GeoJSON requires `options.name`. + * - A dataset document, `{"name": ..., "patch": [...]}`, whose patch is read as + * {@link Patch.read} reads one. + * + * `options.name` names the dataset, replacing the name in latkit's bytes or a document. + * `options.base` is the URL that a document's file slices are relative to; it defaults to the URL + * the source was fetched from. Rejects with an `invalid` error for bytes that are not a valid + * dataset, and with an `io` error when a fetch fails or answers an error status. */ static async read( source: DataSource, @@ -74,49 +90,61 @@ export class Data { return reads(this).name; } - /** Applies every change of `patch`, or none when one is invalid. */ + /** + * Applies every change in `patch` (a {@link Patch} or its bytes), or none of them when one is + * invalid, and returns this dataset. Views draw the new version from their next frame. Throws an + * `invalid` error whose `path` names the failing change, such as `patch[1].Bus.kv`, or when the + * patch names another dataset. + */ apply(patch: Patch | Uint8Array): this { inner(this, 'Data').apply(patch instanceof Uint8Array ? patch : framed(patch)); return this; } - /** The version it holds now, kept as it is while patches change it: O(1). */ + /** The current version as a {@link Snapshot}, which later patches leave unchanged. O(1). */ snapshot(): Snapshot { return wrap(Snapshot, inner(this, 'Data').snapshot()); } - /** Makes `snapshot`, a version of this dataset, the one it holds: what an undo does. */ + /** + * Makes `snapshot`, a version of this dataset, the current version, as an undo does. Views draw + * it from their next frame. Throws an `invalid` error for a snapshot of a dataset with another + * name. + */ restore(snapshot: Snapshot): this { inner(this, 'Data').restore(inner(snapshot, 'Snapshot')); return this; } - /** What it holds now: see {@link Snapshot.describe}. */ + /** Describes the current version; see {@link Snapshot.describe}. */ describe(): Description { return describe(reads(this)); } - /** A value it holds now: see {@link Snapshot.get}. */ + /** Reads one value of the current version; see {@link Snapshot.get}. */ get(table: string, id: Id, field: string, options: { at?: number } = {}): Value { return get(reads(this), table, id, field, options); } - /** The rows a query keeps of what it holds now: see {@link Snapshot.query}. */ + /** Queries the current version; see {@link Snapshot.query}. */ query(query: Query): QueryResult { return reads(this).query(JSON.stringify(query)) as QueryResult; } - /** Rows' extremes over spans of what it holds now: see {@link Snapshot.envelope}. */ + /** Per-row minimums and maximums of the current version; see {@link Snapshot.envelope}. */ envelope(request: EnvelopeRequest): Envelope { return envelope(reads(this), request); } - /** What it holds now as latkit's bytes: the patch that builds it. */ + /** The current version as latkit's bytes: a patch that builds it from an empty dataset. */ toBytes(): Uint8Array { return reads(this).toBytes(); } - /** Frees this handle; views drawing the dataset and its snapshots keep it. */ + /** + * Frees this handle. Views drawing the dataset keep drawing it, and its snapshots stay usable. + * Other methods throw afterwards. + */ destroy(): void { release(this)?.free(); } @@ -126,7 +154,10 @@ export class Data { } } -/** One version of a dataset, as it was when taken: O(1) to keep. A view given one draws it as it is. */ +/** + * One immutable version of a dataset, taken in O(1) by {@link Data.snapshot}. A view given a + * snapshot draws that version unchanged. + */ export class Snapshot { private constructor() {} @@ -134,38 +165,51 @@ export class Snapshot { return reads(this).name; } - /** What the dataset holds: tables, rows, fields and their kinds, the axis and its span. */ + /** + * Describes the dataset: its tables and their row counts, each field and its kind, and the axis + * and the span its sampled fields cover. + */ describe(): Description { return describe(reads(this)); } - /** The value of `field` for the row with `id` in `table`, at axis coordinate `at` when given. */ + /** + * Reads `field` of the row with `id` in `table`. A sampled field reads its last frame at or + * before `options.at`, or its last frame when `at` is omitted. A reference reads the referenced + * row's id. Returns `null` when the table, row, field, or value is missing. + */ get(table: string, id: Id, field: string, options: { at?: number } = {}): Value { return get(reads(this), table, id, field, options); } /** - * The rows of a table `query` keeps, filtered, sorted, and paged, sampled fields read at its - * coordinate, on the CPU through the expressions marks draw with. + * Runs `query` on the CPU: reads a table's rows, filtered, sorted, and paged, through the same + * expressions marks use. Sampled fields are read at `query.at`. Throws an `invalid` error for an + * invalid query. */ query(query: Query): QueryResult { return reads(this).query(JSON.stringify(query)) as QueryResult; } /** - * Each row's least and greatest value of a sampled field over equal spans of a range, of every row - * or of the rows named: row after row, NaN where a span holds no frame. It reads the coarsest - * levels of detail that fit, so a span costs a few reads however many frames it covers. + * Computes each row's minimum and maximum of a sampled field over equal spans of an axis range, + * for every row or the rows `request.ids` lists. Each span reads the coarsest stored levels of + * detail that fit in it, so it costs a few reads regardless of how many frames it covers. Throws + * an `invalid` error for an unknown table or row, or a field that is not sampled, and a `limit` + * error when rows times spans exceeds 2^26. */ envelope(request: EnvelopeRequest): Envelope { return envelope(reads(this), request); } - /** The version as latkit's bytes: the patch that builds it. */ + /** This version as latkit's bytes: a patch that builds it from an empty dataset. */ toBytes(): Uint8Array { return reads(this).toBytes(); } + /** + * Frees this handle. Views given the snapshot keep drawing it; other methods throw afterwards. + */ destroy(): void { release(this)?.free(); } @@ -176,31 +220,31 @@ export class Snapshot { } /** - * A dataset fetched in pieces as views ask for them: plain data naming where they lie. Each view - * draws the pieces its camera and plot window ask for as a dataset of its own, and every view of a - * GPU shares what is fetched. The source's first bytes say what it holds. + * A dataset fetched in pieces as views need them. A `Tiles` is plain data: a dataset name and a + * URL. Each view draws the pieces its camera and plot window need as a dataset of its own, and all + * views of a GPU share the fetched pieces. The first bytes at `url` decide the kind of tiles: * - * - Time tiles, a `frames.json` manifest as `latkit tile` writes it: a dataset whose sampled - * fields' histories are fetched in spans. A line tracing a field fetches the detail its plot - * window draws, a mark showing one the frames about the view's coordinate, and a span still - * loading draws the coarser span above it. - * - Vector tiles: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` template, fetched as - * the camera asks. Each layer is a table of its features of the kind marks draw it as (areas - * polygons, lines lines, vertices points), or named `layer:polygons`, `layer:lines`, or - * `layer:points`. + * - Time tiles: a `frames.json` manifest as `latkit tile` writes it, for a dataset whose sampled + * fields' histories are fetched in spans. A line tracing a field fetches the level of detail its + * plot window shows; other marks fetch the frames around the view's `at`. A span still loading + * shows the coarser span that contains it. + * - Vector tiles: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` URL template, fetched + * as the camera needs them. Each layer becomes a table of features of the kind its marks draw: + * polygons for areas, lines for lines, points for vertices. A table named `layer:polygons`, + * `layer:lines`, or `layer:points` holds that kind regardless of the marks. */ export class Tiles { - /** The tiles at `url`, drawn as dataset `name`. */ + /** Tiles at `url`, drawn as the dataset named `name`. */ constructor( readonly name: string, readonly url: string, ) {} } -/** What a view draws a dataset from, by its name. */ +/** A dataset a view draws; views match it to the scene's marks by the dataset's name. */ export type Source = Data | Snapshot | Tiles; -/** @internal `source` as the bindings take it, each time anew. */ +/** @internal Converts `source` to a new bindings `Source` on every call. */ export function sourceOf(source: Source): bindings.Source { if (source instanceof Tiles) return wasm().Source.tiles(source.name, source.url); return source instanceof Data @@ -208,7 +252,7 @@ export function sourceOf(source: Source): bindings.Source { : inner(source, 'Snapshot').source(); } -/** @internal A snapshot around the bindings' `snapshot`. */ +/** @internal Wraps a bindings snapshot in a public {@link Snapshot}. */ export function snapshotOf(snapshot: bindings.Snapshot): Snapshot { return wrap(Snapshot, snapshot); } @@ -228,29 +272,3 @@ function get(reads: Reads, table: string, id: Id, field: string, { at }: { at?: function envelope(reads: Reads, request: EnvelopeRequest): Envelope { return reads.envelope(JSON.stringify(request)) as Envelope; } - -/** The bytes `source` holds, and the URL they came from. */ -async function bytesOf( - source: DataSource, - wait: Wait, -): Promise<{ bytes: Uint8Array; base: string }> { - if (typeof source === 'string') { - const response = await waited( - fetch(source, { signal: wait.signal }), - wait, - `fetching ${source}`, - ); - return bytesOf(response, wait); - } - if (source instanceof Uint8Array) return { bytes: source, base: '' }; - if (source instanceof ArrayBuffer) return { bytes: new Uint8Array(source), base: '' }; - if (source instanceof Response) { - if (!source.ok) throw new Error(`${source.url}: ${source.status} ${source.statusText}`); - return { bytes: new Uint8Array(await source.arrayBuffer()), base: source.url }; - } - if (source instanceof Blob) - return { bytes: new Uint8Array(await source.arrayBuffer()), base: '' }; - if (source instanceof ReadableStream) - return { bytes: new Uint8Array(await new Response(source).arrayBuffer()), base: '' }; - return { bytes: new TextEncoder().encode(JSON.stringify(source)), base: '' }; -} diff --git a/npm/src/encoder.ts b/npm/src/encoder.ts index 5b295d72..7273853d 100644 --- a/npm/src/encoder.ts +++ b/npm/src/encoder.ts @@ -1,12 +1,15 @@ /** - * The browser's H.264 encoder, as latkit's video asks for one: NV12 pictures (BT.709, limited range) - * in, Annex B units out, each `[time, key, bytes]` in order. WebCodecs encodes, on the GPU's video - * engine where the device has one; the level is the least that holds the size and frame rate. + * The H.264 encoder latkit's videos use in a browser, built on WebCodecs. It takes NV12 pictures + * (BT.709, limited range) and returns Annex B units in order, each `[time, key, bytes]`, with + * `time` in 90 kHz ticks. It prefers hardware encoding and uses the lowest H.264 level that fits + * the size and frame rate. */ +import { thrown, type LatkitError } from './internal.js'; + type Unit = [time: number, key: boolean, bytes: Uint8Array]; -/** H.264 levels by the macroblocks a second and a frame they hold, as their codec byte. */ +/** H.264 levels as `[codec byte, max macroblocks per second, max macroblocks per frame]`. */ const LEVELS: readonly [level: number, perSecond: number, perFrame: number][] = [ [0x1f, 108_000, 3_600], [0x28, 245_760, 8_192], @@ -19,10 +22,16 @@ const LEVELS: readonly [level: number, perSecond: number, perFrame: number][] = [0x3e, 16_711_680, 139_264], ]; -/** Constrained Baseline forbids B pictures; the MP4 writer uses equal decode and presentation times. */ +/** + * Constrained Baseline only. It has no B-frames, so decode and presentation times are equal, as + * the MP4 writer assumes. + */ const PROFILES = ['42e0']; -/** The codec configurations to try for `width`×`height` at `fps`, best first. */ +/** + * The encoder configurations to try for `width`×`height` at `fps`, hardware first. The level is + * the lowest that fits, or 6.2 when none does. + */ function configurations( width: number, height: number, @@ -49,13 +58,18 @@ function configurations( ); } -/** An encoder for `width`×`height` pictures at `fps` and `bitrate` bits a second. */ +/** + * Creates an encoder for `width`×`height` pictures at `fps` frames per second and `bitrate` bits + * per second. Throws an `unsupported` error when the browser has no `VideoEncoder`. Its calls + * reject with an `unsupported` error when the browser encodes no H.264 at that size and rate, and + * with an `io` error when encoding fails. + */ export function encoder(width: number, height: number, fps: number, bitrate: number) { if (typeof VideoEncoder !== 'function') - throw new Error('this browser has no WebCodecs VideoEncoder'); + throw thrown({ code: 'unsupported', message: 'this browser has no WebCodecs VideoEncoder' }); const units: Unit[] = []; const times: { timestamp: number; time: number }[] = []; - let failure: unknown; + let failure: LatkitError | undefined; const waiters = new Set<() => void>(); const wake = () => { for (const resolve of waiters) resolve(); @@ -65,7 +79,7 @@ export function encoder(width: number, height: number, fps: number, bitrate: num output(chunk) { const expected = times.shift(); if (!expected || chunk.timestamp !== expected.timestamp) { - failure = new Error('the encoder reordered or omitted a frame'); + failure = thrown({ code: 'io', message: 'the encoder reordered or omitted a frame' }); close(); return; } @@ -74,7 +88,9 @@ export function encoder(width: number, height: number, fps: number, bitrate: num units.push([expected.time, chunk.type === 'key', bytes]); }, error(error) { - failure = error; + failure = Object.assign(thrown({ code: 'io', message: 'the video encoder failed' }), { + cause: error, + }); wake(); }, }); @@ -89,13 +105,14 @@ export function encoder(width: number, height: number, fps: number, bitrate: num for (const config of configurations(width, height, fps, bitrate)) { if ((await VideoEncoder.isConfigSupported(config)).supported) return video.configure(config); } - throw new Error(`this browser encodes no H.264 at ${width}×${height}, ${fps} frames a second`); + throw thrown({ + code: 'unsupported', + message: `this browser cannot encode H.264 at ${width}×${height}, ${fps} frames a second`, + }); }; const failed = () => { - if (failure || video.state === 'closed') - throw failure instanceof Error - ? failure - : new Error('the video encoder failed', { cause: failure }); + if (failure) throw failure; + if (video.state === 'closed') throw thrown({ code: 'io', message: 'the video encoder closed' }); }; return { async encode(nv12: Uint8Array, time: number, key: boolean): Promise { @@ -115,7 +132,7 @@ export function encoder(width: number, height: number, fps: number, bitrate: num } finally { frame.close(); } - // The encoder sets the pace: a frame waits while two others are still being encoded. + // Backpressure: wait while more than two frames are queued in the encoder. while (video.encodeQueueSize > 2) { await new Promise((resolve) => waiters.add(resolve)); failed(); @@ -129,7 +146,7 @@ export function encoder(width: number, height: number, fps: number, bitrate: num failed(); await video.flush(); failed(); - if (times.length) throw new Error('the encoder omitted a frame'); + if (times.length) throw thrown({ code: 'io', message: 'the encoder omitted a frame' }); return units.splice(0); } finally { close(); diff --git a/npm/src/gpu.ts b/npm/src/gpu.ts index 2dd75c3e..f5be8a8f 100644 --- a/npm/src/gpu.ts +++ b/npm/src/gpu.ts @@ -1,4 +1,4 @@ -/** The GPU: the device views draw with, the memory they share, and the frames they draw in. */ +/** The GPU device: it creates views and canvases, owns their shared memory, and runs frames. */ import type * as bindings from '../pkg/latkit.js'; import { Canvas, type CanvasOptions } from './canvas.js'; @@ -8,7 +8,10 @@ import { firstDevice, load } from './load.js'; import type { Work } from './outputs.js'; import { View, type ViewOptions } from './view.js'; -/** The GPU device views draw with. Views of one GPU draw in one animation frame, sharing its memory. */ +/** + * A WebGPU device that views draw with. The views and canvases of one `Gpu` share its memory and + * draw together in each animation frame, so linked views always show the same moment. + */ export class Gpu { #views = new Set(); #canvases = new Set(); @@ -18,15 +21,19 @@ export class Gpu { hold(this, device); } - /** Asks for a WebGPU device, loading latkit first: the first call takes the one made as it loaded. */ + /** + * Requests a WebGPU device, calling `load()` first. The first call reuses the device that + * `load()` requested. Rejects with an `unsupported` error when the browser has no WebGPU device. + */ static async create(): Promise { await load(); return new Gpu(await (firstDevice() ?? wasm().Gpu.create())); } /** - * A view of `scene`, drawing the datasets its `sources` give. A {@link Canvas} shows it, and a - * {@link Shot} draws it with or without one. + * Creates a view that draws `scene` from the datasets in `options.sources`. Show it in a + * {@link Canvas}, or draw it into pictures with a `Shot`, with or without a canvas. Throws an + * `invalid` error for an invalid scene. */ view(scene: Scene, options: ViewOptions = {}): View { const view = View.made(inner(this, 'Gpu'), scene, () => this.#views.delete(view)); @@ -36,9 +43,11 @@ export class Gpu { } /** - * A canvas showing views as `layout` places them in `target`: a `` element, which sizes - * itself to its CSS box and passes each view the input landing on it, or an `OffscreenCanvas` in a - * worker, which keeps the size it is given at `pixelRatio` pixels a CSS pixel. + * Creates a canvas that shows views in `target`, placed by `options.layout`. A `` element + * sizes itself to its CSS box at the device pixel ratio, and passes pointer, wheel, and key input + * to the views in its panels. An `OffscreenCanvas`, as in a worker, keeps the size it is given at + * `options.pixelRatio` pixels per CSS pixel, and takes no input. Throws an `invalid` error for an + * invalid layout, such as a view already shown in another panel. */ canvas(target: HTMLCanvasElement | OffscreenCanvas, options: CanvasOptions = {}): Canvas { const canvas = Canvas.made(inner(this, 'Gpu'), target, options, () => @@ -49,8 +58,9 @@ export class Gpu { } /** - * Calls `listener` with the frame's timestamp every animation frame, before the views draw, so - * what it changes shows in that frame. Returns a function that stops it. + * Calls `listener` every animation frame with the frame's timestamp in milliseconds, before the + * views draw, so changes it makes show in that frame. While any listener is registered, a frame + * runs every animation frame. Returns a function that removes the listener. */ on(type: 'frame', listener: (now: number) => void): () => void { const handle = inner(this, 'Gpu').onFrame(listener); @@ -61,12 +71,15 @@ export class Gpu { return stop; } - /** Whether the device was lost; a new `Gpu` is needed. */ + /** Whether the device was lost. Drawing then does nothing; create a new `Gpu`. */ get lost(): boolean { return inner(this, 'Gpu').lost; } - /** How much GPU memory data takes. */ + /** + * The GPU memory budget for data, shared by every view of this GPU; see {@link Budget}. Defaults + * to half the device's largest storage buffer, and 32 MiB uploaded per frame. + */ get budget(): Budget { return JSON.parse(inner(this, 'Gpu').budget) as Budget; } @@ -75,48 +88,57 @@ export class Gpu { inner(this, 'Gpu').setBudget(JSON.stringify(budget)); } - /** What this GPU did since it was created, counted exactly; see {@link Work}. */ + /** + * Exact counts of the work this GPU has done since it was created; see {@link Work}. The counts + * repeat for the same budget, device limits, and frames, so tests and benchmarks can assert them. + */ get work(): Work { return JSON.parse(inner(this, 'Gpu').work) as Work; } - /** Bytes on the GPU every view of it draws from: data, and what each view keeps per row. */ + /** + * Bytes of GPU memory the views of this GPU draw from: data pages, and each view's per-row data. + */ get resident(): number { return inner(this, 'Gpu').resident; } - /** Bytes waiting to upload, for every view of this GPU. */ + /** Bytes waiting to upload to the GPU, across every view of this GPU. */ get uploading(): number { return inner(this, 'Gpu').uploading; } /** - * Draws one frame now at `now`, milliseconds on the clock animation frames give: the frame - * listeners, then every canvas that needs a frame. From the first step no animation frames are - * asked for: a harness, as tests and benchmarks are, steps every frame itself, so a run draws the - * same frames however fast the device is. + * Draws one frame immediately at time `now`, in milliseconds on the `requestAnimationFrame` + * clock: it runs the frame listeners, then draws every canvas that needs a frame. After the first + * call, this GPU stops requesting animation frames. A test or benchmark that steps every frame + * itself then draws the same frames on any device. */ step(now: number): void { inner(this, 'Gpu').step(now); } /** - * Resolves once the GPU finished the work submitted to it, every readback landed, and every - * pipeline a view or shot asked for compiled: a frame stepped then draws all of it. + * Resolves once the GPU has finished all submitted work, every readback has arrived, and every + * pipeline a view or shot requested has compiled. A frame stepped after that uses all of it. */ async done(wait: Wait = {}): Promise { - await waited(inner(this, 'Gpu').done(), wait, 'the GPU'); + await waited(inner(this, 'Gpu').done(), wait, 'waiting for the GPU'); } /** - * Resolves when work a view waits on lands after this call (a readback, a compile, a job, a text, - * a fetch), or a view asks for a frame. + * Resolves when, after this call, work a view is waiting on arrives (a readback, a pipeline + * compile, a job, a label text, or a fetch), or a view requests a frame. A harness that steps + * frames awaits it to know when to step again. */ async landed(wait: Wait = {}): Promise { - await waited(inner(this, 'Gpu').landed(), wait, 'work landing'); + await waited(inner(this, 'Gpu').landed(), wait, 'waiting for work'); } - /** Releases this GPU's canvases, views, and frame listeners. Other GPUs and data stay usable. */ + /** + * Destroys this GPU's canvases, views, and frame listeners, then frees the device. Other GPUs and + * datasets stay usable. Calling it again does nothing; other methods throw afterwards. + */ destroy(): void { const device = release(this); if (!device) return; diff --git a/npm/src/index.ts b/npm/src/index.ts index 0384c74b..a8590edb 100644 --- a/npm/src/index.ts +++ b/npm/src/index.ts @@ -1,8 +1,11 @@ /// /** - * latkit: columnar data drawn with WebGPU. Scenes, expressions, and patches are plain data, usable - * anywhere; `await load()` loads the WebAssembly that datasets, views, and pictures are made with. - * Its handles dispose with `using`, whatever library a project compiles against. + * latkit: columnar data drawn with WebGPU. + * + * Scenes, expressions, and patches are plain data and need no WebAssembly. Datasets, views, + * pictures, and `colormapCss` need it, so `await load()` first. Every class with `destroy()` also + * works with `using`; the reference above makes `Symbol.dispose` typecheck whatever a project's + * `lib` setting. */ export type * from './inputs.js'; export type * from './outputs.js'; diff --git a/npm/src/inputs.ts b/npm/src/inputs.ts index c48766f9..46a3e20c 100644 --- a/npm/src/inputs.ts +++ b/npm/src/inputs.ts @@ -1,52 +1,61 @@ -/** Written by `node npm/build.mjs schema` from `npm/schema.json`, what latkit reads: do not edit. */ +/** The types latkit reads, written by `node npm/build.mjs schema` from `npm/schema.json`. Do not edit. */ import type { Color, ExprInput, PaintInput } from './scene.js'; -/** Areas where their options say nothing. */ +/** Defaults for areas. */ export interface AreaTheme { - /** An area's color. */ + /** The area color. */ color?: Color; } /** - * Rows drawn as the areas a polygon field holds: parcels, states, lakes, the land and water of a - * map. The view cuts each row's polygons into triangles once, off the frame when they are large; - * on the earth they bend with the globe and stay whole across the antimeridian. Outline areas with - * a `Lines` mark of the same table, which draws their rings. + * Rows drawn as the areas of a polygon field: parcels, states, lakes, the land and water of a map. + * The view triangulates each row's polygons once, in the background when they are large. On the + * earth, areas follow the globe's curve and stay whole across the antimeridian. To outline areas, + * add a `Lines` mark of the same table, which draws their rings. */ export interface Areas { /** The dataset and table drawn. */ of: [data: string, table: string]; - /** The polygon field each row fills: one polygon's rings, or polygons. `geometry` unless set. */ + /** + * The polygon field each row fills, holding one polygon's rings or several polygons; the field + * named `geometry` by default. + */ geometry?: string | null; - /** Each row's color; the theme's unless set. */ + /** + * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their + * corners' colors. + */ color?: PaintInput | null; - /** Whether each row is drawn and picked. */ + /** Whether each row is drawn and can be picked. Default `true`. */ visible?: ExprInput; /** - * Each row's priority, from its data alone: what matters more draws on top, stays drawn as a table - * too large to draw whole thins to the tiles in view, and is labeled first. + * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are + * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are + * labeled first. */ priority?: ExprInput | null; - /** Tells two marks of one table apart across scenes, and names the mark in hits. */ + /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ name?: string | null; /** - * Whether rows are hovered, clicked, and probed. Unless set, rows that are things are (vertices, - * edges, faces, a plot's traces) and geometry is not (areas, and lines along paths: coasts, - * borders, roads). + * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces + * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. */ pick?: boolean | null; - /** Whether a click selects a row. */ + /** Whether clicking a row selects it. Default `true`. */ select?: boolean; - /** Whether pressing a row drags it instead of panning. */ + /** + * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the + * camera. Default `false`. + */ drag?: boolean; - /** Each row's text: a text field, or a label's options. */ + /** Each row's label: a text field, or label options. */ label?: Label | null; /** - * A legend of the colors at the view's bottom right, under this title: the colormap across its - * domain, ticked; a cell for each category; or a swatch beside each name of a map's `categories`. - * The color maps through a colormap with a domain or named categories; a conditional's `otherwise` - * is read before its cases. + * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap + * across its domain, a cell for each category, or a swatch beside each name of a map's + * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a + * conditional, `otherwise` is searched before the cases. */ legend?: string | null; } @@ -55,25 +64,29 @@ export interface Areas { export interface Axis { /** The axis title. */ label?: string | null; - /** Whether grid lines cross the plot at the ticks. */ + /** Whether grid lines cross the plot at the ticks. Default `true`. */ grid?: boolean; - /** Whether the axis is hidden. */ + /** Whether the axis is hidden. Default `false`. */ hidden?: boolean; /** - * Ticks at these values, each a value or `[value, label]`, their values written unless labeled; - * round values unless set. + * Tick positions, each a value or `[value, label]`; a tick without a label shows its value. Unset, + * ticks fall at round values. */ ticks?: Tick[] | null; - /** How tick values are written. */ + /** How tick values are formatted; `auto` by default. */ format?: Format; /** - * Decimals of fixed values, or of the significand of scientific and engineering ones; as many as - * the ticks' step needs unless set. + * Decimal places of fixed values, or of the significand in scientific and engineering formats, at + * most 15. Unset, as many as the tick step needs. */ precision?: number | null; } -/** An operation of two values. */ +/** + * An operation on two values: arithmetic, `min` and `max`, comparisons (1 for true, 0 for false), + * and logic. `rem` is a floored remainder, which takes the sign of the divisor. `and` and `or` + * treat values above 0.5 as true. + */ export type Binary = | 'add' | 'sub' @@ -92,12 +105,13 @@ export type Binary = | 'and' | 'or'; +/** A two-argument operation, such as `{ "op": "add", "args": ["a", 1] }`. */ export interface BinaryNode { op: Binary; args: [ExprInput, ExprInput]; } -/** RGBA texels, straight alpha, sRGB, rows top to bottom. */ +/** An image of RGBA texels: straight alpha, sRGB, rows top to bottom. */ export interface Bitmap { /** Width and height in texels. */ size: [number, number]; @@ -108,14 +122,14 @@ export interface Bitmap { /** How much GPU memory latkit uses. */ export interface Budget { /** - * Bytes the data every view of the `Gpu` draws may take on the GPU: its pages, and each view's - * rows. A view draws a table too large for its share through the tiles the camera asks for, so - * what it draws always fits; pages no view draws stay cached in the room left, the least recently - * drawn going first. By default half the device's largest storage buffer. The pool of pages grows - * as data needs it, and never shrinks: set the budget before data draws. + * GPU memory in bytes for the data all views of the `Gpu` draw: its pages and each view's per-row + * state. A view draws a table too large for its share as a paged table, drawing only the pages the + * camera needs, so it always fits. Pages no view draws stay cached in the remaining room, least + * recently drawn evicted first. Defaults to half the device's largest storage buffer. The page + * pool grows as data needs it and never shrinks, so set the budget before drawing data. */ bytes: number; - /** Bytes uploaded per frame at most; larger data streams in over frames. */ + /** Most bytes uploaded per frame; larger data streams in over several frames. Defaults to 32 MiB. */ uploadPerFrame: number; } @@ -124,23 +138,24 @@ export type Camera = | { /** * Longitude, latitude, and elevation on the earth; x, y, and elevation on a plane or a diagram. In - * JSON, `[x, y]` looks at the ground. + * JSON, `[x, y]` is shorthand for elevation 0. */ center: [number, number] | [number, number, number]; - /** On the earth the web mercator level; elsewhere, log₂ of pixels per unit. */ + /** The web mercator zoom level on the earth; elsewhere, log₂ of CSS pixels per unit. */ zoom: number; - /** Degrees from looking straight down, up to 75; 0 in a diagram. */ + /** Tilt in degrees from looking straight down, up to 75. Ignored in a diagram. */ pitch: number; - /** Degrees clockwise from north, or from up on a plane; 0 in a diagram. */ + /** Degrees clockwise from north, or from up on a plane. Ignored in a diagram. */ bearing: number; } | { - /** The axis range shown. */ + /** The x-axis range shown. */ x: [number, number]; - /** The value range shown, or none to fit what is in view. */ + /** The value range shown, or `null` to fit the values in view. */ y?: [number, number] | null; }; +/** A constant color. */ export interface ColorNode { /** A CSS color, such as `#ffb000` or `rebeccapurple`. */ color: Color; @@ -149,7 +164,10 @@ export interface ColorNode { /** Colors for a range of values: a catalog name, or your own colors. */ export type Colormap = ColormapName | CustomColormap; -/** How a colormap spans values. */ +/** + * How a colormap maps values: `sequential` and `diverging` maps clamp, `cyclic` maps wrap, and + * `categorical` maps give each integer its own color. + */ export type ColormapKind = 'sequential' | 'diverging' | 'cyclic' | 'categorical'; /** A colormap of the catalog. */ @@ -201,117 +219,135 @@ export type ColormapName = | 'tableau10' | 'turbo'; -/** `value` through `colormap`: from its `domain`, else from the extent of the values shown. */ +/** + * `value` mapped through `colormap` over `domain`, or without a `domain`, over the extent of the + * values shown. A categorical map ignores the domain and bins values by their integer part. + */ export interface ColormapNode { colormap: Colormap; value: ExprInput; domain?: [low: number, high: number] | null; } -/** Whether a text field holds the text, ignoring case: in a mark, a field of its own table. */ +/** + * Whether a text field contains the text, ignoring case. In a mark, the field must belong to the + * mark's own table. + */ export interface ContainsNode { contains: [field: ExprInput, text: string]; } /** - * How a view answers the pointer and keyboard, and how much it moves on its own: a preset, or - * options over the defaults. Each mark says whether its rows pick, select, and drag; these say - * what the view itself does. + * How a view responds to the pointer and keyboard, and how much it animates on its own: a preset + * (`"none"` or `"inspect"`), or options over the defaults. Each mark controls whether its own rows + * pick, select, and drag; these options control the view itself. */ export type Controls = | 'none' | 'inspect' | { - /** Dragging the background moves the camera. */ + /** Whether dragging the background pans the camera. Default `true`. */ pan?: boolean; /** - * What zooms: the wheel and pinching, or the wheel only with Control or Meta held (as a trackpad's - * pinch sends it), leaving the plain wheel to scroll the page; or nothing. + * Which input zooms the view: the wheel and pinching (`wheel`, the default); pinching, and the + * wheel only with Control or Meta held, as trackpad pinches send it, so the plain wheel scrolls + * the page (`modifier`); or nothing (`off`). */ zoom?: Zoom; - /** Dragging with the secondary button, or with Shift, turns and tilts a map or plane. */ + /** + * Whether dragging with the secondary button, or with Shift, rotates and tilts a map or plane. + * Default `true`. + */ turn?: boolean; /** - * Tab steps through the rows nearest the view's middle and arrows to the nearest row their way; - * Enter opens the row stepped to and Space selects it. Otherwise arrows pan, `+` and `-` zoom, - * Home fits, and Escape clears the selection. + * Whether the keyboard controls the view. Default `true`. Tab and Shift+Tab step through the rows + * nearest the view's center, and arrow keys step to the nearest row in their direction. Enter + * opens the stepped-to row and Space selects it. With no row stepped to, arrow keys pan + * (Shift+arrows turn), `+` and `-` zoom, Home fits everything, and Escape clears the selection. */ keyboard?: boolean; /** - * The row the user points at glows. It is hovered either way: events tell it, and `hovered` reads - * it. + * Whether the hovered row glows, and a hovered vertex sized in CSS pixels grows. Default `true`. + * Rows are hovered either way: a `'hovered'` event still fires and `hovered` still reads them. */ hover?: boolean; - /** CSS pixels around the pointer that picks reach; touch reaches 8 more. */ + /** + * How far from the pointer, in CSS pixels, a row can be and still be picked; touch adds 8. Default + * 6. + */ pickRadius?: number; /** - * Dragging with Shift selects the rows in a box, instead of turning; a Shift click still toggles - * its row. + * Whether Shift+drag selects the rows inside a box instead of turning the camera. Shift+click + * still toggles one row. Default `false`. */ selectBox?: boolean; /** - * Dragging from a port, from a vertex with Alt, or an edge off its end draws a connection, told as - * a `'connect'` event. + * Whether the user can draw connections, reported as a `'connect'` event: by dragging from a port, + * from a vertex with Alt held, or an edge's end off its vertex. Default `false`. */ connect?: boolean; - /** Delete and Backspace tell a `'delete'` event with the selection. */ + /** Whether Delete and Backspace report the selection as a `'delete'` event. Default `false`. */ delete?: boolean; /** - * Clicking again where nothing moved selects the next row under the point, nearest first, so rows - * drawn over one another can each be selected. + * Whether clicking again at the same point, with the camera unmoved, selects the next row under + * it, nearest first, so overlapping rows can each be selected. Default `false`. */ cycle?: boolean; /** - * Seconds a touch or a pen held still asks for a menu, as the secondary button does; zero never - * does. + * Seconds a touch or pen must hold still to request a menu (a `'menu'` event), as the secondary + * button does; 0 turns this off. Default 0.5. */ press?: number; /** - * How much the view moves on its own: eases, flights, coasting, orbits, label fades, comets, and - * what reads `elapsed`. + * How much the view animates on its own: eases, camera flights, coasting, orbits, label fades, + * comets, and options that read `elapsed`. `system` (the default) follows the user's + * reduced-motion setting, `full` animates everything, and `reduced` animates only motion the user + * drives. */ motion?: Motion; }; -/** Your own colormap: its kind, its colors, and for a categorical map its categories' names. */ +/** A custom colormap: its kind, its colors, and for a categorical map, its category names. */ export interface CustomColormap { kind: ColormapKind; /** - * Colors blended in OKLab, each at its `[place, color]` from 0 to 1 or evenly between its - * neighbors, as in a CSS gradient; a categorical map's, one per category. + * The colors, blended in OKLab. Each is a color, spaced evenly between its neighbors as in a CSS + * gradient, or a `[place, color]` pair with a place from 0 to 1, in ascending order. A categorical + * map takes one color per category, without places, at most 256. */ colors: (Color | [number, Color])[]; - /** The categories' names, in order, for a legend: a categorical map's alone. */ + /** The category names in category order, shown in legends. Categorical maps only. */ categories?: string[]; } /** - * A sheet for diagrams, flat and seen from straight above: the camera pans and zooms. Positions - * are units with y down, and lengths are units, a unit a CSS pixel at zoom 0 (sizes, padding, - * ports, strokes, text, and the theme's), but for what says `px`, a halo, focus's glow, and the - * pick radius. Zooming scales one fixed picture. Only a diagram has ports, groups, nets, and - * orthogonal routes; it lays out blocks. + * A flat sheet for diagrams, seen from straight above; the camera pans and zooms but does not + * turn. Positions are in units with y down. Lengths are in units too, a unit being a CSS pixel at + * zoom 0: sizes, padding, ports, strokes, text, and the theme's lengths. Sizes given with `px`, + * the text halo, the focus glow, and the pick radius stay in CSS pixels. Zooming scales the whole + * picture. Only a diagram has ports, groups, nets, and orthogonal routes, and the scene's layout + * places its blocks. */ export interface Diagram { - /** The grid under the marks. */ + /** The grid drawn under the marks; dots 20 units apart by default. */ grid?: Grid; } /** How a diagram's blocks, groups, ports, and wires look. */ export interface DiagramTheme { - /** What a group's container is filled with. */ + /** The fill color of group containers. */ group?: Color; - /** The border of blocks and groups, a unit wide; `null` draws none. */ + /** The color of the one-unit border around blocks and groups; `null` draws no border. */ outline?: Color | null; - /** The radius of block and group corners; a block's marker may set its own. */ + /** The corner radius of blocks and groups; a block marker can set its own. */ corner?: number; - /** How wide ports are. */ + /** The width of ports. */ portSize?: number; - /** The radius of an orthogonal wire's bends. */ + /** The bend radius of orthogonal wires. */ bend?: number; - /** Distance between a group's members and its outline. */ + /** The space between a group's members and its border. */ groupPadding?: number; - /** How blocks and groups drop shadows onto the sheet. */ + /** How blocks and groups cast shadows on the sheet. */ shadow?: ShadowTheme; } @@ -319,150 +355,167 @@ export interface DiagramTheme { export type Direction = 'right' | 'left' | 'down' | 'up'; /** - * The earth: positions are longitude and latitude in degrees, its ground in the theme's earth + * The earth: positions are longitude and latitude in degrees. The ground takes the theme's `earth` * colors. */ export interface Earth { - /** A flat map or a globe; a flat map unless set. */ + /** A flat web mercator map (`mercator`, the default) or a globe (`globe`). */ projection?: Projection; - /** Whether lines of longitude and latitude cross the ground, in the theme's grid color. */ + /** + * Whether lines of longitude and latitude cross the ground, in the theme's `grid` color. Default + * `true`. + */ graticule?: boolean; /** - * Lit by the sun as it stands this many seconds after the Unix epoch, plus the view's axis - * coordinate when it shows one: night falls on the ground and what stands on it, down to the - * theme's night. Lit evenly unless set. + * Lights the earth by the sun's position this many seconds after the Unix epoch, plus the view's + * axis coordinate when it has one. The night side darkens the ground and everything on it, down to + * the theme's `earth.night` brightness. Unset, the earth is lit evenly. */ daylight?: number | null; - /** Whether air glows around the globe, in the theme's atmosphere color, brighter where it is day. */ + /** + * Whether air glows around the globe, in the theme's `earth.atmosphere` color, brighter on the day + * side. Default `false`. + */ atmosphere?: boolean; } /** How the earth looks. */ export interface EarthTheme { - /** The ground's color. */ + /** The ground color. */ ground?: Color; - /** The air glowing around the globe, where the earth has an atmosphere. */ + /** The color of the air glowing around the globe, when the earth's `atmosphere` is on. */ atmosphere?: Color; - /** How bright the night side stays where daylight lights the earth, 0 to 1. */ + /** The brightness of the night side when the earth has `daylight`, from 0 to 1. */ night?: number; } -/** Edges where their options say nothing, their lanes, and their arrowheads. */ +/** Defaults for edges, and how parallel edges and arrowheads look. */ export interface EdgeTheme { /** - * An edge's color; `null` blends the colors of each edge's two vertices, and in a diagram takes - * the text color. + * The edge color. `null` blends the colors of each edge's two vertices; a diagram's wires and + * edges without `ends` (nets and poles) then take the text color. */ color?: Color | null; - /** An edge's width. */ + /** The edge width, in the space's unit. */ width?: number; /** - * In a diagram, distance between the lanes of edges joining the same two vertices, and between - * parallel orthogonal wires, which keep twice it apart. + * In a diagram, the distance between edges joining the same two vertices and between parallel + * orthogonal wires. Wires keep twice this distance from blocks where they can. */ spacing?: number; /** - * Elsewhere, how far edges joining the same two vertices bow apart: each lane by this fraction of - * the edge's length at its middle, as wide as eight lanes at most, more packing closer. A bow is - * part of the drawing, so zooming never moves it. + * Outside a diagram, how far edges joining the same two vertices bow apart: each lane by this + * fraction of the edge's length at its middle. A family spreads at most eight lanes wide, and more + * lanes pack closer. A bow is part of the drawing, so it scales with the zoom. */ bow?: number; - /** How far an arrowhead reaches past its edge's width. */ + /** How far an arrowhead extends beyond the edge's width: a head is this plus the width long. */ arrow?: number; } /** - * Rows drawn as strokes between the vertices two reference fields name: branches, links, wires. In - * a diagram, a row without ends is a net: it joins every vertex port whose reference field names - * it. + * Rows drawn as strokes between the vertices named by two reference fields: branches, links, + * wires. In a diagram, a row without ends is a net, joining every vertex port whose reference + * field names it. */ export interface Edges { /** The dataset and table drawn. */ of: [data: string, table: string]; /** The reference fields naming each row's two vertices. */ ends?: [from: string, to: string] | null; - /** A path field of points each row runs through between its ends. */ + /** A path field whose points each row passes through between its ends. */ bends?: string | null; /** - * How each row runs between its ends; on the earth geodesic, elsewhere straight, unless set. - * Orthogonal routes run around blocks, bends rounded; diagrams only. + * How each row runs between its ends: geodesic on the earth and straight elsewhere unless set. + * `orthogonal` routes around blocks with rounded bends, in diagrams only. */ route?: Route | null; - /** Whether each row ends in an arrowhead at its second end; a net's at each input it reaches. */ + /** + * Whether each row has an arrowhead at its second end; a net has one at each input it reaches. + * Default `false`. + */ arrows?: boolean; /** - * Draws each row as a pole from its own vertex down to the ground beneath it, on a plane or the - * earth; it has no ends. + * Draws each row as a pole from its own vertex down to the ground, on a plane or the earth. Poles + * take no `ends`, `bends`, or `joins`. Default `false`. */ poles?: boolean; /** - * Draws each row as a net on a plane or the earth: it joins the vertex rows of `table` whose - * reference field names it, as a star from their middle to each. + * Draws each row as a net on a plane or the earth, joining the vertex rows of `table` whose + * `reference` field names it: a star from their center to each member. */ joins?: [table: string, reference: string] | null; /** - * Each row's width, in the space's unit unless it says its own: units in a diagram, CSS pixels - * elsewhere. + * Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given + * with `px` or `units`. The theme's width unless set. */ width?: Size | null; - /** Whether each row is dashed. */ + /** Whether each row is dashed, with the theme's `dash` period. Default `false`. */ dash?: ExprInput; /** - * Comets flowing along each row at this speed a second, in the lengths of the theme's flow; - * negative flows backward. A speed that changes carries comets on from where they are. + * The speed of comets flowing along each row, per second, in the unit of the theme's `flow` + * lengths. Negative speeds flow backward; 0, the default, draws no comets. When the speed changes, + * comets continue from where they are. */ flow?: ExprInput; - /** Each row's color; the theme's unless set. */ + /** + * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their + * corners' colors. + */ color?: PaintInput | null; - /** Whether each row is drawn and picked. */ + /** Whether each row is drawn and can be picked. Default `true`. */ visible?: ExprInput; /** - * Each row's priority, from its data alone: what matters more draws on top, stays drawn as a table - * too large to draw whole thins to the tiles in view, and is labeled first. + * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are + * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are + * labeled first. */ priority?: ExprInput | null; - /** Tells two marks of one table apart across scenes, and names the mark in hits. */ + /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ name?: string | null; /** - * Whether rows are hovered, clicked, and probed. Unless set, rows that are things are (vertices, - * edges, faces, a plot's traces) and geometry is not (areas, and lines along paths: coasts, - * borders, roads). + * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces + * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. */ pick?: boolean | null; - /** Whether a click selects a row. */ + /** Whether clicking a row selects it. Default `true`. */ select?: boolean; - /** Whether pressing a row drags it instead of panning. */ + /** + * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the + * camera. Default `false`. + */ drag?: boolean; - /** Each row's text: a text field, or a label's options. */ + /** Each row's label: a text field, or label options. */ label?: Label | null; /** - * A legend of the colors at the view's bottom right, under this title: the colormap across its - * domain, ticked; a cell for each category; or a swatch beside each name of a map's `categories`. - * The color maps through a colormap with a domain or named categories; a conditional's `otherwise` - * is read before its cases. + * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap + * across its domain, a cell for each category, or a swatch beside each name of a map's + * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a + * conditional, `otherwise` is searched before the cases. */ legend?: string | null; } /** - * Rows' least and greatest values of a sampled field over equal spans of the axis, to read back: - * field `field` of table `table` over `spans` equal spans from `range[0]` to `range[1]`, of every - * row or of the rows `ids` names, in its order. Plain data, like a query. + * A request for each row's least and greatest value of a sampled field over equal spans of the + * axis: field `field` of table `table`, over `spans` equal spans from `range[0]` to `range[1]`, + * for every row or for the rows `ids` names, in that order. Plain data that deserializes from + * JSON, like a query. */ export interface EnvelopeRequest { - /** The table read. */ + /** The table to read. */ table: string; - /** The sampled field read. */ + /** The sampled field to read. */ field: string; - /** The axis coordinates the spans cover, from the first to the last. */ + /** The axis range the spans cover, `[first, last]`: finite, with `first <= last`. */ range: [number, number]; - /** How many equal spans the range is cut into. */ + /** The number of equal spans to divide the range into; at least 1. */ spans: number; - /** The rows read, in this order; every row unless set. */ + /** The rows to read, in this order; every row when unset. */ ids?: Ids | null; } -/** A value for every row of a mark: a constant, a field, view state, or arithmetic of them. */ +/** A value for each row: a number, a boolean, a field's name, or a node that computes one. */ export type ExprNode = | number | boolean @@ -480,60 +533,73 @@ export type ExprNode = | ColormapNode; /** - * Rows drawn as triangles between the vertices three reference fields name: meshes, surfaces, and - * fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen from - * above shows its exact colors. + * Rows drawn as triangles between the vertices named by three reference fields: meshes, surfaces, + * and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen + * from above shows its exact colors. */ export interface Faces { /** The dataset and table drawn. */ of: [data: string, table: string]; /** - * The reference fields naming each row's three vertices. Without a color, a face blends the colors - * of its corners. + * The reference fields naming each row's three vertices; required. Without a color, a face blends + * the colors of its corners. */ corners?: [a: string, b: string, c: string] | null; - /** Each row's color; the theme's unless set. */ + /** + * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their + * corners' colors. + */ color?: PaintInput | null; - /** Whether each row is drawn and picked. */ + /** Whether each row is drawn and can be picked. Default `true`. */ visible?: ExprInput; /** - * Each row's priority, from its data alone: what matters more draws on top, stays drawn as a table - * too large to draw whole thins to the tiles in view, and is labeled first. + * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are + * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are + * labeled first. */ priority?: ExprInput | null; - /** Tells two marks of one table apart across scenes, and names the mark in hits. */ + /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ name?: string | null; /** - * Whether rows are hovered, clicked, and probed. Unless set, rows that are things are (vertices, - * edges, faces, a plot's traces) and geometry is not (areas, and lines along paths: coasts, - * borders, roads). + * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces + * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. */ pick?: boolean | null; - /** Whether a click selects a row. */ + /** Whether clicking a row selects it. Default `true`. */ select?: boolean; - /** Whether pressing a row drags it instead of panning. */ + /** + * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the + * camera. Default `false`. + */ drag?: boolean; - /** Each row's text: a text field, or a label's options. */ + /** Each row's label: a text field, or label options. */ label?: Label | null; /** - * A legend of the colors at the view's bottom right, under this title: the colormap across its - * domain, ticked; a cell for each category; or a swatch beside each name of a map's `categories`. - * The color maps through a colormap with a domain or named categories; a conditional's `otherwise` - * is read before its cases. + * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap + * across its domain, a cell for each category, or a swatch beside each name of a map's + * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a + * conditional, `otherwise` is searched before the cases. */ legend?: string | null; } -/** A field of each row: of the mark's own table, or from another dataset's table joined by row id. */ +/** + * A field's value in each row: from the mark's own table, or with `from`, from another dataset's + * table joined by row id. + */ export interface FieldNode { + /** The field's name. */ field: string; /** The dataset and table the field is read from, joined by row id. */ from?: [data: string, table: string] | null; - /** Reads a sampled field at its last frame without interpolating. */ + /** + * For a sampled field, reads the last frame at or before the shown coordinate instead of + * interpolating. Default `false`. + */ step?: boolean; } -/** What a view's fit frames. */ +/** The target of a view's fit. */ export type Fit = | 'all' | { @@ -555,56 +621,61 @@ export type Fit = }; /** - * How comets flowing along edges and lines look: each a round head, brightest at its middle, - * trailing a tail that thins and fades. Their spacing and length are in one unit, and each row's - * flow is in it too: in position units, degrees on the earth, comets are part of the drawing, - * zooming with the view and staying where they are on their lines; otherwise they keep their size - * and speed as it zooms. + * How comets flowing along edges and lines look: a round head, brightest at its middle, trailing a + * tail that thins and fades. + * + * `spacing` and `length` must be in the same unit, which also measures each row's `flow` speed. In + * position units (degrees on the earth), comets are part of the drawing: they scale with the zoom + * and stay in place along their lines. In CSS pixels, they keep their on-screen size and speed. A + * plain number is in the space's unit. */ export interface FlowTheme { - /** A comet's color; its alpha is how strongly a comet covers its stroke. */ + /** + * The comet color. Its alpha sets how strongly a comet covers its stroke; a fully transparent + * color turns comets off. + */ color?: Color; - /** Distance between comets. */ + /** The distance between consecutive comets. */ spacing?: Length; - /** How far a comet's tail trails its head. */ + /** The length of a comet's tail behind its head. */ length?: Length; - /** How wide a comet's head is; never narrower than its stroke. */ + /** The width of a comet's head, in the space's unit; never narrower than its stroke. */ width?: number; } -/** How hovered and selected rows glow, and rows outside a selection dim. */ +/** How hovered and selected rows glow, and how rows outside a selection dim. */ export interface FocusTheme { - /** The glow around hovered rows. */ + /** The glow color around hovered rows. */ hover?: Color; - /** The glow around selected rows; `null` glows in each row's own color. */ + /** The glow color around selected rows; `null` glows in each row's own color. */ selected?: Color | null; - /** How far focus glows reach, CSS pixels. */ + /** How far the hover and selection glows reach, in CSS pixels. */ glow?: number; - /** The opacity of rows outside a selection. */ + /** The opacity of rows outside the selection while any row is selected, from 0 to 1. */ dim?: number; - /** How much a hovered vertex sized in CSS pixels grows. */ + /** The scale factor for a hovered vertex sized in CSS pixels; vertices sized in units only glow. */ grow?: number; } -/** How an axis writes its tick values. */ +/** How an axis formats its tick values. */ export type Format = 'auto' | 'fixed' | 'scientific' | 'engineering'; /** A plane's or a diagram's grid: `"off"`, `{ lines: { pitch } }`, or `{ dots: { pitch, snap } }`. */ export type Grid = | 'off' | { - /** Lines `pitch` units apart, thinned by tens as the view zooms out. */ + /** Lines `pitch` units apart, thinned by factors of ten as the view zooms out. */ lines: { /** Units between lines. */ pitch: number; }; } | { - /** Dots `pitch` units apart; drags snap to them when `snap`. */ + /** Dots `pitch` units apart; with `snap`, a diagram's drags snap to them. */ dots: { /** Units between dots. */ pitch: number; - /** Whether moved vertices snap to the dots. */ + /** Whether drag positions snap to the dots, in a diagram. Default `false`. */ snap?: boolean; }; }; @@ -615,64 +686,79 @@ export type Grid = */ export type Id = number | string; -/** Row ids: numbers or texts (null leaves a reference unwired), or `{count}` naming rows 0..count. */ +/** + * Row ids: numbers or texts, or `{count}` for rows 0 to count - 1. In a reference, null names no + * row. + */ export type Ids = (Id | null)[] | { count: number }; /** How a line runs between its samples. */ export type Interpolation = 'linear' | 'stepBefore' | 'stepAfter'; -/** Whether a text field equals the text. */ +/** Whether a text field equals the text exactly. */ export interface IsNode { is: [field: ExprInput, text: string]; } +/** + * Text for each row, placed beside, inside, or along the row's shape and following it every frame: + * a text field, or options. Where labels would overlap, the higher priority shows; among equals, + * labels already shown stay. In a diagram a label is part of the drawing: it scales with the zoom, + * fades out when too small to read, and shows wherever it fits its vertex. + */ export type Label = | ExprInput | { - /** A text field. */ + /** The text field to show. */ text: ExprInput; - /** In the space's unit: units in a diagram, CSS pixels elsewhere; the theme's text size unless set. */ + /** + * Font size in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's + * `text.size` unless set. + */ size?: number | null; /** Each row's text color; the theme's text color unless set. */ color?: PaintInput | null; - /** Where labels would overlap, the higher shows; the mark's priority unless set. */ + /** Where labels would overlap, the higher priority shows; the mark's `priority` unless set. */ priority?: ExprInput | null; - /** - * How far a line runs before it wraps, in the space's unit: units in a diagram, CSS pixels - * elsewhere. - */ + /** The width at which lines wrap, in the space's unit (units in a diagram, CSS pixels elsewhere). */ maxWidth?: number | null; - /** The CSS font family list its texts are set in; the theme's font unless set. */ + /** The CSS font family list for the text; the theme's `text.font` unless set. */ font?: string | null; /** - * The most labels shown at once, those of highest priority among the rows in view; where they - * would overlap, fewer. + * The most labels shown at once: those of highest priority among the rows in view, or fewer where + * they would overlap. No limit unless set. */ max?: number | null; }; -/** Layered drawing: vertices in ranks, edges pointing along `flow`, and nets from their outputs. */ +/** + * Layered drawing: vertices in ranks, edges pointing along `flow`, and nets ranked from their + * outputs. + */ export interface Layered { - /** Where edges point. */ + /** The direction edges point; `right` by default. */ flow?: Direction; - /** Units between vertices of a rank. */ + /** Distance between vertices within a rank, in position units. Default 30. */ gap?: number; - /** Units between ranks. */ + /** Distance between ranks, in position units. Default 80. */ rankGap?: number; } /** - * How the view places vertex rows without positions: a diagram's blocks, sized, or the points of a - * plane or the earth, among the rows linked to them, gaps in the space's units: a name, or - * `{ stress | layered: options }`. A place, once given, holds: rows added later are placed around - * the rest, and `view.arrange` lays every row out again. + * How the view places vertex rows that have no position: a name, or + * `{ stress | layered: options }`. In a diagram it places blocks; on a plane or the earth it + * places points among the rows they link to. Gaps are in position units: diagram units, plane + * units, or degrees on the earth. + * + * A row keeps its place once placed: rows added later are placed around the others, and + * `view.arrange` lays out every row again. */ export type Layout = | 'none' | 'stress' | 'layered' | { - /** Distances on the drawing follow distances in the graph. */ + /** Drawn distances follow graph distances. */ stress: Stress; } | { @@ -681,90 +767,102 @@ export type Layout = }; /** - * A length: a number, in the space's unit, units in a diagram and CSS pixels elsewhere; `{ px }`, - * CSS pixels; or `{ units }`, position units. + * A length: a number in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in + * CSS pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the + * view zooms in. */ export type Length = | number | { - /** CSS pixels, however the view zooms. */ + /** CSS pixels, at any zoom. */ px: number; } | { - /** Position units, which scale as the view zooms. */ + /** Position units (degrees on the earth), which grow on screen as the view zooms in. */ units: number; }; -/** Lines where their options say nothing. */ +/** Defaults for lines. */ export interface LineTheme { - /** A line's color. */ + /** The line color. */ color?: Color; - /** A line's width. */ + /** The line width, in the space's unit. */ width?: number; } /** - * Rows drawn as lines: along a path field (roads, rivers, borders) or a polygon field's rings, - * outlining its areas; on a plot, through a sampled field's history across the data's axis, what a - * monitor shows. On the earth paths bend with the globe and stay whole across the antimeridian. + * Rows drawn as lines along a path field (roads, rivers, borders) or a polygon field's rings (area + * outlines). On a plot, each row instead traces a sampled field's history across the data's axis, + * as a monitor does. On the earth, paths follow the globe's curve and stay whole across the + * antimeridian. */ export interface Lines { /** The dataset and table drawn. */ of: [data: string, table: string]; - /** The path or polygon field each row runs along; `geometry` unless set. Not on a plot. */ + /** + * The path or polygon field each row runs along; the field named `geometry` by default. Not + * allowed on a plot. + */ geometry?: string | null; /** - * The sampled field each row traces across the data's axis, on a plot alone. A field not recorded - * yet draws nothing until its first frame. + * The sampled field each row traces across the data's axis; plots only, where it is required. A + * field with no frames yet draws nothing until its first frame arrives. */ trace?: string | null; - /** How each row runs between its samples, on a plot. */ + /** How each row runs between its samples on a plot; `linear` by default. */ interpolation?: Interpolation; /** - * Each row's width, in the space's unit unless it says its own: units in a diagram, CSS pixels - * elsewhere. + * Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given + * with `px` or `units`. The theme's width unless set. */ width?: Size | null; - /** Whether each row is dashed. */ + /** Whether each row is dashed, with the theme's `dash` period. Default `false`. */ dash?: ExprInput; /** - * Comets flowing along each row at this speed a second, in the lengths of the theme's flow; - * negative flows backward. A speed that changes carries comets on from where they are. + * The speed of comets flowing along each row, per second, in the unit of the theme's `flow` + * lengths. Negative speeds flow backward; 0, the default, draws no comets. When the speed changes, + * comets continue from where they are. */ flow?: ExprInput; - /** Each row's color; the theme's unless set. */ + /** + * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their + * corners' colors. + */ color?: PaintInput | null; - /** Whether each row is drawn and picked. */ + /** Whether each row is drawn and can be picked. Default `true`. */ visible?: ExprInput; /** - * Each row's priority, from its data alone: what matters more draws on top, stays drawn as a table - * too large to draw whole thins to the tiles in view, and is labeled first. + * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are + * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are + * labeled first. */ priority?: ExprInput | null; - /** Tells two marks of one table apart across scenes, and names the mark in hits. */ + /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ name?: string | null; /** - * Whether rows are hovered, clicked, and probed. Unless set, rows that are things are (vertices, - * edges, faces, a plot's traces) and geometry is not (areas, and lines along paths: coasts, - * borders, roads). + * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces + * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. */ pick?: boolean | null; - /** Whether a click selects a row. */ + /** Whether clicking a row selects it. Default `true`. */ select?: boolean; - /** Whether pressing a row drags it instead of panning. */ + /** + * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the + * camera. Default `false`. + */ drag?: boolean; - /** Each row's text: a text field, or a label's options. */ + /** Each row's label: a text field, or label options. */ label?: Label | null; /** - * A legend of the colors at the view's bottom right, under this title: the colormap across its - * domain, ticked; a cell for each category; or a swatch beside each name of a map's `categories`. - * The color maps through a colormap with a domain or named categories; a conditional's `otherwise` - * is read before its cases. + * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap + * across its domain, a cell for each category, or a swatch beside each name of a map's + * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a + * conditional, `otherwise` is searched before the cases. */ legend?: string | null; } -/** One drawn table of a scene: `{ vertices | edges | faces | lines | areas: options }` as JSON. */ +/** One table a scene draws. In JSON: `{ vertices | edges | faces | lines | areas: options }`. */ export type Mark = | { /** Rows drawn as markers. */ @@ -789,7 +887,7 @@ export type Mark = /** * How a vertex is drawn: a shape's name, `"block"`, or - * `{ shape | block | gauge | pie | icon | wgsl: options }`. + * `{ shape | block | gauge | pie | icon | wgsl: options }`. An ellipse by default. */ export type Marker = | 'ellipse' @@ -802,74 +900,77 @@ export type Marker = shape: Shape; } | { - /** A block: a rectangle with rounded corners, sized for diagrams. */ + /** A rectangle with rounded corners that holds its label inside, as a diagram's blocks do. */ block: { - /** Corner radius, CSS pixels, or units in a diagram; the theme's `corner` unless set. */ + /** + * Corner radius in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's + * `diagram.corner` unless set. + */ corner?: number | null; }; } | { - /** A shape's outline, filled to `fill` from 0 to 1: around a circle, up other shapes. */ + /** A shape's outline, filled to `fill` from 0 to 1: around a circle, or upward in other shapes. */ gauge: { - /** How full each row's gauge is. */ + /** How full each row's gauge is, from 0 to 1. */ fill: ExprInput; - /** The outline. */ + /** The outline's shape; an ellipse by default. */ shape?: Shape; }; } | { - /** A circle cut into slices, each as large as its value. */ + /** A circle divided into slices sized in proportion to their values. */ pie: { /** Each slice's value. */ slices: ExprInput[]; /** Each slice's color. */ colors: Color[]; - /** The hole in the middle, as a fraction of the radius. */ + /** The hole in the middle, as a fraction of the radius; 0 by default. */ hole?: number; }; } | { - /** One of `images`, fitted in the marker's box; `index` picks which for each row. */ + /** One of `images`, fitted to the marker's box; `index` picks the image for each row. */ icon: { /** The images. */ images: Bitmap[]; - /** Each row's image, from 0. */ + /** Each row's image index, from 0. */ index: ExprInput; }; } | { /** - * A marker in WGSL: `source` defines `fn marker(f: MarkerFragment) -> MarkerColor`, reading up to - * eight `inputs` as `f.input[i]`. It compiles once, when the view first draws it. + * A custom marker in WGSL. `source` defines `fn marker(f: MarkerFragment) -> MarkerColor`, which + * can read up to eight `inputs` as `f.input[i]`. It compiles once, when the view first draws it. */ wgsl: { - /** The function's source. */ + /** The WGSL source of the `marker` function. */ source: string; - /** Each row's inputs. */ + /** Each row's inputs, at most eight. */ inputs?: ExprInput[]; }; }; -/** A missing value: options that read one take the theme's. */ +/** A missing value. An option that evaluates to missing uses the theme's value. */ export interface MissingNode { missing: true; } -/** How much a view moves on its own. */ +/** How much a view animates on its own. */ export type Motion = 'system' | 'full' | 'reduced'; -/** Which way a sort runs; rows a key reads nothing for come last either way. */ +/** Sort direction. Rows where the key has no value sort last in either direction. */ export type Order = 'ascending' | 'descending'; /** - * A color for every row of a mark: a CSS color, a value through a colormap, or a conditional - * choosing between colors. + * A color for each row: a CSS color, a value through a colormap, or a conditional choosing between + * colors. */ export type PaintNode = Color | ColorNode | ColormapNode | WhenPaintNode | MissingNode; -/** A plane: positions are plane units. */ +/** A plane: positions are in plane units. */ export interface Plane { - /** The grid under the marks. */ + /** The grid drawn under the marks; lines 10 units apart by default. */ grid?: Grid; } @@ -883,14 +984,14 @@ export interface Plot { /** How a plot looks. */ export interface PlotTheme { - /** The line drawn at the shown coordinate. */ + /** The color of the line at the view's current axis coordinate. */ playhead?: Color; } /** - * A vertex's port: where a net its reference field names meets it, on one side of its marker: a - * side, or options. An output drives its net: layouts rank a net from its outputs, and its arrows - * point away from them. + * A port on one side of a vertex's marker, where a net connects: a side name, or options. The + * vertex names the net in a reference field. An output drives its net: layouts rank a net from its + * outputs, and the net's arrows point away from them. */ export type Port = | 'left' @@ -898,62 +999,69 @@ export type Port = | 'top' | 'bottom' | { - /** The side it sits on. */ + /** The side of the marker it sits on. */ side: Side; /** - * Its place among the ports on that side, from the top or the left; in declaration order unless - * set. + * Its position among the ports on its side, counted from the top or left. Ports without an `order` + * follow those with one, in declaration order. */ order?: number | null; /** Its name, drawn beside it inside the marker. */ label?: string | null; - /** Its marker. */ + /** Its shape; an ellipse by default. */ shape?: Shape; - /** Whether it drives the net its reference field names; an input unless set. */ + /** Whether it is an output, which drives its net. Default `false`, an input. */ output?: boolean; }; -/** How the earth is drawn. */ +/** How the earth is projected. */ export type Projection = 'mercator' | 'globe'; -/** A size in CSS pixels, whatever the space's own unit. */ +/** A size in CSS pixels, regardless of the space's unit. */ export interface Px { + /** The size, in CSS pixels. */ px: ExprInput; } /** The rows of a table to read back, run through the expressions marks draw with. */ export interface Query { - /** The table read. */ + /** The table to read. */ of: string; /** - * The rows read, in this order unless sorted; every row unless set. Ids that name no row are - * passed over. + * The rows to read, in this order unless sorted; every row when unset. Ids that name no row are + * skipped. */ ids?: Ids | null; - /** Keeps the rows it is true for. */ + /** Keeps only the rows for which this is true. */ filter?: ExprInput | null; /** - * Keys to sort by, each after those before it; rows a key reads nothing for come last, and rows - * all keys order alike keep their order. + * Keys to sort by, each breaking ties left by the keys before it. Rows where a key has no value + * sort last; rows equal on every key keep their order. */ sort?: [ExprInput, Order][]; - /** The kept rows skipped first. */ + /** How many of the kept rows to skip. */ offset?: number; - /** The most rows read; a limit picks them in linear time before sorting only those. */ + /** + * The most rows to return. When sorting with a limit, the first `offset + limit` rows are selected + * in linear time and only those are sorted. + */ limit?: number | null; /** - * Fields to read by name: a field as it is, anything else as numbers. Every field unless set; none - * set reads the ids alone. + * Fields to return, by output name. A plain field returns that field as stored; any other + * expression returns numbers. Every field when unset; an empty object returns the ids alone. */ select?: { [name: string]: ExprInput } | null; - /** The axis coordinate sampled fields are read at; their last frame unless set. */ + /** The axis coordinate at which sampled fields are read; their last frame when unset. */ at?: number | null; } /** How an edge runs between its ends. */ export type Route = 'straight' | 'geodesic' | 'orthogonal'; -/** A row of a table in a dataset. It survives any change that keeps the names and the id. */ +/** + * One row, identified by dataset name, table name, and id. It stays valid across any change that + * keeps those names and the id. + */ export interface Row { /** The dataset's name. */ data: string; @@ -963,7 +1071,10 @@ export interface Row { id: Id; } -/** Rows of one table in a dataset, by id: what a view selects, fits, and tells as sets of rows. */ +/** + * Rows of one table in a dataset, by id. Views use it for selections, fits, and the rows events + * report. + */ export interface Rows { /** The dataset's name. */ data: string; @@ -974,8 +1085,8 @@ export interface Rows { } /** - * `value` mapped linearly onto the range `scale`, clamped: from its `domain`, else from the extent - * of the values shown. + * `value` mapped linearly from `domain` onto the range `scale`, clamped to the range. Without a + * `domain`, the extent of the values shown is used. */ export interface ScaleNode { scale: [low: number, high: number]; @@ -984,34 +1095,39 @@ export interface ScaleNode { } /** - * What a view draws and how it answers input: a space, marks in drawing order, a layout, a theme, - * controls, how long changes ease, and the title written over it. Plain data: as JSON, each field - * by its name in camelCase, each mark `{ vertices | edges | faces | lines | areas: options }`. + * A view's content and behavior: a space, marks drawn in order, a layout, a theme, controls, an + * ease duration, and an optional title. + * + * A scene is plain data. In JSON, fields are camelCase and each mark is + * `{ vertices | edges | faces | lines | areas: options }`. */ export interface Scene { + /** The coordinate space positions are in. */ space: Space; - /** What the view draws, in drawing order. */ + /** The marks, in drawing order: each draws over the marks before it. */ marks?: Mark[]; /** - * How the view places vertex rows without positions: a diagram's blocks, or points on a plane or - * the earth, gaps in the space's units. + * How the view places vertex rows that have no position: a diagram's blocks, or points on a plane + * or the earth. `none` by default. */ layout?: Layout; + /** Default colors and sizes, and the look of text, focus, and each space; `dark` by default. */ theme?: Theme; + /** How the view responds to the pointer and keyboard. */ controls?: Controls; - /** Seconds that changes ease over; 0.3 unless set. */ + /** Seconds over which scene and data changes animate; 0.3 by default, 0 for none. */ ease?: number; - /** Written at the view's top left, larger than the theme's text. */ + /** A title drawn at the view's top left, larger than the theme's text. */ title?: string | null; - /** Written under the title. */ + /** A subtitle drawn under the title. */ subtitle?: string | null; } -/** How a diagram's blocks and groups drop shadows onto its sheet. */ +/** How a diagram's blocks and groups cast shadows on the sheet, for vertex marks with `shadow` on. */ export interface ShadowTheme { - /** What a shadow darkens toward; its alpha is how dark it gets under the block. */ + /** The color a shadow darkens toward; its alpha sets how dark the shadow is under the block. */ color?: Color; - /** How far a block's or a group's shadow falls below it, and as far again it softens. */ + /** How far a shadow falls below its block or group; its edge softens over the same distance. */ lift?: number; } @@ -1022,16 +1138,18 @@ export type Shape = 'ellipse' | 'rectangle' | 'rounded' | 'diamond'; export type Side = 'left' | 'right' | 'top' | 'bottom'; /** - * A size, in the space's unit unless it says its own: units in a diagram, CSS pixels elsewhere; - * `px` keeps CSS pixels, and `units` takes position units, which scale as the view zooms. + * A size: a value in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in CSS + * pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the + * view zooms in. */ export type Size = ExprInput | Px | Units; /** - * The coordinate space a scene's positions are in: a name, or - * `{ plane | diagram | earth | plot: options }`. Its own unit is what lengths are in unless they - * say theirs: units in a diagram, a flat sheet seen from straight above with y running down, a - * unit a CSS pixel at zoom 0; CSS pixels elsewhere. Only a diagram has ports, groups, nets, and + * The coordinate space of a scene's positions: a name, or + * `{ plane | diagram | earth | plot: options }`. + * + * The space sets the unit of lengths that do not name their own: units in a diagram, where a unit + * is a CSS pixel at zoom 0, and CSS pixels elsewhere. Only a diagram has ports, groups, nets, and * orthogonal routes. */ export type Space = @@ -1044,79 +1162,84 @@ export type Space = earth: Earth; } | { - /** Plane units with elevation: graphs and scenes the camera turns and tilts around. */ + /** Plane units, with elevation; the camera can turn and tilt. */ plane: Plane; } | { - /** The data's axis across, values up. */ + /** The data's axis horizontally and values vertically. */ plot: Plot; } | { - /** A sheet for diagrams: x right and y down in units, every length a unit. */ + /** A flat sheet for diagrams: x right and y down, every length in units. */ diagram: Diagram; }; -/** What the view knows of each row. */ +/** + * View state of each row: `selected`, `hovered`, `pointer` (CSS pixels from a vertex to the + * pointer), `elapsed` (seconds since the view was created), or `zoom` (the camera's zoom). + */ export type State = 'selected' | 'hovered' | 'pointer' | 'elapsed' | 'zoom'; -/** What the view knows of each row. */ +/** View state of each row, such as whether it is selected. */ export interface StateNode { state: State; } /** - * Stress majorization: each pair of vertices pulls toward its graph distance; then their boxes - * part, keeping a quarter of the gap clear. + * Stress majorization: vertices move until drawn distances match graph distances, then overlapping + * boxes separate, keeping a quarter of `gap` between them. */ export interface Stress { - /** Units between adjacent vertices' centers. */ + /** Distance between the centers of linked vertices, in position units. Default 40. */ gap?: number; } -/** Labels, axis ticks, and titles. */ +/** How labels, axis ticks, and titles look. */ export interface TextTheme { - /** Text's color. */ + /** The text color. */ color?: Color; - /** The CSS font family text is set in. */ + /** The CSS font family list. */ font?: string; - /** Text's size. */ + /** The font size, in the space's unit. */ size?: number; - /** CSS pixels of background around label text. */ + /** Width of the background-colored halo around label text, in CSS pixels. */ halo?: number; } /** - * Colors and sizes marks take where their options read nothing, and the look of text, focus, - * motion, and each space, grouped by what they style: a base theme's name, or options over one - * (`base`, dark unless set), each group's over the base's. Lengths are in the space's unit, units - * in a diagram and CSS pixels elsewhere, but for a text's halo and focus's glow, CSS pixels - * everywhere. + * Default colors and sizes for marks whose options leave them unset or missing, and the look of + * text, focus, comets, and each space, grouped by subject. Write a base theme's name (`"dark"` or + * `"light"`), or options over a base (`base`, dark by default); a group's options override only + * the base values they set. + * + * Lengths are in the space's unit (units in a diagram, CSS pixels elsewhere), except the text halo + * and the focus glow, which are always in CSS pixels. */ export type Theme = | 'dark' | 'light' | { - /** Behind everything. */ + /** The background color. */ background?: Color; - /** A plane's or a diagram's grid, and the earth's graticule. */ + /** The color of a plane's or diagram's grid, a plot's grid lines, and the earth's graticule. */ grid?: Color; - /** What a missing color reads as. */ + /** The color a colormap gives a missing value, such as a row whose field has none. */ missing?: Color; - /** Length of a dash. */ + /** The dash period: one dash and one gap of equal length. */ dash?: number; - /** How much a face turned away from the camera darkens, 0 to 1. */ + /** How much faces darken as they turn away from the camera, from 0 to 1. */ shading?: number; - /** Labels, axis ticks, and titles. */ + /** How labels, axis ticks, and titles look. */ text?: TextTheme; - /** Vertices where their options say nothing. */ + /** Defaults for vertices. */ vertex?: VertexTheme; - /** Edges where their options say nothing, their lanes, and their arrowheads. */ + /** Defaults for edges, and how parallel edges and arrowheads look. */ edge?: EdgeTheme; - /** Lines where their options say nothing. */ + /** Defaults for lines. */ line?: LineTheme; - /** Areas where their options say nothing. */ + /** Defaults for areas. */ area?: AreaTheme; - /** How hovered and selected rows glow, and rows outside a selection dim. */ + /** How hovered and selected rows glow, and how rows outside a selection dim. */ focus?: FocusTheme; /** How comets flowing along edges and lines look. */ flow?: FlowTheme; @@ -1130,34 +1253,36 @@ export type Theme = base?: 'dark' | 'light'; }; -/** A tick as written: a value, or `[value, label]`. */ +/** A tick in JSON: a value, or `[value, label]`. */ export type Tick = number | [number, string | null]; -/** An operation of one value. */ +/** An operation on one value. `sin` and `cos` take radians; `not` treats values above 0.5 as true. */ export type Unary = 'neg' | 'not' | 'abs' | 'sqrt' | 'sin' | 'cos' | 'floor' | 'ln' | 'exp'; +/** A one-argument operation, such as `{ "op": "abs", "args": ["x"] }`. */ export interface UnaryNode { op: Unary; args: [ExprInput]; } /** - * A size in position units, whatever the space's own unit, so it scales as the view zooms, kept - * within CSS pixels when given. + * A size in position units (degrees on the earth), regardless of the space's unit, so it scales as + * the view zooms. `atLeast` and `atMost` bound it in CSS pixels. */ export interface Units { + /** The size, in position units. */ units: ExprInput; - /** CSS pixels it keeps at least, however far the view zooms out. */ + /** The minimum on-screen size in CSS pixels, however far the view zooms out. */ atLeast?: number | null; - /** CSS pixels it keeps at most, however far the view zooms in. */ + /** The maximum on-screen size in CSS pixels, however far the view zooms in. */ atMost?: number | null; } -/** Vertices where their options say nothing. */ +/** Defaults for vertices. */ export interface VertexTheme { - /** A vertex's color. */ + /** The vertex color. */ color?: Color; - /** A vertex's radius. */ + /** The vertex radius, in the space's unit. */ radius?: number; } @@ -1166,89 +1291,103 @@ export interface Vertices { /** The dataset and table drawn. */ of: [data: string, table: string]; /** - * Each row's x and y. Without them, the point field `geometry` names places rows; rows without - * either the scene's layout places. + * Each row's x and y. Without a position, the point field that `geometry` names places rows, and + * the scene's layout places rows that have neither. */ position?: [ExprInput, ExprInput] | null; - /** The point field placing each row where `position` is not given; `geometry` unless set. */ + /** + * The point field that places rows when `position` is not set; the field named `geometry` by + * default. + */ geometry?: string | null; /** - * Height above the ground: on the earth, a fraction of its radius. Not in a diagram, which is - * flat. + * Height above the ground; on the earth, a fraction of the earth's radius. Default 0. Not allowed + * in a diagram, which is flat. */ elevation?: ExprInput; /** - * Each row's radius, in the space's unit unless it says its own: units in a diagram, CSS pixels - * elsewhere. + * Each row's radius, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given + * with `px` or `units`; a diagram refuses `px`. The theme's `vertex.radius` unless set. */ radius?: Size | null; /** - * Width and height, for markers that are not round, in the space's unit unless they say their own; - * in a diagram a block's are units, and a block without a size fits its label and ports. + * Each row's width and height, for markers that are not round, in the space's unit unless given + * with `px` or `units`; a diagram refuses `px`. In a diagram, a block without a size fits its + * label and ports. */ size?: [Size, Size] | null; - /** How each row is drawn. */ + /** How each row is drawn; an ellipse by default. */ marker?: Marker; /** A ring around each row's marker in this color; a missing or transparent color draws none. */ ring?: PaintInput | null; /** - * A port on each row for each reference field naming a net, which an `edges` mark of the net table - * without ends draws. Diagrams only. + * Ports on each row, one for each reference field that names a net. An `edges` mark of the net + * table, without `ends`, draws the nets. Diagrams only. */ ports?: [reference: string, port: Port][]; /** - * Makes each row a group: it holds the rows of `table` whose reference field names it, and is - * drawn as a container around them, its label a title in its header. Diagrams only. + * Makes each row a group containing the rows of `table` whose `reference` field names it. A group + * draws as a container around its members, with its label as a title in its header. Diagrams only. */ contains?: [table: string, reference: string] | null; /** - * Whether each row, a block or a group, drops a shadow onto the sheet, as the theme's `shadow` - * says. Diagrams only. + * Whether each block or group casts a shadow on the sheet, styled by the theme's `diagram.shadow`. + * Default `false`. Diagrams only. */ shadow?: boolean; - /** Each row's color; the theme's unless set. */ + /** + * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their + * corners' colors. + */ color?: PaintInput | null; - /** Whether each row is drawn and picked. */ + /** Whether each row is drawn and can be picked. Default `true`. */ visible?: ExprInput; /** - * Each row's priority, from its data alone: what matters more draws on top, stays drawn as a table - * too large to draw whole thins to the tiles in view, and is labeled first. + * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are + * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are + * labeled first. */ priority?: ExprInput | null; - /** Tells two marks of one table apart across scenes, and names the mark in hits. */ + /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ name?: string | null; /** - * Whether rows are hovered, clicked, and probed. Unless set, rows that are things are (vertices, - * edges, faces, a plot's traces) and geometry is not (areas, and lines along paths: coasts, - * borders, roads). + * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces + * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. */ pick?: boolean | null; - /** Whether a click selects a row. */ + /** Whether clicking a row selects it. Default `true`. */ select?: boolean; - /** Whether pressing a row drags it instead of panning. */ + /** + * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the + * camera. Default `false`. + */ drag?: boolean; - /** Each row's text: a text field, or a label's options. */ + /** Each row's label: a text field, or label options. */ label?: Label | null; /** - * A legend of the colors at the view's bottom right, under this title: the colormap across its - * domain, ticked; a cell for each category; or a swatch beside each name of a map's `categories`. - * The color maps through a colormap with a domain or named categories; a conditional's `otherwise` - * is read before its cases. + * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap + * across its domain, a cell for each category, or a swatch beside each name of a map's + * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a + * conditional, `otherwise` is searched before the cases. */ legend?: string | null; } /** The value of the first case whose condition holds, else `otherwise`. */ export interface WhenNode { + /** The cases, as `[condition, value]` pairs tried in order. */ when: [condition: ExprInput, value: ExprInput][]; + /** The value where no condition holds. */ otherwise: ExprInput; } -/** A conditional's color: the first case's whose condition holds, else `otherwise`'s. */ +/** A conditional color: the color of the first case whose condition holds, else `otherwise`. */ export interface WhenPaintNode { + /** The cases, as `[condition, color]` pairs tried in order. */ when: [condition: ExprInput, color: PaintInput][]; + /** The color where no condition holds. */ otherwise: PaintInput; } -/** What zooms a view. */ +/** Which input zooms a view. */ export type Zoom = 'wheel' | 'modifier' | 'off'; diff --git a/npm/src/internal.ts b/npm/src/internal.ts index d72d9b89..e09e90d3 100644 --- a/npm/src/internal.ts +++ b/npm/src/internal.ts @@ -1,40 +1,56 @@ /** - * What the package's classes share and the page never reaches: the WebAssembly bindings, once - * loaded; the bindings' object behind each public one; and how latkit throws and waits. + * Internals shared by the package's classes: access to the WebAssembly bindings, the bindings + * object behind each public object, and latkit's errors and bounded waits. Of these, only the + * `LatkitError` and `Wait` types are public. */ import * as bindings from '../pkg/latkit.js'; import type { Failure } from './outputs.js'; /** - * What latkit throws: an `Error` with a `code`, and for invalid input the `path` to the part, such - * as `marks[2].vertices.color`. `unloaded` is a call made before `await load()`. + * The error latkit throws: an `Error` with a `code`, plus a `path` to the bad part of invalid + * input, such as `marks[2].vertices.color`. The codes: + * + * - `invalid`: the input is malformed or does not fit; `path` says where. + * - `unsupported`: the browser or device cannot do what was asked. + * - `limit`: a budget or device limit would be exceeded. + * - `io`: reading or writing external bytes failed, such as loading latkit or fetching a file. + * - `device-lost`: the GPU device was lost; create a new `Gpu`. + * - `cancelled`: the wait's signal aborted, or the work was cancelled. + * - `timeout`: the wait's signal timed out. + * - `unloaded`: the call needs WebAssembly, and `load()` has not finished. + * - `destroyed`: the object was destroyed, so it can no longer be used. */ export interface LatkitError extends Error { - code: Failure['code'] | 'unloaded'; + code: Failure['code'] | 'unloaded' | 'destroyed'; path?: string; } -/** How long a promise may wait: until `signal` aborts, by default 30 seconds. */ +/** Options that bound how long a promise waits. */ export interface Wait { + /** + * Aborts the wait. A timeout rejects with a `timeout` error; any other abort rejects with a + * `cancelled` error whose `cause` is the abort reason. Either way, latkit cancels the call's + * pending work where it can. Without a signal, each wait times out after 30 seconds. + */ signal?: AbortSignal; } let ready = false; -/** @internal Marks the bindings loaded: classes may make their objects from now on. */ +/** @internal Marks the bindings as loaded, so `wasm()` returns them from now on. */ export function loaded(): void { ready = true; } -/** @internal The bindings, or an `unloaded` error before `load()` finished. */ +/** @internal Returns the bindings. Throws an `unloaded` error until `load()` has finished. */ export function wasm(): typeof bindings { if (!ready) throw thrown({ code: 'unloaded', message: 'latkit is not loaded: call `await load()` first' }); return bindings; } -/** @internal The error `failure` describes, as latkit throws them. */ +/** @internal Builds a {@link LatkitError} from a code, a message, and an optional path. */ export function thrown({ code, message, @@ -43,29 +59,75 @@ export function thrown({ return Object.assign(new Error(message), path === undefined ? { code } : { code, path }); } -/** The bindings' object behind each public one. */ +/** @internal An `io` error saying `message`, with `cause` as its cause. */ +function io(message: string, cause?: unknown): LatkitError { + return Object.assign(thrown({ code: 'io', message }), cause === undefined ? {} : { cause }); +} + +/** + * @internal Reads `source` into bytes, with the URL they came from as `base` (else empty). A string + * is a URL to fetch; a `Response`, `Blob`, or byte stream is read; any other object is written as + * JSON text. Waits within `wait`, and rejects with an `io` error when a fetch fails or answers an + * error status. + */ +export async function bytesOf( + source: Uint8Array | ArrayBuffer | Blob | Response | ReadableStream | string | object, + wait: Wait, +): Promise<{ bytes: Uint8Array; base: string }> { + if (typeof source === 'string') { + const url = source; + const fetched = fetch(url, { signal: wait.signal }).then( + (response) => bytesOf(response, wait), + (cause: unknown) => Promise.reject(io(`${url} could not be fetched`, cause)), + ); + return waited(fetched, wait, `fetching ${url}`); + } + if (source instanceof Uint8Array) return { bytes: source, base: '' }; + if (source instanceof ArrayBuffer) return { bytes: new Uint8Array(source), base: '' }; + if (source instanceof Response) { + const { url, status, statusText } = source; + if (!source.ok) throw io(`${url}: ${status} ${statusText}`); + const body = await source.arrayBuffer().catch((cause: unknown) => { + throw io(`${url} could not be read`, cause); + }); + return { bytes: new Uint8Array(body), base: url }; + } + if (source instanceof Blob) + return { bytes: new Uint8Array(await source.arrayBuffer()), base: '' }; + if (source instanceof ReadableStream) + return { bytes: new Uint8Array(await new Response(source).arrayBuffer()), base: '' }; + return { bytes: new TextEncoder().encode(JSON.stringify(source)), base: '' }; +} + +/** Maps each public object to its bindings object, until it is released. */ const held = new WeakMap(); -/** @internal Gives `owner` the bindings' object `inner`. */ +/** @internal Associates public object `owner` with its bindings object `inner`. */ export function hold(owner: object, inner: object): void { held.set(owner, inner); } -/** @internal `owner`'s object of the bindings, or an error naming `what` once it was destroyed. */ +/** + * @internal Returns `owner`'s bindings object. Once `owner` is destroyed, throws a `destroyed` + * error naming `what`. + */ export function inner(owner: object, what: string): T { const found = held.get(owner); - if (!found) throw new Error(`this ${what} has been destroyed`); + if (!found) throw thrown({ code: 'destroyed', message: `this ${what} has been destroyed` }); return found as T; } -/** @internal Takes `owner`'s object of the bindings away, to free it; `undefined` once it was. */ +/** + * @internal Detaches and returns `owner`'s bindings object, for the caller to free. Returns + * `undefined` if it was already released. + */ export function release(owner: object): T | undefined { const found = held.get(owner); held.delete(owner); return found as T | undefined; } -/** @internal A public object of class `type` around the bindings' `inner`, made without its constructor. */ +/** @internal Creates a `type` around bindings object `inner` without calling its constructor. */ export function wrap(type: { prototype: T }, inner: object): T { const made = Object.create(type.prototype) as T; hold(made, inner); @@ -73,9 +135,11 @@ export function wrap(type: { prototype: T }, inner: object): T } /** - * @internal What `pending` brings: a promise, or a task of the bindings, which is cancelled, so its - * work is skipped, when `signal` (by default 30 seconds) aborts first. Aborting throws a `timeout` - * error naming `what` for a timeout, `cancelled` for any other reason. + * @internal Awaits `pending`, a promise or a bindings task, until `signal` aborts (30 seconds by + * default). If the signal aborts first, a task is cancelled so its work is skipped, and the call + * rejects with an error naming `what`: `timeout` for a timeout, else `cancelled`. Work that fails + * once the signal has aborted, such as a fetch given the same signal, rejects the same way. A task + * is freed either way. */ export async function waited( pending: Promise | bindings.Task, @@ -95,13 +159,19 @@ export async function waited( else signal.addEventListener('abort', stop, { once: true }); try { return await Promise.race([promise, aborted]); + } catch (error) { + // A fetch given the signal rejects with the abort's reason before the abort event fires. + throw signal.aborted ? abortError(signal, what) : error; } finally { signal.removeEventListener('abort', stop); task?.free(); } } -/** Why `signal` aborted what was waiting for `what`, as latkit throws it. */ +/** + * The error for an aborted wait on `what`: `timeout` when `signal` timed out, else `cancelled` + * with the abort reason as its `cause`. + */ function abortError(signal: AbortSignal, what: string): LatkitError { const reason = signal.reason as { name?: string } | undefined; return reason?.name === 'TimeoutError' diff --git a/npm/src/jobs.ts b/npm/src/jobs.ts index 9fb3e13b..6b202ff8 100644 --- a/npm/src/jobs.ts +++ b/npm/src/jobs.ts @@ -1,4 +1,9 @@ -/** Shared on-demand workers. Each receives the compiled module once and owns its WASM memory. */ +/** + * The pool of workers that run latkit's jobs, started on demand. Each worker receives the compiled + * module once and has its own WebAssembly memory. + */ + +/** A worker's message: that it is ready, a job's bytes, or why it failed. */ interface Answer { ready?: boolean; bytes?: Uint8Array; @@ -18,11 +23,17 @@ interface Slot { timer?: ReturnType; } +/** The error for a job that ran too long, with the `timeout` code latkit keeps. */ +function timedOut(): Error { + return Object.assign(new Error('a worker job timed out'), { code: 'timeout' }); +} + /** - * Keeps the workers that do latkit's jobs alive, one job each. latkit queues its jobs and hands one - * over only when it can run, so the pool grows to as many jobs as run at once and nothing waits here. - * Idle workers retire after 30 seconds; startup and jobs have a one-minute deadline. Failure rejects - * the affected job and retires the worker; later jobs get a fresh one. + * Returns the function latkit hands its jobs to. It runs each job on a pooled worker, one job per + * worker at a time. latkit queues jobs itself and hands one over only when it can run, so the pool + * grows only to the number of jobs running at once, and nothing queues here. Idle workers are + * terminated after 30 seconds. A job fails after one minute, including a new worker's startup. A + * failure rejects that job and terminates its worker; later jobs start a new one. */ export function jobs( module: WebAssembly.Module, @@ -94,7 +105,7 @@ export function jobs( clearTimeout(slot.timer); slot.job = job; const active = slot; - slot.timer = setTimeout(() => fail(active, 'worker job timed out'), 60_000); + slot.timer = setTimeout(() => fail(active, timedOut()), 60_000); send(slot); } catch (error) { if (slot) retire(slot); diff --git a/npm/src/load.ts b/npm/src/load.ts index aefc01a8..b2545340 100644 --- a/npm/src/load.ts +++ b/npm/src/load.ts @@ -1,33 +1,43 @@ -/** Loading latkit's WebAssembly: once a page, before data, views, and pictures can be made. */ +/** Loading latkit's WebAssembly. */ import init, * as bindings from '../pkg/latkit.js'; import { encoder } from './encoder.js'; import { loaded, thrown } from './internal.js'; import { jobs } from './jobs.js'; -/** How latkit loads. Options left out keep what loading began with. */ +/** Options for {@link load}. Only the first call's options take effect. */ export interface LoadOptions { - /** The matching package's WASM asset, or an already compiled module. Defaults to the bundled asset. */ + /** + * The URL of this package version's `latkit/wasm` asset, or that asset already compiled. + * Defaults to the bundled asset. + */ wasm?: string | URL | WebAssembly.Module; - /** Makes a worker running the standalone `latkit/worker` bundle. Defaults to the bundled worker. */ + /** + * Creates a worker that runs the standalone `latkit/worker` bundle. Defaults to the bundled + * worker. + */ worker?: () => Worker; } let configured: { wasm?: string | WebAssembly.Module; worker?: () => Worker } | undefined; let loading: Promise | undefined; -/** The page's first device, made as latkit loads, until `Gpu.create()` takes it. */ +/** The GPU device requested while loading, kept until the first `Gpu.create()` takes it. */ let first: Promise | undefined; /** - * Loads latkit's WebAssembly once a page: data, views, and pictures can be made once it resolves. - * Concurrent calls share one load, and a failed one can be tried again; options given after the first - * call must match it. Importing the package neither loads WebAssembly nor asks for a GPU; loading asks - * for the page's GPU adapter while the WebAssembly compiles. `Gpu.create`, `Data.read`, and - * `Patch.read` load it themselves. + * Loads latkit's WebAssembly, once per page. Datasets, views, and pictures can be created once it + * resolves. Later and concurrent calls share the first call's load. Options they pass must equal + * the first call's, or they reject with an `io` error. A failed load rejects with an `io` error and + * can be retried. + * + * Importing the package loads nothing and requests no GPU. `load()` requests a GPU adapter while + * the WebAssembly compiles, so the browser starts its GPU process early, then requests the first + * GPU device for `Gpu.create()`. `Gpu.create()` and `Data.read()` call `load()` themselves, as does + * `Patch.read()` for JSON. */ export function load(options: LoadOptions = {}): Promise { - // URLs as text, so a caller cannot change the configuration while it loads. + // Store a URL as text, so the caller cannot change the configuration while it loads. const wasm = options.wasm instanceof URL ? options.wasm.href : options.wasm; if (loading) { if ( @@ -49,22 +59,28 @@ export function load(options: LoadOptions = {}): Promise { return loading; } -/** Initializes the bindings, their workers and video encoder, and the page's first device. */ +/** + * Instantiates the bindings, installs the job workers and the video encoder where the browser has + * them, and requests the first GPU device. + */ async function initialize(options: { wasm?: string | WebAssembly.Module; worker?: () => Worker }) { await init(options.wasm === undefined ? undefined : { module_or_path: options.wasm }); if (options.worker || typeof Worker === 'function') bindings.setJobs(jobs(bindings.module() as WebAssembly.Module, options.worker)); if (typeof VideoEncoder === 'function') bindings.setEncoder(encoder); loaded(); - // The page's first device is made now, so the page does not wait for it when it draws; it - // compiles nothing until a view opens. + // Request the first device now, so drawing does not wait for it later. The device compiles + // nothing until a view is created. if ((globalThis.navigator as { gpu?: unknown } | undefined)?.gpu) { first = bindings.Gpu.create(); first.catch(() => {}); } } -/** @internal The page's first device, once: what `Gpu.create()` takes before it asks for another. */ +/** + * @internal Returns the device requested while loading, once. Later calls return `undefined`, and + * `Gpu.create()` requests a new device. + */ export function firstDevice(): Promise | undefined { const device = first; first = undefined; @@ -72,9 +88,9 @@ export function firstDevice(): Promise | undefined { } /** - * Asks for a GPU adapter as latkit's own request does, while the WebAssembly loads: the browser starts - * its GPU process meanwhile, which takes most of a cold device's 280 ms, and latkit's request finds it - * running. + * Requests a high-performance GPU adapter, as latkit's own request does, while the WebAssembly + * loads. This starts the browser's GPU process early. Starting it is most of a cold device's + * 280 ms, so latkit's own request later finds the process running. */ function startGpuProcess(): void { const gpu = ( diff --git a/npm/src/outputs.ts b/npm/src/outputs.ts index 6387efd1..f12d5c4e 100644 --- a/npm/src/outputs.ts +++ b/npm/src/outputs.ts @@ -1,26 +1,26 @@ -/** Written by `node npm/build.mjs schema` from `npm/schema.json`, what latkit writes: do not edit. */ +/** The types latkit writes, written by `node npm/build.mjs schema` from `npm/schema.json`. Do not edit. */ import type { Camera, Id, Row, Rows } from './inputs.js'; /** - * What a dataset holds: its tables, their rows and fields, and the axis its sampled fields are - * recorded over and span. + * A summary of a dataset: its tables with their rows and fields, the axis sampled fields are + * recorded over, and the part of the axis they cover. */ export interface Description { /** The dataset's name. */ name: string; - /** The axis's name and unit. */ + /** The axis name and unit, if set. */ axis: [string, string] | null; - /** The first and last coordinates its sampled fields hold. */ + /** The first and last axis coordinates held by any sampled field; `null` when none holds any. */ span: [number, number] | null; - /** Its tables, by name. */ + /** Its tables, sorted by name. */ tables: TableDescription[]; } -/** What the user acts with. */ +/** The input device. */ export type Device = 'mouse' | 'pen' | 'touch' | 'keyboard'; -/** A vertex a connection ends at, and its port when it has the one the connection meets. */ +/** A vertex a connection ends at, and the port the connection meets, if any. */ export interface End { /** The vertex. */ row: Row; @@ -38,15 +38,15 @@ export interface Failure { path?: string; } -/** A field described: what it holds, the table a reference names, the coordinates samples span. */ +/** A summary of one field: its kind, plus its target table or axis span where those apply. */ export interface FieldDescription { /** The field's name. */ name: string; - /** What it holds. */ + /** The kind of values it holds. */ kind: Kind; - /** The table a reference names. */ + /** For a reference, the target table. */ to?: string; - /** The first and last coordinates a sampled field holds. */ + /** For a sampled field, the first and last axis coordinates it holds. */ span?: [number, number]; } @@ -54,11 +54,11 @@ export interface FieldDescription { export interface Hit { /** The row. */ row: Row; - /** The name of the mark that drew it, when it has one: what tells two marks of one table apart. */ + /** The name of the mark that drew it, if named. It tells two marks of one table apart. */ mark?: string; /** - * The point of the row nearest the probe, CSS pixels from the top left: a vertex's or a port's - * middle, the nearest point of a stroke or a triangle. + * The row's point nearest the probe, in CSS pixels from the top left: a vertex's or port's middle, + * or the nearest point of a stroke or triangle. */ point: [number, number]; /** That point in the space's coordinates: x, y, and elevation. */ @@ -71,11 +71,11 @@ export interface Hit { sample?: Sample; } -/** What a field holds. */ +/** The kind of values a field holds. */ export type Kind = 'number' | 'bool' | 'text' | 'point' | 'path' | 'parts' | 'polygons' | 'reference' | 'sampled'; -/** Keys held during an input. */ +/** Modifier keys held during an input. */ export interface Modifiers { /** Shift. */ shift: boolean; @@ -100,22 +100,22 @@ export interface Sample { value: number; } -/** What a view does each frame. */ +/** Counters describing a view's frames. */ export interface Stats { /** Frames rendered. */ frames: number; /** Rows of every mark's table. */ rows: number; /** - * Rows every mark draws: every row of a table drawn whole, the rows of the tiles in view of one - * drawn through tiles. + * Rows every mark draws: every row of a table drawn whole, and the rows of the tiles in view of a + * paged table. */ drawn: number; - /** Draw calls the last frame recorded, labels with the marks. */ + /** Draw calls the last frame recorded, label draws included. */ draws: number; /** - * Instances the last frame's draws drew: points, segments, triangles, glyphs; at most, for - * segments the GPU culls. + * Instances the last frame's draws drew: points, segments, triangles, and glyphs. An upper bound + * for segments, which the GPU may cull. */ instances: number; /** Fetched tiles the view draws, of every tile source. */ @@ -125,32 +125,32 @@ export interface Stats { /** Milliseconds the last frame took to record on the CPU. */ cpuMs: number; /** - * Milliseconds the GPU takes on a frame: the median of the last frames it measured; 0 where the - * device cannot measure. + * Milliseconds the GPU takes per frame: the median of recently measured frames; 0 if the device + * cannot measure. */ gpuMs: number; } -/** A table described: its rows, and its fields by name. */ +/** A summary of one table: its row count and its fields, sorted by name. */ export interface TableDescription { /** The table's name. */ name: string; - /** How many rows it has. */ + /** The number of rows. */ rows: number; - /** Its fields, by name. */ + /** Its fields, sorted by name. */ fields: FieldDescription[]; } /** - * Where, with what keys, and with what the user acted. For keys, the point is the row they point - * at, else the pointer, else the view's middle. + * Where the user acted, with which modifier keys, and with which device. For keyboard input, the + * point is the row the keys point at, else the pointer, else the view's middle. */ export interface Trigger { /** CSS pixels from the top left. */ point: [number, number]; - /** The keys held. */ + /** The modifier keys held. */ modifiers: Modifiers; - /** What acted. */ + /** The device used. */ device: Device; } @@ -160,7 +160,7 @@ export interface Trigger { */ export type Value = number | boolean | string | null | [number, number] | unknown[][]; -/** What happened in a view, for the application to act on, as `view.on` tells it. */ +/** Something that happened in a view, as `view.on` reports it. */ export type ViewEvent = | { type: 'click'; @@ -183,7 +183,7 @@ export type ViewEvent = | { type: 'menu'; detail: { - /** What lies under the point, nearest first. */ + /** The rows under the point, nearest first. */ hits: Hit[]; /** How the user asked. */ trigger: Trigger; @@ -197,8 +197,8 @@ export type ViewEvent = /** The row dragged, as it was hit. */ hit: Hit; /** - * Where the row goes, in the space's coordinates: where it was hit, moved as the pointer moved - * since the press, at the depth the drag started; a vertex keeps where it was grabbed. On a + * The row's new position in the space's coordinates: the hit point moved with the pointer since + * the press, at the depth where the drag started, so a vertex keeps its grab offset. Snapped to a * diagram's dots when its grid snaps. */ position: [number, number, number]; @@ -213,7 +213,7 @@ export type ViewEvent = from: End; /** The vertex it was released over, if any. */ to: End | null; - /** The edge whose other end was dragged, which `to` replaces. */ + /** The edge whose end was dragged off; `to` replaces that end. */ edge: Row | null; /** Where it was released. */ trigger: Trigger; @@ -231,7 +231,7 @@ export type ViewEvent = | { type: 'hovered'; detail: { - /** The row now pointed at; `null` once it left every row. */ + /** The row now hovered; `null` once no row is. */ hit: Hit | null; /** The pointer, or the keys. */ trigger: Trigger; @@ -251,62 +251,65 @@ export type ViewEvent = | { type: 'error'; detail: Failure }; /** - * Work under way a frame stands in for: pipelines the scene draws with compiling, until which the - * view draws nothing new (`compiling`); pages uploading or tables not yet built from them - * (`data`); layouts, routes, lanes, shapes, tiles, or pyramids (`derived`); a label placement or - * texts not yet set (`labels`); a fit due or being measured (`fit`); segment counts, or a - * transition beginning (`marks`). Empty once a frame is complete. + * What a frame still waits on: pipelines the scene draws with, which compile before the view draws + * anything new (`compiling`); pages uploading, or tables not yet built from them (`data`); + * layouts, routes, lanes, shapes, tiles, or pyramids (`derived`); label placement, or text not yet + * set (`labels`); a fit due or being measured (`fit`); segment counts, or a transition starting + * (`marks`). Empty once the frame is complete. */ export type Waiting = ('compiling' | 'data' | 'derived' | 'labels' | 'fit' | 'marks')[]; -/** What a `Gpu` did since it was made; subtract two for what happened between. */ +/** + * Counts of the work a `Gpu` has done since it was created. Subtract two snapshots to get the work + * between them. + */ export interface Work { /** Bytes of data pages uploaded. */ uploaded: number; /** - * Data pages, or pieces of them, uploaded: what a change uploads counts its pages here, and its - * bytes in `uploaded`. + * Data pages, or pieces of pages, uploaded. A change's uploads count pages here and bytes in + * `uploaded`. */ uploads: number; - /** Bytes written besides pages: uniforms, programs, label runs, the selection, textures. */ + /** Bytes written other than pages: uniforms, programs, label runs, the selection, and textures. */ written: number; /** Rows whose channels the GPU evaluated. */ evaluated: number; /** - * Rows of edges and faces whose ends the GPU found where their vertices are held: rows that - * changed, and every row of a page of them naming a vertex now held elsewhere. + * Edge and face rows whose endpoints the GPU resolved to their vertices' resident rows: rows that + * changed, plus every row of a page that references a vertex now resident elsewhere. */ resolved: number; /** - * Bytes of pages of words per row the GPU cleared: those read before anything writes them, and the - * normals faces sum afresh. + * Bytes of per-row word pages the GPU cleared: pages read before anything writes them, and the + * normals that faces sum from scratch. */ cleared: number; /** - * Rows packed into pages of the order a table drawn in part draws in: each page once, the first - * time a view draws it, and again only where a write changed its rows. + * Rows packed into pages of a paged table's draw order. Each page is packed once, the first time a + * view draws it, and again only when a write changes its rows. */ packed: number; /** - * Reads back from the GPU: probes, queries, fits, label placements, segment counts, pictures; not - * the frame timer's, which only some devices have. + * Readbacks from the GPU: probes, queries, fits, label placements, segment counts, and pictures. + * Frame timer readbacks, which only some devices support, are not counted. */ readbacks: number; /** - * Jobs handed out: layouts, routes, lanes, triangulations, pyramids, tiles; to the host's workers, - * or done at once without a host. + * Jobs started: layouts, routes, lanes, triangulations, pyramids, and tiles. They run on the + * host's workers, or immediately without a host. */ jobs: number; - /** Label texts the host was asked to set. */ + /** Label texts the host was asked to rasterize. */ texts: number; /** - * Pipelines compiled: in a browser, each as a view or a shot asks for it ahead, off the GPU's - * thread; natively, where it is first made. + * Pipelines compiled. In a browser, each compiles in the background when a view or shot requests + * it ahead of use. Natively, each compiles when first created. */ pipelines: number; /** - * Pipelines made without being asked for ahead, each compiled where it is first used: in a browser - * on the GPU's thread, holding frames back. None, unless a scene uses what it did not ask for. + * Pipelines not requested ahead of use, compiled when first used. In a browser they compile on the + * GPU's thread and delay frames. Zero unless a scene uses a pipeline it did not request. */ late: number; } diff --git a/npm/src/patch.ts b/npm/src/patch.ts index 678979bb..b244448c 100644 --- a/npm/src/patch.ts +++ b/npm/src/patch.ts @@ -1,22 +1,30 @@ /** - * Patches as latkit's bytes, written here without WebAssembly: a page, a worker, or a server builds - * them alike. The layout is `crates/latkit/src/data/bytes.rs`'s: a frame of `LATK`, its length, the - * dataset it changes, and its operations, little endian throughout. + * Patches written directly as latkit's bytes, without WebAssembly, so a page, a worker, or a + * server can build them. The format matches `crates/latkit/src/data/bytes.rs`: a frame with a + * 12-byte header (the magic `LATK` and the frame's length), the name of the dataset it changes, an + * operation count, and the operations, all little endian. */ import type { Id, Ids } from './inputs.js'; -import { waited, wasm, type Wait } from './internal.js'; +import { bytesOf, thrown, waited, wasm, type Wait } from './internal.js'; import { load } from './load.js'; -/** Row ids as a patch takes them: listed, `{ count }` for rows `0..count`, or a typed array. */ +/** + * Row ids: a list of numbers or strings, `{ count }` for ids `0` to `count - 1`, or a `Uint32Array` + * or `Float64Array` of numbers. + */ export type RowIds = Ids | readonly (Id | null)[] | Uint32Array | Float64Array; -/** A point `[x, y]`, or a list of geometries one level shallower: a path, its parts, or polygons. */ +/** + * A point `[x, y]`, or a nested list of points: a path (a list of points), parts (a list of paths, + * such as a polygon's rings), or polygons (a list of parts). + */ export type Geometry = readonly number[] | readonly Geometry[]; /** - * One field's values, a value per row; `null` is missing. Typed arrays copy at once: numbers, or - * in a `Uint8Array` booleans, nonzero true. Booleans, texts, and geometries also as arrays. + * One field's values, one per row, with `null` for a missing value. Numbers come as a typed array + * or an array. A `Uint8Array` holds booleans, nonzero being true. Booleans, strings, and geometries + * also come as arrays. Values are copied when passed, so the arrays can be reused at once. */ export type Values = | Float64Array @@ -29,21 +37,24 @@ export type Values = | readonly (string | null)[] | readonly (Geometry | null)[]; -/** A field naming rows of another table by id: `null` leaves a row unwired. */ +/** + * A reference field: for each row, the id of a row in table `reference`, or `null` to reference + * none. + */ export interface Reference { reference: string; ids: RowIds; } -/** Values of a field naming rows of `table` by id. */ +/** Builds a {@link Reference} field: for each row, the id of the row of `table` it references. */ export function reference(table: string, ids: RowIds): Reference { return { reference: table, ids }; } -/** A table's fields by name. */ +/** A table's fields by name: values, or a {@link Reference}. */ export type Fields = Readonly>; -/** A byte stream, structurally compatible with the browser and Node ReadableStream. */ +/** A byte stream: anything shaped like a browser or Node.js `ReadableStream` of `Uint8Array`. */ export interface ReadableBytes { getReader(): { read(): Promise<{ done: boolean; value?: Uint8Array }>; @@ -53,28 +64,33 @@ export interface ReadableBytes { } const MAGIC = [0x4c, 0x41, 0x54, 0x4b]; // LATK -/** The magic and the frame's length. */ -const HEADER = 16; -const VERSION = 1; +/** Header bytes: the magic and the frame's length. */ +const HEADER = 12; const OP = { axis: 0, replace: 1, set: 2, remove: 3, append: 4, forget: 5, frames: 6 } as const; -/** Levels of detail kept above frames. */ +/** The highest level of detail; level 0 is frames. */ const LEVELS = 21; const FIELD_VALUES = 0; const FIELD_REFERENCE = 1; const STORE = { f64: 0, f32: 1, i32: 2, u32: 3, u64: 4, bool: 5, text: 6, geometry: 7 } as const; -/** The largest whole number a JavaScript number holds exactly. */ +/** The largest integer a JavaScript number represents exactly, 2^53 - 1. */ const SAFE = Number.MAX_SAFE_INTEGER; const utf8 = new TextEncoder(); -/** What `Patch.read` reads: latkit's bytes or a patch's JSON. */ -export type PatchSource = Uint8Array | ArrayBuffer | Blob | Response | string | object; +/** + * What {@link Patch.read} accepts: bytes, a `Blob`, a `Response`, a byte stream, a URL to fetch as a + * string, or a parsed JSON value. The content is latkit's bytes or a patch's JSON. + */ +export type PatchSource = + Uint8Array | ArrayBuffer | Blob | Response | ReadableStream | string | object; -/** @internal The frame `patch` writes, without copying it: read at once, before the patch changes. */ +/** @internal Returns `patch`'s frame without copying it. Read it before the patch changes again. */ export let framed: (patch: Patch) => Uint8Array; /** - * Changes to a dataset, applied in order and all at once by `Data.apply`: plain data, written as - * latkit's bytes as it is built. + * A list of changes to a dataset, which `Data.apply` applies in order, all or none. The patch is + * encoded as latkit's bytes as it is built, without WebAssembly. Each method adds a change and + * returns the patch. For values it cannot encode, a method throws an `invalid` error with a `path` + * such as `Bus.kv`, and leaves the patch unchanged. * * ```ts * const patch = new Patch('grid') @@ -90,60 +106,50 @@ export class Patch { #data: string | undefined; #writer: Writer; #count = 0; - /** Where the operations start: after the header, the name, and their count. */ + /** Byte offset of the first operation: after the header, dataset name, and operation count. */ #ops: number; - /** Appends of float frames to the same fields of every row of one table, waiting to be written as - * one: a field records its frames in one write, however many calls made them. */ + /** + * Consecutive appends of float frames to the same fields of every row of one table, not yet + * written. They are written as one append, so each field records all their frames in one write. + */ #run: Appends | undefined; - /** An empty patch: to dataset `data`, which it names, or to whichever it is applied to. */ + /** Creates an empty patch for the dataset named `data`, or for any dataset when omitted. */ constructor(data?: string) { this.#data = data || undefined; this.#writer = new Writer(); this.#writer.bytes(MAGIC); - this.#writer.u16(VERSION); - this.#writer.u16(0); this.#writer.zeros(8); this.#writer.str(this.#data ?? ''); this.#writer.u32(0); this.#ops = this.#writer.length; } - /** The dataset the patch changes, when it names one; `Data.apply` refuses it for another. */ + /** + * The name of the dataset this patch is for, if any. `Data.apply` rejects it for a dataset with + * another name. + */ get data(): string | undefined { return this.#data; } /** - * The patch `source` holds: latkit's bytes, one frame as `toBytes` writes it, or its JSON, what - * any language writes without latkit: a list of changes, each named by its method with its - * parameters by name, such as `{"set": {"table": "Bus", "ids": [1], "fields": {"kv": [345]}}}`. - * Slices of binary files, `{file, dtype, offset, count}`, are fetched at once relative to `base`, - * by default the URL the JSON was fetched from. `source` is bytes, a `Blob`, a `Response`, a URL - * to fetch, or the JSON as a value. Bytes read without WebAssembly; JSON loads it, as `load()` - * does. A frame longer than `limit` bytes (256 MiB unless given) is refused. + * Reads a patch from `source`: latkit's bytes, one frame as `toBytes()` writes it, or JSON that + * any language can write without latkit. The JSON is a list of changes. Each change is keyed by + * its method name and holds the method's parameters by name, such as + * `{"set": {"table": "Bus", "ids": [1], "fields": {"kv": [345]}}}`. A field may be a slice of a + * binary file, `{file, dtype, offset, count}`. Slices are fetched at once, relative to + * `options.base`, which defaults to the URL the JSON was fetched from. + * + * Bytes are read without WebAssembly; JSON calls `load()` first. Rejects with a `limit` error for + * a frame longer than `options.limit` bytes (256 MiB by default), with an `invalid` error for + * malformed input, and with an `io` error when a fetch fails or answers an error status. */ static async read( source: PatchSource, options: { base?: string; limit?: number } & Wait = {}, ): Promise { - let base = ''; - if (typeof source === 'string') { - source = await waited( - fetch(source, { signal: options.signal }), - options, - `fetching ${source}`, - ); - } - if (source instanceof Response) { - if (!source.ok) throw new Error(`${source.url}: ${source.status} ${source.statusText}`); - base = source.url; - source = await source.arrayBuffer(); - } - if (source instanceof Blob) source = await source.arrayBuffer(); - if (source instanceof ArrayBuffer) source = new Uint8Array(source); - const bytes = - source instanceof Uint8Array ? source : new TextEncoder().encode(JSON.stringify(source)); + const { bytes, base } = await bytesOf(source, options); if (MAGIC.every((byte, i) => bytes[i] === byte)) return Patch.#owning(bytes.slice(), options.limit); await load(); @@ -151,7 +157,10 @@ export class Patch { return Patch.#owning(await waited(read, options, 'reading a patch'), options.limit); } - /** The patch the frame `bytes` hold, keeping them. */ + /** + * Wraps frame `bytes` as a patch without copying them. Throws unless they are exactly one valid + * frame of at most `limit` bytes. + */ static #owning(bytes: Uint8Array, limit = 1 << 28): Patch { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const length = frameLength(bytes, limit); @@ -165,7 +174,7 @@ export class Patch { bytes.subarray(HEADER + 4, start - 4), ); } catch { - throw malformed('text is not UTF-8'); + throw malformed('the dataset name is not UTF-8'); } const patch = new Patch(); patch.#data = name || undefined; @@ -176,10 +185,13 @@ export class Patch { } /** - * The patches arriving back to back in `stream`, each as soon as its last byte does: a response - * body, a file, or a socket's messages. Each frame says its length up front, so a patch longer - * than `limit` bytes (256 MiB unless given) throws before allocating its body. The parser holds at most one frame, in addition to - * the input chunk and patches the caller retains. Breaking iteration cancels the reader. + * Yields each patch in `stream` as soon as its last byte arrives. The stream carries frames back + * to back, such as a response body, a file, or a socket's messages. Each frame states its length + * up front, so a frame longer than `options.limit` bytes (256 MiB by default) throws a `limit` + * error before its body is allocated. Besides the current chunk and the patches the caller keeps, + * at most one frame is buffered. Breaking out of the loop cancels the stream. Throws an `invalid` + * error for a `limit` that is not a whole number of at least 12 bytes, for bytes that are not + * latkit's, or for a stream that ends inside a frame. * * ```ts * for await (const patch of Patch.stream((await fetch('/feed')).body!)) data.apply(patch); @@ -190,7 +202,7 @@ export class Patch { { limit = 1 << 28 }: { limit?: number } = {}, ): AsyncGenerator { if (!Number.isSafeInteger(limit) || limit < HEADER) - throw new RangeError(`limit must be a safe integer of at least ${HEADER} bytes`); + throw invalid('limit', `a whole number of at least ${HEADER} bytes, not ${limit}`); let buffer = new Uint8Array(HEADER); let used = 0; let length: number | undefined; @@ -219,7 +231,10 @@ export class Patch { if (used !== 0) throw malformed('the stream ended inside a patch'); } - /** Names the axis sampled fields are recorded over, such as `('time', 's')`. */ + /** + * Sets the name and unit of the axis that sampled fields are recorded over, such as + * `axis('time', 's')`. + */ axis(name: string, unit: string): this { return this.#op(OP.axis, (w) => { w.str(name); @@ -227,14 +242,14 @@ export class Patch { }); } - /** Replaces table `table` with these rows and fields, or adds it. */ + /** Replaces table `table` with rows `ids` and `fields`, adding the table if it does not exist. */ replace(table: string, ids: RowIds, fields: Fields = {}): this { return this.#op(OP.replace, (w) => writeTable(w, table, ids, fields)); } /** - * Writes `fields` for rows `ids` of `table`: rows with known ids change, others are added, and - * fields left out keep their values. Adds the table when there is none of its name. + * Writes `fields` for rows `ids` of `table`. Rows with existing ids change, new ids add rows, and + * omitted fields keep their values. Adds the table if it does not exist. */ set(table: string, ids: RowIds, fields: Fields = {}): this { return this.#op(OP.set, (w) => writeTable(w, table, ids, fields)); @@ -249,10 +264,12 @@ export class Patch { } /** - * Records a frame of `table` at each axis coordinate of `at`: for each field, a value per row at - * every coordinate, frame-major; of every row, or of rows `ids`, the rest holding their last - * values. A field's first frame makes it sampled. Appends of float arrays to the same fields of - * every row, one after another, write as one change, so a frame a call costs no more than one call. + * Records a frame of `table` at each axis coordinate in `at`. Each field gives a value per row + * per frame, frame by frame: every row of the first frame, then of the next. Frames cover every + * row, or only rows `options.ids`; other rows keep their last values. A field's first frame makes + * it a sampled field. Consecutive appends of `Float32Array` or `Float64Array` values to the same + * fields of every row are written as one change, so one frame per call costs no more than one + * call with every frame. */ append( table: string, @@ -291,18 +308,23 @@ export class Patch { return this.#op(OP.append, (w) => writeAppend(w, table, ids, frames, entries)); } - /** Forgets every sampled field's frames before axis coordinate `before`. */ + /** Drops every sampled field's frames before axis coordinate `before`. */ forget(before: number): this { return this.#op(OP.forget, (w) => w.f64(before)); } /** - * Holds a run of sampled field `field`'s entries of level `level` (0 unless given), numbered from - * `first`: what a time tile holds, as `latkit tile` writes them. At level 0 the entries are frames, - * each a coordinate in `at` and a value per row of `table` in `values`. At level `k` they are - * buckets of `8^k` frames, each its first and last frame's coordinates in `at`, and in `values` - * each row's least value over each bucket, then each row's greatest. NaN is missing. No entry may - * be held already; frames past the field's widen it. + * Stores a run of entries of sampled field `field` of `table`, at level of detail `level` (0 by + * default), numbered from `first`. This is what a time tile holds, as `latkit tile` writes it. + * + * At level 0 each entry is a frame: a coordinate in `at`, and a value per row in `values`, entry + * by entry. At level `k` each entry is a bucket of `8^k` frames: its first and last frames' + * coordinates in `at`. `values` then holds every bucket's per-row minimums, entry by entry, + * followed by every bucket's per-row maximums. NaN marks a missing value. + * + * None of the entries may be stored already. Entries past the field's last frame extend it. + * Throws an `invalid` error for a level outside 0 to 21, lengths of `at` and `values` that do not + * divide into whole entries and rows, or frame numbers that are not safe nonnegative integers. */ frames( table: string, @@ -321,7 +343,7 @@ export class Patch { ): this { const path = `${table}.${field}`; if (!Number.isInteger(level) || level < 0 || level > LEVELS) - throw invalid(path, `levels run from 0 to ${LEVELS}`); + throw invalid(path, `the level must be a whole number from 0 to ${LEVELS}`); const per = level === 0 ? 1 : 2; const coordinates = Float64Array.from(at); const numbers = values instanceof Float32Array ? values : Float32Array.from(values); @@ -330,7 +352,7 @@ export class Patch { if (!Number.isInteger(entries) || !Number.isInteger(rows)) throw invalid( path, - `${coordinates.length} coordinates and ${numbers.length} values make no run`, + `${coordinates.length} coordinates and ${numbers.length} values do not divide evenly`, ); if ( !Number.isSafeInteger(first) || @@ -350,7 +372,7 @@ export class Patch { }); } - /** Appends `next`'s changes, to this patch's dataset. */ + /** Appends `next`'s changes to this patch, which keeps its own dataset name. */ extend(next: Patch): this { this.#flush(); next.#flush(); @@ -359,22 +381,22 @@ export class Patch { return this; } - /** The patch as bytes: one frame, naming its dataset when it has one. */ + /** A copy of the patch as latkit's bytes: one frame, naming the dataset if the patch does. */ toBytes(): Uint8Array { return this.#frame().slice(); } - /** The frame, without copying it: read at once, before the patch changes. */ + /** Completes the header and returns the frame uncopied. Read it before the patch changes. */ #frame(): Uint8Array { this.#flush(); const view = this.#writer.view(); const data = new DataView(view.buffer, view.byteOffset, view.byteLength); - data.setBigUint64(8, BigInt(view.length), true); + data.setBigUint64(MAGIC.length, BigInt(view.length), true); data.setUint32(this.#ops - 4, this.#count, true); return view; } - /** Writes the appends waiting as one, before anything after them. */ + /** Writes the pending merged appends as one operation, before any later operation. */ #flush(): void { const run = this.#run; if (!run) return; @@ -383,7 +405,7 @@ export class Patch { this.#op(OP.append, (w) => writeAppend(w, run.table, undefined, run.at, entries)); } - /** Writes an operation, or nothing when `body` throws. */ + /** Writes an operation tagged `tag`. If `body` throws, removes what it wrote and rethrows. */ #op(tag: number, body: (w: Writer) => void): this { this.#flush(); const mark = this.#writer.length; @@ -401,14 +423,14 @@ export class Patch { type Floats = Float64Array | Float32Array; -/** Frames of the same fields of every row of a table: coordinates, and each field's values. */ +/** Pending merged appends to one table: the frames' coordinates and each field's value chunks. */ interface Appends { table: string; at: number[]; fields: { name: string; parts: Floats[] }[]; } -/** `parts` one after another, in the first's storage. */ +/** Concatenates `parts` into one array of the first part's type. */ function joined(parts: readonly Floats[]): Floats { if (parts.length === 1) return parts[0]!; const length = parts.reduce((n, p) => n + p.length, 0); @@ -422,7 +444,10 @@ function joined(parts: readonly Floats[]): Floats { return out; } -/** An append: its table, its rows or every row, its frames' coordinates, and its fields' values. */ +/** + * Writes an append's body: the table, its rows or a flag for every row, the frames' coordinates, + * and each field's values. + */ function writeAppend( w: Writer, table: string, @@ -445,10 +470,10 @@ function writeAppend( } } -/** A table: its name, its ids, and its fields, each values or a reference. */ +/** Writes a table's body: its name, its ids, and its fields, each values or a reference. */ function writeTable(w: Writer, table: string, ids: RowIds, fields: Fields): void { const entries = Object.entries(fields); - // Room for the typed columns at once, rather than doubling past them. + // Reserve room for every typed-array column up front, instead of doubling the buffer repeatedly. const typed = (v: unknown) => (ArrayBuffer.isView(v) ? v.byteLength : 0); w.room(entries.reduce((n, [, f]) => n + typed(isReference(f) ? f.ids : f), typed(ids))); w.str(table); @@ -472,37 +497,38 @@ function isReference(field: Values | Reference): field is Reference { return !ArrayBuffer.isView(field) && !Array.isArray(field) && 'reference' in field; } -/** The length a frame's header says, once `bytes` hold it; an error for what is no frame. */ +/** + * Returns the frame length from the header, or `undefined` until `bytes` hold a whole header. + * Throws for bytes that are not a frame, or a frame longer than `limit`. + */ function frameLength(bytes: Uint8Array, limit: number): number | undefined { for (let i = 0; i < Math.min(bytes.length, 4); i++) if (bytes[i] !== MAGIC[i]) throw malformed('not latkit bytes'); if (bytes.length < HEADER) return undefined; const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); - if (view.getUint16(4, true) !== VERSION) throw malformed('unsupported LATK version'); - if (view.getUint16(6, true) !== 0) throw malformed('unsupported LATK flags'); - const encoded = view.getBigUint64(8, true); + const encoded = view.getBigUint64(MAGIC.length, true); if (encoded > BigInt(Number.MAX_SAFE_INTEGER)) throw malformed('frame length is not a safe integer'); const length = Number(encoded); if (length > limit) - throw Object.assign(new Error(`a patch longer than ${limit} bytes`), { code: 'limit' }); - if (length < HEADER) throw malformed('a frame shorter than its header'); + throw thrown({ code: 'limit', message: `the patch is longer than ${limit} bytes` }); + if (length < HEADER) throw malformed('the frame is shorter than its header'); return length; } function malformed(reason: string): Error { - return Object.assign(new Error(`bytes: ${reason}`), { code: 'invalid', path: 'bytes' }); + return invalid('bytes', reason); } function invalid(path: string, reason: string): Error { - return Object.assign(new Error(`${path}: ${reason}`), { code: 'invalid', path }); + return thrown({ code: 'invalid', message: `${path}: ${reason}`, path }); } -/** Rows' ids: `u8 0` and a count, or `u8 1` and their values. */ +/** Writes row ids: `u8 0` and a count, or `u8 1` and the ids as values. */ function writeIds(w: Writer, ids: RowIds, path: string): void { if (!ArrayBuffer.isView(ids) && !Array.isArray(ids) && 'count' in ids) { if (!Number.isInteger(ids.count) || ids.count < 0 || ids.count > 0xffffffff) - throw invalid(path, 'a count of ids is a whole number from 0'); + throw invalid(path, 'a count of ids must be a whole number from 0 to 2^32 - 1'); w.u8(0); w.u32(ids.count); return; @@ -512,8 +538,8 @@ function writeIds(w: Writer, ids: RowIds, path: string): void { } /** - * Ids as values: numbers as `u32` when every one fits, which latkit reads as the `u64` it keeps, - * else as `u64`; texts when any is one; `null` missing. + * Writes ids as values: as text if any id is a string, else as `u32` when every id fits (latkit + * widens them to the `u64` it stores), else as `u64`. `null` marks a missing id. */ function writeIdValues(w: Writer, ids: RowIds, path: string): void { if (ids instanceof Uint32Array) { @@ -536,7 +562,7 @@ function writeIdValues(w: Writer, ids: RowIds, path: string): void { if (id === null || id === undefined) missing = true; else if (typeof id === 'string') texts = true; else if (!Number.isInteger(id) || id < 0 || id > SAFE) - throw invalid(path, `id ${id}: a numeric id is a whole number from 0 to 2^53 - 1`); + throw invalid(path, `id ${id} is not a whole number from 0 to 2^53 - 1`); else if (id > 0xffffffff) wide = true; } if (texts) { @@ -545,7 +571,7 @@ function writeIdValues(w: Writer, ids: RowIds, path: string): void { Array.from(list, (id) => (id === null || id === undefined ? null : String(id))), ); } else { - // Each id as one little-endian word, or two, low then high. + // One little-endian u32 per id, or two when wide: the low word, then the high. const words = new Uint32Array(wide ? n * 2 : n); for (let i = 0; i < n; i++) { const id = (list[i] as number | null) ?? 0; @@ -559,11 +585,11 @@ function writeIdValues(w: Writer, ids: RowIds, path: string): void { writeValidity(w, n, missing ? (i) => list[i] !== null && list[i] !== undefined : null); } -/** Whether an array value is present: `null`, `undefined`, and an empty geometry are missing. */ +/** Whether a value is present. `null`, `undefined`, and an empty geometry count as missing. */ const present = (v: unknown): boolean => v !== null && v !== undefined && !(Array.isArray(v) && v.length === 0); -/** Values as one storage, a validity mask after them when any is missing. */ +/** Writes values in one storage type, followed by a validity mask when any value is missing. */ function writeValues(w: Writer, values: Values, path: string): void { if (ArrayBuffer.isView(values)) { if (values instanceof Uint8Array) { @@ -582,7 +608,7 @@ function writeValues(w: Writer, values: Values, path: string): void { ? STORE.u32 : undefined; if (store === undefined) - throw invalid(path, `${values.constructor.name} is not a column's typed array`); + throw invalid(path, `${values.constructor.name} is not a supported typed array`); w.u8(store); w.u32(values.length); w.bytes(new Uint8Array(values.buffer, values.byteOffset, values.byteLength)); @@ -628,12 +654,12 @@ function writeValues(w: Writer, values: Values, path: string): void { writeGeometry(w, list as readonly (Geometry | null)[], path); break; default: - throw invalid(path, `a field holds numbers, booleans, texts, or geometries, not ${kind}`); + throw invalid(path, `a field holds numbers, booleans, strings, or geometries, not ${kind}`); } writeValidity(w, n, missing ? (i) => present(list[i]) : null); } -/** Texts: their byte offsets, then their UTF-8; a missing text is empty. */ +/** Writes texts as `n + 1` byte offsets, then their UTF-8 bytes. A missing text is empty. */ function writeTexts(w: Writer, texts: readonly (string | null)[]): void { const n = texts.length; let chars = 0; @@ -655,24 +681,28 @@ function writeTexts(w: Writer, texts: readonly (string | null)[]): void { w.bytes(bytes.subarray(0, at)); } -/** Geometries: their depth, each level's offsets into the next, then their `[x, y]` points. */ +/** + * Writes geometries: their nesting depth, each level's offsets into the next level, then the + * `[x, y]` points as `f64`. + */ function writeGeometry(w: Writer, rows: readonly (Geometry | null)[], path: string): void { const depthOf = (g: Geometry): number => typeof g[0] === 'number' ? 0 : Array.isArray(g[0]) ? 1 + depthOf(g[0] as Geometry) : 1; const first = rows.find((g) => present(g)); const depth = first ? depthOf(first) : 0; - if (depth > 3) throw invalid(path, 'geometries nest at most polygons of rings of [x, y] points'); + if (depth > 3) + throw invalid(path, 'geometry nests deeper than polygons of rings of [x, y] points'); const levels: number[][] = Array.from({ length: depth }, () => [0]); const coords: number[] = []; const push = (g: Geometry, level: number): void => { if (level === depth) { if (typeof g[0] !== 'number' || typeof g[1] !== 'number') - throw invalid(path, `geometries nest alike: ${depth} levels deep`); + throw invalid(path, `every geometry must nest ${depth} levels deep, like the first`); coords.push(g[0], g[1]); return; } if (typeof g[0] === 'number') - throw invalid(path, `geometries nest alike: ${depth} levels deep`); + throw invalid(path, `every geometry must nest ${depth} levels deep, like the first`); for (const item of g as readonly Geometry[]) push(item, level + 1); const next = level + 1 < depth ? levels[level + 1]!.length - 1 : coords.length / 2; levels[level]!.push(next); @@ -693,20 +723,20 @@ function writeGeometry(w: Writer, rows: readonly (Geometry | null)[], path: stri w.bytes(new Uint8Array(Float64Array.from(coords).buffer)); } -/** `n` bits, `bit(i)` each, in `u64` words, low bit first. */ +/** Writes `n` bits, bit `i` being `bit(i)`, packed into `u64` words, lowest bit first. */ function writeBits(w: Writer, n: number, bit: (i: number) => boolean): void { const words = new Uint8Array(Math.ceil(n / 64) * 8); for (let i = 0; i < n; i++) if (bit(i)) words[i >> 3]! |= 1 << (i & 7); w.bytes(words); } -/** `u8 0`, or `u8 1` and the bits of which of `n` values are present. */ +/** Writes `u8 0` when every value is present, else `u8 1` and a bit per value, set if present. */ function writeValidity(w: Writer, n: number, valid: ((i: number) => boolean) | null): void { w.u8(valid ? 1 : 0); if (valid) writeBits(w, n, valid); } -/** Bytes written little endian into a buffer that doubles as it fills. */ +/** A little-endian byte writer over a buffer that doubles in size as it fills. */ class Writer { #buffer: Uint8Array; #data: DataView; @@ -722,7 +752,7 @@ class Writer { this.length = owned?.length ?? 0; } - /** Room for `n` more bytes past the end. */ + /** Ensures capacity for `n` more bytes past the end. */ room(n: number): void { if (this.length + n <= this.#buffer.length) return; const grown = new Uint8Array(Math.max(2 * this.#buffer.length, this.length + n)); @@ -754,20 +784,16 @@ class Writer { } u32(v: number): void { - if (!Number.isInteger(v) || v < 0 || v > 0xffffffff) throw new RangeError(`${v} is no u32`); + if (!Number.isInteger(v) || v < 0 || v > 0xffffffff) + throw thrown({ code: 'limit', message: `${v} does not fit in a 32-bit count` }); this.room(4); this.#data.setUint32(this.length, v, true); this.length += 4; } - u16(v: number): void { - this.room(2); - this.#data.setUint16(this.length, v, true); - this.length += 2; - } - u64(v: number): void { - if (!Number.isSafeInteger(v) || v < 0) throw new RangeError(`${v} is no safe unsigned integer`); + if (!Number.isSafeInteger(v) || v < 0) + throw thrown({ code: 'invalid', message: `${v} is not a safe nonnegative integer` }); this.room(8); this.#data.setBigUint64(this.length, BigInt(v), true); this.length += 8; @@ -786,7 +812,10 @@ class Writer { } } -/** The chunks of a stream, read with a reader where async iteration is missing. */ +/** + * Yields the chunks of `stream`, through `getReader()` when it has one, else by async iteration. + * Stopping early cancels the reader. + */ async function* chunks( stream: ReadableBytes | AsyncIterable, ): AsyncGenerator { diff --git a/npm/src/scene.ts b/npm/src/scene.ts index 0e2ae379..73f7da71 100644 --- a/npm/src/scene.ts +++ b/npm/src/scene.ts @@ -1,6 +1,6 @@ /** - * The builders that write scenes' options. A scene is plain data the view reads, its types written - * from the forms latkit reads (`inputs.ts`); nothing here needs WebAssembly. + * Builders for the expressions and colors that scene options take. A scene is plain data, typed in + * `inputs.ts`; nothing here needs WebAssembly. */ import type { @@ -19,35 +19,40 @@ import type { Units, } from './inputs.js'; -/** A CSS color, such as `'#ffb000'` or `'rebeccapurple'`: what a string is in a color option. */ +/** + * A CSS color, such as `'#ffb000'` or `'rebeccapurple'`. In a color option, a string is a color. + */ export type Color = string; -/** What a value is written as: an {@link Expr}, or its JSON form, in which a string names a field. */ +/** A value option: an {@link Expr}, or its JSON form, in which a string is a field name. */ export type ExprInput = Expr | ExprNode; /** - * What a color is written as: a CSS color, a {@link Paint}, or its JSON form. A string is a CSS - * color here; a field reads with {@link field}. + * A color option: a CSS color, a {@link Paint}, or its JSON form. A string here is always a CSS + * color, never a field name; map a field to colors with `field(name).colormap(map)`. */ export type PaintInput = Color | Paint | PaintNode; const nodeOf = (input: ExprInput): ExprNode => (input instanceof Expr ? input.node : input); -/** A value for every row of a mark: a constant, a field, view state, or arithmetic of them. */ +/** A value for every row of a mark: a constant, a field, view state, or arithmetic on them. */ export class Expr { - /** An expression from its JSON form; use the builders for common operations. */ + /** Wraps an expression's JSON form. The builders cover common cases. */ constructor(readonly node: ExprNode) {} toJSON(): ExprNode { return this.node; } - /** Reads this expression's fields from `table` of dataset `data`, joined by row id. */ + /** Reads this expression's fields from table `table` of dataset `data`, joined by row id. */ from(data: string, table: string): Expr { return new Expr(mapFields(this.node, (f) => ({ ...f, from: [data, table] }))); } - /** Reads sampled fields at their last frame without interpolating. */ + /** + * Reads this expression's sampled fields at the last frame at or before the view's `at`, instead + * of interpolating toward the next frame. + */ step(): Expr { return new Expr(mapFields(this.node, (f) => ({ ...f, step: true }))); } @@ -115,9 +120,11 @@ export class Expr { sqrt(): Expr { return unary('sqrt', this); } + /** The sine of the value, an angle in radians. */ sin(): Expr { return unary('sin', this); } + /** The cosine of the value, an angle in radians. */ cos(): Expr { return unary('cos', this); } @@ -136,36 +143,45 @@ export class Expr { return new Expr({ is: [this.node, text] }); } - /** Whether a text field holds `text`, ignoring case: in a mark, a field of its own table. */ + /** + * Whether a text field contains `text`, ignoring case. In a mark, the field must be in the mark's + * own table. + */ contains(text: string): Expr { return new Expr({ contains: [this.node, text] }); } - /** The value through `map`, a color for every row: a catalog name, or your own colors. */ + /** + * Maps the value to a color for every row through `map`, a catalog name or custom colors. The + * domain fits the values shown unless set with {@link Colormapped.domain}. + */ colormap(map: Colormap): Colormapped { return new Colormapped({ colormap: map, value: this.node }); } - /** The value mapped linearly onto `[low, high]`, clamped. */ + /** + * Maps the value linearly onto `[low, high]`, clamped. The domain fits the values shown unless + * set with {@link Mapped.domain}. + */ scale(low: number, high: number): Mapped { return new Mapped({ scale: [low, high], value: this.node }); } } -/** A value mapped onto a range, whose domain can be set. */ +/** A value mapped linearly onto a range, with an input domain that can be set. */ export class Mapped extends Expr { - /** The input values that map to the ends; it fits the values shown unless set. */ + /** Sets the input values that map to the range's ends, instead of fitting the values shown. */ domain(low: number, high: number): Mapped { return new Mapped({ ...(this.node as ScaleNode), domain: [low, high] }); } } /** - * A color for every row of a mark: a CSS color, a value through a colormap, or a conditional choosing - * between colors. Color options take one, or a CSS color as it is. + * A color for every row of a mark: a constant, a value mapped through a colormap, or a conditional + * choosing between colors. Color options take a `Paint` or a CSS color string. */ export class Paint { - /** A color from its JSON form; use the builders for common colors. */ + /** Wraps a color's JSON form. The builders cover common cases. */ constructor(readonly node: PaintNode) {} toJSON(): PaintNode { @@ -173,15 +189,15 @@ export class Paint { } } -/** A value through a colormap, whose domain can be set. */ +/** A value mapped through a colormap, with an input domain that can be set. */ export class Colormapped extends Paint { - /** The input values that map to the ends; it fits the values shown unless set. */ + /** Sets the input values that map to the colormap's ends, instead of fitting the values shown. */ domain(low: number, high: number): Colormapped { return new Colormapped({ ...(this.node as ColormapNode), domain: [low, high] }); } } -/** A missing value, which options take the theme's for: a value and a color alike. */ +/** A missing value, valid as a value or a color. An option given one uses the theme's value. */ export class Missing extends Expr { declare readonly node: MissingNode; @@ -198,7 +214,7 @@ const unary = (op: Unary, a: ExprInput): Expr => new Expr({ op, args: [nodeOf(a) const binary = (op: Binary, a: ExprInput, b: ExprInput): Expr => new Expr({ op, args: [nodeOf(a), nodeOf(b)] }); -/** `node` with every field read through `f`. */ +/** Returns `input`'s JSON form with every field node replaced by `f(field)`. */ function mapFields(input: ExprInput, f: (field: FieldNode) => FieldNode): ExprNode { const node = nodeOf(input); if (typeof node === 'string') return f({ field: node }); @@ -218,34 +234,35 @@ function mapFields(input: ExprInput, f: (field: FieldNode) => FieldNode): ExprNo const state = (name: State): Expr => new Expr({ state: name }); -/** The field `name` of each row. */ +/** Reads field `name` of each row. */ export const field = (name: string): Expr => new Expr({ field: name }); -/** A color constant. */ +/** A constant color, from a CSS color string. */ export const color = (css: Color): Paint => new Paint({ color: css }); /** Whether the row is selected. */ export const selected = (): Expr => state('selected'); /** Whether the pointer is over the row. */ export const hovered = (): Expr => state('hovered'); -/** CSS pixels from a vertex to the pointer. */ +/** CSS pixels from a vertex to the pointer. Vertex marks only, and not in a vertex's position. */ export const pointer = (): Expr => state('pointer'); -/** Seconds since the view was created. */ +/** Seconds since the view was created. A mark that reads it redraws every frame. */ export const elapsed = (): Expr => state('elapsed'); /** The camera's zoom: the web mercator level on the earth, log₂ CSS pixels per unit elsewhere. */ export const zoom = (): Expr => state('zoom'); -/** A missing value: options that read one take the theme's. */ +/** A missing value: an option given one uses the theme's value. */ export const missing = (): Missing => new Missing(); -/** The CSS pixels a size in position units keeps within. */ +/** On-screen limits, in CSS pixels, for a size given in position units. */ export interface Within { - /** CSS pixels it keeps at least, however far the view zooms out. */ + /** The smallest size in CSS pixels, however far the view zooms out. */ atLeast?: number; - /** CSS pixels it keeps at most, however far the view zooms in. */ + /** The largest size in CSS pixels, however far the view zooms in. */ atMost?: number; } /** - * A size in CSS pixels, whatever the space's own unit: a stroke keeps its width on screen as a - * diagram zooms. A number makes a theme's length too. + * A size in CSS pixels regardless of the space's unit, so it stays the same on screen as the view + * zooms: `px(1)` keeps a diagram's stroke 1 pixel wide at any zoom. Given a number, the result also + * works as a theme length. */ export function px(size: number): { px: number }; export function px(size: ExprInput): Px; @@ -254,10 +271,10 @@ export function px(size: ExprInput): Px { } /** - * A size in position units, whatever the space's own unit, so it scales as the view zooms, kept - * within `atLeast` and `atMost` CSS pixels when given: `units('r', { atLeast: 2 })` grows with the map - * and never vanishes. A marker keeps its proportions, its larger side held. A number alone makes a - * theme's length too. + * A size in position units (degrees on the earth) regardless of the space's unit, so it scales as + * the view zooms. `within` limits its on-screen size in CSS pixels: `units('r', { atLeast: 2 })` + * grows with the map and never shrinks below 2 pixels. A marker keeps its proportions, with the + * limits applied to its larger side. Given only a number, the result also works as a theme length. */ export function units(size: number): { units: number }; export function units(size: ExprInput, within?: Within): Units; @@ -265,28 +282,30 @@ export function units(size: ExprInput, within: Within = {}): Units { return { units: nodeOf(size), ...within }; } -/** A case of a conditional: its condition, and its value or color. */ +/** One case of a conditional: a condition, and the value or color it selects. */ type Case = [ExprNode, ExprNode | PaintNode]; -/** What a conditional's values are written as, a value's or a color's. */ +/** A conditional's value input: {@link PaintInput} for colors, else {@link ExprInput}. */ type InputOf = T extends Paint ? PaintInput : ExprInput; /** - * Starts a conditional: `when(c).then(a).when(d).then(b).otherwise(e)`. Its values are colors, a - * string a CSS color, where its first is one; else values. + * Starts a conditional. `when(c).then(a).when(d).then(b).otherwise(e)` takes the value of the first + * case whose condition holds, else `e`. If the first `then` gets a color (a CSS string, a + * {@link Paint}, a missing value, or a color's JSON form), every value of the conditional is a + * color, and strings are CSS colors. Otherwise every value is an expression. */ export function when(condition: ExprInput): When { return new When(nodeOf(condition)); } -/** A conditional's first case, waiting for its value where the condition holds. */ +/** A conditional's first condition, awaiting its value from `then`. */ class When { /** @internal */ constructor(private readonly condition: ExprNode) {} - /** The color where the condition holds: every value of the conditional is a color. */ + /** Sets the color where the condition holds; every value of the conditional is then a color. */ then(value: PaintInput): Then; - /** The value where the condition holds. */ + /** Sets the value where the condition holds. */ then(value: ExprInput): Then; then(value: PaintInput | ExprInput): Then | Then { const cases: Case[] = [[this.condition, written(value)]]; @@ -296,7 +315,7 @@ class When { } } -/** A conditional's cases so far, waiting for another or the value where none holds. */ +/** A conditional's cases so far, awaiting another `when` case or the `otherwise` fallback. */ class Then { /** @internal */ constructor( @@ -304,18 +323,18 @@ class Then { private readonly make: (node: object) => T, ) {} - /** Another case, tried when those before it do not hold. */ + /** Adds a case, tried when no earlier condition holds. */ when(condition: ExprInput): Next { return new Next(this.cases, nodeOf(condition), this.make); } - /** The value where no case holds. */ + /** Sets the value where no condition holds, and returns the finished conditional. */ otherwise(value: InputOf): T { return this.make({ when: this.cases, otherwise: written(value) }); } } -/** A conditional's next case, waiting for its value where the condition holds. */ +/** A conditional's next condition, awaiting its value from `then`. */ class Next { /** @internal */ constructor( @@ -324,20 +343,20 @@ class Next { private readonly make: (node: object) => T, ) {} - /** The value where the condition holds. */ + /** Sets the value where this condition holds. */ then(value: InputOf): Then { return new Then([...this.cases, [this.condition, written(value)]], this.make); } } -/** `value` as it is written. */ +/** The JSON form of `value`. */ function written(value: ExprInput | PaintInput): ExprNode | PaintNode { return value instanceof Expr || value instanceof Paint ? value.node : value; } /** - * Whether a conditional's first value makes it choose colors, as the types say: a CSS color, a color, - * a missing value, or a color's own form. + * Whether a conditional's first value makes it a color conditional: a CSS string, a + * {@link Paint}, a missing value, or a color's JSON form. */ function colored(value: ExprInput | PaintInput): boolean { if (typeof value === 'string' || value instanceof Paint || value instanceof Missing) return true; @@ -345,14 +364,17 @@ function colored(value: ExprInput | PaintInput): boolean { return 'color' in value || 'colormap' in value || 'missing' in value; } -/** Straight-alpha RGBA pixels, compatible with browser ImageData and server-produced pixels. */ +/** RGBA pixels with straight alpha, rows top to bottom. A browser `ImageData` has this shape. */ export interface RgbaImage { width: number; height: number; data: Uint8ClampedArray; } -/** An icon marker showing one of `images` for each row, picked by `index`. */ +/** + * An icon marker: each row shows `images[index]`, fitted in the marker's box, with `index` counting + * from 0. The pixels are copied into the marker. + */ export function icon(images: readonly RgbaImage[], index: ExprInput): Marker { return { icon: { diff --git a/npm/src/shot.ts b/npm/src/shot.ts index 39261a55..3c2bf5a9 100644 --- a/npm/src/shot.ts +++ b/npm/src/shot.ts @@ -1,54 +1,74 @@ -/** Pictures and videos of views: images to encode, and H.264 video written as it is drawn. */ +/** Pictures of views: images to encode as files, and H.264 video written as frames are drawn. */ import type * as bindings from '../pkg/latkit.js'; import { placed, type Panel } from './canvas.js'; -import { hold, inner, release, thrown, waited, wasm, type Wait } from './internal.js'; +import { + hold, + inner, + release, + thrown, + waited, + wasm, + type LatkitError, + type Wait, +} from './internal.js'; import { handleOf } from './view.js'; -/** How a picture is taken. */ +/** Options for a {@link Shot}. */ export interface ShotOptions { - /** Pixels per CSS pixel: a view lays out `size / scale` CSS pixels, so 2 draws twice as sharp. 1 unless given. */ + /** + * Pixels per CSS pixel, 1 by default. Views lay out at `size / scale` CSS pixels, so 2 draws the + * same layout twice as sharp. + */ scale?: number; - /** Keeps alpha where the themes' backgrounds would be. */ + /** Leaves the background transparent where the theme's background color would be. */ transparent?: boolean; /** - * When the picture is taken: once its views are complete and still, transitions, flights, and - * label fades run on until they land (`still`, the default); or once they are complete, as they - * stand now, what moves held where it is (`now`). + * When the picture is taken. `'still'`, the default, waits until every view is complete and + * still: transitions, camera flights, and label fades run until they finish. `'now'` waits only + * until every view is complete, and freezes anything moving where it is. */ capture?: 'still' | 'now'; - /** A still's effect time in seconds, where elapsed time and flowing comets read it. */ + /** + * The time in seconds that `elapsed()` expressions and flowing comets show in a still picture. By + * default they follow the view's clock. + */ elapsed?: number; } /** - * A picture of views, each drawn into its panel of `size` pixels as `layout` places it: drawn while a - * canvas showing it holds its last frame, each view's data as it was when the picture began. A - * canvas's `shot()` takes one of what it shows. + * A picture of views, `size` pixels in all, each view drawn into its layout panel. While a shot + * draws a view, a canvas showing that view keeps its last frame. An image draws each view's data as + * it was when the picture began. A canvas's `shot()` makes a shot of what the canvas shows. * * ```ts * const png = await (await canvas.shot({ scale: 2 }).image()).encode('png'); * ``` */ export class Shot { - /** A picture `size` pixels wide and high, of the views of `options.layout`. */ + /** Creates a shot of `size`, `[width, height]` in pixels, of the views in `options.layout`. */ constructor( readonly size: readonly [number, number], readonly options: ShotOptions & { layout?: readonly Panel[] } = {}, ) {} - /** The picture, once every view is complete as `capture` says. */ + /** + * Draws the picture once every view is complete, as `capture` says, and resolves to the + * {@link Image}. Rejects with an `invalid` error for invalid options or a view in two panels, and + * with the first error a view reports while drawing. + */ async image(wait: Wait = {}): Promise { const taken = wasm().image(this.#json(this.size), this.#views()); - const image = await waited(taken, wait, 'a picture'); + const image = await waited(taken, wait, 'taking a picture'); return Image.made(image); } /** - * A video of these pictures at `fps` frames a second, H.264 in MP4, written to `output` as it is - * drawn, at `bitrate` bits a second or about 0.15 bits a pixel a frame. Its width rounds down to a - * multiple of 4 and its height of 2, the blocks H.264 takes. `signal` aborts it: the output is - * aborted and the views are given back. + * Starts a video of this shot: H.264 in MP4 at `fps` frames per second, written to `output` as + * frames are drawn. `bitrate` is in bits per second, about 0.15 bits per pixel per frame by + * default. The width is rounded down to a multiple of 4 and the height to a multiple of 2, as the + * encoder requires. Canvases showing the views keep their last frame until the video finishes or + * aborts. Aborting `signal` aborts the video and `output`; frames have no other time limit. */ video({ fps, @@ -76,7 +96,7 @@ export class Shot { } } - /** The shot of `size` pixels as the bindings read it: JSON, times in seconds. */ + /** The shot as JSON for the bindings, at `size` pixels, with times in seconds. */ #json(size: readonly [number, number]): string { const { scale = 1, @@ -95,42 +115,47 @@ export class Shot { } } -/** RGBA pixels, straight alpha, rows top to bottom. */ +/** A picture's RGBA pixels, with straight alpha, rows top to bottom. */ export class Image { private constructor(image: bindings.Image) { hold(this, image); } - /** @internal An image of the bindings' `image`. */ + /** @internal Wraps a bindings image. */ static made(image: bindings.Image): Image { return new Image(image); } + /** Width in pixels. */ get width(): number { return pixels(this).width; } + /** Height in pixels. */ get height(): number { return pixels(this).height; } - /** The pixels, copied. */ + /** A copy of the pixels: 4 bytes per pixel, `width * height * 4` bytes in all. */ get rgba(): Uint8Array { return pixels(this).rgba; } /** - * The image as a file, written in a worker: PNG, lossless with alpha; JPEG at `quality` from 1 to - * 100 (90 by default), small, its alpha dropped; or WebP, lossless with alpha, smaller than PNG. + * Encodes the image as a file, in a worker. PNG is lossless with alpha. JPEG uses + * `options.quality` from 1 to 100 (90 by default) and drops alpha. WebP is lossless with alpha + * and smaller than PNG. Rejects with an `invalid` error for a quality outside 1 to 100 or an + * unknown format. */ encode(format: ImageFormat, options: { quality?: number } & Wait = {}): Promise { return waited( pixels(this).encode(format, options.quality), options, - 'writing an image', + 'encoding an image', ); } + /** Frees the pixels. Other methods throw afterwards. */ destroy(): void { release(this)?.free(); } @@ -144,10 +169,13 @@ function pixels(owner: Image): bindings.Image { return inner(owner, 'Image'); } -/** How an image is written as a file. */ +/** An image file format for {@link Image.encode}. */ export type ImageFormat = 'png' | 'jpeg' | 'webp'; -/** A video being drawn, encoded, and written. */ +/** + * A video being drawn, encoded, and written; created by {@link Shot.video}. Draw frames with + * {@link Video.frame} or {@link Video.record}, then call {@link Video.finish}. + */ export class Video { #state: 'open' | 'finishing' | 'aborting' | 'closed' = 'open'; #operation?: Promise; @@ -178,7 +206,7 @@ export class Video { } } - /** @internal A video of the bindings' `video`, written to `writer`. */ + /** @internal Wraps a bindings video whose bytes go to `writer`. */ static made( video: bindings.Video, writer: WritableStreamDefaultWriter, @@ -191,30 +219,47 @@ export class Video { #check(): bindings.Video { const video = this.#state === 'aborting' || this.#state === 'closed' ? undefined : encoder(this); - if (!video) - throw Object.assign( - thrown({ code: 'cancelled', message: 'this Video was finished or aborted' }), - { - cause: this.#reason, - }, - ); + if (!video) throw this.#ended(); return video; } - /** Seconds into the video the next frame shows at; kept once it is done. */ + /** + * The error for a call after the video ended: `cancelled` with the abort reason as its `cause` + * once aborted, else `invalid`. + */ + #ended(): LatkitError { + return this.#aborted + ? Object.assign(thrown({ code: 'cancelled', message: 'this Video was aborted' }), { + cause: this.#reason, + }) + : thrown({ code: 'invalid', message: 'a Video draws no frames after finish()' }); + } + + /** The time in seconds at which the next frame shows. Still readable after the video ends. */ get time(): number { return this.frames / this.#fps; } - /** Frames drawn or held so far; kept once it is done. */ + /** + * Frames recorded so far, including frames repeated because nothing moved. Still readable after + * the video ends. + */ get frames(): number { return encoder(this)?.frames ?? this.#frames; } - /** Draws and writes one frame, waiting for the output's backpressure. One frame draws at a time. */ + /** + * Draws the next frame once its views are complete, and writes its bytes, waiting on the + * output's backpressure. Calls must not overlap: a call made before the previous one resolves, + * or after `finish()`, rejects with an `invalid` error, and a call after `abort()` with a + * `cancelled` error. A failure aborts the video. + */ frame(): Promise { - if (this.#state !== 'open' || this.#operation) - return Promise.reject(new Error('a Video draws one frame at a time, before it finishes')); + if (this.#state !== 'open') return Promise.reject(this.#ended()); + if (this.#operation) + return Promise.reject( + thrown({ code: 'invalid', message: 'a Video draws one frame at a time' }), + ); const operation = (async () => { const bytes = (await this.#check().frame()) as Uint8Array; this.#frames = this.#check().frames; @@ -231,7 +276,12 @@ export class Video { }); } - /** Records until `step(time)` returns false; any step, drawing, output, or progress error aborts it. */ + /** + * Records frames until `step(time)` returns false, then finishes the video. `step` gets the next + * frame's time in seconds and can change the views before that frame is drawn. `onProgress` is + * called after each frame. An error from `step`, drawing, the output, or `onProgress` aborts the + * video and is rethrown. + */ async record( step: (time: number) => boolean | Promise, onProgress?: (progress: { frames: number; time: number }) => void, @@ -248,10 +298,14 @@ export class Video { } } - /** Finishes pending work, writes the end of the file, and closes and unlocks the output. Idempotent. */ + /** + * Waits for the frame in progress, encodes the remaining frames, writes the end of the file, and + * closes and unlocks the output. Later calls return the same promise. Rejects with a `cancelled` + * error if the video was aborted. + */ finish(): Promise { if (this.#finished) return this.#finished; - if (this.#state !== 'open') return Promise.reject(new Error('this Video was aborted')); + if (this.#state !== 'open') return Promise.reject(this.#ended()); this.#state = 'finishing'; const frame = this.#operation; const operation = (async () => { @@ -274,7 +328,11 @@ export class Video { return this.#finished; } - /** Cancels drawing and encoding, aborts the output, and unlocks it once pending writes settle. Idempotent. */ + /** + * Cancels drawing and encoding, aborts the output with `reason`, and unlocks it once pending + * writes settle. Later calls return the same promise. After `finish()` completes, it does + * nothing. + */ abort(reason?: unknown): Promise { if (this.#aborted) return this.#aborted; if (this.#state === 'closed') return Promise.resolve(); @@ -298,7 +356,7 @@ export class Video { } } -/** The video's encoder, while it has one. */ +/** The video's bindings object, or `undefined` once it is released. */ function encoder(owner: Video): bindings.Video | undefined { try { return inner(owner, 'Video'); diff --git a/npm/src/view.ts b/npm/src/view.ts index 364039de..de4183ed 100644 --- a/npm/src/view.ts +++ b/npm/src/view.ts @@ -1,7 +1,7 @@ /** - * Views: a scene drawn from the sources it is given. What a view keeps is a property, read and set - * alike, setters silent; what the user changes is told by an event named like its property; what the - * view is asked is a method, answered by the frames that draw it. + * Views: a scene drawn from the given sources. A view's state is a set of properties, and setting + * one emits no event. When the user changes that state, the view emits an event named after the + * property. Queries are methods, answered by the frames that draw the view. */ import type * as bindings from '../pkg/latkit.js'; @@ -12,30 +12,38 @@ import type { Failure, Hit, Stats, ViewEvent, Waiting } from './outputs.js'; import { Patch } from './patch.js'; /** - * What a view tells its listeners, by type. Intents (`click`, `open`, `menu`, `drag`, `connect`, - * `delete`) are told once each, in the order the user acted. Changes (`hovered`, `selection`, - * `camera`, `waiting`) are named like the property they changed, and tell only their latest. A view - * tells only what the application did not do itself: its setters change state silently. + * The events a view emits, by type, mapped to each event's detail. Intents (`click`, `open`, + * `menu`, `drag`, `connect`, `delete`) are emitted once each, in the order the user acted. Changes + * (`hovered`, `selection`, `camera`, `waiting`) are named after the property that changed and carry + * only its latest value. An `error` event's detail is a {@link LatkitError}. Setters emit nothing, + * so events report only what the application did not do itself. */ export type ViewEvents = { [E in ViewEvent as E['type']]: E extends { type: 'error' } ? LatkitError : E['detail']; }; -/** What a view is made with: the sources of the datasets its scene names. */ +/** Options for `Gpu.view()`. */ export interface ViewOptions { + /** The datasets the scene draws; see {@link View.sources}. */ sources?: readonly Source[]; } /** CSS pixels kept clear at the top, right, bottom, and left. */ export type Insets = [top: number, right: number, bottom: number, left: number]; -/** A point on screen: CSS pixels from the top left. */ +/** A point on screen, in CSS pixels from the view's top left corner. */ export type Point = [x: number, y: number]; -/** A position in the space's coordinates: x, y, and elevation. */ +/** + * A position in the space's coordinates: x, y, and elevation. On the earth, x and y are longitude + * and latitude; on a plot, the axis coordinate and the value. + */ export type Position = [x: number, y: number, elevation: number]; -/** A scene drawn from the sources it is given: shown by a canvas, which passes it input, or drawn by shots. */ +/** + * A scene drawn from its sources; created by `Gpu.view()`. A canvas shows it and passes it input, + * and a `Shot` draws it into pictures. + */ export class View { #listeners = new Map void>>(); #scene: Scene; @@ -51,12 +59,16 @@ export class View { this.#released = released; } - /** @internal A view of `scene` drawn with `device`, which calls `released` once destroyed. */ + /** @internal Creates a view of `scene` on `device`; `released` is called once it is destroyed. */ static made(device: bindings.Gpu, scene: Scene, released: () => void): View { return new View(device, scene, released); } - /** The scene drawn; setting an invalid one throws and keeps the scene drawn. */ + /** + * The scene drawn. Marks that match a mark of the previous scene by kind, dataset, table, and + * name keep their state and ease to their new options. Setting an invalid scene throws an + * `invalid` error and keeps the current one. + */ get scene(): Scene { return this.#scene; } @@ -67,10 +79,11 @@ export class View { } /** - * What the view draws, each a dataset by name: a `Data`, its latest version from the frame after - * each patch applies; a `Snapshot`, as it is; `Tiles`, the pieces the view asks for. A mark its - * dataset no longer fits draws nothing, and the next frame tells why. Two sources of one name are - * invalid. + * The datasets the view draws, matched to marks by dataset name. The view draws a `Data` at its + * latest version, from the frame after each patch; a `Snapshot` unchanged; and `Tiles` as the + * pieces it needs. A mark whose dataset no longer fits it draws nothing; it is listed in + * `failures`, and the next frame emits an `error` event. Setting two sources with the same name + * throws an `invalid` error. */ get sources(): readonly Source[] { return this.#sources; @@ -81,7 +94,10 @@ export class View { this.#sources = [...sources]; } - /** The axis coordinate shown: sampled fields read their last frame at or before it. */ + /** + * The axis coordinate shown, such as a time. Marks read sampled fields at it, interpolating + * between the frame at or before it and the next, unless an expression uses `step()`. + */ get at(): number { return view(this).at; } @@ -90,7 +106,10 @@ export class View { view(this).at = at; } - /** Where the view looks; setting it looks there at once. */ + /** + * Where the view looks. Setting it moves the camera at once; use {@link View.fly} to ease. A + * camera of the wrong kind for the space, such as a plot camera on a map, is ignored. + */ get camera(): Camera { return JSON.parse(view(this).camera) as Camera; } @@ -99,7 +118,10 @@ export class View { view(this).setCamera(JSON.stringify(camera)); } - /** CSS pixels kept clear at the top, right, bottom, and left, as when panels cover the view. */ + /** + * CSS pixels kept clear at the top, right, bottom, and left, such as where page panels cover the + * view. The camera centers and fits within the rest. + */ get insets(): Insets { const [top, right, bottom, left] = view(this).insets; return [top!, right!, bottom!, left!]; @@ -109,7 +131,10 @@ export class View { view(this).insets = Float32Array.from(insets); } - /** Degrees per second a map or plane turns around its center; 0 stops it. A diagram never turns. */ + /** + * Degrees per second the camera turns about its center on a map or plane; 0 stops it. A diagram + * never turns. + */ get orbit(): number { return view(this).orbit; } @@ -118,7 +143,7 @@ export class View { view(this).orbit = degreesPerSecond; } - /** The rows selected, a set per table; setting it selects those and no others. */ + /** The selected rows, one set per table. Setting it selects exactly those rows. */ get selection(): readonly Rows[] { return JSON.parse(view(this).selection) as Rows[]; } @@ -127,88 +152,107 @@ export class View { view(this).setSelection(JSON.stringify(rows)); } - /** What the user points at: the row under the pointer, or the row the keys stepped to. */ + /** The row under the pointer, or the row keyboard navigation moved to; `null` if none. */ get hovered(): Hit | null { return JSON.parse(view(this).hovered) as Hit | null; } - /** The work under way the last frame stood in for; empty once it is complete. */ + /** + * The kinds of pending work that kept the last frame from being complete; empty when it was + * complete. See {@link Waiting}. + */ get waiting(): Waiting { return JSON.parse(view(this).waiting) as Waiting; } - /** - * Whether the last frame drew everything as the data, scene, and camera say, with nothing standing - * in for work under way: what `waiting` lists. A shot waits for it. - */ + /** Whether the last frame was complete, with nothing in `waiting` pending. Shots wait for it. */ get complete(): boolean { return view(this).complete; } - /** Every mark that draws nothing because its data no longer fits it, and why. */ + /** + * An error for each mark that draws nothing because its data no longer fits it, such as a missing + * table. Each error's `path` names the mark. + */ get failures(): readonly LatkitError[] { return (JSON.parse(view(this).failures) as Failure[]).map(thrown); } - /** What the view does each frame. */ + /** Counts and timings of the view's frames; see {@link Stats}. */ get stats(): Stats { return JSON.parse(view(this).stats) as Stats; } - /** Whether the view wants a frame: it changed, or work it waited on landed. */ + /** Whether the view needs a frame, because it changed or work it was waiting on arrived. */ get wantsFrame(): boolean { return view(this).wantsFrame; } - /** Frames what `fit` names on the next frame, keeping pitch and bearing. */ + /** + * Frames what `fit` names, every row by default, keeping pitch and bearing. It takes effect on + * the next frame, or a frame or two later when the extent must be measured on the GPU. + */ fit(fit: Fit = 'all'): void { view(this).fit(JSON.stringify(fit)); } - /** Eases to `camera`. */ + /** + * Eases the camera to `camera` over twice the scene's ease time, or at once when motion is + * reduced. A camera of the wrong kind for the space is ignored. + */ fly(camera: Camera): void { view(this).fly(JSON.stringify(camera)); } - /** Where `position` lands on screen; `null` behind the camera. */ + /** + * Projects `position` to a point on screen with the current camera, for anchoring HTML overlays; + * `null` when it is behind the camera. + */ screen(position: Position): Point | null { const point = view(this).screen(Float64Array.from(position)); return point ? [point[0]!, point[1]!] : null; } - /** The position on the ground under screen point `point`; `null` where the view shows none there. */ + /** + * The position on the ground under screen point `point`: at elevation 0 on a plane, on the map or + * globe on the earth, the values there on a plot. `null` where the point looks past the ground. + */ position(point: Point): Position | null { const position = view(this).position(Float32Array.from(point)); return position ? [position[0]!, position[1]!, position[2]!] : null; } - /** Where `row` lies on screen, or `null` when no mark draws it in view, once a frame draws the view. */ + /** + * Resolves to where `row` is on screen once a frame draws the view, or `null` when no mark draws + * it in view. A diagram's vertices resolve at once. + */ screenOf(row: Row, wait: Wait = {}): Promise { return waited(view(this).screenOf(JSON.stringify(row)), wait, 'locating a row'); } /** - * Every row of a mark that picks within `radius` CSS pixels (0 unless given) of `point`, nearest - * first, once a frame draws the view. + * Resolves, once a frame draws the view, to every row of a pickable mark within `options.radius` + * CSS pixels (0 by default) of `point`, nearest first. */ async pick(point: Point, options: { radius?: number } & Wait = {}): Promise { const hits = view(this).pick(Float32Array.from(point), options.radius ?? 0); - return JSON.parse(await waited(hits, options, 'a pick')) as Hit[]; + return JSON.parse(await waited(hits, options, 'picking')) as Hit[]; } /** - * The rows joined to `row`, a set per table: a vertex's neighbors across edges and nets, or an - * edge's or a net's vertices. + * The rows connected to `row`, one set per table: a vertex's neighbors across edges and nets, or + * an edge's or a net's vertices. The cost is proportional to the row's degree. */ neighbors(row: Row): Rows[] { return JSON.parse(view(this).neighbors(JSON.stringify(row))) as Rows[]; } /** - * Lays every vertex row without a position out again by the scene's layout: a diagram's blocks, or - * points on a plane or the earth. Places a layout gave otherwise hold, so rows added later are - * placed around the rest. Once the layout lands, answers a patch to each dataset writing each row - * laid out into its mark's position fields; applied, the places are the data's own. + * Runs the scene's layout again for every vertex row without a position: a diagram's blocks, or + * points on a plane or the earth. Otherwise a layout keeps the places it gave, so rows added + * later are placed around the rest. Resolves, once the layout finishes, to one patch per dataset + * that writes each laid-out row's place into its mark's position fields. Applying the patches + * makes the places part of the data. Marks whose position is not read from fields are left out. * * ```ts * for (const patch of await view.arrange()) if (patch.data === grid.name) grid.apply(patch); @@ -220,8 +264,9 @@ export class View { } /** - * The version of dataset `name` the view draws: the latest version of a `Data` it took, a snapshot - * given, or what it holds of `Tiles`, the pieces its camera asked for. + * The version of dataset `name` the view draws, or `undefined` if it draws none of that name: the + * latest version of a `Data` the view has taken, a given snapshot, or the pieces of `Tiles` + * fetched for its camera. */ snapshot(name: string): Snapshot | undefined { const snapshot = view(this).snapshot(name); @@ -229,8 +274,9 @@ export class View { } /** - * Calls `listener` with each event of `type` told while it listens; returns a function that stops - * it. What lasts is state to read, as the property each change is named for. + * Calls `listener` with each event of `type` emitted while it is registered, and returns a + * function that removes it. Past events are not replayed; read the current state from the + * property a change event is named after. */ on(type: T, listener: (event: ViewEvents[T]) => void): () => void { let set = this.#listeners.get(type); @@ -239,7 +285,7 @@ export class View { return () => set.delete(listener); } - /** Hands a batch of the view's events to the listeners of each one's type. */ + /** Dispatches a JSON batch of the view's events to the listeners of each event's type. */ #tell(json: string): void { if (this.#listeners.size === 0) return; for (const told of JSON.parse(json) as ViewEvent[]) { @@ -250,7 +296,10 @@ export class View { } } - /** Frees the view, rejecting what it was asked and has not answered; a canvas showing it keeps its last frame. */ + /** + * Frees the view and rejects its unanswered queries. A canvas showing it keeps its last frame. + * Calling it again does nothing; other methods throw afterwards. + */ destroy(): void { const freed = release(this); if (!freed) return; @@ -269,7 +318,7 @@ function view(owner: View): bindings.View { return inner(owner, 'View'); } -/** @internal The view as a canvas shows it or a shot draws it, each time anew. */ +/** @internal Returns a new bindings handle to the view, for a canvas to show or a shot to draw. */ export function handleOf(owner: View): bindings.Handle { return view(owner).handle(); } diff --git a/npm/src/worker.ts b/npm/src/worker.ts index 96ea85de..ce4a71ce 100644 --- a/npm/src/worker.ts +++ b/npm/src/worker.ts @@ -1,6 +1,7 @@ /** - * A worker doing views' jobs: it instantiates the module the page posts first, then answers each - * job's bytes with its answer's. + * The worker that runs views' jobs. The page's first message is the compiled WebAssembly module, + * which the worker instantiates and acknowledges with `{ ready: true }`. Each later message is a + * job's bytes, answered with `{ bytes }` holding the result. Failures post `{ error }`. */ import { initSync, job } from '../pkg/latkit.js'; diff --git a/npm/test/consumer/main.ts b/npm/test/consumer/main.ts index e9feca38..1bba6af7 100644 --- a/npm/test/consumer/main.ts +++ b/npm/test/consumer/main.ts @@ -12,8 +12,8 @@ function destroyed(action: () => unknown): void { action(); } catch (error) { check( - String(error).includes('destroyed'), - `unexpected disposed-handle error: ${String(error)}`, + error instanceof Error && 'code' in error && error.code === 'destroyed', + `unexpected error from a destroyed handle: ${String(error)}`, ); return; } @@ -82,7 +82,7 @@ async function run(custom: boolean, compiled: boolean) { const decoded = await Data.read(data.toBytes()); const kept = decoded.snapshot(); - // Data reads back; patches stream back to back. + // Queries read data back, and patches apply from a stream split into small chunks. const grid = new Data('grid').apply( new Patch() .axis('time', 's') @@ -124,7 +124,7 @@ async function run(custom: boolean, compiled: boolean) { gpu.canvas(canvas('first'), { layout: [{ view: first }] }); gpu.canvas(canvas('second'), { layout: [{ view: second }] }); secondGpu.canvas(canvas('third'), { layout: [{ view: third }] }); - // A view no canvas shows still draws in shots. + // A view that no canvas shows still draws in shots. const headless = gpu.view(scene); for (const view of [first, second, third, headless]) { view.on('error', (error) => { @@ -132,7 +132,7 @@ async function run(custom: boolean, compiled: boolean) { }); view.sources = view === third ? [kept] : [decoded]; } - decoded.destroy(); // views keep the dataset + decoded.destroy(); // the views keep their own reference to the dataset kept.destroy(); await until(() => [first, second, third].every((view) => view.stats.frames > 0 && view.stats.drawn === 1), diff --git a/npm/test/consumer/quickstart.ts b/npm/test/consumer/quickstart.ts index b46ff5c2..ff0d459b 100644 --- a/npm/test/consumer/quickstart.ts +++ b/npm/test/consumer/quickstart.ts @@ -1,6 +1,6 @@ import { Data, Gpu, Patch, field, type Scene } from 'latkit'; -/** A complete, type-checked example, also rendered by the installed-package test. */ +/** Draws two buses on `canvas`, colored by their voltage. */ export async function quickstart(canvas: HTMLCanvasElement) { const gpu = await Gpu.create(); const grid = new Data('grid').apply( diff --git a/npm/test/data.test.mjs b/npm/test/data.test.mjs index 65fcbfe2..85d497d3 100644 --- a/npm/test/data.test.mjs +++ b/npm/test/data.test.mjs @@ -1,5 +1,5 @@ -// Patches written in TypeScript and read by latkit's WebAssembly: the two halves of the byte -// format agree on every kind of value, and data reads back as it was written. Runs against the +// Tests patches written in TypeScript and read by latkit's WebAssembly. Both sides of the binary +// format must agree on every kind of value, and data must read back unchanged. Runs against the // built package (`node npm/build.mjs compile npm`) or the installed one (LATKIT_TEST_PACKAGE). import assert from 'node:assert/strict'; import fs from 'node:fs'; @@ -14,12 +14,12 @@ const { load, Data, Patch, reference, field } = await import( const wasm = new WebAssembly.Module(fs.readFileSync(path.join(directory, 'pkg/latkit_bg.wasm'))); await load({ wasm }); -/** Every chunk of `bytes`, `size` at a time, as a socket might hand them over. */ +/** Yields `bytes` in chunks of `size`, as a socket might deliver them. */ function* pieces(bytes, size) { for (let at = 0; at < bytes.length; at += size) yield bytes.subarray(at, at + size); } -/** Every id of `table` of `data`, in row order. */ +/** The ids of `table` in `data`, in row order. */ const ids = (data, table) => data.query({ of: table }).ids; test('patches carry every kind of value through the bytes', async () => { @@ -109,8 +109,8 @@ test('patches carry every kind of value through the bytes', async () => { ]); assert.equal(get(102, 'path'), null, 'an empty geometry is missing'); assert.equal(get(103, 'area').length, 2, 'two polygons'); - assert.equal(grid.get('Branch', 0, 'from'), 101, 'a reference reads as the id it names'); - assert.equal(grid.get('Branch', 1, 'to'), null, 'null leaves a reference unwired'); + assert.equal(grid.get('Branch', 0, 'from'), 101, 'a reference reads as the referenced id'); + assert.equal(grid.get('Branch', 1, 'to'), null, 'a null id is a missing reference'); assert.equal(grid.get('Named', 'b', 'x'), 2); const kinds = Object.fromEntries( grid @@ -122,7 +122,7 @@ test('patches carry every kind of value through the bytes', async () => { [kinds.kv, kinds.open, kinds.name, kinds.point, kinds.path, kinds.area], ['number', 'bool', 'text', 'point', 'path', 'polygons'], ); - // A dataset's bytes are the patch that builds it, read back alike. + // A dataset's bytes are the patch that builds it, so reading them back gives an equal dataset. const again = new Data('grid').apply(await Patch.read(grid.toBytes())); assert.deepEqual(again.describe(), grid.describe()); assert.deepEqual(again.get('Bus', 103, 'area'), get(103, 'area')); @@ -141,10 +141,10 @@ test('changes apply in place, a snapshot keeps a version, and a patch names its .append('Bus', 2, { v: [9] }, { ids: [4] }), ); assert.deepEqual(ids(data, 'Bus'), [2, 3, 4]); - assert.deepEqual(ids(before, 'Bus'), [1, 2, 3], 'the snapshot holds'); + assert.deepEqual(ids(before, 'Bus'), [1, 2, 3], 'the snapshot is unchanged'); assert.equal(data.get('Bus', 4, 'x'), 40); - assert.equal(data.get('Bus', 4, 'v', { at: 2 }), 9, 'rows named take the frame'); - assert.equal(data.get('Bus', 2, 'v', { at: 2 }), 4, 'the rest hold their last values'); + assert.equal(data.get('Bus', 4, 'v', { at: 2 }), 9, 'the listed rows take the frame'); + assert.equal(data.get('Bus', 2, 'v', { at: 2 }), 4, 'other rows keep their last values'); assert.deepEqual(data.describe().span, [0, 2]); data.apply(new Patch().forget(1)); assert.deepEqual(data.describe().span, [1, 2]); @@ -158,7 +158,7 @@ test('changes apply in place, a snapshot keeps a version, and a patch names its before.destroy(); }); -test('appends one after another write as one change, each frame copied at its call', () => { +test('consecutive appends write one change and copy each frame when called', () => { const frame = new Float32Array(2); const patch = new Patch().replace('Bus', [1, 2], { x: [1, 2] }); for (let f = 0; f < 4; f++) { @@ -167,11 +167,11 @@ test('appends one after another write as one change, each frame copied at its ca } const bytes = patch.toBytes(); const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); - // After the header and the empty name: the count of changes. - assert.equal(view.getUint32(20, true), 2, 'the replace, and one append of every frame'); + // The count of changes follows the 12-byte header and the empty dataset name's length. + assert.equal(view.getUint32(16, true), 2, 'the replace, and one append for all frames'); const data = new Data('grid').apply(patch); const v = (at) => data.get('Bus', 2, 'v', { at }); - assert.deepEqual([v(0), v(1), v(2), v(3)], [0, 10, 20, 30], 'each frame as it was at its call'); + assert.deepEqual([v(0), v(1), v(2), v(3)], [0, 10, 20, 30], 'each frame copied when appended'); const envelope = data.envelope({ table: 'Bus', field: 'v', range: [0, 4], spans: 2, ids: [2] }); assert.deepEqual([...envelope.at], [0, 2, 4]); assert.deepEqual( @@ -186,7 +186,7 @@ test('appends one after another write as one change, each frame copied at its ca data.destroy(); }); -test('time tiles hold runs of a sampled field, holes between them', async () => { +test('time tiles store runs of a sampled field with gaps between them', async () => { const data = new Data('grid').apply(new Patch().replace('Bus', [1, 2], { x: [1, 2] })); data.apply( new Patch() @@ -195,9 +195,9 @@ test('time tiles hold runs of a sampled field, holes between them', async () => ); assert.deepEqual(data.describe().span, [0, 101]); assert.equal(data.get('Bus', 2, 'v', { at: 101 }), 4); - assert.equal(data.get('Bus', 1, 'v', { at: 50 }), null, 'a hole holds no frame'); + assert.equal(data.get('Bus', 1, 'v', { at: 50 }), null, 'a gap has no frame'); const again = new Patch().frames('Bus', 'v', { first: 100, at: [100], values: [1, 2] }); - assert.throws(() => data.apply(again), { code: 'invalid' }, 'frames held already'); + assert.throws(() => data.apply(again), { code: 'invalid' }, 'frames already stored'); assert.throws( () => new Patch().frames('Bus', 'v', { level: 1, first: 0, at: [0, 7, 9], values: [1] }), { code: 'invalid' }, @@ -209,7 +209,7 @@ test('time tiles hold runs of a sampled field, holes between them', async () => back.destroy(); }); -test('a failed change leaves the patch as it was', () => { +test('a failed change leaves the patch unchanged', () => { const patch = new Patch().set('Bus', [1], { x: [1] }); const bytes = patch.toBytes(); assert.throws(() => patch.set('Bus', [2], { x: [1, 'two'] }), { code: 'invalid', path: 'Bus.x' }); @@ -221,7 +221,7 @@ test('a failed change leaves the patch as it was', () => { data.destroy(); }); -test('patches stream back to back, split anywhere, and refuse what is too long', async () => { +test('patches stream back to back, split anywhere, and reject oversized frames', async () => { const sent = [ new Patch('grid').replace('Bus', [7, 8, 9], { x: [1, 2, 3] }), new Patch().set('Bus', [7], { x: [10] }), @@ -243,12 +243,34 @@ test('patches stream back to back, split anywhere, and refuse what is too long', const long = new Patch('grid').replace('Bus', [1], { name: ['x'.repeat(100)] }).toBytes(); const read = (stream, options) => Array.fromAsync(Patch.stream(stream, options)); await assert.rejects(read(pieces(long, 16), { limit: 64 }), { code: 'limit' }); + await assert.rejects(read(pieces(long, 16), { limit: 4 }), { code: 'invalid', path: 'limit' }); await assert.rejects(read(pieces(long.subarray(0, 40), 16)), /ended inside a patch/); const json = new TextEncoder().encode('{"not": "latkit"}'); await assert.rejects(read([json]), { code: 'invalid' }); }); -test('JSON, latkit bytes, and GeoJSON read as datasets, as their first bytes say', async () => { +test('failed fetches are io errors, and aborting one rejects as cancelled', async (t) => { + const missing = new Response('gone', { status: 404 }); + await assert.rejects(Patch.read(missing), { code: 'io' }); + await assert.rejects(Data.read(new Response('gone', { status: 404 })), { code: 'io' }); + t.mock.method(globalThis, 'fetch', async (url, { signal }) => { + signal?.throwIfAborted(); + throw new TypeError(`${url}: no network`); + }); + await assert.rejects(Patch.read('https://example.invalid/p.latk'), { code: 'io' }); + await assert.rejects(Data.read('https://example.invalid/d.latk'), { code: 'io' }); + // Like a real fetch, this one rejects with the abort's reason before latkit hears of the abort. + globalThis.fetch.mock.mockImplementation( + (url, { signal }) => + new Promise((_, reject) => signal.addEventListener('abort', () => reject(signal.reason))), + ); + const controller = new AbortController(); + const reading = Patch.read('https://example.invalid/p.latk', { signal: controller.signal }); + controller.abort(); + await assert.rejects(reading, { code: 'cancelled' }); +}); + +test('Data.read detects JSON, latkit bytes, and GeoJSON by their first bytes', async () => { const document = { name: 'grid', patch: [{ replace: { table: 'Bus', ids: { count: 2 }, fields: { x: [1, 2] } } }], @@ -271,7 +293,7 @@ test('JSON, latkit bytes, and GeoJSON read as datasets, as their first bytes say ); const places = await Data.read(geojson, { name: 'places' }); assert.deepEqual(places.get('points', 0, 'geometry'), [1, 2]); - await assert.rejects(Data.read(geojson), { code: 'invalid' }, 'GeoJSON takes a name'); + await assert.rejects(Data.read(geojson), { code: 'invalid' }, 'GeoJSON needs a name'); const aborted = AbortSignal.abort(); await assert.rejects(Data.read(geojson, { name: 'places', signal: aborted }), { code: 'cancelled', @@ -294,7 +316,7 @@ test('queries filter, sort, page, and count', () => { limit: 1, }); assert.deepEqual(hot.ids, [8]); - assert.equal(hot.total, 2, 'rows kept before paging'); + assert.equal(hot.total, 2, 'rows matched before paging'); assert.ok(hot.fields.loading instanceof Float64Array); const named = data.query({ of: 'Branch', filter: field('name').contains('ED') }); assert.deepEqual(named.ids, [9], 'contains ignores case'); @@ -302,11 +324,11 @@ test('queries filter, sort, page, and count', () => { data.destroy(); }); -test('LATK v1 fixture and long-running sample indexes agree with Rust', async () => { +test('the patch fixture and frame indexes past 2^32 agree with Rust', () => { const fixture = Buffer.from( fs .readFileSync( - new URL('../../crates/latkit/tests/fixtures/latk-v1.hex', import.meta.url), + new URL('../../crates/latkit/tests/fixtures/patch.hex', import.meta.url), 'utf8', ) .trim(), @@ -319,9 +341,6 @@ test('LATK v1 fixture and long-running sample indexes agree with Rust', async () assert.equal(data.get('T', 0, 'v'), 3); assert.deepEqual(data.describe().span, [0, 2]); data.destroy(); - const version = Uint8Array.from(fixture); - version[4] = 2; - await assert.rejects(Patch.read(version), /version/); assert.throws( () => new Patch().frames('T', 'v', { first: Number.MAX_SAFE_INTEGER, at: [0], values: [1] }), /safe/, @@ -344,7 +363,8 @@ test('stream rejects oversized headers and cancels owned readers on early termin const frame = new Patch().axis('t', 's').toBytes(); const oversized = new Uint8Array(2 << 20); oversized.set(frame); - new DataView(oversized.buffer).setBigUint64(8, BigInt(oversized.length), true); + // The frame's length follows the 4-byte magic. + new DataView(oversized.buffer).setBigUint64(4, BigInt(oversized.length), true); const stream = new ReadableStream({ start(c) { c.enqueue(oversized); diff --git a/npm/test/jobs.test.mjs b/npm/test/jobs.test.mjs index 95c0b01b..6bfadf3f 100644 --- a/npm/test/jobs.test.mjs +++ b/npm/test/jobs.test.mjs @@ -69,7 +69,7 @@ test('failed startup, malformed messages, and timeouts reject instead of hanging const message = assert.rejects(run(new Uint8Array()), /read worker/); workers[1].onmessageerror(); await message; - const timeout = assert.rejects(run(new Uint8Array()), /timed out/); + const timeout = assert.rejects(run(new Uint8Array()), { code: 'timeout' }); t.mock.timers.tick(60_000); await timeout; assert(workers.every((worker) => worker.terminated)); @@ -94,7 +94,7 @@ test('worker construction failure rejects, and later calls can recover', async ( t.mock.timers.tick(30_000); }); -test('jobs at once get a worker each; a failed one rejects only its job; idle ones are reused', async (t) => { +test('concurrent jobs run on separate workers, fail alone, and reuse idle ones', async (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }); const workers = []; const run = jobs(module, () => { @@ -104,7 +104,7 @@ test('jobs at once get a worker each; a failed one rejects only its job; idle on }); const lost = assert.rejects(run(new Uint8Array([0])), /lost worker/); const kept = run(new Uint8Array([1])); - assert.equal(workers.length, 2, 'nothing waits in the page: latkit hands over only what runs'); + assert.equal(workers.length, 2, 'latkit hands over only jobs that can run, so none queue here'); workers[0].onerror({ preventDefault() {}, message: 'lost worker' }); await lost; workers[1].answer({ ready: true }); diff --git a/npm/test/load.test.mjs b/npm/test/load.test.mjs index 56d05ee3..bbfd92d7 100644 --- a/npm/test/load.test.mjs +++ b/npm/test/load.test.mjs @@ -20,7 +20,7 @@ test('installed package imports without WASM, retries failures, and shares initi ); assert.equal(requests, 0); assert.deepEqual(field('x').toJSON(), { field: 'x' }); - // Patches are written without WASM; datasets wait for it. + // Writing a patch needs no WASM. Creating a dataset throws until WASM is loaded. const patch = new Patch().replace('Node', { count: 1 }, { x: [42] }); assert.throws(() => new Data('early'), { code: 'unloaded' }); assert.equal(requests, 0); @@ -47,7 +47,7 @@ test('installed package imports without WASM, retries failures, and shares initi assert.equal(data.get('Node', 0, 'x'), 7); data.destroy(); data.destroy(); - assert.throws(() => data.name, /destroyed/); + assert.throws(() => data.name, { code: 'destroyed' }); assert.equal(kept.get('Node', 0, 'x'), 42, 'a snapshot outlives its source'); kept.destroy(); kept.destroy(); diff --git a/npm/test/release.test.mjs b/npm/test/release.test.mjs index 33c29675..751735a7 100644 --- a/npm/test/release.test.mjs +++ b/npm/test/release.test.mjs @@ -27,7 +27,7 @@ test('a partial release resumes only missing artifacts', async () => { await assert.rejects(ensureArtifacts([{ ...artifacts[0], digest: 'different' }]), /must match/); }); -test('registry failures never masquerade as unpublished versions', async (t) => { +test('registry failures are not mistaken for unpublished versions', async (t) => { t.mock.method(globalThis, 'fetch', async () => new Response(null, { status: 503 })); await assert.rejects(registry('https://example.test'), /503/); t.mock.method(globalThis, 'fetch', async () => new Response(null, { status: 404 })); diff --git a/npm/test/video.test.mjs b/npm/test/video.test.mjs index 958a1a66..5ae941e7 100644 --- a/npm/test/video.test.mjs +++ b/npm/test/video.test.mjs @@ -107,6 +107,7 @@ test('abort interrupts a pending finish and is idempotent', async () => { await Promise.all([aborted, rejected]); assert.equal(stream.locked, false); assert.equal(calls.free, 1); + await assert.rejects(video.frame(), { code: 'cancelled', cause: 'stop' }); }); test('concurrent frames reject without disrupting the frame already drawing', async () => { @@ -121,10 +122,11 @@ test('concurrent frames reject without disrupting the frame already drawing', as }, ); const frame = video.frame(); - await assert.rejects(video.frame(), /one frame/); + await assert.rejects(video.frame(), { code: 'invalid', message: /one frame/ }); resolveFrame(new Uint8Array()); await frame; await video.finish(); + await assert.rejects(video.frame(), { code: 'invalid' }, 'no frames after finish()'); }); test('locked outputs make no encoder, and failing to make one unlocks the output', () => { diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 51209aec..0c17aa92 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,24 +1,29 @@ //! Repository tasks, run as `cargo xtask `. //! -//! - `check`: CI's checks in its order, GPU tests required: format, lint, test, document, and package -//! the crates, check that `native` stays out of browser builds, then format, lint, type-check, and -//! test the npm workspace over a fresh `wasm --dev`; then the browser gate. -//! - `browser`: the browser gate (`npm/examples/gate/gate.ts`) over a fresh `wasm --dev`: every -//! shader variant's pipelines compile in Chrome within budget, and every example page draws and -//! answers the pointer without errors or stalls. On `SwiftShader`, as Linux always is, only that -//! every pipeline compiles: pages and times need a GPU. -//! - `bench [--quick] [--only a,b] [--repeats n] [--update] [--software]`: the -//! benchmark (`benchmark/`) over a release `wasm`: every scenario up its sizes in Chrome while the -//! last one fits, each phase's time, GPU time, stalls, and exact work, held to how it may grow with -//! size, to the committed work, and to the times taken on this machine; a lost device stops the -//! sweep. `--quick` runs each scenario once at its smallest size. See `benchmark/run.ts`. -//! - `wasm [--dev]`: build `latkit-js`, bind it with the `wasm-bindgen` CLI matching `Cargo.lock`, -//! and compile the npm wrapper. `--dev` builds faster, without link-time optimization. -//! - `version`: give the crates the npm package's version, as changesets set it. -//! - `package`: build an isolated, optimized npm tarball, the one `pnpm release` publishes, and -//! report its sizes and hashes. -//! - `package-test`: install the latest tarball into a consumer and verify it in Chrome. -//! - `examples [--open]`: `wasm --dev`, then serve the example suite at ; +//! - `check`: CI's checks in CI's order, with GPU tests required. Formats, lints, tests, documents, +//! and packages the crates, and checks that `native` stays out of browser builds. Then formats, +//! lints, type-checks, and tests the npm workspace over a fresh `wasm --dev`, and runs the +//! browser gate. +//! - `browser`: the browser gate (`npm/examples/gate/gate.ts`) over a fresh `wasm --dev`. Checks +//! that every shader variant's pipelines compile in Chrome within budget, and that every example +//! page draws and responds to the pointer without errors or stalls. On `SwiftShader`, which Linux +//! always uses, checks only that every pipeline compiles, since pages and times need a GPU. +//! - `bench [--quick] [--only a,b] [--repeats n] [--update] [--software]`: the benchmark +//! (`benchmark/`) over a release `wasm`. Runs every scenario at increasing sizes in Chrome while +//! the last size stays within budget. Measures each phase's time, GPU time, stalls, and exact +//! work, and judges them against their allowed growth with size, the committed work, and this +//! machine's times. A lost device stops the sweep. `--quick` runs each scenario once at its +//! smallest size. See `benchmark/run.ts`. +//! - `wasm [--dev]`: builds `latkit-js`, binds it with the `wasm-bindgen` CLI version in +//! `Cargo.lock`, and compiles the npm wrapper. `--dev` builds faster, without link-time +//! optimization. +//! - `version`: sets the crates' version to the npm package's, as changesets set it. +//! - `package`: builds an isolated, optimized npm tarball, the one `pnpm release` publishes, and +//! reports its sizes and hashes. +//! - `package-test`: installs the latest tarball into a consumer and verifies it in Chrome. +//! - `release`: runs `npm/release.mjs`, which verifies or publishes each artifact, then tags the +//! release. +//! - `examples [--open]`: `wasm --dev`, then serves the example suite at . //! `--open` opens it in the browser once it is ready. use std::path::{Path, PathBuf}; @@ -67,7 +72,7 @@ fn main() -> ExitCode { type Result = std::result::Result; fn root() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")).parent().expect("xtask sits in the workspace root").to_path_buf() + Path::new(env!("CARGO_MANIFEST_DIR")).parent().expect("xtask is in the workspace root").to_path_buf() } fn run(program: &str, args: &[&str]) -> Result { @@ -90,7 +95,7 @@ fn pnpm(args: &[&str]) -> Result { run(if cfg!(windows) { "pnpm.cmd" } else { "pnpm" }, args) } -/// What `.github/workflows/ci.yml` checks, in its order, then the browser gate. +/// Runs the checks of `.github/workflows/ci.yml` in its order, then the browser gate. fn check() -> Result { let gpu = [("LATKIT_REQUIRE_GPU", "1")]; cargo(&["fmt", "--all", "--check"])?; @@ -104,7 +109,7 @@ fn check() -> Result { &["doc", "--locked", "-p", "latkit", "--no-deps", "--all-features"], &[("RUSTDOCFLAGS", "-D warnings")], )?; - // CI's tree is clean; here uncommitted work is packaged as it stands. + // CI's tree is clean. Locally, package uncommitted work as is. cargo(&["package", "--locked", "--allow-dirty", "-p", "latkit"])?; cargo(&[ "package", @@ -127,7 +132,7 @@ fn check() -> Result { browser_gate() } -/// Builds the package for the browser, then runs the gate over the shaders the build linked. +/// Builds the wasm package, then runs the browser gate over the shaders the build linked. fn browser() -> Result { wasm(true)?; browser_gate() @@ -138,7 +143,7 @@ fn browser_gate() -> Result { run("node", &["npm/examples/gate/gate.ts", &shaders]) } -/// The directory `latkit`'s build script wrote the linked shaders and `shaders.json` to. +/// The directory where `latkit`'s build script wrote the linked shaders and `shaders.json`. fn shaders_dir() -> Result { eprintln!("$ cargo check --locked -p latkit --message-format=json-render-diagnostics"); let output = Command::new(env!("CARGO")) @@ -200,7 +205,7 @@ fn package() -> Result { } fn package_test() -> Result { - // Preparation prints the isolated consumer path into a stable manifest for the browser runner. + // Creates an isolated consumer and adds its path to the package report for the browser runner. run("node", &["npm/build.mjs", "consumer"])?; let report = std::fs::read_to_string(root().join("target/npm/package.json")).map_err(|e| e.to_string())?; let report: serde_json::Value = serde_json::from_str(&report).map_err(|e| e.to_string())?; @@ -224,8 +229,8 @@ fn package_test() -> Result { run("node", &["npm/build.mjs", "verified"]) } -/// Gives the crates the npm package's version, which changesets sets: one version for everything -/// released together. +/// Sets the crates' version to the npm package's, which changesets sets, so everything released +/// together shares one version. fn version() -> Result { let manifest = std::fs::read_to_string(root().join("npm/package.json")).map_err(|e| format!("npm: {e}"))?; let manifest: serde_json::Value = serde_json::from_str(&manifest).map_err(|e| format!("npm: {e}"))?; From 3bafbae2096562d0bc195803e869ca5f3484fc3d Mon Sep 17 00:00:00 2001 From: Luke Lowery Date: Mon, 26 Oct 2026 13:22:39 -0500 Subject: [PATCH 07/14] fix one rendering issue --- .changeset/sources.md | 1 + benchmark/baselines/work.json | 114 +++++++++++++++--------------- crates/latkit/src/derive/sight.rs | 47 ++++++++++-- crates/latkit/tests/scale.rs | 30 ++++++++ 4 files changed, 129 insertions(+), 63 deletions(-) diff --git a/.changeset/sources.md b/.changeset/sources.md index 7ee4859c..a285eaaa 100644 --- a/.changeset/sources.md +++ b/.changeset/sources.md @@ -29,4 +29,5 @@ Views draw from sources, every wait can be cancelled, colors and lengths are typ - Removed: the `'mercator'` and `'globe'` spaces (use `earth`); flat theme keys such as `vertexRadius`, `fontSize`, and `edgeSpacing`; reading a string as a color only when it begins with `#`; `Flow.units`; shadows on edges, lines, areas, and faces, and the theme's `shadow.light` and `shadow.softness`. - Fixed: a view whose next version does not fit releases what it draws so that version can load; when nothing fits, it reports a `limit` error and stops. - Fixed: on the earth, sizes in `units(n)` are degrees, as comet lengths already were, not mercator radians. +- Fixed: zoomed in close on the globe, paged tables and vector tiles keep drawing their finer tiles; past about zoom 12 they fell back to the coarsest. - Fixed: a text the host cannot rasterize reports one error, not one every frame it shows, and a text with nothing to draw is no error. diff --git a/benchmark/baselines/work.json b/benchmark/baselines/work.json index 8a4f7b62..d1291296 100644 --- a/benchmark/baselines/work.json +++ b/benchmark/baselines/work.json @@ -34,7 +34,7 @@ "load": { "uploaded": 200000, "uploads": 15, - "written": 3124, + "written": 3956, "evaluated": 10000, "resolved": 0, "cleared": 40960, @@ -507,7 +507,7 @@ "load": { "uploaded": 2000000, "uploads": 125, - "written": 4660, + "written": 3828, "evaluated": 100000, "resolved": 0, "cleared": 401408, @@ -1145,7 +1145,7 @@ "load": { "uploaded": 36352, "uploads": 7, - "written": 7272, + "written": 8360, "evaluated": 3008, "resolved": 1984, "cleared": 12288, @@ -1312,7 +1312,7 @@ "load": { "uploaded": 358400, "uploads": 25, - "written": 8528, + "written": 7440, "evaluated": 29800, "resolved": 19800, "cleared": 122880, @@ -1479,7 +1479,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 10376, + "written": 9288, "evaluated": 298936, "resolved": 199080, "cleared": 1204224, @@ -1646,7 +1646,7 @@ "load": { "uploaded": 4980736, "uploads": 304, - "written": 17280, + "written": 16192, "evaluated": 290816, "resolved": 69632, "cleared": 835584, @@ -2012,14 +2012,14 @@ "zoom": { "uploaded": 0, "uploads": 0, - "written": 5151984, + "written": 5147672, "evaluated": 0, "resolved": 0, "cleared": 0, "packed": 0, "readbacks": 40, "jobs": 0, - "texts": 1230, + "texts": 1229, "pipelines": 0, "late": 0, "resident": 1269760 @@ -2059,7 +2059,7 @@ "load": { "uploaded": 2400000, "uploads": 150, - "written": 1428688, + "written": 1431184, "evaluated": 100000, "resolved": 0, "cleared": 3612672, @@ -2089,14 +2089,14 @@ "zoom": { "uploaded": 0, "uploads": 0, - "written": 6721996, + "written": 6686612, "evaluated": 0, "resolved": 0, "cleared": 0, "packed": 0, "readbacks": 40, "jobs": 0, - "texts": 1522, + "texts": 1514, "pipelines": 0, "late": 0, "resident": 12443648 @@ -2136,7 +2136,7 @@ "load": { "uploaded": 1966080, "uploads": 120, - "written": 2926340, + "written": 2929668, "evaluated": 81920, "resolved": 0, "cleared": 4653056, @@ -2166,14 +2166,14 @@ "zoom": { "uploaded": 4718592, "uploads": 288, - "written": 11551716, + "written": 11507124, "evaluated": 196608, "resolved": 0, "cleared": 12582912, "packed": 1179648, "readbacks": 44, "jobs": 0, - "texts": 1426, + "texts": 1420, "pipelines": 0, "late": 0, "resident": 27983872 @@ -2307,7 +2307,7 @@ "load": { "uploaded": 12256, "uploads": 7, - "written": 6192, + "written": 7328, "evaluated": 1016, "resolved": 1000, "cleared": 4352, @@ -2829,7 +2829,7 @@ "load": { "uploaded": 18151952, "uploads": 645, - "written": 11664, + "written": 12496, "evaluated": 10000, "resolved": 0, "cleared": 40960, @@ -2951,7 +2951,7 @@ "load": { "uploaded": 40413432, "uploads": 1561, - "written": 21412, + "written": 27236, "evaluated": 30000, "resolved": 0, "cleared": 122880, @@ -3075,7 +3075,7 @@ "load": { "uploaded": 277592, "uploads": 12, - "written": 2932, + "written": 3764, "evaluated": 80, "resolved": 0, "cleared": 256, @@ -3085,7 +3085,7 @@ "texts": 0, "pipelines": 29, "late": 0, - "resident": 304896 + "resident": 304640 }, "pan": { "uploaded": 437472, @@ -3103,19 +3103,19 @@ "resident": 781824 }, "zoom": { - "uploaded": 792992, - "uploads": 43, - "written": 36000, - "evaluated": 1136, + "uploaded": 746424, + "uploads": 39, + "written": 36088, + "evaluated": 1152, "resolved": 0, "cleared": 1024, "packed": 0, "readbacks": 0, - "jobs": 27, + "jobs": 25, "texts": 0, "pipelines": 0, "late": 0, - "resident": 1673472 + "resident": 1621760 } }, "256": { @@ -3165,8 +3165,8 @@ "resident": 719872 }, "pan": { - "uploaded": 648192, - "uploads": 32, + "uploaded": 644096, + "uploads": 31, "written": 35288, "evaluated": 8192, "resolved": 0, @@ -3180,19 +3180,19 @@ "resident": 1449984 }, "zoom": { - "uploaded": 1103232, - "uploads": 77, - "written": 36600, - "evaluated": 18688, + "uploaded": 1134336, + "uploads": 79, + "written": 36476, + "evaluated": 18432, "resolved": 0, "cleared": 16384, "packed": 0, "readbacks": 0, - "jobs": 22, + "jobs": 21, "texts": 0, "pipelines": 0, "late": 0, - "resident": 3207168 + "resident": 3264512 } }, "4096": { @@ -3229,7 +3229,7 @@ "load": { "uploaded": 2638176, "uploads": 258, - "written": 6632, + "written": 7640, "evaluated": 16384, "resolved": 0, "cleared": 65536, @@ -3242,34 +3242,34 @@ "resident": 3194880 }, "pan": { - "uploaded": 1948272, - "uploads": 192, - "written": 44912, - "evaluated": 77824, + "uploaded": 1286320, + "uploads": 128, + "written": 43048, + "evaluated": 57344, "resolved": 0, - "cleared": 114688, + "cleared": 98304, "packed": 0, "readbacks": 0, "jobs": 8, "texts": 0, "pipelines": 0, "late": 0, - "resident": 5464064 + "resident": 4759552 }, "zoom": { - "uploaded": 1747792, - "uploads": 260, - "written": 52420, - "evaluated": 65536, + "uploaded": 2359152, + "uploads": 324, + "written": 53108, + "evaluated": 77824, "resolved": 0, - "cleared": 98304, + "cleared": 114688, "packed": 0, "readbacks": 0, "jobs": 11, "texts": 0, "pipelines": 0, "late": 0, - "resident": 8028160 + "resident": 8060928 } } }, @@ -3308,7 +3308,7 @@ "load": { "uploaded": 25440, "uploads": 22, - "written": 9880, + "written": 7704, "evaluated": 300, "resolved": 0, "cleared": 5120, @@ -3400,7 +3400,7 @@ "load": { "uploaded": 254960, "uploads": 50, - "written": 16104, + "written": 18280, "evaluated": 3000, "resolved": 0, "cleared": 40960, @@ -3492,7 +3492,7 @@ "load": { "uploaded": 2484264, "uploads": 366, - "written": 89544, + "written": 90632, "evaluated": 30000, "resolved": 0, "cleared": 409600, @@ -3586,7 +3586,7 @@ "load": { "uploaded": 49152, "uploads": 2, - "written": 43948, + "written": 39916, "evaluated": 10, "resolved": 0, "cleared": 256, @@ -4093,7 +4093,7 @@ "load": { "uploaded": 20000, "uploads": 3, - "written": 51248, + "written": 55280, "evaluated": 1, "resolved": 0, "cleared": 256, @@ -4512,7 +4512,7 @@ "load": { "uploaded": 1638400, "uploads": 100, - "written": 630104, + "written": 629272, "evaluated": 98304, "resolved": 0, "cleared": 5296640, @@ -4527,7 +4527,7 @@ "move": { "uploaded": 655360, "uploads": 40, - "written": 249104, + "written": 253264, "evaluated": 49152, "resolved": 0, "cleared": 557056, @@ -4700,7 +4700,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 36880, + "written": 41232, "evaluated": 1195744, "resolved": 796320, "cleared": 4816896, @@ -4762,7 +4762,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 71568, + "written": 83536, "evaluated": 2391488, "resolved": 1592640, "cleared": 9666560, @@ -4903,7 +4903,7 @@ "load": { "uploaded": 3588096, "uploads": 219, - "written": 34672, + "written": 35760, "evaluated": 208896, "resolved": 49152, "cleared": 589824, @@ -5166,7 +5166,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 15184, + "written": 19536, "evaluated": 597872, "resolved": 398160, "cleared": 2408448, diff --git a/crates/latkit/src/derive/sight.rs b/crates/latkit/src/derive/sight.rs index 4142297a..e9a8b3e9 100644 --- a/crates/latkit/src/derive/sight.rs +++ b/crates/latkit/src/derive/sight.rs @@ -123,6 +123,16 @@ impl Sight<'_> { (w - center).dot(self.projected.eye - w) > 0.0 } + /// The part of `domain`'s square that one CSS pixel spans at the camera's scale: the earth's + /// square is a turn of longitude, and a plane's is `size` position units. + fn pixel(&self, domain: &Domain) -> f64 { + let square = match *domain { + Domain::Earth => std::f64::consts::TAU, + Domain::Plane { size, .. } => size, + }; + 1.0 / (square * self.projected.px_per_unit[0]).max(f64::MIN_POSITIVE) + } + /// Where tiles draw largest: the camera's center, and the ground at the bottom middle of the /// view, which is the nearest ground when the camera is tilted. pub(crate) fn focus(&self) -> Vec<[f64; 2]> { @@ -212,13 +222,14 @@ impl Sight<'_> { } /// How large tile `key` of `domain`'s square draws, in CSS pixels, at its point nearest each - /// focus, and how far that point is from the view's center. It steps 1/64 of the tile in the - /// square, where all tiles of a level are the same size, so on a flat map every tile of a level - /// measures the same wherever it lies. + /// focus, and how far that point is from the view's center. It scales up how far a step of at + /// most a pixel moves on screen there. A longer step from a large tile could pass the globe's + /// horizon, where nothing projects, once the camera is close to the ground. On a flat map every + /// tile of a level measures the same wherever it lies. pub(crate) fn measure(&self, focus: &[[f64; 2]], domain: &Domain, key: TileKey) -> (f64, f64) { let side = 1.0 / f64::from(1_u32 << key.z); let (west, north) = (f64::from(key.x) * side, f64::from(key.y) * side); - let step = side / 64.0; + let step = (side / 64.0).min(self.pixel(domain)); let (mut size, mut distance) = (0.0_f64, f64::INFINITY); for &f in focus { let [fx, fy] = domain.unit(f); @@ -226,8 +237,9 @@ impl Sight<'_> { let fx = if *domain == Domain::Earth { fx + (west + side / 2.0 - fx).round() } else { fx }; let at = [fx.clamp(west, west + side - step), fy.clamp(north, north + side - step)]; let Some(p) = self.project(domain.position(at)) else { continue }; - let reach = - |q: [f64; 2]| self.project(domain.position(q)).map_or(0.0, |q| 64.0 * (q[0] - p[0]).hypot(q[1] - p[1])); + let reach = |q: [f64; 2]| { + self.project(domain.position(q)).map_or(0.0, |q| side / step * (q[0] - p[0]).hypot(q[1] - p[1])) + }; size = size.max(reach([at[0] + step, at[1]])).max(reach([at[0], at[1] + step])); distance = distance.min((p[0] - self.size[0] / 2.0).hypot(p[1] - self.size[1] / 2.0)); } @@ -274,4 +286,27 @@ mod tests { let b = plane.bounds(TileKey { z: 1, x: 0, y: 0 }, 0.0); assert!(b[0] < 0.0 && b[3] > 5.0, "the northwest quarter: {b:?}"); } + + #[test] + fn tiles_under_the_focus_measure_their_size_on_the_globe_at_any_zoom() { + let globe = crate::Space::Earth(crate::Earth::default().projection(crate::Projection::Globe)); + let size = [800.0, 600.0]; + let center = [-74.0, 40.7]; + // Past zoom 12 the camera's horizon is nearer than a sixty-fourth of the root tile. + for zoom in [3.0, 8.0, 13.0, 18.0, 22.0] { + let camera = crate::Camera::Map { center: [center[0], center[1], 0.0], zoom, pitch: 0.0, bearing: 0.0 }; + let projected = Projected::new(&globe, &camera, size, [0.0, 0.0, size[0], size[1]], 1.0); + let sight = Sight { projected: &projected, size, earth: true }; + let [u, v] = Domain::Earth.unit(center); + for z in [0_u8, 6, 12] { + let n = f64::from(1_u32 << z); + let key = TileKey { z, x: (u * n) as u32, y: (v * n) as u32 }; + let (px, _) = sight.measure(&sight.focus(), &Domain::Earth, key); + // A large tile measures where it is largest: south of the center, a mercator step + // covers more of the globe. + let nominal = 256.0 * 2_f64.powf(zoom) / n; + assert!((0.95..1.3).contains(&(px / nominal)), "zoom {zoom}, level {z}: {px} px, not {nominal}"); + } + } + } } diff --git a/crates/latkit/tests/scale.rs b/crates/latkit/tests/scale.rs index 61b14e06..2436a20a 100644 --- a/crates/latkit/tests/scale.rs +++ b/crates/latkit/tests/scale.rs @@ -122,6 +122,36 @@ fn a_paged_table_draws_the_tiles_in_view_and_its_selected_rows() { assert_eq!(view.stats().drawn, stats.drawn + 1, "the selected row draws regardless of its tile"); } +#[test] +fn a_paged_table_on_the_globe_draws_its_finest_tiles_close_up() { + let Some(gpu) = gpu() else { return }; + // 300,000 buses 0.0002° apart, all in one cell of the coarsest tiles, so only fine tiles hold + // the buses in view. + let (columns, rows) = (600_usize, 500_usize); + let n = columns * rows; + let data = Data::new("grid"); + data.apply( + Table::new("Bus", Ids::count(n as u32)) + .field("lon", (0..n).map(|i| -74.06 + 0.0002 * (i % columns) as f64).collect::>()) + .field("lat", (0..n).map(|i| 40.65 + 0.0002 * (i / columns) as f64).collect::>()), + ) + .expect("valid"); + let earth = latkit::Earth { graticule: false, ..latkit::Earth::default() }.projection(latkit::Projection::Globe); + let scene = Scene::new(earth) + .theme(plain_theme()) + .ease(std::time::Duration::ZERO) + .draw(Vertices::of("grid", "Bus").position("lon", "lat").radius(2.0).color(Color::WHITE)); + let mut view = View::new(&gpu, scene).expect("valid scene"); + // So close that the camera's horizon is nearer than a sixty-fourth of the coarsest tile. + view.set_camera(Camera::Map { center: [-74.0, 40.7, 0.0], zoom: 15.0, pitch: 0.0, bearing: 0.0 }); + view.set_sources([&data]).expect("a source"); + let pixels = frame(&gpu, &mut view); + assert_eq!(errors(&mut view), []); + let lit = pixels.iter().filter(|p| p[0] > 128).count(); + assert!(view.stats().drawn > 100, "the buses in view draw: {}", view.stats().drawn); + assert!(lit > 1000, "the buses in view show: {lit} lit pixels"); +} + #[test] fn panning_a_paged_table_draws_every_row_in_view_as_tiles_change() { let Some(gpu) = gpu() else { return }; From f8606265c37f1588a7d29999f58634ca039d00d1 Mon Sep 17 00:00:00 2001 From: Luke Lowery Date: Mon, 26 Oct 2026 15:03:36 -0500 Subject: [PATCH 08/14] improve api doc --- .changeset/sources.md | 1 + .prettierignore | 5 + crates/latkit-cli/src/args.rs | 211 +- crates/latkit/src/data/forms.rs | 4 +- crates/latkit/src/error.rs | 14 +- crates/latkit/src/expr/form.rs | 29 +- docs/_static/custom.css | 107 + docs/conf.py | 2 +- docs/reference/api.md | 2697 +++++++++-------- docs/reference/cli.md | 539 ++-- docs/reference/types.md | 4935 ++++++++++++++++++------------- npm/docs.mjs | 1063 +++++-- npm/schema.json | 49 +- npm/schema.mjs | 18 +- npm/src/canvas.ts | 2 + npm/src/data.ts | 33 +- npm/src/gpu.ts | 1 + npm/src/inputs.ts | 133 +- npm/src/internal.ts | 29 +- npm/src/outputs.ts | 69 +- npm/src/patch.ts | 8 +- npm/src/scene.ts | 46 +- npm/src/shot.ts | 36 +- npm/src/view.ts | 1 + 24 files changed, 6089 insertions(+), 3943 deletions(-) diff --git a/.changeset/sources.md b/.changeset/sources.md index a285eaaa..29796583 100644 --- a/.changeset/sources.md +++ b/.changeset/sources.md @@ -12,6 +12,7 @@ Views draw from sources, every wait can be cancelled, colors and lengths are typ - Added: `px(n)` and `units(n)` give a length's unit. Lengths default to CSS pixels on planes, the earth, and plots, and to position units in diagrams. A stroke in pixels keeps its width as a diagram zooms. - Added: the theme in groups: `text`, `vertex`, `edge`, `line`, `area`, `focus`, `flow`, `diagram`, `earth`, `plot`. A comet's `flow.spacing` and `flow.length` take `{ px }` or `{ units }`. `diagram.outline`, `corner`, `portSize`, and `bend` style blocks, groups, ports, and wire bends; `shadow: true` on blocks and groups casts a soft shadow `diagram.shadow.lift` below them. - Added: `Gpu.resident` and `Gpu.uploading`, the bytes on the GPU and waiting to upload across all its views; `Work.packed`, `Work.resolved`, `Work.cleared`, and `Work.uploads`. +- Added: the package's types document every member, every value of an enumeration, and every form of a union. - Changed: `view.scene`, `view.selection`, `view.screen(position)`, `view.position(point)`, `view.screenOf(row)`, `view.waiting`, and the `hovered` and `selection` events replace the methods and events removed below. - Changed: every error the package throws is a `LatkitError` with a `code`. A failed fetch or an error status is `io`; a call on a destroyed object has the new code `destroyed`; a bad `Patch.stream` limit and a `Video` drawn out of turn are `invalid`. Errors from the video encoder and worker jobs keep their codes, and an abort during a fetch rejects as `cancelled` or `timeout`. - Changed: latkit's bytes carry no version or flags. A frame's header is the magic `LATK` and the frame's length, 12 bytes; files written by earlier versions no longer open. diff --git a/.prettierignore b/.prettierignore index 32819226..52add60d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -17,3 +17,8 @@ benchmark/baselines # Written by the schema test: LATKIT_BLESS=1 cargo test -p latkit --test schema. npm/schema.json docs/_static/theme.json +# Written by node npm/docs.mjs and LATKIT_BLESS=1 cargo test -p latkit-cli, in MyST's definition +# lists, which Prettier would break. +docs/reference/api.md +docs/reference/types.md +docs/reference/cli.md diff --git a/crates/latkit-cli/src/args.rs b/crates/latkit-cli/src/args.rs index a781ff59..d0285876 100644 --- a/crates/latkit-cli/src/args.rs +++ b/crates/latkit-cli/src/args.rs @@ -10,7 +10,7 @@ use latkit::{ImageFormat, Pixels, Shot}; #[derive(Parser)] #[command(name = "latkit", version, about = "Render scenes of JSON or binary data to images and videos")] pub(crate) struct Cli { - /// Format of errors and render progress on stderr. JSON writes one object per line + /// Format of errors and render progress on stderr #[arg(long, global = true, value_enum, default_value = "human")] pub(crate) diagnostics: Diagnostics, #[command(subcommand)] @@ -19,7 +19,9 @@ pub(crate) struct Cli { #[derive(Clone, Copy, PartialEq, Eq, ValueEnum)] pub(crate) enum Diagnostics { + /// Lines of text, for people Human, + /// One JSON object per line, for programs Json, } @@ -31,7 +33,7 @@ pub(crate) enum Command { Validate(Job), /// Describe datasets as JSON: tables, row counts, fields with kinds, and the axis and its span Inspect { - /// Dataset files, each [NAME=]FILE (- reads stdin) + /// Dataset files, each `[NAME=]FILE` (`-` reads stdin) #[arg(required = true, value_name = "[NAME=]FILE")] data: Vec, /// The longest a dataset may take to load, in seconds @@ -46,21 +48,21 @@ pub(crate) enum Command { }, /// Print the rows a query selects, sorted and paged, as JSON Lines with one object per row Query { - /// The dataset, [NAME=]FILE (- reads stdin) + /// The dataset, `[NAME=]FILE` (`-` reads stdin) #[arg(value_name = "[NAME=]FILE")] data: Source, - /// The query as JSON, such as {"of":"Branch","filter":{"op":"gt","args":["loading",90]}}, - /// or a file containing it (- reads stdin) + /// The query as JSON, such as `{"of":"Branch","filter":{"op":"gt","args":["loading",90]}}`, + /// or a file containing it (`-` reads stdin) #[arg(value_name = "QUERY")] query: String, /// The longest the dataset may take to load, in seconds #[arg(long, default_value = "30", value_parser = deadline)] deadline: Duration, }, - /// Write a dataset as time tiles: frames.json (which --tiles opens), the dataset without its + /// Write a dataset as time tiles: `frames.json` (which `--tiles` opens), the dataset without its /// sampled fields' histories, and those histories in spans at every level of detail Tile { - /// The dataset, [NAME=]FILE (- reads stdin) + /// The dataset, `[NAME=]FILE` (`-` reads stdin) #[arg(value_name = "[NAME=]FILE")] data: Source, /// Directory to write the tiles to @@ -78,18 +80,18 @@ pub(crate) enum Command { /// The inputs and output of `render` and `validate`. #[derive(Args)] pub(crate) struct Job { - /// Scene JSON files, one per panel (- reads stdin) + /// Scene JSON files, one per panel (`-` reads stdin) #[arg(required = true, value_name = "SCENE")] pub(crate) scenes: Vec, - /// A dataset file: a JSON document, latkit bytes, or GeoJSON. NAME= sets its name, which + /// A dataset file: a JSON document, latkit bytes, or GeoJSON. `NAME=` sets its name, which /// GeoJSON requires #[arg(short, long, value_name = "[NAME=]FILE")] pub(crate) data: Vec, - /// Tiles every view fetches as dataset NAME: a time tiles frames.json, or a vector tiles - /// PMTiles file, TileJSON, or {z}/{x}/{y} template + /// Tiles every view fetches as dataset `NAME`: a time tiles `frames.json`, or a vector tiles + /// PMTiles file, TileJSON, or `{z}/{x}/{y}` template #[arg(long, value_name = "NAME=FILE")] pub(crate) tiles: Vec, - /// Output file, %d pattern for an image sequence, or - for stdout + /// Output file, `%d` pattern for an image sequence, or `-` for stdout #[arg(short, long, value_name = "FILE")] pub(crate) output: Option, /// Output format. Defaults to the one the output's extension names, else PNG @@ -98,13 +100,14 @@ pub(crate) struct Job { /// JPEG quality, from 1 (smallest) to 100 (sharpest) #[arg(long, default_value = "90", value_parser = clap::value_parser!(u8).range(1..=100))] pub(crate) quality: u8, - /// The picture's width and height in pixels, such as 1920x1080 + /// The picture's width and height in pixels, such as `1920x1080` #[arg(long, default_value = "1600x900", value_parser = size)] pub(crate) size: [u32; 2], /// Pixels per CSS pixel. Views lay out size / scale CSS pixels, so 2 draws twice as sharp #[arg(long, default_value = "1", value_parser = positive)] pub(crate) scale: f32, - /// A scene's panel of the picture, as X,Y,WIDTH,HEIGHT fractions. Give one per scene, in order + /// A scene's panel of the picture: its x, y, width, and height, as fractions. Give one per + /// scene, in order #[arg(long, value_name = "X,Y,W,H", value_parser = panel)] pub(crate) panel: Vec<[f32; 4]>, /// Keep alpha where the themes' backgrounds would be @@ -116,10 +119,10 @@ pub(crate) struct Job { /// A TTF, OTF, or TTC font file. Repeat for fallbacks. Replaces the system fonts #[arg(long, value_name = "FILE")] pub(crate) font: Vec, - /// A camera JSON file per scene, in scene order. Views without one use --fit + /// A camera JSON file per scene, in scene order. Views without one use `--fit` #[arg(long, value_name = "FILE")] pub(crate) camera: Vec, - /// What views without a camera frame: "all", or a fit as JSON + /// What views without a camera frame: `all`, or a fit as JSON #[arg(long, default_value = "all", value_name = "FIT")] pub(crate) fit: String, /// CSS pixels to keep clear at the top, right, bottom, and left @@ -131,7 +134,7 @@ pub(crate) struct Job { /// A still's fixed effect time in seconds, for elapsed-time expressions and flowing comets #[arg(long, value_parser = seconds, conflicts_with = "frames")] pub(crate) elapsed: Option, - /// Frames as JSON Lines, one line per output frame (- reads stdin) + /// Frames as JSON Lines, one line per output frame (`-` reads stdin) #[arg(long, value_name = "FILE")] pub(crate) frames: Option, /// Output frames per second @@ -164,9 +167,9 @@ pub(crate) enum Input { /// An output format. #[derive(Clone, Copy, PartialEq, Eq, ValueEnum)] pub(crate) enum Format { - /// PNG, lossless with alpha. One picture, or one per frame with a %d output pattern + /// PNG, lossless with alpha. One picture, or one per frame with a `%d` output pattern Png, - /// JPEG at --quality, without alpha. One picture, or one per frame + /// JPEG at `--quality`, without alpha. One picture, or one per frame Jpeg, /// Lossless WebP with alpha. One picture, or one per frame Webp, @@ -370,29 +373,171 @@ fn insets(value: &str) -> Result<[f32; 4], String> { #[cfg(test)] mod tests { + use std::collections::HashMap; + use std::fmt::Write as _; + + use clap::ArgAction; + use super::*; + /// The command reference, `docs/reference/cli.md`, in the form of the JavaScript reference: + /// each command's usage and description, then its arguments and options, each a term over its + /// definition with an anchor of its own. Code spans that name a command or an option link to it. + fn reference() -> String { + let mut cli = Cli::command(); + cli.build(); + // Each command, with its name and anchor. Clap's `help` command and arguments are left out. + let mut commands = vec![("latkit".to_owned(), "cli-latkit".to_owned(), cli.clone())]; + for command in cli.get_subcommands().filter(|c| c.get_name() != "help") { + let name = command.get_name(); + commands.push(( + format!("latkit {name}"), + format!("cli-{name}"), + command.clone().bin_name(format!("latkit {name}")), + )); + } + let shown = |top: bool, arg: &clap::Arg| { + let clap = matches!( + arg.get_action(), + ArgAction::Help | ArgAction::HelpShort | ArgAction::HelpLong | ArgAction::Version + ); + !clap && !arg.is_hide_set() && (top || !arg.is_global_set()) + }; + let id = + |anchor: &str, arg: &clap::Arg| format!("{anchor}-{}", arg.get_long().unwrap_or(arg.get_id().as_str())); + // What code spans link to: each command, and each option, first in its own command. + let mut links = HashMap::new(); + for (name, anchor, command) in &commands { + links.insert(name.clone(), anchor.clone()); + for arg in command.get_arguments().filter(|a| shown(anchor == "cli-latkit", a)) { + if let Some(long) = arg.get_long() { + links.entry(format!("--{long}")).or_insert_with(|| id(anchor, arg)); + } + } + } + let mut text = String::from( + "# Command reference\n\nEvery command and option of the `latkit` command line, written from its help. The \ + [command-line guide](../cli.md) shows them at work. `--help` prints a command's help in the terminal, and \ + `latkit --version` prints the version.\n", + ); + for (name, anchor, command) in &mut commands { + let top = anchor.as_str() == "cli-latkit"; + let own = |arg: &clap::Arg| arg.get_long().map(|long| (format!("--{long}"), id(anchor, arg))); + let mut local = links.clone(); + local.extend(command.get_arguments().filter(|a| shown(top, a)).filter_map(own)); + let usage = command.render_usage().to_string(); + let usage = usage.strip_prefix("Usage: ").expect("clap's usage"); + assert!(!usage.contains('\n'), "{name}: one line of usage"); + let about = command.get_long_about().or(command.get_about()).map(ToString::to_string).unwrap_or_default(); + write!( + text, + "\n({anchor})=\n\n## `{name}`\n\n{{.signature}}\n`{usage}`\n\n{}\n", + linked(&sentence(&about), &local) + ) + .unwrap(); + if top { + text += "\n```{rubric} Commands\n```\n"; + for sub in command.get_subcommands().filter(|c| c.get_name() != "help") { + let about = sub.get_about().map(ToString::to_string).unwrap_or_default(); + let term = format!("[`latkit {0}`](#cli-{0})", sub.get_name()); + write!(text, "\n{}\n", item(None, &term, &linked(&sentence(&about), &local))).unwrap(); + } + } + for (title, positional) in [("Arguments", true), ("Options", false)] { + let args: Vec<&clap::Arg> = + command.get_arguments().filter(|a| shown(top, a) && a.is_positional() == positional).collect(); + if args.is_empty() { + continue; + } + write!(text, "\n```{{rubric}} {title}\n```\n").unwrap(); + for arg in args { + let id = id(anchor, arg); + let mut about = + sentence(&arg.get_long_help().or(arg.get_help()).map(ToString::to_string).unwrap_or_default()); + let defaults: Vec = + arg.get_default_values().iter().map(|v| format!("`{}`", v.to_string_lossy())).collect(); + if arg.get_action().takes_values() && !defaults.is_empty() { + write!(about, " Defaults to {}.", defaults.join(", ")).unwrap(); + } + if top && arg.is_global_set() { + about += " Every command takes it."; + } + let mut definition = linked(&about, &local); + let values = if arg.get_action().takes_values() { arg.get_possible_values() } else { Vec::new() }; + for value in values.iter().filter(|v| !v.is_hide_set()) { + let help = value.get_help().map(ToString::to_string).unwrap_or_default(); + assert!(!help.is_empty(), "{name} {}: value {} has no help", id, value.get_name()); + let term = format!("**`{}`**", value.get_name()); + let value_id = format!("{id}-{}", value.get_name()); + write!(definition, "\n\n{}", item(Some(&value_id), &term, &linked(&sentence(&help), &local))) + .unwrap(); + } + write!(text, "\n{}\n", item(Some(&id), &term(arg), &definition)).unwrap(); + } + } + } + text + } + + /// An argument's or an option's term: `**...**`, or `-d, **--data** <[NAME=]FILE>`. + fn term(arg: &clap::Arg) -> String { + let value = arg + .get_value_names() + .and_then(|names| names.first()) + .map_or_else(|| arg.get_id().as_str().to_uppercase(), ToString::to_string); + if arg.is_positional() { + let many = if arg.get_num_args().is_some_and(|n| n.max_values() > 1) { "..." } else { "" }; + let value = if arg.is_required_set() { format!("<{value}>") } else { format!("[{value}]") }; + return format!("**`{value}{many}`**"); + } + let short = arg.get_short().map(|c| format!("`-{c},` ")).unwrap_or_default(); + let long = arg.get_long().expect("every option has a long name"); + let value = if arg.get_action().takes_values() { format!(" `<{value}>`") } else { String::new() }; + format!("{short}**`--{long}`**{value}") + } + + /// One item of a definition list: `term`, its permalink if it has an anchor, and `definition`, + /// its lines after the first indented to stay in the definition. + fn item(id: Option<&str>, term: &str, definition: &str) -> String { + let (attributes, permalink) = match id { + Some(id) => (format!("{{#{id} .member}}"), format!(" [¶](#{id}){{.headerlink}}")), + None => ("{.member}".to_owned(), String::new()), + }; + let mut lines = definition.trim().lines(); + let first = lines.next().unwrap_or_default(); + let rest: String = lines.map(|l| if l.is_empty() { "\n".to_owned() } else { format!("\n {l}") }).collect(); + format!("{attributes}\n{term}{permalink}\n: {first}{rest}") + } + + /// Help text as a sentence: trimmed, and ending in a period. + fn sentence(help: &str) -> String { + let help = help.trim(); + if help.is_empty() || help.ends_with('.') { help.to_owned() } else { format!("{help}.") } + } + + /// `text` with each code span that `links` names made a link to its anchor. + fn linked(text: &str, links: &HashMap) -> String { + let parts: Vec<&str> = text.split('`').collect(); + assert!(parts.len() % 2 == 1, "unpaired backtick in {text:?}"); + parts + .iter() + .enumerate() + .map(|(i, part)| match links.get(*part) { + _ if i % 2 == 0 => (*part).to_owned(), + Some(anchor) => format!("[`{part}`](#{anchor})"), + None => format!("`{part}`"), + }) + .collect() + } + #[test] fn cli_reference_matches_help() { - use std::fmt::Write as _; - let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); // This checks repository documentation, which is not part of the published CLI crate. if !root.join(".readthedocs.yaml").exists() { return; } - let mut cli = Cli::command(); - let mut text = format!( - "# Command reference\n\nGenerated from Clap. See the [CLI guide](../cli.md) for examples.\n\n## latkit\n\n```text\n{}```\n", - cli.render_long_help() - ); - for command in cli.get_subcommands_mut() { - let name = command.get_name().to_owned(); - let command = command.clone().bin_name(format!("latkit {name}")); - let mut command = command; - write!(text, "\n## {name}\n\n```text\n{}```\n", command.render_long_help()).unwrap(); - } - let text = text.lines().map(str::trim_end).collect::>().join("\n") + "\n"; + let text = reference(); let path = root.join("docs/reference/cli.md"); if std::env::var_os("LATKIT_BLESS").is_some() { std::fs::create_dir_all(path.parent().unwrap()).unwrap(); diff --git a/crates/latkit/src/data/forms.rs b/crates/latkit/src/data/forms.rs index a9850286..f1b9259f 100644 --- a/crates/latkit/src/data/forms.rs +++ b/crates/latkit/src/data/forms.rs @@ -133,12 +133,12 @@ impl JsonSchema for IdsForm { fn json_schema(generator: &mut SchemaGenerator) -> Schema { let id = generator.subschema_for::>(); json_schema!({ - "description": "Row ids: numbers or texts, or `{count}` for rows 0 to count - 1. In a reference, null names no row.", + "description": "Row ids: a list of numbers or texts, or `{count}` for rows 0 to count - 1. In a reference, null names no row.", "anyOf": [ { "type": "array", "items": id }, { "type": "object", - "properties": { "count": { "type": "integer", "minimum": 0, "maximum": u32::MAX } }, + "properties": { "count": { "description": "The number of rows, whose ids are 0 to count - 1.", "type": "integer", "minimum": 0, "maximum": u32::MAX } }, "required": ["count"], "additionalProperties": false, }, diff --git a/crates/latkit/src/error.rs b/crates/latkit/src/error.rs index 7388ae2e..c5ace1bc 100644 --- a/crates/latkit/src/error.rs +++ b/crates/latkit/src/error.rs @@ -93,12 +93,18 @@ impl schemars::JsonSchema for Error { fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { schemars::json_schema!({ - "description": "What failed: its `code`, the message, and for invalid input the `path` to the part, such as `marks[2].vertices.color`.", + "description": "What failed, as JSON: the form of the npm package's `LatkitError`.", "type": "object", "properties": { - "code": { "enum": ["invalid", "unsupported", "limit", "io", "device-lost", "cancelled", "timeout"] }, - "message": { "type": "string" }, - "path": { "type": "string" }, + "code": { + "description": "The kind of failure, as `LatkitError.code` describes it.", + "enum": ["invalid", "unsupported", "limit", "io", "device-lost", "cancelled", "timeout"], + }, + "message": { "description": "What failed, in words.", "type": "string" }, + "path": { + "description": "The invalid part of the input, such as `marks[2].vertices.color`, on `invalid` failures.", + "type": "string", + }, }, "required": ["code", "message"], }) diff --git a/crates/latkit/src/expr/form.rs b/crates/latkit/src/expr/form.rs index 9210dc66..86205bd9 100644 --- a/crates/latkit/src/expr/form.rs +++ b/crates/latkit/src/expr/form.rs @@ -339,21 +339,27 @@ kinds! { } /// View state of each row, such as whether it is selected. StateNode { + /// The state read. state: State, } /// A missing value. An option that evaluates to missing uses the theme's value. MissingNode { + /// Always `true`. #[schemars(extend("const" = true))] missing: bool, } /// A one-argument operation, such as `{ "op": "abs", "args": ["x"] }`. UnaryNode { + /// The operation. op: Unary, + /// Its argument. args: [Expr; 1], } /// A two-argument operation, such as `{ "op": "add", "args": ["a", 1] }`. BinaryNode { + /// The operation. op: Binary, + /// Its arguments, left and right. args: [Expr; 2], } /// The value of the first case whose condition holds, else `otherwise`. @@ -364,33 +370,39 @@ kinds! { /// The value where no condition holds. otherwise: Expr, } - /// Whether a text field equals the text exactly. + /// Whether a text field equals a text exactly. IsNode { + /// The text field, and the text it must equal. #[schemars(extend("tsType" = "[field: ExprInput, text: string]"))] is: (Expr, String), } - /// Whether a text field contains the text, ignoring case. In a mark, the field must belong to - /// the mark's own table. + /// Whether a text field contains a text, ignoring case. In a mark, the field must belong to the + /// mark's own table. ContainsNode { + /// The text field, and the text it must contain. #[schemars(extend("tsType" = "[field: ExprInput, text: string]"))] contains: (Expr, String), } - /// `value` mapped linearly from `domain` onto the range `scale`, clamped to the range. Without - /// a `domain`, the extent of the values shown is used. + /// A value mapped linearly onto a range, clamped to the range. ScaleNode { + /// The range. #[schemars(extend("tsType" = "[low: number, high: number]"))] scale: [f64; 2], + /// The value mapped. value: Expr, + /// The values mapped to the range's ends; the extent of the values shown when unset. #[serde(default)] #[schemars(extend("tsType" = "[low: number, high: number]"))] domain: Option<[f64; 2]>, } - /// `value` mapped through `colormap` over `domain`, or without a `domain`, over the extent of - /// the values shown. A categorical map ignores the domain and bins values by their integer - /// part. + /// A value mapped through a colormap. ColormapNode { + /// The colormap. colormap: Colormap, + /// The value mapped. A categorical map takes each value's integer part as its category. value: Expr, + /// The values mapped to the colormap's ends; the extent of the values shown when unset. A + /// categorical map ignores it. #[serde(default)] #[schemars(extend("tsType" = "[low: number, high: number]"))] domain: Option<[f64; 2]>, @@ -585,6 +597,7 @@ impl JsonSchema for Color { #[derive(Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] struct CustomColormap { + /// How the colors map values. kind: ColormapKind, /// The colors, blended in OKLab. Each is a color, spaced evenly between its neighbors as in a /// CSS gradient, or a `[place, color]` pair with a place from 0 to 1, in ascending order. A diff --git a/docs/_static/custom.css b/docs/_static/custom.css index ae9e5db3..b5933c2c 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -19,6 +19,7 @@ body[data-theme] { --color-table-header-background: transparent; --color-table-border: var(--color-background-border); --color-api-background-hover: var(--color-background-secondary); + --color-highlight-on-target: var(--color-highlighted-background); } ::selection { @@ -148,6 +149,112 @@ body[data-theme] .highlight { padding: 0.75rem 1rem; } +/* Reference: each entry's declaration, then its members, each a signature over its definition. + Types in signatures are links; a member's anchor shows on hover, as a heading's does. A long + signature wraps with its later lines indented. */ + +:is(h1, h2, h3, h4) code.literal { + padding: 0; + border: 0; + background: none; + font-size: 0.95em; +} + +h3:has(> code.literal) { + margin-top: 2.5rem; +} + +p.signature, +dl.member > dt { + font-family: var(--font-stack--monospace); + font-size: var(--font-size--small); + line-height: 1.6; + text-indent: -2rem; +} + +p.signature { + margin: -0.35rem 0 0; + padding-left: 2rem; + color: var(--color-foreground-secondary); +} + +p.signature + p.signature { + margin-top: 0; +} + +p.signature + :not(p.signature) { + margin-top: 0.85rem; +} + +:is(p.signature, dl.member > dt) code.literal { + padding: 0; + border: 0; + background: none; + font-size: inherit; +} + +:is(p.signature, dl.member > dt) a.reference { + text-decoration: none; +} + +:is(p.signature, dl.member > dt) a.reference:hover { + text-decoration: underline; +} + +dl.member { + margin: 0 0 1.25rem; +} + +dl.member > dt { + margin-inline: -0.5rem; + padding: 0.15rem 0.5rem 0.15rem 2.5rem; + border-radius: 0.25rem; + color: var(--color-foreground-secondary); + transition: background 0.1s ease-out; +} + +dl.member > dt:hover { + background: var(--color-api-background-hover); +} + +dl.member:target > dt { + background: var(--color-highlight-on-target); +} + +dl.member > dt strong { + color: var(--color-api-name); + font-weight: 600; +} + +dl.member > dd { + margin: 0.2rem 0 0 1.5rem; +} + +dl.member > dd > :first-child { + margin-top: 0; +} + +dl.member > dd > :last-child { + margin-bottom: 0; +} + +dl.member dl.member { + margin: 0.6rem 0 0; +} + +dl.member dl.member > dd { + margin-top: 0; +} + +article p.rubric { + margin: 2rem 0 0.75rem; + font-size: var(--font-size--small--2); + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--color-foreground-muted); +} + /* Tables */ table.docutils { diff --git a/docs/conf.py b/docs/conf.py index be022546..1c3d047a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,7 @@ extensions = ["myst_parser", "sphinx_copybutton"] exclude_patterns = ["_build", ".venv", "gallery"] -myst_enable_extensions = ["colon_fence", "deflist"] +myst_enable_extensions = ["attrs_block", "attrs_inline", "colon_fence", "deflist"] myst_heading_anchors = 3 copybutton_prompt_text = r"^\$ " diff --git a/docs/reference/api.md b/docs/reference/api.md index a044df7f..1f7426a9 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -1,1358 +1,1685 @@ # JavaScript API -Generated from the shipped declarations. `load()` loads the WebAssembly that datasets, views, -and pictures need. Scenes, expressions, and patches do not need it. Options use the [scene and -result types](types.md). +Every class, function, and type the `latkit` package exports. [`load()`](#load) loads the WebAssembly +that datasets, views, and pictures need; scenes, expressions, and patches need none. Scenes and +their options take the [scene and result types](types.md). ## Loading and waiting -Load the WebAssembly once per page. A wait ends when its signal aborts (30 s by default). +Load the WebAssembly once per page. Every wait ends when its signal aborts, after 30 seconds by default. -### load +(load)= -```ts -/** - * Loads latkit's WebAssembly, once per page. Datasets, views, and pictures can be created once it - * resolves. Later and concurrent calls share the first call's load. Options they pass must equal - * the first call's, or they reject with an `io` error. A failed load rejects with an `io` error and - * can be retried. - * - * Importing the package loads nothing and requests no GPU. `load()` requests a GPU adapter while - * the WebAssembly compiles, so the browser starts its GPU process early, then requests the first - * GPU device for `Gpu.create()`. `Gpu.create()` and `Data.read()` call `load()` themselves, as does - * `Patch.read()` for JSON. - */ -function load(options?: LoadOptions): Promise; -``` +### `load()` -### LoadOptions +{.signature} +`function load(options?:` [`LoadOptions`](#loadoptions)`): Promise` -```ts -/** Options for {@link load}. Only the first call's options take effect. */ -interface LoadOptions { - /** - * The URL of this package version's `latkit/wasm` asset, or that asset already compiled. - * Defaults to the bundled asset. - */ - wasm?: string | URL | WebAssembly.Module; - /** - * Creates a worker that runs the standalone `latkit/worker` bundle. Defaults to the bundled - * worker. - */ - worker?: () => Worker; -} -``` +Loads latkit's WebAssembly, once per page. Datasets, views, and pictures can be created once it +resolves. Later and concurrent calls share the first call's load. Options they pass must equal +the first call's, or they reject with an `io` error. A failed load rejects with an `io` error and +can be retried. -### Wait +Importing the package loads nothing and requests no GPU. `load()` requests a GPU adapter while +the WebAssembly compiles, so the browser starts its GPU process early, then requests the first +GPU device for [`Gpu.create()`](#gpu-create). `Gpu.create()` and [`Data.read()`](#data-read) call `load()` themselves, as does +[`Patch.read()`](#patch-read) for JSON. -```ts -/** Options that bound how long a promise waits. */ -interface Wait { - /** - * Aborts the wait. A timeout rejects with a `timeout` error; any other abort rejects with a - * `cancelled` error whose `cause` is the abort reason. Either way, latkit cancels the call's - * pending work where it can. Without a signal, each wait times out after 30 seconds. - */ - signal?: AbortSignal; -} -``` +(loadoptions)= -### LatkitError +### `LoadOptions` -```ts -/** - * The error latkit throws: an `Error` with a `code`, plus a `path` to the bad part of invalid - * input, such as `marks[2].vertices.color`. The codes: - * - * - `invalid`: the input is malformed or does not fit; `path` says where. - * - `unsupported`: the browser or device cannot do what was asked. - * - `limit`: a budget or device limit would be exceeded. - * - `io`: reading or writing external bytes failed, such as loading latkit or fetching a file. - * - `device-lost`: the GPU device was lost; create a new `Gpu`. - * - `cancelled`: the wait's signal aborted, or the work was cancelled. - * - `timeout`: the wait's signal timed out. - * - `unloaded`: the call needs WebAssembly, and `load()` has not finished. - * - `destroyed`: the object was destroyed, so it can no longer be used. - */ -interface LatkitError extends Error { - code: Failure['code'] | 'unloaded' | 'destroyed'; - path?: string; -} -``` +{.signature} +`interface LoadOptions` + +Options for [`load()`](#load). Only the first call's options take effect. + +{#loadoptions-wasm .member} +**`wasm`**`?: string | URL | WebAssembly.Module` [¶](#loadoptions-wasm){.headerlink} +: The URL of this package version's `latkit/wasm` asset, or that asset already compiled. + Defaults to the bundled asset. + +{#loadoptions-worker .member} +**`worker`**`?: () => Worker` [¶](#loadoptions-worker){.headerlink} +: Creates a worker that runs the standalone `latkit/worker` bundle. Defaults to the bundled + worker. + +(wait)= + +### `Wait` + +{.signature} +`interface Wait` + +Options that bound how long a promise waits. + +{#wait-signal .member} +**`signal`**`?: AbortSignal` [¶](#wait-signal){.headerlink} +: Aborts the wait. A timeout rejects with a `timeout` error; any other abort rejects with a + `cancelled` error whose `cause` is the abort reason. Either way, latkit cancels the call's + pending work where it can. Without a signal, each wait times out after 30 seconds. + +(latkiterror)= + +### `LatkitError` + +{.signature} +`interface LatkitError extends Error` + +The error latkit throws: an `Error` whose `code` says what kind of failure it is. + +{#latkiterror-code .member} +**`code`**`:` [`Failure`](#failure)`['code'] | 'unloaded' | 'destroyed'` [¶](#latkiterror-code){.headerlink} +: The kind of failure: + + - `invalid`: the input is malformed or does not fit; `path` says where. + - `unsupported`: the browser or device cannot do what was asked. + - `limit`: a budget or device limit would be exceeded. + - `io`: reading or writing external bytes failed, such as loading latkit or fetching a file. + - `device-lost`: the GPU device was lost; create a new [`Gpu`](#gpu). + - `cancelled`: the wait's signal aborted, or the work was cancelled. + - `timeout`: the wait's signal timed out. + - `unloaded`: the call needs WebAssembly, and [`load()`](#load) has not finished. + - `destroyed`: the object was destroyed, so it can no longer be used. + +{#latkiterror-path .member} +**`path`**`?: string` [¶](#latkiterror-path){.headerlink} +: The invalid part of the input, such as `marks[2].vertices.color`, on `invalid` errors. ## Data and patches -Datasets of named tables, the patches that change them (written without WebAssembly), and the sources views draw. - -### Data - -````ts -/** - * A dataset that changes in place: named tables of columns, changed only by patches. A patch - * applies completely or not at all. Views given a `Data` draw its latest version, from the frame - * after each patch. A {@link Snapshot} keeps one version and shares its pages with the dataset, so - * keeping earlier versions, as an undo history does, costs only the pages that differ. - * - * ```ts - * const grid = new Data('grid').apply(new Patch().replace('Bus', [1, 2], { kv: [230, 115] })); - * view.sources = [grid]; - * grid.apply(new Patch().set('Bus', [2], { kv: [69] })); // drawn next frame - * ``` - */ -class Data { - /** Creates an empty dataset named `name`. Throws an `unloaded` error before `load()` finishes. */ - constructor(name: string); - /** - * Reads a dataset from `source`, calling `load()` first. Its first bytes decide the format: - * - * - latkit's bytes, as `toBytes()` writes them. - * - GeoJSON, plain or gzip-compressed, read in a worker. Points, line strings, and polygons - * become rows of tables `points`, `lines`, and `polygons`, with the geometry in field - * `geometry` and each property as a field. GeoJSON requires `options.name`. - * - A dataset document, `{"name": ..., "patch": [...]}`, whose patch is read as - * {@link Patch.read} reads one. - * - * `options.name` names the dataset, replacing the name in latkit's bytes or a document. - * `options.base` is the URL that a document's file slices are relative to; it defaults to the URL - * the source was fetched from. Rejects with an `invalid` error for bytes that are not a valid - * dataset, and with an `io` error when a fetch fails or answers an error status. - */ - static read( - source: DataSource, - options?: { - name?: string; - base?: string; - } & Wait, - ): Promise; - get name(): string; - /** - * Applies every change in `patch` (a {@link Patch} or its bytes), or none of them when one is - * invalid, and returns this dataset. Views draw the new version from their next frame. Throws an - * `invalid` error whose `path` names the failing change, such as `patch[1].Bus.kv`, or when the - * patch names another dataset. - */ - apply(patch: Patch | Uint8Array): this; - /** The current version as a {@link Snapshot}, which later patches leave unchanged. O(1). */ - snapshot(): Snapshot; - /** - * Makes `snapshot`, a version of this dataset, the current version, as an undo does. Views draw - * it from their next frame. Throws an `invalid` error for a snapshot of a dataset with another - * name. - */ - restore(snapshot: Snapshot): this; - /** Describes the current version; see {@link Snapshot.describe}. */ - describe(): Description; - /** Reads one value of the current version; see {@link Snapshot.get}. */ - get( - table: string, - id: Id, - field: string, - options?: { - at?: number; - }, - ): Value; - /** Queries the current version; see {@link Snapshot.query}. */ - query(query: Query): QueryResult; - /** Per-row minimums and maximums of the current version; see {@link Snapshot.envelope}. */ - envelope(request: EnvelopeRequest): Envelope; - /** The current version as latkit's bytes: a patch that builds it from an empty dataset. */ - toBytes(): Uint8Array; - /** - * Frees this handle. Views drawing the dataset keep drawing it, and its snapshots stay usable. - * Other methods throw afterwards. - */ - destroy(): void; - [Symbol.dispose](): void; -} -```` - -### Snapshot +Datasets of named tables, the patches that change them, written without WebAssembly, and the sources views draw. -```ts -/** - * One immutable version of a dataset, taken in O(1) by {@link Data.snapshot}. A view given a - * snapshot draws that version unchanged. - */ -class Snapshot { - get name(): string; - /** - * Describes the dataset: its tables and their row counts, each field and its kind, and the axis - * and the span its sampled fields cover. - */ - describe(): Description; - /** - * Reads `field` of the row with `id` in `table`. A sampled field reads its last frame at or - * before `options.at`, or its last frame when `at` is omitted. A reference reads the referenced - * row's id. Returns `null` when the table, row, field, or value is missing. - */ - get( - table: string, - id: Id, - field: string, - options?: { - at?: number; - }, - ): Value; - /** - * Runs `query` on the CPU: reads a table's rows, filtered, sorted, and paged, through the same - * expressions marks use. Sampled fields are read at `query.at`. Throws an `invalid` error for an - * invalid query. - */ - query(query: Query): QueryResult; - /** - * Computes each row's minimum and maximum of a sampled field over equal spans of an axis range, - * for every row or the rows `request.ids` lists. Each span reads the coarsest stored levels of - * detail that fit in it, so it costs a few reads regardless of how many frames it covers. Throws - * an `invalid` error for an unknown table or row, or a field that is not sampled, and a `limit` - * error when rows times spans exceeds 2^26. - */ - envelope(request: EnvelopeRequest): Envelope; - /** This version as latkit's bytes: a patch that builds it from an empty dataset. */ - toBytes(): Uint8Array; - /** - * Frees this handle. Views given the snapshot keep drawing it; other methods throw afterwards. - */ - destroy(): void; - [Symbol.dispose](): void; -} -``` +(data)= -### Tiles +### `Data` -```ts -/** - * A dataset fetched in pieces as views need them. A `Tiles` is plain data: a dataset name and a - * URL. Each view draws the pieces its camera and plot window need as a dataset of its own, and all - * views of a GPU share the fetched pieces. The first bytes at `url` decide the kind of tiles: - * - * - Time tiles: a `frames.json` manifest as `latkit tile` writes it, for a dataset whose sampled - * fields' histories are fetched in spans. A line tracing a field fetches the level of detail its - * plot window shows; other marks fetch the frames around the view's `at`. A span still loading - * shows the coarser span that contains it. - * - Vector tiles: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` URL template, fetched - * as the camera needs them. Each layer becomes a table of features of the kind its marks draw: - * polygons for areas, lines for lines, points for vertices. A table named `layer:polygons`, - * `layer:lines`, or `layer:points` holds that kind regardless of the marks. - */ -class Tiles { - readonly name: string; - readonly url: string; - /** Tiles at `url`, drawn as the dataset named `name`. */ - constructor(name: string, url: string); -} -``` +{.signature} +`class Data` -### Source +A dataset that changes in place: named tables of columns, changed only by patches. A patch +applies completely or not at all. Views given a `Data` draw its latest version, from the frame +after each patch. A [`Snapshot`](#snapshot) keeps one version and shares its pages with the dataset, so +keeping earlier versions, as an undo history does, costs only the pages that differ. ```ts -/** A dataset a view draws; views match it to the scene's marks by the dataset's name. */ -type Source = Data | Snapshot | Tiles; +const grid = new Data('grid').apply(new Patch().replace('Bus', [1, 2], { kv: [230, 115] })); +view.sources = [grid]; +grid.apply(new Patch().set('Bus', [2], { kv: [69] })); // drawn next frame ``` -### DataSource - -```ts -/** - * What {@link Data.read} accepts: bytes, a `Blob`, a `Response`, a byte stream, a URL to fetch as - * a string, or a parsed JSON value. The content is latkit's bytes, GeoJSON, or a dataset document. - */ -type DataSource = - Uint8Array | ArrayBuffer | Blob | Response | ReadableStream | string | object; +```{rubric} Constructor ``` -### QueryResult +{#data-new .member} +**`new Data`**`(name: string)` [¶](#data-new){.headerlink} +: Creates an empty dataset named `name`. Throws an `unloaded` error before [`load()`](#load) finishes. -```ts -/** - * The rows a query returns, as columns in the forms a patch takes: numbers as a `Float64Array` - * (missing values NaN), other values as arrays (missing `null`), and a reference as the ids of the - * rows it references. - */ -interface QueryResult { - table: string; - ids: Id[]; - fields: Record< - string, - | Float64Array - | Value[] - | { - reference: string; - ids: (Id | null)[]; - } - >; - /** How many rows matched the query before `offset` and `limit` applied. */ - total: number; -} +```{rubric} Properties ``` -### Envelope +{#data-name .member} +`readonly` **`name`**`: string` [¶](#data-name){.headerlink} +: The dataset's name, by which a mark's `data` option draws it. -```ts -/** Each row's minimum and maximum of a sampled field over equal spans of the axis. */ -interface Envelope { - /** The span boundaries on the axis: one more than the number of spans. */ - at: Float64Array; - /** - * Each row's minimum in each span: `spans` values per row, row after row. NaN where a span has - * no frame. - */ - least: Float32Array; - /** Each row's maximum in each span, laid out like `least`. */ - greatest: Float32Array; -} +```{rubric} Methods ``` -### Patch - -````ts -/** - * A list of changes to a dataset, which `Data.apply` applies in order, all or none. The patch is - * encoded as latkit's bytes as it is built, without WebAssembly. Each method adds a change and - * returns the patch. For values it cannot encode, a method throws an `invalid` error with a `path` - * such as `Bus.kv`, and leaves the patch unchanged. - * - * ```ts - * const patch = new Patch('grid') - * .replace('Bus', [101, 102], { x: Float64Array.of(0, 10), name: ['North', 'South'] }) - * .append('Bus', 0.5, { vm: Float32Array.of(1.01, 0.99) }); - * ``` - */ -class Patch { - /** Creates an empty patch for the dataset named `data`, or for any dataset when omitted. */ - constructor(data?: string); - /** - * The name of the dataset this patch is for, if any. `Data.apply` rejects it for a dataset with - * another name. - */ - get data(): string | undefined; - /** - * Reads a patch from `source`: latkit's bytes, one frame as `toBytes()` writes it, or JSON that - * any language can write without latkit. The JSON is a list of changes. Each change is keyed by - * its method name and holds the method's parameters by name, such as - * `{"set": {"table": "Bus", "ids": [1], "fields": {"kv": [345]}}}`. A field may be a slice of a - * binary file, `{file, dtype, offset, count}`. Slices are fetched at once, relative to - * `options.base`, which defaults to the URL the JSON was fetched from. - * - * Bytes are read without WebAssembly; JSON calls `load()` first. Rejects with a `limit` error for - * a frame longer than `options.limit` bytes (256 MiB by default), with an `invalid` error for - * malformed input, and with an `io` error when a fetch fails or answers an error status. - */ - static read( - source: PatchSource, - options?: { - base?: string; - limit?: number; - } & Wait, - ): Promise; - /** - * Yields each patch in `stream` as soon as its last byte arrives. The stream carries frames back - * to back, such as a response body, a file, or a socket's messages. Each frame states its length - * up front, so a frame longer than `options.limit` bytes (256 MiB by default) throws a `limit` - * error before its body is allocated. Besides the current chunk and the patches the caller keeps, - * at most one frame is buffered. Breaking out of the loop cancels the stream. Throws an `invalid` - * error for a `limit` that is not a whole number of at least 12 bytes, for bytes that are not - * latkit's, or for a stream that ends inside a frame. - * - * ```ts - * for await (const patch of Patch.stream((await fetch('/feed')).body!)) data.apply(patch); - * ``` - */ - static stream( - stream: ReadableBytes | AsyncIterable, - { - limit, - }?: { - limit?: number; - }, - ): AsyncGenerator; - /** - * Sets the name and unit of the axis that sampled fields are recorded over, such as - * `axis('time', 's')`. - */ - axis(name: string, unit: string): this; - /** Replaces table `table` with rows `ids` and `fields`, adding the table if it does not exist. */ - replace(table: string, ids: RowIds, fields?: Fields): this; - /** - * Writes `fields` for rows `ids` of `table`. Rows with existing ids change, new ids add rows, and - * omitted fields keep their values. Adds the table if it does not exist. - */ - set(table: string, ids: RowIds, fields?: Fields): this; - /** Removes the rows of `table` with `ids`. */ - remove(table: string, ids: RowIds): this; - /** - * Records a frame of `table` at each axis coordinate in `at`. Each field gives a value per row - * per frame, frame by frame: every row of the first frame, then of the next. Frames cover every - * row, or only rows `options.ids`; other rows keep their last values. A field's first frame makes - * it a sampled field. Consecutive appends of `Float32Array` or `Float64Array` values to the same - * fields of every row are written as one change, so one frame per call costs no more than one - * call with every frame. - */ - append( - table: string, - at: number | readonly number[], - fields: Readonly>, - { - ids, - }?: { - ids?: RowIds; - }, - ): this; - /** Drops every sampled field's frames before axis coordinate `before`. */ - forget(before: number): this; - /** - * Stores a run of entries of sampled field `field` of `table`, at level of detail `level` (0 by - * default), numbered from `first`. This is what a time tile holds, as `latkit tile` writes it. - * - * At level 0 each entry is a frame: a coordinate in `at`, and a value per row in `values`, entry - * by entry. At level `k` each entry is a bucket of `8^k` frames: its first and last frames' - * coordinates in `at`. `values` then holds every bucket's per-row minimums, entry by entry, - * followed by every bucket's per-row maximums. NaN marks a missing value. - * - * None of the entries may be stored already. Entries past the field's last frame extend it. - * Throws an `invalid` error for a level outside 0 to 21, lengths of `at` and `values` that do not - * divide into whole entries and rows, or frame numbers that are not safe nonnegative integers. - */ - frames( - table: string, - field: string, - { - level, - first, - at, - values, - }: { - level?: number; - first: number; - at: Float64Array | readonly number[]; - values: Float32Array | readonly number[]; - }, - ): this; - /** Appends `next`'s changes to this patch, which keeps its own dataset name. */ - extend(next: Patch): this; - /** A copy of the patch as latkit's bytes: one frame, naming the dataset if the patch does. */ - toBytes(): Uint8Array; -} -```` - -### PatchSource +{#data-read .member} +`static` **`read`**`(source:` [`DataSource`](#datasource)`, options?: { name?: string; base?: string } &` [`Wait`](#wait)`): Promise` [¶](#data-read){.headerlink} +: Reads a dataset from `source`, calling [`load()`](#load) first. Its first bytes decide the format: -```ts -/** - * What {@link Patch.read} accepts: bytes, a `Blob`, a `Response`, a byte stream, a URL to fetch as a - * string, or a parsed JSON value. The content is latkit's bytes or a patch's JSON. - */ -type PatchSource = - Uint8Array | ArrayBuffer | Blob | Response | ReadableStream | string | object; -``` + - latkit's bytes, as `toBytes()` writes them. + - GeoJSON, plain or gzip-compressed, read in a worker. Points, line strings, and polygons + become rows of tables `points`, `lines`, and `polygons`, with the geometry in field + `geometry` and each property as a field. GeoJSON requires `options.name`. + - A dataset document, `{"name": ..., "patch": [...]}`, whose patch is read as + [`Patch.read()`](#patch-read) reads one. -### RowIds + `options.name` names the dataset, replacing the name in latkit's bytes or a document. + `options.base` is the URL that a document's file slices are relative to; it defaults to the URL + the source was fetched from. Rejects with an `invalid` error for bytes that are not a valid + dataset, and with an `io` error when a fetch fails or answers an error status. -```ts -/** - * Row ids: a list of numbers or strings, `{ count }` for ids `0` to `count - 1`, or a `Uint32Array` - * or `Float64Array` of numbers. - */ -type RowIds = Ids | readonly (Id | null)[] | Uint32Array | Float64Array; -``` +{#data-apply .member} +**`apply`**`(patch:` [`Patch`](#patch) `| Uint8Array): this` [¶](#data-apply){.headerlink} +: Applies every change in `patch` (a [`Patch`](#patch) or its bytes), or none of them when one is + invalid, and returns this dataset. Views draw the new version from their next frame. Throws an + `invalid` error whose `path` names the failing change, such as `patch[1].Bus.kv`, or when the + patch names another dataset. -### Values +{#data-snapshot .member} +**`snapshot`**`():` [`Snapshot`](#snapshot) [¶](#data-snapshot){.headerlink} +: The current version as a [`Snapshot`](#snapshot), which later patches leave unchanged. O(1). -```ts -/** - * One field's values, one per row, with `null` for a missing value. Numbers come as a typed array - * or an array. A `Uint8Array` holds booleans, nonzero being true. Booleans, strings, and geometries - * also come as arrays. Values are copied when passed, so the arrays can be reused at once. - */ -type Values = - | Float64Array - | Float32Array - | Int32Array - | Uint32Array - | Uint8Array - | readonly (number | null)[] - | readonly (boolean | null)[] - | readonly (string | null)[] - | readonly (Geometry | null)[]; +{#data-restore .member} +**`restore`**`(snapshot:` [`Snapshot`](#snapshot)`): this` [¶](#data-restore){.headerlink} +: Makes `snapshot`, a version of this dataset, the current version, as an undo does. Views draw + it from their next frame. Throws an `invalid` error for a snapshot of a dataset with another + name. + +{#data-describe .member} +**`describe`**`():` [`Description`](#description) [¶](#data-describe){.headerlink} +: Describes the current version; see [`Snapshot.describe()`](#snapshot-describe). + +{#data-get .member} +**`get`**`(table: string, id:` [`Id`](#id)`, field: string, options?: { at?: number }):` [`Value`](#value) [¶](#data-get){.headerlink} +: Reads one value of the current version; see [`Snapshot.get()`](#snapshot-get). + +{#data-query .member} +**`query`**`(query:` [`Query`](#query)`):` [`QueryResult`](#queryresult) [¶](#data-query){.headerlink} +: Queries the current version; see [`Snapshot.query()`](#snapshot-query). + +{#data-envelope .member} +**`envelope`**`(request:` [`EnvelopeRequest`](#enveloperequest)`):` [`Envelope`](#envelope) [¶](#data-envelope){.headerlink} +: Per-row minimums and maximums of the current version; see [`Snapshot.envelope()`](#snapshot-envelope). + +{#data-tobytes .member} +**`toBytes`**`(): Uint8Array` [¶](#data-tobytes){.headerlink} +: The current version as latkit's bytes: a patch that builds it from an empty dataset. + +{#data-destroy .member} +**`destroy`**`(): void` [¶](#data-destroy){.headerlink} +: Frees this handle. Views drawing the dataset keep drawing it, and its snapshots stay usable. + Other methods throw afterwards. + +{#data-symbol-dispose .member} +**`[Symbol.dispose]`**`(): void` [¶](#data-symbol-dispose){.headerlink} +: Calls [`Data.destroy()`](#data-destroy), so a `using` declaration destroys it when its block ends. + +(snapshot)= + +### `Snapshot` + +{.signature} +`class Snapshot` + +One immutable version of a dataset, taken in O(1) by [`Data.snapshot()`](#data-snapshot). A view given a +snapshot draws that version unchanged. + +```{rubric} Properties ``` -### Geometry +{#snapshot-name .member} +`readonly` **`name`**`: string` [¶](#snapshot-name){.headerlink} +: The name of the dataset this is a version of. -```ts -/** - * A point `[x, y]`, or a nested list of points: a path (a list of points), parts (a list of paths, - * such as a polygon's rings), or polygons (a list of parts). - */ -type Geometry = readonly number[] | readonly Geometry[]; +```{rubric} Methods +``` + +{#snapshot-describe .member} +**`describe`**`():` [`Description`](#description) [¶](#snapshot-describe){.headerlink} +: Describes the dataset: its tables and their row counts, each field and its kind, and the axis + and the span its sampled fields cover. + +{#snapshot-get .member} +**`get`**`(table: string, id:` [`Id`](#id)`, field: string, options?: { at?: number }):` [`Value`](#value) [¶](#snapshot-get){.headerlink} +: Reads `field` of the row with `id` in `table`. A sampled field reads its last frame at or + before `options.at`, or its last frame when `at` is omitted. A reference reads the referenced + row's id. Returns `null` when the table, row, field, or value is missing. + +{#snapshot-query .member} +**`query`**`(query:` [`Query`](#query)`):` [`QueryResult`](#queryresult) [¶](#snapshot-query){.headerlink} +: Runs `query` on the CPU: reads a table's rows, filtered, sorted, and paged, through the same + expressions marks use. Sampled fields are read at [`query.at`](#query-at). Throws an `invalid` error for an + invalid query. + +{#snapshot-envelope .member} +**`envelope`**`(request:` [`EnvelopeRequest`](#enveloperequest)`):` [`Envelope`](#envelope) [¶](#snapshot-envelope){.headerlink} +: Computes each row's minimum and maximum of a sampled field over equal spans of an axis range, + for every row or the rows `request.ids` lists. Each span reads the coarsest stored levels of + detail that fit in it, so it costs a few reads regardless of how many frames it covers. Throws + an `invalid` error for an unknown table or row, or a field that is not sampled, and a `limit` + error when rows times spans exceeds 2^26. + +{#snapshot-tobytes .member} +**`toBytes`**`(): Uint8Array` [¶](#snapshot-tobytes){.headerlink} +: This version as latkit's bytes: a patch that builds it from an empty dataset. + +{#snapshot-destroy .member} +**`destroy`**`(): void` [¶](#snapshot-destroy){.headerlink} +: Frees this handle. Views given the snapshot keep drawing it; other methods throw afterwards. + +{#snapshot-symbol-dispose .member} +**`[Symbol.dispose]`**`(): void` [¶](#snapshot-symbol-dispose){.headerlink} +: Calls [`Snapshot.destroy()`](#snapshot-destroy), so a `using` declaration destroys it when its block ends. + +(tiles)= + +### `Tiles` + +{.signature} +`class Tiles` + +A dataset fetched in pieces as views need them. A `Tiles` is plain data: a dataset name and a +URL. Each view draws the pieces its camera and plot window need as a dataset of its own, and all +views of a GPU share the fetched pieces. The first bytes at `url` decide the kind of tiles: + +- Time tiles: a `frames.json` manifest as `latkit tile` writes it, for a dataset whose sampled + fields' histories are fetched in spans. A line tracing a field fetches the level of detail its + plot window shows; other marks fetch the frames around the view's `at`. A span still loading + shows the coarser span that contains it. +- Vector tiles: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` URL template, fetched + as the camera needs them. Each layer becomes a table of features of the kind its marks draw: + polygons for areas, lines for lines, points for vertices. A table named `layer:polygons`, + `layer:lines`, or `layer:points` holds that kind regardless of the marks. + +```{rubric} Constructor ``` -### Reference +{#tiles-new .member} +**`new Tiles`**`(name: string, url: string)` [¶](#tiles-new){.headerlink} +: Tiles at `url`, drawn as the dataset named `name`. -```ts -/** - * A reference field: for each row, the id of a row in table `reference`, or `null` to reference - * none. - */ -interface Reference { - reference: string; - ids: RowIds; -} +```{rubric} Properties ``` -### reference +{#tiles-name .member} +`readonly` **`name`**`: string` [¶](#tiles-name){.headerlink} +: The name of the dataset the tiles are drawn as, by which a mark's `data` option draws it. + +{#tiles-url .member} +`readonly` **`url`**`: string` [¶](#tiles-url){.headerlink} +: Where the tiles are: a `frames.json` manifest, a PMTiles archive, a TileJSON document, or a + `{z}/{x}/{y}` URL template. + +(source)= + +### `Source` + +{.signature} +`type Source =` [`Data`](#data) `|` [`Snapshot`](#snapshot) `|` [`Tiles`](#tiles) + +A dataset a view draws; views match it to the scene's marks by the dataset's name. + +{#source-data .member} +[`Data`](#data) [¶](#source-data){.headerlink} +: A dataset that changes in place: named tables of columns, changed only by patches. + +{#source-snapshot .member} +[`Snapshot`](#snapshot) [¶](#source-snapshot){.headerlink} +: One immutable version of a dataset, taken in O(1) by [`Data.snapshot()`](#data-snapshot). + +{#source-tiles .member} +[`Tiles`](#tiles) [¶](#source-tiles){.headerlink} +: A dataset fetched in pieces as views need them. + +(datasource)= + +### `DataSource` + +{.signature} +`type DataSource = Uint8Array | ArrayBuffer | Blob | Response | ReadableStream | string | object` + +What [`Data.read()`](#data-read) accepts: bytes, a `Blob`, a `Response`, a byte stream, a URL to fetch as +a string, or a parsed JSON value. The content is latkit's bytes, GeoJSON, or a dataset document. + +(queryresult)= + +### `QueryResult` + +{.signature} +`interface QueryResult` + +The rows a query returns, as columns in the forms a patch takes. + +{#queryresult-table .member} +**`table`**`: string` [¶](#queryresult-table){.headerlink} +: The table read. + +{#queryresult-ids .member} +**`ids`**`:` [`Id`](#id)`[]` [¶](#queryresult-ids){.headerlink} +: The ids of the rows returned, in order. + +{#queryresult-fields .member} +**`fields`**`: Record` [¶](#queryresult-fields){.headerlink} +: Each selected field's column, by output name: numbers as a `Float64Array` with NaN for missing + values, other values as an array with `null` for missing ones, and a reference as the table it + references and the id of the row each row references. + +{#queryresult-total .member} +**`total`**`: number` [¶](#queryresult-total){.headerlink} +: How many rows matched the query before `offset` and `limit` applied. + +(envelope)= + +### `Envelope` + +{.signature} +`interface Envelope` + +Each row's minimum and maximum of a sampled field over equal spans of the axis. + +{#envelope-at .member} +**`at`**`: Float64Array` [¶](#envelope-at){.headerlink} +: The span boundaries on the axis: one more than the number of spans. + +{#envelope-least .member} +**`least`**`: Float32Array` [¶](#envelope-least){.headerlink} +: Each row's minimum in each span: `spans` values per row, row after row. NaN where a span has + no frame. + +{#envelope-greatest .member} +**`greatest`**`: Float32Array` [¶](#envelope-greatest){.headerlink} +: Each row's maximum in each span, laid out like `least`. + +(patch)= + +### `Patch` + +{.signature} +`class Patch` + +A list of changes to a dataset, which [`Data.apply`](#data-apply) applies in order, all or none. The patch is +encoded as latkit's bytes as it is built, without WebAssembly. Each method adds a change and +returns the patch. For values it cannot encode, a method throws an `invalid` error with a `path` +such as `Bus.kv`, and leaves the patch unchanged. ```ts -/** Builds a {@link Reference} field: for each row, the id of the row of `table` it references. */ -function reference(table: string, ids: RowIds): Reference; +const patch = new Patch('grid') + .replace('Bus', [101, 102], { x: Float64Array.of(0, 10), name: ['North', 'South'] }) + .append('Bus', 0.5, { vm: Float32Array.of(1.01, 0.99) }); ``` -### Fields +```{rubric} Constructor +``` -```ts -/** A table's fields by name: values, or a {@link Reference}. */ -type Fields = Readonly>; +{#patch-new .member} +**`new Patch`**`(data?: string)` [¶](#patch-new){.headerlink} +: Creates an empty patch for the dataset named `data`, or for any dataset when omitted. + +```{rubric} Properties ``` -### ReadableBytes +{#patch-data .member} +`readonly` **`data`**`: string | undefined` [¶](#patch-data){.headerlink} +: The name of the dataset this patch is for, if any. [`Data.apply`](#data-apply) rejects it for a dataset with + another name. -```ts -/** A byte stream: anything shaped like a browser or Node.js `ReadableStream` of `Uint8Array`. */ -interface ReadableBytes { - getReader(): { - read(): Promise<{ - done: boolean; - value?: Uint8Array; - }>; - cancel(reason?: unknown): Promise; - releaseLock(): void; - }; -} +```{rubric} Methods ``` +{#patch-read .member} +`static` **`read`**`(source:` [`PatchSource`](#patchsource)`, options?: { base?: string; limit?: number } &` [`Wait`](#wait)`): Promise` [¶](#patch-read){.headerlink} +: Reads a patch from `source`: latkit's bytes, one frame as `toBytes()` writes it, or JSON that + any language can write without latkit. The JSON is a list of changes. Each change is keyed by + its method name and holds the method's parameters by name, such as + `{"set": {"table": "Bus", "ids": [1], "fields": {"kv": [345]}}}`. A field may be a slice of a + binary file, `{file, dtype, offset, count}`. Slices are fetched at once, relative to + `options.base`, which defaults to the URL the JSON was fetched from. + + Bytes are read without WebAssembly; JSON calls [`load()`](#load) first. Rejects with a `limit` error for + a frame longer than `options.limit` bytes (256 MiB by default), with an `invalid` error for + malformed input, and with an `io` error when a fetch fails or answers an error status. + +{#patch-stream .member} +`static` **`stream`**`(stream:` [`ReadableBytes`](#readablebytes) `| AsyncIterable, { limit }?: { limit?: number }): AsyncGenerator` [¶](#patch-stream){.headerlink} +: Yields each patch in `stream` as soon as its last byte arrives. The stream carries frames back + to back, such as a response body, a file, or a socket's messages. Each frame states its length + up front, so a frame longer than `options.limit` bytes (256 MiB by default) throws a `limit` + error before its body is allocated. Besides the current chunk and the patches the caller keeps, + at most one frame is buffered. Breaking out of the loop cancels the stream. Throws an `invalid` + error for a `limit` that is not a whole number of at least 12 bytes, for bytes that are not + latkit's, or for a stream that ends inside a frame. + + ```ts + for await (const patch of Patch.stream((await fetch('/feed')).body!)) data.apply(patch); + ``` + +{#patch-axis .member} +**`axis`**`(name: string, unit: string): this` [¶](#patch-axis){.headerlink} +: Sets the name and unit of the axis that sampled fields are recorded over, such as + `axis('time', 's')`. + +{#patch-replace .member} +**`replace`**`(table: string, ids:` [`RowIds`](#rowids)`, fields?:` [`Fields`](#fields)`): this` [¶](#patch-replace){.headerlink} +: Replaces table `table` with rows `ids` and `fields`, adding the table if it does not exist. + +{#patch-set .member} +**`set`**`(table: string, ids:` [`RowIds`](#rowids)`, fields?:` [`Fields`](#fields)`): this` [¶](#patch-set){.headerlink} +: Writes `fields` for rows `ids` of `table`. Rows with existing ids change, new ids add rows, and + omitted fields keep their values. Adds the table if it does not exist. + +{#patch-remove .member} +**`remove`**`(table: string, ids:` [`RowIds`](#rowids)`): this` [¶](#patch-remove){.headerlink} +: Removes the rows of `table` with `ids`. + +{#patch-append .member} +**`append`**`(table: string, at: number | readonly number[], fields: Readonly>, { ids }?: { ids?:` [`RowIds`](#rowids) `}): this` [¶](#patch-append){.headerlink} +: Records a frame of `table` at each axis coordinate in `at`. Each field gives a value per row + per frame, frame by frame: every row of the first frame, then of the next. Frames cover every + row, or only rows `options.ids`; other rows keep their last values. A field's first frame makes + it a sampled field. Consecutive appends of `Float32Array` or `Float64Array` values to the same + fields of every row are written as one change, so one frame per call costs no more than one + call with every frame. + +{#patch-forget .member} +**`forget`**`(before: number): this` [¶](#patch-forget){.headerlink} +: Drops every sampled field's frames before axis coordinate `before`. + +{#patch-frames .member} +**`frames`**`(table: string, field: string, { level, first, at, values }: { level?: number; first: number; at: Float64Array | readonly number[]; values: Float32Array | readonly number[] }): this` [¶](#patch-frames){.headerlink} +: Stores a run of entries of sampled field `field` of `table`, at level of detail `level` (0 by + default), numbered from `first`. This is what a time tile holds, as `latkit tile` writes it. + + At level 0 each entry is a frame: a coordinate in `at`, and a value per row in `values`, entry + by entry. At level `k` each entry is a bucket of `8^k` frames: its first and last frames' + coordinates in `at`. `values` then holds every bucket's per-row minimums, entry by entry, + followed by every bucket's per-row maximums. NaN marks a missing value. + + None of the entries may be stored already. Entries past the field's last frame extend it. + Throws an `invalid` error for a level outside 0 to 21, lengths of `at` and `values` that do not + divide into whole entries and rows, or frame numbers that are not safe nonnegative integers. + +{#patch-extend .member} +**`extend`**`(next: Patch): this` [¶](#patch-extend){.headerlink} +: Appends `next`'s changes to this patch, which keeps its own dataset name. + +{#patch-tobytes .member} +**`toBytes`**`(): Uint8Array` [¶](#patch-tobytes){.headerlink} +: A copy of the patch as latkit's bytes: one frame, naming the dataset if the patch does. + +(patchsource)= + +### `PatchSource` + +{.signature} +`type PatchSource = Uint8Array | ArrayBuffer | Blob | Response | ReadableStream | string | object` + +What [`Patch.read()`](#patch-read) accepts: bytes, a `Blob`, a `Response`, a byte stream, a URL to fetch as a +string, or a parsed JSON value. The content is latkit's bytes or a patch's JSON. + +(rowids)= + +### `RowIds` + +{.signature} +`type RowIds =` [`Ids`](#ids) `| readonly (`[`Id`](#id) `| null)[] | Uint32Array | Float64Array` + +Row ids: a list of numbers or strings, `{ count }` for ids `0` to `count - 1`, or a `Uint32Array` +or `Float64Array` of numbers. + +{#rowids-ids .member} +[`Ids`](#ids) [¶](#rowids-ids){.headerlink} +: Row ids: a list of numbers or texts, or `{count}` for rows 0 to count - 1. + +(values)= + +### `Values` + +{.signature} +`type Values = Float64Array | Float32Array | Int32Array | Uint32Array | Uint8Array | readonly (number | null)[] | readonly (boolean | null)[] | readonly (string | null)[] | readonly (`[`Geometry`](#geometry) `| null)[]` + +One field's values, one per row, with `null` for a missing value. Numbers come as a typed array +or an array. A `Uint8Array` holds booleans, nonzero being true. Booleans, strings, and geometries +also come as arrays. Values are copied when passed, so the arrays can be reused at once. + +(geometry)= + +### `Geometry` + +{.signature} +`type Geometry = readonly number[] | readonly Geometry[]` + +A point `[x, y]`, or a nested list of points: a path (a list of points), parts (a list of paths, +such as a polygon's rings), or polygons (a list of parts). + +(reference)= + +### `Reference` + +{.signature} +`interface Reference` + +A reference field: for each row, a row of another table. + +{#reference-reference .member} +**`reference`**`: string` [¶](#reference-reference){.headerlink} +: The table referenced. + +{#reference-ids .member} +**`ids`**`:` [`RowIds`](#rowids) [¶](#reference-ids){.headerlink} +: For each row, the id of the row it references, or `null` to reference none. + +(reference-function)= + +### `reference()` + +{.signature} +`function reference(table: string, ids:` [`RowIds`](#rowids)`):` [`Reference`](#reference) + +Builds a [`Reference`](#reference) field: for each row, the id of the row of `table` it references. + +(fields)= + +### `Fields` + +{.signature} +`type Fields = Readonly>` + +A table's fields by name: values, or a [`Reference`](#reference). + +(readablebytes)= + +### `ReadableBytes` + +{.signature} +`interface ReadableBytes` + +A byte stream: anything shaped like a browser or Node.js `ReadableStream` of `Uint8Array`. + +{#readablebytes-getreader .member} +**`getReader`**`(): { read(): Promise<{ done: boolean; value?: Uint8Array }>; cancel(reason?: unknown): Promise; releaseLock(): void }` [¶](#readablebytes-getreader){.headerlink} +: A reader of the stream's chunks, which latkit reads to the end, or cancels. + ## Expressions Builders for the per-row expressions and colors that scene options take. -### Expr +(expr)= -```ts -/** A value for every row of a mark: a constant, a field, view state, or arithmetic on them. */ -class Expr { - readonly node: ExprNode; - /** Wraps an expression's JSON form. The builders cover common cases. */ - constructor(node: ExprNode); - toJSON(): ExprNode; - /** Reads this expression's fields from table `table` of dataset `data`, joined by row id. */ - from(data: string, table: string): Expr; - /** - * Reads this expression's sampled fields at the last frame at or before the view's `at`, instead - * of interpolating toward the next frame. - */ - step(): Expr; - add(other: ExprInput): Expr; - sub(other: ExprInput): Expr; - mul(other: ExprInput): Expr; - div(other: ExprInput): Expr; - rem(other: ExprInput): Expr; - pow(other: ExprInput): Expr; - min(other: ExprInput): Expr; - max(other: ExprInput): Expr; - clamp(low: ExprInput, high: ExprInput): Expr; - lt(other: ExprInput): Expr; - le(other: ExprInput): Expr; - gt(other: ExprInput): Expr; - ge(other: ExprInput): Expr; - eq(other: ExprInput): Expr; - ne(other: ExprInput): Expr; - and(other: ExprInput): Expr; - or(other: ExprInput): Expr; - not(): Expr; - neg(): Expr; - abs(): Expr; - sqrt(): Expr; - /** The sine of the value, an angle in radians. */ - sin(): Expr; - /** The cosine of the value, an angle in radians. */ - cos(): Expr; - floor(): Expr; - ln(): Expr; - exp(): Expr; - /** Whether a text field equals `text`. */ - is(text: string): Expr; - /** - * Whether a text field contains `text`, ignoring case. In a mark, the field must be in the mark's - * own table. - */ - contains(text: string): Expr; - /** - * Maps the value to a color for every row through `map`, a catalog name or custom colors. The - * domain fits the values shown unless set with {@link Colormapped.domain}. - */ - colormap(map: Colormap): Colormapped; - /** - * Maps the value linearly onto `[low, high]`, clamped. The domain fits the values shown unless - * set with {@link Mapped.domain}. - */ - scale(low: number, high: number): Mapped; -} -``` +### `Expr` -### ExprInput +{.signature} +`class Expr` -```ts -/** A value option: an {@link Expr}, or its JSON form, in which a string is a field name. */ -type ExprInput = Expr | ExprNode; +A value for every row of a mark: a constant, a field, view state, or arithmetic on them. + +```{rubric} Constructor ``` -### Mapped +{#expr-new .member} +**`new Expr`**`(node:` [`ExprNode`](#exprnode)`)` [¶](#expr-new){.headerlink} +: Wraps an expression's JSON form. The builders cover common cases. -```ts -/** A value mapped linearly onto a range, with an input domain that can be set. */ -class Mapped extends Expr { - /** Sets the input values that map to the range's ends, instead of fitting the values shown. */ - domain(low: number, high: number): Mapped; -} +```{rubric} Properties ``` -### Missing +{#expr-node .member} +`readonly` **`node`**`:` [`ExprNode`](#exprnode) [¶](#expr-node){.headerlink} +: The expression's JSON form. -```ts -/** A missing value, valid as a value or a color. An option given one uses the theme's value. */ -class Missing extends Expr { - readonly node: MissingNode; - constructor(); - toJSON(): MissingNode; -} +```{rubric} Methods ``` -### Paint +{#expr-tojson .member} +**`toJSON`**`():` [`ExprNode`](#exprnode) [¶](#expr-tojson){.headerlink} +: The JSON form, which `JSON.stringify` writes for the expression. -```ts -/** - * A color for every row of a mark: a constant, a value mapped through a colormap, or a conditional - * choosing between colors. Color options take a `Paint` or a CSS color string. - */ -class Paint { - readonly node: PaintNode; - /** Wraps a color's JSON form. The builders cover common cases. */ - constructor(node: PaintNode); - toJSON(): PaintNode; -} -``` +{#expr-from .member} +**`from`**`(data: string, table: string): Expr` [¶](#expr-from){.headerlink} +: Reads this expression's fields from table `table` of dataset `data`, joined by row id. -### PaintInput +{#expr-step .member} +**`step`**`(): Expr` [¶](#expr-step){.headerlink} +: Reads this expression's sampled fields at the last frame at or before the view's `at`, instead + of interpolating toward the next frame. -```ts -/** - * A color option: a CSS color, a {@link Paint}, or its JSON form. A string here is always a CSS - * color, never a field name; map a field to colors with `field(name).colormap(map)`. - */ -type PaintInput = Color | Paint | PaintNode; -``` +{#expr-add .member} +**`add`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-add){.headerlink} +: The sum of this value and `other`. -### Colormapped +{#expr-sub .member} +**`sub`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-sub){.headerlink} +: This value minus `other`. -```ts -/** A value mapped through a colormap, with an input domain that can be set. */ -class Colormapped extends Paint { - /** Sets the input values that map to the colormap's ends, instead of fitting the values shown. */ - domain(low: number, high: number): Colormapped; -} -``` +{#expr-mul .member} +**`mul`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-mul){.headerlink} +: The product of this value and `other`. -### Color +{#expr-div .member} +**`div`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-div){.headerlink} +: This value divided by `other`. Dividing by 0 gives a missing value. -```ts -/** - * A CSS color, such as `'#ffb000'` or `'rebeccapurple'`. In a color option, a string is a color. - */ -type Color = string; -``` +{#expr-rem .member} +**`rem`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-rem){.headerlink} +: The remainder of this value divided by `other`, with the sign of `other`: `-1 rem 3` is 2. -### field +{#expr-pow .member} +**`pow`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-pow){.headerlink} +: This value raised to the power `other`. -```ts -/** Reads field `name` of each row. */ -const field: (name: string) => Expr; -``` +{#expr-min .member} +**`min`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-min){.headerlink} +: The smaller of this value and `other`. -### color +{#expr-max .member} +**`max`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-max){.headerlink} +: The larger of this value and `other`. -```ts -/** A constant color, from a CSS color string. */ -const color: (css: Color) => Paint; -``` +{#expr-clamp .member} +**`clamp`**`(low:` [`ExprInput`](#exprinput)`, high:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-clamp){.headerlink} +: This value limited to the range from `low` to `high`. -### missing +{#expr-lt .member} +**`lt`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-lt){.headerlink} +: 1 where this value is less than `other`, else 0. -```ts -/** A missing value: an option given one uses the theme's value. */ -const missing: () => Missing; -``` +{#expr-le .member} +**`le`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-le){.headerlink} +: 1 where this value is at most `other`, else 0. -### when +{#expr-gt .member} +**`gt`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-gt){.headerlink} +: 1 where this value is greater than `other`, else 0. -```ts -/** - * Starts a conditional. `when(c).then(a).when(d).then(b).otherwise(e)` takes the value of the first - * case whose condition holds, else `e`. If the first `then` gets a color (a CSS string, a - * {@link Paint}, a missing value, or a color's JSON form), every value of the conditional is a - * color, and strings are CSS colors. Otherwise every value is an expression. - */ -function when(condition: ExprInput): When; -``` +{#expr-ge .member} +**`ge`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-ge){.headerlink} +: 1 where this value is at least `other`, else 0. -### When +{#expr-eq .member} +**`eq`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-eq){.headerlink} +: 1 where this value equals `other`, else 0. Colors compare too. -```ts -/** A conditional's first condition, awaiting its value from `then`. */ -class When { - /** Sets the color where the condition holds; every value of the conditional is then a color. */ - then(value: PaintInput): Then; - /** Sets the value where the condition holds. */ - then(value: ExprInput): Then; -} -``` +{#expr-ne .member} +**`ne`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-ne){.headerlink} +: 1 where this value differs from `other`, else 0. Colors compare too. -### Then +{#expr-and .member} +**`and`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-and){.headerlink} +: 1 where this value and `other` are both above 0.5, else 0. -```ts -/** A conditional's cases so far, awaiting another `when` case or the `otherwise` fallback. */ -class Then { - /** Adds a case, tried when no earlier condition holds. */ - when(condition: ExprInput): Next; - /** Sets the value where no condition holds, and returns the finished conditional. */ - otherwise(value: InputOf): T; -} -``` +{#expr-or .member} +**`or`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-or){.headerlink} +: 1 where this value or `other` is above 0.5, else 0. -### Next +{#expr-not .member} +**`not`**`(): Expr` [¶](#expr-not){.headerlink} +: 1 where this value is at most 0.5, else 0. -```ts -/** A conditional's next condition, awaiting its value from `then`. */ -class Next { - /** Sets the value where this condition holds. */ - then(value: InputOf): Then; -} -``` +{#expr-neg .member} +**`neg`**`(): Expr` [¶](#expr-neg){.headerlink} +: The value negated. -### InputOf +{#expr-abs .member} +**`abs`**`(): Expr` [¶](#expr-abs){.headerlink} +: The absolute value. -```ts -/** A conditional's value input: {@link PaintInput} for colors, else {@link ExprInput}. */ -type InputOf = T extends Paint ? PaintInput : ExprInput; -``` +{#expr-sqrt .member} +**`sqrt`**`(): Expr` [¶](#expr-sqrt){.headerlink} +: The square root. Negative values give a missing value. -### px +{#expr-sin .member} +**`sin`**`(): Expr` [¶](#expr-sin){.headerlink} +: The sine of the value, an angle in radians. -```ts -/** - * A size in CSS pixels regardless of the space's unit, so it stays the same on screen as the view - * zooms: `px(1)` keeps a diagram's stroke 1 pixel wide at any zoom. Given a number, the result also - * works as a theme length. - */ -function px(size: number): { - px: number; -}; -function px(size: ExprInput): Px; +{#expr-cos .member} +**`cos`**`(): Expr` [¶](#expr-cos){.headerlink} +: The cosine of the value, an angle in radians. + +{#expr-floor .member} +**`floor`**`(): Expr` [¶](#expr-floor){.headerlink} +: The largest whole number at most the value. + +{#expr-ln .member} +**`ln`**`(): Expr` [¶](#expr-ln){.headerlink} +: The natural logarithm. Values at most 0 give a missing value. + +{#expr-exp .member} +**`exp`**`(): Expr` [¶](#expr-exp){.headerlink} +: e raised to the value. + +{#expr-is .member} +**`is`**`(text: string): Expr` [¶](#expr-is){.headerlink} +: Whether a text field equals `text`. + +{#expr-contains .member} +**`contains`**`(text: string): Expr` [¶](#expr-contains){.headerlink} +: Whether a text field contains `text`, ignoring case. In a mark, the field must be in the mark's + own table. + +{#expr-colormap .member} +**`colormap`**`(map:` [`Colormap`](#colormap)`):` [`Colormapped`](#colormapped) [¶](#expr-colormap){.headerlink} +: Maps the value to a color for every row through `map`, a catalog name or custom colors. The + domain fits the values shown unless set with [`Colormapped.domain()`](#colormapped-domain). + +{#expr-scale .member} +**`scale`**`(low: number, high: number):` [`Mapped`](#mapped) [¶](#expr-scale){.headerlink} +: Maps the value linearly onto `[low, high]`, clamped. The domain fits the values shown unless + set with [`Mapped.domain()`](#mapped-domain). + +(exprinput)= + +### `ExprInput` + +{.signature} +`type ExprInput =` [`Expr`](#expr) `|` [`ExprNode`](#exprnode) + +A value option: an [`Expr`](#expr), or its JSON form, in which a string is a field name. + +{#exprinput-expr .member} +[`Expr`](#expr) [¶](#exprinput-expr){.headerlink} +: A value for every row of a mark: a constant, a field, view state, or arithmetic on them. + +{#exprinput-exprnode .member} +[`ExprNode`](#exprnode) [¶](#exprinput-exprnode){.headerlink} +: A value for each row: a number, a boolean, a field's name, or a node that computes one. + +(mapped)= + +### `Mapped` + +{.signature} +`class Mapped extends` [`Expr`](#expr) + +A value mapped linearly onto a range, with an input domain that can be set. + +```{rubric} Methods ``` -### units +{#mapped-domain .member} +**`domain`**`(low: number, high: number): Mapped` [¶](#mapped-domain){.headerlink} +: Sets the input values that map to the range's ends, instead of fitting the values shown. -```ts -/** - * A size in position units (degrees on the earth) regardless of the space's unit, so it scales as - * the view zooms. `within` limits its on-screen size in CSS pixels: `units('r', { atLeast: 2 })` - * grows with the map and never shrinks below 2 pixels. A marker keeps its proportions, with the - * limits applied to its larger side. Given only a number, the result also works as a theme length. - */ -function units(size: number): { - units: number; -}; -function units(size: ExprInput, within?: Within): Units; +(missing)= + +### `Missing` + +{.signature} +`class Missing extends` [`Expr`](#expr) + +A missing value, valid as a value or a color. An option given one uses the theme's value. + +```{rubric} Constructor ``` -### Within +{#missing-new .member} +**`new Missing`**`()` [¶](#missing-new){.headerlink} +: A missing value. [`missing()`](#missing-function) makes one too. -```ts -/** On-screen limits, in CSS pixels, for a size given in position units. */ -interface Within { - /** The smallest size in CSS pixels, however far the view zooms out. */ - atLeast?: number; - /** The largest size in CSS pixels, however far the view zooms in. */ - atMost?: number; -} +```{rubric} Properties ``` -### selected +{#missing-node .member} +`readonly` **`node`**`:` [`MissingNode`](#missingnode) [¶](#missing-node){.headerlink} +: The missing value's JSON form, `{ missing: true }`. -```ts -/** Whether the row is selected. */ -const selected: () => Expr; +```{rubric} Methods ``` -### hovered +{#missing-tojson .member} +**`toJSON`**`():` [`MissingNode`](#missingnode) [¶](#missing-tojson){.headerlink} +: The JSON form, which `JSON.stringify` writes for the missing value. -```ts -/** Whether the pointer is over the row. */ -const hovered: () => Expr; +(paint)= + +### `Paint` + +{.signature} +`class Paint` + +A color for every row of a mark: a constant, a value mapped through a colormap, or a conditional +choosing between colors. Color options take a `Paint` or a CSS color string. + +```{rubric} Constructor ``` -### pointer +{#paint-new .member} +**`new Paint`**`(node:` [`PaintNode`](#paintnode)`)` [¶](#paint-new){.headerlink} +: Wraps a color's JSON form. The builders cover common cases. -```ts -/** CSS pixels from a vertex to the pointer. Vertex marks only, and not in a vertex's position. */ -const pointer: () => Expr; +```{rubric} Properties ``` -### zoom +{#paint-node .member} +`readonly` **`node`**`:` [`PaintNode`](#paintnode) [¶](#paint-node){.headerlink} +: The color's JSON form. -```ts -/** The camera's zoom: the web mercator level on the earth, log₂ CSS pixels per unit elsewhere. */ -const zoom: () => Expr; +```{rubric} Methods ``` -### elapsed +{#paint-tojson .member} +**`toJSON`**`():` [`PaintNode`](#paintnode) [¶](#paint-tojson){.headerlink} +: The JSON form, which `JSON.stringify` writes for the color. -```ts -/** Seconds since the view was created. A mark that reads it redraws every frame. */ -const elapsed: () => Expr; +(paintinput)= + +### `PaintInput` + +{.signature} +`type PaintInput =` [`Color`](#color) `|` [`Paint`](#paint) `|` [`PaintNode`](#paintnode) + +A color option: a CSS color, a [`Paint`](#paint), or its JSON form. A string here is always a CSS +color, never a field name; map a field to colors with `field(name).colormap(map)`. + +{#paintinput-color .member} +[`Color`](#color) [¶](#paintinput-color){.headerlink} +: A CSS color, such as `'#ffb000'` or `'rebeccapurple'`. + +{#paintinput-paint .member} +[`Paint`](#paint) [¶](#paintinput-paint){.headerlink} +: A color for every row of a mark: a constant, a value mapped through a colormap, or a conditional choosing between colors. + +{#paintinput-paintnode .member} +[`PaintNode`](#paintnode) [¶](#paintinput-paintnode){.headerlink} +: A color for each row: a CSS color, a value through a colormap, or a conditional choosing between colors. + +(colormapped)= + +### `Colormapped` + +{.signature} +`class Colormapped extends` [`Paint`](#paint) + +A value mapped through a colormap, with an input domain that can be set. + +```{rubric} Methods ``` -### icon +{#colormapped-domain .member} +**`domain`**`(low: number, high: number): Colormapped` [¶](#colormapped-domain){.headerlink} +: Sets the input values that map to the colormap's ends, instead of fitting the values shown. -```ts -/** - * An icon marker: each row shows `images[index]`, fitted in the marker's box, with `index` counting - * from 0. The pixels are copied into the marker. - */ -function icon(images: readonly RgbaImage[], index: ExprInput): Marker; +(color)= + +### `Color` + +{.signature} +`type Color = string` + +A CSS color, such as `'#ffb000'` or `'rebeccapurple'`. In a color option, a string is a color. + +(field)= + +### `field()` + +{.signature} +`function field(name: string):` [`Expr`](#expr) + +Reads field `name` of each row. + +(color-function)= + +### `color()` + +{.signature} +`function color(css:` [`Color`](#color)`):` [`Paint`](#paint) + +A constant color, from a CSS color string. + +(missing-function)= + +### `missing()` + +{.signature} +`function missing():` [`Missing`](#missing) + +A missing value: an option given one uses the theme's value. + +(when-function)= + +### `when()` + +{.signature} +`function when(condition:` [`ExprInput`](#exprinput)`):` [`When`](#when) + +Starts a conditional. `when(c).then(a).when(d).then(b).otherwise(e)` takes the value of the first +case whose condition holds, else `e`. If the first `then` gets a color (a CSS string, a +[`Paint`](#paint), a missing value, or a color's JSON form), every value of the conditional is a +color, and strings are CSS colors. Otherwise every value is an expression. + +(when)= + +### `When` + +{.signature} +`class When` + +A conditional's first condition, awaiting its value from `then`. + +```{rubric} Methods ``` -### RgbaImage +{#when-then .member} +**`then`**`(value:` [`PaintInput`](#paintinput)`):` [`Then`](#then)`<`[`Paint`](#paint)`>` [¶](#when-then){.headerlink} +: Sets the color where the condition holds; every value of the conditional is then a color. -```ts -/** RGBA pixels with straight alpha, rows top to bottom. A browser `ImageData` has this shape. */ -interface RgbaImage { - width: number; - height: number; - data: Uint8ClampedArray; -} +{.member} +**`then`**`(value:` [`ExprInput`](#exprinput)`):` [`Then`](#then)`<`[`Expr`](#expr)`>` +: Sets the value where the condition holds. + +(then)= + +### `Then` + +{.signature} +`class Then` + +A conditional's cases so far, awaiting another `when` case or the `otherwise` fallback. + +```{rubric} Methods ``` -### colormapCss +{#then-when .member} +**`when`**`(condition:` [`ExprInput`](#exprinput)`):` [`Next`](#next)`` [¶](#then-when){.headerlink} +: Adds a case, tried when no earlier condition holds. -```ts -/** - * Returns a CSS `linear-gradient(...)` with the colors the GPU draws for `map`, a catalog name or a - * custom colormap. `direction` is the gradient's CSS direction, `'to right'` by default. A - * categorical map gives one solid band per category. Needs `await load()` first, and throws an - * `invalid` error for an invalid `map`. - */ -function colormapCss(map: Colormap, direction?: string): string; +{#then-otherwise .member} +**`otherwise`**`(value:` [`InputOf`](#inputof)`): T` [¶](#then-otherwise){.headerlink} +: Sets the value where no condition holds, and returns the finished conditional. + +(next)= + +### `Next` + +{.signature} +`class Next` + +A conditional's next condition, awaiting its value from `then`. + +```{rubric} Methods ``` +{#next-then .member} +**`then`**`(value:` [`InputOf`](#inputof)`):` [`Then`](#then)`` [¶](#next-then){.headerlink} +: Sets the value where this condition holds. + +(inputof)= + +### `InputOf` + +{.signature} +`type InputOf = T extends` [`Paint`](#paint) `?` [`PaintInput`](#paintinput) `:` [`ExprInput`](#exprinput) + +A conditional's value input: [`PaintInput`](#paintinput) for colors, else [`ExprInput`](#exprinput). + +(px-function)= + +### `px()` + +{.signature} +`function px(size: number): { px: number }` + +{.signature} +`function px(size:` [`ExprInput`](#exprinput)`):` [`Px`](#px) + +A size in CSS pixels regardless of the space's unit, so it stays the same on screen as the view +zooms: `px(1)` keeps a diagram's stroke 1 pixel wide at any zoom. Given a number, the result also +works as a theme length. + +(units-function)= + +### `units()` + +{.signature} +`function units(size: number): { units: number }` + +{.signature} +`function units(size:` [`ExprInput`](#exprinput)`, within?:` [`Within`](#within)`):` [`Units`](#units) + +A size in position units (degrees on the earth) regardless of the space's unit, so it scales as +the view zooms. `within` limits its on-screen size in CSS pixels: `units('r', { atLeast: 2 })` +grows with the map and never shrinks below 2 pixels. A marker keeps its proportions, with the +limits applied to its larger side. Given only a number, the result also works as a theme length. + +(within)= + +### `Within` + +{.signature} +`interface Within` + +On-screen limits, in CSS pixels, for a size given in position units. + +{#within-atleast .member} +**`atLeast`**`?: number` [¶](#within-atleast){.headerlink} +: The smallest size in CSS pixels, however far the view zooms out. + +{#within-atmost .member} +**`atMost`**`?: number` [¶](#within-atmost){.headerlink} +: The largest size in CSS pixels, however far the view zooms in. + +(selected)= + +### `selected()` + +{.signature} +`function selected():` [`Expr`](#expr) + +Whether the row is selected. + +(hovered)= + +### `hovered()` + +{.signature} +`function hovered():` [`Expr`](#expr) + +Whether the pointer is over the row. + +(pointer)= + +### `pointer()` + +{.signature} +`function pointer():` [`Expr`](#expr) + +CSS pixels from a vertex to the pointer. Vertex marks only, and not in a vertex's position. + +(zoom-function)= + +### `zoom()` + +{.signature} +`function zoom():` [`Expr`](#expr) + +The camera's zoom: the web mercator level on the earth, log₂ CSS pixels per unit elsewhere. + +(elapsed)= + +### `elapsed()` + +{.signature} +`function elapsed():` [`Expr`](#expr) + +Seconds since the view was created. A mark that reads it redraws every frame. + +(icon)= + +### `icon()` + +{.signature} +`function icon(images: readonly` [`RgbaImage`](#rgbaimage)`[], index:` [`ExprInput`](#exprinput)`):` [`Marker`](#marker) + +An icon marker: each row shows `images[index]`, fitted in the marker's box, with `index` counting +from 0. The pixels are copied into the marker. + +(rgbaimage)= + +### `RgbaImage` + +{.signature} +`interface RgbaImage` + +RGBA pixels with straight alpha, rows top to bottom. A browser `ImageData` has this shape. + +{#rgbaimage-width .member} +**`width`**`: number` [¶](#rgbaimage-width){.headerlink} +: The width in pixels. + +{#rgbaimage-height .member} +**`height`**`: number` [¶](#rgbaimage-height){.headerlink} +: The height in pixels. + +{#rgbaimage-data .member} +**`data`**`: Uint8ClampedArray` [¶](#rgbaimage-data){.headerlink} +: 4 bytes per pixel, red, green, blue, and alpha, row after row. + +(colormapcss)= + +### `colormapCss()` + +{.signature} +`function colormapCss(map:` [`Colormap`](#colormap)`, direction?: string): string` + +Returns a CSS `linear-gradient(...)` with the colors the GPU draws for `map`, a catalog name or a +custom colormap. `direction` is the gradient's CSS direction, `'to right'` by default. A +categorical map gives one solid band per category. Needs `await load()` first, and throws an +`invalid` error for an invalid `map`. + ## Views A GPU makes views, and canvases that show them. -### Gpu +(gpu)= -```ts -/** - * A WebGPU device that views draw with. The views and canvases of one `Gpu` share its memory and - * draw together in each animation frame, so linked views always show the same moment. - */ -class Gpu { - /** - * Requests a WebGPU device, calling `load()` first. The first call reuses the device that - * `load()` requested. Rejects with an `unsupported` error when the browser has no WebGPU device. - */ - static create(): Promise; - /** - * Creates a view that draws `scene` from the datasets in `options.sources`. Show it in a - * {@link Canvas}, or draw it into pictures with a `Shot`, with or without a canvas. Throws an - * `invalid` error for an invalid scene. - */ - view(scene: Scene, options?: ViewOptions): View; - /** - * Creates a canvas that shows views in `target`, placed by `options.layout`. A `` element - * sizes itself to its CSS box at the device pixel ratio, and passes pointer, wheel, and key input - * to the views in its panels. An `OffscreenCanvas`, as in a worker, keeps the size it is given at - * `options.pixelRatio` pixels per CSS pixel, and takes no input. Throws an `invalid` error for an - * invalid layout, such as a view already shown in another panel. - */ - canvas(target: HTMLCanvasElement | OffscreenCanvas, options?: CanvasOptions): Canvas; - /** - * Calls `listener` every animation frame with the frame's timestamp in milliseconds, before the - * views draw, so changes it makes show in that frame. While any listener is registered, a frame - * runs every animation frame. Returns a function that removes the listener. - */ - on(type: 'frame', listener: (now: number) => void): () => void; - /** Whether the device was lost. Drawing then does nothing; create a new `Gpu`. */ - get lost(): boolean; - /** - * The GPU memory budget for data, shared by every view of this GPU; see {@link Budget}. Defaults - * to half the device's largest storage buffer, and 32 MiB uploaded per frame. - */ - get budget(): Budget; - set budget(budget: Budget); - /** - * Exact counts of the work this GPU has done since it was created; see {@link Work}. The counts - * repeat for the same budget, device limits, and frames, so tests and benchmarks can assert them. - */ - get work(): Work; - /** - * Bytes of GPU memory the views of this GPU draw from: data pages, and each view's per-row data. - */ - get resident(): number; - /** Bytes waiting to upload to the GPU, across every view of this GPU. */ - get uploading(): number; - /** - * Draws one frame immediately at time `now`, in milliseconds on the `requestAnimationFrame` - * clock: it runs the frame listeners, then draws every canvas that needs a frame. After the first - * call, this GPU stops requesting animation frames. A test or benchmark that steps every frame - * itself then draws the same frames on any device. - */ - step(now: number): void; - /** - * Resolves once the GPU has finished all submitted work, every readback has arrived, and every - * pipeline a view or shot requested has compiled. A frame stepped after that uses all of it. - */ - done(wait?: Wait): Promise; - /** - * Resolves when, after this call, work a view is waiting on arrives (a readback, a pipeline - * compile, a job, a label text, or a fetch), or a view requests a frame. A harness that steps - * frames awaits it to know when to step again. - */ - landed(wait?: Wait): Promise; - /** - * Destroys this GPU's canvases, views, and frame listeners, then frees the device. Other GPUs and - * datasets stay usable. Calling it again does nothing; other methods throw afterwards. - */ - destroy(): void; - [Symbol.dispose](): void; -} -``` +### `Gpu` -### View - -````ts -/** - * A scene drawn from its sources; created by `Gpu.view()`. A canvas shows it and passes it input, - * and a `Shot` draws it into pictures. - */ -class View { - /** - * The scene drawn. Marks that match a mark of the previous scene by kind, dataset, table, and - * name keep their state and ease to their new options. Setting an invalid scene throws an - * `invalid` error and keeps the current one. - */ - get scene(): Scene; - set scene(scene: Scene); - /** - * The datasets the view draws, matched to marks by dataset name. The view draws a `Data` at its - * latest version, from the frame after each patch; a `Snapshot` unchanged; and `Tiles` as the - * pieces it needs. A mark whose dataset no longer fits it draws nothing; it is listed in - * `failures`, and the next frame emits an `error` event. Setting two sources with the same name - * throws an `invalid` error. - */ - get sources(): readonly Source[]; - set sources(sources: readonly Source[]); - /** - * The axis coordinate shown, such as a time. Marks read sampled fields at it, interpolating - * between the frame at or before it and the next, unless an expression uses `step()`. - */ - get at(): number; - set at(at: number); - /** - * Where the view looks. Setting it moves the camera at once; use {@link View.fly} to ease. A - * camera of the wrong kind for the space, such as a plot camera on a map, is ignored. - */ - get camera(): Camera; - set camera(camera: Camera); - /** - * CSS pixels kept clear at the top, right, bottom, and left, such as where page panels cover the - * view. The camera centers and fits within the rest. - */ - get insets(): Insets; - set insets(insets: Insets); - /** - * Degrees per second the camera turns about its center on a map or plane; 0 stops it. A diagram - * never turns. - */ - get orbit(): number; - set orbit(degreesPerSecond: number); - /** The selected rows, one set per table. Setting it selects exactly those rows. */ - get selection(): readonly Rows[]; - set selection(rows: readonly Rows[]); - /** The row under the pointer, or the row keyboard navigation moved to; `null` if none. */ - get hovered(): Hit | null; - /** - * The kinds of pending work that kept the last frame from being complete; empty when it was - * complete. See {@link Waiting}. - */ - get waiting(): Waiting; - /** Whether the last frame was complete, with nothing in `waiting` pending. Shots wait for it. */ - get complete(): boolean; - /** - * An error for each mark that draws nothing because its data no longer fits it, such as a missing - * table. Each error's `path` names the mark. - */ - get failures(): readonly LatkitError[]; - /** Counts and timings of the view's frames; see {@link Stats}. */ - get stats(): Stats; - /** Whether the view needs a frame, because it changed or work it was waiting on arrived. */ - get wantsFrame(): boolean; - /** - * Frames what `fit` names, every row by default, keeping pitch and bearing. It takes effect on - * the next frame, or a frame or two later when the extent must be measured on the GPU. - */ - fit(fit?: Fit): void; - /** - * Eases the camera to `camera` over twice the scene's ease time, or at once when motion is - * reduced. A camera of the wrong kind for the space is ignored. - */ - fly(camera: Camera): void; - /** - * Projects `position` to a point on screen with the current camera, for anchoring HTML overlays; - * `null` when it is behind the camera. - */ - screen(position: Position): Point | null; - /** - * The position on the ground under screen point `point`: at elevation 0 on a plane, on the map or - * globe on the earth, the values there on a plot. `null` where the point looks past the ground. - */ - position(point: Point): Position | null; - /** - * Resolves to where `row` is on screen once a frame draws the view, or `null` when no mark draws - * it in view. A diagram's vertices resolve at once. - */ - screenOf(row: Row, wait?: Wait): Promise; - /** - * Resolves, once a frame draws the view, to every row of a pickable mark within `options.radius` - * CSS pixels (0 by default) of `point`, nearest first. - */ - pick( - point: Point, - options?: { - radius?: number; - } & Wait, - ): Promise; - /** - * The rows connected to `row`, one set per table: a vertex's neighbors across edges and nets, or - * an edge's or a net's vertices. The cost is proportional to the row's degree. - */ - neighbors(row: Row): Rows[]; - /** - * Runs the scene's layout again for every vertex row without a position: a diagram's blocks, or - * points on a plane or the earth. Otherwise a layout keeps the places it gave, so rows added - * later are placed around the rest. Resolves, once the layout finishes, to one patch per dataset - * that writes each laid-out row's place into its mark's position fields. Applying the patches - * makes the places part of the data. Marks whose position is not read from fields are left out. - * - * ```ts - * for (const patch of await view.arrange()) if (patch.data === grid.name) grid.apply(patch); - * ``` - */ - arrange(wait?: Wait): Promise; - /** - * The version of dataset `name` the view draws, or `undefined` if it draws none of that name: the - * latest version of a `Data` the view has taken, a given snapshot, or the pieces of `Tiles` - * fetched for its camera. - */ - snapshot(name: string): Snapshot | undefined; - /** - * Calls `listener` with each event of `type` emitted while it is registered, and returns a - * function that removes it. Past events are not replayed; read the current state from the - * property a change event is named after. - */ - on(type: T, listener: (event: ViewEvents[T]) => void): () => void; - /** - * Frees the view and rejects its unanswered queries. A canvas showing it keeps its last frame. - * Calling it again does nothing; other methods throw afterwards. - */ - destroy(): void; - [Symbol.dispose](): void; -} -```` - -### ViewOptions +{.signature} +`class Gpu` -```ts -/** Options for `Gpu.view()`. */ -interface ViewOptions { - /** The datasets the scene draws; see {@link View.sources}. */ - sources?: readonly Source[]; -} +A WebGPU device that views draw with. The views and canvases of one `Gpu` share its memory and +draw together in each animation frame, so linked views always show the same moment. + +```{rubric} Properties ``` -### ViewEvents +{#gpu-lost .member} +`readonly` **`lost`**`: boolean` [¶](#gpu-lost){.headerlink} +: Whether the device was lost. Drawing then does nothing; create a new [`Gpu`](#gpu). -```ts -/** - * The events a view emits, by type, mapped to each event's detail. Intents (`click`, `open`, - * `menu`, `drag`, `connect`, `delete`) are emitted once each, in the order the user acted. Changes - * (`hovered`, `selection`, `camera`, `waiting`) are named after the property that changed and carry - * only its latest value. An `error` event's detail is a {@link LatkitError}. Setters emit nothing, - * so events report only what the application did not do itself. - */ -type ViewEvents = { - [E in ViewEvent as E['type']]: E extends { - type: 'error'; - } - ? LatkitError - : E['detail']; -}; -``` +{#gpu-budget .member} +**`budget`**`:` [`Budget`](#budget) [¶](#gpu-budget){.headerlink} +: The GPU memory budget for data, shared by every view of this GPU; see [`Budget`](#budget). Defaults + to half the device's largest storage buffer, and 32 MiB uploaded per frame. -### Insets +{#gpu-work .member} +`readonly` **`work`**`:` [`Work`](#work) [¶](#gpu-work){.headerlink} +: Exact counts of the work this GPU has done since it was created; see [`Work`](#work). The counts + repeat for the same budget, device limits, and frames, so tests and benchmarks can assert them. -```ts -/** CSS pixels kept clear at the top, right, bottom, and left. */ -type Insets = [top: number, right: number, bottom: number, left: number]; +{#gpu-resident .member} +`readonly` **`resident`**`: number` [¶](#gpu-resident){.headerlink} +: Bytes of GPU memory the views of this GPU draw from: data pages, and each view's per-row data. + +{#gpu-uploading .member} +`readonly` **`uploading`**`: number` [¶](#gpu-uploading){.headerlink} +: Bytes waiting to upload to the GPU, across every view of this GPU. + +```{rubric} Methods ``` -### Point +{#gpu-create .member} +`static` **`create`**`(): Promise` [¶](#gpu-create){.headerlink} +: Requests a WebGPU device, calling [`load()`](#load) first. The first call reuses the device that + `load()` requested. Rejects with an `unsupported` error when the browser has no WebGPU device. + +{#gpu-view .member} +**`view`**`(scene:` [`Scene`](#scene)`, options?:` [`ViewOptions`](#viewoptions)`):` [`View`](#view) [¶](#gpu-view){.headerlink} +: Creates a view that draws `scene` from the datasets in `options.sources`. Show it in a + [`Canvas`](#canvas), or draw it into pictures with a `Shot`, with or without a canvas. Throws an + `invalid` error for an invalid scene. + +{#gpu-canvas .member} +**`canvas`**`(target: HTMLCanvasElement | OffscreenCanvas, options?:` [`CanvasOptions`](#canvasoptions)`):` [`Canvas`](#canvas) [¶](#gpu-canvas){.headerlink} +: Creates a canvas that shows views in `target`, placed by `options.layout`. A `` element + sizes itself to its CSS box at the device pixel ratio, and passes pointer, wheel, and key input + to the views in its panels. An `OffscreenCanvas`, as in a worker, keeps the size it is given at + `options.pixelRatio` pixels per CSS pixel, and takes no input. Throws an `invalid` error for an + invalid layout, such as a view already shown in another panel. + +{#gpu-on .member} +**`on`**`(type: 'frame', listener: (now: number) => void): () => void` [¶](#gpu-on){.headerlink} +: Calls `listener` every animation frame with the frame's timestamp in milliseconds, before the + views draw, so changes it makes show in that frame. While any listener is registered, a frame + runs every animation frame. Returns a function that removes the listener. + +{#gpu-step .member} +**`step`**`(now: number): void` [¶](#gpu-step){.headerlink} +: Draws one frame immediately at time `now`, in milliseconds on the `requestAnimationFrame` + clock: it runs the frame listeners, then draws every canvas that needs a frame. After the first + call, this GPU stops requesting animation frames. A test or benchmark that steps every frame + itself then draws the same frames on any device. + +{#gpu-done .member} +**`done`**`(wait?:` [`Wait`](#wait)`): Promise` [¶](#gpu-done){.headerlink} +: Resolves once the GPU has finished all submitted work, every readback has arrived, and every + pipeline a view or shot requested has compiled. A frame stepped after that uses all of it. + +{#gpu-landed .member} +**`landed`**`(wait?:` [`Wait`](#wait)`): Promise` [¶](#gpu-landed){.headerlink} +: Resolves when, after this call, work a view is waiting on arrives (a readback, a pipeline + compile, a job, a label text, or a fetch), or a view requests a frame. A harness that steps + frames awaits it to know when to step again. + +{#gpu-destroy .member} +**`destroy`**`(): void` [¶](#gpu-destroy){.headerlink} +: Destroys this GPU's canvases, views, and frame listeners, then frees the device. Other GPUs and + datasets stay usable. Calling it again does nothing; other methods throw afterwards. + +{#gpu-symbol-dispose .member} +**`[Symbol.dispose]`**`(): void` [¶](#gpu-symbol-dispose){.headerlink} +: Calls [`Gpu.destroy()`](#gpu-destroy), so a `using` declaration destroys it when its block ends. + +(view)= + +### `View` + +{.signature} +`class View` + +A scene drawn from its sources; created by [`Gpu.view()`](#gpu-view). A canvas shows it and passes it input, +and a [`Shot`](#shot) draws it into pictures. + +```{rubric} Properties +``` -```ts -/** A point on screen, in CSS pixels from the view's top left corner. */ -type Point = [x: number, y: number]; +{#view-scene .member} +**`scene`**`:` [`Scene`](#scene) [¶](#view-scene){.headerlink} +: The scene drawn. Marks that match a mark of the previous scene by kind, dataset, table, and + name keep their state and ease to their new options. Setting an invalid scene throws an + `invalid` error and keeps the current one. + +{#view-sources .member} +**`sources`**`: readonly` [`Source`](#source)`[]` [¶](#view-sources){.headerlink} +: The datasets the view draws, matched to marks by dataset name. The view draws a [`Data`](#data) at its + latest version, from the frame after each patch; a [`Snapshot`](#snapshot) unchanged; and [`Tiles`](#tiles) as the + pieces it needs. A mark whose dataset no longer fits it draws nothing; it is listed in + `failures`, and the next frame emits an `error` event. Setting two sources with the same name + throws an `invalid` error. + +{#view-at .member} +**`at`**`: number` [¶](#view-at){.headerlink} +: The axis coordinate shown, such as a time. Marks read sampled fields at it, interpolating + between the frame at or before it and the next, unless an expression uses `step()`. + +{#view-camera .member} +**`camera`**`:` [`Camera`](#camera) [¶](#view-camera){.headerlink} +: Where the view looks. Setting it moves the camera at once; use [`View.fly()`](#view-fly) to ease. A + camera of the wrong kind for the space, such as a plot camera on a map, is ignored. + +{#view-insets .member} +**`insets`**`:` [`Insets`](#insets) [¶](#view-insets){.headerlink} +: CSS pixels kept clear at the top, right, bottom, and left, such as where page panels cover the + view. The camera centers and fits within the rest. + +{#view-orbit .member} +**`orbit`**`: number` [¶](#view-orbit){.headerlink} +: Degrees per second the camera turns about its center on a map or plane; 0 stops it. A diagram + never turns. + +{#view-selection .member} +**`selection`**`: readonly` [`Rows`](#rows)`[]` [¶](#view-selection){.headerlink} +: The selected rows, one set per table. Setting it selects exactly those rows. + +{#view-hovered .member} +`readonly` **`hovered`**`:` [`Hit`](#hit) `| null` [¶](#view-hovered){.headerlink} +: The row under the pointer, or the row keyboard navigation moved to; `null` if none. + +{#view-waiting .member} +`readonly` **`waiting`**`:` [`Waiting`](#waiting) [¶](#view-waiting){.headerlink} +: The kinds of pending work that kept the last frame from being complete; empty when it was + complete. See [`Waiting`](#waiting). + +{#view-complete .member} +`readonly` **`complete`**`: boolean` [¶](#view-complete){.headerlink} +: Whether the last frame was complete, with nothing in `waiting` pending. Shots wait for it. + +{#view-failures .member} +`readonly` **`failures`**`: readonly` [`LatkitError`](#latkiterror)`[]` [¶](#view-failures){.headerlink} +: An error for each mark that draws nothing because its data no longer fits it, such as a missing + table. Each error's `path` names the mark. + +{#view-stats .member} +`readonly` **`stats`**`:` [`Stats`](#stats) [¶](#view-stats){.headerlink} +: Counts and timings of the view's frames; see [`Stats`](#stats). + +{#view-wantsframe .member} +`readonly` **`wantsFrame`**`: boolean` [¶](#view-wantsframe){.headerlink} +: Whether the view needs a frame, because it changed or work it was waiting on arrived. + +```{rubric} Methods ``` -### Position +{#view-fit .member} +**`fit`**`(fit?:` [`Fit`](#fit)`): void` [¶](#view-fit){.headerlink} +: Frames what `fit` names, every row by default, keeping pitch and bearing. It takes effect on + the next frame, or a frame or two later when the extent must be measured on the GPU. -```ts -/** - * A position in the space's coordinates: x, y, and elevation. On the earth, x and y are longitude - * and latitude; on a plot, the axis coordinate and the value. - */ -type Position = [x: number, y: number, elevation: number]; +{#view-fly .member} +**`fly`**`(camera:` [`Camera`](#camera)`): void` [¶](#view-fly){.headerlink} +: Eases the camera to `camera` over twice the scene's ease time, or at once when motion is + reduced. A camera of the wrong kind for the space is ignored. + +{#view-screen .member} +**`screen`**`(position:` [`Position`](#position)`):` [`Point`](#point) `| null` [¶](#view-screen){.headerlink} +: Projects `position` to a point on screen with the current camera, for anchoring HTML overlays; + `null` when it is behind the camera. + +{#view-position .member} +**`position`**`(point:` [`Point`](#point)`):` [`Position`](#position) `| null` [¶](#view-position){.headerlink} +: The position on the ground under screen point `point`: at elevation 0 on a plane, on the map or + globe on the earth, the values there on a plot. `null` where the point looks past the ground. + +{#view-screenof .member} +**`screenOf`**`(row:` [`Row`](#row)`, wait?:` [`Wait`](#wait)`): Promise<`[`Point`](#point) `| null>` [¶](#view-screenof){.headerlink} +: Resolves to where `row` is on screen once a frame draws the view, or `null` when no mark draws + it in view. A diagram's vertices resolve at once. + +{#view-pick .member} +**`pick`**`(point:` [`Point`](#point)`, options?: { radius?: number } &` [`Wait`](#wait)`): Promise<`[`Hit`](#hit)`[]>` [¶](#view-pick){.headerlink} +: Resolves, once a frame draws the view, to every row of a pickable mark within `options.radius` + CSS pixels (0 by default) of `point`, nearest first. + +{#view-neighbors .member} +**`neighbors`**`(row:` [`Row`](#row)`):` [`Rows`](#rows)`[]` [¶](#view-neighbors){.headerlink} +: The rows connected to `row`, one set per table: a vertex's neighbors across edges and nets, or + an edge's or a net's vertices. The cost is proportional to the row's degree. + +{#view-arrange .member} +**`arrange`**`(wait?:` [`Wait`](#wait)`): Promise<`[`Patch`](#patch)`[]>` [¶](#view-arrange){.headerlink} +: Runs the scene's layout again for every vertex row without a position: a diagram's blocks, or + points on a plane or the earth. Otherwise a layout keeps the places it gave, so rows added + later are placed around the rest. Resolves, once the layout finishes, to one patch per dataset + that writes each laid-out row's place into its mark's position fields. Applying the patches + makes the places part of the data. Marks whose position is not read from fields are left out. + + ```ts + for (const patch of await view.arrange()) if (patch.data === grid.name) grid.apply(patch); + ``` + +{#view-snapshot .member} +**`snapshot`**`(name: string):` [`Snapshot`](#snapshot) `| undefined` [¶](#view-snapshot){.headerlink} +: The version of dataset `name` the view draws, or `undefined` if it draws none of that name: the + latest version of a [`Data`](#data) the view has taken, a given snapshot, or the pieces of [`Tiles`](#tiles) + fetched for its camera. + +{#view-on .member} +**`on`**`(type: T, listener: (event:` [`ViewEvents`](#viewevents)`[T]) => void): () => void` [¶](#view-on){.headerlink} +: Calls `listener` with each event of `type` emitted while it is registered, and returns a + function that removes it. Past events are not replayed; read the current state from the + property a change event is named after. + +{#view-destroy .member} +**`destroy`**`(): void` [¶](#view-destroy){.headerlink} +: Frees the view and rejects its unanswered queries. A canvas showing it keeps its last frame. + Calling it again does nothing; other methods throw afterwards. + +{#view-symbol-dispose .member} +**`[Symbol.dispose]`**`(): void` [¶](#view-symbol-dispose){.headerlink} +: Calls [`View.destroy()`](#view-destroy), so a `using` declaration destroys it when its block ends. + +(viewoptions)= + +### `ViewOptions` + +{.signature} +`interface ViewOptions` + +Options for [`Gpu.view()`](#gpu-view). + +{#viewoptions-sources .member} +**`sources`**`?: readonly` [`Source`](#source)`[]` [¶](#viewoptions-sources){.headerlink} +: The datasets the scene draws; see [`View.sources`](#view-sources). + +(viewevents)= + +### `ViewEvents` + +{.signature} +`type ViewEvents = { [E in` [`ViewEvent`](#viewevent) `as E['type']]: E extends { type: 'error' } ?` [`LatkitError`](#latkiterror) `: E['detail'] }` + +The events a view emits, by type, mapped to each event's detail. Intents (`click`, `open`, +`menu`, `drag`, `connect`, `delete`) are emitted once each, in the order the user acted. Changes +(`hovered`, `selection`, `camera`, `waiting`) are named after the property that changed and carry +only its latest value. An `error` event's detail is a [`LatkitError`](#latkiterror). Setters emit nothing, +so events report only what the application did not do itself. + +(insets)= + +### `Insets` + +{.signature} +`type Insets = [top: number, right: number, bottom: number, left: number]` + +CSS pixels kept clear at the top, right, bottom, and left. + +(point)= + +### `Point` + +{.signature} +`type Point = [x: number, y: number]` + +A point on screen, in CSS pixels from the view's top left corner. + +(position)= + +### `Position` + +{.signature} +`type Position = [x: number, y: number, elevation: number]` + +A position in the space's coordinates: x, y, and elevation. On the earth, x and y are longitude +and latitude; on a plot, the axis coordinate and the value. + +(canvas)= + +### `Canvas` + +{.signature} +`class Canvas` + +Shows views in a `` element or an `OffscreenCanvas`, each in its layout panel; created by +[`Gpu.canvas()`](#gpu-canvas). A frame redraws only the views that need it, or every view after a resize or a +layout change. + +```{rubric} Properties ``` -### Canvas +{#canvas-element .member} +`readonly` **`element`**`: HTMLCanvasElement | OffscreenCanvas` [¶](#canvas-element){.headerlink} +: The `` element or `OffscreenCanvas` it draws into. -```ts -/** - * Shows views in a `` element or an `OffscreenCanvas`, each in its layout panel; created by - * `Gpu.canvas()`. A frame redraws only the views that need it, or every view after a resize or a - * layout change. - */ -class Canvas { - /** The `` element or `OffscreenCanvas` it draws into. */ - get element(): HTMLCanvasElement | OffscreenCanvas; - /** - * The views shown and their regions; later panels draw over earlier ones. Setting it replaces the - * shown views. A view can show in only one panel of one canvas at a time. Setting a layout that - * breaks this, or that has a panel covering no pixels, throws an `invalid` error. - */ - get layout(): readonly Panel[]; - set layout(layout: readonly Panel[]); - /** - * Returns a {@link Shot} of exactly what the canvas shows: its views in its layout, at the - * canvas's CSS size times `options.scale` (1 by default) in pixels. - */ - shot(options?: ShotOptions): Shot; - /** - * Stops drawing and frees the canvas. The views stay usable, and the element keeps its last - * frame. Calling it again does nothing. - */ - destroy(): void; - [Symbol.dispose](): void; -} +{#canvas-layout .member} +**`layout`**`: readonly` [`Panel`](#panel)`[]` [¶](#canvas-layout){.headerlink} +: The views shown and their regions; later panels draw over earlier ones. Setting it replaces the + shown views. A view can show in only one panel of one canvas at a time. Setting a layout that + breaks this, or that has a panel covering no pixels, throws an `invalid` error. + +```{rubric} Methods ``` -### CanvasOptions +{#canvas-shot .member} +**`shot`**`(options?:` [`ShotOptions`](#shotoptions)`):` [`Shot`](#shot) [¶](#canvas-shot){.headerlink} +: Returns a [`Shot`](#shot) of exactly what the canvas shows: its views in its layout, at the + canvas's CSS size times `options.scale` (1 by default) in pixels. + +{#canvas-destroy .member} +**`destroy`**`(): void` [¶](#canvas-destroy){.headerlink} +: Stops drawing and frees the canvas. The views stay usable, and the element keeps its last + frame. Calling it again does nothing. + +{#canvas-symbol-dispose .member} +**`[Symbol.dispose]`**`(): void` [¶](#canvas-symbol-dispose){.headerlink} +: Calls [`Canvas.destroy()`](#canvas-destroy), so a `using` declaration destroys it when its block ends. + +(canvasoptions)= + +### `CanvasOptions` + +{.signature} +`interface CanvasOptions` + +Options for [`Gpu.canvas()`](#gpu-canvas). + +{#canvasoptions-layout .member} +**`layout`**`?: readonly` [`Panel`](#panel)`[]` [¶](#canvasoptions-layout){.headerlink} +: The views shown and their regions; later panels draw over earlier ones. None by default. + +{#canvasoptions-pixelratio .member} +**`pixelRatio`**`?: number` [¶](#canvasoptions-pixelratio){.headerlink} +: Pixels per CSS pixel of an `OffscreenCanvas`, 1 by default; it must be positive. A `` + element uses the device pixel ratio instead. + +(panel)= + +### `Panel` + +{.signature} +`interface Panel` + +A view and the region of a canvas or picture it draws in. + +{#panel-view .member} +**`view`**`:` [`View`](#view) [¶](#panel-view){.headerlink} +: The view drawn. + +{#panel-at .member} +**`at`**`?:` [`Rect`](#rect) [¶](#panel-at){.headerlink} +: Where the view draws; the whole canvas or picture by default. + +(rect)= + +### `Rect` + +{.signature} +`type Rect = readonly [x: number, y: number, width: number, height: number]` + +A region `[x, y, width, height]` in fractions of a canvas or picture, from its top left. + +## Pictures and video + +Shots draw views into images and video. + +(shot)= + +### `Shot` + +{.signature} +`class Shot` + +A picture of views, `size` pixels in all, each view drawn into its layout panel. While a shot +draws a view, a canvas showing that view keeps its last frame. An image draws each view's data as +it was when the picture began. A canvas's `shot()` makes a shot of what the canvas shows. ```ts -/** Options for `Gpu.canvas()`. */ -interface CanvasOptions { - /** The views shown and their regions; later panels draw over earlier ones. None by default. */ - layout?: readonly Panel[]; - /** - * Pixels per CSS pixel of an `OffscreenCanvas`, 1 by default; it must be positive. A `` - * element uses the device pixel ratio instead. - */ - pixelRatio?: number; -} +const png = await (await canvas.shot({ scale: 2 }).image()).encode('png'); ``` -### Panel +```{rubric} Constructor +``` -```ts -/** A view and the region of a canvas or picture it draws in. */ -interface Panel { - view: View; - /** Where the view draws; the whole canvas or picture by default. */ - at?: Rect; -} +{#shot-new .member} +**`new Shot`**`(size: readonly [number, number], options?:` [`ShotOptions`](#shotoptions) `& { layout?: readonly` [`Panel`](#panel)`[] })` [¶](#shot-new){.headerlink} +: Creates a shot of `size`, `[width, height]` in pixels, of the views in `options.layout`. + +```{rubric} Properties ``` -### Rect +{#shot-size .member} +`readonly` **`size`**`: readonly [number, number]` [¶](#shot-size){.headerlink} +: The picture's size, `[width, height]` in pixels. -```ts -/** A region `[x, y, width, height]` in fractions of a canvas or picture, from its top left. */ -type Rect = readonly [x: number, y: number, width: number, height: number]; +{#shot-options .member} +`readonly` **`options`**`:` [`ShotOptions`](#shotoptions) `& { layout?: readonly` [`Panel`](#panel)`[] }` [¶](#shot-options){.headerlink} +: How the picture is taken, and in `layout`, the panel each view is drawn into. + +```{rubric} Methods ``` -## Pictures and video +{#shot-image .member} +**`image`**`(wait?:` [`Wait`](#wait)`): Promise<`[`Image`](#image)`>` [¶](#shot-image){.headerlink} +: Draws the picture once every view is complete, as `capture` says, and resolves to the + [`Image`](#image). Rejects with an `invalid` error for invalid options or a view in two panels, and + with the first error a view reports while drawing. -Shots draw views into images and video. +{#shot-video .member} +**`video`**`(options: { fps: number; output: WritableStream; bitrate?: number } &` [`Wait`](#wait)`):` [`Video`](#video) [¶](#shot-video){.headerlink} +: Starts a video of this shot: H.264 in MP4 at `options.fps` frames per second, written to + `options.output` as frames are drawn. `options.bitrate` is in bits per second, about 0.15 bits + per pixel per frame by default. The width is rounded down to a multiple of 4 and the height to a + multiple of 2, as the encoder requires. Canvases showing the views keep their last frame until + the video finishes or aborts. Aborting `options.signal` aborts the video and its output; frames + have no other time limit. -### Shot - -````ts -/** - * A picture of views, `size` pixels in all, each view drawn into its layout panel. While a shot - * draws a view, a canvas showing that view keeps its last frame. An image draws each view's data as - * it was when the picture began. A canvas's `shot()` makes a shot of what the canvas shows. - * - * ```ts - * const png = await (await canvas.shot({ scale: 2 }).image()).encode('png'); - * ``` - */ -class Shot { - readonly size: readonly [number, number]; - readonly options: ShotOptions & { - layout?: readonly Panel[]; - }; - /** Creates a shot of `size`, `[width, height]` in pixels, of the views in `options.layout`. */ - constructor( - size: readonly [number, number], - options?: ShotOptions & { - layout?: readonly Panel[]; - }, - ); - /** - * Draws the picture once every view is complete, as `capture` says, and resolves to the - * {@link Image}. Rejects with an `invalid` error for invalid options or a view in two panels, and - * with the first error a view reports while drawing. - */ - image(wait?: Wait): Promise; - /** - * Starts a video of this shot: H.264 in MP4 at `fps` frames per second, written to `output` as - * frames are drawn. `bitrate` is in bits per second, about 0.15 bits per pixel per frame by - * default. The width is rounded down to a multiple of 4 and the height to a multiple of 2, as the - * encoder requires. Canvases showing the views keep their last frame until the video finishes or - * aborts. Aborting `signal` aborts the video and `output`; frames have no other time limit. - */ - video({ - fps, - output, - bitrate, - signal, - }: { - fps: number; - output: WritableStream; - bitrate?: number; - } & Wait): Video; -} -```` - -### ShotOptions +(shotoptions)= -```ts -/** Options for a {@link Shot}. */ -interface ShotOptions { - /** - * Pixels per CSS pixel, 1 by default. Views lay out at `size / scale` CSS pixels, so 2 draws the - * same layout twice as sharp. - */ - scale?: number; - /** Leaves the background transparent where the theme's background color would be. */ - transparent?: boolean; - /** - * When the picture is taken. `'still'`, the default, waits until every view is complete and - * still: transitions, camera flights, and label fades run until they finish. `'now'` waits only - * until every view is complete, and freezes anything moving where it is. - */ - capture?: 'still' | 'now'; - /** - * The time in seconds that `elapsed()` expressions and flowing comets show in a still picture. By - * default they follow the view's clock. - */ - elapsed?: number; -} +### `ShotOptions` + +{.signature} +`interface ShotOptions` + +Options for a [`Shot`](#shot). + +{#shotoptions-scale .member} +**`scale`**`?: number` [¶](#shotoptions-scale){.headerlink} +: Pixels per CSS pixel, 1 by default. Views lay out at `size / scale` CSS pixels, so 2 draws the + same layout twice as sharp. + +{#shotoptions-transparent .member} +**`transparent`**`?: boolean` [¶](#shotoptions-transparent){.headerlink} +: Leaves the background transparent where the theme's background color would be. + +{#shotoptions-capture .member} +**`capture`**`?: 'still' | 'now'` [¶](#shotoptions-capture){.headerlink} +: When the picture is taken. `'still'`, the default, waits until every view is complete and + still: transitions, camera flights, and label fades run until they finish. `'now'` waits only + until every view is complete, and freezes anything moving where it is. + +{#shotoptions-elapsed .member} +**`elapsed`**`?: number` [¶](#shotoptions-elapsed){.headerlink} +: The time in seconds that [`elapsed()`](#elapsed) expressions and flowing comets show in a still picture. By + default they follow the view's clock. + +(image)= + +### `Image` + +{.signature} +`class Image` + +A picture's RGBA pixels, with straight alpha, rows top to bottom. + +```{rubric} Properties ``` -### Image +{#image-width .member} +`readonly` **`width`**`: number` [¶](#image-width){.headerlink} +: Width in pixels. -```ts -/** A picture's RGBA pixels, with straight alpha, rows top to bottom. */ -class Image { - /** Width in pixels. */ - get width(): number; - /** Height in pixels. */ - get height(): number; - /** A copy of the pixels: 4 bytes per pixel, `width * height * 4` bytes in all. */ - get rgba(): Uint8Array; - /** - * Encodes the image as a file, in a worker. PNG is lossless with alpha. JPEG uses - * `options.quality` from 1 to 100 (90 by default) and drops alpha. WebP is lossless with alpha - * and smaller than PNG. Rejects with an `invalid` error for a quality outside 1 to 100 or an - * unknown format. - */ - encode( - format: ImageFormat, - options?: { - quality?: number; - } & Wait, - ): Promise; - /** Frees the pixels. Other methods throw afterwards. */ - destroy(): void; - [Symbol.dispose](): void; -} +{#image-height .member} +`readonly` **`height`**`: number` [¶](#image-height){.headerlink} +: Height in pixels. + +{#image-rgba .member} +`readonly` **`rgba`**`: Uint8Array` [¶](#image-rgba){.headerlink} +: A copy of the pixels: 4 bytes per pixel, `width * height * 4` bytes in all. + +```{rubric} Methods ``` -### ImageFormat +{#image-encode .member} +**`encode`**`(format:` [`ImageFormat`](#imageformat)`, options?: { quality?: number } &` [`Wait`](#wait)`): Promise` [¶](#image-encode){.headerlink} +: Encodes the image as a file, in a worker. PNG is lossless with alpha. JPEG uses + `options.quality` from 1 to 100 (90 by default) and drops alpha. WebP is lossless with alpha + and smaller than PNG. Rejects with an `invalid` error for a quality outside 1 to 100 or an + unknown format. -```ts -/** An image file format for {@link Image.encode}. */ -type ImageFormat = 'png' | 'jpeg' | 'webp'; +{#image-destroy .member} +**`destroy`**`(): void` [¶](#image-destroy){.headerlink} +: Frees the pixels. Other methods throw afterwards. + +{#image-symbol-dispose .member} +**`[Symbol.dispose]`**`(): void` [¶](#image-symbol-dispose){.headerlink} +: Calls [`Image.destroy()`](#image-destroy), so a `using` declaration destroys it when its block ends. + +(imageformat)= + +### `ImageFormat` + +{.signature} +`type ImageFormat = 'png' | 'jpeg' | 'webp'` + +An image file format for [`Image.encode()`](#image-encode). + +(video)= + +### `Video` + +{.signature} +`class Video` + +A video being drawn, encoded, and written; created by [`Shot.video()`](#shot-video). Draw frames with +[`Video.frame()`](#video-frame) or [`Video.record()`](#video-record), then call [`Video.finish()`](#video-finish). + +```{rubric} Properties ``` -### Video +{#video-time .member} +`readonly` **`time`**`: number` [¶](#video-time){.headerlink} +: The time in seconds at which the next frame shows. Still readable after the video ends. -```ts -/** - * A video being drawn, encoded, and written; created by {@link Shot.video}. Draw frames with - * {@link Video.frame} or {@link Video.record}, then call {@link Video.finish}. - */ -class Video { - /** The time in seconds at which the next frame shows. Still readable after the video ends. */ - get time(): number; - /** - * Frames recorded so far, including frames repeated because nothing moved. Still readable after - * the video ends. - */ - get frames(): number; - /** - * Draws the next frame once its views are complete, and writes its bytes, waiting on the - * output's backpressure. Calls must not overlap: a call made before the previous one resolves, - * or after `finish()`, rejects with an `invalid` error, and a call after `abort()` with a - * `cancelled` error. A failure aborts the video. - */ - frame(): Promise; - /** - * Records frames until `step(time)` returns false, then finishes the video. `step` gets the next - * frame's time in seconds and can change the views before that frame is drawn. `onProgress` is - * called after each frame. An error from `step`, drawing, the output, or `onProgress` aborts the - * video and is rethrown. - */ - record( - step: (time: number) => boolean | Promise, - onProgress?: (progress: { frames: number; time: number }) => void, - ): Promise; - /** - * Waits for the frame in progress, encodes the remaining frames, writes the end of the file, and - * closes and unlocks the output. Later calls return the same promise. Rejects with a `cancelled` - * error if the video was aborted. - */ - finish(): Promise; - /** - * Cancels drawing and encoding, aborts the output with `reason`, and unlocks it once pending - * writes settle. Later calls return the same promise. After `finish()` completes, it does - * nothing. - */ - abort(reason?: unknown): Promise; -} +{#video-frames .member} +`readonly` **`frames`**`: number` [¶](#video-frames){.headerlink} +: Frames recorded so far, including frames repeated because nothing moved. Still readable after + the video ends. + +```{rubric} Methods ``` + +{#video-frame .member} +**`frame`**`(): Promise` [¶](#video-frame){.headerlink} +: Draws the next frame once its views are complete, and writes its bytes, waiting on the + output's backpressure. Calls must not overlap: a call made before the previous one resolves, + or after `finish()`, rejects with an `invalid` error, and a call after `abort()` with a + `cancelled` error. A failure aborts the video. + +{#video-record .member} +**`record`**`(step: (time: number) => boolean | Promise, onProgress?: (progress: { frames: number; time: number }) => void): Promise` [¶](#video-record){.headerlink} +: Records frames until `step(time)` returns false, then finishes the video. `step` gets the next + frame's time in seconds and can change the views before that frame is drawn. `onProgress` is + called after each frame. An error from `step`, drawing, the output, or `onProgress` aborts the + video and is rethrown. + +{#video-finish .member} +**`finish`**`(): Promise` [¶](#video-finish){.headerlink} +: Waits for the frame in progress, encodes the remaining frames, writes the end of the file, and + closes and unlocks the output. Later calls return the same promise. Rejects with a `cancelled` + error if the video was aborted. + +{#video-abort .member} +**`abort`**`(reason?: unknown): Promise` [¶](#video-abort){.headerlink} +: Cancels drawing and encoding, aborts the output with `reason`, and unlocks it once pending + writes settle. Later calls return the same promise. After `finish()` completes, it does + nothing. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index afc203b5..228c549f 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1,366 +1,417 @@ # Command reference -Generated from Clap. See the [CLI guide](../cli.md) for examples. +Every command and option of the `latkit` command line, written from its help. The [command-line guide](../cli.md) shows them at work. `--help` prints a command's help in the terminal, and `latkit --version` prints the version. -## latkit +(cli-latkit)= -```text -Render scenes of JSON or binary data to images and videos +## `latkit` -Usage: latkit [OPTIONS] +{.signature} +`latkit [OPTIONS] ` -Commands: - render Draw scenes of datasets into an image, image sequence, raw frames, or a video - validate Check every input render reads, including frames, without a GPU - inspect Describe datasets as JSON: tables, row counts, fields with kinds, and the axis and its span - schema Print the JSON Schema of every input latkit reads, or of one input - query Print the rows a query selects, sorted and paged, as JSON Lines with one object per row - tile Write a dataset as time tiles: frames.json (which --tiles opens), the dataset without its sampled fields' histories, and those histories in spans at every level of detail - help Print this message or the help of the given subcommand(s) +Render scenes of JSON or binary data to images and videos. -Options: - --diagnostics - Format of errors and render progress on stderr. JSON writes one object per line +```{rubric} Commands +``` - [default: human] - [possible values: human, json] +{.member} +[`latkit render`](#cli-render) +: Draw scenes of datasets into an image, image sequence, raw frames, or a video. - -h, --help - Print help +{.member} +[`latkit validate`](#cli-validate) +: Check every input render reads, including frames, without a GPU. - -V, --version - Print version -``` +{.member} +[`latkit inspect`](#cli-inspect) +: Describe datasets as JSON: tables, row counts, fields with kinds, and the axis and its span. -## render +{.member} +[`latkit schema`](#cli-schema) +: Print the JSON Schema of every input latkit reads, or of one input. -```text -Draw scenes of datasets into an image, image sequence, raw frames, or a video +{.member} +[`latkit query`](#cli-query) +: Print the rows a query selects, sorted and paged, as JSON Lines with one object per row. -Usage: latkit render [OPTIONS] ... +{.member} +[`latkit tile`](#cli-tile) +: Write a dataset as time tiles: `frames.json` (which [`--tiles`](#cli-render-tiles) opens), the dataset without its sampled fields' histories, and those histories in spans at every level of detail. -Arguments: - ... - Scene JSON files, one per panel (- reads stdin) +```{rubric} Options +``` -Options: - -d, --data <[NAME=]FILE> - A dataset file: a JSON document, latkit bytes, or GeoJSON. NAME= sets its name, which GeoJSON requires +{#cli-latkit-diagnostics .member} +**`--diagnostics`** `` [¶](#cli-latkit-diagnostics){.headerlink} +: Format of errors and render progress on stderr. Defaults to `human`. Every command takes it. - --diagnostics - Format of errors and render progress on stderr. JSON writes one object per line + {#cli-latkit-diagnostics-human .member} + **`human`** [¶](#cli-latkit-diagnostics-human){.headerlink} + : Lines of text, for people. - [default: human] - [possible values: human, json] + {#cli-latkit-diagnostics-json .member} + **`json`** [¶](#cli-latkit-diagnostics-json){.headerlink} + : One JSON object per line, for programs. - --tiles - Tiles every view fetches as dataset NAME: a time tiles frames.json, or a vector tiles PMTiles file, TileJSON, or {z}/{x}/{y} template +(cli-render)= - -o, --output - Output file, %d pattern for an image sequence, or - for stdout +## `latkit render` - --format - Output format. Defaults to the one the output's extension names, else PNG +{.signature} +`latkit render [OPTIONS] ...` - Possible values: - - png: PNG, lossless with alpha. One picture, or one per frame with a %d output pattern - - jpeg: JPEG at --quality, without alpha. One picture, or one per frame - - webp: Lossless WebP with alpha. One picture, or one per frame - - rgba: Raw RGBA with straight alpha, frames back to back - - y4m: YUV4MPEG2 (4:2:0, BT.709 limited range), which any encoder reads - - mp4: H.264 in fragmented MP4, from the platform's encoder +Draw scenes of datasets into an image, image sequence, raw frames, or a video. - --quality - JPEG quality, from 1 (smallest) to 100 (sharpest) +```{rubric} Arguments +``` - [default: 90] +{#cli-render-scenes .member} +**`...`** [¶](#cli-render-scenes){.headerlink} +: Scene JSON files, one per panel (`-` reads stdin). - --size - The picture's width and height in pixels, such as 1920x1080 +```{rubric} Options +``` - [default: 1600x900] +{#cli-render-data .member} +`-d,` **`--data`** `<[NAME=]FILE>` [¶](#cli-render-data){.headerlink} +: A dataset file: a JSON document, latkit bytes, or GeoJSON. `NAME=` sets its name, which GeoJSON requires. - --scale - Pixels per CSS pixel. Views lay out size / scale CSS pixels, so 2 draws twice as sharp +{#cli-render-tiles .member} +**`--tiles`** `` [¶](#cli-render-tiles){.headerlink} +: Tiles every view fetches as dataset `NAME`: a time tiles `frames.json`, or a vector tiles PMTiles file, TileJSON, or `{z}/{x}/{y}` template. - [default: 1] +{#cli-render-output .member} +`-o,` **`--output`** `` [¶](#cli-render-output){.headerlink} +: Output file, `%d` pattern for an image sequence, or `-` for stdout. - --panel - A scene's panel of the picture, as X,Y,WIDTH,HEIGHT fractions. Give one per scene, in order +{#cli-render-format .member} +**`--format`** `` [¶](#cli-render-format){.headerlink} +: Output format. Defaults to the one the output's extension names, else PNG. - --transparent - Keep alpha where the themes' backgrounds would be + {#cli-render-format-png .member} + **`png`** [¶](#cli-render-format-png){.headerlink} + : PNG, lossless with alpha. One picture, or one per frame with a `%d` output pattern. - --deadline - The longest a picture or video frame may take, in seconds + {#cli-render-format-jpeg .member} + **`jpeg`** [¶](#cli-render-format-jpeg){.headerlink} + : JPEG at [`--quality`](#cli-render-quality), without alpha. One picture, or one per frame. - [default: 30] + {#cli-render-format-webp .member} + **`webp`** [¶](#cli-render-format-webp){.headerlink} + : Lossless WebP with alpha. One picture, or one per frame. - --font - A TTF, OTF, or TTC font file. Repeat for fallbacks. Replaces the system fonts + {#cli-render-format-rgba .member} + **`rgba`** [¶](#cli-render-format-rgba){.headerlink} + : Raw RGBA with straight alpha, frames back to back. - --camera - A camera JSON file per scene, in scene order. Views without one use --fit + {#cli-render-format-y4m .member} + **`y4m`** [¶](#cli-render-format-y4m){.headerlink} + : YUV4MPEG2 (4:2:0, BT.709 limited range), which any encoder reads. - --fit - What views without a camera frame: "all", or a fit as JSON + {#cli-render-format-mp4 .member} + **`mp4`** [¶](#cli-render-format-mp4){.headerlink} + : H.264 in fragmented MP4, from the platform's encoder. - [default: all] +{#cli-render-quality .member} +**`--quality`** `` [¶](#cli-render-quality){.headerlink} +: JPEG quality, from 1 (smallest) to 100 (sharpest). Defaults to `90`. - --insets - CSS pixels to keep clear at the top, right, bottom, and left +{#cli-render-size .member} +**`--size`** `` [¶](#cli-render-size){.headerlink} +: The picture's width and height in pixels, such as `1920x1080`. Defaults to `1600x900`. - --at - The axis coordinate to show. Sampled fields show their last frame at or before it +{#cli-render-scale .member} +**`--scale`** `` [¶](#cli-render-scale){.headerlink} +: Pixels per CSS pixel. Views lay out size / scale CSS pixels, so 2 draws twice as sharp. Defaults to `1`. - --elapsed - A still's fixed effect time in seconds, for elapsed-time expressions and flowing comets +{#cli-render-panel .member} +**`--panel`** `` [¶](#cli-render-panel){.headerlink} +: A scene's panel of the picture: its x, y, width, and height, as fractions. Give one per scene, in order. - --frames - Frames as JSON Lines, one line per output frame (- reads stdin) +{#cli-render-transparent .member} +**`--transparent`** [¶](#cli-render-transparent){.headerlink} +: Keep alpha where the themes' backgrounds would be. - --fps - Output frames per second +{#cli-render-deadline .member} +**`--deadline`** `` [¶](#cli-render-deadline){.headerlink} +: The longest a picture or video frame may take, in seconds. Defaults to `30`. - [default: 60] +{#cli-render-font .member} +**`--font`** `` [¶](#cli-render-font){.headerlink} +: A TTF, OTF, or TTC font file. Repeat for fallbacks. Replaces the system fonts. - --bitrate - MP4 bitrate in bits per second. Defaults to about 0.15 bits per pixel per frame +{#cli-render-camera .member} +**`--camera`** `` [¶](#cli-render-camera){.headerlink} +: A camera JSON file per scene, in scene order. Views without one use [`--fit`](#cli-render-fit). - -h, --help - Print help (see a summary with '-h') -``` +{#cli-render-fit .member} +**`--fit`** `` [¶](#cli-render-fit){.headerlink} +: What views without a camera frame: `all`, or a fit as JSON. Defaults to `all`. -## validate +{#cli-render-insets .member} +**`--insets`** `` [¶](#cli-render-insets){.headerlink} +: CSS pixels to keep clear at the top, right, bottom, and left. -```text -Check every input render reads, including frames, without a GPU +{#cli-render-at .member} +**`--at`** `` [¶](#cli-render-at){.headerlink} +: The axis coordinate to show. Sampled fields show their last frame at or before it. -Usage: latkit validate [OPTIONS] ... +{#cli-render-elapsed .member} +**`--elapsed`** `` [¶](#cli-render-elapsed){.headerlink} +: A still's fixed effect time in seconds, for elapsed-time expressions and flowing comets. -Arguments: - ... - Scene JSON files, one per panel (- reads stdin) +{#cli-render-frames .member} +**`--frames`** `` [¶](#cli-render-frames){.headerlink} +: Frames as JSON Lines, one line per output frame (`-` reads stdin). -Options: - -d, --data <[NAME=]FILE> - A dataset file: a JSON document, latkit bytes, or GeoJSON. NAME= sets its name, which GeoJSON requires +{#cli-render-fps .member} +**`--fps`** `` [¶](#cli-render-fps){.headerlink} +: Output frames per second. Defaults to `60`. - --diagnostics - Format of errors and render progress on stderr. JSON writes one object per line +{#cli-render-bitrate .member} +**`--bitrate`** `` [¶](#cli-render-bitrate){.headerlink} +: MP4 bitrate in bits per second. Defaults to about 0.15 bits per pixel per frame. - [default: human] - [possible values: human, json] +(cli-validate)= - --tiles - Tiles every view fetches as dataset NAME: a time tiles frames.json, or a vector tiles PMTiles file, TileJSON, or {z}/{x}/{y} template +## `latkit validate` - -o, --output - Output file, %d pattern for an image sequence, or - for stdout +{.signature} +`latkit validate [OPTIONS] ...` - --format - Output format. Defaults to the one the output's extension names, else PNG +Check every input render reads, including frames, without a GPU. - Possible values: - - png: PNG, lossless with alpha. One picture, or one per frame with a %d output pattern - - jpeg: JPEG at --quality, without alpha. One picture, or one per frame - - webp: Lossless WebP with alpha. One picture, or one per frame - - rgba: Raw RGBA with straight alpha, frames back to back - - y4m: YUV4MPEG2 (4:2:0, BT.709 limited range), which any encoder reads - - mp4: H.264 in fragmented MP4, from the platform's encoder +```{rubric} Arguments +``` - --quality - JPEG quality, from 1 (smallest) to 100 (sharpest) +{#cli-validate-scenes .member} +**`...`** [¶](#cli-validate-scenes){.headerlink} +: Scene JSON files, one per panel (`-` reads stdin). - [default: 90] +```{rubric} Options +``` - --size - The picture's width and height in pixels, such as 1920x1080 +{#cli-validate-data .member} +`-d,` **`--data`** `<[NAME=]FILE>` [¶](#cli-validate-data){.headerlink} +: A dataset file: a JSON document, latkit bytes, or GeoJSON. `NAME=` sets its name, which GeoJSON requires. - [default: 1600x900] +{#cli-validate-tiles .member} +**`--tiles`** `` [¶](#cli-validate-tiles){.headerlink} +: Tiles every view fetches as dataset `NAME`: a time tiles `frames.json`, or a vector tiles PMTiles file, TileJSON, or `{z}/{x}/{y}` template. - --scale - Pixels per CSS pixel. Views lay out size / scale CSS pixels, so 2 draws twice as sharp +{#cli-validate-output .member} +`-o,` **`--output`** `` [¶](#cli-validate-output){.headerlink} +: Output file, `%d` pattern for an image sequence, or `-` for stdout. - [default: 1] +{#cli-validate-format .member} +**`--format`** `` [¶](#cli-validate-format){.headerlink} +: Output format. Defaults to the one the output's extension names, else PNG. - --panel - A scene's panel of the picture, as X,Y,WIDTH,HEIGHT fractions. Give one per scene, in order + {#cli-validate-format-png .member} + **`png`** [¶](#cli-validate-format-png){.headerlink} + : PNG, lossless with alpha. One picture, or one per frame with a `%d` output pattern. - --transparent - Keep alpha where the themes' backgrounds would be + {#cli-validate-format-jpeg .member} + **`jpeg`** [¶](#cli-validate-format-jpeg){.headerlink} + : JPEG at [`--quality`](#cli-validate-quality), without alpha. One picture, or one per frame. - --deadline - The longest a picture or video frame may take, in seconds + {#cli-validate-format-webp .member} + **`webp`** [¶](#cli-validate-format-webp){.headerlink} + : Lossless WebP with alpha. One picture, or one per frame. - [default: 30] + {#cli-validate-format-rgba .member} + **`rgba`** [¶](#cli-validate-format-rgba){.headerlink} + : Raw RGBA with straight alpha, frames back to back. - --font - A TTF, OTF, or TTC font file. Repeat for fallbacks. Replaces the system fonts + {#cli-validate-format-y4m .member} + **`y4m`** [¶](#cli-validate-format-y4m){.headerlink} + : YUV4MPEG2 (4:2:0, BT.709 limited range), which any encoder reads. - --camera - A camera JSON file per scene, in scene order. Views without one use --fit + {#cli-validate-format-mp4 .member} + **`mp4`** [¶](#cli-validate-format-mp4){.headerlink} + : H.264 in fragmented MP4, from the platform's encoder. - --fit - What views without a camera frame: "all", or a fit as JSON +{#cli-validate-quality .member} +**`--quality`** `` [¶](#cli-validate-quality){.headerlink} +: JPEG quality, from 1 (smallest) to 100 (sharpest). Defaults to `90`. - [default: all] +{#cli-validate-size .member} +**`--size`** `` [¶](#cli-validate-size){.headerlink} +: The picture's width and height in pixels, such as `1920x1080`. Defaults to `1600x900`. - --insets - CSS pixels to keep clear at the top, right, bottom, and left +{#cli-validate-scale .member} +**`--scale`** `` [¶](#cli-validate-scale){.headerlink} +: Pixels per CSS pixel. Views lay out size / scale CSS pixels, so 2 draws twice as sharp. Defaults to `1`. - --at - The axis coordinate to show. Sampled fields show their last frame at or before it +{#cli-validate-panel .member} +**`--panel`** `` [¶](#cli-validate-panel){.headerlink} +: A scene's panel of the picture: its x, y, width, and height, as fractions. Give one per scene, in order. - --elapsed - A still's fixed effect time in seconds, for elapsed-time expressions and flowing comets +{#cli-validate-transparent .member} +**`--transparent`** [¶](#cli-validate-transparent){.headerlink} +: Keep alpha where the themes' backgrounds would be. - --frames - Frames as JSON Lines, one line per output frame (- reads stdin) +{#cli-validate-deadline .member} +**`--deadline`** `` [¶](#cli-validate-deadline){.headerlink} +: The longest a picture or video frame may take, in seconds. Defaults to `30`. - --fps - Output frames per second +{#cli-validate-font .member} +**`--font`** `` [¶](#cli-validate-font){.headerlink} +: A TTF, OTF, or TTC font file. Repeat for fallbacks. Replaces the system fonts. - [default: 60] +{#cli-validate-camera .member} +**`--camera`** `` [¶](#cli-validate-camera){.headerlink} +: A camera JSON file per scene, in scene order. Views without one use [`--fit`](#cli-validate-fit). - --bitrate - MP4 bitrate in bits per second. Defaults to about 0.15 bits per pixel per frame +{#cli-validate-fit .member} +**`--fit`** `` [¶](#cli-validate-fit){.headerlink} +: What views without a camera frame: `all`, or a fit as JSON. Defaults to `all`. - -h, --help - Print help (see a summary with '-h') -``` +{#cli-validate-insets .member} +**`--insets`** `` [¶](#cli-validate-insets){.headerlink} +: CSS pixels to keep clear at the top, right, bottom, and left. -## inspect +{#cli-validate-at .member} +**`--at`** `` [¶](#cli-validate-at){.headerlink} +: The axis coordinate to show. Sampled fields show their last frame at or before it. -```text -Describe datasets as JSON: tables, row counts, fields with kinds, and the axis and its span +{#cli-validate-elapsed .member} +**`--elapsed`** `` [¶](#cli-validate-elapsed){.headerlink} +: A still's fixed effect time in seconds, for elapsed-time expressions and flowing comets. -Usage: latkit inspect [OPTIONS] <[NAME=]FILE>... +{#cli-validate-frames .member} +**`--frames`** `` [¶](#cli-validate-frames){.headerlink} +: Frames as JSON Lines, one line per output frame (`-` reads stdin). -Arguments: - <[NAME=]FILE>... - Dataset files, each [NAME=]FILE (- reads stdin) +{#cli-validate-fps .member} +**`--fps`** `` [¶](#cli-validate-fps){.headerlink} +: Output frames per second. Defaults to `60`. -Options: - --deadline - The longest a dataset may take to load, in seconds +{#cli-validate-bitrate .member} +**`--bitrate`** `` [¶](#cli-validate-bitrate){.headerlink} +: MP4 bitrate in bits per second. Defaults to about 0.15 bits per pixel per frame. - [default: 30] +(cli-inspect)= - --diagnostics - Format of errors and render progress on stderr. JSON writes one object per line +## `latkit inspect` - [default: human] - [possible values: human, json] +{.signature} +`latkit inspect [OPTIONS] <[NAME=]FILE>...` - -h, --help - Print help +Describe datasets as JSON: tables, row counts, fields with kinds, and the axis and its span. + +```{rubric} Arguments ``` -## schema +{#cli-inspect-data .member} +**`<[NAME=]FILE>...`** [¶](#cli-inspect-data){.headerlink} +: Dataset files, each `[NAME=]FILE` (`-` reads stdin). -```text -Print the JSON Schema of every input latkit reads, or of one input +```{rubric} Options +``` -Usage: latkit schema [OPTIONS] [INPUT] +{#cli-inspect-deadline .member} +**`--deadline`** `` [¶](#cli-inspect-deadline){.headerlink} +: The longest a dataset may take to load, in seconds. Defaults to `30`. -Arguments: - [INPUT] - Print only this input's schema +(cli-schema)= - Possible values: - - scene: A scene - - data: A dataset's JSON document - - patch: A patch - - query: A query for `latkit query` - - camera: A camera for `--camera` - - fit: A fit for `--fit` - - frames: One line of a `--frames` file +## `latkit schema` -Options: - --diagnostics - Format of errors and render progress on stderr. JSON writes one object per line +{.signature} +`latkit schema [OPTIONS] [INPUT]` - [default: human] - [possible values: human, json] +Print the JSON Schema of every input latkit reads, or of one input. - -h, --help - Print help (see a summary with '-h') +```{rubric} Arguments ``` -## query +{#cli-schema-input .member} +**`[INPUT]`** [¶](#cli-schema-input){.headerlink} +: Print only this input's schema. -```text -Print the rows a query selects, sorted and paged, as JSON Lines with one object per row + {#cli-schema-input-scene .member} + **`scene`** [¶](#cli-schema-input-scene){.headerlink} + : A scene. -Usage: latkit query [OPTIONS] <[NAME=]FILE> + {#cli-schema-input-data .member} + **`data`** [¶](#cli-schema-input-data){.headerlink} + : A dataset's JSON document. -Arguments: - <[NAME=]FILE> - The dataset, [NAME=]FILE (- reads stdin) + {#cli-schema-input-patch .member} + **`patch`** [¶](#cli-schema-input-patch){.headerlink} + : A patch. - - The query as JSON, such as {"of":"Branch","filter":{"op":"gt","args":["loading",90]}}, or a file containing it (- reads stdin) + {#cli-schema-input-query .member} + **`query`** [¶](#cli-schema-input-query){.headerlink} + : A query for [`latkit query`](#cli-query). -Options: - --deadline - The longest the dataset may take to load, in seconds + {#cli-schema-input-camera .member} + **`camera`** [¶](#cli-schema-input-camera){.headerlink} + : A camera for [`--camera`](#cli-render-camera). - [default: 30] + {#cli-schema-input-fit .member} + **`fit`** [¶](#cli-schema-input-fit){.headerlink} + : A fit for [`--fit`](#cli-render-fit). - --diagnostics - Format of errors and render progress on stderr. JSON writes one object per line + {#cli-schema-input-frames .member} + **`frames`** [¶](#cli-schema-input-frames){.headerlink} + : One line of a [`--frames`](#cli-render-frames) file. - [default: human] - [possible values: human, json] +(cli-query)= - -h, --help - Print help -``` +## `latkit query` -## tile +{.signature} +`latkit query [OPTIONS] <[NAME=]FILE> ` -```text -Write a dataset as time tiles: frames.json (which --tiles opens), the dataset without its sampled fields' histories, and those histories in spans at every level of detail +Print the rows a query selects, sorted and paged, as JSON Lines with one object per row. -Usage: latkit tile [OPTIONS] --output <[NAME=]FILE> +```{rubric} Arguments +``` -Arguments: - <[NAME=]FILE> - The dataset, [NAME=]FILE (- reads stdin) +{#cli-query-data .member} +**`<[NAME=]FILE>`** [¶](#cli-query-data){.headerlink} +: The dataset, `[NAME=]FILE` (`-` reads stdin). -Options: - --diagnostics - Format of errors and render progress on stderr. JSON writes one object per line +{#cli-query-query .member} +**``** [¶](#cli-query-query){.headerlink} +: The query as JSON, such as `{"of":"Branch","filter":{"op":"gt","args":["loading",90]}}`, or a file containing it (`-` reads stdin). - [default: human] - [possible values: human, json] +```{rubric} Options +``` - -o, --output - Directory to write the tiles to +{#cli-query-deadline .member} +**`--deadline`** `` [¶](#cli-query-deadline){.headerlink} +: The longest the dataset may take to load, in seconds. Defaults to `30`. - --chunk - Entries per span: frames at full detail, buckets at coarser levels. A multiple of 8 +(cli-tile)= - [default: 256] +## `latkit tile` - --deadline - The longest the dataset may take to load, in seconds +{.signature} +`latkit tile [OPTIONS] --output <[NAME=]FILE>` - [default: 30] +Write a dataset as time tiles: `frames.json` (which [`--tiles`](#cli-render-tiles) opens), the dataset without its sampled fields' histories, and those histories in spans at every level of detail. - -h, --help - Print help +```{rubric} Arguments ``` -## help +{#cli-tile-data .member} +**`<[NAME=]FILE>`** [¶](#cli-tile-data){.headerlink} +: The dataset, `[NAME=]FILE` (`-` reads stdin). + +```{rubric} Options +``` -```text -Print this message or the help of the given subcommand(s) +{#cli-tile-output .member} +`-o,` **`--output`** `` [¶](#cli-tile-output){.headerlink} +: Directory to write the tiles to. -Usage: latkit help [COMMAND]... +{#cli-tile-chunk .member} +**`--chunk`** `` [¶](#cli-tile-chunk){.headerlink} +: Entries per span: frames at full detail, buckets at coarser levels. A multiple of 8. Defaults to `256`. -Arguments: - [COMMAND]... - Print help for the subcommand(s) -``` +{#cli-tile-deadline .member} +**`--deadline`** `` [¶](#cli-tile-deadline){.headerlink} +: The longest the dataset may take to load, in seconds. Defaults to `30`. diff --git a/docs/reference/types.md b/docs/reference/types.md index 0db590c7..8322c9e9 100644 --- a/docs/reference/types.md +++ b/docs/reference/types.md @@ -1,2128 +1,2859 @@ # Scene and result types -Generated from the same schema as the package types. Each field documents its meaning and defaults. +The types of the scenes latkit draws and of what it reports, written from the same schema as the +package's types. Each property gives its meaning and default. ## Scenes A scene: its space, how it responds to input, and how it is laid out. -### Scene - -```ts -/** - * A view's content and behavior: a space, marks drawn in order, a layout, a theme, controls, an - * ease duration, and an optional title. - * - * A scene is plain data. In JSON, fields are camelCase and each mark is - * `{ vertices | edges | faces | lines | areas: options }`. - */ -interface Scene { - /** The coordinate space positions are in. */ - space: Space; - /** The marks, in drawing order: each draws over the marks before it. */ - marks?: Mark[]; - /** - * How the view places vertex rows that have no position: a diagram's blocks, or points on a plane - * or the earth. `none` by default. - */ - layout?: Layout; - /** Default colors and sizes, and the look of text, focus, and each space; `dark` by default. */ - theme?: Theme; - /** How the view responds to the pointer and keyboard. */ - controls?: Controls; - /** Seconds over which scene and data changes animate; 0.3 by default, 0 for none. */ - ease?: number; - /** A title drawn at the view's top left, larger than the theme's text. */ - title?: string | null; - /** A subtitle drawn under the title. */ - subtitle?: string | null; -} -``` - -### Space - -```ts -/** - * The coordinate space of a scene's positions: a name, or - * `{ plane | diagram | earth | plot: options }`. - * - * The space sets the unit of lengths that do not name their own: units in a diagram, where a unit - * is a CSS pixel at zoom 0, and CSS pixels elsewhere. Only a diagram has ports, groups, nets, and - * orthogonal routes. - */ -type Space = - | 'plane' - | 'diagram' - | 'earth' - | 'plot' - | { - /** Longitude and latitude in degrees, on a flat map or a globe. */ - earth: Earth; - } - | { - /** Plane units, with elevation; the camera can turn and tilt. */ - plane: Plane; - } - | { - /** The data's axis horizontally and values vertically. */ - plot: Plot; - } - | { - /** A flat sheet for diagrams: x right and y down, every length in units. */ - diagram: Diagram; - }; -``` - -### Plane - -```ts -/** A plane: positions are in plane units. */ -interface Plane { - /** The grid drawn under the marks; lines 10 units apart by default. */ - grid?: Grid; -} -``` - -### Earth - -```ts -/** - * The earth: positions are longitude and latitude in degrees. The ground takes the theme's `earth` - * colors. - */ -interface Earth { - /** A flat web mercator map (`mercator`, the default) or a globe (`globe`). */ - projection?: Projection; - /** - * Whether lines of longitude and latitude cross the ground, in the theme's `grid` color. Default - * `true`. - */ - graticule?: boolean; - /** - * Lights the earth by the sun's position this many seconds after the Unix epoch, plus the view's - * axis coordinate when it has one. The night side darkens the ground and everything on it, down to - * the theme's `earth.night` brightness. Unset, the earth is lit evenly. - */ - daylight?: number | null; - /** - * Whether air glows around the globe, in the theme's `earth.atmosphere` color, brighter on the day - * side. Default `false`. - */ - atmosphere?: boolean; -} -``` - -### Projection - -```ts -/** How the earth is projected. */ -type Projection = 'mercator' | 'globe'; -``` - -### Diagram - -```ts -/** - * A flat sheet for diagrams, seen from straight above; the camera pans and zooms but does not - * turn. Positions are in units with y down. Lengths are in units too, a unit being a CSS pixel at - * zoom 0: sizes, padding, ports, strokes, text, and the theme's lengths. Sizes given with `px`, - * the text halo, the focus glow, and the pick radius stay in CSS pixels. Zooming scales the whole - * picture. Only a diagram has ports, groups, nets, and orthogonal routes, and the scene's layout - * places its blocks. - */ -interface Diagram { - /** The grid drawn under the marks; dots 20 units apart by default. */ - grid?: Grid; -} -``` - -### Plot - -```ts -/** A plot: x is the data's axis, y the values. */ -interface Plot { - /** The horizontal axis. */ - x?: Axis; - /** The vertical axis. */ - y?: Axis; -} -``` - -### Axis - -```ts -/** A plot axis. */ -interface Axis { - /** The axis title. */ - label?: string | null; - /** Whether grid lines cross the plot at the ticks. Default `true`. */ - grid?: boolean; - /** Whether the axis is hidden. Default `false`. */ - hidden?: boolean; - /** - * Tick positions, each a value or `[value, label]`; a tick without a label shows its value. Unset, - * ticks fall at round values. - */ - ticks?: Tick[] | null; - /** How tick values are formatted; `auto` by default. */ - format?: Format; - /** - * Decimal places of fixed values, or of the significand in scientific and engineering formats, at - * most 15. Unset, as many as the tick step needs. - */ - precision?: number | null; -} -``` - -### Tick - -```ts -/** A tick in JSON: a value, or `[value, label]`. */ -type Tick = number | [number, string | null]; -``` - -### Format - -```ts -/** How an axis formats its tick values. */ -type Format = 'auto' | 'fixed' | 'scientific' | 'engineering'; -``` - -### Grid - -```ts -/** A plane's or a diagram's grid: `"off"`, `{ lines: { pitch } }`, or `{ dots: { pitch, snap } }`. */ -type Grid = - | 'off' - | { - /** Lines `pitch` units apart, thinned by factors of ten as the view zooms out. */ - lines: { - /** Units between lines. */ - pitch: number; - }; - } - | { - /** Dots `pitch` units apart; with `snap`, a diagram's drags snap to them. */ - dots: { - /** Units between dots. */ - pitch: number; - /** Whether drag positions snap to the dots, in a diagram. Default `false`. */ - snap?: boolean; - }; - }; -``` - -### Controls - -```ts -/** - * How a view responds to the pointer and keyboard, and how much it animates on its own: a preset - * (`"none"` or `"inspect"`), or options over the defaults. Each mark controls whether its own rows - * pick, select, and drag; these options control the view itself. - */ -type Controls = - | 'none' - | 'inspect' - | { - /** Whether dragging the background pans the camera. Default `true`. */ - pan?: boolean; - /** - * Which input zooms the view: the wheel and pinching (`wheel`, the default); pinching, and the - * wheel only with Control or Meta held, as trackpad pinches send it, so the plain wheel scrolls - * the page (`modifier`); or nothing (`off`). - */ - zoom?: Zoom; - /** - * Whether dragging with the secondary button, or with Shift, rotates and tilts a map or plane. - * Default `true`. - */ - turn?: boolean; - /** - * Whether the keyboard controls the view. Default `true`. Tab and Shift+Tab step through the rows - * nearest the view's center, and arrow keys step to the nearest row in their direction. Enter - * opens the stepped-to row and Space selects it. With no row stepped to, arrow keys pan - * (Shift+arrows turn), `+` and `-` zoom, Home fits everything, and Escape clears the selection. - */ - keyboard?: boolean; - /** - * Whether the hovered row glows, and a hovered vertex sized in CSS pixels grows. Default `true`. - * Rows are hovered either way: a `'hovered'` event still fires and `hovered` still reads them. - */ - hover?: boolean; - /** - * How far from the pointer, in CSS pixels, a row can be and still be picked; touch adds 8. Default - * 6. - */ - pickRadius?: number; - /** - * Whether Shift+drag selects the rows inside a box instead of turning the camera. Shift+click - * still toggles one row. Default `false`. - */ - selectBox?: boolean; - /** - * Whether the user can draw connections, reported as a `'connect'` event: by dragging from a port, - * from a vertex with Alt held, or an edge's end off its vertex. Default `false`. - */ - connect?: boolean; - /** Whether Delete and Backspace report the selection as a `'delete'` event. Default `false`. */ - delete?: boolean; - /** - * Whether clicking again at the same point, with the camera unmoved, selects the next row under - * it, nearest first, so overlapping rows can each be selected. Default `false`. - */ - cycle?: boolean; - /** - * Seconds a touch or pen must hold still to request a menu (a `'menu'` event), as the secondary - * button does; 0 turns this off. Default 0.5. - */ - press?: number; - /** - * How much the view animates on its own: eases, camera flights, coasting, orbits, label fades, - * comets, and options that read `elapsed`. `system` (the default) follows the user's - * reduced-motion setting, `full` animates everything, and `reduced` animates only motion the user - * drives. - */ - motion?: Motion; - }; -``` - -### Motion - -```ts -/** How much a view animates on its own. */ -type Motion = 'system' | 'full' | 'reduced'; -``` - -### Zoom - -```ts -/** Which input zooms a view. */ -type Zoom = 'wheel' | 'modifier' | 'off'; -``` - -### Layout - -```ts -/** - * How the view places vertex rows that have no position: a name, or - * `{ stress | layered: options }`. In a diagram it places blocks; on a plane or the earth it - * places points among the rows they link to. Gaps are in position units: diagram units, plane - * units, or degrees on the earth. - * - * A row keeps its place once placed: rows added later are placed around the others, and - * `view.arrange` lays out every row again. - */ -type Layout = - | 'none' - | 'stress' - | 'layered' - | { - /** Drawn distances follow graph distances. */ - stress: Stress; - } - | { - /** Ranks along a direction, for flows and hierarchies. */ - layered: Layered; - }; -``` - -### Layered - -```ts -/** - * Layered drawing: vertices in ranks, edges pointing along `flow`, and nets ranked from their - * outputs. - */ -interface Layered { - /** The direction edges point; `right` by default. */ - flow?: Direction; - /** Distance between vertices within a rank, in position units. Default 30. */ - gap?: number; - /** Distance between ranks, in position units. Default 80. */ - rankGap?: number; -} -``` - -### Stress - -```ts -/** - * Stress majorization: vertices move until drawn distances match graph distances, then overlapping - * boxes separate, keeping a quarter of `gap` between them. - */ -interface Stress { - /** Distance between the centers of linked vertices, in position units. Default 40. */ - gap?: number; -} -``` - -### Direction - -```ts -/** A direction on screen. */ -type Direction = 'right' | 'left' | 'down' | 'up'; -``` - -### Order - -```ts -/** Sort direction. Rows where the key has no value sort last in either direction. */ -type Order = 'ascending' | 'descending'; -``` - -### Budget - -```ts -/** How much GPU memory latkit uses. */ -interface Budget { - /** - * GPU memory in bytes for the data all views of the `Gpu` draw: its pages and each view's per-row - * state. A view draws a table too large for its share as a paged table, drawing only the pages the - * camera needs, so it always fits. Pages no view draws stay cached in the remaining room, least - * recently drawn evicted first. Defaults to half the device's largest storage buffer. The page - * pool grows as data needs it and never shrinks, so set the budget before drawing data. - */ - bytes: number; - /** Most bytes uploaded per frame; larger data streams in over several frames. Defaults to 32 MiB. */ - uploadPerFrame: number; -} -``` +(scene)= + +### `Scene` + +{.signature} +`interface Scene` + +A view's content and behavior: a space, marks drawn in order, a layout, a theme, controls, an +ease duration, and an optional title. + +A scene is plain data. In JSON, fields are camelCase and each mark is +`{ vertices | edges | faces | lines | areas: options }`. + +{#scene-space .member} +**`space`**`:` [`Space`](#space) [¶](#scene-space){.headerlink} +: The coordinate space positions are in. + +{#scene-marks .member} +**`marks`**`?:` [`Mark`](#mark)`[]` [¶](#scene-marks){.headerlink} +: The marks, in drawing order: each draws over the marks before it. + +{#scene-layout .member} +**`layout`**`?:` [`Layout`](#layout) [¶](#scene-layout){.headerlink} +: How the view places vertex rows that have no position: a diagram's blocks, or points on a plane + or the earth. `none` by default. + +{#scene-theme .member} +**`theme`**`?:` [`Theme`](#theme) [¶](#scene-theme){.headerlink} +: Default colors and sizes, and the look of text, focus, and each space; `dark` by default. + +{#scene-controls .member} +**`controls`**`?:` [`Controls`](#controls) [¶](#scene-controls){.headerlink} +: How the view responds to the pointer and keyboard. + +{#scene-ease .member} +**`ease`**`?: number` [¶](#scene-ease){.headerlink} +: Seconds over which scene and data changes animate; 0.3 by default, 0 for none. + +{#scene-title .member} +**`title`**`?: string | null` [¶](#scene-title){.headerlink} +: A title drawn at the view's top left, larger than the theme's text. + +{#scene-subtitle .member} +**`subtitle`**`?: string | null` [¶](#scene-subtitle){.headerlink} +: A subtitle drawn under the title. + +(space)= + +### `Space` + +{.signature} +`type Space = 'plane' | 'diagram' | 'earth' | 'plot' | { earth:` [`Earth`](#earth) `} | { plane:` [`Plane`](#plane) `} | { plot:` [`Plot`](#plot) `} | { diagram:` [`Diagram`](#diagram) `}` + +The coordinate space of a scene's positions: a name, or +`{ plane | diagram | earth | plot: options }`. + +The space sets the unit of lengths that do not name their own: units in a diagram, where a unit +is a CSS pixel at zoom 0, and CSS pixels elsewhere. Only a diagram has ports, groups, nets, and +orthogonal routes. + +{#space-earth .member} +`{` **`earth`**`:` [`Earth`](#earth) `}` [¶](#space-earth){.headerlink} +: Longitude and latitude in degrees, on a flat map or a globe. + +{#space-plane .member} +`{` **`plane`**`:` [`Plane`](#plane) `}` [¶](#space-plane){.headerlink} +: Plane units, with elevation; the camera can turn and tilt. + +{#space-plot .member} +`{` **`plot`**`:` [`Plot`](#plot) `}` [¶](#space-plot){.headerlink} +: The data's axis horizontally and values vertically. + +{#space-diagram .member} +`{` **`diagram`**`:` [`Diagram`](#diagram) `}` [¶](#space-diagram){.headerlink} +: A flat sheet for diagrams: x right and y down, every length in units. + +(plane)= + +### `Plane` + +{.signature} +`interface Plane` + +A plane: positions are in plane units. + +{#plane-grid .member} +**`grid`**`?:` [`Grid`](#grid) [¶](#plane-grid){.headerlink} +: The grid drawn under the marks; lines 10 units apart by default. + +(earth)= + +### `Earth` + +{.signature} +`interface Earth` + +The earth: positions are longitude and latitude in degrees. The ground takes the theme's `earth` +colors. + +{#earth-projection .member} +**`projection`**`?:` [`Projection`](#projection) [¶](#earth-projection){.headerlink} +: A flat web mercator map (`mercator`, the default) or a globe (`globe`). + +{#earth-graticule .member} +**`graticule`**`?: boolean` [¶](#earth-graticule){.headerlink} +: Whether lines of longitude and latitude cross the ground, in the theme's `grid` color. Default + `true`. + +{#earth-daylight .member} +**`daylight`**`?: number | null` [¶](#earth-daylight){.headerlink} +: Lights the earth by the sun's position this many seconds after the Unix epoch, plus the view's + axis coordinate when it has one. The night side darkens the ground and everything on it, down to + the theme's `earth.night` brightness. Unset, the earth is lit evenly. + +{#earth-atmosphere .member} +**`atmosphere`**`?: boolean` [¶](#earth-atmosphere){.headerlink} +: Whether air glows around the globe, in the theme's `earth.atmosphere` color, brighter on the day + side. Default `false`. + +(projection)= + +### `Projection` + +{.signature} +`type Projection = 'mercator' | 'globe'` + +How the earth is projected. + +{#projection-mercator .member} +**`'mercator'`** [¶](#projection-mercator){.headerlink} +: Web mercator. + +{#projection-globe .member} +**`'globe'`** [¶](#projection-globe){.headerlink} +: A sphere. + +(diagram)= + +### `Diagram` + +{.signature} +`interface Diagram` + +A flat sheet for diagrams, seen from straight above; the camera pans and zooms but does not +turn. Positions are in units with y down. Lengths are in units too, a unit being a CSS pixel at +zoom 0: sizes, padding, ports, strokes, text, and the theme's lengths. Sizes given with `px`, +the text halo, the focus glow, and the pick radius stay in CSS pixels. Zooming scales the whole +picture. Only a diagram has ports, groups, nets, and orthogonal routes, and the scene's layout +places its blocks. + +{#diagram-grid .member} +**`grid`**`?:` [`Grid`](#grid) [¶](#diagram-grid){.headerlink} +: The grid drawn under the marks; dots 20 units apart by default. + +(plot)= + +### `Plot` + +{.signature} +`interface Plot` + +A plot: x is the data's axis, y the values. + +{#plot-x .member} +**`x`**`?:` [`Axis`](#axis) [¶](#plot-x){.headerlink} +: The horizontal axis. + +{#plot-y .member} +**`y`**`?:` [`Axis`](#axis) [¶](#plot-y){.headerlink} +: The vertical axis. + +(axis)= + +### `Axis` + +{.signature} +`interface Axis` + +A plot axis. + +{#axis-label .member} +**`label`**`?: string | null` [¶](#axis-label){.headerlink} +: The axis title. + +{#axis-grid .member} +**`grid`**`?: boolean` [¶](#axis-grid){.headerlink} +: Whether grid lines cross the plot at the ticks. Default `true`. + +{#axis-hidden .member} +**`hidden`**`?: boolean` [¶](#axis-hidden){.headerlink} +: Whether the axis is hidden. Default `false`. + +{#axis-ticks .member} +**`ticks`**`?:` [`Tick`](#tick)`[] | null` [¶](#axis-ticks){.headerlink} +: Tick positions, each a value or `[value, label]`; a tick without a label shows its value. Unset, + ticks fall at round values. + +{#axis-format .member} +**`format`**`?:` [`Format`](#format) [¶](#axis-format){.headerlink} +: How tick values are formatted; `auto` by default. + +{#axis-precision .member} +**`precision`**`?: number | null` [¶](#axis-precision){.headerlink} +: Decimal places of fixed values, or of the significand in scientific and engineering formats, at + most 15. Unset, as many as the tick step needs. + +(tick)= + +### `Tick` + +{.signature} +`type Tick = number | [number, string | null]` + +A tick in JSON: a value, or `[value, label]`. + +(format)= + +### `Format` + +{.signature} +`type Format = 'auto' | 'fixed' | 'scientific' | 'engineering'` + +How an axis formats its tick values. + +{#format-auto .member} +**`'auto'`** [¶](#format-auto){.headerlink} +: Fixed decimals, scientific past a million or below a ten-thousandth. + +{#format-fixed .member} +**`'fixed'`** [¶](#format-fixed){.headerlink} +: Fixed decimals: `1500.0`. + +{#format-scientific .member} +**`'scientific'`** [¶](#format-scientific){.headerlink} +: A significand and a power of ten: `1.5e3`. + +{#format-engineering .member} +**`'engineering'`** [¶](#format-engineering){.headerlink} +: A significand and a power of ten that is a multiple of three: `1.5e3`, `150e-6`. + +(grid)= + +### `Grid` + +{.signature} +`type Grid = 'off' | { lines: { … } } | { dots: { … } }` + +A plane's or a diagram's grid: `"off"`, `{ lines: { pitch } }`, or `{ dots: { pitch, snap } }`. + +{#grid-off .member} +**`'off'`** [¶](#grid-off){.headerlink} +: No grid. + +{#grid-lines .member} +`{` **`lines`**`: { pitch: number } }` [¶](#grid-lines){.headerlink} +: Lines `pitch` units apart, thinned by factors of ten as the view zooms out. + + {#grid-lines-pitch .member} + **`pitch`**`: number` [¶](#grid-lines-pitch){.headerlink} + : Units between lines. + +{#grid-dots .member} +`{` **`dots`**`: { pitch: number; snap?: boolean } }` [¶](#grid-dots){.headerlink} +: Dots `pitch` units apart; with `snap`, a diagram's drags snap to them. + + {#grid-dots-pitch .member} + **`pitch`**`: number` [¶](#grid-dots-pitch){.headerlink} + : Units between dots. + + {#grid-dots-snap .member} + **`snap`**`?: boolean` [¶](#grid-dots-snap){.headerlink} + : Whether drag positions snap to the dots, in a diagram. Default `false`. + +(controls)= + +### `Controls` + +{.signature} +`type Controls = 'none' | 'inspect' | { … }` + +How a view responds to the pointer and keyboard, and how much it animates on its own: a preset +(`"none"` or `"inspect"`), or options over the defaults. Each mark controls whether its own rows +pick, select, and drag; these options control the view itself. + +{#controls-pan .member} +**`pan`**`?: boolean` [¶](#controls-pan){.headerlink} +: Whether dragging the background pans the camera. Default `true`. + +{#controls-zoom .member} +**`zoom`**`?:` [`Zoom`](#zoom) [¶](#controls-zoom){.headerlink} +: Which input zooms the view: the wheel and pinching (`wheel`, the default); pinching, and the + wheel only with Control or Meta held, as trackpad pinches send it, so the plain wheel scrolls + the page (`modifier`); or nothing (`off`). + +{#controls-turn .member} +**`turn`**`?: boolean` [¶](#controls-turn){.headerlink} +: Whether dragging with the secondary button, or with Shift, rotates and tilts a map or plane. + Default `true`. + +{#controls-keyboard .member} +**`keyboard`**`?: boolean` [¶](#controls-keyboard){.headerlink} +: Whether the keyboard controls the view. Default `true`. Tab and Shift+Tab step through the rows + nearest the view's center, and arrow keys step to the nearest row in their direction. Enter + opens the stepped-to row and Space selects it. With no row stepped to, arrow keys pan + (Shift+arrows turn), `+` and `-` zoom, Home fits everything, and Escape clears the selection. + +{#controls-hover .member} +**`hover`**`?: boolean` [¶](#controls-hover){.headerlink} +: Whether the hovered row glows, and a hovered vertex sized in CSS pixels grows. Default `true`. + Rows are hovered either way: a `'hovered'` event still fires and `hovered` still reads them. + +{#controls-pickradius .member} +**`pickRadius`**`?: number` [¶](#controls-pickradius){.headerlink} +: How far from the pointer, in CSS pixels, a row can be and still be picked; touch adds 8. Default + 6. + +{#controls-selectbox .member} +**`selectBox`**`?: boolean` [¶](#controls-selectbox){.headerlink} +: Whether Shift+drag selects the rows inside a box instead of turning the camera. Shift+click + still toggles one row. Default `false`. + +{#controls-connect .member} +**`connect`**`?: boolean` [¶](#controls-connect){.headerlink} +: Whether the user can draw connections, reported as a `'connect'` event: by dragging from a port, + from a vertex with Alt held, or an edge's end off its vertex. Default `false`. + +{#controls-delete .member} +**`delete`**`?: boolean` [¶](#controls-delete){.headerlink} +: Whether Delete and Backspace report the selection as a `'delete'` event. Default `false`. + +{#controls-cycle .member} +**`cycle`**`?: boolean` [¶](#controls-cycle){.headerlink} +: Whether clicking again at the same point, with the camera unmoved, selects the next row under + it, nearest first, so overlapping rows can each be selected. Default `false`. + +{#controls-press .member} +**`press`**`?: number` [¶](#controls-press){.headerlink} +: Seconds a touch or pen must hold still to request a menu (a `'menu'` event), as the secondary + button does; 0 turns this off. Default 0.5. + +{#controls-motion .member} +**`motion`**`?:` [`Motion`](#motion) [¶](#controls-motion){.headerlink} +: How much the view animates on its own: eases, camera flights, coasting, orbits, label fades, + comets, and options that read `elapsed`. `system` (the default) follows the user's + reduced-motion setting, `full` animates everything, and `reduced` animates only motion the user + drives. + +(motion)= + +### `Motion` + +{.signature} +`type Motion = 'system' | 'full' | 'reduced'` + +How much a view animates on its own. + +{#motion-system .member} +**`'system'`** [¶](#motion-system){.headerlink} +: Follows the system: reduced when the user asked their system for less motion. + +{#motion-full .member} +**`'full'`** [¶](#motion-full){.headerlink} +: All motion. + +{#motion-reduced .member} +**`'reduced'`** [¶](#motion-reduced){.headerlink} +: Only motion the user drives: changes apply at once, flights jump, nothing coasts or orbits, + labels appear without fading, and comets and options that read `elapsed` stay still. + +(zoom)= + +### `Zoom` + +{.signature} +`type Zoom = 'wheel' | 'modifier' | 'off'` + +Which input zooms a view. + +{#zoom-wheel .member} +**`'wheel'`** [¶](#zoom-wheel){.headerlink} +: The wheel, pinching, and the keys. + +{#zoom-modifier .member} +**`'modifier'`** [¶](#zoom-modifier){.headerlink} +: Pinching and the keys, and the wheel only with Control or Meta held (as trackpad pinches send + it), so the plain wheel scrolls the page. + +{#zoom-off .member} +**`'off'`** [¶](#zoom-off){.headerlink} +: No input zooms. + +(layout)= + +### `Layout` + +{.signature} +`type Layout = 'none' | 'stress' | 'layered' | { stress:` [`Stress`](#stress) `} | { layered:` [`Layered`](#layered) `}` + +How the view places vertex rows that have no position: a name, or +`{ stress | layered: options }`. In a diagram it places blocks; on a plane or the earth it +places points among the rows they link to. Gaps are in position units: diagram units, plane +units, or degrees on the earth. + +A row keeps its place once placed: rows added later are placed around the others, and +[`view.arrange`](#view-arrange) lays out every row again. + +{#layout-stress .member} +`{` **`stress`**`:` [`Stress`](#stress) `}` [¶](#layout-stress){.headerlink} +: Drawn distances follow graph distances. + +{#layout-layered .member} +`{` **`layered`**`:` [`Layered`](#layered) `}` [¶](#layout-layered){.headerlink} +: Ranks along a direction, for flows and hierarchies. + +(layered)= + +### `Layered` + +{.signature} +`interface Layered` + +Layered drawing: vertices in ranks, edges pointing along `flow`, and nets ranked from their +outputs. + +{#layered-flow .member} +**`flow`**`?:` [`Direction`](#direction) [¶](#layered-flow){.headerlink} +: The direction edges point; `right` by default. + +{#layered-gap .member} +**`gap`**`?: number` [¶](#layered-gap){.headerlink} +: Distance between vertices within a rank, in position units. Default 30. + +{#layered-rankgap .member} +**`rankGap`**`?: number` [¶](#layered-rankgap){.headerlink} +: Distance between ranks, in position units. Default 80. + +(stress)= + +### `Stress` + +{.signature} +`interface Stress` + +Stress majorization: vertices move until drawn distances match graph distances, then overlapping +boxes separate, keeping a quarter of `gap` between them. + +{#stress-gap .member} +**`gap`**`?: number` [¶](#stress-gap){.headerlink} +: Distance between the centers of linked vertices, in position units. Default 40. + +(direction)= + +### `Direction` + +{.signature} +`type Direction = 'right' | 'left' | 'down' | 'up'` + +A direction on screen. + +{#direction-right .member} +**`'right'`** [¶](#direction-right){.headerlink} +: Toward the right. + +{#direction-left .member} +**`'left'`** [¶](#direction-left){.headerlink} +: Toward the left. + +{#direction-down .member} +**`'down'`** [¶](#direction-down){.headerlink} +: Toward the bottom. + +{#direction-up .member} +**`'up'`** [¶](#direction-up){.headerlink} +: Toward the top. + +(order)= + +### `Order` + +{.signature} +`type Order = 'ascending' | 'descending'` + +Sort direction. Rows where the key has no value sort last in either direction. + +{#order-ascending .member} +**`'ascending'`** [¶](#order-ascending){.headerlink} +: Smallest first. + +{#order-descending .member} +**`'descending'`** [¶](#order-descending){.headerlink} +: Largest first. + +(budget)= + +### `Budget` + +{.signature} +`interface Budget` + +How much GPU memory latkit uses. + +{#budget-bytes .member} +**`bytes`**`: number` [¶](#budget-bytes){.headerlink} +: GPU memory in bytes for the data all views of the [`Gpu`](#gpu) draw: its pages and each view's per-row + state. A view draws a table too large for its share as a paged table, drawing only the pages the + camera needs, so it always fits. Pages no view draws stay cached in the remaining room, least + recently drawn evicted first. Defaults to half the device's largest storage buffer. The page + pool grows as data needs it and never shrinks, so set the budget before drawing data. + +{#budget-uploadperframe .member} +**`uploadPerFrame`**`: number` [¶](#budget-uploadperframe){.headerlink} +: Most bytes uploaded per frame; larger data streams in over several frames. Defaults to 32 MiB. ## Themes -How a scene looks, a group for each kind of thing it draws, and the lengths its options take. - -### Theme - -```ts -/** - * Default colors and sizes for marks whose options leave them unset or missing, and the look of - * text, focus, comets, and each space, grouped by subject. Write a base theme's name (`"dark"` or - * `"light"`), or options over a base (`base`, dark by default); a group's options override only - * the base values they set. - * - * Lengths are in the space's unit (units in a diagram, CSS pixels elsewhere), except the text halo - * and the focus glow, which are always in CSS pixels. - */ -type Theme = - | 'dark' - | 'light' - | { - /** The background color. */ - background?: Color; - /** The color of a plane's or diagram's grid, a plot's grid lines, and the earth's graticule. */ - grid?: Color; - /** The color a colormap gives a missing value, such as a row whose field has none. */ - missing?: Color; - /** The dash period: one dash and one gap of equal length. */ - dash?: number; - /** How much faces darken as they turn away from the camera, from 0 to 1. */ - shading?: number; - /** How labels, axis ticks, and titles look. */ - text?: TextTheme; - /** Defaults for vertices. */ - vertex?: VertexTheme; - /** Defaults for edges, and how parallel edges and arrowheads look. */ - edge?: EdgeTheme; - /** Defaults for lines. */ - line?: LineTheme; - /** Defaults for areas. */ - area?: AreaTheme; - /** How hovered and selected rows glow, and how rows outside a selection dim. */ - focus?: FocusTheme; - /** How comets flowing along edges and lines look. */ - flow?: FlowTheme; - /** How a diagram's blocks, groups, ports, and wires look. */ - diagram?: DiagramTheme; - /** How the earth looks. */ - earth?: EarthTheme; - /** How a plot looks. */ - plot?: PlotTheme; - /** The theme these options change; dark unless set. */ - base?: 'dark' | 'light'; - }; -``` - -### TextTheme - -```ts -/** How labels, axis ticks, and titles look. */ -interface TextTheme { - /** The text color. */ - color?: Color; - /** The CSS font family list. */ - font?: string; - /** The font size, in the space's unit. */ - size?: number; - /** Width of the background-colored halo around label text, in CSS pixels. */ - halo?: number; -} -``` - -### VertexTheme - -```ts -/** Defaults for vertices. */ -interface VertexTheme { - /** The vertex color. */ - color?: Color; - /** The vertex radius, in the space's unit. */ - radius?: number; -} -``` - -### EdgeTheme - -```ts -/** Defaults for edges, and how parallel edges and arrowheads look. */ -interface EdgeTheme { - /** - * The edge color. `null` blends the colors of each edge's two vertices; a diagram's wires and - * edges without `ends` (nets and poles) then take the text color. - */ - color?: Color | null; - /** The edge width, in the space's unit. */ - width?: number; - /** - * In a diagram, the distance between edges joining the same two vertices and between parallel - * orthogonal wires. Wires keep twice this distance from blocks where they can. - */ - spacing?: number; - /** - * Outside a diagram, how far edges joining the same two vertices bow apart: each lane by this - * fraction of the edge's length at its middle. A family spreads at most eight lanes wide, and more - * lanes pack closer. A bow is part of the drawing, so it scales with the zoom. - */ - bow?: number; - /** How far an arrowhead extends beyond the edge's width: a head is this plus the width long. */ - arrow?: number; -} -``` - -### LineTheme - -```ts -/** Defaults for lines. */ -interface LineTheme { - /** The line color. */ - color?: Color; - /** The line width, in the space's unit. */ - width?: number; -} -``` - -### AreaTheme - -```ts -/** Defaults for areas. */ -interface AreaTheme { - /** The area color. */ - color?: Color; -} -``` - -### FlowTheme - -```ts -/** - * How comets flowing along edges and lines look: a round head, brightest at its middle, trailing a - * tail that thins and fades. - * - * `spacing` and `length` must be in the same unit, which also measures each row's `flow` speed. In - * position units (degrees on the earth), comets are part of the drawing: they scale with the zoom - * and stay in place along their lines. In CSS pixels, they keep their on-screen size and speed. A - * plain number is in the space's unit. - */ -interface FlowTheme { - /** - * The comet color. Its alpha sets how strongly a comet covers its stroke; a fully transparent - * color turns comets off. - */ - color?: Color; - /** The distance between consecutive comets. */ - spacing?: Length; - /** The length of a comet's tail behind its head. */ - length?: Length; - /** The width of a comet's head, in the space's unit; never narrower than its stroke. */ - width?: number; -} -``` - -### FocusTheme - -```ts -/** How hovered and selected rows glow, and how rows outside a selection dim. */ -interface FocusTheme { - /** The glow color around hovered rows. */ - hover?: Color; - /** The glow color around selected rows; `null` glows in each row's own color. */ - selected?: Color | null; - /** How far the hover and selection glows reach, in CSS pixels. */ - glow?: number; - /** The opacity of rows outside the selection while any row is selected, from 0 to 1. */ - dim?: number; - /** The scale factor for a hovered vertex sized in CSS pixels; vertices sized in units only glow. */ - grow?: number; -} -``` - -### DiagramTheme - -```ts -/** How a diagram's blocks, groups, ports, and wires look. */ -interface DiagramTheme { - /** The fill color of group containers. */ - group?: Color; - /** The color of the one-unit border around blocks and groups; `null` draws no border. */ - outline?: Color | null; - /** The corner radius of blocks and groups; a block marker can set its own. */ - corner?: number; - /** The width of ports. */ - portSize?: number; - /** The bend radius of orthogonal wires. */ - bend?: number; - /** The space between a group's members and its border. */ - groupPadding?: number; - /** How blocks and groups cast shadows on the sheet. */ - shadow?: ShadowTheme; -} -``` - -### ShadowTheme - -```ts -/** How a diagram's blocks and groups cast shadows on the sheet, for vertex marks with `shadow` on. */ -interface ShadowTheme { - /** The color a shadow darkens toward; its alpha sets how dark the shadow is under the block. */ - color?: Color; - /** How far a shadow falls below its block or group; its edge softens over the same distance. */ - lift?: number; -} -``` - -### EarthTheme - -```ts -/** How the earth looks. */ -interface EarthTheme { - /** The ground color. */ - ground?: Color; - /** The color of the air glowing around the globe, when the earth's `atmosphere` is on. */ - atmosphere?: Color; - /** The brightness of the night side when the earth has `daylight`, from 0 to 1. */ - night?: number; -} -``` - -### PlotTheme - -```ts -/** How a plot looks. */ -interface PlotTheme { - /** The color of the line at the view's current axis coordinate. */ - playhead?: Color; -} -``` - -### Length - -```ts -/** - * A length: a number in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in - * CSS pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the - * view zooms in. - */ -type Length = - | number - | { - /** CSS pixels, at any zoom. */ - px: number; - } - | { - /** Position units (degrees on the earth), which grow on screen as the view zooms in. */ - units: number; - }; -``` +How a scene looks: a group for each kind of thing it draws, and the lengths its options take. + +(theme)= + +### `Theme` + +{.signature} +`type Theme = 'dark' | 'light' | { … }` + +Default colors and sizes for marks whose options leave them unset or missing, and the look of +text, focus, comets, and each space, grouped by subject. Write a base theme's name (`"dark"` or +`"light"`), or options over a base (`base`, dark by default); a group's options override only +the base values they set. + +Lengths are in the space's unit (units in a diagram, CSS pixels elsewhere), except the text halo +and the focus glow, which are always in CSS pixels. + +{#theme-background .member} +**`background`**`?:` [`Color`](#color) [¶](#theme-background){.headerlink} +: The background color. + +{#theme-grid .member} +**`grid`**`?:` [`Color`](#color) [¶](#theme-grid){.headerlink} +: The color of a plane's or diagram's grid, a plot's grid lines, and the earth's graticule. + +{#theme-missing .member} +**`missing`**`?:` [`Color`](#color) [¶](#theme-missing){.headerlink} +: The color a colormap gives a missing value, such as a row whose field has none. + +{#theme-dash .member} +**`dash`**`?: number` [¶](#theme-dash){.headerlink} +: The dash period: one dash and one gap of equal length. + +{#theme-shading .member} +**`shading`**`?: number` [¶](#theme-shading){.headerlink} +: How much faces darken as they turn away from the camera, from 0 to 1. + +{#theme-text .member} +**`text`**`?:` [`TextTheme`](#texttheme) [¶](#theme-text){.headerlink} +: How labels, axis ticks, and titles look. + +{#theme-vertex .member} +**`vertex`**`?:` [`VertexTheme`](#vertextheme) [¶](#theme-vertex){.headerlink} +: Defaults for vertices. + +{#theme-edge .member} +**`edge`**`?:` [`EdgeTheme`](#edgetheme) [¶](#theme-edge){.headerlink} +: Defaults for edges, and how parallel edges and arrowheads look. + +{#theme-line .member} +**`line`**`?:` [`LineTheme`](#linetheme) [¶](#theme-line){.headerlink} +: Defaults for lines. + +{#theme-area .member} +**`area`**`?:` [`AreaTheme`](#areatheme) [¶](#theme-area){.headerlink} +: Defaults for areas. + +{#theme-focus .member} +**`focus`**`?:` [`FocusTheme`](#focustheme) [¶](#theme-focus){.headerlink} +: How hovered and selected rows glow, and how rows outside a selection dim. + +{#theme-flow .member} +**`flow`**`?:` [`FlowTheme`](#flowtheme) [¶](#theme-flow){.headerlink} +: How comets flowing along edges and lines look. + +{#theme-diagram .member} +**`diagram`**`?:` [`DiagramTheme`](#diagramtheme) [¶](#theme-diagram){.headerlink} +: How a diagram's blocks, groups, ports, and wires look. + +{#theme-earth .member} +**`earth`**`?:` [`EarthTheme`](#earththeme) [¶](#theme-earth){.headerlink} +: How the earth looks. + +{#theme-plot .member} +**`plot`**`?:` [`PlotTheme`](#plottheme) [¶](#theme-plot){.headerlink} +: How a plot looks. + +{#theme-base .member} +**`base`**`?: 'dark' | 'light'` [¶](#theme-base){.headerlink} +: The theme these options change; dark unless set. + +(texttheme)= + +### `TextTheme` + +{.signature} +`interface TextTheme` + +How labels, axis ticks, and titles look. + +{#texttheme-color .member} +**`color`**`?:` [`Color`](#color) [¶](#texttheme-color){.headerlink} +: The text color. + +{#texttheme-font .member} +**`font`**`?: string` [¶](#texttheme-font){.headerlink} +: The CSS font family list. + +{#texttheme-size .member} +**`size`**`?: number` [¶](#texttheme-size){.headerlink} +: The font size, in the space's unit. + +{#texttheme-halo .member} +**`halo`**`?: number` [¶](#texttheme-halo){.headerlink} +: Width of the background-colored halo around label text, in CSS pixels. + +(vertextheme)= + +### `VertexTheme` + +{.signature} +`interface VertexTheme` + +Defaults for vertices. + +{#vertextheme-color .member} +**`color`**`?:` [`Color`](#color) [¶](#vertextheme-color){.headerlink} +: The vertex color. + +{#vertextheme-radius .member} +**`radius`**`?: number` [¶](#vertextheme-radius){.headerlink} +: The vertex radius, in the space's unit. + +(edgetheme)= + +### `EdgeTheme` + +{.signature} +`interface EdgeTheme` + +Defaults for edges, and how parallel edges and arrowheads look. + +{#edgetheme-color .member} +**`color`**`?:` [`Color`](#color) `| null` [¶](#edgetheme-color){.headerlink} +: The edge color. `null` blends the colors of each edge's two vertices; a diagram's wires and + edges without `ends` (nets and poles) then take the text color. + +{#edgetheme-width .member} +**`width`**`?: number` [¶](#edgetheme-width){.headerlink} +: The edge width, in the space's unit. + +{#edgetheme-spacing .member} +**`spacing`**`?: number` [¶](#edgetheme-spacing){.headerlink} +: In a diagram, the distance between edges joining the same two vertices and between parallel + orthogonal wires. Wires keep twice this distance from blocks where they can. + +{#edgetheme-bow .member} +**`bow`**`?: number` [¶](#edgetheme-bow){.headerlink} +: Outside a diagram, how far edges joining the same two vertices bow apart: each lane by this + fraction of the edge's length at its middle. A family spreads at most eight lanes wide, and more + lanes pack closer. A bow is part of the drawing, so it scales with the zoom. + +{#edgetheme-arrow .member} +**`arrow`**`?: number` [¶](#edgetheme-arrow){.headerlink} +: How far an arrowhead extends beyond the edge's width: a head is this plus the width long. + +(linetheme)= + +### `LineTheme` + +{.signature} +`interface LineTheme` + +Defaults for lines. + +{#linetheme-color .member} +**`color`**`?:` [`Color`](#color) [¶](#linetheme-color){.headerlink} +: The line color. + +{#linetheme-width .member} +**`width`**`?: number` [¶](#linetheme-width){.headerlink} +: The line width, in the space's unit. + +(areatheme)= + +### `AreaTheme` + +{.signature} +`interface AreaTheme` + +Defaults for areas. + +{#areatheme-color .member} +**`color`**`?:` [`Color`](#color) [¶](#areatheme-color){.headerlink} +: The area color. + +(flowtheme)= + +### `FlowTheme` + +{.signature} +`interface FlowTheme` + +How comets flowing along edges and lines look: a round head, brightest at its middle, trailing a +tail that thins and fades. + +`spacing` and `length` must be in the same unit, which also measures each row's `flow` speed. In +position units (degrees on the earth), comets are part of the drawing: they scale with the zoom +and stay in place along their lines. In CSS pixels, they keep their on-screen size and speed. A +plain number is in the space's unit. + +{#flowtheme-color .member} +**`color`**`?:` [`Color`](#color) [¶](#flowtheme-color){.headerlink} +: The comet color. Its alpha sets how strongly a comet covers its stroke; a fully transparent + color turns comets off. + +{#flowtheme-spacing .member} +**`spacing`**`?:` [`Length`](#length) [¶](#flowtheme-spacing){.headerlink} +: The distance between consecutive comets. + +{#flowtheme-length .member} +**`length`**`?:` [`Length`](#length) [¶](#flowtheme-length){.headerlink} +: The length of a comet's tail behind its head. + +{#flowtheme-width .member} +**`width`**`?: number` [¶](#flowtheme-width){.headerlink} +: The width of a comet's head, in the space's unit; never narrower than its stroke. + +(focustheme)= + +### `FocusTheme` + +{.signature} +`interface FocusTheme` + +How hovered and selected rows glow, and how rows outside a selection dim. + +{#focustheme-hover .member} +**`hover`**`?:` [`Color`](#color) [¶](#focustheme-hover){.headerlink} +: The glow color around hovered rows. + +{#focustheme-selected .member} +**`selected`**`?:` [`Color`](#color) `| null` [¶](#focustheme-selected){.headerlink} +: The glow color around selected rows; `null` glows in each row's own color. + +{#focustheme-glow .member} +**`glow`**`?: number` [¶](#focustheme-glow){.headerlink} +: How far the hover and selection glows reach, in CSS pixels. + +{#focustheme-dim .member} +**`dim`**`?: number` [¶](#focustheme-dim){.headerlink} +: The opacity of rows outside the selection while any row is selected, from 0 to 1. + +{#focustheme-grow .member} +**`grow`**`?: number` [¶](#focustheme-grow){.headerlink} +: The scale factor for a hovered vertex sized in CSS pixels; vertices sized in units only glow. + +(diagramtheme)= + +### `DiagramTheme` + +{.signature} +`interface DiagramTheme` + +How a diagram's blocks, groups, ports, and wires look. + +{#diagramtheme-group .member} +**`group`**`?:` [`Color`](#color) [¶](#diagramtheme-group){.headerlink} +: The fill color of group containers. + +{#diagramtheme-outline .member} +**`outline`**`?:` [`Color`](#color) `| null` [¶](#diagramtheme-outline){.headerlink} +: The color of the one-unit border around blocks and groups; `null` draws no border. + +{#diagramtheme-corner .member} +**`corner`**`?: number` [¶](#diagramtheme-corner){.headerlink} +: The corner radius of blocks and groups; a block marker can set its own. + +{#diagramtheme-portsize .member} +**`portSize`**`?: number` [¶](#diagramtheme-portsize){.headerlink} +: The width of ports. + +{#diagramtheme-bend .member} +**`bend`**`?: number` [¶](#diagramtheme-bend){.headerlink} +: The bend radius of orthogonal wires. + +{#diagramtheme-grouppadding .member} +**`groupPadding`**`?: number` [¶](#diagramtheme-grouppadding){.headerlink} +: The space between a group's members and its border. + +{#diagramtheme-shadow .member} +**`shadow`**`?:` [`ShadowTheme`](#shadowtheme) [¶](#diagramtheme-shadow){.headerlink} +: How blocks and groups cast shadows on the sheet. + +(shadowtheme)= + +### `ShadowTheme` + +{.signature} +`interface ShadowTheme` + +How a diagram's blocks and groups cast shadows on the sheet, for vertex marks with `shadow` on. + +{#shadowtheme-color .member} +**`color`**`?:` [`Color`](#color) [¶](#shadowtheme-color){.headerlink} +: The color a shadow darkens toward; its alpha sets how dark the shadow is under the block. + +{#shadowtheme-lift .member} +**`lift`**`?: number` [¶](#shadowtheme-lift){.headerlink} +: How far a shadow falls below its block or group; its edge softens over the same distance. + +(earththeme)= + +### `EarthTheme` + +{.signature} +`interface EarthTheme` + +How the earth looks. + +{#earththeme-ground .member} +**`ground`**`?:` [`Color`](#color) [¶](#earththeme-ground){.headerlink} +: The ground color. + +{#earththeme-atmosphere .member} +**`atmosphere`**`?:` [`Color`](#color) [¶](#earththeme-atmosphere){.headerlink} +: The color of the air glowing around the globe, when the earth's `atmosphere` is on. + +{#earththeme-night .member} +**`night`**`?: number` [¶](#earththeme-night){.headerlink} +: The brightness of the night side when the earth has `daylight`, from 0 to 1. + +(plottheme)= + +### `PlotTheme` + +{.signature} +`interface PlotTheme` + +How a plot looks. + +{#plottheme-playhead .member} +**`playhead`**`?:` [`Color`](#color) [¶](#plottheme-playhead){.headerlink} +: The color of the line at the view's current axis coordinate. + +(length)= + +### `Length` + +{.signature} +`type Length = number | { px: number } | { units: number }` + +A length: a number in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in +CSS pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the +view zooms in. + +{#length-px .member} +`{` **`px`**`: number }` [¶](#length-px){.headerlink} +: CSS pixels, at any zoom. + +{#length-units .member} +`{` **`units`**`: number }` [¶](#length-units){.headerlink} +: Position units (degrees on the earth), which grow on screen as the view zooms in. + +## Marks + +The marks that draw the tables of a scene. + +(mark)= + +### `Mark` + +{.signature} +`type Mark = { vertices:` [`Vertices`](#vertices) `} | { edges:` [`Edges`](#edges) `} | { faces:` [`Faces`](#faces) `} | { lines:` [`Lines`](#lines) `} | { areas:` [`Areas`](#areas) `}` + +One table a scene draws. In JSON: `{ vertices | edges | faces | lines | areas: options }`. + +{#mark-vertices .member} +`{` **`vertices`**`:` [`Vertices`](#vertices) `}` [¶](#mark-vertices){.headerlink} +: Rows drawn as markers. + +{#mark-edges .member} +`{` **`edges`**`:` [`Edges`](#edges) `}` [¶](#mark-edges){.headerlink} +: Rows drawn as strokes between two vertices. + +{#mark-faces .member} +`{` **`faces`**`:` [`Faces`](#faces) `}` [¶](#mark-faces){.headerlink} +: Rows drawn as triangles between three vertices. + +{#mark-lines .member} +`{` **`lines`**`:` [`Lines`](#lines) `}` [¶](#mark-lines){.headerlink} +: Rows drawn as lines along their paths, or on a plot through their samples. + +{#mark-areas .member} +`{` **`areas`**`:` [`Areas`](#areas) `}` [¶](#mark-areas){.headerlink} +: Rows drawn as the areas their polygons cover. + +(vertices)= + +### `Vertices` + +{.signature} +`interface Vertices` + +Rows drawn as markers: points of a map, nodes of a graph, blocks of a diagram. + +{#vertices-of .member} +**`of`**`: [data: string, table: string]` [¶](#vertices-of){.headerlink} +: The dataset and table drawn. + +{#vertices-position .member} +**`position`**`?: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] | null` [¶](#vertices-position){.headerlink} +: Each row's x and y. Without a position, the point field that `geometry` names places rows, and + the scene's layout places rows that have neither. + +{#vertices-geometry .member} +**`geometry`**`?: string | null` [¶](#vertices-geometry){.headerlink} +: The point field that places rows when `position` is not set; the field named `geometry` by + default. + +{#vertices-elevation .member} +**`elevation`**`?:` [`ExprInput`](#exprinput) [¶](#vertices-elevation){.headerlink} +: Height above the ground; on the earth, a fraction of the earth's radius. Default 0. Not allowed + in a diagram, which is flat. + +{#vertices-radius .member} +**`radius`**`?:` [`Size`](#size) `| null` [¶](#vertices-radius){.headerlink} +: Each row's radius, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given + with `px` or `units`; a diagram refuses `px`. The theme's `vertex.radius` unless set. + +{#vertices-size .member} +**`size`**`?: [`[`Size`](#size)`,` [`Size`](#size)`] | null` [¶](#vertices-size){.headerlink} +: Each row's width and height, for markers that are not round, in the space's unit unless given + with `px` or `units`; a diagram refuses `px`. In a diagram, a block without a size fits its + label and ports. + +{#vertices-marker .member} +**`marker`**`?:` [`Marker`](#marker) [¶](#vertices-marker){.headerlink} +: How each row is drawn; an ellipse by default. + +{#vertices-ring .member} +**`ring`**`?:` [`PaintInput`](#paintinput) `| null` [¶](#vertices-ring){.headerlink} +: A ring around each row's marker in this color; a missing or transparent color draws none. + +{#vertices-ports .member} +**`ports`**`?: [reference: string, port:` [`Port`](#port)`][]` [¶](#vertices-ports){.headerlink} +: Ports on each row, one for each reference field that names a net. An `edges` mark of the net + table, without `ends`, draws the nets. Diagrams only. + +{#vertices-contains .member} +**`contains`**`?: [table: string, reference: string] | null` [¶](#vertices-contains){.headerlink} +: Makes each row a group containing the rows of `table` whose `reference` field names it. A group + draws as a container around its members, with its label as a title in its header. Diagrams only. + +{#vertices-shadow .member} +**`shadow`**`?: boolean` [¶](#vertices-shadow){.headerlink} +: Whether each block or group casts a shadow on the sheet, styled by the theme's `diagram.shadow`. + Default `false`. Diagrams only. + +{#vertices-color .member} +**`color`**`?:` [`PaintInput`](#paintinput) `| null` [¶](#vertices-color){.headerlink} +: Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their + corners' colors. + +{#vertices-visible .member} +**`visible`**`?:` [`ExprInput`](#exprinput) [¶](#vertices-visible){.headerlink} +: Whether each row is drawn and can be picked. Default `true`. + +{#vertices-priority .member} +**`priority`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#vertices-priority){.headerlink} +: Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are + the last dropped when a table too large to draw whole is thinned to the tiles in view, and are + labeled first. + +{#vertices-name .member} +**`name`**`?: string | null` [¶](#vertices-name){.headerlink} +: Distinguishes two marks of the same table across scenes, and names the mark in hits. + +{#vertices-pick .member} +**`pick`**`?: boolean | null` [¶](#vertices-pick){.headerlink} +: Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces + do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. + +{#vertices-select .member} +**`select`**`?: boolean` [¶](#vertices-select){.headerlink} +: Whether clicking a row selects it. Default `true`. + +{#vertices-drag .member} +**`drag`**`?: boolean` [¶](#vertices-drag){.headerlink} +: Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the + camera. Default `false`. + +{#vertices-label .member} +**`label`**`?:` [`Label`](#label) `| null` [¶](#vertices-label){.headerlink} +: Each row's label: a text field, or label options. + +{#vertices-legend .member} +**`legend`**`?: string | null` [¶](#vertices-legend){.headerlink} +: A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap + across its domain, a cell for each category, or a swatch beside each name of a map's + `categories`. `color` must map through a colormap with a fixed domain or named categories; in a + conditional, `otherwise` is searched before the cases. + +(edges)= + +### `Edges` + +{.signature} +`interface Edges` + +Rows drawn as strokes between the vertices named by two reference fields: branches, links, +wires. In a diagram, a row without ends is a net, joining every vertex port whose reference +field names it. + +{#edges-of .member} +**`of`**`: [data: string, table: string]` [¶](#edges-of){.headerlink} +: The dataset and table drawn. + +{#edges-ends .member} +**`ends`**`?: [from: string, to: string] | null` [¶](#edges-ends){.headerlink} +: The reference fields naming each row's two vertices. + +{#edges-bends .member} +**`bends`**`?: string | null` [¶](#edges-bends){.headerlink} +: A path field whose points each row passes through between its ends. + +{#edges-route .member} +**`route`**`?:` [`Route`](#route) `| null` [¶](#edges-route){.headerlink} +: How each row runs between its ends: geodesic on the earth and straight elsewhere unless set. + `orthogonal` routes around blocks with rounded bends, in diagrams only. + +{#edges-arrows .member} +**`arrows`**`?: boolean` [¶](#edges-arrows){.headerlink} +: Whether each row has an arrowhead at its second end; a net has one at each input it reaches. + Default `false`. + +{#edges-poles .member} +**`poles`**`?: boolean` [¶](#edges-poles){.headerlink} +: Draws each row as a pole from its own vertex down to the ground, on a plane or the earth. Poles + take no `ends`, `bends`, or `joins`. Default `false`. + +{#edges-joins .member} +**`joins`**`?: [table: string, reference: string] | null` [¶](#edges-joins){.headerlink} +: Draws each row as a net on a plane or the earth, joining the vertex rows of `table` whose + `reference` field names it: a star from their center to each member. + +{#edges-width .member} +**`width`**`?:` [`Size`](#size) `| null` [¶](#edges-width){.headerlink} +: Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given + with `px` or `units`. The theme's width unless set. + +{#edges-dash .member} +**`dash`**`?:` [`ExprInput`](#exprinput) [¶](#edges-dash){.headerlink} +: Whether each row is dashed, with the theme's `dash` period. Default `false`. + +{#edges-flow .member} +**`flow`**`?:` [`ExprInput`](#exprinput) [¶](#edges-flow){.headerlink} +: The speed of comets flowing along each row, per second, in the unit of the theme's `flow` + lengths. Negative speeds flow backward; 0, the default, draws no comets. When the speed changes, + comets continue from where they are. + +{#edges-color .member} +**`color`**`?:` [`PaintInput`](#paintinput) `| null` [¶](#edges-color){.headerlink} +: Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their + corners' colors. + +{#edges-visible .member} +**`visible`**`?:` [`ExprInput`](#exprinput) [¶](#edges-visible){.headerlink} +: Whether each row is drawn and can be picked. Default `true`. + +{#edges-priority .member} +**`priority`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#edges-priority){.headerlink} +: Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are + the last dropped when a table too large to draw whole is thinned to the tiles in view, and are + labeled first. + +{#edges-name .member} +**`name`**`?: string | null` [¶](#edges-name){.headerlink} +: Distinguishes two marks of the same table across scenes, and names the mark in hits. + +{#edges-pick .member} +**`pick`**`?: boolean | null` [¶](#edges-pick){.headerlink} +: Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces + do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. + +{#edges-select .member} +**`select`**`?: boolean` [¶](#edges-select){.headerlink} +: Whether clicking a row selects it. Default `true`. + +{#edges-drag .member} +**`drag`**`?: boolean` [¶](#edges-drag){.headerlink} +: Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the + camera. Default `false`. + +{#edges-label .member} +**`label`**`?:` [`Label`](#label) `| null` [¶](#edges-label){.headerlink} +: Each row's label: a text field, or label options. + +{#edges-legend .member} +**`legend`**`?: string | null` [¶](#edges-legend){.headerlink} +: A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap + across its domain, a cell for each category, or a swatch beside each name of a map's + `categories`. `color` must map through a colormap with a fixed domain or named categories; in a + conditional, `otherwise` is searched before the cases. + +(lines)= + +### `Lines` + +{.signature} +`interface Lines` + +Rows drawn as lines along a path field (roads, rivers, borders) or a polygon field's rings (area +outlines). On a plot, each row instead traces a sampled field's history across the data's axis, +as a monitor does. On the earth, paths follow the globe's curve and stay whole across the +antimeridian. + +{#lines-of .member} +**`of`**`: [data: string, table: string]` [¶](#lines-of){.headerlink} +: The dataset and table drawn. + +{#lines-geometry .member} +**`geometry`**`?: string | null` [¶](#lines-geometry){.headerlink} +: The path or polygon field each row runs along; the field named `geometry` by default. Not + allowed on a plot. + +{#lines-trace .member} +**`trace`**`?: string | null` [¶](#lines-trace){.headerlink} +: The sampled field each row traces across the data's axis; plots only, where it is required. A + field with no frames yet draws nothing until its first frame arrives. + +{#lines-interpolation .member} +**`interpolation`**`?:` [`Interpolation`](#interpolation) [¶](#lines-interpolation){.headerlink} +: How each row runs between its samples on a plot; `linear` by default. + +{#lines-width .member} +**`width`**`?:` [`Size`](#size) `| null` [¶](#lines-width){.headerlink} +: Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given + with `px` or `units`. The theme's width unless set. + +{#lines-dash .member} +**`dash`**`?:` [`ExprInput`](#exprinput) [¶](#lines-dash){.headerlink} +: Whether each row is dashed, with the theme's `dash` period. Default `false`. + +{#lines-flow .member} +**`flow`**`?:` [`ExprInput`](#exprinput) [¶](#lines-flow){.headerlink} +: The speed of comets flowing along each row, per second, in the unit of the theme's `flow` + lengths. Negative speeds flow backward; 0, the default, draws no comets. When the speed changes, + comets continue from where they are. + +{#lines-color .member} +**`color`**`?:` [`PaintInput`](#paintinput) `| null` [¶](#lines-color){.headerlink} +: Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their + corners' colors. + +{#lines-visible .member} +**`visible`**`?:` [`ExprInput`](#exprinput) [¶](#lines-visible){.headerlink} +: Whether each row is drawn and can be picked. Default `true`. + +{#lines-priority .member} +**`priority`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#lines-priority){.headerlink} +: Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are + the last dropped when a table too large to draw whole is thinned to the tiles in view, and are + labeled first. + +{#lines-name .member} +**`name`**`?: string | null` [¶](#lines-name){.headerlink} +: Distinguishes two marks of the same table across scenes, and names the mark in hits. + +{#lines-pick .member} +**`pick`**`?: boolean | null` [¶](#lines-pick){.headerlink} +: Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces + do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. + +{#lines-select .member} +**`select`**`?: boolean` [¶](#lines-select){.headerlink} +: Whether clicking a row selects it. Default `true`. + +{#lines-drag .member} +**`drag`**`?: boolean` [¶](#lines-drag){.headerlink} +: Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the + camera. Default `false`. + +{#lines-label .member} +**`label`**`?:` [`Label`](#label) `| null` [¶](#lines-label){.headerlink} +: Each row's label: a text field, or label options. + +{#lines-legend .member} +**`legend`**`?: string | null` [¶](#lines-legend){.headerlink} +: A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap + across its domain, a cell for each category, or a swatch beside each name of a map's + `categories`. `color` must map through a colormap with a fixed domain or named categories; in a + conditional, `otherwise` is searched before the cases. + +(areas)= + +### `Areas` + +{.signature} +`interface Areas` + +Rows drawn as the areas of a polygon field: parcels, states, lakes, the land and water of a map. +The view triangulates each row's polygons once, in the background when they are large. On the +earth, areas follow the globe's curve and stay whole across the antimeridian. To outline areas, +add a [`Lines`](#lines) mark of the same table, which draws their rings. + +{#areas-of .member} +**`of`**`: [data: string, table: string]` [¶](#areas-of){.headerlink} +: The dataset and table drawn. + +{#areas-geometry .member} +**`geometry`**`?: string | null` [¶](#areas-geometry){.headerlink} +: The polygon field each row fills, holding one polygon's rings or several polygons; the field + named `geometry` by default. + +{#areas-color .member} +**`color`**`?:` [`PaintInput`](#paintinput) `| null` [¶](#areas-color){.headerlink} +: Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their + corners' colors. + +{#areas-visible .member} +**`visible`**`?:` [`ExprInput`](#exprinput) [¶](#areas-visible){.headerlink} +: Whether each row is drawn and can be picked. Default `true`. + +{#areas-priority .member} +**`priority`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#areas-priority){.headerlink} +: Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are + the last dropped when a table too large to draw whole is thinned to the tiles in view, and are + labeled first. + +{#areas-name .member} +**`name`**`?: string | null` [¶](#areas-name){.headerlink} +: Distinguishes two marks of the same table across scenes, and names the mark in hits. + +{#areas-pick .member} +**`pick`**`?: boolean | null` [¶](#areas-pick){.headerlink} +: Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces + do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. + +{#areas-select .member} +**`select`**`?: boolean` [¶](#areas-select){.headerlink} +: Whether clicking a row selects it. Default `true`. + +{#areas-drag .member} +**`drag`**`?: boolean` [¶](#areas-drag){.headerlink} +: Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the + camera. Default `false`. + +{#areas-label .member} +**`label`**`?:` [`Label`](#label) `| null` [¶](#areas-label){.headerlink} +: Each row's label: a text field, or label options. + +{#areas-legend .member} +**`legend`**`?: string | null` [¶](#areas-legend){.headerlink} +: A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap + across its domain, a cell for each category, or a swatch beside each name of a map's + `categories`. `color` must map through a colormap with a fixed domain or named categories; in a + conditional, `otherwise` is searched before the cases. + +(faces)= + +### `Faces` + +{.signature} +`interface Faces` + +Rows drawn as triangles between the vertices named by three reference fields: meshes, surfaces, +and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen +from above shows its exact colors. + +{#faces-of .member} +**`of`**`: [data: string, table: string]` [¶](#faces-of){.headerlink} +: The dataset and table drawn. + +{#faces-corners .member} +**`corners`**`?: [a: string, b: string, c: string] | null` [¶](#faces-corners){.headerlink} +: The reference fields naming each row's three vertices; required. Without a color, a face blends + the colors of its corners. + +{#faces-color .member} +**`color`**`?:` [`PaintInput`](#paintinput) `| null` [¶](#faces-color){.headerlink} +: Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their + corners' colors. + +{#faces-visible .member} +**`visible`**`?:` [`ExprInput`](#exprinput) [¶](#faces-visible){.headerlink} +: Whether each row is drawn and can be picked. Default `true`. + +{#faces-priority .member} +**`priority`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#faces-priority){.headerlink} +: Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are + the last dropped when a table too large to draw whole is thinned to the tiles in view, and are + labeled first. + +{#faces-name .member} +**`name`**`?: string | null` [¶](#faces-name){.headerlink} +: Distinguishes two marks of the same table across scenes, and names the mark in hits. + +{#faces-pick .member} +**`pick`**`?: boolean | null` [¶](#faces-pick){.headerlink} +: Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces + do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. + +{#faces-select .member} +**`select`**`?: boolean` [¶](#faces-select){.headerlink} +: Whether clicking a row selects it. Default `true`. + +{#faces-drag .member} +**`drag`**`?: boolean` [¶](#faces-drag){.headerlink} +: Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the + camera. Default `false`. + +{#faces-label .member} +**`label`**`?:` [`Label`](#label) `| null` [¶](#faces-label){.headerlink} +: Each row's label: a text field, or label options. + +{#faces-legend .member} +**`legend`**`?: string | null` [¶](#faces-legend){.headerlink} +: A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap + across its domain, a cell for each category, or a swatch beside each name of a map's + `categories`. `color` must map through a colormap with a fixed domain or named categories; in a + conditional, `otherwise` is searched before the cases. + +(marker)= + +### `Marker` + +{.signature} +`type Marker = 'ellipse' | 'rectangle' | 'rounded' | 'diamond' | 'block' | { shape:` [`Shape`](#shape) `} | { block: { … } } | { gauge: { … } } | { pie: { … } } | { icon: { … } } | { wgsl: { … } }` + +How a vertex is drawn: a shape's name, `"block"`, or +`{ shape | block | gauge | pie | icon | wgsl: options }`. An ellipse by default. + +{#marker-shape .member} +`{` **`shape`**`:` [`Shape`](#shape) `}` [¶](#marker-shape){.headerlink} +: A filled shape. + +{#marker-block .member} +`{` **`block`**`: { corner?: number | null } }` [¶](#marker-block){.headerlink} +: A rectangle with rounded corners that holds its label inside, as a diagram's blocks do. + + {#marker-block-corner .member} + **`corner`**`?: number | null` [¶](#marker-block-corner){.headerlink} + : Corner radius in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's + `diagram.corner` unless set. + +{#marker-gauge .member} +`{` **`gauge`**`: { fill:` [`ExprInput`](#exprinput)`; shape?:` [`Shape`](#shape) `} }` [¶](#marker-gauge){.headerlink} +: A shape's outline, filled to `fill` from 0 to 1: around a circle, or upward in other shapes. + + {#marker-gauge-fill .member} + **`fill`**`:` [`ExprInput`](#exprinput) [¶](#marker-gauge-fill){.headerlink} + : How full each row's gauge is, from 0 to 1. + + {#marker-gauge-shape .member} + **`shape`**`?:` [`Shape`](#shape) [¶](#marker-gauge-shape){.headerlink} + : The outline's shape; an ellipse by default. + +{#marker-pie .member} +`{` **`pie`**`: { slices:` [`ExprInput`](#exprinput)`[]; colors:` [`Color`](#color)`[]; hole?: number } }` [¶](#marker-pie){.headerlink} +: A circle divided into slices sized in proportion to their values. + + {#marker-pie-slices .member} + **`slices`**`:` [`ExprInput`](#exprinput)`[]` [¶](#marker-pie-slices){.headerlink} + : Each slice's value. + + {#marker-pie-colors .member} + **`colors`**`:` [`Color`](#color)`[]` [¶](#marker-pie-colors){.headerlink} + : Each slice's color. + + {#marker-pie-hole .member} + **`hole`**`?: number` [¶](#marker-pie-hole){.headerlink} + : The hole in the middle, as a fraction of the radius; 0 by default. + +{#marker-icon .member} +`{` **`icon`**`: { images:` [`Bitmap`](#bitmap)`[]; index:` [`ExprInput`](#exprinput) `} }` [¶](#marker-icon){.headerlink} +: One of `images`, fitted to the marker's box; `index` picks the image for each row. + + {#marker-icon-images .member} + **`images`**`:` [`Bitmap`](#bitmap)`[]` [¶](#marker-icon-images){.headerlink} + : The images. + + {#marker-icon-index .member} + **`index`**`:` [`ExprInput`](#exprinput) [¶](#marker-icon-index){.headerlink} + : Each row's image index, from 0. + +{#marker-wgsl .member} +`{` **`wgsl`**`: { source: string; inputs?:` [`ExprInput`](#exprinput)`[] } }` [¶](#marker-wgsl){.headerlink} +: A custom marker in WGSL. `source` defines `fn marker(f: MarkerFragment) -> MarkerColor`, which + can read up to eight `inputs` as `f.input[i]`. It compiles once, when the view first draws it. + + {#marker-wgsl-source .member} + **`source`**`: string` [¶](#marker-wgsl-source){.headerlink} + : The WGSL source of the `marker` function. + + {#marker-wgsl-inputs .member} + **`inputs`**`?:` [`ExprInput`](#exprinput)`[]` [¶](#marker-wgsl-inputs){.headerlink} + : Each row's inputs, at most eight. + +(shape)= + +### `Shape` + +{.signature} +`type Shape = 'ellipse' | 'rectangle' | 'rounded' | 'diamond'` + +A marker's outline. + +{#shape-ellipse .member} +**`'ellipse'`** [¶](#shape-ellipse){.headerlink} +: An ellipse: a circle when its width and height are equal. + +{#shape-rectangle .member} +**`'rectangle'`** [¶](#shape-rectangle){.headerlink} +: A rectangle. + +{#shape-rounded .member} +**`'rounded'`** [¶](#shape-rounded){.headerlink} +: A rectangle with rounded corners. + +{#shape-diamond .member} +**`'diamond'`** [¶](#shape-diamond){.headerlink} +: A diamond. + +(bitmap)= + +### `Bitmap` + +{.signature} +`interface Bitmap` + +An image of RGBA texels: straight alpha, sRGB, rows top to bottom. + +{#bitmap-size .member} +**`size`**`: [number, number]` [¶](#bitmap-size){.headerlink} +: Width and height in texels. + +{#bitmap-rgba .member} +**`rgba`**`: number[]` [¶](#bitmap-rgba){.headerlink} +: `size[0] * size[1] * 4` bytes. + +(port)= + +### `Port` + +{.signature} +`type Port = 'left' | 'right' | 'top' | 'bottom' | { … }` + +A port on one side of a vertex's marker, where a net connects: a side name, or options. The +vertex names the net in a reference field. An output drives its net: layouts rank a net from its +outputs, and the net's arrows point away from them. + +{#port-side .member} +**`side`**`:` [`Side`](#side) [¶](#port-side){.headerlink} +: The side of the marker it sits on. + +{#port-order .member} +**`order`**`?: number | null` [¶](#port-order){.headerlink} +: Its position among the ports on its side, counted from the top or left. Ports without an `order` + follow those with one, in declaration order. + +{#port-label .member} +**`label`**`?: string | null` [¶](#port-label){.headerlink} +: Its name, drawn beside it inside the marker. + +{#port-shape .member} +**`shape`**`?:` [`Shape`](#shape) [¶](#port-shape){.headerlink} +: Its shape; an ellipse by default. + +{#port-output .member} +**`output`**`?: boolean` [¶](#port-output){.headerlink} +: Whether it is an output, which drives its net. Default `false`, an input. + +(side)= + +### `Side` + +{.signature} +`type Side = 'left' | 'right' | 'top' | 'bottom'` + +A side of a marker. + +{#side-left .member} +**`'left'`** [¶](#side-left){.headerlink} +: The left side. + +{#side-right .member} +**`'right'`** [¶](#side-right){.headerlink} +: The right side. + +{#side-top .member} +**`'top'`** [¶](#side-top){.headerlink} +: The top. + +{#side-bottom .member} +**`'bottom'`** [¶](#side-bottom){.headerlink} +: The bottom. + +(route)= + +### `Route` + +{.signature} +`type Route = 'straight' | 'geodesic' | 'orthogonal'` + +How an edge runs between its ends. + +{#route-straight .member} +**`'straight'`** [¶](#route-straight){.headerlink} +: A straight line on screen. + +{#route-geodesic .member} +**`'geodesic'`** [¶](#route-geodesic){.headerlink} +: Along the great circle, on the earth. + +{#route-orthogonal .member} +**`'orthogonal'`** [¶](#route-orthogonal){.headerlink} +: Horizontal and vertical runs around vertices that leave and enter at their sides, with rounded + bends. Diagrams only. + +(interpolation)= + +### `Interpolation` + +{.signature} +`type Interpolation = 'linear' | 'stepBefore' | 'stepAfter'` + +How a line runs between its samples. + +{#interpolation-linear .member} +**`'linear'`** [¶](#interpolation-linear){.headerlink} +: Straight from each sample to the next. + +{#interpolation-stepbefore .member} +**`'stepBefore'`** [¶](#interpolation-stepbefore){.headerlink} +: Each value holds from the previous sample up to its own, so the line steps at the previous + sample. + +{#interpolation-stepafter .member} +**`'stepAfter'`** [¶](#interpolation-stepafter){.headerlink} +: Each value holds until the next sample, where the line steps. + +(label)= + +### `Label` + +{.signature} +`type Label =` [`ExprInput`](#exprinput) `| { … }` + +Text for each row, placed beside, inside, or along the row's shape and following it every frame: +a text field, or options. Where labels would overlap, the higher priority shows; among equals, +labels already shown stay. In a diagram a label is part of the drawing: it scales with the zoom, +fades out when too small to read, and shows wherever it fits its vertex. + +{#label-exprinput .member} +[`ExprInput`](#exprinput) [¶](#label-exprinput){.headerlink} +: A value option: an [`Expr`](#expr), or its JSON form, in which a string is a field name. + +{#label-text .member} +**`text`**`:` [`ExprInput`](#exprinput) [¶](#label-text){.headerlink} +: The text field to show. + +{#label-size .member} +**`size`**`?: number | null` [¶](#label-size){.headerlink} +: Font size in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's + `text.size` unless set. + +{#label-color .member} +**`color`**`?:` [`PaintInput`](#paintinput) `| null` [¶](#label-color){.headerlink} +: Each row's text color; the theme's text color unless set. + +{#label-priority .member} +**`priority`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#label-priority){.headerlink} +: Where labels would overlap, the higher priority shows; the mark's `priority` unless set. + +{#label-maxwidth .member} +**`maxWidth`**`?: number | null` [¶](#label-maxwidth){.headerlink} +: The width at which lines wrap, in the space's unit (units in a diagram, CSS pixels elsewhere). + +{#label-font .member} +**`font`**`?: string | null` [¶](#label-font){.headerlink} +: The CSS font family list for the text; the theme's `text.font` unless set. + +{#label-max .member} +**`max`**`?: number | null` [¶](#label-max){.headerlink} +: The most labels shown at once: those of highest priority among the rows in view, or fewer where + they would overlap. No limit unless set. + +(size)= + +### `Size` + +{.signature} +`type Size =` [`ExprInput`](#exprinput) `|` [`Px`](#px) `|` [`Units`](#units) + +A size: a value in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in CSS +pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the +view zooms in. + +{#size-exprinput .member} +[`ExprInput`](#exprinput) [¶](#size-exprinput){.headerlink} +: A value option: an [`Expr`](#expr), or its JSON form, in which a string is a field name. + +{#size-px .member} +[`Px`](#px) [¶](#size-px){.headerlink} +: A size in CSS pixels, regardless of the space's unit. + +{#size-units .member} +[`Units`](#units) [¶](#size-units){.headerlink} +: A size in position units (degrees on the earth), regardless of the space's unit, so it scales as the view zooms. + +(px)= + +### `Px` + +{.signature} +`interface Px` + +A size in CSS pixels, regardless of the space's unit. + +{#px-px .member} +**`px`**`:` [`ExprInput`](#exprinput) [¶](#px-px){.headerlink} +: The size, in CSS pixels. + +(units)= + +### `Units` + +{.signature} +`interface Units` + +A size in position units (degrees on the earth), regardless of the space's unit, so it scales as +the view zooms. `atLeast` and `atMost` bound it in CSS pixels. + +{#units-units .member} +**`units`**`:` [`ExprInput`](#exprinput) [¶](#units-units){.headerlink} +: The size, in position units. + +{#units-atleast .member} +**`atLeast`**`?: number | null` [¶](#units-atleast){.headerlink} +: The minimum on-screen size in CSS pixels, however far the view zooms out. + +{#units-atmost .member} +**`atMost`**`?: number | null` [¶](#units-atmost){.headerlink} +: The maximum on-screen size in CSS pixels, however far the view zooms in. + +## Expressions + +Expressions as JSON: a constant, a field, or one of these nodes. Colors take CSS strings where values take field names. + +(exprnode)= + +### `ExprNode` + +{.signature} +`type ExprNode = number | boolean | string |` [`FieldNode`](#fieldnode) `|` [`ColorNode`](#colornode) `|` [`StateNode`](#statenode) `|` [`MissingNode`](#missingnode) `|` [`UnaryNode`](#unarynode) `|` [`BinaryNode`](#binarynode) `|` [`WhenNode`](#whennode) `|` [`IsNode`](#isnode) `|` [`ContainsNode`](#containsnode) `|` [`ScaleNode`](#scalenode) `|` [`ColormapNode`](#colormapnode) + +A value for each row: a number, a boolean, a field's name, or a node that computes one. + +{#exprnode-fieldnode .member} +[`FieldNode`](#fieldnode) [¶](#exprnode-fieldnode){.headerlink} +: A field's value in each row: from the mark's own table, or with `from`, from another dataset's table joined by row id. + +{#exprnode-colornode .member} +[`ColorNode`](#colornode) [¶](#exprnode-colornode){.headerlink} +: A constant color. + +{#exprnode-statenode .member} +[`StateNode`](#statenode) [¶](#exprnode-statenode){.headerlink} +: View state of each row, such as whether it is selected. + +{#exprnode-missingnode .member} +[`MissingNode`](#missingnode) [¶](#exprnode-missingnode){.headerlink} +: A missing value. + +{#exprnode-unarynode .member} +[`UnaryNode`](#unarynode) [¶](#exprnode-unarynode){.headerlink} +: A one-argument operation, such as `{ "op": "abs", "args": ["x"] }`. + +{#exprnode-binarynode .member} +[`BinaryNode`](#binarynode) [¶](#exprnode-binarynode){.headerlink} +: A two-argument operation, such as `{ "op": "add", "args": ["a", 1] }`. + +{#exprnode-whennode .member} +[`WhenNode`](#whennode) [¶](#exprnode-whennode){.headerlink} +: The value of the first case whose condition holds, else `otherwise`. + +{#exprnode-isnode .member} +[`IsNode`](#isnode) [¶](#exprnode-isnode){.headerlink} +: Whether a text field equals a text exactly. + +{#exprnode-containsnode .member} +[`ContainsNode`](#containsnode) [¶](#exprnode-containsnode){.headerlink} +: Whether a text field contains a text, ignoring case. + +{#exprnode-scalenode .member} +[`ScaleNode`](#scalenode) [¶](#exprnode-scalenode){.headerlink} +: A value mapped linearly onto a range, clamped to the range. + +{#exprnode-colormapnode .member} +[`ColormapNode`](#colormapnode) [¶](#exprnode-colormapnode){.headerlink} +: A value mapped through a colormap. + +(paintnode)= + +### `PaintNode` + +{.signature} +`type PaintNode =` [`Color`](#color) `|` [`ColorNode`](#colornode) `|` [`ColormapNode`](#colormapnode) `|` [`WhenPaintNode`](#whenpaintnode) `|` [`MissingNode`](#missingnode) + +A color for each row: a CSS color, a value through a colormap, or a conditional choosing between +colors. + +{#paintnode-color .member} +[`Color`](#color) [¶](#paintnode-color){.headerlink} +: A CSS color, such as `#ffb000` or `rebeccapurple`. + +{#paintnode-colornode .member} +[`ColorNode`](#colornode) [¶](#paintnode-colornode){.headerlink} +: A constant color. + +{#paintnode-colormapnode .member} +[`ColormapNode`](#colormapnode) [¶](#paintnode-colormapnode){.headerlink} +: A value mapped through a colormap. + +{#paintnode-whenpaintnode .member} +[`WhenPaintNode`](#whenpaintnode) [¶](#paintnode-whenpaintnode){.headerlink} +: A conditional color: the color of the first case whose condition holds, else `otherwise`. + +{#paintnode-missingnode .member} +[`MissingNode`](#missingnode) [¶](#paintnode-missingnode){.headerlink} +: A missing value. + +(fieldnode)= + +### `FieldNode` + +{.signature} +`interface FieldNode` + +A field's value in each row: from the mark's own table, or with `from`, from another dataset's +table joined by row id. + +{#fieldnode-field .member} +**`field`**`: string` [¶](#fieldnode-field){.headerlink} +: The field's name. + +{#fieldnode-from .member} +**`from`**`?: [data: string, table: string] | null` [¶](#fieldnode-from){.headerlink} +: The dataset and table the field is read from, joined by row id. + +{#fieldnode-step .member} +**`step`**`?: boolean` [¶](#fieldnode-step){.headerlink} +: For a sampled field, reads the last frame at or before the shown coordinate instead of + interpolating. Default `false`. + +(colornode)= + +### `ColorNode` + +{.signature} +`interface ColorNode` + +A constant color. + +{#colornode-color .member} +**`color`**`:` [`Color`](#color) [¶](#colornode-color){.headerlink} +: A CSS color, such as `#ffb000` or `rebeccapurple`. + +(statenode)= + +### `StateNode` + +{.signature} +`interface StateNode` + +View state of each row, such as whether it is selected. + +{#statenode-state .member} +**`state`**`:` [`State`](#state) [¶](#statenode-state){.headerlink} +: The state read. + +(missingnode)= + +### `MissingNode` + +{.signature} +`interface MissingNode` + +A missing value. An option that evaluates to missing uses the theme's value. + +{#missingnode-missing .member} +**`missing`**`: true` [¶](#missingnode-missing){.headerlink} +: Always `true`. + +(unarynode)= + +### `UnaryNode` + +{.signature} +`interface UnaryNode` + +A one-argument operation, such as `{ "op": "abs", "args": ["x"] }`. + +{#unarynode-op .member} +**`op`**`:` [`Unary`](#unary) [¶](#unarynode-op){.headerlink} +: The operation. + +{#unarynode-args .member} +**`args`**`: [`[`ExprInput`](#exprinput)`]` [¶](#unarynode-args){.headerlink} +: Its argument. + +(binarynode)= + +### `BinaryNode` + +{.signature} +`interface BinaryNode` + +A two-argument operation, such as `{ "op": "add", "args": ["a", 1] }`. + +{#binarynode-op .member} +**`op`**`:` [`Binary`](#binary) [¶](#binarynode-op){.headerlink} +: The operation. + +{#binarynode-args .member} +**`args`**`: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`]` [¶](#binarynode-args){.headerlink} +: Its arguments, left and right. + +(whennode)= + +### `WhenNode` + +{.signature} +`interface WhenNode` + +The value of the first case whose condition holds, else `otherwise`. + +{#whennode-when .member} +**`when`**`: [condition:` [`ExprInput`](#exprinput)`, value:` [`ExprInput`](#exprinput)`][]` [¶](#whennode-when){.headerlink} +: The cases, as `[condition, value]` pairs tried in order. + +{#whennode-otherwise .member} +**`otherwise`**`:` [`ExprInput`](#exprinput) [¶](#whennode-otherwise){.headerlink} +: The value where no condition holds. + +(whenpaintnode)= + +### `WhenPaintNode` + +{.signature} +`interface WhenPaintNode` + +A conditional color: the color of the first case whose condition holds, else `otherwise`. + +{#whenpaintnode-when .member} +**`when`**`: [condition:` [`ExprInput`](#exprinput)`, color:` [`PaintInput`](#paintinput)`][]` [¶](#whenpaintnode-when){.headerlink} +: The cases, as `[condition, color]` pairs tried in order. + +{#whenpaintnode-otherwise .member} +**`otherwise`**`:` [`PaintInput`](#paintinput) [¶](#whenpaintnode-otherwise){.headerlink} +: The color where no condition holds. + +(isnode)= + +### `IsNode` + +{.signature} +`interface IsNode` + +Whether a text field equals a text exactly. + +{#isnode-is .member} +**`is`**`: [field:` [`ExprInput`](#exprinput)`, text: string]` [¶](#isnode-is){.headerlink} +: The text field, and the text it must equal. + +(containsnode)= + +### `ContainsNode` + +{.signature} +`interface ContainsNode` + +Whether a text field contains a text, ignoring case. In a mark, the field must belong to the +mark's own table. + +{#containsnode-contains .member} +**`contains`**`: [field:` [`ExprInput`](#exprinput)`, text: string]` [¶](#containsnode-contains){.headerlink} +: The text field, and the text it must contain. + +(scalenode)= + +### `ScaleNode` + +{.signature} +`interface ScaleNode` + +A value mapped linearly onto a range, clamped to the range. + +{#scalenode-scale .member} +**`scale`**`: [low: number, high: number]` [¶](#scalenode-scale){.headerlink} +: The range. + +{#scalenode-value .member} +**`value`**`:` [`ExprInput`](#exprinput) [¶](#scalenode-value){.headerlink} +: The value mapped. + +{#scalenode-domain .member} +**`domain`**`?: [low: number, high: number] | null` [¶](#scalenode-domain){.headerlink} +: The values mapped to the range's ends; the extent of the values shown when unset. + +(colormapnode)= + +### `ColormapNode` + +{.signature} +`interface ColormapNode` + +A value mapped through a colormap. + +{#colormapnode-colormap .member} +**`colormap`**`:` [`Colormap`](#colormap) [¶](#colormapnode-colormap){.headerlink} +: The colormap. + +{#colormapnode-value .member} +**`value`**`:` [`ExprInput`](#exprinput) [¶](#colormapnode-value){.headerlink} +: The value mapped. A categorical map takes each value's integer part as its category. + +{#colormapnode-domain .member} +**`domain`**`?: [low: number, high: number] | null` [¶](#colormapnode-domain){.headerlink} +: The values mapped to the colormap's ends; the extent of the values shown when unset. A + categorical map ignores it. + +(unary)= + +### `Unary` + +{.signature} +`type Unary = 'neg' | 'not' | 'abs' | 'sqrt' | 'sin' | 'cos' | 'floor' | 'ln' | 'exp'` + +An operation on one value. `sin` and `cos` take radians; `not` treats values above 0.5 as true. + +(binary)= + +### `Binary` + +{.signature} +`type Binary = 'add' | 'sub' | 'mul' | 'div' | 'rem' | 'pow' | 'min' | 'max' | 'lt' | 'le' | 'gt' | 'ge' | 'eq' | 'ne' | 'and' | 'or'` + +An operation on two values: arithmetic, `min` and `max`, comparisons (1 for true, 0 for false), +and logic. `rem` is a floored remainder, which takes the sign of the divisor. `and` and `or` +treat values above 0.5 as true. + +(state)= + +### `State` + +{.signature} +`type State = 'selected' | 'hovered' | 'pointer' | 'elapsed' | 'zoom'` + +View state of each row: `selected`, `hovered`, `pointer` (CSS pixels from a vertex to the +pointer), `elapsed` (seconds since the view was created), or `zoom` (the camera's zoom). -## Marks +(colormap)= -The marks that draw the tables of a scene. +### `Colormap` -### Mark - -```ts -/** One table a scene draws. In JSON: `{ vertices | edges | faces | lines | areas: options }`. */ -type Mark = - | { - /** Rows drawn as markers. */ - vertices: Vertices; - } - | { - /** Rows drawn as strokes between two vertices. */ - edges: Edges; - } - | { - /** Rows drawn as triangles between three vertices. */ - faces: Faces; - } - | { - /** Rows drawn as lines along their paths, or on a plot through their samples. */ - lines: Lines; - } - | { - /** Rows drawn as the areas their polygons cover. */ - areas: Areas; - }; -``` - -### Vertices - -```ts -/** Rows drawn as markers: points of a map, nodes of a graph, blocks of a diagram. */ -interface Vertices { - /** The dataset and table drawn. */ - of: [data: string, table: string]; - /** - * Each row's x and y. Without a position, the point field that `geometry` names places rows, and - * the scene's layout places rows that have neither. - */ - position?: [ExprInput, ExprInput] | null; - /** - * The point field that places rows when `position` is not set; the field named `geometry` by - * default. - */ - geometry?: string | null; - /** - * Height above the ground; on the earth, a fraction of the earth's radius. Default 0. Not allowed - * in a diagram, which is flat. - */ - elevation?: ExprInput; - /** - * Each row's radius, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given - * with `px` or `units`; a diagram refuses `px`. The theme's `vertex.radius` unless set. - */ - radius?: Size | null; - /** - * Each row's width and height, for markers that are not round, in the space's unit unless given - * with `px` or `units`; a diagram refuses `px`. In a diagram, a block without a size fits its - * label and ports. - */ - size?: [Size, Size] | null; - /** How each row is drawn; an ellipse by default. */ - marker?: Marker; - /** A ring around each row's marker in this color; a missing or transparent color draws none. */ - ring?: PaintInput | null; - /** - * Ports on each row, one for each reference field that names a net. An `edges` mark of the net - * table, without `ends`, draws the nets. Diagrams only. - */ - ports?: [reference: string, port: Port][]; - /** - * Makes each row a group containing the rows of `table` whose `reference` field names it. A group - * draws as a container around its members, with its label as a title in its header. Diagrams only. - */ - contains?: [table: string, reference: string] | null; - /** - * Whether each block or group casts a shadow on the sheet, styled by the theme's `diagram.shadow`. - * Default `false`. Diagrams only. - */ - shadow?: boolean; - /** - * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their - * corners' colors. - */ - color?: PaintInput | null; - /** Whether each row is drawn and can be picked. Default `true`. */ - visible?: ExprInput; - /** - * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are - * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are - * labeled first. - */ - priority?: ExprInput | null; - /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ - name?: string | null; - /** - * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces - * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. - */ - pick?: boolean | null; - /** Whether clicking a row selects it. Default `true`. */ - select?: boolean; - /** - * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the - * camera. Default `false`. - */ - drag?: boolean; - /** Each row's label: a text field, or label options. */ - label?: Label | null; - /** - * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap - * across its domain, a cell for each category, or a swatch beside each name of a map's - * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a - * conditional, `otherwise` is searched before the cases. - */ - legend?: string | null; -} -``` - -### Edges - -```ts -/** - * Rows drawn as strokes between the vertices named by two reference fields: branches, links, - * wires. In a diagram, a row without ends is a net, joining every vertex port whose reference - * field names it. - */ -interface Edges { - /** The dataset and table drawn. */ - of: [data: string, table: string]; - /** The reference fields naming each row's two vertices. */ - ends?: [from: string, to: string] | null; - /** A path field whose points each row passes through between its ends. */ - bends?: string | null; - /** - * How each row runs between its ends: geodesic on the earth and straight elsewhere unless set. - * `orthogonal` routes around blocks with rounded bends, in diagrams only. - */ - route?: Route | null; - /** - * Whether each row has an arrowhead at its second end; a net has one at each input it reaches. - * Default `false`. - */ - arrows?: boolean; - /** - * Draws each row as a pole from its own vertex down to the ground, on a plane or the earth. Poles - * take no `ends`, `bends`, or `joins`. Default `false`. - */ - poles?: boolean; - /** - * Draws each row as a net on a plane or the earth, joining the vertex rows of `table` whose - * `reference` field names it: a star from their center to each member. - */ - joins?: [table: string, reference: string] | null; - /** - * Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given - * with `px` or `units`. The theme's width unless set. - */ - width?: Size | null; - /** Whether each row is dashed, with the theme's `dash` period. Default `false`. */ - dash?: ExprInput; - /** - * The speed of comets flowing along each row, per second, in the unit of the theme's `flow` - * lengths. Negative speeds flow backward; 0, the default, draws no comets. When the speed changes, - * comets continue from where they are. - */ - flow?: ExprInput; - /** - * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their - * corners' colors. - */ - color?: PaintInput | null; - /** Whether each row is drawn and can be picked. Default `true`. */ - visible?: ExprInput; - /** - * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are - * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are - * labeled first. - */ - priority?: ExprInput | null; - /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ - name?: string | null; - /** - * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces - * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. - */ - pick?: boolean | null; - /** Whether clicking a row selects it. Default `true`. */ - select?: boolean; - /** - * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the - * camera. Default `false`. - */ - drag?: boolean; - /** Each row's label: a text field, or label options. */ - label?: Label | null; - /** - * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap - * across its domain, a cell for each category, or a swatch beside each name of a map's - * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a - * conditional, `otherwise` is searched before the cases. - */ - legend?: string | null; -} -``` - -### Lines - -```ts -/** - * Rows drawn as lines along a path field (roads, rivers, borders) or a polygon field's rings (area - * outlines). On a plot, each row instead traces a sampled field's history across the data's axis, - * as a monitor does. On the earth, paths follow the globe's curve and stay whole across the - * antimeridian. - */ -interface Lines { - /** The dataset and table drawn. */ - of: [data: string, table: string]; - /** - * The path or polygon field each row runs along; the field named `geometry` by default. Not - * allowed on a plot. - */ - geometry?: string | null; - /** - * The sampled field each row traces across the data's axis; plots only, where it is required. A - * field with no frames yet draws nothing until its first frame arrives. - */ - trace?: string | null; - /** How each row runs between its samples on a plot; `linear` by default. */ - interpolation?: Interpolation; - /** - * Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given - * with `px` or `units`. The theme's width unless set. - */ - width?: Size | null; - /** Whether each row is dashed, with the theme's `dash` period. Default `false`. */ - dash?: ExprInput; - /** - * The speed of comets flowing along each row, per second, in the unit of the theme's `flow` - * lengths. Negative speeds flow backward; 0, the default, draws no comets. When the speed changes, - * comets continue from where they are. - */ - flow?: ExprInput; - /** - * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their - * corners' colors. - */ - color?: PaintInput | null; - /** Whether each row is drawn and can be picked. Default `true`. */ - visible?: ExprInput; - /** - * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are - * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are - * labeled first. - */ - priority?: ExprInput | null; - /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ - name?: string | null; - /** - * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces - * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. - */ - pick?: boolean | null; - /** Whether clicking a row selects it. Default `true`. */ - select?: boolean; - /** - * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the - * camera. Default `false`. - */ - drag?: boolean; - /** Each row's label: a text field, or label options. */ - label?: Label | null; - /** - * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap - * across its domain, a cell for each category, or a swatch beside each name of a map's - * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a - * conditional, `otherwise` is searched before the cases. - */ - legend?: string | null; -} -``` - -### Areas - -```ts -/** - * Rows drawn as the areas of a polygon field: parcels, states, lakes, the land and water of a map. - * The view triangulates each row's polygons once, in the background when they are large. On the - * earth, areas follow the globe's curve and stay whole across the antimeridian. To outline areas, - * add a `Lines` mark of the same table, which draws their rings. - */ -interface Areas { - /** The dataset and table drawn. */ - of: [data: string, table: string]; - /** - * The polygon field each row fills, holding one polygon's rings or several polygons; the field - * named `geometry` by default. - */ - geometry?: string | null; - /** - * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their - * corners' colors. - */ - color?: PaintInput | null; - /** Whether each row is drawn and can be picked. Default `true`. */ - visible?: ExprInput; - /** - * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are - * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are - * labeled first. - */ - priority?: ExprInput | null; - /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ - name?: string | null; - /** - * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces - * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. - */ - pick?: boolean | null; - /** Whether clicking a row selects it. Default `true`. */ - select?: boolean; - /** - * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the - * camera. Default `false`. - */ - drag?: boolean; - /** Each row's label: a text field, or label options. */ - label?: Label | null; - /** - * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap - * across its domain, a cell for each category, or a swatch beside each name of a map's - * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a - * conditional, `otherwise` is searched before the cases. - */ - legend?: string | null; -} -``` - -### Faces - -```ts -/** - * Rows drawn as triangles between the vertices named by three reference fields: meshes, surfaces, - * and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen - * from above shows its exact colors. - */ -interface Faces { - /** The dataset and table drawn. */ - of: [data: string, table: string]; - /** - * The reference fields naming each row's three vertices; required. Without a color, a face blends - * the colors of its corners. - */ - corners?: [a: string, b: string, c: string] | null; - /** - * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their - * corners' colors. - */ - color?: PaintInput | null; - /** Whether each row is drawn and can be picked. Default `true`. */ - visible?: ExprInput; - /** - * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are - * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are - * labeled first. - */ - priority?: ExprInput | null; - /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ - name?: string | null; - /** - * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces - * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. - */ - pick?: boolean | null; - /** Whether clicking a row selects it. Default `true`. */ - select?: boolean; - /** - * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the - * camera. Default `false`. - */ - drag?: boolean; - /** Each row's label: a text field, or label options. */ - label?: Label | null; - /** - * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap - * across its domain, a cell for each category, or a swatch beside each name of a map's - * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a - * conditional, `otherwise` is searched before the cases. - */ - legend?: string | null; -} -``` - -### Marker - -```ts -/** - * How a vertex is drawn: a shape's name, `"block"`, or - * `{ shape | block | gauge | pie | icon | wgsl: options }`. An ellipse by default. - */ -type Marker = - | 'ellipse' - | 'rectangle' - | 'rounded' - | 'diamond' - | 'block' - | { - /** A filled shape. */ - shape: Shape; - } - | { - /** A rectangle with rounded corners that holds its label inside, as a diagram's blocks do. */ - block: { - /** - * Corner radius in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's - * `diagram.corner` unless set. - */ - corner?: number | null; - }; - } - | { - /** A shape's outline, filled to `fill` from 0 to 1: around a circle, or upward in other shapes. */ - gauge: { - /** How full each row's gauge is, from 0 to 1. */ - fill: ExprInput; - /** The outline's shape; an ellipse by default. */ - shape?: Shape; - }; - } - | { - /** A circle divided into slices sized in proportion to their values. */ - pie: { - /** Each slice's value. */ - slices: ExprInput[]; - /** Each slice's color. */ - colors: Color[]; - /** The hole in the middle, as a fraction of the radius; 0 by default. */ - hole?: number; - }; - } - | { - /** One of `images`, fitted to the marker's box; `index` picks the image for each row. */ - icon: { - /** The images. */ - images: Bitmap[]; - /** Each row's image index, from 0. */ - index: ExprInput; - }; - } - | { - /** - * A custom marker in WGSL. `source` defines `fn marker(f: MarkerFragment) -> MarkerColor`, which - * can read up to eight `inputs` as `f.input[i]`. It compiles once, when the view first draws it. - */ - wgsl: { - /** The WGSL source of the `marker` function. */ - source: string; - /** Each row's inputs, at most eight. */ - inputs?: ExprInput[]; - }; - }; -``` - -### Shape - -```ts -/** A marker's outline. */ -type Shape = 'ellipse' | 'rectangle' | 'rounded' | 'diamond'; -``` - -### Bitmap - -```ts -/** An image of RGBA texels: straight alpha, sRGB, rows top to bottom. */ -interface Bitmap { - /** Width and height in texels. */ - size: [number, number]; - /** `size[0] * size[1] * 4` bytes. */ - rgba: number[]; -} -``` - -### Port - -```ts -/** - * A port on one side of a vertex's marker, where a net connects: a side name, or options. The - * vertex names the net in a reference field. An output drives its net: layouts rank a net from its - * outputs, and the net's arrows point away from them. - */ -type Port = - | 'left' - | 'right' - | 'top' - | 'bottom' - | { - /** The side of the marker it sits on. */ - side: Side; - /** - * Its position among the ports on its side, counted from the top or left. Ports without an `order` - * follow those with one, in declaration order. - */ - order?: number | null; - /** Its name, drawn beside it inside the marker. */ - label?: string | null; - /** Its shape; an ellipse by default. */ - shape?: Shape; - /** Whether it is an output, which drives its net. Default `false`, an input. */ - output?: boolean; - }; -``` - -### Side - -```ts -/** A side of a marker. */ -type Side = 'left' | 'right' | 'top' | 'bottom'; -``` - -### Route - -```ts -/** How an edge runs between its ends. */ -type Route = 'straight' | 'geodesic' | 'orthogonal'; -``` - -### Interpolation - -```ts -/** How a line runs between its samples. */ -type Interpolation = 'linear' | 'stepBefore' | 'stepAfter'; -``` - -### Label - -```ts -/** - * Text for each row, placed beside, inside, or along the row's shape and following it every frame: - * a text field, or options. Where labels would overlap, the higher priority shows; among equals, - * labels already shown stay. In a diagram a label is part of the drawing: it scales with the zoom, - * fades out when too small to read, and shows wherever it fits its vertex. - */ -type Label = - | ExprInput - | { - /** The text field to show. */ - text: ExprInput; - /** - * Font size in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's - * `text.size` unless set. - */ - size?: number | null; - /** Each row's text color; the theme's text color unless set. */ - color?: PaintInput | null; - /** Where labels would overlap, the higher priority shows; the mark's `priority` unless set. */ - priority?: ExprInput | null; - /** The width at which lines wrap, in the space's unit (units in a diagram, CSS pixels elsewhere). */ - maxWidth?: number | null; - /** The CSS font family list for the text; the theme's `text.font` unless set. */ - font?: string | null; - /** - * The most labels shown at once: those of highest priority among the rows in view, or fewer where - * they would overlap. No limit unless set. - */ - max?: number | null; - }; -``` - -### Size - -```ts -/** - * A size: a value in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in CSS - * pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the - * view zooms in. - */ -type Size = ExprInput | Px | Units; -``` - -### Px - -```ts -/** A size in CSS pixels, regardless of the space's unit. */ -interface Px { - /** The size, in CSS pixels. */ - px: ExprInput; -} -``` - -### Units - -```ts -/** - * A size in position units (degrees on the earth), regardless of the space's unit, so it scales as - * the view zooms. `atLeast` and `atMost` bound it in CSS pixels. - */ -interface Units { - /** The size, in position units. */ - units: ExprInput; - /** The minimum on-screen size in CSS pixels, however far the view zooms out. */ - atLeast?: number | null; - /** The maximum on-screen size in CSS pixels, however far the view zooms in. */ - atMost?: number | null; -} -``` +{.signature} +`type Colormap =` [`ColormapName`](#colormapname) `|` [`CustomColormap`](#customcolormap) -## Expressions +Colors for a range of values: a catalog name, or your own colors. -Expressions as JSON: a constant, a field, or one of these nodes. Colors take CSS strings where values take field names. +{#colormap-colormapname .member} +[`ColormapName`](#colormapname) [¶](#colormap-colormapname){.headerlink} +: A colormap of the catalog. + +{#colormap-customcolormap .member} +[`CustomColormap`](#customcolormap) [¶](#colormap-customcolormap){.headerlink} +: A custom colormap: its kind, its colors, and for a categorical map, its category names. + +(colormapname)= + +### `ColormapName` + +{.signature} +`type ColormapName = 'amber' | 'amp' | 'batlow' | 'cividis' | 'crest' | 'deep' | 'devon' | 'flare' | 'grays' | 'haline' | 'inferno' | 'lajolla' | 'magma' | 'mako' | 'oslo' | 'plasma' | 'rocket' | 'thermal' | 'turku' | 'viridis' | 'balance' | 'berlin' | 'brbg' | 'broc' | 'coolwarm' | 'cork' | 'icefire' | 'managua' | 'piyg' | 'puor' | 'rdbu' | 'spectral' | 'vanimo' | 'vik' | 'vlag' | 'phase' | 'romao' | 'twilight' | 'dark2' | 'okabeito' | 'paired' | 'set2' | 'tab10' | 'tab20' | 'tableau10' | 'turbo'` + +A colormap of the catalog. + +(colormapkind)= + +### `ColormapKind` + +{.signature} +`type ColormapKind = 'sequential' | 'diverging' | 'cyclic' | 'categorical'` + +How a colormap maps values: `sequential` and `diverging` maps clamp, `cyclic` maps wrap, and +`categorical` maps give each integer its own color. + +{#colormapkind-sequential .member} +**`'sequential'`** [¶](#colormapkind-sequential){.headerlink} +: Low to high. + +{#colormapkind-diverging .member} +**`'diverging'`** [¶](#colormapkind-diverging){.headerlink} +: Below and above a midpoint. + +{#colormapkind-cyclic .member} +**`'cyclic'`** [¶](#colormapkind-cyclic){.headerlink} +: Around a circle: values wrap. -### ExprNode - -```ts -/** A value for each row: a number, a boolean, a field's name, or a node that computes one. */ -type ExprNode = - | number - | boolean - | string - | FieldNode - | ColorNode - | StateNode - | MissingNode - | UnaryNode - | BinaryNode - | WhenNode - | IsNode - | ContainsNode - | ScaleNode - | ColormapNode; -``` - -### PaintNode - -```ts -/** - * A color for each row: a CSS color, a value through a colormap, or a conditional choosing between - * colors. - */ -type PaintNode = Color | ColorNode | ColormapNode | WhenPaintNode | MissingNode; -``` - -### FieldNode - -```ts -/** - * A field's value in each row: from the mark's own table, or with `from`, from another dataset's - * table joined by row id. - */ -interface FieldNode { - /** The field's name. */ - field: string; - /** The dataset and table the field is read from, joined by row id. */ - from?: [data: string, table: string] | null; - /** - * For a sampled field, reads the last frame at or before the shown coordinate instead of - * interpolating. Default `false`. - */ - step?: boolean; -} -``` - -### ColorNode - -```ts -/** A constant color. */ -interface ColorNode { - /** A CSS color, such as `#ffb000` or `rebeccapurple`. */ - color: Color; -} -``` - -### StateNode - -```ts -/** View state of each row, such as whether it is selected. */ -interface StateNode { - state: State; -} -``` - -### MissingNode - -```ts -/** A missing value. An option that evaluates to missing uses the theme's value. */ -interface MissingNode { - missing: true; -} -``` - -### UnaryNode - -```ts -/** A one-argument operation, such as `{ "op": "abs", "args": ["x"] }`. */ -interface UnaryNode { - op: Unary; - args: [ExprInput]; -} -``` - -### BinaryNode - -```ts -/** A two-argument operation, such as `{ "op": "add", "args": ["a", 1] }`. */ -interface BinaryNode { - op: Binary; - args: [ExprInput, ExprInput]; -} -``` - -### WhenNode - -```ts -/** The value of the first case whose condition holds, else `otherwise`. */ -interface WhenNode { - /** The cases, as `[condition, value]` pairs tried in order. */ - when: [condition: ExprInput, value: ExprInput][]; - /** The value where no condition holds. */ - otherwise: ExprInput; -} -``` - -### WhenPaintNode - -```ts -/** A conditional color: the color of the first case whose condition holds, else `otherwise`. */ -interface WhenPaintNode { - /** The cases, as `[condition, color]` pairs tried in order. */ - when: [condition: ExprInput, color: PaintInput][]; - /** The color where no condition holds. */ - otherwise: PaintInput; -} -``` - -### IsNode - -```ts -/** Whether a text field equals the text exactly. */ -interface IsNode { - is: [field: ExprInput, text: string]; -} -``` - -### ContainsNode - -```ts -/** - * Whether a text field contains the text, ignoring case. In a mark, the field must belong to the - * mark's own table. - */ -interface ContainsNode { - contains: [field: ExprInput, text: string]; -} -``` - -### ScaleNode - -```ts -/** - * `value` mapped linearly from `domain` onto the range `scale`, clamped to the range. Without a - * `domain`, the extent of the values shown is used. - */ -interface ScaleNode { - scale: [low: number, high: number]; - value: ExprInput; - domain?: [low: number, high: number] | null; -} -``` - -### ColormapNode - -```ts -/** - * `value` mapped through `colormap` over `domain`, or without a `domain`, over the extent of the - * values shown. A categorical map ignores the domain and bins values by their integer part. - */ -interface ColormapNode { - colormap: Colormap; - value: ExprInput; - domain?: [low: number, high: number] | null; -} -``` - -### Unary - -```ts -/** An operation on one value. `sin` and `cos` take radians; `not` treats values above 0.5 as true. */ -type Unary = 'neg' | 'not' | 'abs' | 'sqrt' | 'sin' | 'cos' | 'floor' | 'ln' | 'exp'; -``` - -### Binary - -```ts -/** - * An operation on two values: arithmetic, `min` and `max`, comparisons (1 for true, 0 for false), - * and logic. `rem` is a floored remainder, which takes the sign of the divisor. `and` and `or` - * treat values above 0.5 as true. - */ -type Binary = - | 'add' - | 'sub' - | 'mul' - | 'div' - | 'rem' - | 'pow' - | 'min' - | 'max' - | 'lt' - | 'le' - | 'gt' - | 'ge' - | 'eq' - | 'ne' - | 'and' - | 'or'; -``` - -### State - -```ts -/** - * View state of each row: `selected`, `hovered`, `pointer` (CSS pixels from a vertex to the - * pointer), `elapsed` (seconds since the view was created), or `zoom` (the camera's zoom). - */ -type State = 'selected' | 'hovered' | 'pointer' | 'elapsed' | 'zoom'; -``` - -### Colormap - -```ts -/** Colors for a range of values: a catalog name, or your own colors. */ -type Colormap = ColormapName | CustomColormap; -``` - -### ColormapName - -```ts -/** A colormap of the catalog. */ -type ColormapName = - | 'amber' - | 'amp' - | 'batlow' - | 'cividis' - | 'crest' - | 'deep' - | 'devon' - | 'flare' - | 'grays' - | 'haline' - | 'inferno' - | 'lajolla' - | 'magma' - | 'mako' - | 'oslo' - | 'plasma' - | 'rocket' - | 'thermal' - | 'turku' - | 'viridis' - | 'balance' - | 'berlin' - | 'brbg' - | 'broc' - | 'coolwarm' - | 'cork' - | 'icefire' - | 'managua' - | 'piyg' - | 'puor' - | 'rdbu' - | 'spectral' - | 'vanimo' - | 'vik' - | 'vlag' - | 'phase' - | 'romao' - | 'twilight' - | 'dark2' - | 'okabeito' - | 'paired' - | 'set2' - | 'tab10' - | 'tab20' - | 'tableau10' - | 'turbo'; -``` - -### ColormapKind - -```ts -/** - * How a colormap maps values: `sequential` and `diverging` maps clamp, `cyclic` maps wrap, and - * `categorical` maps give each integer its own color. - */ -type ColormapKind = 'sequential' | 'diverging' | 'cyclic' | 'categorical'; -``` - -### CustomColormap - -```ts -/** A custom colormap: its kind, its colors, and for a categorical map, its category names. */ -interface CustomColormap { - kind: ColormapKind; - /** - * The colors, blended in OKLab. Each is a color, spaced evenly between its neighbors as in a CSS - * gradient, or a `[place, color]` pair with a place from 0 to 1, in ascending order. A categorical - * map takes one color per category, without places, at most 256. - */ - colors: (Color | [number, Color])[]; - /** The category names in category order, shown in legends. Categorical maps only. */ - categories?: string[]; -} -``` +{#colormapkind-categorical .member} +**`'categorical'`** [¶](#colormapkind-categorical){.headerlink} +: Distinct colors for distinct integers. + +(customcolormap)= + +### `CustomColormap` + +{.signature} +`interface CustomColormap` + +A custom colormap: its kind, its colors, and for a categorical map, its category names. + +{#customcolormap-kind .member} +**`kind`**`:` [`ColormapKind`](#colormapkind) [¶](#customcolormap-kind){.headerlink} +: How the colors map values. + +{#customcolormap-colors .member} +**`colors`**`: (`[`Color`](#color) `| [number,` [`Color`](#color)`])[]` [¶](#customcolormap-colors){.headerlink} +: The colors, blended in OKLab. Each is a color, spaced evenly between its neighbors as in a CSS + gradient, or a `[place, color]` pair with a place from 0 to 1, in ascending order. A categorical + map takes one color per category, without places, at most 256. + +{#customcolormap-categories .member} +**`categories`**`?: string[]` [¶](#customcolormap-categories){.headerlink} +: The category names in category order, shown in legends. Categorical maps only. ## Rows, cameras, and queries Identifying rows, framing them, and querying them. -### Id - -```ts -/** - * A row's id: a whole number from 0, or a text. A number above 2^53 - 1 is written as its decimal - * text, which names the same row. - */ -type Id = number | string; -``` - -### Ids - -```ts -/** - * Row ids: numbers or texts, or `{count}` for rows 0 to count - 1. In a reference, null names no - * row. - */ -type Ids = - | (Id | null)[] - | { - count: number; - }; -``` - -### Row - -```ts -/** - * One row, identified by dataset name, table name, and id. It stays valid across any change that - * keeps those names and the id. - */ -interface Row { - /** The dataset's name. */ - data: string; - /** The table's name. */ - table: string; - /** The row's id in the table. */ - id: Id; -} -``` - -### Rows - -```ts -/** - * Rows of one table in a dataset, by id. Views use it for selections, fits, and the rows events - * report. - */ -interface Rows { - /** The dataset's name. */ - data: string; - /** The table's name. */ - table: string; - /** The rows' ids in the table. */ - ids: Id[]; -} -``` - -### Camera - -```ts -/** Where a view looks. */ -type Camera = - | { - /** - * Longitude, latitude, and elevation on the earth; x, y, and elevation on a plane or a diagram. In - * JSON, `[x, y]` is shorthand for elevation 0. - */ - center: [number, number] | [number, number, number]; - /** The web mercator zoom level on the earth; elsewhere, log₂ of CSS pixels per unit. */ - zoom: number; - /** Tilt in degrees from looking straight down, up to 75. Ignored in a diagram. */ - pitch: number; - /** Degrees clockwise from north, or from up on a plane. Ignored in a diagram. */ - bearing: number; - } - | { - /** The x-axis range shown. */ - x: [number, number]; - /** The value range shown, or `null` to fit the values in view. */ - y?: [number, number] | null; - }; -``` - -### Fit - -```ts -/** The target of a view's fit. */ -type Fit = - | 'all' - | { - /** These rows. */ - rows: Rows[]; - } - | { - /** Brings this row into view, keeping the zoom. */ - reveal: Row; - } - | { - /** The box from `min` to `max`: x, y, and elevation. */ - bounds: { - /** The least corner. */ - min: [number, number, number]; - /** The greatest corner. */ - max: [number, number, number]; - }; - }; -``` - -### Query - -```ts -/** The rows of a table to read back, run through the expressions marks draw with. */ -interface Query { - /** The table to read. */ - of: string; - /** - * The rows to read, in this order unless sorted; every row when unset. Ids that name no row are - * skipped. - */ - ids?: Ids | null; - /** Keeps only the rows for which this is true. */ - filter?: ExprInput | null; - /** - * Keys to sort by, each breaking ties left by the keys before it. Rows where a key has no value - * sort last; rows equal on every key keep their order. - */ - sort?: [ExprInput, Order][]; - /** How many of the kept rows to skip. */ - offset?: number; - /** - * The most rows to return. When sorting with a limit, the first `offset + limit` rows are selected - * in linear time and only those are sorted. - */ - limit?: number | null; - /** - * Fields to return, by output name. A plain field returns that field as stored; any other - * expression returns numbers. Every field when unset; an empty object returns the ids alone. - */ - select?: { - [name: string]: ExprInput; - } | null; - /** The axis coordinate at which sampled fields are read; their last frame when unset. */ - at?: number | null; -} -``` - -### EnvelopeRequest - -```ts -/** - * A request for each row's least and greatest value of a sampled field over equal spans of the - * axis: field `field` of table `table`, over `spans` equal spans from `range[0]` to `range[1]`, - * for every row or for the rows `ids` names, in that order. Plain data that deserializes from - * JSON, like a query. - */ -interface EnvelopeRequest { - /** The table to read. */ - table: string; - /** The sampled field to read. */ - field: string; - /** The axis range the spans cover, `[first, last]`: finite, with `first <= last`. */ - range: [number, number]; - /** The number of equal spans to divide the range into; at least 1. */ - spans: number; - /** The rows to read, in this order; every row when unset. */ - ids?: Ids | null; -} -``` +(id)= + +### `Id` + +{.signature} +`type Id = number | string` + +A row's id: a whole number from 0, or a text. A number above 2^53 - 1 is written as its decimal +text, which names the same row. + +(ids)= + +### `Ids` + +{.signature} +`type Ids = (`[`Id`](#id) `| null)[] | { count: number }` + +Row ids: a list of numbers or texts, or `{count}` for rows 0 to count - 1. In a reference, null +names no row. + +{#ids-count .member} +`{` **`count`**`: number }` [¶](#ids-count){.headerlink} +: The number of rows, whose ids are 0 to count - 1. + +(row)= + +### `Row` + +{.signature} +`interface Row` + +One row, identified by dataset name, table name, and id. It stays valid across any change that +keeps those names and the id. + +{#row-data .member} +**`data`**`: string` [¶](#row-data){.headerlink} +: The dataset's name. + +{#row-table .member} +**`table`**`: string` [¶](#row-table){.headerlink} +: The table's name. + +{#row-id .member} +**`id`**`:` [`Id`](#id) [¶](#row-id){.headerlink} +: The row's id in the table. + +(rows)= + +### `Rows` + +{.signature} +`interface Rows` + +Rows of one table in a dataset, by id. Views use it for selections, fits, and the rows events +report. + +{#rows-data .member} +**`data`**`: string` [¶](#rows-data){.headerlink} +: The dataset's name. + +{#rows-table .member} +**`table`**`: string` [¶](#rows-table){.headerlink} +: The table's name. + +{#rows-ids .member} +**`ids`**`:` [`Id`](#id)`[]` [¶](#rows-ids){.headerlink} +: The rows' ids in the table. + +(camera)= + +### `Camera` + +{.signature} +`type Camera = { … } | { x: [number, number]; y?: [number, number] | null }` + +Where a view looks. + +A camera over the earth, a plane, or a diagram: the point it looks at and turns about, its zoom, +its tilt, and its rotation. A diagram is always seen from straight above, upright. + +{#camera-center .member} +**`center`**`: [number, number] | [number, number, number]` [¶](#camera-center){.headerlink} +: Longitude, latitude, and elevation on the earth; x, y, and elevation on a plane or a diagram. In + JSON, `[x, y]` is shorthand for elevation 0. + +{#camera-zoom .member} +**`zoom`**`: number` [¶](#camera-zoom){.headerlink} +: The web mercator zoom level on the earth; elsewhere, log₂ of CSS pixels per unit. + +{#camera-pitch .member} +**`pitch`**`: number` [¶](#camera-pitch){.headerlink} +: Tilt in degrees from looking straight down, up to 75. Ignored in a diagram. + +{#camera-bearing .member} +**`bearing`**`: number` [¶](#camera-bearing){.headerlink} +: Degrees clockwise from north, or from up on a plane. Ignored in a diagram. + +A plot camera: the x range shown, and the value range, or `null` to fit the values in view. + +{#camera-x .member} +**`x`**`: [number, number]` [¶](#camera-x){.headerlink} +: The x-axis range shown. + +{#camera-y .member} +**`y`**`?: [number, number] | null` [¶](#camera-y){.headerlink} +: The value range shown, or `null` to fit the values in view. + +(fit)= + +### `Fit` + +{.signature} +`type Fit = 'all' | { rows:` [`Rows`](#rows)`[] } | { reveal:` [`Row`](#row) `} | { bounds: { … } }` + +The target of a view's fit. + +{#fit-all .member} +**`'all'`** [¶](#fit-all){.headerlink} +: Everything drawn. + +{#fit-rows .member} +`{` **`rows`**`:` [`Rows`](#rows)`[] }` [¶](#fit-rows){.headerlink} +: These rows. + +{#fit-reveal .member} +`{` **`reveal`**`:` [`Row`](#row) `}` [¶](#fit-reveal){.headerlink} +: Brings this row into view, keeping the zoom. + +{#fit-bounds .member} +`{` **`bounds`**`: { min: [number, number, number]; max: [number, number, number] } }` [¶](#fit-bounds){.headerlink} +: The box from `min` to `max`: x, y, and elevation. + + {#fit-bounds-min .member} + **`min`**`: [number, number, number]` [¶](#fit-bounds-min){.headerlink} + : The least corner. + + {#fit-bounds-max .member} + **`max`**`: [number, number, number]` [¶](#fit-bounds-max){.headerlink} + : The greatest corner. + +(query)= + +### `Query` + +{.signature} +`interface Query` + +The rows of a table to read back, run through the expressions marks draw with. + +{#query-of .member} +**`of`**`: string` [¶](#query-of){.headerlink} +: The table to read. + +{#query-ids .member} +**`ids`**`?:` [`Ids`](#ids) `| null` [¶](#query-ids){.headerlink} +: The rows to read, in this order unless sorted; every row when unset. Ids that name no row are + skipped. + +{#query-filter .member} +**`filter`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#query-filter){.headerlink} +: Keeps only the rows for which this is true. + +{#query-sort .member} +**`sort`**`?: [`[`ExprInput`](#exprinput)`,` [`Order`](#order)`][]` [¶](#query-sort){.headerlink} +: Keys to sort by, each breaking ties left by the keys before it. Rows where a key has no value + sort last; rows equal on every key keep their order. + +{#query-offset .member} +**`offset`**`?: number` [¶](#query-offset){.headerlink} +: How many of the kept rows to skip. + +{#query-limit .member} +**`limit`**`?: number | null` [¶](#query-limit){.headerlink} +: The most rows to return. When sorting with a limit, the first `offset + limit` rows are selected + in linear time and only those are sorted. + +{#query-select .member} +**`select`**`?: { [name: string]:` [`ExprInput`](#exprinput) `} | null` [¶](#query-select){.headerlink} +: Fields to return, by output name. A plain field returns that field as stored; any other + expression returns numbers. Every field when unset; an empty object returns the ids alone. + +{#query-at .member} +**`at`**`?: number | null` [¶](#query-at){.headerlink} +: The axis coordinate at which sampled fields are read; their last frame when unset. + +(enveloperequest)= + +### `EnvelopeRequest` + +{.signature} +`interface EnvelopeRequest` + +A request for each row's least and greatest value of a sampled field over equal spans of the +axis: field `field` of table `table`, over `spans` equal spans from `range[0]` to `range[1]`, +for every row or for the rows `ids` names, in that order. Plain data that deserializes from +JSON, like a query. + +{#enveloperequest-table .member} +**`table`**`: string` [¶](#enveloperequest-table){.headerlink} +: The table to read. + +{#enveloperequest-field .member} +**`field`**`: string` [¶](#enveloperequest-field){.headerlink} +: The sampled field to read. + +{#enveloperequest-range .member} +**`range`**`: [number, number]` [¶](#enveloperequest-range){.headerlink} +: The axis range the spans cover, `[first, last]`: finite, with `first <= last`. + +{#enveloperequest-spans .member} +**`spans`**`: number` [¶](#enveloperequest-spans){.headerlink} +: The number of equal spans to divide the range into; at least 1. + +{#enveloperequest-ids .member} +**`ids`**`?:` [`Ids`](#ids) `| null` [¶](#enveloperequest-ids){.headerlink} +: The rows to read, in this order; every row when unset. ## Results View events and hits, dataset descriptions and values, and frame statistics. -### ViewEvent - -```ts -/** Something that happened in a view, as `view.on` reports it. */ -type ViewEvent = - | { - type: 'click'; - detail: { - /** The row. */ - hit: Hit; - /** How the user clicked it. */ - trigger: Trigger; - }; - } - | { - type: 'open'; - detail: { - /** The row. */ - hit: Hit; - /** How the user opened it. */ - trigger: Trigger; - }; - } - | { - type: 'menu'; - detail: { - /** The rows under the point, nearest first. */ - hits: Hit[]; - /** How the user asked. */ - trigger: Trigger; - }; - } - | { - type: 'drag'; - detail: { - /** Whether the drag starts, moves, or ends. */ - phase: Phase; - /** The row dragged, as it was hit. */ - hit: Hit; - /** - * The row's new position in the space's coordinates: the hit point moved with the pointer since - * the press, at the depth where the drag started, so a vertex keeps its grab offset. Snapped to a - * diagram's dots when its grid snaps. - */ - position: [number, number, number]; - /** The pointer. */ - trigger: Trigger; - }; - } - | { - type: 'connect'; - detail: { - /** The end it was drawn from: for an edge, the end that stayed. */ - from: End; - /** The vertex it was released over, if any. */ - to: End | null; - /** The edge whose end was dragged off; `to` replaces that end. */ - edge: Row | null; - /** Where it was released. */ - trigger: Trigger; - }; - } - | { - type: 'delete'; - detail: { - /** The rows selected. */ - rows: Rows[]; - /** The key. */ - trigger: Trigger; - }; - } - | { - type: 'hovered'; - detail: { - /** The row now hovered; `null` once no row is. */ - hit: Hit | null; - /** The pointer, or the keys. */ - trigger: Trigger; - }; - } - | { - type: 'selection'; - detail: { - /** The rows selected now. */ - rows: Rows[]; - /** How the user changed it; `null` when the data dropped rows from it. */ - trigger: Trigger | null; - }; - } - | { - type: 'camera'; - detail: Camera; - } - | { - type: 'waiting'; - detail: Waiting; - } - | { - type: 'error'; - detail: Failure; - }; -``` - -### Hit - -```ts -/** A row under a point, as the frame drew it. */ -interface Hit { - /** The row. */ - row: Row; - /** The name of the mark that drew it, if named. It tells two marks of one table apart. */ - mark?: string; - /** - * The row's point nearest the probe, in CSS pixels from the top left: a vertex's or port's middle, - * or the nearest point of a stroke or triangle. - */ - point: [number, number]; - /** That point in the space's coordinates: x, y, and elevation. */ - position: [number, number, number]; - /** CSS pixels from the probe to the row. */ - distance: number; - /** The port hit, when a vertex's port was. */ - port?: string; - /** The sample hit, when a line tracing a sampled field was. */ - sample?: Sample; -} -``` - -### End - -```ts -/** A vertex a connection ends at, and the port the connection meets, if any. */ -interface End { - /** The vertex. */ - row: Row; - /** The port, by the reference field naming its net. */ - port?: string; -} -``` - -### Failure - -```ts -/** - * What failed: its `code`, the message, and for invalid input the `path` to the part, such as - * `marks[2].vertices.color`. - */ -interface Failure { - code: 'invalid' | 'unsupported' | 'limit' | 'io' | 'device-lost' | 'cancelled' | 'timeout'; - message: string; - path?: string; -} -``` - -### Phase - -```ts -/** The phase of a `Drag`. */ -type Phase = 'start' | 'move' | 'end'; -``` - -### Trigger - -```ts -/** - * Where the user acted, with which modifier keys, and with which device. For keyboard input, the - * point is the row the keys point at, else the pointer, else the view's middle. - */ -interface Trigger { - /** CSS pixels from the top left. */ - point: [number, number]; - /** The modifier keys held. */ - modifiers: Modifiers; - /** The device used. */ - device: Device; -} -``` - -### Device - -```ts -/** The input device. */ -type Device = 'mouse' | 'pen' | 'touch' | 'keyboard'; -``` - -### Modifiers - -```ts -/** Modifier keys held during an input. */ -interface Modifiers { - /** Shift. */ - shift: boolean; - /** Control. */ - control: boolean; - /** Alt or Option. */ - alt: boolean; - /** Meta: Command or Windows. */ - meta: boolean; -} -``` - -### Description - -```ts -/** - * A summary of a dataset: its tables with their rows and fields, the axis sampled fields are - * recorded over, and the part of the axis they cover. - */ -interface Description { - /** The dataset's name. */ - name: string; - /** The axis name and unit, if set. */ - axis: [string, string] | null; - /** The first and last axis coordinates held by any sampled field; `null` when none holds any. */ - span: [number, number] | null; - /** Its tables, sorted by name. */ - tables: TableDescription[]; -} -``` - -### TableDescription - -```ts -/** A summary of one table: its row count and its fields, sorted by name. */ -interface TableDescription { - /** The table's name. */ - name: string; - /** The number of rows. */ - rows: number; - /** Its fields, sorted by name. */ - fields: FieldDescription[]; -} -``` - -### FieldDescription - -```ts -/** A summary of one field: its kind, plus its target table or axis span where those apply. */ -interface FieldDescription { - /** The field's name. */ - name: string; - /** The kind of values it holds. */ - kind: Kind; - /** For a reference, the target table. */ - to?: string; - /** For a sampled field, the first and last axis coordinates it holds. */ - span?: [number, number]; -} -``` - -### Kind - -```ts -/** The kind of values a field holds. */ -type Kind = - 'number' | 'bool' | 'text' | 'point' | 'path' | 'parts' | 'polygons' | 'reference' | 'sampled'; -``` - -### Value - -```ts -/** - * A value read from a dataset: a number (`null` when not finite), boolean, or text; a geometry as - * nested `[x, y]` points; a reference as the id of the row it names. - */ -type Value = number | boolean | string | null | [number, number] | unknown[][]; -``` - -### Sample - -```ts -/** A sample of a traced field: its frame, the axis coordinate it was recorded at, and its value. */ -interface Sample { - /** The frame, numbered from the first ever recorded. JSON uses a decimal string above 2^53 - 1. */ - frame: Id; - /** The axis coordinate. */ - at: number; - /** The value. */ - value: number; -} -``` - -### Stats - -```ts -/** Counters describing a view's frames. */ -interface Stats { - /** Frames rendered. */ - frames: number; - /** Rows of every mark's table. */ - rows: number; - /** - * Rows every mark draws: every row of a table drawn whole, and the rows of the tiles in view of a - * paged table. - */ - drawn: number; - /** Draw calls the last frame recorded, label draws included. */ - draws: number; - /** - * Instances the last frame's draws drew: points, segments, triangles, and glyphs. An upper bound - * for segments, which the GPU may cull. - */ - instances: number; - /** Fetched tiles the view draws, of every tile source. */ - tiles: number; - /** Tiles being fetched or decoded, of every tile source. */ - tilesLoading: number; - /** Milliseconds the last frame took to record on the CPU. */ - cpuMs: number; - /** - * Milliseconds the GPU takes per frame: the median of recently measured frames; 0 if the device - * cannot measure. - */ - gpuMs: number; -} -``` - -### Waiting - -```ts -/** - * What a frame still waits on: pipelines the scene draws with, which compile before the view draws - * anything new (`compiling`); pages uploading, or tables not yet built from them (`data`); - * layouts, routes, lanes, shapes, tiles, or pyramids (`derived`); label placement, or text not yet - * set (`labels`); a fit due or being measured (`fit`); segment counts, or a transition starting - * (`marks`). Empty once the frame is complete. - */ -type Waiting = ('compiling' | 'data' | 'derived' | 'labels' | 'fit' | 'marks')[]; -``` - -### Work - -```ts -/** - * Counts of the work a `Gpu` has done since it was created. Subtract two snapshots to get the work - * between them. - */ -interface Work { - /** Bytes of data pages uploaded. */ - uploaded: number; - /** - * Data pages, or pieces of pages, uploaded. A change's uploads count pages here and bytes in - * `uploaded`. - */ - uploads: number; - /** Bytes written other than pages: uniforms, programs, label runs, the selection, and textures. */ - written: number; - /** Rows whose channels the GPU evaluated. */ - evaluated: number; - /** - * Edge and face rows whose endpoints the GPU resolved to their vertices' resident rows: rows that - * changed, plus every row of a page that references a vertex now resident elsewhere. - */ - resolved: number; - /** - * Bytes of per-row word pages the GPU cleared: pages read before anything writes them, and the - * normals that faces sum from scratch. - */ - cleared: number; - /** - * Rows packed into pages of a paged table's draw order. Each page is packed once, the first time a - * view draws it, and again only when a write changes its rows. - */ - packed: number; - /** - * Readbacks from the GPU: probes, queries, fits, label placements, segment counts, and pictures. - * Frame timer readbacks, which only some devices support, are not counted. - */ - readbacks: number; - /** - * Jobs started: layouts, routes, lanes, triangulations, pyramids, and tiles. They run on the - * host's workers, or immediately without a host. - */ - jobs: number; - /** Label texts the host was asked to rasterize. */ - texts: number; - /** - * Pipelines compiled. In a browser, each compiles in the background when a view or shot requests - * it ahead of use. Natively, each compiles when first created. - */ - pipelines: number; - /** - * Pipelines not requested ahead of use, compiled when first used. In a browser they compile on the - * GPU's thread and delay frames. Zero unless a scene uses a pipeline it did not request. - */ - late: number; -} -``` +(viewevent)= + +### `ViewEvent` + +{.signature} +`type ViewEvent = { type: 'click'; detail: { … } } | { type: 'open'; detail: { … } } | { type: 'menu'; detail: { … } } | { type: 'drag'; detail: { … } } | { type: 'connect'; detail: { … } } | { type: 'delete'; detail: { … } } | { type: 'hovered'; detail: { … } } | { type: 'selection'; detail: { … } } | { type: 'camera'; detail:` [`Camera`](#camera) `} | { type: 'waiting'; detail:` [`Waiting`](#waiting) `} | { type: 'error'; detail:` [`Failure`](#failure) `}` + +Something that happened in a view, as [`view.on`](#view-on) reports it. + +{#viewevent-click .member} +`{ type:` **`'click'`**`; detail: { hit:` [`Hit`](#hit)`; trigger:` [`Trigger`](#trigger) `} }` [¶](#viewevent-click){.headerlink} +: A row was clicked. + + {#viewevent-click-hit .member} + **`hit`**`:` [`Hit`](#hit) [¶](#viewevent-click-hit){.headerlink} + : The row. + + {#viewevent-click-trigger .member} + **`trigger`**`:` [`Trigger`](#trigger) [¶](#viewevent-click-trigger){.headerlink} + : How the user clicked it. + +{#viewevent-open .member} +`{ type:` **`'open'`**`; detail: { hit:` [`Hit`](#hit)`; trigger:` [`Trigger`](#trigger) `} }` [¶](#viewevent-open){.headerlink} +: A row was double-clicked, or Enter was pressed on the row the keyboard points at. + + {#viewevent-open-hit .member} + **`hit`**`:` [`Hit`](#hit) [¶](#viewevent-open-hit){.headerlink} + : The row. + + {#viewevent-open-trigger .member} + **`trigger`**`:` [`Trigger`](#trigger) [¶](#viewevent-open-trigger){.headerlink} + : How the user opened it. + +{#viewevent-menu .member} +`{ type:` **`'menu'`**`; detail: { hits:` [`Hit`](#hit)`[]; trigger:` [`Trigger`](#trigger) `} }` [¶](#viewevent-menu){.headerlink} +: A context menu was requested: by a secondary click, the menu key, or a touch or pen held still. + Lists the rows under the trigger's point, for a menu of actions on them. + + {#viewevent-menu-hits .member} + **`hits`**`:` [`Hit`](#hit)`[]` [¶](#viewevent-menu-hits){.headerlink} + : The rows under the point, nearest first. + + {#viewevent-menu-trigger .member} + **`trigger`**`:` [`Trigger`](#trigger) [¶](#viewevent-menu-trigger){.headerlink} + : How the user asked. + +{#viewevent-drag .member} +`{ type:` **`'drag'`**`; detail: { phase:` [`Phase`](#phase)`; hit:` [`Hit`](#hit)`; position: [number, number, number]; trigger:` [`Trigger`](#trigger) `} }` [¶](#viewevent-drag){.headerlink} +: A row of a draggable mark is dragged. The drag starts once the pointer moves more than 4 CSS + pixels from where it pressed the row. + + {#viewevent-drag-phase .member} + **`phase`**`:` [`Phase`](#phase) [¶](#viewevent-drag-phase){.headerlink} + : Whether the drag starts, moves, or ends. + + {#viewevent-drag-hit .member} + **`hit`**`:` [`Hit`](#hit) [¶](#viewevent-drag-hit){.headerlink} + : The row dragged, as it was hit. + + {#viewevent-drag-position .member} + **`position`**`: [number, number, number]` [¶](#viewevent-drag-position){.headerlink} + : The row's new position in the space's coordinates: the hit point moved with the pointer since + the press, at the depth where the drag started, so a vertex keeps its grab offset. Snapped to a + diagram's dots when its grid snaps. + + {#viewevent-drag-trigger .member} + **`trigger`**`:` [`Trigger`](#trigger) [¶](#viewevent-drag-trigger){.headerlink} + : The pointer. + +{#viewevent-connect .member} +`{ type:` **`'connect'`**`; detail: { from:` [`End`](#end)`; to:` [`End`](#end) `| null; edge:` [`Row`](#row) `| null; trigger:` [`Trigger`](#trigger) `} }` [¶](#viewevent-connect){.headerlink} +: A connection was drawn with [`controls.connect`](#controls-connect) on: from a port, from a vertex with Alt held, or + by dragging an edge's end off it. + + {#viewevent-connect-from .member} + **`from`**`:` [`End`](#end) [¶](#viewevent-connect-from){.headerlink} + : The end it was drawn from: for an edge, the end that stayed. + + {#viewevent-connect-to .member} + **`to`**`:` [`End`](#end) `| null` [¶](#viewevent-connect-to){.headerlink} + : The vertex it was released over, if any. + + {#viewevent-connect-edge .member} + **`edge`**`:` [`Row`](#row) `| null` [¶](#viewevent-connect-edge){.headerlink} + : The edge whose end was dragged off; `to` replaces that end. + + {#viewevent-connect-trigger .member} + **`trigger`**`:` [`Trigger`](#trigger) [¶](#viewevent-connect-trigger){.headerlink} + : Where it was released. + +{#viewevent-delete .member} +`{ type:` **`'delete'`**`; detail: { rows:` [`Rows`](#rows)`[]; trigger:` [`Trigger`](#trigger) `} }` [¶](#viewevent-delete){.headerlink} +: Delete or Backspace was pressed with rows selected and [`controls.delete`](#controls-delete) on. + + {#viewevent-delete-rows .member} + **`rows`**`:` [`Rows`](#rows)`[]` [¶](#viewevent-delete-rows){.headerlink} + : The rows selected. + + {#viewevent-delete-trigger .member} + **`trigger`**`:` [`Trigger`](#trigger) [¶](#viewevent-delete-trigger){.headerlink} + : The key. + +{#viewevent-hovered .member} +`{ type:` **`'hovered'`**`; detail: { hit:` [`Hit`](#hit) `| null; trigger:` [`Trigger`](#trigger) `} }` [¶](#viewevent-hovered){.headerlink} +: The hovered row changed: [`view.hovered`](#view-hovered). + + {#viewevent-hovered-hit .member} + **`hit`**`:` [`Hit`](#hit) `| null` [¶](#viewevent-hovered-hit){.headerlink} + : The row now hovered; `null` once no row is. + + {#viewevent-hovered-trigger .member} + **`trigger`**`:` [`Trigger`](#trigger) [¶](#viewevent-hovered-trigger){.headerlink} + : The pointer, or the keys. + +{#viewevent-selection .member} +`{ type:` **`'selection'`**`; detail: { rows:` [`Rows`](#rows)`[]; trigger:` [`Trigger`](#trigger) `| null } }` [¶](#viewevent-selection){.headerlink} +: The selection changed: [`view.selection`](#view-selection). + + {#viewevent-selection-rows .member} + **`rows`**`:` [`Rows`](#rows)`[]` [¶](#viewevent-selection-rows){.headerlink} + : The rows selected now. + + {#viewevent-selection-trigger .member} + **`trigger`**`:` [`Trigger`](#trigger) `| null` [¶](#viewevent-selection-trigger){.headerlink} + : How the user changed it; `null` when the data dropped rows from it. + +{#viewevent-camera .member} +`{ type:` **`'camera'`**`; detail:` [`Camera`](#camera) `}` [¶](#viewevent-camera){.headerlink} +: The camera moved by a gesture, fit, flight, coast, or orbit: [`view.camera`](#view-camera). + +{#viewevent-waiting .member} +`{ type:` **`'waiting'`**`; detail:` [`Waiting`](#waiting) `}` [¶](#viewevent-waiting){.headerlink} +: The work the last frame waited on changed; empty once frames are complete: [`view.waiting`](#view-waiting). + +{#viewevent-error .member} +`{ type:` **`'error'`**`; detail:` [`Failure`](#failure) `}` [¶](#viewevent-error){.headerlink} +: Something failed; reported once. A mark that no longer fits its data is also listed in + [`view.failures`](#view-failures). + +(hit)= + +### `Hit` + +{.signature} +`interface Hit` + +A row under a point, as the frame drew it. + +{#hit-row .member} +**`row`**`:` [`Row`](#row) [¶](#hit-row){.headerlink} +: The row. + +{#hit-mark .member} +**`mark`**`?: string` [¶](#hit-mark){.headerlink} +: The name of the mark that drew it, if named. It tells two marks of one table apart. + +{#hit-point .member} +**`point`**`: [number, number]` [¶](#hit-point){.headerlink} +: The row's point nearest the probe, in CSS pixels from the top left: a vertex's or port's middle, + or the nearest point of a stroke or triangle. + +{#hit-position .member} +**`position`**`: [number, number, number]` [¶](#hit-position){.headerlink} +: That point in the space's coordinates: x, y, and elevation. + +{#hit-distance .member} +**`distance`**`: number` [¶](#hit-distance){.headerlink} +: CSS pixels from the probe to the row. + +{#hit-port .member} +**`port`**`?: string` [¶](#hit-port){.headerlink} +: The port hit, when a vertex's port was. + +{#hit-sample .member} +**`sample`**`?:` [`Sample`](#sample) [¶](#hit-sample){.headerlink} +: The sample hit, when a line tracing a sampled field was. + +(end)= + +### `End` + +{.signature} +`interface End` + +A vertex a connection ends at, and the port the connection meets, if any. + +{#end-row .member} +**`row`**`:` [`Row`](#row) [¶](#end-row){.headerlink} +: The vertex. + +{#end-port .member} +**`port`**`?: string` [¶](#end-port){.headerlink} +: The port, by the reference field naming its net. + +(failure)= + +### `Failure` + +{.signature} +`interface Failure` + +What failed, as JSON: the form of the npm package's [`LatkitError`](#latkiterror). + +{#failure-code .member} +**`code`**`: 'invalid' | 'unsupported' | 'limit' | 'io' | 'device-lost' | 'cancelled' | 'timeout'` [¶](#failure-code){.headerlink} +: The kind of failure, as [`LatkitError.code`](#latkiterror-code) describes it. + +{#failure-message .member} +**`message`**`: string` [¶](#failure-message){.headerlink} +: What failed, in words. + +{#failure-path .member} +**`path`**`?: string` [¶](#failure-path){.headerlink} +: The invalid part of the input, such as `marks[2].vertices.color`, on `invalid` failures. + +(phase)= + +### `Phase` + +{.signature} +`type Phase = 'start' | 'move' | 'end'` + +The phase of a `Drag`. + +{#phase-start .member} +**`'start'`** [¶](#phase-start){.headerlink} +: The pointer moved far enough to drag. + +{#phase-move .member} +**`'move'`** [¶](#phase-move){.headerlink} +: The pointer moved. + +{#phase-end .member} +**`'end'`** [¶](#phase-end){.headerlink} +: The pointer was released, or the drag was cancelled. + +(trigger)= + +### `Trigger` + +{.signature} +`interface Trigger` + +Where the user acted, with which modifier keys, and with which device. For keyboard input, the +point is the row the keys point at, else the pointer, else the view's middle. + +{#trigger-point .member} +**`point`**`: [number, number]` [¶](#trigger-point){.headerlink} +: CSS pixels from the top left. + +{#trigger-modifiers .member} +**`modifiers`**`:` [`Modifiers`](#modifiers) [¶](#trigger-modifiers){.headerlink} +: The modifier keys held. + +{#trigger-device .member} +**`device`**`:` [`Device`](#device) [¶](#trigger-device){.headerlink} +: The device used. + +(device)= + +### `Device` + +{.signature} +`type Device = 'mouse' | 'pen' | 'touch' | 'keyboard'` + +The input device. + +{#device-mouse .member} +**`'mouse'`** [¶](#device-mouse){.headerlink} +: A mouse or a trackpad. + +{#device-pen .member} +**`'pen'`** [¶](#device-pen){.headerlink} +: A pen. + +{#device-touch .member} +**`'touch'`** [¶](#device-touch){.headerlink} +: A finger: picks reach 8 CSS pixels farther. + +{#device-keyboard .member} +**`'keyboard'`** [¶](#device-keyboard){.headerlink} +: The keyboard. + +(modifiers)= + +### `Modifiers` + +{.signature} +`interface Modifiers` + +Modifier keys held during an input. + +{#modifiers-shift .member} +**`shift`**`: boolean` [¶](#modifiers-shift){.headerlink} +: Shift. + +{#modifiers-control .member} +**`control`**`: boolean` [¶](#modifiers-control){.headerlink} +: Control. + +{#modifiers-alt .member} +**`alt`**`: boolean` [¶](#modifiers-alt){.headerlink} +: Alt or Option. + +{#modifiers-meta .member} +**`meta`**`: boolean` [¶](#modifiers-meta){.headerlink} +: Meta: Command or Windows. + +(description)= + +### `Description` + +{.signature} +`interface Description` + +A summary of a dataset: its tables with their rows and fields, the axis sampled fields are +recorded over, and the part of the axis they cover. + +{#description-name .member} +**`name`**`: string` [¶](#description-name){.headerlink} +: The dataset's name. + +{#description-axis .member} +**`axis`**`: [string, string] | null` [¶](#description-axis){.headerlink} +: The axis name and unit, if set. + +{#description-span .member} +**`span`**`: [number, number] | null` [¶](#description-span){.headerlink} +: The first and last axis coordinates held by any sampled field; `null` when none holds any. + +{#description-tables .member} +**`tables`**`:` [`TableDescription`](#tabledescription)`[]` [¶](#description-tables){.headerlink} +: Its tables, sorted by name. + +(tabledescription)= + +### `TableDescription` + +{.signature} +`interface TableDescription` + +A summary of one table: its row count and its fields, sorted by name. + +{#tabledescription-name .member} +**`name`**`: string` [¶](#tabledescription-name){.headerlink} +: The table's name. + +{#tabledescription-rows .member} +**`rows`**`: number` [¶](#tabledescription-rows){.headerlink} +: The number of rows. + +{#tabledescription-fields .member} +**`fields`**`:` [`FieldDescription`](#fielddescription)`[]` [¶](#tabledescription-fields){.headerlink} +: Its fields, sorted by name. + +(fielddescription)= + +### `FieldDescription` + +{.signature} +`interface FieldDescription` + +A summary of one field: its kind, plus its target table or axis span where those apply. + +{#fielddescription-name .member} +**`name`**`: string` [¶](#fielddescription-name){.headerlink} +: The field's name. + +{#fielddescription-kind .member} +**`kind`**`:` [`Kind`](#kind) [¶](#fielddescription-kind){.headerlink} +: The kind of values it holds. + +{#fielddescription-to .member} +**`to`**`?: string` [¶](#fielddescription-to){.headerlink} +: For a reference, the target table. + +{#fielddescription-span .member} +**`span`**`?: [number, number]` [¶](#fielddescription-span){.headerlink} +: For a sampled field, the first and last axis coordinates it holds. + +(kind)= + +### `Kind` + +{.signature} +`type Kind = 'number' | 'bool' | 'text' | 'point' | 'path' | 'parts' | 'polygons' | 'reference' | 'sampled'` + +The kind of values a field holds. + +{#kind-number .member} +**`'number'`** [¶](#kind-number){.headerlink} +: Numbers. + +{#kind-bool .member} +**`'bool'`** [¶](#kind-bool){.headerlink} +: Booleans. + +{#kind-text .member} +**`'text'`** [¶](#kind-text){.headerlink} +: Texts. + +{#kind-point .member} +**`'point'`** [¶](#kind-point){.headerlink} +: `[x, y]` points. + +{#kind-path .member} +**`'path'`** [¶](#kind-path){.headerlink} +: Paths of points. + +{#kind-parts .member} +**`'parts'`** [¶](#kind-parts){.headerlink} +: Parts: the rings of a polygon, or the paths of a multi-path. + +{#kind-polygons .member} +**`'polygons'`** [¶](#kind-polygons){.headerlink} +: Polygons of rings. + +{#kind-reference .member} +**`'reference'`** [¶](#kind-reference){.headerlink} +: Rows of another table, by id. + +{#kind-sampled .member} +**`'sampled'`** [¶](#kind-sampled){.headerlink} +: Numbers sampled over the dataset's axis. + +(value)= + +### `Value` + +{.signature} +`type Value = number | boolean | string | null | [number, number] | unknown[][]` + +A value read from a dataset: a number (`null` when not finite), boolean, or text; a geometry as +nested `[x, y]` points; a reference as the id of the row it names. + +(sample)= + +### `Sample` + +{.signature} +`interface Sample` + +A sample of a traced field: its frame, the axis coordinate it was recorded at, and its value. + +{#sample-frame .member} +**`frame`**`:` [`Id`](#id) [¶](#sample-frame){.headerlink} +: The frame, numbered from the first ever recorded. JSON uses a decimal string above 2^53 - 1. + +{#sample-at .member} +**`at`**`: number` [¶](#sample-at){.headerlink} +: The axis coordinate. + +{#sample-value .member} +**`value`**`: number` [¶](#sample-value){.headerlink} +: The value. + +(stats)= + +### `Stats` + +{.signature} +`interface Stats` + +Counters describing a view's frames. + +{#stats-frames .member} +**`frames`**`: number` [¶](#stats-frames){.headerlink} +: Frames rendered. + +{#stats-rows .member} +**`rows`**`: number` [¶](#stats-rows){.headerlink} +: Rows of every mark's table. + +{#stats-drawn .member} +**`drawn`**`: number` [¶](#stats-drawn){.headerlink} +: Rows every mark draws: every row of a table drawn whole, and the rows of the tiles in view of a + paged table. + +{#stats-draws .member} +**`draws`**`: number` [¶](#stats-draws){.headerlink} +: Draw calls the last frame recorded, label draws included. + +{#stats-instances .member} +**`instances`**`: number` [¶](#stats-instances){.headerlink} +: Instances the last frame's draws drew: points, segments, triangles, and glyphs. An upper bound + for segments, which the GPU may cull. + +{#stats-tiles .member} +**`tiles`**`: number` [¶](#stats-tiles){.headerlink} +: Fetched tiles the view draws, of every tile source. + +{#stats-tilesloading .member} +**`tilesLoading`**`: number` [¶](#stats-tilesloading){.headerlink} +: Tiles being fetched or decoded, of every tile source. + +{#stats-cpums .member} +**`cpuMs`**`: number` [¶](#stats-cpums){.headerlink} +: Milliseconds the last frame took to record on the CPU. + +{#stats-gpums .member} +**`gpuMs`**`: number` [¶](#stats-gpums){.headerlink} +: Milliseconds the GPU takes per frame: the median of recently measured frames; 0 if the device + cannot measure. + +(waiting)= + +### `Waiting` + +{.signature} +`type Waiting = ('compiling' | 'data' | 'derived' | 'labels' | 'fit' | 'marks')[]` + +What a frame still waits on: pipelines the scene draws with, which compile before the view draws +anything new (`compiling`); pages uploading, or tables not yet built from them (`data`); +layouts, routes, lanes, shapes, tiles, or pyramids (`derived`); label placement, or text not yet +set (`labels`); a fit due or being measured (`fit`); segment counts, or a transition starting +(`marks`). Empty once the frame is complete. + +(work)= + +### `Work` + +{.signature} +`interface Work` + +Counts of the work a [`Gpu`](#gpu) has done since it was created. Subtract two snapshots to get the work +between them. + +{#work-uploaded .member} +**`uploaded`**`: number` [¶](#work-uploaded){.headerlink} +: Bytes of data pages uploaded. + +{#work-uploads .member} +**`uploads`**`: number` [¶](#work-uploads){.headerlink} +: Data pages, or pieces of pages, uploaded. A change's uploads count pages here and bytes in + `uploaded`. + +{#work-written .member} +**`written`**`: number` [¶](#work-written){.headerlink} +: Bytes written other than pages: uniforms, programs, label runs, the selection, and textures. + +{#work-evaluated .member} +**`evaluated`**`: number` [¶](#work-evaluated){.headerlink} +: Rows whose channels the GPU evaluated. + +{#work-resolved .member} +**`resolved`**`: number` [¶](#work-resolved){.headerlink} +: Edge and face rows whose endpoints the GPU resolved to their vertices' resident rows: rows that + changed, plus every row of a page that references a vertex now resident elsewhere. + +{#work-cleared .member} +**`cleared`**`: number` [¶](#work-cleared){.headerlink} +: Bytes of per-row word pages the GPU cleared: pages read before anything writes them, and the + normals that faces sum from scratch. + +{#work-packed .member} +**`packed`**`: number` [¶](#work-packed){.headerlink} +: Rows packed into pages of a paged table's draw order. Each page is packed once, the first time a + view draws it, and again only when a write changes its rows. + +{#work-readbacks .member} +**`readbacks`**`: number` [¶](#work-readbacks){.headerlink} +: Readbacks from the GPU: probes, queries, fits, label placements, segment counts, and pictures. + Frame timer readbacks, which only some devices support, are not counted. + +{#work-jobs .member} +**`jobs`**`: number` [¶](#work-jobs){.headerlink} +: Jobs started: layouts, routes, lanes, triangulations, pyramids, and tiles. They run on the + host's workers, or immediately without a host. + +{#work-texts .member} +**`texts`**`: number` [¶](#work-texts){.headerlink} +: Label texts the host was asked to rasterize. + +{#work-pipelines .member} +**`pipelines`**`: number` [¶](#work-pipelines){.headerlink} +: Pipelines compiled. In a browser, each compiles in the background when a view or shot requests + it ahead of use. Natively, each compiles when first created. + +{#work-late .member} +**`late`**`: number` [¶](#work-late){.headerlink} +: Pipelines not requested ahead of use, compiled when first used. In a browser they compile on the + GPU's thread and delay frames. Zero unless a scene uses a pipeline it did not request. diff --git a/npm/docs.mjs b/npm/docs.mjs index 69934a60..c497e898 100644 --- a/npm/docs.mjs +++ b/npm/docs.mjs @@ -1,7 +1,11 @@ /** - * Writes the JavaScript reference pages in `docs/reference` from the package's declarations, or - * with `--check` asserts that the checked-in pages are current. The pages are checked in so Read - * the Docs can build with Python alone. + * Writes the reference pages in `docs/reference` from the package's TypeScript: the JavaScript API + * from the shipped declarations, and the scene and result types from the types the schema writes. + * Each entry has a heading, its declaration, and its documentation, then its members or a union's + * forms: each a signature and its definition, with an anchor of its own. Types in signatures, and + * code spans in the documentation that name an entry or a member, link to it. With `--check`, + * asserts that the checked-in pages are current instead. The pages are checked in so Read the Docs + * can build them with Python alone. */ import assert from 'node:assert/strict'; import fs from 'node:fs'; @@ -11,7 +15,9 @@ import prettier from 'prettier'; const root = path.resolve(import.meta.dirname, '..'); const check = process.argv.includes('--check'); -const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); +const prettierConfig = await prettier.resolveConfig(path.join(root, 'npm/src/index.ts')); + +// Reading declarations /** * `text` with its first comment blanked out. Every module opens with a comment about itself, and @@ -24,298 +30,857 @@ function withoutModuleDoc(text) { return text.slice(0, first.pos) + ' '.repeat(first.end - first.pos) + text.slice(first.end); } -/** Maps each top-level declaration of `file` by name to its printed signature. */ -function declarations(file) { - const text = withoutModuleDoc(fs.readFileSync(path.join(root, file), 'utf8')); - const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true); +/** `text` with every comment blanked out and positions kept, so code read from it has none. */ +function uncommented(text) { + const scanner = ts.createScanner( + ts.ScriptTarget.Latest, + false, + ts.LanguageVariant.Standard, + text, + ); + let out = ''; + let at = 0; + for (let kind = scanner.scan(); kind !== ts.SyntaxKind.EndOfFileToken; kind = scanner.scan()) { + if ( + kind === ts.SyntaxKind.SingleLineCommentTrivia || + kind === ts.SyntaxKind.MultiLineCommentTrivia + ) { + const [start, end] = [scanner.getTokenStart(), scanner.getTokenEnd()]; + out += text.slice(at, start) + text.slice(start, end).replace(/[^\n]/g, ' '); + at = end; + } + } + return out + text.slice(at); +} + +/** + * The doc comment just before position `pos` of `text`, as Markdown; '' when there is none. It may + * sit on the line before, or on the same line, as before a union member: `| /** … *\/ { … }`. + */ +function docBefore(text, pos) { + const ranges = [ + ...(ts.getTrailingCommentRanges(text, pos) ?? []), + ...(ts.getLeadingCommentRanges(text, pos) ?? []), + ]; + const comment = ranges.filter((range) => text.startsWith('/**', range.pos)).at(-1); + if (!comment) return ''; + return text + .slice(comment.pos + 3, comment.end - 2) + .split('\n') + .map((line) => line.replace(/^\s*\* ?/, '')) + .join('\n') + .trim(); +} + +const has = (node, kind) => (ts.getModifiers?.(node) ?? []).some((m) => m.kind === kind); + +/** + * The declarations of `file`, by name. Each has a `kind` (function, const, class, interface, or + * type), its doc, and, by kind: its overloads, its members, or its union's branches. + */ +function read(file) { + const raw = withoutModuleDoc(fs.readFileSync(path.join(root, file), 'utf8')); + const source = ts.createSourceFile(file, raw, ts.ScriptTarget.Latest, true); + const code = uncommented(raw); + /** The node's code, without comments, on one line. */ + const text = (node) => code.slice(node.getStart(source), node.end).replace(/\s+/g, ' ').trim(); + const doc = (node) => docBefore(raw, node.pos); + /** A declaration's type parameters, in full or (`names`) by name only, as `<…>`. */ + const generics = (node, names = false) => + node.typeParameters + ? `<${node.typeParameters.map((p) => (names ? p.name.getText(source) : text(p))).join(', ')}>` + : ''; + + /** + * The members of a class, an interface, or an object type, in order. A getter and its setter are + * one property, and a method's overloads one method. A property of an object type lists that + * type's members as its `fields`. + */ + const members = (node) => { + const out = []; + const byName = new Map(); + for (const m of node.members) { + if (has(m, ts.SyntaxKind.PrivateKeyword) || has(m, ts.SyntaxKind.ProtectedKeyword)) continue; + if (m.name && ts.isPrivateIdentifier(m.name)) continue; + const about = doc(m); + if (/^@internal\b/.test(about)) continue; + const isStatic = has(m, ts.SyntaxKind.StaticKeyword); + let name = 'new'; + if (m.name) { + name = ts.isComputedPropertyName(m.name) + ? `[${m.name.expression.getText(source)}]` + : m.name.getText(source); + } + if (ts.isGetAccessor(m) || ts.isSetAccessor(m)) { + let entry = byName.get(name); + if (!entry) { + entry = { kind: 'property', name, doc: '', type: '', readonly: true, static: isStatic }; + byName.set(name, entry); + out.push(entry); + } + if (ts.isGetAccessor(m)) entry.type = text(m.type); + else { + entry.readonly = false; + entry.type ||= text(m.parameters[0].type); + } + entry.doc ||= about; + } else if (ts.isPropertyDeclaration(m) || ts.isPropertySignature(m)) { + out.push({ + kind: 'property', + name, + doc: about, + type: m.type ? text(m.type) : 'unknown', + optional: Boolean(m.questionToken), + readonly: has(m, ts.SyntaxKind.ReadonlyKeyword), + static: isStatic, + fields: m.type && ts.isTypeLiteralNode(m.type) ? members(m.type) : [], + }); + } else if ( + ts.isConstructorDeclaration(m) || + ts.isMethodDeclaration(m) || + ts.isMethodSignature(m) + ) { + const overload = { code: text(m), doc: about }; + const known = byName.get(`${name}()`); + if (known) { + known.overloads.push(overload); + known.doc ||= about; + continue; + } + const kind = ts.isConstructorDeclaration(m) ? 'constructor' : 'method'; + const entry = { kind, name, doc: about, overloads: [overload], static: isStatic }; + byName.set(`${name}()`, entry); + out.push(entry); + } else assert.fail(`${file}: ${text(m)} is a kind of member the reference cannot show`); + } + return out; + }; + + /** + * A union's branches, each with its doc, or the one type of a type that is no union. The first + * branch's doc may come before the union's leading `|`. + */ + const branches = (node) => + (ts.isUnionTypeNode(node) ? node.types : [node]).map((t, i) => { + const branch = { text: text(t), doc: doc(t) || (i === 0 ? doc(node) : '') }; + if (ts.isLiteralTypeNode(t)) return { ...branch, kind: 'literal' }; + if (ts.isTypeReferenceNode(t) && !t.typeArguments) + return { ...branch, kind: 'named', name: t.typeName.getText(source) }; + if (ts.isTypeLiteralNode(t)) return { ...branch, kind: 'object', members: members(t) }; + return { ...branch, kind: 'other' }; + }); + const found = new Map(); for (const node of source.statements) { if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node) || ts.isEmptyStatement(node)) continue; - const name = - node.name?.getText(source) ?? - (ts.isVariableStatement(node) - ? node.declarationList.declarations[0].name.getText(source) - : undefined); - if (!name) continue; - const signature = printer - .printNode(ts.EmitHint.Unspecified, node, source) - .replace(/\bexport declare /g, '') - .replace(/\bexport /g, '') - .replace(/^declare /gm, '') - .replace(/^\s*(?:#private;|private .*;)\s*$/gm, '') - .trim(); - // An overloaded function keeps every signature, in order. - found.set(name, found.has(name) ? `${found.get(name)}\n${signature}` : signature); + const about = doc(node); + if (ts.isFunctionDeclaration(node)) { + const name = node.name.getText(source); + const overload = { code: text(node).replace(/^export (declare )?/, ''), doc: about }; + const known = found.get(name); + if (known) { + known.overloads.push(overload); + known.doc ||= about; + } else found.set(name, { kind: 'function', name, doc: about, overloads: [overload] }); + } else if (ts.isVariableStatement(node)) { + // A constant holding a function reads as a function. + const variable = node.declarationList.declarations[0]; + const name = variable.name.getText(source); + const type = variable.type; + const callable = Boolean(type && ts.isFunctionTypeNode(type)); + const declaration = callable + ? `function ${name}${generics(type)}(${type.parameters.map(text).join(', ')}): ${text(type.type)}` + : `const ${name}: ${type ? text(type) : 'unknown'}`; + found.set(name, { + kind: callable ? 'function' : 'const', + name, + doc: about, + overloads: [{ code: declaration, doc: about }], + }); + } else if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) { + const name = node.name.getText(source); + found.set(name, { + kind: ts.isClassDeclaration(node) ? 'class' : 'interface', + name, + generics: generics(node), + parameters: generics(node, true), + doc: about, + heritage: (node.heritageClauses ?? []).map(text), + members: members(node), + }); + } else if (ts.isTypeAliasDeclaration(node)) { + const name = node.name.getText(source); + found.set(name, { + kind: 'type', + name, + generics: generics(node), + parameters: generics(node, true), + doc: about, + type: text(node.type), + branches: branches(node.type), + }); + } } return found; } +// Anchors and links + +/** An anchor for a reference entry or one of its members: lowercase words joined by hyphens. */ +const anchor = (...parts) => + parts + .join('-') + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); + /** - * Builds a reference page of the declarations in `files`: `head`, then each section of `sections` - * as `[title, description, names]`, with its declarations in the order listed. Throws if a - * declaration is in no section or a section names a declaration that does not exist. + * What each documented name links to: its anchor, how a link to it reads, and its doc. Anchors are + * labels for the whole site, so a link needs no page. */ -function page(head, files, sections) { - const all = new Map(files.flatMap((file) => [...declarations(file)])); - const listed = sections.flatMap(([, , names]) => names); - const unlisted = [...all.keys()].filter((name) => !listed.includes(name)); - const missing = listed.filter((name) => !all.has(name)); - assert.deepEqual(unlisted, [], `npm/docs.mjs lists no section for ${unlisted.join(', ')}`); - assert.deepEqual(missing, [], `npm/docs.mjs lists undeclared names: ${missing.join(', ')}`); - return ( - head + - sections - .map( - ([title, about, names]) => - `## ${title}\n\n${about}\n\n` + - names.map((name) => `### ${name}\n\n\`\`\`ts\n${all.get(name)}\n\`\`\`\n`).join('\n'), - ) - .join('\n') +const targets = new Map(); + +/** The Markdown link to `name`, an entry or `Owner.member`; undefined if the reference lacks it. */ +function link(name, label) { + const target = targets.get(name); + return target && `[${label ?? `\`${target.label}\``}](#${target.anchor})`; +} + +/** + * The entry or member a code span names, if the reference documents it: `Name`, `Name.member`, + * `instance.member` for a class's `Name.member`, or a call of one of these or of a function, + * `name()`. + */ +function named(span) { + const call = span.endsWith('()'); + const bare = call ? span.slice(0, -2) : span; + if (!/^[A-Za-z]\w*(\.\w+)?$/.test(bare)) return undefined; + const [owner, member] = bare.split('.'); + if (!member) return /^[A-Z]/.test(owner) || call ? owner : undefined; + return `${owner[0].toUpperCase()}${owner.slice(1)}.${member}`; +} + +/** + * `doc` with each `{@link Name}` made a link, and the first code span that names an entry or a + * member other than `self` made one too. Fenced code is left as it is. Throws for a `{@link}` to + * an unknown name. + */ +function linked(doc, self) { + const done = new Set([self]); + const prose = (text) => + text + .replace(/\{@link\s+([\w.$]+)(?:\s*\|\s*|\s+)?([^}]*)\}/g, (_, name, label) => { + const made = link(name, label.trim() || undefined); + assert(made, `{@link ${name}} names nothing the reference documents`); + done.add(name); + return made; + }) + .replace(/(? { + const name = named(code); + if (!name || done.has(name) || !targets.has(name)) return span; + done.add(name); + return link(name, span); + }); + return doc + .split(/(```[\s\S]*?```)/) + .map((part, i) => (i % 2 ? part : prose(part))) + .join(''); +} + +/** What lacks documentation, reported together once the pages are written. */ +const undocumented = []; + +/** Notes `what` as undocumented unless `doc` says something. */ +const documented = (doc, what) => doc || undocumented.push(what); + +/** The first sentence of `doc`, on one line. */ +const summary = (doc) => (doc.match(/^[\s\S]*?\.(?=\s|$)/)?.[0] ?? doc).replace(/\s+/g, ' '); + +// Signatures + +/** + * `code` on one line, as Prettier formats it, without a final semicolon. `code` is a declaration; + * with `as` set to `member`, a member of a class or an interface, and to `type`, a type. + */ +async function oneLine(code, as = 'declaration') { + const [before, after] = { + declaration: ['declare ', ''], + member: ['declare class Members {\n', '\n}'], + type: ['type Type = ', ''], + }[as]; + const out = await prettier.format(`${before}${code}${after}`, { + ...prettierConfig, + parser: 'typescript', + printWidth: 100_000, + }); + const lines = out.trim().split('\n'); + const body = as === 'member' ? lines.slice(1, -1) : lines; + assert.equal(body.length, 1, `${code} prints on more than one line`); + const line = body[0].trim().replace(/;$/, ''); + return as === 'member' ? line : line.slice(before.length); +} + +/** + * A code span of `text`, with the spaces at its ends outside it: Markdown would trim a span that + * starts and ends with a space. + */ +function codeSpan(text) { + const [, lead, inner, trail] = text.match(/^( *)(.*?)( *)$/); + if (!inner) return text; + const ticks = '`'.repeat(Math.max(0, ...(inner.match(/`+/g) ?? []).map((t) => t.length)) + 1); + const pad = /^`|`$/.test(inner) ? ' ' : ''; + return `${lead}${ticks}${pad}${inner}${pad}${ticks}${trail}`; +} + +/** `code` as code spans, with each documented type other than `self` a link. */ +function linkedCode(code, self) { + const scanner = ts.createScanner( + ts.ScriptTarget.Latest, + false, + ts.LanguageVariant.Standard, + code, ); + const out = []; + let at = 0; + for (let kind = scanner.scan(); kind !== ts.SyntaxKind.EndOfFileToken; kind = scanner.scan()) { + const word = scanner.getTokenText(); + if (kind !== ts.SyntaxKind.Identifier || !/^[A-Z]/.test(word) || word === self) continue; + const made = link(word, `\`${word}\``); + if (!made) continue; + out.push(codeSpan(code.slice(at, scanner.getTokenStart())), made); + at = scanner.getTokenEnd(); + } + out.push(codeSpan(code.slice(at))); + return out.join(''); +} + +/** + * A signature as Markdown: `code`, a one-line declaration, with each documented type other than + * `self` a link, and the name it declares, `code.slice(...name)`, in bold. + */ +function signature(code, self, name) { + if (!name) return linkedCode(code, self); + const [start, end] = name; + return [ + linkedCode(code.slice(0, start), self), + `**${codeSpan(code.slice(start, end))}**`, + linkedCode(code.slice(end), self), + ].join(''); +} + +/** Where the name begins in a member's one-line declaration: after its modifiers. */ +const nameAt = (code) => code.match(/^((static|readonly|abstract|override|async) )*/)[0].length; + +/** + * A union as its declaration shows it, on one line, its forms' details left to the items below + * it: an object of more than two keys reads `{ … }`, and so does an object type within a form. + */ +async function unionDeclaration(entry) { + const shown = entry.branches.map((b) => { + if (b.kind !== 'object') return b.text; + if (b.members.length > 2) return 'Hidden'; + const keys = b.members.map((m) => { + const type = m.fields?.length ? 'Hidden' : m.type; + return `${m.name}${m.optional ? '?' : ''}: ${type}`; + }); + return `{ ${keys.join('; ')} }`; + }); + const line = await oneLine(`type ${entry.name}${entry.generics} = ${shown.join(' | ')}`); + return line.replace(/\bHidden\b/g, '{ … }'); +} + +// Rendering + +/** Every anchor the pages define, to check that each is defined once. */ +const anchors = []; + +/** + * One item of a definition list: `term`, its permalink, and `definition`. An item with an anchor + * is a target for links. Lines after the first are indented to stay in the definition. + */ +function item(id, term, definition) { + if (id) anchors.push(id); + const attributes = id ? `{#${id} .member}` : '{.member}'; + const permalink = id ? ` [¶](#${id}){.headerlink}` : ''; + const lines = definition.trim().split('\n'); + const indented = [lines[0], ...lines.slice(1).map((l) => (l ? ` ${l}` : ''))].join('\n'); + return `${attributes}\n${term}${permalink}\n: ${indented}`; +} + +/** + * The items of `member` of entry `owner`, at `path` below it: one for each overload of a method, + * the first with the member's anchor, and one for a property, with an item for each of its fields + * in its definition. + */ +async function memberItems(owner, member, path = []) { + const id = anchor(owner, ...path, member.name); + const self = [owner, ...path, member.name].join('.'); + documented(member.doc, `${self} has no documentation`); + if (member.kind === 'property') { + const modifiers = `${member.static ? 'static ' : ''}${member.readonly ? 'readonly ' : ''}`; + const optional = member.optional ? '?' : ''; + const code = await oneLine(`${modifiers}${member.name}${optional}: ${member.type}`, 'member'); + const at = nameAt(code); + const fields = []; + for (const field of member.fields ?? []) + fields.push(...(await memberItems(owner, field, [...path, member.name]))); + const term = signature(code, owner, [at, at + member.name.length]); + return [item(id, term, [linked(member.doc, self), ...fields].join('\n\n'))]; + } + const items = []; + for (const [i, overload] of member.overloads.entries()) { + let code = await oneLine(overload.code, 'member'); + let name = [nameAt(code), nameAt(code) + member.name.length]; + if (member.kind === 'constructor') { + code = code.replace(/^constructor/, `new ${owner}`); + name = [0, `new ${owner}`.length]; + } + const term = signature(code, owner, name); + items.push(item(i === 0 ? id : undefined, term, linked(overload.doc || member.doc, self))); + } + return items; } -const pages = { - 'api.md': page( - '# JavaScript API\n\nGenerated from the shipped declarations. `load()` loads the WebAssembly that datasets, views,\nand pictures need. Scenes, expressions, and patches do not need it. Options use the [scene and\nresult types](types.md).\n\n', - ['load', 'internal', 'data', 'patch', 'scene', 'colormap', 'gpu', 'view', 'canvas', 'shot'].map( - (name) => `npm/dist/${name}.d.ts`, +/** The items of a class's or an interface's members: properties, then methods, each under a rubric. */ +async function membersOf(entry) { + const groups = [ + ['Constructor', entry.members.filter((m) => m.kind === 'constructor')], + ['Properties', entry.members.filter((m) => m.kind === 'property')], + ['Methods', entry.members.filter((m) => m.kind === 'method')], + ].filter(([, members]) => members.length); + const out = []; + for (const [title, members] of groups) { + if (groups.length > 1 || entry.kind === 'class') out.push(`\`\`\`{rubric} ${title}\n\`\`\``); + for (const member of members) out.push(...(await memberItems(entry.name, member))); + } + return out; +} + +/** + * The key an event union's branches share, as `type` in `{ type: 'name'; detail: … }`: every + * branch is an object with this literal key and at most one other. Undefined for other unions. + */ +function eventKey(entry) { + const objects = entry.branches.filter((b) => b.kind === 'object'); + if (objects.length !== entry.branches.length || objects.length < 2) return undefined; + const literal = (type) => /^(['"]).*\1$/.test(type); + return objects[0].members.find((m) => + objects.every( + (b) => b.members.length <= 2 && b.members.some((o) => o.name === m.name && literal(o.type)), ), + )?.name; +} + +/** + * The items of a union's branches: each documented value; each named type, with its summary; and + * each form. An event, `{ type: 'name'; detail: … }`, or a tagged form, `{ key: options }`, is one + * item, its fields in its definition; any other form lists its keys, after its doc when the union + * has several such forms. + */ +async function branchesOf(entry) { + const owner = entry.name; + const key = eventKey(entry); + const forms = entry.branches.filter((b) => b.kind === 'object' && !key && b.members.length > 1); + const out = []; + for (const branch of entry.branches) { + if (branch.kind === 'literal' && branch.doc) { + const term = signature(branch.text, owner, [0, branch.text.length]); + out.push(item(anchor(owner, branch.text), term, linked(branch.doc, owner))); + } else if (branch.kind === 'named' && targets.has(branch.name)) { + const about = branch.doc || summary(targets.get(branch.name).doc); + documented(about, `${owner} ${branch.name} has no documentation`); + out.push(item(anchor(owner, branch.name), link(branch.name), linked(about, owner))); + } else if (branch.kind === 'object' && (key || branch.members.length === 1)) { + const code = await oneLine(branch.text, 'type'); + const tag = key ? branch.members.find((m) => m.name === key).type : branch.members[0].name; + const payload = key ? branch.members.find((m) => m.name !== key) : branch.members[0]; + const name = key ? tag.slice(1, -1) : tag; + const about = branch.doc || (key ? '' : payload.doc); + documented(about, `${owner} ${tag} has no documentation`); + const fields = []; + for (const field of payload?.fields ?? []) + fields.push(...(await memberItems(owner, field, [name]))); + const at = code.indexOf(tag); + const term = signature(code, owner, [at, at + tag.length]); + out.push(item(anchor(owner, name), term, [linked(about, owner), ...fields].join('\n\n'))); + } else if (branch.kind === 'object') { + if (forms.length > 1) { + documented(branch.doc, `a form of ${owner} has no documentation`); + out.push(linked(branch.doc, owner)); + } + for (const member of branch.members) out.push(...(await memberItems(owner, member))); + } + } + return out; +} + +/** + * An entry's heading text: a function's name with parentheses, any other name with its type + * parameters' names. + */ +const heading = (entry) => + entry.kind === 'function' ? `${entry.name}()` : `${entry.name}${entry.parameters ?? ''}`; + +/** An entry's declaration: a line for each overload of a function, else one line. */ +async function declaration(entry) { + if (entry.overloads) return Promise.all(entry.overloads.map((o) => oneLine(o.code))); + if (entry.kind === 'type') { + if (entry.branches.length > 1) return [await unionDeclaration(entry)]; + return [await oneLine(`type ${entry.name}${entry.generics} = ${entry.type}`)]; + } + return [[`${entry.kind} ${entry.name}${entry.generics}`, ...entry.heritage].join(' ')]; +} + +/** An entry, as its page shows it. */ +async function render(entry) { + anchors.push(entry.anchor); + const lines = (await declaration(entry)).map((line) => signature(line, entry.name)); + documented(entry.doc, `${entry.name} has no documentation`); + const parts = [ + `(${entry.anchor})=`, + `### \`${heading(entry)}\``, + ...lines.map((line) => `{.signature}\n${line}`), + linked(entry.doc, entry.name), + ]; + if (entry.members) parts.push(...(await membersOf(entry))); + if (entry.branches && entry.branches.length > 1) parts.push(...(await branchesOf(entry))); + return parts.join('\n\n'); +} + +/** + * A reference page: `head`, then each section of `sections` as `[title, description, names]`, its + * entries in the order listed. + */ +async function page(head, entries, sections) { + const out = [linked(head)]; + for (const [title, about, names] of sections) { + out.push(`## ${title}`, linked(about)); + for (const name of names) out.push(await render(entries.get(name))); + } + return `${out.join('\n\n')}\n`; +} + +// The pages + +const API = [ + 'load', + 'internal', + 'data', + 'patch', + 'scene', + 'colormap', + 'gpu', + 'view', + 'canvas', + 'shot', +]; +const TYPES = ['inputs', 'outputs']; + +const sections = { + 'api.md': [ [ + 'Loading and waiting', + 'Load the WebAssembly once per page. Every wait ends when its signal aborts, after 30 seconds by default.', + ['load', 'LoadOptions', 'Wait', 'LatkitError'], + ], + [ + 'Data and patches', + 'Datasets of named tables, the patches that change them, written without WebAssembly, and the sources views draw.', [ - 'Loading and waiting', - 'Load the WebAssembly once per page. A wait ends when its signal aborts (30 s by default).', - ['load', 'LoadOptions', 'Wait', 'LatkitError'], - ], - [ - 'Data and patches', - 'Datasets of named tables, the patches that change them (written without WebAssembly), and the sources views draw.', - [ - 'Data', - 'Snapshot', - 'Tiles', - 'Source', - 'DataSource', - 'QueryResult', - 'Envelope', - 'Patch', - 'PatchSource', - 'RowIds', - 'Values', - 'Geometry', - 'Reference', - 'reference', - 'Fields', - 'ReadableBytes', - ], - ], - [ - 'Expressions', - 'Builders for the per-row expressions and colors that scene options take.', - [ - 'Expr', - 'ExprInput', - 'Mapped', - 'Missing', - 'Paint', - 'PaintInput', - 'Colormapped', - 'Color', - 'field', - 'color', - 'missing', - 'when', - 'When', - 'Then', - 'Next', - 'InputOf', - 'px', - 'units', - 'Within', - 'selected', - 'hovered', - 'pointer', - 'zoom', - 'elapsed', - 'icon', - 'RgbaImage', - 'colormapCss', - ], - ], - [ - 'Views', - 'A GPU makes views, and canvases that show them.', - [ - 'Gpu', - 'View', - 'ViewOptions', - 'ViewEvents', - 'Insets', - 'Point', - 'Position', - 'Canvas', - 'CanvasOptions', - 'Panel', - 'Rect', - ], + 'Data', + 'Snapshot', + 'Tiles', + 'Source', + 'DataSource', + 'QueryResult', + 'Envelope', + 'Patch', + 'PatchSource', + 'RowIds', + 'Values', + 'Geometry', + 'Reference', + 'reference', + 'Fields', + 'ReadableBytes', ], + ], + [ + 'Expressions', + 'Builders for the per-row expressions and colors that scene options take.', [ - 'Pictures and video', - 'Shots draw views into images and video.', - ['Shot', 'ShotOptions', 'Image', 'ImageFormat', 'Video'], + 'Expr', + 'ExprInput', + 'Mapped', + 'Missing', + 'Paint', + 'PaintInput', + 'Colormapped', + 'Color', + 'field', + 'color', + 'missing', + 'when', + 'When', + 'Then', + 'Next', + 'InputOf', + 'px', + 'units', + 'Within', + 'selected', + 'hovered', + 'pointer', + 'zoom', + 'elapsed', + 'icon', + 'RgbaImage', + 'colormapCss', ], ], - ), - 'types.md': page( - '# Scene and result types\n\nGenerated from the same schema as the package types. Each field documents its meaning and defaults.\n\n', - ['inputs', 'outputs'].map((name) => `npm/src/${name}.ts`), [ + 'Views', + 'A GPU makes views, and canvases that show them.', [ - 'Scenes', - 'A scene: its space, how it responds to input, and how it is laid out.', - [ - 'Scene', - 'Space', - 'Plane', - 'Earth', - 'Projection', - 'Diagram', - 'Plot', - 'Axis', - 'Tick', - 'Format', - 'Grid', - 'Controls', - 'Motion', - 'Zoom', - 'Layout', - 'Layered', - 'Stress', - 'Direction', - 'Order', - 'Budget', - ], + 'Gpu', + 'View', + 'ViewOptions', + 'ViewEvents', + 'Insets', + 'Point', + 'Position', + 'Canvas', + 'CanvasOptions', + 'Panel', + 'Rect', ], + ], + [ + 'Pictures and video', + 'Shots draw views into images and video.', + ['Shot', 'ShotOptions', 'Image', 'ImageFormat', 'Video'], + ], + ], + 'types.md': [ + [ + 'Scenes', + 'A scene: its space, how it responds to input, and how it is laid out.', [ - 'Themes', - 'How a scene looks, a group for each kind of thing it draws, and the lengths its options take.', - [ - 'Theme', - 'TextTheme', - 'VertexTheme', - 'EdgeTheme', - 'LineTheme', - 'AreaTheme', - 'FlowTheme', - 'FocusTheme', - 'DiagramTheme', - 'ShadowTheme', - 'EarthTheme', - 'PlotTheme', - 'Length', - ], + 'Scene', + 'Space', + 'Plane', + 'Earth', + 'Projection', + 'Diagram', + 'Plot', + 'Axis', + 'Tick', + 'Format', + 'Grid', + 'Controls', + 'Motion', + 'Zoom', + 'Layout', + 'Layered', + 'Stress', + 'Direction', + 'Order', + 'Budget', ], + ], + [ + 'Themes', + 'How a scene looks: a group for each kind of thing it draws, and the lengths its options take.', [ - 'Marks', - 'The marks that draw the tables of a scene.', - [ - 'Mark', - 'Vertices', - 'Edges', - 'Lines', - 'Areas', - 'Faces', - 'Marker', - 'Shape', - 'Bitmap', - 'Port', - 'Side', - 'Route', - 'Interpolation', - 'Label', - 'Size', - 'Px', - 'Units', - ], + 'Theme', + 'TextTheme', + 'VertexTheme', + 'EdgeTheme', + 'LineTheme', + 'AreaTheme', + 'FlowTheme', + 'FocusTheme', + 'DiagramTheme', + 'ShadowTheme', + 'EarthTheme', + 'PlotTheme', + 'Length', ], + ], + [ + 'Marks', + 'The marks that draw the tables of a scene.', [ - 'Expressions', - 'Expressions as JSON: a constant, a field, or one of these nodes. Colors take CSS strings where values take field names.', - [ - 'ExprNode', - 'PaintNode', - 'FieldNode', - 'ColorNode', - 'StateNode', - 'MissingNode', - 'UnaryNode', - 'BinaryNode', - 'WhenNode', - 'WhenPaintNode', - 'IsNode', - 'ContainsNode', - 'ScaleNode', - 'ColormapNode', - 'Unary', - 'Binary', - 'State', - 'Colormap', - 'ColormapName', - 'ColormapKind', - 'CustomColormap', - ], + 'Mark', + 'Vertices', + 'Edges', + 'Lines', + 'Areas', + 'Faces', + 'Marker', + 'Shape', + 'Bitmap', + 'Port', + 'Side', + 'Route', + 'Interpolation', + 'Label', + 'Size', + 'Px', + 'Units', ], + ], + [ + 'Expressions', + 'Expressions as JSON: a constant, a field, or one of these nodes. Colors take CSS strings where values take field names.', [ - 'Rows, cameras, and queries', - 'Identifying rows, framing them, and querying them.', - ['Id', 'Ids', 'Row', 'Rows', 'Camera', 'Fit', 'Query', 'EnvelopeRequest'], + 'ExprNode', + 'PaintNode', + 'FieldNode', + 'ColorNode', + 'StateNode', + 'MissingNode', + 'UnaryNode', + 'BinaryNode', + 'WhenNode', + 'WhenPaintNode', + 'IsNode', + 'ContainsNode', + 'ScaleNode', + 'ColormapNode', + 'Unary', + 'Binary', + 'State', + 'Colormap', + 'ColormapName', + 'ColormapKind', + 'CustomColormap', ], + ], + [ + 'Rows, cameras, and queries', + 'Identifying rows, framing them, and querying them.', + ['Id', 'Ids', 'Row', 'Rows', 'Camera', 'Fit', 'Query', 'EnvelopeRequest'], + ], + [ + 'Results', + 'View events and hits, dataset descriptions and values, and frame statistics.', [ - 'Results', - 'View events and hits, dataset descriptions and values, and frame statistics.', - [ - 'ViewEvent', - 'Hit', - 'End', - 'Failure', - 'Phase', - 'Trigger', - 'Device', - 'Modifiers', - 'Description', - 'TableDescription', - 'FieldDescription', - 'Kind', - 'Value', - 'Sample', - 'Stats', - 'Waiting', - 'Work', - ], + 'ViewEvent', + 'Hit', + 'End', + 'Failure', + 'Phase', + 'Trigger', + 'Device', + 'Modifiers', + 'Description', + 'TableDescription', + 'FieldDescription', + 'Kind', + 'Value', + 'Sample', + 'Stats', + 'Waiting', + 'Work', ], ], - ), + ], +}; + +const heads = { + 'api.md': `# JavaScript API + +Every class, function, and type the \`latkit\` package exports. \`load()\` loads the WebAssembly +that datasets, views, and pictures need; scenes, expressions, and patches need none. Scenes and +their options take the [scene and result types](types.md).`, + 'types.md': `# Scene and result types + +The types of the scenes latkit draws and of what it reports, written from the same schema as the +package's types. Each property gives its meaning and default.`, +}; + +/** The declarations of every file in `files`, read from `dir`. */ +const readAll = (files, dir, ext) => new Map(files.flatMap((f) => [...read(`${dir}/${f}${ext}`)])); +const entries = { + 'api.md': readAll(API, 'npm/dist', '.d.ts'), + 'types.md': readAll(TYPES, 'npm/src', '.ts'), }; -for (const [name, content] of Object.entries(pages)) { +// Anchors ignore case, so a builder named like a type on either page, such as `color()` beside +// `Color`, takes `-function` after its name. +const types = new Set( + Object.values(entries) + .flatMap((all) => [...all.values()]) + .filter((e) => e.kind !== 'function') + .map((e) => anchor(e.name)), +); +// Every declaration is listed in exactly one section, and documented on exactly one page. +for (const [name, all] of Object.entries(entries)) { + const listed = sections[name].flatMap(([, , names]) => names); + const unlisted = [...all.keys()].filter((n) => !listed.includes(n)); + const missing = listed.filter((n) => !all.has(n)); + assert.deepEqual(unlisted, [], `npm/docs.mjs lists no section for ${unlisted.join(', ')}`); + assert.deepEqual(missing, [], `npm/docs.mjs lists undeclared names: ${missing.join(', ')}`); + for (const entry of all.values()) { + assert(!targets.has(entry.name), `${entry.name} is documented twice`); + const collides = entry.kind === 'function' && types.has(anchor(entry.name)); + entry.anchor = collides ? anchor(entry.name, 'function') : anchor(entry.name); + const label = heading(entry).replace(/<.*>$/, ''); + targets.set(entry.name, { anchor: entry.anchor, label, doc: entry.doc }); + for (const member of entry.members ?? []) { + const prefix = entry.kind === 'class' ? `${entry.name}.` : ''; + const call = member.kind === 'method' ? '()' : ''; + targets.set(`${entry.name}.${member.name}`, { + anchor: anchor(entry.name, member.name), + label: `${prefix}${member.name}${call}`, + doc: member.doc, + }); + } + // A union's forms list their keys as members do, except an event's, which nest in the event. + const forms = entry.branches && !eventKey(entry) ? entry.branches : []; + for (const member of forms.flatMap((b) => (b.kind === 'object' ? b.members : []))) { + targets.set(`${entry.name}.${member.name}`, { + anchor: anchor(entry.name, member.name), + label: member.name, + doc: member.doc, + }); + } + } +} + +const written = new Map(); +for (const name of Object.keys(entries)) { const file = path.join(root, 'docs/reference', name); - const formatted = await prettier.format(content, { - ...(await prettier.resolveConfig(file)), - parser: 'markdown', - }); + const content = await page(heads[name], entries[name], sections[name]); // The reference must use npm's names. No line may contain a Rust path (`::`), and types.md, // which the schema writes from Rust's documentation, may not contain a Rust call (`name()`). const rust = name === 'types.md' ? /::|\w\(\)/ : /::/; - const leftover = formatted.split('\n').filter((line) => rust.test(line)); + const leftover = content.split('\n').filter((line) => rust.test(line)); assert.deepEqual(leftover, [], `${file}: Rust's names in the TypeScript reference`); + assert(!content.includes('{@link'), `${file}: a link left unresolved`); + written.set(file, content); +} +// Anchors are labels for the whole site, so each must be unique, and none may be a heading's slug. +const slugs = [ + ...Object.values(heads), + ...Object.values(sections) + .flat() + .map(([title]) => title), +].map((text) => anchor(text.split('\n')[0].replace(/^# /, ''))); +const twice = anchors.filter((a, i) => anchors.indexOf(a) !== i || slugs.includes(a)); +assert.deepEqual(twice, [], 'anchors used twice'); +assert.deepEqual(undocumented, [], 'every entry, member, and form documents itself'); +for (const [file, content] of written) { if (check) assert.equal( fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), - formatted, + content, `${file}: run node npm/docs.mjs after building the package`, ); else { fs.mkdirSync(path.dirname(file), { recursive: true }); - fs.writeFileSync(file, formatted); + fs.writeFileSync(file, content); } } diff --git a/npm/schema.json b/npm/schema.json index 26a2725c..d3096db9 100644 --- a/npm/schema.json +++ b/npm/schema.json @@ -731,7 +731,8 @@ "additionalProperties": false, "properties": { "state": { - "$ref": "#/$defs/State" + "$ref": "#/$defs/State", + "description": "The state read." } }, "required": [ @@ -756,6 +757,7 @@ "properties": { "missing": { "type": "boolean", + "description": "Always `true`.", "const": true } }, @@ -769,7 +771,8 @@ "additionalProperties": false, "properties": { "op": { - "$ref": "#/$defs/Unary" + "$ref": "#/$defs/Unary", + "description": "The operation." }, "args": { "type": "array", @@ -777,7 +780,8 @@ "$ref": "#/$defs/Expr" }, "minItems": 1, - "maxItems": 1 + "maxItems": 1, + "description": "Its argument." } }, "required": [ @@ -806,7 +810,8 @@ "additionalProperties": false, "properties": { "op": { - "$ref": "#/$defs/Binary" + "$ref": "#/$defs/Binary", + "description": "The operation." }, "args": { "type": "array", @@ -814,7 +819,8 @@ "$ref": "#/$defs/Expr" }, "minItems": 2, - "maxItems": 2 + "maxItems": 2, + "description": "Its arguments, left and right." } }, "required": [ @@ -894,13 +900,14 @@ ], "minItems": 2, "maxItems": 2, + "description": "The text field, and the text it must equal.", "tsType": "[field: ExprInput, text: string]" } }, "required": [ "is" ], - "description": "Whether a text field equals the text exactly." + "description": "Whether a text field equals a text exactly." }, "ContainsNode": { "type": "object", @@ -918,13 +925,14 @@ ], "minItems": 2, "maxItems": 2, + "description": "The text field, and the text it must contain.", "tsType": "[field: ExprInput, text: string]" } }, "required": [ "contains" ], - "description": "Whether a text field contains the text, ignoring case. In a mark, the field must belong to\nthe mark's own table." + "description": "Whether a text field contains a text, ignoring case. In a mark, the field must belong to the\nmark's own table." }, "ScaleNode": { "type": "object", @@ -938,10 +946,12 @@ }, "minItems": 2, "maxItems": 2, + "description": "The range.", "tsType": "[low: number, high: number]" }, "value": { - "$ref": "#/$defs/Expr" + "$ref": "#/$defs/Expr", + "description": "The value mapped." }, "domain": { "type": [ @@ -954,6 +964,7 @@ }, "minItems": 2, "maxItems": 2, + "description": "The values mapped to the range's ends; the extent of the values shown when unset.", "tsType": "[low: number, high: number]" } }, @@ -961,17 +972,19 @@ "scale", "value" ], - "description": "`value` mapped linearly from `domain` onto the range `scale`, clamped to the range. Without\na `domain`, the extent of the values shown is used." + "description": "A value mapped linearly onto a range, clamped to the range." }, "ColormapNode": { "type": "object", "additionalProperties": false, "properties": { "colormap": { - "$ref": "#/$defs/Colormap" + "$ref": "#/$defs/Colormap", + "description": "The colormap." }, "value": { - "$ref": "#/$defs/Expr" + "$ref": "#/$defs/Expr", + "description": "The value mapped. A categorical map takes each value's integer part as its category." }, "domain": { "type": [ @@ -984,6 +997,7 @@ }, "minItems": 2, "maxItems": 2, + "description": "The values mapped to the colormap's ends; the extent of the values shown when unset. A\ncategorical map ignores it.", "tsType": "[low: number, high: number]" } }, @@ -991,7 +1005,7 @@ "colormap", "value" ], - "description": "`value` mapped through `colormap` over `domain`, or without a `domain`, over the extent of\nthe values shown. A categorical map ignores the domain and bins values by their integer\npart." + "description": "A value mapped through a colormap." }, "Colormap": { "description": "Colors for a range of values: a catalog name, or your own colors.", @@ -1061,7 +1075,8 @@ "additionalProperties": false, "properties": { "kind": { - "$ref": "#/$defs/ColormapKind" + "$ref": "#/$defs/ColormapKind", + "description": "How the colors map values." }, "colors": { "type": "array", @@ -3208,7 +3223,7 @@ "description": "One change in a patch, keyed by the name of the `Patch` method that makes it." }, "Ids": { - "description": "Row ids: numbers or texts, or `{count}` for rows 0 to count - 1. In a reference, null names no row.", + "description": "Row ids: a list of numbers or texts, or `{count}` for rows 0 to count - 1. In a reference, null names no row.", "anyOf": [ { "type": "array", @@ -3227,6 +3242,7 @@ "type": "object", "properties": { "count": { + "description": "The number of rows, whose ids are 0 to count - 1.", "type": "integer", "minimum": 0, "maximum": 4294967295 @@ -4527,10 +4543,11 @@ } }, "Failure": { - "description": "What failed: its `code`, the message, and for invalid input the `path` to the part, such as `marks[2].vertices.color`.", + "description": "What failed, as JSON: the form of the npm package's `LatkitError`.", "type": "object", "properties": { "code": { + "description": "The kind of failure, as `LatkitError.code` describes it.", "enum": [ "invalid", "unsupported", @@ -4542,9 +4559,11 @@ ] }, "message": { + "description": "What failed, in words.", "type": "string" }, "path": { + "description": "The invalid part of the input, such as `marks[2].vertices.color`, on `invalid` failures.", "type": "string" } }, diff --git a/npm/schema.mjs b/npm/schema.mjs index 6877d09c..7689aca0 100644 --- a/npm/schema.mjs +++ b/npm/schema.mjs @@ -113,16 +113,20 @@ function declare(defs, prefix, roots, declared) { return 'unknown'; } }; - // A union member's description moves onto the single key of a tagged variant, because TypeScript - // shows no documentation on union members. + // A union member's description moves onto the single key of a tagged variant, where editors show + // it. Any other member keeps its description as a doc comment before it, which the reference + // pages read: what each value of an enumeration means, and each form of a type with several. const members = (schemas) => { const parts = schemas.map((s) => { const keys = Object.keys(s.properties ?? {}); - if (!s.description || keys.length !== 1 || s.properties[keys[0]].description) return type(s); - return type({ - ...s, - properties: { [keys[0]]: { ...s.properties[keys[0]], description: s.description } }, - }); + if (!s.description) return type(s); + if (keys.length === 1 && !s.properties[keys[0]].description) { + return type({ + ...s, + properties: { [keys[0]]: { ...s.properties[keys[0]], description: s.description } }, + }); + } + return `${doc(s.description).trimEnd()} ${type({ ...s, description: undefined })}`; }); return [...new Set(parts)].join(' | '); }; diff --git a/npm/src/canvas.ts b/npm/src/canvas.ts index 3fa29a63..9bfca606 100644 --- a/npm/src/canvas.ts +++ b/npm/src/canvas.ts @@ -10,6 +10,7 @@ export type Rect = readonly [x: number, y: number, width: number, height: number /** A view and the region of a canvas or picture it draws in. */ export interface Panel { + /** The view drawn. */ view: View; /** Where the view draws; the whole canvas or picture by default. */ at?: Rect; @@ -123,6 +124,7 @@ export class Canvas { this.#released(); } + /** Calls {@link Canvas.destroy}, so a `using` declaration destroys it when its block ends. */ [Symbol.dispose](): void { this.destroy(); } diff --git a/npm/src/data.ts b/npm/src/data.ts index e4620a90..02b57f40 100644 --- a/npm/src/data.ts +++ b/npm/src/data.ts @@ -7,14 +7,17 @@ import { load } from './load.js'; import type { Description, Value } from './outputs.js'; import { framed, type Patch } from './patch.js'; -/** - * The rows a query returns, as columns in the forms a patch takes: numbers as a `Float64Array` - * (missing values NaN), other values as arrays (missing `null`), and a reference as the ids of the - * rows it references. - */ +/** The rows a query returns, as columns in the forms a patch takes. */ export interface QueryResult { + /** The table read. */ table: string; + /** The ids of the rows returned, in order. */ ids: Id[]; + /** + * Each selected field's column, by output name: numbers as a `Float64Array` with NaN for missing + * values, other values as an array with `null` for missing ones, and a reference as the table it + * references and the id of the row each row references. + */ fields: Record; /** How many rows matched the query before `offset` and `limit` applied. */ total: number; @@ -86,6 +89,7 @@ export class Data { return wrap(Data, await waited(read, options, 'reading a dataset')); } + /** The dataset's name, by which a mark's `data` option draws it. */ get name(): string { return reads(this).name; } @@ -149,6 +153,7 @@ export class Data { release(this)?.free(); } + /** Calls {@link Data.destroy}, so a `using` declaration destroys it when its block ends. */ [Symbol.dispose](): void { this.destroy(); } @@ -161,6 +166,7 @@ export class Data { export class Snapshot { private constructor() {} + /** The name of the dataset this is a version of. */ get name(): string { return reads(this).name; } @@ -214,6 +220,7 @@ export class Snapshot { release(this)?.free(); } + /** Calls {@link Snapshot.destroy}, so a `using` declaration destroys it when its block ends. */ [Symbol.dispose](): void { this.destroy(); } @@ -234,11 +241,19 @@ export class Snapshot { * `layer:lines`, or `layer:points` holds that kind regardless of the marks. */ export class Tiles { + /** The name of the dataset the tiles are drawn as, by which a mark's `data` option draws it. */ + readonly name: string; + /** + * Where the tiles are: a `frames.json` manifest, a PMTiles archive, a TileJSON document, or a + * `{z}/{x}/{y}` URL template. + */ + readonly url: string; + /** Tiles at `url`, drawn as the dataset named `name`. */ - constructor( - readonly name: string, - readonly url: string, - ) {} + constructor(name: string, url: string) { + this.name = name; + this.url = url; + } } /** A dataset a view draws; views match it to the scene's marks by the dataset's name. */ diff --git a/npm/src/gpu.ts b/npm/src/gpu.ts index f5be8a8f..c367c825 100644 --- a/npm/src/gpu.ts +++ b/npm/src/gpu.ts @@ -148,6 +148,7 @@ export class Gpu { device.free(); } + /** Calls {@link Gpu.destroy}, so a `using` declaration destroys it when its block ends. */ [Symbol.dispose](): void { this.destroy(); } diff --git a/npm/src/inputs.ts b/npm/src/inputs.ts index 46a3e20c..9901a6a9 100644 --- a/npm/src/inputs.ts +++ b/npm/src/inputs.ts @@ -107,7 +107,9 @@ export type Binary = /** A two-argument operation, such as `{ "op": "add", "args": ["a", 1] }`. */ export interface BinaryNode { + /** The operation. */ op: Binary; + /** Its arguments, left and right. */ args: [ExprInput, ExprInput]; } @@ -135,7 +137,10 @@ export interface Budget { /** Where a view looks. */ export type Camera = - | { + /** + * A camera over the earth, a plane, or a diagram: the point it looks at and turns about, its zoom, + * its tilt, and its rotation. A diagram is always seen from straight above, upright. + */ | { /** * Longitude, latitude, and elevation on the earth; x, y, and elevation on a plane or a diagram. In * JSON, `[x, y]` is shorthand for elevation 0. @@ -148,7 +153,7 @@ export type Camera = /** Degrees clockwise from north, or from up on a plane. Ignored in a diagram. */ bearing: number; } - | { + | /** A plot camera: the x range shown, and the value range, or `null` to fit the values in view. */ { /** The x-axis range shown. */ x: [number, number]; /** The value range shown, or `null` to fit the values in view. */ @@ -168,7 +173,11 @@ export type Colormap = ColormapName | CustomColormap; * How a colormap maps values: `sequential` and `diverging` maps clamp, `cyclic` maps wrap, and * `categorical` maps give each integer its own color. */ -export type ColormapKind = 'sequential' | 'diverging' | 'cyclic' | 'categorical'; +export type ColormapKind = + | /** Low to high. */ 'sequential' + | /** Below and above a midpoint. */ 'diverging' + | /** Around a circle: values wrap. */ 'cyclic' + | /** Distinct colors for distinct integers. */ 'categorical'; /** A colormap of the catalog. */ export type ColormapName = @@ -219,21 +228,25 @@ export type ColormapName = | 'tableau10' | 'turbo'; -/** - * `value` mapped through `colormap` over `domain`, or without a `domain`, over the extent of the - * values shown. A categorical map ignores the domain and bins values by their integer part. - */ +/** A value mapped through a colormap. */ export interface ColormapNode { + /** The colormap. */ colormap: Colormap; + /** The value mapped. A categorical map takes each value's integer part as its category. */ value: ExprInput; + /** + * The values mapped to the colormap's ends; the extent of the values shown when unset. A + * categorical map ignores it. + */ domain?: [low: number, high: number] | null; } /** - * Whether a text field contains the text, ignoring case. In a mark, the field must belong to the + * Whether a text field contains a text, ignoring case. In a mark, the field must belong to the * mark's own table. */ export interface ContainsNode { + /** The text field, and the text it must contain. */ contains: [field: ExprInput, text: string]; } @@ -309,13 +322,14 @@ export type Controls = /** A custom colormap: its kind, its colors, and for a categorical map, its category names. */ export interface CustomColormap { + /** How the colors map values. */ kind: ColormapKind; /** * The colors, blended in OKLab. Each is a color, spaced evenly between its neighbors as in a CSS * gradient, or a `[place, color]` pair with a place from 0 to 1, in ascending order. A categorical * map takes one color per category, without places, at most 256. */ - colors: (Color | [number, Color])[]; + colors: (/** A CSS color, such as `#ffb000` or `rebeccapurple`. */ Color | [number, Color])[]; /** The category names in category order, shown in legends. Categorical maps only. */ categories?: string[]; } @@ -352,7 +366,11 @@ export interface DiagramTheme { } /** A direction on screen. */ -export type Direction = 'right' | 'left' | 'down' | 'up'; +export type Direction = + | /** Toward the right. */ 'right' + | /** Toward the left. */ 'left' + | /** Toward the bottom. */ 'down' + | /** Toward the top. */ 'up'; /** * The earth: positions are longitude and latitude in degrees. The ground takes the theme's `earth` @@ -519,7 +537,7 @@ export interface EnvelopeRequest { export type ExprNode = | number | boolean - | string + | /** A field's name. */ string | FieldNode | ColorNode | StateNode @@ -601,7 +619,7 @@ export interface FieldNode { /** The target of a view's fit. */ export type Fit = - | 'all' + | /** Everything drawn. */ 'all' | { /** These rows. */ rows: Rows[]; @@ -658,11 +676,15 @@ export interface FocusTheme { } /** How an axis formats its tick values. */ -export type Format = 'auto' | 'fixed' | 'scientific' | 'engineering'; +export type Format = + | /** Fixed decimals, scientific past a million or below a ten-thousandth. */ 'auto' + | /** Fixed decimals: `1500.0`. */ 'fixed' + | /** A significand and a power of ten: `1.5e3`. */ 'scientific' + | /** A significand and a power of ten that is a multiple of three: `1.5e3`, `150e-6`. */ 'engineering'; /** A plane's or a diagram's grid: `"off"`, `{ lines: { pitch } }`, or `{ dots: { pitch, snap } }`. */ export type Grid = - | 'off' + | /** No grid. */ 'off' | { /** Lines `pitch` units apart, thinned by factors of ten as the view zooms out. */ lines: { @@ -687,16 +709,28 @@ export type Grid = export type Id = number | string; /** - * Row ids: numbers or texts, or `{count}` for rows 0 to count - 1. In a reference, null names no - * row. + * Row ids: a list of numbers or texts, or `{count}` for rows 0 to count - 1. In a reference, null + * names no row. */ -export type Ids = (Id | null)[] | { count: number }; +export type Ids = + | (Id | null)[] + | { + /** The number of rows, whose ids are 0 to count - 1. */ + count: number; + }; /** How a line runs between its samples. */ -export type Interpolation = 'linear' | 'stepBefore' | 'stepAfter'; - -/** Whether a text field equals the text exactly. */ +export type Interpolation = + | /** Straight from each sample to the next. */ 'linear' + | /** + * Each value holds from the previous sample up to its own, so the line steps at the previous + * sample. + */ 'stepBefore' + | /** Each value holds until the next sample, where the line steps. */ 'stepAfter'; + +/** Whether a text field equals a text exactly. */ export interface IsNode { + /** The text field, and the text it must equal. */ is: [field: ExprInput, text: string]; } @@ -953,20 +987,32 @@ export type Marker = /** A missing value. An option that evaluates to missing uses the theme's value. */ export interface MissingNode { + /** Always `true`. */ missing: true; } /** How much a view animates on its own. */ -export type Motion = 'system' | 'full' | 'reduced'; +export type Motion = + | /** Follows the system: reduced when the user asked their system for less motion. */ 'system' + | /** All motion. */ 'full' + | /** + * Only motion the user drives: changes apply at once, flights jump, nothing coasts or orbits, + * labels appear without fading, and comets and options that read `elapsed` stay still. + */ 'reduced'; /** Sort direction. Rows where the key has no value sort last in either direction. */ -export type Order = 'ascending' | 'descending'; +export type Order = /** Smallest first. */ 'ascending' | /** Largest first. */ 'descending'; /** * A color for each row: a CSS color, a value through a colormap, or a conditional choosing between * colors. */ -export type PaintNode = Color | ColorNode | ColormapNode | WhenPaintNode | MissingNode; +export type PaintNode = + | /** A CSS color, such as `#ffb000` or `rebeccapurple`. */ Color + | ColorNode + | ColormapNode + | WhenPaintNode + | MissingNode; /** A plane: positions are in plane units. */ export interface Plane { @@ -1015,7 +1061,7 @@ export type Port = }; /** How the earth is projected. */ -export type Projection = 'mercator' | 'globe'; +export type Projection = /** Web mercator. */ 'mercator' | /** A sphere. */ 'globe'; /** A size in CSS pixels, regardless of the space's unit. */ export interface Px { @@ -1056,7 +1102,13 @@ export interface Query { } /** How an edge runs between its ends. */ -export type Route = 'straight' | 'geodesic' | 'orthogonal'; +export type Route = + | /** A straight line on screen. */ 'straight' + | /** Along the great circle, on the earth. */ 'geodesic' + | /** + * Horizontal and vertical runs around vertices that leave and enter at their sides, with rounded + * bends. Diagrams only. + */ 'orthogonal'; /** * One row, identified by dataset name, table name, and id. It stays valid across any change that @@ -1084,13 +1136,13 @@ export interface Rows { ids: Id[]; } -/** - * `value` mapped linearly from `domain` onto the range `scale`, clamped to the range. Without a - * `domain`, the extent of the values shown is used. - */ +/** A value mapped linearly onto a range, clamped to the range. */ export interface ScaleNode { + /** The range. */ scale: [low: number, high: number]; + /** The value mapped. */ value: ExprInput; + /** The values mapped to the range's ends; the extent of the values shown when unset. */ domain?: [low: number, high: number] | null; } @@ -1132,10 +1184,18 @@ export interface ShadowTheme { } /** A marker's outline. */ -export type Shape = 'ellipse' | 'rectangle' | 'rounded' | 'diamond'; +export type Shape = + | /** An ellipse: a circle when its width and height are equal. */ 'ellipse' + | /** A rectangle. */ 'rectangle' + | /** A rectangle with rounded corners. */ 'rounded' + | /** A diamond. */ 'diamond'; /** A side of a marker. */ -export type Side = 'left' | 'right' | 'top' | 'bottom'; +export type Side = + | /** The left side. */ 'left' + | /** The right side. */ 'right' + | /** The top. */ 'top' + | /** The bottom. */ 'bottom'; /** * A size: a value in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in CSS @@ -1182,6 +1242,7 @@ export type State = 'selected' | 'hovered' | 'pointer' | 'elapsed' | 'zoom'; /** View state of each row, such as whether it is selected. */ export interface StateNode { + /** The state read. */ state: State; } @@ -1261,7 +1322,9 @@ export type Unary = 'neg' | 'not' | 'abs' | 'sqrt' | 'sin' | 'cos' | 'floor' | ' /** A one-argument operation, such as `{ "op": "abs", "args": ["x"] }`. */ export interface UnaryNode { + /** The operation. */ op: Unary; + /** Its argument. */ args: [ExprInput]; } @@ -1390,4 +1453,10 @@ export interface WhenPaintNode { } /** Which input zooms a view. */ -export type Zoom = 'wheel' | 'modifier' | 'off'; +export type Zoom = + | /** The wheel, pinching, and the keys. */ 'wheel' + | /** + * Pinching and the keys, and the wheel only with Control or Meta held (as trackpad pinches send + * it), so the plain wheel scrolls the page. + */ 'modifier' + | /** No input zooms. */ 'off'; diff --git a/npm/src/internal.ts b/npm/src/internal.ts index e09e90d3..543a8e1a 100644 --- a/npm/src/internal.ts +++ b/npm/src/internal.ts @@ -7,22 +7,23 @@ import * as bindings from '../pkg/latkit.js'; import type { Failure } from './outputs.js'; -/** - * The error latkit throws: an `Error` with a `code`, plus a `path` to the bad part of invalid - * input, such as `marks[2].vertices.color`. The codes: - * - * - `invalid`: the input is malformed or does not fit; `path` says where. - * - `unsupported`: the browser or device cannot do what was asked. - * - `limit`: a budget or device limit would be exceeded. - * - `io`: reading or writing external bytes failed, such as loading latkit or fetching a file. - * - `device-lost`: the GPU device was lost; create a new `Gpu`. - * - `cancelled`: the wait's signal aborted, or the work was cancelled. - * - `timeout`: the wait's signal timed out. - * - `unloaded`: the call needs WebAssembly, and `load()` has not finished. - * - `destroyed`: the object was destroyed, so it can no longer be used. - */ +/** The error latkit throws: an `Error` whose `code` says what kind of failure it is. */ export interface LatkitError extends Error { + /** + * The kind of failure: + * + * - `invalid`: the input is malformed or does not fit; `path` says where. + * - `unsupported`: the browser or device cannot do what was asked. + * - `limit`: a budget or device limit would be exceeded. + * - `io`: reading or writing external bytes failed, such as loading latkit or fetching a file. + * - `device-lost`: the GPU device was lost; create a new `Gpu`. + * - `cancelled`: the wait's signal aborted, or the work was cancelled. + * - `timeout`: the wait's signal timed out. + * - `unloaded`: the call needs WebAssembly, and `load()` has not finished. + * - `destroyed`: the object was destroyed, so it can no longer be used. + */ code: Failure['code'] | 'unloaded' | 'destroyed'; + /** The invalid part of the input, such as `marks[2].vertices.color`, on `invalid` errors. */ path?: string; } diff --git a/npm/src/outputs.ts b/npm/src/outputs.ts index f12d5c4e..fca5ed3a 100644 --- a/npm/src/outputs.ts +++ b/npm/src/outputs.ts @@ -18,7 +18,11 @@ export interface Description { } /** The input device. */ -export type Device = 'mouse' | 'pen' | 'touch' | 'keyboard'; +export type Device = + | /** A mouse or a trackpad. */ 'mouse' + | /** A pen. */ 'pen' + | /** A finger: picks reach 8 CSS pixels farther. */ 'touch' + | /** The keyboard. */ 'keyboard'; /** A vertex a connection ends at, and the port the connection meets, if any. */ export interface End { @@ -28,13 +32,13 @@ export interface End { port?: string; } -/** - * What failed: its `code`, the message, and for invalid input the `path` to the part, such as - * `marks[2].vertices.color`. - */ +/** What failed, as JSON: the form of the npm package's `LatkitError`. */ export interface Failure { + /** The kind of failure, as `LatkitError.code` describes it. */ code: 'invalid' | 'unsupported' | 'limit' | 'io' | 'device-lost' | 'cancelled' | 'timeout'; + /** What failed, in words. */ message: string; + /** The invalid part of the input, such as `marks[2].vertices.color`, on `invalid` failures. */ path?: string; } @@ -73,7 +77,15 @@ export interface Hit { /** The kind of values a field holds. */ export type Kind = - 'number' | 'bool' | 'text' | 'point' | 'path' | 'parts' | 'polygons' | 'reference' | 'sampled'; + | /** Numbers. */ 'number' + | /** Booleans. */ 'bool' + | /** Texts. */ 'text' + | /** `[x, y]` points. */ 'point' + | /** Paths of points. */ 'path' + | /** Parts: the rings of a polygon, or the paths of a multi-path. */ 'parts' + | /** Polygons of rings. */ 'polygons' + | /** Rows of another table, by id. */ 'reference' + | /** Numbers sampled over the dataset's axis. */ 'sampled'; /** Modifier keys held during an input. */ export interface Modifiers { @@ -88,7 +100,10 @@ export interface Modifiers { } /** The phase of a `Drag`. */ -export type Phase = 'start' | 'move' | 'end'; +export type Phase = + | /** The pointer moved far enough to drag. */ 'start' + | /** The pointer moved. */ 'move' + | /** The pointer was released, or the drag was cancelled. */ 'end'; /** A sample of a traced field: its frame, the axis coordinate it was recorded at, and its value. */ export interface Sample { @@ -162,7 +177,7 @@ export type Value = number | boolean | string | null | [number, number] | unknow /** Something that happened in a view, as `view.on` reports it. */ export type ViewEvent = - | { + | /** A row was clicked. */ { type: 'click'; detail: { /** The row. */ @@ -171,7 +186,7 @@ export type ViewEvent = trigger: Trigger; }; } - | { + | /** A row was double-clicked, or Enter was pressed on the row the keyboard points at. */ { type: 'open'; detail: { /** The row. */ @@ -180,7 +195,10 @@ export type ViewEvent = trigger: Trigger; }; } - | { + | /** + * A context menu was requested: by a secondary click, the menu key, or a touch or pen held still. + * Lists the rows under the trigger's point, for a menu of actions on them. + */ { type: 'menu'; detail: { /** The rows under the point, nearest first. */ @@ -189,7 +207,10 @@ export type ViewEvent = trigger: Trigger; }; } - | { + | /** + * A row of a draggable mark is dragged. The drag starts once the pointer moves more than 4 CSS + * pixels from where it pressed the row. + */ { type: 'drag'; detail: { /** Whether the drag starts, moves, or ends. */ @@ -206,7 +227,10 @@ export type ViewEvent = trigger: Trigger; }; } - | { + | /** + * A connection was drawn with `controls.connect` on: from a port, from a vertex with Alt held, or + * by dragging an edge's end off it. + */ { type: 'connect'; detail: { /** The end it was drawn from: for an edge, the end that stayed. */ @@ -219,7 +243,7 @@ export type ViewEvent = trigger: Trigger; }; } - | { + | /** Delete or Backspace was pressed with rows selected and `controls.delete` on. */ { type: 'delete'; detail: { /** The rows selected. */ @@ -228,7 +252,7 @@ export type ViewEvent = trigger: Trigger; }; } - | { + | /** The hovered row changed: `view.hovered`. */ { type: 'hovered'; detail: { /** The row now hovered; `null` once no row is. */ @@ -237,7 +261,7 @@ export type ViewEvent = trigger: Trigger; }; } - | { + | /** The selection changed: `view.selection`. */ { type: 'selection'; detail: { /** The rows selected now. */ @@ -246,9 +270,18 @@ export type ViewEvent = trigger: Trigger | null; }; } - | { type: 'camera'; detail: Camera } - | { type: 'waiting'; detail: Waiting } - | { type: 'error'; detail: Failure }; + | /** The camera moved by a gesture, fit, flight, coast, or orbit: `view.camera`. */ { + type: 'camera'; + detail: Camera; + } + | /** The work the last frame waited on changed; empty once frames are complete: `view.waiting`. */ { + type: 'waiting'; + detail: Waiting; + } + | /** + * Something failed; reported once. A mark that no longer fits its data is also listed in + * `view.failures`. + */ { type: 'error'; detail: Failure }; /** * What a frame still waits on: pipelines the scene draws with, which compile before the view draws diff --git a/npm/src/patch.ts b/npm/src/patch.ts index b244448c..e2a87d54 100644 --- a/npm/src/patch.ts +++ b/npm/src/patch.ts @@ -37,12 +37,11 @@ export type Values = | readonly (string | null)[] | readonly (Geometry | null)[]; -/** - * A reference field: for each row, the id of a row in table `reference`, or `null` to reference - * none. - */ +/** A reference field: for each row, a row of another table. */ export interface Reference { + /** The table referenced. */ reference: string; + /** For each row, the id of the row it references, or `null` to reference none. */ ids: RowIds; } @@ -56,6 +55,7 @@ export type Fields = Readonly>; /** A byte stream: anything shaped like a browser or Node.js `ReadableStream` of `Uint8Array`. */ export interface ReadableBytes { + /** A reader of the stream's chunks, which latkit reads to the end, or cancels. */ getReader(): { read(): Promise<{ done: boolean; value?: Uint8Array }>; cancel(reason?: unknown): Promise; diff --git a/npm/src/scene.ts b/npm/src/scene.ts index 73f7da71..aa7d1682 100644 --- a/npm/src/scene.ts +++ b/npm/src/scene.ts @@ -37,9 +37,15 @@ const nodeOf = (input: ExprInput): ExprNode => (input instanceof Expr ? input.no /** A value for every row of a mark: a constant, a field, view state, or arithmetic on them. */ export class Expr { + /** The expression's JSON form. */ + readonly node: ExprNode; + /** Wraps an expression's JSON form. The builders cover common cases. */ - constructor(readonly node: ExprNode) {} + constructor(node: ExprNode) { + this.node = node; + } + /** The JSON form, which `JSON.stringify` writes for the expression. */ toJSON(): ExprNode { return this.node; } @@ -57,66 +63,87 @@ export class Expr { return new Expr(mapFields(this.node, (f) => ({ ...f, step: true }))); } + /** The sum of this value and `other`. */ add(other: ExprInput): Expr { return binary('add', this, other); } + /** This value minus `other`. */ sub(other: ExprInput): Expr { return binary('sub', this, other); } + /** The product of this value and `other`. */ mul(other: ExprInput): Expr { return binary('mul', this, other); } + /** This value divided by `other`. Dividing by 0 gives a missing value. */ div(other: ExprInput): Expr { return binary('div', this, other); } + /** The remainder of this value divided by `other`, with the sign of `other`: `-1 rem 3` is 2. */ rem(other: ExprInput): Expr { return binary('rem', this, other); } + /** This value raised to the power `other`. */ pow(other: ExprInput): Expr { return binary('pow', this, other); } + /** The smaller of this value and `other`. */ min(other: ExprInput): Expr { return binary('min', this, other); } + /** The larger of this value and `other`. */ max(other: ExprInput): Expr { return binary('max', this, other); } + /** This value limited to the range from `low` to `high`. */ clamp(low: ExprInput, high: ExprInput): Expr { return this.max(low).min(high); } + /** 1 where this value is less than `other`, else 0. */ lt(other: ExprInput): Expr { return binary('lt', this, other); } + /** 1 where this value is at most `other`, else 0. */ le(other: ExprInput): Expr { return binary('le', this, other); } + /** 1 where this value is greater than `other`, else 0. */ gt(other: ExprInput): Expr { return binary('gt', this, other); } + /** 1 where this value is at least `other`, else 0. */ ge(other: ExprInput): Expr { return binary('ge', this, other); } + /** 1 where this value equals `other`, else 0. Colors compare too. */ eq(other: ExprInput): Expr { return binary('eq', this, other); } + /** 1 where this value differs from `other`, else 0. Colors compare too. */ ne(other: ExprInput): Expr { return binary('ne', this, other); } + /** 1 where this value and `other` are both above 0.5, else 0. */ and(other: ExprInput): Expr { return binary('and', this, other); } + /** 1 where this value or `other` is above 0.5, else 0. */ or(other: ExprInput): Expr { return binary('or', this, other); } + /** 1 where this value is at most 0.5, else 0. */ not(): Expr { return unary('not', this); } + /** The value negated. */ neg(): Expr { return unary('neg', this); } + /** The absolute value. */ abs(): Expr { return unary('abs', this); } + /** The square root. Negative values give a missing value. */ sqrt(): Expr { return unary('sqrt', this); } @@ -128,12 +155,15 @@ export class Expr { cos(): Expr { return unary('cos', this); } + /** The largest whole number at most the value. */ floor(): Expr { return unary('floor', this); } + /** The natural logarithm. Values at most 0 give a missing value. */ ln(): Expr { return unary('ln', this); } + /** e raised to the value. */ exp(): Expr { return unary('exp', this); } @@ -181,9 +211,15 @@ export class Mapped extends Expr { * choosing between colors. Color options take a `Paint` or a CSS color string. */ export class Paint { + /** The color's JSON form. */ + readonly node: PaintNode; + /** Wraps a color's JSON form. The builders cover common cases. */ - constructor(readonly node: PaintNode) {} + constructor(node: PaintNode) { + this.node = node; + } + /** The JSON form, which `JSON.stringify` writes for the color. */ toJSON(): PaintNode { return this.node; } @@ -199,12 +235,15 @@ export class Colormapped extends Paint { /** A missing value, valid as a value or a color. An option given one uses the theme's value. */ export class Missing extends Expr { + /** The missing value's JSON form, `{ missing: true }`. */ declare readonly node: MissingNode; + /** A missing value. {@link missing} makes one too. */ constructor() { super({ missing: true }); } + /** The JSON form, which `JSON.stringify` writes for the missing value. */ override toJSON(): MissingNode { return this.node; } @@ -366,8 +405,11 @@ function colored(value: ExprInput | PaintInput): boolean { /** RGBA pixels with straight alpha, rows top to bottom. A browser `ImageData` has this shape. */ export interface RgbaImage { + /** The width in pixels. */ width: number; + /** The height in pixels. */ height: number; + /** 4 bytes per pixel, red, green, blue, and alpha, row after row. */ data: Uint8ClampedArray; } diff --git a/npm/src/shot.ts b/npm/src/shot.ts index 3c2bf5a9..544f4d31 100644 --- a/npm/src/shot.ts +++ b/npm/src/shot.ts @@ -46,11 +46,19 @@ export interface ShotOptions { * ``` */ export class Shot { + /** The picture's size, `[width, height]` in pixels. */ + readonly size: readonly [number, number]; + /** How the picture is taken, and in `layout`, the panel each view is drawn into. */ + readonly options: ShotOptions & { layout?: readonly Panel[] }; + /** Creates a shot of `size`, `[width, height]` in pixels, of the views in `options.layout`. */ constructor( - readonly size: readonly [number, number], - readonly options: ShotOptions & { layout?: readonly Panel[] } = {}, - ) {} + size: readonly [number, number], + options: ShotOptions & { layout?: readonly Panel[] } = {}, + ) { + this.size = size; + this.options = options; + } /** * Draws the picture once every view is complete, as `capture` says, and resolves to the @@ -64,18 +72,17 @@ export class Shot { } /** - * Starts a video of this shot: H.264 in MP4 at `fps` frames per second, written to `output` as - * frames are drawn. `bitrate` is in bits per second, about 0.15 bits per pixel per frame by - * default. The width is rounded down to a multiple of 4 and the height to a multiple of 2, as the - * encoder requires. Canvases showing the views keep their last frame until the video finishes or - * aborts. Aborting `signal` aborts the video and `output`; frames have no other time limit. + * Starts a video of this shot: H.264 in MP4 at `options.fps` frames per second, written to + * `options.output` as frames are drawn. `options.bitrate` is in bits per second, about 0.15 bits + * per pixel per frame by default. The width is rounded down to a multiple of 4 and the height to a + * multiple of 2, as the encoder requires. Canvases showing the views keep their last frame until + * the video finishes or aborts. Aborting `options.signal` aborts the video and its output; frames + * have no other time limit. */ - video({ - fps, - output, - bitrate, - signal, - }: { fps: number; output: WritableStream; bitrate?: number } & Wait): Video { + video( + options: { fps: number; output: WritableStream; bitrate?: number } & Wait, + ): Video { + const { fps, output, bitrate, signal } = options; signal?.throwIfAborted(); const writer = output.getWriter(); try { @@ -160,6 +167,7 @@ export class Image { release(this)?.free(); } + /** Calls {@link Image.destroy}, so a `using` declaration destroys it when its block ends. */ [Symbol.dispose](): void { this.destroy(); } diff --git a/npm/src/view.ts b/npm/src/view.ts index de4183ed..08fc8a15 100644 --- a/npm/src/view.ts +++ b/npm/src/view.ts @@ -309,6 +309,7 @@ export class View { this.#released(); } + /** Calls {@link View.destroy}, so a `using` declaration destroys it when its block ends. */ [Symbol.dispose](): void { this.destroy(); } From 33a06ede8426bc64906989281c35262fc12c5f1e Mon Sep 17 00:00:00 2001 From: Luke Lowery Date: Mon, 26 Oct 2026 22:57:11 -0500 Subject: [PATCH 09/14] mis improvments and hardening --- .changeset/sources.md | 18 +- .github/workflows/ci.yml | 42 +- .github/workflows/release.yml | 3 +- .prettierignore | 3 +- CONTRIBUTING.md | 8 +- benchmark/baselines/work.json | 234 ++--- benchmark/docs.ts | 17 +- benchmark/judge.ts | 6 +- benchmark/page/data.ts | 9 +- benchmark/page/scenarios.ts | 8 +- benchmark/page/stage.ts | 34 +- benchmark/run.ts | 27 +- crates/latkit-cli/src/args.rs | 225 ++--- crates/latkit-cli/src/inputs.rs | 15 +- crates/latkit-cli/src/main.rs | 51 +- crates/latkit-cli/src/render.rs | 24 +- crates/latkit-cli/src/sinks.rs | 80 +- crates/latkit-cli/tests/cli.rs | 20 +- crates/latkit-js/src/canvas.rs | 16 +- crates/latkit-js/src/convert.rs | 31 +- crates/latkit-js/src/lib.rs | 9 + crates/latkit-js/src/shot.rs | 123 +-- crates/latkit-js/src/view.rs | 18 +- crates/latkit/shaders/compute/lists.wesl | 13 +- crates/latkit/shaders/compute/probe.wesl | 106 +-- crates/latkit/shaders/draw/glyphs.wesl | 5 +- crates/latkit/shaders/draw/overlay.wesl | 4 +- crates/latkit/shaders/lib/center.wesl | 59 ++ crates/latkit/shaders/lib/label.wesl | 97 +- crates/latkit/shaders/lib/read.wesl | 16 + crates/latkit/shaders/lib/trace.wesl | 18 +- crates/latkit/src/data/bytes.rs | 234 +++-- crates/latkit/src/data/forms.rs | 92 +- crates/latkit/src/data/frames.rs | 5 +- crates/latkit/src/data/json.rs | 10 +- crates/latkit/src/data/name.rs | 15 + crates/latkit/src/data/source.rs | 38 +- crates/latkit/src/data/values.rs | 180 ++-- crates/latkit/src/derive/lanes.rs | 2 +- crates/latkit/src/derive/mod.rs | 116 ++- crates/latkit/src/derive/places.rs | 82 +- crates/latkit/src/derive/pyramid.rs | 9 +- crates/latkit/src/derive/shapes.rs | 135 ++- crates/latkit/src/derive/sight.rs | 31 +- crates/latkit/src/derive/stars.rs | 12 +- crates/latkit/src/derive/tiled.rs | 61 +- crates/latkit/src/error.rs | 34 +- crates/latkit/src/expr/colormap.rs | 10 +- crates/latkit/src/expr/compile.rs | 17 +- crates/latkit/src/expr/cpu.rs | 2 +- crates/latkit/src/expr/form.rs | 119 ++- crates/latkit/src/expr/mod.rs | 73 +- crates/latkit/src/geo/geojson.rs | 25 +- crates/latkit/src/geo/mercator.rs | 42 + crates/latkit/src/geo/mod.rs | 6 +- crates/latkit/src/geo/mvt.rs | 68 +- crates/latkit/src/geo/pmtiles.rs | 16 +- crates/latkit/src/geo/tiles.rs | 45 +- crates/latkit/src/gpu/atlas.rs | 45 +- crates/latkit/src/gpu/colormaps.rs | 82 +- crates/latkit/src/gpu/grown.rs | 35 + crates/latkit/src/gpu/icons.rs | 98 +- crates/latkit/src/gpu/layers.rs | 135 +++ crates/latkit/src/gpu/mod.rs | 6 +- crates/latkit/src/gpu/pipelines.rs | 46 +- crates/latkit/src/host/job.rs | 405 +++------ crates/latkit/src/image.rs | 20 + crates/latkit/src/json.rs | 6 + crates/latkit/src/lib.rs | 1 + crates/latkit/src/order.rs | 27 + crates/latkit/src/pending.rs | 46 + crates/latkit/src/scene/areas.rs | 2 +- crates/latkit/src/scene/controls.rs | 28 +- crates/latkit/src/scene/edges.rs | 15 +- crates/latkit/src/scene/faces.rs | 2 +- crates/latkit/src/scene/form.rs | 34 +- crates/latkit/src/scene/layout.rs | 35 +- crates/latkit/src/scene/lines.rs | 3 +- crates/latkit/src/scene/mark.rs | 171 +++- crates/latkit/src/scene/marker.rs | 37 +- crates/latkit/src/scene/port.rs | 46 +- crates/latkit/src/scene/space.rs | 33 +- crates/latkit/src/scene/tests.rs | 12 + crates/latkit/src/scene/theme.rs | 20 +- crates/latkit/src/scene/vertices.rs | 2 +- crates/latkit/src/shot.rs | 45 +- crates/latkit/src/tiles/cache.rs | 47 +- crates/latkit/src/tiles/mod.rs | 2 +- crates/latkit/src/tiles/sets.rs | 2 +- crates/latkit/src/tiles/vector.rs | 18 +- crates/latkit/src/video/mod.rs | 14 +- crates/latkit/src/view/camera.rs | 20 +- crates/latkit/src/view/ends.rs | 2 +- crates/latkit/src/view/fit.rs | 56 +- crates/latkit/src/view/focus.rs | 15 +- crates/latkit/src/view/frame.rs | 76 +- crates/latkit/src/view/graph.rs | 15 + crates/latkit/src/view/labels.rs | 103 +-- crates/latkit/src/view/lists.rs | 11 +- crates/latkit/src/view/marks.rs | 102 ++- crates/latkit/src/view/mod.rs | 8 +- crates/latkit/src/view/passes.rs | 274 +++--- crates/latkit/src/view/plan.rs | 58 +- crates/latkit/src/view/probe.rs | 79 +- crates/latkit/src/view/program.rs | 8 +- crates/latkit/src/view/query.rs | 54 +- crates/latkit/src/view/resources.rs | 48 +- crates/latkit/src/view/sync.rs | 233 ++--- crates/latkit/src/view/timer.rs | 6 +- crates/latkit/src/view/transition.rs | 45 +- crates/latkit/src/view/uniforms.rs | 10 +- crates/latkit/src/view/variants.rs | 293 ++++-- crates/latkit/src/view/words.rs | 17 +- crates/latkit/src/web.rs | 6 + crates/latkit/src/wire.rs | 159 ++++ crates/latkit/tests/areas.rs | 32 +- crates/latkit/tests/picks.rs | 40 +- crates/latkit/tests/shots.rs | 35 +- crates/latkit/tests/tiles.rs | 14 + crates/latkit/tests/videos.rs | 10 +- docs/cli.md | 49 +- docs/gallery/flow.ts | 61 +- docs/gallery/frequency.ts | 10 +- docs/gallery/grid.ts | 4 +- docs/gallery/grids.ts | 131 +-- docs/gallery/opening.ts | 42 +- docs/gallery/record.ts | 2 +- docs/gallery/render.ts | 6 +- docs/gallery/texas.ts | 4 +- docs/guide.md | 377 +++----- docs/reference/api.md | 716 +++++++-------- docs/reference/cli.json | 836 ++++++++++++++++++ docs/reference/cli.md | 118 +-- docs/reference/types.md | 467 ++-------- npm/README.md | 52 +- npm/docs.mjs | 226 +++-- npm/examples/gate/browser.ts | 49 +- npm/examples/gate/gate.ts | 54 +- npm/examples/gate/package.ts | 14 +- npm/examples/gate/quantile.ts | 7 + npm/examples/gate/tsconfig.json | 2 +- npm/examples/scripts/history.ts | 11 +- npm/examples/src/examples/colormaps/index.ts | 90 +- .../src/examples/networks/cases.test.ts | 53 +- npm/examples/src/examples/networks/cases.ts | 218 ++--- .../src/examples/networks/continent.ts | 3 +- npm/examples/src/examples/networks/format.ts | 153 ++++ npm/examples/src/examples/networks/index.ts | 19 +- npm/examples/src/examples/networks/maps.ts | 16 +- npm/examples/src/examples/networks/states.ts | 19 +- npm/examples/src/examples/power-flow/grid.ts | 109 +-- npm/examples/src/examples/soft-body/burst.ts | 7 +- npm/examples/src/random.ts | 7 + npm/examples/tsconfig.json | 2 + npm/schema.json | 831 ++++++----------- npm/schema.mjs | 21 +- npm/src/canvas.ts | 94 +- npm/src/colormap.ts | 15 +- npm/src/data.ts | 287 +++--- npm/src/encoder.ts | 4 +- npm/src/gpu.ts | 108 +-- npm/src/index.ts | 33 +- npm/src/inputs.ts | 259 ++---- npm/src/internal.ts | 203 +++-- npm/src/jobs.ts | 17 +- npm/src/load.ts | 39 +- npm/src/patch.ts | 147 ++- npm/src/scene.ts | 16 +- npm/src/shot.ts | 257 +++--- npm/src/view.ts | 77 +- npm/test/data.test.mjs | 36 + npm/test/jobs.test.mjs | 7 +- npm/test/video.test.mjs | 45 +- xtask/src/main.rs | 92 +- 174 files changed, 6308 insertions(+), 6381 deletions(-) create mode 100644 crates/latkit/shaders/lib/center.wesl create mode 100644 crates/latkit/src/geo/mercator.rs create mode 100644 crates/latkit/src/gpu/grown.rs create mode 100644 crates/latkit/src/gpu/layers.rs create mode 100644 crates/latkit/src/wire.rs create mode 100644 docs/reference/cli.json create mode 100644 npm/examples/gate/quantile.ts create mode 100644 npm/examples/src/examples/networks/format.ts diff --git a/.changeset/sources.md b/.changeset/sources.md index 29796583..ba55f8fb 100644 --- a/.changeset/sources.md +++ b/.changeset/sources.md @@ -12,12 +12,17 @@ Views draw from sources, every wait can be cancelled, colors and lengths are typ - Added: `px(n)` and `units(n)` give a length's unit. Lengths default to CSS pixels on planes, the earth, and plots, and to position units in diagrams. A stroke in pixels keeps its width as a diagram zooms. - Added: the theme in groups: `text`, `vertex`, `edge`, `line`, `area`, `focus`, `flow`, `diagram`, `earth`, `plot`. A comet's `flow.spacing` and `flow.length` take `{ px }` or `{ units }`. `diagram.outline`, `corner`, `portSize`, and `bend` style blocks, groups, ports, and wire bends; `shadow: true` on blocks and groups casts a soft shadow `diagram.shadow.lift` below them. - Added: `Gpu.resident` and `Gpu.uploading`, the bytes on the GPU and waiting to upload across all its views; `Work.packed`, `Work.resolved`, `Work.cleared`, and `Work.uploads`. -- Added: the package's types document every member, every value of an enumeration, and every form of a union. +- Added: `Handle`, the base of every object that holds memory (`Data`, `Snapshot`, `Gpu`, `View`, `Canvas`, `Image`): `destroy()` and `using`. `Dataset` holds the reads `Data` and `Snapshot` share. +- Added: `colormaps()` lists the catalog's colormaps with their names, labels, and kinds. +- Added: the package's types document every member, every value of an enumeration, every form of a union, and every field of an options object. +- Added: the CLI's `--camera` takes a camera as JSON, as `latkit query` takes a query, or a file of it. - Changed: `view.scene`, `view.selection`, `view.screen(position)`, `view.position(point)`, `view.screenOf(row)`, `view.waiting`, and the `hovered` and `selection` events replace the methods and events removed below. - Changed: every error the package throws is a `LatkitError` with a `code`. A failed fetch or an error status is `io`; a call on a destroyed object has the new code `destroyed`; a bad `Patch.stream` limit and a `Video` drawn out of turn are `invalid`. Errors from the video encoder and worker jobs keep their codes, and an abort during a fetch rejects as `cancelled` or `timeout`. - Changed: latkit's bytes carry no version or flags. A frame's header is the magic `LATK` and the frame's length, 12 bytes; files written by earlier versions no longer open. - Changed: `theme.missing` is the color a colormap gives a missing value. An option given `missing()` still takes its theme value. -- Changed: `new Patch(dataset)` names its dataset; `Patch.read(source)` reads a patch's bytes or JSON, from bytes, a `Blob`, a `Response`, a byte stream, or a URL; `Data.read(source, { name, base })` reads a dataset's bytes, JSON, or GeoJSON; `patch.append(table, at, fields, { ids })`; `patch.frames(table, field, { level, first, at, values })`; `data.envelope({ table, field, range, spans, ids })`; `field(name).from(data, table)` reads another dataset. +- Changed: `new Patch(dataset)` names its dataset; `Patch.read(source)` reads a patch's bytes or JSON, and `Data.read(source, { name, base })` a dataset's bytes, JSON, or GeoJSON, from a `ByteSource`: bytes, a `Blob`, a `Response`, a byte stream or async iterable, or a URL, read within the wait's `signal`; `patch.append(table, at, fields, { ids })`; `patch.frames(table, field, { level, first, at, values })`; `data.envelope({ table, field, range, spans, ids })`; `field(name).from(data, table)` reads another dataset. +- Changed: a video's size rounds down to whole 4×2 blocks, in the browser and the CLI alike; a smaller size is `invalid`. +- Changed: `view.screen` and `view.position` given the wrong number of coordinates throw `invalid` instead of answering `null`. - Changed: the `'earth'` space, `{ earth: { projection: 'mercator' | 'globe', graticule, daylight, atmosphere } }`. The ground color is the theme's `earth.ground`. - Changed: a table too large to draw whole draws in pages that keep their GPU slots as the camera moves. Pages no longer drawn stay on the GPU while the budget allows, so coming back is free, and a write rebuilds only the pages it touched. Appending rows never thins what draws. Lanes, net stars, joins, and fitted domains follow the data, not the camera. - Changed: edges whose ends lie far apart draw within the budget: their endpoint rows load individually, and only edges whose endpoints moved look them up again. @@ -32,3 +37,12 @@ Views draw from sources, every wait can be cancelled, colors and lengths are typ - Fixed: on the earth, sizes in `units(n)` are degrees, as comet lengths already were, not mercator radians. - Fixed: zoomed in close on the globe, paged tables and vector tiles keep drawing their finer tiles; past about zoom 12 they fell back to the coarsest. - Fixed: a text the host cannot rasterize reports one error, not one every frame it shows, and a text with nothing to draw is no error. +- Fixed: a layout or shaping job that fails runs once until its inputs change, not every frame. +- Fixed: at the GPU's texture-layer limit, a new icon fails with a `limit` error instead of overwriting the last one. +- Fixed: `view.screenOf` answers `null` for a hidden stroke or face, and for a face beyond the globe's horizon. +- Fixed: edges and faces no longer compile a bounds pipeline they never use before their first frame. +- Fixed: a `priority` that tests a field only with `contains()` now reads that field when ranking a paged table's rows. +- Fixed: a JSON patch's errors name the failing change the same way wherever they are found, such as `[0].append.fields.x`. +- Fixed: a tile index that is neither TileJSON nor PMTiles fails as `invalid`, and tile errors keep their code. +- Fixed: the CLI names an output it cannot write in its error, rather than blaming the frames file. +- Fixed: a transition that starts in the frame a mark's tables grow no longer writes its rows over other data on the GPU, which could draw rows in the wrong colors or places. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 834fff28..65f39f8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,10 @@ name: CI # What a release depends on: the crates build, pass their tests, document, and package on every -# platform they ship to; the npm tarball works in a consumer; the docs build. Frame budgets and the -# benchmark need a GPU, which runners lack: run `cargo xtask check` and `cargo xtask bench` locally. +# platform they ship to; the npm tarball works in a consumer; the docs build. The jobs that build +# Rust run their steps as `cargo xtask ci `, and `cargo xtask check` runs every job's steps. +# Frame budgets and the benchmark need a GPU, which runners lack: run `cargo xtask check` and +# `cargo xtask bench` locally. on: pull_request: push: @@ -89,29 +91,10 @@ jobs: - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - - run: cargo fmt --all --check - - run: cargo clippy --locked --workspace --all-targets --all-features -- -D warnings - - run: cargo clippy --locked -p latkit --no-default-features --all-targets -- -D warnings - - run: cargo clippy --locked -p latkit-js --target wasm32-unknown-unknown -- -D warnings # Lavapipe, Vulkan on the CPU, runs the GPU tests instead of skipping; text tests need fonts. - name: Install a software Vulkan driver and fonts for the GPU tests run: sudo apt-get update && sudo apt-get install -y mesa-vulkan-drivers fonts-dejavu-core - - run: cargo test --locked --workspace - env: - LATKIT_REQUIRE_GPU: 1 - - run: cargo test --locked -p latkit --no-default-features - env: - LATKIT_REQUIRE_GPU: 1 - - name: Document the library - run: cargo doc --locked -p latkit --no-deps --all-features - env: - RUSTDOCFLAGS: -D warnings - - name: Verify the native package - run: cargo package --locked -p latkit - - name: Verify the browser-target package - run: cargo package --locked -p latkit --target wasm32-unknown-unknown --features web - - name: Keep native font code out of browser builds - run: cargo check --locked -p latkit --target wasm32-unknown-unknown --features native,web + - run: cargo xtask ci rust # The CLI's native code on the other platforms it installs on; Linux runs in `rust`. native-cli: @@ -129,8 +112,7 @@ jobs: - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - - run: cargo test --locked --package latkit --features native --lib --test validation - - run: cargo test --locked --package latkit-cli + - run: cargo xtask ci native-cli # The npm package: built from the crate, checked, tested, every pipeline compiled in Chrome on # SwiftShader, and the packed tarball installed and drawn in a consumer. @@ -156,7 +138,8 @@ jobs: - name: Read the WASM bindings generator's version id: bindgen run: | - echo "version=$(python3 -c 'import tomllib; print(next(p["version"] for p in tomllib.load(open("Cargo.lock", "rb"))["package"] if p["name"] == "wasm-bindgen"))')" >> "$GITHUB_OUTPUT" + version=$(cargo xtask bindgen-version) + echo "version=$version" >> "$GITHUB_OUTPUT" - uses: actions/cache@v4 id: bindgen-cache with: @@ -166,14 +149,7 @@ jobs: if: steps.bindgen-cache.outputs.cache-hit != 'true' run: cargo install wasm-bindgen-cli --version "${{ steps.bindgen.outputs.version }}" --locked - run: pnpm --dir npm/examples exec playwright-core install --with-deps chromium - - run: cargo xtask wasm --dev - - run: pnpm lint - - run: node npm/docs.mjs --check - - run: pnpm typecheck - - run: pnpm test - - run: cargo xtask browser - - run: cargo xtask package - - run: cargo xtask package-test + - run: cargo xtask ci npm # The docs as Read the Docs builds them, from the locked environment: a warning fails. docs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f3ec8441..4a013368 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,7 +90,8 @@ jobs: if: env.PUBLISH == 'true' id: bindgen run: | - echo "version=$(python3 -c 'import tomllib; print(next(p["version"] for p in tomllib.load(open("Cargo.lock", "rb"))["package"] if p["name"] == "wasm-bindgen"))')" >> "$GITHUB_OUTPUT" + version=$(cargo xtask bindgen-version) + echo "version=$version" >> "$GITHUB_OUTPUT" - uses: actions/cache@v4 if: env.PUBLISH == 'true' id: bindgen-cache diff --git a/.prettierignore b/.prettierignore index 52add60d..a4d40692 100644 --- a/.prettierignore +++ b/.prettierignore @@ -17,8 +17,9 @@ benchmark/baselines # Written by the schema test: LATKIT_BLESS=1 cargo test -p latkit --test schema. npm/schema.json docs/_static/theme.json -# Written by node npm/docs.mjs and LATKIT_BLESS=1 cargo test -p latkit-cli, in MyST's definition +# Written by LATKIT_BLESS=1 cargo test -p latkit-cli, then node npm/docs.mjs, in MyST's definition # lists, which Prettier would break. +docs/reference/cli.json docs/reference/api.md docs/reference/types.md docs/reference/cli.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7fc11bd..f0a91183 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ Requires Rust (pinned in `rust-toolchain.toml`), Node.js 24, pnpm 10.30, and uv ```sh pnpm install cargo xtask examples --open # the example pages at http://127.0.0.1:5200 -cargo xtask check # format, lint, test, typecheck, and the browser gate +cargo xtask check # every CI job's steps: format, lint, test, browser gate, package cargo xtask bench --quick # every benchmark scenario once, at its smallest size ``` @@ -35,14 +35,14 @@ regenerate its generated parts: ```sh uv run --project docs --group docs sphinx-build -W -b html docs target/docs # the site -node npm/docs.mjs # the JavaScript API and type references +LATKIT_BLESS=1 cargo test -p latkit-cli # the CLI's command tree, docs/reference/cli.json +node npm/docs.mjs # the API, type, and command references LATKIT_BLESS=1 cargo test -p latkit --test schema # the palette: latkit's default themes cargo build --locked -p latkit-cli --profile cli node docs/gallery/render.ts [name...] # the pictures, drawn by the latkit CLI node benchmark/docs.ts # the performance table, from a baseline ``` -Drawing the pictures also needs the npm package built (`cargo xtask wasm`). The CLI reference, -`docs/reference/cli.md`, is regenerated by `LATKIT_BLESS=1 cargo test -p latkit-cli`. +Drawing the pictures also needs the npm package built (`cargo xtask wasm`). [Releasing](RELEASING.md) covers versions and publishing. diff --git a/benchmark/baselines/work.json b/benchmark/baselines/work.json index d1291296..b33b26f5 100644 --- a/benchmark/baselines/work.json +++ b/benchmark/baselines/work.json @@ -34,7 +34,7 @@ "load": { "uploaded": 200000, "uploads": 15, - "written": 3956, + "written": 3124, "evaluated": 10000, "resolved": 0, "cleared": 40960, @@ -42,7 +42,7 @@ "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 27, + "pipelines": 26, "late": 0, "resident": 901120 } @@ -91,7 +91,7 @@ "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 27, + "pipelines": 26, "late": 0, "resident": 90112 }, @@ -303,7 +303,7 @@ "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 27, + "pipelines": 26, "late": 0, "resident": 901120 }, @@ -507,7 +507,7 @@ "load": { "uploaded": 2000000, "uploads": 125, - "written": 3828, + "written": 4660, "evaluated": 100000, "resolved": 0, "cleared": 401408, @@ -515,7 +515,7 @@ "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 27, + "pipelines": 26, "late": 0, "resident": 8830976 }, @@ -727,7 +727,7 @@ "readbacks": 1, "jobs": 1, "texts": 0, - "pipelines": 27, + "pipelines": 26, "late": 0, "resident": 6094848 }, @@ -939,7 +939,7 @@ "readbacks": 1, "jobs": 1, "texts": 0, - "pipelines": 27, + "pipelines": 26, "late": 0, "resident": 6094848 }, @@ -1145,7 +1145,7 @@ "load": { "uploaded": 36352, "uploads": 7, - "written": 8360, + "written": 12712, "evaluated": 3008, "resolved": 1984, "cleared": 12288, @@ -1153,7 +1153,7 @@ "readbacks": 3, "jobs": 0, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 180224 }, @@ -1312,7 +1312,7 @@ "load": { "uploaded": 358400, "uploads": 25, - "written": 7440, + "written": 11792, "evaluated": 29800, "resolved": 19800, "cleared": 122880, @@ -1320,7 +1320,7 @@ "readbacks": 3, "jobs": 0, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 1802240 }, @@ -1479,7 +1479,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 9288, + "written": 12552, "evaluated": 298936, "resolved": 199080, "cleared": 1204224, @@ -1487,7 +1487,7 @@ "readbacks": 3, "jobs": 1, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 17661952 }, @@ -1646,7 +1646,7 @@ "load": { "uploaded": 4980736, "uploads": 304, - "written": 16192, + "written": 20544, "evaluated": 290816, "resolved": 69632, "cleared": 835584, @@ -1654,7 +1654,7 @@ "readbacks": 5, "jobs": 2, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 17481728 }, @@ -1813,7 +1813,7 @@ "load": { "uploaded": 5144576, "uploads": 314, - "written": 17640, + "written": 20904, "evaluated": 299008, "resolved": 69632, "cleared": 851968, @@ -1821,7 +1821,7 @@ "readbacks": 5, "jobs": 2, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 17924096 }, @@ -1982,15 +1982,15 @@ "load": { "uploaded": 240000, "uploads": 18, - "written": 711104, + "written": 712768, "evaluated": 10000, "resolved": 0, "cleared": 368640, "packed": 0, - "readbacks": 6, + "readbacks": 7, "jobs": 0, "texts": 187, - "pipelines": 34, + "pipelines": 33, "late": 0, "resident": 1269760 }, @@ -2012,14 +2012,14 @@ "zoom": { "uploaded": 0, "uploads": 0, - "written": 5147672, + "written": 5139848, "evaluated": 0, "resolved": 0, "cleared": 0, "packed": 0, "readbacks": 40, "jobs": 0, - "texts": 1229, + "texts": 1227, "pipelines": 0, "late": 0, "resident": 1269760 @@ -2059,15 +2059,15 @@ "load": { "uploaded": 2400000, "uploads": 150, - "written": 1431184, + "written": 1432848, "evaluated": 100000, "resolved": 0, "cleared": 3612672, "packed": 0, - "readbacks": 6, + "readbacks": 7, "jobs": 0, "texts": 187, - "pipelines": 34, + "pipelines": 33, "late": 0, "resident": 12443648 }, @@ -2089,14 +2089,14 @@ "zoom": { "uploaded": 0, "uploads": 0, - "written": 6686612, + "written": 6727060, "evaluated": 0, "resolved": 0, "cleared": 0, "packed": 0, "readbacks": 40, "jobs": 0, - "texts": 1514, + "texts": 1522, "pipelines": 0, "late": 0, "resident": 12443648 @@ -2136,15 +2136,15 @@ "load": { "uploaded": 1966080, "uploads": 120, - "written": 2929668, + "written": 2931332, "evaluated": 81920, "resolved": 0, "cleared": 4653056, "packed": 491520, - "readbacks": 15, + "readbacks": 16, "jobs": 1, "texts": 338, - "pipelines": 34, + "pipelines": 33, "late": 0, "resident": 3604480 }, @@ -2166,14 +2166,14 @@ "zoom": { "uploaded": 4718592, "uploads": 288, - "written": 11507124, + "written": 11504400, "evaluated": 196608, "resolved": 0, "cleared": 12582912, "packed": 1179648, "readbacks": 44, "jobs": 0, - "texts": 1420, + "texts": 1419, "pipelines": 0, "late": 0, "resident": 27983872 @@ -2215,7 +2215,7 @@ "load": { "uploaded": 1456, "uploads": 7, - "written": 6240, + "written": 10544, "evaluated": 116, "resolved": 100, "cleared": 768, @@ -2223,7 +2223,7 @@ "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 8448 }, @@ -2307,7 +2307,7 @@ "load": { "uploaded": 12256, "uploads": 7, - "written": 7328, + "written": 9456, "evaluated": 1016, "resolved": 1000, "cleared": 4352, @@ -2315,7 +2315,7 @@ "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 51456 }, @@ -2399,7 +2399,7 @@ "load": { "uploaded": 120256, "uploads": 13, - "written": 7384, + "written": 9560, "evaluated": 10016, "resolved": 10000, "cleared": 41216, @@ -2407,7 +2407,7 @@ "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 493824 }, @@ -2491,7 +2491,7 @@ "load": { "uploaded": 1200256, "uploads": 79, - "written": 7260, + "written": 10524, "evaluated": 100016, "resolved": 100000, "cleared": 401664, @@ -2499,7 +2499,7 @@ "readbacks": 1, "jobs": 1, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 4819200 }, @@ -2593,7 +2593,7 @@ "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 1641472 }, @@ -2707,7 +2707,7 @@ "load": { "uploaded": 4180168, "uploads": 147, - "written": 6244, + "written": 7908, "evaluated": 1000, "resolved": 0, "cleared": 4096, @@ -2715,7 +2715,7 @@ "readbacks": 1, "jobs": 1, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 4501504 }, @@ -2829,7 +2829,7 @@ "load": { "uploaded": 18151952, "uploads": 645, - "written": 12496, + "written": 13328, "evaluated": 10000, "resolved": 0, "cleared": 40960, @@ -2837,7 +2837,7 @@ "readbacks": 1, "jobs": 1, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 19697664 }, @@ -2951,7 +2951,7 @@ "load": { "uploaded": 40413432, "uploads": 1561, - "written": 27236, + "written": 31396, "evaluated": 30000, "resolved": 0, "cleared": 122880, @@ -2959,7 +2959,7 @@ "readbacks": 1, "jobs": 1, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 43819008 }, @@ -3083,7 +3083,7 @@ "readbacks": 0, "jobs": 4, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 304640 }, @@ -3103,19 +3103,19 @@ "resident": 781824 }, "zoom": { - "uploaded": 746424, - "uploads": 39, - "written": 36088, - "evaluated": 1152, + "uploaded": 792224, + "uploads": 42, + "written": 36072, + "evaluated": 1232, "resolved": 0, "cleared": 1024, "packed": 0, "readbacks": 0, - "jobs": 25, + "jobs": 24, "texts": 0, "pipelines": 0, "late": 0, - "resident": 1621760 + "resident": 1672448 } }, "256": { @@ -3160,13 +3160,13 @@ "readbacks": 0, "jobs": 4, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 719872 }, "pan": { - "uploaded": 644096, - "uploads": 31, + "uploaded": 648192, + "uploads": 32, "written": 35288, "evaluated": 8192, "resolved": 0, @@ -3180,15 +3180,15 @@ "resident": 1449984 }, "zoom": { - "uploaded": 1134336, + "uploaded": 1142528, "uploads": 79, - "written": 36476, - "evaluated": 18432, + "written": 36524, + "evaluated": 19456, "resolved": 0, "cleared": 16384, "packed": 0, "readbacks": 0, - "jobs": 21, + "jobs": 23, "texts": 0, "pipelines": 0, "late": 0, @@ -3229,7 +3229,7 @@ "load": { "uploaded": 2638176, "uploads": 258, - "written": 7640, + "written": 6632, "evaluated": 16384, "resolved": 0, "cleared": 65536, @@ -3237,14 +3237,14 @@ "readbacks": 0, "jobs": 4, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 3194880 }, "pan": { "uploaded": 1286320, "uploads": 128, - "written": 43048, + "written": 43052, "evaluated": 57344, "resolved": 0, "cleared": 98304, @@ -3257,19 +3257,19 @@ "resident": 4759552 }, "zoom": { - "uploaded": 2359152, - "uploads": 324, - "written": 53108, - "evaluated": 77824, + "uploaded": 2769536, + "uploads": 388, + "written": 51952, + "evaluated": 65536, "resolved": 0, - "cleared": 114688, + "cleared": 131072, "packed": 0, "readbacks": 0, - "jobs": 11, + "jobs": 10, "texts": 0, "pipelines": 0, "late": 0, - "resident": 8060928 + "resident": 8675328 } } }, @@ -3308,7 +3308,7 @@ "load": { "uploaded": 25440, "uploads": 22, - "written": 7704, + "written": 9880, "evaluated": 300, "resolved": 0, "cleared": 5120, @@ -3316,7 +3316,7 @@ "readbacks": 1, "jobs": 2, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 52736 }, @@ -3408,7 +3408,7 @@ "readbacks": 1, "jobs": 2, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 479232 }, @@ -3492,7 +3492,7 @@ "load": { "uploaded": 2484264, "uploads": 366, - "written": 90632, + "written": 89544, "evaluated": 30000, "resolved": 0, "cleared": 409600, @@ -3500,7 +3500,7 @@ "readbacks": 1, "jobs": 2, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 4747264 }, @@ -3586,7 +3586,7 @@ "load": { "uploaded": 49152, "uploads": 2, - "written": 39916, + "written": 43948, "evaluated": 10, "resolved": 0, "cleared": 256, @@ -3594,7 +3594,7 @@ "readbacks": 1, "jobs": 0, "texts": 22, - "pipelines": 30, + "pipelines": 29, "late": 0, "resident": 524800 }, @@ -3693,7 +3693,7 @@ "load": { "uploaded": 417792, "uploads": 3, - "written": 39928, + "written": 43960, "evaluated": 100, "resolved": 0, "cleared": 512, @@ -3701,7 +3701,7 @@ "readbacks": 1, "jobs": 0, "texts": 22, - "pipelines": 30, + "pipelines": 29, "late": 0, "resident": 788992 }, @@ -3800,7 +3800,7 @@ "load": { "uploaded": 4104192, "uploads": 17, - "written": 40096, + "written": 44128, "evaluated": 1000, "resolved": 0, "cleared": 4096, @@ -3808,7 +3808,7 @@ "readbacks": 1, "jobs": 0, "texts": 22, - "pipelines": 30, + "pipelines": 29, "late": 0, "resident": 4493312 }, @@ -3907,7 +3907,7 @@ "load": { "uploaded": 40968192, "uploads": 158, - "written": 46872, + "written": 42456, "evaluated": 10000, "resolved": 0, "cleared": 40960, @@ -3915,7 +3915,7 @@ "readbacks": 1, "jobs": 0, "texts": 22, - "pipelines": 30, + "pipelines": 29, "late": 0, "resident": 41787392 }, @@ -4016,7 +4016,7 @@ "load": { "uploaded": 12000, "uploads": 2, - "written": 48788, + "written": 52820, "evaluated": 1, "resolved": 0, "cleared": 256, @@ -4024,7 +4024,7 @@ "readbacks": 1, "jobs": 0, "texts": 26, - "pipelines": 30, + "pipelines": 29, "late": 0, "resident": 524800 }, @@ -4093,7 +4093,7 @@ "load": { "uploaded": 20000, "uploads": 3, - "written": 55280, + "written": 51248, "evaluated": 1, "resolved": 0, "cleared": 256, @@ -4101,7 +4101,7 @@ "readbacks": 1, "jobs": 0, "texts": 26, - "pipelines": 30, + "pipelines": 29, "late": 0, "resident": 786944 }, @@ -4178,7 +4178,7 @@ "readbacks": 1, "jobs": 0, "texts": 26, - "pipelines": 30, + "pipelines": 29, "late": 0, "resident": 1573376 }, @@ -4255,7 +4255,7 @@ "readbacks": 1, "jobs": 0, "texts": 26, - "pipelines": 30, + "pipelines": 29, "late": 0, "resident": 1573376 }, @@ -4334,7 +4334,7 @@ "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 27, + "pipelines": 26, "late": 0, "resident": 90112 }, @@ -4396,7 +4396,7 @@ "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 27, + "pipelines": 26, "late": 0, "resident": 901120 }, @@ -4450,7 +4450,7 @@ "load": { "uploaded": 2000000, "uploads": 125, - "written": 4660, + "written": 3828, "evaluated": 100000, "resolved": 0, "cleared": 401408, @@ -4458,7 +4458,7 @@ "readbacks": 1, "jobs": 0, "texts": 0, - "pipelines": 27, + "pipelines": 26, "late": 0, "resident": 8830976 }, @@ -4520,14 +4520,14 @@ "readbacks": 1, "jobs": 1, "texts": 0, - "pipelines": 27, + "pipelines": 26, "late": 0, "resident": 16904704 }, "move": { "uploaded": 655360, "uploads": 40, - "written": 253264, + "written": 245776, "evaluated": 49152, "resolved": 0, "cleared": 557056, @@ -4576,7 +4576,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 9192, + "written": 11368, "evaluated": 298936, "resolved": 199080, "cleared": 1204224, @@ -4584,7 +4584,7 @@ "readbacks": 1, "jobs": 1, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 17661952 }, @@ -4638,7 +4638,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 17360, + "written": 21712, "evaluated": 597872, "resolved": 398160, "cleared": 2408448, @@ -4646,7 +4646,7 @@ "readbacks": 2, "jobs": 2, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 31711232 }, @@ -4700,7 +4700,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 41232, + "written": 49936, "evaluated": 1195744, "resolved": 796320, "cleared": 4816896, @@ -4708,7 +4708,7 @@ "readbacks": 4, "jobs": 4, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 59809792 }, @@ -4762,7 +4762,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 83536, + "written": 93328, "evaluated": 2391488, "resolved": 1592640, "cleared": 9666560, @@ -4770,7 +4770,7 @@ "readbacks": 8, "jobs": 8, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 116006912 }, @@ -4826,7 +4826,7 @@ "load": { "uploaded": 2686976, "uploads": 164, - "written": 27780, + "written": 32132, "evaluated": 163840, "resolved": 49152, "cleared": 442368, @@ -4834,7 +4834,7 @@ "readbacks": 1, "jobs": 2, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 9027584 }, @@ -4903,7 +4903,7 @@ "load": { "uploaded": 3588096, "uploads": 219, - "written": 35760, + "written": 41200, "evaluated": 208896, "resolved": 49152, "cleared": 589824, @@ -4911,7 +4911,7 @@ "readbacks": 1, "jobs": 2, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 12435456 }, @@ -4980,7 +4980,7 @@ "load": { "uploaded": 3588096, "uploads": 219, - "written": 34680, + "written": 37944, "evaluated": 208896, "resolved": 49152, "cleared": 589824, @@ -4988,7 +4988,7 @@ "readbacks": 1, "jobs": 2, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 12435456 }, @@ -5059,7 +5059,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 19536, + "written": 23888, "evaluated": 597872, "resolved": 398160, "cleared": 2408448, @@ -5067,7 +5067,7 @@ "readbacks": 2, "jobs": 2, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 31711232 }, @@ -5166,7 +5166,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 19536, + "written": 21712, "evaluated": 597872, "resolved": 398160, "cleared": 2408448, @@ -5174,7 +5174,7 @@ "readbacks": 2, "jobs": 2, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 31711232 }, @@ -5273,7 +5273,7 @@ "load": { "uploaded": 3589760, "uploads": 223, - "written": 17360, + "written": 19536, "evaluated": 597872, "resolved": 398160, "cleared": 2408448, @@ -5281,7 +5281,7 @@ "readbacks": 2, "jobs": 2, "texts": 0, - "pipelines": 29, + "pipelines": 28, "late": 0, "resident": 31711232 }, diff --git a/benchmark/docs.ts b/benchmark/docs.ts index c51f279e..97b4b6e8 100644 --- a/benchmark/docs.ts +++ b/benchmark/docs.ts @@ -10,16 +10,11 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import type { TimeBaseline } from './judge.ts'; import { scenarios } from './page/scenarios.ts'; -interface Measured { - time: number; - gpu: number; -} -interface Baseline { - machine: string; - times: Record>>; -} +/** One size's phases of a scenario, each with its times. */ +type Phases = TimeBaseline['times'][string][number]; const here = path.dirname(fileURLToPath(import.meta.url)); const baselines = path.join(here, 'baselines'); @@ -33,7 +28,7 @@ const file = .sort() .at(-1)!, ); -const baseline = JSON.parse(fs.readFileSync(file, 'utf8')) as Baseline; +const baseline = JSON.parse(fs.readFileSync(file, 'utf8')) as TimeBaseline; /** Phases that act every frame while the view moves or plays. */ const MOVING = ['pan', 'zoom', 'move', 'scrub', 'narrow', 'stream']; @@ -41,7 +36,7 @@ const MOVING = ['pan', 'zoom', 'move', 'scrub', 'narrow', 'stream']; const PICKING = ['pick', 'hover']; const ms = (v: number) => (v >= 100 ? Math.round(v).toLocaleString('en-US') : v.toFixed(1)); -const most = (phases: Record, names: string[], of: (m: Measured) => number) => { +const most = (phases: Phases, names: string[], of: (m: Phases[string]) => number) => { const found = names.filter((n) => phases[n]).map((n) => of(phases[n]!)); return found.length ? ms(Math.max(...found)) : ''; }; @@ -55,7 +50,7 @@ for (const [name, sizes] of Object.entries(baseline.times)) { .map(Number) .sort((a, b) => a - b) .at(-1)!; - const phases = sizes[String(largest)]!; + const phases = sizes[largest]!; rows.push([ scenario.about[0]!.toUpperCase() + scenario.about.slice(1), `${largest.toLocaleString('en-US')} ${scenario.unit}`, diff --git a/benchmark/judge.ts b/benchmark/judge.ts index 504a7ee7..9cc585d9 100644 --- a/benchmark/judge.ts +++ b/benchmark/judge.ts @@ -12,6 +12,7 @@ * its baseline. A machine stall slows one repeat, but a regression slows every one. */ import type { Work } from 'latkit'; +import { quantile } from '../npm/examples/gate/quantile.ts'; import type { Listed, Run } from './page/main.ts'; /** A phase at one size, summarized across repeats: its fastest times and its least work. */ @@ -73,8 +74,6 @@ const STEADY = 2; const WORK_FLOOR = { bytes: 64 * 1024, count: 8 }; const BYTES = new Set(['uploaded', 'written', 'resident']); -const median = (xs: number[]) => - [...xs].sort((a, b) => a - b)[Math.floor((xs.length - 1) / 2)] ?? 0; const sum = (xs: number[]) => xs.reduce((a, b) => a + b, 0); const mib = (n: number) => `${(n / 2 ** 20).toFixed(1)} MiB`; @@ -93,12 +92,13 @@ function summarize(runs: Run[]): By { const { scenario, size, phases } = group[0]!; for (const [phase, { steady }] of Object.entries(phases)) { const costs = group.map((r) => r.phases[phase]!); + const frames = costs.map((c) => c.frames); const work = costs.map((c) => ({ ...c.work, resident: c.resident })); ((out[scenario] ??= {})[size] ??= {})[phase] = { steady, time: Math.min(...costs.map((c) => (steady ? c.cpu : c.ms))), gpu: Math.min(...costs.map((c) => c.gpu)), - frames: median(costs.map((c) => c.frames)), + frames: quantile(frames, 0.5), work: Object.fromEntries( Object.keys(work[0]!).map((k) => [ k, diff --git a/benchmark/page/data.ts b/benchmark/page/data.ts index 4805a393..895cefa9 100644 --- a/benchmark/page/data.ts +++ b/benchmark/page/data.ts @@ -4,20 +4,15 @@ * arrays. */ import { reference, type Values } from 'latkit'; +import { congruential } from '../../npm/examples/src/random.ts'; import type { Write } from './stage.ts'; /** Samples per second on the time axis. */ const RATE = 60; -/** A seeded generator of numbers in [0, 1), the same every run. */ -function random(seed: number): () => number { - let s = seed >>> 0 || 1; - return () => (s = (Math.imul(s, 1664525) + 1013904223) >>> 0) / 2 ** 32; -} - /** `n` points in the unit square, each with a value, and a name when `named`. */ export function scatter(n: number, seed = 1, named = false): Write { - const next = random(seed); + const next = congruential(seed); const [x, y, v] = [new Float64Array(n), new Float64Array(n), new Float32Array(n)]; for (let i = 0; i < n; i++) [x[i], y[i], v[i]] = [next(), next(), next()]; const fields: Record = { x, y, v }; diff --git a/benchmark/page/scenarios.ts b/benchmark/page/scenarios.ts index 1f545b64..5b09c1f2 100644 --- a/benchmark/page/scenarios.ts +++ b/benchmark/page/scenarios.ts @@ -23,7 +23,7 @@ import { type Space, } from 'latkit'; import * as make from './data.ts'; -import { deadline, type Cost, type Stage, type Write } from './stage.ts'; +import type { Cost, Stage, Write } from './stage.ts'; export interface Scenario { /** What the scenario stresses. */ @@ -264,10 +264,8 @@ const hover = (): Phase => ({ name: 'hover', order: { time: 0, bytes: 0 }, run: async (s) => { - const [hit] = await deadline( - s.until(s.view.pick(s.middle, { radius: 40 })), - 10_000, - 'a row to hover', + const [hit] = await s.until( + s.view.pick(s.middle, { radius: 40, signal: AbortSignal.timeout(10_000) }), ); if (!hit) { const seen = { middle: s.middle, camera: s.view.camera, stats: s.view.stats }; diff --git a/benchmark/page/stage.ts b/benchmark/page/stage.ts index 390080e6..2de00ff4 100644 --- a/benchmark/page/stage.ts +++ b/benchmark/page/stage.ts @@ -9,7 +9,7 @@ * every view is complete, and no tile is loading. Work in progress that wakes no frame is awaited * until it finishes. A steady phase acts before each of a set number of frames, so each action * shows in its own frame, and its CPU and GPU times are per frame. A lost device fails the phase at - * once, and every wait races a deadline. + * once, and every wait has a deadline that cancels the work it waits on. */ import { Data, @@ -21,6 +21,7 @@ import { type View, type Work, } from 'latkit'; +import { quantile } from '../../npm/examples/gate/quantile.ts'; /** Writes a dataset's rows into a patch, from arrays made beforehand. */ export type Write = (patch: Patch) => Patch; @@ -59,18 +60,6 @@ export const LIMIT = 10_000; /** Frames a steady phase runs before it starts measuring. */ export const WARM = 10; -const quantile = (xs: number[], q: number) => - xs.length ? [...xs].sort((a, b) => a - b)[Math.floor(q * (xs.length - 1))]! : 0; - -/** Races `work` against a timer that rejects after `ms` with an error naming `what`. */ -export function deadline(work: Promise, ms: number, what: string): Promise { - let timer: ReturnType | undefined; - const late = new Promise((_, reject) => { - timer = setTimeout(() => reject(new Error(`${what} took over ${ms} ms`)), ms); - }); - return Promise.race([work, late]).finally(() => clearTimeout(timer)); -} - /** The frames a phase drew, and the CPU and GPU times of those measured. */ interface Drawn { frames: number; @@ -216,7 +205,15 @@ export class Stage { if (this.views.some((v) => v.wantsFrame)) { await this.#frame(drawn, measuring); frames++; - } else await Promise.race([coming, this.gpu.landed().catch(() => {})]); + } else { + // Waits for the answer or for work to land, then stops waiting for work, so no wait is left + // pending. + const answered = new AbortController(); + const timeout = AbortSignal.timeout(Math.max(0, late - performance.now())); + const landed = this.gpu.landed({ signal: AbortSignal.any([answered.signal, timeout]) }); + await Promise.race([coming, landed.catch(() => {})]); + answered.abort(); + } } return answer; } @@ -271,7 +268,7 @@ export class Stage { if (this.gpu.lost) throw new Error('the GPU device was lost'); const before = this.views.map((v) => v.stats.frames); this.gpu.step((this.#now += FRAME)); - await deadline(this.gpu.done(), DEADLINE, 'a frame'); + await this.gpu.done({ signal: AbortSignal.timeout(DEADLINE) }); let [drew, cpu, gpu] = [false, 0, 0]; this.views.forEach((view, i) => { const stats = view.stats; @@ -290,7 +287,7 @@ export class Stage { * tile is loading. Work in progress that wakes no frame, such as a layout or a fetch, is awaited * until it finishes. `measuring` is true for a once phase, whose every frame is measured. */ async #settle(drawn: Drawn, measuring: boolean): Promise { - await deadline(this.gpu.done(), DEADLINE, 'the GPU'); + await this.gpu.done({ signal: AbortSignal.timeout(DEADLINE) }); for (let frames = 0; frames < MOST;) { if (this.gpu.lost) throw new Error('the GPU device was lost'); if (this.views.some((v) => v.wantsFrame)) { @@ -299,7 +296,10 @@ export class Stage { continue; } if (this.views.every((v) => v.complete && !v.stats.tilesLoading)) return; - await deadline(this.gpu.landed(), DEADLINE, `waiting on ${this.#waiting()}`); + await this.gpu.landed({ signal: AbortSignal.timeout(DEADLINE) }).catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`waiting on ${this.#waiting() || 'nothing'}: ${message}`); + }); } throw new Error( `still drawing after ${MOST} frames: waiting on ${this.#waiting() || 'nothing'}`, diff --git a/benchmark/run.ts b/benchmark/run.ts index 31d505d0..07321062 100644 --- a/benchmark/run.ts +++ b/benchmark/run.ts @@ -22,8 +22,14 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseArgs } from 'node:util'; -import { createServer } from 'vite'; -import { STEP, deadline, launch, type Chrome } from '../npm/examples/gate/browser.ts'; +import { + STEP, + deadline, + errorsOf, + launch, + serve, + type Chrome, +} from '../npm/examples/gate/browser.ts'; import { judge, times, work, type TimeBaseline, type WorkBaseline } from './judge.ts'; import { report } from './report.ts'; import type { Listed, Run } from './page/main.ts'; @@ -95,16 +101,7 @@ function outgrown( return undefined; } -const server = await createServer({ - root: here, - configFile: path.join(here, 'vite.config.ts'), - logLevel: 'error', - server: { port: 5230, strictPort: false }, -}); -await deadline(server.listen(), STEP, 'serve the bench'); -const address = server.resolvedUrls?.local[0]; -if (!address) throw new Error('the bench has no address'); -const url = address; +const { url, close } = await serve(here, 5230, 'the bench'); const runs: Run[] = []; const failures: string[] = []; const notes: string[] = []; @@ -126,11 +123,9 @@ async function measure(warm: Chrome, scenario: Listed, size: number): Promise { const page = await deadline(chrome.browser.newPage(PAGE), STEP, 'open a page'); - const errors: string[] = []; + const errors = errorsOf(page); let [phase, heard] = ['opening', performance.now()]; - page.on('pageerror', (e) => errors.push(e.message)); page.on('console', (m) => { - if (m.type() === 'error') errors.push(m.text()); if (m.type() === 'debug' && m.text().startsWith('phase ')) [phase, heard] = [m.text().slice(6), performance.now()]; }); @@ -220,7 +215,7 @@ try { } } finally { await warm.close(); - await server.close(); + await close(); } const read = (file: string): T | undefined => diff --git a/crates/latkit-cli/src/args.rs b/crates/latkit-cli/src/args.rs index d0285876..e0fe8482 100644 --- a/crates/latkit-cli/src/args.rs +++ b/crates/latkit-cli/src/args.rs @@ -51,8 +51,8 @@ pub(crate) enum Command { /// The dataset, `[NAME=]FILE` (`-` reads stdin) #[arg(value_name = "[NAME=]FILE")] data: Source, - /// The query as JSON, such as `{"of":"Branch","filter":{"op":"gt","args":["loading",90]}}`, - /// or a file containing it (`-` reads stdin) + /// The query: JSON, such as `{"of":"Branch","filter":{"op":"gt","args":["loading",90]}}`, or + /// a file of it (`-` reads stdin) #[arg(value_name = "QUERY")] query: String, /// The longest the dataset may take to load, in seconds @@ -100,7 +100,8 @@ pub(crate) struct Job { /// JPEG quality, from 1 (smallest) to 100 (sharpest) #[arg(long, default_value = "90", value_parser = clap::value_parser!(u8).range(1..=100))] pub(crate) quality: u8, - /// The picture's width and height in pixels, such as `1920x1080` + /// The picture's width and height in pixels, such as `1920x1080`. A video rounds its width down + /// to a multiple of 4 and its height to an even number #[arg(long, default_value = "1600x900", value_parser = size)] pub(crate) size: [u32; 2], /// Pixels per CSS pixel. Views lay out size / scale CSS pixels, so 2 draws twice as sharp @@ -119,9 +120,10 @@ pub(crate) struct Job { /// A TTF, OTF, or TTC font file. Repeat for fallbacks. Replaces the system fonts #[arg(long, value_name = "FILE")] pub(crate) font: Vec, - /// A camera JSON file per scene, in scene order. Views without one use `--fit` - #[arg(long, value_name = "FILE")] - pub(crate) camera: Vec, + /// A camera per scene, in scene order: JSON, or a file of it (`-` reads stdin). Views without + /// one use `--fit` + #[arg(long, value_name = "CAMERA")] + pub(crate) camera: Vec, /// What views without a camera frame: `all`, or a fit as JSON #[arg(long, default_value = "all", value_name = "FIT")] pub(crate) fit: String, @@ -285,8 +287,8 @@ impl Cli { return Ok(()); } }; - let stdin = job.scenes.iter().chain(job.data.iter().map(|s| &s.path)).chain(&job.frames).chain(&job.camera); - if stdin.filter(|path| is_stdio(path)).count() > 1 { + let files = job.scenes.iter().chain(job.data.iter().map(|s| &s.path)).chain(&job.frames).map(PathBuf::as_path); + if files.chain(job.camera.iter().map(Path::new)).filter(|path| is_stdio(path)).count() > 1 { return Err(Self::command().error(ErrorKind::ArgumentConflict, "only one input can read stdin")); } if job.tiles.iter().any(|t| t.name.is_none()) { @@ -373,161 +375,66 @@ fn insets(value: &str) -> Result<[f32; 4], String> { #[cfg(test)] mod tests { - use std::collections::HashMap; - use std::fmt::Write as _; - use clap::ArgAction; + use serde_json::{Value, json}; use super::*; - /// The command reference, `docs/reference/cli.md`, in the form of the JavaScript reference: - /// each command's usage and description, then its arguments and options, each a term over its - /// definition with an anchor of its own. Code spans that name a command or an option link to it. - fn reference() -> String { + /// The command tree as JSON, which `npm/docs.mjs` renders as the command reference: each + /// command's name, usage, and description, then its arguments and options. Clap's own `help` + /// command and its help and version flags are left out, and a command's copies of global + /// options too. + fn commands() -> Value { let mut cli = Cli::command(); cli.build(); - // Each command, with its name and anchor. Clap's `help` command and arguments are left out. - let mut commands = vec![("latkit".to_owned(), "cli-latkit".to_owned(), cli.clone())]; - for command in cli.get_subcommands().filter(|c| c.get_name() != "help") { - let name = command.get_name(); - commands.push(( - format!("latkit {name}"), - format!("cli-{name}"), - command.clone().bin_name(format!("latkit {name}")), - )); - } - let shown = |top: bool, arg: &clap::Arg| { - let clap = matches!( - arg.get_action(), - ArgAction::Help | ArgAction::HelpShort | ArgAction::HelpLong | ArgAction::Version - ); - !clap && !arg.is_hide_set() && (top || !arg.is_global_set()) - }; - let id = - |anchor: &str, arg: &clap::Arg| format!("{anchor}-{}", arg.get_long().unwrap_or(arg.get_id().as_str())); - // What code spans link to: each command, and each option, first in its own command. - let mut links = HashMap::new(); - for (name, anchor, command) in &commands { - links.insert(name.clone(), anchor.clone()); - for arg in command.get_arguments().filter(|a| shown(anchor == "cli-latkit", a)) { - if let Some(long) = arg.get_long() { - links.entry(format!("--{long}")).or_insert_with(|| id(anchor, arg)); - } - } - } - let mut text = String::from( - "# Command reference\n\nEvery command and option of the `latkit` command line, written from its help. The \ - [command-line guide](../cli.md) shows them at work. `--help` prints a command's help in the terminal, and \ - `latkit --version` prints the version.\n", - ); - for (name, anchor, command) in &mut commands { - let top = anchor.as_str() == "cli-latkit"; - let own = |arg: &clap::Arg| arg.get_long().map(|long| (format!("--{long}"), id(anchor, arg))); - let mut local = links.clone(); - local.extend(command.get_arguments().filter(|a| shown(top, a)).filter_map(own)); - let usage = command.render_usage().to_string(); - let usage = usage.strip_prefix("Usage: ").expect("clap's usage"); + let command = |command: &clap::Command, name: String, top: bool| { + let shown = |arg: &&clap::Arg| { + let clap = matches!( + arg.get_action(), + ArgAction::Help | ArgAction::HelpShort | ArgAction::HelpLong | ArgAction::Version + ); + !clap && !arg.is_hide_set() && (top || !arg.is_global_set()) + }; + let usage = command.clone().bin_name(&name).render_usage().to_string(); + let usage = usage.strip_prefix("Usage: ").expect("clap's usage").to_owned(); assert!(!usage.contains('\n'), "{name}: one line of usage"); - let about = command.get_long_about().or(command.get_about()).map(ToString::to_string).unwrap_or_default(); - write!( - text, - "\n({anchor})=\n\n## `{name}`\n\n{{.signature}}\n`{usage}`\n\n{}\n", - linked(&sentence(&about), &local) - ) - .unwrap(); - if top { - text += "\n```{rubric} Commands\n```\n"; - for sub in command.get_subcommands().filter(|c| c.get_name() != "help") { - let about = sub.get_about().map(ToString::to_string).unwrap_or_default(); - let term = format!("[`latkit {0}`](#cli-{0})", sub.get_name()); - write!(text, "\n{}\n", item(None, &term, &linked(&sentence(&about), &local))).unwrap(); - } - } - for (title, positional) in [("Arguments", true), ("Options", false)] { - let args: Vec<&clap::Arg> = - command.get_arguments().filter(|a| shown(top, a) && a.is_positional() == positional).collect(); - if args.is_empty() { - continue; - } - write!(text, "\n```{{rubric}} {title}\n```\n").unwrap(); - for arg in args { - let id = id(anchor, arg); - let mut about = - sentence(&arg.get_long_help().or(arg.get_help()).map(ToString::to_string).unwrap_or_default()); - let defaults: Vec = - arg.get_default_values().iter().map(|v| format!("`{}`", v.to_string_lossy())).collect(); - if arg.get_action().takes_values() && !defaults.is_empty() { - write!(about, " Defaults to {}.", defaults.join(", ")).unwrap(); - } - if top && arg.is_global_set() { - about += " Every command takes it."; - } - let mut definition = linked(&about, &local); - let values = if arg.get_action().takes_values() { arg.get_possible_values() } else { Vec::new() }; - for value in values.iter().filter(|v| !v.is_hide_set()) { - let help = value.get_help().map(ToString::to_string).unwrap_or_default(); - assert!(!help.is_empty(), "{name} {}: value {} has no help", id, value.get_name()); - let term = format!("**`{}`**", value.get_name()); - let value_id = format!("{id}-{}", value.get_name()); - write!(definition, "\n\n{}", item(Some(&value_id), &term, &linked(&sentence(&help), &local))) - .unwrap(); - } - write!(text, "\n{}\n", item(Some(&id), &term(arg), &definition)).unwrap(); - } - } - } - text - } - - /// An argument's or an option's term: `**...**`, or `-d, **--data** <[NAME=]FILE>`. - fn term(arg: &clap::Arg) -> String { - let value = arg - .get_value_names() - .and_then(|names| names.first()) - .map_or_else(|| arg.get_id().as_str().to_uppercase(), ToString::to_string); - if arg.is_positional() { - let many = if arg.get_num_args().is_some_and(|n| n.max_values() > 1) { "..." } else { "" }; - let value = if arg.is_required_set() { format!("<{value}>") } else { format!("[{value}]") }; - return format!("**`{value}{many}`**"); - } - let short = arg.get_short().map(|c| format!("`-{c},` ")).unwrap_or_default(); - let long = arg.get_long().expect("every option has a long name"); - let value = if arg.get_action().takes_values() { format!(" `<{value}>`") } else { String::new() }; - format!("{short}**`--{long}`**{value}") - } - - /// One item of a definition list: `term`, its permalink if it has an anchor, and `definition`, - /// its lines after the first indented to stay in the definition. - fn item(id: Option<&str>, term: &str, definition: &str) -> String { - let (attributes, permalink) = match id { - Some(id) => (format!("{{#{id} .member}}"), format!(" [¶](#{id}){{.headerlink}}")), - None => ("{.member}".to_owned(), String::new()), + let text = |s: Option<&clap::builder::StyledStr>| s.map(ToString::to_string).unwrap_or_default(); + let args: Vec = command + .get_arguments() + .filter(shown) + .map(|arg| { + let takes = arg.get_action().takes_values(); + let values: Vec = if takes { arg.get_possible_values() } else { Vec::new() } + .iter() + .filter(|v| !v.is_hide_set()) + .map(|v| json!({ "name": v.get_name(), "help": text(v.get_help()) })) + .collect(); + json!({ + "id": arg.get_id().as_str(), + "long": arg.get_long(), + "short": arg.get_short().map(String::from), + "value": takes.then(|| { + let name = arg.get_value_names().and_then(|names| names.first()); + name.map_or_else(|| arg.get_id().as_str().to_uppercase(), ToString::to_string) + }), + "positional": arg.is_positional(), + "required": arg.is_required_set(), + "many": arg.get_num_args().is_some_and(|n| n.max_values() > 1), + "global": arg.is_global_set(), + "help": text(arg.get_long_help().or(arg.get_help())), + "defaults": if takes { arg.get_default_values().iter().map(|v| v.to_string_lossy()).collect() } else { Vec::new() }, + "values": values, + }) + }) + .collect(); + let about = text(command.get_long_about().or(command.get_about())); + json!({ "name": name, "usage": usage, "about": about, "args": args }) }; - let mut lines = definition.trim().lines(); - let first = lines.next().unwrap_or_default(); - let rest: String = lines.map(|l| if l.is_empty() { "\n".to_owned() } else { format!("\n {l}") }).collect(); - format!("{attributes}\n{term}{permalink}\n: {first}{rest}") - } - - /// Help text as a sentence: trimmed, and ending in a period. - fn sentence(help: &str) -> String { - let help = help.trim(); - if help.is_empty() || help.ends_with('.') { help.to_owned() } else { format!("{help}.") } - } - - /// `text` with each code span that `links` names made a link to its anchor. - fn linked(text: &str, links: &HashMap) -> String { - let parts: Vec<&str> = text.split('`').collect(); - assert!(parts.len() % 2 == 1, "unpaired backtick in {text:?}"); - parts - .iter() - .enumerate() - .map(|(i, part)| match links.get(*part) { - _ if i % 2 == 0 => (*part).to_owned(), - Some(anchor) => format!("[`{part}`](#{anchor})"), - None => format!("`{part}`"), - }) - .collect() + let mut all = vec![command(&cli, "latkit".into(), true)]; + for sub in cli.get_subcommands().filter(|c| c.get_name() != "help") { + all.push(command(sub, format!("latkit {}", sub.get_name()), false)); + } + Value::Array(all) } #[test] @@ -537,13 +444,13 @@ mod tests { if !root.join(".readthedocs.yaml").exists() { return; } - let text = reference(); - let path = root.join("docs/reference/cli.md"); + let text = format!("{}\n", serde_json::to_string_pretty(&commands()).unwrap()); + let path = root.join("docs/reference/cli.json"); if std::env::var_os("LATKIT_BLESS").is_some() { - std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write(&path, &text).unwrap(); } - assert_eq!(std::fs::read_to_string(path).unwrap().replace("\r\n", "\n"), text); + let message = "run with LATKIT_BLESS=1, then node npm/docs.mjs"; + assert_eq!(std::fs::read_to_string(path).unwrap().replace("\r\n", "\n"), text, "{message}"); } #[test] diff --git a/crates/latkit-cli/src/inputs.rs b/crates/latkit-cli/src/inputs.rs index f2fc2ce6..3aa69375 100644 --- a/crates/latkit-cli/src/inputs.rs +++ b/crates/latkit-cli/src/inputs.rs @@ -42,13 +42,18 @@ pub(crate) fn document(path: &Path) -> Result { latkit::from_json(text).map_err(|e| Failure::at(e, path)) } -/// Reads a scene from its JSON file. -pub(crate) fn scene(path: &Path) -> Result { - document(path) +/// Reads argument `arg` of the option or argument `name` as a `T`: JSON when it starts with `{`, +/// else the document in the file it names. +pub(crate) fn json(arg: &str, name: &str) -> Result { + if arg.trim_start().starts_with('{') { + latkit::from_json(arg).map_err(|e| Failure::from(e.at(name))) + } else { + document(Path::new(arg)) + } } -/// Reads a camera from its JSON file. -pub(crate) fn camera(path: &Path) -> Result { +/// Reads a scene from its JSON file. +pub(crate) fn scene(path: &Path) -> Result { document(path) } diff --git a/crates/latkit-cli/src/main.rs b/crates/latkit-cli/src/main.rs index 8361d090..c55b7bd5 100644 --- a/crates/latkit-cli/src/main.rs +++ b/crates/latkit-cli/src/main.rs @@ -8,7 +8,7 @@ mod report; mod sinks; use std::future::Future; -use std::io::{self, Write}; +use std::io; use std::path::Path; use std::process::ExitCode; use std::sync::Arc; @@ -21,6 +21,7 @@ use serde_json::{Value, json}; use args::{Cli, Command, Input, Source}; use report::Report; +use sinks::{Output, written}; fn main() -> ExitCode { let cli = Cli::parse(); @@ -45,7 +46,8 @@ fn main() -> ExitCode { } } -/// A command's failure: the library's error, and the file it came from, if any. +/// A command's failure: the library's error, and the input file at fault, if any. An output that +/// fails to be written is named in the error's message instead. #[derive(Debug)] pub(crate) struct Failure { pub(crate) error: Error, @@ -53,12 +55,12 @@ pub(crate) struct Failure { } impl Failure { - /// `error`, caused by the file at `path`. + /// `error`, caused by the input file at `path`. pub(crate) fn at(error: Error, path: &Path) -> Failure { Failure { error, source: Some(path.to_string_lossy().into_owned()) } } - /// An I/O `error` with the file at `path`. + /// An I/O `error` reading the input file at `path`. pub(crate) fn io(error: &io::Error, path: &Path) -> Failure { Failure::at(Error::Io(error.to_string().into()), path) } @@ -70,6 +72,13 @@ impl From for Failure { } } +/// Prints `value` to stdout as indented JSON. +fn print_json(value: &impl serde::Serialize) -> Result<(), Failure> { + let mut out = Output::create(Path::new("-"))?; + out.json(value, true)?; + Ok(out.commit()?) +} + /// Prints every dataset's description as JSON to stdout: its name, axis and span, and each table's /// rows and fields. fn inspect(sources: &[Source], deadline: Duration) -> Result<(), Failure> { @@ -78,9 +87,7 @@ fn inspect(sources: &[Source], deadline: Duration) -> Result<(), Failure> { for source in sources { all.push(inputs::dataset(source, &host, deadline)?.snapshot().describe()); } - let mut out = io::stdout().lock(); - serde_json::to_writer_pretty(&mut out, &all).map_err(|e| Failure::from(Error::Io(e.to_string().into())))?; - writeln!(out).map_err(|e| Failure::io(&e, Path::new("-"))) + print_json(&all) } /// Prints the JSON Schema of every input to stdout. With `input`, the root `$ref` points to that @@ -107,22 +114,17 @@ fn schema(input: Option) -> Result<(), Failure> { }; schemas["$ref"] = json!(format!("#/$defs/{name}")); } - let mut out = io::stdout().lock(); - serde_json::to_writer_pretty(&mut out, &schemas).map_err(|e| Failure::from(Error::Io(e.to_string().into())))?; - writeln!(out).map_err(|e| Failure::io(&e, Path::new("-"))) + print_json(&schemas) } /// Prints the rows `query` selects from the dataset as JSON Lines to stdout. Each line has `id`, /// then each field. A reference prints as its target's id, and a missing value as `null`. fn query(source: &Source, query: &str, deadline: Duration) -> Result<(), Failure> { - let text = - if query.trim_start().starts_with('{') { query.as_bytes().to_vec() } else { inputs::read(Path::new(query))? }; - let text = std::str::from_utf8(&text).map_err(|_| Failure::from(Error::invalid("not UTF-8 text").at("query")))?; - let query: Query = latkit::from_json(text).map_err(|e| Failure::from(e.at("query")))?; + let query: Query = inputs::json(query, "query")?; let rows = inputs::dataset(source, &latkit::host::Native::new(), deadline)?.snapshot().query(&query)?.table; let columns: Vec<(String, Values)> = rows.fields().map(|(name, _)| (name.to_string(), rows.values(name).expect("the field exists"))).collect(); - let mut out = io::BufWriter::new(io::stdout().lock()); + let mut out = Output::create(Path::new("-"))?; for (row, id) in rows.ids().enumerate() { let mut line = serde_json::Map::with_capacity(columns.len() + 1); line.insert("id".into(), json!(id)); @@ -133,30 +135,21 @@ fn query(source: &Source, query: &str, deadline: Duration) -> Result<(), Failure }; line.insert(name.clone(), value); } - serde_json::to_writer(&mut out, &line).map_err(|e| Failure::from(Error::Io(e.to_string().into())))?; - writeln!(out).map_err(|e| Failure::io(&e, Path::new("-")))?; + out.json(&line, false)?; } - out.flush().map_err(|e| Failure::io(&e, Path::new("-"))) + Ok(out.commit()?) } /// Writes the dataset as time tiles in directory `output`: the manifest, the dataset without its /// sampled fields' histories, and those histories in spans of `chunk` entries. fn tile(source: &Source, output: &Path, chunk: u32, deadline: Duration) -> Result<(), Failure> { let data = inputs::dataset(source, &latkit::host::Native::new(), deadline)?.snapshot(); - let mut failure = None; - let written = data.to_tiles(chunk, |path, bytes| { + let tiled = data.to_tiles(chunk, |path, bytes| { let file = output.join(path); let wrote = file.parent().map_or(Ok(()), std::fs::create_dir_all).and_then(|()| std::fs::write(&file, bytes)); - wrote.map_err(|e| { - failure = Some(Failure::io(&e, &file)); - Error::Io(e.to_string().into()) - }) + wrote.map_err(|e| written(&e, &file)) }); - match (written, failure) { - (Ok(()), _) => Ok(()), - (Err(_), Some(failure)) => Err(failure), - (Err(error), None) => Err(Failure::from(error)), - } + Ok(tiled?) } /// Drives `future` on this thread and returns its result, or [`Error::Timeout`] at `deadline`. diff --git a/crates/latkit-cli/src/render.rs b/crates/latkit-cli/src/render.rs index 44cd3d78..f44c6c9d 100644 --- a/crates/latkit-cli/src/render.rs +++ b/crates/latkit-cli/src/render.rs @@ -39,14 +39,9 @@ impl Inputs { Tiles::new(name.as_deref().unwrap_or_default(), path.to_string_lossy().as_ref()) }) .collect(); - let cameras = job.camera.iter().map(|p| inputs::camera(p)).collect::, _>>()?; - for (i, camera) in cameras.iter().enumerate() { - if !camera.fits(scenes[i].1.space()) { - return Err(Failure::at( - Error::invalid("the camera does not match the space of its scene"), - &job.camera[i], - )); - } + let cameras: Vec = job.camera.iter().map(|c| inputs::json(c, "--camera")).collect::>()?; + if let Some(i) = cameras.iter().zip(&scenes).position(|(camera, (_, scene))| !camera.fits(scene.space())) { + return Err(Error::invalid("the camera does not match the space of its scene").at(i).at("--camera").into()); } let inputs = Inputs { scenes, data, tiles, cameras, fit: inputs::fit(&job.fit)?, host }; for (path, scene) in &inputs.scenes { @@ -97,8 +92,8 @@ pub(crate) fn render(job: &Job, report: &mut Report) -> Result<(), Failure> { let image = block(job.deadline, shot.image(&mut views.iter_mut().collect::>()))?; let mut out = Output::create(output)?; match job.format().image(job.quality) { - Some(format) => image.write(format, &mut out).map_err(|e| Failure::at(e, output))?, - None => std::io::Write::write_all(&mut out, &image.rgba).map_err(|e| Failure::io(&e, output))?, + Some(format) => image.write(format, &mut out)?, + None => out.put(&image.rgba)?, } out.commit()?; report.done(1, &views, &gpu); @@ -143,8 +138,7 @@ struct Sequence<'a> { impl Sequence<'_> { fn run(&mut self, frames: Ahead, sink: S) -> Result<(), Failure> { let (job, path) = (self.job, self.path); - let output = job.output.as_deref().unwrap_or(path); - let mut video = job.shot().video(self.gpu, job.fps, sink).map_err(|e| Failure::at(e, output))?; + let mut video = job.shot().video(self.gpu, job.fps, sink)?; let mut any = false; for next in frames { let (index, frame, patches) = next?; @@ -152,14 +146,16 @@ impl Sequence<'_> { let at = |error: Error| Failure::at(error.at(index).at("frames"), path); apply(&self.inputs.data, &self.inputs.scenes, &frame, patches).map_err(at)?; steer(self.views, &frame); - block(job.deadline, video.frame(&mut self.views.iter_mut().collect::>())).map_err(at)?; + // Only invalid input is the frame's fault; an output that fails names itself. + let drawn = block(job.deadline, video.frame(&mut self.views.iter_mut().collect::>())); + drawn.map_err(|error| if let Error::Invalid { .. } = error { at(error) } else { error.into() })?; self.report.progress(video.frames(), video.time()); } if !any { return Err(Failure::at(Error::invalid("the frames file has no frames"), path)); } let frames = video.frames(); - block(job.deadline, video.finish()).map_err(|e| Failure::at(e, output))?.commit()?; + block(job.deadline, video.finish())?.commit()?; self.report.done(frames, self.views, self.gpu); Ok(()) } diff --git a/crates/latkit-cli/src/sinks.rs b/crates/latkit-cli/src/sinks.rs index e20b0c50..f9f8c718 100644 --- a/crates/latkit-cli/src/sinks.rs +++ b/crates/latkit-cli/src/sinks.rs @@ -1,5 +1,6 @@ //! Output destinations: an output written whole or not at all, and the video sinks besides the -//! library's MP4: image files, raw RGBA, and YUV4MPEG2. +//! library's MP4: image files, raw RGBA, and YUV4MPEG2. An output's errors name it in their message, +//! whoever writes it, since they reach the command through the library's video. use std::collections::VecDeque; use std::fs::File; @@ -10,14 +11,20 @@ use std::path::{Path, PathBuf}; use latkit::host::{Host, Job, Native}; use latkit::{Encoding, Error, Image, ImageFormat, Mp4, Pending, Pixels, Sink}; -use crate::Failure; use crate::args::{is_stdio, pattern}; /// The most image files encoding at once on the host's workers. const ENCODING: usize = 4; -fn written(error: &io::Error, path: &Path) -> Error { - Error::Io(format!("{}: {error}", path.display()).into()) +/// `error`, its message naming the output `path`: a file, or stdout for `-`. +fn named(error: &io::Error, path: &Path) -> io::Error { + let name = if is_stdio(path) { "stdout".to_owned() } else { path.display().to_string() }; + io::Error::new(error.kind(), format!("{name}: {error}")) +} + +/// The library's I/O error for failing to write the output `path`. +pub(crate) fn written(error: &io::Error, path: &Path) -> Error { + Error::Io(named(error, path).to_string().into()) } /// Bytes to stdout, or to a temporary file that replaces `path` only once every byte is written. @@ -28,25 +35,24 @@ pub(crate) enum Output { impl Output { /// An output to `path`, or stdout for `-`. - pub(crate) fn create(path: &Path) -> Result { + pub(crate) fn create(path: &Path) -> latkit::Result { if is_stdio(path) { return Ok(Output::Stdout(BufWriter::new(io::stdout()))); } let parent = path.parent().filter(|p| !p.as_os_str().is_empty()).unwrap_or(Path::new(".")); let suffix = path.extension().map(|e| format!(".{}", e.to_string_lossy())).unwrap_or_default(); - let temporary = - tempfile::Builder::new().suffix(&suffix).tempfile_in(parent).map_err(|e| Failure::io(&e, path))?; + let temporary = tempfile::Builder::new().suffix(&suffix).tempfile_in(parent).map_err(|e| written(&e, path))?; let (file, temporary) = temporary.into_parts(); Ok(Output::File { path: path.to_path_buf(), file: BufWriter::new(file), temporary }) } /// Flushes the remaining bytes and, for a file, moves it into place at its destination. - pub(crate) fn commit(self) -> Result<(), Failure> { + pub(crate) fn commit(self) -> latkit::Result<()> { match self { - Output::Stdout(mut out) => out.flush().map_err(|e| Failure::io(&e, Path::new("-"))), + Output::Stdout(mut out) => out.flush().map_err(|e| written(&e, Path::new("-"))), Output::File { path, file, temporary } => { - drop(file.into_inner().map_err(|e| Failure::io(e.error(), &path))?); - temporary.persist(&path).map_err(|e| Failure::io(&e.error, &path)) + drop(file.into_inner().map_err(|e| written(e.error(), &path))?); + temporary.persist(&path).map_err(|e| written(&e.error, &path)) } } } @@ -58,35 +64,54 @@ impl Output { } } - /// Writes `bytes`. Errors name the destination. - fn put(&mut self, bytes: &[u8]) -> latkit::Result<()> { - self.write_all(bytes).map_err(|e| written(&e, self.path())) + /// Writes `bytes`. + pub(crate) fn put(&mut self, bytes: &[u8]) -> latkit::Result<()> { + self.write_all(bytes).map_err(|e| Error::Io(e.to_string().into())) + } + + /// Writes `value` as JSON, indented if `pretty`, and a newline. + pub(crate) fn json(&mut self, value: &impl serde::Serialize, pretty: bool) -> latkit::Result<()> { + let wrote = if pretty { + serde_json::to_writer_pretty(&mut *self, value) + } else { + serde_json::to_writer(&mut *self, value) + }; + wrote.map_err(|e| Error::Io(e.to_string().into()))?; + self.put(b"\n") + } + + /// Flushes the bytes written so far. + fn flushed(&mut self) -> latkit::Result<()> { + self.flush().map_err(|e| Error::Io(e.to_string().into())) } } +/// Writes whose errors name the output. impl Write for Output { fn write(&mut self, bytes: &[u8]) -> io::Result { - match self { + let wrote = match self { Output::Stdout(out) => out.write(bytes), Output::File { file, .. } => file.write(bytes), - } + }; + wrote.map_err(|e| named(&e, self.path())) } fn flush(&mut self) -> io::Result<()> { - match self { + let flushed = match self { Output::Stdout(out) => out.flush(), Output::File { file, .. } => file.flush(), - } + }; + flushed.map_err(|e| named(&e, self.path())) } } /// Finalizes a sink's output once its video is finished, leaving it written and in place. pub(crate) trait Commit { - fn commit(self) -> Result<(), Failure>; + fn commit(self) -> latkit::Result<()>; } impl Commit for Mp4 { - fn commit(self) -> Result<(), Failure> { + fn commit(self) -> latkit::Result<()> { self.into_inner().commit() } } @@ -144,10 +169,9 @@ impl Images { } match &mut self.to { To::Numbered(names) => { - let path = Images::path(names, index); - let mut out = Output::create(&path).map_err(|f| f.error)?; + let mut out = Output::create(&Images::path(names, index))?; out.put(&self.last)?; - out.commit().map_err(|f| f.error) + out.commit() } To::One(out) => out.put(&self.last), } @@ -189,7 +213,7 @@ impl Sink for Images { } impl Commit for Images { - fn commit(self) -> Result<(), Failure> { + fn commit(self) -> latkit::Result<()> { match self.to { To::Numbered(_) => Ok(()), To::One(out) => out.commit(), @@ -220,12 +244,12 @@ impl Sink for Raw { } fn finish(&mut self, _: u64) -> impl Future> { - ready(self.out.flush().map_err(|e| written(&e, self.out.path()))) + ready(self.out.flushed()) } } impl Commit for Raw { - fn commit(self) -> Result<(), Failure> { + fn commit(self) -> latkit::Result<()> { self.out.commit() } } @@ -270,12 +294,12 @@ impl Sink for Y4m { } fn finish(&mut self, _: u64) -> impl Future> { - ready(self.out.flush().map_err(|e| written(&e, self.out.path()))) + ready(self.out.flushed()) } } impl Commit for Y4m { - fn commit(self) -> Result<(), Failure> { + fn commit(self) -> latkit::Result<()> { self.out.commit() } } diff --git a/crates/latkit-cli/tests/cli.rs b/crates/latkit-cli/tests/cli.rs index 765d0160..efeb4820 100644 --- a/crates/latkit-cli/tests/cli.rs +++ b/crates/latkit-cli/tests/cli.rs @@ -91,7 +91,7 @@ fn usage_errors_exit_2_before_reading_or_drawing() { vec!["render", "missing.json", "-o", "out.png", "--deadline", "0"], vec!["render", "missing.json", "-o", "out.gif"], vec!["render", "missing.json", "-o", "out.mp4"], - vec!["render", "missing.json", "--frames", "f.jsonl", "-o", "out.mp4", "--size", "62x64"], + vec!["render", "missing.json", "--frames", "f.jsonl", "-o", "out.mp4", "--size", "3x64"], vec!["render", "missing.json", "--frames", "f.jsonl", "-o", "out.mp4", "--transparent"], vec!["render", "missing.json", "--frames", "f.jsonl", "-o", "out.mp4", "--fps", "0"], vec!["render", "missing.json", "--frames", "f.jsonl", "-o", "out/%04d.y4m"], @@ -134,6 +134,12 @@ fn validation_reads_everything_without_a_gpu_and_is_silent_on_success() { let told: serde_json::Value = serde_json::from_slice(&output.stderr).unwrap(); assert_eq!(told["detail"]["path"], "frames[0].camera[0]"); assert!(told["detail"]["source"].as_str().unwrap().ends_with("bad.jsonl")); + // `--camera` takes JSON or a file of it, as a query does. + ok(&run(dir.path(), &["validate", "scene.json", "-d", "grid.json", "--camera", CAMERA], None)); + let plot = r#"{"x": [0, 1], "y": [0, 1]}"#; + let args = ["--diagnostics", "json", "validate", "scene.json", "-d", "grid.json", "--camera", plot]; + let told: serde_json::Value = serde_json::from_slice(&run(dir.path(), &args, None).stderr).unwrap(); + assert_eq!(told["detail"]["path"], "--camera[0]"); } #[test] @@ -347,6 +353,13 @@ fn a_frame_stream_writes_every_output_and_repeats_unchanged_frames() { |n: u32| Image::from_png(&std::fs::read(dir.path().join(format!("pngs/frame-{n:03}.png"))).unwrap()).unwrap(); assert_eq!(&png(0).rgba[..], first); assert_eq!(png(1), png(2)); + // A file that cannot be written is named in the error, which is not the frames file's fault. + let mut args = [&["--diagnostics", "json"][..], &COMMON].concat(); + args.extend(["-o", "absent/frame-%03d.png"]); + let stderr = String::from_utf8(run(dir.path(), &args, None).stderr).unwrap(); + let told: serde_json::Value = serde_json::from_str(stderr.lines().last().unwrap()).unwrap(); + assert_eq!((&told["detail"]["code"], &told["detail"]["source"]), (&"io".into(), &serde_json::Value::Null)); + assert!(told["detail"]["message"].as_str().unwrap().contains("absent/frame-000.png"), "{told}"); let mut args = COMMON.to_vec(); args.extend(["-o", "out.y4m"]); @@ -355,6 +368,11 @@ fn a_frame_stream_writes_every_output_and_repeats_unchanged_frames() { let header = b"YUV4MPEG2 W64 H64 F60:1 Ip A1:1 C420jpeg XCOLORRANGE=LIMITED\n"; assert!(y4m.starts_with(header)); assert_eq!(y4m.len(), header.len() + 3 * (6 + 64 * 64 * 3 / 2)); + // A video rounds its size down to whole 4×2 blocks. + args[8] = "--size=67x65"; + args[10] = "odd.y4m"; + ok(&run(dir.path(), &args, None)); + assert!(std::fs::read(dir.path().join("odd.y4m")).unwrap().starts_with(header)); #[cfg(windows)] { diff --git a/crates/latkit-js/src/canvas.rs b/crates/latkit-js/src/canvas.rs index 209af4bd..a8c8ae19 100644 --- a/crates/latkit-js/src/canvas.rs +++ b/crates/latkit-js/src/canvas.rs @@ -9,13 +9,9 @@ use crate::Error; #[wasm_bindgen] pub struct Handle(pub(crate) PageView); -/// The views behind `handles`. -pub(crate) fn views_of(handles: Vec) -> Vec { - handles.into_iter().map(|h| h.0).collect() -} - -/// Pairs each of `views` with its panel. `at` holds four fractions per view, `[x, y, width, -/// height]`, in order. Fails unless `at` has four values per view. +/// Pairs each of `views` with its panel, the one encoding of a canvas's or a shot's layout. `at` +/// holds four fractions per view, `[x, y, width, height]`, in order. Fails unless `at` has four +/// values per view. pub(crate) fn layout(views: Vec, at: &[f32]) -> Result, Error> { if at.len() != views.len() * 4 { return Err(latkit::Error::invalid("expected four fractions per view").at("layout").into()); @@ -36,4 +32,10 @@ impl Canvas { pub fn set_layout(&self, views: Vec, at: &[f32]) -> Result<(), Error> { Ok(self.0.set_layout(layout(views, at)?)?) } + + /// The canvas's size in CSS pixels, `[width, height]`. + #[wasm_bindgen(getter, js_name = cssSize)] + pub fn css_size(&self) -> Vec { + self.0.css_size().to_vec() + } } diff --git a/crates/latkit-js/src/convert.rs b/crates/latkit-js/src/convert.rs index 380c4d2c..b4dff35f 100644 --- a/crates/latkit-js/src/convert.rs +++ b/crates/latkit-js/src/convert.rs @@ -5,9 +5,6 @@ use js_sys::{Array, Float64Array, Object, Reflect}; use latkit::Error; use wasm_bindgen::{JsCast, JsValue}; -/// The largest integer a JavaScript number represents exactly: 2^53 - 1. -const SAFE: f64 = 9_007_199_254_740_991.0; - /// `value` as a JavaScript value. Numbers, booleans, and text convert directly. A geometry becomes /// nested `[x, y]` arrays, and a referenced row becomes its id. fn value(value: &latkit::Value) -> JsValue { @@ -69,23 +66,21 @@ pub(crate) fn object(entries: &[(&str, JsValue)]) -> JsValue { object.into() } -/// An id as a number or string. A number above 2^53 - 1 becomes its decimal string, which -/// identifies the same row. +/// An id as JSON writes it: a number, or a string for a text id or a number above 2^53 − 1. pub(crate) fn id_value(id: &latkit::Id) -> JsValue { - match id { - latkit::Id::Number(n) if *n as f64 <= SAFE => (*n as f64).into(), - latkit::Id::Number(n) => JsValue::from_str(&n.to_string()), - latkit::Id::Text(t) => JsValue::from_str(t), - } + id.exact_number().map_or_else(|| JsValue::from_str(&id.to_string()), JsValue::from) } -/// Parses a JavaScript id: an integer from 0 to 2^53 - 1, or a string. +/// Parses a JavaScript id: a whole number from 0 to 2^53 − 1, or a string. pub(crate) fn id(value: &JsValue) -> Result { - if let Some(text) = value.as_string() { - return Ok(latkit::Id::from(text)); - } - match value.as_f64() { - Some(n) if n.fract() == 0.0 && (0.0..=SAFE).contains(&n) => Ok(latkit::Id::Number(n as u64)), - _ => Err(Error::invalid("expected a whole number from 0 to 2^53 - 1, or a string").at("id")), - } + value + .as_string() + .map(latkit::Id::from) + .or_else(|| value.as_f64().and_then(latkit::Id::from_number)) + .ok_or_else(|| Error::invalid("expected a whole number from 0 to 2^53 - 1, or a string").at("id")) +} + +/// `values` as exactly `N` numbers, or an invalid error at `what`. +pub(crate) fn fixed(values: &[T], what: &str) -> Result<[T; N], Error> { + values.try_into().map_err(|_| Error::invalid(format!("expected {N} numbers")).at(what)) } diff --git a/crates/latkit-js/src/lib.rs b/crates/latkit-js/src/lib.rs index 17f1767b..47b5f6cb 100644 --- a/crates/latkit-js/src/lib.rs +++ b/crates/latkit-js/src/lib.rs @@ -62,6 +62,15 @@ pub fn module() -> JsValue { wasm_bindgen::module() } +/// The catalog of named colormaps, in order, as JSON `[{ name, label, kind }]`. +#[wasm_bindgen] +pub fn colormaps() -> String { + let catalog: Vec = latkit::Colormap::catalog() + .map(|map| serde_json::json!({ "name": map.name(), "label": map.label(), "kind": map.kind() })) + .collect(); + to_json(&catalog) +} + /// A CSS `linear-gradient` toward `direction` through `map`, given as JSON (a catalog name or /// custom colors), in the colors the GPU draws. A categorical map gets one band per category. #[wasm_bindgen(js_name = colormapCss)] diff --git a/crates/latkit-js/src/shot.rs b/crates/latkit-js/src/shot.rs index c47ac4a5..8f165b14 100644 --- a/crates/latkit-js/src/shot.rs +++ b/crates/latkit-js/src/shot.rs @@ -1,19 +1,19 @@ -//! Pictures and videos of views. A shot crosses as JSON, `{ size, scale, panels, transparent, -//! capture, elapsed }` with times in seconds, along with its panels' views. Canvases showing those -//! views keep their last frames meanwhile. The page decides how long to wait. An image's pixels -//! cross once, and its files are encoded in workers. A video returns the bytes each frame produced, -//! for the page to write. +//! Pictures and videos of views. A shot crosses as JSON, `{ size, scale, transparent, capture, +//! elapsed }` with times in seconds, and its layout as a canvas's does: views, and four fractions +//! per view. Canvases showing those views keep their last frames meanwhile. The page decides how +//! long to wait. An image's pixels cross once, and its files are encoded in workers. A video returns +//! the bytes each frame produced, for the page to write. use std::cell::RefCell; use std::io::Write; use std::rc::Rc; -use futures_util::future::{AbortHandle, Abortable}; use latkit::host::Host as _; +use latkit::web::PageView; use wasm_bindgen::prelude::*; use crate::Error; -use crate::canvas::{Handle, views_of}; +use crate::canvas::{Handle, layout}; use crate::pending::{Task, task}; /// RGBA pixels, straight alpha, rows top to bottom. @@ -63,18 +63,21 @@ impl Image { } } -/// Takes `shot`'s (JSON) picture of `views`, one per panel, once they are complete as its capture -/// says. +/// Takes `shot`'s (JSON) picture of `views`, each at its four fractions of `at`, once they are +/// complete as its capture says. #[wasm_bindgen] -pub fn image(shot: &str, views: Vec) -> Result { - let shot = shot_of(shot)?; - let views = views_of(views); +pub fn image(shot: &str, views: Vec, at: &[f32]) -> Result { + let (shot, views) = shot_of(shot, views, at)?; Ok(task(async move { Ok(Image(latkit::web::image(&shot, &views).await?)) })) } -/// The shot `json` describes, with no deadline, since the page decides how long to wait. -fn shot_of(json: &str) -> Result { - Ok(crate::json::("shot", json)?.deadline(None)) +/// The shot `json` describes, with its panels from `views` and `at`, and no deadline, since the +/// page decides how long to wait; and its views. +fn shot_of(json: &str, views: Vec, at: &[f32]) -> Result<(latkit::Shot, Vec), Error> { + let panels = layout(views, at)?; + let shot = crate::json::("shot", json)?.deadline(None); + let shot = shot.panels(panels.iter().map(|panel| panel.at).collect::>()); + Ok((shot, panels.into_iter().map(|panel| panel.view).collect())) } /// Bytes a video wrote and the page has not yet taken. @@ -92,26 +95,19 @@ impl Write for Written { } } -/// A video of a shot's pictures of its views, encoded with WebCodecs as frames are drawn. +/// A video of a shot's pictures of its views, encoded with WebCodecs as frames are drawn. One +/// operation runs at a time; cancelling its task drops the video with it. #[wasm_bindgen] pub struct Video { /// The video. `None` while a frame is drawing it, and after it is finished or cancelled. inner: Rc>>>>, - views: Vec, + views: Vec, written: Written, - fps: f64, frames: Rc>, - active: Rc>>, } #[wasm_bindgen] impl Video { - /// The time in seconds at which the next frame shows. - #[wasm_bindgen(getter)] - pub fn time(&self) -> f64 { - *self.frames.borrow() as f64 / self.fps - } - /// Frames drawn or held so far. #[wasm_bindgen(getter)] pub fn frames(&self) -> f64 { @@ -120,76 +116,50 @@ impl Video { /// Draws the next frame of its views. Resolves to the file bytes ready to write, once there is /// room for another frame. - pub fn frame(&self) -> js_sys::Promise { + pub fn frame(&self) -> Task { let (inner, written, frames) = (self.inner.clone(), self.written.clone(), self.frames.clone()); let views = self.views.clone(); - self.cancellable(async move { + task(async move { let taken = inner.borrow_mut().take(); - let error = || latkit::Error::invalid("the video is finished or cancelled"); - let mut video = taken.ok_or_else(|| JsValue::from(Error::from(error())))?; + let mut video = taken.ok_or_else(|| latkit::Error::invalid("the video is finished or cancelled"))?; let drawn = latkit::web::frame(&mut video, &views).await; *frames.borrow_mut() = video.frames(); *inner.borrow_mut() = Some(video); - drawn.map_err(|e| JsValue::from(Error::from(e)))?; - Ok(js_sys::Uint8Array::from(&std::mem::take(&mut *written.0.borrow_mut())[..]).into()) + drawn?; + Ok(written.take()) }) } - /// Cancels the operation in progress and ends the video. Its views and encoder are released - /// once the cancelled future is dropped. + /// Ends the video and releases its views and encoder. A task in progress is cancelled by the + /// page. pub fn cancel(&self) { - if let Some(active) = self.active.borrow_mut().take() { - active.abort(); - } self.inner.borrow_mut().take(); } /// Encodes the remaining frames and ends the video. Resolves to the file's last bytes. - pub fn finish(&self) -> js_sys::Promise { + pub fn finish(&self) -> Task { let (inner, written) = (self.inner.clone(), self.written.clone()); - self.cancellable(async move { - let taken = inner.borrow_mut().take(); - let video = taken.ok_or_else(|| JsValue::from(Error::from(latkit::Error::Cancelled)))?; - video.finish().await.map_err(|e| JsValue::from(Error::from(e)))?; - Ok(js_sys::Uint8Array::from(&std::mem::take(&mut *written.0.borrow_mut())[..]).into()) - }) - } -} - -impl Video { - fn cancellable( - &self, - future: impl std::future::Future> + 'static, - ) -> js_sys::Promise { - let mut active = self.active.borrow_mut(); - if active.is_some() { - return js_sys::Promise::reject(&JsValue::from(Error::from(latkit::Error::invalid( - "a video runs one operation at a time", - )))); - } - let (abort, registration) = AbortHandle::new_pair(); - *active = Some(abort); - let active = self.active.clone(); - wasm_bindgen_futures::future_to_promise(async move { - let result = Abortable::new(future, registration).await; - active.borrow_mut().take(); - result.unwrap_or_else(|_| Err(JsValue::from(Error::from(latkit::Error::Cancelled)))) + task(async move { + let video = inner.borrow_mut().take().ok_or(latkit::Error::Cancelled)?; + video.finish().await?; + Ok(written.take()) }) } } -impl Drop for Video { - fn drop(&mut self) { - self.cancel(); +impl Written { + /// The bytes written since the last call. + fn take(&self) -> js_sys::Uint8Array { + js_sys::Uint8Array::from(&std::mem::take(&mut *self.0.borrow_mut())[..]) } } -/// A video of `shot`'s (JSON) pictures of `views`, one per panel, at `fps` frames per second. It -/// encodes MP4 at `bitrate` bits per second, by default about 0.15 bits per pixel per frame. +/// A video of `shot`'s (JSON) pictures of `views`, each at its four fractions of `at`, at `fps` +/// frames per second. It encodes MP4 at `bitrate` bits per second, by default about 0.15 bits per +/// pixel per frame. #[wasm_bindgen] -pub fn video(shot: &str, views: Vec, fps: f64, bitrate: Option) -> Result { - let shot = shot_of(shot)?; - let views = views_of(views); +pub fn video(shot: &str, views: Vec, at: &[f32], fps: f64, bitrate: Option) -> Result { + let (shot, views) = shot_of(shot, views, at)?; let gpu = views.first().ok_or_else(|| latkit::Error::invalid("expected at least one view").at("layout"))?.gpu(); let written = Written::default(); let mut mp4 = latkit::Mp4::new(written.clone()); @@ -197,14 +167,7 @@ pub fn video(shot: &str, views: Vec, fps: f64, bitrate: Option) -> mp4 = mp4.bitrate(bitrate as u32); } let video = shot.video(&gpu, fps, mp4)?; - Ok(Video { - inner: Rc::new(RefCell::new(Some(video))), - views, - written, - fps, - frames: Rc::default(), - active: Rc::default(), - }) + Ok(Video { inner: Rc::new(RefCell::new(Some(video))), views, written, frames: Rc::default() }) } /// Sets the factory for video encoders: `create(width, height, fps, bitrate)` returns one. The npm diff --git a/crates/latkit-js/src/view.rs b/crates/latkit-js/src/view.rs index f019cad3..93b923b4 100644 --- a/crates/latkit-js/src/view.rs +++ b/crates/latkit-js/src/view.rs @@ -4,6 +4,7 @@ use wasm_bindgen::prelude::*; use crate::canvas::Handle; +use crate::convert::fixed; use crate::data::{Snapshot, Source}; use crate::pending::{Task, task}; use crate::{Error, json, to_json}; @@ -84,15 +85,15 @@ impl View { } /// The screen point of `position` (x, y, elevation), in CSS pixels from the top left. - pub fn screen(&self, position: &[f64]) -> Option> { - let position: [f64; 3] = position.try_into().ok()?; - self.view.read(|v| v.screen(position)).map(Vec::from) + pub fn screen(&self, position: &[f64]) -> Result>, Error> { + let position = fixed::<_, 3>(position, "position")?; + Ok(self.view.read(|v| v.screen(position)).map(Vec::from)) } /// The position on the ground under screen point `point`. - pub fn position(&self, point: &[f32]) -> Option> { - let point: [f32; 2] = point.try_into().ok()?; - self.view.read(|v| v.position(point)).map(Vec::from) + pub fn position(&self, point: &[f32]) -> Result>, Error> { + let point = fixed::<_, 2>(point, "point")?; + Ok(self.view.read(|v| v.position(point)).map(Vec::from)) } #[wasm_bindgen(getter)] @@ -102,8 +103,7 @@ impl View { #[wasm_bindgen(setter)] pub fn set_insets(&self, insets: &[f32]) -> Result<(), Error> { - let insets: [f32; 4] = - insets.try_into().map_err(|_| latkit::Error::invalid("expected four numbers").at("insets"))?; + let insets = fixed::<_, 4>(insets, "insets")?; self.view.with(|v| v.set_insets(insets)); Ok(()) } @@ -170,7 +170,7 @@ impl View { /// Every row within `radius` CSS pixels of `point`, nearest first, as JSON hits. pub fn pick(&self, point: &[f32], radius: f32) -> Result { - let point: [f32; 2] = point.try_into().map_err(|_| latkit::Error::invalid("expected [x, y]").at("point"))?; + let point = fixed::<_, 2>(point, "point")?; let pending = self.view.with(|v| v.pick(point, radius)); Ok(task(async move { Ok(JsValue::from_str(&to_json(&pending.await?))) })) } diff --git a/crates/latkit/shaders/compute/lists.wesl b/crates/latkit/shaders/compute/lists.wesl index 8fa0b8e1..332d7b15 100644 --- a/crates/latkit/shaders/compute/lists.wesl +++ b/crates/latkit/shaders/compute/lists.wesl @@ -11,6 +11,7 @@ import package::lib::types::{ DISPATCH_ROWS, RUN_WORDS, FADE_IN, FADE_OUT, DRAWN_ENTERING, DRAWN_LEAVING, item, }; import package::lib::lists::{lists, list_start}; +import package::lib::read::last_at_most; import package::lib::path::{row_segments, path_point, path_length, PATH}; import package::lib::drawn::held_row; @@ -104,17 +105,7 @@ fn expand_runs(@builtin(global_invocation_id) id: vec3u) { if i >= mark.rows + mark.exits { return; } - var lo = 0u; - var hi = max(program[mark.row_runs], 1u); - while hi - lo > 1u { - let mid = (lo + hi) / 2u; - if program[mark.row_runs + 1u + mid * RUN_WORDS] <= i { - lo = mid; - } else { - hi = mid; - } - } - let run = mark.row_runs + 1u + lo * RUN_WORDS; + let run = last_at_most(mark.row_runs + 1u, max(program[mark.row_runs], 1u), RUN_WORDS, 0u, i); let fade = program[run + 2u]; let entering = select(0u, DRAWN_ENTERING, (fade & FADE_IN) != 0u); let leaving = select(0u, DRAWN_LEAVING, (fade & FADE_OUT) != 0u); diff --git a/crates/latkit/shaders/compute/probe.wesl b/crates/latkit/shaders/compute/probe.wesl index e8c48e9c..f4df4746 100644 --- a/crates/latkit/shaders/compute/probe.wesl +++ b/crates/latkit/shaders/compute/probe.wesl @@ -16,21 +16,20 @@ // view maps to a table row using its record of that frame. import package::lib::types::{ - NONE, FADE_OUT, POINTS, STROKES, TRIANGLES, MARK_EDGES, PATH_GEOMETRY, PROBE_KEY, PROBE_INSTANCE, PROBE_DEPTH, - PROBE_X, PROBE_Y, PROBE_UNDER, PROBE_ROW, PROBE_SEGMENT, PROBE_LOCATE_X, PROBE_LOCATE_Y, PROBE_LOCATED, - STEP_NEAREST, STEP_TOPMOST, STEP_DESCRIBE, STEP_HITS, STEP_BOXED, STEP_LOCATE, STEP_UNDER, HIT_WORDS, item, + NONE, FADE_OUT, POINTS, STROKES, MARK_EDGES, PROBE_KEY, PROBE_INSTANCE, PROBE_DEPTH, PROBE_X, PROBE_Y, + PROBE_UNDER, PROBE_ROW, PROBE_SEGMENT, PROBE_LOCATE_X, PROBE_LOCATE_Y, PROBE_LOCATED, STEP_NEAREST, STEP_TOPMOST, + STEP_DESCRIBE, STEP_HITS, STEP_BOXED, STEP_LOCATE, STEP_UNDER, HIT_WORDS, item, }; import package::lib::bind::{frame, eye, mark, word}; import package::lib::state::row_at; import package::lib::project::{to_screen, clip_of}; import package::lib::points::{point, port_at}; -import package::lib::path::{ - PATH, Segment, segment, path_segment, segment_screen, stroke_style, stroke_instances, path_count, -}; -import package::lib::triangles::{triangle_corner, triangle_item, triangle_instances, area_anchor}; +import package::lib::path::{segment, segment_screen, stroke_style, stroke_instances, path_count}; +import package::lib::triangles::{triangle_corner, triangle_item, triangle_instances}; import package::lib::marker::outline_distance; import package::lib::mark::PRIMITIVE; -import package::lib::drawn::{drawn_row, held_row}; +import package::lib::drawn::drawn_row; +import package::lib::center::{row_center, Center}; // Which step a dispatch takes. struct Step { @@ -120,21 +119,15 @@ fn seen_point(instance: u32) -> Seen { return s; } -// A stroke segment: the instance's, or when locating, the middle segment of drawn row `instance`'s -// path. -fn seen_segment(instance: u32, locating: bool) -> Seen { +// A stroke segment. +fn seen_segment(instance: u32) -> Seen { var s: Seen; - var g: Segment; - if locating { - g = path_segment(instance, path_count(instance) / 2u, 0.0); - } else { - g = segment(instance); - } + let g = segment(instance); s.item = g.item; s.row = g.row; s.part = g.index; s.kept = answers(g.item, g.row, g.leaving); - if !s.kept && !locating { + if !s.kept { return s; } let style = stroke_style(g.row); @@ -147,14 +140,14 @@ fn seen_segment(instance: u32, locating: bool) -> Seen { return s; } -// A triangle: a face, or one of an area's. When locating, drawn row `instance`'s face. -fn seen_triangle(instance: u32, locating: bool) -> Seen { +// A triangle: a face, or one of an area's. +fn seen_triangle(instance: u32) -> Seen { var s: Seen; - s.item = select(triangle_item(instance), instance, locating); + s.item = triangle_item(instance); let d = drawn_row(s.item); s.row = d.row; s.kept = answers(s.item, s.row, d.leaving); - if !s.kept && !locating { + if !s.kept { return s; } let ka = triangle_corner(instance, 0u); @@ -173,12 +166,12 @@ fn seen_triangle(instance: u32, locating: bool) -> Seen { return s; } -// Instance `instance` of the mark's primitive on screen; when locating, drawn row `instance`. -fn seen(instance: u32, locating: bool) -> Seen { +// Instance `instance` of the mark's primitive on screen. +fn seen(instance: u32) -> Seen { switch PRIMITIVE { case POINTS: { return seen_point(instance); } - case STROKES: { return seen_segment(instance, locating); } - default: { return seen_triangle(instance, locating); } + case STROKES: { return seen_segment(instance); } + default: { return seen_triangle(instance); } } } @@ -260,23 +253,6 @@ fn boxed(s: Seen) -> bool { } } -// Where area held row `row` lies on screen (its label anchor), with z 1 where it shows. -fn anchored(row: u32) -> vec3f { - let anchor = area_anchor(row); - let clip = clip_of(anchor.world); - return vec3f(to_screen(clip), f32(anchor.shown && anchor.ahead && clip.w > 0.0)); -} - -// Where the located row lies on screen, with z 1 where it shows: a point's center, the middle of a -// path's middle segment, or a face's centroid. -fn located(s: Seen) -> vec3f { - switch PRIMITIVE { - case POINTS: { return vec3f(s.a.xy, f32(s.shown && s.front.x)); } - case STROKES: { return vec3f(0.5 * (s.a.xy + s.b.xy), f32(s.front.x)); } - default: { return vec3f((s.a.xy + s.b.xy + s.c.xy) / 3.0, f32(all(s.front))); } - } -} - fn drawn_depth(texel: vec2i) -> f32 { return textureLoad(depth, texel, 0); } @@ -328,11 +304,15 @@ fn append(r: Reach) { atomicStore(&probe[entry + 5u], bitcast(r.depth)); } -// Writes where the located row lies. -fn locate(at: vec3f) { - atomicStore(&probe[PROBE_LOCATE_X], bitcast(at.x)); - atomicStore(&probe[PROBE_LOCATE_Y], bitcast(at.y)); - atomicStore(&probe[PROBE_LOCATED], u32(at.z)); +// Writes where drawn row `item` lies on screen, and whether it shows there. +fn locate(item: u32) { + var center = Center(vec2f(0.0), false); + if item < mark.rows { + center = row_center(item); + } + atomicStore(&probe[PROBE_LOCATE_X], bitcast(center.at.x)); + atomicStore(&probe[PROBE_LOCATE_Y], bitcast(center.at.y)); + atomicStore(&probe[PROBE_LOCATED], u32(center.shown)); } @compute @workgroup_size(256) @@ -348,23 +328,15 @@ fn answer(@builtin(global_invocation_id) id: vec3u) { atomicStore(&probe[PROBE_UNDER], bitcast(under())); return; } - // Locating reads the drawn row the query names. Describing reads the topmost instance the hover - // found, if it belongs to this mark. Every step that reads an instance goes through the single - // `seen` call below. - let locating = s == STEP_LOCATE; + // Locating reads the drawn row the query names. + if s == STEP_LOCATE { + locate(bitcast(frame.query.x)); + return; + } + // Describing reads the topmost instance the hover found, if it belongs to this mark. Every step + // that reads an instance goes through the single `seen` call below. var i = item(id); - if locating { - i = bitcast(frame.query.x); - if i >= mark.rows { - locate(vec3f(0.0)); - return; - } - // An area is located at its label anchor. - if PRIMITIVE == TRIANGLES && PATH == PATH_GEOMETRY { - locate(anchored(held_row(i))); - return; - } - } else if s == STEP_DESCRIBE { + if s == STEP_DESCRIBE { let key = atomicLoad(&probe[PROBE_KEY]); let instance = atomicLoad(&probe[PROBE_INSTANCE]); if key == NONE || instance == 0u || 255u - (key & 255u) != min(mark.index, 255u) { @@ -374,11 +346,7 @@ fn answer(@builtin(global_invocation_id) id: vec3u) { } else if i >= instances() { return; } - let drawn = seen(i, locating); - if locating { - locate(located(drawn)); - return; - } + let drawn = seen(i); if s == STEP_BOXED { if boxed(drawn) { atomicOr(&probe[mark.query + drawn.row / 32u], 1u << (drawn.row % 32u)); diff --git a/crates/latkit/shaders/draw/glyphs.wesl b/crates/latkit/shaders/draw/glyphs.wesl index 38140fc8..97995abe 100644 --- a/crates/latkit/shaders/draw/glyphs.wesl +++ b/crates/latkit/shaders/draw/glyphs.wesl @@ -13,7 +13,7 @@ import package::lib::read::missing_bits; import package::lib::points::{shown_point, port_at, port_inward}; import package::lib::marker::theme_port_size; import package::lib::label::{anchor, length_px, legibility, text_at, fits, Text}; -import package::lib::project::culled; +import package::lib::project::{culled, to_clip}; import package::lib::color::output; import package::lib::drawn::drawn_row; @@ -37,8 +37,7 @@ fn quad(t: Text, corner: u32, color: vec4f) -> Varyings { if frame.space != SPACE_DIAGRAM { p = round(p * eye.pixel_ratio) / eye.pixel_ratio; } - let ndc = p / eye.size * 2.0 - 1.0; - out.clip = vec4f(ndc.x, -ndc.y, 0.0, 1.0); + out.clip = to_clip(p, vec4f(0.0, 0.0, 0.0, 1.0)); out.uv = mix(t.uv_lo, t.uv_hi, q); out.color = color; out.scale = t.scale; diff --git a/crates/latkit/shaders/draw/overlay.wesl b/crates/latkit/shaders/draw/overlay.wesl index e78479fa..0142f112 100644 --- a/crates/latkit/shaders/draw/overlay.wesl +++ b/crates/latkit/shaders/draw/overlay.wesl @@ -5,6 +5,7 @@ import package::lib::types::{Sprite, SPRITE_TEXT, SPRITE_BOX, SPRITE_FILL, TEXT_SPREAD}; import package::lib::bind::{eye, ramp}; import package::lib::mark::coverage; +import package::lib::project::to_clip; import package::lib::color::output; @group(2) @binding(7) var sprites: array; @@ -31,8 +32,7 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) sprite: u let along = mix(s.a - dir * reach, s.b + dir * reach, quad.x); p = along + vec2f(-dir.y, dir.x) * (quad.y * 2.0 - 1.0) * reach; } - let ndc = p / eye.size * 2.0 - 1.0; - return Varyings(vec4f(ndc.x, -ndc.y, 0.0, 1.0), p, sprite); + return Varyings(to_clip(p, vec4f(0.0, 0.0, 0.0, 1.0)), p, sprite); } @fragment diff --git a/crates/latkit/shaders/lib/center.wesl b/crates/latkit/shaders/lib/center.wesl new file mode 100644 index 00000000..943a5774 --- /dev/null +++ b/crates/latkit/shaders/lib/center.wesl @@ -0,0 +1,59 @@ +// Where a drawn row lies on screen: the point a screen query locates a row at, and the point its +// label anchors to. + +import package::lib::types::{POINTS, STROKES, PATH_TRACE, PATH_GEOMETRY}; +import package::lib::mark::PRIMITIVE; +import package::lib::project::{to_screen, clip_of}; +import package::lib::points::{point, Point}; +import package::lib::path::{PATH, path_segment, segment_screen, stroke_style, path_count}; +import package::lib::triangles::{triangle_corner, area_anchor, Corner}; +import package::lib::drawn::held_row; + +// A row's center on screen in CSS pixels, and whether the row shows there. +struct Center { + at: vec2f, + shown: bool, +} + +// Point `p`'s center, shown when its row is visible and placed, in front of the eye. +fn point_center(p: Point) -> Center { + return Center(to_screen(p.clip), p.shown); +} + +// Corner `c` on screen, shown when it has a position, in front of the eye and ahead of the globe's +// horizon. +fn corner_center(c: Corner) -> Center { + let clip = clip_of(c.world); + return Center(to_screen(clip), c.shown && c.ahead && clip.w > 0.0); +} + +// Where drawn row `item` lies on screen: a point's center; the middle of a path's middle segment, +// or a trace's last sample; an area's label anchor, inside its largest polygon; or a face's +// centroid. The row shows there when it is visible and that point is on screen. +fn row_center(item: u32) -> Center { + switch PRIMITIVE { + case POINTS: { + return point_center(point(held_row(item))); + } + case STROKES: { + let count = path_count(item); + let last = PATH == PATH_TRACE; + let s = path_segment(item, select(count / 2u, max(count, 1u) - 1u, last), 0.0); + let on = segment_screen(s); + let shown = count > 0u && on.shown && stroke_style(s.row).shown; + return Center(select(0.5 * (on.a + on.b), on.b, last), shown); + } + default: { + if PATH == PATH_GEOMETRY { + return corner_center(area_anchor(held_row(item))); + } + var center = Center(vec2f(0.0), true); + for (var k = 0u; k < 3u; k++) { + let c = corner_center(triangle_corner(item, k)); + center.at += c.at / 3.0; + center.shown = center.shown && c.shown; + } + return center; + } + } +} diff --git a/crates/latkit/shaders/lib/label.wesl b/crates/latkit/shaders/lib/label.wesl index 86d53e08..7b81397d 100644 --- a/crates/latkit/shaders/lib/label.wesl +++ b/crates/latkit/shaders/lib/label.wesl @@ -5,15 +5,13 @@ // text scales with the sheet and fades out once too small to read. import package::lib::types::{ - POINTS, STROKES, MARKER_BLOCK, SIZED, GROUP, PATH_TRACE, PATH_GEOMETRY, TEXT_BASE, TEXT_SPREAD, HEADER_BAND, - SPACE_DIAGRAM, + POINTS, MARKER_BLOCK, SIZED, GROUP, PATH_TRACE, TEXT_BASE, TEXT_SPREAD, HEADER_BAND, SPACE_DIAGRAM, }; import package::lib::bind::{frame, eye, mark}; import package::lib::mark::PRIMITIVE; -import package::lib::project::{to_screen, clip_of}; import package::lib::points::point; -import package::lib::path::{PATH, Segment, segment_screen, path_count}; -import package::lib::triangles::{triangle_corner, area_anchor}; +import package::lib::path::PATH; +import package::lib::center::{row_center, point_center}; import package::lib::drawn::held_row; // A label's anchor point in CSS pixels, its alignment there (0 left or top, 0.5 centered), the room @@ -30,67 +28,38 @@ fn length_px(size: f32) -> f32 { return size * frame.unit; } -// The label anchor of drawn row `item`. +// The label anchor of drawn row `item`: centered on the row's center, except beside a point's +// marker, inside a block or a sized marker, in a group's header band, and beside a trace's last +// sample. fn anchor(item: u32) -> Anchor { - var a: Anchor; - let row = held_row(item); - switch PRIMITIVE { - case POINTS: { - let p = point(row); - a.shown = p.shown; - let center = to_screen(p.clip); - let text = length_px(mark.label_size); - if (mark.flags & GROUP) != 0u { - // A group's title sits in its header band, inset from the left. - let header = text * HEADER_BAND; - a.at = center + vec2f(-p.half.x + frame.group_padding, -p.half.y + 0.5 * header); - a.align = vec2f(0.0, 0.5); - a.room = vec2f(2.0 * (p.half.x - frame.group_padding), header); - } else if mark.marker == MARKER_BLOCK || (mark.flags & SIZED) != 0u { - // A block, or a marker with a width and height, shows its label inside if it fits. - a.at = center; - a.align = vec2f(0.5); - a.room = vec2f(2.0 * p.half.x - text, 2.0 * p.half.y); - } else { - a.at = center + vec2f(p.half.x + 0.4 * text, 0.0); - a.align = vec2f(0.0, 0.5); - } - } - case STROKES: { - let count = path_count(item); - // A trace labels its last sample; other paths their middle. - let last = PATH == PATH_TRACE; - let index = select(count / 2u, max(count, 1u) - 1u, last); - let on = segment_screen(Segment(row, item, index, 0.0, 1.0, 1.0, false)); - a.shown = count > 0u && on.shown; - if last { - a.at = on.b + vec2f(6.0, 0.0); - a.align = vec2f(0.0, 0.5); - } else { - a.at = 0.5 * (on.a + on.b); - a.align = vec2f(0.5); - } - } - default: { - a.align = vec2f(0.5); - if PATH == PATH_GEOMETRY { - // An area's label sits inside its largest polygon, farthest from its edges. - let c = area_anchor(row); - let clip = clip_of(c.world); - a.shown = c.shown && c.ahead && clip.w > 0.0; - a.at = to_screen(clip); - return a; - } - var sum = vec2f(0.0); - a.shown = true; - for (var k = 0u; k < 3u; k++) { - let c = triangle_corner(item, k); - let clip = clip_of(c.world); - a.shown = a.shown && c.shown && c.ahead && clip.w > 0.0; - sum += to_screen(clip); - } - a.at = sum / 3.0; + var a = Anchor(vec2f(0.0), vec2f(0.5), vec2f(0.0), false); + if PRIMITIVE != POINTS { + let c = row_center(item); + a.at = c.at; + a.shown = c.shown; + if PATH == PATH_TRACE { + a.at.x += 6.0; + a.align = vec2f(0.0, 0.5); } + return a; + } + let p = point(held_row(item)); + let c = point_center(p); + a.shown = c.shown; + let text = length_px(mark.label_size); + if (mark.flags & GROUP) != 0u { + // A group's title sits in its header band, inset from the left. + let header = text * HEADER_BAND; + a.at = c.at + vec2f(-p.half.x + frame.group_padding, -p.half.y + 0.5 * header); + a.align = vec2f(0.0, 0.5); + a.room = vec2f(2.0 * (p.half.x - frame.group_padding), header); + } else if mark.marker == MARKER_BLOCK || (mark.flags & SIZED) != 0u { + // A block, or a marker with a width and height, shows its label inside if it fits. + a.at = c.at; + a.room = vec2f(2.0 * p.half.x - text, 2.0 * p.half.y); + } else { + a.at = c.at + vec2f(p.half.x + 0.4 * text, 0.0); + a.align = vec2f(0.0, 0.5); } return a; } diff --git a/crates/latkit/shaders/lib/read.wesl b/crates/latkit/shaders/lib/read.wesl index a910aa29..956e363b 100644 --- a/crates/latkit/shaders/lib/read.wesl +++ b/crates/latkit/shaders/lib/read.wesl @@ -28,6 +28,22 @@ fn column_word(column: u32, word: u32) -> u32 { return program[mark.columns + column * COLUMN_WORDS + word]; } +// Binary search: where the last of `count` items of `stride` words at `at` in `program` whose word +// `key` is at most `value` starts; the first item when none is. +fn last_at_most(at: u32, count: u32, stride: u32, key: u32, value: u32) -> u32 { + var lo = 0u; + var hi = count; + while hi - lo > 1u { + let mid = (lo + hi) / 2u; + if program[at + mid * stride + key] <= value { + lo = mid; + } else { + hi = mid; + } + } + return at + lo * stride; +} + fn page_word(block: u32, offset: u32, fallback: u32) -> u32 { if block == NONE { return fallback; diff --git a/crates/latkit/shaders/lib/trace.wesl b/crates/latkit/shaders/lib/trace.wesl index 253374de..2ed94ce3 100644 --- a/crates/latkit/shaders/lib/trace.wesl +++ b/crates/latkit/shaders/lib/trace.wesl @@ -17,7 +17,7 @@ import package::lib::types::{NONE, MISSING, COLUMN_TABLE}; import package::lib::bind::{program, mark, word}; -import package::lib::read::{column_word, page_of}; +import package::lib::read::{column_word, page_of, last_at_most}; const HEADER: u32 = 4u; const TRACE_RUN_WORDS: u32 = 4u; @@ -27,22 +27,6 @@ fn trace_table() -> u32 { return column_word(mark.trace, COLUMN_TABLE); } -// Binary search: where the last of `count` items of `stride` words at `at` whose word `key` is at -// most `value` starts. -fn last_at_most(at: u32, count: u32, stride: u32, key: u32, value: u32) -> u32 { - var lo = 0u; - var hi = count; - while hi - lo > 1u { - let mid = (lo + hi) / 2u; - if program[at + mid * stride + key] <= value { - lo = mid; - } else { - hi = mid; - } - } - return at + lo * stride; -} - // A run of the trace: its level's index, its first entry, its entry count, and its first point. struct TraceRun { slot: u32, diff --git a/crates/latkit/src/data/bytes.rs b/crates/latkit/src/data/bytes.rs index 9bf05884..9010a71c 100644 --- a/crates/latkit/src/data/bytes.rs +++ b/crates/latkit/src/data/bytes.rs @@ -45,6 +45,7 @@ use super::samples::{self, MAX_LEVEL, Run, Samples}; use super::table::{Field, Reference, Table}; use super::text::Text; use super::values::{Array, Values}; +use crate::wire::{In, Out}; use crate::{Error, Result}; const MAGIC: &[u8; 4] = b"LATK"; @@ -93,86 +94,85 @@ const STORE_TEXT: u8 = 6; const STORE_GEOMETRY: u8 = 7; pub(crate) fn encode(patch: &Patch) -> Vec { - let mut w = Writer(Vec::new()); - w.0.extend_from_slice(MAGIC); - w.0.extend_from_slice(&[0; 8]); - w.str(patch.data.as_deref().unwrap_or("")); - w.u32(patch.ops.len()); + let mut w = Writer(Out::default()); + w.0.0.extend_from_slice(MAGIC); + w.0.u64(0); + w.0.str(patch.data.as_deref().unwrap_or("")); + w.0.len(patch.ops.len()); for op in &patch.ops { match op { Op::Axis { name, unit } => { - w.u8(OP_AXIS); - w.str(name); - w.str(unit); + w.0.u8(OP_AXIS); + w.0.str(name); + w.0.str(unit); } Op::Replace(table) => { - w.u8(OP_REPLACE); + w.0.u8(OP_REPLACE); w.table(table); } Op::Set(table) => { - w.u8(OP_SET); + w.0.u8(OP_SET); w.table(table); } Op::Remove { table, ids } => { - w.u8(OP_REMOVE); - w.str(table); + w.0.u8(OP_REMOVE); + w.0.str(table); w.ids(ids); } Op::Append { table, at, ids, fields } => { - w.u8(OP_APPEND); - w.str(table); + w.0.u8(OP_APPEND); + w.0.str(table); match ids { Some(ids) => { - w.u8(1); + w.0.u8(1); w.ids(ids); } - None => w.u8(0), + None => w.0.u8(0), } - w.u32(at.len()); - for &t in at { - w.f64(t); - } - w.u32(fields.len()); + w.0.len(at.len()); + w.0.pod(at); + w.0.len(fields.len()); for (name, values) in fields { - w.str(name); + w.0.str(name); w.values(values); } } Op::Forget { before } => { - w.u8(OP_FORGET); - w.f64(*before); + w.0.u8(OP_FORGET); + w.0.f64(*before); } Op::Frames { table, field, run, at, values } => { - w.u8(OP_FRAMES); - w.str(table); - w.str(field); + w.0.u8(OP_FRAMES); + w.0.str(table); + w.0.str(field); let per_row = run.len() * samples::sides(run.level); - w.u32(values.len().checked_div(per_row).unwrap_or(0)); + w.0.len(values.len().checked_div(per_row).unwrap_or(0)); w.run(run, at, values); } } } - let length = w.0.len() as u64; - w.0[MAGIC.len()..HEADER].copy_from_slice(&length.to_le_bytes()); - w.0 + let mut bytes = w.0.0; + let length = bytes.len() as u64; + bytes[MAGIC.len()..HEADER].copy_from_slice(&length.to_le_bytes()); + bytes } /// Encodes `values` alone, in the format's `values` layout. Jobs use it to pass columns to and /// from the host. #[cfg(feature = "gpu")] pub(crate) fn values_bytes(values: &Values) -> Vec { - let mut w = Writer(Vec::new()); + let mut w = Writer(Out::default()); w.values(values); - w.0 + w.0.0 } /// Decodes values written by [`values_bytes`]. Errors if `bytes` are malformed or extend past /// the values. #[cfg(feature = "gpu")] pub(crate) fn read_values(bytes: &[u8]) -> Result { - let mut r = Reader { bytes, at: 0, remaining: DecodeLimits::default().decoded_bytes }; + let mut r = Reader { input: In::new(bytes), remaining: DecodeLimits::default().decoded_bytes }; let values = r.values()?; - if r.at != bytes.len() { + if !r.input.is_empty() { return Err(malformed("trailing bytes")); } Ok(values) @@ -255,10 +255,10 @@ fn length(bytes: &[u8], limit: usize) -> Result> { /// `bytes` holds only part of the frame. fn frame(bytes: &[u8], limits: DecodeLimits) -> Result> { let Some(length) = length(bytes, limits.frame_bytes)? else { return Ok(None) }; - let Some(frame) = bytes.get(..length) else { return Ok(None) }; - let mut r = Reader { bytes: frame, at: HEADER, remaining: limits.decoded_bytes }; + let Some(frame) = bytes.get(HEADER..length) else { return Ok(None) }; + let mut r = Reader { input: In::new(frame), remaining: limits.decoded_bytes }; let patch = r.patch()?; - if r.at != frame.len() { + if !r.input.is_empty() { return Err(malformed("a frame longer than its patch")); } Ok(Some((patch, length))) @@ -314,70 +314,43 @@ fn too_long(limit: usize) -> Error { Error::Limit(format!("a patch longer than {limit} bytes").into()) } -struct Writer(Vec); +/// Writes the format's parts. +struct Writer(Out); impl Writer { - fn u8(&mut self, v: u8) { - self.0.push(v); - } - - fn u32(&mut self, v: usize) { - self.0.extend_from_slice(&u32::try_from(v).expect("counts fit in u32").to_le_bytes()); - } - - fn u64(&mut self, v: u64) { - self.0.extend_from_slice(&v.to_le_bytes()); - } - - fn f64(&mut self, v: f64) { - self.0.extend_from_slice(&v.to_le_bytes()); - } - - fn str(&mut self, s: &str) { - self.u32(s.len()); - self.0.extend_from_slice(s.as_bytes()); - } - - fn slice(&mut self, values: &[T]) { - #[cfg(target_endian = "little")] - self.0.extend_from_slice(bytemuck::cast_slice(values)); - #[cfg(target_endian = "big")] - compile_error!("latkit's byte format requires a little-endian target"); - } - fn table(&mut self, table: &Table) { - self.str(&table.name); + self.0.str(&table.name); match &table.ids { RowIds::Count(n) => { - self.u8(0); - self.u32(*n as usize); + self.0.u8(0); + self.0.u32(*n); } RowIds::Listed(column) => { - self.u8(1); + self.0.u8(1); self.values(&column.values()); } } - self.u32(table.fields.len()); + self.0.len(table.fields.len()); for (name, field) in &table.fields { - self.str(name); + self.0.str(name); match field { Field::Values(column, _) => { - self.u8(FIELD_VALUES); + self.0.u8(FIELD_VALUES); self.column(column); } Field::Reference(reference) => { - self.u8(FIELD_REFERENCE); - self.str(&reference.to); + self.0.u8(FIELD_REFERENCE); + self.0.str(&reference.to); self.column(&reference.ids); } Field::Sampled(samples, _) => { - self.u8(FIELD_SAMPLED); - self.u32(table.len()); - self.u64(samples.start()); - self.u64(samples.end() - samples.start()); - self.u8(samples.levels() as u8); + self.0.u8(FIELD_SAMPLED); + self.0.len(table.len()); + self.0.u64(samples.start()); + self.0.u64(samples.end() - samples.start()); + self.0.u8(samples.levels() as u8); let runs = samples.stored_runs(); - self.u32(runs.len()); + self.0.len(runs.len()); let rows: Vec = (0..table.len()).collect(); for run in &runs { let (at, values) = samples.read(run, &rows); @@ -391,21 +364,21 @@ impl Writer { /// Writes `run`: its level, first entry, and entry count, then its coordinates `at` and its /// `values`. fn run(&mut self, run: &Run, at: &[f64], values: &[f32]) { - self.u8(run.level as u8); - self.u64(run.entries.start); - self.u32(run.len()); - self.slice(at); - self.slice(values); + self.0.u8(run.level as u8); + self.0.u64(run.entries.start); + self.0.len(run.len()); + self.0.pod(at); + self.0.pod(values); } fn ids(&mut self, ids: &Ids) { match &ids.0 { IdList::Count(n) => { - self.u8(0); - self.u32(*n as usize); + self.0.u8(0); + self.0.u32(*n); } IdList::Listed(values) => { - self.u8(1); + self.0.u8(1); self.values(values); } } @@ -424,53 +397,56 @@ impl Writer { Array::U32(v) => self.numbers(STORE_U32, v), Array::U64(v) => self.numbers(STORE_U64, v), Array::Bool(bits) => { - self.u8(STORE_BOOL); - self.u32(len); - self.slice(&bits.to_words()); + self.0.u8(STORE_BOOL); + self.0.len(len); + self.0.pod(&bits.to_words()); } Array::Text(text) => { - self.u8(STORE_TEXT); - self.u32(len); + self.0.u8(STORE_TEXT); + self.0.len(len); let (offsets, bytes) = text.compact(); - self.slice(&offsets); - self.str(bytes); + self.0.pod(&offsets); + self.0.str(bytes); } Array::Geometry(geometry) => { - self.u8(STORE_GEOMETRY); - self.u32(len); - self.u8(geometry.depth()); + self.0.u8(STORE_GEOMETRY); + self.0.len(len); + self.0.u8(geometry.depth()); let (levels, coords) = geometry.compact(); for level in &levels { - self.u32(level.len()); - self.slice(level); + self.0.len(level.len()); + self.0.pod(level); } - self.u32(coords.len()); - self.slice(bytemuck::cast_slice::<[f64; 2], f64>(&coords)); + self.0.len(coords.len()); + self.0.pod(&coords); } } match &values.valid { Some(bits) => { - self.u8(1); - self.slice(&bits.to_words()); + self.0.u8(1); + self.0.pod(&bits.to_words()); } - None => self.u8(0), + None => self.0.u8(0), } } fn numbers(&mut self, store: u8, values: &[T]) { - self.u8(store); - self.u32(values.len()); - self.slice(values); + self.0.u8(store); + self.0.len(values.len()); + self.0.pod(values); } } /// Decodes one frame whose bytes are all present. `remaining` is the decoded-storage budget left. struct Reader<'a> { - bytes: &'a [u8], - at: usize, + input: In<'a>, remaining: usize, } +fn truncated() -> Error { + malformed("truncated") +} + impl Reader<'_> { fn charge(&mut self, count: usize, size: usize) -> Result<()> { let bytes = count.checked_mul(size).ok_or_else(|| malformed("decoded size overflow"))?; @@ -481,44 +457,34 @@ impl Reader<'_> { Ok(()) } - /// Errors as truncated unless `n` more bytes remain in the frame. - fn reach(&self, n: usize) -> Result<()> { - if self.at.saturating_add(n) > self.bytes.len() { - return Err(malformed("truncated")); - } - Ok(()) - } - fn take(&mut self, n: usize) -> Result<&[u8]> { - self.reach(n)?; - let end = self.at + n; - let slice = &self.bytes[self.at..end]; - self.at = end; - Ok(slice) + self.input.bytes(n).ok_or_else(truncated) } fn u8(&mut self) -> Result { - Ok(self.take(1)?[0]) + self.input.u8().ok_or_else(truncated) } fn u32(&mut self) -> Result { - Ok(u32::from_le_bytes(self.take(4)?.try_into().expect("four bytes"))) + self.input.u32().ok_or_else(truncated) } /// Reads a `u32` count of items of at least `size` bytes each. Errors if the remaining bytes /// cannot hold that many, so a corrupt count cannot cause a huge allocation. fn len(&mut self, size: usize) -> Result { let n = self.u32()? as usize; - self.reach(n.saturating_mul(size))?; + if !self.input.has(n.saturating_mul(size)) { + return Err(truncated()); + } Ok(n) } fn u64(&mut self) -> Result { - Ok(u64::from_le_bytes(self.take(8)?.try_into().expect("eight bytes"))) + self.input.u64().ok_or_else(truncated) } fn f64(&mut self) -> Result { - Ok(f64::from_le_bytes(self.take(8)?.try_into().expect("eight bytes"))) + self.input.f64().ok_or_else(truncated) } fn name(&mut self) -> Result { @@ -534,10 +500,10 @@ impl Reader<'_> { // Charge four times the raw size, to cover conversion into column pages and the sample // levels derived from runs. self.charge(n, 4 * size_of::())?; - let bytes = self.take(n.checked_mul(size_of::()).ok_or_else(|| malformed("too large"))?)?; - let mut out = vec![T::zeroed(); n]; - bytemuck::cast_slice_mut::(&mut out).copy_from_slice(bytes); - Ok(out) + if n.checked_mul(size_of::()).is_none() { + return Err(malformed("too large")); + } + self.input.pod(n).ok_or_else(truncated) } fn ids(&mut self) -> Result { diff --git a/crates/latkit/src/data/forms.rs b/crates/latkit/src/data/forms.rs index f1b9259f..090b7c9c 100644 --- a/crates/latkit/src/data/forms.rs +++ b/crates/latkit/src/data/forms.rs @@ -12,15 +12,13 @@ use serde::Deserialize; use serde::de::{self, DeserializeSeed, Deserializer, IgnoredAny, MapAccess, SeqAccess, Visitor}; use super::ids::Ids; -use super::name::Id; +use super::name::{Id, Name}; +use super::ordered::Ordered; use super::patch::{Op, Patch}; use super::table::Table; use super::values::{Value, Values}; use crate::{Error, Result}; -/// The largest float accepted as a numeric id, 2^53 − 1. Larger floats may already be rounded. -const SAFE: f64 = 9_007_199_254_740_991.0; - const NUMERIC_ID: &str = "a numeric id must be a whole number from 0 to 2^53 - 1"; /// A dataset's JSON document: its name and the patch that builds it. @@ -257,39 +255,41 @@ impl<'de> Deserialize<'de> for Id { impl OpForm { /// The field columns of a replace, set, or append; empty for other changes. - pub(crate) fn columns(&mut self) -> &mut [(String, Column)] { + pub(crate) fn columns(&mut self) -> &mut [(Name, Column)] { match self { OpForm::Replace(TableForm { fields, .. }) | OpForm::Set(TableForm { fields, .. }) - | OpForm::Append { fields, .. } => &mut fields.0, + | OpForm::Append { fields, .. } => fields.columns(), _ => &mut [], } } } -/// Builds the patch for `ops`. Every slice of a file must already be replaced by its values. +/// Builds the patch for `ops`. Every slice of a file must already be replaced by its values. Errors +/// name the part as JSON reading does, such as `[2].set.fields.x`. pub(crate) fn patch(ops: Vec) -> Result { let mut patch = Patch::new(); for (i, op) in ops.into_iter().enumerate() { let op = match op { OpForm::Axis { name, unit } => Op::Axis { name: name.into(), unit: unit.into() }, - OpForm::Replace(form) => Op::Replace(form.table().map_err(|e| e.at(i))?), - OpForm::Set(form) => Op::Set(form.table().map_err(|e| e.at(i))?), + OpForm::Replace(form) => Op::Replace(form.table().map_err(|e| e.at("replace").at(i))?), + OpForm::Set(form) => Op::Set(form.table().map_err(|e| e.at("set").at(i))?), OpForm::Remove { table, ids } => Op::Remove { table: table.into(), ids: ids.0 }, OpForm::Append { at, table, ids, fields } => { let at = match at { At::One(at) => vec![at], At::Many(at) => at, }; - let mut columns = Vec::with_capacity(fields.0.len()); - for (name, column) in fields.0 { + let fields = fields.into_columns(); + let mut columns = Vec::with_capacity(fields.len()); + for (name, column) in fields { let values = match column { Column::Reference { .. } => Err(Error::invalid("append records values, not references")), column => column.values(), }; columns.push(( name.as_str().into(), - values.map_err(|e| e.at(name.as_str()).at(table.as_str()).at(i))?, + values.map_err(|e| e.at(name.as_str()).at("fields").at("append").at(i))?, )); } Op::Append { table: table.into(), at, ids: ids.map(|ids| ids.0), fields: columns } @@ -309,11 +309,11 @@ impl TableForm { fn table(self) -> Result
    { let name = self.table; let mut table = Table::new(name.as_str(), self.ids.0); - for (field, column) in self.fields.0 { + for (field, column) in self.fields.into_columns() { table = match column { Column::Reference { to, ids } => table.reference(field, to, ids), column => { - let values = column.values().map_err(|e| e.at(field.as_str()).at(name.as_str()))?; + let values = column.values().map_err(|e| e.at(field.as_str()).at("fields"))?; table.field(field, values) } }; @@ -417,50 +417,34 @@ impl Column { } } -/// A table's fields in the order written. -#[derive(Default)] -pub(crate) struct Fields(pub(crate) Vec<(String, Column)>); +/// A table's fields in the order written, each name once. +#[derive(Default, Deserialize)] +#[serde(transparent)] +pub(crate) struct Fields(Ordered); -impl<'de> Deserialize<'de> for Fields { - fn deserialize>(deserializer: D) -> std::result::Result { - struct Map; - impl<'de> Visitor<'de> for Map { - type Value = Fields; - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("an object of fields") - } - fn visit_map>(self, mut map: A) -> std::result::Result { - let mut fields = Vec::new(); - while let Some(name) = map.next_key::()? { - if fields.iter().any(|(n, _)| *n == name) { - return Err(de::Error::custom(format!("field {name} appears twice"))); - } - let column = map.next_value_seed(ColumnSeed(&name))?; - fields.push((name, column)); - } - Ok(Fields(fields)) - } - } - deserializer.deserialize_map(Map) +impl Fields { + pub(crate) fn columns(&mut self) -> &mut [(Name, Column)] { + &mut self.0.0 } -} -/// Deserializes a column, naming its field in errors. -struct ColumnSeed<'a>(&'a str); + fn into_columns(self) -> Vec<(Name, Column)> { + self.0.0 + } +} -impl<'de> DeserializeSeed<'de> for ColumnSeed<'_> { - type Value = Column; - fn deserialize>(self, deserializer: D) -> std::result::Result { - deserializer.deserialize_any(ColumnVisitor(self.0)) +/// Reads a column. Errors name only what is wrong; reading names the field in the error's path. +impl<'de> Deserialize<'de> for Column { + fn deserialize>(deserializer: D) -> std::result::Result { + deserializer.deserialize_any(ColumnVisitor) } } -struct ColumnVisitor<'a>(&'a str); +struct ColumnVisitor; -impl<'de> Visitor<'de> for ColumnVisitor<'_> { +impl<'de> Visitor<'de> for ColumnVisitor { type Value = Column; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "field {}: an array of values, a reference, or a slice of a file", self.0) + f.write_str("an array of values, a reference, or a slice of a file") } fn visit_seq>(self, mut seq: A) -> std::result::Result { @@ -470,7 +454,7 @@ impl<'de> Visitor<'de> for ColumnVisitor<'_> { } Values::try_from(values).map(Column::Values).map_err(|e| { let reason = if let Error::Invalid { reason, .. } = e { reason } else { e.to_string().into() }; - de::Error::custom(format!("field {}: {reason}", self.0)) + de::Error::custom(reason) }) } @@ -486,11 +470,11 @@ impl<'de> Visitor<'de> for ColumnVisitor<'_> { "count" => count = Some(map.next_value::()?), other => { map.next_value::()?; - return Err(de::Error::custom(format!("field {}: unknown key {other}", self.0))); + return Err(de::Error::custom(format!("unknown key {other}"))); } } } - let missing = |what: &str| de::Error::custom(format!("field {}: {what}", self.0)); + let missing = |what: &str| de::Error::custom(what); match (reference, file) { (Some(to), None) if dtype.is_none() && offset.is_none() && count.is_none() => { Ok(Column::Reference { to, ids: ids.ok_or_else(|| missing("a reference needs ids"))? }) @@ -719,11 +703,7 @@ impl<'de> Deserialize<'de> for Key { Ok(Key(Some(Id::Number(n)))) } fn visit_f64(self, v: f64) -> std::result::Result { - if v.fract() == 0.0 && (0.0..=SAFE).contains(&v) { - Ok(Key(Some(Id::Number(v as u64)))) - } else { - Err(E::custom(NUMERIC_ID)) - } + Id::from_number(v).map(|id| Key(Some(id))).ok_or_else(|| E::custom(NUMERIC_ID)) } fn visit_str(self, v: &str) -> std::result::Result { Ok(Key(Some(Id::from(v)))) diff --git a/crates/latkit/src/data/frames.rs b/crates/latkit/src/data/frames.rs index d106b974..bbae7a6c 100644 --- a/crates/latkit/src/data/frames.rs +++ b/crates/latkit/src/data/frames.rs @@ -79,9 +79,8 @@ impl Manifest { #[serde(rename = "frames")] _frames: Vec, } - let text = std::str::from_utf8(bytes).ok()?; - crate::from_json::(text).ok()?; - let manifest = crate::from_json::(text).and_then(|m| { + crate::json::from_bytes::(bytes).ok()?; + let manifest = crate::json::from_bytes::(bytes).and_then(|m| { if !chunks(m.chunk) { return Err(Error::invalid("a span must hold a multiple of 8 entries, from 8 to 2^20").at("chunk")); } diff --git a/crates/latkit/src/data/json.rs b/crates/latkit/src/data/json.rs index 000b850c..f567606a 100644 --- a/crates/latkit/src/data/json.rs +++ b/crates/latkit/src/data/json.rs @@ -299,13 +299,15 @@ mod tests { let reading = Reading { host: Some(&host), ..Reading::default() }; pollster::block_on(Patch::read(json.as_bytes(), reading)).map(|_| ()).unwrap_err().to_string() }; - assert!( - error(r#"[{"replace": {"table": "Bus", "ids": {"count": 2}, "fields": {"x": [1, "a"]}}}]"#) - .contains("field x") - ); + let mixed = error(r#"[{"replace": {"table": "Bus", "ids": {"count": 2}, "fields": {"x": [1, "a"]}}}]"#); + assert!(mixed.starts_with("[0].replace.fields.x: "), "{mixed}"); assert!(error(r#"[{"bend": {}}]"#).contains("unknown variant")); let named = error(r#"[{"axis": {"name": "t", "unit": "s"}}, {"remove": {"table": 3, "ids": []}}]"#); assert!(named.starts_with("[1].remove.table"), "{named}"); + // Errors found after reading name the part the same way. + let reference = + error(r#"[{"append": {"table": "Bus", "at": 0, "fields": {"x": {"reference": "Other", "ids": [1]}}}}]"#); + assert!(reference.starts_with("[0].append.fields.x: append records values"), "{reference}"); assert!( error( r#"[{"replace": {"table": "Bus", "ids": {"count": 1}, "fields": {"x": {"file": "f", "count": 1}}}}]"# diff --git a/crates/latkit/src/data/name.rs b/crates/latkit/src/data/name.rs index b85cb589..1b7bf3d1 100644 --- a/crates/latkit/src/data/name.rs +++ b/crates/latkit/src/data/name.rs @@ -125,6 +125,21 @@ impl Id { Id::Text(t) => IdRef::Text(t), } } + + /// The id as a number every JSON reader represents exactly, as it serializes; `None` for a + /// text id or a number above 2^53 − 1. + pub fn exact_number(&self) -> Option { + match self { + Id::Number(n) if *n <= SAFE => Some(*n as f64), + _ => None, + } + } + + /// The id a JSON or JavaScript number names: a whole number from 0 to 2^53 − 1. `None` for any + /// other number, which may already be rounded. + pub fn from_number(number: f64) -> Option { + (number.fract() == 0.0 && (0.0..=SAFE as f64).contains(&number)).then_some(Id::Number(number as u64)) + } } /// The largest whole number every JSON reader represents exactly. diff --git a/crates/latkit/src/data/source.rs b/crates/latkit/src/data/source.rs index 2e76c0f6..31a2c4c7 100644 --- a/crates/latkit/src/data/source.rs +++ b/crates/latkit/src/data/source.rs @@ -2,11 +2,11 @@ //! [`Snapshot`], or [`Tiles`](crate::Tiles) fetched in pieces. use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; -use std::task::Waker; use super::name::Name; use super::patch::Patch; use super::snapshot::Snapshot; +use crate::pending::Watchers; use crate::{Error, Result}; /// A dataset that changes in place: named tables of columns, changed only by patches. Clones share @@ -34,9 +34,9 @@ pub struct Data(Arc); struct Shared { name: Name, current: Mutex, - /// A waker per view drawing the dataset, keyed by view id. Applying a patch wakes each one to - /// request a frame. - watchers: Mutex>, + /// A waker per view drawing the dataset, by view id. Applying a patch wakes each one to request + /// a frame. + watchers: Watchers, } impl Data { @@ -58,7 +58,7 @@ impl Data { let mut current = self.current(); *current = current.applied(patch.into())?; drop(current); - self.wake(); + self.0.watchers.wake_all(); Ok(()) } @@ -75,37 +75,19 @@ impl Data { return Err(Error::invalid(format!("a snapshot of '{}' restored to '{}'", snapshot.name(), self.name()))); } *self.current() = snapshot.clone(); - self.wake(); + self.0.watchers.wake_all(); Ok(()) } - /// Registers `waker` to request a frame from view `view` whenever a patch applies, replacing - /// that view's previous waker. + /// The views drawing the dataset, which applying a patch wakes to request a frame. #[cfg(feature = "gpu")] - pub(crate) fn watch(&self, view: u64, waker: Waker) { - let mut watchers = self.0.watchers.lock().unwrap_or_else(PoisonError::into_inner); - watchers.retain(|(v, _)| *v != view); - watchers.push((view, waker)); - } - - /// Removes view `view`'s waker. - #[cfg(feature = "gpu")] - pub(crate) fn unwatch(&self, view: u64) { - self.0.watchers.lock().unwrap_or_else(PoisonError::into_inner).retain(|(v, _)| *v != view); + pub(crate) fn watchers(&self) -> &Watchers { + &self.0.watchers } fn current(&self) -> MutexGuard<'_, Snapshot> { self.0.current.lock().unwrap_or_else(PoisonError::into_inner) } - - /// Wakes every view drawing the dataset to request a frame. - fn wake(&self) { - let wakers: Vec = - self.0.watchers.lock().unwrap_or_else(PoisonError::into_inner).iter().map(|(_, w)| w.clone()).collect(); - for waker in wakers { - waker.wake(); - } - } } impl From for Data { @@ -114,7 +96,7 @@ impl From for Data { Data(Arc::new(Shared { name: snapshot.name().clone(), current: Mutex::new(snapshot), - watchers: Mutex::default(), + watchers: Watchers::default(), })) } } diff --git a/crates/latkit/src/data/values.rs b/crates/latkit/src/data/values.rs index 31f9c169..da4c2a01 100644 --- a/crates/latkit/src/data/values.rs +++ b/crates/latkit/src/data/values.rs @@ -148,6 +148,10 @@ pub(crate) enum Picks<'a> { Writes { from: &'a [Option], kept: usize }, } +/// A run of values to copy: a source and its first row (consecutive rows follow), or `None` for +/// missing values, with the run's length. +type Run = (Option<(usize, usize)>, usize); + impl Picks<'_> { fn len(self) -> usize { match self { @@ -169,10 +173,9 @@ impl Picks<'_> { } } - /// The picks grouped into runs, in order. Each run is a source and its first row (consecutive - /// rows follow), or `None` for missing values, with the run's length. - fn runs(self) -> Vec<(Option<(usize, usize)>, usize)> { - let mut runs: Vec<(Option<(usize, usize)>, usize)> = Vec::new(); + /// The picks grouped into runs, in order. + fn runs(self) -> Vec { + let mut runs: Vec = Vec::new(); for i in 0..self.len() { let pick = self.get(i); match runs.last_mut() { @@ -260,35 +263,21 @@ impl Values { Values { array, valid: self.valid.as_ref().map(|v| v.slice(range)) } } - /// Maps every value, read as by `number` (`None` where missing), through `f`. One loop per - /// storage type, so the storage is matched once rather than per value. - pub(crate) fn map_numbers(&self, mut f: impl FnMut(Option) -> T) -> Vec { - macro_rules! each { - ($v:expr, $number:expr) => { - match &self.valid { - None => $v.iter().map(|&x| f(Some($number(x)))).collect(), - Some(valid) => $v.iter().enumerate().map(|(i, &x)| f(valid.get(i).then(|| $number(x)))).collect(), - } - }; - } - match &self.array { - Array::F64(v) => each!(v, |x: f64| x), - Array::F32(v) => each!(v, f64::from), - Array::I32(v) => each!(v, f64::from), - Array::U32(v) => each!(v, f64::from), - Array::U64(v) => each!(v, |x: u64| x as f64), - _ => (0..self.len()).map(|i| f(self.is_valid(i).then(|| self.number(i)))).collect(), - } + /// Maps every value, read as by `number` (`None` where missing), through `f`. + pub(crate) fn map_numbers(&self, f: impl FnMut(Option) -> T) -> Vec { + let mut out = Vec::with_capacity(self.len()); + self.extend_numbers(&mut out, f); + out } - /// Calls `f` with every value, read as by `number` (`None` where missing). One loop per storage - /// type, with no allocation. - pub(crate) fn each_number(&self, mut f: impl FnMut(Option)) { + /// Appends every value to `out`, read as by `number` (`None` where missing) and mapped through + /// `f`. One loop per storage type, so the storage is matched once rather than per value. + pub(crate) fn extend_numbers(&self, out: &mut Vec, mut f: impl FnMut(Option) -> T) { macro_rules! each { ($v:expr, $number:expr) => { match &self.valid { - None => $v.iter().for_each(|&x| f(Some($number(x)))), - Some(valid) => $v.iter().enumerate().for_each(|(i, &x)| f(valid.get(i).then(|| $number(x)))), + None => out.extend($v.iter().map(|&x| f(Some($number(x))))), + Some(valid) => out.extend($v.iter().enumerate().map(|(i, &x)| f(valid.get(i).then(|| $number(x))))), } }; } @@ -298,7 +287,7 @@ impl Values { Array::I32(v) => each!(v, f64::from), Array::U32(v) => each!(v, f64::from), Array::U64(v) => each!(v, |x: u64| x as f64), - _ => (0..self.len()).for_each(|i| f(self.is_valid(i).then(|| self.number(i)))), + _ => out.extend((0..self.len()).map(|i| f(self.is_valid(i).then(|| self.number(i))))), } } @@ -341,19 +330,67 @@ impl Values { Values { array: Array::Geometry(geometry), valid: None } } + /// Geometry of `depth` copying each of `rows`: a row of a geometry, or `None` for an empty row, + /// which draws nothing. + pub(crate) fn of_rows<'g>(depth: u8, rows: impl IntoIterator>) -> Values { + let mut builder = GeometryBuilder::new(depth); + for row in rows { + match row { + Some((geometry, i)) => builder.push_from(geometry, i), + None => builder.push_empty(), + } + } + Values::of_geometry(builder.finish()) + } + + /// A field of `values` typed by what is present: numbers if every present value is a number, + /// booleans if every one is a boolean, and texts otherwise, with other values written as text. + /// GeoJSON and vector tiles type their properties so. + pub(crate) fn infer(values: &[Option]) -> Values { + let present = || values.iter().flatten(); + if present().all(|v| matches!(v, Value::Number(_))) { + let numbers = values.iter().map(|v| if let Some(Value::Number(n)) = v { Some(*n) } else { None }); + return numbers.collect::>().into(); + } + if present().all(|v| matches!(v, Value::Bool(_))) { + let booleans = values.iter().map(|v| if let Some(Value::Bool(b)) = v { Some(*b) } else { None }); + return booleans.collect::>().into(); + } + let text = |value: &Value| match value { + Value::Text(text) => text.to_string(), + Value::Number(n) => n.to_string(), + Value::Bool(b) => b.to_string(), + other => serde_json::to_string(other).unwrap_or_default(), + }; + values.iter().map(|v| v.as_ref().map(text)).collect::>>().into() + } + /// The values `picks` names among `sources`, missing where it names none. /// /// The result keeps the first source's storage; numbers from other sources convert to it. A /// source of another kind is an error. pub(crate) fn assemble(sources: &[&Values], picks: Picks<'_>) -> Result { - let len = picks.len(); - let like = sources.first().expect("a source"); - let kind = like.kind(); + let kind = sources.first().expect("a source").kind(); if let Some(other) = sources.iter().find(|s| s.kind() != kind) { return Err(Error::invalid(format!("{:?} values for a {kind:?} field", other.kind()))); } // Consecutive rows from one source copy as a run. - let runs = picks.runs(); + Ok(Values::of_runs(sources, &picks.runs())) + } + + /// `parts` concatenated in the first part's storage, converting numbers from other storages. + /// Joins a column's pages by copying each part whole. Every part must have the first's kind. + pub(crate) fn concat(parts: &[&Values]) -> Values { + debug_assert!(parts.iter().all(|p| p.kind() == parts[0].kind())); + let runs: Vec = parts.iter().enumerate().map(|(s, part)| (Some((s, 0)), part.len())).collect(); + Values::of_runs(parts, &runs) + } + + /// The values `runs` copy from `sources`, in the first source's storage; numbers from other + /// sources convert to it. + fn of_runs(sources: &[&Values], runs: &[Run]) -> Values { + let len = runs.iter().map(|&(_, n)| n).sum(); + let like = sources.first().expect("a source"); macro_rules! numbers { ($variant:ident, $t:ty, $store:expr) => {{ let cast: Vec = sources.iter().map(|s| s.cast(|x| x as $t, $store)).collect(); @@ -365,7 +402,7 @@ impl Values { }) .collect(); let mut out: Vec<$t> = Vec::with_capacity(len); - for &(run, n) in &runs { + for &(run, n) in runs { match run { Some((s, r)) => out.extend_from_slice(&typed[s][r..r + n]), None => out.resize(out.len() + n, <$t>::default()), @@ -388,16 +425,17 @@ impl Values { ), Array::Text(_) => { let mut builder = TextBuilder::with_capacity(len); - for &(run, n) in &runs { - for k in 0..n { - builder.push(run.and_then(|(s, r)| sources[s].text().map(|t| t.get(r + k))).unwrap_or("")); + for &(run, n) in runs { + match run.and_then(|(s, r)| Some((sources[s].text()?, r))) { + Some((text, r)) => (r..r + n).for_each(|i| builder.push(text.get(i))), + None => (0..n).for_each(|_| builder.push("")), } } Array::Text(builder.finish()) } Array::Geometry(g) => { let mut builder = GeometryBuilder::new(g.depth()); - for &(run, n) in &runs { + for &(run, n) in runs { match run.and_then(|(s, r)| Some((sources[s].geometry()?, r))) { Some((geometry, r)) => builder.push_rows(geometry, r..r + n), None => (0..n).for_each(|_| builder.push_empty()), @@ -406,22 +444,22 @@ impl Values { Array::Geometry(builder.finish()) } }; - // No validity bits are needed when no pick is missing and no picked source has missing + // No validity bits are needed when no run is missing and no source it reads has missing // values. let gaps = runs.iter().any(|(run, _)| run.is_none_or(|(s, _)| sources[s].valid.is_some())); if !gaps { - return Ok(Values { array, valid: None }); + return Values { array, valid: None }; } let mut bits = BitsBuilder::with_capacity(len); let mut all = true; - for &(run, n) in &runs { + for &(run, n) in runs { for k in 0..n { let valid = run.is_some_and(|(s, r)| sources[s].is_valid(r + k)); all &= valid; bits.push(valid); } } - Ok(Values { array, valid: (!all).then(|| bits.finish()) }) + Values { array, valid: (!all).then(|| bits.finish()) } } /// Whether `other` holds exactly these values in the same storage. Compares the stored @@ -468,64 +506,6 @@ impl Values { } } - /// `parts` concatenated in the first part's storage, converting numbers from other storages. - /// Joins a column's pages by copying each part whole. Every part must have the first's kind. - pub(crate) fn concat(parts: &[&Values]) -> Values { - let first = parts.first().expect("a part"); - debug_assert!(parts.iter().all(|p| p.kind() == first.kind())); - let len = parts.iter().map(|p| p.len()).sum(); - macro_rules! numbers { - ($variant:ident, $t:ty, $store:expr) => {{ - let mut out: Vec<$t> = Vec::with_capacity(len); - for part in parts { - match &part.cast(|x| x as $t, $store).array { - Array::$variant(v) => out.extend_from_slice(v), - _ => unreachable!("cast to the first part's storage"), - } - } - Array::$variant(Buffer::from(out)) - }}; - } - let array = match &first.array { - Array::F64(_) => numbers!(F64, f64, Store::F64), - Array::F32(_) => numbers!(F32, f32, Store::F32), - Array::I32(_) => numbers!(I32, i32, Store::I32), - Array::U32(_) => numbers!(U32, u32, Store::U32), - Array::U64(_) => numbers!(U64, u64, Store::U64), - Array::Bool(_) => { - let mut bits = BitsBuilder::with_capacity(len); - for part in parts { - if let Array::Bool(v) = &part.array { - (0..v.len()).for_each(|i| bits.push(v.get(i))); - } - } - Array::Bool(bits.finish()) - } - Array::Text(_) => { - let mut builder = TextBuilder::with_capacity(len); - for text in parts.iter().filter_map(|p| p.text()) { - (0..text.len()).for_each(|i| builder.push(text.get(i))); - } - Array::Text(builder.finish()) - } - Array::Geometry(g) => { - let mut builder = GeometryBuilder::new(g.depth()); - for geometry in parts.iter().filter_map(|p| p.geometry()) { - (0..geometry.len()).for_each(|i| builder.push_from(geometry, i)); - } - Array::Geometry(builder.finish()) - } - }; - let valid = parts.iter().any(|p| p.valid.is_some()).then(|| { - let mut bits = BitsBuilder::with_capacity(len); - for part in parts { - (0..part.len()).for_each(|i| bits.push(part.is_valid(i))); - } - bits.finish() - }); - Values { array, valid } - } - /// Numbers converted to `store`; other kinds unchanged. fn cast(&self, convert: impl Fn(f64) -> T, store: Store) -> Values where diff --git a/crates/latkit/src/derive/lanes.rs b/crates/latkit/src/derive/lanes.rs index 0f41f981..b3ab6d37 100644 --- a/crates/latkit/src/derive/lanes.rs +++ b/crates/latkit/src/derive/lanes.rs @@ -114,7 +114,7 @@ fn reference<'t>(table: &'t Table, name: &str) -> Option<&'t Reference> { /// The lane words in a lanes job's answer, each a little-endian `u32`. `None` for an empty answer, /// which means no rows share endpoints. fn answered(bytes: &[u8]) -> Option> { - (!bytes.is_empty()).then(|| bytes.as_chunks::<4>().0.iter().map(|&w| u32::from_le_bytes(w)).collect()) + (!bytes.is_empty()).then(|| crate::wire::words(bytes)) } /// Each row's lane word, as shaders read it, from the rows' endpoints: twice the lane plus `HALF` diff --git a/crates/latkit/src/derive/mod.rs b/crates/latkit/src/derive/mod.rs index 4a2eaee1..fda9659f 100644 --- a/crates/latkit/src/derive/mod.rs +++ b/crates/latkit/src/derive/mod.rs @@ -43,8 +43,13 @@ use places::Places; /// Hashes `inputs` into a stamp. Never 0, which means no value has been made yet. pub(crate) fn stamp_of(inputs: impl Hash) -> u64 { + stamp_with(|hasher| inputs.hash(hasher)) +} + +/// A stamp of what `hash` writes, for inputs that hash themselves into a hasher. +pub(crate) fn stamp_with(hash: impl FnOnce(&mut FxHasher)) -> u64 { let mut hasher = FxHasher::default(); - inputs.hash(&mut hasher); + hash(&mut hasher); hasher.finish().max(1) } @@ -66,16 +71,72 @@ impl Answer for Pending> { } } +/// Work under way: its answer, the stamp of the inputs it was started for, and the context its +/// answer is read with. Work that lands counts as done for its stamp, whether it succeeded or +/// failed, so no work for that stamp starts again: a job runs once for each set of inputs. Stamp 0 +/// is for work started on request rather than for inputs, which this does not track. +pub(crate) struct Working>> { + pending: Option<(u64, A, C)>, + /// The stamp of the work that landed last; 0 for none. + done: u64, +} + +impl Default for Working { + fn default() -> Self { + Working { pending: None, done: 0 } + } +} + +impl Working { + /// Whether work is pending. + pub(crate) fn busy(&self) -> bool { + self.pending.is_some() + } + + /// Whether work for `stamp` may start: none is pending, and none for `stamp` landed. + pub(crate) fn may_start(&self, stamp: u64) -> bool { + self.pending.is_none() && (stamp == 0 || stamp != self.done) + } + + /// The stamp of the pending work. + pub(crate) fn stamp(&self) -> Option { + self.pending.as_ref().map(|(stamp, ..)| *stamp) + } + + /// The context of the pending work. + pub(crate) fn context(&self) -> Option<&C> { + self.pending.as_ref().map(|(_, _, context)| context) + } + + /// Records `answer` as pending for `stamp`, read with `context`, replacing any pending work. + pub(crate) fn start(&mut self, stamp: u64, answer: A, context: C) { + self.pending = Some((stamp, answer, context)); + } + + /// Drops the pending work; its answer is ignored. + pub(crate) fn cancel(&mut self) { + self.pending = None; + } + + /// Takes finished work: its stamp, context, and bytes or error. + pub(crate) fn landed(&mut self) -> Option<(u64, C, Result>)> { + let answer = self.pending.as_ref()?.1.take()?; + let (stamp, _, context) = self.pending.take().expect("pending work"); + self.done = stamp; + Some((stamp, context, answer)) + } +} + /// A derived value, the stamp it was made from, and any pending work toward a newer stamp. pub(crate) struct Derived { pub(crate) value: T, stamp: u64, - working: Option<(u64, A)>, + working: Working<(), A>, } impl Default for Derived { fn default() -> Self { - Derived { value: T::default(), stamp: 0, working: None } + Derived { value: T::default(), stamp: 0, working: Working::default() } } } @@ -87,35 +148,31 @@ impl Derived { /// Whether `stamp` matches neither the value nor the pending work, so new work is needed. pub(crate) fn due(&self, stamp: u64) -> bool { - stamp != self.stamp && self.working.as_ref().is_none_or(|(s, _)| *s != stamp) + stamp != self.stamp && self.working.stamp() != Some(stamp) } /// Stores `value` as made from `stamp`, dropping any pending work. pub(crate) fn make(&mut self, stamp: u64, value: T) { self.value = value; - self.stamp = stamp; - self.working = None; + self.mark(stamp); } /// Marks the current value, unchanged, as made from `stamp`, dropping any pending work. pub(crate) fn mark(&mut self, stamp: u64) { self.stamp = stamp; - self.working = None; + self.working.cancel(); } /// Records `work` as pending toward `stamp`, replacing any pending work. pub(crate) fn start(&mut self, stamp: u64, work: A) { - self.working = Some((stamp, work)); + self.working.start(stamp, work, ()); } /// Takes the bytes of finished work and the stamp it was for. Failed work marks its stamp as /// made and keeps the old value, so it is not retried until the inputs change. pub(crate) fn landed(&mut self) -> Option<(u64, Vec)> { - let (stamp, work) = self.working.as_ref()?; - let stamp = *stamp; - let landed = work.take()?; - self.working = None; - let Ok(bytes) = landed else { + let (stamp, (), answer) = self.working.landed()?; + let Ok(bytes) = answer else { self.stamp = stamp; return None; }; @@ -124,7 +181,7 @@ impl Derived { /// Whether work is pending. pub(crate) fn busy(&self) -> bool { - self.working.is_some() + self.working.busy() } } @@ -509,6 +566,37 @@ pub(crate) fn answers(bytes: &[u8], fields: &[&str]) -> Vec<(Key, Vec usize { + self.0.load(Ordering::Relaxed) + } + } + + impl Host for Failing { + fn rasterize(&self, _: &TextRun) -> crate::Result { + Err(crate::Error::Unsupported("this host has no text".into())) + } + + fn spawn(&self, _: Job) -> Pending> { + self.0.fetch_add(1, Ordering::Relaxed); + Pending::ready(Err(crate::Error::Io("the worker stopped".into()))) + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/latkit/src/derive/places.rs b/crates/latkit/src/derive/places.rs index 5f5d90e7..565802f0 100644 --- a/crates/latkit/src/derive/places.rs +++ b/crates/latkit/src/derive/places.rs @@ -7,14 +7,16 @@ //! every unpositioned row again. The result is a table beside each vertex table, holding its rows' //! positions, given or laid out, which the vertex marks read. +use std::hash::Hash; + use rustc_hash::FxHashMap; use super::graph::{key_of, links, marks, position}; -use super::{Derive, Input, Jobs, Tables, answers, beside, kept}; +use super::{Derive, Input, Jobs, Tables, Working, answers, beside, kept, stamp_of, stamp_with}; +use crate::Layout; use crate::data::{Name, Row, Value, Values}; use crate::expr::cpu::Rows; use crate::host::{Job, Key, Placement}; -use crate::{Layout, Pending}; #[derive(Default)] pub(crate) struct Places { @@ -29,14 +31,14 @@ pub(crate) struct Places { arranging: bool, /// The result of the last arrange, until the view takes it. arranged: Option>, - placing: Option, + /// The layout job, for the stamp of the inputs it lays out. + placing: Working, /// Bumps whenever an answer arrives. landed: u64, } -/// A pending layout job: the generation it was started in, and the row each answer key names. +/// A layout job's context: the generation it was started in, and the row each answer key names. struct Placing { - pending: Pending>, generation: u64, rows: FxHashMap, } @@ -56,22 +58,24 @@ impl Derive for Places { // Repeat so a job the host finishes at once is taken before the frame draws. for _ in 0..4 { self.settle(); - let mut hasher = rustc_hash::FxHasher::default(); - std::hash::Hash::hash(&(input.scene_version, self.landed, self.generation), &mut hasher); - input.structure(&mut hasher); - let stamp = std::hash::Hasher::finish(&hasher).max(1); + let inputs = stamp_with(|hasher| { + (input.scene_version, self.generation).hash(hasher); + input.structure(hasher); + }); + // An answer changes the tables, but not what a layout would be asked to place. + let stamp = stamp_of((inputs, self.landed)); if stamp == self.stamp { break; } self.stamp = stamp; - self.build(input, jobs); + self.build(input, inputs, jobs); changed = true; } changed } fn busy(&self) -> bool { - self.placing.is_some() + self.placing.busy() } fn tables(&self) -> &Tables { @@ -90,7 +94,7 @@ impl Places { /// Lays out every row without a position again, dropping any pending layout. pub(crate) fn arrange(&mut self) { self.arranging = true; - self.placing = None; + self.placing.cancel(); self.generation += 1; self.stamp = 0; } @@ -101,8 +105,7 @@ impl Places { } fn settle(&mut self) { - let Some(answer) = self.placing.as_ref().and_then(|p| p.pending.take()) else { return }; - let placing = self.placing.take().expect("a layout under way"); + let Some((_, placing, answer)) = self.placing.landed() else { return }; if placing.generation == self.generation { let mut placed = Vec::new(); for (key, values) in answer.map(|bytes| answers(&bytes, &["x", "y"])).unwrap_or_default() { @@ -121,8 +124,9 @@ impl Places { self.landed += 1; } - /// Writes each vertex table's positions, starting a layout job for rows without one. - fn build(&mut self, input: &Input<'_>, jobs: &Jobs<'_>) { + /// Writes each vertex table's positions, starting a layout job for rows without one unless a + /// job for the same `inputs` already ran. + fn build(&mut self, input: &Input<'_>, inputs: u64, jobs: &Jobs<'_>) { let (vertex_marks, edge_marks) = marks(input); let mut keys = Vec::new(); let mut pins = Vec::new(); @@ -136,7 +140,7 @@ impl Places { } let arranging = self.arranging; let wanted = |i: usize| pins[i].is_none() && (arranging || !self.laid.contains_key(&keys[i])); - if self.placing.is_none() && (0..keys.len()).any(wanted) { + if self.placing.may_start(inputs) && (0..keys.len()).any(wanted) { let pinned: Vec> = (0..keys.len()) .map(|i| pins[i].or_else(|| (!arranging).then(|| self.laid.get(&keys[i]).copied()).flatten())) .collect(); @@ -159,8 +163,8 @@ impl Places { keys: &job_keys, }; let pending = jobs.spawn(Job::layout(&input.scene.layout, &placement)); - self.placing = Some(Placing { pending, generation: self.generation, rows }); - } else if self.arranging && self.placing.is_none() { + self.placing.start(inputs, pending, Placing { generation: self.generation, rows }); + } else if self.arranging && !self.placing.busy() { // Nothing to lay out: the arrange finishes with no rows. self.arranging = false; self.arranged = Some(Vec::new()); @@ -181,3 +185,43 @@ impl Places { self.tables = tables; } } + +#[cfg(test)] +mod tests { + use std::sync::{Arc, Mutex}; + use std::task::Waker; + + use super::*; + use crate::derive::testing::Failing; + use crate::gpu::work::Counters; + use crate::{Data, Ids, Plane, Scene, Stress, Table, Vertices}; + + #[test] + fn a_failed_layout_runs_once_until_its_inputs_change() { + let data = Data::new("g"); + data.apply(Table::new("V", Ids::count(3))).expect("valid"); + let data = data.snapshot(); + let scene = Scene::new(Plane::default()).layout(Stress::default()).draw(Vertices::of("g", "V")); + let (host, counters, errors) = (Failing::default(), Counters::default(), Arc::new(Mutex::new(Vec::new()))); + let mut places = Places::default(); + let frame = |places: &mut Places| { + let input = Input { + scene: &scene, + scene_version: 1, + data: std::slice::from_ref(&data), + host: Some(&host), + dragging: false, + paged: None, + }; + places.update(&input, &mut Jobs::new(Some(&host), Waker::noop(), &counters, &errors, u32::MAX)); + }; + for _ in 0..5 { + frame(&mut places); + } + assert_eq!(host.jobs(), 1, "one job for these inputs, not one per frame"); + assert_eq!(errors.lock().expect("unpoisoned").len(), 1, "its failure is reported once"); + places.arrange(); + frame(&mut places); + assert_eq!(host.jobs(), 2, "laying out again asks again"); + } +} diff --git a/crates/latkit/src/derive/pyramid.rs b/crates/latkit/src/derive/pyramid.rs index 16d02324..b7cf09ab 100644 --- a/crates/latkit/src/derive/pyramid.rs +++ b/crates/latkit/src/derive/pyramid.rs @@ -278,11 +278,8 @@ fn tie(row: u32) -> u32 { fn ranks(priority: &[f32], n: usize) -> Vec { (0..n) .map(|r| { - let p = priority.get(r).copied().filter(|p| !p.is_nan()).map_or(0, |p| { - let bits = p.to_bits(); - if bits >> 31 == 1 { !bits } else { bits | 0x8000_0000 } - }); - u64::from(p) << 32 | u64::from(tie(r as u32)) + let p = priority.get(r).copied().filter(|p| !p.is_nan()).map_or(0, crate::order::number32); + crate::order::pair(p, tie(r as u32)) }) .collect() } @@ -700,7 +697,7 @@ impl Pyramid { /// Decodes the pyramids a job answered, as [`encode`] wrote them, each with its table's index /// among those asked. Stops at the first incomplete record. pub(crate) fn read(bytes: &[u8]) -> Vec<(u32, Pyramid)> { - let words: Vec = bytes.as_chunks::<4>().0.iter().map(|&w| u32::from_le_bytes(w)).collect(); + let words = crate::wire::words(bytes); let mut words = words.as_slice(); let mut take = |n: usize| -> Option<&[u32]> { let (head, rest) = words.split_at_checked(n)?; diff --git a/crates/latkit/src/derive/shapes.rs b/crates/latkit/src/derive/shapes.rs index b7424516..4ddc65bd 100644 --- a/crates/latkit/src/derive/shapes.rs +++ b/crates/latkit/src/derive/shapes.rs @@ -17,14 +17,14 @@ use rustc_hash::{FxHashMap, FxHashSet}; use super::order::Order; -use super::{Input, Jobs, Paged, Tables, beside, stamp_of}; +use super::{Input, Jobs, Paged, Tables, Working, beside, stamp_of}; use crate::data::{ Column, Field, Geometry, GeometryBuilder, Id, Keyed, Name, Page, PageId, RowIds, Table, Values, Version, }; use crate::gpu::shaders::PAGE_ROWS; use crate::host::{Job, Shaping, shapes_answer}; use crate::tiles::TileSets; -use crate::{Mark, Pending, Scene, Space}; +use crate::{Mark, Scene, Space}; /// Estimated microseconds to triangulate one point. const TRIANGULATE: f64 = 0.5; @@ -56,8 +56,8 @@ impl Missing { fn parts(self, column: &Column) -> (Vec, Vec) { let (mut parts, mut asked) = (Vec::new(), Vec::new()); for (id, values) in self.pages { + asked.push(Asked::Page(id, values.len())); parts.push(values); - asked.push(Asked::Page(id)); } if !self.rows.is_empty() { let rows: Vec = self.rows.iter().map(|&(row, ..)| row).collect(); @@ -68,13 +68,32 @@ impl Missing { } } -/// What one part of a job shapes: a whole page of the field, by page id; or rows, by id, each with -/// its geometry digest. +/// What one part of a job shapes: a whole page of the field, by page id, with its row count; or +/// rows, by id, each with its geometry digest. +#[derive(Hash)] enum Asked { - Page(PageId), + Page(PageId, usize), Rows(Vec<(Id, u64)>), } +impl Asked { + /// Empty shapes of depth `depth` for every row this part asks for. + fn empty(&self, depth: u8) -> Values { + empty_shapes( + depth, + match self { + Asked::Page(_, rows) => *rows, + Asked::Rows(rows) => rows.len(), + }, + ) + } +} + +/// `rows` empty shapes of depth `depth`, which draw nothing. +fn empty_shapes(depth: u8, rows: usize) -> Values { + Values::of_rows(depth, std::iter::repeat_n(None, rows)) +} + /// Shapes made since the last complete set: whole pages, by the id of the field page each was made /// from; and rows, by id, each with the geometry digest it was made from and its part and index in /// `parts`. @@ -90,7 +109,7 @@ impl Landed { fn land(&mut self, asked: Vec, shapes: Vec) { for (asked, shapes) in asked.into_iter().zip(shapes) { match asked { - Asked::Page(id) => { + Asked::Page(id, _) => { self.pages.insert(id, Page::new(shapes)); } Asked::Rows(rows) => { @@ -191,8 +210,8 @@ struct Shaped { paging: Option, /// The pages of the previous order. New pages copy the shapes of unchanged rows from it. before: Option, - /// A pending job, with what each of its parts shapes. - working: Option<(Pending>, Vec)>, + /// A shaping job, for the stamp of what it asks, with what each of its parts shapes. + working: Working>, /// Bumps whenever a job's answer arrives. landings: u64, } @@ -245,7 +264,7 @@ impl Shapes { /// Whether a shape job is pending. The rows it shapes draw nothing until it finishes. pub(crate) fn busy(&self) -> bool { - self.shaped.values().any(|s| s.working.is_some()) + self.shaped.values().any(|s| s.working.busy()) } /// Whether every drawn row has its shape. @@ -308,20 +327,11 @@ impl Shaped { /// Updates the shapes to `source`: as a paged table when `paged` is given, else drawn whole. /// Returns whether they changed. fn update(&mut self, want: &Want, source: &Table, paged: Option<&Paged>, jobs: &mut Jobs<'_>) -> bool { - if let Some((pending, _)) = &self.working - && let Some(answer) = pending.take() - { - let (_, asked) = self.working.take().expect("a job under way"); - let shapes = answer.ok().and_then(|bytes| shapes_answer(&bytes)).unwrap_or_default(); - if shapes.len() == asked.len() { - self.landed.land(asked, shapes); - } - self.landings += 1; - } let Some(Field::Values(column, version)) = source.field_named(&want.field) else { return self.forget() }; let Some(depth) = column.kind().depth().filter(|&depth| want.shaping.takes(depth)) else { return self.forget(); }; + self.settle(want.shaping.depth(depth)); if let Some(paged) = paged { // If it draws whole again later, its field is rewritten from scratch. let had = self.field.take().is_some(); @@ -336,6 +346,17 @@ impl Shaped { self.update_whole(want, source, column, *version, depth, jobs) || had } + /// Takes a finished job's shapes, of depth `depth`. A job that failed, or that answered other + /// parts than it was asked, gives its rows empty shapes: they draw nothing until their geometry + /// changes. + fn settle(&mut self, depth: u8) { + let Some((_, asked, answer)) = self.working.landed() else { return }; + let shapes = answer.ok().and_then(|bytes| shapes_answer(&bytes)).filter(|shapes| shapes.len() == asked.len()); + let shapes = shapes.unwrap_or_else(|| asked.iter().map(|part| part.empty(depth)).collect()); + self.landed.land(asked, shapes); + self.landings += 1; + } + /// Uses `field`, made elsewhere (by vector tiles), as its shapes. Returns whether they changed. fn take(&mut self, field: &Field) -> bool { let changed = self.field.as_ref().is_none_or(|f| f.version() != field.version()); @@ -343,10 +364,12 @@ impl Shaped { changed } - /// Drops its shapes once its field no longer needs shaping. Returns whether it had any. + /// Drops its shapes and any pending job once its field no longer needs shaping. Returns whether + /// it had any shapes. fn forget(&mut self) -> bool { let had = self.field.is_some() || self.paging.is_some(); (self.field, self.paging, self.before, self.stamp) = (None, None, None, 0); + self.working.cancel(); had } @@ -428,7 +451,7 @@ impl Shaped { paging.shapes.kept.retain(|d, _| paged.kept.binary_search(d).is_ok()); // Make drawn pages not made yet, or waiting on rows while no job runs; and, while no job // runs, shape kept rows not shaped yet. - let running = self.working.is_some(); + let running = self.working.busy(); let made = &paging.shapes.pages; let make: Vec = paged.pages.iter().copied().filter(|p| made.get(p).is_none_or(|page| !page.whole && !running)).collect(); @@ -460,7 +483,7 @@ impl Shaped { paging.shapes.version = Version::next(); // Every page complete and no job pending: the pages hold every shape that arrived, and the // previous order is no longer needed. - if self.working.is_none() && paging.shapes.pages.values().all(|page| page.whole) { + if !self.working.busy() && paging.shapes.pages.values().all(|page| page.whole) { self.landed = Landed::default(); self.before = None; } @@ -515,13 +538,17 @@ impl Shaped { Ok(()) } - /// Starts a job to shape `missing`, unless one is pending; a later update then retries. + /// Starts a job to shape `missing`, unless one is pending, in which case a later update asks + /// again, or the same job already ran. fn ask(&mut self, want: &Want, column: &Column, missing: Missing, jobs: &mut Jobs<'_>) { - if self.working.is_some() { + if self.working.busy() { return; } let (parts, asked) = missing.parts(column); - self.working = Some((jobs.spawn(Job::shapes(want.shaping, &parts)), asked)); + let stamp = stamp_of(&asked); + if self.working.may_start(stamp) { + self.working.start(stamp, jobs.spawn(Job::shapes(want.shaping, &parts)), asked); + } } /// Builds the derived column for `column` of `source`, page by page, and returns what is left @@ -556,7 +583,7 @@ impl Shaped { continue; } let Some(geometry) = page.values.geometry() else { - pages.push(Page::new(Values::of_geometry(GeometryBuilder::new(depth).finish()))); + pages.push(Page::new(empty_shapes(depth, 0))); continue; }; // A path page needing no bending is its own shape; sharing it means no new upload. @@ -574,9 +601,7 @@ impl Shaped { }); if fresh && kept.is_none() && !found(start) { // Every row needs shaping: shape the page whole; it draws nothing until then. - let mut empty = GeometryBuilder::new(depth); - (0..len).for_each(|_| empty.push_empty()); - pages.push(Page::new(Values::of_geometry(empty.finish()))); + pages.push(Page::new(empty_shapes(depth, len))); missing.pages.push((page.id, page.values.clone())); continue; } @@ -597,8 +622,7 @@ impl Shaped { } pages.push(Page::new(Values::of_geometry(shapes.finish()))); } - let like = Values::of_geometry(GeometryBuilder::new(depth).finish()); - (Column::of_pages(pages, &like), missing) + (Column::of_pages(pages, &empty_shapes(depth, 0)), missing) } /// The shapes of the page that `page` was written over, searching back through its recorded @@ -720,14 +744,13 @@ fn points_of(column: &Column, row: usize) -> usize { /// The geometry of rows `rows` of geometry column `column`, in that order. fn geometry_of(column: &Column, rows: &[usize]) -> Values { let depth = column.kind().depth().expect("a geometry column"); - let mut geometry = GeometryBuilder::new(depth); - for &row in rows { - match column.at(row).filter(|&(values, i)| values.is_valid(i)) { - Some((values, i)) if let Some(g) = values.geometry() => geometry.push_from(g, i), - _ => geometry.push_empty(), - } - } - Values::of_geometry(geometry.finish()) + Values::of_rows( + depth, + rows.iter().map(|&row| { + let (values, i) = column.at(row).filter(|&(values, i)| values.is_valid(i))?; + Some((values.geometry()?, i)) + }), + ) } #[cfg(test)] @@ -737,6 +760,7 @@ mod tests { use super::*; use crate::derive::pyramid::Pyramid; + use crate::derive::testing::Failing; use crate::gpu::work::Counters; use crate::{Areas, Data, Ids, Plane, Scene, Snapshot}; @@ -857,4 +881,35 @@ mod tests { assert!(update(&mut shapes, &data)); assert!(points(&derived(&shapes), row) >= 4, "then its shapes arrive"); } + + #[test] + fn a_failed_shaping_job_draws_its_rows_empty_and_runs_once() { + let data = Data::new("map"); + data.apply(Table::new("Zone", Ids::count(3)).field("geometry", vec![square(0.0), square(2.0), square(4.0)])) + .expect("valid"); + let data = data.snapshot(); + let scene = Scene::new(Plane::default()).draw(Areas::of("map", "Zone")); + let (host, counters, errors) = (Failing::default(), Counters::default(), Arc::new(Mutex::new(Vec::new()))); + let mut shapes = Shapes::default(); + for _ in 0..5 { + let input = Input { + scene: &scene, + scene_version: 1, + data: std::slice::from_ref(&data), + host: Some(&host), + dragging: false, + paged: None, + }; + // No frame budget: the rows are shaped in a job. + shapes.update( + &input, + &TileSets::default(), + &mut Jobs::new(Some(&host), Waker::noop(), &counters, &errors, 0), + ); + } + assert_eq!(host.jobs(), 1, "one job, not one per frame"); + assert!(!shapes.busy(), "the failure finished the work"); + let column = derived(&shapes); + assert!((0..3).all(|row| column.at(row).is_some()), "every row has a shape, drawing nothing"); + } } diff --git a/crates/latkit/src/derive/sight.rs b/crates/latkit/src/derive/sight.rs index e9a8b3e9..054a5124 100644 --- a/crates/latkit/src/derive/sight.rs +++ b/crates/latkit/src/derive/sight.rs @@ -6,13 +6,12 @@ use glam::DVec3; +use crate::geo::mercator; use crate::tiles::TileKey; use crate::view::Projected; /// CSS pixels a tile spans when drawn at its own level. pub(crate) const TILE_PX: f64 = 512.0; -/// The latitude Web Mercator reaches, north and south, in degrees. -const MAX_LATITUDE: f64 = 85.051_128_779_806_59; /// The square a space's tiles divide: the earth's Web Mercator square, or a square of a plane. #[derive(Clone, Copy, PartialEq, Debug)] @@ -26,6 +25,23 @@ pub(crate) enum Domain { } impl Domain { + /// The words a job writes the square as: zeros for the earth, else 1, its west and south edges, + /// and its size. + pub(crate) fn words(self) -> [f64; 4] { + match self { + Domain::Earth => [0.0; 4], + Domain::Plane { min, size } => [1.0, min[0], min[1], size], + } + } + + /// The square written as `words`. + pub(crate) fn from_words(words: [f64; 4]) -> Domain { + match words { + [0.0, ..] => Domain::Earth, + [_, x, y, size] => Domain::Plane { min: [x, y], size }, + } + } + /// The plane square containing `bounds` (west, south, east, north), centered on them and 2% /// larger so nothing lies on its edge. Inverted bounds give the unit square. pub(crate) fn plane(bounds: [f64; 4]) -> Domain { @@ -39,11 +55,7 @@ impl Domain { /// earth, latitude is clamped to Web Mercator's range and longitude wraps. pub(crate) fn unit(&self, p: [f64; 2]) -> [f64; 2] { match *self { - Domain::Earth => { - let lat = p[1].clamp(-MAX_LATITUDE, MAX_LATITUDE).to_radians(); - let y = 0.5 - (std::f64::consts::FRAC_PI_4 + lat / 2.0).tan().ln() / std::f64::consts::TAU; - [((p[0] + 180.0) / 360.0).rem_euclid(1.0), y] - } + Domain::Earth => [((p[0] + 180.0) / 360.0).rem_euclid(1.0), mercator::down(p[1])], Domain::Plane { min, size } => [(p[0] - min[0]) / size, 1.0 - (p[1] - min[1]) / size], } } @@ -51,10 +63,7 @@ impl Domain { /// The position at `u` in the square; the inverse of `unit`. pub(crate) fn position(&self, u: [f64; 2]) -> [f64; 2] { match *self { - Domain::Earth => { - let lat = (std::f64::consts::PI * (1.0 - 2.0 * u[1])).sinh().atan().to_degrees(); - [u[0] * 360.0 - 180.0, lat] - } + Domain::Earth => [u[0] * 360.0 - 180.0, mercator::latitude_down(u[1])], Domain::Plane { min, size } => [min[0] + u[0] * size, min[1] + (1.0 - u[1]) * size], } } diff --git a/crates/latkit/src/derive/stars.rs b/crates/latkit/src/derive/stars.rs index bffb112d..d72f271a 100644 --- a/crates/latkit/src/derive/stars.rs +++ b/crates/latkit/src/derive/stars.rs @@ -6,8 +6,10 @@ //! the mark draws like any geometry. They are recomputed only when the data, the scene, or the //! layout's places change. +use std::hash::Hash; + use super::graph::{VertexMark, marks, position}; -use super::{Input, Tables, beside, kept}; +use super::{Input, Tables, beside, kept, stamp_with}; use crate::data::{Field, Name, UNWIRED, Values}; use crate::expr::cpu::Rows; use crate::{Mark, Space}; @@ -30,10 +32,10 @@ impl Stars { } let field = |t: &crate::Table, name: &str| t.field_named(name).map(Field::version); let placed: Vec<_> = places.iter().map(|(key, t)| (key, field(t, "x"), field(t, "y"))).collect(); - let mut hasher = rustc_hash::FxHasher::default(); - std::hash::Hash::hash(&(input.scene_version, placed), &mut hasher); - input.structure(&mut hasher); - let stamp = std::hash::Hasher::finish(&hasher).max(1); + let stamp = stamp_with(|hasher| { + (input.scene_version, placed).hash(hasher); + input.structure(hasher); + }); if stamp == self.stamp { return false; } diff --git a/crates/latkit/src/derive/tiled.rs b/crates/latkit/src/derive/tiled.rs index 1a19371f..e62d33f6 100644 --- a/crates/latkit/src/derive/tiled.rs +++ b/crates/latkit/src/derive/tiled.rs @@ -19,11 +19,10 @@ use std::sync::Arc; use super::order::{Lifted, Order}; use super::pyramid::{Leveling, Places, Placing, Pyramid, answer}; use super::sight::{Domain, Sight}; -use super::{Jobs, stamp_of}; -use crate::Pending; +use super::{Jobs, Working, stamp_of}; use crate::data::{Column, Field, Keyed, Name, Named, Reference, Snapshot, Table, UNWIRED, Version}; +use crate::expr::Expr; use crate::expr::cpu::Rows; -use crate::expr::{Expr, Node}; use crate::gpu::shaders::PAGE_ROWS; use crate::host::Job; @@ -128,7 +127,7 @@ impl Tiled { /// Whether every paged table draws from its pyramid: no pyramid job is pending and no table /// draws its sample. pub(crate) fn complete(&self) -> bool { - self.sets.iter().all(|s| s.working.is_none() && !s.sampling) + self.sets.iter().all(|s| !s.working.busy() && !s.sampling) } /// The bounds of every row of paged table `table` of dataset `data` (west, south, east, north, @@ -156,10 +155,9 @@ impl Placed { } } -/// A dataset's pending pyramid job: the tables it answers, each with its index among those asked, -/// its order version when asked, and its bounds; and the square the pyramids tile. -struct Working { - pending: Pending>, +/// What a dataset's pyramid job answers: the tables, each with its index among those asked, its +/// order version when asked, and its bounds; and the square the pyramids tile. +struct Pyramids { tables: Vec<(u32, Name, Version, Option<[f64; 4]>)>, domain: Domain, } @@ -210,7 +208,8 @@ struct Set { stamp: u64, /// Each table's latest pyramid. placed: Named, - working: Option, + /// The pyramid job, for the stamp it was asked at. + working: Working, /// The inputs of the last pyramid request, by table. read: Named, /// Per table: the stamp of its inputs, the rows that sampled edges ending at it end at (lifted @@ -226,7 +225,7 @@ impl Set { name, stamp: 0, placed: Named::default(), - working: None, + working: Working::default(), read: Named::default(), lifted: Named::default(), sampling: false, @@ -342,21 +341,18 @@ impl Set { } let served = planned.as_is() || self.placed.get(&table.name).is_some_and(|p| p.serves(table)); let appended = table.len().saturating_sub(read.rows); - self.working.is_some() || (served && appended < (read.rows / 8).max(PAGE_ROWS as usize)) + self.working.busy() || (served && appended < (read.rows / 8).max(PAGE_ROWS as usize)) }) } /// Takes the pyramids of a finished job. Returns true when a job succeeded. A failed job is not /// retried until its inputs change. fn land(&mut self) -> bool { - let Some(working) = &self.working else { return false }; - let Some(landed) = working.pending.take() else { return false }; - let working = self.working.take().expect("under way"); - let Ok(bytes) = landed else { return false }; + let Some((_, asked, Ok(bytes))) = self.working.landed() else { return false }; for (index, pyramid) in Pyramid::read(&bytes) { - if let Some((_, name, order, bounds)) = working.tables.iter().find(|t| t.0 == index) { + if let Some((_, name, order, bounds)) = asked.tables.iter().find(|t| t.0 == index) { let placed = - Placed { order: *order, domain: working.domain, pyramid: Arc::new(pyramid), bounds: *bounds }; + Placed { order: *order, domain: asked.domain, pyramid: Arc::new(pyramid), bounds: *bounds }; self.placed.insert(name.clone(), placed); } } @@ -367,7 +363,7 @@ impl Set { /// in one job for the dataset. fn level(&mut self, placing: &[(&Table, &Planned)], ask: &Ask<'_>, jobs: &mut Jobs<'_>) { // Drop any pending job; its answer would no longer serve. - self.working = None; + self.working.cancel(); self.read = placing.iter().map(|(table, planned)| (table.name.clone(), Read::new(table, planned, ask.earth))).collect(); let (leveling, bounds) = leveling(ask, placing); @@ -393,8 +389,7 @@ impl Set { } return; } - let pending = jobs.spawn(Job::pyramid(&leveling)); - self.working = Some(Working { pending, tables, domain }); + self.working.start(self.stamp, jobs.spawn(Job::pyramid(&leveling)), Pyramids { tables, domain }); } } @@ -554,21 +549,11 @@ fn column_of<'t>(table: &'t Table, name: &Name) -> Option<&'t Column> { /// The fields `expr` reads of its mark's own table. fn fields_of(expr: &Expr, out: &mut Vec) { - match &*expr.0 { - Node::Field(f) if f.from.is_none() => out.push(f.name.clone()), - Node::Unary(_, a) | Node::Is(a, _) => fields_of(a, out), - Node::Binary(_, a, b) => { - fields_of(a, out); - fields_of(b, out); + expr.for_each_field(&mut |f| { + if f.from.is_none() { + out.push(f.name.clone()); } - Node::When(c, a, b) => { - fields_of(c, out); - fields_of(a, out); - fields_of(b, out); - } - Node::Scale { input, .. } | Node::Colormap { input, .. } => fields_of(input, out), - _ => {} - } + }); } /// The box (west, south, east, north) around every placed row of `places`; inverted when none is. @@ -700,4 +685,12 @@ mod tests { let again = lift(&drawn, &[1], &[10, 10], 0, 300, Some(&(from, lifted, step))); assert_eq!(again.0, from, "computed once while its edges are unchanged"); } + + #[test] + fn a_pyramid_reads_every_field_its_priority_tests() { + let priority = crate::when(crate::field("name").contains("lake")).then(crate::field("size")).otherwise(0.0); + let x = [crate::field("x"), crate::field("y")]; + let planned = Planned { draw: Draw::Tiles { rows: 100 }, priority: Some(priority), place: Place::Points(x) }; + assert_eq!(fields_read(&planned), ["name", "size", "x", "y"].map(Name::from)); + } } diff --git a/crates/latkit/src/error.rs b/crates/latkit/src/error.rs index c5ace1bc..7eddf059 100644 --- a/crates/latkit/src/error.rs +++ b/crates/latkit/src/error.rs @@ -39,6 +39,9 @@ pub enum Error { pub type Result = std::result::Result; impl Error { + /// Every kind's [`code`](Error::code). + const CODES: [&'static str; 7] = ["invalid", "unsupported", "limit", "io", "device-lost", "cancelled", "timeout"]; + /// An [`Error::Invalid`] at the root of the input. pub fn invalid(reason: impl Into>) -> Error { Error::Invalid { path: Path::default(), reason: reason.into() } @@ -69,6 +72,21 @@ impl Error { other => other, } } + + /// This error, of the same kind, naming `source` as where it happened: the first step of an + /// invalid input's path, or the start of another error's message. + #[cfg(feature = "gpu")] + #[must_use] + pub(crate) fn within(self, source: &str) -> Error { + let named = |message: Cow<'static, str>| Cow::Owned(format!("{source}: {message}")); + match self { + Error::Invalid { .. } => self.at(source), + Error::Unsupported(message) => Error::Unsupported(named(message)), + Error::Limit(message) => Error::Limit(named(message)), + Error::Io(message) => Error::Io(named(message)), + other => other, + } + } } /// Serializes as `{ code, message, path? }`, the shape of the npm package's `LatkitError`. @@ -98,7 +116,7 @@ impl schemars::JsonSchema for Error { "properties": { "code": { "description": "The kind of failure, as `LatkitError.code` describes it.", - "enum": ["invalid", "unsupported", "limit", "io", "device-lost", "cancelled", "timeout"], + "enum": Error::CODES, }, "message": { "description": "What failed, in words.", "type": "string" }, "path": { @@ -178,4 +196,18 @@ mod tests { fn only_invalid_errors_take_steps() { assert_eq!(Error::DeviceLost.at("marks"), Error::DeviceLost); } + + #[test] + fn the_schema_lists_every_code() { + let every = [ + Error::invalid(""), + Error::Unsupported("".into()), + Error::Limit("".into()), + Error::Io("".into()), + Error::DeviceLost, + Error::Cancelled, + Error::Timeout, + ]; + assert_eq!(every.map(|e| e.code()), Error::CODES); + } } diff --git a/crates/latkit/src/expr/colormap.rs b/crates/latkit/src/expr/colormap.rs index 30f5eb4b..666a8227 100644 --- a/crates/latkit/src/expr/colormap.rs +++ b/crates/latkit/src/expr/colormap.rs @@ -100,12 +100,12 @@ struct Entry { include!(concat!(env!("OUT_DIR"), "/colormaps.rs")); -/// The catalog's names, in order. -pub(crate) fn catalog() -> impl Iterator { - CATALOG.iter().map(|e| e.name) -} - impl Colormap { + /// Every catalog colormap, in order. + pub fn catalog() -> impl Iterator { + (0..CATALOG.len()).map(|i| Colormap(Map::Catalog(i as u8))) + } + /// The catalog colormap named `name`, such as `"viridis"`; `None` if the catalog has none. pub fn named(name: &str) -> Option { CATALOG.iter().position(|e| e.name == name).map(|i| Colormap(Map::Catalog(i as u8))) diff --git a/crates/latkit/src/expr/compile.rs b/crates/latkit/src/expr/compile.rs index ff0b8178..d85280c2 100644 --- a/crates/latkit/src/expr/compile.rs +++ b/crates/latkit/src/expr/compile.rs @@ -325,13 +325,7 @@ impl<'a> Compiler<'a> { Node::Field(f) if self.fields.kind(f.from.as_ref(), &f.name) == Some(Kind::Sampled) => { Some("a sampled field") } - Node::Unary(_, a) | Node::Is(a, _) | Node::Contains(a, _) => self.unstructural(a), - Node::Binary(_, a, b) => self.unstructural(a).or_else(|| self.unstructural(b)), - Node::When(c, a, b) => { - self.unstructural(c).or_else(|| self.unstructural(a)).or_else(|| self.unstructural(b)) - } - Node::Scale { input, .. } | Node::Colormap { input, .. } => self.unstructural(input), - _ => None, + _ => expr.children().find_map(|child| self.unstructural(child)), } } @@ -760,14 +754,7 @@ fn binary_name(op: Binary) -> &'static str { } fn mentions_pointer(e: &Expr) -> bool { - match &*e.0 { - Node::State(State::Pointer) => true, - Node::Unary(_, a) | Node::Is(a, _) | Node::Contains(a, _) => mentions_pointer(a), - Node::Binary(_, a, b) => mentions_pointer(a) || mentions_pointer(b), - Node::When(c, a, b) => mentions_pointer(c) || mentions_pointer(a) || mentions_pointer(b), - Node::Scale { input, .. } | Node::Colormap { input, .. } => mentions_pointer(input), - _ => false, - } + e.any(&|e| matches!(&*e.0, Node::State(State::Pointer))) } fn ty_name(ty: Ty) -> &'static str { diff --git a/crates/latkit/src/expr/cpu.rs b/crates/latkit/src/expr/cpu.rs index b2721a8c..bd2889df 100644 --- a/crates/latkit/src/expr/cpu.rs +++ b/crates/latkit/src/expr/cpu.rs @@ -63,7 +63,7 @@ impl<'a> Rows<'a> { let pages = column.pages().iter().map(|p| &p.values); match (column.kind(), f.component) { (Kind::Number | Kind::Bool, None) => { - pages.for_each(|v| v.each_number(|x| out.push(make(x.unwrap_or(f64::NAN))))); + pages.for_each(|v| v.extend_numbers(&mut out, |x| make(x.unwrap_or(f64::NAN)))); return out; } (Kind::Point, Some(k)) => { diff --git a/crates/latkit/src/expr/form.rs b/crates/latkit/src/expr/form.rs index 86205bd9..ea89e7f0 100644 --- a/crates/latkit/src/expr/form.rs +++ b/crates/latkit/src/expr/form.rs @@ -116,75 +116,60 @@ impl<'de> Visitor<'de> for Reading { } } -/// Every key that an expression object, a size, or label options can have; each kind accepts only -/// its own keys. A conditional's values are read as the caller decides. -#[derive(Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub(crate) struct Form { - field: Option, - color: Option, - state: Option, - missing: Option, - op: Option, - when: Option>, - is: Option<(Expr, String)>, - contains: Option<(Expr, String)>, - scale: Option<[f64; 2]>, - colormap: Option, - from: Option<(Name, Name)>, - step: Option, - args: Option>, - otherwise: Option, - value: Option, - domain: Option<[f64; 2]>, - pub(crate) px: Option, - pub(crate) units: Option, - pub(crate) at_least: Option, - pub(crate) at_most: Option, - pub(crate) text: Option, - pub(crate) size: Option, - pub(crate) priority: Option, - pub(crate) max_width: Option, - pub(crate) font: Option>, - pub(crate) max: Option, +/// Declares `Form` and `Form::held` from one list of keys, each with its JSON spelling. +macro_rules! form { + ($($vis:vis $key:ident $json:literal: $ty:ty),+ $(,)?) => { + /// Every key that an expression object, a size, or label options can have; each kind + /// accepts only its own keys. A conditional's values are read as the caller decides. + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + pub(crate) struct Form { + $(#[serde(rename = $json)] $vis $key: Option<$ty>,)+ + } + + impl Form { + /// The keys present, as JSON spells them. + fn held(&self) -> Vec<&'static str> { + let mut held = Vec::new(); + $(if self.$key.is_some() { + held.push($json); + })+ + held + } + } + }; } -impl Form { - /// The keys present, as JSON spells them. - fn held(&self) -> Vec<&'static str> { - [ - ("field", self.field.is_some()), - ("color", self.color.is_some()), - ("state", self.state.is_some()), - ("missing", self.missing.is_some()), - ("op", self.op.is_some()), - ("when", self.when.is_some()), - ("is", self.is.is_some()), - ("contains", self.contains.is_some()), - ("scale", self.scale.is_some()), - ("colormap", self.colormap.is_some()), - ("from", self.from.is_some()), - ("step", self.step.is_some()), - ("args", self.args.is_some()), - ("otherwise", self.otherwise.is_some()), - ("value", self.value.is_some()), - ("domain", self.domain.is_some()), - ("px", self.px.is_some()), - ("units", self.units.is_some()), - ("atLeast", self.at_least.is_some()), - ("atMost", self.at_most.is_some()), - ("text", self.text.is_some()), - ("size", self.size.is_some()), - ("priority", self.priority.is_some()), - ("maxWidth", self.max_width.is_some()), - ("font", self.font.is_some()), - ("max", self.max.is_some()), - ] - .into_iter() - .filter_map(|(key, held)| held.then_some(key)) - .collect() - } +form! { + field "field": Name, + color "color": Written, + state "state": State, + missing "missing": bool, + op "op": String, + when "when": Vec<(Expr, Written)>, + is "is": (Expr, String), + contains "contains": (Expr, String), + scale "scale": [f64; 2], + colormap "colormap": Colormap, + from "from": (Name, Name), + step "step": bool, + args "args": Vec, + otherwise "otherwise": Written, + value "value": Expr, + domain "domain": [f64; 2], + pub(crate) px "px": Expr, + pub(crate) units "units": Expr, + pub(crate) at_least "atLeast": f32, + pub(crate) at_most "atMost": f32, + pub(crate) text "text": Expr, + pub(crate) size "size": f32, + pub(crate) priority "priority": Expr, + pub(crate) max_width "maxWidth": f32, + pub(crate) font "font": Arc, + pub(crate) max "max": u32, +} +impl Form { /// Errors unless every key present is one of `keys`, naming `what` and the first other key. pub(crate) fn only(&self, what: &str, keys: &[&str]) -> Result<(), String> { match self.held().into_iter().find(|key| !keys.contains(key)) { @@ -647,7 +632,7 @@ impl JsonSchema for Colormap { } fn json_schema(generator: &mut SchemaGenerator) -> Schema { - let names: Vec<&str> = super::colormap::catalog().collect(); + let names: Vec<&str> = Colormap::catalog().filter_map(|map| map.name()).collect(); generator.definitions_mut().insert( "ColormapName".into(), serde_json::json!({ "description": "A colormap of the catalog.", "type": "string", "enum": names }), diff --git a/crates/latkit/src/expr/mod.rs b/crates/latkit/src/expr/mod.rs index 55d9e026..dd10ffb5 100644 --- a/crates/latkit/src/expr/mod.rs +++ b/crates/latkit/src/expr/mod.rs @@ -338,28 +338,35 @@ impl Expr { } } + /// The expressions this one is made of, in order: an operand or input, a binary operation's + /// two sides, or a conditional's condition, value, and alternative. + pub(crate) fn children(&self) -> impl Iterator { + let (a, b, c) = match &*self.0 { + Node::Unary(_, a) + | Node::Is(a, _) + | Node::Contains(a, _) + | Node::Scale { input: a, .. } + | Node::Colormap { input: a, .. } => (Some(a), None, None), + Node::Binary(_, a, b) => (Some(a), Some(b), None), + Node::When(c, a, b) => (Some(c), Some(a), Some(b)), + Node::Number(_) | Node::Bool(_) | Node::Color(_) | Node::Missing | Node::Field(_) | Node::State(_) => { + (None, None, None) + } + }; + [a, b, c].into_iter().flatten() + } + + /// Whether `test` holds for this expression or any expression it is made of. + pub(crate) fn any(&self, test: &impl Fn(&Expr) -> bool) -> bool { + test(self) || self.children().any(|child| child.any(test)) + } + /// Calls `f` with each field that a `contains` in the expression tests, and the text it looks /// for. pub(crate) fn for_each_contains<'e>(&'e self, f: &mut impl FnMut(&'e FieldRef, &'e str)) { match &*self.0 { - Node::Contains(a, text) => { - if let Node::Field(field) = &*a.0 { - f(field, text); - } - } - Node::Unary(_, a) | Node::Is(a, _) | Node::Scale { input: a, .. } | Node::Colormap { input: a, .. } => { - a.for_each_contains(f); - } - Node::Binary(_, a, b) => { - a.for_each_contains(f); - b.for_each_contains(f); - } - Node::When(c, a, b) => { - c.for_each_contains(f); - a.for_each_contains(f); - b.for_each_contains(f); - } - Node::Number(_) | Node::Bool(_) | Node::Color(_) | Node::Missing | Node::Field(_) | Node::State(_) => {} + Node::Contains(a, text) if let Node::Field(field) = &*a.0 => f(field, text), + _ => self.children().for_each(|child| child.for_each_contains(f)), } } @@ -367,38 +374,16 @@ impl Expr { pub(crate) fn for_each_field<'e>(&'e self, f: &mut impl FnMut(&'e FieldRef)) { match &*self.0 { Node::Field(field) => f(field), - Node::Unary(_, a) - | Node::Is(a, _) - | Node::Contains(a, _) - | Node::Scale { input: a, .. } - | Node::Colormap { input: a, .. } => a.for_each_field(f), - Node::Binary(_, a, b) => { - a.for_each_field(f); - b.for_each_field(f); - } - Node::When(c, a, b) => { - c.for_each_field(f); - a.for_each_field(f); - b.for_each_field(f); - } - Node::Number(_) | Node::Bool(_) | Node::Color(_) | Node::Missing | Node::State(_) => {} + _ => self.children().for_each(|child| child.for_each_field(f)), } } /// Whether the expression has a fitted domain, which makes each row's value depend on every /// row. pub(crate) fn fits(&self) -> bool { - match &*self.0 { - Node::Scale { domain: Domain::Fit, .. } | Node::Colormap { domain: Domain::Fit, .. } => true, - Node::Unary(_, a) - | Node::Is(a, _) - | Node::Contains(a, _) - | Node::Scale { input: a, .. } - | Node::Colormap { input: a, .. } => a.fits(), - Node::Binary(_, a, b) => a.fits() || b.fits(), - Node::When(c, a, b) => c.fits() || a.fits() || b.fits(), - Node::Number(_) | Node::Bool(_) | Node::Color(_) | Node::Missing | Node::Field(_) | Node::State(_) => false, - } + self.any(&|e| { + matches!(&*e.0, Node::Scale { domain: Domain::Fit, .. } | Node::Colormap { domain: Domain::Fit, .. }) + }) } /// The field's name, when the expression is a plain field of the mark's own table. diff --git a/crates/latkit/src/geo/geojson.rs b/crates/latkit/src/geo/geojson.rs index b616b60a..dfe46545 100644 --- a/crates/latkit/src/geo/geojson.rs +++ b/crates/latkit/src/geo/geojson.rs @@ -7,7 +7,7 @@ use serde::Deserialize; use serde::de::{self, IgnoredAny, SeqAccess, Visitor}; use serde_json::{Map, Value as Json}; -use crate::data::{Ids, Name, Patch, Snapshot, Table, Values}; +use crate::data::{Ids, Name, Patch, Snapshot, Table, Value, Values}; use crate::{Error, Result}; type Point = [f64; 2]; @@ -25,7 +25,7 @@ type Point = [f64; 2]; /// GeoJSON. pub(crate) fn geojson(name: impl Into, bytes: &[u8]) -> Result { let bytes = super::gunzipped(bytes)?; - let top: Top = serde_json::from_slice(&bytes).map_err(|e| Error::invalid(format!("not GeoJSON: {e}")))?; + let top: Top = crate::json::from_bytes(&bytes).map_err(|e| Error::invalid(format!("not GeoJSON: {e}")))?; let mut tables = [Rows::default(), Rows::default(), Rows::default()]; let mut add = |feature: Feature| { if let Some(geometry) = feature.geometry { @@ -202,26 +202,21 @@ fn ids(ids: &[Option]) -> Option { None } -/// Converts a property's values into a field: numbers if every present value is a number, booleans -/// if every one is a boolean, and text otherwise, with non-string values written as JSON. +/// Converts a property's values into a field, typed as [`Values::infer`] types them, with arrays +/// and objects written as JSON text. fn property(values: &[Option<&Json>]) -> Values { - let present = || values.iter().flatten(); - if present().all(|v| v.is_number()) { - return values.iter().map(|v| v.and_then(Json::as_f64)).collect::>().into(); - } - if present().all(|v| v.is_boolean()) { - return values.iter().map(|v| v.and_then(Json::as_bool)).collect::>().into(); - } - let texts: Vec> = values + let values: Vec> = values .iter() .map(|v| { v.map(|v| match v { - Json::String(text) => text.clone(), - other => other.to_string(), + Json::Number(n) => Value::Number(n.as_f64().unwrap_or(f64::NAN)), + Json::Bool(b) => Value::Bool(*b), + Json::String(text) => Value::Text(text.as_str().into()), + other => Value::Text(other.to_string().into()), }) }) .collect(); - texts.into() + Values::infer(&values) } /// Coordinates of any nesting depth: a position, a list of positions, a list of those, or a list of diff --git a/crates/latkit/src/geo/mercator.rs b/crates/latkit/src/geo/mercator.rs new file mode 100644 index 00000000..da3fa33d --- /dev/null +++ b/crates/latkit/src/geo/mercator.rs @@ -0,0 +1,42 @@ +//! Web Mercator: the projection of the earth's map camera, and of the square its tiles divide. + +use std::f64::consts::{FRAC_PI_4, PI, TAU}; + +/// The latitude Web Mercator reaches, north and south, in degrees: where its square ends. +pub(crate) const MAX_LATITUDE: f64 = 85.051_128_779_806_59; + +/// The projected y of `latitude` in degrees, clamped to [`MAX_LATITUDE`]: radians north of the +/// equator. +pub(crate) fn y(latitude: f64) -> f64 { + let lat = latitude.clamp(-MAX_LATITUDE, MAX_LATITUDE).to_radians(); + (FRAC_PI_4 + lat / 2.0).tan().ln() +} + +/// The latitude in degrees at projected y `y`; the inverse of [`y`]. +pub(crate) fn latitude(y: f64) -> f64 { + y.sinh().atan().to_degrees() +} + +/// How far down the square `latitude` lies, from 0 at its north edge to 1 at its south. +pub(crate) fn down(latitude: f64) -> f64 { + 0.5 - y(latitude) / TAU +} + +/// The latitude `t` of the way down the square; the inverse of [`down`]. +pub(crate) fn latitude_down(t: f64) -> f64 { + latitude(PI * (1.0 - 2.0 * t)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn latitudes_survive_the_round_trip_and_the_square_ends_at_the_limit() { + for lat in [-80.0, -37.5, 0.0, 12.25, 85.0] { + assert!((latitude(y(lat)) - lat).abs() < 1e-9, "{lat}"); + assert!((latitude_down(down(lat)) - lat).abs() < 1e-9, "{lat}"); + } + assert!(down(MAX_LATITUDE).abs() < 1e-12 && (down(-MAX_LATITUDE) - 1.0).abs() < 1e-12); + } +} diff --git a/crates/latkit/src/geo/mod.rs b/crates/latkit/src/geo/mod.rs index 322a990c..94166e92 100644 --- a/crates/latkit/src/geo/mod.rs +++ b/crates/latkit/src/geo/mod.rs @@ -1,10 +1,12 @@ -//! Geography: GeoJSON and vector tiles read as data, polygon triangulation, and lines given extra -//! points so they follow the globe. +//! Geography: GeoJSON and vector tiles read as data, polygon triangulation, lines given extra +//! points so they follow the globe, and Web Mercator. #[cfg(feature = "gpu")] mod earcut; mod geojson; #[cfg(feature = "gpu")] +pub(crate) mod mercator; +#[cfg(feature = "gpu")] pub(crate) mod mvt; #[cfg(feature = "gpu")] pub(crate) mod pmtiles; diff --git a/crates/latkit/src/geo/mvt.rs b/crates/latkit/src/geo/mvt.rs index 14b5c81d..c2ea7ed6 100644 --- a/crates/latkit/src/geo/mvt.rs +++ b/crates/latkit/src/geo/mvt.rs @@ -1,7 +1,9 @@ //! Mapbox vector tile decoding. Feature geometry is converted to longitude and latitude and clipped //! to the tile, so neighboring tiles never overlap. +use crate::Value; use crate::tiles::TileKey; +use crate::wire::In; type Point = [f64; 2]; @@ -13,13 +15,7 @@ pub(crate) enum Kind { Polygons, } -/// A property value as stored in a tile. -#[derive(Clone, PartialEq, Debug)] -pub(crate) enum Property { - Text(String), - Number(f64), - Bool(bool), -} +crate::wire::codes!(Kind { Points = 0, Lines = 1, Polygons = 2 }); /// A feature's geometry, in degrees of longitude and latitude. #[derive(Clone, PartialEq, Debug)] @@ -39,11 +35,12 @@ impl Shape { } } -/// One feature: its shape, and its properties as pairs of key index and value. +/// One feature: its shape, and its properties as pairs of key index and value: a text, number, or +/// boolean. #[derive(Clone, Debug)] pub(crate) struct Feature { pub(crate) shape: Shape, - pub(crate) properties: Vec<(u32, Property)>, + pub(crate) properties: Vec<(u32, Value)>, } /// One layer: its name, its property keys, and its features. @@ -92,19 +89,19 @@ fn layer(bytes: &[u8], key: TileKey) -> Option { Some(out) } -fn value(bytes: &[u8]) -> Option { +fn value(bytes: &[u8]) -> Option { let mut reader = Reader::new(bytes); - let mut out = Property::Bool(false); + let mut out = Value::Bool(false); while !reader.done() { let (field, wire) = reader.tag()?; out = match (field, wire) { - (1, LENGTH) => Property::Text(String::from_utf8_lossy(reader.bytes()?).into_owned()), - (2, FIXED32) => Property::Number(f64::from(f32::from_le_bytes(reader.take()?))), - (3, FIXED64) => Property::Number(f64::from_le_bytes(reader.take()?)), - (4, VARINT) => Property::Number(reader.varint()? as i64 as f64), - (5, VARINT) => Property::Number(reader.varint()? as f64), - (6, VARINT) => Property::Number(zigzag(reader.varint()?) as f64), - (7, VARINT) => Property::Bool(reader.varint()? != 0), + (1, LENGTH) => Value::Text(String::from_utf8_lossy(reader.bytes()?).into()), + (2, FIXED32) => Value::Number(f64::from(f32::from_le_bytes(reader.take()?))), + (3, FIXED64) => Value::Number(f64::from_le_bytes(reader.take()?)), + (4, VARINT) => Value::Number(reader.varint()? as i64 as f64), + (5, VARINT) => Value::Number(reader.varint()? as f64), + (6, VARINT) => Value::Number(zigzag(reader.varint()?) as f64), + (7, VARINT) => Value::Bool(reader.varint()? != 0), _ => { reader.skip(wire)?; continue; @@ -116,7 +113,7 @@ fn value(bytes: &[u8]) -> Option { /// Decodes one feature. `None`, which drops only this feature, if it lies wholly outside the tile /// or is malformed. -fn feature(bytes: &[u8], values: &[Property], frame: &Frame) -> Option { +fn feature(bytes: &[u8], values: &[Value], frame: &Frame) -> Option { let mut reader = Reader::new(bytes); let (mut tags, mut geometry, mut kind) = (Vec::new(), Vec::new(), 0); while !reader.done() { @@ -162,8 +159,7 @@ impl Frame { let n = f64::from(1_u32 << self.key.z.min(31)); let x = (f64::from(self.key.x) + p[0] / self.extent) / n; let y = (f64::from(self.key.y) + p[1] / self.extent) / n; - let lat = (std::f64::consts::PI * (1.0 - 2.0 * y)).sinh().atan().to_degrees(); - [x * 360.0 - 180.0, lat] + [x * 360.0 - 180.0, super::mercator::latitude_down(y)] } /// Decodes points, dropping those outside the tile. @@ -509,30 +505,19 @@ const LENGTH: u8 = 2; const FIXED32: u8 = 5; /// Reads protocol buffer fields in order. Reads return `None` past the end or on a malformed field. -struct Reader<'a> { - bytes: &'a [u8], -} +struct Reader<'a>(In<'a>); impl<'a> Reader<'a> { fn new(bytes: &'a [u8]) -> Reader<'a> { - Reader { bytes } + Reader(In::new(bytes)) } fn done(&self) -> bool { - self.bytes.is_empty() + self.0.is_empty() } fn varint(&mut self) -> Option { - let mut value = 0_u64; - for shift in (0..64).step_by(7) { - let (&byte, rest) = self.bytes.split_first()?; - self.bytes = rest; - value |= u64::from(byte & 0x7f) << shift; - if byte < 0x80 { - return Some(value); - } - } - None + self.0.varint() } /// The next field's number and wire type. @@ -541,17 +526,14 @@ impl<'a> Reader<'a> { Some(((tag >> 3) as u32, (tag & 7) as u8)) } + /// A length-delimited field's bytes. fn bytes(&mut self) -> Option<&'a [u8]> { let len = usize::try_from(self.varint()?).ok()?; - let (head, rest) = self.bytes.split_at_checked(len)?; - self.bytes = rest; - Some(head) + self.0.bytes(len) } fn take(&mut self) -> Option<[u8; N]> { - let (head, rest) = self.bytes.split_first_chunk::()?; - self.bytes = rest; - Some(*head) + self.0.take() } fn skip(&mut self, wire: u8) -> Option<()> { @@ -646,7 +628,7 @@ pub(crate) mod tests { assert_eq!(ring.len(), 5, "closed"); assert!((ring[0][0] + 90.0).abs() < 1e-9, "a quarter in: {:?}", ring[0]); assert!(ring[0][1] > 60.0 && ring[0][1] < 70.0, "mercator latitude: {:?}", ring[0]); - assert_eq!(feature.properties, vec![(0, Property::Text("lake".into()))]); + assert_eq!(feature.properties, vec![(0, Value::Text("lake".into()))]); } #[test] diff --git a/crates/latkit/src/geo/pmtiles.rs b/crates/latkit/src/geo/pmtiles.rs index 0c3d2a9a..3c7f16ff 100644 --- a/crates/latkit/src/geo/pmtiles.rs +++ b/crates/latkit/src/geo/pmtiles.rs @@ -2,6 +2,7 @@ //! the tile data, each read with range requests. use crate::tiles::TileKey; +use crate::wire::In; /// Size of the header in bytes. pub(crate) const HEADER: usize = 127; @@ -83,19 +84,8 @@ pub(crate) enum Entry { impl Directory { /// Decodes a directory from already decompressed `bytes`. `None` if malformed. pub(crate) fn decode(bytes: &[u8]) -> Option { - let mut at = 0; - let mut next = || -> Option { - let mut value = 0_u64; - for shift in (0..64).step_by(7) { - let byte = *bytes.get(at)?; - at += 1; - value |= u64::from(byte & 0x7f) << shift; - if byte < 0x80 { - return Some(value); - } - } - None - }; + let mut input = In::new(bytes); + let mut next = || input.varint(); let count = usize::try_from(next()?).ok()?; if count > bytes.len() { return None; diff --git a/crates/latkit/src/geo/tiles.rs b/crates/latkit/src/geo/tiles.rs index 2bf13bf0..a328c653 100644 --- a/crates/latkit/src/geo/tiles.rs +++ b/crates/latkit/src/geo/tiles.rs @@ -1,7 +1,7 @@ //! Vector tiles as data: tile URL templates, and decoding a tile into table rows with their shapes. -use super::mvt::{self, Kind, Property, Shape}; -use crate::data::{Ids, Name, Patch, Table, Values}; +use super::mvt::{self, Kind, Shape}; +use crate::data::{Ids, Name, Patch, Table, Value, Values}; use crate::host::Shaping; use crate::tiles::TileKey; @@ -40,8 +40,7 @@ impl Template { bounds: Option>, scheme: Option, } - let text = std::str::from_utf8(bytes).map_err(|e| format!("{base} is not TileJSON: {e}"))?; - let json = crate::from_json::(text).map_err(|e| format!("{base} is not TileJSON: {e}"))?; + let json = crate::json::from_bytes::(bytes).map_err(|e| format!("{base} is not TileJSON: {e}"))?; let template = json.tiles.first().ok_or_else(|| format!("the TileJSON at {base} lists no tile URLs"))?; if let Some(format) = json.format.as_deref() && ["png", "jpg", "jpeg", "webp", "avif"].contains(&format) @@ -101,7 +100,7 @@ pub(crate) fn decode(bytes: &[u8], key: TileKey, tables: &[Decoded]) -> Option

    = Vec::new(); - let mut properties: Vec<&[(u32, Property)]> = Vec::new(); + let mut properties: Vec<&[(u32, Value)]> = Vec::new(); for feature in layer.features.iter().filter(|f| f.shape.kind() == decoded.kind) { match &feature.shape { Shape::Points(points) => { @@ -150,12 +149,12 @@ pub(crate) fn decode(bytes: &[u8], key: TileKey, tables: &[Decoded]) -> Option

    > = properties[..count] + let values: Vec> = properties[..count] .iter() - .map(|props| props.iter().find(|(i, _)| *i as usize == k).map(|(_, v)| v)) + .map(|props| props.iter().find(|(i, _)| *i as usize == k).map(|(_, v)| v.clone())) .collect(); if values.iter().any(Option::is_some) { - table = table.field(key.as_str(), field(&values)); + table = table.field(key.as_str(), Values::infer(&values)); } } patch = patch.set(table); @@ -163,36 +162,6 @@ pub(crate) fn decode(bytes: &[u8], key: TileKey, tables: &[Decoded]) -> Option

    ]) -> Values { - let present = || values.iter().flatten(); - if present().all(|v| matches!(v, Property::Number(_))) { - return values - .iter() - .map(|v| v.and_then(|v| if let Property::Number(n) = v { Some(*n) } else { None })) - .collect::>() - .into(); - } - if present().all(|v| matches!(v, Property::Bool(_))) { - return values - .iter() - .map(|v| v.and_then(|v| if let Property::Bool(b) = v { Some(*b) } else { None })) - .collect::>() - .into(); - } - values.iter().map(|v| v.map(text)).collect::>>().into() -} - -/// Formats a property as text. -pub(crate) fn text(property: &Property) -> String { - match property { - Property::Text(t) => t.clone(), - Property::Number(n) => n.to_string(), - Property::Bool(b) => b.to_string(), - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/latkit/src/gpu/atlas.rs b/crates/latkit/src/gpu/atlas.rs index 0daebc66..f32b74d6 100644 --- a/crates/latkit/src/gpu/atlas.rs +++ b/crates/latkit/src/gpu/atlas.rs @@ -17,12 +17,17 @@ use std::sync::Arc; use rustc_hash::FxHashMap; -use crate::gpu::pipelines::Pipelines; +use crate::gpu::grown::Grown; +use crate::gpu::pipelines::{Pipelines, Variant}; use crate::gpu::shaders::{FIELD_AT, FIELD_COLUMNS, FIELD_COVERAGE, FIELD_HEIGHT, FIELD_ROW, FIELD_WIDTH, FIELD_WORDS}; use crate::gpu::shaders::{Shader, TEXT_BASE, TEXT_SPREAD}; use crate::gpu::work::{Count, Counters, Queue}; use crate::host::{Host, Raster, TextRun}; +/// The passes building distance fields: along columns, then along rows. +pub(crate) const FIELDS: [Variant; 2] = + [Variant::compute(Shader::Fields, "columns"), Variant::compute(Shader::Fields, "rows")]; + /// Texels across the atlas: room for about 1,400 labels of ten characters at the base size. const SIZE: u32 = 4096; /// Texels between runs, so filtering never reads a neighbor. @@ -114,8 +119,9 @@ pub(crate) struct Atlas { runs: HashMap>, /// Rasterized runs whose fields the next frame builds. waiting: Vec, - /// Scratch buffer for building fields, and its bind group. - work: Option<(wgpu::Buffer, wgpu::BindGroup)>, + /// Scratch buffer for building fields, and its bind group once built. + work: Grown, + work_group: Option, /// Frame counter for shelf use. clock: u64, /// Changes whenever runs are evicted, so anything referencing them looks them up again. @@ -146,7 +152,8 @@ impl Atlas { shelves: Vec::new(), runs: HashMap::new(), waiting: Vec::new(), - work: None, + work: Grown::new(device, "latkit fields", wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC), + work_group: None, clock: 0, evictions: 0, counters, @@ -309,9 +316,10 @@ impl Atlas { pass.set_bind_group(0, &pipelines.empty, &[]); pass.set_bind_group(1, &pipelines.empty, &[]); pass.set_bind_group(2, &group, &[]); - pass.set_pipeline(&pipelines.compute(device, Shader::Fields, "columns")); + let [columns, rows] = FIELDS; + pass.set_pipeline(&pipelines.compute(device, columns)); pass.dispatch_workgroups(most[0].div_ceil(16), most[1].div_ceil(16), laid.len() as u32); - pass.set_pipeline(&pipelines.compute(device, Shader::Fields, "rows")); + pass.set_pipeline(&pipelines.compute(device, rows)); pass.dispatch_workgroups(most[0].div_ceil(4).div_ceil(16), most[1].div_ceil(16), laid.len() as u32); } for (w, run) in laid.iter().zip(&runs) { @@ -336,30 +344,21 @@ impl Atlas { } } - /// The scratch buffer for building fields, at least `bytes` long, and its bind group. A new - /// buffer gets half again as much room. + /// The scratch buffer for building fields, at least `bytes` long, and its bind group. fn fit_work( &mut self, device: &wgpu::Device, pipelines: &mut Pipelines, bytes: u64, ) -> (wgpu::Buffer, wgpu::BindGroup) { - if let Some((buffer, group)) = self.work.as_ref().filter(|(buffer, _)| buffer.size() >= bytes) { - return (buffer.clone(), group.clone()); + if self.work.ensure(device, bytes) || self.work_group.is_none() { + self.work_group = Some(device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("latkit fields"), + layout: &pipelines.pass_layout(device, Shader::Fields), + entries: &[wgpu::BindGroupEntry { binding: 0, resource: self.work.buffer.as_entire_binding() }], + })); } - let buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("latkit fields"), - size: (bytes + bytes / 2).next_multiple_of(256), - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - let group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("latkit fields"), - layout: &pipelines.pass_layout(device, Shader::Fields), - entries: &[wgpu::BindGroupEntry { binding: 0, resource: buffer.as_entire_binding() }], - }); - self.work = Some((buffer.clone(), group.clone())); - (buffer, group) + (self.work.buffer.clone(), self.work_group.clone().expect("built above")) } /// Finds room for `width` by `height` texels, returning the shelf and top-left corner. diff --git a/crates/latkit/src/gpu/colormaps.rs b/crates/latkit/src/gpu/colormaps.rs index 06901284..26bb89be 100644 --- a/crates/latkit/src/gpu/colormaps.rs +++ b/crates/latkit/src/gpu/colormaps.rs @@ -1,85 +1,19 @@ //! Colormaps on the GPU: one 256-texel layer per colormap a program uses, in one texture array. -use std::collections::HashMap; - +use super::layers::Layers; +use crate::Result; use crate::expr::{Colormap, ColormapKey}; use crate::gpu::work::Queue; -use crate::{Error, Result}; - -const INITIAL_LAYERS: u32 = 16; -pub(crate) struct Colormaps { - texture: wgpu::Texture, - pub(crate) view: wgpu::TextureView, - layers: HashMap, - capacity: u32, - /// Changes whenever the texture is replaced, so views rebind it. - pub(crate) generation: u64, -} - -fn create(device: &wgpu::Device, layers: u32) -> (wgpu::Texture, wgpu::TextureView) { - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("latkit colormaps"), - size: wgpu::Extent3d { width: 256, height: 1, depth_or_array_layers: layers }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC, - view_formats: &[], - }); - let view = texture.create_view(&wgpu::TextureViewDescriptor { - dimension: Some(wgpu::TextureViewDimension::D2Array), - ..Default::default() - }); - (texture, view) -} +pub(crate) type Colormaps = Layers; impl Colormaps { - pub(crate) fn new(device: &wgpu::Device) -> Colormaps { - let (texture, view) = create(device, INITIAL_LAYERS); - Colormaps { texture, view, layers: HashMap::new(), capacity: INITIAL_LAYERS, generation: 0 } + pub(crate) fn colormaps(device: &wgpu::Device) -> Colormaps { + Layers::new(device, "colormaps", [256, 1], 16) } - /// The layer holding `colormap`, uploading it the first time. Errors once the device's texture - /// array layer limit is reached. - pub(crate) fn layer(&mut self, device: &wgpu::Device, queue: &Queue, colormap: &Colormap) -> Result { - let key = colormap.key(); - if let Some(&layer) = self.layers.get(&key) { - return Ok(layer); - } - let layer = self.layers.len() as u32; - if layer >= self.capacity { - let capacity = (self.capacity * 2).min(device.limits().max_texture_array_layers); - if layer >= capacity { - return Err(Error::Limit(format!("more than {capacity} distinct colormaps").into())); - } - let (texture, view) = create(device, capacity); - let mut encoder = - device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("latkit colormaps") }); - encoder.copy_texture_to_texture( - self.texture.as_image_copy(), - texture.as_image_copy(), - wgpu::Extent3d { width: 256, height: 1, depth_or_array_layers: self.capacity }, - ); - queue.submit([encoder.finish()]); - self.texture = texture; - self.view = view; - self.capacity = capacity; - self.generation += 1; - } - queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: &self.texture, - mip_level: 0, - origin: wgpu::Origin3d { x: 0, y: 0, z: layer }, - aspect: wgpu::TextureAspect::All, - }, - bytemuck::cast_slice(colormap.texels()), - wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(256 * 4), rows_per_image: Some(1) }, - wgpu::Extent3d { width: 256, height: 1, depth_or_array_layers: 1 }, - ); - self.layers.insert(key, layer); - Ok(layer) + /// The layer holding `colormap`, uploading it the first time. + pub(crate) fn colormap(&mut self, device: &wgpu::Device, queue: &Queue, colormap: &Colormap) -> Result { + self.layer(device, queue, colormap.key(), || bytemuck::cast_slice(colormap.texels()).to_vec()) } } diff --git a/crates/latkit/src/gpu/grown.rs b/crates/latkit/src/gpu/grown.rs new file mode 100644 index 00000000..3c139adb --- /dev/null +++ b/crates/latkit/src/gpu/grown.rs @@ -0,0 +1,35 @@ +//! Buffers that grow: when one runs out of room, it is replaced by one half again as large as +//! needed, so a buffer that keeps growing is replaced only a few times. + +/// A buffer replaced by one 1.5 times the size needed when it runs out of room. +pub(crate) struct Grown { + pub(crate) buffer: wgpu::Buffer, + label: &'static str, + usage: wgpu::BufferUsages, +} + +impl Grown { + pub(crate) fn new(device: &wgpu::Device, label: &'static str, usage: wgpu::BufferUsages) -> Grown { + Grown { buffer: buffer(device, label, usage, 256), label, usage } + } + + /// Makes room for `bytes`; true when the buffer was replaced and its contents lost. + pub(crate) fn ensure(&mut self, device: &wgpu::Device, bytes: u64) -> bool { + if bytes <= self.buffer.size() { + return false; + } + self.buffer = buffer(device, self.label, self.usage, (bytes + bytes / 2).next_multiple_of(256)); + true + } +} + +/// A buffer of at least `bytes` (at least 256, a whole number of words) for `usage`, which writes +/// can copy into. +pub(crate) fn buffer(device: &wgpu::Device, label: &str, usage: wgpu::BufferUsages, bytes: u64) -> wgpu::Buffer { + device.create_buffer(&wgpu::BufferDescriptor { + label: Some(label), + size: bytes.max(256).next_multiple_of(4), + usage: usage | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }) +} diff --git a/crates/latkit/src/gpu/icons.rs b/crates/latkit/src/gpu/icons.rs index 307465dc..55a8cb93 100644 --- a/crates/latkit/src/gpu/icons.rs +++ b/crates/latkit/src/gpu/icons.rs @@ -2,96 +2,44 @@ //! a common size. Markers reference their images, and markers showing the same image share its //! layer. -use std::collections::HashMap; +use std::hash::{Hash, Hasher}; use std::sync::Arc; +use super::layers::Layers; +use crate::Result; use crate::gpu::work::Queue; /// Texels across an icon layer. const SIZE: u32 = 64; -pub(crate) struct Icons { - texture: wgpu::Texture, - pub(crate) view: wgpu::TextureView, - /// Each image's layer, keyed by the address of its pixels, which are kept alive so the address - /// is not reused. - layers: HashMap, u32)>, - capacity: u32, - /// Changes whenever the texture is replaced, so views rebind it. - pub(crate) generation: u64, +/// An image's pixels, the same when shared: markers showing one image share its layer. Holding the +/// pixels keeps their address, which identifies them, from being reused. +pub(crate) struct Pixels(Arc<[u8]>); + +impl PartialEq for Pixels { + fn eq(&self, other: &Pixels) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } } -fn create(device: &wgpu::Device, layers: u32) -> (wgpu::Texture, wgpu::TextureView) { - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("latkit icons"), - size: wgpu::Extent3d { width: SIZE, height: SIZE, depth_or_array_layers: layers }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC, - view_formats: &[], - }); - let view = texture.create_view(&wgpu::TextureViewDescriptor { - dimension: Some(wgpu::TextureViewDimension::D2Array), - ..Default::default() - }); - (texture, view) +impl Eq for Pixels {} + +impl Hash for Pixels { + fn hash(&self, hasher: &mut H) { + Arc::as_ptr(&self.0).cast::().hash(hasher); + } } +pub(crate) type Icons = Layers; + impl Icons { - pub(crate) fn new(device: &wgpu::Device) -> Icons { - let (texture, view) = create(device, 1); - Icons { texture, view, layers: HashMap::new(), capacity: 1, generation: 0 } + pub(crate) fn icons(device: &wgpu::Device) -> Icons { + Layers::new(device, "icons", [SIZE, SIZE], 1) } /// The layer of each of `images`, uploading the ones not yet on the GPU. - pub(crate) fn layers(&mut self, device: &wgpu::Device, queue: &Queue, images: &[crate::Image]) -> Vec { - let needed = self.layers.len() as u32 + images.len() as u32; - if needed > self.capacity { - let capacity = needed.next_power_of_two().min(device.limits().max_texture_array_layers); - let (texture, view) = create(device, capacity); - let mut encoder = - device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("latkit icons") }); - encoder.copy_texture_to_texture( - self.texture.as_image_copy(), - texture.as_image_copy(), - wgpu::Extent3d { width: SIZE, height: SIZE, depth_or_array_layers: self.capacity }, - ); - queue.submit([encoder.finish()]); - self.texture = texture; - self.view = view; - self.capacity = capacity; - self.generation += 1; - } - images - .iter() - .map(|image| { - let key = image.rgba.as_ptr() as usize; - if let Some((_, layer)) = self.layers.get(&key) { - return *layer; - } - let layer = (self.layers.len() as u32).min(self.capacity - 1); - let texels = fit(image); - queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: &self.texture, - mip_level: 0, - origin: wgpu::Origin3d { x: 0, y: 0, z: layer }, - aspect: wgpu::TextureAspect::All, - }, - &texels, - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(SIZE * 4), - rows_per_image: Some(SIZE), - }, - wgpu::Extent3d { width: SIZE, height: SIZE, depth_or_array_layers: 1 }, - ); - self.layers.insert(key, (image.rgba.clone(), layer)); - layer - }) - .collect() + pub(crate) fn of(&mut self, device: &wgpu::Device, queue: &Queue, images: &[crate::Image]) -> Result> { + images.iter().map(|image| self.layer(device, queue, Pixels(image.rgba.clone()), || fit(image))).collect() } } diff --git a/crates/latkit/src/gpu/layers.rs b/crates/latkit/src/gpu/layers.rs new file mode 100644 index 00000000..9559419f --- /dev/null +++ b/crates/latkit/src/gpu/layers.rs @@ -0,0 +1,135 @@ +//! Texture arrays holding one layer per distinct image, such as each colormap or icon a program +//! uses. An array grows by copying itself into one twice as deep, up to the device's layer limit. + +use std::collections::HashMap; +use std::hash::Hash; + +use crate::gpu::work::Queue; +use crate::{Error, Result}; + +pub(crate) struct Layers { + /// What the layers hold, for labels and errors, such as "colormaps". + what: &'static str, + /// Texels across and down each layer. + size: [u32; 2], + texture: wgpu::Texture, + pub(crate) view: wgpu::TextureView, + /// The layer holding each key. + held: HashMap, + capacity: u32, + /// Changes whenever the texture is replaced, so views rebind it. + pub(crate) generation: u64, +} + +impl Layers { + /// An array of `capacity` layers of `size` texels, holding `what`. + pub(crate) fn new(device: &wgpu::Device, what: &'static str, size: [u32; 2], capacity: u32) -> Layers { + let (texture, view) = create(device, what, size, capacity); + Layers { what, size, texture, view, held: HashMap::new(), capacity, generation: 0 } + } + + /// The layer holding `key`, uploading `texels()` (RGBA, row after row) the first time. Errors + /// once the array reaches the device's limit on layers. + pub(crate) fn layer( + &mut self, + device: &wgpu::Device, + queue: &Queue, + key: K, + texels: impl FnOnce() -> Vec, + ) -> Result { + if let Some(&layer) = self.held.get(&key) { + return Ok(layer); + } + let layer = self.held.len() as u32; + if layer >= self.capacity { + self.grow(device, queue)?; + } + let [width, height] = self.size; + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &self.texture, + mip_level: 0, + origin: wgpu::Origin3d { x: 0, y: 0, z: layer }, + aspect: wgpu::TextureAspect::All, + }, + &texels(), + wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(width * 4), rows_per_image: Some(height) }, + wgpu::Extent3d { width, height, depth_or_array_layers: 1 }, + ); + self.held.insert(key, layer); + Ok(layer) + } + + /// Doubles the array's layers, within the device's limit, copying the layers it holds. + fn grow(&mut self, device: &wgpu::Device, queue: &Queue) -> Result<()> { + let capacity = (self.capacity * 2).min(device.limits().max_texture_array_layers); + if capacity <= self.capacity { + return Err(Error::Limit(format!("more than {capacity} distinct {}", self.what).into())); + } + let (texture, view) = create(device, self.what, self.size, capacity); + let label = format!("latkit {}", self.what); + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some(&label) }); + let [width, height] = self.size; + encoder.copy_texture_to_texture( + self.texture.as_image_copy(), + texture.as_image_copy(), + wgpu::Extent3d { width, height, depth_or_array_layers: self.capacity }, + ); + queue.submit([encoder.finish()]); + (self.texture, self.view, self.capacity) = (texture, view, capacity); + self.generation += 1; + Ok(()) + } +} + +fn create( + device: &wgpu::Device, + what: &str, + [width, height]: [u32; 2], + layers: u32, +) -> (wgpu::Texture, wgpu::TextureView) { + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some(&format!("latkit {what}")), + size: wgpu::Extent3d { width, height, depth_or_array_layers: layers }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + let view = texture.create_view(&wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2Array), + ..Default::default() + }); + (texture, view) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn layers_grow_for_new_keys_and_stop_at_the_device_limit() { + let gpu = match pollster::block_on(crate::Gpu::new()) { + Ok(gpu) => gpu, + Err(error) if std::env::var_os("LATKIT_REQUIRE_GPU").is_some() => panic!("GPU required: {error}"), + Err(_) => return, + }; + let (device, queue) = (&gpu.0.device, &gpu.0.queue); + let texel = || vec![255; 4]; + let mut layers = Layers::::new(device, "tests", [1, 1], 1); + let first: Vec = (0..5).map(|key| layers.layer(device, queue, key, texel).expect("room")).collect(); + assert_eq!(first, [0, 1, 2, 3, 4], "each key gets its own layer"); + assert_eq!(layers.generation, 3, "grown from 1 to 2, 4, then 8 layers"); + assert_eq!(layers.layer(device, queue, 2, texel).expect("held"), 2, "a key keeps its layer"); + assert_eq!(layers.generation, 3, "and nothing grows for it"); + let max = device.limits().max_texture_array_layers; + let mut full = Layers::::new(device, "tests", [1, 1], max); + for key in 0..max { + full.layer(device, queue, key, texel).expect("within the limit"); + } + let over = full.layer(device, queue, max, texel); + assert!(matches!(over, Err(Error::Limit(_))), "past the limit, an error, not a reused layer"); + } +} diff --git a/crates/latkit/src/gpu/mod.rs b/crates/latkit/src/gpu/mod.rs index d46add8b..5a584523 100644 --- a/crates/latkit/src/gpu/mod.rs +++ b/crates/latkit/src/gpu/mod.rs @@ -5,7 +5,9 @@ pub(crate) mod arena; pub(crate) mod atlas; pub(crate) mod colormaps; pub(crate) mod dictionary; +pub(crate) mod grown; pub(crate) mod icons; +mod layers; pub(crate) mod packed; pub(crate) mod pipelines; pub(crate) mod readback; @@ -324,12 +326,12 @@ impl Gpu { arena: Arena::new(&device, budget.bytes), pieces: arena::Pieces::default(), pipelines: Pipelines::new(&device, counters.clone(), gpu.clone()), - colormaps: Colormaps::new(&device), + colormaps: Colormaps::colormaps(&device), dictionaries: Dictionaries::default(), packed: Packed::new(counters.clone()), tiles: crate::tiles::Cache::default(), atlas: Atlas::new(&device, counters.clone()), - icons: Icons::new(&device), + icons: Icons::icons(&device), readbacks: Readbacks::counted(counters.clone()), host: None, budget, diff --git a/crates/latkit/src/gpu/pipelines.rs b/crates/latkit/src/gpu/pipelines.rs index 14d51b75..c1d20522 100644 --- a/crates/latkit/src/gpu/pipelines.rs +++ b/crates/latkit/src/gpu/pipelines.rs @@ -81,11 +81,12 @@ pub(crate) struct Reads { impl Reads { pub(crate) const ALL: Reads = Reads { joined: true, timed: true, viewed: true }; + pub(crate) const NONE: Reads = Reads { joined: false, timed: false, viewed: false }; } impl Variant { /// `shader` drawing into `format` with `samples` samples per pixel. - pub(crate) fn draw(shader: Shader, format: wgpu::TextureFormat, samples: u32) -> Variant { + pub(crate) const fn draw(shader: Shader, format: wgpu::TextureFormat, samples: u32) -> Variant { Variant { shader, entry: "vertex", @@ -97,7 +98,7 @@ impl Variant { } /// `shader`'s compute entry point `entry`. - pub(crate) fn compute(shader: Shader, entry: &'static str) -> Variant { + pub(crate) const fn compute(shader: Shader, entry: &'static str) -> Variant { Variant { shader, entry, primitive: 0, path: PATH_ENDS, reads: Reads::ALL, target: None } } @@ -539,24 +540,22 @@ impl Pipelines { }) } - /// The pipeline drawing a custom marker of `source` with `shader` (one of the custom variants), - /// or `None` once its WGSL failed to compile. + /// The pipeline `variant` of a custom marker's shader draws a marker of `source` with, or + /// `None` once its WGSL failed to compile. pub(crate) fn custom( &mut self, device: &wgpu::Device, - source: &str, - shader: Shader, - format: wgpu::TextureFormat, - samples: u32, + source: &Arc, + variant: Variant, ) -> Option { - let key = (Arc::::from(source), Variant::draw(shader, format, samples)); + let key = (source.clone(), variant); if let Some(custom) = self.custom.get(&key) { return (!custom.failed).then(|| custom.pipeline.clone()); } self.count(&Asked::Custom(key.0.clone(), key.1)); let scope = device.push_error_scope(wgpu::ErrorFilter::Validation); - let module = custom_module(device, source, shader); - let pipeline = self.pipeline(device, key.1, &module); + let module = custom_module(device, source, variant.shader); + let pipeline = self.pipeline(device, variant, &module); let check: Check = Box::pin(scope.pop()); let custom = Custom { pipeline: pipeline.clone(), check: Some(check), failed: false }; self.custom.insert(key, custom); @@ -580,19 +579,8 @@ impl Pipelines { errors } - /// The pipeline drawing `shader` into `format` with `samples` per pixel. - pub(crate) fn render( - &mut self, - device: &wgpu::Device, - shader: Shader, - format: wgpu::TextureFormat, - samples: u32, - ) -> wgpu::RenderPipeline { - self.render_pipeline(device, Variant::draw(shader, format, samples)) - } - /// The render pipeline `variant` names. - pub(crate) fn render_pipeline(&mut self, device: &wgpu::Device, variant: Variant) -> wgpu::RenderPipeline { + pub(crate) fn render(&mut self, device: &wgpu::Device, variant: Variant) -> wgpu::RenderPipeline { if let Some(pipeline) = self.render.get(&variant) { return pipeline.clone(); } @@ -642,18 +630,8 @@ impl Pipelines { }) } - /// The compute pipeline of `shader`'s entry point `entry`. - pub(crate) fn compute( - &mut self, - device: &wgpu::Device, - shader: Shader, - entry: &'static str, - ) -> wgpu::ComputePipeline { - self.compute_pipeline(device, Variant::compute(shader, entry)) - } - /// The compute pipeline `variant` names. - pub(crate) fn compute_pipeline(&mut self, device: &wgpu::Device, variant: Variant) -> wgpu::ComputePipeline { + pub(crate) fn compute(&mut self, device: &wgpu::Device, variant: Variant) -> wgpu::ComputePipeline { if let Some(pipeline) = self.compute.get(&variant) { return pipeline.clone(); } diff --git a/crates/latkit/src/host/job.rs b/crates/latkit/src/host/job.rs index a2dd2302..ab672f14 100644 --- a/crates/latkit/src/host/job.rs +++ b/crates/latkit/src/host/job.rs @@ -20,6 +20,7 @@ use crate::layout::boxes::Boxes; use crate::layout::route::{Net, Terminal, route}; use crate::layout::{layered::layered, levels::Graph, stress::stress}; use crate::tiles::TileKey; +use crate::wire::{In, Out, codes}; use crate::{Direction, Layered, Layout}; /// Work latkit hands its [`Host`](super::Host) to run in the background: bytes in, bytes out. @@ -32,17 +33,19 @@ pub struct Job(Vec); /// A job's type, stored as its first word. #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Kind { - Layered = 1, - Stress = 2, - Routes = 3, - Shapes = 4, - Tile = 5, - GeoJson = 6, - Lanes = 7, - Pyramid = 8, - Image = 9, + Layered, + Stress, + Routes, + Shapes, + Tile, + GeoJson, + Lanes, + Pyramid, + Image, } +codes!(Kind { Layered = 1, Stress = 2, Routes = 3, Shapes = 4, Tile = 5, GeoJson = 6, Lanes = 7, Pyramid = 8, Image = 9 }); + /// The shapes computed from each row's geometry. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub(crate) enum Shaping { @@ -186,26 +189,17 @@ impl Job { /// pyramid's order and tiles, or an encoded image file. Returns no bytes if the job is /// malformed or fails. pub fn run(self) -> Vec { - let mut input = Reader::new(&self.0); - let kind = input.u32(); - if kind == Some(Kind::Image as u32) { - return run_image(&mut input).unwrap_or_default(); - } - if kind == Some(Kind::Lanes as u32) { - return run_lanes(&mut input).unwrap_or_default(); - } - if kind == Some(Kind::Pyramid as u32) { - return run_pyramid(&mut input).unwrap_or_default(); - } - if kind == Some(Kind::Shapes as u32) { - return run_shapes(&mut input).unwrap_or_default(); - } + let mut input = In::new(&self.0); + let Some(kind) = input.u32().and_then(Kind::from_code) else { return Vec::new() }; let answer = match kind { - Some(k) if k == Kind::Layered as u32 || k == Kind::Stress as u32 => run_layout(k, &mut input), - Some(k) if k == Kind::Routes as u32 => run_routes(&mut input), - Some(k) if k == Kind::Tile as u32 => run_tile(&mut input), - Some(k) if k == Kind::GeoJson as u32 => run_geojson(&mut input), - _ => None, + Kind::Image => return run_image(&mut input).unwrap_or_default(), + Kind::Lanes => return run_lanes(&mut input).unwrap_or_default(), + Kind::Pyramid => return run_pyramid(&mut input).unwrap_or_default(), + Kind::Shapes => return run_shapes(&mut input).unwrap_or_default(), + Kind::Layered | Kind::Stress => run_layout(kind, &mut input), + Kind::Routes => run_routes(&mut input), + Kind::Tile => run_tile(&mut input), + Kind::GeoJson => run_geojson(&mut input), }; answer.map(|patch| patch.to_bytes()).unwrap_or_default() } @@ -215,90 +209,74 @@ impl Job { pub(crate) fn layout(layout: &Layout, placement: &Placement<'_>) -> Job { let mut out = match *layout { Layout::Stress(stress) => { - let mut out = Writer::new(Kind::Stress); + let mut out = job(Kind::Stress); out.f64(stress.gap); out } Layout::Layered(layered) => { - let mut out = Writer::new(Kind::Layered); - out.u32(match layered.flow { - Direction::Right => 0, - Direction::Left => 1, - Direction::Down => 2, - Direction::Up => 3, - }); + let mut out = job(Kind::Layered); + out.u32(layered.flow.code()); out.f64(layered.gap); out.f64(layered.rank_gap); out } Layout::None => unreachable!("a sheet requests a layout only when its scene has one"), }; - out.u32(placement.sizes.len() as u32); + out.len(placement.sizes.len()); for v in 0..placement.sizes.len() { - out.f64s(&placement.sizes[v]); + out.pod(&placement.sizes[v]); out.u32(u32::from(placement.pinned[v].is_some())); - out.f64s(&placement.pinned[v].unwrap_or_default()); + out.pod(&placement.pinned[v].unwrap_or_default()); out.u32(placement.groups[v].unwrap_or(u32::MAX)); - out.f64s(&placement.pads[v]); + out.pod(&placement.pads[v]); out.u32(u32::from(placement.keys[v].is_some())); if let Some(key) = &placement.keys[v] { - out.key(key); + write_key(&mut out, key); } } - out.u32(placement.links.len() as u32); - for link in placement.links { - out.u32(link[0]); - out.u32(link[1]); - } + out.len(placement.links.len()); + out.pod(placement.links); Job(out.0) } /// A job computing `shaping` for every row of each geometry column in `parts`. Its result holds /// each part's shapes, read by [`shapes_answer`]. pub(crate) fn shapes(shaping: Shaping, parts: &[Values]) -> Job { - let mut out = Writer::new(Kind::Shapes); + let mut out = job(Kind::Shapes); out.u32(shaping.code()); - out.columns(parts); + write_columns(&mut out, parts); Job(out.0) } /// A job decoding tile `key` from `bytes`, optionally gzip-compressed, into rows of `tables` /// with their derived shapes. Its result is a patch of those rows. pub(crate) fn tile(key: TileKey, tables: &[Decoded], bytes: &[u8]) -> Job { - let mut out = Writer::new(Kind::Tile); + let mut out = job(Kind::Tile); out.u32(u32::from(key.z)); out.u32(key.x); out.u32(key.y); - out.u32(tables.len() as u32); + out.len(tables.len()); for decoded in tables { - out.text(&decoded.layer); - out.u32(match decoded.kind { - Geometry::Points => 0, - Geometry::Lines => 1, - Geometry::Polygons => 2, - }); - out.text(&decoded.table); - out.u32(decoded.shapes.len() as u32); + out.str(&decoded.layer); + out.u32(decoded.kind.code()); + out.str(&decoded.table); + out.len(decoded.shapes.len()); for shaping in &decoded.shapes { out.u32(shaping.code()); } } - out.0.extend_from_slice(bytes); + out.bytes(bytes); Job(out.0) } /// A job encoding `image` as a file of `format`. Its result is the file's bytes, or no bytes if /// the image cannot be encoded. pub fn encode(image: &crate::Image, format: crate::ImageFormat) -> Job { - let mut out = Writer::new(Kind::Image); - out.u32(match format { - crate::ImageFormat::Png => 0, - crate::ImageFormat::Jpeg { quality } => 0x100 | u32::from(quality), - crate::ImageFormat::WebP => 1, - }); + let mut out = job(Kind::Image); + out.u32(format.code()); out.u32(image.size[0]); out.u32(image.size[1]); - out.0.extend_from_slice(&image.rgba); + out.bytes(&image.rgba); Job(out.0) } @@ -310,9 +288,9 @@ impl Job { if !crate::data::geojson(bytes) { return None; } - let mut out = Writer::new(Kind::GeoJson); - out.text(name); - out.0.extend_from_slice(bytes); + let mut out = job(Kind::GeoJson); + out.str(name); + out.bytes(bytes); Some(Job(out.0)) } @@ -320,10 +298,10 @@ impl Job { /// rows' endpoints `from` and `to`. Its result is every row's lane word as a little-endian /// `u32`, or no bytes when no two rows share endpoints. pub(crate) fn lanes(from: &[u32], to: &[u32]) -> Job { - let mut out = Writer::new(Kind::Lanes); - out.u32(from.len() as u32); - out.words(from); - out.words(to); + let mut out = job(Kind::Lanes); + out.len(from.len()); + out.pod(from); + out.pod(to); Job(out.0) } @@ -331,12 +309,9 @@ impl Job { /// once. Its result holds each table's pyramid, read by /// [`Pyramid::read`](crate::derive::pyramid::Pyramid::read). pub(crate) fn pyramid(leveling: &Leveling) -> Job { - let mut out = Writer::new(Kind::Pyramid); - match leveling.domain { - Domain::Earth => out.f64s(&[0.0, 0.0, 0.0, 0.0]), - Domain::Plane { min, size } => out.f64s(&[1.0, min[0], min[1], size]), - } - out.u32(leveling.tables.len() as u32); + let mut out = job(Kind::Pyramid); + out.pod(&leveling.domain.words()); + out.len(leveling.tables.len()); for table in &leveling.tables { let places = match &table.places { Places::None => 0, @@ -352,15 +327,15 @@ impl Job { out.u32(flags); match &table.places { Places::None => {} - Places::Points(points) => out.words(bytemuck::cast_slice(&points[..])), - Places::Boxes(boxes) => out.words(bytemuck::cast_slice(&boxes[..])), + Places::Points(points) => out.pod(&points[..]), + Places::Boxes(boxes) => out.pod(&boxes[..]), } - out.words(bytemuck::cast_slice(&table.priority)); + out.pod(&table.priority[..]); if let Some((target, ends)) = &table.ends { out.u32(*target); - out.u32(ends.len() as u32); + out.len(ends.len()); for end in ends { - out.words(end); + out.pod(&end[..]); } } } @@ -369,27 +344,23 @@ impl Job { /// A job routing `wiring`. Its result sets each keyed row's wire polylines as field `route`. pub(crate) fn routes(wiring: &Wiring<'_>) -> Job { - let mut out = Writer::new(Kind::Routes); + let mut out = job(Kind::Routes); out.f64(wiring.spacing); for boxes in [wiring.obstacles, wiring.groups] { - out.u32(boxes.len() as u32); - for b in boxes { - out.f64s(b); - } + out.len(boxes.len()); + out.pod(boxes); } - out.u32(wiring.fixed.len() as u32); + out.len(wiring.fixed.len()); for line in wiring.fixed { - out.u32(line.len() as u32); - for point in line { - out.f64s(point); - } + out.len(line.len()); + out.pod(line.as_slice()); } - out.u32(wiring.nets.len() as u32); + out.len(wiring.nets.len()); for (net, key) in wiring.nets.iter().zip(wiring.keys) { - out.key(key); - out.u32(net.terminals.len() as u32); + write_key(&mut out, key); + out.len(net.terminals.len()); for terminal in &net.terminals { - out.f64s(&terminal.at); + out.pod(&terminal.at); out.u32(terminal.side.map_or(u32::MAX, Side::code)); out.u32(terminal.owner.unwrap_or(u32::MAX)); } @@ -398,6 +369,13 @@ impl Job { } } +/// A job's bytes so far: its kind. +fn job(kind: Kind) -> Out { + let mut out = Out::default(); + out.u32(kind.code()); + out +} + impl std::fmt::Debug for Job { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("Job").field(&self.0.len()).finish() @@ -419,16 +397,11 @@ fn keyed(keys: &[Key], values: Vec, write: impl Fn(Table, Vec) -> Table tables.into_iter().fold(Patch::new(), |patch, (name, ids, values)| patch.set(write(Table::new(name, ids), values))) } -fn run_layout(kind: u32, input: &mut Reader<'_>) -> Option { - let options = if kind == Kind::Stress as u32 { +fn run_layout(kind: Kind, input: &mut In<'_>) -> Option { + let options = if kind == Kind::Stress { Layout::Stress(crate::Stress { gap: input.f64()? }) } else { - let flow = match input.u32()? { - 0 => Direction::Right, - 1 => Direction::Left, - 2 => Direction::Down, - _ => Direction::Up, - }; + let flow = Direction::from_code(input.u32()?)?; Layout::Layered(Layered { flow, gap: input.f64()?, rank_gap: input.f64()? }) }; let count = input.u32()? as usize; @@ -442,7 +415,7 @@ fn run_layout(kind: u32, input: &mut Reader<'_>) -> Option { groups.push(Some(input.u32()?).filter(|&g| (g as usize) < count)); pads.push([input.f64()?, input.f64()?, input.f64()?, input.f64()?]); let keyed = input.u32()? != 0; - keys.push(if keyed { Some(input.key()?) } else { None }); + keys.push(if keyed { Some(read_key(input)?) } else { None }); } let links = input.u32()?; let links = (0..links).map(|_| Some([input.u32()?, input.u32()?])).collect::>>()?; @@ -464,7 +437,7 @@ fn run_layout(kind: u32, input: &mut Reader<'_>) -> Option { })) } -fn run_routes(input: &mut Reader<'_>) -> Option { +fn run_routes(input: &mut In<'_>) -> Option { let spacing = input.f64()?; let mut boxes = || { let count = input.u32()?; @@ -481,7 +454,7 @@ fn run_routes(input: &mut Reader<'_>) -> Option { let count = input.u32()?; let (mut nets, mut keys) = (Vec::new(), Vec::new()); for _ in 0..count { - keys.push(input.key()?); + keys.push(read_key(input)?); let terminals = input.u32()?; let terminals = (0..terminals) .map(|_| { @@ -499,29 +472,25 @@ fn run_routes(input: &mut Reader<'_>) -> Option { /// Reads a shapes job's result: a column per part in the job. `None` if `bytes` are malformed. pub(crate) fn shapes_answer(bytes: &[u8]) -> Option> { - Reader::new(bytes).columns() + read_columns(&mut In::new(bytes)) } -fn run_shapes(input: &mut Reader<'_>) -> Option> { +fn run_shapes(input: &mut In<'_>) -> Option> { let shaping = Shaping::from_code(input.u32()?)?; - let parts = input.columns()?; - let mut out = Writer(Vec::new()); - out.columns(&parts.iter().map(|part| shaping.shapes_of(part)).collect::>()); + let parts = read_columns(input)?; + let mut out = Out::default(); + write_columns(&mut out, &parts.iter().map(|part| shaping.shapes_of(part)).collect::>()); Some(out.0) } -fn run_tile(input: &mut Reader<'_>) -> Option { +fn run_tile(input: &mut In<'_>) -> Option { let key = TileKey { z: u8::try_from(input.u32()?).ok()?, x: input.u32()?, y: input.u32()? }; let count = input.u32()?; let tables = (0..count) .map(|_| { - let layer = input.text()?; - let kind = match input.u32()? { - 0 => Geometry::Points, - 1 => Geometry::Lines, - _ => Geometry::Polygons, - }; - let table = input.text()?.into(); + let layer = input.str()?.to_owned(); + let kind = Geometry::from_code(input.u32()?)?; + let table = input.str()?.into(); let shapes = (0..input.u32()?).map(|_| Shaping::from_code(input.u32()?)).collect::>>()?; Some(Decoded { layer, kind, table, shapes }) }) @@ -529,26 +498,19 @@ fn run_tile(input: &mut Reader<'_>) -> Option { decode(input.rest(), key, &tables) } -fn run_image(input: &mut Reader<'_>) -> Option> { - let format = match input.u32()? { - 0 => crate::ImageFormat::Png, - 1 => crate::ImageFormat::WebP, - jpeg => crate::ImageFormat::Jpeg { quality: (jpeg & 0xff) as u8 }, - }; +fn run_image(input: &mut In<'_>) -> Option> { + let format = crate::ImageFormat::from_code(input.u32()?)?; let size = [input.u32()?, input.u32()?]; crate::Image::new(size, input.rest()).ok()?.encode(format).ok() } -fn run_geojson(input: &mut Reader<'_>) -> Option { - let name = input.text()?; +fn run_geojson(input: &mut In<'_>) -> Option { + let name = input.str()?; crate::geo::geojson(name, input.rest()).ok().map(|data| data.to_patch()) } -fn run_pyramid(input: &mut Reader<'_>) -> Option> { - let domain = match [input.f64()?, input.f64()?, input.f64()?, input.f64()?] { - [0.0, ..] => Domain::Earth, - [_, x, y, size] => Domain::Plane { min: [x, y], size }, - }; +fn run_pyramid(input: &mut In<'_>) -> Option> { + let domain = Domain::from_words(input.pod::(4)?.try_into().ok()?); let count = input.u32()?; let mut tables = Vec::with_capacity(count as usize); for _ in 0..count { @@ -557,15 +519,15 @@ fn run_pyramid(input: &mut Reader<'_>) -> Option> { let n = rows as usize; let has = |bit: u32| flags & 1 << bit != 0; let places = match flags >> 2 & 3 { - 1 => Places::Points(input.words(2 * n)?.as_chunks::<2>().0.iter().map(|w| w.map(f32::from_bits)).collect()), - 2 => Places::Boxes(input.words(4 * n)?.as_chunks::<4>().0.iter().map(|w| w.map(f32::from_bits)).collect()), + 1 => Places::Points(input.pod::<[f32; 2]>(n)?.into()), + 2 => Places::Boxes(input.pod::<[f32; 4]>(n)?.into()), _ => Places::None, }; - let priority = if has(4) { input.words(n)?.into_iter().map(f32::from_bits).collect() } else { Arc::from([]) }; + let priority = if has(4) { input.pod::(n)?.into() } else { Arc::from([]) }; let ends = if has(5) { let target = input.u32()?; let count = input.u32()?; - Some((target, (0..count).map(|_| input.words(n).map(Arc::from)).collect::>>()?)) + Some((target, (0..count).map(|_| input.pod::(n).map(Arc::from)).collect::>>()?)) } else { None }; @@ -574,142 +536,59 @@ fn run_pyramid(input: &mut Reader<'_>) -> Option> { Some(crate::derive::pyramid::encode(&crate::derive::pyramid::answer(&Leveling { domain, tables }))) } -fn run_lanes(input: &mut Reader<'_>) -> Option> { +fn run_lanes(input: &mut In<'_>) -> Option> { let rows = input.u32()? as usize; - let (from, to) = (input.words(rows)?, input.words(rows)?); + let (from, to) = (input.pod::(rows)?, input.pod::(rows)?); let words = crate::derive::lane_words(&from, &to)?; - Some(words.iter().flat_map(|word| word.to_le_bytes()).collect()) + let mut out = Out::default(); + out.pod(&words); + Some(out.0) } -/// Writes little-endian words, floats, and keys. -struct Writer(Vec); - -impl Writer { - fn new(kind: Kind) -> Writer { - let mut out = Writer(Vec::new()); - out.u32(kind as u32); - out - } - - fn u32(&mut self, v: u32) { - self.0.extend(v.to_le_bytes()); - } - - fn f64(&mut self, v: f64) { - self.0.extend(v.to_le_bytes()); - } - - /// Writes words, as one block copy on little-endian machines. - fn words(&mut self, words: &[u32]) { - if cfg!(target_endian = "little") { - self.0.extend_from_slice(bytemuck::cast_slice(words)); - } else { - self.0.extend(words.iter().flat_map(|w| w.to_le_bytes())); - } - } - - fn f64s(&mut self, vs: &[f64]) { - for &v in vs { - self.f64(v); - } +/// Writes columns, each as its byte length followed by its values' bytes. +fn write_columns(out: &mut Out, columns: &[Values]) { + out.len(columns.len()); + for values in columns { + let bytes = values_bytes(values); + out.len(bytes.len()); + out.bytes(&bytes); } +} - fn text(&mut self, text: &str) { - self.u32(text.len() as u32); - self.0.extend(text.as_bytes()); - } +/// Reads columns written by [`write_columns`]. +fn read_columns(input: &mut In<'_>) -> Option> { + let count = input.u32()?; + (0..count) + .map(|_| { + let len = input.u32()? as usize; + read_values(input.bytes(len)?).ok() + }) + .collect() +} - /// Writes columns, each as its byte length followed by its values' bytes. - fn columns(&mut self, columns: &[Values]) { - self.u32(columns.len() as u32); - for values in columns { - let bytes = values_bytes(values); - self.u32(bytes.len() as u32); - self.0.extend(bytes); +/// Writes a key: 1 and a numeric id, or 2 and a text id, then the table name. +fn write_key(out: &mut Out, (table, id): &Key) { + match id { + Id::Number(n) => { + out.u32(1); + out.u64(*n); } - } - - /// Writes a key: 1 and a numeric id, or 2 and a text id, then the table name. - fn key(&mut self, (table, id): &Key) { - match id { - Id::Number(n) => { - self.u32(1); - self.0.extend(n.to_le_bytes()); - } - Id::Text(t) => { - self.u32(2); - self.text(t); - } + Id::Text(text) => { + out.u32(2); + out.str(text); } - self.text(table); } + out.str(table); } -/// Reads little-endian words, floats, and keys. Reads return `None` past the end. -struct Reader<'a> { - bytes: &'a [u8], -} - -impl<'a> Reader<'a> { - fn new(bytes: &'a [u8]) -> Reader<'a> { - Reader { bytes } - } - - /// Takes all unread bytes. - fn rest(&mut self) -> &'a [u8] { - std::mem::take(&mut self.bytes) - } - - fn take(&mut self) -> Option<[u8; N]> { - let (head, rest) = self.bytes.split_first_chunk::()?; - self.bytes = rest; - Some(*head) - } - - fn u32(&mut self) -> Option { - self.take().map(u32::from_le_bytes) - } - - fn f64(&mut self) -> Option { - self.take().map(f64::from_le_bytes) - } - - /// Reads `n` words. - fn words(&mut self, n: usize) -> Option> { - let (head, rest) = self.bytes.split_at_checked(n.checked_mul(4)?)?; - self.bytes = rest; - Some(head.as_chunks::<4>().0.iter().map(|&w| u32::from_le_bytes(w)).collect()) - } - - fn text(&mut self) -> Option { - let len = self.u32()? as usize; - let (head, rest) = self.bytes.split_at_checked(len)?; - self.bytes = rest; - String::from_utf8(head.to_vec()).ok() - } - - /// Reads columns written by [`Writer::columns`]. - fn columns(&mut self) -> Option> { - let count = self.u32()?; - (0..count) - .map(|_| { - let len = self.u32()? as usize; - let (head, rest) = self.bytes.split_at_checked(len)?; - self.bytes = rest; - read_values(head).ok() - }) - .collect() - } - - /// Reads a key written by [`Writer::key`]. - fn key(&mut self) -> Option { - let id = match self.u32()? { - 1 => Id::Number(u64::from_le_bytes(self.take()?)), - 2 => Id::from(self.text()?), - _ => return None, - }; - Some((self.text()?.into(), id)) - } +/// Reads a key written by [`write_key`]. +fn read_key(input: &mut In<'_>) -> Option { + let id = match input.u32()? { + 1 => Id::Number(input.u64()?), + 2 => Id::from(input.str()?), + _ => return None, + }; + Some((input.str()?.into(), id)) } #[cfg(test)] diff --git a/crates/latkit/src/image.rs b/crates/latkit/src/image.rs index b14bfefa..b6aecfdd 100644 --- a/crates/latkit/src/image.rs +++ b/crates/latkit/src/image.rs @@ -129,6 +129,26 @@ impl ImageFormat { _ => None, } } + + /// The code an encoding job writes the format as: 0 for PNG, 1 for WebP, and `0x100` plus the + /// quality for JPEG. + pub(crate) fn code(self) -> u32 { + match self { + ImageFormat::Png => 0, + ImageFormat::WebP => 1, + ImageFormat::Jpeg { quality } => 0x100 | u32::from(quality), + } + } + + /// The format written as `code`. + pub(crate) fn from_code(code: u32) -> Option { + match code { + 0 => Some(ImageFormat::Png), + 1 => Some(ImageFormat::WebP), + 0x100..=0x1ff => Some(ImageFormat::Jpeg { quality: (code & 0xff) as u8 }), + _ => None, + } + } } impl std::fmt::Display for ImageFormat { diff --git a/crates/latkit/src/json.rs b/crates/latkit/src/json.rs index 53525dc8..af7878df 100644 --- a/crates/latkit/src/json.rs +++ b/crates/latkit/src/json.rs @@ -26,6 +26,12 @@ pub fn from_json(text: &str) -> Result { Ok(value) } +/// Deserializes JSON `bytes` as `T`, as [`from_json`] reads text. Bytes that are not UTF-8 are +/// invalid too. +pub(crate) fn from_bytes(bytes: &[u8]) -> Result { + from_json(std::str::from_utf8(bytes).map_err(|e| Error::invalid(format!("not UTF-8: {e}")))?) +} + /// Deserializes `T` from `deserializer`. Errors name the path to the part that could not be read. pub(crate) fn read<'de, T: Deserialize<'de>, D: Deserializer<'de>>(deserializer: D) -> Result where diff --git a/crates/latkit/src/lib.rs b/crates/latkit/src/lib.rs index 36d301d5..58ce704e 100644 --- a/crates/latkit/src/lib.rs +++ b/crates/latkit/src/lib.rs @@ -50,6 +50,7 @@ mod video; mod view; #[cfg(all(feature = "web", target_arch = "wasm32"))] pub mod web; +mod wire; #[cfg(feature = "gpu")] pub use data::Source; diff --git a/crates/latkit/src/order.rs b/crates/latkit/src/order.rs index 6ac434a5..2f04c32d 100644 --- a/crates/latkit/src/order.rs +++ b/crates/latkit/src/order.rs @@ -57,6 +57,20 @@ pub(crate) fn number(x: f64) -> u64 { if bits >> 63 == 1 { !bits } else { bits | 1 << 63 } } +/// Maps `x` to a key that orders as `f32::total_cmp` does: the words `lib/reduce.wesl` reduces +/// values to. +#[cfg(feature = "gpu")] +pub(crate) fn number32(x: f32) -> u32 { + let bits = x.to_bits(); + if bits >> 31 == 1 { !bits } else { bits | 1 << 31 } +} + +/// The value whose key is `key`; the inverse of [`number32`]. +#[cfg(feature = "gpu")] +pub(crate) fn from_number32(key: u32) -> f32 { + f32::from_bits(if key >> 31 == 1 { key & !(1 << 31) } else { !key }) +} + /// Packs two keys into one that orders by `high`, then `low`. pub(crate) fn pair(high: u32, low: u32) -> u64 { u64::from(high) << 32 | u64::from(low) @@ -103,4 +117,17 @@ mod tests { by_name(&mut names, |n| n); assert_eq!(names, ["a", "a", "b", "c"]); } + + #[cfg(feature = "gpu")] + #[test] + fn narrow_keys_order_like_their_values_and_invert() { + use super::{from_number32, number32}; + let values = [-1e9_f32, -2.5, -0.0, 0.0, 1e-9, 3.0, 1e9]; + for pair in values.windows(2) { + assert!(number32(pair[0]) <= number32(pair[1]), "{pair:?}"); + } + for v in values { + assert_eq!(from_number32(number32(v)), v); + } + } } diff --git a/crates/latkit/src/pending.rs b/crates/latkit/src/pending.rs index 33e94d7f..57c794ce 100644 --- a/crates/latkit/src/pending.rs +++ b/crates/latkit/src/pending.rs @@ -140,6 +140,52 @@ impl Future for Pending { } } +/// The wakers of whatever watches something, by the watcher's id, each the watcher's latest. Waking +/// wakes every watcher. +#[derive(Default)] +pub(crate) struct Watchers(Mutex>); + +impl Watchers { + /// Registers `waker` for watcher `id`, replacing its previous one. + #[cfg(feature = "gpu")] + pub(crate) fn watch(&self, id: u64, waker: &Waker) { + let mut watchers = self.0.lock().unwrap_or_else(PoisonError::into_inner); + match watchers.iter_mut().find(|(watcher, _)| *watcher == id) { + Some((_, kept)) => kept.clone_from(waker), + None => watchers.push((id, waker.clone())), + } + } + + /// Removes watcher `id`'s waker. Returns how many watchers are left. + #[cfg(feature = "gpu")] + pub(crate) fn unwatch(&self, id: u64) -> usize { + let mut watchers = self.0.lock().unwrap_or_else(PoisonError::into_inner); + watchers.retain(|(watcher, _)| *watcher != id); + watchers.len() + } + + /// Wakes every watcher. + pub(crate) fn wake_all(&self) { + let wakers: Vec = + self.0.lock().unwrap_or_else(PoisonError::into_inner).iter().map(|(_, waker)| waker.clone()).collect(); + for waker in wakers { + waker.wake(); + } + } +} + +/// A waker that wakes every watcher. +#[cfg(feature = "gpu")] +impl std::task::Wake for Watchers { + fn wake(self: Arc) { + self.wake_all(); + } + + fn wake_by_ref(self: &Arc) { + self.wake_all(); + } +} + impl std::fmt::Debug for Pending { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("Pending") diff --git a/crates/latkit/src/scene/areas.rs b/crates/latkit/src/scene/areas.rs index 45299433..36a01353 100644 --- a/crates/latkit/src/scene/areas.rs +++ b/crates/latkit/src/scene/areas.rs @@ -52,7 +52,7 @@ impl Areas { None => c.theme("color", wgsl::THEME_AREA, color)?, } c.channel("visible", &self.style.visible, Store::Flag { word: wgsl::AREA_FLAGS, bit: wgsl::VISIBLE })?; - let mut drawing = Drawing::new(MarkKind::Areas, wgsl::AREA_WORDS); + let mut drawing = Drawing::new(MarkKind::Areas); drawing.geometry = c.derived_path(&triangles_of(&field)); Ok(drawing) } diff --git a/crates/latkit/src/scene/controls.rs b/crates/latkit/src/scene/controls.rs index a739c63e..10b7b6d2 100644 --- a/crates/latkit/src/scene/controls.rs +++ b/crates/latkit/src/scene/controls.rs @@ -1,16 +1,16 @@ use std::time::Duration; use schemars::JsonSchema; -use serde::{Deserialize, Deserializer}; +use serde::Deserialize; -use super::form::{Presets, preset_or, presets, seconds}; +use super::form::{preset_schema, presets, seconds}; /// How a view responds to the pointer and keyboard, and how much it animates on its own: a preset /// (`"none"` or `"inspect"`), or options over the defaults. Each mark controls whether its own rows /// pick, select, and drag; these options control the view itself. #[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(remote = "Self", default, rename_all = "camelCase", deny_unknown_fields)] -#[schemars(transform = presets::)] +#[schemars(transform = preset_schema::)] pub struct Controls { /// Whether dragging the background pans the camera. Default `true`. pub pan: bool, @@ -87,27 +87,7 @@ pub enum Motion { Reduced, } -impl Presets for Controls { - const NAMES: &'static [&'static str] = &["none", "inspect"]; - - fn preset(name: &str) -> Option { - match name { - "none" => Some(Controls::none()), - "inspect" => Some(Controls::inspect()), - _ => None, - } - } - - fn options<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { - Controls::deserialize(deserializer) - } -} - -impl<'de> Deserialize<'de> for Controls { - fn deserialize>(deserializer: D) -> Result { - preset_or(deserializer) - } -} +presets!(Controls { "none" => Controls::none(), "inspect" => Controls::inspect() }); impl Default for Controls { fn default() -> Controls { diff --git a/crates/latkit/src/scene/edges.rs b/crates/latkit/src/scene/edges.rs index 68b8637b..27ad0794 100644 --- a/crates/latkit/src/scene/edges.rs +++ b/crates/latkit/src/scene/edges.rs @@ -144,20 +144,13 @@ impl Edges { theme_width: wgsl::THEME_EDGE_WIDTH, }; self.stroke.compile(c, &words, context.diagram())?; - let mut drawing = Drawing::new(MarkKind::Edges, wgsl::EDGE_WORDS); + let mut drawing = Drawing::new(MarkKind::Edges); drawing.size_px = self.stroke.px(); drawing.flags = flags; - if self.poles { - drawing.path = wgsl::PATH_POLES; - } else if self.joins.is_some() { - // Each net's star follows the route the view computes from its members. - drawing.path = wgsl::PATH_GEOMETRY; + if self.joins.is_some() || context.diagram() { + // A net's star and every wire in a diagram follow the route the view computes. drawing.geometry = c.derived_path("route"); - } else if context.diagram() { - // Every wire follows the route its sheet computed. - drawing.path = wgsl::PATH_GEOMETRY; - drawing.geometry = c.derived_path("route"); - } else { + } else if !self.poles { drawing.geometry = self.bends.as_ref().map_or(wgsl::NONE, |bends| c.path(bends)); drawing.lane = c.derived_bits("lane"); } diff --git a/crates/latkit/src/scene/faces.rs b/crates/latkit/src/scene/faces.rs index 679354db..179bd150 100644 --- a/crates/latkit/src/scene/faces.rs +++ b/crates/latkit/src/scene/faces.rs @@ -47,7 +47,7 @@ impl Faces { None => flags |= wgsl::ENDS_COLOR, } c.channel("visible", &self.style.visible, Store::Flag { word: wgsl::FACE_FLAGS, bit: wgsl::VISIBLE })?; - let mut drawing = Drawing::new(MarkKind::Faces, wgsl::FACE_WORDS); + let mut drawing = Drawing::new(MarkKind::Faces); drawing.flags = flags; Ok(drawing) } diff --git a/crates/latkit/src/scene/form.rs b/crates/latkit/src/scene/form.rs index 475f2874..2293e9e9 100644 --- a/crates/latkit/src/scene/form.rs +++ b/crates/latkit/src/scene/form.rs @@ -48,9 +48,41 @@ pub(crate) fn preset_or<'de, D: Deserializer<'de>, T: Presets>(deserializer: D) deserializer.deserialize_any(Named(PhantomData)) } +/// Declares a type's presets, each name once, as `Presets`. The type reads its options with the +/// `Deserialize` that serde derives as `remote = "Self"`. Unless it ends `; read by hand`, it also +/// gets a `Deserialize` that takes a preset's name or options. +macro_rules! presets { + ($type:ident { $($name:literal => $value:expr),+ $(,)? }) => { + presets!($type { $($name => $value),+ }; read by hand); + impl<'de> serde::Deserialize<'de> for $type { + fn deserialize>(deserializer: D) -> Result<$type, D::Error> { + crate::scene::form::preset_or(deserializer) + } + } + }; + ($type:ident { $($name:literal => $value:expr),+ $(,)? }; read by hand) => { + impl crate::scene::form::Presets for $type { + const NAMES: &'static [&'static str] = &[$($name),+]; + + fn preset(name: &str) -> Option<$type> { + match name { + $($name => Some($value),)+ + _ => None, + } + } + + fn options<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<$type, D::Error> { + $type::deserialize(deserializer) + } + } + }; +} + +pub(crate) use presets; + /// Rewrites `T`'s derived schema to accept a preset name or the derived form, moving the /// description to the whole. -pub(crate) fn presets(schema: &mut Schema) { +pub(crate) fn preset_schema(schema: &mut Schema) { let mut derived = std::mem::replace(schema, json_schema!({})); let description = derived.remove("description"); // Drop unit variants that a preset already names; the preset's name covers them. diff --git a/crates/latkit/src/scene/layout.rs b/crates/latkit/src/scene/layout.rs index 915ba198..77f5ee46 100644 --- a/crates/latkit/src/scene/layout.rs +++ b/crates/latkit/src/scene/layout.rs @@ -1,7 +1,7 @@ use schemars::JsonSchema; -use serde::{Deserialize, Deserializer}; +use serde::Deserialize; -use super::form::{Presets, preset_or, presets}; +use super::form::{preset_schema, presets}; /// How the view places vertex rows that have no position: a name, or /// `{ stress | layered: options }`. In a diagram it places blocks; on a plane or the earth it @@ -12,7 +12,7 @@ use super::form::{Presets, preset_or, presets}; /// [`View::arrange`](crate::View::arrange) lays out every row again. #[derive(Clone, Copy, PartialEq, Debug, Default, Deserialize, JsonSchema)] #[serde(remote = "Self", rename_all = "camelCase", deny_unknown_fields)] -#[schemars(transform = presets::)] +#[schemars(transform = preset_schema::)] pub enum Layout { /// No layout: vertices without a position are not drawn. The default. #[default] @@ -23,28 +23,11 @@ pub enum Layout { Layered(Layered), } -impl Presets for Layout { - const NAMES: &'static [&'static str] = &["none", "stress", "layered"]; - - fn preset(name: &str) -> Option { - match name { - "none" => Some(Layout::None), - "stress" => Some(Layout::Stress(Stress::default())), - "layered" => Some(Layout::Layered(Layered::default())), - _ => None, - } - } - - fn options<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { - Layout::deserialize(deserializer) - } -} - -impl<'de> Deserialize<'de> for Layout { - fn deserialize>(deserializer: D) -> Result { - preset_or(deserializer) - } -} +presets!(Layout { + "none" => Layout::None, + "stress" => Layout::Stress(Stress::default()), + "layered" => Layout::Layered(Layered::default()), +}); /// Stress majorization: vertices move until drawn distances match graph distances, then overlapping /// boxes separate, keeping a quarter of `gap` between them. @@ -123,6 +106,8 @@ pub enum Direction { Up, } +crate::wire::codes!(Direction { Right = 0, Left = 1, Down = 2, Up = 3 }); + impl From for Layout { fn from(stress: Stress) -> Layout { Layout::Stress(stress) diff --git a/crates/latkit/src/scene/lines.rs b/crates/latkit/src/scene/lines.rs index 79492fec..7f4e36cc 100644 --- a/crates/latkit/src/scene/lines.rs +++ b/crates/latkit/src/scene/lines.rs @@ -69,7 +69,7 @@ impl Lines { } pub(crate) fn compile(&self, c: &mut Compiler<'_>, context: Context<'_>) -> Result { - let mut drawing = Drawing::new(MarkKind::Lines, wgsl::LINE_WORDS); + let mut drawing = Drawing::new(MarkKind::Lines); drawing.size_px = self.stroke.px(); if matches!(context.space, Space::Plot(_)) { if self.geometry.is_some() { @@ -102,7 +102,6 @@ impl Lines { } None => return Err(Error::invalid("name the path field each row runs along").at("geometry")), } - drawing.path = wgsl::PATH_GEOMETRY; // On the earth, a line follows a derived copy of its path that bends with the globe. drawing.geometry = if matches!(context.space, Space::Earth(_)) { c.derived_path(&path_of(&field)) diff --git a/crates/latkit/src/scene/mark.rs b/crates/latkit/src/scene/mark.rs index 30927449..2473c8e3 100644 --- a/crates/latkit/src/scene/mark.rs +++ b/crates/latkit/src/scene/mark.rs @@ -165,6 +165,52 @@ impl MarkKind { } } + /// The code shaders tell it by. + pub(crate) fn code(self) -> u32 { + match self { + MarkKind::Vertices => wgsl::MARK_VERTICES, + MarkKind::Edges => wgsl::MARK_EDGES, + MarkKind::Faces => wgsl::MARK_FACES, + MarkKind::Lines => wgsl::MARK_LINES, + MarkKind::Areas => wgsl::MARK_AREAS, + } + } + + /// Kind words per row. + pub(crate) fn stride(self) -> u32 { + match self { + MarkKind::Vertices => wgsl::VERTEX_WORDS, + MarkKind::Edges => wgsl::EDGE_WORDS, + MarkKind::Faces => wgsl::FACE_WORDS, + MarkKind::Lines => wgsl::LINE_WORDS, + MarkKind::Areas => wgsl::AREA_WORDS, + } + } + + /// The shaders that draw its rows: every row, and the rows its focus lights, which faces lack: + /// a face shows focus by its color alone. With `custom`, a vertex mark's custom marker's. + pub(crate) fn shaders(self, custom: bool) -> (wgsl::Shader, Option) { + use wgsl::Shader; + match self { + MarkKind::Vertices if custom => (Shader::PointsCustom, Some(Shader::PointsCustomFocus)), + MarkKind::Vertices => (Shader::Points, Some(Shader::PointsFocus)), + MarkKind::Edges | MarkKind::Lines => (Shader::Strokes, Some(Shader::StrokesFocus)), + MarkKind::Areas => (Shader::Areas, Some(Shader::AreasFocus)), + MarkKind::Faces => (Shader::Triangles, None), + } + } + + /// The bounds shader's entry point that measures its extents for a fit; `None` for edges and + /// faces, whose rows lie between the vertices that frame them. + pub(crate) fn bounds(self) -> Option<&'static str> { + match self { + MarkKind::Vertices => Some("points"), + MarkKind::Lines => Some("strokes"), + MarkKind::Areas => Some("areas"), + MarkKind::Edges | MarkKind::Faces => None, + } + } + /// The key a mark of this kind has in JSON. pub(crate) fn name(self) -> &'static str { match self { @@ -202,6 +248,14 @@ impl Primitive { Primitive::Triangles => wgsl::TRIANGLES, } } + + /// Vertices per instance: 4 for the quad of a point or segment, 3 for a triangle. + pub(crate) fn corners(self) -> u32 { + match self { + Primitive::Points | Primitive::Strokes => 4, + Primitive::Triangles => 3, + } + } } /// How a vertex mark's markers draw: the marker kind (built in or custom), its shape, and the @@ -265,13 +319,10 @@ impl Context<'_> { #[derive(Clone, PartialEq, Debug)] pub(crate) struct Drawing { pub(crate) kind: MarkKind, - /// The primitive the mark draws its rows with. - pub(crate) primitive: Primitive, /// The path a stroke's segments follow, or where a triangle's corners come from: one of the /// `PATH_*` codes. pub(crate) path: u32, - /// Kind words per row, then the extra words per row that follow them. - pub(crate) stride: u32, + /// Extra words per row, after the kind's own. pub(crate) extras: u32, pub(crate) flags: u32, pub(crate) markers: Markers, @@ -293,18 +344,11 @@ pub(crate) struct Drawing { } impl Drawing { - pub(crate) fn new(kind: MarkKind, stride: u32) -> Drawing { - let path = match kind { - MarkKind::Edges | MarkKind::Faces => wgsl::PATH_ENDS, - MarkKind::Lines => wgsl::PATH_TRACE, - MarkKind::Areas => wgsl::PATH_GEOMETRY, - MarkKind::Vertices => wgsl::NONE, - }; + /// A drawing of a `kind` mark, before its options are compiled. [`Mark::compile`] sets its path. + pub(crate) fn new(kind: MarkKind) -> Drawing { Drawing { kind, - primitive: kind.primitive(), - path, - stride, + path: wgsl::PATH_ENDS, extras: 0, flags: 0, markers: Markers::default(), @@ -387,19 +431,19 @@ impl Mark { stroke.flow != Stroke::default().flow } - /// The path a stroke mark's rows follow, or where an area's triangles come from: one of the - /// `PATH_` codes, which its pipelines are built for; `None` for vertices and faces. In a - /// diagram every wire follows the route its sheet computed. On a plot a line traces its - /// samples, and elsewhere it follows its path field. - pub(crate) fn path(&self, space: &Space) -> Option { + /// The path a stroke mark's rows follow, or where a triangle's corners come from: one of the + /// `PATH_` codes, which its pipelines are built for. A net's star and every wire in a diagram + /// follow the route the view computes. On a plot a line traces its samples, and elsewhere it + /// follows its path field. Faces take their corners from the vertices they join, and vertices + /// have no path, so both read `PATH_ENDS`. + pub(crate) fn path(&self, space: &Space) -> u32 { match self { - Mark::Edges(e) if e.poles => Some(wgsl::PATH_POLES), - Mark::Edges(e) if e.joins.is_some() => Some(wgsl::PATH_GEOMETRY), - Mark::Edges(_) if matches!(space, Space::Diagram(_)) => Some(wgsl::PATH_GEOMETRY), - Mark::Edges(_) => Some(wgsl::PATH_ENDS), - Mark::Lines(_) if matches!(space, Space::Plot(_)) => Some(wgsl::PATH_TRACE), - Mark::Lines(_) | Mark::Areas(_) => Some(wgsl::PATH_GEOMETRY), - Mark::Vertices(_) | Mark::Faces(_) => None, + Mark::Edges(e) if e.poles => wgsl::PATH_POLES, + Mark::Edges(e) if e.joins.is_some() => wgsl::PATH_GEOMETRY, + Mark::Edges(_) if matches!(space, Space::Diagram(_)) => wgsl::PATH_GEOMETRY, + Mark::Lines(_) if matches!(space, Space::Plot(_)) => wgsl::PATH_TRACE, + Mark::Lines(_) | Mark::Areas(_) => wgsl::PATH_GEOMETRY, + Mark::Vertices(_) | Mark::Edges(_) | Mark::Faces(_) => wgsl::PATH_ENDS, } } @@ -488,6 +532,7 @@ impl Mark { Mark::Lines(m) => m.compile(compiler, context), Mark::Areas(m) => m.compile(compiler, context), }?; + drawing.path = self.path(context.space); let style = self.style(); if let Some(priority) = &style.priority { compiler.check_data("priority", priority)?; @@ -510,7 +555,7 @@ fn compile_label( drawing: &mut Drawing, ) -> Result<()> { let extra = drawing.extras; - let word = |k: u32| drawing.stride + extra + k; + let word = |k: u32| drawing.kind.stride() + extra + k; let column = c.text("text", &label.text, word(wgsl::LABEL_CODE))?; let priority: Expr = priority.cloned().unwrap_or_else(|| 0.0.into()).scale(0.0..=255.0).into(); c.channel("priority", &priority, Store::Number { word: word(wgsl::LABEL_PRIORITY), fallback: None })?; @@ -539,11 +584,69 @@ pub(crate) fn described(lines: &[&str]) -> String { lines.iter().map(|line| line.strip_prefix(' ').unwrap_or(line)).take_while(|line| !line.starts_with("```")); let paragraphs: Vec = prose.collect::>().split(|line| line.is_empty()).map(|p| p.join(" ")).collect(); - paragraphs.into_iter().filter(|p| !p.is_empty()).collect::>().join( - " - -", - ) + paragraphs.into_iter().filter(|p| !p.is_empty()).collect::>().join("\n\n") +} + +/// The options every mark's JSON form has, and those of the marks drawn as strokes. +const MARK_OPTIONS: [&str; 10] = + ["of", "color", "visible", "priority", "name", "pick", "select", "drag", "label", "legend"]; +const STROKE_OPTIONS: [&str; 3] = ["width", "dash", "flow"]; + +/// A mark's schema from its form's: the options every mark has, and when `stroked` the stroke +/// options, moved to the `MarkOptions` and `StrokeOptions` definitions it extends, so each is +/// described once. +pub(crate) fn extending( + generator: &mut schemars::SchemaGenerator, + mut form: schemars::Schema, + stroked: bool, +) -> schemars::Schema { + let mut parts = vec![shared(generator, &mut form, "MarkOptions", "The options every mark has.", &MARK_OPTIONS)]; + if stroked { + let about = "The options of marks drawn as strokes: edges and lines."; + parts.push(shared(generator, &mut form, "StrokeOptions", about, &STROKE_OPTIONS)); + } + // The whole refuses keys no part has; a part alone cannot, since it lacks the others' keys. + form.remove("additionalProperties"); + parts.push(form.into()); + schemars::json_schema!({ "allOf": parts, "unevaluatedProperties": false }) +} + +/// Moves `keys` from `form`'s properties into definition `name`, described as `about`, which the +/// first mark to share it defines. Returns a reference to it. +fn shared( + generator: &mut schemars::SchemaGenerator, + form: &mut schemars::Schema, + name: &str, + about: &str, + keys: &[&str], +) -> serde_json::Value { + use serde_json::{Value, json}; + let properties = form.get_mut("properties").and_then(Value::as_object_mut).expect("a mark's form has options"); + let moved: serde_json::Map = keys + .iter() + .map(|&key| { + (key.to_owned(), properties.shift_remove(key).unwrap_or_else(|| panic!("every mark has option {key}"))) + }) + .collect(); + let required: Vec = match form.get_mut("required").and_then(Value::as_array_mut) { + Some(required) => { + let (shared, own) = required.drain(..).partition(|key| key.as_str().is_some_and(|key| keys.contains(&key))); + *required = own; + shared + } + None => Vec::new(), + }; + if form.get("required").and_then(Value::as_array).is_some_and(Vec::is_empty) { + form.remove("required"); + } + generator.definitions_mut().entry(name).or_insert_with(|| { + let mut definition = json!({ "type": "object", "description": about, "properties": moved }); + if !required.is_empty() { + definition["required"] = required.into(); + } + definition + }); + json!({ "$ref": format!("#/$defs/{name}") }) } /// Declares a mark type: a struct holding the options every mark has, the stroke options when it @@ -581,7 +684,9 @@ macro_rules! mark { } fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - let mut schema =
    ::json_schema(generator); + let form = ::json_schema(generator); + let stroke: &[&str] = &[$(stringify!($stroke)),*]; + let mut schema = $crate::scene::mark::extending(generator, form, !stroke.is_empty()); schema.insert("description".into(), $crate::scene::mark::described(&[$($line),*]).into()); schema } diff --git a/crates/latkit/src/scene/marker.rs b/crates/latkit/src/scene/marker.rs index 222aa038..e5432389 100644 --- a/crates/latkit/src/scene/marker.rs +++ b/crates/latkit/src/scene/marker.rs @@ -1,9 +1,9 @@ use std::sync::Arc; use schemars::JsonSchema; -use serde::{Deserialize, Deserializer}; +use serde::Deserialize; -use super::form::{Presets, preset_or, presets}; +use super::form::{preset_schema, presets}; use super::mark::Markers; use crate::gpu::shaders as wgsl; use crate::{Color, Expr, Image}; @@ -12,7 +12,7 @@ use crate::{Color, Expr, Image}; /// `{ shape | block | gauge | pie | icon | wgsl: options }`. An ellipse by default. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(remote = "Self", rename_all = "camelCase", deny_unknown_fields)] -#[schemars(transform = presets::)] +#[schemars(transform = preset_schema::)] pub enum Marker { /// A filled shape. Shape(Shape), @@ -75,30 +75,13 @@ pub enum Shape { Diamond, } -impl Presets for Marker { - const NAMES: &'static [&'static str] = &["ellipse", "rectangle", "rounded", "diamond", "block"]; - - fn preset(name: &str) -> Option { - Some(match name { - "ellipse" => Marker::Shape(Shape::Ellipse), - "rectangle" => Marker::Shape(Shape::Rectangle), - "rounded" => Marker::Shape(Shape::Rounded), - "diamond" => Marker::Shape(Shape::Diamond), - "block" => Marker::block(), - _ => return None, - }) - } - - fn options<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { - Marker::deserialize(deserializer) - } -} - -impl<'de> Deserialize<'de> for Marker { - fn deserialize>(deserializer: D) -> Result { - preset_or(deserializer) - } -} +presets!(Marker { + "ellipse" => Marker::Shape(Shape::Ellipse), + "rectangle" => Marker::Shape(Shape::Rectangle), + "rounded" => Marker::Shape(Shape::Rounded), + "diamond" => Marker::Shape(Shape::Diamond), + "block" => Marker::block(), +}); impl Default for Marker { fn default() -> Marker { diff --git a/crates/latkit/src/scene/port.rs b/crates/latkit/src/scene/port.rs index e66d4c12..295a89e7 100644 --- a/crates/latkit/src/scene/port.rs +++ b/crates/latkit/src/scene/port.rs @@ -1,8 +1,8 @@ use schemars::JsonSchema; -use serde::{Deserialize, Deserializer}; +use serde::Deserialize; use super::Shape; -use super::form::{Presets, preset_or, presets}; +use super::form::{preset_schema, presets}; use crate::gpu::shaders as wgsl; /// A port on one side of a vertex's marker, where a net connects: a side name, or options. The @@ -15,7 +15,7 @@ use crate::gpu::shaders as wgsl; /// ``` #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(remote = "Self", rename_all = "camelCase", deny_unknown_fields)] -#[schemars(transform = presets::)] +#[schemars(transform = preset_schema::)] pub struct Port { /// The side of the marker it sits on. pub side: Side, @@ -34,24 +34,12 @@ pub struct Port { pub output: bool, } -impl Presets for Port { - const NAMES: &'static [&'static str] = &["left", "right", "top", "bottom"]; - - fn preset(name: &str) -> Option { - let side = [Side::Left, Side::Right, Side::Top, Side::Bottom]; - Port::NAMES.iter().position(|n| *n == name).map(|i| Port::new(side[i])) - } - - fn options<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { - Port::deserialize(deserializer) - } -} - -impl<'de> Deserialize<'de> for Port { - fn deserialize>(deserializer: D) -> Result { - preset_or(deserializer) - } -} +presets!(Port { + "left" => Port::new(Side::Left), + "right" => Port::new(Side::Right), + "top" => Port::new(Side::Top), + "bottom" => Port::new(Side::Bottom), +}); /// A side of a marker. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Deserialize, JsonSchema)] @@ -108,18 +96,4 @@ impl From for Port { } } -impl Side { - pub(crate) fn code(self) -> u32 { - match self { - Side::Left => 0, - Side::Right => 1, - Side::Top => 2, - Side::Bottom => 3, - } - } - - /// The side of `code`, or `None` for any other number. - pub(crate) fn from_code(code: u32) -> Option { - [Side::Left, Side::Right, Side::Top, Side::Bottom].get(code as usize).copied() - } -} +crate::wire::codes!(Side { Left = 0, Right = 1, Top = 2, Bottom = 3 }); diff --git a/crates/latkit/src/scene/space.rs b/crates/latkit/src/scene/space.rs index ecccec2a..8620f538 100644 --- a/crates/latkit/src/scene/space.rs +++ b/crates/latkit/src/scene/space.rs @@ -1,7 +1,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Deserializer}; -use super::form::{Presets, preset_or, presets}; +use super::form::{preset_schema, presets}; /// The coordinate space of a scene's positions: a name, or /// `{ plane | diagram | earth | plot: options }`. @@ -11,7 +11,7 @@ use super::form::{Presets, preset_or, presets}; /// orthogonal routes. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(remote = "Self", rename_all = "camelCase", deny_unknown_fields)] -#[schemars(transform = presets::)] +#[schemars(transform = preset_schema::)] pub enum Space { /// Longitude and latitude in degrees, on a flat map or a globe. Earth(Earth), @@ -23,29 +23,12 @@ pub enum Space { Diagram(Diagram), } -impl Presets for Space { - const NAMES: &'static [&'static str] = &["plane", "diagram", "earth", "plot"]; - - fn preset(name: &str) -> Option { - Some(match name { - "plane" => Space::Plane(Plane::default()), - "diagram" => Space::Diagram(Diagram::default()), - "earth" => Space::Earth(Earth::default()), - "plot" => Space::Plot(Plot::default()), - _ => return None, - }) - } - - fn options<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { - Space::deserialize(deserializer) - } -} - -impl<'de> Deserialize<'de> for Space { - fn deserialize>(deserializer: D) -> Result { - preset_or(deserializer) - } -} +presets!(Space { + "plane" => Space::Plane(Plane::default()), + "diagram" => Space::Diagram(Diagram::default()), + "earth" => Space::Earth(Earth::default()), + "plot" => Space::Plot(Plot::default()), +}); impl Default for Space { fn default() -> Space { diff --git a/crates/latkit/src/scene/tests.rs b/crates/latkit/src/scene/tests.rs index 652376ad..8bbc7b02 100644 --- a/crates/latkit/src/scene/tests.rs +++ b/crates/latkit/src/scene/tests.rs @@ -35,6 +35,18 @@ fn scenes_match_builders() { assert_eq!(scene(json), built); } +#[test] +fn every_mark_defaults_as_its_json_form_does() { + let json = |kind: &str| -> Mark { + serde_json::from_str(&format!(r#"{{ "{kind}": {{ "of": ["d", "t"] }} }}"#)).expect(kind) + }; + assert_eq!(json("vertices"), Vertices::of("d", "t").into()); + assert_eq!(json("edges"), Edges::of("d", "t").into()); + assert_eq!(json("faces"), Faces::of("d", "t").into()); + assert_eq!(json("lines"), Lines::of("d", "t").into()); + assert_eq!(json("areas"), Areas::of("d", "t").into()); +} + #[test] fn rows_pick_by_default_and_geometry_only_when_asked() { let picks = |mark: Mark| mark.picks(); diff --git a/crates/latkit/src/scene/theme.rs b/crates/latkit/src/scene/theme.rs index e14cb635..648a22fa 100644 --- a/crates/latkit/src/scene/theme.rs +++ b/crates/latkit/src/scene/theme.rs @@ -5,7 +5,7 @@ use serde::de::{self, Deserializer}; use serde::{Deserialize, Serialize, Serializer}; use serde_json::Value; -use super::form::{Presets, presets}; +use super::form::{Presets, preset_schema, presets}; use crate::Color; /// Default colors and sizes for marks whose options leave them unset or missing, and the look of @@ -392,21 +392,7 @@ impl Default for Theme { } } -impl Presets for Theme { - const NAMES: &'static [&'static str] = &["dark", "light"]; - - fn preset(name: &str) -> Option { - match name { - "dark" => Some(Theme::dark()), - "light" => Some(Theme::light()), - _ => None, - } - } - - fn options<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { - Theme::deserialize(deserializer) - } -} +presets!(Theme { "dark" => Theme::dark(), "light" => Theme::light() }; read by hand); /// Reads a base theme's name, or options merged over the base's serialized form group by group: an /// omitted option keeps the base's value, and `null` clears an optional color. @@ -467,7 +453,7 @@ fn with_base(schema: &mut schemars::Schema) { let base = serde_json::json!({ "description": "The theme these options change; dark unless set.", "enum": Theme::NAMES }); properties.insert("base".into(), base); } - presets::(schema); + preset_schema::(schema); } #[cfg(test)] diff --git a/crates/latkit/src/scene/vertices.rs b/crates/latkit/src/scene/vertices.rs index 0838bf6b..2a7c3caa 100644 --- a/crates/latkit/src/scene/vertices.rs +++ b/crates/latkit/src/scene/vertices.rs @@ -189,7 +189,7 @@ impl Vertices { let column = c.bits(reference).map_err(|e| e.at(reference).at("port"))?; ports.push((reference.clone(), port.clone(), column)); } - let mut drawing = Drawing::new(MarkKind::Vertices, wgsl::VERTEX_WORDS); + let mut drawing = Drawing::new(MarkKind::Vertices); // The marker's half-size stays within its size's pixel bounds; a `size` bounds the whole // width, so its bounds are halved. drawing.size_px = match (&self.size, &self.radius) { diff --git a/crates/latkit/src/shot.rs b/crates/latkit/src/shot.rs index 222640ec..2d9efd8c 100644 --- a/crates/latkit/src/shot.rs +++ b/crates/latkit/src/shot.rs @@ -161,24 +161,31 @@ impl Shot { } /// Checks without a GPU that the shot's pictures can compose into `pixels`. Requires a positive - /// scale and panels that cover pixels. NV12 also requires a width that is a multiple of 4, a - /// height that is a multiple of 2, and no transparency. + /// scale, a size of at least one whole block of `pixels` (4×2 for NV12), and panels that cover + /// pixels. NV12 also requires no transparency. pub fn check(&self, pixels: Pixels) -> Result<()> { if !(self.scale.is_finite() && self.scale > 0.0) { return Err(Error::invalid("a shot's scale must be positive").at("scale")); } - self.panels.rects(self.size, align(pixels))?; - let [width, height] = self.size; - if pixels == Pixels::Nv12 && (width % 4 != 0 || height % 2 != 0) { - let reason = format!("NV12 video needs a width divisible by 4 and an even height, not {width}×{height}"); - return Err(Error::invalid(reason).at("size")); + let size = self.rounded(pixels).size; + if size.contains(&0) { + let [width, height] = align(pixels); + return Err(Error::invalid(format!("must be at least {width}×{height} pixels")).at("size")); } + self.panels.rects(size, align(pixels))?; if pixels == Pixels::Nv12 && self.transparent { return Err(Error::invalid("a video has no alpha channel, so it cannot be transparent").at("transparent")); } Ok(()) } + /// This shot with its size rounded down to whole blocks of `pixels`: for NV12, a width that is + /// a multiple of 4 and an even height. + pub(crate) fn rounded(&self, pixels: Pixels) -> Shot { + let block = align(pixels); + Shot { size: [0, 1].map(|i| self.size[i] - self.size[i] % block[i]), ..self.clone() } + } + /// Takes the picture of `views`, one per panel, once every view is complete as the [`Capture`] /// says. Each view draws its data as of when the picture began. /// @@ -513,12 +520,13 @@ impl Drawing { } } -/// The compose entry points for `pixels`: the background fill and the per-panel pass, plus the -/// pixel block each thread writes. -fn composing(pixels: Pixels) -> (&'static str, &'static str, [u32; 2]) { +/// The compose passes for `pixels`: the background fill and the per-panel pass, plus the pixel +/// block each thread writes. +fn composing(pixels: Pixels) -> (Variant, Variant, [u32; 2]) { + let pass = |entry| Variant::compute(Shader::Compose, entry); match pixels { - Pixels::Rgba => ("fill_rgba", "rgba", [1, 1]), - Pixels::Nv12 => ("fill_nv12", "nv12", [4, 2]), + Pixels::Rgba => (pass("fill_rgba"), pass("rgba"), [1, 1]), + Pixels::Nv12 => (pass("fill_nv12"), pass("nv12"), [4, 2]), } } @@ -527,14 +535,7 @@ impl Picture { /// together, and `waker` wakes when they finish. fn composes(&self, pixels: Pixels, waker: &Waker) -> bool { let (fill, each, _) = composing(pixels); - let needs: Vec = self - .fill - .as_ref() - .map(|_| fill) - .into_iter() - .chain([each]) - .map(|entry| Asked::Shader(Variant::compute(Shader::Compose, entry))) - .collect(); + let needs: Vec = self.fill.as_ref().map(|_| fill).into_iter().chain([each]).map(Asked::Shader).collect(); let mut inner = self.gpu.0.lock(); inner.pipelines.ask(self.gpu.device(), &needs, crate::gpu::pipelines::Urgency::Draw, waker); inner.pipelines.ready(&needs) @@ -552,12 +553,12 @@ impl Picture { pass.set_bind_group(0, &inner.pipelines.empty, &[]); pass.set_bind_group(1, &inner.pipelines.empty, &[]); if let Some(group) = &self.fill { - pass.set_pipeline(&inner.pipelines.compute(device, Shader::Compose, fill)); + pass.set_pipeline(&inner.pipelines.compute(device, fill)); pass.set_bind_group(2, group, &[]); let [x, y] = groups(size); pass.dispatch_workgroups(x, y, 1); } - let pipeline = inner.pipelines.compute(device, Shader::Compose, each); + let pipeline = inner.pipelines.compute(device, each); pass.set_pipeline(&pipeline); for panel in &self.panels { pass.set_bind_group(2, &panel.group, &[]); diff --git a/crates/latkit/src/tiles/cache.rs b/crates/latkit/src/tiles/cache.rs index f3d79b57..db2375fe 100644 --- a/crates/latkit/src/tiles/cache.rs +++ b/crates/latkit/src/tiles/cache.rs @@ -4,8 +4,8 @@ //! all of them. use std::collections::hash_map::Entry; -use std::sync::{Arc, Mutex, PoisonError}; -use std::task::{Wake, Waker}; +use std::sync::Arc; +use std::task::Waker; use rustc_hash::FxHashMap; @@ -13,6 +13,7 @@ use super::{frames, vector}; use crate::data::Manifest; use crate::derive::Jobs; use crate::host::Request; +use crate::pending::Watchers; use crate::{Error, Pending}; /// Bytes first read from an archive source (a URL ending in `.pmtiles`): its header, and in most @@ -25,10 +26,11 @@ pub(crate) struct Cache { sources: FxHashMap, Fetched>, } -/// One tile source: its open state, and the waker for the sets drawing it. +/// One tile source: its open state, the sets drawing it, and the waker that wakes them all, which +/// its work wakes as it finishes. pub(crate) struct Fetched { pub(crate) state: State, - sets: Arc, + sets: Arc, waker: Waker, } @@ -47,24 +49,6 @@ pub(crate) enum Holds { Vector(Box), } -/// The sets drawing a source, each with its waker. Work finishing for the source wakes them all. -#[derive(Default)] -struct Sets(Mutex>); - -impl Wake for Sets { - fn wake(self: Arc) { - self.wake_by_ref(); - } - - fn wake_by_ref(self: &Arc) { - let wakers: Vec = - self.0.lock().unwrap_or_else(PoisonError::into_inner).iter().map(|(_, w)| w.clone()).collect(); - for waker in wakers { - waker.wake(); - } - } -} - impl Cache { /// The source at `url`, registering set `set` as drawing it, to be woken by `jobs`'s waker as /// the source's work finishes. Starts opening the source if no set drew it before, and opens it @@ -73,18 +57,13 @@ impl Cache { let fetched = match self.sources.entry(url.clone()) { Entry::Occupied(entry) => entry.into_mut(), Entry::Vacant(entry) => { - let sets = Arc::new(Sets::default()); + let sets = Arc::new(Watchers::default()); let waker = Waker::from(sets.clone()); let state = start(url, &jobs.waking(&waker)); entry.insert(Fetched { state, sets, waker }) } }; - { - let mut sets = fetched.sets.0.lock().unwrap_or_else(PoisonError::into_inner); - if !sets.iter().any(|(s, _)| *s == set) { - sets.push((set, jobs.waker().clone())); - } - } + fetched.sets.watch(set, jobs.waker()); fetched.settle(url, jobs); fetched } @@ -103,12 +82,7 @@ impl Cache { /// it. pub(crate) fn release(&mut self, url: &str, set: u64) { let Some(fetched) = self.sources.get_mut(url) else { return }; - let left = { - let mut sets = fetched.sets.0.lock().unwrap_or_else(PoisonError::into_inner); - sets.retain(|(s, _)| *s != set); - sets.len() - }; - if left == 0 { + if fetched.sets.unwatch(set) == 0 { self.sources.remove(url); return; } @@ -140,7 +114,8 @@ impl Fetched { Some(Err(error)) => State::Failed(error), None => match vector::Fetched::open(url, &bytes, &jobs) { Ok(vector) => State::Open(Holds::Vector(Box::new(vector))), - Err(reason) => State::Failed(Error::Io(reason.into())), + // The bytes arrived, but they are not a tile index. + Err(reason) => State::Failed(Error::invalid(reason)), }, }, }; diff --git a/crates/latkit/src/tiles/mod.rs b/crates/latkit/src/tiles/mod.rs index 3f38b1e9..361b5a5f 100644 --- a/crates/latkit/src/tiles/mod.rs +++ b/crates/latkit/src/tiles/mod.rs @@ -113,7 +113,7 @@ impl TileKey { /// West, south, east, and north, in degrees. pub(crate) fn bounds(self) -> [f64; 4] { let n = f64::from(1_u32 << self.z); - let latitude = |t: f64| (std::f64::consts::PI * (1.0 - 2.0 * t)).sinh().atan().to_degrees(); + let latitude = crate::geo::mercator::latitude_down; let (x, y) = (f64::from(self.x), f64::from(self.y)); [x / n * 360.0 - 180.0, latitude((y + 1.0) / n), (x + 1.0) / n * 360.0 - 180.0, latitude(y / n)] } diff --git a/crates/latkit/src/tiles/sets.rs b/crates/latkit/src/tiles/sets.rs index 4510451d..d62c1986 100644 --- a/crates/latkit/src/tiles/sets.rs +++ b/crates/latkit/src/tiles/sets.rs @@ -221,7 +221,7 @@ impl TileSets { && !set.told { set.told = true; - self.errors.push(Error::Io(format!("tiles {}: {error}", set.tiles.name).into())); + self.errors.push(error.within(&set.tiles.name)); } if std::mem::take(&mut set.changed) { self.changed.retain(|data| data.name() != set.data.name()); diff --git a/crates/latkit/src/tiles/vector.rs b/crates/latkit/src/tiles/vector.rs index 22c32a7a..17b3eefb 100644 --- a/crates/latkit/src/tiles/vector.rs +++ b/crates/latkit/src/tiles/vector.rs @@ -135,22 +135,24 @@ impl Fetched { if let Opened::Root(header, pending) = &self.opened { let header = *header; if let Some(landed) = pending.take() { - let opened = landed - .map_err(|error| error.to_string()) - .and_then(|bytes| archive(header, &bytes).ok_or_else(|| corrupt(&self.url))); - self.opened = opened.unwrap_or_else(|reason| Opened::Failed(Error::Io(reason.into()))); + let opened = + landed.and_then(|bytes| archive(header, &bytes).ok_or_else(|| Error::invalid(corrupt(&self.url)))); + self.opened = opened.unwrap_or_else(Opened::Failed); } } let landed: Vec<_> = - self.leaf_fetches.iter().filter_map(|(at, pending)| pending.take().map(|r| (*at, r.ok()))).collect(); + self.leaf_fetches.iter().filter_map(|(at, pending)| pending.take().map(|r| (*at, r))).collect(); for (at, bytes) in landed { self.leaf_fetches.remove(&at); let gzip = matches!(self.opened, Opened::Archive(h, _) if h.gzip_directories); - match bytes.and_then(|b| inflate(&b, gzip)).and_then(|b| Directory::decode(&b)) { - Some(leaf) => { + let leaf = bytes.and_then(|b| { + inflate(&b, gzip).and_then(|b| Directory::decode(&b)).ok_or_else(|| Error::invalid(corrupt(&self.url))) + }); + match leaf { + Ok(leaf) => { self.leaves.insert(at, leaf); } - None => self.tiling.fail(Error::Io("a tile directory could not be read".into())), + Err(error) => self.tiling.fail(error), } } match &self.opened { diff --git a/crates/latkit/src/video/mod.rs b/crates/latkit/src/video/mod.rs index 06966a32..3a17035f 100644 --- a/crates/latkit/src/video/mod.rs +++ b/crates/latkit/src/video/mod.rs @@ -36,7 +36,7 @@ pub enum Pixels { /// [`Image`](crate::Image). Rgba, /// NV12: a luma plane, then a plane of interleaved Cb and Cr at half resolution, BT.709 limited - /// range. The picture's width must be a multiple of 4 and its height a multiple of 2. + /// range, in blocks of 4×2 pixels. Nv12, } @@ -202,18 +202,20 @@ pub struct Video { } impl Shot { - /// A video of this shot's pictures at `fps` frames per second, each frame passed to `sink`. - /// Errors if `fps` is outside 1 to 240, if the shot does not suit the sink's pixel format (see - /// [`Shot::check`]), or if the sink cannot begin. + /// A video of this shot's pictures at `fps` frames per second, each frame passed to `sink`. The + /// size is rounded down to whole blocks of the sink's pixel format: for NV12, a width that is a + /// multiple of 4 and an even height. Errors if `fps` is outside 1 to 240, if the shot does not + /// suit the sink's pixel format (see [`Shot::check`]), or if the sink cannot begin. pub fn video(&self, gpu: &Gpu, fps: f64, mut sink: S) -> Result> { if !(fps.is_finite() && (1.0..=240.0).contains(&fps)) { return Err(Error::invalid("must be 1 to 240 frames a second").at("fps")); } self.check(S::PIXELS)?; + let shot = self.rounded(S::PIXELS); let host = gpu.0.lock().host.clone(); - sink.begin(&Encoding::new(self.size(), fps), host.as_deref())?; + sink.begin(&Encoding::new(shot.size(), fps), host.as_deref())?; Ok(Video { - drawing: Drawing::later(self.clone(), S::PIXELS, false), + drawing: Drawing::later(shot, S::PIXELS, false), sink, fps, index: 0, diff --git a/crates/latkit/src/view/camera.rs b/crates/latkit/src/view/camera.rs index 05fc76d1..e44b4e16 100644 --- a/crates/latkit/src/view/camera.rs +++ b/crates/latkit/src/view/camera.rs @@ -1,8 +1,9 @@ -use std::f64::consts::{PI, TAU}; +use std::f64::consts::TAU; use glam::{DMat3, DMat4, DVec3, DVec4}; use crate::Space; +use crate::geo::mercator::{self, MAX_LATITUDE, latitude}; use crate::gpu::shaders as wgsl; /// Where a view looks. @@ -61,7 +62,6 @@ mod center { /// Vertical field of view in radians, chosen so the camera stands 1.5 view heights from its center. const FOV: f64 = 0.643_501_108_793_284_4; -const MAX_LATITUDE: f64 = 85.051_128_779_806_59; pub(crate) const MAX_PITCH: f64 = 75.0; const MAX_ZOOM: f64 = 24.0; @@ -288,7 +288,7 @@ impl Camera { Camera::Plot { x, y: Some(pad(min[1], max[1], room[1])) } } Space::Earth(_) => { - let (y0, y1) = (mercator_y(min[1]), mercator_y(max[1])); + let (y0, y1) = (mercator::y(min[1]), mercator::y(max[1])); let dx = (max[0] - min[0]).to_radians().max(1e-9); let dy = (y1 - y0).max(1e-9); let px_per_radian = (room[0] / dx).min(room[1] / dy); @@ -348,15 +348,6 @@ fn wrap(lon: f64) -> f64 { (lon + 180.0).rem_euclid(360.0) - 180.0 } -pub(crate) fn mercator_y(latitude: f64) -> f64 { - let lat = latitude.clamp(-MAX_LATITUDE, MAX_LATITUDE).to_radians(); - (PI / 4.0 + lat / 2.0).tan().ln() -} - -pub(crate) fn latitude(mercator_y: f64) -> f64 { - (2.0 * mercator_y.exp().atan() - PI / 2.0).to_degrees() -} - /// A camera's projection of a space into a view of some size, which frames and gestures use to /// convert between positions, world coordinates, and the screen. /// @@ -432,7 +423,7 @@ impl Projected { let world_from_view = view.inverse(); let eye = world_from_view.transform_point3(DVec3::ZERO); let light = world_from_view.transform_vector3(DVec3::new(-0.4, 0.8, 1.0)).normalize_or_zero(); - let mercator = if space_code == wgsl::SPACE_EARTH { mercator_y(center[1]) } else { 0.0 }; + let mercator = if space_code == wgsl::SPACE_EARTH { mercator::y(center[1]) } else { 0.0 }; Projected { clip_from_world, world_from_clip: clip_from_world.inverse(), @@ -497,7 +488,7 @@ impl Projected { if self.space != wgsl::SPACE_EARTH { return DVec3::new(x - self.center[0], y - self.center[1], z); } - let flat = DVec3::new((x - self.center[0]).to_radians(), mercator_y(y) - self.mercator_y, z); + let flat = DVec3::new((x - self.center[0]).to_radians(), mercator::y(y) - self.mercator_y, z); if self.globe <= 0.0 { return flat; } @@ -733,7 +724,6 @@ mod tests { let ground = view.position(view.ground(s).expect("the ground")); assert!((ground[0] - p[0]).abs() < 1e-6 && (ground[1] - p[1]).abs() < 1e-6, "globe {globe}: {ground:?}"); } - assert!((latitude(mercator_y(37.5)) - 37.5).abs() < 1e-9); } #[test] diff --git a/crates/latkit/src/view/ends.rs b/crates/latkit/src/view/ends.rs index ca7dffe9..5b1397dc 100644 --- a/crates/latkit/src/view/ends.rs +++ b/crates/latkit/src/view/ends.rs @@ -199,7 +199,7 @@ impl View { for &i in marks { let shown = &mut self.marks[i]; let rows = shown.live(); - pass.set_pipeline(&inner.pipelines.compute_pipeline(device, Variant::channels(shown.reads()))); + pass.set_pipeline(&inner.pipelines.compute(device, Variant::channels(shown.reads()))); pass.set_bind_group(1, mark, &[offset(i)]); dispatch_blocks(pass, rows, shown.end_block_count); let blocks = shown.unresolved.of(rows); diff --git a/crates/latkit/src/view/fit.rs b/crates/latkit/src/view/fit.rs index 9fb078f7..fb5a8e0d 100644 --- a/crates/latkit/src/view/fit.rs +++ b/crates/latkit/src/view/fit.rs @@ -10,6 +10,7 @@ use super::View; use super::camera::Bounds; use super::event::Fit; use super::marks::Shown; +use crate::derive::Working; use crate::gpu::readback::Readback; use crate::gpu::shaders as wgsl; use crate::scene::mark::MarkKind; @@ -19,8 +20,8 @@ use crate::{Camera, Space}; pub(crate) struct Fitting { /// A fit waiting for its frame. pub(crate) wanted: Option, - /// The extents readback in flight, and the fit it measures for. - reading: Option<(Readback, Option)>, + /// The extents being read back, and the fit they are measured for. + reading: Working, Readback>, /// Whether a plot's traces changed since they were last measured. pub(crate) plot_due: bool, } @@ -28,17 +29,17 @@ pub(crate) struct Fitting { impl Fitting { /// Whether extents are being read back. pub(crate) fn reading(&self) -> bool { - self.reading.is_some() + self.reading.busy() } /// Whether a fit is waiting to be measured; never while extents are being read back. pub(crate) fn due(&self) -> bool { - self.wanted.is_some() && self.reading.is_none() + self.wanted.is_some() && !self.reading.busy() } /// Whether a fit is about to move the camera: one waiting, or its extents being read back. pub(crate) fn framing(&self) -> bool { - self.wanted.is_some() || self.reading.as_ref().is_some_and(|(_, fit)| fit.is_some()) + self.wanted.is_some() || self.reading.context().is_some_and(Option::is_some) } } @@ -68,11 +69,9 @@ impl View { } _ => {} } - let Some((readback, _)) = &self.fitting.reading else { return }; - let Some(bytes) = readback.take() else { return }; - let fit = self.fitting.reading.take().and_then(|(_, fit)| fit); + let Some((_, fit, bytes)) = self.fitting.reading.landed() else { return }; if let Ok(bytes) = bytes { - self.measured(bytemuck::cast_slice(&bytes), fit.as_ref()); + self.measured(&crate::wire::words(&bytes), fit.as_ref()); } self.fitted(); } @@ -87,7 +86,7 @@ impl View { /// What this frame measures, if anything: a fit whose marks are drawn, or a plot's traces. pub(super) fn measuring(&mut self) -> Option { - if self.fitting.reading.is_some() || self.size[0] <= 0.0 { + if self.fitting.reading.busy() || self.size[0] <= 0.0 { return None; } // A plot with traces fits everything to their whole span once their data is available. @@ -143,7 +142,7 @@ impl View { } pub(super) fn measuring_started(&mut self, readback: Readback, fit: Option) { - self.fitting.reading = Some((readback, fit)); + self.fitting.reading.start(0, readback, fit); } /// Frames the extents measured for `fit`, or fits a plot's value range to its traces. @@ -165,8 +164,8 @@ impl View { continue; } for k in 0..3 { - lo[k] = lo[k].min(f64::from(unordered(!e[k]))); - hi[k] = hi[k].max(f64::from(unordered(e[3 + k]))); + lo[k] = lo[k].min(f64::from(crate::order::from_number32(!e[k]))); + hi[k] = hi[k].max(f64::from(crate::order::from_number32(e[3 + k]))); } } self.frame_fit(lo, hi, fit); @@ -278,35 +277,8 @@ impl View { } impl super::marks::Shown { - /// Whether this mark's extents frame a fit: vertices, lines, and areas. + /// Whether this mark's extents frame a fit: whether its kind measures them. pub(crate) fn measured(&self) -> bool { - matches!(self.kind(), MarkKind::Vertices | MarkKind::Lines | MarkKind::Areas) - } -} - -/// The `f32` an order-preserving word encodes; `lib/reduce.wesl`'s `unordered`. -fn unordered(u: u32) -> f32 { - f32::from_bits(if u & 0x8000_0000 != 0 { u & 0x7fff_ffff } else { !u }) -} - -#[cfg(test)] -mod tests { - use super::unordered; - - /// `lib/reduce.wesl`'s `ordered`, the inverse of `unordered`. - fn ordered(x: f32) -> u32 { - let b = x.to_bits(); - if b & 0x8000_0000 != 0 { !b } else { b | 0x8000_0000 } - } - - #[test] - fn ordered_words_sort_like_their_values() { - let values = [-1e9_f32, -2.5, -0.0, 0.0, 1e-9, 3.0, 1e9]; - for pair in values.windows(2) { - assert!(ordered(pair[0]) <= ordered(pair[1]), "{pair:?}"); - } - for v in values { - assert_eq!(unordered(ordered(v)), v); - } + self.kind().bounds().is_some() } } diff --git a/crates/latkit/src/view/focus.rs b/crates/latkit/src/view/focus.rs index fd615a33..b6ac6a7a 100644 --- a/crates/latkit/src/view/focus.rs +++ b/crates/latkit/src/view/focus.rs @@ -10,7 +10,7 @@ use rustc_hash::FxHashMap; use super::View; use super::event::{Event, Hit}; -use super::sync::{Source, table_of}; +use super::sync::Source; use super::words::Kept; use crate::data::{Field, PAGE_ROWS, Reference}; use crate::expr::compile::Deps; @@ -190,8 +190,10 @@ impl View { /// row's instances draw nothing; none for faces. fn pieces(&self, i: usize, lit: &[u32]) -> u32 { let shown = &self.marks[i]; - let Some(compiled) = &shown.compiled else { return 0 }; - match compiled.drawing.primitive { + if shown.compiled.is_none() { + return 0; + } + match shown.kind().primitive() { Primitive::Points => 1, Primitive::Strokes if !shown.listed() => shown.segments_per_row().max(1), Primitive::Strokes if shown.path() == wgsl::PATH_ENDS => (self.longest(i, lit) + 1).max(wgsl::MAX_PIECES), @@ -207,11 +209,8 @@ impl View { let shown = &self.marks[i]; let Some(compiled) = &shown.compiled else { return 0 }; let Some(column) = compiled.program.columns.get(compiled.drawing.geometry as usize) else { return 0 }; - let style = shown.mark.style(); - let Some(data) = self.snapshot(&style.data) else { return 0 }; - let Some(table) = data.table(&style.table) else { return 0 }; - let source = Source { data, table, derived: self.derived.beside(&style.data, &style.table), paged: Vec::new() }; - let Some((_, table)) = table_of(column, &source, &self.snapshots) else { return 0 }; + let Some(source) = Source::of(shown.mark.style(), &self.snapshots, &self.derived) else { return 0 }; + let Some((_, table)) = source.table_of(column, &self.snapshots) else { return 0 }; let Some(Field::Values(values, _)) = table.field_named(&column.name) else { return 0 }; let pages = values.pages(); lit.iter() diff --git a/crates/latkit/src/view/frame.rs b/crates/latkit/src/view/frame.rs index 8c8cb601..b9915932 100644 --- a/crates/latkit/src/view/frame.rs +++ b/crates/latkit/src/view/frame.rs @@ -1,8 +1,9 @@ //! One frame, in order: tick the clock, settle readbacks, advance the camera, derive the tables //! marks read beside their data, lay out drawings, sync columns, stream uploads, give marks word -//! pages for their rows, freeze transitions, pack the program buffer, write focus words and +//! pages for their rows, start transitions, pack the program buffer, write focus words and //! uniforms, then record the compute, label, render, probe, and query passes into the caller's -//! encoder. +//! encoder. No pass is recorded before the program buffer is packed, since packing may move a +//! mark's words with copies recorded in the encoder. //! //! The frame submits nothing itself except setup that must run before its recorded passes: //! clearing newly handed-out pages, and copying buffers that grew. @@ -12,7 +13,7 @@ use web_time::Instant; use super::drawing::Drawn; use super::marks::Holding; use super::resources::{MARK_STRIDE, Resources}; -use super::sync::{Inputs, Rebuilt, Rows, Source, Window, table_of}; +use super::sync::{Inputs, Rebuilt, Rows, Source, Window}; use super::transition::{bound, holds_alike, in_place, moved}; use super::words::Kept; use super::{Camera, Event, Frame, View, timer, uniforms}; @@ -156,6 +157,7 @@ impl View { for shown in &mut self.marks { shown.before = shown.held(); shown.words.settle(); + shown.remap = wgsl::NONE; shown.carry = false; } self.events.extend(inner.atlas.errors.drain(..).map(Event::Error)); @@ -215,7 +217,7 @@ impl View { let fresh = self.fit_words(inner); self.fit_easing(inner); inner.arena.clear(device, &gpu.0.queue); - self.transitions(encoder, inner); + self.transitions(inner); self.attach_ends(); let domains = self.place_domains(); let resources = self.resources.as_mut().expect("created"); @@ -232,32 +234,16 @@ impl View { fn fit_words(&mut self, inner: &mut Inner) -> bool { let gpu = self.gpu.clone(); let device = (&gpu.0.device, &gpu.0.queue); - let mut normals = vec![false; self.marks.len()]; - for shown in self.marks.iter().filter(|m| m.kind() == MarkKind::Faces) { - let Some(compiled) = &shown.compiled else { continue }; - for end in &compiled.program.ends { - if let Some(m) = super::graph::end_mark(&self.marks, &shown.mark.style().data, &end.table) { - normals[m] = true; - } - } - } + let summing = super::graph::summing(&self.marks); let (mut fresh, mut short, mut changed) = (false, false, false); - for (i, shown) in self.marks.iter_mut().enumerate() { + for (shown, summing) in self.marks.iter_mut().zip(summing) { let rows = shown.page_rows(); - let labeled = shown.compiled.as_ref().is_some_and(|c| c.drawing.label.is_some()); - let fits = [ - (Kept::State, shown.stride() + shown.extras()), - (Kept::Focus, 1), - (Kept::Candidates, if labeled { wgsl::CANDIDATE_WORDS } else { 0 }), - (Kept::Normals, if normals[i] { wgsl::NORMAL_WORDS } else { 0 }), - ]; - for (kept, width) in fits { - let had = shown.words.pages(kept).blocks.len(); - let Some(new) = shown.words.fit(kept, &rows, width, device, &mut inner.arena) else { + for (kept, width) in shown.kept_widths(summing) { + let Some(fitted) = shown.words.fit(kept, &rows, width, device, &mut inner.arena) else { short = true; continue; }; - if new.is_empty() && had == shown.words.pages(kept).blocks.len() { + if !fitted.changed { continue; } shown.packed = false; @@ -265,8 +251,8 @@ impl View { fresh |= kept == Kept::Focus; // Rows in new state pages evaluate every channel, writing every word first. if kept == Kept::State { - shown.slots_bound(&new); - shown.fresh.extend(&new); + shown.slots_bound(&fitted.new); + shown.fresh.extend(&fitted.new); } } } @@ -540,10 +526,8 @@ impl View { for (i, shown) in self.marks.iter_mut().enumerate() { let Some(compiled) = &shown.compiled else { continue }; let style = shown.mark.style(); - let Some(data) = self.snapshots.iter().find(|d| *d.name() == style.data) else { continue }; - let Some(table) = data.table(&style.table) else { continue }; - let derived = self.derived.beside(&style.data, &style.table); - let source = Source { data, table, derived, paged: self.derived.paged(&style.data, &style.table) }; + let Some(mut source) = Source::of(style, &self.snapshots, &self.derived) else { continue }; + source.paged = self.derived.paged(&style.data, &style.table); let drawing = self.drawings.get(&(style.data.clone(), style.table.clone())).and_then(Drawn::latest); let rows = drawing.map_or(Rows::Own, |drawing| Rows::Held(drawing)); let window = Window { steps: compiled.drawing.flags & (wgsl::STEP_BEFORE | wgsl::STEP_AFTER), ..window }; @@ -618,30 +602,19 @@ impl View { Some(drawing) => drawing.live(), None => self.snapshot(&key.0).and_then(|d| d.table(&key.1)).map_or(0, |t| t.len() as u32), }; - // Size new pages as `page_rows` will: a whole page per slot of a paged table, else the - // table's own rows, a page at a time. - let held = live + shown.exits; + // New pages are sized as they will be once the rows arrive. let mut rows = shown.page_rows(); let had = rows.len(); - for p in had as u32..held.div_ceil(wgsl::PAGE_ROWS) { - let start = p * wgsl::PAGE_ROWS; - let full = drawing.is_some() && start < live; - rows.push(if full { wgsl::PAGE_ROWS } else { held.min(start + wgsl::PAGE_ROWS) - start }); - } + rows.extend(super::marks::page_rows(live, live + shown.exits, drawing.is_some()).into_iter().skip(had)); if rows.len() == had { return true; } - let labeled = shown.compiled.as_ref().is_some_and(|c| c.drawing.label.is_some()); - let fits = [ - (Kept::State, shown.stride() + shown.extras()), - (Kept::Focus, 1), - (Kept::Candidates, if labeled { wgsl::CANDIDATE_WORDS } else { 0 }), - ]; + let widths = shown.kept_widths(super::graph::summing(&self.marks)[i]); let gpu = self.gpu.clone(); let shown = &mut self.marks[i]; - fits.into_iter().all(|(kept, width)| { + widths.into_iter().all(|(kept, width)| { let fitted = shown.words.fit(kept, &rows, width, (&gpu.0.device, &gpu.0.queue), &mut inner.arena); - shown.packed &= fitted.as_ref().is_some_and(Vec::is_empty); + shown.packed &= fitted.as_ref().is_some_and(|f| !f.changed); fitted.is_some() }) } @@ -718,12 +691,13 @@ impl View { shown.packed = false; if let Some(published) = &published { shown.columns_changed(published.data, published.blocks.as_deref()); - let source = Source { data, table, derived: self.derived.beside(&key.0, &key.1), paged: Vec::new() }; + let source = Source::of(shown.mark.style(), &self.snapshots, &self.derived); for text in &program.texts { let column = &program.columns[text.column as usize]; - let code = table_of(column, &source, &self.snapshots).map_or(wgsl::NONE, |(d, t)| { - inner.dictionaries.field(d, &t.name, &column.name).code_of(&text.text) - }); + let code = + source.as_ref().and_then(|s| s.table_of(column, &self.snapshots)).map_or(wgsl::NONE, |(d, t)| { + inner.dictionaries.field(d, &t.name, &column.name).code_of(&text.text) + }); shown.constants[text.constant as usize] = code; } } diff --git a/crates/latkit/src/view/graph.rs b/crates/latkit/src/view/graph.rs index 986767b8..29c2015d 100644 --- a/crates/latkit/src/view/graph.rs +++ b/crates/latkit/src/view/graph.rs @@ -114,6 +114,21 @@ impl View { } } +/// For each of `marks`, whether faces sum their normals at its rows: whether it is the vertex mark +/// some face mark's corners attach to. +pub(super) fn summing(marks: &[Shown]) -> Vec { + let mut summing = vec![false; marks.len()]; + for shown in marks.iter().filter(|m| m.kind() == MarkKind::Faces) { + let Some(compiled) = &shown.compiled else { continue }; + for end in &compiled.program.ends { + if let Some(m) = end_mark(marks, &shown.mark.style().data, &end.table) { + summing[m] = true; + } + } + } + summing +} + /// The vertex mark drawing `table` that ends attach to: the unnamed one, else the first. pub(super) fn end_mark(marks: &[Shown], data: &Name, table: &Name) -> Option { let drawing = |i: &usize| { diff --git a/crates/latkit/src/view/labels.rs b/crates/latkit/src/view/labels.rs index 8a79b9fb..d974a6ad 100644 --- a/crates/latkit/src/view/labels.rs +++ b/crates/latkit/src/view/labels.rs @@ -21,16 +21,19 @@ use web_time::Instant; use super::View; use super::passes::{dispatch, offset}; -use super::sync::{Source, table_of}; +use super::sync::Source; +use super::variants::{CELLS, MarkVariants, PLACING, SELECT}; use super::words::Kept; use crate::Space; use crate::derive::{Derived, stamp_of}; use crate::gpu::Inner; use crate::gpu::atlas::{self, Glyphs}; +use crate::gpu::dictionary::Dictionary; use crate::gpu::pipelines::Variant; use crate::gpu::readback::Readback; use crate::gpu::shaders::{self as wgsl, Shader}; use crate::host::TextRun; +use crate::scene::mark::Labeling; /// Rounds of claim and decide each placement runs. const ROUNDS: u32 = 6; @@ -107,33 +110,30 @@ impl Labels { } impl View { - /// The text run for code `code` of mark `i`'s label text, at the base size regardless of the - /// size it shows at, as a diagram's sheet measures it. - fn run_of(&self, i: usize, code: u32, inner: &mut Inner) -> Option { + /// Mark `i`'s labeling and the dictionary of its label text's codes, wherever the text is: in + /// the mark's table, a joined one, or a table the view derives beside it. `None` while the + /// mark has no label, or the table holding its text is missing. + fn label_texts<'g>(&self, i: usize, inner: &'g mut Inner) -> Option<(&Labeling, &'g mut Dictionary)> { let shown = &self.marks[i]; let compiled = shown.compiled.as_ref()?; let labeling = compiled.drawing.label.as_ref()?; let column = &compiled.program.columns[labeling.column as usize]; - let style = shown.mark.style(); - let data = self.snapshot(&style.data)?; - let source = Source { data, table: data.table(&style.table)?, derived: [None; 3], paged: Vec::new() }; - let (d, t) = table_of(column, &source, &self.snapshots)?; - let text = inner.dictionaries.field(d, &t.name, &column.name).text(code)?.clone(); + let source = Source::of(shown.mark.style(), &self.snapshots, &self.derived)?; + let (data, table) = source.table_of(column, &self.snapshots)?; + Some((labeling, inner.dictionaries.field(data, &table.name, &column.name))) + } + + /// The text run for code `code` of mark `i`'s label text, at the base size regardless of the + /// size it shows at, as a diagram's sheet measures it. + fn run_of(&self, i: usize, code: u32, inner: &mut Inner) -> Option { + let (labeling, dictionary) = self.label_texts(i, inner)?; + let text = dictionary.text(code)?.clone(); Some(atlas::run(text, labeling.font.clone(), labeling.size, labeling.max_width)) } /// Codes mark `i`'s label text has so far. fn codes_of(&self, i: usize, inner: &mut Inner) -> u32 { - let shown = &self.marks[i]; - let Some(compiled) = &shown.compiled else { return 0 }; - let Some(labeling) = &compiled.drawing.label else { return 0 }; - let column = &compiled.program.columns[labeling.column as usize]; - let style = shown.mark.style(); - let Some(data) = self.snapshot(&style.data) else { return 0 }; - let Some(table) = data.table(&style.table) else { return 0 }; - let source = Source { data, table, derived: [None; 3], paged: Vec::new() }; - let Some((d, t)) = table_of(column, &source, &self.snapshots) else { return 0 }; - inner.dictionaries.field(d, &t.name, &column.name).len() as u32 + self.label_texts(i, inner).map_or(0, |(_, dictionary)| dictionary.len() as u32) } /// Sets the texts the last placement requested, within the frame's budget. The next placement @@ -141,7 +141,7 @@ impl View { pub(super) fn settle_labels(&mut self, inner: &mut Inner) { let Some((_, bytes)) = self.labels.placed.landed() else { return }; let asked = std::mem::take(&mut self.labels.asked); - let words: &[u32] = bytemuck::cast_slice(&bytes); + let words = crate::wire::words(&bytes); let started = Instant::now(); let (mut set, mut left) = (0, false); 'marks: for (i, slots) in asked { @@ -287,21 +287,10 @@ impl View { /// The run table words for mark `i`'s `codes`: where the atlas holds each text already set, and /// the measured size of the rest. fn run_words(&self, i: usize, codes: Range, inner: &mut Inner) -> Vec { - let shown = &self.marks[i]; let written = self.labels.written.get(&i); - let texts = 'texts: { - let Some(compiled) = &shown.compiled else { break 'texts None }; - let Some(labeling) = &compiled.drawing.label else { break 'texts None }; - let column = &compiled.program.columns[labeling.column as usize]; - let style = shown.mark.style(); - let Some(data) = self.snapshot(&style.data) else { break 'texts None }; - let Some(table) = data.table(&style.table) else { break 'texts None }; - let source = Source { data, table, derived: [None; 3], paged: Vec::new() }; - let Some((d, t)) = table_of(column, &source, &self.snapshots) else { break 'texts None }; - let dictionary = inner.dictionaries.field(d, &t.name, &column.name); - Some(codes.clone().map(|code| dictionary.text(code).cloned()).collect::>()) - .map(|texts| (texts, labeling)) - }; + let texts = self.label_texts(i, inner).map(|(labeling, dictionary)| { + (codes.clone().map(|code| dictionary.text(code).cloned()).collect::>(), labeling) + }); let host = inner.host.clone(); let mut words = Vec::with_capacity(2 * codes.len()); for (k, code) in codes.enumerate() { @@ -414,39 +403,33 @@ impl View { timestamp_writes: None, }); pass.set_bind_group(0, &groups.view, &[]); - let marks: Vec<(usize, u32, u32)> = - labeled.iter().map(|&(i, _)| (i, self.marks[i].rows, self.marks[i].path())).collect(); + let marks: Vec<(usize, u32, MarkVariants<'_>)> = + labeled.iter().map(|&(i, _)| (i, self.marks[i].rows, MarkVariants::shown(&self.marks[i]))).collect(); // Measure candidates at their anchors, with a pipeline for each mark's primitive; // placement reads the result. if due { pass.set_bind_group(2, &measuring, &[]); - for &(i, rows, path) in &marks { - let primitive = self.marks[i].primitive(); - let variant = Variant::compute(Shader::Candidates, "candidates").path(path).primitive(primitive); - pass.set_pipeline(&inner.pipelines.compute_pipeline(device, variant)); + for &(i, rows, variants) in &marks { + pass.set_pipeline(&inner.pipelines.compute(device, variants.candidates())); pass.set_bind_group(1, &groups.mark, &[offset(i)]); dispatch(&mut pass, rows); } } pass.set_bind_group(2, &group, &[]); // A diagram's labels claim no cells. - let cleared = (!diagram).then(|| { - ["clear_cells", "reset_cells"].map(|entry| inner.pipelines.compute(device, Shader::Labels, entry)) - }); + let cleared = (!diagram).then(|| CELLS.map(|variant| inner.pipelines.compute(device, variant))); let capped: Vec<(usize, u32)> = labeled.iter().filter(|(_, s)| s.select != wgsl::NONE).map(|&(i, _)| (i, self.marks[i].rows)).collect(); - let selecting = (!capped.is_empty() && !diagram).then(|| { - ["select_start", "select_count", "select_find", "select_cut"] - .map(|entry| inner.pipelines.compute(device, Shader::Labels, entry)) - }); + let selecting = (!capped.is_empty() && !diagram) + .then(|| SELECT.map(|variant| inner.pipelines.compute(device, variant))); let mut each = |pass: &mut wgpu::ComputePass<'_>, entry: &'static str| { - for &(i, rows, path) in &marks { - let variant = Variant::compute(Shader::Labels, entry).path(path); - pass.set_pipeline(&inner.pipelines.compute_pipeline(device, variant)); + for &(i, rows, variants) in &marks { + pass.set_pipeline(&inner.pipelines.compute(device, variants.placing(entry))); pass.set_bind_group(1, &groups.mark, &[offset(i)]); dispatch(pass, rows); } }; + let [claim, decide, occupy, ask, fade] = PLACING; let cells = |pass: &mut wgpu::ComputePass<'_>, pipeline: &wgpu::ComputePipeline| { pass.set_bind_group(1, &groups.mark, &[offset(labeled[0].0)]); pass.set_pipeline(pipeline); @@ -459,15 +442,15 @@ impl View { } cells(&mut pass, clear); for _ in 0..ROUNDS { - each(&mut pass, "claim"); - each(&mut pass, "decide"); - each(&mut pass, "occupy"); + each(&mut pass, claim); + each(&mut pass, decide); + each(&mut pass, occupy); cells(&mut pass, reset); } - each(&mut pass, "ask"); + each(&mut pass, ask); } if !diagram { - each(&mut pass, "fade"); + each(&mut pass, fade); } } if due { @@ -531,7 +514,7 @@ fn select( impl View { /// Draws every labeled mark's labels, then every vertex mark's port labels. `glyphs` holds the - /// shader and pass bind group for labels, then for port labels. + /// pass bind groups of their glyph shaders. #[allow(clippy::too_many_arguments, reason = "the main pass's draw state")] pub(super) fn draw_labels( &self, @@ -541,17 +524,15 @@ impl View { format: wgpu::TextureFormat, samples: u32, groups: &super::resources::Groups, - glyphs: [(Shader, &wgpu::BindGroup); 2], + [labels_group, ports_group]: [&wgpu::BindGroup; 2], ) { let device = &self.gpu.0.device; - let [(labels, labels_group), (ports, ports_group)] = glyphs; for (i, slots) in self.labels.slots.iter().enumerate() { let shown = &self.marks[i]; if slots.is_none() || shown.rows == 0 { continue; } - let variant = Variant::draw(labels, format, samples).path(shown.path()).primitive(shown.primitive()); - pass.set_pipeline(&inner.pipelines.render_pipeline(device, variant)); + pass.set_pipeline(&inner.pipelines.render(device, MarkVariants::shown(shown).glyphs(format, samples))); pass.set_bind_group(2, labels_group, &[]); pass.set_bind_group(1, &groups.mark, &[offset(i)]); drawn.draw(pass, 4, shown.rows); @@ -562,7 +543,7 @@ impl View { if !labeled || !shown.drawable() || shown.rows == 0 { continue; } - pass.set_pipeline(&inner.pipelines.render(device, ports, format, samples)); + pass.set_pipeline(&inner.pipelines.render(device, Variant::draw(Shader::GlyphsPorts, format, samples))); pass.set_bind_group(2, ports_group, &[]); pass.set_bind_group(1, &groups.mark, &[offset(i)]); drawn.draw(pass, 4, shown.rows * shown.port_count()); diff --git a/crates/latkit/src/view/lists.rs b/crates/latkit/src/view/lists.rs index c20ac575..0071bb1a 100644 --- a/crates/latkit/src/view/lists.rs +++ b/crates/latkit/src/view/lists.rs @@ -12,7 +12,7 @@ use super::passes::dispatch; use crate::derive::{Derived, stamp_of}; use crate::gpu::Inner; use crate::gpu::readback::Readback; -use crate::gpu::shaders::{self as wgsl, Shader}; +use crate::gpu::shaders as wgsl; use crate::scene::mark::MarkKind; /// A listed mark's segment list (see `Shown::listed`), used when its rows have different numbers @@ -97,7 +97,7 @@ impl View { let mut at = 0; for shown in &mut self.marks { if let Some((stamp, bytes)) = shown.segments.found.landed() { - let found = bytes.get(..4).map_or(0, |w| u32::from_le_bytes([w[0], w[1], w[2], w[3]])); + let found = crate::wire::In::new(&bytes).u32().unwrap_or(0); let kept = shown.segments.list.map_or(0, |l| l.kept); let room = if found > kept { found + found / 4 } else { shown.segments.found.value }; shown.segments.found.make(stamp, room); @@ -215,10 +215,11 @@ pub(super) fn scan( device: &wgpu::Device, items: u32, ) { - pass.set_pipeline(&pipelines.compute(device, Shader::Lists, "scan_blocks")); + let [blocks, sums, add] = super::variants::SCAN; + pass.set_pipeline(&pipelines.compute(device, blocks)); dispatch(pass, items.max(1)); - pass.set_pipeline(&pipelines.compute(device, Shader::Lists, "scan_sums")); + pass.set_pipeline(&pipelines.compute(device, sums)); pass.dispatch_workgroups(1, 1, 1); - pass.set_pipeline(&pipelines.compute(device, Shader::Lists, "add_sums")); + pass.set_pipeline(&pipelines.compute(device, add)); dispatch(pass, items); } diff --git a/crates/latkit/src/view/marks.rs b/crates/latkit/src/view/marks.rs index 928a393a..bb329b2c 100644 --- a/crates/latkit/src/view/marks.rs +++ b/crates/latkit/src/view/marks.rs @@ -67,6 +67,16 @@ pub(crate) struct Holding { pub(crate) drawing: Option>, } +impl Holding { + /// The table row at held row `h`, if it holds one there. + pub(crate) fn source_row(&self, h: u32) -> Option { + match &self.drawing { + Some(drawing) => drawing.source(h), + None => (h < self.table.len() as u32).then_some(h), + } + } +} + pub(crate) struct Shown { pub(crate) mark: Mark, pub(crate) key: Key, @@ -126,9 +136,10 @@ pub(crate) struct Shown { pub(crate) offsets: Offsets, pub(crate) packed: bool, pub(crate) written: Vec, - /// A transition waiting to freeze. `transition` is the running one, `remap` where its remap - /// words start, and `carry` whether this frame froze rows at new places, so their comets' - /// travel follows them. + /// A transition waiting to start. `transition` is the running one, and `remap` where its remap + /// words start in the frame it starts, which the compute pass freezes it from (`NONE` in other + /// frames). `carry` is whether this frame froze rows at new places, so their comets' travel + /// follows them. pub(crate) begin: Option, pub(crate) transition: Option, pub(crate) remap: u32, @@ -190,7 +201,7 @@ impl Shown { written: Vec::new(), begin: None, transition: None, - remap: 0, + remap: wgsl::NONE, carry: false, query: wgsl::NONE, segments: super::lists::Segments::default(), @@ -220,10 +231,7 @@ impl Shown { /// The table row at held row `h`. pub(crate) fn source_row(&self, h: u32) -> Option { - match self.drawing() { - Some(drawing) => drawing.source(h), - None => (h < self.rows).then_some(h), - } + self.holding.as_ref()?.source_row(h) } /// The held row of table row `r`, if the mark holds it. @@ -283,21 +291,12 @@ impl Shown { /// Whether its state pages have the current row width and cover every held row. pub(crate) fn pages_held(&self) -> bool { let state = self.words.pages(Kept::State); - state.width == self.stride() + self.extras() - && state.blocks.len() as u32 >= self.held().div_ceil(wgsl::PAGE_ROWS) + state.width == self.width() && state.blocks.len() as u32 >= self.held().div_ceil(wgsl::PAGE_ROWS) } - /// The row count of each page of held rows: a full page per slot of a paged table, since slots - /// evaluate whole, or an unpaged table's rows split into pages. Exits follow in either case. + /// The row count of each page of its held rows, as [`page_rows`] lays them out. pub(crate) fn page_rows(&self) -> Vec { - let (live, held) = (self.live(), self.held()); - let full = |start: u32| self.drawing().is_some() && start < live; - (0..held.div_ceil(wgsl::PAGE_ROWS)) - .map(|p| { - let start = p * wgsl::PAGE_ROWS; - if full(start) { wgsl::PAGE_ROWS } else { held.min(start + wgsl::PAGE_ROWS) - start } - }) - .collect() + page_rows(self.live(), self.held(), self.drawing().is_some()) } /// Releases its tables and pages on the GPU, as when the mark is removed. @@ -308,7 +307,7 @@ impl Shown { /// Kind words per row. pub(crate) fn stride(&self) -> u32 { - self.compiled.as_ref().map_or_else(|| kind_stride(self.kind()), |c| c.drawing.stride) + self.kind().stride() } /// Extra words per row. @@ -316,11 +315,29 @@ impl Shown { self.compiled.as_ref().map_or(0, |c| c.drawing.extras) } + /// Words per row: the kind's, then the extras. + pub(crate) fn width(&self) -> u32 { + self.stride() + self.extras() + } + + /// Words per row in each kind of word page its held rows take, 0 for a kind it keeps none of: + /// its state, its focus word, its label candidates when labeled, and when faces are `summing` + /// their normals at its rows, those. + pub(crate) fn kept_widths(&self, summing: bool) -> [(Kept, u32); 4] { + let labeled = self.compiled.as_ref().is_some_and(|c| c.drawing.label.is_some()); + [ + (Kept::State, self.width()), + (Kept::Focus, 1), + (Kept::Candidates, if labeled { wgsl::CANDIDATE_WORDS } else { 0 }), + (Kept::Normals, if summing { wgsl::NORMAL_WORDS } else { 0 }), + ] + } + /// What its channels and fitted domains read besides its own table's static fields, which picks /// their pipeline variant. Before it compiles, neither joins, samples, nor the view. pub(crate) fn reads(&self) -> crate::gpu::pipelines::Reads { let Some(program) = self.compiled.as_ref().map(|c| &c.program) else { - return crate::gpu::pipelines::Reads { joined: false, timed: false, viewed: false }; + return crate::gpu::pipelines::Reads::NONE; }; let viewing = [wgsl::OP_SELECTED, wgsl::OP_HOVERED, wgsl::OP_POINTER, wgsl::OP_ZOOM]; crate::gpu::pipelines::Reads { @@ -330,13 +347,9 @@ impl Shown { } } - /// The `PATH_` code its pipelines are built for: the path its strokes follow, or where its - /// triangles' corners come from. + /// The `PATH_` code its pipelines are built for, as [`Mark::path`](crate::Mark) decides it. pub(crate) fn path(&self) -> u32 { - match &self.compiled { - Some(c) if c.drawing.primitive != Primitive::Points => c.drawing.path, - _ => wgsl::PATH_ENDS, - } + self.compiled.as_ref().map_or(wgsl::PATH_ENDS, |c| c.drawing.path) } /// Whether a list numbers its instances: strokes whose rows have different segment counts @@ -344,7 +357,7 @@ impl Shown { /// have different triangle counts. pub(crate) fn listed(&self) -> bool { let Some(compiled) = &self.compiled else { return false }; - match compiled.drawing.primitive { + match self.kind().primitive() { Primitive::Strokes => compiled.drawing.geometry != wgsl::NONE || self.curved(), Primitive::Triangles => self.path() == wgsl::PATH_GEOMETRY, Primitive::Points => false, @@ -370,7 +383,7 @@ impl Shown { let rows = (self.rows + self.exits) as usize; let Some(compiled) = &self.compiled else { return 1 }; let points = self.tables.points(compiled.drawing.geometry) as usize; - if compiled.drawing.primitive == Primitive::Triangles { + if self.kind().primitive() == Primitive::Triangles { return (points / 3 + rows).max(1) as u32; } let pieces = if !self.curved() { @@ -383,14 +396,6 @@ impl Shown { (points + rows + pieces).max(1) as u32 } - /// Vertices per instance: 4 for the quad of a point or segment, 3 for a triangle. - pub(crate) fn corners(&self) -> u32 { - match self.compiled.as_ref().map(|c| c.drawing.primitive) { - Some(Primitive::Triangles) => 3, - _ => 4, - } - } - /// A bit mask of the channels label placement reads: row position, size, and visibility, and /// label text and priority. pub(crate) fn placing_channels(&self) -> u32 { @@ -581,7 +586,7 @@ impl Shown { /// numbers and colors interpolate, a position's high words move by their offset, and other /// stored words snap to the new value. A word no channel stores keeps its value. fn blends(&self) -> Vec { - let mut blends = vec![wgsl::BLEND_KEEP; (self.stride() + self.extras()) as usize]; + let mut blends = vec![wgsl::BLEND_KEEP; self.width() as usize]; let mut set = |word: u32, blend: u32| { if let Some(b) = blends.get_mut(word as usize) { *b = blend; @@ -682,15 +687,16 @@ impl Blocks { } } -/// Kind words per row of a mark of `kind`. -pub(crate) fn kind_stride(kind: MarkKind) -> u32 { - match kind { - MarkKind::Vertices => wgsl::VERTEX_WORDS, - MarkKind::Edges => wgsl::EDGE_WORDS, - MarkKind::Faces => wgsl::FACE_WORDS, - MarkKind::Lines => wgsl::LINE_WORDS, - MarkKind::Areas => wgsl::AREA_WORDS, - } +/// The row count of each page of `held` rows, the first `live` of them not exits: a full page per +/// slot of a `paged` table, since slots evaluate whole, or an unpaged table's rows split into +/// pages. Exits follow in either case. +pub(crate) fn page_rows(live: u32, held: u32, paged: bool) -> Vec { + (0..held.div_ceil(wgsl::PAGE_ROWS)) + .map(|p| { + let start = p * wgsl::PAGE_ROWS; + if paged && start < live { wgsl::PAGE_ROWS } else { held.min(start + wgsl::PAGE_ROWS) - start } + }) + .collect() } fn shape_code(shape: Shape) -> u32 { diff --git a/crates/latkit/src/view/mod.rs b/crates/latkit/src/view/mod.rs index 9e62d70c..31d788bb 100644 --- a/crates/latkit/src/view/mod.rs +++ b/crates/latkit/src/view/mod.rs @@ -319,7 +319,7 @@ impl View { if let Some(mut before) = before.and_then(Option::take) { before.tables.release(&mut inner); shown.inherit(&mut before); - if easing && shown.compiled.as_ref().map(|c| c.drawing.stride) == Some(before.stride()) { + if easing && shown.compiled.is_some() { shown.begin(None); } } @@ -394,13 +394,13 @@ impl View { } for old in &self.sources { if let Source::Data(data) = old { - data.unwatch(self.id); + data.watchers().unwatch(self.id); } } for source in &sources { match source { Source::Data(data) => { - data.watch(self.id, self.waker.clone()); + data.watchers().watch(self.id, &self.waker); self.take_newer(data.snapshot(), &mut inner); } Source::Snapshot(snapshot) => self.take_newer(snapshot.clone(), &mut inner), @@ -987,7 +987,7 @@ impl Drop for View { drop(inner); for source in &self.sources { if let Source::Data(data) = source { - data.unwatch(self.id); + data.watchers().unwatch(self.id); } } } diff --git a/crates/latkit/src/view/passes.rs b/crates/latkit/src/view/passes.rs index 185b6b1c..456474a7 100644 --- a/crates/latkit/src/view/passes.rs +++ b/crates/latkit/src/view/passes.rs @@ -1,14 +1,15 @@ //! A frame's GPU passes. //! -//! The compute pass expands paged marks' drawn rows from their runs, evaluates dirty channels over -//! the changed row blocks (reducing fitted domains first), resolves endpoints, eases marks in -//! transition, moves comets, numbers segments, sums face normals when their corners moved, and -//! measures extents when asked. The render pass draws the backdrop, then every mark's primitive and +//! The compute pass freezes what rows showed for transitions starting this frame, expands paged +//! marks' drawn rows from their runs, evaluates dirty channels over the changed row blocks +//! (reducing fitted domains first), resolves endpoints, eases marks in transition, moves comets, +//! numbers segments, sums face normals when their corners moved, and measures extents when asked. The render pass draws the backdrop, then every mark's primitive and //! ports in scene order with depth, then the labels and the overlay. A pending probe then reads //! what the frame drew, through the same geometry. use super::resources::MARK_STRIDE; -use super::sync::{Source, table_of}; +use super::sync::Source; +use super::variants::{CARRY, EASE, EXPAND_RUNS, GATHER, MarkVariants, NORMALS, TRAVEL}; use super::words::Kept; use super::{Frame, View}; use crate::Space; @@ -17,7 +18,7 @@ use crate::derive::stamp_of; use crate::expr::compile::{Deps, Reads}; use crate::expr::cpu::Rows; use crate::gpu::Inner; -use crate::gpu::pipelines::Variant; +use crate::gpu::pipelines::{Asked, Variant}; use crate::gpu::readback::Readback; use crate::gpu::shaders::{self as wgsl, Shader}; use crate::gpu::work::Count; @@ -120,29 +121,19 @@ impl Marks<'_> { shown: &super::marks::Shown, lit: bool, ) { - let Some(compiled) = &shown.compiled else { return }; - let custom = compiled.drawing.markers.custom.is_some(); - let areas = shown.path() == wgsl::PATH_GEOMETRY; - let (shader, corners) = match (compiled.drawing.primitive, lit) { - (Primitive::Points, false) if custom => (Shader::PointsCustom, 4), - (Primitive::Points, false) => (Shader::Points, 4), - (Primitive::Points, true) if custom => (Shader::PointsCustomFocus, 4), - (Primitive::Points, true) => (Shader::PointsFocus, 4), - (Primitive::Strokes, false) => (Shader::Strokes, 4), - (Primitive::Strokes, true) => (Shader::StrokesFocus, 4), - (Primitive::Triangles, false) if areas => (Shader::Areas, 3), - (Primitive::Triangles, false) => (Shader::Triangles, 3), - (Primitive::Triangles, true) if areas => (Shader::AreasFocus, 3), - // A face shows focus by its color alone, so it has no lit draw. - (Primitive::Triangles, true) => return, + if shown.compiled.is_none() { + return; + } + let variants = MarkVariants::shown(shown); + let (Some(shader), Some(asked)) = (variants.shader(lit), variants.draw(lit, self.format, self.samples)) else { + return; }; - let pipeline = if let Some(source) = &compiled.drawing.markers.custom { - inner.pipelines.custom(self.device, source, shader, self.format, self.samples) - } else { - let variant = Variant::draw(shader, self.format, self.samples).path(shown.path()); - Some(inner.pipelines.render_pipeline(self.device, variant)) + let pipeline = match asked { + Asked::Shader(variant) => Some(inner.pipelines.render(self.device, variant)), + Asked::Custom(source, variant) => inner.pipelines.custom(self.device, &source, variant), }; let Some(pipeline) = pipeline else { return }; + let corners = shown.kind().primitive().corners(); let group = &self.passes.iter().find(|(s, _)| *s == shader).expect("listed").1; pass.set_bind_group(1, &self.groups.mark, &[offset(i)]); let clip = self.clip.filter(|_| shown.kind() == MarkKind::Lines); @@ -172,12 +163,13 @@ impl Marks<'_> { shown: &super::marks::Shown, ) { let ports = shown.port_count(); - if ports == 0 || !shown.compiled.as_ref().is_some_and(|c| c.drawing.primitive == Primitive::Points) { + if ports == 0 || shown.compiled.is_none() || shown.kind() != MarkKind::Vertices { return; } let group = &self.passes.iter().find(|(s, _)| *s == Shader::PointsPorts).expect("listed").1; pass.set_bind_group(1, &self.groups.mark, &[offset(i)]); - pass.set_pipeline(&inner.pipelines.render(self.device, Shader::PointsPorts, self.format, self.samples)); + let variant = Variant::draw(Shader::PointsPorts, self.format, self.samples); + pass.set_pipeline(&inner.pipelines.render(self.device, variant)); pass.set_bind_group(2, group, &[]); drawn.draw(pass, 4, (shown.rows + shown.exits) * ports); } @@ -185,6 +177,8 @@ impl Marks<'_> { /// The marks each step of a frame's compute pass covers. struct Work { + /// Marks whose transitions start this frame, freezing what their rows showed. + frozen: Vec, evaluated: Vec, /// Marks whose rows resolve changed endpoints. resolved: Vec, @@ -247,20 +241,17 @@ pub(super) fn offset(index: usize) -> u32 { } /// The least and greatest finite value of `input` over `rows`, encoded as `lib/reduce.wesl` keeps -/// them: the complement of the least's order-preserving bits, then the greatest's bits. `[0, 0]` -/// when no row has a finite value. +/// them: the complement of the least's key, then the greatest's. `[0, 0]` when no row has a finite +/// value. fn fit(rows: &Rows<'_>, input: &crate::Expr) -> [u32; 2] { - let ordered = |x: f32| { - let b = x.to_bits(); - if b & 0x8000_0000 != 0 { !b } else { b | 0x8000_0000 } - }; + use crate::order::number32; let (mut least, mut most) = (f32::INFINITY, f32::NEG_INFINITY); for x in rows.numbers(input, |v| v as f32) { if x.is_finite() { (least, most) = (least.min(x), most.max(x)); } } - if least > most { [0, 0] } else { [!ordered(least), ordered(most)] } + if least > most { [0, 0] } else { [!number32(least), number32(most)] } } impl super::marks::Shown { @@ -301,7 +292,7 @@ impl super::marks::Shown { return list.kept; } let rows = self.rows + self.exits; - match self.compiled.as_ref().map(|c| c.drawing.primitive) { + match self.compiled.as_ref().map(|_| self.kind().primitive()) { Some(Primitive::Strokes) => rows * self.segments_per_row(), Some(_) => rows, None => 0, @@ -313,7 +304,7 @@ impl super::marks::Shown { if let Some(list) = &self.segments.list { return list.kept; } - match self.compiled.as_ref().map(|c| c.drawing.primitive) { + match self.compiled.as_ref().map(|_| self.kind().primitive()) { Some(Primitive::Strokes) => self.rows * self.segments_per_row(), Some(Primitive::Points) => self.rows * (1 + self.port_count()), Some(_) => self.rows, @@ -321,11 +312,6 @@ impl super::marks::Shown { } } - /// The code of the primitive it draws. - pub(crate) fn primitive(&self) -> u32 { - self.compiled.as_ref().map_or(0, |c| c.drawing.primitive.code()) - } - /// Records the draw of its instances, indirect from its segment list when it has one. pub(super) fn record( &self, @@ -364,7 +350,7 @@ impl View { for &i in work { let shown = &mut self.marks[i]; let into = state; - pass.set_pipeline(&inner.pipelines.compute_pipeline(device, Variant::channels(shown.reads()))); + pass.set_pipeline(&inner.pipelines.compute(device, Variant::channels(shown.reads()))); pass.set_bind_group(1, group, &[offset(i)]); let rows = shown.live(); // A changed domain dirties every block, so the reductions run over the listed blocks. A @@ -387,9 +373,10 @@ impl View { pass.set_bind_group(2, state, &[step(wgsl::STEP_CHANNELS)]); } - /// Records the frame's compute pass: evaluates dirty channels, and sums face normals when their - /// inputs changed. With `measure`, it also measures the extents of every measured mark and - /// returns their readback. With `travel`, comets move by the frame's step. + /// Records the frame's compute pass: freezes starting transitions, evaluates dirty channels, and + /// sums face normals when their inputs changed. With `measure`, it also measures the extents of + /// every measured mark and returns their readback. With `travel`, comets move by the frame's + /// step. pub(super) fn compute( &mut self, encoder: &mut wgpu::CommandEncoder, @@ -399,9 +386,9 @@ impl View { ) -> Option { let gpu = self.gpu.clone(); let device = &gpu.0.device; - let Work { evaluated: work, resolved, eased, segments, faces, measured, carried, travelled, expanded } = + let Work { frozen, evaluated: work, resolved, eased, segments, faces, measured, carried, travelled, expanded } = self.work(measure, travel); - let all = [&work, &resolved, &eased, &segments, &faces, &measured, &carried, &travelled, &expanded]; + let all = [&frozen, &work, &resolved, &eased, &segments, &faces, &measured, &carried, &travelled, &expanded]; if all.iter().all(|marks| marks.is_empty()) { return None; } @@ -422,6 +409,7 @@ impl View { } } let groups = resources.groups(&gpu.0, inner); + let freeze_group = (!frozen.is_empty()).then(|| resources.pass(device, inner, Shader::Freeze)); let evaluate_group = resources.pass(device, inner, Shader::Evaluate); let normals_group = (!faces.is_empty()).then(|| resources.pass(device, inner, Shader::Normals)); let bounds_group = (!measured.is_empty()).then(|| resources.pass(device, inner, Shader::Bounds)); @@ -432,7 +420,7 @@ impl View { gpu.0.queue.write_buffer( resources.lists(), u64::from(list.header) * 4, - bytemuck::cast_slice(&list.header_words(self.marks[i].corners())), + bytemuck::cast_slice(&list.header_words(self.marks[i].kind().primitive().corners())), ); } { @@ -441,42 +429,40 @@ impl View { timestamp_writes: None, }); pass.set_bind_group(0, &groups.view, &[]); - // Each drawn row's word first: every later pass reads it. + let held = super::marks::Shown::held; + // Starting transitions freeze what their rows showed as the frame began, before any + // pass writes it, and after packing placed their page tables. + if let Some(group) = &freeze_group { + pass.set_bind_group(2, group, &[]); + self.over_marks(&mut pass, inner, &groups.mark, GATHER, &frozen, held); + } + // Each drawn row's word first, exits included: every later pass reads it. if let Some(group) = &lists_group { pass.set_bind_group(2, group, &[]); - self.expand_runs(&mut pass, inner, &groups.mark, &expanded); + self.over_marks(&mut pass, inner, &groups.mark, EXPAND_RUNS, &expanded, |s| s.rows + s.exits); } pass.set_bind_group(2, &evaluate_group, &[super::resources::step(wgsl::STEP_CHANNELS)]); - self.every_row(&mut pass, inner, &groups.mark, "carry", &carried); + self.over_marks(&mut pass, inner, &groups.mark, CARRY, &carried, held); self.evaluate(&mut pass, inner, &groups.mark, &evaluate_group, &work); // Before anything that reads where rows' endpoints are held. self.resolve_ends(&mut pass, inner, &groups.mark, &evaluate_group, &resolved); // After evaluating, so marks ease toward this frame's values. - self.every_row(&mut pass, inner, &groups.mark, "ease", &eased); + self.over_marks(&mut pass, inner, &groups.mark, EASE, &eased, held); // After evaluating, so comets travel at this frame's flow. - self.every_row(&mut pass, inner, &groups.mark, "travel", &travelled); + self.over_marks(&mut pass, inner, &groups.mark, TRAVEL, &travelled, held); if let Some(group) = &lists_group { pass.set_bind_group(2, group, &[]); self.count_segments(&mut pass, inner, &groups.mark, &segments); } if let Some(group) = &normals_group { - pass.set_pipeline(&inner.pipelines.compute(device, Shader::Normals, "accumulate")); pass.set_bind_group(2, group, &[]); - for &i in &faces { - pass.set_bind_group(1, &groups.mark, &[offset(i)]); - dispatch(&mut pass, self.marks[i].rows); - } + self.over_marks(&mut pass, inner, &groups.mark, NORMALS, &faces, |s| s.rows); } if let Some(group) = &bounds_group { pass.set_bind_group(2, group, &[]); for &i in &measured { - let entry = match self.marks[i].kind() { - MarkKind::Vertices => "points", - MarkKind::Areas => "areas", - _ => "strokes", - }; - let variant = Variant::compute(Shader::Bounds, entry).path(self.marks[i].path()); - pass.set_pipeline(&inner.pipelines.compute_pipeline(device, variant)); + let Some(bounds) = MarkVariants::shown(&self.marks[i]).bounds() else { continue }; + pass.set_pipeline(&inner.pipelines.compute(device, bounds)); pass.set_bind_group(1, &groups.mark, &[offset(i)]); dispatch(&mut pass, self.marks[i].live_instances()); } @@ -511,10 +497,11 @@ impl View { } } - /// Chooses the marks each compute step covers this frame: evaluation, endpoint resolution, - /// easing, segment counting, face normals, measuring, comet carry, run expansion, and with - /// `travel`, comet travel. + /// Chooses the marks each compute step covers this frame: freezing, evaluation, endpoint + /// resolution, easing, segment counting, face normals, measuring, comet carry, run expansion, + /// and with `travel`, comet travel. fn work(&mut self, measure: bool, travel: bool) -> Work { + let frozen = (0..self.marks.len()).filter(|&i| self.marks[i].remap != wgsl::NONE).collect(); for shown in &mut self.marks { // A mark without rows has nothing to evaluate and no endpoints to resolve. if shown.held() == 0 && shown.tables.synced() { @@ -559,7 +546,7 @@ impl View { let carried = (0..self.marks.len()).filter(|i| self.marks[*i].carry && flowing(i)).collect(); let travelled = (0..self.marks.len()).filter(|i| travel && flowing(i)).collect(); let expanded = self.drawn_rows_due(); - Work { evaluated: work, resolved, eased, segments, faces, measured, carried, travelled, expanded } + Work { frozen, evaluated: work, resolved, eased, segments, faces, measured, carried, travelled, expanded } } /// Writes the blocks buffer, which each mark's uniform points into. For each mark, it lists the @@ -583,41 +570,23 @@ impl View { resources.write_blocks(&gpu.0.device, &gpu.0.queue, &words); } - /// Writes the word of each drawn row of `marks`, exits included, from their runs. - fn expand_runs( - &self, - pass: &mut wgpu::ComputePass<'_>, - inner: &mut Inner, - mark: &wgpu::BindGroup, - marks: &[usize], - ) { - if marks.is_empty() { - return; - } - pass.set_pipeline(&inner.pipelines.compute(&self.gpu.0.device, Shader::Lists, "expand_runs")); - for &i in marks { - pass.set_bind_group(1, mark, &[offset(i)]); - dispatch(pass, self.marks[i].rows + self.marks[i].exits); - } - } - - /// Runs the expression machine's entry point `entry` over every held row of `marks`, exits - /// included. - fn every_row( + /// Runs `variant` over each of `marks`, one invocation per item `items` counts for the mark. + fn over_marks( &self, pass: &mut wgpu::ComputePass<'_>, inner: &mut Inner, mark: &wgpu::BindGroup, - entry: &'static str, + variant: Variant, marks: &[usize], + items: impl Fn(&super::marks::Shown) -> u32, ) { if marks.is_empty() { return; } - pass.set_pipeline(&inner.pipelines.compute(&self.gpu.0.device, Shader::Evaluate, entry)); + pass.set_pipeline(&inner.pipelines.compute(&self.gpu.0.device, variant)); for &i in marks { pass.set_bind_group(1, mark, &[offset(i)]); - dispatch(pass, self.marks[i].held()); + dispatch(pass, items(&self.marks[i])); } } @@ -632,14 +601,12 @@ impl View { let device = &self.gpu.0.device; for &i in marks { let items = self.marks[i].rows + self.marks[i].exits; - let path = self.marks[i].path(); + let [count, write] = MarkVariants::shown(&self.marks[i]).segments(); pass.set_bind_group(1, mark, &[offset(i)]); - let count = Variant::compute(Shader::Lists, "count_segments").path(path); - pass.set_pipeline(&inner.pipelines.compute_pipeline(device, count)); + pass.set_pipeline(&inner.pipelines.compute(device, count)); dispatch(pass, items); super::lists::scan(pass, &mut inner.pipelines, device, items); - let write = Variant::compute(Shader::Lists, "write_segments").path(path); - pass.set_pipeline(&inner.pipelines.compute_pipeline(device, write)); + pass.set_pipeline(&inner.pipelines.compute(device, write)); dispatch(pass, items); } } @@ -652,10 +619,10 @@ impl View { let shown = &self.marks[i]; let Some(compiled) = &shown.compiled else { return Vec::new() }; let program = &compiled.program; - let style = shown.mark.style(); - let table = self.snapshot(&style.data).and_then(|d| Some((d, d.table(&style.table)?))); - let Some((data, table)) = table else { return vec![0; 2 * program.domains.len()] }; - let source = Source { data, table, derived: self.derived.beside(&style.data, &style.table), paged: Vec::new() }; + let Some(source) = Source::of(shown.mark.style(), &self.snapshots, &self.derived) else { + return vec![0; 2 * program.domains.len()]; + }; + let table = source.table; let rows = Rows::new(table, &self.snapshots).at(self.at); let mut fitted = Vec::with_capacity(program.domains.len()); for (d, (reads, input)) in program.domains.iter().zip(&program.fitted).enumerate() { @@ -663,7 +630,7 @@ impl View { .filter(|&c| reads.columns & Reads::column(c) != 0) .map(|c| { let column = &program.columns[c]; - table_of(column, &source, &self.snapshots)?.1.field_named(&column.name).map(Field::version) + source.table_of(column, &self.snapshots)?.1.field_named(&column.name).map(Field::version) }) .collect(); let sampled = reads.deps.intersects(Deps::SAMPLES).then_some(self.at.to_bits()); @@ -760,7 +727,7 @@ impl View { let mut pass = main_pass(encoder, view, resolve, &depth, background, probing); pass.set_bind_group(0, &groups.view, &[]); pass.set_bind_group(1, &groups.mark, &[0]); - pass.set_pipeline(&inner.pipelines.render(device, Shader::Backdrop, frame.format, samples)); + pass.set_pipeline(&inner.pipelines.render(device, Variant::draw(Shader::Backdrop, frame.format, samples))); pass.set_bind_group(2, group(Shader::Backdrop), &[]); drawn.draw(&mut pass, 3, 1); let marks = Marks { @@ -791,10 +758,10 @@ impl View { if !texts { return; } - let glyphs = [Shader::Glyphs, Shader::GlyphsPorts].map(|shader| (shader, group(shader))); + let glyphs = [Shader::Glyphs, Shader::GlyphsPorts].map(group); self.draw_labels(&mut pass, drawn, inner, frame.format, samples, &groups, glyphs); if sprites > 0 { - pass.set_pipeline(&inner.pipelines.render(device, Shader::Overlay, frame.format, samples)); + pass.set_pipeline(&inner.pipelines.render(device, Variant::draw(Shader::Overlay, frame.format, samples))); pass.set_bind_group(2, group(Shader::Overlay), &[]); drawn.draw(&mut pass, 4, sprites); } @@ -803,69 +770,54 @@ impl View { /// Records the probe pass: finds the drawn row nearest the probe point and the nearest depth /// drawn there, and returns their readback. pub(super) fn probe(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner) -> Readback { - let gpu = self.gpu.clone(); - let device = &gpu.0.device; let probed: Vec = (0..self.marks.len()).filter(|&i| self.marks[i].probed()).collect(); // Any drawn mark's probe program can read the depth under the probe; the first one does. - let under = self.marks.iter().position(super::marks::Shown::drawable); - let resources = self.resources.as_mut().expect("created before passes"); - let shader = crate::gpu::pipelines::probe(resources.samples()); + let under: Vec = self.marks.iter().position(super::marks::Shown::drawable).into_iter().collect(); let mut reset = [0_u32; wgsl::PROBE_WORDS as usize]; reset[wgsl::PROBE_KEY as usize] = wgsl::NONE; - gpu.0.queue.write_buffer(resources.probe(), 0, bytemuck::cast_slice(&reset)); - let groups = resources.groups(&gpu.0, inner); - let group = resources.pass(device, inner, shader); - { - let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { - label: Some("latkit probe"), - timestamp_writes: None, - }); - pass.set_bind_group(0, &groups.view, &[]); - // Each step runs over every mark before the next: the nearest key, then its topmost - // instance, then where that lies. - for step in [wgsl::STEP_NEAREST, wgsl::STEP_TOPMOST, wgsl::STEP_DESCRIBE] { - pass.set_bind_group(2, &group, &[super::resources::step(step)]); - for &i in &probed { - self.probe_step(&mut pass, inner, shader, &groups.mark, i, step); - } - } - if let Some(i) = under { - pass.set_bind_group(2, &group, &[super::resources::step(wgsl::STEP_UNDER)]); - self.probe_step(&mut pass, inner, shader, &groups.mark, i, wgsl::STEP_UNDER); - } - } + let probe = self.resources.as_ref().expect("created before passes").probe(); + self.gpu.0.queue.write_buffer(probe, 0, bytemuck::cast_slice(&reset)); + // Each step runs over every mark before the next: the nearest key, then its topmost + // instance, then where that lies. + let steps = [wgsl::STEP_NEAREST, wgsl::STEP_TOPMOST, wgsl::STEP_DESCRIBE].map(|step| (step, &probed[..])); + self.probe_pass(encoder, inner, "latkit probe", &[&steps[..], &[(wgsl::STEP_UNDER, &under[..])]].concat()); let resources = self.resources.as_ref().expect("created before passes"); - Readback::new( - &inner.readbacks, - device, - encoder, - resources.probe(), - 0, - u64::from(wgsl::PROBE_WORDS) * 4, - &self.waker, - ) + let (device, bytes) = (&self.gpu.0.device, u64::from(wgsl::PROBE_WORDS) * 4); + Readback::new(&inner.readbacks, device, encoder, resources.probe(), 0, bytes, &self.waker) } - /// Records step `step` of mark `i`'s probe program `shader`, with the step's pass group bound. - /// It dispatches over every instance probes cover, or a single workgroup for a step that reads - /// one result. - pub(super) fn probe_step( - &self, - pass: &mut wgpu::ComputePass<'_>, + /// Records a compute pass named `label` that runs each of `steps` of the probe program in turn: + /// a `STEP_` code, over each of the marks listed with it. + pub(super) fn probe_pass( + &mut self, + encoder: &mut wgpu::CommandEncoder, inner: &mut Inner, - shader: Shader, - mark: &wgpu::BindGroup, - i: usize, - step: u32, + label: &str, + steps: &[(u32, &[usize])], ) { - let shown = &self.marks[i]; - let variant = Variant::probe(shader, shown.primitive()).path(shown.path()); - pass.set_pipeline(&inner.pipelines.compute_pipeline(&self.gpu.0.device, variant)); - pass.set_bind_group(1, mark, &[offset(i)]); - if matches!(step, wgsl::STEP_DESCRIBE | wgsl::STEP_LOCATE | wgsl::STEP_UNDER) { - pass.dispatch_workgroups(1, 1, 1); - } else { - dispatch(pass, shown.live_instances()); + let gpu = self.gpu.clone(); + let device = &gpu.0.device; + let resources = self.resources.as_mut().expect("created before passes"); + let shader = crate::gpu::pipelines::probe(resources.samples()); + let groups = resources.groups(&gpu.0, inner); + let group = resources.pass(device, inner, shader); + let mut pass = + encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some(label), timestamp_writes: None }); + pass.set_bind_group(0, &groups.view, &[]); + for &(step, marks) in steps { + pass.set_bind_group(2, &group, &[super::resources::step(step)]); + for &i in marks { + let shown = &self.marks[i]; + pass.set_pipeline(&inner.pipelines.compute(device, MarkVariants::shown(shown).probe(shader))); + pass.set_bind_group(1, &groups.mark, &[offset(i)]); + // A step that reads one result runs once; the rest run over every instance probes + // cover. + if matches!(step, wgsl::STEP_DESCRIBE | wgsl::STEP_LOCATE | wgsl::STEP_UNDER) { + pass.dispatch_workgroups(1, 1, 1); + } else { + dispatch(&mut pass, shown.live_instances()); + } + } } } } diff --git a/crates/latkit/src/view/plan.rs b/crates/latkit/src/view/plan.rs index f32ff288..451d73b2 100644 --- a/crates/latkit/src/view/plan.rs +++ b/crates/latkit/src/view/plan.rs @@ -18,20 +18,19 @@ //! are planned within a smaller part of the share until the share changes. That way the current //! drawing and its replacement both fit, with room left to keep idle pages. -use std::hash::{Hash, Hasher}; +use std::hash::Hash; -use rustc_hash::{FxHashMap, FxHasher}; +use rustc_hash::FxHashMap; use super::View; use super::marks::Shown; use crate::data::{Field, Keyed, Name, Table}; use crate::derive::tiled::{Draw, Place, Planned, Plans}; -use crate::derive::{Derived, stamp_of}; +use crate::derive::{Derived, stamp_of, stamp_with}; use crate::expr::compile::Words; use crate::gpu::arena::{path_page_bytes, rounded}; use crate::gpu::shaders as wgsl; use crate::gpu::{Ask, Ledger}; -use crate::scene::mark::MarkKind; use crate::{Expr, Kind, Mark, Space}; /// Rows past which a table a mark places is paged: more than a screen shows at one row per cell. @@ -166,17 +165,17 @@ impl View { /// A stamp of the costs' inputs: the scene, and for each table a mark draws, its dataset's /// schema, its row structure, its geometry's version, and whether it comes from tiles. fn costs_stamp(&self) -> u64 { - let mut hasher = FxHasher::default(); - self.scene_version.hash(&mut hasher); - for shown in &self.marks { - let style = shown.mark.style(); - let data = self.snapshot(&style.data); - let table = data.and_then(|d| d.table(&style.table)); - let geometry = geometry_of(&shown.mark).and_then(|f| table?.field_named(&f)).map(Field::version); - let owned = self.derived.tiles.owns(&style.data); - (data.map(crate::Snapshot::schema), table.map(|t| t.structure), geometry, owned).hash(&mut hasher); - } - hasher.finish().max(1) + stamp_with(|hasher| { + self.scene_version.hash(hasher); + for shown in &self.marks { + let style = shown.mark.style(); + let data = self.snapshot(&style.data); + let table = data.and_then(|d| d.table(&style.table)); + let geometry = geometry_of(&shown.mark).and_then(|f| table?.field_named(&f)).map(Field::version); + let owned = self.derived.tiles.owns(&style.data); + (data.map(crate::Snapshot::schema), table.map(|t| t.structure), geometry, owned).hash(hasher); + } + }) } /// Plans how each table the scene draws fits in `share` bytes given `costs`, giving paged @@ -256,14 +255,8 @@ impl View { let mut plans = Plans::default(); let mut costs: Keyed<(Name, Name), Cost> = Keyed::default(); let mut shared: FxHashMap = FxHashMap::default(); - // Tables that faces end at keep the normals the faces sum at their rows. - let mut summed: Vec<(Name, Name)> = Vec::new(); - for shown in self.marks.iter().filter(|m| m.kind() == MarkKind::Faces) { - let Some(compiled) = &shown.compiled else { continue }; - let data = &shown.mark.style().data; - summed.extend(compiled.program.ends.iter().map(|end| (data.clone(), end.table.clone()))); - } - for shown in &self.marks { + let summing = super::graph::summing(&self.marks); + for (shown, summing) in self.marks.iter().zip(summing) { let style = shown.mark.style(); let Some(table) = self.snapshot(&style.data).and_then(|d| d.table(&style.table)) else { continue }; let key = (style.data.clone(), style.table.clone()); @@ -278,10 +271,9 @@ impl View { if placing && planned.place == Place::Nowhere { planned.place = place_of(&shown.mark, table); } - let normals = shown.kind() == MarkKind::Vertices && summed.contains(&key); let cost = costs.entry(key).or_insert(Cost { rows: table.len(), per_page: 0.0, geometry: 0.0, traced: 0.0 }); - cost.per_page += page_bytes(shown, easing, normals); + cost.per_page += page_bytes(shown, easing, summing); cost.geometry += geometry_bytes(shown, table); cost.traced += traced_bytes(shown, table); for (piece, bytes) in self.shared_pages(shown, table) { @@ -370,18 +362,16 @@ fn column_page_bytes(low: bool) -> f64 { } /// Bytes a page of `shown`'s rows takes, as the arena allocates them: a page of each column it -/// reads, plus each kind of per-row words. Those are its state, focus words, and label candidates; -/// the `normals` faces sum, at a vertex mark they attach to; and when `easing`, a transition's -/// frozen and settled words. -fn page_bytes(shown: &Shown, easing: bool, normals: bool) -> f64 { +/// reads, plus each kind of word page it keeps (with the normals faces are `summing` at its rows), +/// and when `easing`, a transition's frozen and settled words. +fn page_bytes(shown: &Shown, easing: bool, summing: bool) -> f64 { let Some(compiled) = &shown.compiled else { return 0.0 }; let columns: f64 = compiled.program.columns.iter().filter(|c| c.words != Words::Path).map(|c| column_page_bytes(c.low)).sum(); - let state = block_bytes(compiled.drawing.stride + compiled.drawing.extras); - let labels = if compiled.drawing.label.is_some() { block_bytes(wgsl::CANDIDATE_WORDS) } else { 0.0 }; - let normals = if normals { block_bytes(wgsl::NORMAL_WORDS) } else { 0.0 }; - let eased = if easing { 2.0 * state } else { 0.0 }; - columns + state + block_bytes(1) + labels + normals + eased + let kept: f64 = + shown.kept_widths(summing).iter().filter(|&&(_, width)| width > 0).map(|&(_, width)| block_bytes(width)).sum(); + let eased = if easing { 2.0 * block_bytes(shown.width()) } else { 0.0 }; + columns + kept + eased } /// Bytes the traces `shown` draws of `table` take per pixel column: `TRACE_COLUMN_BYTES` for each diff --git a/crates/latkit/src/view/probe.rs b/crates/latkit/src/view/probe.rs index ec15a73f..0bc68a65 100644 --- a/crates/latkit/src/view/probe.rs +++ b/crates/latkit/src/view/probe.rs @@ -12,10 +12,11 @@ use super::gestures::Gesture; use super::marks::{Holding, Shown}; use super::sync::Plan; use super::{Device, View}; +use crate::derive::Working; use crate::gpu::readback::Readback; use crate::gpu::shaders as wgsl; use crate::scene::mark::MarkKind; -use crate::{Camera, Name, Row}; +use crate::{Camera, Name, Row, wire}; /// A hit, with its depth (for dragging) and, for an edge, the nearer endpoint: 0 for the first, /// 1 for the second. @@ -61,17 +62,34 @@ impl Drew { pub(super) fn row(&self, i: usize, held: u32) -> Option<(Row, usize)> { let mark = self.marks.get(i)?; let holding = mark.holding.as_ref()?; - let r = match &holding.drawing { - Some(drawing) => drawing.source(held)?, - None => (held < holding.table.len() as u32).then_some(held)?, - }; + let r = holding.source_row(held)?; let id = holding.table.ids.id(r as usize)?; Some((Row { data: mark.data.clone(), table: mark.table.clone(), id }, r as usize)) } - /// The name of mark `i`. - pub(super) fn name(&self, i: usize) -> Option { - self.marks.get(i)?.name.clone() + /// The hit at held row `held` of mark `i` as the frame drew it: at screen `point` and `depth`, + /// `distance` 1/64 CSS pixels from where it was asked, on `part` (a port's number from 1, or a + /// segment's index along its path). `None` when the frame held no such row. + pub(super) fn hit( + &self, + i: usize, + held: u32, + point: [f32; 2], + depth: f32, + distance: u32, + part: u32, + ) -> Option { + let (row, at) = self.row(i, held)?; + Some(Hit { + row, + mark: self.marks.get(i)?.name.clone(), + index: i, + point, + position: self.projected.position(self.projected.unproject(point.map(f64::from), f64::from(depth))), + distance: distance as f32 / 64.0, + port: self.port(i, part), + sample: self.sample(i, at, part), + }) } /// Whether a box's selection picks mark `i`'s rows. @@ -80,7 +98,7 @@ impl Drew { } /// The name of port `number` of vertex mark `i`, numbered from 1; `None` for 0, the marker. - pub(super) fn port(&self, i: usize, number: u32) -> Option { + fn port(&self, i: usize, number: u32) -> Option { let mark = self.marks.get(i)?; if !mark.vertices || number == 0 { return None; @@ -89,7 +107,7 @@ impl Drew { } /// The sample that point `point` of row `row`'s traced path shows, on mark `i` tracing a field. - pub(super) fn sample(&self, i: usize, row: usize, point: u32) -> Option { + fn sample(&self, i: usize, row: usize, point: u32) -> Option { let mark = self.marks.get(i)?; let (field, plan) = mark.traced.as_ref()?; let (level, index, side) = plan.entry(point)?; @@ -101,18 +119,14 @@ impl Drew { let value = samples.entry(row, level, side, index)?; Some(Sample { frame, at: samples.drawn_at(level, index)?, value }) } - - /// The position of screen point `point` at `depth`, through the frame's camera. - pub(super) fn position(&self, point: [f32; 2], depth: f64) -> [f64; 3] { - self.projected.position(self.projected.unproject(point.map(f64::from), depth)) - } } #[derive(Default)] pub(crate) struct Probing { /// The next point to probe. pub(crate) wanted: Option<[f32; 2]>, - reading: Option<([f32; 2], Readback, Drew)>, + /// The probe being read back: the point it asked about, and what its frame drew. + reading: Working<([f32; 2], Drew), Readback>, /// The latest answer and the point it was asked for. pub(crate) latest: Option<([f32; 2], Answer)>, } @@ -120,7 +134,7 @@ pub(crate) struct Probing { impl Probing { /// Whether a point is waiting for a frame to probe it. Never while a probe is being read back. pub(crate) fn due(&self) -> bool { - self.wanted.is_some() && self.reading.is_none() + self.wanted.is_some() && !self.reading.busy() } pub(crate) fn hovered(&self) -> Option<&Probed> { @@ -131,7 +145,7 @@ impl Probing { /// is pending. pub(crate) fn at(&self, point: [f32; 2]) -> Option { let (asked, answer) = self.latest.as_ref()?; - (self.wanted.is_none() && self.reading.is_none() && near(*asked, point, 2.0)).then(|| answer.clone()) + (self.wanted.is_none() && !self.reading.busy() && near(*asked, point, 2.0)).then(|| answer.clone()) } } @@ -151,7 +165,7 @@ impl View { { self.probing.wanted.get_or_insert(pointer.point); } - if self.probing.reading.is_some() || !answers { + if self.probing.reading.busy() || !answers { return None; } // Where nothing is drawn, there is nothing to probe. @@ -166,7 +180,8 @@ impl View { } pub(super) fn probing(&mut self, point: [f32; 2], readback: Readback) { - self.probing.reading = Some((point, readback, self.drew())); + let drew = self.drew(); + self.probing.reading.start(0, readback, (point, drew)); } /// A record of what this frame drew, for decoding answers about it. @@ -198,11 +213,8 @@ impl View { /// Hands a finished probe's answer to hover and gestures. pub(super) fn settle_probe(&mut self) { - let Some((point, readback, _)) = &self.probing.reading else { return }; - let Some(bytes) = readback.take() else { return }; - let point = *point; - let (_, _, drew) = self.probing.reading.take().expect("reading"); - let answer = bytes.map_or_else(|_| Answer::default(), |bytes| answer(bytemuck::cast_slice(&bytes), &drew)); + let Some((_, (point, drew), bytes)) = self.probing.reading.landed() else { return }; + let answer = bytes.map_or_else(|_| Answer::default(), |bytes| answer(&wire::words(&bytes), &drew)); self.landed(point, answer); } @@ -240,22 +252,11 @@ fn hit(word: &impl Fn(u32) -> u32, drew: &Drew) -> Option { if key == wgsl::NONE || word(wgsl::PROBE_INSTANCE) == 0 { return None; } - let mark = 255 - (key & 255) as usize; - // The probe reports a held row; map it to its table row. - let (row, at) = drew.row(mark, word(wgsl::PROBE_ROW))?; let depth = f32::from_bits(word(wgsl::PROBE_DEPTH)); let point = [f32::from_bits(word(wgsl::PROBE_X)), f32::from_bits(word(wgsl::PROBE_Y))]; - let hit = Hit { - row, - mark: drew.name(mark), - index: mark, - point, - position: drew.position(point, f64::from(depth)), - distance: (key >> 8) as f32 / 64.0, - port: drew.port(mark, word(wgsl::PROBE_SEGMENT)), - sample: drew.sample(mark, at, word(wgsl::PROBE_SEGMENT)), - }; - Some(Probed { hit, depth, end: word(wgsl::PROBE_SEGMENT) }) + let part = word(wgsl::PROBE_SEGMENT); + let hit = drew.hit(255 - (key & 255) as usize, word(wgsl::PROBE_ROW), point, depth, key >> 8, part)?; + Some(Probed { hit, depth, end: part }) } impl super::marks::Shown { diff --git a/crates/latkit/src/view/program.rs b/crates/latkit/src/view/program.rs index 4a58dff7..489c988a 100644 --- a/crates/latkit/src/view/program.rs +++ b/crates/latkit/src/view/program.rs @@ -49,11 +49,13 @@ impl View { let Some(compiled) = &mut shown.compiled else { continue }; let markers = &mut compiled.drawing.markers; if !markers.icons.is_empty() { - let layers = inner.icons.layers(&gpu.0.device, &gpu.0.queue, &markers.icons); - markers.palette = std::iter::once(layers.len() as u32).chain(layers).collect(); + match inner.icons.of(&gpu.0.device, &gpu.0.queue, &markers.icons) { + Ok(layers) => markers.palette = std::iter::once(layers.len() as u32).chain(layers).collect(), + Err(error) => self.events.tell(Event::Error(error)), + } } for (at, colormap) in &compiled.program.colormaps { - match inner.colormaps.layer(&gpu.0.device, &gpu.0.queue, colormap) { + match inner.colormaps.colormap(&gpu.0.device, &gpu.0.queue, colormap) { Ok(layer) => shown.code[*at as usize][1] = layer | colormap.kind().code() << 16, Err(error) => self.events.tell(Event::Error(error)), } diff --git a/crates/latkit/src/view/query.rs b/crates/latkit/src/view/query.rs index 7280277f..b2b3e38d 100644 --- a/crates/latkit/src/view/query.rs +++ b/crates/latkit/src/view/query.rs @@ -14,7 +14,7 @@ use rustc_hash::FxHashSet; use super::View; use super::event::{Hit, Trigger}; use super::probe::Drew; -use super::resources::step; +use crate::derive::Working; use crate::gpu::Inner; use crate::gpu::readback::Readback; use crate::gpu::shaders as wgsl; @@ -97,11 +97,11 @@ struct Running { words: u32, } -/// A query whose answer the GPU is copying back, and what its frame drew. +/// What reading a query's answer needs: the query, where each mark's results are, and what its +/// frame drew. struct Reading { ask: Ask, found: Vec, - readback: Readback, drew: Drew, } @@ -109,7 +109,8 @@ struct Reading { pub(crate) struct Queries { waiting: VecDeque, running: Option, - reading: Option, + /// The query whose answer the GPU is copying back. + reading: Working, } impl Queries { @@ -165,7 +166,7 @@ impl View { /// room for its results after the probe's words, and returns its query words and whether it /// reads depth. pub(super) fn next_query(&mut self) -> Option<([f32; 4], bool)> { - if self.queries.running.is_some() || self.queries.reading.is_some() { + if self.queries.running.is_some() || self.queries.reading.busy() { return None; } let ask = std::iter::from_fn(|| self.queries.waiting.pop_front()).find(|ask| !ask.cancelled())?; @@ -204,8 +205,7 @@ impl View { let Some(Running { ask, found, words }) = self.queries.running.take() else { return }; let gpu = self.gpu.clone(); let (device, queue) = (&gpu.0.device, &gpu.0.queue); - let resources = self.resources.as_mut().expect("created before queries"); - let probe = resources.probe().clone(); + let probe = self.resources.as_ref().expect("created before queries").probe().clone(); let start = u64::from(wgsl::PROBE_WORDS) * 4; // Results start empty: for hits, a zero count and each mark's capacity; for a box, no rows. match &ask { @@ -217,35 +217,22 @@ impl View { Ask::Boxed { .. } if words > 0 => encoder.clear_buffer(&probe, start, Some(u64::from(words) * 4)), Ask::Boxed { .. } | Ask::Locate { .. } => {} } - let shader = crate::gpu::pipelines::probe(resources.samples()); - let groups = resources.groups(&gpu.0, inner); - let group = resources.pass(device, inner, shader); - { - let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { - label: Some("latkit query"), - timestamp_writes: None, - }); - pass.set_bind_group(0, &groups.view, &[]); - pass.set_bind_group(2, &group, &[step(ask.step())]); - for f in &found { - self.probe_step(&mut pass, inner, shader, &groups.mark, f.mark, ask.step()); - } - } + let marks: Vec = found.iter().map(|f| f.mark).collect(); + self.probe_pass(encoder, inner, "latkit query", &[(ask.step(), &marks)]); let (at, bytes) = match &ask { Ask::Locate { .. } => (u64::from(wgsl::PROBE_LOCATE_X) * 4, 16), _ => (start, u64::from(words.max(1)) * 4), }; let readback = Readback::new(&inner.readbacks, device, encoder, &probe, at, bytes, &self.waker); - self.queries.reading = Some(Reading { ask, found, readback, drew: self.drew() }); + let drew = self.drew(); + self.queries.reading.start(0, readback, Reading { ask, found, drew }); } /// Answers a query whose readback has arrived. pub(super) fn settle_query(&mut self) { - let Some(reading) = &self.queries.reading else { return }; - let Some(bytes) = reading.readback.take() else { return }; - let Reading { ask, found, drew, .. } = self.queries.reading.take().expect("reading"); - let words: Vec = match bytes { - Ok(bytes) => bytemuck::cast_slice(&bytes).to_vec(), + let Some((_, Reading { ask, found, drew }, bytes)) = self.queries.reading.landed() else { return }; + let words = match bytes { + Ok(bytes) => crate::wire::words(&bytes), Err(error) => { match ask { Ask::Hits { reply, .. } => reply.resolve(Err(error)), @@ -316,19 +303,8 @@ fn hits(found: &[Found], words: &[u32], drew: &Drew) -> Vec { if !seen.insert((i, entry[0])) { continue; } - let Some((row, at)) = drew.row(i, entry[0]) else { continue }; let point = [f32::from_bits(entry[3]), f32::from_bits(entry[4])]; - let depth = f64::from(f32::from_bits(entry[5])); - hits.push(Hit { - row, - mark: drew.name(i), - index: i, - point, - position: drew.position(point, depth), - distance: entry[1] as f32 / 64.0, - port: drew.port(i, entry[2]), - sample: drew.sample(i, at, entry[2]), - }); + hits.extend(drew.hit(i, entry[0], point, f32::from_bits(entry[5]), entry[1], entry[2])); } hits } diff --git a/crates/latkit/src/view/resources.rs b/crates/latkit/src/view/resources.rs index ef1333ba..0ee0e406 100644 --- a/crates/latkit/src/view/resources.rs +++ b/crates/latkit/src/view/resources.rs @@ -1,6 +1,6 @@ -//! A view's buffers, textures, and bind groups. A buffer that runs out of room is replaced by one -//! 1.5 times the size needed. Replacing a buffer increments the generation, and bind groups are -//! rebuilt when a generation they were built from changes. +//! A view's buffers, textures, and bind groups. A buffer that runs out of room grows +//! ([`Grown`]). Replacing a buffer increments the generation, and bind groups are rebuilt when a +//! generation they were built from changes. //! //! Every shader's pass group (group 2) uses the same bindings: 0 the arena pages (the data, and the //! marks' per-row words), 2 extents, 4 domains, 5 probe words and query results, 6 depth, @@ -10,6 +10,7 @@ use std::collections::HashMap; use std::num::NonZeroU64; +use crate::gpu::grown::{self, Grown}; use crate::gpu::shaders::{self as wgsl, Shader}; use crate::gpu::work::Queue; use crate::gpu::{Inner, Shared}; @@ -44,37 +45,6 @@ fn steps(device: &wgpu::Device) -> wgpu::Buffer { /// the texts placement requests. const LABELS: wgpu::BufferUsages = wgpu::BufferUsages::STORAGE.union(wgpu::BufferUsages::COPY_SRC); -/// A buffer that is replaced by one 1.5 times the size needed when it runs out of room. -struct Grown { - buffer: wgpu::Buffer, - label: &'static str, - usage: wgpu::BufferUsages, -} - -impl Grown { - fn new(device: &wgpu::Device, label: &'static str, usage: wgpu::BufferUsages) -> Grown { - Grown { buffer: create(device, label, usage, 256), label, usage } - } - - /// Makes room for `bytes`; true when the buffer was replaced and its contents lost. - fn ensure(&mut self, device: &wgpu::Device, bytes: u64) -> bool { - if bytes <= self.buffer.size() { - return false; - } - self.buffer = create(device, self.label, self.usage, (bytes + bytes / 2).next_multiple_of(256)); - true - } -} - -fn create(device: &wgpu::Device, label: &str, usage: wgpu::BufferUsages, bytes: u64) -> wgpu::Buffer { - device.create_buffer(&wgpu::BufferDescriptor { - label: Some(label), - size: bytes.max(256).next_multiple_of(4), - usage: usage | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }) -} - /// The bind groups every pipeline shares. #[derive(Clone)] pub(crate) struct Groups { @@ -128,8 +98,8 @@ impl Resources { let storage = wgpu::BufferUsages::STORAGE; let uniform = wgpu::BufferUsages::UNIFORM; Resources { - frame: create(device, "latkit frame", uniform, size_of::() as u64), - eye: create(device, "latkit eye", uniform, size_of::() as u64), + frame: grown::buffer(device, "latkit frame", uniform, size_of::() as u64), + eye: grown::buffer(device, "latkit eye", uniform, size_of::() as u64), marks: Grown::new(device, "latkit marks", uniform), // A copy source, so growing keeps its contents. program: Grown::new(device, "latkit program", storage | wgpu::BufferUsages::COPY_SRC), @@ -146,7 +116,7 @@ impl Resources { "latkit lists", storage | wgpu::BufferUsages::INDIRECT | wgpu::BufferUsages::COPY_SRC, ), - labels: create(device, "latkit labels", LABELS, 256), + labels: grown::buffer(device, "latkit labels", LABELS, 256), generation: 0, lists_generation: 0, groups: None, @@ -236,7 +206,7 @@ impl Resources { words: u32, ) { let size = u64::from(words) * 4; - let between = create(device, "latkit program move", wgpu::BufferUsages::COPY_SRC, size); + let between = grown::buffer(device, "latkit program move", wgpu::BufferUsages::COPY_SRC, size); let program = &self.program.buffer; encoder.copy_buffer_to_buffer(program, u64::from(from) * 4, &between, 0, size); encoder.copy_buffer_to_buffer(&between, 0, program, u64::from(to) * 4, size); @@ -267,7 +237,7 @@ impl Resources { /// Replaces the labels buffer with `words` of zeros. pub(crate) fn replace_labels(&mut self, device: &wgpu::Device, words: u64) { - self.labels = create(device, "latkit labels", LABELS, (words + 1) * 4); + self.labels = grown::buffer(device, "latkit labels", LABELS, (words + 1) * 4); self.generation += 1; } diff --git a/crates/latkit/src/view/sync.rs b/crates/latkit/src/view/sync.rs index 4947f3db..d7d44044 100644 --- a/crates/latkit/src/view/sync.rs +++ b/crates/latkit/src/view/sync.rs @@ -25,17 +25,18 @@ use std::ops::Range; use std::sync::Arc; use super::drawing::{Drawing, Holds}; -use crate::data::{Column, Field, FramePage, GeometryBuilder, PageId, Run, Samples, Values, Version, detail}; -use crate::derive::Beside; +use crate::data::{Column, Field, FramePage, PageId, Run, Samples, Values, Version, detail}; use crate::derive::order::Order; use crate::derive::shapes::PagedShapes; use crate::derive::stamp_of; +use crate::derive::{Beside, Derivations}; use crate::expr::compile::{ColumnUse, Program, Reads, Words}; use crate::gpu::Inner; use crate::gpu::arena::{Ask, Content, PageKey, PathPage, path_pieces}; use crate::gpu::packed::{self, Packing}; use crate::gpu::shaders as wgsl; use crate::gpu::work::Count; +use crate::scene::mark::Style; use crate::{Name, Snapshot, Table}; const WORDS: usize = wgsl::COLUMN_WORDS as usize; @@ -318,10 +319,36 @@ pub(crate) struct Source<'a> { } impl<'a> Source<'a> { + /// The source of a mark drawing `style`'s table: its dataset among `all`, and the tables + /// `derived` holds beside it, without paged shapes, which only syncing reads. `None` until the + /// dataset has the table. + pub(crate) fn of(style: &Style, all: &'a [Snapshot], derived: &'a Derivations) -> Option> { + let data = all.iter().find(|d| *d.name() == style.data)?; + let table = data.table(&style.table)?; + Some(Source { data, table, derived: derived.beside(&style.data, &style.table), paged: Vec::new() }) + } + /// The shapes written as field `name`, made one page of the drawn order at a time. fn paged(&self, name: &Name) -> Option<&'a PagedShapes> { self.paged.iter().find(|(field, _)| field == name).map(|&(_, shapes)| shapes) } + + /// The dataset and table `column` reads: the mark's own, another one among `all` joined by id, + /// or the derived table beside the mark's own that holds the field. + pub(crate) fn table_of(&self, column: &ColumnUse, all: &'a [Snapshot]) -> Option<(&'a Name, &'a Table)> { + if column.derived { + let mut beside = self.derived.iter().flatten(); + let table = beside.find(|t| t.field_named(&column.name).is_some()).copied().or(self.derived[0])?; + return Some((self.data.name(), table)); + } + match &column.from { + None => Some((self.data.name(), self.table)), + Some((data, table)) => { + let data = all.iter().find(|d| d.name() == data)?; + Some((data.name(), data.table(table)?)) + } + } + } } /// What a sync reads besides the mark's own rows: every dataset, the shown coordinate, the trace @@ -607,27 +634,6 @@ fn columns_from(program: &Program, from: &(Name, Name)) -> u64 { .fold(0, |bits, (i, _)| bits | Reads::column(i)) } -/// The dataset and table a column reads: the mark's own, another one joined by id, or the derived -/// table beside the mark's own that holds the field. -pub(crate) fn table_of<'a>( - column: &ColumnUse, - source: &Source<'a>, - all: &'a [Snapshot], -) -> Option<(&'a Name, &'a Table)> { - if column.derived { - let mut beside = source.derived.iter().flatten(); - let table = beside.find(|t| t.field_named(&column.name).is_some()).copied().or(source.derived[0])?; - return Some((source.data.name(), table)); - } - match &column.from { - None => Some((source.data.name(), source.table)), - Some((data, table)) => { - let data = all.iter().find(|d| d.name() == data)?; - Some((data.name(), data.table(table)?)) - } - } -} - /// The table a join reads. fn joined<'a>((data, table): &(Name, Name), all: &'a [Snapshot]) -> Option<&'a Table> { all.iter().find(|d| d.name() == data)?.table(table) @@ -657,17 +663,46 @@ fn plan(samples: &Samples, window: Window) -> Plan { Plan { level, runs, points: point, steps: window.steps } } +/// Where sampled `column` reads `samples` at axis coordinate `at`: the frame shown there, and how +/// far toward the next one it is; 0 for a column that steps. +fn shown_frame(samples: &Samples, column: &ColumnUse, at: f64) -> (Option, f32) { + let frame = samples.frame_at(at); + let t = if column.step { 0.0 } else { frame.map_or(0.0, |f| samples.t(f, at)) }; + (frame, t) +} + +/// A column descriptor with `flags`, reading no low parts and no join. +fn descriptor(flags: u32) -> [u32; WORDS] { + let mut d = [0; WORDS]; + d[wgsl::COLUMN_FLAGS as usize] = flags; + d[wgsl::COLUMN_LOW as usize] = wgsl::NONE; + d[wgsl::COLUMN_JOIN as usize] = wgsl::NONE; + d +} + +/// The cache key of a column of `table` packed in `drawing`'s order: field `field` as `words`. +fn packing(drawing: &Drawing, source: &Source<'_>, table: &Name, field: Name, words: Words) -> Packing { + Packing { + order: drawing.order.id(), + origin: source.data.origin(), + table: table.clone(), + field, + words, + low: false, + reads: 0, + } +} + fn stamp(column: &ColumnUse, source: &Source<'_>, inputs: &Inputs<'_>) -> Stamp { if let Some(shapes) = source.paged(&column.name).filter(|_| column.derived) { return Stamp::Field(shapes.version, shapes.order); } - let Some((_, table)) = table_of(column, source, inputs.all) else { return Stamp::Missing }; + let Some((_, table)) = source.table_of(column, inputs.all) else { return Stamp::Missing }; match table.field_named(&column.name) { None => Stamp::Missing, Some(Field::Sampled(samples, version)) if column.trace => Stamp::Trace(*version, plan(samples, inputs.window)), Some(Field::Sampled(samples, version)) => { - let frame = samples.frame_at(inputs.at); - let t = if column.step { 0.0 } else { frame.map_or(0.0, |f| samples.t(f, inputs.at)) }; + let (frame, t) = shown_frame(samples, column, inputs.at); Stamp::Sampled(*version, frame, t.to_bits()) } Some(Field::Reference(reference)) => { @@ -723,28 +758,16 @@ impl<'g> Builder<'g> { } fn column(&mut self, column: &ColumnUse, source: &Source<'_>, rows: Rows<'_>, inputs: &Inputs<'_>) -> [u32; WORDS] { - let mut d = [0; WORDS]; - d[wgsl::COLUMN_LOW as usize] = wgsl::NONE; - if column.words == Words::Bits { - d[wgsl::COLUMN_FLAGS as usize] = wgsl::BITS; - } + let mut d = descriptor(if column.words == Words::Bits { wgsl::BITS } else { 0 }); if let Rows::Held(drawing) = rows && let Some(shapes) = source.paged(&column.name).filter(|_| column.derived) { d[wgsl::COLUMN_ROWS as usize] = drawing.live(); - let packing = Packing { - order: drawing.order.id(), - origin: source.data.origin(), - table: source.table.name.clone(), - field: column.name.clone(), - words: Words::Path, - low: false, - reads: 0, - }; + let packing = packing(drawing, source, &source.table.name, column.name.clone(), Words::Path); self.shaped(shapes, drawing, &packing); return d; } - let Some((data, table)) = table_of(column, source, inputs.all) else { + let Some((data, table)) = source.table_of(column, inputs.all) else { self.pages = None; return d; }; @@ -752,15 +775,7 @@ impl<'g> Builder<'g> { let field = table.field_named(&column.name); if let Rows::Held(drawing) = rows { d[wgsl::COLUMN_ROWS as usize] = drawing.live(); - let packing = Packing { - order: drawing.order.id(), - origin: source.data.origin(), - table: table.name.clone(), - field: column.name.clone(), - words: column.words, - low: false, - reads: 0, - }; + let packing = packing(drawing, source, &table.name, column.name.clone(), column.words); self.held(column, data, field, drawing, packing, inputs, &mut d); return d; } @@ -800,18 +815,12 @@ impl<'g> Builder<'g> { } } Some(Field::Sampled(samples, _)) if column.trace => { - self.pages = None; - d[wgsl::COLUMN_FLAGS as usize] |= wgsl::SAMPLED; let blocks: Vec<(Samples, usize)> = (0..table.len().div_ceil(wgsl::PAGE_ROWS as usize)).map(|b| (samples.clone(), b)).collect(); - self.trace(samples, &blocks, plan(samples, inputs.window)); + self.trace(&mut d, samples, &blocks, inputs.window); } Some(Field::Sampled(samples, _)) => { - self.pages = None; - d[wgsl::COLUMN_FLAGS as usize] |= wgsl::SAMPLED | if column.step { wgsl::STEP } else { 0 }; - let frame = samples.frame_at(inputs.at); - let t = if column.step { 0.0 } else { frame.map_or(0.0, |f| samples.t(f, inputs.at)) }; - d[wgsl::COLUMN_T as usize] = t.to_bits(); + let frame = self.sampled(&mut d, column, samples, inputs.at); self.cursors(samples, frame, table.len()); } None => self.pages = None, @@ -819,6 +828,16 @@ impl<'g> Builder<'g> { d } + /// Sets descriptor `d` to read sampled `column` at axis coordinate `at`, through a cursor per + /// page of rows. Returns the frame shown there. + fn sampled(&mut self, d: &mut [u32; WORDS], column: &ColumnUse, samples: &Samples, at: f64) -> Option { + self.pages = None; + d[wgsl::COLUMN_FLAGS as usize] |= wgsl::SAMPLED | if column.step { wgsl::STEP } else { 0 }; + let (frame, t) = shown_frame(samples, column, at); + d[wgsl::COLUMN_T as usize] = t.to_bits(); + frame + } + /// A paged table's column: one page packed in the table's order per slot, cached under /// `packing`. #[allow(clippy::too_many_arguments, reason = "one column's pages in its order")] @@ -863,8 +882,6 @@ impl<'g> Builder<'g> { }); } Some(Field::Sampled(samples, version)) if column.trace => { - self.pages = None; - d[wgsl::COLUMN_FLAGS as usize] |= wgsl::SAMPLED; let packing = Packing { reads: stamp_of(version), ..packing }; let blocks: Vec<(Samples, usize)> = (0..slots) .map(|s| match slot_key(&packing, drawing, s) { @@ -874,18 +891,13 @@ impl<'g> Builder<'g> { } }) .collect(); - self.trace(samples, &blocks, plan(samples, inputs.window)); + self.trace(d, samples, &blocks, inputs.window); } Some(Field::Sampled(samples, version)) => { - self.pages = None; - d[wgsl::COLUMN_FLAGS as usize] |= wgsl::SAMPLED | if column.step { wgsl::STEP } else { 0 }; - let frame = samples.frame_at(inputs.at); - let t = if column.step { 0.0 } else { frame.map_or(0.0, |f| samples.t(f, inputs.at)) }; - d[wgsl::COLUMN_T as usize] = t.to_bits(); - // Six words per slot, laid out as `cursors` writes them. + let frame = self.sampled(d, column, samples, inputs.at); + // A cursor per slot, reading the frame packed in the slot's rows. for s in 0..slots { - let at = self.words.len(); - self.words.extend([wgsl::NONE, 0, wgsl::NONE, 0, 0, 0]); + let at = self.cursor_words(); let Some((base, page)) = slot_key(&packing, drawing, s) else { continue }; for (k, f) in [frame, frame.map(|f| f + 1)].into_iter().enumerate() { let Some(f) = f.filter(|&f| samples.held(0, f)) else { continue }; @@ -898,8 +910,7 @@ impl<'g> Builder<'g> { counters.add(Count::Packed, rows.len() as u64); Content::Computed(packed::frame(samples, &rows, Some(f))) }; - self.place(at + 2 * k, key, wgsl::PAGE_ROWS, content); - self.words[at + 4 + k] = wgsl::PAGE_ROWS; + self.cursor(at, k, key, 0, wgsl::PAGE_ROWS, content); } } } @@ -960,22 +971,15 @@ impl<'g> Builder<'g> { all: &[Snapshot], ) -> [u32; WORDS] { self.pages = None; - let mut d = [0; WORDS]; - d[wgsl::COLUMN_FLAGS as usize] = wgsl::BITS; + let mut d = descriptor(wgsl::BITS); d[wgsl::COLUMN_ROWS as usize] = source.table.len() as u32; - d[wgsl::COLUMN_LOW as usize] = wgsl::NONE; - d[wgsl::COLUMN_JOIN as usize] = wgsl::NONE; if let Rows::Held(drawing) = rows { d[wgsl::COLUMN_ROWS as usize] = drawing.live(); let Some(other) = joined(from, all) else { return d }; + let field = format!("{}/{}", from.0, from.1).into(); let packing = Packing { - order: drawing.order.id(), - origin: source.data.origin(), - table: source.table.name.clone(), - field: format!("{}/{}", from.0, from.1).into(), - words: Words::Bits, - low: false, reads: stamp_of((source.table.structure, other.structure, other.order)), + ..packing(drawing, source, &source.table.name, field, Words::Bits) }; self.held_pages(drawing, &packing, None, |_, rows| packed::joined(source.table, rows, other)); return d; @@ -1009,12 +1013,10 @@ impl<'g> Builder<'g> { /// A paged table's geometry column: one page of points per slot, cached under `packing`. fn held_path(&mut self, column: &Column, drawing: &Drawing, packing: &Packing) { - let per = wgsl::PATH_PAGE_WORDS as usize; - self.words.resize(per * drawing.slot_count(), 0); + self.words.resize(wgsl::PATH_PAGE_WORDS as usize * drawing.slot_count(), 0); for s in 0..drawing.slot_count() { let Some(id) = slot_page(&mut self.gpu.packed, packing, drawing, Some(column), s) else { - self.words[per * s..per * (s + 1)].copy_from_slice(&[wgsl::NONE, 0, 0, 0]); - self.list(None); + self.no_path_page(s); continue; }; self.list(Some(id)); @@ -1032,8 +1034,7 @@ impl<'g> Builder<'g> { /// page, and of its kept rows individually, in pages cached under `packing` by their contents. /// A page draws nothing until its shapes are made. fn shaped(&mut self, shapes: &PagedShapes, drawing: &Drawing, packing: &Packing) { - let per = wgsl::PATH_PAGE_WORDS as usize; - self.words.resize(per * drawing.slot_count(), 0); + self.words.resize(wgsl::PATH_PAGE_WORDS as usize * drawing.slot_count(), 0); let alike = shapes.order == drawing.order.id(); for s in 0..drawing.slot_count() { let page: Option<(PageId, std::borrow::Cow<'_, Values>)> = match drawing.holds(s) { @@ -1042,23 +1043,17 @@ impl<'g> Builder<'g> { } Holds::Kept(_) if alike => { let places = drawing.places(s); - let mut kept = GeometryBuilder::new(shapes.depth); - for d in &places { - match shapes.kept.get(d).and_then(Values::geometry) { - Some(shape) => kept.push_from(shape, 0), - None => kept.push_empty(), - } - } + let kept = places.iter().map(|d| Some((shapes.kept.get(d).and_then(Values::geometry)?, 0))); + let kept = Values::of_rows(shapes.depth, kept); let packing = Packing { order: stamp_of((packing.order, places, shapes.version)), ..packing.clone() }; let id = self.gpu.packed.page(&packing, &drawing.order, None, 0); - Some((id, std::borrow::Cow::Owned(Values::of_geometry(kept.finish())))) + Some((id, std::borrow::Cow::Owned(kept))) } _ => None, }; let Some((id, shaped)) = page else { - self.words[per * s..per * (s + 1)].copy_from_slice(&[wgsl::NONE, 0, 0, 0]); - self.list(None); + self.no_path_page(s); continue; }; self.list(Some(id)); @@ -1072,6 +1067,13 @@ impl<'g> Builder<'g> { } } + /// Page `p` of a geometry column's table, holding no rows: no row table and no points. + fn no_path_page(&mut self, p: usize) { + let per = wgsl::PATH_PAGE_WORDS as usize; + self.words[per * p..per * (p + 1)].copy_from_slice(&[wgsl::NONE, 0, 0, 0]); + self.list(None); + } + /// Page `p` (`id`, with `rows` rows) of a geometry column's table: its row table, then its /// pieces of points, each keyed by its contents, so a rewritten page uploads only the pieces /// whose points changed. `pieces` builds them when the GPU lacks one, and `points` counts the @@ -1122,8 +1124,11 @@ impl<'g> Builder<'g> { /// entry, words per entry). /// /// Offsets within the table are relative to it. `blocks` gives each page of rows the samples it - /// reads and its block among them. - fn trace(&mut self, axis: &Samples, blocks: &[(Samples, usize)], plan: Plan) { + /// reads and its block among them. Descriptor `d` reads it as sampled, across `window`. + fn trace(&mut self, d: &mut [u32; WORDS], axis: &Samples, blocks: &[(Samples, usize)], window: Window) { + self.pages = None; + d[wgsl::COLUMN_FLAGS as usize] |= wgsl::SAMPLED; + let plan = plan(axis, window); // Each run has its own origin, so runs billions of frames apart still get 32-bit GPU // offsets. CPU plans keep absolute 64-bit indexes. self.words.extend([plan.runs.len() as u32, blocks.len() as u32, plan.runs.len() as u32, plan.points]); @@ -1174,22 +1179,34 @@ impl<'g> Builder<'g> { } } - /// Six words per page of rows: this frame's block and word, the next frame's, and their row - /// counts. + /// A cursor per page of `rows` rows, at `frame` and the frame after it. fn cursors(&mut self, samples: &Samples, frame: Option, rows: usize) { for block in 0..rows.div_ceil(wgsl::PAGE_ROWS as usize) { - let at = self.words.len(); - self.words.extend([wgsl::NONE, 0, wgsl::NONE, 0, 0, 0]); - for (i, f) in [frame, frame.map(|f| f + 1)].into_iter().enumerate() { + let at = self.cursor_words(); + for (k, f) in [frame, frame.map(|f| f + 1)].into_iter().enumerate() { let Some((page, base)) = f.and_then(|f| samples.cursor(block, f)) else { continue }; let key = PageKey::of(page.id, Words::Values, false); - let rows_in_page = page.per; - self.place(at + 2 * i, key, base + rows_in_page, || Content::Frames(page.clone())); - self.words[at + 2 * i + 1] = base; - self.words[at + 4 + i] = rows_in_page; + self.cursor(at, k, key, base, page.per, || Content::Frames(page.clone())); } } } + + /// Appends a cursor's six words, as `lib/read.wesl` reads them: the block and word of the + /// frame shown, the same for the next frame, and both frames' row counts. Each frame reads + /// nothing until [`cursor`](Builder::cursor) sets it. Returns where the words start. + fn cursor_words(&mut self) -> usize { + let at = self.words.len(); + self.words.extend([wgsl::NONE, 0, wgsl::NONE, 0, 0, 0]); + at + } + + /// Sets frame `k` (0 the frame shown, 1 the next) of the cursor at `at` to `rows` rows from word + /// `base` of page `key`, built by `content` when the arena lacks it. + fn cursor(&mut self, at: usize, k: usize, key: PageKey, base: u32, rows: u32, content: impl FnOnce() -> Content) { + self.place(at + 2 * k, key, base + rows, content); + self.words[at + 2 * k + 1] = base; + self.words[at + 4 + k] = rows; + } } /// The cache key for the page in slot `s` of `drawing`, for a column packed as `packing`: a page of diff --git a/crates/latkit/src/view/timer.rs b/crates/latkit/src/view/timer.rs index c78ab7ff..001179f7 100644 --- a/crates/latkit/src/view/timer.rs +++ b/crates/latkit/src/view/timer.rs @@ -100,9 +100,9 @@ impl Timer { /// Milliseconds between the two timestamps of `bytes`, ticks `period` nanoseconds long. fn elapsed_ms(bytes: &[u8], period: f64) -> Option { - let tick = |i: usize| Some(u64::from_le_bytes(bytes.get(i * 8..i * 8 + 8)?.try_into().ok()?)); - let ticks = tick(1)?.checked_sub(tick(0)?)?; - Some((ticks as f64 * period / 1e6) as f32) + let mut ticks = crate::wire::In::new(bytes); + let (start, end) = (ticks.u64()?, ticks.u64()?); + Some((end.checked_sub(start)? as f64 * period / 1e6) as f32) } /// Records an empty pass that writes timestamp `beginning` at its start, or `end` at its end. diff --git a/crates/latkit/src/view/transition.rs b/crates/latkit/src/view/transition.rs index 8329d61f..9b735a38 100644 --- a/crates/latkit/src/view/transition.rs +++ b/crates/latkit/src/view/transition.rs @@ -27,8 +27,7 @@ use super::marks::{Holding, Shown}; use super::words::Kept; use crate::data::{RowIds, Table}; use crate::gpu::Inner; -use crate::gpu::shaders::PAGE_ROWS; -use crate::gpu::shaders::{self as wgsl, Shader}; +use crate::gpu::shaders::{self as wgsl, PAGE_ROWS}; use crate::scene::mark::MarkKind; /// A running transition: its start on the view's clock and its length in seconds. @@ -261,9 +260,9 @@ impl Shown { } impl View { - /// Freezes every transition starting this frame and advances the running ones. Returns whether - /// any is running. - pub(super) fn transitions(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner) -> bool { + /// Starts every transition beginning this frame, which this frame's compute pass freezes, and + /// advances the running ones. + pub(super) fn transitions(&mut self, inner: &mut Inner) { // A drag moves rows at once. let seconds = if self.dragging() { 0.0 } else { self.ease().as_secs_f64() }; let mut starting: Vec = (0..self.marks.len()).filter(|&i| self.marks[i].begin.is_some()).collect(); @@ -281,13 +280,12 @@ impl View { }); } if !starting.is_empty() { - self.freeze(encoder, inner, &starting); + self.write_remap(&starting); let start = self.clock.now(); for &i in &starting { self.marks[i].transition = Some(Transition { start, seconds }); } } - let mut running = false; for (i, shown) in self.marks.iter_mut().enumerate() { let Some(transition) = &shown.transition else { continue }; // A transition beginning this frame runs through it, as a zero-length one must for its @@ -302,18 +300,17 @@ impl View { shown.tables.thaw(inner); } } else { - running = true; // Rows ease only when the clock moves; their channels evaluated once already. shown.easing = self.clock.moved(); } } - running } - /// Records each starting mark's shown state into its frozen words. The remap words give, per - /// mark: its state pages as the frame began, how many rows showed, each new row's old row when - /// rows moved, and where the vertices its endpoints reference moved, if they did. - fn freeze(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner, starting: &[usize]) { + /// Writes the remap words from which the compute pass freezes each starting mark's shown state + /// into its frozen words, per mark: its state pages as the frame began, how many rows showed, + /// each new row's old row when rows moved, and where the vertices its endpoints reference + /// moved, if they did. + fn write_remap(&mut self, starting: &[usize]) { let gpu = self.gpu.clone(); let (device, queue) = (&gpu.0.device, &gpu.0.queue); let mut words: Vec = Vec::new(); @@ -340,7 +337,7 @@ impl View { let shown = &mut self.marks[i]; let begin = shown.begin.take().expect("starting"); let before = shown.words.state_before().clone(); - let width = shown.stride() + shown.extras(); + let width = shown.width(); // If the row width changed, there is nothing to ease from: every row enters, fading in. let sources = if before.width == width { begin.sources } else { Some(vec![wgsl::NONE; shown.held() as usize]) }; @@ -378,21 +375,6 @@ impl View { } let resources = self.resources.as_mut().expect("created before transitions"); resources.write_remap(device, queue, &words); - inner.arena.clear(device, queue); - inner.arena.written(); - let groups = resources.groups(&gpu.0, inner); - let group = resources.pass(device, inner, Shader::Freeze); - let pipeline = inner.pipelines.compute(device, Shader::Freeze, "gather"); - // The mark uniforms this frame writes before submitting hold each mark's remap offset. - let mut pass = encoder - .begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("latkit freeze"), timestamp_writes: None }); - pass.set_pipeline(&pipeline); - pass.set_bind_group(0, &groups.view, &[]); - pass.set_bind_group(2, &group, &[]); - for &i in starting { - pass.set_bind_group(1, &groups.mark, &[super::passes::offset(i)]); - super::passes::dispatch(&mut pass, self.marks[i].held()); - } } /// Allocates frozen and settled words for every held row of each mark whose transition starts @@ -404,12 +386,11 @@ impl View { for shown in &mut self.marks { let easing = shown.begin.is_some() || shown.transition.is_some(); let rows = shown.page_rows(); - let width = if easing { shown.stride() + shown.extras() } else { 0 }; + let width = if easing { shown.width() } else { 0 }; let mut fits = true; for kept in [Kept::Frozen, Kept::Settled] { - let had = shown.words.pages(kept).blocks.len(); match shown.words.fit(kept, &rows, width, device, &mut inner.arena) { - Some(new) => shown.packed &= new.is_empty() && had == shown.words.pages(kept).blocks.len(), + Some(fitted) => shown.packed &= !fitted.changed, None => fits = false, } } diff --git a/crates/latkit/src/view/uniforms.rs b/crates/latkit/src/view/uniforms.rs index 0a32ce73..c6172f4d 100644 --- a/crates/latkit/src/view/uniforms.rs +++ b/crates/latkit/src/view/uniforms.rs @@ -175,13 +175,7 @@ pub(crate) fn mark( ) -> wgsl::Mark { let mut mark: wgsl::Mark = bytemuck::Zeroable::zeroed(); mark.index = index as u32; - mark.kind = match shown.kind() { - MarkKind::Vertices => wgsl::MARK_VERTICES, - MarkKind::Edges => wgsl::MARK_EDGES, - MarkKind::Faces => wgsl::MARK_FACES, - MarkKind::Lines => wgsl::MARK_LINES, - MarkKind::Areas => wgsl::MARK_AREAS, - }; + mark.kind = shown.kind().code(); mark.rows = shown.rows; mark.exits = shown.exits; mark.held = shown.held(); @@ -230,7 +224,7 @@ pub(crate) fn mark( if let Some(compiled) = &shown.compiled { let program = &compiled.program; let drawing = &compiled.drawing; - mark.primitive = drawing.primitive.code(); + mark.primitive = shown.kind().primitive().code(); mark.channels = program.channels.len() as u32; mark.reductions = program.domains.len() as u32; mark.units = program.units; diff --git a/crates/latkit/src/view/variants.rs b/crates/latkit/src/view/variants.rs index b199ec1e..425da979 100644 --- a/crates/latkit/src/view/variants.rs +++ b/crates/latkit/src/view/variants.rs @@ -12,20 +12,152 @@ //! use. Pipelines the scene never uses are never requested. A picture takes no input: it requests //! everything it draws with, text, transitions, and lit rows at draw urgency, and draws once all of //! them are ready. +//! +//! Each pipeline is named once, here: the passes that serve every mark alike as constants, and a +//! mark's own through [`MarkVariants`]. The view requests and runs them by these same names, so it +//! runs only what it requested. + +use std::sync::Arc; use super::View; +use super::marks::Shown; use crate::gpu::Inner; +use crate::gpu::atlas::FIELDS; use crate::gpu::pipelines::{self, Asked, Urgency, Variant}; use crate::gpu::shaders::{PATH_ENDS, PATH_GEOMETRY, Shader}; +use crate::scene::mark::MarkKind; use crate::{Mark, Marker, Scene, Space}; -/// The passes of the selection that caps a mark's label count. -const SELECT: [&str; 4] = ["select_start", "select_count", "select_find", "select_cut"]; +/// Writes the word of each drawn row of a paged mark from its runs. +pub(super) const EXPAND_RUNS: Variant = Variant::compute(Shader::Lists, "expand_runs"); + +/// The scan of a segment list's counts into each row's first instance. +pub(super) const SCAN: [Variant; 3] = [ + Variant::compute(Shader::Lists, "scan_blocks"), + Variant::compute(Shader::Lists, "scan_sums"), + Variant::compute(Shader::Lists, "add_sums"), +]; + +/// Eases a mark in transition from its frozen words toward its settled ones. +pub(super) const EASE: Variant = Variant::compute(Shader::Evaluate, "ease"); + +/// Moves comets along their rows at this frame's flow. +pub(super) const TRAVEL: Variant = Variant::compute(Shader::Evaluate, "travel"); + +/// Carries comets' travel over to the rows a transition moved. +pub(super) const CARRY: Variant = Variant::compute(Shader::Evaluate, "carry"); + +/// Freezes the words a transition eases from. +pub(super) const GATHER: Variant = Variant::compute(Shader::Freeze, "gather"); -/// The passes that place each labeled mark's labels once its candidates are measured. A diagram's -/// labels need no placement. +/// Sums each face's normal into its corners. +pub(super) const NORMALS: Variant = Variant::compute(Shader::Normals, "accumulate"); + +/// Clears the cells labels claim, then resets them between placement rounds. +pub(super) const CELLS: [Variant; 2] = + [Variant::compute(Shader::Labels, "clear_cells"), Variant::compute(Shader::Labels, "reset_cells")]; + +/// The selection that caps a mark's label count. +pub(super) const SELECT: [Variant; 4] = [ + Variant::compute(Shader::Labels, "select_start"), + Variant::compute(Shader::Labels, "select_count"), + Variant::compute(Shader::Labels, "select_find"), + Variant::compute(Shader::Labels, "select_cut"), +]; + +/// The passes that place each labeled mark's labels once its candidates are measured, for +/// [`MarkVariants::placing`]. A diagram's labels need no placement. pub(super) const PLACING: [&str; 5] = ["claim", "decide", "occupy", "ask", "fade"]; +/// What a mark's pipelines are specialized for: its kind, the `PATH_` code its rows follow, and a +/// custom marker's WGSL. +#[derive(Clone, Copy)] +pub(super) struct MarkVariants<'a> { + kind: MarkKind, + path: u32, + custom: Option<&'a Arc>, +} + +impl<'a> MarkVariants<'a> { + /// The variants `mark` uses in `space`, requested before it compiles. + fn of(mark: &'a Mark, space: &Space) -> MarkVariants<'a> { + let custom = match mark { + Mark::Vertices(vertices) => match &vertices.marker { + Marker::Wgsl { source, .. } => Some(source), + _ => None, + }, + _ => None, + }; + MarkVariants { kind: mark.kind(), path: mark.path(space), custom } + } + + /// The variants `shown` runs. + pub(super) fn shown(shown: &'a Shown) -> MarkVariants<'a> { + let custom = shown.compiled.as_ref().and_then(|c| c.drawing.markers.custom.as_ref()); + MarkVariants { kind: shown.kind(), path: shown.path(), custom } + } + + fn primitive(self) -> u32 { + self.kind.primitive().code() + } + + /// The shader drawing its rows, or with `lit`, the rows its focus lights; `None` for a face's + /// lit rows. + pub(super) fn shader(self, lit: bool) -> Option { + let (every, focus) = self.kind.shaders(self.custom.is_some()); + if lit { focus } else { Some(every) } + } + + /// The pipeline drawing its rows, or with `lit`, the rows its focus lights. A custom marker's + /// WGSL compiles together with its shader's. + pub(super) fn draw(self, lit: bool, format: wgpu::TextureFormat, samples: u32) -> Option { + let variant = Variant::draw(self.shader(lit)?, format, samples).path(self.path); + Some(match self.custom { + Some(source) => Asked::Custom(source.clone(), variant), + None => Asked::Shader(variant), + }) + } + + /// The pass measuring its rows' extents for a fit; `None` for edges and faces. + pub(super) fn bounds(self) -> Option { + Some(Variant::compute(Shader::Bounds, self.kind.bounds()?).path(self.path)) + } + + /// Whether a list may number its instances, for rows whose counts differ: edges between ends, + /// which may pass through bends, follow great circles, or bow into lanes; and every path + /// through geometry, including areas' triangles. + fn listed(self) -> bool { + self.path == PATH_GEOMETRY || (self.kind == MarkKind::Edges && self.path == PATH_ENDS) + } + + /// The passes counting each row's instances into its list, then writing them, around a + /// [`SCAN`]. + pub(super) fn segments(self) -> [Variant; 2] { + ["count_segments", "write_segments"].map(|entry| Variant::compute(Shader::Lists, entry).path(self.path)) + } + + /// Its program for the probe `shader`, which answers hover, queries, and the depth under the + /// pointer for its geometry. + pub(super) fn probe(self, shader: Shader) -> Variant { + Variant::probe(shader, self.primitive()).path(self.path) + } + + /// The pass measuring its label candidates at their anchors. + pub(super) fn candidates(self) -> Variant { + Variant::compute(Shader::Candidates, "candidates").path(self.path).primitive(self.primitive()) + } + + /// The placement pass `entry`, one of [`PLACING`]. + pub(super) fn placing(self, entry: &'static str) -> Variant { + Variant::compute(Shader::Labels, entry).path(self.path) + } + + /// The pipeline drawing its labels' glyphs. + pub(super) fn glyphs(self, format: wgpu::TextureFormat, samples: u32) -> Variant { + Variant::draw(Shader::Glyphs, format, samples).path(self.path).primitive(self.primitive()) + } +} + /// The pipelines a view requested for frames into one target, with or without input, by tier: draw, /// text, and answer (transitions, lit rows, probes, and queries). pub(super) struct Asking { @@ -36,29 +168,29 @@ pub(super) struct Asking { answering: Vec, } -/// The pipelines nearly every scene draws with, which the GPU compiles ahead for later views, in -/// this order: channels, strokes between endpoints and through geometry, points and the backdrop, -/// their bounds and lists, ports, then text. +/// The pipelines nearly every scene draws with, which the GPU compiles ahead for later views: +/// channels, edges, lines along paths, vertices and their labels, and the backdrop, ports, and +/// overlay. #[cfg(all(feature = "web", target_arch = "wasm32"))] pub(crate) fn common(format: wgpu::TextureFormat, samples: u32) -> Vec { - let draw = |shader| Variant::draw(shader, format, samples); - let mut needs = vec![ - Variant::channels(pipelines::Reads { joined: false, timed: false, viewed: false }), - draw(Shader::Strokes).path(PATH_ENDS), - draw(Shader::Strokes).path(PATH_GEOMETRY), - draw(Shader::Points), - draw(Shader::Backdrop), - Variant::compute(Shader::Bounds, "points"), - ]; - for path in [PATH_ENDS, PATH_GEOMETRY] { - needs.push(Variant::compute(Shader::Bounds, "strokes").path(path)); - needs.extend(["count_segments", "write_segments"].map(|e| Variant::compute(Shader::Lists, e).path(path))); + let draw = |shader| Asked::Shader(Variant::draw(shader, format, samples)); + let marks = [(MarkKind::Edges, PATH_ENDS), (MarkKind::Lines, PATH_GEOMETRY), (MarkKind::Vertices, PATH_ENDS)] + .map(|(kind, path)| MarkVariants { kind, path, custom: None }); + let mut needs = vec![Asked::Shader(Variant::channels(pipelines::Reads::NONE))]; + needs.extend(marks.iter().filter_map(|m| m.draw(false, format, samples))); + needs.push(draw(Shader::Backdrop)); + for m in &marks { + needs.extend(m.bounds().map(Asked::Shader)); + if m.listed() { + needs.extend(m.segments().map(Asked::Shader)); + } } - needs.extend(["scan_blocks", "scan_sums", "add_sums"].map(|e| Variant::compute(Shader::Lists, e))); - needs.extend([draw(Shader::PointsPorts), draw(Shader::Overlay), draw(Shader::Glyphs), draw(Shader::GlyphsPorts)]); - needs.push(Variant::compute(Shader::Candidates, "candidates")); - needs.extend(["columns", "rows"].map(|e| Variant::compute(Shader::Fields, e))); - needs.into_iter().map(Asked::Shader).collect() + needs.extend(SCAN.map(Asked::Shader)); + needs.extend([draw(Shader::PointsPorts), draw(Shader::Overlay), draw(Shader::GlyphsPorts)]); + let vertices = marks[2]; + needs.extend([vertices.glyphs(format, samples), vertices.candidates()].map(Asked::Shader)); + needs.extend(FIELDS.map(Asked::Shader)); + needs } impl View { @@ -145,44 +277,42 @@ struct Stages { /// The pipelines frames of `scene` into `format` at `samples` per pixel use, except each mark's /// channel pipeline, which depends on what its channels read. fn stages(scene: &Scene, format: wgpu::TextureFormat, samples: u32) -> Stages { - let any = |test: fn(&Mark) -> bool| scene.marks.iter().any(test); + let any = |test: &dyn Fn(&Mark) -> bool| scene.marks.iter().any(test); let draw = |shader| Asked::Shader(Variant::draw(shader, format, samples)); - let compute = |shader, entry| Asked::Shader(Variant::compute(shader, entry)); let mut stages = Stages { draw: vec![draw(Shader::Backdrop)], - changes: vec![compute(Shader::Evaluate, "ease"), compute(Shader::Freeze, "gather")], + changes: vec![Asked::Shader(EASE), Asked::Shader(GATHER)], ..Stages::default() }; let controls = scene.controls; - let diagram = matches!(scene.space, Space::Diagram(_)); // The overlay draws a plot's axes, the title, the legends, and the selection box and connection // line that gestures draw. - let legends = any(|m| m.style().legend.is_some() && m.style().color.is_some()); + let legends = any(&|m| m.style().legend.is_some() && m.style().color.is_some()); let written = matches!(scene.space, Space::Plot(_)) || scene.title.is_some() || scene.subtitle.is_some() || legends; if written || controls.select_box || controls.connect { stages.text.push(draw(Shader::Overlay)); } // The distance fields of text a frame sets are built before the frame draws. - let labeled = any(|m| { + let labeled = any(&|m| { m.style().label.is_some() || matches!(m, Mark::Vertices(v) if v.ports.iter().any(|(_, port)| port.label.is_some())) }); if written || labeled { - stages.text.extend(["columns", "rows"].map(|e| compute(Shader::Fields, e))); + stages.text.extend(FIELDS.map(Asked::Shader)); } - if any(|m| m.style().label.is_some()) && !diagram { - stages.text.extend([compute(Shader::Labels, "clear_cells"), compute(Shader::Labels, "reset_cells")]); + if any(&|m| m.style().label.is_some()) && !matches!(scene.space, Space::Diagram(_)) { + stages.text.extend(CELLS.map(Asked::Shader)); } - if any(Mark::flows) { - stages.draw.push(compute(Shader::Evaluate, "travel")); - stages.changes.push(compute(Shader::Evaluate, "carry")); + if any(&Mark::flows) { + stages.draw.push(Asked::Shader(TRAVEL)); + stages.changes.push(Asked::Shader(CARRY)); } - if any(|m| matches!(m, Mark::Edges(_) | Mark::Lines(_) | Mark::Areas(_))) { - stages.draw.extend(["scan_blocks", "scan_sums", "add_sums"].map(|e| compute(Shader::Lists, e))); + if any(&|m| MarkVariants::of(m, &scene.space).listed()) { + stages.draw.extend(SCAN.map(Asked::Shader)); } // Any table may become paged: its data can outgrow the budget, or a mark can give a priority. if !scene.marks.is_empty() { - stages.draw.push(compute(Shader::Lists, "expand_runs")); + stages.draw.push(Asked::Shader(EXPAND_RUNS)); } for mark in &scene.marks { mark_stages(mark, scene, format, samples, &mut stages); @@ -194,69 +324,54 @@ fn stages(scene: &Scene, format: wgpu::TextureFormat, samples: u32) -> Stages { /// or an area's triangle geometry; its lit-row draw, for the rows that hover, selection, and edge /// endpoints light; its probe; and its labels. fn mark_stages(mark: &Mark, scene: &Scene, format: wgpu::TextureFormat, samples: u32, stages: &mut Stages) { - let draw = |shader| Variant::draw(shader, format, samples); - let compute = |shader, entry| Asked::Shader(Variant::compute(shader, entry)); - let style = mark.style(); - let primitive = mark.kind().primitive().code(); - let path = mark.path(&scene.space).unwrap_or(PATH_ENDS); - let (shader, lit) = match mark { - Mark::Vertices(v) => { - if let Marker::Wgsl { source, .. } = &v.marker { - // A custom marker's WGSL compiles together with its shader's. - let custom = - |shader, format, samples| Asked::Custom(source.clone(), Variant::draw(shader, format, samples)); - stages.draw.push(custom(Shader::PointsCustom, format, samples)); - stages.lit.push(custom(Shader::PointsCustomFocus, format, samples)); - (None, None) - } else { - (Some(Shader::Points), Some(Shader::PointsFocus)) - } - } - Mark::Faces(_) => (Some(Shader::Triangles), None), - Mark::Areas(_) => (Some(Shader::Areas), Some(Shader::AreasFocus)), - Mark::Edges(_) | Mark::Lines(_) => (Some(Shader::Strokes), Some(Shader::StrokesFocus)), - }; - stages.draw.extend(shader.map(|s| Asked::Shader(draw(s).path(path)))); - stages.lit.extend(lit.map(|s| Asked::Shader(draw(s).path(path)))); - let bounds = match mark { - Mark::Vertices(_) => "points", - Mark::Areas(_) => "areas", - _ => "strokes", - }; - stages.draw.push(Asked::Shader(Variant::compute(Shader::Bounds, bounds).path(path))); - // A list numbers rows with differing segment counts, and areas with differing triangle counts. - if matches!(mark, Mark::Edges(_)) || path == PATH_GEOMETRY { - stages.draw.extend( - ["count_segments", "write_segments"].map(|e| Asked::Shader(Variant::compute(Shader::Lists, e).path(path))), - ); + let variants = MarkVariants::of(mark, &scene.space); + stages.draw.extend(variants.draw(false, format, samples)); + stages.lit.extend(variants.draw(true, format, samples)); + stages.draw.extend(variants.bounds().map(Asked::Shader)); + if variants.listed() { + stages.draw.extend(variants.segments().map(Asked::Shader)); } // One probe program answers hover, queries, and the depth under the pointer for the mark's // geometry. Every mark gets one, so any drawn row can be located, pickable or not. - stages.input.push(Asked::Shader(Variant::probe(pipelines::probe(samples), primitive).path(path))); - if style.label.is_some() { - // Measuring label candidates and drawing glyphs are built for the mark's primitive. A - // diagram's labels need no placement. - stages.text.push(Asked::Shader(draw(Shader::Glyphs).path(path).primitive(primitive))); - stages - .text - .push(Asked::Shader(Variant::compute(Shader::Candidates, "candidates").path(path).primitive(primitive))); + stages.input.push(Asked::Shader(variants.probe(pipelines::probe(samples)))); + if let Some(label) = &mark.style().label { + stages.text.extend([variants.glyphs(format, samples), variants.candidates()].map(Asked::Shader)); if !matches!(scene.space, Space::Diagram(_)) { - stages.text.extend(PLACING.map(|e| Asked::Shader(Variant::compute(Shader::Labels, e).path(path)))); - } - if style.label.as_ref().is_some_and(|l| l.max.is_some()) && !matches!(scene.space, Space::Diagram(_)) { - stages.text.extend(SELECT.map(|e| compute(Shader::Labels, e))); + stages.text.extend(PLACING.map(|entry| Asked::Shader(variants.placing(entry)))); + if label.max.is_some() { + stages.text.extend(SELECT.map(Asked::Shader)); + } } } match mark { - Mark::Faces(_) => stages.draw.push(compute(Shader::Normals, "accumulate")), + Mark::Faces(_) => stages.draw.push(Asked::Shader(NORMALS)), Mark::Vertices(vertices) => { if !vertices.ports.is_empty() { - stages.draw.push(Asked::Shader(draw(Shader::PointsPorts))); + stages.draw.push(Asked::Shader(Variant::draw(Shader::PointsPorts, format, samples))); } if vertices.ports.iter().any(|(_, port)| port.label.is_some()) { - stages.text.push(Asked::Shader(draw(Shader::GlyphsPorts))); + stages.text.push(Asked::Shader(Variant::draw(Shader::GlyphsPorts, format, samples))); } } Mark::Edges(_) | Mark::Lines(_) | Mark::Areas(_) => {} } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Edges, Lines, Plane, Vertices}; + + #[test] + fn only_marks_that_frame_a_fit_ask_for_a_bounds_pass() { + let scene = Scene::new(Plane::default()) + .draw(Vertices::of("g", "V")) + .draw(Edges::of("g", "E").ends("from", "to")) + .draw(Lines::of("g", "L")); + let Stages { draw, .. } = stages(&scene, wgpu::TextureFormat::Rgba8Unorm, 1); + let asks = |entry, path| draw.contains(&Asked::Shader(Variant::compute(Shader::Bounds, entry).path(path))); + assert!(asks("points", PATH_ENDS), "vertices frame a fit"); + assert!(asks("strokes", PATH_GEOMETRY), "so do lines along their paths"); + assert!(!asks("strokes", PATH_ENDS), "edges lie between the vertices that frame them"); + } +} diff --git a/crates/latkit/src/view/words.rs b/crates/latkit/src/view/words.rs index c4be5f8d..5e608192 100644 --- a/crates/latkit/src/view/words.rs +++ b/crates/latkit/src/view/words.rs @@ -47,6 +47,13 @@ impl Default for Pages { } } +/// What fitting one kind's pages changed: the pages given new blocks, whose words start over, and +/// whether its page table changed at all. +pub(crate) struct Fitted { + pub(crate) new: Vec, + pub(crate) changed: bool, +} + /// A mark's pages of every kind. #[derive(Default)] pub(crate) struct Words { @@ -73,8 +80,8 @@ impl Words { /// Fits `kept`'s pages to hold `rows[p]` rows on page `p`, at `width` words per row; `width` 0 /// frees them all. A page whose block is too small gets a new block, as does every page when - /// the width changes. Blocks no longer used are freed. Returns the pages given new blocks, - /// whose words start over, or `None` (with nothing changed) when the arena has no room now. + /// the width changes. Blocks no longer used are freed. Returns what changed, or `None` (with + /// nothing changed) when the arena has no room now. pub(crate) fn fit( &mut self, kept: Kept, @@ -82,12 +89,12 @@ impl Words { width: u32, (device, queue): (&wgpu::Device, &Queue), arena: &mut Arena, - ) -> Option> { + ) -> Option { let rows = if width == 0 { &[][..] } else { rows }; let have = &self.pages[kept as usize]; let holds = |p: usize| have.width == width && have.words.get(p).is_some_and(|&w| w >= rows[p] * width); if have.blocks.len() == rows.len() && (0..rows.len()).all(holds) { - return Some(Vec::new()); + return Some(Fitted { new: Vec::new(), changed: false }); } let mut next = Pages { width, blocks: Arc::new([]), words: Vec::with_capacity(rows.len()) }; let mut blocks = Vec::with_capacity(rows.len()); @@ -120,7 +127,7 @@ impl Words { if kept == Kept::State && self.before.is_none() { self.before = Some(have); } - Some(new) + Some(Fitted { new, changed: true }) } /// Frees every page, as when the mark is removed. diff --git a/crates/latkit/src/web.rs b/crates/latkit/src/web.rs index bbda8263..d5932aff 100644 --- a/crates/latkit/src/web.rs +++ b/crates/latkit/src/web.rs @@ -574,6 +574,12 @@ impl Canvas { Target::Offscreen { .. } => None, } } + + /// The canvas's size in CSS pixels: its pixels over its pixels per CSS pixel. + pub fn css_size(&self) -> [f32; 2] { + let (size, ratio) = self.0.borrow().measure(); + size.map(|side| side as f32 / ratio) + } } impl Painted { diff --git a/crates/latkit/src/wire.rs b/crates/latkit/src/wire.rs new file mode 100644 index 00000000..c21df0d0 --- /dev/null +++ b/crates/latkit/src/wire.rs @@ -0,0 +1,159 @@ +//! Little-endian bytes: the encoding of latkit's byte format, of the jobs it hands its host, and of +//! the binary formats it reads. [`Out`] writes values; [`In`] reads them back, returning `None` +//! past the end. + +#[cfg(target_endian = "big")] +compile_error!("latkit's byte formats require a little-endian target"); + +/// Bytes being written. +#[derive(Default)] +pub(crate) struct Out(pub(crate) Vec); + +impl Out { + pub(crate) fn u8(&mut self, v: u8) { + self.0.push(v); + } + + pub(crate) fn u32(&mut self, v: u32) { + self.0.extend_from_slice(&v.to_le_bytes()); + } + + /// A count or a length, as a `u32`. + pub(crate) fn len(&mut self, n: usize) { + self.u32(u32::try_from(n).expect("counts fit in u32")); + } + + pub(crate) fn u64(&mut self, v: u64) { + self.0.extend_from_slice(&v.to_le_bytes()); + } + + pub(crate) fn f64(&mut self, v: f64) { + self.0.extend_from_slice(&v.to_le_bytes()); + } + + /// A text: its byte length, then its UTF-8. + pub(crate) fn str(&mut self, text: &str) { + self.len(text.len()); + self.0.extend_from_slice(text.as_bytes()); + } + + /// Plain values, as their bytes. + pub(crate) fn pod(&mut self, values: &[T]) { + self.0.extend_from_slice(bytemuck::cast_slice(values)); + } + + /// Bytes as they are. + pub(crate) fn bytes(&mut self, bytes: &[u8]) { + self.0.extend_from_slice(bytes); + } +} + +/// Gives a fieldless enum the `u32` code it is written as, `code`, and its reverse, `from_code`, +/// from one list of its variants and their codes. +#[cfg(feature = "gpu")] +macro_rules! codes { + ($type:ty { $($variant:ident = $code:literal),+ $(,)? }) => { + impl $type { + /// The code this is written as. + pub(crate) fn code(self) -> u32 { + match self { + $(Self::$variant => $code,)+ + } + } + + /// The value written as `code`; `None` for a code no value has. + pub(crate) fn from_code(code: u32) -> Option { + match code { + $($code => Some(Self::$variant),)+ + _ => None, + } + } + } + }; +} + +#[cfg(feature = "gpu")] +pub(crate) use codes; + +/// Bytes being read, front to back. +pub(crate) struct In<'a>(&'a [u8]); + +impl<'a> In<'a> { + pub(crate) fn new(bytes: &'a [u8]) -> In<'a> { + In(bytes) + } + + /// The bytes not read yet. + pub(crate) fn rest(&self) -> &'a [u8] { + self.0 + } + + pub(crate) fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Whether at least `n` bytes are left. + pub(crate) fn has(&self, n: usize) -> bool { + n <= self.0.len() + } + + pub(crate) fn bytes(&mut self, n: usize) -> Option<&'a [u8]> { + let (head, rest) = self.0.split_at_checked(n)?; + self.0 = rest; + Some(head) + } + + pub(crate) fn take(&mut self) -> Option<[u8; N]> { + let (head, rest) = self.0.split_first_chunk::()?; + self.0 = rest; + Some(*head) + } + + pub(crate) fn u8(&mut self) -> Option { + self.take().map(u8::from_le_bytes) + } + + pub(crate) fn u32(&mut self) -> Option { + self.take().map(u32::from_le_bytes) + } + + pub(crate) fn u64(&mut self) -> Option { + self.take().map(u64::from_le_bytes) + } + + pub(crate) fn f64(&mut self) -> Option { + self.take().map(f64::from_le_bytes) + } + + /// A text written by [`Out::str`]; `None` unless it is UTF-8. + pub(crate) fn str(&mut self) -> Option<&'a str> { + let n = self.u32()? as usize; + std::str::from_utf8(self.bytes(n)?).ok() + } + + /// `n` plain values, copied out of their bytes. + pub(crate) fn pod(&mut self, n: usize) -> Option> { + let bytes = self.bytes(n.checked_mul(size_of::())?)?; + let mut values = vec![T::zeroed(); n]; + bytemuck::cast_slice_mut::(&mut values).copy_from_slice(bytes); + Some(values) + } + + /// A varint: seven bits a byte, lowest first, as protocol buffers and PMTiles write them. + pub(crate) fn varint(&mut self) -> Option { + let mut value = 0_u64; + for shift in (0..64).step_by(7) { + let byte = self.u8()?; + value |= u64::from(byte & 0x7f) << shift; + if byte < 0x80 { + return Some(value); + } + } + None + } +} + +/// The words of `bytes`, ignoring a trailing partial word. +pub(crate) fn words(bytes: &[u8]) -> Vec { + bytes.as_chunks::<4>().0.iter().map(|&word| u32::from_le_bytes(word)).collect() +} diff --git a/crates/latkit/tests/areas.rs b/crates/latkit/tests/areas.rs index c519266e..222abe72 100644 --- a/crates/latkit/tests/areas.rs +++ b/crates/latkit/tests/areas.rs @@ -1,6 +1,7 @@ //! Areas: polygons filled through triangles the view derives, with holes left open. Areas pick //! where they cover, a fit frames them, and lines trace their rings. On the earth they stay whole -//! across the antimeridian, and polygons too large for one frame are triangulated in a job. +//! across the antimeridian. Polygons too large for one frame are triangulated in a job, and ease in +//! with each row's color when it lands. #![cfg(feature = "gpu")] @@ -70,6 +71,35 @@ fn areas_fill_their_polygons_and_leave_their_holes() { assert_eq!(errors(&mut view), Vec::new()); } +#[test] +fn areas_triangulated_in_a_job_ease_in_with_each_row_s_color() { + let Some(gpu) = gpu() else { return }; + // A page of unit squares, more than one frame triangulates, valued from 0 to 1 row by row. + let side = 64_u32; + let cells: Vec> = + (0..side * side).map(|i| vec![square(f64::from(i % side), f64::from(i / side), 1.0)]).collect(); + let values: Vec = (0..side * side).map(|i| f64::from(i) / f64::from(side * side - 1)).collect(); + let data = Data::new("map"); + data.apply(Table::new("Zone", Ids::count(side * side)).field("geometry", cells).field("v", values)).expect("valid"); + let color = latkit::field("v").colormap(latkit::Colormap::VIRIDIS).domain(0.0..=1.0); + let mut view = view(&gpu, Scene::new(plane()).draw(Areas::of("map", "Zone").color(color)), &data); + let middle = f64::from(side) / 2.0; + view.set_camera(Camera::Map { center: [middle, middle, 0.0], zoom: 0.75, pitch: 0.0, bearing: 0.0 }); + // The triangles landing start a transition; once it settles, each row shows its own color. + let pixels = frame(&gpu, &mut view); + let cell = |x: f64, y: f64| { + let [px, py] = view.screen([x + 0.5, y + 0.5, 0.0]).expect("in view"); + at(&pixels, px as u32, py as u32) + }; + // Viridis runs from dark purple through teal to yellow. + let far = f64::from(side - 3); + let [low, half, high] = [cell(2.0, 2.0), cell(2.0, middle), cell(far, far)]; + assert!(low[0] < 90 && low[1] < 20 && low[2] > 60, "0 is purple: {low:?}"); + assert!(half[1] > 120 && half[2] > 120 && half[0] < 60, "0.5 is teal: {half:?}"); + assert!(high[0] > 200 && high[1] > 200 && high[2] < 80, "1 is yellow: {high:?}"); + assert_eq!(errors(&mut view), Vec::new()); +} + #[test] fn pickable_areas_return_their_rows_and_a_fit_frames_them() { let Some(gpu) = gpu() else { return }; diff --git a/crates/latkit/tests/picks.rs b/crates/latkit/tests/picks.rs index 58b49494..ee27cd03 100644 --- a/crates/latkit/tests/picks.rs +++ b/crates/latkit/tests/picks.rs @@ -11,8 +11,8 @@ use std::task::{Context, Poll, Waker}; use common::{SIZE, camera, errors, frame, gpu, plain_theme, plane, step}; use latkit::{ - Axis, Button, Camera, Controls, Data, Device, Edges, Event, Ids, Input, Lines, Modifiers, Patch, Plot, Scene, - Table, Vertices, View, + Axis, Button, Camera, Controls, Data, Device, Edges, Event, Faces, Ids, Input, Lines, Modifiers, Patch, Plot, + Scene, Table, Vertices, View, field, }; /// Renders frames until `pending` resolves, within `SETTLE` frames. @@ -162,6 +162,42 @@ fn locate_finds_a_row_on_screen() { assert_eq!(answer(&gpu, &mut view, pending), None, "no such row"); } +#[test] +fn hidden_rows_have_no_screen_position() { + let Some(gpu) = gpu() else { return }; + let data = graph(); + let links = Table::new("Link", vec![0_u64, 1]) + .reference("from", "Node", vec![1_u64, 1]) + .reference("to", "Node", vec![3_u64, 2]) + .field("on", vec![false, true]); + let faces = Table::new("Face", vec![0_u64, 1]) + .reference("a", "Node", vec![1_u64, 1]) + .reference("b", "Node", vec![2_u64, 2]) + .reference("c", "Node", vec![3_u64, 3]) + .field("on", vec![false, true]); + data.apply(Patch::new().replace(links).set(faces)).expect("valid"); + let scene = Scene::new(plane()) + .theme(plain_theme()) + .draw(Faces::of("graph", "Face").corners("a", "b", "c").visible(field("on"))) + .draw(Edges::of("graph", "Link").ends("from", "to").width(3.0).visible(field("on"))) + .draw(Vertices::of("graph", "Node").position("x", "y").radius(4.0)); + let mut view = View::new(&gpu, scene).expect("valid scene"); + view.set_camera(camera()); + view.set_sources([&data]).expect("a source"); + frame(&gpu, &mut view); + let mut screen_of = |table: &str, id: u64| { + let pending = view.screen_of(&latkit::Row::new("graph", table, id)); + answer(&gpu, &mut view, pending) + }; + let c = SIZE as f32 / 2.0; + let link = screen_of("Link", 1).expect("a shown edge"); + assert!((link[0] - (c + 5.0)).abs() < 1.0 && (link[1] - c).abs() < 1.0, "the edge's middle: {link:?}"); + assert_eq!(screen_of("Link", 0), None, "a hidden edge"); + let face = screen_of("Face", 1).expect("a shown face"); + assert!((face[0] - (c - 10.0)).abs() < 1.0 && (face[1] - (c - 10.0)).abs() < 1.0, "the centroid: {face:?}"); + assert_eq!(screen_of("Face", 0), None, "a hidden face"); +} + #[test] fn a_shift_drag_selects_the_rows_in_its_box() { let Some(gpu) = gpu() else { return }; diff --git a/crates/latkit/tests/shots.rs b/crates/latkit/tests/shots.rs index 5447be9e..d6ef3bd2 100644 --- a/crates/latkit/tests/shots.rs +++ b/crates/latkit/tests/shots.rs @@ -9,7 +9,9 @@ mod common; use std::time::Duration; use common::{SIZE, at, camera, close, frame, gpu, plain_theme, plane, step}; -use latkit::{Color, Data, Ids, Image, Scene, Shot, Table, Theme, Vertices, View}; +use latkit::{ + Areas, Color, Data, Edges, Faces, Fit, Ids, Image, Label, Lines, Patch, Scene, Shot, Table, Theme, Vertices, View, +}; const RED: [u8; 4] = [255, 0, 0, 255]; const BLACK: [u8; 4] = [0, 0, 0, 255]; @@ -64,6 +66,37 @@ fn pictures_compile_only_the_pipelines_they_draw_with() { assert_eq!(gpu.work().late, 0, "no pipeline compiles at first use"); } +#[test] +fn every_kind_of_mark_draws_with_the_pipelines_it_requested() { + let Some(gpu) = gpu() else { return }; + let data = Data::new("graph"); + let square = vec![vec![[-60.0, -60.0], [-40.0, -60.0], [-40.0, -40.0], [-60.0, -40.0], [-60.0, -60.0]]]; + let nodes = Table::new("Node", vec![1_u64, 2, 3]) + .field("x", vec![-20.0, 20.0, 0.0]) + .field("y", vec![0.0, 0.0, 20.0]) + .field("name", vec!["a", "b", "c"]); + let link = + Table::new("Link", vec![1_u64]).reference("from", "Node", vec![1_u64]).reference("to", "Node", vec![2_u64]); + let face = Table::new("Face", vec![1_u64]) + .reference("a", "Node", vec![1_u64]) + .reference("b", "Node", vec![2_u64]) + .reference("c", "Node", vec![3_u64]); + let zone = Table::new("Zone", vec![1_u64]).field("geometry", vec![square]); + data.apply(Patch::new().set(nodes).set(link).set(face).set(zone)).expect("valid"); + let scene = Scene::new(plane()) + .theme(plain_theme()) + .draw(Areas::of("graph", "Zone")) + .draw(Lines::of("graph", "Zone")) + .draw(Faces::of("graph", "Face").corners("a", "b", "c")) + .draw(Edges::of("graph", "Link").ends("from", "to")) + .draw(Vertices::of("graph", "Node").position("x", "y").label(Label::new("name"))); + let mut view = view(&gpu, scene, &data); + // A fit measures every mark that frames it. + view.fit(Fit::All); + pollster::block_on(Shot::new([SIZE, SIZE]).image(&mut [&mut view])).expect("a picture"); + assert_eq!(gpu.work().late, 0, "every pipeline drawn with was requested, by the same name"); +} + #[test] fn a_scaled_picture_draws_the_same_view_sharper() { let Some(gpu) = gpu() else { return }; diff --git a/crates/latkit/tests/tiles.rs b/crates/latkit/tests/tiles.rs index 04784848..73094ce1 100644 --- a/crates/latkit/tests/tiles.rs +++ b/crates/latkit/tests/tiles.rs @@ -117,6 +117,20 @@ impl Host for Serving { } } +#[test] +fn an_index_that_is_not_tile_json_fails_as_invalid_input() { + let Some(gpu) = gpu() else { return }; + // The host serves empty bytes for anything outside `tiles/`. + gpu.set_host(Serving(Arc::new(Served::default()))); + let scene = Scene::new(Earth::default()).draw(Areas::of("base", "water")); + let mut view = View::new(&gpu, scene).expect("valid scene"); + view.set_sources([&Tiles::new("base", "index.json")]).expect("a source"); + frame(&gpu, &mut view); + let errors = errors(&mut view); + let [latkit::Error::Invalid { path, .. }] = errors.as_slice() else { panic!("one invalid input: {errors:?}") }; + assert_eq!(path.to_string(), "base", "named by the tiles' dataset"); +} + #[test] fn tiles_the_camera_needs_draw_as_a_dataset() { let Some(gpu) = gpu() else { return }; diff --git a/crates/latkit/tests/videos.rs b/crates/latkit/tests/videos.rs index cbe02acd..08c61a8b 100644 --- a/crates/latkit/tests/videos.rs +++ b/crates/latkit/tests/videos.rs @@ -143,15 +143,15 @@ fn labeled_marks_easing_between_frames_complete_every_frame() { } #[test] -fn a_video_needs_an_encoder_and_a_size_in_whole_blocks() { +fn a_video_needs_an_encoder_and_rounds_its_size_to_whole_blocks() { let Some(gpu) = gpu() else { return }; assert!(Shot::new([SIZE, SIZE]).video(&gpu, 30.0, Mp4::new(Vec::new())).is_err(), "a host without an encoder"); gpu.set_host(Keeping(Pictures::default())); assert!(Shot::new([SIZE, SIZE]).video(&gpu, 0.0, Mp4::new(Vec::new())).is_err(), "a frame rate above zero"); - assert!( - Shot::new([SIZE + 2, SIZE]).video(&gpu, 30.0, Mp4::new(Vec::new())).is_err(), - "a width of whole 4-pixel blocks" - ); + // The encoder checks it is given whole 4×2 blocks: SIZE by SIZE. + assert!(Shot::new([SIZE + 3, SIZE + 1]).video(&gpu, 30.0, Mp4::new(Vec::new())).is_ok()); + let error = Shot::new([3, SIZE]).video(&gpu, 30.0, Mp4::new(Vec::new())).map(drop).unwrap_err(); + assert_eq!(error.to_string(), "size: must be at least 4×2 pixels"); let mut view = view(&gpu); let mut two = Shot::new([SIZE, SIZE]).panels([[0.0, 0.0, 0.5, 1.0], [0.5, 0.0, 0.5, 1.0]]); let mut video = two.video(&gpu, 30.0, Mp4::new(Vec::new())).expect("made"); diff --git a/docs/cli.md b/docs/cli.md index dbdd0e96..55cd608a 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,46 +1,28 @@ # Command line -The `latkit` executable renders scenes to images, image sequences, raw frames, and video on the -machine's GPU, with the same renderer as the browser. Its inputs are latkit's JSON, so any language -can write them. +The `latkit` executable draws [scenes](guide.md#scenes) of datasets to images, image sequences, raw +frames, and video on the machine's GPU, with the same renderer as the browser. Its inputs are +latkit's JSON, so any language can write them: a dataset is a [JSON document](guide.md#json), +latkit's [bytes](guide.md#bytes), or GeoJSON named with `NAME=`. The +[command reference](reference/cli.md) documents every command and option. ```sh cargo install latkit-cli # or, from the repository: cargo build --locked -p latkit-cli --profile cli -latkit validate scene.json -d grid.json +latkit validate scene.json -d grid.json # check every input without a GPU latkit render scene.json -d grid.json -o grid.png --size 2400x1350 --scale 2 +latkit render scene.json -d grid.json --camera '{"center":[-97,31],"zoom":5,"pitch":0,"bearing":0}' -o tx.png latkit render map.json plot.json -d grid.json -d states=usa.geojson \ --panel 0,0,1,0.8 --panel 0,0.8,1,0.2 --frames frames.jsonl --fps 60 -o film.mp4 latkit render scene.json -d grid.json --frames - --format y4m -o - | ffmpeg -i - film.webm -latkit inspect grid.bin +latkit render scene.json -d grid.json --frames frames.jsonl -o frames/%04d.png +latkit inspect grid.bin # tables, fields, and the axis, as JSON latkit query grid.bin '{"of":"Branch","filter":{"op":"gt","args":["loading",90]},"limit":20}' -latkit tile history.bin -o history/ +latkit tile history.bin -o history/ # time tiles, which --tiles opens latkit render plot.json --tiles grid=history/frames.json -o year.png latkit schema scene > scene.schema.json ``` -| Command | Does | -| ---------- | --------------------------------------------------------------------------------------------------------- | -| `render` | draws the scenes, one per `--panel`, and writes `--output` | -| `validate` | reads and checks everything `render` would, frames included, without a GPU | -| `inspect` | prints datasets as JSON: tables, rows, fields and their kinds, the axis and its span | -| `query` | prints the rows a query matches, sorted and paged, as JSON Lines | -| `tile` | writes a dataset as [time tiles](guide.md#fetch-long-histories-in-spans) in `--output` | -| `schema` | prints the JSON Schema of every input, or of `scene`, `data`, `patch`, `query`, `camera`, `fit`, `frames` | - -## Inputs - -- **Scenes** are [scene](guide.md#scenes) JSON files, one per panel; their titles, subtitles, and - mark legends are drawn into the picture. `--camera FILE` sets a scene's camera, in scene order; a - view without one frames `--fit` (`all`, or a fit as JSON). `--insets T,R,B,L` keeps CSS pixels - clear at each edge. -- **Datasets** (`-d [NAME=]FILE`) are a [JSON document](guide.md#json), latkit's - [bytes](guide.md#bytes), or GeoJSON, which needs a `NAME`. A `NAME` renames any dataset. - Binary file slices load relative to the document. -- **Tiles** (`--tiles NAME=FILE`) are a time-tile `frames.json` written by `tile`, or vector tiles - on disk: a PMTiles file, TileJSON, or a `{z}/{x}/{y}` template. -- **Fonts** (`--font FILE`, repeatable) replace the system fonts; the first is used for generic - families such as `sans-serif`. -- At most one input can read stdin (`-`). +At most one input reads stdin (`-`). ## Frames @@ -100,7 +82,8 @@ default). ## Diagnostics Exit codes: 0 success, 1 failure, 2 usage error. Errors print as `latkit: SOURCE: message`, where -`SOURCE` is the input file; for invalid input, the message starts with the path to the invalid -part, such as `frames[3].patch.grid[0].Bus.x`. `--diagnostics json` writes one JSON object per line -to stderr, `{ type, detail }`: `progress` after each frame, `stats` when done, and `error` with its -`code`, `message`, `path`, and `source`. +`SOURCE` is the input file at fault; for invalid input, the message starts with the path to the +invalid part, such as `frames[3].patch.grid[0].Bus.x`. An output that cannot be written is named in +the message. `--diagnostics json` writes one JSON object per line to stderr, `{ type, detail }`: +`progress` after each frame, `stats` when done, and `error` with its `code`, `message`, `path`, and +`source`. diff --git a/docs/gallery/flow.ts b/docs/gallery/flow.ts index 707d3e19..99d3c838 100644 --- a/docs/gallery/flow.ts +++ b/docs/gallery/flow.ts @@ -1,19 +1,42 @@ /** * A DC power flow. It computes each bus's voltage angle from its injection, with branches weighted - * by their susceptance, and how the angles shift when one branch opens. + * by their susceptance, each branch's flow from the angles, and how the angles shift when one branch + * opens. Each island, a set of buses joined by branches in service, balances on its own. */ -/** A network of buses and the branches between them. Reactance is in per unit. */ +/** The power base in MVA. Injections are in per unit of it, and flows come out in MW. */ +export const BASE = 100; + +/** A network of buses and the branches between them. Reactance is in per unit. Branches marked in + * `out` are out of service: they join nothing and carry nothing. */ export interface Network { readonly buses: number; readonly from: Uint32Array; readonly to: Uint32Array; - readonly reactance: Float32Array; + readonly reactance: Float32Array | Float64Array; + readonly out?: Uint8Array; } /** The smallest reactance magnitude a branch is given, so no susceptance is infinite. */ const STIFFEST = 1e-4; +/** A branch's reactance magnitude, so series capacitors (negative reactance) count by their size. */ +function reactance(net: Network, e: number): number { + return Math.max(Math.abs(net.reactance[e]!), STIFFEST); +} + +/** Returns each bus's island, as the row of one bus shared by every bus of the island. */ +export function islands(net: Network): Uint32Array { + const { buses, from, to, out } = net; + const root = Uint32Array.from({ length: buses }, (_, b) => b); + const find = (b: number): number => { + while (root[b] !== b) b = root[b] = root[root[b]!]!; + return b; + }; + for (let e = 0; e < from.length; e++) if (!out?.[e]) root[find(from[e]!)] = find(to[e]!); + return Uint32Array.from({ length: buses }, (_, b) => find(b)); +} + /** The network's susceptance matrix in compressed sparse rows, with the diagonal stored separately, * and each bus's island. */ class Susceptance { @@ -26,9 +49,10 @@ class Susceptance { constructor(net: Network) { this.net = net; - const { buses, from, to } = net; + const { buses, from, to, out } = net; const degree = new Uint32Array(buses + 1); for (let e = 0; e < from.length; e++) { + if (out?.[e]) continue; degree[from[e]! + 1]!++; degree[to[e]! + 1]!++; } @@ -39,6 +63,7 @@ class Susceptance { this.diagonal = new Float64Array(buses); const fill = degree.slice(0, buses); for (let e = 0; e < from.length; e++) { + if (out?.[e]) continue; const [a, c, w] = [from[e]!, to[e]!, this.susceptance(e)]; this.column[fill[a]!] = c; this.weight[fill[a]!++] = w; @@ -47,20 +72,12 @@ class Susceptance { this.diagonal[a]! += w; this.diagonal[c]! += w; } - // Find the islands with union-find, so each one is balanced separately. - const root = Uint32Array.from({ length: buses }, (_, b) => b); - const find = (b: number): number => { - while (root[b] !== b) b = root[b] = root[root[b]!]!; - return b; - }; - for (let e = 0; e < from.length; e++) root[find(from[e]!)] = find(to[e]!); - this.island = Uint32Array.from({ length: buses }, (_, b) => find(b)); + this.island = islands(net); } - /** A branch's susceptance: the inverse of its reactance's magnitude, so series capacitors - * (negative reactance) count by their size. */ + /** A branch's susceptance: the inverse of its reactance. */ susceptance(e: number): number { - return 1 / Math.max(Math.abs(this.net.reactance[e]!), STIFFEST); + return 1 / reactance(this.net, e); } /** Writes `B·v` into `out`. */ @@ -86,12 +103,12 @@ class Susceptance { } /** Solves `B·θ = p` for the angles `θ`, after balancing `p` per island, by conjugate gradients - * with a diagonal (Jacobi) preconditioner. */ + * with a diagonal (Jacobi) preconditioner. A bus with no branch in service keeps an angle of 0. */ solve(injection: Float64Array, tolerance = 1e-10): Float64Array { const n = injection.length; const p = this.balanced(injection); const theta = new Float64Array(n); - const scale = this.diagonal.map((v) => 1 / v); + const scale = this.diagonal.map((v) => (v > 0 ? 1 / v : 0)); const r = p.slice(); const z = r.map((v, b) => v * scale[b]!); const d = z.slice(); @@ -124,6 +141,16 @@ export function angles(net: Network, injection: Float64Array): Float64Array { return new Susceptance(net).solve(injection); } +/** Returns each branch's flow in MW from its `from` end to its `to` end, given the angles + * `theta`. */ +export function flows(net: Network, theta: Float64Array): Float64Array { + const flow = new Float64Array(net.from.length); + for (let e = 0; e < flow.length; e++) + if (!net.out?.[e]) + flow[e] = ((theta[net.from[e]!]! - theta[net.to[e]!]!) * BASE) / reactance(net, e); + return flow; +} + /** * Returns how each bus's angle shifts when `branch` opens, given the angles `theta` before. The * branch's flow moves onto the rest of the network, as if injected at one end and withdrawn at the diff --git a/docs/gallery/frequency.ts b/docs/gallery/frequency.ts index c34e562b..1c1f28cd 100644 --- a/docs/gallery/frequency.ts +++ b/docs/gallery/frequency.ts @@ -2,13 +2,13 @@ import { THEME, type Piece } from './look.ts'; import { readCase } from './grids.ts'; -export default function frequency(): Piece { - const c = readCase('activsg10k'); +export default async function frequency(): Promise { + const c = await readCase('activsg10k'); const fault = c.header.fault!; - const [fx, fy] = [c.positions[2 * fault.bus]!, c.positions[2 * fault.bus + 1]!]; + const [fx, fy] = [c.lon[fault.bus]!, c.lat[fault.bus]!]; // Each machine's distance from the fault in degrees. The disturbance reaches near machines first. const distance = Float32Array.from(c.machineBus, (b) => - Math.hypot(c.positions[2 * b]! - fx, c.positions[2 * b + 1]! - fy), + Math.hypot(c.lon[b]! - fx, c.lat[b]! - fy), ); c.patch.set('Machine', { count: c.header.machines }, { distance }); const machines = c.header.machines.toLocaleString('en-US'); @@ -17,7 +17,7 @@ export default function frequency(): Piece { alt: `The speed of ${machines} machines over ten seconds after a fault, near ones bright and swinging hardest, far ones dark`, data: [c.patch], // x spans the whole recording. With `y` null, the speed range fits what is in view. - camera: { x: [0, (c.header.frames! - 1) * c.header.dt!], y: null }, + camera: { x: [0, c.simulation!.duration], y: null }, scene: { space: { plot: { x: { label: 'Time (s)' }, y: { label: 'Speed (mHz from 60 Hz)' } } }, ease: 0, diff --git a/docs/gallery/grid.ts b/docs/gallery/grid.ts index 1f51a6de..50665dbf 100644 --- a/docs/gallery/grid.ts +++ b/docs/gallery/grid.ts @@ -32,8 +32,8 @@ export function states(outline = '#26364a'): Mark[] { ]; } -export default function grid(): Piece { - const c = readCase('activsg70k'); +export default async function grid(): Promise { + const c = await readCase('activsg70k'); return { name: 'grid', alt: 'Seventy thousand buses of a synthetic grid on the United States, its lines colored and widened by voltage', diff --git a/docs/gallery/grids.ts b/docs/gallery/grids.ts index 020f9d9d..9308b394 100644 --- a/docs/gallery/grids.ts +++ b/docs/gallery/grids.ts @@ -1,130 +1,65 @@ /** * Reads the synthetic grid cases that the example suite fetches from `npm/examples/public/grids` * into one dataset named `grid`. It holds buses with their voltage level, the branches between - * them, machines, and, for a case that simulated a fault, each machine's speed per frame. The file - * format is the suite's own: an "LKG1" magic, a JSON header, and typed arrays at the offsets the - * header gives. + * them, machines, and, for a case that simulated a fault, each machine's speed per frame. The + * suite's `format.ts` describes the files and reads them. */ import fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { gunzipSync } from 'node:zlib'; import { Patch, reference } from 'latkit'; +import { + inflate, + parseCase, + type CaseFile, +} from '../../npm/examples/src/examples/networks/format.ts'; -interface Header { - readonly case: string; - readonly buses: number; - readonly branches: number; - readonly machines: number; - readonly frames?: number; - readonly dt?: number; - readonly speedScale?: number; - readonly fault?: { readonly bus: number; readonly on: number; readonly off: number }; - readonly arrays: Readonly< - Record - >; -} - -export interface Case { - readonly name: string; - readonly header: Header; +/** A case, with its dataset `grid` written as a patch. */ +export interface Case extends CaseFile { readonly patch: Patch; - /** Each bus's longitude and latitude, interleaved. */ - readonly positions: Float32Array; - /** Each machine's bus row. */ - readonly machineBus: Uint32Array; - /** Returns one of the case's arrays by name, such as `branch.reactance`. Throws if it is - * missing. */ - readonly array: (name: string, of: new (b: ArrayBuffer, o: number, l: number) => T) => T; -} - -/** The voltage level of `kv`: 0 below 100 kV, then 1 to 5 for 115–161, 230, 345, 500, and - * 765 kV. */ -export function levelOf(kv: number): number { - return kv >= 700 ? 5 : kv >= 450 ? 4 : kv >= 300 ? 3 : kv >= 200 ? 2 : kv >= 100 ? 1 : 0; } -export function readCase(id: string): Case { - const file = fileURLToPath(new URL(`../../npm/examples/public/grids/${id}.bin`, import.meta.url)); - const raw = fs.readFileSync(file); - const bytes = raw[0] === 0x1f && raw[1] === 0x8b ? gunzipSync(raw) : raw; - const buffer = bytes.buffer.slice( - bytes.byteOffset, - bytes.byteOffset + bytes.byteLength, - ) as ArrayBuffer; - const view = new DataView(buffer); - if (view.getUint32(0, true) !== 0x31474b4c) throw new Error(`${id} is not a grid case`); - const length = view.getUint32(4, true); - const header = JSON.parse(new TextDecoder().decode(new Uint8Array(buffer, 8, length))) as Header; - const body = Math.ceil((8 + length) / 8) * 8; - const array = (name: string, of: new (b: ArrayBuffer, o: number, l: number) => T): T => { - const found = header.arrays[name]; - if (!found) throw new Error(`${id} has no ${name}`); - return new of(buffer, body + found[1], found[2]); - }; - const names = (name: string): string[] => { - const offsets = array(`${name}.offsets`, Uint32Array); - const text = array(`${name}.bytes`, Uint8Array); - const decoder = new TextDecoder(); - return Array.from({ length: offsets.length - 1 }, (_, i) => - decoder.decode(text.subarray(offsets[i]!, offsets[i + 1]!)), - ); - }; - const [n, m, g] = [header.buses, header.branches, header.machines]; - const position = array('bus.position', Float32Array); - const kv = array('bus.kv', Float32Array); - const ends = array('branch.ends', Uint32Array); - const machineBus = array('machine.bus', Uint32Array); - const generation = array('bus.generation', Float32Array); - const from = new Uint32Array(m); - const to = new Uint32Array(m); - const lineKv = new Float32Array(m); - for (let e = 0; e < m; e++) { - [from[e], to[e]] = [ends[2 * e]!, ends[2 * e + 1]!]; - lineKv[e] = Math.max(kv[from[e]!]!, kv[to[e]!]!); - } - const lon = Float64Array.from({ length: n }, (_, b) => position[2 * b]!); - const lat = Float64Array.from({ length: n }, (_, b) => position[2 * b + 1]!); +export async function readCase(id: string): Promise { + const file = new URL(`../../npm/examples/public/grids/${id}.bin`, import.meta.url); + const c = parseCase(id, await inflate(fs.readFileSync(file))); + const { buses: n, branches: m, machines: g } = c.header; const patch = new Patch('grid') .replace( 'Bus', { count: n }, { - lon, - lat, - name: names('bus.name'), - kv, - level: Float32Array.from(kv, levelOf), - load: array('bus.load', Float32Array), - generation, + lon: c.lon, + lat: c.lat, + name: c.names('bus.name'), + kv: c.kv, + level: c.level, + load: c.array('bus.load', Float32Array), + generation: c.array('bus.generation', Float32Array), }, ) .replace( 'Line', { count: m }, { - from: reference('Bus', from), - to: reference('Bus', to), - kv: lineKv, - level: Float32Array.from(lineKv, levelOf), + from: reference('Bus', c.from), + to: reference('Bus', c.to), + kv: c.lineKv, + level: c.lineLevel, }, ) .replace( 'Machine', { count: g }, { - lon: Float64Array.from(machineBus, (b) => lon[b]!), - lat: Float64Array.from(machineBus, (b) => lat[b]!), - power: array('machine.power', Float32Array), - mva: array('machine.mva', Float32Array), + lon: Float64Array.from(c.machineBus, (b) => c.lon[b]!), + lat: Float64Array.from(c.machineBus, (b) => c.lat[b]!), + power: c.array('machine.power', Float32Array), + mva: c.array('machine.mva', Float32Array), }, ); - if (header.frames !== undefined && header.dt !== undefined) { - // Speeds are stored in per unit of 60 Hz. This scale converts them to mHz. - const scale = (header.speedScale ?? 1) * 60_000; - const speeds = Float32Array.from(array('machine.speed', Int16Array), (v) => v * scale); + if (c.simulation) { + const { frames, dt, speeds } = c.simulation; patch.axis('time', 's'); - for (let f = 0; f < header.frames; f++) - patch.append('Machine', f * header.dt, { speed: speeds.subarray(f * g, (f + 1) * g) }); + for (let f = 0; f < frames; f++) + patch.append('Machine', f * dt, { speed: speeds.subarray(f * g, (f + 1) * g) }); } - return { name: header.case, header, patch, positions: position, machineBus, array }; + return { ...c, patch }; } diff --git a/docs/gallery/opening.ts b/docs/gallery/opening.ts index 6f742632..14df2401 100644 --- a/docs/gallery/opening.ts +++ b/docs/gallery/opening.ts @@ -8,8 +8,8 @@ import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { gunzipSync } from 'node:zlib'; import { Patch, reference, type ExprInput } from 'latkit'; -import { angles, shifts } from './flow.ts'; -import { levelOf, readCase } from './grids.ts'; +import { BASE, angles, flows, shifts } from './flow.ts'; +import { readCase } from './grids.ts'; import { THEME, worldMap, type Piece } from './look.ts'; import { STATES } from './grid.ts'; @@ -21,49 +21,41 @@ function frequency(): Float32Array { return Float32Array.from(tenMicrohertz, (v) => v * 1e-5); } -export default function opening(): Piece { - const c = readCase('activsg70k'); +export default async function opening(): Promise { + const c = await readCase('activsg70k'); const [n, m] = [c.header.buses, c.header.branches]; - const ends = c.array('branch.ends', Uint32Array); - const kv = c.array('bus.kv', Float32Array); const number = c.array('bus.number', Uint32Array); const load = c.array('bus.load', Float32Array); const generation = c.array('bus.generation', Float32Array); const net = { buses: n, - from: Uint32Array.from({ length: m }, (_, e) => ends[2 * e]!), - to: Uint32Array.from({ length: m }, (_, e) => ends[2 * e + 1]!), + from: c.from, + to: c.to, reactance: c.array('branch.reactance', Float32Array), }; const [i, j] = [number.indexOf(59757), number.indexOf(59774)]; const line = net.from.findIndex( (a, e) => (a === i && net.to[e] === j) || (a === j && net.to[e] === i), ); - // Injections on a 100 MVA base, so the angles come out in radians. + // Injections in per unit, so the angles come out in radians. const theta = angles( net, - Float64Array.from({ length: n }, (_, b) => (generation[b]! - load[b]!) / 100), + Float64Array.from({ length: n }, (_, b) => (generation[b]! - load[b]!) / BASE), ); - const shift = shifts(net, theta, line); - const mw = (e: number, angle: Float64Array) => - ((angle[net.from[e]!]! - angle[net.to[e]!]!) * 100) / - Math.max(Math.abs(net.reactance[e]!), 1e-4); + const moved = flows(net, shifts(net, theta, line)); // Each branch's change in flow magnitude once the line opens, as asinh(ΔMW / 4), so a change of a // few MW still shows. - const gain = Float32Array.from({ length: m }, (_, e) => { - const before = mw(e, theta); - return Math.asinh((Math.abs(before + mw(e, shift)) - Math.abs(before)) / 4); - }); - const level = Float32Array.from({ length: m }, (_, e) => - levelOf(Math.max(kv[net.from[e]!]!, kv[net.to[e]!]!)), + const gain = Float32Array.from(flows(net, theta), (before, e) => + Math.asinh((Math.abs(before + moved[e]!) - Math.abs(before)) / 4), ); + const level = c.lineLevel; const hz = frequency(); // A pole to the ground from one bus of 345 kV or more in each 0.75° cell, so the heights are // readable without crowding the view with poles. const cells = new Set(); const pole = Float32Array.from({ length: n }, (_, b) => { - const cell = `${Math.floor(c.positions[2 * b]! / 0.75)} ${Math.floor(c.positions[2 * b + 1]! / 0.75)}`; - if (kv[b]! < 345 || cells.has(cell)) return 0; + const cell = `${Math.floor(c.lon[b]! / 0.75)} ${Math.floor(c.lat[b]! / 0.75)}`; + if (c.kv[b]! < 345 || cells.has(cell)) return 0; cells.add(cell); return 1; }); @@ -72,9 +64,9 @@ export default function opening(): Piece { 'Bus', { count: n }, { - lon: Float64Array.from({ length: n }, (_, b) => c.positions[2 * b]!), - lat: Float64Array.from({ length: n }, (_, b) => c.positions[2 * b + 1]!), - kv, + lon: c.lon, + lat: c.lat, + kv: c.kv, hz, // Height as a fraction of the earth's radius: zero at deviations of -16 mHz and below, // rising with frequency, with the highest peaks compressed by asinh. diff --git a/docs/gallery/record.ts b/docs/gallery/record.ts index ac489871..8024bf27 100644 --- a/docs/gallery/record.ts +++ b/docs/gallery/record.ts @@ -31,7 +31,7 @@ fs.readSync(file, bytes, 0, bytes.length, frame * bytes.length); fs.closeSync(file); const hz = new Float32Array(bytes.buffer, bytes.byteOffset, count); -const c = readCase('activsg70k'); +const c = await readCase('activsg70k'); const number = c.array('bus.number', Uint32Array); const row = new Map(meta.bus_ids.map((id, i) => [id, i])); const out = Int16Array.from(number, (id) => { diff --git a/docs/gallery/render.ts b/docs/gallery/render.ts index 2d1dba96..30bbd38e 100644 --- a/docs/gallery/render.ts +++ b/docs/gallery/render.ts @@ -38,8 +38,10 @@ const fonts = ['Inter-Regular.ttf', 'Inter-SemiBold.ttf'].map((f) => ); async function render(name: string): Promise { - const { default: make } = (await import(`./${name}.ts`)) as { default: () => Piece }; - const piece = make(); + const { default: make } = (await import(`./${name}.ts`)) as { + default: () => Piece | Promise; + }; + const piece = await make(); const file = (suffix: string) => path.join(work, `${piece.name}.${suffix}`); fs.writeFileSync(file('scene.json'), JSON.stringify(piece.scene)); const data = piece.data.flatMap((d, i) => { diff --git a/docs/gallery/texas.ts b/docs/gallery/texas.ts index b9af0237..d3dcbbab 100644 --- a/docs/gallery/texas.ts +++ b/docs/gallery/texas.ts @@ -4,8 +4,8 @@ import { THEME, type Piece } from './look.ts'; import { readCase } from './grids.ts'; import { LEVELS, STATES, WIDTHS, states } from './grid.ts'; -export default function texas(): Piece { - const c = readCase('activsg2000'); +export default async function texas(): Promise { + const c = await readCase('activsg2000'); return { name: 'texas', alt: 'Two thousand buses of a synthetic Texas grid: generators as amber discs sized by output, comets along its highest-voltage lines', diff --git a/docs/guide.md b/docs/guide.md index b85e5d87..b45aab6c 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -1,12 +1,12 @@ # Guide -How latkit works: datasets and the patches that change them, scenes, views, pictures and video, -the data formats, and deployment. +How to use latkit: write and change datasets, describe scenes, show them in views, and draw +pictures and video. The [API reference](reference/api.md) documents every method and option. ## Data A dataset is a name and a set of tables. A table has rows, each with an id, and fields, each with -one value per row. A patch applies atomically: all of its changes, or none. +one value per row. A [patch](reference/api.md#patch) changes a dataset: all of its changes, or none. ### Write a dataset @@ -34,32 +34,17 @@ const grid = new Data('grid').apply( ); ``` -- **Ids** are a list of nonnegative safe integers (`0..2^53 - 1`) or strings, or `{ count: n }` - for rows `0` to `n - 1`. -- **Values** are one column per field: typed arrays of numbers, or arrays of numbers, booleans, - strings, or geometries. `null` is missing. A `Uint8Array` holds booleans, so use `Uint32Array` - for small unsigned integers. -- **Geometries** are `[x, y]` points, paths of points, parts (a polygon's rings, or a path's - pieces), or polygons. +- **Ids** are nonnegative safe integers or strings, or `{ count: n }` for rows `0` to `n - 1`. +- **Values** are one column per field: typed arrays, or arrays of numbers, booleans, strings, or + geometries, with `null` for missing. A `Uint8Array` holds booleans. +- **Geometries** are `[x, y]` points, paths of points, parts (a polygon's rings), or polygons. - **References** name rows of another table by id: `reference(table, ids)`. -| Change | Does | -| ---------------------------------------------------- | ----------------------------------------------------------------------- | -| `replace(table, ids, fields)` | replaces the table, or adds it | -| `set(table, ids, fields)` | updates those rows and adds unknown ids; other fields keep their values | -| `remove(table, ids)` | removes the rows | -| `axis(name, unit)` | names the axis that sampled fields are recorded over | -| `append(table, at, fields, { ids })` | records a frame at each coordinate of `at`, for every row or for `ids` | -| `forget(before)` | drops the frames before a coordinate | -| `frames(table, field, { level, first, at, values })` | adds a run of a sampled field's stored entries, as a time tile does | - -`patch.extend(next)` appends another patch's changes. - ### Change it `apply` changes a dataset in place; every view drawing it shows the change on the next frame. -`snapshot()` keeps the current version in O(1) by sharing pages with the dataset, so an undo history -costs only the pages that changed: +`snapshot()` keeps the current version in O(1) by sharing pages, so an undo history costs only the +pages that changed: ```ts const before = grid.snapshot(); @@ -69,13 +54,10 @@ grid.restore(before); // and undone ### Record over time -A field becomes sampled with its first frame. Sampled values are stored as `f32`, even when given -as a `Float64Array`, so a history is for visualization, not a lossless `f64` archive. Axis -coordinates stay `f64`, and static columns keep their original numeric type. NaN marks a missing -value in sampled fields and in numeric query results. A view shows the axis coordinate `view.at`: -marks interpolate each sampled field linearly between the frames on either side of it, or show the -last frame at or before it when read with `field(name).step()`. `get` and `query` read the last -frame at or before the `at` they are given, or the last frame without one. +A field becomes sampled with its first frame. A view shows the axis coordinate `view.at`: marks +interpolate each sampled field between the frames on either side of it, or show the last frame at +or before it when read with `field(name).step()`. Sampled values are stored as `f32` for drawing; +axis coordinates stay `f64`. ```ts grid.apply(new Patch().axis('time', 's')); @@ -86,7 +68,7 @@ grid.apply(new Patch().forget(1)); // a rolling window view.at = 1.2; ``` -As it records, each sampled field keeps levels of detail: level `k` holds each row's minimum and +Each sampled field keeps levels of detail as it records: level `k` holds each row's minimum and maximum over buckets of `8^k` frames. A line tracing a field on a plot reads the level that gives each pixel column one or two buckets, so a long history draws as fast as a short one. @@ -102,26 +84,21 @@ latkit tile history.latk -o history/ view.sources = [new Tiles('grid', 'history/frames.json')]; ``` -`tile` writes `frames.json`, `base.latk` (the dataset without its sampled fields' frames), and each -sampled field's history in spans of `--chunk` entries (256 by default) at every level of detail. In -Rust, `Snapshot::to_tiles` writes the same files. A line tracing a field fetches the spans its plot -window shows, at the detail it draws. A mark showing a field fetches the frames around `view.at`. -While a span loads, the view draws the coarser span above it, as a map shows the parent tile while a -tile loads. +A line tracing a field fetches the spans its plot window shows, at the detail it draws; other marks +fetch the frames around `view.at`. While a span loads, the view draws the coarser span above it. In +Rust, `Snapshot::to_tiles` writes the same files. ### Stream -`patch.toBytes()` encodes a patch as one frame, and frames written back to back form a stream. Each -frame's header gives its length, so `Patch.stream` refuses a frame longer than `limit` (256 MiB by -default) before reading its body. It buffers at most one frame, besides the current input chunk and -the patches you keep, and cancels the reader on an error or when you break out of the loop. A server -in any language can write the same frames ([format](#bytes)). +`patch.toBytes()` encodes a patch as one frame, and frames written back to back form a stream that +a server in any language can write ([format](#bytes)). `Patch.stream` yields each patch as its last +byte arrives: ```ts for await (const patch of Patch.stream((await fetch('/feed')).body!)) grid.apply(patch); ``` -`new Patch('grid')` names its dataset; applying it to any other dataset throws. +`new Patch('grid')` names its dataset; applying it to another dataset fails. ### Read files @@ -131,14 +108,12 @@ const saved = await Data.read(bytes); // latkit's bytes, as data.toBytes() wrote const states = await Data.read(geojson, { name: 'states' }); // GeoJSON, gzip-compressed or not ``` -GeoJSON becomes the tables `points`, `lines`, and `polygons`, each with a `geometry` field and one -field per property; it is parsed in a worker. [Formats](#json) describes the JSON document. -`Patch.read(source)` reads one patch from the same kinds of source; reading bytes needs no -WebAssembly. +GeoJSON becomes the tables `points`, `lines`, and `polygons`. [Formats](#json) describes the JSON +document. `Patch.read` reads one patch from the same kinds of source. ### Read back -Reading needs no GPU. Queries use the same expressions as marks: +Reading needs no GPU, and queries use the same expressions as marks: ```ts grid.get('Bus', 101, 'name'); // 'Northgate' @@ -152,20 +127,14 @@ const hot = grid.query({ // { table, ids, fields: { name: string[], loading: Float64Array }, total } ``` -`total` is the number of rows the query matched before `limit`. `field('name').contains('north')` -matches text, ignoring case. `describe()` lists the tables, their rows, their fields and field -kinds, and the axis with its span. `envelope({ table, field, range, spans, ids })` returns each -row's minimum and maximum of a sampled field over equal spans of the axis. It reads the levels of -detail, so each span costs a few reads however many frames it covers. A `Snapshot` has the same -read methods and reads its own version. - -The [API reference](reference/api.md) lists every method's signature. +[`Dataset`](reference/api.md#dataset) lists the reads, `describe()` and `envelope()` among them. A +`Snapshot` reads its own version the same way. ## Scenes A scene is plain data: a `space`, a list of `marks` drawn in order, and optional `layout`, `theme`, -`controls`, `ease` (the seconds a change eases over, 0.3 by default), `title`, and `subtitle`. -Setting `view.scene = next` replaces the scene, and each row eases from where it was drawn. +`controls`, `ease` (the seconds a change eases over), `title`, and `subtitle`. Setting +`view.scene = next` replaces the scene, and each row eases from where it was drawn. ```ts const scene: Scene = { @@ -188,6 +157,8 @@ const scene: Scene = { }; ``` +The [scene types](reference/types.md#scene) document every option and its default. + ### Spaces | Space | Positions | @@ -197,37 +168,30 @@ const scene: Scene = { | `'plot'` | the axis coordinate across, values up | | `'diagram'` | a flat sheet, y down, sizes in position units; ports, groups, layouts | -To set options, write the space as an object: `{ plane: { grid } }`, `{ diagram: { grid } }`, -`{ earth: { projection, graticule, daylight, atmosphere } }`, or `{ plot: { x, y } }`. The earth's -`projection` is `'mercator'` (the default) or `'globe'`. A grid is `'off'`, `{ lines: { pitch } }`, -or `{ dots: { pitch, snap } }`. A plot's axes reserve room for their labels and titles. +Written as an object, a space takes options: `{ earth: { projection: 'globe' } }`, +`{ plane: { grid: { lines: { pitch: 10 } } } }`, or a plot's `{ plot: { x, y } }` axes. ### Marks -Every mark names its table with `of: [dataset, table]` and takes `color`, `visible`, `priority`, -`label`, `legend`, `name`, `pick`, `select`, and `drag`. By default, the pointer hovers, clicks, -and probes vertices, edges, faces, and a plot's traces, but not geometry such as areas and lines -along paths (coasts, borders, roads). Set `pick: true` to make, for example, a map's states or cells -respond to the pointer. - -| Mark | Draws each row as | Its own options | -| ---------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | -| `vertices` | a marker at `position` (or at the point field `geometry`) | `elevation`, `radius`, `size`, `marker`, `ring`, `ports`, `contains`, `shadow` | -| `edges` | a stroke between the two rows its `ends` fields reference | `ends`, `bends`, `route`, `arrows`, `width`, `dash`, `flow` | -| `lines` | a stroke along a path field; on a plot, a sampled field traced over time | `geometry`, `trace`, `interpolation`, `width`, `dash`, `flow` | -| `areas` | the area a polygon field covers | `geometry` | -| `faces` | a triangle between three vertices | `corners` | - -`flow` animates comets along a stroke. In a diagram, vertices are blocks with `ports`. A vertex mark -with `contains` draws each row as a group around the rows that reference it. An edges mark of a -table without `ends` draws nets, each joining the ports that reference it. The theme's `diagram` -group (`outline`, `corner`, `portSize`, `bend`) styles blocks, groups, ports, and wires, and -`shadow: true` gives blocks and groups a drop shadow. +Every mark names its table with `of: [dataset, table]`. The pointer hovers and clicks vertices, +edges, faces, and a plot's traces; set `pick: true` to make areas and lines respond too. + +| Mark | Draws each row as | +| ---------- | ------------------------------------------------------------------------ | +| `vertices` | a marker at `position` (or at the point field `geometry`) | +| `edges` | a stroke between the two rows its `ends` fields reference | +| `lines` | a stroke along a path field; on a plot, a sampled field traced over time | +| `areas` | the area a polygon field covers | +| `faces` | a triangle between three vertices | + +`flow` animates comets along a stroke. In a diagram, vertices are blocks with `ports`, a vertex +mark with `contains` draws groups around the rows that reference them, and an edges mark of a table +without `ends` draws nets joining the ports that reference it. ### Expressions -Options that vary by row take an expression: a number, a field name, or a builder. Color options -differ: a string is a CSS color, and a field sets colors only through a colormap or a conditional. +Options that vary by row take an expression: a number, a field name, or a builder. A string given +to a color option is a CSS color; a field sets colors through a colormap or a conditional. ```ts import { field, when, selected, units, px } from 'latkit'; @@ -242,32 +206,23 @@ field('vm').from('weather', 'Station'); // read from another dataset, joined by ``` Sizes are in CSS pixels on a plane, the earth, and a plot, and in position units in a diagram, -unless wrapped in `px()` or `units()`; on the earth a position unit is a degree. State expressions -read the view: `selected()`, `hovered()`, `pointer()`, `elapsed()`, and `zoom()`. A colormap is a -catalog name or your own `{ kind, colors }`; a categorical colormap can name its `categories` for -the legend. A colormap draws a missing value in the theme's `missing` color, while an option given -`missing()` takes its theme value. `colormapCss(map)` returns the same colors as a CSS gradient. +unless wrapped in `px()` or `units()`. State expressions read the view: `selected()`, `hovered()`, +`pointer()`, `elapsed()`, and `zoom()`. `colormaps()` lists the catalog, and `colormapCss(map)` +gives a colormap's colors as a CSS gradient for legends drawn in HTML. ### Themes, controls, and layouts - **Theme**: `'dark'`, `'light'`, or overrides of a base theme, grouped by what they style: - `{ base: 'light', text: { size: 12 }, edge: { width: 2 } }`. `edge: { color: null }` colors each - edge by blending its two vertices' colors, and `focus: { selected: null }` draws the selection - glow in each row's own color. The `flow` group's comet `spacing` and `length` take `{ px: n }` or - `{ units: n }`. -- **Controls**: options `pan`, `zoom`, `turn`, `keyboard`, `hover`, `pickRadius`, `selectBox`, - `connect`, `delete`, `cycle`, `press`, and `motion`; by default the pointer pans, zooms, turns, - and hovers. The presets are `'inspect'`, which leaves the camera to the application, and - `'none'`, for a still picture. -- **Layout** places vertex rows that have no position, in any space but a plot: `'stress'`, - `'layered'`, or their options. Without a layout, such rows are not drawn. A row keeps its place - once laid out; `view.arrange()` lays out every row again. + `{ base: 'light', text: { size: 12 }, edge: { width: 2 } }`. +- **Controls**: what the pointer and keys do. By default the pointer pans, zooms, turns, and + hovers; `'inspect'` leaves the camera to the application, and `'none'` suits a still picture. +- **Layout** places vertex rows that have no position: `'stress'` or `'layered'`. A row keeps its + place once laid out. ## Views A `Gpu` creates views and canvases. A view draws one scene from its sources, the datasets its marks -name. A canvas shows views, one per panel, and passes each view the pointer and key input on its -panel. +name. A canvas shows views, one per panel, and passes each the pointer and key input on its panel. ```ts const gpu = await Gpu.create(); @@ -281,20 +236,9 @@ gpu.canvas(element, { }); ``` -### Sources - -`view.sources` lists the datasets a view draws, each matched by its name. A source is one of: - -- `Data`: the view draws its latest version, from the frame after each patch applies. -- `Snapshot`: the view draws that version, unchanged. -- `Tiles`: pieces fetched as the view needs them, shared by every view of the GPU. The URL is a - [time tiles](#fetch-long-histories-in-spans) `frames.json` manifest, or vector tiles fetched as - the camera moves: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` template. Each vector - layer becomes a table of the features that match the mark drawing it (polygons, lines, or points); - name the table `layer:polygons`, `layer:lines`, or `layer:points` to choose explicitly. - -`view.snapshot(name)` returns the version of a source the view draws now, including the tiles -fetched so far. +`view.sources` matches datasets to marks by name: a `Data` draws at its latest version, a +`Snapshot` as it is, and [`Tiles`](reference/api.md#tiles) as the pieces the view needs, from time +tiles or vector tiles. ### Camera @@ -309,11 +253,9 @@ view.orbit = 6; // degrees a second ### Events -`view.on(type, listener)` returns a function that removes the listener. Intent events (`click`, -`open`, `menu`, `drag`, `connect`, `delete`) fire once each, in the order the user acted. Change -events (`hovered`, `selection`, `camera`, `waiting`) report only the latest state, which the -property of the same name always holds (`view.hovered`, `view.selection`, and so on). Setters fire -no events, so events report only what the application did not do itself. +Intent events (`click`, `open`, `menu`, `drag`, `connect`, `delete`) fire once each, in the order +the user acted. Change events (`hovered`, `selection`, `camera`, `waiting`) carry the latest state, +which the property of the same name holds. Setters fire no events. ```ts view.on('click', ({ hit }) => console.log(hit.row, hit.point)); @@ -324,8 +266,7 @@ view.on('error', (error) => console.error(error.code, error.path, error.message) ### Rows -A row is `{ data, table, id }`. A set of rows in one table is `{ data, table, ids }`; the -selection, `selection` and `delete` events, fits, and `neighbors` use sets: +A row is `{ data, table, id }`, and a set of rows in one table `{ data, table, ids }`: ```ts view.selection = [{ data: 'grid', table: 'Bus', ids: [101, 102] }]; @@ -334,66 +275,33 @@ const hits = await view.pick([x, y], { radius: 8 }); // rows within 8 CSS pixels const at = await view.screenOf({ data: 'grid', table: 'Bus', id: 101 }); // [x, y] on screen, or null ``` -`pick` and `screenOf` resolve once a frame draws the view. Calls that wait take `{ signal }` and -stop waiting when it aborts; without one, they time out after 30 seconds. They reject with code -`timeout` on a timeout, and `cancelled` when aborted or when the view is destroyed. - ### Layouts into data -A scene's `layout` places vertex rows that have no position. `view.arrange()` lays them all out -again and resolves to one patch per dataset that writes their places into the mark's position -fields. Applied, the places become part of the data: +`view.arrange()` lays out every vertex row without a position again, and resolves to one patch per +dataset that writes their places into the mark's position fields. Applied, the places become part +of the data: ```ts for (const patch of await view.arrange()) if (patch.data === grid.name) grid.apply(patch); ``` -### Stats and memory - -`view.stats` reports, per frame, the rows drawn, draw calls, tiles, and CPU and GPU milliseconds. -`gpu.resident` and `gpu.uploading` are the bytes of data on the GPU and waiting to upload, across all -views of that GPU. `gpu.work` counts exactly what the GPU did: bytes uploaded, written, and cleared; -rows evaluated, resolved (edge and face endpoints found), and packed; readbacks, jobs, texts, and -pipelines compiled. - -`gpu.budget = { bytes, uploadPerFrame }` caps the GPU memory data may take (by default half the -device's largest storage buffer) and the bytes uploaded per frame (32 MiB by default). Set it before -data draws: the memory pool grows as needed and never shrinks. The views of one GPU share the -budget: a view that needs less than an even split gets all it needs, and the others divide the -rest. Pages that several views draw count once. - -A table too large for its view's share is paged: the view draws only the pages the camera needs. A -page the camera leaves stays resident while the budget allows, so returning to it needs no upload. -Drawn edges need their endpoints resident: a vertex page that many drawn edges end in is made -resident whole, and scattered endpoints are made resident individually, so edges between far-apart -vertices still fit the budget. - -Each frame does only the work its changes need. A patch re-evaluates only the pages it wrote, unless -a fitted domain or a join reads them. A transition evaluates once, then blends each frame. Rows of a -paged table keep their GPU slots while the camera moves, so only newly needed rows upload. - -`Data`, `Snapshot`, `Gpu`, `View`, `Canvas`, and `Image` have an idempotent `destroy()` and -`Symbol.dispose`, so they work with `using`. Destroying a `Gpu` also destroys its canvases, views, -and frame listeners. - -Finish a video with `await video.finish()` or cancel it with `await video.abort(reason)`. `record` -finishes the video itself, and aborts it on an error. - -`gpu.lost` tells whether the device is gone. To recover, create a new `Gpu`, views, and canvases, -and give them your scenes and datasets again. `view.failures` lists the marks that draw nothing -because their data no longer fits them, with the reason. To drive frames yourself, call -`gpu.step(now)`: after the first call, latkit stops requesting animation frames, and -`view.wantsFrame` tells whether a view needs one. `gpu.done()` resolves once submitted GPU work, -readbacks, and requested pipeline compiles have finished; `gpu.landed()` resolves when work a view -waits on (a readback, compile, job, text, or fetch) arrives, or a view needs a frame. -`view.position([x, y])` is the position under a canvas point, and `view.screen(position)` is the -screen point where a position draws. +### Memory + +`gpu.budget = { bytes, uploadPerFrame }` caps the GPU memory data takes and the bytes uploaded per +frame; set it before data draws. The views of one GPU share it: a view that needs less than an even +split gets all it needs, and the others divide the rest. A table too large for its view's share is +paged, and the view draws only the pages its camera needs. Each frame does only the work its +changes need: a patch re-evaluates only the pages it wrote. + +`view.stats`, `gpu.work`, `gpu.resident`, and `gpu.uploading` report what the views draw and what +the GPU did. A test or benchmark drives frames itself with `gpu.step(now)`, waiting on +`gpu.landed()` between them. ## Pictures and video -A `Shot` is plain data: a size in pixels and how views are drawn into it. It draws views whether or -not a canvas shows them; a canvas showing one keeps its last frame while the shot draws. -`canvas.shot()` makes a shot of exactly what the canvas shows. +A [`Shot`](reference/api.md#shot) is plain data: a size in pixels and the views drawn into it. It +draws views whether or not a canvas shows them, and `canvas.shot()` makes a shot of exactly what a +canvas shows. ```ts const shot = new Shot([3840, 2160], { scale: 2, layout: [{ view }] }); // 1920 by 1080 CSS pixels @@ -402,47 +310,25 @@ const png = await image.encode('png'); // or 'jpeg' with { quality }, or 'webp' image.destroy(); ``` -| Option | Does | -| ------------- | --------------------------------------------------------------------------------------------- | -| `scale` | draws `scale` pixels per CSS pixel (1 by default) | -| `layout` | the views and where each draws, `at: [x, y, width, height]` as fractions | -| `transparent` | keeps alpha where the themes' backgrounds would be | -| `capture` | waits until views are complete and still (`'still'`, the default), or only complete (`'now'`) | -| `elapsed` | fixes `elapsed()` and animated effects, such as comets, at `elapsed` seconds | - -Images are encoded in a worker where workers are available, and on the calling thread otherwise. - ### Video -`shot.video({ fps, output, bitrate })` encodes H.264 MP4 with WebCodecs as frames are drawn, and -writes it to a `WritableStream`, such as a file's or an in-memory buffer's. `bitrate` defaults to -about 0.15 bits per pixel per frame. Each frame draws the views at its own time, `frames / fps` -seconds, so motion follows the video rather than the wall clock. `record(step)` calls `step(time)` -before each frame and finishes the video when it returns `false`. +`shot.video({ fps, output })` encodes H.264 MP4 with WebCodecs as frames are drawn, and writes it +to a `WritableStream`. Each frame draws the views at its own time, `frames / fps` seconds, so motion +follows the video rather than the wall clock, and a frame where nothing moved repeats the one +before. `record(step)` calls `step(time)` before each frame, and finishes the video when it returns +`false`: ```ts const file = await (await showSaveFilePicker()).createWritable(); -const video = new Shot([1920, 1080], { layout: [{ view }] }).video({ - fps: 60, - output: file, - bitrate: 12e6, -}); +const video = new Shot([1920, 1080], { layout: [{ view }] }).video({ fps: 60, output: file }); await video.record((time) => { view.at = time; return time < 10; }); ``` -The width is rounded down to a multiple of 4 and the height to a multiple of 2, as H.264 needs. A -frame where nothing moved is not drawn; the previous frame is shown again. A `signal` passed to -`shot.video` aborts the video: its output is aborted and its views are released. - -For manual control, call `await video.frame()` for each frame, then `await video.finish()`. -`finish` and `abort` are idempotent; both release the output's writer lock, the encoder, and the -abort listener. `frames` and `time` stay readable afterwards. An error in a step, frame, write, or -progress callback aborts `record`. Aborting cancels pending drawing and encoding at once, but the -writer lock is released only after the stream settles its in-flight write. In the browser, the -output is Constrained Baseline H.264, with output timestamps checked against input order. +To draw frames one at a time, call `video.frame()`, then `video.finish()`, or `video.abort()` to +stop. ## Formats @@ -494,16 +380,10 @@ an object keyed by its method name, with its parameters by name: } ``` -- `replace` and `set` take `{ table, ids, fields }`, `remove` `{ table, ids }`, `append` - `{ at, table, ids?, fields }`, `forget` `{ before }`, `axis` `{ name, unit }`, and `frames` - `{ table, field, level?, first, at, values }`, a run of a sampled field's entries as stored in a - [time tile](#fetch-long-histories-in-spans). -- `ids` are numbers or strings, or `{ "count": n }`; a `null` id in a reference leaves that row - unconnected. -- A field is an array of one kind of value, a reference `{ reference, ids }`, or a slice of a binary - file beside the document, `{ file, dtype, offset, count }`: `count` values starting at byte - `offset`, of dtype `f32le`, `f64le`, `i32le`, `u32le`, `u64le`, `u8` (numbers), or `bool` (one - byte each, nonzero is true). +A field is an array of one kind of value, a reference `{ reference, ids }`, or a slice of a binary +file beside the document, `{ file, dtype, offset, count }`: `count` values starting at byte +`offset`, of dtype `f32le`, `f64le`, `i32le`, `u32le`, `u64le`, `u8` (numbers), or `bool` (one byte +each, nonzero is true). A `null` id in a reference leaves that row unconnected. (bytes)= @@ -531,31 +411,23 @@ endian. | text | `u32` byte length, UTF-8 | Bits are packed into `u64` words, low bit first. A sampled field's entries are frames at level 0, -and buckets of `8^k` frames at level `k`. A bucket is left out when all eight entries one level -below it are stored, since it can be computed from them. Writers outside latkit should record -frames as `append` changes. +and buckets of `8^k` frames at level `k`, from 0 to 21. A bucket is left out when all eight entries +one level below it are stored, since it can be computed from them. Writers outside latkit should +record frames as `append` changes. The header is 12 bytes: the magic `LATK` and the frame's length. Readers reject other bytes, unknown operation or storage tags, truncated frames, and trailing bytes. The format carries no version: a release that changes it says so, and files written before it no longer open. Rust and JavaScript tests both check the committed `patch.hex` fixture. -Absolute frame indexes are `u64`; counts within an encoded run are `u32`. A run's end and its span -in frames must fit in `u64`. JavaScript builders accept only safe integers, including the run's end -(`2^53 - 1` at most). Levels 0 through 21 are valid. The writer records the highest stored level, so -a change to the renderer's detail policy does not change how a file reads. Appends currently derive -up to six levels. Tile manifests record each field's `levels`. In query and pick JSON output, sample -indexes above JavaScript's safe-integer range are written as decimal strings. - -[Stream](#stream) describes the limits `Patch.stream` applies. In Rust, `Patch::from_bytes` decodes -with the default `DecodeLimits`: a 256 MiB frame and 512 MiB of decoded storage (a conservative -estimate). `Patch::stream(read, limits)` takes other limits. Applying patches and keeping snapshots -cost memory beyond decoding. +Absolute frame indexes are `u64`; counts within an encoded run are `u32`. JavaScript builders +accept only safe integers. In Rust, `Patch::from_bytes` decodes with the default `DecodeLimits`: a +256 MiB frame and 512 MiB of decoded storage; `Patch::stream(read, limits)` takes others. ## Deploying Install `latkit` from npm for JavaScript, and `latkit-cli` from Cargo for the `latkit` command. -The Rust library is the `latkit` crate; the crate that binds it to WebAssembly is internal. +The Rust library is the `latkit` crate. | Capability | Browser | Node | Native CLI | | ----------------------------------- | ------------------------------ | -------------------------------- | ---------------------------- | @@ -565,15 +437,14 @@ The Rust library is the `latkit` crate; the crate that binds it to WebAssembly i | File/URL slices | Browser fetch and CORS | Supply data directly | Local paths and `file:` URLs | | H.264 MP4 | WebCodecs Constrained Baseline | Use the CLI | Windows Media Foundation | -Serve WASM with `Content-Type: application/wasm`. WebGPU requires HTTPS or localhost and a -supported device. Serve the JavaScript, WASM, and worker from the same package version. Where native -MP4 encoding is unavailable, the CLI still writes images and raw frames. +Serve WASM with `Content-Type: application/wasm`, and the JavaScript, WASM, and worker from the +same package version. WebGPU requires HTTPS or localhost and a supported device. ### Assets and workers By default, `load()` finds the WASM and worker files next to the installed modules. With Vite or -another bundler that supports URL imports, you can pass their URLs explicitly, which also works under -nested deployment paths: +another bundler that supports URL imports, pass their URLs, which also works under nested +deployment paths: ```ts import { load } from 'latkit'; @@ -583,15 +454,10 @@ import workerUrl from 'latkit/worker?url'; await load({ wasm: wasmUrl, worker: () => new Worker(workerUrl) }); ``` -`latkit/worker` is a standalone classic worker. A custom factory may use a Blob URL; keep the URL -valid while the factory can be called, and revoke it when the application shuts down. Workers start -on demand and stop after 30 seconds idle. Where workers are unavailable, jobs run on the calling -thread. - -Your Content Security Policy must allow the WASM asset and data origins in `connect-src`, -WebAssembly compilation (typically `'wasm-unsafe-eval'` in `script-src`), and the worker URL in -`worker-src`. Add `blob:` only when using Blob workers. Cross-origin resources need CORS headers. -latkit never changes your policy. +`latkit/worker` is a standalone classic worker, fine as a Blob URL. Workers start on demand and +stop after 30 seconds idle; where workers are unavailable, jobs run on the calling thread. Your +Content Security Policy must allow the WASM asset and data origins in `connect-src`, WebAssembly +compilation (typically `'wasm-unsafe-eval'` in `script-src`), and the worker URL in `worker-src`. ### Server-side builders and data @@ -604,10 +470,7 @@ const expression = field('value').gt(0).toJSON(); Patches and expressions need no WebAssembly. To use datasets and queries in Node, compile the package's WASM bytes into a `WebAssembly.Module` and pass it to `load({ wasm })`. To render in a -worker, pass an `OffscreenCanvas` to `gpu.canvas`, and call `gpu.step(now)` yourself if the -environment has no animation-frame loop. - -See [resource lifetimes](#stats-and-memory) and [video cleanup](#video). +worker, pass an `OffscreenCanvas` to `gpu.canvas`. ## In Rust @@ -628,16 +491,10 @@ let saved = grid.snapshot().to_bytes(); let restored = Data::read(&saved, Reading::default()).await?; ``` -A `Data` changes in place, and its clones share it; a view drawing it draws its latest version, and -a `Snapshot` keeps one version. `Data::read` and `Patch::read` take a `Reading`: the dataset's name, -the base its slices are relative to, and the `resource::Fetch` that fetches them. A `Fetch` -implements `fetch(&Request) -> Pending>`; `Pending::ready` wraps an immediate result, and -`Pending::new` returns a pending value with a resolver for asynchronous work. Every rendering -`Host` is also a `Fetch`, so the native and browser hosts read slices directly. - +`Data::read` and `Patch::read` take a `Reading`: the dataset's name, the base its slices are +relative to, and the `resource::Fetch` that fetches them. Every rendering `Host` is also a `Fetch`. Enable `gpu` for scenes, views, and wgpu rendering; `native` adds system fonts, jobs, files, and -Windows H.264. `web` enables browser integration for the WASM target. Native file loading includes -no HTTP client; applications can provide one through `Fetch` or `Host`. +Windows H.264; `web` adds browser integration for the WASM target. ### Share an existing device @@ -649,10 +506,6 @@ device.set_device_lost_callback(move |_, _| loss.lost()); device.on_uncaptured_error(std::sync::Arc::new(move |error| events.error(&error))); ``` -Forward device loss and uncaptured errors from your application's own callbacks, as above. -`from_wgpu` checks the device's limits and leaves its callbacks to you. Clone one `Gpu` for views -that should share caches and a budget. `Gpu::new` instead creates its own device and installs its -own callbacks. After device loss, create a new device and views; datasets and scenes can be reused. - -The [Rust API reference](https://docs.rs/latkit) documents each type, method, feature, and example. -The [format specification](#bytes) describes cross-language LATK storage. +`from_wgpu` checks the device's limits and leaves its callbacks to you, so forward device loss and +uncaptured errors from your own, as above. Clone one `Gpu` for views that should share caches and a +budget. The [Rust API reference](https://docs.rs/latkit) documents each type, feature, and example. diff --git a/docs/reference/api.md b/docs/reference/api.md index 1f7426a9..e63c4d9b 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -1,48 +1,70 @@ # JavaScript API -Every class, function, and type the `latkit` package exports. [`load()`](#load) loads the WebAssembly -that datasets, views, and pictures need; scenes, expressions, and patches need none. Scenes and -their options take the [scene and result types](types.md). +Every class, function, and type the `latkit` package exports. Scenes and their options take the +[scene and result types](types.md). Four rules hold throughout, so the entries below leave them +out: -## Loading and waiting +- **Loading.** Scenes, expressions, and patches are plain data. Datasets, views, pictures, and the + colormap functions need latkit's WebAssembly: until [`load()`](#load) finishes, they throw an + `unloaded` error. [`Gpu.create()`](#gpu-create), [`Data.read()`](#data-read), and [`Patch.read()`](#patch-read) of JSON load it + themselves. +- **Errors.** Everything latkit throws or rejects with is a [`LatkitError`](#latkiterror). Input it cannot use + fails with an `invalid` error whose `path` names the part, such as + `marks[2].vertices.color`, and changes nothing. +- **Waits.** Every promise waits within its [`Wait`](#wait): until its `signal` aborts, or 30 seconds. +- **Lifetimes.** A [`Handle`](#handle) holds memory until `destroy()`, or a `using` declaration, frees it. -Load the WebAssembly once per page. Every wait ends when its signal aborts, after 30 seconds by default. +## Loading, errors, and lifetimes + +What every part of the package shares. (load)= ### `load()` {.signature} -`function load(options?:` [`LoadOptions`](#loadoptions)`): Promise` +`function load(options?: { wasm?: string | URL | WebAssembly.Module; worker?: () => Worker }): Promise` + +Loads latkit's WebAssembly, once per page. Later and concurrent calls share the first call's +load; options they pass must equal the first call's, or they reject with an `io` error. A failed +load rejects with an `io` error and can be retried. `load()` also requests a GPU adapter while +the WebAssembly compiles, so the browser starts its GPU process early, then the first device for +[`Gpu.create()`](#gpu-create). -Loads latkit's WebAssembly, once per page. Datasets, views, and pictures can be created once it -resolves. Later and concurrent calls share the first call's load. Options they pass must equal -the first call's, or they reject with an `io` error. A failed load rejects with an `io` error and -can be retried. +{#load-wasm .member} +**`wasm`**`?: string | URL | WebAssembly.Module` [¶](#load-wasm){.headerlink} +: The URL of this version's `latkit/wasm` asset, or that asset compiled; bundled by default. -Importing the package loads nothing and requests no GPU. `load()` requests a GPU adapter while -the WebAssembly compiles, so the browser starts its GPU process early, then requests the first -GPU device for [`Gpu.create()`](#gpu-create). `Gpu.create()` and [`Data.read()`](#data-read) call `load()` themselves, as does -[`Patch.read()`](#patch-read) for JSON. +{#load-worker .member} +**`worker`**`?: () => Worker` [¶](#load-worker){.headerlink} +: Creates a worker running the standalone `latkit/worker` bundle; bundled by default. -(loadoptions)= +(latkiterror)= -### `LoadOptions` +### `LatkitError` {.signature} -`interface LoadOptions` +`interface LatkitError extends Error` -Options for [`load()`](#load). Only the first call's options take effect. +The error latkit throws: an `Error` whose `code` says what kind of failure it is. -{#loadoptions-wasm .member} -**`wasm`**`?: string | URL | WebAssembly.Module` [¶](#loadoptions-wasm){.headerlink} -: The URL of this package version's `latkit/wasm` asset, or that asset already compiled. - Defaults to the bundled asset. +{#latkiterror-code .member} +**`code`**`:` [`Failure`](#failure)`['code'] | 'unloaded' | 'destroyed'` [¶](#latkiterror-code){.headerlink} +: The kind of failure: + + - `invalid`: the input is malformed or does not fit; `path` says where. + - `unsupported`: the browser or device cannot do what was asked. + - `limit`: a budget or device limit would be exceeded. + - `io`: reading or writing external bytes failed, such as loading latkit or fetching a file. + - `device-lost`: the GPU device was lost; create a new [`Gpu`](#gpu). + - `cancelled`: the wait's signal aborted, or the work was cancelled. + - `timeout`: the wait's signal timed out. + - `unloaded`: the call needs WebAssembly, and [`load()`](#load) has not finished. + - `destroyed`: the object was destroyed, so it can no longer be used. -{#loadoptions-worker .member} -**`worker`**`?: () => Worker` [¶](#loadoptions-worker){.headerlink} -: Creates a worker that runs the standalone `latkit/worker` bundle. Defaults to the bundled - worker. +{#latkiterror-path .member} +**`path`**`?: string` [¶](#latkiterror-path){.headerlink} +: The invalid part of the input, such as `marks[2].vertices.color`, on `invalid` errors. (wait)= @@ -59,48 +81,89 @@ Options that bound how long a promise waits. `cancelled` error whose `cause` is the abort reason. Either way, latkit cancels the call's pending work where it can. Without a signal, each wait times out after 30 seconds. -(latkiterror)= +(handle)= -### `LatkitError` +### `Handle` {.signature} -`interface LatkitError extends Error` +`abstract class Handle` -The error latkit throws: an `Error` whose `code` says what kind of failure it is. +An object that holds WebAssembly memory, and GPU memory for a [`Gpu`](#gpu), until it is destroyed: +`destroy()` frees it, as does a `using` declaration when its block ends. Once it is destroyed, +its other methods throw a `destroyed` error. -{#latkiterror-code .member} -**`code`**`:` [`Failure`](#failure)`['code'] | 'unloaded' | 'destroyed'` [¶](#latkiterror-code){.headerlink} -: The kind of failure: +```{rubric} Methods +``` - - `invalid`: the input is malformed or does not fit; `path` says where. - - `unsupported`: the browser or device cannot do what was asked. - - `limit`: a budget or device limit would be exceeded. - - `io`: reading or writing external bytes failed, such as loading latkit or fetching a file. - - `device-lost`: the GPU device was lost; create a new [`Gpu`](#gpu). - - `cancelled`: the wait's signal aborted, or the work was cancelled. - - `timeout`: the wait's signal timed out. - - `unloaded`: the call needs WebAssembly, and [`load()`](#load) has not finished. - - `destroyed`: the object was destroyed, so it can no longer be used. - -{#latkiterror-path .member} -**`path`**`?: string` [¶](#latkiterror-path){.headerlink} -: The invalid part of the input, such as `marks[2].vertices.color`, on `invalid` errors. +{#handle-destroy .member} +**`destroy`**`(): void` [¶](#handle-destroy){.headerlink} +: Frees the object's memory. Calling it again does nothing. ## Data and patches Datasets of named tables, the patches that change them, written without WebAssembly, and the sources views draw. +(dataset)= + +### `Dataset` + +{.signature} +`abstract class Dataset extends` [`Handle`](#handle) + +Named tables of columns, read the same way from a [`Data`](#data), which reads its current version, +and a [`Snapshot`](#snapshot), which reads its own. + +```{rubric} Properties +``` + +{#dataset-name .member} +`readonly` **`name`**`: string` [¶](#dataset-name){.headerlink} +: The dataset's name, by which a mark's `data` option draws it. + +```{rubric} Methods +``` + +{#dataset-describe .member} +**`describe`**`():` [`Description`](#description) [¶](#dataset-describe){.headerlink} +: The tables and their row counts, each field and its kind, and the axis and its span. + +{#dataset-get .member} +**`get`**`(table: string, id:` [`Id`](#id)`, field: string, options?: { at?: number }):` [`Value`](#value) [¶](#dataset-get){.headerlink} +: Reads `field` of the row with `id` in `table`: for a sampled field, its last frame at or before + `options.at`, or its last frame; for a reference, the referenced row's id. `null` when the + table, row, field, or value is missing. + + {#dataset-get-at .member} + **`at`**`?: number` [¶](#dataset-get-at){.headerlink} + : The axis coordinate to read a sampled field at. + +{#dataset-query .member} +**`query`**`(query:` [`Query`](#query)`):` [`QueryResult`](#queryresult) [¶](#dataset-query){.headerlink} +: Reads a table's rows, filtered, sorted, and paged, through the same expressions marks use. + Sampled fields are read at [`query.at`](#query-at). + +{#dataset-envelope .member} +**`envelope`**`(request:` [`EnvelopeRequest`](#enveloperequest)`):` [`Envelope`](#envelope) [¶](#dataset-envelope){.headerlink} +: Each row's minimum and maximum of a sampled field over equal spans of an axis range, for every + row or the rows `request.ids` lists. Each span reads the coarsest stored levels of detail that + fit in it, so it costs a few reads however many frames it covers. Throws a `limit` error when + rows times spans exceeds 2^26. + +{#dataset-tobytes .member} +**`toBytes`**`(): Uint8Array` [¶](#dataset-tobytes){.headerlink} +: The dataset as latkit's bytes: a patch that builds it from an empty dataset. + (data)= ### `Data` {.signature} -`class Data` +`class Data extends` [`Dataset`](#dataset) -A dataset that changes in place: named tables of columns, changed only by patches. A patch -applies completely or not at all. Views given a `Data` draw its latest version, from the frame -after each patch. A [`Snapshot`](#snapshot) keeps one version and shares its pages with the dataset, so -keeping earlier versions, as an undo history does, costs only the pages that differ. +A dataset that changes in place, only by patches, each applied whole or not at all. Views draw +its latest version from the frame after each patch. A [`Snapshot`](#snapshot) keeps one version and +shares its pages with the dataset, so an undo history costs only the pages that differ. +Destroying it leaves views drawing it and its snapshots usable. ```ts const grid = new Data('grid').apply(new Patch().replace('Bus', [1, 2], { kv: [230, 115] })); @@ -113,40 +176,34 @@ grid.apply(new Patch().set('Bus', [2], { kv: [69] })); // drawn next frame {#data-new .member} **`new Data`**`(name: string)` [¶](#data-new){.headerlink} -: Creates an empty dataset named `name`. Throws an `unloaded` error before [`load()`](#load) finishes. - -```{rubric} Properties -``` - -{#data-name .member} -`readonly` **`name`**`: string` [¶](#data-name){.headerlink} -: The dataset's name, by which a mark's `data` option draws it. +: Creates an empty dataset named `name`. ```{rubric} Methods ``` {#data-read .member} -`static` **`read`**`(source:` [`DataSource`](#datasource)`, options?: { name?: string; base?: string } &` [`Wait`](#wait)`): Promise` [¶](#data-read){.headerlink} -: Reads a dataset from `source`, calling [`load()`](#load) first. Its first bytes decide the format: +`static` **`read`**`(source:` [`ByteSource`](#bytesource) `| object, options?: { name?: string; base?: string } &` [`Wait`](#wait)`): Promise` [¶](#data-read){.headerlink} +: Reads a dataset from `source`. Its first bytes decide the format: - latkit's bytes, as `toBytes()` writes them. - GeoJSON, plain or gzip-compressed, read in a worker. Points, line strings, and polygons become rows of tables `points`, `lines`, and `polygons`, with the geometry in field - `geometry` and each property as a field. GeoJSON requires `options.name`. + `geometry` and each property as a field. - A dataset document, `{"name": ..., "patch": [...]}`, whose patch is read as [`Patch.read()`](#patch-read) reads one. - `options.name` names the dataset, replacing the name in latkit's bytes or a document. - `options.base` is the URL that a document's file slices are relative to; it defaults to the URL - the source was fetched from. Rejects with an `invalid` error for bytes that are not a valid - dataset, and with an `io` error when a fetch fails or answers an error status. + {#data-read-name .member} + **`name`**`?: string` [¶](#data-read-name){.headerlink} + : Names the dataset, replacing the name its bytes or document give; GeoJSON requires it. + + {#data-read-base .member} + **`base`**`?: string` [¶](#data-read-base){.headerlink} + : The URL a document's file slices are relative to; by default, the one it came from. {#data-apply .member} **`apply`**`(patch:` [`Patch`](#patch) `| Uint8Array): this` [¶](#data-apply){.headerlink} -: Applies every change in `patch` (a [`Patch`](#patch) or its bytes), or none of them when one is - invalid, and returns this dataset. Views draw the new version from their next frame. Throws an - `invalid` error whose `path` names the failing change, such as `patch[1].Bus.kv`, or when the - patch names another dataset. +: Applies every change in `patch`, or none when one is invalid, and returns this dataset. An + `invalid` error's `path` names the failing change, such as `patch[1].Bus.kv`. {#data-snapshot .member} **`snapshot`**`():` [`Snapshot`](#snapshot) [¶](#data-snapshot){.headerlink} @@ -154,95 +211,17 @@ grid.apply(new Patch().set('Bus', [2], { kv: [69] })); // drawn next frame {#data-restore .member} **`restore`**`(snapshot:` [`Snapshot`](#snapshot)`): this` [¶](#data-restore){.headerlink} -: Makes `snapshot`, a version of this dataset, the current version, as an undo does. Views draw - it from their next frame. Throws an `invalid` error for a snapshot of a dataset with another - name. - -{#data-describe .member} -**`describe`**`():` [`Description`](#description) [¶](#data-describe){.headerlink} -: Describes the current version; see [`Snapshot.describe()`](#snapshot-describe). - -{#data-get .member} -**`get`**`(table: string, id:` [`Id`](#id)`, field: string, options?: { at?: number }):` [`Value`](#value) [¶](#data-get){.headerlink} -: Reads one value of the current version; see [`Snapshot.get()`](#snapshot-get). - -{#data-query .member} -**`query`**`(query:` [`Query`](#query)`):` [`QueryResult`](#queryresult) [¶](#data-query){.headerlink} -: Queries the current version; see [`Snapshot.query()`](#snapshot-query). - -{#data-envelope .member} -**`envelope`**`(request:` [`EnvelopeRequest`](#enveloperequest)`):` [`Envelope`](#envelope) [¶](#data-envelope){.headerlink} -: Per-row minimums and maximums of the current version; see [`Snapshot.envelope()`](#snapshot-envelope). - -{#data-tobytes .member} -**`toBytes`**`(): Uint8Array` [¶](#data-tobytes){.headerlink} -: The current version as latkit's bytes: a patch that builds it from an empty dataset. - -{#data-destroy .member} -**`destroy`**`(): void` [¶](#data-destroy){.headerlink} -: Frees this handle. Views drawing the dataset keep drawing it, and its snapshots stay usable. - Other methods throw afterwards. - -{#data-symbol-dispose .member} -**`[Symbol.dispose]`**`(): void` [¶](#data-symbol-dispose){.headerlink} -: Calls [`Data.destroy()`](#data-destroy), so a `using` declaration destroys it when its block ends. +: Makes `snapshot`, a version of this dataset, the current version, as an undo does. (snapshot)= ### `Snapshot` {.signature} -`class Snapshot` +`class Snapshot extends` [`Dataset`](#dataset) One immutable version of a dataset, taken in O(1) by [`Data.snapshot()`](#data-snapshot). A view given a -snapshot draws that version unchanged. - -```{rubric} Properties -``` - -{#snapshot-name .member} -`readonly` **`name`**`: string` [¶](#snapshot-name){.headerlink} -: The name of the dataset this is a version of. - -```{rubric} Methods -``` - -{#snapshot-describe .member} -**`describe`**`():` [`Description`](#description) [¶](#snapshot-describe){.headerlink} -: Describes the dataset: its tables and their row counts, each field and its kind, and the axis - and the span its sampled fields cover. - -{#snapshot-get .member} -**`get`**`(table: string, id:` [`Id`](#id)`, field: string, options?: { at?: number }):` [`Value`](#value) [¶](#snapshot-get){.headerlink} -: Reads `field` of the row with `id` in `table`. A sampled field reads its last frame at or - before `options.at`, or its last frame when `at` is omitted. A reference reads the referenced - row's id. Returns `null` when the table, row, field, or value is missing. - -{#snapshot-query .member} -**`query`**`(query:` [`Query`](#query)`):` [`QueryResult`](#queryresult) [¶](#snapshot-query){.headerlink} -: Runs `query` on the CPU: reads a table's rows, filtered, sorted, and paged, through the same - expressions marks use. Sampled fields are read at [`query.at`](#query-at). Throws an `invalid` error for an - invalid query. - -{#snapshot-envelope .member} -**`envelope`**`(request:` [`EnvelopeRequest`](#enveloperequest)`):` [`Envelope`](#envelope) [¶](#snapshot-envelope){.headerlink} -: Computes each row's minimum and maximum of a sampled field over equal spans of an axis range, - for every row or the rows `request.ids` lists. Each span reads the coarsest stored levels of - detail that fit in it, so it costs a few reads regardless of how many frames it covers. Throws - an `invalid` error for an unknown table or row, or a field that is not sampled, and a `limit` - error when rows times spans exceeds 2^26. - -{#snapshot-tobytes .member} -**`toBytes`**`(): Uint8Array` [¶](#snapshot-tobytes){.headerlink} -: This version as latkit's bytes: a patch that builds it from an empty dataset. - -{#snapshot-destroy .member} -**`destroy`**`(): void` [¶](#snapshot-destroy){.headerlink} -: Frees this handle. Views given the snapshot keep drawing it; other methods throw afterwards. - -{#snapshot-symbol-dispose .member} -**`[Symbol.dispose]`**`(): void` [¶](#snapshot-symbol-dispose){.headerlink} -: Calls [`Snapshot.destroy()`](#snapshot-destroy), so a `using` declaration destroys it when its block ends. +snapshot draws that version, and keeps drawing it once the snapshot is destroyed. (tiles)= @@ -251,18 +230,17 @@ snapshot draws that version unchanged. {.signature} `class Tiles` -A dataset fetched in pieces as views need them. A `Tiles` is plain data: a dataset name and a -URL. Each view draws the pieces its camera and plot window need as a dataset of its own, and all -views of a GPU share the fetched pieces. The first bytes at `url` decide the kind of tiles: +A dataset fetched in pieces as views need them: plain data, a dataset name and a URL. Each view +draws the pieces its camera and plot window need, and the views of a GPU share them. The first +bytes at `url` decide the kind: -- Time tiles: a `frames.json` manifest as `latkit tile` writes it, for a dataset whose sampled - fields' histories are fetched in spans. A line tracing a field fetches the level of detail its - plot window shows; other marks fetch the frames around the view's `at`. A span still loading - shows the coarser span that contains it. -- Vector tiles: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` URL template, fetched - as the camera needs them. Each layer becomes a table of features of the kind its marks draw: - polygons for areas, lines for lines, points for vertices. A table named `layer:polygons`, - `layer:lines`, or `layer:points` holds that kind regardless of the marks. +- Time tiles: a `frames.json` manifest as `latkit tile` writes it. A line tracing a sampled field + fetches the detail its plot window shows; other marks fetch the frames around the view's `at`. + A span still loading shows the coarser span that contains it. +- Vector tiles: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` URL template. Each + layer becomes a table of the features its marks draw: polygons for areas, lines for lines, + points for vertices. A table named `layer:polygons`, `layer:lines`, or `layer:points` holds + that kind whatever the marks. ```{rubric} Constructor ``` @@ -276,12 +254,11 @@ views of a GPU share the fetched pieces. The first bytes at `url` decide the kin {#tiles-name .member} `readonly` **`name`**`: string` [¶](#tiles-name){.headerlink} -: The name of the dataset the tiles are drawn as, by which a mark's `data` option draws it. +: The name of the dataset the tiles are drawn as. {#tiles-url .member} `readonly` **`url`**`: string` [¶](#tiles-url){.headerlink} -: Where the tiles are: a `frames.json` manifest, a PMTiles archive, a TileJSON document, or a - `{z}/{x}/{y}` URL template. +: Where the tiles are. (source)= @@ -292,28 +269,6 @@ views of a GPU share the fetched pieces. The first bytes at `url` decide the kin A dataset a view draws; views match it to the scene's marks by the dataset's name. -{#source-data .member} -[`Data`](#data) [¶](#source-data){.headerlink} -: A dataset that changes in place: named tables of columns, changed only by patches. - -{#source-snapshot .member} -[`Snapshot`](#snapshot) [¶](#source-snapshot){.headerlink} -: One immutable version of a dataset, taken in O(1) by [`Data.snapshot()`](#data-snapshot). - -{#source-tiles .member} -[`Tiles`](#tiles) [¶](#source-tiles){.headerlink} -: A dataset fetched in pieces as views need them. - -(datasource)= - -### `DataSource` - -{.signature} -`type DataSource = Uint8Array | ArrayBuffer | Blob | Response | ReadableStream | string | object` - -What [`Data.read()`](#data-read) accepts: bytes, a `Blob`, a `Response`, a byte stream, a URL to fetch as -a string, or a parsed JSON value. The content is latkit's bytes, GeoJSON, or a dataset document. - (queryresult)= ### `QueryResult` @@ -356,8 +311,7 @@ Each row's minimum and maximum of a sampled field over equal spans of the axis. {#envelope-least .member} **`least`**`: Float32Array` [¶](#envelope-least){.headerlink} -: Each row's minimum in each span: `spans` values per row, row after row. NaN where a span has - no frame. +: Each row's minimum in each span, `spans` values per row, row after row; NaN without frames. {#envelope-greatest .member} **`greatest`**`: Float32Array` [¶](#envelope-greatest){.headerlink} @@ -370,10 +324,9 @@ Each row's minimum and maximum of a sampled field over equal spans of the axis. {.signature} `class Patch` -A list of changes to a dataset, which [`Data.apply`](#data-apply) applies in order, all or none. The patch is -encoded as latkit's bytes as it is built, without WebAssembly. Each method adds a change and -returns the patch. For values it cannot encode, a method throws an `invalid` error with a `path` -such as `Bus.kv`, and leaves the patch unchanged. +A list of changes to a dataset, which [`Data.apply`](#data-apply) applies in order, all or none. It is written +as latkit's bytes as it is built, without WebAssembly; each method adds a change and returns the +patch. ```ts const patch = new Patch('grid') @@ -400,32 +353,38 @@ const patch = new Patch('grid') ``` {#patch-read .member} -`static` **`read`**`(source:` [`PatchSource`](#patchsource)`, options?: { base?: string; limit?: number } &` [`Wait`](#wait)`): Promise` [¶](#patch-read){.headerlink} +`static` **`read`**`(source:` [`ByteSource`](#bytesource) `| object, options?: { base?: string; limit?: number } &` [`Wait`](#wait)`): Promise` [¶](#patch-read){.headerlink} : Reads a patch from `source`: latkit's bytes, one frame as `toBytes()` writes it, or JSON that any language can write without latkit. The JSON is a list of changes. Each change is keyed by its method name and holds the method's parameters by name, such as `{"set": {"table": "Bus", "ids": [1], "fields": {"kv": [345]}}}`. A field may be a slice of a - binary file, `{file, dtype, offset, count}`. Slices are fetched at once, relative to - `options.base`, which defaults to the URL the JSON was fetched from. + binary file, `{file, dtype, offset, count}`, fetched at once. Bytes are read without + WebAssembly. + + {#patch-read-base .member} + **`base`**`?: string` [¶](#patch-read-base){.headerlink} + : The URL the JSON's file slices are relative to; by default, the one it came from. - Bytes are read without WebAssembly; JSON calls [`load()`](#load) first. Rejects with a `limit` error for - a frame longer than `options.limit` bytes (256 MiB by default), with an `invalid` error for - malformed input, and with an `io` error when a fetch fails or answers an error status. + {#patch-read-limit .member} + **`limit`**`?: number` [¶](#patch-read-limit){.headerlink} + : The longest frame in bytes, 256 MiB by default; a longer one fails with a `limit` error. {#patch-stream .member} -`static` **`stream`**`(stream:` [`ReadableBytes`](#readablebytes) `| AsyncIterable, { limit }?: { limit?: number }): AsyncGenerator` [¶](#patch-stream){.headerlink} +`static` **`stream`**`(stream:` [`ReadableBytes`](#readablebytes) `| AsyncIterable, options?: { limit?: number }): AsyncGenerator` [¶](#patch-stream){.headerlink} : Yields each patch in `stream` as soon as its last byte arrives. The stream carries frames back - to back, such as a response body, a file, or a socket's messages. Each frame states its length - up front, so a frame longer than `options.limit` bytes (256 MiB by default) throws a `limit` - error before its body is allocated. Besides the current chunk and the patches the caller keeps, - at most one frame is buffered. Breaking out of the loop cancels the stream. Throws an `invalid` - error for a `limit` that is not a whole number of at least 12 bytes, for bytes that are not - latkit's, or for a stream that ends inside a frame. + to back, such as a response body, a file, or a socket's messages. Besides the current chunk + and the patches the caller keeps, at most one frame is buffered. Breaking out of the loop + cancels the stream. Throws an `invalid` error for a stream that ends inside a frame. ```ts for await (const patch of Patch.stream((await fetch('/feed')).body!)) data.apply(patch); ``` + {#patch-stream-limit .member} + **`limit`**`?: number` [¶](#patch-stream-limit){.headerlink} + : The longest frame in bytes, at least 12; 256 MiB by default. Each frame states its length + up front, so a longer one fails with a `limit` error before its body is allocated. + {#patch-axis .member} **`axis`**`(name: string, unit: string): this` [¶](#patch-axis){.headerlink} : Sets the name and unit of the axis that sampled fields are recorded over, such as @@ -445,31 +404,43 @@ const patch = new Patch('grid') : Removes the rows of `table` with `ids`. {#patch-append .member} -**`append`**`(table: string, at: number | readonly number[], fields: Readonly>, { ids }?: { ids?:` [`RowIds`](#rowids) `}): this` [¶](#patch-append){.headerlink} +**`append`**`(table: string, at: number | readonly number[], fields: Readonly>, options?: { ids?:` [`RowIds`](#rowids) `}): this` [¶](#patch-append){.headerlink} : Records a frame of `table` at each axis coordinate in `at`. Each field gives a value per row - per frame, frame by frame: every row of the first frame, then of the next. Frames cover every - row, or only rows `options.ids`; other rows keep their last values. A field's first frame makes - it a sampled field. Consecutive appends of `Float32Array` or `Float64Array` values to the same - fields of every row are written as one change, so one frame per call costs no more than one - call with every frame. + per frame, frame by frame. A field's first frame makes it a sampled field. Consecutive appends + of typed float arrays to the same fields of every row write as one change, so a frame per call + costs no more than one call with every frame. + + {#patch-append-ids .member} + **`ids`**`?:` [`RowIds`](#rowids) [¶](#patch-append-ids){.headerlink} + : The rows the frames cover, every row by default; other rows keep their last values. {#patch-forget .member} **`forget`**`(before: number): this` [¶](#patch-forget){.headerlink} : Drops every sampled field's frames before axis coordinate `before`. {#patch-frames .member} -**`frames`**`(table: string, field: string, { level, first, at, values }: { level?: number; first: number; at: Float64Array | readonly number[]; values: Float32Array | readonly number[] }): this` [¶](#patch-frames){.headerlink} -: Stores a run of entries of sampled field `field` of `table`, at level of detail `level` (0 by - default), numbered from `first`. This is what a time tile holds, as `latkit tile` writes it. +**`frames`**`(table: string, field: string, run: { level?: number; first: number; at: Float64Array | readonly number[]; values: Float32Array | readonly number[] }): this` [¶](#patch-frames){.headerlink} +: Stores a run of entries of sampled field `field` of `table`, at one level of detail. This is + what a time tile holds, as `latkit tile` writes it. At level 0 each entry is a frame; at level + `k`, a bucket of `8^k` frames. None of the entries may be stored already. Entries past the + field's last frame extend it. + + {#patch-frames-level .member} + **`level`**`?: number` [¶](#patch-frames-level){.headerlink} + : The level of detail, from 0 to 21; 0 by default. - At level 0 each entry is a frame: a coordinate in `at`, and a value per row in `values`, entry - by entry. At level `k` each entry is a bucket of `8^k` frames: its first and last frames' - coordinates in `at`. `values` then holds every bucket's per-row minimums, entry by entry, - followed by every bucket's per-row maximums. NaN marks a missing value. + {#patch-frames-first .member} + **`first`**`: number` [¶](#patch-frames-first){.headerlink} + : The first entry's number: a safe nonnegative integer. - None of the entries may be stored already. Entries past the field's last frame extend it. - Throws an `invalid` error for a level outside 0 to 21, lengths of `at` and `values` that do not - divide into whole entries and rows, or frame numbers that are not safe nonnegative integers. + {#patch-frames-at .member} + **`at`**`: Float64Array | readonly number[]` [¶](#patch-frames-at){.headerlink} + : Each frame's coordinate, or each bucket's first and last frames' coordinates. + + {#patch-frames-values .member} + **`values`**`: Float32Array | readonly number[]` [¶](#patch-frames-values){.headerlink} + : A value per row, entry by entry; at a bucket level, every bucket's per-row minimums, then + every bucket's per-row maximums. NaN marks a missing value. {#patch-extend .member} **`extend`**`(next: Patch): this` [¶](#patch-extend){.headerlink} @@ -479,15 +450,29 @@ const patch = new Patch('grid') **`toBytes`**`(): Uint8Array` [¶](#patch-tobytes){.headerlink} : A copy of the patch as latkit's bytes: one frame, naming the dataset if the patch does. -(patchsource)= +(bytesource)= + +### `ByteSource` + +{.signature} +`type ByteSource = Uint8Array | ArrayBuffer | Blob | Response |` [`ReadableBytes`](#readablebytes) `| AsyncIterable | string` + +Bytes to read: an array or buffer, a `Blob`, a `Response`, a byte stream, or a URL to fetch as a +string. Reading waits within its [`Wait`](#wait), and fails with an `io` error when a fetch fails or +answers an error status, or the bytes cannot be read. -### `PatchSource` +(readablebytes)= + +### `ReadableBytes` {.signature} -`type PatchSource = Uint8Array | ArrayBuffer | Blob | Response | ReadableStream | string | object` +`interface ReadableBytes` + +A byte stream: anything shaped like a browser or Node.js `ReadableStream` of `Uint8Array`. -What [`Patch.read()`](#patch-read) accepts: bytes, a `Blob`, a `Response`, a byte stream, a URL to fetch as a -string, or a parsed JSON value. The content is latkit's bytes or a patch's JSON. +{#readablebytes-getreader .member} +**`getReader`**`(): { read(): Promise<{ done: boolean; value?: Uint8Array }>; cancel(reason?: unknown): Promise; releaseLock(): void }` [¶](#readablebytes-getreader){.headerlink} +: A reader of the stream's chunks, which latkit reads to the end, or cancels. (rowids)= @@ -499,10 +484,6 @@ string, or a parsed JSON value. The content is latkit's bytes or a patch's JSON. Row ids: a list of numbers or strings, `{ count }` for ids `0` to `count - 1`, or a `Uint32Array` or `Float64Array` of numbers. -{#rowids-ids .member} -[`Ids`](#ids) [¶](#rowids-ids){.headerlink} -: Row ids: a list of numbers or texts, or `{count}` for rows 0 to count - 1. - (values)= ### `Values` @@ -559,19 +540,6 @@ Builds a [`Reference`](#reference) field: for each row, the id of the row of `ta A table's fields by name: values, or a [`Reference`](#reference). -(readablebytes)= - -### `ReadableBytes` - -{.signature} -`interface ReadableBytes` - -A byte stream: anything shaped like a browser or Node.js `ReadableStream` of `Uint8Array`. - -{#readablebytes-getreader .member} -**`getReader`**`(): { read(): Promise<{ done: boolean; value?: Uint8Array }>; cancel(reason?: unknown): Promise; releaseLock(): void }` [¶](#readablebytes-getreader){.headerlink} -: A reader of the stream's chunks, which latkit reads to the end, or cancels. - ## Expressions Builders for the per-row expressions and colors that scene options take. @@ -747,14 +715,6 @@ A value for every row of a mark: a constant, a field, view state, or arithmetic A value option: an [`Expr`](#expr), or its JSON form, in which a string is a field name. -{#exprinput-expr .member} -[`Expr`](#expr) [¶](#exprinput-expr){.headerlink} -: A value for every row of a mark: a constant, a field, view state, or arithmetic on them. - -{#exprinput-exprnode .member} -[`ExprNode`](#exprnode) [¶](#exprinput-exprnode){.headerlink} -: A value for each row: a number, a boolean, a field's name, or a node that computes one. - (mapped)= ### `Mapped` @@ -842,18 +802,6 @@ choosing between colors. Color options take a `Paint` or a CSS color string. A color option: a CSS color, a [`Paint`](#paint), or its JSON form. A string here is always a CSS color, never a field name; map a field to colors with `field(name).colormap(map)`. -{#paintinput-color .member} -[`Color`](#color) [¶](#paintinput-color){.headerlink} -: A CSS color, such as `'#ffb000'` or `'rebeccapurple'`. - -{#paintinput-paint .member} -[`Paint`](#paint) [¶](#paintinput-paint){.headerlink} -: A color for every row of a mark: a constant, a value mapped through a colormap, or a conditional choosing between colors. - -{#paintinput-paintnode .member} -[`PaintNode`](#paintnode) [¶](#paintinput-paintnode){.headerlink} -: A color for each row: a CSS color, a value through a colormap, or a conditional choosing between colors. - (colormapped)= ### `Colormapped` @@ -1005,30 +953,13 @@ works as a theme length. `function units(size: number): { units: number }` {.signature} -`function units(size:` [`ExprInput`](#exprinput)`, within?:` [`Within`](#within)`):` [`Units`](#units) +`function units(size:` [`ExprInput`](#exprinput)`, limits?: Omit<`[`Units`](#units)`, 'units'>):` [`Units`](#units) A size in position units (degrees on the earth) regardless of the space's unit, so it scales as -the view zooms. `within` limits its on-screen size in CSS pixels: `units('r', { atLeast: 2 })` +the view zooms. `limits` bound its on-screen size in CSS pixels: `units('r', { atLeast: 2 })` grows with the map and never shrinks below 2 pixels. A marker keeps its proportions, with the limits applied to its larger side. Given only a number, the result also works as a theme length. -(within)= - -### `Within` - -{.signature} -`interface Within` - -On-screen limits, in CSS pixels, for a size given in position units. - -{#within-atleast .member} -**`atLeast`**`?: number` [¶](#within-atleast){.headerlink} -: The smallest size in CSS pixels, however far the view zooms out. - -{#within-atmost .member} -**`atMost`**`?: number` [¶](#within-atmost){.headerlink} -: The largest size in CSS pixels, however far the view zooms in. - (selected)= ### `selected()` @@ -1105,6 +1036,16 @@ RGBA pixels with straight alpha, rows top to bottom. A browser `ImageData` has t **`data`**`: Uint8ClampedArray` [¶](#rgbaimage-data){.headerlink} : 4 bytes per pixel, red, green, blue, and alpha, row after row. +(colormaps)= + +### `colormaps()` + +{.signature} +`function colormaps(): { name:` [`ColormapName`](#colormapname)`; label: string; kind:` [`ColormapKind`](#colormapkind) `}[]` + +The catalog's colormaps, in order: each one's name, its title for people, such as +`'Red / Blue'`, and its kind. + (colormapcss)= ### `colormapCss()` @@ -1114,8 +1055,7 @@ RGBA pixels with straight alpha, rows top to bottom. A browser `ImageData` has t Returns a CSS `linear-gradient(...)` with the colors the GPU draws for `map`, a catalog name or a custom colormap. `direction` is the gradient's CSS direction, `'to right'` by default. A -categorical map gives one solid band per category. Needs `await load()` first, and throws an -`invalid` error for an invalid `map`. +categorical map gives one solid band per category. ## Views @@ -1126,10 +1066,11 @@ A GPU makes views, and canvases that show them. ### `Gpu` {.signature} -`class Gpu` +`class Gpu extends` [`Handle`](#handle) A WebGPU device that views draw with. The views and canvases of one `Gpu` share its memory and -draw together in each animation frame, so linked views always show the same moment. +draw together in each animation frame, so linked views always show the same moment. Destroying +it destroys its canvases, views, and frame listeners; other GPUs and datasets stay usable. ```{rubric} Properties ``` @@ -1150,33 +1091,42 @@ draw together in each animation frame, so linked views always show the same mome {#gpu-resident .member} `readonly` **`resident`**`: number` [¶](#gpu-resident){.headerlink} -: Bytes of GPU memory the views of this GPU draw from: data pages, and each view's per-row data. +: Bytes of GPU memory its views draw from: data pages, and each view's per-row data. {#gpu-uploading .member} `readonly` **`uploading`**`: number` [¶](#gpu-uploading){.headerlink} -: Bytes waiting to upload to the GPU, across every view of this GPU. +: Bytes waiting to upload to the GPU, across its views. ```{rubric} Methods ``` {#gpu-create .member} `static` **`create`**`(): Promise` [¶](#gpu-create){.headerlink} -: Requests a WebGPU device, calling [`load()`](#load) first. The first call reuses the device that - `load()` requested. Rejects with an `unsupported` error when the browser has no WebGPU device. +: Requests a WebGPU device; the first call takes the one [`load()`](#load) requested. Rejects with an + `unsupported` error when the browser has no WebGPU device. {#gpu-view .member} -**`view`**`(scene:` [`Scene`](#scene)`, options?:` [`ViewOptions`](#viewoptions)`):` [`View`](#view) [¶](#gpu-view){.headerlink} -: Creates a view that draws `scene` from the datasets in `options.sources`. Show it in a - [`Canvas`](#canvas), or draw it into pictures with a `Shot`, with or without a canvas. Throws an - `invalid` error for an invalid scene. +**`view`**`(scene:` [`Scene`](#scene)`, options?: { sources?: readonly` [`Source`](#source)`[] }):` [`View`](#view) [¶](#gpu-view){.headerlink} +: Creates a view that draws `scene`. Show it in a [`Canvas`](#canvas), or draw it into pictures with a + [`Shot`](#shot), with or without a canvas. + + {#gpu-view-sources .member} + **`sources`**`?: readonly` [`Source`](#source)`[]` [¶](#gpu-view-sources){.headerlink} + : The datasets the scene draws; see [`View.sources`](#view-sources). {#gpu-canvas .member} -**`canvas`**`(target: HTMLCanvasElement | OffscreenCanvas, options?:` [`CanvasOptions`](#canvasoptions)`):` [`Canvas`](#canvas) [¶](#gpu-canvas){.headerlink} -: Creates a canvas that shows views in `target`, placed by `options.layout`. A `` element - sizes itself to its CSS box at the device pixel ratio, and passes pointer, wheel, and key input - to the views in its panels. An `OffscreenCanvas`, as in a worker, keeps the size it is given at - `options.pixelRatio` pixels per CSS pixel, and takes no input. Throws an `invalid` error for an - invalid layout, such as a view already shown in another panel. +**`canvas`**`(target: HTMLCanvasElement | OffscreenCanvas, options?: { layout?: readonly` [`Panel`](#panel)`[]; pixelRatio?: number }):` [`Canvas`](#canvas) [¶](#gpu-canvas){.headerlink} +: Creates a canvas that shows views in `target`. A `` element sizes itself to its CSS box + at the device pixel ratio, and passes pointer, wheel, and key input to the views in its panels. + An `OffscreenCanvas`, as in a worker, keeps the size it is given and takes no input. + + {#gpu-canvas-layout .member} + **`layout`**`?: readonly` [`Panel`](#panel)`[]` [¶](#gpu-canvas-layout){.headerlink} + : The views shown and their regions; see [`Canvas.layout`](#canvas-layout). None by default. + + {#gpu-canvas-pixelratio .member} + **`pixelRatio`**`?: number` [¶](#gpu-canvas-pixelratio){.headerlink} + : Pixels per CSS pixel of an `OffscreenCanvas`, 1 by default. {#gpu-on .member} **`on`**`(type: 'frame', listener: (now: number) => void): () => void` [¶](#gpu-on){.headerlink} @@ -1186,14 +1136,14 @@ draw together in each animation frame, so linked views always show the same mome {#gpu-step .member} **`step`**`(now: number): void` [¶](#gpu-step){.headerlink} -: Draws one frame immediately at time `now`, in milliseconds on the `requestAnimationFrame` - clock: it runs the frame listeners, then draws every canvas that needs a frame. After the first - call, this GPU stops requesting animation frames. A test or benchmark that steps every frame - itself then draws the same frames on any device. +: Draws one frame now, at `now` milliseconds on the `requestAnimationFrame` clock: it runs the + frame listeners, then draws every canvas that needs a frame. After the first call, the GPU + requests no more animation frames, so a test or benchmark that steps every frame draws the + same frames on any device. {#gpu-done .member} **`done`**`(wait?:` [`Wait`](#wait)`): Promise` [¶](#gpu-done){.headerlink} -: Resolves once the GPU has finished all submitted work, every readback has arrived, and every +: Resolves once the GPU has finished its submitted work, every readback has arrived, and every pipeline a view or shot requested has compiled. A frame stepped after that uses all of it. {#gpu-landed .member} @@ -1202,24 +1152,16 @@ draw together in each animation frame, so linked views always show the same mome compile, a job, a label text, or a fetch), or a view requests a frame. A harness that steps frames awaits it to know when to step again. -{#gpu-destroy .member} -**`destroy`**`(): void` [¶](#gpu-destroy){.headerlink} -: Destroys this GPU's canvases, views, and frame listeners, then frees the device. Other GPUs and - datasets stay usable. Calling it again does nothing; other methods throw afterwards. - -{#gpu-symbol-dispose .member} -**`[Symbol.dispose]`**`(): void` [¶](#gpu-symbol-dispose){.headerlink} -: Calls [`Gpu.destroy()`](#gpu-destroy), so a `using` declaration destroys it when its block ends. - (view)= ### `View` {.signature} -`class View` +`class View extends` [`Handle`](#handle) A scene drawn from its sources; created by [`Gpu.view()`](#gpu-view). A canvas shows it and passes it input, -and a [`Shot`](#shot) draws it into pictures. +and a [`Shot`](#shot) draws it into pictures. Destroying it rejects its unanswered queries; a canvas +showing it keeps its last frame. ```{rubric} Properties ``` @@ -1227,16 +1169,14 @@ and a [`Shot`](#shot) draws it into pictures. {#view-scene .member} **`scene`**`:` [`Scene`](#scene) [¶](#view-scene){.headerlink} : The scene drawn. Marks that match a mark of the previous scene by kind, dataset, table, and - name keep their state and ease to their new options. Setting an invalid scene throws an - `invalid` error and keeps the current one. + name keep their state and ease to their new options. An invalid scene leaves the current one. {#view-sources .member} **`sources`**`: readonly` [`Source`](#source)`[]` [¶](#view-sources){.headerlink} -: The datasets the view draws, matched to marks by dataset name. The view draws a [`Data`](#data) at its - latest version, from the frame after each patch; a [`Snapshot`](#snapshot) unchanged; and [`Tiles`](#tiles) as the - pieces it needs. A mark whose dataset no longer fits it draws nothing; it is listed in - `failures`, and the next frame emits an `error` event. Setting two sources with the same name - throws an `invalid` error. +: The datasets the view draws, matched to marks by dataset name, which must be unique. The view + draws a [`Data`](#data) at its latest version, from the frame after each patch; a [`Snapshot`](#snapshot) unchanged; + and [`Tiles`](#tiles) as the pieces it needs. A mark whose dataset no longer fits it draws nothing; it is + listed in `failures`, and the next frame emits an `error` event. {#view-at .member} **`at`**`: number` [¶](#view-at){.headerlink} @@ -1318,8 +1258,12 @@ and a [`Shot`](#shot) draws it into pictures. {#view-pick .member} **`pick`**`(point:` [`Point`](#point)`, options?: { radius?: number } &` [`Wait`](#wait)`): Promise<`[`Hit`](#hit)`[]>` [¶](#view-pick){.headerlink} -: Resolves, once a frame draws the view, to every row of a pickable mark within `options.radius` - CSS pixels (0 by default) of `point`, nearest first. +: Resolves, once a frame draws the view, to every row of a pickable mark near `point`, nearest + first. + + {#view-pick-radius .member} + **`radius`**`?: number` [¶](#view-pick-radius){.headerlink} + : How near, in CSS pixels; 0 by default. {#view-neighbors .member} **`neighbors`**`(row:` [`Row`](#row)`):` [`Rows`](#rows)`[]` [¶](#view-neighbors){.headerlink} @@ -1328,11 +1272,9 @@ and a [`Shot`](#shot) draws it into pictures. {#view-arrange .member} **`arrange`**`(wait?:` [`Wait`](#wait)`): Promise<`[`Patch`](#patch)`[]>` [¶](#view-arrange){.headerlink} -: Runs the scene's layout again for every vertex row without a position: a diagram's blocks, or - points on a plane or the earth. Otherwise a layout keeps the places it gave, so rows added - later are placed around the rest. Resolves, once the layout finishes, to one patch per dataset - that writes each laid-out row's place into its mark's position fields. Applying the patches - makes the places part of the data. Marks whose position is not read from fields are left out. +: Lays out every vertex row without a position again, and resolves to one patch per dataset that + writes each row's place into its mark's position fields; applied, the places become part of + the data. Marks whose position is not read from fields are left out. ```ts for (const patch of await view.arrange()) if (patch.data === grid.name) grid.apply(patch); @@ -1350,28 +1292,6 @@ and a [`Shot`](#shot) draws it into pictures. function that removes it. Past events are not replayed; read the current state from the property a change event is named after. -{#view-destroy .member} -**`destroy`**`(): void` [¶](#view-destroy){.headerlink} -: Frees the view and rejects its unanswered queries. A canvas showing it keeps its last frame. - Calling it again does nothing; other methods throw afterwards. - -{#view-symbol-dispose .member} -**`[Symbol.dispose]`**`(): void` [¶](#view-symbol-dispose){.headerlink} -: Calls [`View.destroy()`](#view-destroy), so a `using` declaration destroys it when its block ends. - -(viewoptions)= - -### `ViewOptions` - -{.signature} -`interface ViewOptions` - -Options for [`Gpu.view()`](#gpu-view). - -{#viewoptions-sources .member} -**`sources`**`?: readonly` [`Source`](#source)`[]` [¶](#viewoptions-sources){.headerlink} -: The datasets the scene draws; see [`View.sources`](#view-sources). - (viewevents)= ### `ViewEvents` @@ -1418,11 +1338,12 @@ and latitude; on a plot, the axis coordinate and the value. ### `Canvas` {.signature} -`class Canvas` +`class Canvas extends` [`Handle`](#handle) Shows views in a `` element or an `OffscreenCanvas`, each in its layout panel; created by [`Gpu.canvas()`](#gpu-canvas). A frame redraws only the views that need it, or every view after a resize or a -layout change. +layout change. Destroying it stops drawing; the views stay usable, and the element keeps its last +frame. ```{rubric} Properties ``` @@ -1434,43 +1355,15 @@ layout change. {#canvas-layout .member} **`layout`**`: readonly` [`Panel`](#panel)`[]` [¶](#canvas-layout){.headerlink} : The views shown and their regions; later panels draw over earlier ones. Setting it replaces the - shown views. A view can show in only one panel of one canvas at a time. Setting a layout that - breaks this, or that has a panel covering no pixels, throws an `invalid` error. + shown views. A view shows in at most one panel of one canvas at a time. ```{rubric} Methods ``` {#canvas-shot .member} **`shot`**`(options?:` [`ShotOptions`](#shotoptions)`):` [`Shot`](#shot) [¶](#canvas-shot){.headerlink} -: Returns a [`Shot`](#shot) of exactly what the canvas shows: its views in its layout, at the - canvas's CSS size times `options.scale` (1 by default) in pixels. - -{#canvas-destroy .member} -**`destroy`**`(): void` [¶](#canvas-destroy){.headerlink} -: Stops drawing and frees the canvas. The views stay usable, and the element keeps its last - frame. Calling it again does nothing. - -{#canvas-symbol-dispose .member} -**`[Symbol.dispose]`**`(): void` [¶](#canvas-symbol-dispose){.headerlink} -: Calls [`Canvas.destroy()`](#canvas-destroy), so a `using` declaration destroys it when its block ends. - -(canvasoptions)= - -### `CanvasOptions` - -{.signature} -`interface CanvasOptions` - -Options for [`Gpu.canvas()`](#gpu-canvas). - -{#canvasoptions-layout .member} -**`layout`**`?: readonly` [`Panel`](#panel)`[]` [¶](#canvasoptions-layout){.headerlink} -: The views shown and their regions; later panels draw over earlier ones. None by default. - -{#canvasoptions-pixelratio .member} -**`pixelRatio`**`?: number` [¶](#canvasoptions-pixelratio){.headerlink} -: Pixels per CSS pixel of an `OffscreenCanvas`, 1 by default; it must be positive. A `` - element uses the device pixel ratio instead. +: A [`Shot`](#shot) of exactly what the canvas shows: its views in its layout, at the canvas's CSS + size times `options.scale` in pixels. (panel)= @@ -1522,7 +1415,11 @@ const png = await (await canvas.shot({ scale: 2 }).image()).encode('png'); {#shot-new .member} **`new Shot`**`(size: readonly [number, number], options?:` [`ShotOptions`](#shotoptions) `& { layout?: readonly` [`Panel`](#panel)`[] })` [¶](#shot-new){.headerlink} -: Creates a shot of `size`, `[width, height]` in pixels, of the views in `options.layout`. +: Creates a shot of `size`, `[width, height]` in pixels. + + {#shot-new-layout .member} + **`layout`**`?: readonly` [`Panel`](#panel)`[]` [¶](#shot-new-layout){.headerlink} + : The views drawn, each in its panel of the picture. ```{rubric} Properties ``` @@ -1541,17 +1438,26 @@ const png = await (await canvas.shot({ scale: 2 }).image()).encode('png'); {#shot-image .member} **`image`**`(wait?:` [`Wait`](#wait)`): Promise<`[`Image`](#image)`>` [¶](#shot-image){.headerlink} : Draws the picture once every view is complete, as `capture` says, and resolves to the - [`Image`](#image). Rejects with an `invalid` error for invalid options or a view in two panels, and - with the first error a view reports while drawing. + [`Image`](#image). Rejects with the first error a view reports while drawing. {#shot-video .member} **`video`**`(options: { fps: number; output: WritableStream; bitrate?: number } &` [`Wait`](#wait)`):` [`Video`](#video) [¶](#shot-video){.headerlink} -: Starts a video of this shot: H.264 in MP4 at `options.fps` frames per second, written to - `options.output` as frames are drawn. `options.bitrate` is in bits per second, about 0.15 bits - per pixel per frame by default. The width is rounded down to a multiple of 4 and the height to a - multiple of 2, as the encoder requires. Canvases showing the views keep their last frame until - the video finishes or aborts. Aborting `options.signal` aborts the video and its output; frames - have no other time limit. +: Starts a video of this shot: H.264 in MP4, written to `options.output` as frames are drawn. The + width is rounded down to a multiple of 4 and the height to an even number, as the encoder + requires. Canvases showing the views keep their last frame until the video finishes or aborts. + Aborting `options.signal` aborts the video and its output; frames have no other time limit. + + {#shot-video-fps .member} + **`fps`**`: number` [¶](#shot-video-fps){.headerlink} + : Frames per second. + + {#shot-video-output .member} + **`output`**`: WritableStream` [¶](#shot-video-output){.headerlink} + : Where the file's bytes go, locked until the video finishes or aborts. + + {#shot-video-bitrate .member} + **`bitrate`**`?: number` [¶](#shot-video-bitrate){.headerlink} + : Bits per second; about 0.15 bits per pixel per frame by default. (shotoptions)= @@ -1587,7 +1493,7 @@ Options for a [`Shot`](#shot). ### `Image` {.signature} -`class Image` +`class Image extends` [`Handle`](#handle) A picture's RGBA pixels, with straight alpha, rows top to bottom. @@ -1611,18 +1517,12 @@ A picture's RGBA pixels, with straight alpha, rows top to bottom. {#image-encode .member} **`encode`**`(format:` [`ImageFormat`](#imageformat)`, options?: { quality?: number } &` [`Wait`](#wait)`): Promise` [¶](#image-encode){.headerlink} -: Encodes the image as a file, in a worker. PNG is lossless with alpha. JPEG uses - `options.quality` from 1 to 100 (90 by default) and drops alpha. WebP is lossless with alpha - and smaller than PNG. Rejects with an `invalid` error for a quality outside 1 to 100 or an - unknown format. - -{#image-destroy .member} -**`destroy`**`(): void` [¶](#image-destroy){.headerlink} -: Frees the pixels. Other methods throw afterwards. - -{#image-symbol-dispose .member} -**`[Symbol.dispose]`**`(): void` [¶](#image-symbol-dispose){.headerlink} -: Calls [`Image.destroy()`](#image-destroy), so a `using` declaration destroys it when its block ends. +: Encodes the image as a file, in a worker. PNG is lossless with alpha. JPEG drops alpha. WebP + is lossless with alpha and smaller than PNG. + + {#image-encode-quality .member} + **`quality`**`?: number` [¶](#image-encode-quality){.headerlink} + : JPEG quality, from 1 (smallest) to 100 (sharpest); 90 by default. (imageformat)= diff --git a/docs/reference/cli.json b/docs/reference/cli.json new file mode 100644 index 00000000..f388d642 --- /dev/null +++ b/docs/reference/cli.json @@ -0,0 +1,836 @@ +[ + { + "name": "latkit", + "usage": "latkit [OPTIONS] ", + "about": "Render scenes of JSON or binary data to images and videos", + "args": [ + { + "id": "diagnostics", + "long": "diagnostics", + "short": null, + "value": "DIAGNOSTICS", + "positional": false, + "required": false, + "many": false, + "global": true, + "help": "Format of errors and render progress on stderr", + "defaults": [ + "human" + ], + "values": [ + { + "name": "human", + "help": "Lines of text, for people" + }, + { + "name": "json", + "help": "One JSON object per line, for programs" + } + ] + } + ] + }, + { + "name": "latkit render", + "usage": "latkit render [OPTIONS] ...", + "about": "Draw scenes of datasets into an image, image sequence, raw frames, or a video", + "args": [ + { + "id": "scenes", + "long": null, + "short": null, + "value": "SCENE", + "positional": true, + "required": true, + "many": true, + "global": false, + "help": "Scene JSON files, one per panel (`-` reads stdin)", + "defaults": [], + "values": [] + }, + { + "id": "data", + "long": "data", + "short": "d", + "value": "[NAME=]FILE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "A dataset file: a JSON document, latkit bytes, or GeoJSON. `NAME=` sets its name, which GeoJSON requires", + "defaults": [], + "values": [] + }, + { + "id": "tiles", + "long": "tiles", + "short": null, + "value": "NAME=FILE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Tiles every view fetches as dataset `NAME`: a time tiles `frames.json`, or a vector tiles PMTiles file, TileJSON, or `{z}/{x}/{y}` template", + "defaults": [], + "values": [] + }, + { + "id": "output", + "long": "output", + "short": "o", + "value": "FILE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Output file, `%d` pattern for an image sequence, or `-` for stdout", + "defaults": [], + "values": [] + }, + { + "id": "format", + "long": "format", + "short": null, + "value": "FORMAT", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Output format. Defaults to the one the output's extension names, else PNG", + "defaults": [], + "values": [ + { + "name": "png", + "help": "PNG, lossless with alpha. One picture, or one per frame with a `%d` output pattern" + }, + { + "name": "jpeg", + "help": "JPEG at `--quality`, without alpha. One picture, or one per frame" + }, + { + "name": "webp", + "help": "Lossless WebP with alpha. One picture, or one per frame" + }, + { + "name": "rgba", + "help": "Raw RGBA with straight alpha, frames back to back" + }, + { + "name": "y4m", + "help": "YUV4MPEG2 (4:2:0, BT.709 limited range), which any encoder reads" + }, + { + "name": "mp4", + "help": "H.264 in fragmented MP4, from the platform's encoder" + } + ] + }, + { + "id": "quality", + "long": "quality", + "short": null, + "value": "QUALITY", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "JPEG quality, from 1 (smallest) to 100 (sharpest)", + "defaults": [ + "90" + ], + "values": [] + }, + { + "id": "size", + "long": "size", + "short": null, + "value": "SIZE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "The picture's width and height in pixels, such as `1920x1080`. A video rounds its width down to a multiple of 4 and its height to an even number", + "defaults": [ + "1600x900" + ], + "values": [] + }, + { + "id": "scale", + "long": "scale", + "short": null, + "value": "SCALE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Pixels per CSS pixel. Views lay out size / scale CSS pixels, so 2 draws twice as sharp", + "defaults": [ + "1" + ], + "values": [] + }, + { + "id": "panel", + "long": "panel", + "short": null, + "value": "X,Y,W,H", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "A scene's panel of the picture: its x, y, width, and height, as fractions. Give one per scene, in order", + "defaults": [], + "values": [] + }, + { + "id": "transparent", + "long": "transparent", + "short": null, + "value": null, + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Keep alpha where the themes' backgrounds would be", + "defaults": [], + "values": [] + }, + { + "id": "deadline", + "long": "deadline", + "short": null, + "value": "DEADLINE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "The longest a picture or video frame may take, in seconds", + "defaults": [ + "30" + ], + "values": [] + }, + { + "id": "font", + "long": "font", + "short": null, + "value": "FILE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "A TTF, OTF, or TTC font file. Repeat for fallbacks. Replaces the system fonts", + "defaults": [], + "values": [] + }, + { + "id": "camera", + "long": "camera", + "short": null, + "value": "CAMERA", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "A camera per scene, in scene order: JSON, or a file of it (`-` reads stdin). Views without one use `--fit`", + "defaults": [], + "values": [] + }, + { + "id": "fit", + "long": "fit", + "short": null, + "value": "FIT", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "What views without a camera frame: `all`, or a fit as JSON", + "defaults": [ + "all" + ], + "values": [] + }, + { + "id": "insets", + "long": "insets", + "short": null, + "value": "T,R,B,L", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "CSS pixels to keep clear at the top, right, bottom, and left", + "defaults": [], + "values": [] + }, + { + "id": "at", + "long": "at", + "short": null, + "value": "AT", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "The axis coordinate to show. Sampled fields show their last frame at or before it", + "defaults": [], + "values": [] + }, + { + "id": "elapsed", + "long": "elapsed", + "short": null, + "value": "ELAPSED", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "A still's fixed effect time in seconds, for elapsed-time expressions and flowing comets", + "defaults": [], + "values": [] + }, + { + "id": "frames", + "long": "frames", + "short": null, + "value": "FILE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Frames as JSON Lines, one line per output frame (`-` reads stdin)", + "defaults": [], + "values": [] + }, + { + "id": "fps", + "long": "fps", + "short": null, + "value": "FPS", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Output frames per second", + "defaults": [ + "60" + ], + "values": [] + }, + { + "id": "bitrate", + "long": "bitrate", + "short": null, + "value": "BITRATE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "MP4 bitrate in bits per second. Defaults to about 0.15 bits per pixel per frame", + "defaults": [], + "values": [] + } + ] + }, + { + "name": "latkit validate", + "usage": "latkit validate [OPTIONS] ...", + "about": "Check every input render reads, including frames, without a GPU", + "args": [ + { + "id": "scenes", + "long": null, + "short": null, + "value": "SCENE", + "positional": true, + "required": true, + "many": true, + "global": false, + "help": "Scene JSON files, one per panel (`-` reads stdin)", + "defaults": [], + "values": [] + }, + { + "id": "data", + "long": "data", + "short": "d", + "value": "[NAME=]FILE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "A dataset file: a JSON document, latkit bytes, or GeoJSON. `NAME=` sets its name, which GeoJSON requires", + "defaults": [], + "values": [] + }, + { + "id": "tiles", + "long": "tiles", + "short": null, + "value": "NAME=FILE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Tiles every view fetches as dataset `NAME`: a time tiles `frames.json`, or a vector tiles PMTiles file, TileJSON, or `{z}/{x}/{y}` template", + "defaults": [], + "values": [] + }, + { + "id": "output", + "long": "output", + "short": "o", + "value": "FILE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Output file, `%d` pattern for an image sequence, or `-` for stdout", + "defaults": [], + "values": [] + }, + { + "id": "format", + "long": "format", + "short": null, + "value": "FORMAT", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Output format. Defaults to the one the output's extension names, else PNG", + "defaults": [], + "values": [ + { + "name": "png", + "help": "PNG, lossless with alpha. One picture, or one per frame with a `%d` output pattern" + }, + { + "name": "jpeg", + "help": "JPEG at `--quality`, without alpha. One picture, or one per frame" + }, + { + "name": "webp", + "help": "Lossless WebP with alpha. One picture, or one per frame" + }, + { + "name": "rgba", + "help": "Raw RGBA with straight alpha, frames back to back" + }, + { + "name": "y4m", + "help": "YUV4MPEG2 (4:2:0, BT.709 limited range), which any encoder reads" + }, + { + "name": "mp4", + "help": "H.264 in fragmented MP4, from the platform's encoder" + } + ] + }, + { + "id": "quality", + "long": "quality", + "short": null, + "value": "QUALITY", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "JPEG quality, from 1 (smallest) to 100 (sharpest)", + "defaults": [ + "90" + ], + "values": [] + }, + { + "id": "size", + "long": "size", + "short": null, + "value": "SIZE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "The picture's width and height in pixels, such as `1920x1080`. A video rounds its width down to a multiple of 4 and its height to an even number", + "defaults": [ + "1600x900" + ], + "values": [] + }, + { + "id": "scale", + "long": "scale", + "short": null, + "value": "SCALE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Pixels per CSS pixel. Views lay out size / scale CSS pixels, so 2 draws twice as sharp", + "defaults": [ + "1" + ], + "values": [] + }, + { + "id": "panel", + "long": "panel", + "short": null, + "value": "X,Y,W,H", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "A scene's panel of the picture: its x, y, width, and height, as fractions. Give one per scene, in order", + "defaults": [], + "values": [] + }, + { + "id": "transparent", + "long": "transparent", + "short": null, + "value": null, + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Keep alpha where the themes' backgrounds would be", + "defaults": [], + "values": [] + }, + { + "id": "deadline", + "long": "deadline", + "short": null, + "value": "DEADLINE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "The longest a picture or video frame may take, in seconds", + "defaults": [ + "30" + ], + "values": [] + }, + { + "id": "font", + "long": "font", + "short": null, + "value": "FILE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "A TTF, OTF, or TTC font file. Repeat for fallbacks. Replaces the system fonts", + "defaults": [], + "values": [] + }, + { + "id": "camera", + "long": "camera", + "short": null, + "value": "CAMERA", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "A camera per scene, in scene order: JSON, or a file of it (`-` reads stdin). Views without one use `--fit`", + "defaults": [], + "values": [] + }, + { + "id": "fit", + "long": "fit", + "short": null, + "value": "FIT", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "What views without a camera frame: `all`, or a fit as JSON", + "defaults": [ + "all" + ], + "values": [] + }, + { + "id": "insets", + "long": "insets", + "short": null, + "value": "T,R,B,L", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "CSS pixels to keep clear at the top, right, bottom, and left", + "defaults": [], + "values": [] + }, + { + "id": "at", + "long": "at", + "short": null, + "value": "AT", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "The axis coordinate to show. Sampled fields show their last frame at or before it", + "defaults": [], + "values": [] + }, + { + "id": "elapsed", + "long": "elapsed", + "short": null, + "value": "ELAPSED", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "A still's fixed effect time in seconds, for elapsed-time expressions and flowing comets", + "defaults": [], + "values": [] + }, + { + "id": "frames", + "long": "frames", + "short": null, + "value": "FILE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Frames as JSON Lines, one line per output frame (`-` reads stdin)", + "defaults": [], + "values": [] + }, + { + "id": "fps", + "long": "fps", + "short": null, + "value": "FPS", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Output frames per second", + "defaults": [ + "60" + ], + "values": [] + }, + { + "id": "bitrate", + "long": "bitrate", + "short": null, + "value": "BITRATE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "MP4 bitrate in bits per second. Defaults to about 0.15 bits per pixel per frame", + "defaults": [], + "values": [] + } + ] + }, + { + "name": "latkit inspect", + "usage": "latkit inspect [OPTIONS] <[NAME=]FILE>...", + "about": "Describe datasets as JSON: tables, row counts, fields with kinds, and the axis and its span", + "args": [ + { + "id": "data", + "long": null, + "short": null, + "value": "[NAME=]FILE", + "positional": true, + "required": true, + "many": true, + "global": false, + "help": "Dataset files, each `[NAME=]FILE` (`-` reads stdin)", + "defaults": [], + "values": [] + }, + { + "id": "deadline", + "long": "deadline", + "short": null, + "value": "DEADLINE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "The longest a dataset may take to load, in seconds", + "defaults": [ + "30" + ], + "values": [] + } + ] + }, + { + "name": "latkit schema", + "usage": "latkit schema [OPTIONS] [INPUT]", + "about": "Print the JSON Schema of every input latkit reads, or of one input", + "args": [ + { + "id": "input", + "long": null, + "short": null, + "value": "INPUT", + "positional": true, + "required": false, + "many": false, + "global": false, + "help": "Print only this input's schema", + "defaults": [], + "values": [ + { + "name": "scene", + "help": "A scene" + }, + { + "name": "data", + "help": "A dataset's JSON document" + }, + { + "name": "patch", + "help": "A patch" + }, + { + "name": "query", + "help": "A query for `latkit query`" + }, + { + "name": "camera", + "help": "A camera for `--camera`" + }, + { + "name": "fit", + "help": "A fit for `--fit`" + }, + { + "name": "frames", + "help": "One line of a `--frames` file" + } + ] + } + ] + }, + { + "name": "latkit query", + "usage": "latkit query [OPTIONS] <[NAME=]FILE> ", + "about": "Print the rows a query selects, sorted and paged, as JSON Lines with one object per row", + "args": [ + { + "id": "data", + "long": null, + "short": null, + "value": "[NAME=]FILE", + "positional": true, + "required": true, + "many": false, + "global": false, + "help": "The dataset, `[NAME=]FILE` (`-` reads stdin)", + "defaults": [], + "values": [] + }, + { + "id": "query", + "long": null, + "short": null, + "value": "QUERY", + "positional": true, + "required": true, + "many": false, + "global": false, + "help": "The query: JSON, such as `{\"of\":\"Branch\",\"filter\":{\"op\":\"gt\",\"args\":[\"loading\",90]}}`, or a file of it (`-` reads stdin)", + "defaults": [], + "values": [] + }, + { + "id": "deadline", + "long": "deadline", + "short": null, + "value": "DEADLINE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "The longest the dataset may take to load, in seconds", + "defaults": [ + "30" + ], + "values": [] + } + ] + }, + { + "name": "latkit tile", + "usage": "latkit tile [OPTIONS] --output

    <[NAME=]FILE>", + "about": "Write a dataset as time tiles: `frames.json` (which `--tiles` opens), the dataset without its sampled fields' histories, and those histories in spans at every level of detail", + "args": [ + { + "id": "data", + "long": null, + "short": null, + "value": "[NAME=]FILE", + "positional": true, + "required": true, + "many": false, + "global": false, + "help": "The dataset, `[NAME=]FILE` (`-` reads stdin)", + "defaults": [], + "values": [] + }, + { + "id": "output", + "long": "output", + "short": "o", + "value": "DIR", + "positional": false, + "required": true, + "many": false, + "global": false, + "help": "Directory to write the tiles to", + "defaults": [], + "values": [] + }, + { + "id": "chunk", + "long": "chunk", + "short": null, + "value": "CHUNK", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Entries per span: frames at full detail, buckets at coarser levels. A multiple of 8", + "defaults": [ + "256" + ], + "values": [] + }, + { + "id": "deadline", + "long": "deadline", + "short": null, + "value": "DEADLINE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "The longest the dataset may take to load, in seconds", + "defaults": [ + "30" + ], + "values": [] + } + ] + } +] diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 228c549f..3712221c 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -118,7 +118,7 @@ Draw scenes of datasets into an image, image sequence, raw frames, or a video. {#cli-render-size .member} **`--size`** `` [¶](#cli-render-size){.headerlink} -: The picture's width and height in pixels, such as `1920x1080`. Defaults to `1600x900`. +: The picture's width and height in pixels, such as `1920x1080`. A video rounds its width down to a multiple of 4 and its height to an even number. Defaults to `1600x900`. {#cli-render-scale .member} **`--scale`** `` [¶](#cli-render-scale){.headerlink} @@ -141,8 +141,8 @@ Draw scenes of datasets into an image, image sequence, raw frames, or a video. : A TTF, OTF, or TTC font file. Repeat for fallbacks. Replaces the system fonts. {#cli-render-camera .member} -**`--camera`** `` [¶](#cli-render-camera){.headerlink} -: A camera JSON file per scene, in scene order. Views without one use [`--fit`](#cli-render-fit). +**`--camera`** `` [¶](#cli-render-camera){.headerlink} +: A camera per scene, in scene order: JSON, or a file of it (`-` reads stdin). Views without one use [`--fit`](#cli-render-fit). {#cli-render-fit .member} **`--fit`** `` [¶](#cli-render-fit){.headerlink} @@ -181,115 +181,7 @@ Draw scenes of datasets into an image, image sequence, raw frames, or a video. Check every input render reads, including frames, without a GPU. -```{rubric} Arguments -``` - -{#cli-validate-scenes .member} -**`...`** [¶](#cli-validate-scenes){.headerlink} -: Scene JSON files, one per panel (`-` reads stdin). - -```{rubric} Options -``` - -{#cli-validate-data .member} -`-d,` **`--data`** `<[NAME=]FILE>` [¶](#cli-validate-data){.headerlink} -: A dataset file: a JSON document, latkit bytes, or GeoJSON. `NAME=` sets its name, which GeoJSON requires. - -{#cli-validate-tiles .member} -**`--tiles`** `` [¶](#cli-validate-tiles){.headerlink} -: Tiles every view fetches as dataset `NAME`: a time tiles `frames.json`, or a vector tiles PMTiles file, TileJSON, or `{z}/{x}/{y}` template. - -{#cli-validate-output .member} -`-o,` **`--output`** `` [¶](#cli-validate-output){.headerlink} -: Output file, `%d` pattern for an image sequence, or `-` for stdout. - -{#cli-validate-format .member} -**`--format`** `` [¶](#cli-validate-format){.headerlink} -: Output format. Defaults to the one the output's extension names, else PNG. - - {#cli-validate-format-png .member} - **`png`** [¶](#cli-validate-format-png){.headerlink} - : PNG, lossless with alpha. One picture, or one per frame with a `%d` output pattern. - - {#cli-validate-format-jpeg .member} - **`jpeg`** [¶](#cli-validate-format-jpeg){.headerlink} - : JPEG at [`--quality`](#cli-validate-quality), without alpha. One picture, or one per frame. - - {#cli-validate-format-webp .member} - **`webp`** [¶](#cli-validate-format-webp){.headerlink} - : Lossless WebP with alpha. One picture, or one per frame. - - {#cli-validate-format-rgba .member} - **`rgba`** [¶](#cli-validate-format-rgba){.headerlink} - : Raw RGBA with straight alpha, frames back to back. - - {#cli-validate-format-y4m .member} - **`y4m`** [¶](#cli-validate-format-y4m){.headerlink} - : YUV4MPEG2 (4:2:0, BT.709 limited range), which any encoder reads. - - {#cli-validate-format-mp4 .member} - **`mp4`** [¶](#cli-validate-format-mp4){.headerlink} - : H.264 in fragmented MP4, from the platform's encoder. - -{#cli-validate-quality .member} -**`--quality`** `` [¶](#cli-validate-quality){.headerlink} -: JPEG quality, from 1 (smallest) to 100 (sharpest). Defaults to `90`. - -{#cli-validate-size .member} -**`--size`** `` [¶](#cli-validate-size){.headerlink} -: The picture's width and height in pixels, such as `1920x1080`. Defaults to `1600x900`. - -{#cli-validate-scale .member} -**`--scale`** `` [¶](#cli-validate-scale){.headerlink} -: Pixels per CSS pixel. Views lay out size / scale CSS pixels, so 2 draws twice as sharp. Defaults to `1`. - -{#cli-validate-panel .member} -**`--panel`** `` [¶](#cli-validate-panel){.headerlink} -: A scene's panel of the picture: its x, y, width, and height, as fractions. Give one per scene, in order. - -{#cli-validate-transparent .member} -**`--transparent`** [¶](#cli-validate-transparent){.headerlink} -: Keep alpha where the themes' backgrounds would be. - -{#cli-validate-deadline .member} -**`--deadline`** `` [¶](#cli-validate-deadline){.headerlink} -: The longest a picture or video frame may take, in seconds. Defaults to `30`. - -{#cli-validate-font .member} -**`--font`** `` [¶](#cli-validate-font){.headerlink} -: A TTF, OTF, or TTC font file. Repeat for fallbacks. Replaces the system fonts. - -{#cli-validate-camera .member} -**`--camera`** `` [¶](#cli-validate-camera){.headerlink} -: A camera JSON file per scene, in scene order. Views without one use [`--fit`](#cli-validate-fit). - -{#cli-validate-fit .member} -**`--fit`** `` [¶](#cli-validate-fit){.headerlink} -: What views without a camera frame: `all`, or a fit as JSON. Defaults to `all`. - -{#cli-validate-insets .member} -**`--insets`** `` [¶](#cli-validate-insets){.headerlink} -: CSS pixels to keep clear at the top, right, bottom, and left. - -{#cli-validate-at .member} -**`--at`** `` [¶](#cli-validate-at){.headerlink} -: The axis coordinate to show. Sampled fields show their last frame at or before it. - -{#cli-validate-elapsed .member} -**`--elapsed`** `` [¶](#cli-validate-elapsed){.headerlink} -: A still's fixed effect time in seconds, for elapsed-time expressions and flowing comets. - -{#cli-validate-frames .member} -**`--frames`** `` [¶](#cli-validate-frames){.headerlink} -: Frames as JSON Lines, one line per output frame (`-` reads stdin). - -{#cli-validate-fps .member} -**`--fps`** `` [¶](#cli-validate-fps){.headerlink} -: Output frames per second. Defaults to `60`. - -{#cli-validate-bitrate .member} -**`--bitrate`** `` [¶](#cli-validate-bitrate){.headerlink} -: MP4 bitrate in bits per second. Defaults to about 0.15 bits per pixel per frame. +Takes [`latkit render`](#cli-render)'s arguments and options. (cli-inspect)= @@ -376,7 +268,7 @@ Print the rows a query selects, sorted and paged, as JSON Lines with one object {#cli-query-query .member} **``** [¶](#cli-query-query){.headerlink} -: The query as JSON, such as `{"of":"Branch","filter":{"op":"gt","args":["loading",90]}}`, or a file containing it (`-` reads stdin). +: The query: JSON, such as `{"of":"Branch","filter":{"op":"gt","args":["loading",90]}}`, or a file of it (`-` reads stdin). ```{rubric} Options ``` diff --git a/docs/reference/types.md b/docs/reference/types.md index 8322c9e9..a140299b 100644 --- a/docs/reference/types.md +++ b/docs/reference/types.md @@ -909,19 +909,96 @@ One table a scene draws. In JSON: `{ vertices | edges | faces | lines | areas: o `{` **`areas`**`:` [`Areas`](#areas) `}` [¶](#mark-areas){.headerlink} : Rows drawn as the areas their polygons cover. +(markoptions)= + +### `MarkOptions` + +{.signature} +`interface MarkOptions` + +The options every mark has. + +{#markoptions-of .member} +**`of`**`: [data: string, table: string]` [¶](#markoptions-of){.headerlink} +: The dataset and table drawn. + +{#markoptions-color .member} +**`color`**`?:` [`PaintInput`](#paintinput) `| null` [¶](#markoptions-color){.headerlink} +: Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their + corners' colors. + +{#markoptions-visible .member} +**`visible`**`?:` [`ExprInput`](#exprinput) [¶](#markoptions-visible){.headerlink} +: Whether each row is drawn and can be picked. Default `true`. + +{#markoptions-priority .member} +**`priority`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#markoptions-priority){.headerlink} +: Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are + the last dropped when a table too large to draw whole is thinned to the tiles in view, and are + labeled first. + +{#markoptions-name .member} +**`name`**`?: string | null` [¶](#markoptions-name){.headerlink} +: Distinguishes two marks of the same table across scenes, and names the mark in hits. + +{#markoptions-pick .member} +**`pick`**`?: boolean | null` [¶](#markoptions-pick){.headerlink} +: Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces + do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. + +{#markoptions-select .member} +**`select`**`?: boolean` [¶](#markoptions-select){.headerlink} +: Whether clicking a row selects it. Default `true`. + +{#markoptions-drag .member} +**`drag`**`?: boolean` [¶](#markoptions-drag){.headerlink} +: Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the + camera. Default `false`. + +{#markoptions-label .member} +**`label`**`?:` [`Label`](#label) `| null` [¶](#markoptions-label){.headerlink} +: Each row's label: a text field, or label options. + +{#markoptions-legend .member} +**`legend`**`?: string | null` [¶](#markoptions-legend){.headerlink} +: A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap + across its domain, a cell for each category, or a swatch beside each name of a map's + `categories`. `color` must map through a colormap with a fixed domain or named categories; in a + conditional, `otherwise` is searched before the cases. + +(strokeoptions)= + +### `StrokeOptions` + +{.signature} +`interface StrokeOptions` + +The options of marks drawn as strokes: edges and lines. + +{#strokeoptions-width .member} +**`width`**`?:` [`Size`](#size) `| null` [¶](#strokeoptions-width){.headerlink} +: Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given + with `px` or `units`. The theme's width unless set. + +{#strokeoptions-dash .member} +**`dash`**`?:` [`ExprInput`](#exprinput) [¶](#strokeoptions-dash){.headerlink} +: Whether each row is dashed, with the theme's `dash` period. Default `false`. + +{#strokeoptions-flow .member} +**`flow`**`?:` [`ExprInput`](#exprinput) [¶](#strokeoptions-flow){.headerlink} +: The speed of comets flowing along each row, per second, in the unit of the theme's `flow` + lengths. Negative speeds flow backward; 0, the default, draws no comets. When the speed changes, + comets continue from where they are. + (vertices)= ### `Vertices` {.signature} -`interface Vertices` +`interface Vertices extends` [`MarkOptions`](#markoptions) Rows drawn as markers: points of a map, nodes of a graph, blocks of a diagram. -{#vertices-of .member} -**`of`**`: [data: string, table: string]` [¶](#vertices-of){.headerlink} -: The dataset and table drawn. - {#vertices-position .member} **`position`**`?: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] | null` [¶](#vertices-position){.headerlink} : Each row's x and y. Without a position, the point field that `geometry` names places rows, and @@ -971,65 +1048,17 @@ Rows drawn as markers: points of a map, nodes of a graph, blocks of a diagram. : Whether each block or group casts a shadow on the sheet, styled by the theme's `diagram.shadow`. Default `false`. Diagrams only. -{#vertices-color .member} -**`color`**`?:` [`PaintInput`](#paintinput) `| null` [¶](#vertices-color){.headerlink} -: Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their - corners' colors. - -{#vertices-visible .member} -**`visible`**`?:` [`ExprInput`](#exprinput) [¶](#vertices-visible){.headerlink} -: Whether each row is drawn and can be picked. Default `true`. - -{#vertices-priority .member} -**`priority`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#vertices-priority){.headerlink} -: Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are - the last dropped when a table too large to draw whole is thinned to the tiles in view, and are - labeled first. - -{#vertices-name .member} -**`name`**`?: string | null` [¶](#vertices-name){.headerlink} -: Distinguishes two marks of the same table across scenes, and names the mark in hits. - -{#vertices-pick .member} -**`pick`**`?: boolean | null` [¶](#vertices-pick){.headerlink} -: Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces - do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. - -{#vertices-select .member} -**`select`**`?: boolean` [¶](#vertices-select){.headerlink} -: Whether clicking a row selects it. Default `true`. - -{#vertices-drag .member} -**`drag`**`?: boolean` [¶](#vertices-drag){.headerlink} -: Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the - camera. Default `false`. - -{#vertices-label .member} -**`label`**`?:` [`Label`](#label) `| null` [¶](#vertices-label){.headerlink} -: Each row's label: a text field, or label options. - -{#vertices-legend .member} -**`legend`**`?: string | null` [¶](#vertices-legend){.headerlink} -: A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap - across its domain, a cell for each category, or a swatch beside each name of a map's - `categories`. `color` must map through a colormap with a fixed domain or named categories; in a - conditional, `otherwise` is searched before the cases. - (edges)= ### `Edges` {.signature} -`interface Edges` +`interface Edges extends` [`MarkOptions`](#markoptions)`,` [`StrokeOptions`](#strokeoptions) Rows drawn as strokes between the vertices named by two reference fields: branches, links, wires. In a diagram, a row without ends is a net, joining every vertex port whose reference field names it. -{#edges-of .member} -**`of`**`: [data: string, table: string]` [¶](#edges-of){.headerlink} -: The dataset and table drawn. - {#edges-ends .member} **`ends`**`?: [from: string, to: string] | null` [¶](#edges-ends){.headerlink} : The reference fields naming each row's two vertices. @@ -1058,81 +1087,18 @@ field names it. : Draws each row as a net on a plane or the earth, joining the vertex rows of `table` whose `reference` field names it: a star from their center to each member. -{#edges-width .member} -**`width`**`?:` [`Size`](#size) `| null` [¶](#edges-width){.headerlink} -: Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given - with `px` or `units`. The theme's width unless set. - -{#edges-dash .member} -**`dash`**`?:` [`ExprInput`](#exprinput) [¶](#edges-dash){.headerlink} -: Whether each row is dashed, with the theme's `dash` period. Default `false`. - -{#edges-flow .member} -**`flow`**`?:` [`ExprInput`](#exprinput) [¶](#edges-flow){.headerlink} -: The speed of comets flowing along each row, per second, in the unit of the theme's `flow` - lengths. Negative speeds flow backward; 0, the default, draws no comets. When the speed changes, - comets continue from where they are. - -{#edges-color .member} -**`color`**`?:` [`PaintInput`](#paintinput) `| null` [¶](#edges-color){.headerlink} -: Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their - corners' colors. - -{#edges-visible .member} -**`visible`**`?:` [`ExprInput`](#exprinput) [¶](#edges-visible){.headerlink} -: Whether each row is drawn and can be picked. Default `true`. - -{#edges-priority .member} -**`priority`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#edges-priority){.headerlink} -: Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are - the last dropped when a table too large to draw whole is thinned to the tiles in view, and are - labeled first. - -{#edges-name .member} -**`name`**`?: string | null` [¶](#edges-name){.headerlink} -: Distinguishes two marks of the same table across scenes, and names the mark in hits. - -{#edges-pick .member} -**`pick`**`?: boolean | null` [¶](#edges-pick){.headerlink} -: Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces - do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. - -{#edges-select .member} -**`select`**`?: boolean` [¶](#edges-select){.headerlink} -: Whether clicking a row selects it. Default `true`. - -{#edges-drag .member} -**`drag`**`?: boolean` [¶](#edges-drag){.headerlink} -: Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the - camera. Default `false`. - -{#edges-label .member} -**`label`**`?:` [`Label`](#label) `| null` [¶](#edges-label){.headerlink} -: Each row's label: a text field, or label options. - -{#edges-legend .member} -**`legend`**`?: string | null` [¶](#edges-legend){.headerlink} -: A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap - across its domain, a cell for each category, or a swatch beside each name of a map's - `categories`. `color` must map through a colormap with a fixed domain or named categories; in a - conditional, `otherwise` is searched before the cases. - (lines)= ### `Lines` {.signature} -`interface Lines` +`interface Lines extends` [`MarkOptions`](#markoptions)`,` [`StrokeOptions`](#strokeoptions) Rows drawn as lines along a path field (roads, rivers, borders) or a polygon field's rings (area outlines). On a plot, each row instead traces a sampled field's history across the data's axis, as a monitor does. On the earth, paths follow the globe's curve and stay whole across the antimeridian. -{#lines-of .member} -**`of`**`: [data: string, table: string]` [¶](#lines-of){.headerlink} -: The dataset and table drawn. - {#lines-geometry .member} **`geometry`**`?: string | null` [¶](#lines-geometry){.headerlink} : The path or polygon field each row runs along; the field named `geometry` by default. Not @@ -1147,194 +1113,39 @@ antimeridian. **`interpolation`**`?:` [`Interpolation`](#interpolation) [¶](#lines-interpolation){.headerlink} : How each row runs between its samples on a plot; `linear` by default. -{#lines-width .member} -**`width`**`?:` [`Size`](#size) `| null` [¶](#lines-width){.headerlink} -: Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given - with `px` or `units`. The theme's width unless set. - -{#lines-dash .member} -**`dash`**`?:` [`ExprInput`](#exprinput) [¶](#lines-dash){.headerlink} -: Whether each row is dashed, with the theme's `dash` period. Default `false`. - -{#lines-flow .member} -**`flow`**`?:` [`ExprInput`](#exprinput) [¶](#lines-flow){.headerlink} -: The speed of comets flowing along each row, per second, in the unit of the theme's `flow` - lengths. Negative speeds flow backward; 0, the default, draws no comets. When the speed changes, - comets continue from where they are. - -{#lines-color .member} -**`color`**`?:` [`PaintInput`](#paintinput) `| null` [¶](#lines-color){.headerlink} -: Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their - corners' colors. - -{#lines-visible .member} -**`visible`**`?:` [`ExprInput`](#exprinput) [¶](#lines-visible){.headerlink} -: Whether each row is drawn and can be picked. Default `true`. - -{#lines-priority .member} -**`priority`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#lines-priority){.headerlink} -: Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are - the last dropped when a table too large to draw whole is thinned to the tiles in view, and are - labeled first. - -{#lines-name .member} -**`name`**`?: string | null` [¶](#lines-name){.headerlink} -: Distinguishes two marks of the same table across scenes, and names the mark in hits. - -{#lines-pick .member} -**`pick`**`?: boolean | null` [¶](#lines-pick){.headerlink} -: Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces - do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. - -{#lines-select .member} -**`select`**`?: boolean` [¶](#lines-select){.headerlink} -: Whether clicking a row selects it. Default `true`. - -{#lines-drag .member} -**`drag`**`?: boolean` [¶](#lines-drag){.headerlink} -: Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the - camera. Default `false`. - -{#lines-label .member} -**`label`**`?:` [`Label`](#label) `| null` [¶](#lines-label){.headerlink} -: Each row's label: a text field, or label options. - -{#lines-legend .member} -**`legend`**`?: string | null` [¶](#lines-legend){.headerlink} -: A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap - across its domain, a cell for each category, or a swatch beside each name of a map's - `categories`. `color` must map through a colormap with a fixed domain or named categories; in a - conditional, `otherwise` is searched before the cases. - (areas)= ### `Areas` {.signature} -`interface Areas` +`interface Areas extends` [`MarkOptions`](#markoptions) Rows drawn as the areas of a polygon field: parcels, states, lakes, the land and water of a map. The view triangulates each row's polygons once, in the background when they are large. On the earth, areas follow the globe's curve and stay whole across the antimeridian. To outline areas, add a [`Lines`](#lines) mark of the same table, which draws their rings. -{#areas-of .member} -**`of`**`: [data: string, table: string]` [¶](#areas-of){.headerlink} -: The dataset and table drawn. - {#areas-geometry .member} **`geometry`**`?: string | null` [¶](#areas-geometry){.headerlink} : The polygon field each row fills, holding one polygon's rings or several polygons; the field named `geometry` by default. -{#areas-color .member} -**`color`**`?:` [`PaintInput`](#paintinput) `| null` [¶](#areas-color){.headerlink} -: Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their - corners' colors. - -{#areas-visible .member} -**`visible`**`?:` [`ExprInput`](#exprinput) [¶](#areas-visible){.headerlink} -: Whether each row is drawn and can be picked. Default `true`. - -{#areas-priority .member} -**`priority`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#areas-priority){.headerlink} -: Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are - the last dropped when a table too large to draw whole is thinned to the tiles in view, and are - labeled first. - -{#areas-name .member} -**`name`**`?: string | null` [¶](#areas-name){.headerlink} -: Distinguishes two marks of the same table across scenes, and names the mark in hits. - -{#areas-pick .member} -**`pick`**`?: boolean | null` [¶](#areas-pick){.headerlink} -: Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces - do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. - -{#areas-select .member} -**`select`**`?: boolean` [¶](#areas-select){.headerlink} -: Whether clicking a row selects it. Default `true`. - -{#areas-drag .member} -**`drag`**`?: boolean` [¶](#areas-drag){.headerlink} -: Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the - camera. Default `false`. - -{#areas-label .member} -**`label`**`?:` [`Label`](#label) `| null` [¶](#areas-label){.headerlink} -: Each row's label: a text field, or label options. - -{#areas-legend .member} -**`legend`**`?: string | null` [¶](#areas-legend){.headerlink} -: A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap - across its domain, a cell for each category, or a swatch beside each name of a map's - `categories`. `color` must map through a colormap with a fixed domain or named categories; in a - conditional, `otherwise` is searched before the cases. - (faces)= ### `Faces` {.signature} -`interface Faces` +`interface Faces extends` [`MarkOptions`](#markoptions) Rows drawn as triangles between the vertices named by three reference fields: meshes, surfaces, and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen from above shows its exact colors. -{#faces-of .member} -**`of`**`: [data: string, table: string]` [¶](#faces-of){.headerlink} -: The dataset and table drawn. - {#faces-corners .member} **`corners`**`?: [a: string, b: string, c: string] | null` [¶](#faces-corners){.headerlink} : The reference fields naming each row's three vertices; required. Without a color, a face blends the colors of its corners. -{#faces-color .member} -**`color`**`?:` [`PaintInput`](#paintinput) `| null` [¶](#faces-color){.headerlink} -: Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their - corners' colors. - -{#faces-visible .member} -**`visible`**`?:` [`ExprInput`](#exprinput) [¶](#faces-visible){.headerlink} -: Whether each row is drawn and can be picked. Default `true`. - -{#faces-priority .member} -**`priority`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#faces-priority){.headerlink} -: Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are - the last dropped when a table too large to draw whole is thinned to the tiles in view, and are - labeled first. - -{#faces-name .member} -**`name`**`?: string | null` [¶](#faces-name){.headerlink} -: Distinguishes two marks of the same table across scenes, and names the mark in hits. - -{#faces-pick .member} -**`pick`**`?: boolean | null` [¶](#faces-pick){.headerlink} -: Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces - do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. - -{#faces-select .member} -**`select`**`?: boolean` [¶](#faces-select){.headerlink} -: Whether clicking a row selects it. Default `true`. - -{#faces-drag .member} -**`drag`**`?: boolean` [¶](#faces-drag){.headerlink} -: Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the - camera. Default `false`. - -{#faces-label .member} -**`label`**`?:` [`Label`](#label) `| null` [¶](#faces-label){.headerlink} -: Each row's label: a text field, or label options. - -{#faces-legend .member} -**`legend`**`?: string | null` [¶](#faces-legend){.headerlink} -: A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap - across its domain, a cell for each category, or a swatch beside each name of a map's - `categories`. `color` must map through a colormap with a fixed domain or named categories; in a - conditional, `otherwise` is searched before the cases. - (marker)= ### `Marker` @@ -1566,10 +1377,6 @@ a text field, or options. Where labels would overlap, the higher priority shows; labels already shown stay. In a diagram a label is part of the drawing: it scales with the zoom, fades out when too small to read, and shows wherever it fits its vertex. -{#label-exprinput .member} -[`ExprInput`](#exprinput) [¶](#label-exprinput){.headerlink} -: A value option: an [`Expr`](#expr), or its JSON form, in which a string is a field name. - {#label-text .member} **`text`**`:` [`ExprInput`](#exprinput) [¶](#label-text){.headerlink} : The text field to show. @@ -1611,18 +1418,6 @@ A size: a value in the space's unit (units in a diagram, CSS pixels elsewhere), pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the view zooms in. -{#size-exprinput .member} -[`ExprInput`](#exprinput) [¶](#size-exprinput){.headerlink} -: A value option: an [`Expr`](#expr), or its JSON form, in which a string is a field name. - -{#size-px .member} -[`Px`](#px) [¶](#size-px){.headerlink} -: A size in CSS pixels, regardless of the space's unit. - -{#size-units .member} -[`Units`](#units) [¶](#size-units){.headerlink} -: A size in position units (degrees on the earth), regardless of the space's unit, so it scales as the view zooms. - (px)= ### `Px` @@ -1671,50 +1466,6 @@ Expressions as JSON: a constant, a field, or one of these nodes. Colors take CSS A value for each row: a number, a boolean, a field's name, or a node that computes one. -{#exprnode-fieldnode .member} -[`FieldNode`](#fieldnode) [¶](#exprnode-fieldnode){.headerlink} -: A field's value in each row: from the mark's own table, or with `from`, from another dataset's table joined by row id. - -{#exprnode-colornode .member} -[`ColorNode`](#colornode) [¶](#exprnode-colornode){.headerlink} -: A constant color. - -{#exprnode-statenode .member} -[`StateNode`](#statenode) [¶](#exprnode-statenode){.headerlink} -: View state of each row, such as whether it is selected. - -{#exprnode-missingnode .member} -[`MissingNode`](#missingnode) [¶](#exprnode-missingnode){.headerlink} -: A missing value. - -{#exprnode-unarynode .member} -[`UnaryNode`](#unarynode) [¶](#exprnode-unarynode){.headerlink} -: A one-argument operation, such as `{ "op": "abs", "args": ["x"] }`. - -{#exprnode-binarynode .member} -[`BinaryNode`](#binarynode) [¶](#exprnode-binarynode){.headerlink} -: A two-argument operation, such as `{ "op": "add", "args": ["a", 1] }`. - -{#exprnode-whennode .member} -[`WhenNode`](#whennode) [¶](#exprnode-whennode){.headerlink} -: The value of the first case whose condition holds, else `otherwise`. - -{#exprnode-isnode .member} -[`IsNode`](#isnode) [¶](#exprnode-isnode){.headerlink} -: Whether a text field equals a text exactly. - -{#exprnode-containsnode .member} -[`ContainsNode`](#containsnode) [¶](#exprnode-containsnode){.headerlink} -: Whether a text field contains a text, ignoring case. - -{#exprnode-scalenode .member} -[`ScaleNode`](#scalenode) [¶](#exprnode-scalenode){.headerlink} -: A value mapped linearly onto a range, clamped to the range. - -{#exprnode-colormapnode .member} -[`ColormapNode`](#colormapnode) [¶](#exprnode-colormapnode){.headerlink} -: A value mapped through a colormap. - (paintnode)= ### `PaintNode` @@ -1729,22 +1480,6 @@ colors. [`Color`](#color) [¶](#paintnode-color){.headerlink} : A CSS color, such as `#ffb000` or `rebeccapurple`. -{#paintnode-colornode .member} -[`ColorNode`](#colornode) [¶](#paintnode-colornode){.headerlink} -: A constant color. - -{#paintnode-colormapnode .member} -[`ColormapNode`](#colormapnode) [¶](#paintnode-colormapnode){.headerlink} -: A value mapped through a colormap. - -{#paintnode-whenpaintnode .member} -[`WhenPaintNode`](#whenpaintnode) [¶](#paintnode-whenpaintnode){.headerlink} -: A conditional color: the color of the first case whose condition holds, else `otherwise`. - -{#paintnode-missingnode .member} -[`MissingNode`](#missingnode) [¶](#paintnode-missingnode){.headerlink} -: A missing value. - (fieldnode)= ### `FieldNode` @@ -1984,14 +1719,6 @@ pointer), `elapsed` (seconds since the view was created), or `zoom` (the camera' Colors for a range of values: a catalog name, or your own colors. -{#colormap-colormapname .member} -[`ColormapName`](#colormapname) [¶](#colormap-colormapname){.headerlink} -: A colormap of the catalog. - -{#colormap-customcolormap .member} -[`CustomColormap`](#customcolormap) [¶](#colormap-customcolormap){.headerlink} -: A custom colormap: its kind, its colors, and for a categorical map, its category names. - (colormapname)= ### `ColormapName` @@ -2285,7 +2012,7 @@ View events and hits, dataset descriptions and values, and frame statistics. Something that happened in a view, as [`view.on`](#view-on) reports it. {#viewevent-click .member} -`{ type:` **`'click'`**`; detail: { hit:` [`Hit`](#hit)`; trigger:` [`Trigger`](#trigger) `} }` [¶](#viewevent-click){.headerlink} +**`'click'`** [¶](#viewevent-click){.headerlink} : A row was clicked. {#viewevent-click-hit .member} @@ -2297,7 +2024,7 @@ Something that happened in a view, as [`view.on`](#view-on) reports it. : How the user clicked it. {#viewevent-open .member} -`{ type:` **`'open'`**`; detail: { hit:` [`Hit`](#hit)`; trigger:` [`Trigger`](#trigger) `} }` [¶](#viewevent-open){.headerlink} +**`'open'`** [¶](#viewevent-open){.headerlink} : A row was double-clicked, or Enter was pressed on the row the keyboard points at. {#viewevent-open-hit .member} @@ -2309,7 +2036,7 @@ Something that happened in a view, as [`view.on`](#view-on) reports it. : How the user opened it. {#viewevent-menu .member} -`{ type:` **`'menu'`**`; detail: { hits:` [`Hit`](#hit)`[]; trigger:` [`Trigger`](#trigger) `} }` [¶](#viewevent-menu){.headerlink} +**`'menu'`** [¶](#viewevent-menu){.headerlink} : A context menu was requested: by a secondary click, the menu key, or a touch or pen held still. Lists the rows under the trigger's point, for a menu of actions on them. @@ -2322,7 +2049,7 @@ Something that happened in a view, as [`view.on`](#view-on) reports it. : How the user asked. {#viewevent-drag .member} -`{ type:` **`'drag'`**`; detail: { phase:` [`Phase`](#phase)`; hit:` [`Hit`](#hit)`; position: [number, number, number]; trigger:` [`Trigger`](#trigger) `} }` [¶](#viewevent-drag){.headerlink} +**`'drag'`** [¶](#viewevent-drag){.headerlink} : A row of a draggable mark is dragged. The drag starts once the pointer moves more than 4 CSS pixels from where it pressed the row. @@ -2345,7 +2072,7 @@ Something that happened in a view, as [`view.on`](#view-on) reports it. : The pointer. {#viewevent-connect .member} -`{ type:` **`'connect'`**`; detail: { from:` [`End`](#end)`; to:` [`End`](#end) `| null; edge:` [`Row`](#row) `| null; trigger:` [`Trigger`](#trigger) `} }` [¶](#viewevent-connect){.headerlink} +**`'connect'`** [¶](#viewevent-connect){.headerlink} : A connection was drawn with [`controls.connect`](#controls-connect) on: from a port, from a vertex with Alt held, or by dragging an edge's end off it. @@ -2366,7 +2093,7 @@ Something that happened in a view, as [`view.on`](#view-on) reports it. : Where it was released. {#viewevent-delete .member} -`{ type:` **`'delete'`**`; detail: { rows:` [`Rows`](#rows)`[]; trigger:` [`Trigger`](#trigger) `} }` [¶](#viewevent-delete){.headerlink} +**`'delete'`** [¶](#viewevent-delete){.headerlink} : Delete or Backspace was pressed with rows selected and [`controls.delete`](#controls-delete) on. {#viewevent-delete-rows .member} @@ -2378,7 +2105,7 @@ Something that happened in a view, as [`view.on`](#view-on) reports it. : The key. {#viewevent-hovered .member} -`{ type:` **`'hovered'`**`; detail: { hit:` [`Hit`](#hit) `| null; trigger:` [`Trigger`](#trigger) `} }` [¶](#viewevent-hovered){.headerlink} +**`'hovered'`** [¶](#viewevent-hovered){.headerlink} : The hovered row changed: [`view.hovered`](#view-hovered). {#viewevent-hovered-hit .member} @@ -2390,7 +2117,7 @@ Something that happened in a view, as [`view.on`](#view-on) reports it. : The pointer, or the keys. {#viewevent-selection .member} -`{ type:` **`'selection'`**`; detail: { rows:` [`Rows`](#rows)`[]; trigger:` [`Trigger`](#trigger) `| null } }` [¶](#viewevent-selection){.headerlink} +**`'selection'`** [¶](#viewevent-selection){.headerlink} : The selection changed: [`view.selection`](#view-selection). {#viewevent-selection-rows .member} @@ -2402,15 +2129,15 @@ Something that happened in a view, as [`view.on`](#view-on) reports it. : How the user changed it; `null` when the data dropped rows from it. {#viewevent-camera .member} -`{ type:` **`'camera'`**`; detail:` [`Camera`](#camera) `}` [¶](#viewevent-camera){.headerlink} +**`'camera'`**`:` [`Camera`](#camera) [¶](#viewevent-camera){.headerlink} : The camera moved by a gesture, fit, flight, coast, or orbit: [`view.camera`](#view-camera). {#viewevent-waiting .member} -`{ type:` **`'waiting'`**`; detail:` [`Waiting`](#waiting) `}` [¶](#viewevent-waiting){.headerlink} +**`'waiting'`**`:` [`Waiting`](#waiting) [¶](#viewevent-waiting){.headerlink} : The work the last frame waited on changed; empty once frames are complete: [`view.waiting`](#view-waiting). {#viewevent-error .member} -`{ type:` **`'error'`**`; detail:` [`Failure`](#failure) `}` [¶](#viewevent-error){.headerlink} +**`'error'`**`:` [`Failure`](#failure) [¶](#viewevent-error){.headerlink} : Something failed; reported once. A mark that no longer fits its data is also listed in [`view.failures`](#view-failures). diff --git a/npm/README.md b/npm/README.md index 597e0b9d..0ce376f4 100644 --- a/npm/README.md +++ b/npm/README.md @@ -1,7 +1,7 @@ # latkit -Columnar data drawn as vertices, edges, lines, and areas with WebGPU. -[Documentation](https://latkit.readthedocs.io). +Large networks, such as power grids, drawn with WebGPU: datasets of tables, scenes of vertices, +edges, lines, and areas over them, and pictures and video of the views. ```ts import { Data, Gpu, Patch, field, load, type Scene } from 'latkit'; @@ -28,49 +28,11 @@ const gpu = await Gpu.create(); const view = gpu.view(scene, { sources: [grid] }); gpu.canvas(document.querySelector('canvas')!, { layout: [{ view }] }); view.fit(); -``` - -- **Plain data needs no WebAssembly.** Scenes, expressions, and patches are built without loading - anything, so a worker or a Node server writes patches as well as a page. `load()` loads the - WebAssembly once per page; `Gpu.create()` requests a WebGPU device. -- **Views draw sources.** `view.sources` names the datasets a view draws: a `Data` at its latest - version from the frame after each patch applies, a `Snapshot` as it is, or `Tiles` fetched in - pieces as the camera and plot window ask. -- **Data changes in place.** `data.apply(patch)` makes every change or none. `data.snapshot()` keeps a - version in O(1), sharing its pages, as an undo history does; `data.restore(snapshot)` brings it - back. -- **Patches are bytes.** A patch is a frame of latkit's byte format, written in TypeScript as it is - built; servers in any language write the same frames. `Patch.stream(body)` yields them as they - arrive, and `Data.read(source)` reads a dataset's bytes, its JSON, or GeoJSON. -- **Data reads back** without a GPU, through the expressions marks draw with: - `data.query({ of, filter, sort, select, limit })`, `data.describe()`, `data.envelope(...)`. -- **Types come from the schema.** Scene, query, camera, and fit types (`inputs.ts`) and events, - hits, and stats (`outputs.ts`) are written from latkit's JSON Schema; `latkit schema` prints it. -- **Waits end at a signal.** Picks, locations, layouts, pictures, and encodings take `{ signal }` - and give up after 30 seconds unless it says otherwise: with `timeout` when it timed out, with - `cancelled` when it was aborted or the view went. -- **Errors** throw a `LatkitError`: a `code` (`invalid`, `unsupported`, `limit`, `io`, `device-lost`, - `cancelled`, `timeout`, or `unloaded` before `load()`) and, for invalid input, the `path` to the - part. -- **Handles** (`Data`, `Snapshot`, `Gpu`, `View`, `Canvas`, `Image`) have idempotent `destroy()` and - `Symbol.dispose`. -Workers start when a view first needs one, share the compiled module, and retire after 30 seconds -idle. For your own asset hosting, pass the matching `latkit/wasm` and `latkit/worker` exports; the -worker is a standalone script, fine as a Blob URL: - -```ts -import wasmUrl from 'latkit/wasm?url'; -import workerUrl from 'latkit/worker?url'; - -await load({ wasm: wasmUrl, worker: () => new Worker(workerUrl) }); +grid.apply(new Patch().set('Bus', [102], { kv: [345] })); // drawn the next frame ``` -Serve WebAssembly as `application/wasm`, in a secure context for WebGPU. - -From the repository: `cargo xtask wasm --dev` builds the package for development, and -`cargo xtask package` builds an optimized tarball under `target/npm`. - -The binary format uses 64-bit frame indexes. Sample values use `f32` and axis coordinates use -`f64`. `Video.record()` cleans up on failure; manual recordings must finish or abort. -See [deploying](https://latkit.readthedocs.io/en/latest/guide.html#deploying). +Scenes, expressions, and patches are plain data, typed from latkit's schema, and need no +WebAssembly, so a worker or a server builds them too. The +[guide](https://latkit.readthedocs.io/en/latest/guide.html) shows each task, and the +[API reference](https://latkit.readthedocs.io/en/latest/reference/api.html) documents every export. diff --git a/npm/docs.mjs b/npm/docs.mjs index c497e898..76ea3786 100644 --- a/npm/docs.mjs +++ b/npm/docs.mjs @@ -1,11 +1,12 @@ /** - * Writes the reference pages in `docs/reference` from the package's TypeScript: the JavaScript API - * from the shipped declarations, and the scene and result types from the types the schema writes. - * Each entry has a heading, its declaration, and its documentation, then its members or a union's - * forms: each a signature and its definition, with an anchor of its own. Types in signatures, and - * code spans in the documentation that name an entry or a member, link to it. With `--check`, - * asserts that the checked-in pages are current instead. The pages are checked in so Read the Docs - * can build them with Python alone. + * Writes the reference pages in `docs/reference`: the JavaScript API from the package's shipped + * declarations, the scene and result types from the types the schema writes, and the command + * reference from the command tree the CLI's test writes to `cli.json`. Each entry has a heading, + * its declaration, and its documentation, then its members or a union's forms: each a signature + * and its definition, with an anchor of its own. Types in signatures, and code spans in the + * documentation that name an entry or a member, link to it. With `--check`, asserts that the + * checked-in pages are current instead. The pages are checked in so Read the Docs can build them + * with Python alone. */ import assert from 'node:assert/strict'; import fs from 'node:fs'; @@ -91,10 +92,21 @@ function read(file) { ? `<${node.typeParameters.map((p) => (names ? p.name.getText(source) : text(p))).join(', ')}>` : ''; + /** + * The fields of a function's options: the members of each parameter typed as an object type, or + * as an intersection with one, such as `{ radius?: number } & Wait`. + */ + const optionsOf = (node) => + node.parameters.flatMap((p) => { + const types = p.type && ts.isIntersectionTypeNode(p.type) ? p.type.types : [p.type]; + return types.filter((t) => t && ts.isTypeLiteralNode(t)).flatMap((t) => members(t)); + }); + /** * The members of a class, an interface, or an object type, in order. A getter and its setter are * one property, and a method's overloads one method. A property of an object type lists that - * type's members as its `fields`. + * type's members as its `fields`, and a method its options' members. `Symbol.dispose` is left to + * `Handle`'s documentation. */ const members = (node) => { const out = []; @@ -102,15 +114,11 @@ function read(file) { for (const m of node.members) { if (has(m, ts.SyntaxKind.PrivateKeyword) || has(m, ts.SyntaxKind.ProtectedKeyword)) continue; if (m.name && ts.isPrivateIdentifier(m.name)) continue; + if (m.name && ts.isComputedPropertyName(m.name)) continue; const about = doc(m); if (/^@internal\b/.test(about)) continue; const isStatic = has(m, ts.SyntaxKind.StaticKeyword); - let name = 'new'; - if (m.name) { - name = ts.isComputedPropertyName(m.name) - ? `[${m.name.expression.getText(source)}]` - : m.name.getText(source); - } + const name = m.name ? m.name.getText(source) : 'new'; if (ts.isGetAccessor(m) || ts.isSetAccessor(m)) { let entry = byName.get(name); if (!entry) { @@ -140,7 +148,7 @@ function read(file) { ts.isMethodDeclaration(m) || ts.isMethodSignature(m) ) { - const overload = { code: text(m), doc: about }; + const overload = { code: text(m), doc: about, options: optionsOf(m) }; const known = byName.get(`${name}()`); if (known) { known.overloads.push(overload); @@ -177,7 +185,8 @@ function read(file) { const about = doc(node); if (ts.isFunctionDeclaration(node)) { const name = node.name.getText(source); - const overload = { code: text(node).replace(/^export (declare )?/, ''), doc: about }; + const code = text(node).replace(/^export (declare )?/, ''); + const overload = { code, doc: about, options: optionsOf(node) }; const known = found.get(name); if (known) { known.overloads.push(overload); @@ -202,6 +211,7 @@ function read(file) { const name = node.name.getText(source); found.set(name, { kind: ts.isClassDeclaration(node) ? 'class' : 'interface', + abstract: has(node, ts.SyntaxKind.AbstractKeyword), name, generics: generics(node), parameters: generics(node, true), @@ -294,9 +304,6 @@ const undocumented = []; /** Notes `what` as undocumented unless `doc` says something. */ const documented = (doc, what) => doc || undocumented.push(what); -/** The first sentence of `doc`, on one line. */ -const summary = (doc) => (doc.match(/^[\s\S]*?\.(?=\s|$)/)?.[0] ?? doc).replace(/\s+/g, ' '); - // Signatures /** @@ -437,11 +444,20 @@ async function memberItems(owner, member, path = []) { name = [0, `new ${owner}`.length]; } const term = signature(code, owner, name); - items.push(item(i === 0 ? id : undefined, term, linked(overload.doc || member.doc, self))); + const options = await optionItems(owner, [...path, member.name], overload.options); + const about = linked(overload.doc || member.doc, self); + items.push(item(i === 0 ? id : undefined, term, [about, ...options].join('\n\n'))); } return items; } +/** The items of a function's or a method's options, at `path` below entry `owner`. */ +async function optionItems(owner, path, options = []) { + const items = []; + for (const option of options) items.push(...(await memberItems(owner, option, path))); + return items; +} + /** The items of a class's or an interface's members: properties, then methods, each under a rubric. */ async function membersOf(entry) { const groups = [ @@ -473,10 +489,11 @@ function eventKey(entry) { } /** - * The items of a union's branches: each documented value; each named type, with its summary; and - * each form. An event, `{ type: 'name'; detail: … }`, or a tagged form, `{ key: options }`, is one - * item, its fields in its definition; any other form lists its keys, after its doc when the union - * has several such forms. + * The items of a union's branches: each documented value or named type, and each form. A named + * type without a doc of its own needs no item: the declaration links it. An event, + * `{ type: 'name'; detail: … }`, is one item named by its tag, its detail's fields in its + * definition. A tagged form, `{ key: options }`, is one item, its fields in its definition. Any + * other form lists its keys, after its doc when the union has several such forms. */ async function branchesOf(entry) { const owner = entry.name; @@ -487,12 +504,9 @@ async function branchesOf(entry) { if (branch.kind === 'literal' && branch.doc) { const term = signature(branch.text, owner, [0, branch.text.length]); out.push(item(anchor(owner, branch.text), term, linked(branch.doc, owner))); - } else if (branch.kind === 'named' && targets.has(branch.name)) { - const about = branch.doc || summary(targets.get(branch.name).doc); - documented(about, `${owner} ${branch.name} has no documentation`); - out.push(item(anchor(owner, branch.name), link(branch.name), linked(about, owner))); + } else if (branch.kind === 'named' && branch.doc && targets.has(branch.name)) { + out.push(item(anchor(owner, branch.name), link(branch.name), linked(branch.doc, owner))); } else if (branch.kind === 'object' && (key || branch.members.length === 1)) { - const code = await oneLine(branch.text, 'type'); const tag = key ? branch.members.find((m) => m.name === key).type : branch.members[0].name; const payload = key ? branch.members.find((m) => m.name !== key) : branch.members[0]; const name = key ? tag.slice(1, -1) : tag; @@ -501,8 +515,16 @@ async function branchesOf(entry) { const fields = []; for (const field of payload?.fields ?? []) fields.push(...(await memberItems(owner, field, [name]))); - const at = code.indexOf(tag); - const term = signature(code, owner, [at, at + tag.length]); + let term; + if (key) { + // An event reads as its tag, and the type of a detail that has no fields of its own. + const detail = payload && !payload.fields?.length ? `: ${payload.type}` : ''; + term = `**${codeSpan(tag)}**${detail && linkedCode(detail, owner)}`; + } else { + const code = await oneLine(branch.text, 'type'); + const at = code.indexOf(tag); + term = signature(code, owner, [at, at + tag.length]); + } out.push(item(anchor(owner, name), term, [linked(about, owner), ...fields].join('\n\n'))); } else if (branch.kind === 'object') { if (forms.length > 1) { @@ -529,7 +551,8 @@ async function declaration(entry) { if (entry.branches.length > 1) return [await unionDeclaration(entry)]; return [await oneLine(`type ${entry.name}${entry.generics} = ${entry.type}`)]; } - return [[`${entry.kind} ${entry.name}${entry.generics}`, ...entry.heritage].join(' ')]; + const kind = `${entry.abstract ? 'abstract ' : ''}${entry.kind}`; + return [[`${kind} ${entry.name}${entry.generics}`, ...entry.heritage].join(' ')]; } /** An entry, as its page shows it. */ @@ -543,6 +566,8 @@ async function render(entry) { ...lines.map((line) => `{.signature}\n${line}`), linked(entry.doc, entry.name), ]; + const options = (entry.overloads ?? []).find((o) => o.options?.length)?.options; + parts.push(...(await optionItems(entry.name, [], options))); if (entry.members) parts.push(...(await membersOf(entry))); if (entry.branches && entry.branches.length > 1) parts.push(...(await branchesOf(entry))); return parts.join('\n\n'); @@ -561,6 +586,95 @@ async function page(head, entries, sections) { return `${out.join('\n\n')}\n`; } +// The command reference + +/** + * The command reference, from the command tree the CLI's test writes to `cli.json`: each command's + * usage and description, then its arguments and options, each a term over its definition with an + * anchor of its own. Code spans that name a command or an option link to it. A command that takes + * `render`'s arguments says so instead of listing them again. + */ +function cliPage() { + const commands = JSON.parse(fs.readFileSync(path.join(root, 'docs/reference/cli.json'), 'utf8')); + const anchorOf = (command) => `cli-${command.name.split(' ')[1] ?? 'latkit'}`; + const idOf = (command, arg) => `${anchorOf(command)}-${arg.long ?? arg.id}`; + // What code spans link to: each command, and each option, first in its own command. + const links = new Map(commands.map((command) => [command.name, anchorOf(command)])); + for (const command of commands) + for (const arg of command.args) + if (arg.long && !links.has(`--${arg.long}`)) links.set(`--${arg.long}`, idOf(command, arg)); + const sentence = (help) => { + const text = help.trim(); + return !text || text.endsWith('.') ? text : `${text}.`; + }; + const term = (arg) => { + if (arg.positional) { + const value = arg.required ? `<${arg.value}>` : `[${arg.value}]`; + return `**\`${value}${arg.many ? '...' : ''}\`**`; + } + const short = arg.short ? `\`-${arg.short},\` ` : ''; + return `${short}**\`--${arg.long}\`**${arg.value ? ` \`<${arg.value}>\`` : ''}`; + }; + const render = commands.find((command) => command.name === 'latkit render'); + const out = [ + '# Command reference', + "Every command and option of the `latkit` command line, written from its help. The [command-line guide](../cli.md) shows them at work. `--help` prints a command's help in the terminal, and `latkit --version` prints the version.", + ]; + for (const command of commands) { + const top = command.name === 'latkit'; + const anchor = anchorOf(command); + anchors.push(anchor); + const local = new Map(links); + for (const arg of command.args) if (arg.long) local.set(`--${arg.long}`, idOf(command, arg)); + const linked = (text) => + text.replace(/`([^`\n]+)`/g, (span, code) => + local.has(code) ? `[${span}](#${local.get(code)})` : span, + ); + out.push( + `(${anchor})=`, + `## \`${command.name}\``, + `{.signature}\n\`${command.usage}\``, + linked(sentence(command.about)), + ); + if (top) { + out.push('```{rubric} Commands\n```'); + for (const sub of commands.slice(1)) + out.push( + item(undefined, `[\`${sub.name}\`](#${anchorOf(sub)})`, linked(sentence(sub.about))), + ); + } + if ( + !top && + command !== render && + JSON.stringify(command.args) === JSON.stringify(render.args) + ) { + out.push(`Takes ${linked('`latkit render`')}'s arguments and options.`); + continue; + } + for (const [title, positional] of [ + ['Arguments', true], + ['Options', false], + ]) { + const args = command.args.filter((arg) => arg.positional === positional); + if (!args.length) continue; + out.push(`\`\`\`{rubric} ${title}\n\`\`\``); + for (const arg of args) { + const id = idOf(command, arg); + let about = sentence(arg.help); + if (arg.defaults.length) + about += ` Defaults to ${arg.defaults.map((v) => `\`${v}\``).join(', ')}.`; + if (top && arg.global) about += ' Every command takes it.'; + const values = arg.values.map((value) => { + assert(value.help, `${command.name} ${id}: value ${value.name} has no help`); + return item(`${id}-${value.name}`, `**\`${value.name}\`**`, linked(sentence(value.help))); + }); + out.push(item(id, term(arg), [linked(about), ...values].join('\n\n'))); + } + } + } + return `${out.join('\n\n')}\n`; +} + // The pages const API = [ @@ -580,30 +694,30 @@ const TYPES = ['inputs', 'outputs']; const sections = { 'api.md': [ [ - 'Loading and waiting', - 'Load the WebAssembly once per page. Every wait ends when its signal aborts, after 30 seconds by default.', - ['load', 'LoadOptions', 'Wait', 'LatkitError'], + 'Loading, errors, and lifetimes', + 'What every part of the package shares.', + ['load', 'LatkitError', 'Wait', 'Handle'], ], [ 'Data and patches', 'Datasets of named tables, the patches that change them, written without WebAssembly, and the sources views draw.', [ + 'Dataset', 'Data', 'Snapshot', 'Tiles', 'Source', - 'DataSource', 'QueryResult', 'Envelope', 'Patch', - 'PatchSource', + 'ByteSource', + 'ReadableBytes', 'RowIds', 'Values', 'Geometry', 'Reference', 'reference', 'Fields', - 'ReadableBytes', ], ], [ @@ -628,7 +742,6 @@ const sections = { 'InputOf', 'px', 'units', - 'Within', 'selected', 'hovered', 'pointer', @@ -636,25 +749,14 @@ const sections = { 'elapsed', 'icon', 'RgbaImage', + 'colormaps', 'colormapCss', ], ], [ 'Views', 'A GPU makes views, and canvases that show them.', - [ - 'Gpu', - 'View', - 'ViewOptions', - 'ViewEvents', - 'Insets', - 'Point', - 'Position', - 'Canvas', - 'CanvasOptions', - 'Panel', - 'Rect', - ], + ['Gpu', 'View', 'ViewEvents', 'Insets', 'Point', 'Position', 'Canvas', 'Panel', 'Rect'], ], [ 'Pictures and video', @@ -713,6 +815,8 @@ const sections = { 'The marks that draw the tables of a scene.', [ 'Mark', + 'MarkOptions', + 'StrokeOptions', 'Vertices', 'Edges', 'Lines', @@ -792,9 +896,19 @@ const sections = { const heads = { 'api.md': `# JavaScript API -Every class, function, and type the \`latkit\` package exports. \`load()\` loads the WebAssembly -that datasets, views, and pictures need; scenes, expressions, and patches need none. Scenes and -their options take the [scene and result types](types.md).`, +Every class, function, and type the \`latkit\` package exports. Scenes and their options take the +[scene and result types](types.md). Four rules hold throughout, so the entries below leave them +out: + +- **Loading.** Scenes, expressions, and patches are plain data. Datasets, views, pictures, and the + colormap functions need latkit's WebAssembly: until \`load()\` finishes, they throw an + \`unloaded\` error. \`Gpu.create()\`, \`Data.read()\`, and \`Patch.read()\` of JSON load it + themselves. +- **Errors.** Everything latkit throws or rejects with is a \`LatkitError\`. Input it cannot use + fails with an \`invalid\` error whose \`path\` names the part, such as + \`marks[2].vertices.color\`, and changes nothing. +- **Waits.** Every promise waits within its \`Wait\`: until its \`signal\` aborts, or 30 seconds. +- **Lifetimes.** A \`Handle\` holds memory until \`destroy()\`, or a \`using\` declaration, frees it.`, 'types.md': `# Scene and result types The types of the scenes latkit draws and of what it reports, written from the same schema as the @@ -862,6 +976,8 @@ for (const name of Object.keys(entries)) { assert(!content.includes('{@link'), `${file}: a link left unresolved`); written.set(file, content); } +written.set(path.join(root, 'docs/reference/cli.md'), cliPage()); + // Anchors are labels for the whole site, so each must be unique, and none may be a heading's slug. const slugs = [ ...Object.values(heads), diff --git a/npm/examples/gate/browser.ts b/npm/examples/gate/browser.ts index ec1e7ec0..376aad20 100644 --- a/npm/examples/gate/browser.ts +++ b/npm/examples/gate/browser.ts @@ -1,11 +1,13 @@ /** - * Helpers shared by the gate and the benchmark: launching Chrome, and deadlines for every wait. + * Helpers shared by the gate and the benchmark: serving a page, launching Chrome, collecting a + * page's errors, and deadlines for every wait. */ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { chromium, type Browser } from 'playwright-core'; +import { chromium, type Browser, type Page } from 'playwright-core'; +import { createServer } from 'vite'; /** Milliseconds any one step may take before giving up on it. */ export const STEP = 60_000; @@ -19,6 +21,49 @@ export function deadline(work: Promise, ms: number, step: string): Promise return Promise.race([work, late]).finally(() => clearTimeout(timer)); } +/** A Vite dev server, and the address it serves at. */ +export interface Served { + readonly url: string; + /** Stops the server, waiting at most 10 s. */ + close(): Promise; +} + +/** Serves the Vite project in `root` with its `vite.config.ts`, at `port` or the next free port. + * `what` names it in errors. */ +export async function serve(root: string, port: number, what: string): Promise { + const server = await deadline( + createServer({ + root, + configFile: path.join(root, 'vite.config.ts'), + logLevel: 'error', + server: { port, strictPort: false }, + }), + STEP, + `configure ${what}`, + ); + const close = () => deadline(server.close(), 10_000, `stop serving ${what}`).catch(() => {}); + try { + await deadline(server.listen(), STEP, `serve ${what}`); + const url = server.resolvedUrls?.local[0]; + if (!url) throw new Error(`${what} has no address`); + return { url, close }; + } catch (error) { + await close(); + throw error; + } +} + +/** Collects `page`'s uncaught errors and console errors as they happen, each cut to 300 + * characters. */ +export function errorsOf(page: Page): string[] { + const errors: string[] = []; + page.on('pageerror', (error) => errors.push(`page: ${error.message.slice(0, 300)}`)); + page.on('console', (message) => { + if (message.type() === 'error') errors.push(`console: ${message.text().slice(0, 300)}`); + }); + return errors; +} + /** Returns the path of the Chrome to drive: `$LATKIT_CHROME`, or else the newest Chromium in * Playwright's browser cache. Throws if there is none. */ export function chrome(): string { diff --git a/npm/examples/gate/gate.ts b/npm/examples/gate/gate.ts index 70a7eca7..84384a8d 100644 --- a/npm/examples/gate/gate.ts +++ b/npm/examples/gate/gate.ts @@ -20,8 +20,8 @@ import fs from 'node:fs'; import http from 'node:http'; import path from 'node:path'; import type { Page } from 'playwright-core'; -import { createServer } from 'vite'; -import { STEP, deadline, launch, type Chrome } from './browser.ts'; +import { STEP, deadline, errorsOf, launch, serve, type Chrome } from './browser.ts'; +import { quantile } from './quantile.ts'; /** Maximum milliseconds Chrome may take to compile one pipeline cold. The slowest takes about * 0.35 s. A view opens no sooner than its slowest pipeline; the others compile in parallel. */ @@ -276,21 +276,18 @@ async function visit(url: string, id: string): Promise { * the pointer and keys (hovering a grid of points, zooming, dragging, clicking, right-clicking, and * stepping with Tab and the arrows) and exports a picture. */ async function open(url: string, id: string): Promise { - const opened: Opened = { wrong: [], slow: [] }; - const { wrong, slow } = opened; const chrome = await cold(); try { - const context = await chrome.browser.newContext({ - viewport: { width: 1280, height: 800 }, - deviceScaleFactor: 1, - }); - const page = await context.newPage(); - page.on('console', (message) => { - if (message.type() === 'error') wrong.push(`console: ${message.text().slice(0, 300)}`); - }); - page.on('pageerror', (error) => wrong.push(`page: ${error.message.slice(0, 300)}`)); - // Tracks the longest gap between animation frames since the last reset, the display's frame - // interval (the median of the last 120 gaps), and each gap longer than `LONG` with what the + const context = await deadline( + chrome.browser.newContext({ viewport: { width: 1280, height: 800 }, deviceScaleFactor: 1 }), + STEP, + 'a browser context', + ); + const page = await deadline(context.newPage(), STEP, 'a page'); + const opened: Opened = { wrong: errorsOf(page), slow: [] }; + const { wrong, slow } = opened; + // Tracks the longest gap between animation frames since the last reset, the last 120 gaps, + // whose median is the display's frame interval, and each gap longer than `LONG` with what the // views were doing. await page.addInitScript((long: number) => { interface Stage { @@ -299,22 +296,22 @@ async function open(url: string, id: string): Promise { } const w = window as unknown as { gap: number; + gaps: number[]; long: Long[]; resetGap(): void; - interval(): number; stage?: Stage; }; let last = 0; let work: Record = {}; const gaps: number[] = []; w.gap = 0; + w.gaps = gaps; w.long = []; w.resetGap = () => { w.gap = 0; w.long = []; last = 0; }; - w.interval = () => [...gaps].sort((a, b) => a - b)[gaps.length >> 1] ?? 1000 / 60; const tick = (now: number) => { const stage = w.stage; const done = stage?.gpu.work ?? {}; @@ -446,13 +443,14 @@ async function open(url: string, id: string): Promise { wrong.push(`no picture exported: ${said}`); } const { gap, step } = longest; - const { interval, late } = await page.evaluate(() => { + const { gaps, late } = await page.evaluate(() => { const w = window as unknown as { - interval(): number; + gaps: number[]; stage?: { gpu: { work: { late: number } } }; }; - return { interval: w.interval(), late: w.stage?.gpu.work.late ?? 0 }; + return { gaps: w.gaps, late: w.stage?.gpu.work.late ?? 0 }; }); + const interval = gaps.length ? quantile(gaps, 0.5) : 1000 / 60; // A gap of n intervals missed n - 1 frames, so a gap over MISSED + 1.5 intervals missed more // than MISSED. const budget = (MISSED + 1.5) * interval; @@ -467,10 +465,10 @@ async function open(url: string, id: string): Promise { slow.push(`missed ${missed(gap)} frames in a row on ${step} (${gap.toFixed(0)} ms)`); if (late > 0) wrong.push(`${late} pipelines compiled at first use, delaying the frames that use them`); + return opened; } finally { await chrome.close(); } - return opened; } /** Formats a frame longer than `LONG` for the log: each view's last CPU and GPU times and `waiting` @@ -488,20 +486,12 @@ function describe({ gap, cpuMs, gpuMs, waiting, work }: Long): string { /** Serves the suite and visits the pages named in `only`, or, if it is empty, every page in the * example picker. */ async function suite(only: string[]): Promise { - const server = await createServer({ - root: examples, - configFile: path.join(examples, 'vite.config.ts'), - server: { port: 5210, strictPort: false }, - logLevel: 'error', - }); - await deadline(server.listen(), STEP, 'serve the suite'); + const { url, close } = await serve(examples, 5210, 'the suite'); try { - const url = server.resolvedUrls?.local[0]; - if (!url) throw new Error('the suite has no address'); const chrome = await cold(); let ids: string[]; try { - const page = await chrome.browser.newPage(); + const page = await deadline(chrome.browser.newPage(), STEP, 'a page'); await deadline(page.goto(url), STEP, 'open the suite'); const picker = page.getByRole('combobox', { name: 'Example', exact: true }); await deadline(picker.waitFor(), STEP, 'the example picker'); @@ -517,7 +507,7 @@ async function suite(only: string[]): Promise { } return failures; } finally { - await server.close(); + await close(); } } diff --git a/npm/examples/gate/package.ts b/npm/examples/gate/package.ts index 62b631a9..c4ec4a66 100644 --- a/npm/examples/gate/package.ts +++ b/npm/examples/gate/package.ts @@ -6,7 +6,7 @@ import http from 'node:http'; import os from 'node:os'; import path from 'node:path'; import { build } from 'vite'; -import { deadline, launch, STEP, type Chrome } from './browser.ts'; +import { deadline, errorsOf, launch, STEP, type Chrome } from './browser.ts'; const root = path.resolve(import.meta.dirname, '../../..'); const reportPath = path.join(root, 'target/npm/package.json'); @@ -50,6 +50,11 @@ await build({ const dist = path.join(consumer, 'dist'); const server = http.createServer((request, response) => { const url = new URL(request.url ?? '/', 'http://localhost'); + // Chrome asks every site for an icon. The consumer has none, and a 404 would log a console error. + if (url.pathname === '/favicon.ico') { + response.writeHead(204).end(); + return; + } const relative = decodeURIComponent(url.pathname).replace(/^\/nested\/app\//, ''); const file = path.resolve(dist, relative || 'index.html'); if ( @@ -76,10 +81,9 @@ const timings: unknown[] = []; try { for (const mode of ['default', 'custom', 'compiled']) { chrome = await launch(['--disable-gpu-shader-disk-cache']); - const page = await chrome.browser.newPage(); - const errors: string[] = []; + const page = await deadline(chrome.browser.newPage(), STEP, 'open a page'); + const errors = errorsOf(page); const requests: string[] = []; - page.on('pageerror', (error) => errors.push(error.message)); page.on('request', (request) => requests.push(request.url())); await page.addInitScript(() => { const original = window.requestAnimationFrame.bind(window); @@ -121,7 +125,7 @@ try { ); assert.deepEqual(errors, []); timings.push({ mode, ...(result as object) }); - await page.close(); + await deadline(page.close(), 5_000, 'close the page'); await chrome.close(); } const measurement = { diff --git a/npm/examples/gate/quantile.ts b/npm/examples/gate/quantile.ts new file mode 100644 index 00000000..325f8a0b --- /dev/null +++ b/npm/examples/gate/quantile.ts @@ -0,0 +1,7 @@ +/** Returns the `q` quantile of `values`, from 0 (the least) to 1 (the greatest): the value that + * fraction of the way through them in order, rounding down. Returns 0 when there are none. The + * benchmark's page and runner use it too, so it imports nothing. */ +export function quantile(values: readonly number[], q: number): number { + if (!values.length) return 0; + return [...values].sort((a, b) => a - b)[Math.floor(q * (values.length - 1))]!; +} diff --git a/npm/examples/gate/tsconfig.json b/npm/examples/gate/tsconfig.json index 9a603b88..3ecd24b8 100644 --- a/npm/examples/gate/tsconfig.json +++ b/npm/examples/gate/tsconfig.json @@ -13,5 +13,5 @@ "types": ["node"], "lib": ["ES2023", "DOM", "DOM.Iterable"] }, - "include": ["gate.ts", "browser.ts", "package.ts"] + "include": ["gate.ts", "browser.ts", "package.ts", "quantile.ts"] } diff --git a/npm/examples/scripts/history.ts b/npm/examples/scripts/history.ts index 707e08df..c8534e1e 100644 --- a/npm/examples/scripts/history.ts +++ b/npm/examples/scripts/history.ts @@ -10,6 +10,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { Patch } from 'latkit'; +import { random } from '../src/random.ts'; /** The bus count, the area names, the seconds between samples, and the seconds in a day. */ const BUSES = 64; @@ -24,16 +25,6 @@ const DISTURBANCES = [ { at: 18.5 * 3600, area: 0, depth: 0.06 }, ]; -/** A seeded random generator of numbers in [0, 1), the same as the examples' `random`. */ -function random(seed: number): () => number { - return () => { - seed = (seed + 0x6d2b79f5) | 0; - let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); - t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; -} - /** The system load at second `t` of the day, between 0 and 1. It is low at night and peaks in the * morning and evening. */ function load(t: number): number { diff --git a/npm/examples/src/examples/colormaps/index.ts b/npm/examples/src/examples/colormaps/index.ts index 6516b69b..2e374f38 100644 --- a/npm/examples/src/examples/colormaps/index.ts +++ b/npm/examples/src/examples/colormaps/index.ts @@ -8,6 +8,7 @@ import { Data, Patch, colormapCss, + colormaps, field, type Colormap, type ColormapName, @@ -71,60 +72,19 @@ type Show = 'all' | 'inside' | 'outside' | 'not' | 'class' | 'others'; /** A checkerboard behind each ramp, so a colormap's transparent parts are visible. */ const CHECKS = 'repeating-conic-gradient(#888 0 25%, #ccc 0 50%) 0 0 / 8px 8px'; -/** Every catalog colormap grouped by kind, then the custom ones. */ -const CATALOG: readonly (readonly [string, readonly Choice[]])[] = [ - [ - 'Sequential', - [ - 'viridis', - 'cividis', - 'plasma', - 'inferno', - 'magma', - 'mako', - 'rocket', - 'crest', - 'flare', - 'amber', - 'amp', - 'batlow', - 'deep', - 'devon', - 'grays', - 'haline', - 'lajolla', - 'oslo', - 'thermal', - 'turku', - 'turbo', - ], - ], - [ - 'Diverging', - [ - 'balance', - 'berlin', - 'brbg', - 'broc', - 'coolwarm', - 'cork', - 'icefire', - 'managua', - 'piyg', - 'puor', - 'rdbu', - 'spectral', - 'vanimo', - 'vik', - 'vlag', - ], - ], - ['Cyclic', ['phase', 'romao', 'twilight']], - ['Categorical', ['tab10', 'tab20', 'tableau10', 'set2', 'dark2', 'paired', 'okabeito']], - ['Custom', ['custom', 'diverging', 'cyclic']], -]; -const CATEGORICAL = new Set(CATALOG.find(([kind]) => kind === 'Categorical')![1]); -const KINDS = ['All', ...CATALOG.map(([kind]) => kind)] as const; +/** The catalog's colormaps grouped by kind, each kind titled, then the custom ones. */ +function catalog(): (readonly [kind: string, names: readonly Choice[]])[] { + const all = colormaps(); + const kinds = [...new Set(all.map((map) => map.kind))]; + const title = (kind: string) => `${kind[0]!.toUpperCase()}${kind.slice(1)}`; + return [ + ...kinds.map( + (kind) => + [title(kind), all.filter((map) => map.kind === kind).map((map) => map.name)] as const, + ), + ['Custom', ['custom', 'diverging', 'cyclic']], + ]; +} /** Fields the cells can show, each a value from 0 to 1 at a point. Categorical colormaps show the * value's class instead. */ @@ -171,6 +131,8 @@ function hexagons(columns: number, rows: number, value: FieldName): Patch { } export default function page(stage: Stage): void { + const byKind = catalog(); + const categoricalMaps = new Set(byKind.find(([kind]) => kind === 'Categorical')?.[1]); let colormap: Choice = 'viridis'; let value: FieldName = 'peaks'; let size: [number, number] = [96, 64]; @@ -209,7 +171,7 @@ export default function page(stage: Stage): void { }; const scene = (): Scene => { - const categorical = CATEGORICAL.has(colormap); + const categorical = categoricalMaps.has(colormap); const input = categorical ? field('class') : TRANSFORMS[transform]![1](field('value')); const mapped = input.colormap(mapOf(colormap)); const outlines: Mark[] = outline @@ -262,7 +224,7 @@ export default function page(stage: Stage): void { view.scene = scene(); legend.ramp( mapOf(colormap), - `${colormap}${CATEGORICAL.has(colormap) ? ', ten classes' : reversed ? ', reversed' : ''}`, + `${colormap}${categoricalMaps.has(colormap) ? ', ten classes' : reversed ? ', reversed' : ''}`, ); legend.hidden = ownLegend; for (const [name, swatch] of swatches) @@ -277,7 +239,7 @@ export default function page(stage: Stage): void { }; const choose = palette.choice( 'Colormap', - CATALOG.flatMap(([kind, names]) => + byKind.flatMap(([kind, names]) => names.map( (name) => [ @@ -423,11 +385,11 @@ export default function page(stage: Stage): void { // The catalog as CSS ramps: the same colors the GPU draws, drawn by the browser over a // checkerboard so transparent parts show. - const catalog = stage.section('Catalog'); + const ramps = stage.section('Catalog'); const groups: [string, HTMLElement[]][] = []; - catalog.choice( + ramps.choice( 'Kind', - KINDS.map((kind) => [kind, kind] as const), + ['All', ...byKind.map(([kind]) => kind)].map((kind) => [kind, kind] as const), 'All', (shown) => { for (const [kind, elements] of groups) @@ -435,11 +397,11 @@ export default function page(stage: Stage): void { }, ); const swatches = new Map(); - for (const [kind, names] of CATALOG) { + for (const [kind, names] of byKind) { const title = h('p', { class: 'note' }, kind); const grid = h('div', { class: 'swatches' }); groups.push([kind, [title, grid]]); - catalog.content.append(title); + ramps.content.append(title); for (const name of names) { const swatch = h( 'button', @@ -453,7 +415,7 @@ export default function page(stage: Stage): void { 'click', () => { colormap = name; - choose.selectedIndex = CATALOG.flatMap(([, n]) => n).indexOf(name); + choose.selectedIndex = byKind.flatMap(([, n]) => n).indexOf(name); restyle(); }, { signal: stage.signal }, @@ -461,7 +423,7 @@ export default function page(stage: Stage): void { swatches.set(name, swatch); grid.append(swatch); } - catalog.content.append(grid); + ramps.content.append(grid); } stage.meter(view); diff --git a/npm/examples/src/examples/networks/cases.test.ts b/npm/examples/src/examples/networks/cases.test.ts index ff56521a..7e1ce7e2 100644 --- a/npm/examples/src/examples/networks/cases.test.ts +++ b/npm/examples/src/examples/networks/cases.test.ts @@ -1,39 +1,40 @@ import { readFileSync } from 'node:fs'; -import { gunzipSync } from 'node:zlib'; import { describe, expect, test } from 'vitest'; import { readCase } from './cases.js'; +import { inflate } from './format.js'; const CASES = ['wecc240', 'activsg2000', 'activsg10k', 'activsg25k', 'activsg70k'] as const; -/** Reads case `id` from the file the page fetches, inflating it if it is gzip-compressed. */ -function read(id: string) { - const raw = readFileSync(`npm/examples/public/grids/${id}.bin`); - const bytes = raw[0] === 0x1f && raw[1] === 0x8b ? gunzipSync(raw) : raw; - return readCase(id, bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)); +/** Reads case `id` from the file the page fetches. */ +async function read(id: string) { + return readCase(id, await inflate(readFileSync(`npm/examples/public/grids/${id}.bin`))); } describe('grid cases', () => { - test.each(CASES)('%s reads into buses, lines, and machines positioned on the earth', (id) => { - const found = read(id); - const rows = Object.fromEntries( - found.data.describe().tables.map((table) => [table.name, table.rows]), - ); - expect(rows).toMatchObject({ Bus: found.buses, Line: found.lines, Machine: found.machines }); - expect(found.buses).toBeGreaterThan(200); - for (let i = 0; i < found.positions.length; i += 2) { - expect(Math.abs(found.positions[i]!)).toBeLessThanOrEqual(180); - expect(Math.abs(found.positions[i + 1]!)).toBeLessThanOrEqual(90); - } - // Every line's `from` and `to` reference a bus of the case, and every machine's bus row exists. - const lines = found.data.query({ of: 'Line', select: { from: 'from', to: 'to' } }); - const ends = lines.fields as Record; - expect(ends.from!.ids.every((id) => id !== null)).toBe(true); - expect(ends.to!.ids.every((id) => id !== null)).toBe(true); - expect(found.machineBus.every((bus) => bus < found.buses)).toBe(true); - }); + test.each(CASES)( + '%s reads into buses, lines, and machines positioned on the earth', + async (id) => { + const found = await read(id); + const rows = Object.fromEntries( + found.data.describe().tables.map((table) => [table.name, table.rows]), + ); + expect(rows).toMatchObject({ Bus: found.buses, Line: found.lines, Machine: found.machines }); + expect(found.buses).toBeGreaterThan(200); + for (let i = 0; i < found.positions.length; i += 2) { + expect(Math.abs(found.positions[i]!)).toBeLessThanOrEqual(180); + expect(Math.abs(found.positions[i + 1]!)).toBeLessThanOrEqual(90); + } + // Every line's `from` and `to` reference a bus of the case, and every machine's bus row exists. + const lines = found.data.query({ of: 'Line', select: { from: 'from', to: 'to' } }); + const ends = lines.fields as Record; + expect(ends.from!.ids.every((id) => id !== null)).toBe(true); + expect(ends.to!.ids.every((id) => id !== null)).toBe(true); + expect(found.machineBus.every((bus) => bus < found.buses)).toBe(true); + }, + ); - test('a simulated case faults at a second, and its machines swing after', () => { - const { simulation, machines } = read('wecc240'); + test('a simulated case faults at a second, and its machines swing after', async () => { + const { simulation, machines } = await read('wecc240'); expect(simulation?.fault?.on).toBeCloseTo(1, 1); const frame = Math.round(3 / simulation!.dt); const speeds = simulation!.speeds.subarray(frame * machines, (frame + 1) * machines); diff --git a/npm/examples/src/examples/networks/cases.ts b/npm/examples/src/examples/networks/cases.ts index bf4f4ac7..ece0728a 100644 --- a/npm/examples/src/examples/networks/cases.ts +++ b/npm/examples/src/examples/networks/cases.ts @@ -1,44 +1,9 @@ /** - * Synthetic grid cases. The page fetches them gzip-compressed from `public/grids`. - * - * A case file starts with the magic "LKG1" and a u32 header length, followed by a JSON header with - * the case's name and counts. Typed arrays follow on eight-byte boundaries. The header's `arrays` - * gives each as [type, offset, length], with the offset in bytes from the first eight-byte boundary - * after the header and the length in elements. The arrays hold: - * - per bus: number, name, kV, longitude and latitude, and load and generation in MW; - * - per branch: its two bus rows, reactance, and whether it is a transformer; - * - per machine: bus row, name, rating, and output. - * - * Simulated cases also hold each machine's speed deviation for every frame, `dt` seconds apart, as - * int16 in steps of `speedScale` per unit, and the fault the simulation applied. + * Synthetic grid cases, which the page fetches gzip-compressed from `public/grids`. `format.ts` + * describes the files and reads them. */ import { Data, Patch, reference } from 'latkit'; - -interface Header { - readonly case: string; - readonly buses: number; - readonly branches: number; - readonly machines: number; - readonly frames?: number; - readonly dt?: number; - readonly speedScale?: number; - readonly fault?: { readonly bus: number; readonly on: number; readonly off: number }; - readonly arrays: Readonly< - Record - >; -} - -/** A case's simulation: each machine's speed deviation over the simulated time. */ -export interface Simulation { - readonly frames: number; - readonly dt: number; - readonly duration: number; - /** Speed deviation in mHz, one value per machine for each frame, frame-major. */ - readonly speeds: Float32Array; - /** The largest deviation either way, in mHz: the int16 maximum at the file's `speedScale`. */ - readonly largest: number; - readonly fault?: { readonly bus: string; readonly on: number; readonly off: number }; -} +import { inflate, parseCase, type Simulation } from './format.js'; export interface Case { readonly data: Data; @@ -57,128 +22,81 @@ export interface Case { /** Each machine's bus row. `machinePower` is each machine's output in MW. */ readonly machineBus: Uint32Array; readonly machinePower: Float32Array; - readonly simulation?: Simulation; + /** The simulation, with the fault it applied at the bus named `bus`. */ + readonly simulation?: Simulation & { + readonly fault?: { readonly bus: string; readonly on: number; readonly off: number }; + }; } -/** The voltage level of a bus or line, an index into `LEVELS`: 0 below 100 kV, then 115–161, 230, - * 345, 500, and 765 kV. */ -export function levelOf(kv: number): number { - return kv >= 700 ? 5 : kv >= 450 ? 4 : kv >= 300 ? 3 : kv >= 200 ? 2 : kv >= 100 ? 1 : 0; +/** The bounding box of interleaved longitudes and latitudes: west, south, east, and north. */ +export function extent(positions: Float32Array): [number, number, number, number] { + const box: [number, number, number, number] = [Infinity, Infinity, -Infinity, -Infinity]; + for (let i = 0; i < positions.length; i += 2) { + box[0] = Math.min(box[0], positions[i]!); + box[1] = Math.min(box[1], positions[i + 1]!); + box[2] = Math.max(box[2], positions[i]!); + box[3] = Math.max(box[3], positions[i + 1]!); + } + return box; } -export const LEVELS = ['< 100 kV', '115–161 kV', '230 kV', '345 kV', '500 kV', '765 kV'] as const; - -const MAGIC = 0x31474b4c; -const text = new TextDecoder(); -/** Reads a case file into a dataset named `grid` with tables `Bus`, `Line`, `Machine`, and, for a - * simulated fault, `Fault`. Throws if the bytes are not a case. */ +/** Reads an inflated case file into a dataset named `grid` with tables `Bus`, `Line`, `Machine`, + * and, for a simulated fault, `Fault`. Throws if the bytes are not a case. */ export function readCase(id: string, buffer: ArrayBuffer, downloaded = buffer.byteLength): Case { - const view = new DataView(buffer); - if (buffer.byteLength < 8 || view.getUint32(0, true) !== MAGIC) - throw new Error(`${id} is not a grid case`); - const length = view.getUint32(4, true); - const header = JSON.parse(text.decode(new Uint8Array(buffer, 8, length))) as Header; - const body = Math.ceil((8 + length) / 8) * 8; - const array = (name: string, of: new (b: ArrayBuffer, o: number, l: number) => T): T => { - const found = header.arrays[name]; - if (!found) throw new Error(`${id} has no ${name}`); - return new of(buffer, body + found[1], found[2]); - }; - const names = (name: string): string[] => { - const offsets = array(`${name}.offsets`, Uint32Array); - const bytes = array(`${name}.bytes`, Uint8Array); - return Array.from({ length: offsets.length - 1 }, (_, i) => - text.decode(bytes.subarray(offsets[i]!, offsets[i + 1]!)), - ); - }; - const n = header.buses; - const m = header.branches; - const g = header.machines; - const position = array('bus.position', Float32Array); - const kv = array('bus.kv', Float32Array); - const number = array('bus.number', Uint32Array); - const ends = array('branch.ends', Uint32Array); - const machineBus = array('machine.bus', Uint32Array); - const load = array('bus.load', Float32Array); - const generation = array('bus.generation', Float32Array); - const machinePower = array('machine.power', Float32Array); - const busNames = names('bus.name'); - const lon = new Float64Array(n); - const lat = new Float64Array(n); - for (let b = 0; b < n; b++) { - lon[b] = position[b * 2]!; - lat[b] = position[b * 2 + 1]!; - } - const from = new Uint32Array(m); - const to = new Uint32Array(m); - const lineKv = new Float32Array(m); - const transformer = Array.from(array('branch.transformer', Uint8Array), (v) => v !== 0); - for (let e = 0; e < m; e++) { - const a = ends[e * 2]!; - const b = ends[e * 2 + 1]!; - from[e] = number[a]!; - to[e] = number[b]!; - lineKv[e] = Math.max(kv[a]!, kv[b]!); - } - const machineLon = new Float64Array(g); - const machineLat = new Float64Array(g); - for (let k = 0; k < g; k++) { - machineLon[k] = lon[machineBus[k]!]!; - machineLat[k] = lat[machineBus[k]!]!; - } - const simulated = header.frames !== undefined && header.dt !== undefined; - const fault = simulated ? header.fault : undefined; + const c = parseCase(id, buffer); + const { buses, branches: lines, machines } = c.header; + const number = c.array('bus.number', Uint32Array); + const load = c.array('bus.load', Float32Array); + const generation = c.array('bus.generation', Float32Array); + const machinePower = c.array('machine.power', Float32Array); + const busNames = c.names('bus.name'); + const fault = c.simulation && c.header.fault; + // Buses are keyed by number, so lines reference them by number. + const numbered = (rows: Uint32Array) => Uint32Array.from(rows, (b) => number[b]!); const patch = new Patch() .set('Bus', number, { - lon, - lat, + lon: c.lon, + lat: c.lat, name: busNames, number: Float64Array.from(number), - kv, - level: Float32Array.from(kv, levelOf), + kv: c.kv, + level: c.level, load, generation, }) .set( 'Line', - { count: m }, + { count: lines }, { - from: reference('Bus', from), - to: reference('Bus', to), - kv: lineKv, - level: Float32Array.from(lineKv, levelOf), - reactance: array('branch.reactance', Float32Array), - transformer, + from: reference('Bus', numbered(c.from)), + to: reference('Bus', numbered(c.to)), + kv: c.lineKv, + level: c.lineLevel, + reactance: c.array('branch.reactance', Float32Array), + transformer: Array.from(c.array('branch.transformer', Uint8Array), (v) => v !== 0), }, ) .set( 'Machine', - { count: g }, + { count: machines }, { - lon: machineLon, - lat: machineLat, - name: names('machine.name'), + lon: Float64Array.from(c.machineBus, (b) => c.lon[b]!), + lat: Float64Array.from(c.machineBus, (b) => c.lat[b]!), + name: c.names('machine.name'), power: machinePower, - mva: array('machine.mva', Float32Array), + mva: c.array('machine.mva', Float32Array), }, ); - if (fault) patch.set('Fault', { count: 1 }, { lon: [lon[fault.bus]!], lat: [lat[fault.bus]!] }); - let simulation: Simulation | undefined; - if (simulated) { - const frames = header.frames!; - const dt = header.dt!; - // Converts int16 steps to mHz: a step is `speedScale` per unit, and 1 per unit is 60 Hz. - const scale = (header.speedScale ?? 1) * 60_000; - const speeds = Float32Array.from(array('machine.speed', Int16Array), (value) => value * scale); + if (fault) + patch.set('Fault', { count: 1 }, { lon: [c.lon[fault.bus]!], lat: [c.lat[fault.bus]!] }); + let simulation: Case['simulation']; + if (c.simulation) { + const { frames, dt, speeds } = c.simulation; patch.axis('time', 's'); for (let f = 0; f < frames; f++) - patch.append('Machine', f * dt, { speed: speeds.subarray(f * g, (f + 1) * g) }); + patch.append('Machine', f * dt, { speed: speeds.subarray(f * machines, (f + 1) * machines) }); simulation = { - frames, - dt, - duration: (frames - 1) * dt, - speeds, - largest: 32767 * scale, + ...c.simulation, ...(fault ? { fault: { bus: busNames[fault.bus] ?? `bus ${fault.bus}`, on: fault.on, off: fault.off }, @@ -188,16 +106,16 @@ export function readCase(id: string, buffer: ArrayBuffer, downloaded = buffer.by } return { data: new Data('grid').apply(patch), - name: header.case, - buses: n, - lines: m, - machines: g, + name: c.header.case, + buses, + lines, + machines, bytes: downloaded, named: true, - positions: position, + positions: c.positions, load, generation, - machineBus, + machineBus: c.machineBus, machinePower, ...(simulation ? { simulation } : {}), }; @@ -211,15 +129,15 @@ export const megabytes = (received: number, total: number | undefined) => `${(received / 1e6).toFixed(1)}${total ? ` of ${(total / 1e6).toFixed(1)}` : ''} MB`; /** - * Downloads a file. Returns its bytes, inflated if they arrive gzip-compressed, and the number of - * bytes received before inflating. `progress` is called for each chunk. It gets no total once the - * bytes received exceed the response's length, as they do when the transfer is compressed. + * Downloads a file and returns its bytes. `progress` is called for each chunk. It gets no total + * once the bytes received exceed the response's length, as they do when the transfer is + * compressed. */ export async function download( url: string, signal: AbortSignal, progress?: Progress, -): Promise<{ readonly bytes: Uint8Array; readonly received: number }> { +): Promise> { const response = await fetch(url, { signal: AbortSignal.any([signal, AbortSignal.timeout(180_000)]), }); @@ -235,12 +153,7 @@ export async function download( } const bytes = new Uint8Array(received); chunks.reduce((at, chunk) => (bytes.set(chunk, at), at + chunk.length), 0); - if (bytes[0] !== 0x1f || bytes[1] !== 0x8b) return { bytes, received: bytes.length }; - const inflated = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip')); - return { - bytes: new Uint8Array(await new Response(inflated).arrayBuffer()), - received: bytes.length, - }; + return bytes; } export async function loadCase( @@ -248,7 +161,6 @@ export async function loadCase( signal: AbortSignal, progress?: Progress, ): Promise { - const url = `${import.meta.env.BASE_URL}grids/${id}.bin`; - const { bytes, received } = await download(url, signal, progress); - return readCase(id, bytes.buffer as ArrayBuffer, received); + const bytes = await download(`${import.meta.env.BASE_URL}grids/${id}.bin`, signal, progress); + return readCase(id, await inflate(bytes), bytes.length); } diff --git a/npm/examples/src/examples/networks/continent.ts b/npm/examples/src/examples/networks/continent.ts index a9940f73..41a50f39 100644 --- a/npm/examples/src/examples/networks/continent.ts +++ b/npm/examples/src/examples/networks/continent.ts @@ -8,7 +8,8 @@ import { random } from '../../random.js'; * same grid, in time linear in its size. */ import { Data, Patch, reference } from 'latkit'; -import { levelOf, type Case } from './cases.js'; +import type { Case } from './cases.js'; +import { levelOf } from './format.js'; /** Metro areas: name, longitude, latitude, and millions of people. */ const CITIES: readonly (readonly [string, number, number, number])[] = [ diff --git a/npm/examples/src/examples/networks/format.ts b/npm/examples/src/examples/networks/format.ts new file mode 100644 index 00000000..08cb5c7e --- /dev/null +++ b/npm/examples/src/examples/networks/format.ts @@ -0,0 +1,153 @@ +/** + * The synthetic grid cases in `public/grids`, which the networks page and the docs gallery read. + * + * A case file is gzip-compressed. Inflated, it starts with the magic "LKG1" and a u32 header length, + * followed by a JSON header with the case's name and counts. Typed arrays follow on eight-byte + * boundaries. The header's `arrays` gives each as [type, offset, length], with the offset in bytes + * from the first eight-byte boundary after the header and the length in elements. The arrays hold: + * - per bus: number, name, kV, longitude and latitude, and load and generation in MW; + * - per branch: its two bus rows, reactance, and whether it is a transformer; + * - per machine: bus row, name, rating, and output. + * + * Simulated cases also hold each machine's speed deviation for every frame, `dt` seconds apart, as + * int16 in steps of `speedScale` per unit, and the fault the simulation applied. + */ + +export interface Header { + readonly case: string; + readonly buses: number; + readonly branches: number; + readonly machines: number; + readonly frames?: number; + readonly dt?: number; + readonly speedScale?: number; + readonly fault?: { readonly bus: number; readonly on: number; readonly off: number }; + readonly arrays: Readonly< + Record + >; +} + +/** A case's simulation: each machine's speed deviation over the simulated time. */ +export interface Simulation { + readonly frames: number; + readonly dt: number; + readonly duration: number; + /** Speed deviation in mHz, one value per machine for each frame, frame-major. */ + readonly speeds: Float32Array; + /** The largest deviation either way, in mHz: the int16 maximum at the file's `speedScale`. */ + readonly largest: number; +} + +/** A case file's header and arrays, and the columns its readers derive from them. */ +export interface CaseFile { + readonly header: Header; + /** Returns one of the case's arrays by name, such as `branch.reactance`. Throws if it is + * missing. */ + array(name: string, of: new (b: ArrayBuffer, o: number, l: number) => T): T; + /** Returns the strings of one of the case's text arrays, such as `bus.name`. */ + names(name: string): string[]; + /** Each bus's longitude and latitude, interleaved in `positions` and apart in `lon` and `lat`. */ + readonly positions: Float32Array; + readonly lon: Float64Array; + readonly lat: Float64Array; + /** Each bus's kV and voltage level. */ + readonly kv: Float32Array; + readonly level: Float32Array; + /** Each branch's two bus rows, its kV (the higher of its buses'), and its voltage level. */ + readonly from: Uint32Array; + readonly to: Uint32Array; + readonly lineKv: Float32Array; + readonly lineLevel: Float32Array; + /** Each machine's bus row. */ + readonly machineBus: Uint32Array; + readonly simulation?: Simulation; +} + +/** The voltage level of a bus or line, an index into `LEVELS`: 0 below 100 kV, then 115–161, 230, + * 345, 500, and 765 kV. */ +export function levelOf(kv: number): number { + return kv >= 700 ? 5 : kv >= 450 ? 4 : kv >= 300 ? 3 : kv >= 200 ? 2 : kv >= 100 ? 1 : 0; +} +export const LEVELS = ['< 100 kV', '115–161 kV', '230 kV', '345 kV', '500 kV', '765 kV'] as const; + +/** Returns `bytes` as an `ArrayBuffer`, inflated if they are gzip-compressed, as case files are. */ +export function inflate(bytes: Uint8Array): Promise { + if (bytes[0] !== 0x1f || bytes[1] !== 0x8b) + return Promise.resolve( + bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), + ); + const inflated = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip')); + return new Response(inflated).arrayBuffer(); +} + +const MAGIC = 0x31474b4c; +const text = new TextDecoder(); + +/** Reads an inflated case file. Throws if the bytes are not a case, naming it `id`. */ +export function parseCase(id: string, buffer: ArrayBuffer): CaseFile { + const view = new DataView(buffer); + if (buffer.byteLength < 8 || view.getUint32(0, true) !== MAGIC) + throw new Error(`${id} is not a grid case`); + const length = view.getUint32(4, true); + const header = JSON.parse(text.decode(new Uint8Array(buffer, 8, length))) as Header; + const body = Math.ceil((8 + length) / 8) * 8; + const array = (name: string, of: new (b: ArrayBuffer, o: number, l: number) => T): T => { + const found = header.arrays[name]; + if (!found) throw new Error(`${id} has no ${name}`); + return new of(buffer, body + found[1], found[2]); + }; + const names = (name: string): string[] => { + const offsets = array(`${name}.offsets`, Uint32Array); + const bytes = array(`${name}.bytes`, Uint8Array); + return Array.from({ length: offsets.length - 1 }, (_, i) => + text.decode(bytes.subarray(offsets[i]!, offsets[i + 1]!)), + ); + }; + const n = header.buses; + const m = header.branches; + const positions = array('bus.position', Float32Array); + const kv = array('bus.kv', Float32Array); + const ends = array('branch.ends', Uint32Array); + const lon = new Float64Array(n); + const lat = new Float64Array(n); + for (let b = 0; b < n; b++) { + lon[b] = positions[b * 2]!; + lat[b] = positions[b * 2 + 1]!; + } + const from = new Uint32Array(m); + const to = new Uint32Array(m); + const lineKv = new Float32Array(m); + for (let e = 0; e < m; e++) { + from[e] = ends[e * 2]!; + to[e] = ends[e * 2 + 1]!; + lineKv[e] = Math.max(kv[from[e]!]!, kv[to[e]!]!); + } + let simulation: Simulation | undefined; + if (header.frames !== undefined && header.dt !== undefined) { + // Converts int16 steps to mHz: a step is `speedScale` per unit, and 1 per unit is 60 Hz. + const scale = (header.speedScale ?? 1) * 60_000; + simulation = { + frames: header.frames, + dt: header.dt, + duration: (header.frames - 1) * header.dt, + speeds: Float32Array.from(array('machine.speed', Int16Array), (value) => value * scale), + largest: 32767 * scale, + }; + } + return { + header, + array, + names, + positions, + lon, + lat, + kv, + level: Float32Array.from(kv, levelOf), + from, + to, + lineKv, + lineLevel: Float32Array.from(lineKv, levelOf), + machineBus: array('machine.bus', Uint32Array), + ...(simulation ? { simulation } : {}), + }; +} diff --git a/npm/examples/src/examples/networks/index.ts b/npm/examples/src/examples/networks/index.ts index 2cba3a68..57a9c7cb 100644 --- a/npm/examples/src/examples/networks/index.ts +++ b/npm/examples/src/examples/networks/index.ts @@ -37,8 +37,9 @@ import { h } from '../../ui/dom.js'; import { each } from '../../ui/rows.js'; import type { Stage } from '../../ui/stage.js'; import { theme } from '../../ui/theme.js'; -import { LEVELS, loadCase, megabytes, type Case } from './cases.js'; +import { extent, loadCase, megabytes, type Case } from './cases.js'; import { continent } from './continent.js'; +import { LEVELS } from './format.js'; import { lattice } from './lattice.js'; import { LOOK, MAPS, type Loaded, type MapChoice } from './maps.js'; import { STATE_FIELDS, joinStates, type Joined, type StateField } from './states.js'; @@ -112,19 +113,6 @@ interface Grid { readonly zoned: boolean; } -/** The bounding box of a case's buses. */ -function extent(positions: Float32Array): Fit { - const min: [number, number, number] = [Infinity, Infinity, 0]; - const max: [number, number, number] = [-Infinity, -Infinity, 0]; - for (let i = 0; i < positions.length; i += 2) { - min[0] = Math.min(min[0], positions[i]!); - min[1] = Math.min(min[1], positions[i + 1]!); - max[0] = Math.max(max[0], positions[i]!); - max[1] = Math.max(max[1], positions[i + 1]!); - } - return { bounds: { min, max } }; -} - /** Groups a case's buses into zones by the `ZONE`-degree square each lies in. The patch adds a * `Zone` row per square and a `zone` reference from each bus to its zone. An edges mark draws each * zone as a star joining its buses. */ @@ -957,6 +945,7 @@ export default async function page(stage: Stage): Promise { const zoned = found.named; if (zoned) found.data.apply(zones(found)); const peak = (values: Float32Array) => values.reduce((top, v) => Math.max(top, v), 0); + const [west, south, east, north] = extent(found.positions); next = { id, label, @@ -964,7 +953,7 @@ export default async function page(stage: Stage): Promise { buses: found.buses, lines: found.lines, case: found, - bounds: extent(found.positions), + bounds: { bounds: { min: [west, south, 0], max: [east, north, 0] } }, peaks: { load: peak(found.load), generation: peak(found.generation), diff --git a/npm/examples/src/examples/networks/maps.ts b/npm/examples/src/examples/networks/maps.ts index 4c4aeb22..eadfb274 100644 --- a/npm/examples/src/examples/networks/maps.ts +++ b/npm/examples/src/examples/networks/maps.ts @@ -5,7 +5,7 @@ * Natural Earth's repository; * - OpenStreetMap's planet as OpenFreeMap's vector tiles. * - * GeoJSON downloads as bytes and is read in a worker. Tiles are fetched as the camera needs them. + * GeoJSON, plain or gzip-compressed, is read in a worker. Tiles are fetched as the camera needs them. */ import { Data, Tiles, field, type ColormapName, type Mark, type Source } from 'latkit'; import { download, megabytes } from './cases.js'; @@ -63,13 +63,9 @@ const borders: MapChoice = { const started = performance.now(); const files = ['coastline', 'countries', 'provinces'] as const; const sets = await Promise.all( - files.map(async (name) => { - const { bytes } = await download( - `${import.meta.env.BASE_URL}maps/${name}.geojson.gz`, - signal, - ); - return Data.read(bytes, { name, signal }); - }), + files.map((name) => + Data.read(`${import.meta.env.BASE_URL}maps/${name}.geojson.gz`, { name, signal }), + ), ); return { sources: sets, @@ -93,7 +89,7 @@ function states(scale: '50m' | '10m', label: string): MapChoice { attribution: NATURAL_EARTH, async load(signal, progress) { const started = performance.now(); - const { bytes, received } = await download( + const bytes = await download( `${NE}ne_${scale}_admin_1_states_provinces.geojson`, signal, (got, total) => progress(`downloading ${megabytes(got, total)}`), @@ -103,7 +99,7 @@ function states(scale: '50m' | '10m', label: string): MapChoice { return { sources: [data], states: data, - summary: `${(received / 1e6).toFixed(1)} MB in ${seconds(started)}, read in ${seconds(fetched)}`, + summary: `${(bytes.length / 1e6).toFixed(1)} MB in ${seconds(started)}, read in ${seconds(fetched)}`, marks: ({ labels, states }) => [ { // Each state carries the grid's statistics, which hovering it shows in the inspector. diff --git a/npm/examples/src/examples/networks/states.ts b/npm/examples/src/examples/networks/states.ts index a34cb1c8..4a245fad 100644 --- a/npm/examples/src/examples/networks/states.ts +++ b/npm/examples/src/examples/networks/states.ts @@ -4,7 +4,7 @@ * simulated grid, each state also gets its machines' largest speed swing, frame by frame. */ import { Patch, type Data, type Id, type Snapshot } from 'latkit'; -import type { Case } from './cases.js'; +import { extent, type Case } from './cases.js'; /** The per-state fields, with their labels and units. */ export const STATE_FIELDS = { @@ -52,13 +52,7 @@ function inside(state: State, x: number, y: number): boolean { * snapshot of the states from before, for restoring them. */ export function joinStates(states: Data, grid: Case): Joined { const { positions, simulation } = grid; - const extent = [Infinity, Infinity, -Infinity, -Infinity]; - for (let b = 0; b < grid.buses; b++) { - extent[0] = Math.min(extent[0]!, positions[b * 2]!); - extent[1] = Math.min(extent[1]!, positions[b * 2 + 1]!); - extent[2] = Math.max(extent[2]!, positions[b * 2]!); - extent[3] = Math.max(extent[3]!, positions[b * 2 + 1]!); - } + const box = extent(positions); const ids: Id[] = states.query({ of: 'polygons', select: {} }).ids; const found: State[] = []; ids.forEach((id, row) => { @@ -73,10 +67,7 @@ export function joinStates(states: Data, grid: Case): Joined { bounds[3] = Math.max(bounds[3], y); } const meets = - bounds[0] <= extent[2]! && - bounds[2] >= extent[0]! && - bounds[1] <= extent[3]! && - bounds[3] >= extent[1]!; + bounds[0] <= box[2] && bounds[2] >= box[0] && bounds[1] <= box[3] && bounds[3] >= box[1]; if (meets) found.push({ row, bounds, rings }); }); // Index the states by one-degree cell, so each bus tests only the states near it. @@ -84,8 +75,8 @@ export function joinStates(states: Data, grid: Case): Joined { const cell = (x: number, y: number) => Math.floor(x + 180) * 180 + Math.floor(y + 90); for (const state of found) { const [w, s, e, n] = state.bounds; - for (let x = Math.floor(Math.max(w, extent[0]!)); x <= Math.min(e, extent[2]!); x++) - for (let y = Math.floor(Math.max(s, extent[1]!)); y <= Math.min(n, extent[3]!); y++) { + for (let x = Math.floor(Math.max(w, box[0])); x <= Math.min(e, box[2]); x++) + for (let y = Math.floor(Math.max(s, box[1])); y <= Math.min(n, box[3]); y++) { const key = cell(x, y); let list = cells.get(key); if (!list) cells.set(key, (list = [])); diff --git a/npm/examples/src/examples/power-flow/grid.ts b/npm/examples/src/examples/power-flow/grid.ts index da2d0158..5293ad38 100644 --- a/npm/examples/src/examples/power-flow/grid.ts +++ b/npm/examples/src/examples/power-flow/grid.ts @@ -2,6 +2,7 @@ * A model of the grid a control room watches. Plants dispatch to meet demand, a DC power flow sets * every branch's flow, protection trips overloaded branches, and each island balances on its own. */ +import { BASE, angles, flows, islands, type Network } from '../../../../../docs/gallery/flow.js'; import texas from './texas.json'; /** The kinds of substation, in color index order. `fuels` lists the categories of each kind. */ @@ -46,7 +47,6 @@ export class Grid { readonly load = Uint8Array.from(texas.substations, (s) => (s.category === 'Load' ? 1 : 0)); readonly kind = Float32Array.from(texas.substations, kindOf); readonly rating = Float64Array.from(texas.branches, (b) => b.pmax); - readonly #x: Float64Array; /** Each plant's output as a share of its capacity, or each load's share of demand served. * `injection` holds the MW each substation injects. */ readonly fill = new Float32Array(this.n); @@ -62,6 +62,8 @@ export class Grid { readonly #reclose = new Float64Array(this.m); /** For each plant, the time until which it is out of service. */ readonly #plantOut = new Float64Array(this.n); + /** The branches as the power flow sees them, with the out of service ones in `out`. */ + readonly #net: Network; /** Demand as a share of the loads' size, and the simulated time in seconds. */ demand = 0.78; t = 0; @@ -71,12 +73,18 @@ export class Grid { constructor() { const s = texas.substations; - this.#x = Float64Array.from(texas.branches, (b) => { - const [a, c] = [s[b.from]!, s[b.to]!]; - const km = - Math.hypot((a.lon - c.lon) * Math.cos((a.lat * Math.PI) / 180), a.lat - c.lat) * 111; - return Math.max(0.002, REACTANCE * km); - }); + this.#net = { + buses: this.n, + from: Uint32Array.from(texas.branches, (b) => b.from), + to: Uint32Array.from(texas.branches, (b) => b.to), + reactance: Float64Array.from(texas.branches, (b) => { + const [a, c] = [s[b.from]!, s[b.to]!]; + const km = + Math.hypot((a.lon - c.lon) * Math.cos((a.lat * Math.PI) / 180), a.lat - c.lat) * 111; + return Math.max(0.002, REACTANCE * km); + }), + out: this.out, + }; this.step(0); } @@ -132,9 +140,15 @@ export class Grid { } // Split the grid into islands joined by branches in service, and balance each on its own // plants. - const island = this.#islands(); + const island = islands(this.#net); + const groups = new Map(); + for (let i = 0; i < n; i++) { + const members = groups.get(island[i]!); + if (members) members.push(i); + else groups.set(island[i]!, [i]); + } this.dark = 0; - for (const members of island) { + for (const members of groups.values()) { const asked = members.reduce((sum, i) => sum + want[i]!, 0); const able = members.reduce((sum, i) => sum + can[i]!, 0); const served = able > 0 ? Math.min(1, able / Math.max(asked, 1e-9)) : 0; @@ -149,8 +163,10 @@ export class Grid { this.injection[i] = can[i]! * dispatch; } } - this.#solve(members); } + // The flows follow from the bus angles, which the injections in per unit set. + const perUnit = Float64Array.from(this.injection, (mw) => mw / BASE); + this.flow.set(flows(this.#net, angles(this.#net, perUnit))); // Protection trips a branch that stays over its trip setting. Now and then a plant trips too. for (let j = 0; j < m; j++) { this.loading[j] = this.out[j] ? 0 : Math.abs(this.flow[j]!) / this.rating[j]!; @@ -168,77 +184,4 @@ export class Grid { tripped(i: number): boolean { return !this.load[i] && this.t < this.#plantOut[i]!; } - - /** Groups the substations into islands joined by branches in service. */ - #islands(): number[][] { - const seen = new Uint8Array(this.n); - const near: number[][] = Array.from({ length: this.n }, () => []); - texas.branches.forEach((b, j) => { - if (!this.out[j]) { - near[b.from]!.push(b.to); - near[b.to]!.push(b.from); - } - }); - const islands: number[][] = []; - for (let start = 0; start < this.n; start++) { - if (seen[start]) continue; - const members = [start]; - seen[start] = 1; - for (let k = 0; k < members.length; k++) - for (const next of near[members[k]!]!) { - if (seen[next]) continue; - seen[next] = 1; - members.push(next); - } - islands.push(members); - } - return islands; - } - - /** Solves the DC power flow of one island. Bus angles come from the injections (per unit on a - * 100 MVA base), with the first member as the reference bus. Branch flows follow from the - * angles. */ - #solve(members: number[]): void { - const size = members.length - 1; - const at = new Map(members.slice(1).map((bus, k) => [bus, k])); - const b = Array.from({ length: size }, () => new Float64Array(size + 1)); - texas.branches.forEach((branch, j) => { - if (this.out[j] || !(at.has(branch.from) || branch.from === members[0])) return; - if (!(at.has(branch.to) || branch.to === members[0])) return; - const y = 1 / this.#x[j]!; - const [p, q] = [at.get(branch.from), at.get(branch.to)]; - if (p !== undefined) b[p]![p]! += y; - if (q !== undefined) b[q]![q]! += y; - if (p !== undefined && q !== undefined) { - b[p]![q]! -= y; - b[q]![p]! -= y; - } - }); - members.slice(1).forEach((bus, k) => (b[k]![size] = this.injection[bus]! / 100)); - // Gaussian elimination with partial pivoting, which is fast enough for an island this small. - for (let c = 0; c < size; c++) { - let pivot = c; - for (let r = c + 1; r < size; r++) - if (Math.abs(b[r]![c]!) > Math.abs(b[pivot]![c]!)) pivot = r; - [b[c], b[pivot]] = [b[pivot]!, b[c]!]; - const d = b[c]![c]! || 1e-12; - for (let r = c + 1; r < size; r++) { - const f = b[r]![c]! / d; - if (f) for (let k = c; k <= size; k++) b[r]![k]! -= f * b[c]![k]!; - } - } - const theta = new Float64Array(size); - for (let r = size - 1; r >= 0; r--) { - let sum = b[r]![size]!; - for (let k = r + 1; k < size; k++) sum -= b[r]![k]! * theta[k]!; - theta[r] = sum / (b[r]![r]! || 1e-12); - } - const angle = (bus: number) => (bus === members[0] ? 0 : theta[at.get(bus)!]!); - texas.branches.forEach((branch, j) => { - if (at.has(branch.from) || branch.from === members[0]) - this.flow[j] = this.out[j] - ? 0 - : ((angle(branch.from) - angle(branch.to)) * 100) / this.#x[j]!; - }); - } } diff --git a/npm/examples/src/examples/soft-body/burst.ts b/npm/examples/src/examples/soft-body/burst.ts index df5ea674..7f046031 100644 --- a/npm/examples/src/examples/soft-body/burst.ts +++ b/npm/examples/src/examples/soft-body/burst.ts @@ -1,3 +1,4 @@ +import { congruential } from '../../random.js'; import type { Frame } from './body.js'; import { fracture, type Piece, type Vec3 } from './fracture.js'; import type { Mesh } from './mesh.js'; @@ -96,11 +97,7 @@ export class Burst { this.targetBody[i + 2] = this.mesh.positions[i + 2]! * scale + 0.025; } // Seeded random variation, sampled only at launch. Replay records the resulting positions. - let seed = 0x12b9; - const random = () => { - seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0; - return seed / 4294967296; - }; + const random = congruential(0x12b9); for (const chunk of this.chunks) { chunk.position = [0, 0, 0]; chunk.target = [0, 0, 0]; diff --git a/npm/examples/src/random.ts b/npm/examples/src/random.ts index d25b2d27..92d1bb3a 100644 --- a/npm/examples/src/random.ts +++ b/npm/examples/src/random.ts @@ -8,3 +8,10 @@ export function random(seed: number): () => number { return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } + +/** Returns a seeded linear congruential generator of numbers in [0, 1): a simpler sequence than + * `random`'s, which the soft body's burst and the benchmark's data are drawn from. */ +export function congruential(seed: number): () => number { + let s = seed >>> 0 || 1; + return () => (s = (Math.imul(s, 1664525) + 1013904223) >>> 0) / 2 ** 32; +} diff --git a/npm/examples/tsconfig.json b/npm/examples/tsconfig.json index 11d13109..0d60eb49 100644 --- a/npm/examples/tsconfig.json +++ b/npm/examples/tsconfig.json @@ -5,6 +5,8 @@ "moduleResolution": "bundler", "strict": true, "noUncheckedIndexedAccess": true, + "allowImportingTsExtensions": true, + "noEmit": true, "skipLibCheck": true, "types": ["vite/client", "node"], "lib": ["ES2022", "DOM", "DOM.Iterable", "ESNext.Disposable"] diff --git a/npm/schema.json b/npm/schema.json index d3096db9..95182e66 100644 --- a/npm/schema.json +++ b/npm/schema.json @@ -432,195 +432,119 @@ "description": "One table a scene draws. In JSON: `{ vertices | edges | faces | lines | areas: options }`." }, "Vertices": { - "type": "object", - "additionalProperties": false, - "properties": { - "of": { - "type": "array", - "prefixItems": [ - { - "type": "string" - }, - { - "type": "string" - } - ], - "minItems": 2, - "maxItems": 2, - "description": "The dataset and table drawn.", - "tsType": "[data: string, table: string]" - }, - "position": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/$defs/Expr" - }, - "minItems": 2, - "maxItems": 2, - "description": "Each row's x and y. Without a position, the point field that `geometry` names places rows,\nand the scene's layout places rows that have neither." - }, - "geometry": { - "type": [ - "string", - "null" - ], - "description": "The point field that places rows when `position` is not set; the field named `geometry` by\ndefault." - }, - "elevation": { - "$ref": "#/$defs/Expr", - "description": "Height above the ground; on the earth, a fraction of the earth's radius. Default 0. Not\nallowed in a diagram, which is flat." + "allOf": [ + { + "$ref": "#/$defs/MarkOptions" }, - "radius": { - "anyOf": [ - { - "$ref": "#/$defs/Size" + { + "type": "object", + "description": "The mark's JSON form: `of`, its own options, then the stroke options and the options\nevery mark has.", + "properties": { + "position": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/Expr" + }, + "minItems": 2, + "maxItems": 2, + "description": "Each row's x and y. Without a position, the point field that `geometry` names places rows,\nand the scene's layout places rows that have neither." }, - { - "type": "null" - } - ], - "description": "Each row's radius, in the space's unit (units in a diagram, CSS pixels elsewhere) unless\ngiven with `px` or `units`; a diagram refuses `px`. The theme's `vertex.radius` unless set." - }, - "size": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/$defs/Size" - }, - "minItems": 2, - "maxItems": 2, - "description": "Each row's width and height, for markers that are not round, in the space's unit unless\ngiven with `px` or `units`; a diagram refuses `px`. In a diagram, a block without a size\nfits its label and ports." - }, - "marker": { - "$ref": "#/$defs/Marker", - "description": "How each row is drawn; an ellipse by default." - }, - "ring": { - "anyOf": [ - { - "$ref": "#/$defs/Paint" + "geometry": { + "type": [ + "string", + "null" + ], + "description": "The point field that places rows when `position` is not set; the field named `geometry` by\ndefault." }, - { - "type": "null" - } - ], - "description": "A ring around each row's marker in this color; a missing or transparent color draws none." - }, - "ports": { - "type": "array", - "items": { - "type": "array", - "prefixItems": [ - { - "type": "string" + "elevation": { + "$ref": "#/$defs/Expr", + "description": "Height above the ground; on the earth, a fraction of the earth's radius. Default 0. Not\nallowed in a diagram, which is flat." + }, + "radius": { + "anyOf": [ + { + "$ref": "#/$defs/Size" + }, + { + "type": "null" + } + ], + "description": "Each row's radius, in the space's unit (units in a diagram, CSS pixels elsewhere) unless\ngiven with `px` or `units`; a diagram refuses `px`. The theme's `vertex.radius` unless set." + }, + "size": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/Size" }, - { - "$ref": "#/$defs/Port" - } - ], - "minItems": 2, - "maxItems": 2 - }, - "description": "Ports on each row, one for each reference field that names a net. An `edges` mark of the net\ntable, without `ends`, draws the nets. Diagrams only.", - "tsType": "[reference: string, port: Port][]" - }, - "contains": { - "type": [ - "array", - "null" - ], - "prefixItems": [ - { - "type": "string" + "minItems": 2, + "maxItems": 2, + "description": "Each row's width and height, for markers that are not round, in the space's unit unless\ngiven with `px` or `units`; a diagram refuses `px`. In a diagram, a block without a size\nfits its label and ports." }, - { - "type": "string" - } - ], - "minItems": 2, - "maxItems": 2, - "description": "Makes each row a group containing the rows of `table` whose `reference` field names it. A\ngroup draws as a container around its members, with its label as a title in its header.\nDiagrams only.", - "tsType": "[table: string, reference: string]" - }, - "shadow": { - "type": "boolean", - "description": "Whether each block or group casts a shadow on the sheet, styled by the theme's\n`diagram.shadow`. Default `false`. Diagrams only." - }, - "color": { - "anyOf": [ - { - "$ref": "#/$defs/Paint" + "marker": { + "$ref": "#/$defs/Marker", + "description": "How each row is drawn; an ellipse by default." }, - { - "type": "null" - } - ], - "description": "Each row's color. Unset, rows take the theme's default for the mark; faces instead\nblend their corners' colors." - }, - "visible": { - "$ref": "#/$defs/Expr", - "description": "Whether each row is drawn and can be picked. Default `true`." - }, - "priority": { - "anyOf": [ - { - "$ref": "#/$defs/Expr" + "ring": { + "anyOf": [ + { + "$ref": "#/$defs/Paint" + }, + { + "type": "null" + } + ], + "description": "A ring around each row's marker in this color; a missing or transparent color draws none." }, - { - "type": "null" - } - ], - "description": "Each row's priority, from its data alone (no view state). Higher-priority rows draw\non top, are the last dropped when a table too large to draw whole is thinned to the\ntiles in view, and are labeled first." - }, - "name": { - "type": [ - "string", - "null" - ], - "description": "Distinguishes two marks of the same table across scenes, and names the mark in hits." - }, - "pick": { - "type": [ - "boolean", - "null" - ], - "description": "Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and\nplot traces do, and geometry (areas, and lines along paths such as coasts, borders,\nand roads) does not." - }, - "select": { - "type": "boolean", - "description": "Whether clicking a row selects it. Default `true`." - }, - "drag": { - "type": "boolean", - "description": "Whether pressing and moving a row drags it, reported as\n[`Event::Drag`](crate::Event), instead of panning the camera. Default `false`." - }, - "label": { - "anyOf": [ - { - "$ref": "#/$defs/Label" + "ports": { + "type": "array", + "items": { + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/Port" + } + ], + "minItems": 2, + "maxItems": 2 + }, + "description": "Ports on each row, one for each reference field that names a net. An `edges` mark of the net\ntable, without `ends`, draws the nets. Diagrams only.", + "tsType": "[reference: string, port: Port][]" }, - { - "type": "null" + "contains": { + "type": [ + "array", + "null" + ], + "prefixItems": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "minItems": 2, + "maxItems": 2, + "description": "Makes each row a group containing the rows of `table` whose `reference` field names it. A\ngroup draws as a container around its members, with its label as a title in its header.\nDiagrams only.", + "tsType": "[table: string, reference: string]" + }, + "shadow": { + "type": "boolean", + "description": "Whether each block or group casts a shadow on the sheet, styled by the theme's\n`diagram.shadow`. Default `false`. Diagrams only." } - ], - "description": "Each row's label: a text field, or label options." - }, - "legend": { - "type": [ - "string", - "null" - ], - "description": "A legend of the mark's colors at the view's bottom right, under this title: a ticked\ncolormap across its domain, a cell for each category, or a swatch beside each name\nof a map's `categories`. `color` must map through a colormap with a fixed domain or\nnamed categories; in a conditional, `otherwise` is searched before the cases." + } } - }, - "required": [ - "of" ], + "unevaluatedProperties": false, "description": "Rows drawn as markers: points of a map, nodes of a graph, blocks of a diagram." }, "Expr": { @@ -1659,9 +1583,9 @@ ], "description": "Text for each row, placed beside, inside, or along the row's shape and following it every frame:\na text field, or options. Where labels would overlap, the higher priority shows; among equals,\nlabels already shown stay. In a diagram a label is part of the drawing: it scales with the zoom,\nfades out when too small to read, and shows wherever it fits its vertex.\n\n```\n# use latkit::{field, Label, Vertices};\nlet buses = Vertices::of(\"grid\", \"Bus\").position(\"lon\", \"lat\").label(Label::new(\"name\").priority(field(\"kv\")));\n```" }, - "Edges": { + "MarkOptions": { "type": "object", - "additionalProperties": false, + "description": "The options every mark has.", "properties": { "of": { "type": "array", @@ -1678,112 +1602,31 @@ "description": "The dataset and table drawn.", "tsType": "[data: string, table: string]" }, - "ends": { - "type": [ - "array", - "null" - ], - "prefixItems": [ + "color": { + "anyOf": [ { - "type": "string" + "$ref": "#/$defs/Paint" }, { - "type": "string" + "type": "null" } ], - "minItems": 2, - "maxItems": 2, - "description": "The reference fields naming each row's two vertices.", - "tsType": "[from: string, to: string]" + "description": "Each row's color. Unset, rows take the theme's default for the mark; faces instead\nblend their corners' colors." }, - "bends": { - "type": [ - "string", - "null" - ], - "description": "A path field whose points each row passes through between its ends." + "visible": { + "$ref": "#/$defs/Expr", + "description": "Whether each row is drawn and can be picked. Default `true`." }, - "route": { + "priority": { "anyOf": [ { - "$ref": "#/$defs/Route" + "$ref": "#/$defs/Expr" }, { "type": "null" } ], - "description": "How each row runs between its ends: geodesic on the earth and straight elsewhere unless set.\n`orthogonal` routes around blocks with rounded bends, in diagrams only." - }, - "arrows": { - "type": "boolean", - "description": "Whether each row has an arrowhead at its second end; a net has one at each input it reaches.\nDefault `false`." - }, - "poles": { - "type": "boolean", - "description": "Draws each row as a pole from its own vertex down to the ground, on a plane or the earth.\nPoles take no `ends`, `bends`, or `joins`. Default `false`." - }, - "joins": { - "type": [ - "array", - "null" - ], - "prefixItems": [ - { - "type": "string" - }, - { - "type": "string" - } - ], - "minItems": 2, - "maxItems": 2, - "description": "Draws each row as a net on a plane or the earth, joining the vertex rows of `table` whose\n`reference` field names it: a star from their center to each member.", - "tsType": "[table: string, reference: string]" - }, - "width": { - "anyOf": [ - { - "$ref": "#/$defs/Size" - }, - { - "type": "null" - } - ], - "description": "Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere)\nunless given with `px` or `units`. The theme's width unless set." - }, - "dash": { - "$ref": "#/$defs/Expr", - "description": "Whether each row is dashed, with the theme's `dash` period. Default `false`." - }, - "flow": { - "$ref": "#/$defs/Expr", - "description": "The speed of comets flowing along each row, per second, in the unit of the theme's\n`flow` lengths. Negative speeds flow backward; 0, the default, draws no comets. When\nthe speed changes, comets continue from where they are." - }, - "color": { - "anyOf": [ - { - "$ref": "#/$defs/Paint" - }, - { - "type": "null" - } - ], - "description": "Each row's color. Unset, rows take the theme's default for the mark; faces instead\nblend their corners' colors." - }, - "visible": { - "$ref": "#/$defs/Expr", - "description": "Whether each row is drawn and can be picked. Default `true`." - }, - "priority": { - "anyOf": [ - { - "$ref": "#/$defs/Expr" - }, - { - "type": "null" - } - ], - "description": "Each row's priority, from its data alone (no view state). Higher-priority rows draw\non top, are the last dropped when a table too large to draw whole is thinned to the\ntiles in view, and are labeled first." + "description": "Each row's priority, from its data alone (no view state). Higher-priority rows draw\non top, are the last dropped when a table too large to draw whole is thinned to the\ntiles in view, and are labeled first." }, "name": { "type": [ @@ -1828,7 +1671,86 @@ }, "required": [ "of" + ] + }, + "Edges": { + "allOf": [ + { + "$ref": "#/$defs/MarkOptions" + }, + { + "$ref": "#/$defs/StrokeOptions" + }, + { + "type": "object", + "description": "The mark's JSON form: `of`, its own options, then the stroke options and the options\nevery mark has.", + "properties": { + "ends": { + "type": [ + "array", + "null" + ], + "prefixItems": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "minItems": 2, + "maxItems": 2, + "description": "The reference fields naming each row's two vertices.", + "tsType": "[from: string, to: string]" + }, + "bends": { + "type": [ + "string", + "null" + ], + "description": "A path field whose points each row passes through between its ends." + }, + "route": { + "anyOf": [ + { + "$ref": "#/$defs/Route" + }, + { + "type": "null" + } + ], + "description": "How each row runs between its ends: geodesic on the earth and straight elsewhere unless set.\n`orthogonal` routes around blocks with rounded bends, in diagrams only." + }, + "arrows": { + "type": "boolean", + "description": "Whether each row has an arrowhead at its second end; a net has one at each input it reaches.\nDefault `false`." + }, + "poles": { + "type": "boolean", + "description": "Draws each row as a pole from its own vertex down to the ground, on a plane or the earth.\nPoles take no `ends`, `bends`, or `joins`. Default `false`." + }, + "joins": { + "type": [ + "array", + "null" + ], + "prefixItems": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "minItems": 2, + "maxItems": 2, + "description": "Draws each row as a net on a plane or the earth, joining the vertex rows of `table` whose\n`reference` field names it: a star from their center to each member.", + "tsType": "[table: string, reference: string]" + } + } + } ], + "unevaluatedProperties": false, "description": "Rows drawn as strokes between the vertices named by two reference fields: branches, links, wires. In a diagram, a row without ends is a net, joining every vertex port whose reference field names it." }, "Route": { @@ -1851,147 +1773,10 @@ ], "description": "How an edge runs between its ends." }, - "Faces": { - "type": "object", - "additionalProperties": false, - "properties": { - "of": { - "type": "array", - "prefixItems": [ - { - "type": "string" - }, - { - "type": "string" - } - ], - "minItems": 2, - "maxItems": 2, - "description": "The dataset and table drawn.", - "tsType": "[data: string, table: string]" - }, - "corners": { - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - }, - "minItems": 3, - "maxItems": 3, - "description": "The reference fields naming each row's three vertices; required. Without a color, a face\nblends the colors of its corners.", - "tsType": "[a: string, b: string, c: string]" - }, - "color": { - "anyOf": [ - { - "$ref": "#/$defs/Paint" - }, - { - "type": "null" - } - ], - "description": "Each row's color. Unset, rows take the theme's default for the mark; faces instead\nblend their corners' colors." - }, - "visible": { - "$ref": "#/$defs/Expr", - "description": "Whether each row is drawn and can be picked. Default `true`." - }, - "priority": { - "anyOf": [ - { - "$ref": "#/$defs/Expr" - }, - { - "type": "null" - } - ], - "description": "Each row's priority, from its data alone (no view state). Higher-priority rows draw\non top, are the last dropped when a table too large to draw whole is thinned to the\ntiles in view, and are labeled first." - }, - "name": { - "type": [ - "string", - "null" - ], - "description": "Distinguishes two marks of the same table across scenes, and names the mark in hits." - }, - "pick": { - "type": [ - "boolean", - "null" - ], - "description": "Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and\nplot traces do, and geometry (areas, and lines along paths such as coasts, borders,\nand roads) does not." - }, - "select": { - "type": "boolean", - "description": "Whether clicking a row selects it. Default `true`." - }, - "drag": { - "type": "boolean", - "description": "Whether pressing and moving a row drags it, reported as\n[`Event::Drag`](crate::Event), instead of panning the camera. Default `false`." - }, - "label": { - "anyOf": [ - { - "$ref": "#/$defs/Label" - }, - { - "type": "null" - } - ], - "description": "Each row's label: a text field, or label options." - }, - "legend": { - "type": [ - "string", - "null" - ], - "description": "A legend of the mark's colors at the view's bottom right, under this title: a ticked\ncolormap across its domain, a cell for each category, or a swatch beside each name\nof a map's `categories`. `color` must map through a colormap with a fixed domain or\nnamed categories; in a conditional, `otherwise` is searched before the cases." - } - }, - "required": [ - "of" - ], - "description": "Rows drawn as triangles between the vertices named by three reference fields: meshes, surfaces, and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen from above shows its exact colors." - }, - "Lines": { + "StrokeOptions": { "type": "object", - "additionalProperties": false, + "description": "The options of marks drawn as strokes: edges and lines.", "properties": { - "of": { - "type": "array", - "prefixItems": [ - { - "type": "string" - }, - { - "type": "string" - } - ], - "minItems": 2, - "maxItems": 2, - "description": "The dataset and table drawn.", - "tsType": "[data: string, table: string]" - }, - "geometry": { - "type": [ - "string", - "null" - ], - "description": "The path or polygon field each row runs along; the field named `geometry` by default. Not\nallowed on a plot." - }, - "trace": { - "type": [ - "string", - "null" - ], - "description": "The sampled field each row traces across the data's axis; plots only, where it is required.\nA field with no frames yet draws nothing until its first frame arrives." - }, - "interpolation": { - "$ref": "#/$defs/Interpolation", - "description": "How each row runs between its samples on a plot; `linear` by default." - }, "width": { "anyOf": [ { @@ -2010,77 +1795,71 @@ "flow": { "$ref": "#/$defs/Expr", "description": "The speed of comets flowing along each row, per second, in the unit of the theme's\n`flow` lengths. Negative speeds flow backward; 0, the default, draws no comets. When\nthe speed changes, comets continue from where they are." + } + } + }, + "Faces": { + "allOf": [ + { + "$ref": "#/$defs/MarkOptions" }, - "color": { - "anyOf": [ - { - "$ref": "#/$defs/Paint" - }, - { - "type": "null" + { + "type": "object", + "description": "The mark's JSON form: `of`, its own options, then the stroke options and the options\nevery mark has.", + "properties": { + "corners": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + }, + "minItems": 3, + "maxItems": 3, + "description": "The reference fields naming each row's three vertices; required. Without a color, a face\nblends the colors of its corners.", + "tsType": "[a: string, b: string, c: string]" } - ], - "description": "Each row's color. Unset, rows take the theme's default for the mark; faces instead\nblend their corners' colors." + } + } + ], + "unevaluatedProperties": false, + "description": "Rows drawn as triangles between the vertices named by three reference fields: meshes, surfaces, and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen from above shows its exact colors." + }, + "Lines": { + "allOf": [ + { + "$ref": "#/$defs/MarkOptions" }, - "visible": { - "$ref": "#/$defs/Expr", - "description": "Whether each row is drawn and can be picked. Default `true`." + { + "$ref": "#/$defs/StrokeOptions" }, - "priority": { - "anyOf": [ - { - "$ref": "#/$defs/Expr" + { + "type": "object", + "description": "The mark's JSON form: `of`, its own options, then the stroke options and the options\nevery mark has.", + "properties": { + "geometry": { + "type": [ + "string", + "null" + ], + "description": "The path or polygon field each row runs along; the field named `geometry` by default. Not\nallowed on a plot." }, - { - "type": "null" - } - ], - "description": "Each row's priority, from its data alone (no view state). Higher-priority rows draw\non top, are the last dropped when a table too large to draw whole is thinned to the\ntiles in view, and are labeled first." - }, - "name": { - "type": [ - "string", - "null" - ], - "description": "Distinguishes two marks of the same table across scenes, and names the mark in hits." - }, - "pick": { - "type": [ - "boolean", - "null" - ], - "description": "Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and\nplot traces do, and geometry (areas, and lines along paths such as coasts, borders,\nand roads) does not." - }, - "select": { - "type": "boolean", - "description": "Whether clicking a row selects it. Default `true`." - }, - "drag": { - "type": "boolean", - "description": "Whether pressing and moving a row drags it, reported as\n[`Event::Drag`](crate::Event), instead of panning the camera. Default `false`." - }, - "label": { - "anyOf": [ - { - "$ref": "#/$defs/Label" + "trace": { + "type": [ + "string", + "null" + ], + "description": "The sampled field each row traces across the data's axis; plots only, where it is required.\nA field with no frames yet draws nothing until its first frame arrives." }, - { - "type": "null" + "interpolation": { + "$ref": "#/$defs/Interpolation", + "description": "How each row runs between its samples on a plot; `linear` by default." } - ], - "description": "Each row's label: a text field, or label options." - }, - "legend": { - "type": [ - "string", - "null" - ], - "description": "A legend of the mark's colors at the view's bottom right, under this title: a ticked\ncolormap across its domain, a cell for each category, or a swatch beside each name\nof a map's `categories`. `color` must map through a colormap with a fixed domain or\nnamed categories; in a conditional, `otherwise` is searched before the cases." + } } - }, - "required": [ - "of" ], + "unevaluatedProperties": false, "description": "Rows drawn as lines along a path field (roads, rivers, borders) or a polygon field's rings (area outlines). On a plot, each row instead traces a sampled field's history across the data's axis, as a monitor does. On the earth, paths follow the globe's curve and stay whole across the antimeridian." }, "Interpolation": { @@ -2104,101 +1883,25 @@ "description": "How a line runs between its samples." }, "Areas": { - "type": "object", - "additionalProperties": false, - "properties": { - "of": { - "type": "array", - "prefixItems": [ - { - "type": "string" - }, - { - "type": "string" - } - ], - "minItems": 2, - "maxItems": 2, - "description": "The dataset and table drawn.", - "tsType": "[data: string, table: string]" - }, - "geometry": { - "type": [ - "string", - "null" - ], - "description": "The polygon field each row fills, holding one polygon's rings or several polygons; the field\nnamed `geometry` by default." - }, - "color": { - "anyOf": [ - { - "$ref": "#/$defs/Paint" - }, - { - "type": "null" - } - ], - "description": "Each row's color. Unset, rows take the theme's default for the mark; faces instead\nblend their corners' colors." - }, - "visible": { - "$ref": "#/$defs/Expr", - "description": "Whether each row is drawn and can be picked. Default `true`." - }, - "priority": { - "anyOf": [ - { - "$ref": "#/$defs/Expr" - }, - { - "type": "null" - } - ], - "description": "Each row's priority, from its data alone (no view state). Higher-priority rows draw\non top, are the last dropped when a table too large to draw whole is thinned to the\ntiles in view, and are labeled first." - }, - "name": { - "type": [ - "string", - "null" - ], - "description": "Distinguishes two marks of the same table across scenes, and names the mark in hits." - }, - "pick": { - "type": [ - "boolean", - "null" - ], - "description": "Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and\nplot traces do, and geometry (areas, and lines along paths such as coasts, borders,\nand roads) does not." - }, - "select": { - "type": "boolean", - "description": "Whether clicking a row selects it. Default `true`." - }, - "drag": { - "type": "boolean", - "description": "Whether pressing and moving a row drags it, reported as\n[`Event::Drag`](crate::Event), instead of panning the camera. Default `false`." + "allOf": [ + { + "$ref": "#/$defs/MarkOptions" }, - "label": { - "anyOf": [ - { - "$ref": "#/$defs/Label" - }, - { - "type": "null" + { + "type": "object", + "description": "The mark's JSON form: `of`, its own options, then the stroke options and the options\nevery mark has.", + "properties": { + "geometry": { + "type": [ + "string", + "null" + ], + "description": "The polygon field each row fills, holding one polygon's rings or several polygons; the field\nnamed `geometry` by default." } - ], - "description": "Each row's label: a text field, or label options." - }, - "legend": { - "type": [ - "string", - "null" - ], - "description": "A legend of the mark's colors at the view's bottom right, under this title: a ticked\ncolormap across its domain, a cell for each category, or a swatch beside each name\nof a map's `categories`. `color` must map through a colormap with a fixed domain or\nnamed categories; in a conditional, `otherwise` is searched before the cases." + } } - }, - "required": [ - "of" ], + "unevaluatedProperties": false, "description": "Rows drawn as the areas of a polygon field: parcels, states, lakes, the land and water of a map. The view triangulates each row's polygons once, in the background when they are large. On the earth, areas follow the globe's curve and stay whole across the antimeridian. To outline areas, add a [`Lines`](crate::Lines) mark of the same table, which draws their rings." }, "Layout": { diff --git a/npm/schema.mjs b/npm/schema.mjs index 7689aca0..74574979 100644 --- a/npm/schema.mjs +++ b/npm/schema.mjs @@ -1,9 +1,9 @@ /** * Generates npm's types from `schema.json`. The types latkit reads (`$defs`) become `inputs.ts`, * and the types it writes (`outputs`) become `outputs.ts`. Each definition reachable from the roots - * becomes a type, documented with its schema description. An output that an input already declares - * uses that input's type. A schema's `tsType` names a hand-written type to use instead, such as - * `ExprInput`. + * becomes a type, documented with its schema description; one that extends shared options becomes + * an interface extending them. An output that an input already declares uses that input's type. A + * schema's `tsType` names a hand-written type to use instead, such as `ExprInput`. */ import fs from 'node:fs'; import path from 'node:path'; @@ -90,6 +90,7 @@ function declare(defs, prefix, roots, declared) { if (name !== undefined) return defs[name]?.tsType ?? name; if ('const' in schema) return JSON.stringify(schema.const); if (schema.enum) return schema.enum.map((v) => JSON.stringify(v)).join(' | '); + if (schema.allOf) return schema.allOf.map(type).join(' & '); const union = schema.anyOf ?? schema.oneOf; if (union) return members(union); if (Array.isArray(schema.type)) { @@ -173,12 +174,24 @@ function declare(defs, prefix, roots, declared) { return `{ ${lines.join('\n')} }`; }; + // An object that extends shared options, `allOf` references and its own object, is an interface + // extending theirs. + const extending = (schema) => { + const parts = schema.allOf ?? []; + const own = parts.filter((p) => ref(p) === undefined); + return own.length === 1 && own[0].type === 'object' + ? { bases: parts.map(ref).filter((n) => n !== undefined), own: own[0] } + : undefined; + }; const names = [...reached].filter((name) => !declared.has(name) && !defs[name].tsType).sort(); const source = names .map((name) => { const schema = defs[name]; - const body = type({ ...schema, description: undefined, tsType: undefined }); const comment = schema.description ? doc(schema.description) : ''; + const extended = extending(schema); + if (extended) + return `${comment}export interface ${name} extends ${extended.bases.join(', ')} ${object(extended.own)}`; + const body = type({ ...schema, description: undefined, tsType: undefined }); return schema.type === 'object' && schema.properties ? `${comment}export interface ${name} ${body}` : `${comment}export type ${name} = ${body};`; diff --git a/npm/src/canvas.ts b/npm/src/canvas.ts index 9bfca606..8049b3e9 100644 --- a/npm/src/canvas.ts +++ b/npm/src/canvas.ts @@ -1,7 +1,7 @@ /** Canvases that show views in a `` element or an `OffscreenCanvas`. */ import type * as bindings from '../pkg/latkit.js'; -import { hold, inner, release } from './internal.js'; +import { Handle, inner } from './internal.js'; import { Shot, type ShotOptions } from './shot.js'; import { handleOf, type View } from './view.js'; @@ -16,49 +16,29 @@ export interface Panel { at?: Rect; } -/** Options for `Gpu.canvas()`. */ -export interface CanvasOptions { - /** The views shown and their regions; later panels draw over earlier ones. None by default. */ - layout?: readonly Panel[]; - /** - * Pixels per CSS pixel of an `OffscreenCanvas`, 1 by default; it must be positive. A `` - * element uses the device pixel ratio instead. - */ - pixelRatio?: number; -} - /** The rect covering a whole canvas or picture. */ const WHOLE: Rect = [0, 0, 1, 1]; /** * Shows views in a `` element or an `OffscreenCanvas`, each in its layout panel; created by * `Gpu.canvas()`. A frame redraws only the views that need it, or every view after a resize or a - * layout change. + * layout change. Destroying it stops drawing; the views stay usable, and the element keeps its last + * frame. */ -export class Canvas { +export class Canvas extends Handle { #target: HTMLCanvasElement | OffscreenCanvas; - #pixelRatio: number; - #layout: readonly Panel[] = []; + #layout: readonly Panel[]; #released: () => void; private constructor( device: bindings.Gpu, target: HTMLCanvasElement | OffscreenCanvas, - options: CanvasOptions, + options: { layout?: readonly Panel[]; pixelRatio?: number }, released: () => void, ) { const layout = options.layout ?? []; - hold( - this, - device.canvas( - target, - layout.map(({ view }) => handleOf(view)), - placed(layout), - options.pixelRatio, - ), - ); + super(device.canvas(target, ...encoded(layout), options.pixelRatio), 'Canvas'); this.#target = target; - this.#pixelRatio = options.pixelRatio ?? 1; this.#layout = [...layout]; this.#released = released; } @@ -67,7 +47,7 @@ export class Canvas { static made( device: bindings.Gpu, target: HTMLCanvasElement | OffscreenCanvas, - options: CanvasOptions, + options: { layout?: readonly Panel[]; pixelRatio?: number }, released: () => void, ): Canvas { return new Canvas(device, target, options, released); @@ -80,61 +60,43 @@ export class Canvas { /** * The views shown and their regions; later panels draw over earlier ones. Setting it replaces the - * shown views. A view can show in only one panel of one canvas at a time. Setting a layout that - * breaks this, or that has a panel covering no pixels, throws an `invalid` error. + * shown views. A view shows in at most one panel of one canvas at a time. */ get layout(): readonly Panel[] { return this.#layout; } set layout(layout: readonly Panel[]) { - canvas(this).setLayout( - layout.map(({ view }) => handleOf(view)), - placed(layout), - ); + inner(this).setLayout(...encoded(layout)); this.#layout = [...layout]; } /** - * Returns a {@link Shot} of exactly what the canvas shows: its views in its layout, at the - * canvas's CSS size times `options.scale` (1 by default) in pixels. + * A {@link Shot} of exactly what the canvas shows: its views in its layout, at the canvas's CSS + * size times `options.scale` in pixels. */ shot(options: ShotOptions = {}): Shot { const scale = options.scale ?? 1; - const target = this.#target; - const [width, height] = - 'clientWidth' in target - ? [target.clientWidth, target.clientHeight] - : [target.width / this.#pixelRatio, target.height / this.#pixelRatio]; - const size = [ - Math.max(1, Math.round(width * scale)), - Math.max(1, Math.round(height * scale)), - ] as const; - return new Shot(size, { ...options, layout: this.#layout }); + const css = inner(this).cssSize; + const [width = 1, height = 1] = Array.from(css, (side) => + Math.max(1, Math.round(side * scale)), + ); + return new Shot([width, height], { ...options, layout: this.#layout }); } - /** - * Stops drawing and frees the canvas. The views stay usable, and the element keeps its last - * frame. Calling it again does nothing. - */ - destroy(): void { - const freed = release(this); - if (!freed) return; - freed.free(); + /** @internal Tells the GPU the canvas is gone. */ + protected override freeing(): void { this.#released(); } - - /** Calls {@link Canvas.destroy}, so a `using` declaration destroys it when its block ends. */ - [Symbol.dispose](): void { - this.destroy(); - } } -function canvas(owner: Canvas): bindings.Canvas { - return inner(owner, 'Canvas'); -} - -/** @internal Flattens each panel's rect into four floats; the whole target where `at` is unset. */ -export function placed(layout: readonly Panel[]): Float32Array { - return Float32Array.from(layout.flatMap(({ at }) => at ?? WHOLE)); +/** + * @internal A layout as the bindings take it: each panel's view, and its rect's four fractions in + * one array, the whole target where `at` is unset. + */ +export function encoded(layout: readonly Panel[]): [bindings.Handle[], Float32Array] { + return [ + layout.map(({ view }) => handleOf(view)), + Float32Array.from(layout.flatMap(({ at }) => at ?? WHOLE)), + ]; } diff --git a/npm/src/colormap.ts b/npm/src/colormap.ts index b40d5ee2..2b79caae 100644 --- a/npm/src/colormap.ts +++ b/npm/src/colormap.ts @@ -1,14 +1,21 @@ -/** Colormaps as CSS gradients, for legends drawn in HTML. */ +/** The colormap catalog, and colormaps as CSS gradients for legends drawn in HTML. */ -import type { Colormap } from './inputs.js'; +import type { Colormap, ColormapKind, ColormapName } from './inputs.js'; import { wasm } from './internal.js'; /** * Returns a CSS `linear-gradient(...)` with the colors the GPU draws for `map`, a catalog name or a * custom colormap. `direction` is the gradient's CSS direction, `'to right'` by default. A - * categorical map gives one solid band per category. Needs `await load()` first, and throws an - * `invalid` error for an invalid `map`. + * categorical map gives one solid band per category. */ export function colormapCss(map: Colormap, direction = 'to right'): string { return wasm().colormapCss(JSON.stringify(map), direction); } + +/** + * The catalog's colormaps, in order: each one's name, its title for people, such as + * `'Red / Blue'`, and its kind. + */ +export function colormaps(): { name: ColormapName; label: string; kind: ColormapKind }[] { + return JSON.parse(wasm().colormaps()) as ReturnType; +} diff --git a/npm/src/data.ts b/npm/src/data.ts index 02b57f40..a18c06fd 100644 --- a/npm/src/data.ts +++ b/npm/src/data.ts @@ -2,7 +2,7 @@ import type * as bindings from '../pkg/latkit.js'; import type { EnvelopeRequest, Id, Query } from './inputs.js'; -import { bytesOf, hold, inner, release, wasm, waited, wrap, type Wait } from './internal.js'; +import { bytesOf, Handle, inner, wasm, waited, type ByteSource, type Wait } from './internal.js'; import { load } from './load.js'; import type { Description, Value } from './outputs.js'; import { framed, type Patch } from './patch.js'; @@ -27,30 +27,76 @@ export interface QueryResult { export interface Envelope { /** The span boundaries on the axis: one more than the number of spans. */ at: Float64Array; - /** - * Each row's minimum in each span: `spans` values per row, row after row. NaN where a span has - * no frame. - */ + /** Each row's minimum in each span, `spans` values per row, row after row; NaN without frames. */ least: Float32Array; /** Each row's maximum in each span, laid out like `least`. */ greatest: Float32Array; } +/** A bindings object that reads one version of a dataset: a `Data` or a `Snapshot`. */ +type Reads = bindings.Data | bindings.Snapshot; + /** - * What {@link Data.read} accepts: bytes, a `Blob`, a `Response`, a byte stream, a URL to fetch as - * a string, or a parsed JSON value. The content is latkit's bytes, GeoJSON, or a dataset document. + * Named tables of columns, read the same way from a {@link Data}, which reads its current version, + * and a {@link Snapshot}, which reads its own. */ -export type DataSource = - Uint8Array | ArrayBuffer | Blob | Response | ReadableStream | string | object; +export abstract class Dataset extends Handle { + /** The dataset's name, by which a mark's `data` option draws it. */ + get name(): string { + return inner(this).name; + } -/** A bindings object that reads one version of a dataset: a `Data` or a `Snapshot`. */ -type Reads = bindings.Data | bindings.Snapshot; + /** The tables and their row counts, each field and its kind, and the axis and its span. */ + describe(): Description { + return JSON.parse(inner(this).describe()) as Description; + } + + /** + * Reads `field` of the row with `id` in `table`: for a sampled field, its last frame at or before + * `options.at`, or its last frame; for a reference, the referenced row's id. `null` when the + * table, row, field, or value is missing. + */ + get( + table: string, + id: Id, + field: string, + options: { + /** The axis coordinate to read a sampled field at. */ + at?: number; + } = {}, + ): Value { + return JSON.parse(inner(this).get(table, id, field, options.at)) as Value; + } + + /** + * Reads a table's rows, filtered, sorted, and paged, through the same expressions marks use. + * Sampled fields are read at `query.at`. + */ + query(query: Query): QueryResult { + return inner(this).query(JSON.stringify(query)) as QueryResult; + } + + /** + * Each row's minimum and maximum of a sampled field over equal spans of an axis range, for every + * row or the rows `request.ids` lists. Each span reads the coarsest stored levels of detail that + * fit in it, so it costs a few reads however many frames it covers. Throws a `limit` error when + * rows times spans exceeds 2^26. + */ + envelope(request: EnvelopeRequest): Envelope { + return inner(this).envelope(JSON.stringify(request)) as Envelope; + } + + /** The dataset as latkit's bytes: a patch that builds it from an empty dataset. */ + toBytes(): Uint8Array { + return inner(this).toBytes(); + } +} /** - * A dataset that changes in place: named tables of columns, changed only by patches. A patch - * applies completely or not at all. Views given a `Data` draw its latest version, from the frame - * after each patch. A {@link Snapshot} keeps one version and shares its pages with the dataset, so - * keeping earlier versions, as an undo history does, costs only the pages that differ. + * A dataset that changes in place, only by patches, each applied whole or not at all. Views draw + * its latest version from the frame after each patch. A {@link Snapshot} keeps one version and + * shares its pages with the dataset, so an undo history costs only the pages that differ. + * Destroying it leaves views drawing it and its snapshots usable. * * ```ts * const grid = new Data('grid').apply(new Patch().replace('Bus', [1, 2], { kv: [230, 115] })); @@ -58,195 +104,93 @@ type Reads = bindings.Data | bindings.Snapshot; * grid.apply(new Patch().set('Bus', [2], { kv: [69] })); // drawn next frame * ``` */ -export class Data { - /** Creates an empty dataset named `name`. Throws an `unloaded` error before `load()` finishes. */ - constructor(name: string) { - hold(this, new (wasm().Data)(name)); +export class Data extends Dataset { + /** Creates an empty dataset named `name`. */ + constructor(name: string); + /** @internal Wraps the bindings dataset `bound`. */ + constructor(bound: bindings.Data); + constructor(from: string | bindings.Data) { + super(typeof from === 'string' ? new (wasm().Data)(from) : from, 'Data'); } /** - * Reads a dataset from `source`, calling `load()` first. Its first bytes decide the format: + * Reads a dataset from `source`. Its first bytes decide the format: * * - latkit's bytes, as `toBytes()` writes them. * - GeoJSON, plain or gzip-compressed, read in a worker. Points, line strings, and polygons * become rows of tables `points`, `lines`, and `polygons`, with the geometry in field - * `geometry` and each property as a field. GeoJSON requires `options.name`. + * `geometry` and each property as a field. * - A dataset document, `{"name": ..., "patch": [...]}`, whose patch is read as * {@link Patch.read} reads one. - * - * `options.name` names the dataset, replacing the name in latkit's bytes or a document. - * `options.base` is the URL that a document's file slices are relative to; it defaults to the URL - * the source was fetched from. Rejects with an `invalid` error for bytes that are not a valid - * dataset, and with an `io` error when a fetch fails or answers an error status. */ static async read( - source: DataSource, - options: { name?: string; base?: string } & Wait = {}, + source: ByteSource | object, + options: { + /** Names the dataset, replacing the name its bytes or document give; GeoJSON requires it. */ + name?: string; + /** The URL a document's file slices are relative to; by default, the one it came from. */ + base?: string; + } & Wait = {}, ): Promise { await load(); const { bytes, base } = await bytesOf(source, options); const read = wasm().Data.read(bytes, options.name, options.base ?? base); - return wrap(Data, await waited(read, options, 'reading a dataset')); - } - - /** The dataset's name, by which a mark's `data` option draws it. */ - get name(): string { - return reads(this).name; + return new Data(await waited(read, options, 'reading a dataset')); } /** - * Applies every change in `patch` (a {@link Patch} or its bytes), or none of them when one is - * invalid, and returns this dataset. Views draw the new version from their next frame. Throws an - * `invalid` error whose `path` names the failing change, such as `patch[1].Bus.kv`, or when the - * patch names another dataset. + * Applies every change in `patch`, or none when one is invalid, and returns this dataset. An + * `invalid` error's `path` names the failing change, such as `patch[1].Bus.kv`. */ apply(patch: Patch | Uint8Array): this { - inner(this, 'Data').apply(patch instanceof Uint8Array ? patch : framed(patch)); + inner(this).apply(patch instanceof Uint8Array ? patch : framed(patch)); return this; } /** The current version as a {@link Snapshot}, which later patches leave unchanged. O(1). */ snapshot(): Snapshot { - return wrap(Snapshot, inner(this, 'Data').snapshot()); + return Snapshot.made(inner(this).snapshot()); } - /** - * Makes `snapshot`, a version of this dataset, the current version, as an undo does. Views draw - * it from their next frame. Throws an `invalid` error for a snapshot of a dataset with another - * name. - */ + /** Makes `snapshot`, a version of this dataset, the current version, as an undo does. */ restore(snapshot: Snapshot): this { - inner(this, 'Data').restore(inner(snapshot, 'Snapshot')); + inner(this).restore(inner(snapshot)); return this; } - - /** Describes the current version; see {@link Snapshot.describe}. */ - describe(): Description { - return describe(reads(this)); - } - - /** Reads one value of the current version; see {@link Snapshot.get}. */ - get(table: string, id: Id, field: string, options: { at?: number } = {}): Value { - return get(reads(this), table, id, field, options); - } - - /** Queries the current version; see {@link Snapshot.query}. */ - query(query: Query): QueryResult { - return reads(this).query(JSON.stringify(query)) as QueryResult; - } - - /** Per-row minimums and maximums of the current version; see {@link Snapshot.envelope}. */ - envelope(request: EnvelopeRequest): Envelope { - return envelope(reads(this), request); - } - - /** The current version as latkit's bytes: a patch that builds it from an empty dataset. */ - toBytes(): Uint8Array { - return reads(this).toBytes(); - } - - /** - * Frees this handle. Views drawing the dataset keep drawing it, and its snapshots stay usable. - * Other methods throw afterwards. - */ - destroy(): void { - release(this)?.free(); - } - - /** Calls {@link Data.destroy}, so a `using` declaration destroys it when its block ends. */ - [Symbol.dispose](): void { - this.destroy(); - } } /** * One immutable version of a dataset, taken in O(1) by {@link Data.snapshot}. A view given a - * snapshot draws that version unchanged. + * snapshot draws that version, and keeps drawing it once the snapshot is destroyed. */ -export class Snapshot { - private constructor() {} - - /** The name of the dataset this is a version of. */ - get name(): string { - return reads(this).name; - } - - /** - * Describes the dataset: its tables and their row counts, each field and its kind, and the axis - * and the span its sampled fields cover. - */ - describe(): Description { - return describe(reads(this)); - } - - /** - * Reads `field` of the row with `id` in `table`. A sampled field reads its last frame at or - * before `options.at`, or its last frame when `at` is omitted. A reference reads the referenced - * row's id. Returns `null` when the table, row, field, or value is missing. - */ - get(table: string, id: Id, field: string, options: { at?: number } = {}): Value { - return get(reads(this), table, id, field, options); - } - - /** - * Runs `query` on the CPU: reads a table's rows, filtered, sorted, and paged, through the same - * expressions marks use. Sampled fields are read at `query.at`. Throws an `invalid` error for an - * invalid query. - */ - query(query: Query): QueryResult { - return reads(this).query(JSON.stringify(query)) as QueryResult; +export class Snapshot extends Dataset { + private constructor(bound: bindings.Snapshot) { + super(bound, 'Snapshot'); } - /** - * Computes each row's minimum and maximum of a sampled field over equal spans of an axis range, - * for every row or the rows `request.ids` lists. Each span reads the coarsest stored levels of - * detail that fit in it, so it costs a few reads regardless of how many frames it covers. Throws - * an `invalid` error for an unknown table or row, or a field that is not sampled, and a `limit` - * error when rows times spans exceeds 2^26. - */ - envelope(request: EnvelopeRequest): Envelope { - return envelope(reads(this), request); - } - - /** This version as latkit's bytes: a patch that builds it from an empty dataset. */ - toBytes(): Uint8Array { - return reads(this).toBytes(); - } - - /** - * Frees this handle. Views given the snapshot keep drawing it; other methods throw afterwards. - */ - destroy(): void { - release(this)?.free(); - } - - /** Calls {@link Snapshot.destroy}, so a `using` declaration destroys it when its block ends. */ - [Symbol.dispose](): void { - this.destroy(); + /** @internal Wraps the bindings snapshot `bound`. */ + static made(bound: bindings.Snapshot): Snapshot { + return new Snapshot(bound); } } /** - * A dataset fetched in pieces as views need them. A `Tiles` is plain data: a dataset name and a - * URL. Each view draws the pieces its camera and plot window need as a dataset of its own, and all - * views of a GPU share the fetched pieces. The first bytes at `url` decide the kind of tiles: + * A dataset fetched in pieces as views need them: plain data, a dataset name and a URL. Each view + * draws the pieces its camera and plot window need, and the views of a GPU share them. The first + * bytes at `url` decide the kind: * - * - Time tiles: a `frames.json` manifest as `latkit tile` writes it, for a dataset whose sampled - * fields' histories are fetched in spans. A line tracing a field fetches the level of detail its - * plot window shows; other marks fetch the frames around the view's `at`. A span still loading - * shows the coarser span that contains it. - * - Vector tiles: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` URL template, fetched - * as the camera needs them. Each layer becomes a table of features of the kind its marks draw: - * polygons for areas, lines for lines, points for vertices. A table named `layer:polygons`, - * `layer:lines`, or `layer:points` holds that kind regardless of the marks. + * - Time tiles: a `frames.json` manifest as `latkit tile` writes it. A line tracing a sampled field + * fetches the detail its plot window shows; other marks fetch the frames around the view's `at`. + * A span still loading shows the coarser span that contains it. + * - Vector tiles: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` URL template. Each + * layer becomes a table of the features its marks draw: polygons for areas, lines for lines, + * points for vertices. A table named `layer:polygons`, `layer:lines`, or `layer:points` holds + * that kind whatever the marks. */ export class Tiles { - /** The name of the dataset the tiles are drawn as, by which a mark's `data` option draws it. */ + /** The name of the dataset the tiles are drawn as. */ readonly name: string; - /** - * Where the tiles are: a `frames.json` manifest, a PMTiles archive, a TileJSON document, or a - * `{z}/{x}/{y}` URL template. - */ + /** Where the tiles are. */ readonly url: string; /** Tiles at `url`, drawn as the dataset named `name`. */ @@ -262,28 +206,5 @@ export type Source = Data | Snapshot | Tiles; /** @internal Converts `source` to a new bindings `Source` on every call. */ export function sourceOf(source: Source): bindings.Source { if (source instanceof Tiles) return wasm().Source.tiles(source.name, source.url); - return source instanceof Data - ? inner(source, 'Data').source() - : inner(source, 'Snapshot').source(); -} - -/** @internal Wraps a bindings snapshot in a public {@link Snapshot}. */ -export function snapshotOf(snapshot: bindings.Snapshot): Snapshot { - return wrap(Snapshot, snapshot); -} - -function reads(owner: Data | Snapshot): Reads { - return inner(owner, owner instanceof Data ? 'Data' : 'Snapshot'); -} - -function describe(reads: Reads): Description { - return JSON.parse(reads.describe()) as Description; -} - -function get(reads: Reads, table: string, id: Id, field: string, { at }: { at?: number }): Value { - return JSON.parse(reads.get(table, id, field, at)) as Value; -} - -function envelope(reads: Reads, request: EnvelopeRequest): Envelope { - return reads.envelope(JSON.stringify(request)) as Envelope; + return inner(source).source(); } diff --git a/npm/src/encoder.ts b/npm/src/encoder.ts index 7273853d..87b549e1 100644 --- a/npm/src/encoder.ts +++ b/npm/src/encoder.ts @@ -88,9 +88,7 @@ export function encoder(width: number, height: number, fps: number, bitrate: num units.push([expected.time, chunk.type === 'key', bytes]); }, error(error) { - failure = Object.assign(thrown({ code: 'io', message: 'the video encoder failed' }), { - cause: error, - }); + failure = thrown({ code: 'io', message: 'the video encoder failed', cause: error }); wake(); }, }); diff --git a/npm/src/gpu.ts b/npm/src/gpu.ts index c367c825..3846cc97 100644 --- a/npm/src/gpu.ts +++ b/npm/src/gpu.ts @@ -1,29 +1,31 @@ /** The GPU device: it creates views and canvases, owns their shared memory, and runs frames. */ import type * as bindings from '../pkg/latkit.js'; -import { Canvas, type CanvasOptions } from './canvas.js'; +import { Canvas, type Panel } from './canvas.js'; +import type { Source } from './data.js'; import type { Budget, Scene } from './inputs.js'; -import { hold, inner, release, wasm, waited, type Wait } from './internal.js'; +import { Handle, inner, wasm, waited, type Wait } from './internal.js'; import { firstDevice, load } from './load.js'; import type { Work } from './outputs.js'; -import { View, type ViewOptions } from './view.js'; +import { View } from './view.js'; /** * A WebGPU device that views draw with. The views and canvases of one `Gpu` share its memory and - * draw together in each animation frame, so linked views always show the same moment. + * draw together in each animation frame, so linked views always show the same moment. Destroying + * it destroys its canvases, views, and frame listeners; other GPUs and datasets stay usable. */ -export class Gpu { +export class Gpu extends Handle { #views = new Set(); #canvases = new Set(); #listeners = new Set<() => void>(); private constructor(device: bindings.Gpu) { - hold(this, device); + super(device, 'Gpu'); } /** - * Requests a WebGPU device, calling `load()` first. The first call reuses the device that - * `load()` requested. Rejects with an `unsupported` error when the browser has no WebGPU device. + * Requests a WebGPU device; the first call takes the one `load()` requested. Rejects with an + * `unsupported` error when the browser has no WebGPU device. */ static async create(): Promise { await load(); @@ -31,28 +33,37 @@ export class Gpu { } /** - * Creates a view that draws `scene` from the datasets in `options.sources`. Show it in a - * {@link Canvas}, or draw it into pictures with a `Shot`, with or without a canvas. Throws an - * `invalid` error for an invalid scene. + * Creates a view that draws `scene`. Show it in a {@link Canvas}, or draw it into pictures with a + * `Shot`, with or without a canvas. */ - view(scene: Scene, options: ViewOptions = {}): View { - const view = View.made(inner(this, 'Gpu'), scene, () => this.#views.delete(view)); + view( + scene: Scene, + options: { + /** The datasets the scene draws; see {@link View.sources}. */ + sources?: readonly Source[]; + } = {}, + ): View { + const view = View.made(device(this), scene, () => this.#views.delete(view)); this.#views.add(view); if (options.sources) view.sources = options.sources; return view; } /** - * Creates a canvas that shows views in `target`, placed by `options.layout`. A `` element - * sizes itself to its CSS box at the device pixel ratio, and passes pointer, wheel, and key input - * to the views in its panels. An `OffscreenCanvas`, as in a worker, keeps the size it is given at - * `options.pixelRatio` pixels per CSS pixel, and takes no input. Throws an `invalid` error for an - * invalid layout, such as a view already shown in another panel. + * Creates a canvas that shows views in `target`. A `` element sizes itself to its CSS box + * at the device pixel ratio, and passes pointer, wheel, and key input to the views in its panels. + * An `OffscreenCanvas`, as in a worker, keeps the size it is given and takes no input. */ - canvas(target: HTMLCanvasElement | OffscreenCanvas, options: CanvasOptions = {}): Canvas { - const canvas = Canvas.made(inner(this, 'Gpu'), target, options, () => - this.#canvases.delete(canvas), - ); + canvas( + target: HTMLCanvasElement | OffscreenCanvas, + options: { + /** The views shown and their regions; see {@link Canvas.layout}. None by default. */ + layout?: readonly Panel[]; + /** Pixels per CSS pixel of an `OffscreenCanvas`, 1 by default. */ + pixelRatio?: number; + } = {}, + ): Canvas { + const canvas = Canvas.made(device(this), target, options, () => this.#canvases.delete(canvas)); this.#canvases.add(canvas); return canvas; } @@ -63,7 +74,7 @@ export class Gpu { * runs every animation frame. Returns a function that removes the listener. */ on(type: 'frame', listener: (now: number) => void): () => void { - const handle = inner(this, 'Gpu').onFrame(listener); + const handle = device(this).onFrame(listener); const stop = (): void => { if (this.#listeners.delete(stop)) handle.free(); }; @@ -73,7 +84,7 @@ export class Gpu { /** Whether the device was lost. Drawing then does nothing; create a new `Gpu`. */ get lost(): boolean { - return inner(this, 'Gpu').lost; + return device(this).lost; } /** @@ -81,11 +92,11 @@ export class Gpu { * to half the device's largest storage buffer, and 32 MiB uploaded per frame. */ get budget(): Budget { - return JSON.parse(inner(this, 'Gpu').budget) as Budget; + return JSON.parse(device(this).budget) as Budget; } set budget(budget: Budget) { - inner(this, 'Gpu').setBudget(JSON.stringify(budget)); + device(this).setBudget(JSON.stringify(budget)); } /** @@ -93,37 +104,35 @@ export class Gpu { * repeat for the same budget, device limits, and frames, so tests and benchmarks can assert them. */ get work(): Work { - return JSON.parse(inner(this, 'Gpu').work) as Work; + return JSON.parse(device(this).work) as Work; } - /** - * Bytes of GPU memory the views of this GPU draw from: data pages, and each view's per-row data. - */ + /** Bytes of GPU memory its views draw from: data pages, and each view's per-row data. */ get resident(): number { - return inner(this, 'Gpu').resident; + return device(this).resident; } - /** Bytes waiting to upload to the GPU, across every view of this GPU. */ + /** Bytes waiting to upload to the GPU, across its views. */ get uploading(): number { - return inner(this, 'Gpu').uploading; + return device(this).uploading; } /** - * Draws one frame immediately at time `now`, in milliseconds on the `requestAnimationFrame` - * clock: it runs the frame listeners, then draws every canvas that needs a frame. After the first - * call, this GPU stops requesting animation frames. A test or benchmark that steps every frame - * itself then draws the same frames on any device. + * Draws one frame now, at `now` milliseconds on the `requestAnimationFrame` clock: it runs the + * frame listeners, then draws every canvas that needs a frame. After the first call, the GPU + * requests no more animation frames, so a test or benchmark that steps every frame draws the + * same frames on any device. */ step(now: number): void { - inner(this, 'Gpu').step(now); + device(this).step(now); } /** - * Resolves once the GPU has finished all submitted work, every readback has arrived, and every + * Resolves once the GPU has finished its submitted work, every readback has arrived, and every * pipeline a view or shot requested has compiled. A frame stepped after that uses all of it. */ async done(wait: Wait = {}): Promise { - await waited(inner(this, 'Gpu').done(), wait, 'waiting for the GPU'); + await waited(device(this).done(), wait, 'waiting for the GPU'); } /** @@ -132,24 +141,17 @@ export class Gpu { * frames awaits it to know when to step again. */ async landed(wait: Wait = {}): Promise { - await waited(inner(this, 'Gpu').landed(), wait, 'waiting for work'); + await waited(device(this).landed(), wait, 'waiting for work'); } - /** - * Destroys this GPU's canvases, views, and frame listeners, then frees the device. Other GPUs and - * datasets stay usable. Calling it again does nothing; other methods throw afterwards. - */ - destroy(): void { - const device = release(this); - if (!device) return; + /** @internal Destroys the GPU's canvases, views, and frame listeners before its device. */ + protected override freeing(): void { for (const canvas of this.#canvases) canvas.destroy(); for (const view of this.#views) view.destroy(); for (const stop of this.#listeners) stop(); - device.free(); } +} - /** Calls {@link Gpu.destroy}, so a `using` declaration destroys it when its block ends. */ - [Symbol.dispose](): void { - this.destroy(); - } +function device(owner: Gpu): bindings.Gpu { + return inner(owner); } diff --git a/npm/src/index.ts b/npm/src/index.ts index a8590edb..c32f4892 100644 --- a/npm/src/index.ts +++ b/npm/src/index.ts @@ -3,19 +3,25 @@ * latkit: columnar data drawn with WebGPU. * * Scenes, expressions, and patches are plain data and need no WebAssembly. Datasets, views, - * pictures, and `colormapCss` need it, so `await load()` first. Every class with `destroy()` also - * works with `using`; the reference above makes `Symbol.dispose` typecheck whatever a project's - * `lib` setting. + * pictures, and the colormap functions need it, so `await load()` first. Every `Handle` also works + * with `using`; the reference above makes `Symbol.dispose` typecheck whatever a project's `lib` + * setting. */ export type * from './inputs.js'; export type * from './outputs.js'; -export { load, type LoadOptions } from './load.js'; -export type { LatkitError, Wait } from './internal.js'; +export { load } from './load.js'; +export { + Handle, + type ByteSource, + type LatkitError, + type ReadableBytes, + type Wait, +} from './internal.js'; export { Data, + Dataset, Snapshot, Tiles, - type DataSource, type Envelope, type QueryResult, type Source, @@ -25,22 +31,13 @@ export { reference, type Fields, type Geometry, - type PatchSource, - type ReadableBytes, type Reference, type RowIds, type Values, } from './patch.js'; export * from './scene.js'; -export { colormapCss } from './colormap.js'; +export { colormapCss, colormaps } from './colormap.js'; export { Gpu } from './gpu.js'; -export { - View, - type Insets, - type Point, - type Position, - type ViewEvents, - type ViewOptions, -} from './view.js'; -export { Canvas, type CanvasOptions, type Panel, type Rect } from './canvas.js'; +export { View, type Insets, type Point, type Position, type ViewEvents } from './view.js'; +export { Canvas, type Panel, type Rect } from './canvas.js'; export { Image, Shot, Video, type ImageFormat, type ShotOptions } from './shot.js'; diff --git a/npm/src/inputs.ts b/npm/src/inputs.ts index 9901a6a9..61081dca 100644 --- a/npm/src/inputs.ts +++ b/npm/src/inputs.ts @@ -14,50 +14,12 @@ export interface AreaTheme { * earth, areas follow the globe's curve and stay whole across the antimeridian. To outline areas, * add a `Lines` mark of the same table, which draws their rings. */ -export interface Areas { - /** The dataset and table drawn. */ - of: [data: string, table: string]; +export interface Areas extends MarkOptions { /** * The polygon field each row fills, holding one polygon's rings or several polygons; the field * named `geometry` by default. */ geometry?: string | null; - /** - * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their - * corners' colors. - */ - color?: PaintInput | null; - /** Whether each row is drawn and can be picked. Default `true`. */ - visible?: ExprInput; - /** - * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are - * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are - * labeled first. - */ - priority?: ExprInput | null; - /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ - name?: string | null; - /** - * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces - * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. - */ - pick?: boolean | null; - /** Whether clicking a row selects it. Default `true`. */ - select?: boolean; - /** - * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the - * camera. Default `false`. - */ - drag?: boolean; - /** Each row's label: a text field, or label options. */ - label?: Label | null; - /** - * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap - * across its domain, a cell for each category, or a swatch beside each name of a map's - * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a - * conditional, `otherwise` is searched before the cases. - */ - legend?: string | null; } /** A plot axis. */ @@ -436,9 +398,7 @@ export interface EdgeTheme { * wires. In a diagram, a row without ends is a net, joining every vertex port whose reference * field names it. */ -export interface Edges { - /** The dataset and table drawn. */ - of: [data: string, table: string]; +export interface Edges extends MarkOptions, StrokeOptions { /** The reference fields naming each row's two vertices. */ ends?: [from: string, to: string] | null; /** A path field whose points each row passes through between its ends. */ @@ -463,55 +423,6 @@ export interface Edges { * `reference` field names it: a star from their center to each member. */ joins?: [table: string, reference: string] | null; - /** - * Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given - * with `px` or `units`. The theme's width unless set. - */ - width?: Size | null; - /** Whether each row is dashed, with the theme's `dash` period. Default `false`. */ - dash?: ExprInput; - /** - * The speed of comets flowing along each row, per second, in the unit of the theme's `flow` - * lengths. Negative speeds flow backward; 0, the default, draws no comets. When the speed changes, - * comets continue from where they are. - */ - flow?: ExprInput; - /** - * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their - * corners' colors. - */ - color?: PaintInput | null; - /** Whether each row is drawn and can be picked. Default `true`. */ - visible?: ExprInput; - /** - * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are - * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are - * labeled first. - */ - priority?: ExprInput | null; - /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ - name?: string | null; - /** - * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces - * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. - */ - pick?: boolean | null; - /** Whether clicking a row selects it. Default `true`. */ - select?: boolean; - /** - * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the - * camera. Default `false`. - */ - drag?: boolean; - /** Each row's label: a text field, or label options. */ - label?: Label | null; - /** - * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap - * across its domain, a cell for each category, or a swatch beside each name of a map's - * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a - * conditional, `otherwise` is searched before the cases. - */ - legend?: string | null; } /** @@ -555,50 +466,12 @@ export type ExprNode = * and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen * from above shows its exact colors. */ -export interface Faces { - /** The dataset and table drawn. */ - of: [data: string, table: string]; +export interface Faces extends MarkOptions { /** * The reference fields naming each row's three vertices; required. Without a color, a face blends * the colors of its corners. */ corners?: [a: string, b: string, c: string] | null; - /** - * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their - * corners' colors. - */ - color?: PaintInput | null; - /** Whether each row is drawn and can be picked. Default `true`. */ - visible?: ExprInput; - /** - * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are - * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are - * labeled first. - */ - priority?: ExprInput | null; - /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ - name?: string | null; - /** - * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces - * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. - */ - pick?: boolean | null; - /** Whether clicking a row selects it. Default `true`. */ - select?: boolean; - /** - * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the - * camera. Default `false`. - */ - drag?: boolean; - /** Each row's label: a text field, or label options. */ - label?: Label | null; - /** - * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap - * across its domain, a cell for each category, or a swatch beside each name of a map's - * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a - * conditional, `otherwise` is searched before the cases. - */ - legend?: string | null; } /** @@ -830,9 +703,7 @@ export interface LineTheme { * as a monitor does. On the earth, paths follow the globe's curve and stay whole across the * antimeridian. */ -export interface Lines { - /** The dataset and table drawn. */ - of: [data: string, table: string]; +export interface Lines extends MarkOptions, StrokeOptions { /** * The path or polygon field each row runs along; the field named `geometry` by default. Not * allowed on a plot. @@ -845,19 +716,35 @@ export interface Lines { trace?: string | null; /** How each row runs between its samples on a plot; `linear` by default. */ interpolation?: Interpolation; - /** - * Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given - * with `px` or `units`. The theme's width unless set. - */ - width?: Size | null; - /** Whether each row is dashed, with the theme's `dash` period. Default `false`. */ - dash?: ExprInput; - /** - * The speed of comets flowing along each row, per second, in the unit of the theme's `flow` - * lengths. Negative speeds flow backward; 0, the default, draws no comets. When the speed changes, - * comets continue from where they are. - */ - flow?: ExprInput; +} + +/** One table a scene draws. In JSON: `{ vertices | edges | faces | lines | areas: options }`. */ +export type Mark = + | { + /** Rows drawn as markers. */ + vertices: Vertices; + } + | { + /** Rows drawn as strokes between two vertices. */ + edges: Edges; + } + | { + /** Rows drawn as triangles between three vertices. */ + faces: Faces; + } + | { + /** Rows drawn as lines along their paths, or on a plot through their samples. */ + lines: Lines; + } + | { + /** Rows drawn as the areas their polygons cover. */ + areas: Areas; + }; + +/** The options every mark has. */ +export interface MarkOptions { + /** The dataset and table drawn. */ + of: [data: string, table: string]; /** * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their * corners' colors. @@ -896,29 +783,6 @@ export interface Lines { legend?: string | null; } -/** One table a scene draws. In JSON: `{ vertices | edges | faces | lines | areas: options }`. */ -export type Mark = - | { - /** Rows drawn as markers. */ - vertices: Vertices; - } - | { - /** Rows drawn as strokes between two vertices. */ - edges: Edges; - } - | { - /** Rows drawn as triangles between three vertices. */ - faces: Faces; - } - | { - /** Rows drawn as lines along their paths, or on a plot through their samples. */ - lines: Lines; - } - | { - /** Rows drawn as the areas their polygons cover. */ - areas: Areas; - }; - /** * How a vertex is drawn: a shape's name, `"block"`, or * `{ shape | block | gauge | pie | icon | wgsl: options }`. An ellipse by default. @@ -1255,6 +1119,23 @@ export interface Stress { gap?: number; } +/** The options of marks drawn as strokes: edges and lines. */ +export interface StrokeOptions { + /** + * Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given + * with `px` or `units`. The theme's width unless set. + */ + width?: Size | null; + /** Whether each row is dashed, with the theme's `dash` period. Default `false`. */ + dash?: ExprInput; + /** + * The speed of comets flowing along each row, per second, in the unit of the theme's `flow` + * lengths. Negative speeds flow backward; 0, the default, draws no comets. When the speed changes, + * comets continue from where they are. + */ + flow?: ExprInput; +} + /** How labels, axis ticks, and titles look. */ export interface TextTheme { /** The text color. */ @@ -1350,9 +1231,7 @@ export interface VertexTheme { } /** Rows drawn as markers: points of a map, nodes of a graph, blocks of a diagram. */ -export interface Vertices { - /** The dataset and table drawn. */ - of: [data: string, table: string]; +export interface Vertices extends MarkOptions { /** * Each row's x and y. Without a position, the point field that `geometry` names places rows, and * the scene's layout places rows that have neither. @@ -1398,42 +1277,6 @@ export interface Vertices { * Default `false`. Diagrams only. */ shadow?: boolean; - /** - * Each row's color. Unset, rows take the theme's default for the mark; faces instead blend their - * corners' colors. - */ - color?: PaintInput | null; - /** Whether each row is drawn and can be picked. Default `true`. */ - visible?: ExprInput; - /** - * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, are - * the last dropped when a table too large to draw whole is thinned to the tiles in view, and are - * labeled first. - */ - priority?: ExprInput | null; - /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ - name?: string | null; - /** - * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces - * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. - */ - pick?: boolean | null; - /** Whether clicking a row selects it. Default `true`. */ - select?: boolean; - /** - * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the - * camera. Default `false`. - */ - drag?: boolean; - /** Each row's label: a text field, or label options. */ - label?: Label | null; - /** - * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap - * across its domain, a cell for each category, or a swatch beside each name of a map's - * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a - * conditional, `otherwise` is searched before the cases. - */ - legend?: string | null; } /** The value of the first case whose condition holds, else `otherwise`. */ diff --git a/npm/src/internal.ts b/npm/src/internal.ts index 543a8e1a..31577437 100644 --- a/npm/src/internal.ts +++ b/npm/src/internal.ts @@ -1,7 +1,7 @@ /** - * Internals shared by the package's classes: access to the WebAssembly bindings, the bindings - * object behind each public object, and latkit's errors and bounded waits. Of these, only the - * `LatkitError` and `Wait` types are public. + * What the package's classes share: access to the WebAssembly bindings, the `Handle` base class, + * latkit's errors and bounded waits, and reading bytes. Of these, `Handle`, `LatkitError`, `Wait`, + * `ByteSource`, and `ReadableBytes` are public. */ import * as bindings from '../pkg/latkit.js'; @@ -51,88 +51,171 @@ export function wasm(): typeof bindings { return bindings; } -/** @internal Builds a {@link LatkitError} from a code, a message, and an optional path. */ +/** @internal Builds a {@link LatkitError}: a code, a message, and optionally a path and a cause. */ export function thrown({ code, message, path, -}: Pick & { code: LatkitError['code'] }): LatkitError { - return Object.assign(new Error(message), path === undefined ? { code } : { code, path }); + cause, +}: Pick & { + code: LatkitError['code']; + cause?: unknown; +}): LatkitError { + const error = new Error(message, cause === undefined ? undefined : { cause }); + return Object.assign(error, path === undefined ? { code } : { code, path }); } -/** @internal An `io` error saying `message`, with `cause` as its cause. */ -function io(message: string, cause?: unknown): LatkitError { - return Object.assign(thrown({ code: 'io', message }), cause === undefined ? {} : { cause }); +/** The bindings object behind each handle, until it is destroyed, and the name its errors use. */ +const held = new WeakMap(); + +/** + * An object that holds WebAssembly memory, and GPU memory for a `Gpu`, until it is destroyed: + * `destroy()` frees it, as does a `using` declaration when its block ends. Once it is destroyed, + * its other methods throw a `destroyed` error. + */ +export abstract class Handle { + /** @internal Holds `bound`, the bindings object it frees; errors name the object `what`. */ + constructor(bound: { free(): void }, what: string) { + held.set(this, { bound, what }); + } + + /** Frees the object's memory. Calling it again does nothing. */ + destroy(): void { + const entry = held.get(this); + const bound = entry?.bound; + if (!bound) return; + entry.bound = undefined; + this.freeing(); + bound.free(); + } + + /** @internal Runs once, as the object is destroyed, before its memory is freed. */ + protected freeing(): void {} + + /** Calls `destroy()`, so a `using` declaration destroys the object when its block ends. */ + [Symbol.dispose](): void { + this.destroy(); + } +} + +/** @internal Returns `owner`'s bindings object. Throws a `destroyed` error once it is destroyed. */ +export function inner(owner: Handle): T { + const entry = held.get(owner)!; + if (!entry.bound) + throw thrown({ code: 'destroyed', message: `this ${entry.what} has been destroyed` }); + return entry.bound as T; +} + +/** A byte stream: anything shaped like a browser or Node.js `ReadableStream` of `Uint8Array`. */ +export interface ReadableBytes { + /** A reader of the stream's chunks, which latkit reads to the end, or cancels. */ + getReader(): { + read(): Promise<{ done: boolean; value?: Uint8Array }>; + cancel(reason?: unknown): Promise; + releaseLock(): void; + }; } /** - * @internal Reads `source` into bytes, with the URL they came from as `base` (else empty). A string - * is a URL to fetch; a `Response`, `Blob`, or byte stream is read; any other object is written as - * JSON text. Waits within `wait`, and rejects with an `io` error when a fetch fails or answers an - * error status. + * Bytes to read: an array or buffer, a `Blob`, a `Response`, a byte stream, or a URL to fetch as a + * string. Reading waits within its {@link Wait}, and fails with an `io` error when a fetch fails or + * answers an error status, or the bytes cannot be read. + */ +export type ByteSource = + Uint8Array | ArrayBuffer | Blob | Response | ReadableBytes | AsyncIterable | string; + +/** + * @internal Reads `source` into bytes, with the URL they came from as `base` (else empty). Any + * object that is not a {@link ByteSource} is written as JSON text. Waits within `wait`, cancelling + * a fetch or stream it gives up on. */ export async function bytesOf( - source: Uint8Array | ArrayBuffer | Blob | Response | ReadableStream | string | object, + source: ByteSource | object, wait: Wait, ): Promise<{ bytes: Uint8Array; base: string }> { - if (typeof source === 'string') { - const url = source; - const fetched = fetch(url, { signal: wait.signal }).then( - (response) => bytesOf(response, wait), - (cause: unknown) => Promise.reject(io(`${url} could not be fetched`, cause)), + const failed = (message: string) => (cause: unknown) => + Promise.reject(thrown({ code: 'io', message, cause })); + const reading = (what: string, bytes: Promise) => + waited( + bytes.then((read) => new Uint8Array(read), failed(`${what} could not be read`)), + wait, + `reading ${what}`, ); - return waited(fetched, wait, `fetching ${url}`); - } if (source instanceof Uint8Array) return { bytes: source, base: '' }; if (source instanceof ArrayBuffer) return { bytes: new Uint8Array(source), base: '' }; + if (typeof source === 'string') { + const fetched = fetch(source, { signal: wait.signal }).catch( + failed(`${source} could not be fetched`), + ); + return bytesOf(await waited(fetched, wait, `fetching ${source}`), wait); + } if (source instanceof Response) { const { url, status, statusText } = source; - if (!source.ok) throw io(`${url}: ${status} ${statusText}`); - const body = await source.arrayBuffer().catch((cause: unknown) => { - throw io(`${url} could not be read`, cause); - }); - return { bytes: new Uint8Array(body), base: url }; + if (!source.ok) throw thrown({ code: 'io', message: `${url}: ${status} ${statusText}` }); + return { bytes: await reading(url || 'the response', source.arrayBuffer()), base: url }; } if (source instanceof Blob) - return { bytes: new Uint8Array(await source.arrayBuffer()), base: '' }; - if (source instanceof ReadableStream) - return { bytes: new Uint8Array(await new Response(source).arrayBuffer()), base: '' }; + return { bytes: await reading('the blob', source.arrayBuffer()), base: '' }; + if ('getReader' in source || Symbol.asyncIterator in source) { + const stream = source as ReadableBytes | AsyncIterable; + return { + bytes: await waited(collect(stream, wait.signal), wait, 'reading the stream'), + base: '', + }; + } return { bytes: new TextEncoder().encode(JSON.stringify(source)), base: '' }; } -/** Maps each public object to its bindings object, until it is released. */ -const held = new WeakMap(); - -/** @internal Associates public object `owner` with its bindings object `inner`. */ -export function hold(owner: object, inner: object): void { - held.set(owner, inner); +/** The bytes of `stream` in one array; see {@link chunks}. */ +async function collect( + stream: ReadableBytes | AsyncIterable, + signal?: AbortSignal, +): Promise { + const parts: Uint8Array[] = []; + for await (const chunk of chunks(stream, signal)) parts.push(chunk); + const bytes = new Uint8Array(parts.reduce((n, part) => n + part.length, 0)); + parts.reduce((at, part) => (bytes.set(part, at), at + part.length), 0); + return bytes; } /** - * @internal Returns `owner`'s bindings object. Once `owner` is destroyed, throws a `destroyed` - * error naming `what`. + * @internal Yields the chunks of `stream`, through `getReader()` when it has one, else by async + * iteration, until the stream ends or `signal` aborts. Stopping early cancels the stream, with the + * abort's reason. A failed read throws an `io` error. */ -export function inner(owner: object, what: string): T { - const found = held.get(owner); - if (!found) throw thrown({ code: 'destroyed', message: `this ${what} has been destroyed` }); - return found as T; -} - -/** - * @internal Detaches and returns `owner`'s bindings object, for the caller to free. Returns - * `undefined` if it was already released. - */ -export function release(owner: object): T | undefined { - const found = held.get(owner); - held.delete(owner); - return found as T | undefined; -} - -/** @internal Creates a `type` around bindings object `inner` without calling its constructor. */ -export function wrap(type: { prototype: T }, inner: object): T { - const made = Object.create(type.prototype) as T; - hold(made, inner); - return made; +export async function* chunks( + stream: ReadableBytes | AsyncIterable, + signal?: AbortSignal, +): AsyncGenerator { + const failed = (cause: unknown) => + thrown({ code: 'io', message: 'the stream could not be read', cause }); + if (!('getReader' in stream)) { + try { + for await (const chunk of stream) { + if (signal?.aborted) return; + yield chunk; + } + } catch (cause) { + throw failed(cause); + } + return; + } + const reader = stream.getReader(); + const cancel = () => void reader.cancel(signal?.reason).catch(() => {}); + signal?.addEventListener('abort', cancel, { once: true }); + let ended = false; + try { + while (!signal?.aborted) { + const read = await reader.read().catch((cause: unknown) => Promise.reject(failed(cause))); + if ((ended = read.done)) return; + if (!read.value) throw thrown({ code: 'io', message: 'a stream chunk is missing' }); + yield read.value; + } + } finally { + signal?.removeEventListener('abort', cancel); + if (!ended) await reader.cancel(signal?.reason).catch(() => {}); + reader.releaseLock(); + } } /** @@ -177,7 +260,5 @@ function abortError(signal: AbortSignal, what: string): LatkitError { const reason = signal.reason as { name?: string } | undefined; return reason?.name === 'TimeoutError' ? thrown({ code: 'timeout', message: `${what} timed out` }) - : Object.assign(thrown({ code: 'cancelled', message: `${what} was cancelled` }), { - cause: reason, - }); + : thrown({ code: 'cancelled', message: `${what} was cancelled`, cause: reason }); } diff --git a/npm/src/jobs.ts b/npm/src/jobs.ts index 6b202ff8..1f211d17 100644 --- a/npm/src/jobs.ts +++ b/npm/src/jobs.ts @@ -3,6 +3,8 @@ * module once and has its own WebAssembly memory. */ +import { thrown, type LatkitError } from './internal.js'; + /** A worker's message: that it is ready, a job's bytes, or why it failed. */ interface Answer { ready?: boolean; @@ -13,7 +15,7 @@ interface Answer { interface Job { bytes: Uint8Array; resolve(bytes: Uint8Array): void; - reject(error: Error): void; + reject(error: LatkitError): void; } interface Slot { @@ -23,9 +25,11 @@ interface Slot { timer?: ReturnType; } -/** The error for a job that ran too long, with the `timeout` code latkit keeps. */ -function timedOut(): Error { - return Object.assign(new Error('a worker job timed out'), { code: 'timeout' }); +/** The error for a failed job: `cause` itself if it is one, else an `io` error saying it. */ +function failure(cause: unknown): LatkitError { + if (cause instanceof Error && 'code' in cause) return cause as LatkitError; + const message = cause instanceof Error ? cause.message : String(cause); + return thrown({ code: 'io', message, cause: cause instanceof Error ? cause : undefined }); } /** @@ -53,7 +57,7 @@ export function jobs( const fail = (slot: Slot, cause: unknown): void => { const job = slot.job; retire(slot); - job?.reject(cause instanceof Error ? cause : new Error(String(cause))); + job?.reject(failure(cause)); }; const send = (slot: Slot): void => { @@ -105,11 +109,12 @@ export function jobs( clearTimeout(slot.timer); slot.job = job; const active = slot; + const timedOut = () => thrown({ code: 'timeout', message: 'a worker job timed out' }); slot.timer = setTimeout(() => fail(active, timedOut()), 60_000); send(slot); } catch (error) { if (slot) retire(slot); - reject(error instanceof Error ? error : new Error(String(error))); + reject(failure(error)); } }); } diff --git a/npm/src/load.ts b/npm/src/load.ts index b2545340..c8548ecc 100644 --- a/npm/src/load.ts +++ b/npm/src/load.ts @@ -5,20 +5,6 @@ import { encoder } from './encoder.js'; import { loaded, thrown } from './internal.js'; import { jobs } from './jobs.js'; -/** Options for {@link load}. Only the first call's options take effect. */ -export interface LoadOptions { - /** - * The URL of this package version's `latkit/wasm` asset, or that asset already compiled. - * Defaults to the bundled asset. - */ - wasm?: string | URL | WebAssembly.Module; - /** - * Creates a worker that runs the standalone `latkit/worker` bundle. Defaults to the bundled - * worker. - */ - worker?: () => Worker; -} - let configured: { wasm?: string | WebAssembly.Module; worker?: () => Worker } | undefined; let loading: Promise | undefined; @@ -26,17 +12,20 @@ let loading: Promise | undefined; let first: Promise | undefined; /** - * Loads latkit's WebAssembly, once per page. Datasets, views, and pictures can be created once it - * resolves. Later and concurrent calls share the first call's load. Options they pass must equal - * the first call's, or they reject with an `io` error. A failed load rejects with an `io` error and - * can be retried. - * - * Importing the package loads nothing and requests no GPU. `load()` requests a GPU adapter while - * the WebAssembly compiles, so the browser starts its GPU process early, then requests the first - * GPU device for `Gpu.create()`. `Gpu.create()` and `Data.read()` call `load()` themselves, as does - * `Patch.read()` for JSON. + * Loads latkit's WebAssembly, once per page. Later and concurrent calls share the first call's + * load; options they pass must equal the first call's, or they reject with an `io` error. A failed + * load rejects with an `io` error and can be retried. `load()` also requests a GPU adapter while + * the WebAssembly compiles, so the browser starts its GPU process early, then the first device for + * `Gpu.create()`. */ -export function load(options: LoadOptions = {}): Promise { +export function load( + options: { + /** The URL of this version's `latkit/wasm` asset, or that asset compiled; bundled by default. */ + wasm?: string | URL | WebAssembly.Module; + /** Creates a worker running the standalone `latkit/worker` bundle; bundled by default. */ + worker?: () => Worker; + } = {}, +): Promise { // Store a URL as text, so the caller cannot change the configuration while it loads. const wasm = options.wasm instanceof URL ? options.wasm.href : options.wasm; if (loading) { @@ -54,7 +43,7 @@ export function load(options: LoadOptions = {}): Promise { loading = initialize(configured).catch((cause: unknown) => { configured = undefined; loading = undefined; - throw Object.assign(thrown({ code: 'io', message: 'could not load latkit' }), { cause }); + throw thrown({ code: 'io', message: 'could not load latkit', cause }); }); return loading; } diff --git a/npm/src/patch.ts b/npm/src/patch.ts index e2a87d54..2d902c0d 100644 --- a/npm/src/patch.ts +++ b/npm/src/patch.ts @@ -6,7 +6,16 @@ */ import type { Id, Ids } from './inputs.js'; -import { bytesOf, thrown, waited, wasm, type Wait } from './internal.js'; +import { + bytesOf, + chunks, + thrown, + waited, + wasm, + type ByteSource, + type ReadableBytes, + type Wait, +} from './internal.js'; import { load } from './load.js'; /** @@ -53,19 +62,11 @@ export function reference(table: string, ids: RowIds): Reference { /** A table's fields by name: values, or a {@link Reference}. */ export type Fields = Readonly>; -/** A byte stream: anything shaped like a browser or Node.js `ReadableStream` of `Uint8Array`. */ -export interface ReadableBytes { - /** A reader of the stream's chunks, which latkit reads to the end, or cancels. */ - getReader(): { - read(): Promise<{ done: boolean; value?: Uint8Array }>; - cancel(reason?: unknown): Promise; - releaseLock(): void; - }; -} - const MAGIC = [0x4c, 0x41, 0x54, 0x4b]; // LATK /** Header bytes: the magic and the frame's length. */ const HEADER = 12; +/** The longest frame read by default, in bytes: 256 MiB. */ +const LIMIT = 1 << 28; const OP = { axis: 0, replace: 1, set: 2, remove: 3, append: 4, forget: 5, frames: 6 } as const; /** The highest level of detail; level 0 is frames. */ const LEVELS = 21; @@ -76,21 +77,13 @@ const STORE = { f64: 0, f32: 1, i32: 2, u32: 3, u64: 4, bool: 5, text: 6, geomet const SAFE = Number.MAX_SAFE_INTEGER; const utf8 = new TextEncoder(); -/** - * What {@link Patch.read} accepts: bytes, a `Blob`, a `Response`, a byte stream, a URL to fetch as a - * string, or a parsed JSON value. The content is latkit's bytes or a patch's JSON. - */ -export type PatchSource = - Uint8Array | ArrayBuffer | Blob | Response | ReadableStream | string | object; - /** @internal Returns `patch`'s frame without copying it. Read it before the patch changes again. */ export let framed: (patch: Patch) => Uint8Array; /** - * A list of changes to a dataset, which `Data.apply` applies in order, all or none. The patch is - * encoded as latkit's bytes as it is built, without WebAssembly. Each method adds a change and - * returns the patch. For values it cannot encode, a method throws an `invalid` error with a `path` - * such as `Bus.kv`, and leaves the patch unchanged. + * A list of changes to a dataset, which `Data.apply` applies in order, all or none. It is written + * as latkit's bytes as it is built, without WebAssembly; each method adds a change and returns the + * patch. * * ```ts * const patch = new Patch('grid') @@ -138,16 +131,17 @@ export class Patch { * any language can write without latkit. The JSON is a list of changes. Each change is keyed by * its method name and holds the method's parameters by name, such as * `{"set": {"table": "Bus", "ids": [1], "fields": {"kv": [345]}}}`. A field may be a slice of a - * binary file, `{file, dtype, offset, count}`. Slices are fetched at once, relative to - * `options.base`, which defaults to the URL the JSON was fetched from. - * - * Bytes are read without WebAssembly; JSON calls `load()` first. Rejects with a `limit` error for - * a frame longer than `options.limit` bytes (256 MiB by default), with an `invalid` error for - * malformed input, and with an `io` error when a fetch fails or answers an error status. + * binary file, `{file, dtype, offset, count}`, fetched at once. Bytes are read without + * WebAssembly. */ static async read( - source: PatchSource, - options: { base?: string; limit?: number } & Wait = {}, + source: ByteSource | object, + options: { + /** The URL the JSON's file slices are relative to; by default, the one it came from. */ + base?: string; + /** The longest frame in bytes, 256 MiB by default; a longer one fails with a `limit` error. */ + limit?: number; + } & Wait = {}, ): Promise { const { bytes, base } = await bytesOf(source, options); if (MAGIC.every((byte, i) => bytes[i] === byte)) @@ -161,7 +155,7 @@ export class Patch { * Wraps frame `bytes` as a patch without copying them. Throws unless they are exactly one valid * frame of at most `limit` bytes. */ - static #owning(bytes: Uint8Array, limit = 1 << 28): Patch { + static #owning(bytes: Uint8Array, limit = LIMIT): Patch { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const length = frameLength(bytes, limit); if (length === undefined || length !== bytes.length) throw malformed('not one whole frame'); @@ -186,12 +180,9 @@ export class Patch { /** * Yields each patch in `stream` as soon as its last byte arrives. The stream carries frames back - * to back, such as a response body, a file, or a socket's messages. Each frame states its length - * up front, so a frame longer than `options.limit` bytes (256 MiB by default) throws a `limit` - * error before its body is allocated. Besides the current chunk and the patches the caller keeps, - * at most one frame is buffered. Breaking out of the loop cancels the stream. Throws an `invalid` - * error for a `limit` that is not a whole number of at least 12 bytes, for bytes that are not - * latkit's, or for a stream that ends inside a frame. + * to back, such as a response body, a file, or a socket's messages. Besides the current chunk + * and the patches the caller keeps, at most one frame is buffered. Breaking out of the loop + * cancels the stream. Throws an `invalid` error for a stream that ends inside a frame. * * ```ts * for await (const patch of Patch.stream((await fetch('/feed')).body!)) data.apply(patch); @@ -199,8 +190,15 @@ export class Patch { */ static async *stream( stream: ReadableBytes | AsyncIterable, - { limit = 1 << 28 }: { limit?: number } = {}, + options: { + /** + * The longest frame in bytes, at least 12; 256 MiB by default. Each frame states its length + * up front, so a longer one fails with a `limit` error before its body is allocated. + */ + limit?: number; + } = {}, ): AsyncGenerator { + const { limit = LIMIT } = options; if (!Number.isSafeInteger(limit) || limit < HEADER) throw invalid('limit', `a whole number of at least ${HEADER} bytes, not ${limit}`); let buffer = new Uint8Array(HEADER); @@ -265,18 +263,20 @@ export class Patch { /** * Records a frame of `table` at each axis coordinate in `at`. Each field gives a value per row - * per frame, frame by frame: every row of the first frame, then of the next. Frames cover every - * row, or only rows `options.ids`; other rows keep their last values. A field's first frame makes - * it a sampled field. Consecutive appends of `Float32Array` or `Float64Array` values to the same - * fields of every row are written as one change, so one frame per call costs no more than one - * call with every frame. + * per frame, frame by frame. A field's first frame makes it a sampled field. Consecutive appends + * of typed float arrays to the same fields of every row write as one change, so a frame per call + * costs no more than one call with every frame. */ append( table: string, at: number | readonly number[], fields: Readonly>, - { ids }: { ids?: RowIds } = {}, + options: { + /** The rows the frames cover, every row by default; other rows keep their last values. */ + ids?: RowIds; + } = {}, ): this { + const { ids } = options; const frames = typeof at === 'number' ? [at] : at; const entries = Object.entries(fields); const floats = @@ -314,33 +314,29 @@ export class Patch { } /** - * Stores a run of entries of sampled field `field` of `table`, at level of detail `level` (0 by - * default), numbered from `first`. This is what a time tile holds, as `latkit tile` writes it. - * - * At level 0 each entry is a frame: a coordinate in `at`, and a value per row in `values`, entry - * by entry. At level `k` each entry is a bucket of `8^k` frames: its first and last frames' - * coordinates in `at`. `values` then holds every bucket's per-row minimums, entry by entry, - * followed by every bucket's per-row maximums. NaN marks a missing value. - * - * None of the entries may be stored already. Entries past the field's last frame extend it. - * Throws an `invalid` error for a level outside 0 to 21, lengths of `at` and `values` that do not - * divide into whole entries and rows, or frame numbers that are not safe nonnegative integers. + * Stores a run of entries of sampled field `field` of `table`, at one level of detail. This is + * what a time tile holds, as `latkit tile` writes it. At level 0 each entry is a frame; at level + * `k`, a bucket of `8^k` frames. None of the entries may be stored already. Entries past the + * field's last frame extend it. */ frames( table: string, field: string, - { - level = 0, - first, - at, - values, - }: { + run: { + /** The level of detail, from 0 to 21; 0 by default. */ level?: number; + /** The first entry's number: a safe nonnegative integer. */ first: number; + /** Each frame's coordinate, or each bucket's first and last frames' coordinates. */ at: Float64Array | readonly number[]; + /** + * A value per row, entry by entry; at a bucket level, every bucket's per-row minimums, then + * every bucket's per-row maximums. NaN marks a missing value. + */ values: Float32Array | readonly number[]; }, ): this { + const { level = 0, first, at, values } = run; const path = `${table}.${field}`; if (!Number.isInteger(level) || level < 0 || level > LEVELS) throw invalid(path, `the level must be a whole number from 0 to ${LEVELS}`); @@ -811,32 +807,3 @@ class Writer { this.bytes(bytes); } } - -/** - * Yields the chunks of `stream`, through `getReader()` when it has one, else by async iteration. - * Stopping early cancels the reader. - */ -async function* chunks( - stream: ReadableBytes | AsyncIterable, -): AsyncGenerator { - if (!('getReader' in stream)) { - yield* stream; - return; - } - const reader = stream.getReader(); - let completed = false; - try { - for (;;) { - const { done, value } = await reader.read(); - if (done) { - completed = true; - return; - } - if (!value) throw malformed('a stream chunk is missing'); - yield value; - } - } finally { - if (!completed) await reader.cancel().catch(() => {}); - reader.releaseLock(); - } -} diff --git a/npm/src/scene.ts b/npm/src/scene.ts index aa7d1682..96889f3a 100644 --- a/npm/src/scene.ts +++ b/npm/src/scene.ts @@ -290,14 +290,6 @@ export const zoom = (): Expr => state('zoom'); /** A missing value: an option given one uses the theme's value. */ export const missing = (): Missing => new Missing(); -/** On-screen limits, in CSS pixels, for a size given in position units. */ -export interface Within { - /** The smallest size in CSS pixels, however far the view zooms out. */ - atLeast?: number; - /** The largest size in CSS pixels, however far the view zooms in. */ - atMost?: number; -} - /** * A size in CSS pixels regardless of the space's unit, so it stays the same on screen as the view * zooms: `px(1)` keeps a diagram's stroke 1 pixel wide at any zoom. Given a number, the result also @@ -311,14 +303,14 @@ export function px(size: ExprInput): Px { /** * A size in position units (degrees on the earth) regardless of the space's unit, so it scales as - * the view zooms. `within` limits its on-screen size in CSS pixels: `units('r', { atLeast: 2 })` + * the view zooms. `limits` bound its on-screen size in CSS pixels: `units('r', { atLeast: 2 })` * grows with the map and never shrinks below 2 pixels. A marker keeps its proportions, with the * limits applied to its larger side. Given only a number, the result also works as a theme length. */ export function units(size: number): { units: number }; -export function units(size: ExprInput, within?: Within): Units; -export function units(size: ExprInput, within: Within = {}): Units { - return { units: nodeOf(size), ...within }; +export function units(size: ExprInput, limits?: Omit): Units; +export function units(size: ExprInput, limits: Omit = {}): Units { + return { units: nodeOf(size), ...limits }; } /** One case of a conditional: a condition, and the value or color it selects. */ diff --git a/npm/src/shot.ts b/npm/src/shot.ts index 544f4d31..4a6be6ae 100644 --- a/npm/src/shot.ts +++ b/npm/src/shot.ts @@ -1,18 +1,8 @@ /** Pictures of views: images to encode as files, and H.264 video written as frames are drawn. */ import type * as bindings from '../pkg/latkit.js'; -import { placed, type Panel } from './canvas.js'; -import { - hold, - inner, - release, - thrown, - waited, - wasm, - type LatkitError, - type Wait, -} from './internal.js'; -import { handleOf } from './view.js'; +import { encoded, type Panel } from './canvas.js'; +import { Handle, inner, thrown, waited, wasm, type LatkitError, type Wait } from './internal.js'; /** Options for a {@link Shot}. */ export interface ShotOptions { @@ -51,10 +41,13 @@ export class Shot { /** How the picture is taken, and in `layout`, the panel each view is drawn into. */ readonly options: ShotOptions & { layout?: readonly Panel[] }; - /** Creates a shot of `size`, `[width, height]` in pixels, of the views in `options.layout`. */ + /** Creates a shot of `size`, `[width, height]` in pixels. */ constructor( size: readonly [number, number], - options: ShotOptions & { layout?: readonly Panel[] } = {}, + options: ShotOptions & { + /** The views drawn, each in its panel of the picture. */ + layout?: readonly Panel[]; + } = {}, ) { this.size = size; this.options = options; @@ -62,70 +55,52 @@ export class Shot { /** * Draws the picture once every view is complete, as `capture` says, and resolves to the - * {@link Image}. Rejects with an `invalid` error for invalid options or a view in two panels, and - * with the first error a view reports while drawing. + * {@link Image}. Rejects with the first error a view reports while drawing. */ async image(wait: Wait = {}): Promise { - const taken = wasm().image(this.#json(this.size), this.#views()); - const image = await waited(taken, wait, 'taking a picture'); - return Image.made(image); + const taken = wasm().image(this.#json(), ...encoded(this.options.layout ?? [])); + return Image.made(await waited(taken, wait, 'taking a picture')); } /** - * Starts a video of this shot: H.264 in MP4 at `options.fps` frames per second, written to - * `options.output` as frames are drawn. `options.bitrate` is in bits per second, about 0.15 bits - * per pixel per frame by default. The width is rounded down to a multiple of 4 and the height to a - * multiple of 2, as the encoder requires. Canvases showing the views keep their last frame until - * the video finishes or aborts. Aborting `options.signal` aborts the video and its output; frames - * have no other time limit. + * Starts a video of this shot: H.264 in MP4, written to `options.output` as frames are drawn. The + * width is rounded down to a multiple of 4 and the height to an even number, as the encoder + * requires. Canvases showing the views keep their last frame until the video finishes or aborts. + * Aborting `options.signal` aborts the video and its output; frames have no other time limit. */ video( - options: { fps: number; output: WritableStream; bitrate?: number } & Wait, + options: { + /** Frames per second. */ + fps: number; + /** Where the file's bytes go, locked until the video finishes or aborts. */ + output: WritableStream; + /** Bits per second; about 0.15 bits per pixel per frame by default. */ + bitrate?: number; + } & Wait, ): Video { - const { fps, output, bitrate, signal } = options; - signal?.throwIfAborted(); - const writer = output.getWriter(); + options.signal?.throwIfAborted(); + const writer = options.output.getWriter(); try { - const [width, height] = this.size; - const blocks = [ - Math.max(4, width - (width % 4)), - Math.max(2, height - (height % 2)), - ] as const; - return Video.made( - wasm().video(this.#json(blocks), this.#views(), fps, bitrate), - writer, - fps, - signal, - ); + const layout = encoded(this.options.layout ?? []); + const video = wasm().video(this.#json(), ...layout, options.fps, options.bitrate); + return Video.made(video, writer, options.fps, options.signal); } catch (error) { writer.releaseLock(); throw error; } } - /** The shot as JSON for the bindings, at `size` pixels, with times in seconds. */ - #json(size: readonly [number, number]): string { - const { - scale = 1, - transparent = false, - capture = 'still', - elapsed, - layout = [], - } = this.options; - const placing = Array.from(placed(layout)); - const panels = Array.from({ length: layout.length }, (_, i) => placing.slice(i * 4, i * 4 + 4)); - return JSON.stringify({ size: [...size], scale, panels, transparent, capture, elapsed }); - } - - #views(): bindings.Handle[] { - return (this.options.layout ?? []).map(({ view }) => handleOf(view)); + /** The shot as JSON for the bindings, with times in seconds; the layout crosses on its own. */ + #json(): string { + const { scale = 1, transparent = false, capture = 'still', elapsed } = this.options; + return JSON.stringify({ size: [...this.size], scale, transparent, capture, elapsed }); } } /** A picture's RGBA pixels, with straight alpha, rows top to bottom. */ -export class Image { +export class Image extends Handle { private constructor(image: bindings.Image) { - hold(this, image); + super(image, 'Image'); } /** @internal Wraps a bindings image. */ @@ -135,48 +110,35 @@ export class Image { /** Width in pixels. */ get width(): number { - return pixels(this).width; + return inner(this).width; } /** Height in pixels. */ get height(): number { - return pixels(this).height; + return inner(this).height; } /** A copy of the pixels: 4 bytes per pixel, `width * height * 4` bytes in all. */ get rgba(): Uint8Array { - return pixels(this).rgba; + return inner(this).rgba; } /** - * Encodes the image as a file, in a worker. PNG is lossless with alpha. JPEG uses - * `options.quality` from 1 to 100 (90 by default) and drops alpha. WebP is lossless with alpha - * and smaller than PNG. Rejects with an `invalid` error for a quality outside 1 to 100 or an - * unknown format. + * Encodes the image as a file, in a worker. PNG is lossless with alpha. JPEG drops alpha. WebP + * is lossless with alpha and smaller than PNG. */ - encode(format: ImageFormat, options: { quality?: number } & Wait = {}): Promise { - return waited( - pixels(this).encode(format, options.quality), - options, - 'encoding an image', - ); - } - - /** Frees the pixels. Other methods throw afterwards. */ - destroy(): void { - release(this)?.free(); - } - - /** Calls {@link Image.destroy}, so a `using` declaration destroys it when its block ends. */ - [Symbol.dispose](): void { - this.destroy(); + encode( + format: ImageFormat, + options: { + /** JPEG quality, from 1 (smallest) to 100 (sharpest); 90 by default. */ + quality?: number; + } & Wait = {}, + ): Promise { + const encoding = inner(this).encode(format, options.quality); + return waited(encoding, options, 'encoding an image'); } } -function pixels(owner: Image): bindings.Image { - return inner(owner, 'Image'); -} - /** An image file format for {@link Image.encode}. */ export type ImageFormat = 'png' | 'jpeg' | 'webp'; @@ -185,8 +147,11 @@ export type ImageFormat = 'png' | 'jpeg' | 'webp'; * {@link Video.frame} or {@link Video.record}, then call {@link Video.finish}. */ export class Video { + #video: bindings.Video | undefined; #state: 'open' | 'finishing' | 'aborting' | 'closed' = 'open'; + /** The operation in progress, and the bindings task it waits on. */ #operation?: Promise; + #task?: bindings.Task; #finished?: Promise; #aborted?: Promise; #reason: unknown; @@ -201,13 +166,11 @@ export class Video { fps: number, signal?: AbortSignal, ) { - hold(this, video); + this.#video = video; this.#writer = writer; this.#fps = fps; if (signal) { - const abort = () => { - void this.abort(signal.reason); - }; + const abort = () => void this.abort(signal.reason); signal.addEventListener('abort', abort, { once: true }); this.#removeAbort = () => signal.removeEventListener('abort', abort); if (signal.aborted) abort(); @@ -224,25 +187,6 @@ export class Video { return new Video(video, writer, fps, signal); } - #check(): bindings.Video { - const video = - this.#state === 'aborting' || this.#state === 'closed' ? undefined : encoder(this); - if (!video) throw this.#ended(); - return video; - } - - /** - * The error for a call after the video ended: `cancelled` with the abort reason as its `cause` - * once aborted, else `invalid`. - */ - #ended(): LatkitError { - return this.#aborted - ? Object.assign(thrown({ code: 'cancelled', message: 'this Video was aborted' }), { - cause: this.#reason, - }) - : thrown({ code: 'invalid', message: 'a Video draws no frames after finish()' }); - } - /** The time in seconds at which the next frame shows. Still readable after the video ends. */ get time(): number { return this.frames / this.#fps; @@ -253,7 +197,7 @@ export class Video { * the video ends. */ get frames(): number { - return encoder(this)?.frames ?? this.#frames; + return this.#video?.frames ?? this.#frames; } /** @@ -268,20 +212,12 @@ export class Video { return Promise.reject( thrown({ code: 'invalid', message: 'a Video draws one frame at a time' }), ); - const operation = (async () => { - const bytes = (await this.#check().frame()) as Uint8Array; - this.#frames = this.#check().frames; + return this.#run(async () => { + const bytes = await this.#await(this.#open().frame()); + // An abort while the frame drew rejects it, even if the frame finished first. + this.#open(); if (bytes.length) await this.#writer.write(bytes); - })(); - this.#operation = operation; - return operation - .catch(async (error: unknown) => { - await this.abort(error); - throw error; - }) - .finally(() => { - if (this.#operation === operation) this.#operation = undefined; - }); + }); } /** @@ -316,23 +252,15 @@ export class Video { if (this.#state !== 'open') return Promise.reject(this.#ended()); this.#state = 'finishing'; const frame = this.#operation; - const operation = (async () => { + this.#finished = this.#run(async () => { await frame; - const bytes = (await this.#check().finish()) as Uint8Array; - this.#check(); + const bytes = await this.#await(this.#open().finish()); if (bytes.length) await this.#writer.write(bytes); - this.#check(); + this.#open(); await this.#writer.close(); - })(); - this.#operation = operation; - this.#finished = operation - .then(() => { - if (this.#state === 'finishing') this.#release(); - }) - .catch(async (error: unknown) => { - await this.abort(error); - throw error; - }); + }).then(() => { + if (this.#state === 'finishing') this.#release(); + }); return this.#finished; } @@ -347,28 +275,63 @@ export class Video { this.#state = 'aborting'; this.#reason = reason; this.#frames = this.frames; - encoder(this)?.cancel(); + this.#task?.cancel(); + this.#video?.cancel(); this.#aborted = Promise.allSettled([this.#operation, this.#writer.abort(reason)]).then(() => this.#release(), ); return this.#aborted; } + /** Runs `operation` as the one in progress. A failure aborts the video and is rethrown. */ + #run(operation: () => Promise): Promise { + const running = operation(); + this.#operation = running; + return running + .catch(async (error: unknown) => { + await this.abort(error); + throw error; + }) + .finally(() => { + if (this.#operation === running) this.#operation = undefined; + }); + } + + /** Awaits bindings task `task`, which `abort()` cancels, and frees it. */ + async #await(task: bindings.Task): Promise { + this.#task = task; + try { + return (await task.promise) as Uint8Array; + } finally { + this.#task = undefined; + task.free(); + } + } + + /** The bindings video while the video is open or finishing; else throws why it ended. */ + #open(): bindings.Video { + const open = this.#state === 'open' || this.#state === 'finishing'; + if (!open || !this.#video) throw this.#ended(); + return this.#video; + } + + /** + * The error for a call after the video ended: `cancelled` with the abort reason as its `cause` + * once aborted, else `invalid`. + */ + #ended(): LatkitError { + return this.#aborted + ? thrown({ code: 'cancelled', message: 'this Video was aborted', cause: this.#reason }) + : thrown({ code: 'invalid', message: 'a Video draws no frames after finish()' }); + } + #release(): void { this.#frames = this.frames; - release(this)?.free(); + this.#video?.free(); + this.#video = undefined; this.#removeAbort?.(); this.#removeAbort = undefined; this.#writer.releaseLock(); this.#state = 'closed'; } } - -/** The video's bindings object, or `undefined` once it is released. */ -function encoder(owner: Video): bindings.Video | undefined { - try { - return inner(owner, 'Video'); - } catch { - return undefined; - } -} diff --git a/npm/src/view.ts b/npm/src/view.ts index 08fc8a15..8164857d 100644 --- a/npm/src/view.ts +++ b/npm/src/view.ts @@ -5,9 +5,9 @@ */ import type * as bindings from '../pkg/latkit.js'; -import { snapshotOf, sourceOf, type Snapshot, type Source } from './data.js'; +import { Snapshot, sourceOf, type Source } from './data.js'; import type { Camera, Fit, Row, Rows, Scene } from './inputs.js'; -import { hold, inner, release, thrown, waited, type LatkitError, type Wait } from './internal.js'; +import { Handle, inner, thrown, waited, type LatkitError, type Wait } from './internal.js'; import type { Failure, Hit, Stats, ViewEvent, Waiting } from './outputs.js'; import { Patch } from './patch.js'; @@ -22,12 +22,6 @@ export type ViewEvents = { [E in ViewEvent as E['type']]: E extends { type: 'error' } ? LatkitError : E['detail']; }; -/** Options for `Gpu.view()`. */ -export interface ViewOptions { - /** The datasets the scene draws; see {@link View.sources}. */ - sources?: readonly Source[]; -} - /** CSS pixels kept clear at the top, right, bottom, and left. */ export type Insets = [top: number, right: number, bottom: number, left: number]; @@ -42,19 +36,23 @@ export type Position = [x: number, y: number, elevation: number]; /** * A scene drawn from its sources; created by `Gpu.view()`. A canvas shows it and passes it input, - * and a `Shot` draws it into pictures. + * and a `Shot` draws it into pictures. Destroying it rejects its unanswered queries; a canvas + * showing it keeps its last frame. */ -export class View { +export class View extends Handle { #listeners = new Map void>>(); #scene: Scene; #sources: readonly Source[] = []; #released: () => void; private constructor(device: bindings.Gpu, scene: Scene, released: () => void) { - hold( - this, - device.view(JSON.stringify(scene), (json: string) => this.#tell(json)), - ); + // The view sends events once it exists, so the callback finds it through `told`. + const told: { view?: View } = {}; + const tell = (json: string) => { + if (told.view) told.view.#tell(json); + }; + super(device.view(JSON.stringify(scene), tell), 'View'); + told.view = this; this.#scene = scene; this.#released = released; } @@ -66,8 +64,7 @@ export class View { /** * The scene drawn. Marks that match a mark of the previous scene by kind, dataset, table, and - * name keep their state and ease to their new options. Setting an invalid scene throws an - * `invalid` error and keeps the current one. + * name keep their state and ease to their new options. An invalid scene leaves the current one. */ get scene(): Scene { return this.#scene; @@ -79,11 +76,10 @@ export class View { } /** - * The datasets the view draws, matched to marks by dataset name. The view draws a `Data` at its - * latest version, from the frame after each patch; a `Snapshot` unchanged; and `Tiles` as the - * pieces it needs. A mark whose dataset no longer fits it draws nothing; it is listed in - * `failures`, and the next frame emits an `error` event. Setting two sources with the same name - * throws an `invalid` error. + * The datasets the view draws, matched to marks by dataset name, which must be unique. The view + * draws a `Data` at its latest version, from the frame after each patch; a `Snapshot` unchanged; + * and `Tiles` as the pieces it needs. A mark whose dataset no longer fits it draws nothing; it is + * listed in `failures`, and the next frame emits an `error` event. */ get sources(): readonly Source[] { return this.#sources; @@ -231,10 +227,16 @@ export class View { } /** - * Resolves, once a frame draws the view, to every row of a pickable mark within `options.radius` - * CSS pixels (0 by default) of `point`, nearest first. + * Resolves, once a frame draws the view, to every row of a pickable mark near `point`, nearest + * first. */ - async pick(point: Point, options: { radius?: number } & Wait = {}): Promise { + async pick( + point: Point, + options: { + /** How near, in CSS pixels; 0 by default. */ + radius?: number; + } & Wait = {}, + ): Promise { const hits = view(this).pick(Float32Array.from(point), options.radius ?? 0); return JSON.parse(await waited(hits, options, 'picking')) as Hit[]; } @@ -248,11 +250,9 @@ export class View { } /** - * Runs the scene's layout again for every vertex row without a position: a diagram's blocks, or - * points on a plane or the earth. Otherwise a layout keeps the places it gave, so rows added - * later are placed around the rest. Resolves, once the layout finishes, to one patch per dataset - * that writes each laid-out row's place into its mark's position fields. Applying the patches - * makes the places part of the data. Marks whose position is not read from fields are left out. + * Lays out every vertex row without a position again, and resolves to one patch per dataset that + * writes each row's place into its mark's position fields; applied, the places become part of + * the data. Marks whose position is not read from fields are left out. * * ```ts * for (const patch of await view.arrange()) if (patch.data === grid.name) grid.apply(patch); @@ -270,7 +270,7 @@ export class View { */ snapshot(name: string): Snapshot | undefined { const snapshot = view(this).snapshot(name); - return snapshot ? snapshotOf(snapshot) : undefined; + return snapshot ? Snapshot.made(snapshot) : undefined; } /** @@ -296,27 +296,16 @@ export class View { } } - /** - * Frees the view and rejects its unanswered queries. A canvas showing it keeps its last frame. - * Calling it again does nothing; other methods throw afterwards. - */ - destroy(): void { - const freed = release(this); - if (!freed) return; + /** @internal Drops the listeners and tells the GPU the view is gone. */ + protected override freeing(): void { for (const listeners of this.#listeners.values()) listeners.clear(); this.#listeners.clear(); - freed.free(); this.#released(); } - - /** Calls {@link View.destroy}, so a `using` declaration destroys it when its block ends. */ - [Symbol.dispose](): void { - this.destroy(); - } } function view(owner: View): bindings.View { - return inner(owner, 'View'); + return inner(owner); } /** @internal Returns a new bindings handle to the view, for a canvas to show or a shot to draw. */ diff --git a/npm/test/data.test.mjs b/npm/test/data.test.mjs index 85d497d3..9928c814 100644 --- a/npm/test/data.test.mjs +++ b/npm/test/data.test.mjs @@ -270,6 +270,42 @@ test('failed fetches are io errors, and aborting one rejects as cancelled', asyn await assert.rejects(reading, { code: 'cancelled' }); }); +test('blobs and streams read within their signal, fail as io, and cancel when stopped', async () => { + const bytes = new Patch().axis('t', 's').toBytes(); + const length = async (source) => (await Patch.read(source)).toBytes().length; + assert.equal(await length(new Blob([bytes])), bytes.length); + assert.equal( + await length( + (async function* () { + yield bytes; + })(), + ), + bytes.length, + ); + const stopped = AbortSignal.abort('stop'); + await assert.rejects(Patch.read(new Blob([bytes]), { signal: stopped }), { code: 'cancelled' }); + const failing = () => + new ReadableStream({ + pull() { + throw new Error('the socket closed'); + }, + }); + const closed = (error) => error.code === 'io' && error.cause.message === 'the socket closed'; + await assert.rejects(Patch.read(failing()), closed); + await assert.rejects(async () => { + for await (const patch of Patch.stream(failing())) void patch; + }, closed); + const cancelled = []; + const stalled = () => + new ReadableStream({ pull: () => new Promise(() => {}), cancel: (why) => cancelled.push(why) }); + const controller = new AbortController(); + const reading = Patch.read(stalled(), { signal: controller.signal }); + controller.abort('stop'); + await assert.rejects(reading, { code: 'cancelled', cause: 'stop' }); + await assert.rejects(Data.read(stalled(), { signal: stopped }), { code: 'cancelled' }); + assert.deepEqual(cancelled, ['stop', 'stop'], 'a stream stopped mid-read or before is cancelled'); +}); + test('Data.read detects JSON, latkit bytes, and GeoJSON by their first bytes', async () => { const document = { name: 'grid', diff --git a/npm/test/jobs.test.mjs b/npm/test/jobs.test.mjs index 6bfadf3f..8523d6a5 100644 --- a/npm/test/jobs.test.mjs +++ b/npm/test/jobs.test.mjs @@ -3,11 +3,8 @@ import path from 'node:path'; import { test } from 'node:test'; import { pathToFileURL } from 'node:url'; -const { jobs } = await import( - process.env.LATKIT_TEST_PACKAGE - ? pathToFileURL(path.join(process.env.LATKIT_TEST_PACKAGE, 'dist/jobs.js')).href - : new URL('../src/jobs.ts', import.meta.url).href -); +const directory = process.env.LATKIT_TEST_PACKAGE ?? path.resolve(import.meta.dirname, '..'); +const { jobs } = await import(pathToFileURL(path.join(directory, 'dist/jobs.js')).href); const module = new WebAssembly.Module(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0])); diff --git a/npm/test/video.test.mjs b/npm/test/video.test.mjs index 5ae941e7..6568ecaa 100644 --- a/npm/test/video.test.mjs +++ b/npm/test/video.test.mjs @@ -6,7 +6,17 @@ import { test } from 'node:test'; const directory = process.env.LATKIT_TEST_PACKAGE ?? path.resolve(import.meta.dirname, '..'); const { Video, Shot } = await import(pathToFileURL(path.join(directory, 'dist/shot.js')).href); -function recording(sink = {}, options = {}) { +/** A bindings task of `promise`. Cancelling it rejects the promise, as the bindings' tasks do. */ +function task(promise) { + let cancel; + const cancelled = new Promise((_, reject) => { + cancel = () => reject(new Error('cancelled')); + }); + return { promise: Promise.race([promise, cancelled]), cancel, free() {} }; +} + +/** A video over fake bindings, whose `frame` and `finish` promises can be replaced. */ +function recording(sink = {}, { frame, finish } = {}) { const calls = { free: 0, cancel: 0, abort: 0, close: 0 }; const stream = new WritableStream({ abort() { @@ -19,12 +29,12 @@ function recording(sink = {}, options = {}) { }); const inner = { frames: 0, - async frame() { + frame() { this.frames++; - return Uint8Array.of(1); + return task(frame?.() ?? Promise.resolve(Uint8Array.of(1))); }, - async finish() { - return Uint8Array.of(2); + finish() { + return task(finish?.() ?? Promise.resolve(Uint8Array.of(2))); }, free() { calls.free++; @@ -32,7 +42,6 @@ function recording(sink = {}, options = {}) { cancel() { calls.cancel++; }, - ...options, }; return { video: Video.made(inner, stream.getWriter(), 30), stream, calls, inner }; } @@ -85,20 +94,7 @@ test('write failures close the encoder and release the output lock', async () => }); test('abort interrupts a pending finish and is idempotent', async () => { - let rejectFinish; - const { video, stream, calls } = recording( - {}, - { - finish: () => - new Promise((_, reject) => { - rejectFinish = reject; - }), - cancel() { - calls.cancel++; - rejectFinish(new Error('cancelled')); - }, - }, - ); + const { video, stream, calls } = recording({}, { finish: () => new Promise(() => {}) }); const finished = video.finish(); await Promise.resolve(); const rejected = assert.rejects(finished, /cancelled/); @@ -110,6 +106,15 @@ test('abort interrupts a pending finish and is idempotent', async () => { await assert.rejects(video.frame(), { code: 'cancelled', cause: 'stop' }); }); +test('an abort while a frame draws rejects the frame, even one that finished first', async () => { + const { video, stream } = recording({}, { frame: () => Promise.resolve(new Uint8Array()) }); + const frame = video.frame(); + const aborted = video.abort('stop'); + await assert.rejects(frame, { code: 'cancelled', cause: 'stop' }); + await aborted; + assert.equal(stream.locked, false); +}); + test('concurrent frames reject without disrupting the frame already drawing', async () => { let resolveFrame; const { video } = recording( diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 0c17aa92..42e661f7 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,9 +1,15 @@ //! Repository tasks, run as `cargo xtask `. //! -//! - `check`: CI's checks in CI's order, with GPU tests required. Formats, lints, tests, documents, -//! and packages the crates, and checks that `native` stays out of browser builds. Then formats, -//! lints, type-checks, and tests the npm workspace over a fresh `wasm --dev`, and runs the -//! browser gate. +//! - `check`: every CI job's steps (`ci `), in CI's order. Locally, the crates package with +//! uncommitted work, and the browser gate judges frame budgets on the GPU. +//! - `ci `: the steps of one job of `.github/workflows/ci.yml`, which runs each job this way: +//! - `format`: Prettier over every file. CI's `format` job has no Rust toolchain, so it runs +//! `pnpm format:check` itself. +//! - `rust`: formats, lints, tests (with GPU tests required), documents, and packages the crates, +//! and checks that `native` stays out of browser builds. +//! - `native-cli`: tests the CLI's native code, which CI runs on macOS and Windows. +//! - `npm`: builds the npm package with `wasm --dev`; lints, documents, type-checks, and tests +//! it; runs the browser gate over that build; then `package` and `package-test`. //! - `browser`: the browser gate (`npm/examples/gate/gate.ts`) over a fresh `wasm --dev`. Checks //! that every shader variant's pipelines compile in Chrome within budget, and that every example //! page draws and responds to the pointer without errors or stalls. On `SwiftShader`, which Linux @@ -17,6 +23,8 @@ //! - `wasm [--dev]`: builds `latkit-js`, binds it with the `wasm-bindgen` CLI version in //! `Cargo.lock`, and compiles the npm wrapper. `--dev` builds faster, without link-time //! optimization. +//! - `bindgen-version`: prints the `wasm-bindgen` version in `Cargo.lock`, which CI installs the +//! CLI at. //! - `version`: sets the crates' version to the npm package's, as changesets set it. //! - `package`: builds an isolated, optimized npm tarball, the one `pnpm release` publishes, and //! reports its sizes and hashes. @@ -34,6 +42,7 @@ fn main() -> ExitCode { let dev = std::env::args().any(|a| a == "--dev"); let result = match task.as_str() { "check" => check(), + "ci" => ci(&std::env::args().nth(2).unwrap_or_default()), "browser" => browser(), "bench" => wasm(false).and_then(|()| { let flags: Vec = std::env::args().skip(2).collect(); @@ -42,6 +51,7 @@ fn main() -> ExitCode { run("node", &args) }), "wasm" => wasm(dev), + "bindgen-version" => bindgen_version().map(|version| println!("{version}")), "version" => version(), "package" => package(), "package-test" => package_test(), @@ -55,7 +65,7 @@ fn main() -> ExitCode { }), _ => { eprintln!( - "usage: cargo xtask " + "usage: cargo xtask |browser|bench [flags]|wasm [--dev]|bindgen-version|version|package|package-test|release|examples [--open]>" ); return ExitCode::FAILURE; } @@ -95,8 +105,29 @@ fn pnpm(args: &[&str]) -> Result { run(if cfg!(windows) { "pnpm.cmd" } else { "pnpm" }, args) } -/// Runs the checks of `.github/workflows/ci.yml` in its order, then the browser gate. +/// The jobs of `.github/workflows/ci.yml` that `ci` runs the steps of, in its order. +const JOBS: [&str; 4] = ["format", "rust", "native-cli", "npm"]; + +/// Runs every CI job's steps in CI's order. fn check() -> Result { + for job in JOBS { + ci(job)?; + } + Ok(()) +} + +/// Runs the steps of CI job `job`. +fn ci(job: &str) -> Result { + match job { + "format" => pnpm(&["format:check"]), + "rust" => rust(), + "native-cli" => native_cli(), + "npm" => npm(), + _ => Err(format!("no CI job {job:?}; the jobs are {}", JOBS.join(", "))), + } +} + +fn rust() -> Result { let gpu = [("LATKIT_REQUIRE_GPU", "1")]; cargo(&["fmt", "--all", "--check"])?; cargo(&["clippy", "--locked", "--workspace", "--all-targets", "--all-features", "--", "-D", "warnings"])?; @@ -109,27 +140,35 @@ fn check() -> Result { &["doc", "--locked", "-p", "latkit", "--no-deps", "--all-features"], &[("RUSTDOCFLAGS", "-D warnings")], )?; - // CI's tree is clean. Locally, package uncommitted work as is. - cargo(&["package", "--locked", "--allow-dirty", "-p", "latkit"])?; - cargo(&[ - "package", - "--locked", - "--allow-dirty", - "-p", - "latkit", - "--target", - "wasm32-unknown-unknown", - "--features", - "web", - ])?; - cargo(&["check", "--locked", "-p", "latkit", "--target", "wasm32-unknown-unknown", "--features", "native,web"])?; + let package = |target: &[&str]| { + let mut args = vec!["package", "--locked"]; + // CI's tree is clean. Locally, package uncommitted work as is. + if std::env::var_os("CI").is_none() { + args.push("--allow-dirty"); + } + args.extend(["-p", "latkit"]); + args.extend(target); + cargo(&args) + }; + package(&[])?; + package(&["--target", "wasm32-unknown-unknown", "--features", "web"])?; + cargo(&["check", "--locked", "-p", "latkit", "--target", "wasm32-unknown-unknown", "--features", "native,web"]) +} + +fn native_cli() -> Result { + cargo(&["test", "--locked", "--package", "latkit", "--features", "native", "--lib", "--test", "validation"])?; + cargo(&["test", "--locked", "--package", "latkit-cli"]) +} + +fn npm() -> Result { wasm(true)?; - pnpm(&["format:check"])?; pnpm(&["lint"])?; run("node", &["npm/docs.mjs", "--check"])?; pnpm(&["typecheck"])?; pnpm(&["test"])?; - browser_gate() + browser_gate()?; + package()?; + package_test() } /// Builds the wasm package, then runs the browser gate over the shaders the build linked. @@ -174,8 +213,7 @@ fn wasm(dev: bool) -> Result { fn build_wasm(dev: bool, output: &str) -> Result { let profile = if dev { "wasm-dev" } else { "wasm" }; cargo(&["build", "--locked", "-p", "latkit-js", "--profile", profile, "--target", "wasm32-unknown-unknown"])?; - let lock = std::fs::read_to_string(root().join("Cargo.lock")).map_err(|e| format!("Cargo.lock: {e}"))?; - let version = locked_version(&lock, "wasm-bindgen").ok_or("wasm-bindgen is not in Cargo.lock")?; + let version = bindgen_version()?; let installed = Command::new("wasm-bindgen").arg("--version").output().ok(); let installed = installed.and_then(|o| String::from_utf8(o.stdout).ok()).unwrap_or_default(); if installed.split_whitespace().nth(1) != Some(version.as_str()) { @@ -244,6 +282,12 @@ fn version() -> Result { cargo(&["update", "--workspace", "--offline"]) } +/// The `wasm-bindgen` version in `Cargo.lock`, which the `wasm-bindgen` CLI must match. +fn bindgen_version() -> Result { + let lock = std::fs::read_to_string(root().join("Cargo.lock")).map_err(|e| format!("Cargo.lock: {e}"))?; + locked_version(&lock, "wasm-bindgen").ok_or_else(|| "wasm-bindgen is not in Cargo.lock".to_owned()) +} + /// The version of `package` in a `Cargo.lock`. fn locked_version(lock: &str, package: &str) -> Option { let mut lines = lock.lines(); From d1116060e59c58c8dcb6cbbfe11bd9709c76b3ab Mon Sep 17 00:00:00 2001 From: Luke Lowery Date: Tue, 27 Oct 2026 02:52:56 -0500 Subject: [PATCH 10/14] remove some flaky features --- .changeset/camera.md | 11 + .changeset/labels.md | 10 + .changeset/sources.md | 3 +- .changeset/tiles.md | 11 + .changeset/values.md | 11 + README.md | 6 +- benchmark/judge.ts | 3 +- benchmark/page/scenarios.ts | 15 +- crates/latkit-cli/README.md | 2 +- .../examples/branch-opening/render.py | 22 +- .../latkit-cli/examples/bus-fault/render.py | 10 +- .../examples/wecc240-fault/render.py | 21 +- crates/latkit-cli/src/args.rs | 4 +- crates/latkit-cli/tests/cli.rs | 9 +- crates/latkit/build/shaders.rs | 3 +- crates/latkit/shaders/compute/candidates.wesl | 77 -- crates/latkit/shaders/compute/labels.wesl | 253 ---- crates/latkit/shaders/compute/texts.wesl | 72 ++ crates/latkit/shaders/draw/backdrop.wesl | 12 +- crates/latkit/shaders/draw/glyphs.wesl | 28 +- crates/latkit/shaders/lib/label.wesl | 8 +- crates/latkit/shaders/lib/placement.wesl | 40 - crates/latkit/shaders/lib/types.wesl | 56 +- crates/latkit/src/data/tests.rs | 6 +- crates/latkit/src/derive/mod.rs | 4 +- crates/latkit/src/derive/order.rs | 34 +- crates/latkit/src/derive/pyramid.rs | 33 +- crates/latkit/src/derive/shapes.rs | 3 +- crates/latkit/src/derive/sight.rs | 112 +- crates/latkit/src/derive/tiled.rs | 19 +- crates/latkit/src/expr/compile.rs | 36 +- crates/latkit/src/expr/cpu.rs | 21 +- crates/latkit/src/expr/form.rs | 557 +++++---- crates/latkit/src/expr/mod.rs | 67 +- crates/latkit/src/gpu/atlas.rs | 79 +- crates/latkit/src/gpu/mod.rs | 2 +- crates/latkit/src/gpu/work.rs | 4 +- crates/latkit/src/host/job.rs | 6 +- crates/latkit/src/layout/layered.rs | 83 +- crates/latkit/src/lib.rs | 2 +- crates/latkit/src/scene/areas.rs | 2 +- crates/latkit/src/scene/controls.rs | 9 +- crates/latkit/src/scene/edges.rs | 2 +- crates/latkit/src/scene/faces.rs | 4 +- crates/latkit/src/scene/label.rs | 60 +- crates/latkit/src/scene/layout.rs | 8 +- crates/latkit/src/scene/lines.rs | 2 +- crates/latkit/src/scene/mark.rs | 60 +- crates/latkit/src/scene/mod.rs | 5 +- crates/latkit/src/scene/space.rs | 5 + crates/latkit/src/scene/tests.rs | 52 +- crates/latkit/src/scene/vertices.rs | 12 +- crates/latkit/src/sheet/mod.rs | 8 +- crates/latkit/src/shot.rs | 2 +- crates/latkit/src/tiles/mod.rs | 4 +- crates/latkit/src/tiles/sets.rs | 25 +- crates/latkit/src/tiles/vector.rs | 307 +++-- crates/latkit/src/view/camera.rs | 216 +++- crates/latkit/src/view/drawing.rs | 43 +- crates/latkit/src/view/event.rs | 8 +- crates/latkit/src/view/fit.rs | 8 +- crates/latkit/src/view/frame.rs | 22 +- crates/latkit/src/view/gestures.rs | 10 +- crates/latkit/src/view/labels.rs | 386 ++----- crates/latkit/src/view/legends.rs | 2 +- crates/latkit/src/view/marks.rs | 34 +- crates/latkit/src/view/mod.rs | 44 +- crates/latkit/src/view/plan.rs | 23 +- crates/latkit/src/view/resources.rs | 4 +- crates/latkit/src/view/rig.rs | 52 +- crates/latkit/src/view/uniforms.rs | 6 - crates/latkit/src/view/variants.rs | 45 +- crates/latkit/src/view/words.rs | 15 +- crates/latkit/tests/areas.rs | 9 +- crates/latkit/tests/gestures.rs | 28 +- crates/latkit/tests/graph.rs | 35 +- crates/latkit/tests/groups.rs | 6 +- crates/latkit/tests/labels.rs | 124 +- crates/latkit/tests/layouts.rs | 18 +- crates/latkit/tests/markers.rs | 14 +- crates/latkit/tests/picks.rs | 6 +- crates/latkit/tests/render.rs | 88 +- crates/latkit/tests/scale.rs | 84 +- crates/latkit/tests/shots.rs | 23 +- crates/latkit/tests/tiles.rs | 9 +- crates/latkit/tests/transitions.rs | 6 +- crates/latkit/tests/validation.rs | 48 +- crates/latkit/tests/videos.rs | 16 +- docs/cli.md | 2 +- docs/examples/texas.md | 2 +- docs/gallery/bloom.ts | 6 +- docs/gallery/clifford.ts | 4 +- docs/gallery/currents.ts | 2 +- docs/gallery/diagram.ts | 11 +- docs/gallery/frequency.ts | 4 +- docs/gallery/globe.ts | 18 +- docs/gallery/grid.ts | 8 +- docs/gallery/helios.ts | 8 +- docs/gallery/opening.ts | 32 +- docs/gallery/terrain.ts | 6 +- docs/gallery/texas.ts | 28 +- docs/gallery/tides.ts | 2 +- docs/gallery/torus.ts | 6 +- docs/guide.md | 30 +- docs/performance.md | 6 +- docs/reference/api.md | 187 +-- docs/reference/cli.json | 2 +- docs/reference/cli.md | 2 +- docs/reference/types.md | 303 +++-- npm/README.md | 2 +- npm/docs.mjs | 26 +- npm/examples/src/examples/colormaps/index.ts | 15 +- npm/examples/src/examples/diagram/index.ts | 20 +- .../src/examples/linked-views/index.ts | 2 +- npm/examples/src/examples/live-data/index.ts | 4 +- .../src/examples/networks/cases.test.ts | 2 +- npm/examples/src/examples/networks/index.ts | 47 +- npm/examples/src/examples/networks/maps.ts | 12 +- npm/examples/src/examples/power-flow/index.ts | 7 +- npm/examples/src/examples/soft-body/index.ts | 8 +- npm/schema.json | 1024 +++++++++++------ npm/schema.mjs | 2 +- npm/src/data.ts | 3 +- npm/src/inputs.ts | 294 +++-- npm/src/outputs.ts | 18 +- npm/src/scene.ts | 230 ++-- npm/src/shot.ts | 2 +- npm/src/view.ts | 10 +- npm/test/consumer/main.ts | 4 +- npm/test/consumer/quickstart.ts | 2 +- npm/test/data.test.mjs | 2 +- npm/test/load.test.mjs | 2 +- npm/test/scenes.js | 40 +- 133 files changed, 3259 insertions(+), 2942 deletions(-) create mode 100644 .changeset/camera.md create mode 100644 .changeset/labels.md create mode 100644 .changeset/tiles.md create mode 100644 .changeset/values.md delete mode 100644 crates/latkit/shaders/compute/candidates.wesl delete mode 100644 crates/latkit/shaders/compute/labels.wesl create mode 100644 crates/latkit/shaders/compute/texts.wesl delete mode 100644 crates/latkit/shaders/lib/placement.wesl diff --git a/.changeset/camera.md b/.changeset/camera.md new file mode 100644 index 00000000..5890574c --- /dev/null +++ b/.changeset/camera.md @@ -0,0 +1,11 @@ +--- +'latkit': minor +--- + +A flat map is one world, and every camera move keeps within the camera's reach. + +- Changed: a flat map's ground ends at the antimeridian, as it ends at Web Mercator's latitude limit. Its camera zooms out only until the whole world fits inside the insets, and pans only until the world's edges meet the view's. A globe's camera zooms out until the globe spans a third of the view's shorter side. +- Changed: setting the camera, flights, fits, orbits, and every gesture keep the camera within reach, which fits narrow to what they framed, and its pitch within 75°. When a resize, new insets, or a new space moves the camera back within reach, a `camera` event reports it. +- Changed: a flight on a flat map eases across the world instead of the short way over the antimeridian; on a globe, longitude still turns the short way. +- Fixed: panning a flat map no longer wraps its center across the antimeridian, which moved every row a world away. +- Fixed: a scene with another kind of space drops the old space's fits and the camera motion under way. diff --git a/.changeset/labels.md b/.changeset/labels.md new file mode 100644 index 00000000..fc32c26e --- /dev/null +++ b/.changeset/labels.md @@ -0,0 +1,10 @@ +--- +'latkit': minor +--- + +A label depends on its own row alone: no placement, no fades, and no label hides another. + +- Added: a label's `text` takes a `when` whose values are its text field or `null`. A row whose text is missing has no label, so `when(zoom().ge(6)).then(field('name')).otherwise(null)` labels rows from zoom 6 on. +- Changed: a label shows wherever its row shows, its text is set, and it fits the room its row gives, in every space as in a diagram. Labels move with the camera in the frame it moves, keep showing while their mark gains texts, and never inherit another row's state as tiles change. +- Changed: `Waiting`'s `labels` is label texts being read back or not yet set. +- Removed: a label's `priority` and `max`, and label fades. A mark's `priority` still orders its drawing and thinning. diff --git a/.changeset/sources.md b/.changeset/sources.md index ba55f8fb..a3077469 100644 --- a/.changeset/sources.md +++ b/.changeset/sources.md @@ -8,7 +8,6 @@ Views draw from sources, every wait can be cancelled, colors and lengths are typ - Added: `load({ wasm, worker })` loads the WebAssembly; using the library before it finishes throws `unloaded`. Every class and builder is exported from `latkit`. - Added: every wait takes `{ signal }` and times out after 30 seconds by default: picks, `screenOf`, `arrange`, pictures, encodings, videos, `gpu.done()`, and `gpu.landed()`. A timeout rejects with `timeout`; an abort or a destroyed view rejects with `cancelled` and drops the work in progress. - Added: `gpu.canvas(element, { layout: [{ view, at }] })` and `canvas.layout`; `canvas.shot()` captures exactly what a canvas shows; `new Shot(size, { scale, layout, transparent, capture, elapsed })`, where `capture: 'now'` takes views as they are; `shot.video({ fps, output, bitrate, signal })`. -- Added: typed colors. In a color option a string is a CSS color, `field(name)` reads a field, and `color(css)`, colormaps, and `when(...)` build colors (`Paint`, `PaintInput`). - Added: `px(n)` and `units(n)` give a length's unit. Lengths default to CSS pixels on planes, the earth, and plots, and to position units in diagrams. A stroke in pixels keeps its width as a diagram zooms. - Added: the theme in groups: `text`, `vertex`, `edge`, `line`, `area`, `focus`, `flow`, `diagram`, `earth`, `plot`. A comet's `flow.spacing` and `flow.length` take `{ px }` or `{ units }`. `diagram.outline`, `corner`, `portSize`, and `bend` style blocks, groups, ports, and wire bends; `shadow: true` on blocks and groups casts a soft shadow `diagram.shadow.lift` below them. - Added: `Gpu.resident` and `Gpu.uploading`, the bytes on the GPU and waiting to upload across all its views; `Work.packed`, `Work.resolved`, `Work.cleared`, and `Work.uploads`. @@ -19,7 +18,7 @@ Views draw from sources, every wait can be cancelled, colors and lengths are typ - Changed: `view.scene`, `view.selection`, `view.screen(position)`, `view.position(point)`, `view.screenOf(row)`, `view.waiting`, and the `hovered` and `selection` events replace the methods and events removed below. - Changed: every error the package throws is a `LatkitError` with a `code`. A failed fetch or an error status is `io`; a call on a destroyed object has the new code `destroyed`; a bad `Patch.stream` limit and a `Video` drawn out of turn are `invalid`. Errors from the video encoder and worker jobs keep their codes, and an abort during a fetch rejects as `cancelled` or `timeout`. - Changed: latkit's bytes carry no version or flags. A frame's header is the magic `LATK` and the frame's length, 12 bytes; files written by earlier versions no longer open. -- Changed: `theme.missing` is the color a colormap gives a missing value. An option given `missing()` still takes its theme value. +- Changed: `theme.missing` is the color a colormap gives a missing value. An option given a missing value (`null`) still takes its theme value. - Changed: `new Patch(dataset)` names its dataset; `Patch.read(source)` reads a patch's bytes or JSON, and `Data.read(source, { name, base })` a dataset's bytes, JSON, or GeoJSON, from a `ByteSource`: bytes, a `Blob`, a `Response`, a byte stream or async iterable, or a URL, read within the wait's `signal`; `patch.append(table, at, fields, { ids })`; `patch.frames(table, field, { level, first, at, values })`; `data.envelope({ table, field, range, spans, ids })`; `field(name).from(data, table)` reads another dataset. - Changed: a video's size rounds down to whole 4×2 blocks, in the browser and the CLI alike; a smaller size is `invalid`. - Changed: `view.screen` and `view.position` given the wrong number of coordinates throw `invalid` instead of answering `null`. diff --git a/.changeset/tiles.md b/.changeset/tiles.md new file mode 100644 index 00000000..2ba5fc27 --- /dev/null +++ b/.changeset/tiles.md @@ -0,0 +1,11 @@ +--- +'latkit': minor +--- + +A vector tile costs the rows it brings, and tiles split where they draw largest. + +- Changed: a vector tile landing appends its rows, so rows already drawn neither upload nor evaluate again. A tile that stops drawing keeps its rows and draws them again if it returns; once rows no tile draws outnumber the drawn rows by a page, they are removed at once. Until then `view.snapshot(name)` of a vector source includes them. +- Changed: a tile measures the area it covers on screen, so tiles toward the globe's edge and a tilted map's horizon stay coarse. When the tile budget binds, the tiles drawn largest split first, for vector tiles and paged tables alike. At most 128 vector tiles draw at once. +- Changed: a vector table draws each tile's rows in the order of the mark's `priority`, highest on top. +- Fixed: tiles past the antimeridian of a flat map are no longer fetched as if near. +- Fixed: two tiles landing together whose field reads as different kinds no longer fail every update. diff --git a/.changeset/values.md b/.changeset/values.md new file mode 100644 index 00000000..e7d45522 --- /dev/null +++ b/.changeset/values.md @@ -0,0 +1,11 @@ +--- +'latkit': minor +--- + +One way to write a value: `'$name'` reads a field, any other string is a CSS color, and `null` is missing, in every option and query. + +- Changed: in a scene or a query, a string `'$name'` reads field `name` and any other string is a CSS color, whatever the option; `field(name)` writes `'$name'`. A color option that computes a number or a boolean is an error that says how to map it to colors. +- Changed: an operation is an object with one key naming it: `{ add: [a, b] }`, `{ not: [a] }`, `{ is: ['$name', 'Austin'] }`. `null` is a missing value. One `when` chooses between values and colors alike. +- Changed: `.colormap()` returns `Mapped`, as `.scale()` does. +- Renamed: the layered layout's `flow` to `direction`, so `flow` means only comets. +- Removed: `{ op, args }`, `{ color }`, and `{ missing: true }`; `Paint`, `PaintInput`, `Colormapped`, `Missing`, `color()`, and `missing()`; bare strings as field names. In Rust, `Paint` and `Expr` from a string or a `Name`. diff --git a/README.md b/README.md index 704a3f3b..29626793 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ const scene: Scene = { { vertices: { of: ['grid', 'Bus'], - position: ['x', 'y'], + position: ['$x', '$y'], radius: 6, color: field('kv').colormap('viridis'), }, @@ -88,9 +88,9 @@ latkit render scene.json --data grid.json --frames frames.jsonl --fps 60 --outpu { "vertices": { "of": ["grid", "Bus"], - "position": ["x", "y"], + "position": ["$x", "$y"], "radius": 6, - "color": { "colormap": "viridis", "value": { "field": "kv" } } + "color": { "colormap": "viridis", "value": "$kv" } } } ] diff --git a/benchmark/judge.ts b/benchmark/judge.ts index 9cc585d9..fbb44691 100644 --- a/benchmark/judge.ts +++ b/benchmark/judge.ts @@ -68,7 +68,8 @@ const EXACT = new Set([ 'resident', ]); /** Factor by which other work counts may exceed the baseline. The frame a job or fetch finishes in - * changes label placements and tiles by up to about 1.7×. A per-frame blowup changes far more. */ + * changes the label texts set and the tiles by up to about 1.7×. A per-frame blowup changes far + * more. */ const STEADY = 2; /** Minimum baseline values for work held within `STEADY`: one for byte counts, one for the rest. */ const WORK_FLOOR = { bytes: 64 * 1024, count: 8 }; diff --git a/benchmark/page/scenarios.ts b/benchmark/page/scenarios.ts index 5b09c1f2..bd2ef410 100644 --- a/benchmark/page/scenarios.ts +++ b/benchmark/page/scenarios.ts @@ -91,7 +91,7 @@ const dots = (space: Space, colormap: ColormapName = 'viridis', label?: string): { vertices: { of: ['d', 'Point'], - position: ['x', 'y'], + position: ['$x', '$y'], color: field('v').colormap(colormap).domain(0, 1), ...(label ? { label: field(label) } : {}), }, @@ -108,7 +108,7 @@ const grid = (space: Space): Scene => ({ { vertices: { of: ['g', 'Bus'], - position: ['lon', 'lat'], + position: ['$lon', '$lat'], color: field('load').colormap('viridis').domain(0, 1), }, }, @@ -124,14 +124,14 @@ const plot: Scene = { const sheet: Scene = { space: 'diagram', ease: 0, - layout: { layered: { flow: 'right' } }, + layout: { layered: { direction: 'right' } }, marks: [ { edges: { of: ['d', 'Net'], route: 'orthogonal' } }, { vertices: { of: ['d', 'Block'], marker: 'block', - label: { text: 'name' }, + label: { text: '$name' }, ports: [ ['in', 'left'], ['out', { side: 'right', output: true }], @@ -163,7 +163,8 @@ const pan = (order: Order = { time: 0 }): Phase => ({ }, }); -/** Zooms two levels in and back out, which re-places labels and refines tiles. */ +/** Zooms two levels in and back out, which asks for the texts of labels coming into view and + * refines tiles. */ const zoom = (): Phase => ({ name: 'zoom', order: { time: 0 }, @@ -473,7 +474,7 @@ export const scenarios: Record = { ], }, labels: { - about: 'every vertex labeled: placement, collisions, and text layout', + about: 'every vertex labeled: the texts in view found, set, and drawn', unit: 'rows', // From 10,000 rows on, more labels show than a frame sets texts for, so a frame's CPU time // stays at the text budget however many rows are labeled. @@ -494,7 +495,7 @@ export const scenarios: Record = { ease: 0, marks: [ { edges: { of: ['d', 'Link'], ends: ['from', 'to'] } }, - { vertices: { of: ['d', 'Point'], position: ['x', 'y'] } }, + { vertices: { of: ['d', 'Point'], position: ['$x', '$y'] } }, ], }, data: { d: make.lanes(n) }, diff --git a/crates/latkit-cli/README.md b/crates/latkit-cli/README.md index 260ea8c6..ef1fb576 100644 --- a/crates/latkit-cli/README.md +++ b/crates/latkit-cli/README.md @@ -9,7 +9,7 @@ cargo install latkit-cli latkit validate scene.json -d grid.json latkit render scene.json -d grid.json -o grid.png --size 2400x1350 --scale 2 latkit render scene.json -d grid.json --frames frames.jsonl --fps 60 -o film.mp4 -latkit query grid.bin '{"of":"Branch","filter":{"op":"gt","args":["loading",90]},"limit":20}' +latkit query grid.bin '{"of":"Branch","filter":{"gt":["$loading",90]},"limit":20}' latkit tile history.bin -o history/ # time tiles a view fetches in spans: --tiles grid=history/frames.json latkit schema scene > scene.schema.json ``` diff --git a/crates/latkit-cli/examples/branch-opening/render.py b/crates/latkit-cli/examples/branch-opening/render.py index 61a1aac3..247bf82c 100644 --- a/crates/latkit-cli/examples/branch-opening/render.py +++ b/crates/latkit-cli/examples/branch-opening/render.py @@ -37,7 +37,7 @@ def write(path, value): def op(name, *args): - return {"op": name, "args": list(args)} + return {name: list(args)} def sha256(path): @@ -124,9 +124,9 @@ def append(start, end): # asinh(df / 0.02), expressed with the CLI's ordinary GPU expression operators. # Use abs/sign to avoid cancellation in ln(x + sqrt(x*x+1)) for negative x. - x = op("div", op("abs", "df"), 0.02) + x = op("div", op("abs", "$df"), 0.02) magnitude = op("ln", op("add", x, op("sqrt", op("add", op("mul", x, x), 1)))) - signed = {"when": [[op("lt", "df", 0), op("neg", magnitude)]], "otherwise": magnitude} + signed = {"when": [[op("lt", "$df", 0), op("neg", magnitude)]], "otherwise": magnitude} height_scale = HEIGHT_GAIN * 1.7 * 0.15 * max(max(lon) - min(lon), max(lat) - min(lat)) elevation = op("mul", signed, height_scale / math.asinh(0.35 / 0.02)) theme = {"background": BACKGROUND, "text": {"color": "#c9d4dc", "font": "Segoe UI, sans-serif", "size": 15}} @@ -138,17 +138,17 @@ def append(start, end): "subtitle": f"Little Rock - Newark, 765 kV / Opens at 1 s / Constant {SPEED:g}x speed", "marks": [ {"lines": {"of": ["states", "polygons"], "color": "#5c7a8f6b", "width": 0.75, - "visible": op("not", op("or", {"is": ["name", "Alaska"]}, - {"is": ["name", "Hawaii"]}))}}, - {"edges": {"of": ["grid", "Branch"], "ends": ["from", "to"], "width": "width", "visible": "in_service"}}, + "visible": op("not", op("or", {"is": ["$name", "Alaska"]}, + {"is": ["$name", "Hawaii"]}))}}, + {"edges": {"of": ["grid", "Branch"], "ends": ["from", "to"], "width": "$width", "visible": "$in_service"}}, {"edges": {"of": ["grid", "Outage"], "ends": ["from", "to"], "width": 2.5, - "color": "#ffde86", "dash": "opened", - "label": {"text": "status", "size": 13, "priority": 100}}}, - {"vertices": {"of": ["grid", "Bus"], "position": ["lon", "lat"], "radius": 0.8, + "color": "#ffde86", "dash": "$opened", + "label": {"text": "$status", "size": 13}}}, + {"vertices": {"of": ["grid", "Bus"], "position": ["$lon", "$lat"], "radius": 0.8, "elevation": elevation, "color": {"colormap": {"kind": "diverging", "colors": PALETTE}, - "value": "df", "domain": [-0.2, 0.2]}, - "label": {"text": "label", "size": 14, "priority": 101}, + "value": "$df", "domain": [-0.2, 0.2]}, + "label": {"text": "$label", "size": 14}, "legend": "Bus frequency deviation / Hz"}}, ], }, diff --git a/crates/latkit-cli/examples/bus-fault/render.py b/crates/latkit-cli/examples/bus-fault/render.py index 78141bbb..c852a972 100644 --- a/crates/latkit-cli/examples/bus-fault/render.py +++ b/crates/latkit-cli/examples/bus-fault/render.py @@ -95,12 +95,12 @@ def frame(k): "subtitle": f"A fault on bus {FAULT} at 1 s, cleared at 1.15 s", "marks": [ {"lines": {"of": ["states", "polygons"], "color": "#5c7a8f6b", "width": 0.75}}, - {"edges": {"of": ["grid", "Branch"], "ends": ["from", "to"], "width": "width"}}, + {"edges": {"of": ["grid", "Branch"], "ends": ["from", "to"], "width": "$width"}}, {"vertices": { - "of": ["grid", "Bus"], "position": ["lon", "lat"], "radius": 0.8, - "elevation": {"op": "mul", "args": ["height", height_scale]}, + "of": ["grid", "Bus"], "position": ["$lon", "$lat"], "radius": 0.8, + "elevation": {"mul": ["$height", height_scale]}, "color": {"colormap": {"kind": "diverging", "colors": PALETTE}, - "value": "df", "domain": [-0.05, 0.05]}, + "value": "$df", "domain": [-0.05, 0.05]}, "legend": "Bus frequency deviation · Hz", }}, ], @@ -110,7 +110,7 @@ def frame(k): "ease": 0, "theme": {**theme, "text": {**theme["text"], "size": 13}, "plot": {"playhead": "#eff5f8"}}, "marks": [{"lines": {"of": ["trace", "Bus"], "trace": "df", "width": 1.4, - "color": {"colormap": "tableau10", "value": "group"}}}], + "color": {"colormap": "tableau10", "value": "$group"}}}], }, "map-camera.json": {"center": [-87.8, 35], "zoom": math.log2(40), "pitch": 55, "bearing": 8}, "plot-camera.json": {"x": [times[0], times[-1]], "y": [-0.05, 0.05]}, diff --git a/crates/latkit-cli/examples/wecc240-fault/render.py b/crates/latkit-cli/examples/wecc240-fault/render.py index d631253a..c4c43f5e 100644 --- a/crates/latkit-cli/examples/wecc240-fault/render.py +++ b/crates/latkit-cli/examples/wecc240-fault/render.py @@ -25,7 +25,7 @@ def write(path, value): def op(name, *args): - return {"op": name, "args": list(args)} + return {name: list(args)} def playback(): @@ -82,7 +82,6 @@ def column(name, rows): "lon": [b["extension"]["longitude"] for b in buses], "lat": [b["extension"]["latitude"] for b in buses], "label": [named.get(i, "") for i in ids], - "priority": [1000 if i == 4005 else 1 for i in ids], "is_fault": [i == 4005 for i in ids], "faulted": [i == 4005 and 1 <= frames[0] < 1.15 for i in ids], "voltage_trace": [i in voltage_ids for i in ids], @@ -106,9 +105,9 @@ def column(name, rows): write(work / "grid.json", {"name": "grid", "patch": patch}) # The original samples fit in memory for 243 buses. No interpolated cache is needed: # Latkit reads at each frame's time and preserves the duplicate event timestamps. - x = op("div", op("abs", "df"), 0.01) + x = op("div", op("abs", "$df"), 0.01) magnitude = op("ln", op("add", x, op("sqrt", op("add", op("mul", x, x), 1)))) - signed = {"when": [[op("lt", "df", 0), op("neg", magnitude)]], "otherwise": magnitude} + signed = {"when": [[op("lt", "$df", 0), op("neg", magnitude)]], "otherwise": magnitude} elevation = op("mul", signed, 1.4 / math.asinh(0.1 / 0.01)) theme = {"background": "#0a111b", "text": {"color": "#d5e1ed", "font": "Segoe UI, sans-serif", "size": 17}} map_scene = { @@ -118,12 +117,12 @@ def column(name, rows): "subtitle": "JOHN DAY / Bus 4005 / Applied 1.00 s / Cleared 1.15 s", "marks": [ {"lines": {"of": ["states", "polygons"], "color": "#58718b70", "width": 0.9}}, - {"edges": {"of": ["grid", "Branch"], "ends": ["from", "to"], "width": "width"}}, + {"edges": {"of": ["grid", "Branch"], "ends": ["from", "to"], "width": "$width"}}, {"vertices": { - "of": ["grid", "Bus"], "position": ["lon", "lat"], "elevation": elevation, - "radius": {"when": [["faulted", 8], ["is_fault", 5]], "otherwise": 2.8}, - "color": {"colormap": {"kind": "diverging", "colors": PALETTE}, "value": "df", "domain": [-0.1, 0.1]}, - "label": {"text": "label", "size": 15, "priority": "priority"}, + "of": ["grid", "Bus"], "position": ["$lon", "$lat"], "elevation": elevation, + "radius": {"when": [["$faulted", 8], ["$is_fault", 5]], "otherwise": 2.8}, + "color": {"colormap": {"kind": "diverging", "colors": PALETTE}, "value": "$df", "domain": [-0.1, 0.1]}, + "label": {"text": "$label", "size": 15}, "legend": "Bus frequency deviation / Hz", }}, ], @@ -143,9 +142,9 @@ def plot(title, table, field, visible, color): write(work / "map.json", map_scene) write(work / "rotor.json", plot(f"{rotor_names[0]} / Rotor speed deviation / Hz", - "Machine", "df", "traced", "#ffcb72")) + "Machine", "df", "$traced", "#ffcb72")) write(work / "voltage.json", plot("4005 / JOHN DAY / Voltage / p.u.", - "Bus", "vm", "voltage_trace", "#66d6ef")) + "Bus", "vm", "$voltage_trace", "#66d6ef")) write(work / "map-camera.json", camera(frames[0])) write(work / "rotor-camera.json", {"x": [0, 10], "y": [-0.05, 0.12]}) write(work / "voltage-camera.json", {"x": [0, 10], "y": [0.88, 1.12]}) diff --git a/crates/latkit-cli/src/args.rs b/crates/latkit-cli/src/args.rs index e0fe8482..4ede7dc9 100644 --- a/crates/latkit-cli/src/args.rs +++ b/crates/latkit-cli/src/args.rs @@ -51,8 +51,8 @@ pub(crate) enum Command { /// The dataset, `[NAME=]FILE` (`-` reads stdin) #[arg(value_name = "[NAME=]FILE")] data: Source, - /// The query: JSON, such as `{"of":"Branch","filter":{"op":"gt","args":["loading",90]}}`, or - /// a file of it (`-` reads stdin) + /// The query: JSON, such as `{"of":"Branch","filter":{"gt":["$loading",90]}}`, or a file of + /// it (`-` reads stdin) #[arg(value_name = "QUERY")] query: String, /// The longest the dataset may take to load, in seconds diff --git a/crates/latkit-cli/tests/cli.rs b/crates/latkit-cli/tests/cli.rs index efeb4820..1e1bc55b 100644 --- a/crates/latkit-cli/tests/cli.rs +++ b/crates/latkit-cli/tests/cli.rs @@ -8,7 +8,7 @@ use std::process::{Command, Output, Stdio}; use latkit::Image; const SCENE: &str = - r#"{"space":"plane","ease":0,"marks":[{"vertices":{"of":["grid","Bus"],"position":["x","y"],"radius":6}}]}"#; + r#"{"space":"plane","ease":0,"marks":[{"vertices":{"of":["grid","Bus"],"position":["$x","$y"],"radius":6}}]}"#; const DATA: &str = r#"{"name":"grid","patch":[{"replace":{"table":"Bus","ids":[10,20],"fields":{"x":[-10,10],"y":[0,0],"name":["Alpha","Beta"]}}}]}"#; const CAMERA: &str = r#"{"center":[0,0],"zoom":2,"pitch":0,"bearing":0}"#; @@ -197,8 +197,7 @@ fn schema_describes_every_input_and_frames() { #[test] fn query_writes_one_json_line_per_row() { let dir = fixture(); - let query = - r#"{"of":"Bus","sort":[["x","descending"]],"select":{"name":"name","twice":{"op":"mul","args":["x",2]}}}"#; + let query = r#"{"of":"Bus","sort":[["$x","descending"]],"select":{"name":"$name","twice":{"mul":["$x",2]}}}"#; let output = run(dir.path(), &["query", "grid.json", query], None); ok(&output); let lines: Vec = @@ -210,7 +209,7 @@ fn query_writes_one_json_line_per_row() { serde_json::json!({ "id": 10, "name": "Alpha", "twice": -20.0 }), ] ); - std::fs::write(dir.path().join("query.json"), r#"{"of":"Bus","filter":{"op":"lt","args":["x",0]}}"#).unwrap(); + std::fs::write(dir.path().join("query.json"), r#"{"of":"Bus","filter":{"lt":["$x",0]}}"#).unwrap(); let output = run(dir.path(), &["query", "-", "query.json"], Some(DATA.as_bytes())); ok(&output); assert_eq!(String::from_utf8(output.stdout).unwrap().lines().count(), 1); @@ -258,7 +257,7 @@ fn a_tiled_history_draws_the_same_as_the_whole_dataset() { #[test] fn a_labeled_still_renders_to_a_file_and_to_stdout() { let dir = fixture(); - let scene = r#"{"space":"diagram","layout":{"layered":{}},"title":"Grid","marks":[{"vertices":{"of":["grid","Bus"],"label":"name"}}]}"#; + let scene = r#"{"space":"diagram","layout":{"layered":{}},"title":"Grid","marks":[{"vertices":{"of":["grid","Bus"],"label":"$name"}}]}"#; std::fs::write(dir.path().join("scene.json"), scene).unwrap(); std::fs::write(dir.path().join("out.png"), b"replace me").unwrap(); let args = ["render", "scene.json", "-d", "grid.json", "-o", "out.png", "--size", "127x129", "--transparent"]; diff --git a/crates/latkit/build/shaders.rs b/crates/latkit/build/shaders.rs index 82473c8d..4d304401 100644 --- a/crates/latkit/build/shaders.rs +++ b/crates/latkit/build/shaders.rs @@ -51,8 +51,7 @@ const ENTRIES: &[Entry] = &[ Entry { module: "draw::triangles", variants: &[("main", &[])] }, Entry { module: "draw::areas", variants: &[("main", &[]), ("focus", &["focused"])] }, Entry { module: "draw::glyphs", variants: &[("main", &[]), ("ports", &["ports"])] }, - Entry { module: "compute::candidates", variants: &[("main", &["writes"])] }, - Entry { module: "compute::labels", variants: &[("main", &["writes"])] }, + Entry { module: "compute::texts", variants: &[("main", &[])] }, Entry { module: "draw::overlay", variants: &[("main", &[])] }, Entry { module: "compute::compose", variants: &[("main", &[])] }, Entry { module: "compute::fields", variants: &[("main", &[])] }, diff --git a/crates/latkit/shaders/compute/candidates.wesl b/crates/latkit/shaders/compute/candidates.wesl deleted file mode 100644 index ba501ca8..00000000 --- a/crates/latkit/shaders/compute/candidates.wesl +++ /dev/null @@ -1,77 +0,0 @@ -// The first step of label placement. It measures each labeled row's label box at its anchor, from -// its text's size (as set, or as measured until it is set). Each label gets a key: its priority, -// then whether it was placed in the previous frame, then its id. A label that arrives later still -// displaces lower-priority ones, and among equals, labels already shown keep their place. -// `compute/labels.wesl` places them. Each pipeline is made for one primitive and compiles only that -// primitive's anchors. A diagram's labels scale with its sheet and need no placement, so only this -// step runs there, to request the texts of labels in view. - -import package::lib::types::{ - VISIBLE, LABEL_CODE, LABEL_PRIORITY, CANDIDATE_BOX, CANDIDATE_KEY, CANDIDATE_STATE, WANTS, PLACED_LABEL, - STICKY, UNSET, TEXT_UNSET, SPACE_DIAGRAM, item, -}; -import package::lib::bind::{frame, eye, mark, word}; -import package::lib::state::{row_at, extra_at}; -import package::lib::read::missing_bits; -import package::lib::label::{anchor, length_px, legibility, text_at, fits, in_view}; -import package::lib::placement::{label_word, put, candidate, want, seen}; -import package::lib::drawn::drawn_row; - -@compute @workgroup_size(256) -fn candidates(@builtin(global_invocation_id) id: vec3u) { - let i = item(id); - if i >= mark.rows { - return; - } - let drawn = drawn_row(i); - let row = drawn.row; - let at = candidate(row); - let before = word(at + CANDIDATE_STATE); - let sticky = (before & PLACED_LABEL) != 0u; - let code_bits = word(extra_at(row, mark.label + LABEL_CODE)); - var next = select(0u, STICKY, sticky); - let flags = word(row_at(row) + mark.stride - 1u); - // A row whose tile is leaving gets no label. - if missing_bits(code_bits) || (flags & VISIBLE) == 0u || drawn.leaving { - put(at + CANDIDATE_STATE, next); - return; - } - let code = u32(bitcast(code_bits)); - let uv0 = label_word(mark.runs + 2u * code); - let uv1 = label_word(mark.runs + 2u * code + 1u); - let a = anchor(i); - let size = length_px(mark.label_size); - let diagram = frame.space == SPACE_DIAGRAM; - let unset = (uv0 & TEXT_UNSET) != 0u; - if (uv0 == 0u && uv1 == 0u) || (unset && diagram) { - // The atlas lacks this text, and the label cannot wait for placement to request it. Request - // it now if the label could show: legible and near the view. - let near = 0.25 * eye.size; - if a.shown && legibility(size) > 0.0 && in_view(a.at - near, a.at + near) { - want(code); - } - put(at + CANDIDATE_STATE, next); - return; - } - if unset { - next |= UNSET; - } - let t = text_at(uv0 & ~TEXT_UNSET, uv1, size, a.at, a.align); - if a.shown && in_view(t.lo, t.hi) { - // A diagram marks every label in view as seen here; elsewhere `ask` marks placed ones. - if diagram { - seen(code); - } - if fits(t.hi - t.lo, a.room) { - next |= WANTS; - } - } - put(at + CANDIDATE_BOX, bitcast(t.lo.x)); - put(at + CANDIDATE_BOX + 1u, bitcast(t.lo.y)); - put(at + CANDIDATE_BOX + 2u, bitcast(t.hi.x)); - put(at + CANDIDATE_BOX + 3u, bitcast(t.hi.y)); - let level = u32(clamp(bitcast(word(extra_at(row, mark.label + LABEL_PRIORITY))), 0.0, 255.0)); - let id_bits = (mark.label_id + row + 1u) & 0x7fffffu; - put(at + CANDIDATE_KEY, ((255u - level) << 24u) | select(0x800000u, 0u, sticky) | id_bits); - put(at + CANDIDATE_STATE, next); -} diff --git a/crates/latkit/shaders/compute/labels.wesl b/crates/latkit/shaders/compute/labels.wesl deleted file mode 100644 index 21eca13b..00000000 --- a/crates/latkit/shaders/compute/labels.wesl +++ /dev/null @@ -1,253 +0,0 @@ -// Label placement, after `compute/candidates.wesl` measures each label. Placement runs rounds of -// three steps. `claim`: each label needing room writes its key into every cell it covers, and each -// cell keeps the least key; a label drops if a placed label occupies one of its cells. `decide`: a -// label is placed if every cell it covers kept its key. `occupy`: placed labels occupy their cells. -// Cells not occupied are cleared between rounds. The result matches a sequential greedy pass by -// key. -// `ask` then requests the texts of placed labels the atlas lacks, and marks the others as seen so -// the atlas keeps them. Only shown labels get their text set, however many rows are in view. `fade` -// eases each label in or out. Placement only decides which labels show; draws place them from their -// anchors every frame. -// -// A mark that shows at most `label_max` labels keeps, among those needing room, the ones with the -// least keys, before any claims. Keys are unique, so a radix select over three digits of 11, 11, -// and 10 bits finds the greatest kept key exactly. Each digit takes a pass over the candidates and -// one over its buckets. - -import package::lib::types::{ - NONE, LABEL_CODE, CANDIDATE_BOX, CANDIDATE_KEY, CANDIDATE_STATE, CANDIDATE_ALPHA, WANTS, PLACED_LABEL, DROPPED, - UNSET, OCCUPIED, CELL, item, -}; -import package::lib::bind::{frame, mark, word}; -import package::lib::state::extra_at; -import package::lib::read::missing_bits; -import package::lib::placement::{labels, label_word, put_label, put, candidate, want, seen}; -import package::lib::drawn::held_row; - -// Buckets per digit of the select that caps labels. -const SELECT_BUCKETS: u32 = 2048u; - -// Word `k` of the select's state, after its buckets: 0, the key digits found so far; 1, how many of -// the least keys remain to keep; 2, the digit counted next (4 once fewer labels need room than the -// cap). -fn select_word(k: u32) -> u32 { - return mark.label_select + SELECT_BUCKETS + k; -} - -// Where digit `d` of a key starts, and how many bits it has. -fn digit(d: u32) -> vec2u { - switch d { - case 0u: { return vec2u(21u, 11u); } - case 1u: { return vec2u(10u, 11u); } - default: { return vec2u(0u, 10u); } - } -} - -// The held row of the drawn row invocation `id` covers, or NONE past the mark's drawn rows. -fn labeled(id: vec3u) -> u32 { - let i = item(id); - return select(NONE, held_row(i), i < mark.rows); -} - -fn wanting(row: u32) -> bool { - return (word(candidate(row) + CANDIDATE_STATE) & WANTS) != 0u; -} - -@compute @workgroup_size(256) -fn select_start(@builtin(global_invocation_id) id: vec3u) { - let i = item(id); - if mark.label_max == NONE { - return; - } - if i < SELECT_BUCKETS { - put_label(mark.label_select + i, 0u); - } - if i == 0u { - put_label(select_word(0u), 0u); - put_label(select_word(1u), mark.label_max); - put_label(select_word(2u), 0u); - } -} - -@compute @workgroup_size(256) -fn select_count(@builtin(global_invocation_id) id: vec3u) { - let row = labeled(id); - let d = label_word(select_word(2u)); - if row == NONE || mark.label_max == NONE || d >= 3u || !wanting(row) { - return; - } - let key = word(candidate(row) + CANDIDATE_KEY); - let at = digit(d); - // Only keys whose higher digits match those found so far count. - let above = at.x + at.y; - if above < 32u && (key >> above) != label_word(select_word(0u)) { - return; - } - atomicAdd(&labels[mark.label_select + ((key >> at.x) & (SELECT_BUCKETS - 1u))], 1u); -} - -// Finds the bucket holding the greatest key to keep, and clears the buckets for the next digit. -@compute @workgroup_size(1) -fn select_find() { - let d = label_word(select_word(2u)); - // A cap of 0 keeps no label: `select_cut` drops them all. - if mark.label_max == NONE || mark.label_max == 0u || d >= 3u { - return; - } - var left = label_word(select_word(1u)); - var found = SELECT_BUCKETS; - for (var b = 0u; b < SELECT_BUCKETS; b++) { - let n = label_word(mark.label_select + b); - put_label(mark.label_select + b, 0u); - if found == SELECT_BUCKETS { - if n >= left { - found = b; - } else { - left -= n; - } - } - } - if found == SELECT_BUCKETS { - // Fewer labels need room than the cap, so none is cut. - put_label(select_word(2u), 4u); - return; - } - put_label(select_word(0u), (label_word(select_word(0u)) << digit(d).y) | found); - put_label(select_word(1u), left); - put_label(select_word(2u), d + 1u); -} - -@compute @workgroup_size(256) -fn select_cut(@builtin(global_invocation_id) id: vec3u) { - let row = labeled(id); - if row == NONE || mark.label_max == NONE || !wanting(row) || label_word(select_word(2u)) == 4u { - return; - } - let at = candidate(row) + CANDIDATE_STATE; - if mark.label_max == 0u || word(candidate(row) + CANDIDATE_KEY) > label_word(select_word(0u)) { - put(at, word(at) & ~WANTS); - } -} - -// The cells row `row`'s label covers: least and greatest column and row. -fn cells_of(row: u32) -> vec4u { - let at = candidate(row); - let lo = vec2f(bitcast(word(at + CANDIDATE_BOX)), bitcast(word(at + CANDIDATE_BOX + 1u))); - let hi = vec2f(bitcast(word(at + CANDIDATE_BOX + 2u)), bitcast(word(at + CANDIDATE_BOX + 3u))); - let top = vec2f(f32(frame.cell_columns), f32(frame.cell_rows)) - 1.0; - let a = vec2u(clamp(floor(lo / CELL), vec2f(0.0), top)); - let b = vec2u(clamp(floor(hi / CELL), vec2f(0.0), top)); - return vec4u(a, b); -} - -fn cell(x: u32, y: u32) -> u32 { - return frame.cells + y * frame.cell_columns + x; -} - -// Whether row `row`'s label still needs room this round. -fn undecided(row: u32) -> bool { - let s = word(candidate(row) + CANDIDATE_STATE); - return (s & WANTS) != 0u && (s & (PLACED_LABEL | DROPPED)) == 0u; -} - -@compute @workgroup_size(256) -fn clear_cells(@builtin(global_invocation_id) id: vec3u) { - let i = item(id); - if i < frame.cell_columns * frame.cell_rows { - put_label(frame.cells + i, NONE); - } -} - -@compute @workgroup_size(256) -fn reset_cells(@builtin(global_invocation_id) id: vec3u) { - let i = item(id); - if i < frame.cell_columns * frame.cell_rows && label_word(frame.cells + i) != OCCUPIED { - put_label(frame.cells + i, NONE); - } -} - -@compute @workgroup_size(256) -fn claim(@builtin(global_invocation_id) id: vec3u) { - let row = labeled(id); - if row == NONE || !undecided(row) { - return; - } - let at = candidate(row); - let key = word(at + CANDIDATE_KEY); - let c = cells_of(row); - for (var y = c.y; y <= c.w; y++) { - for (var x = c.x; x <= c.z; x++) { - if label_word(cell(x, y)) == OCCUPIED { - put(at + CANDIDATE_STATE, word(at + CANDIDATE_STATE) | DROPPED); - return; - } - atomicMin(&labels[cell(x, y)], key); - } - } -} - -@compute @workgroup_size(256) -fn decide(@builtin(global_invocation_id) id: vec3u) { - let row = labeled(id); - if row == NONE || !undecided(row) { - return; - } - let at = candidate(row); - let key = word(at + CANDIDATE_KEY); - let c = cells_of(row); - for (var y = c.y; y <= c.w; y++) { - for (var x = c.x; x <= c.z; x++) { - if label_word(cell(x, y)) != key { - return; - } - } - } - put(at + CANDIDATE_STATE, word(at + CANDIDATE_STATE) | PLACED_LABEL); -} - -@compute @workgroup_size(256) -fn occupy(@builtin(global_invocation_id) id: vec3u) { - let row = labeled(id); - if row == NONE || (word(candidate(row) + CANDIDATE_STATE) & PLACED_LABEL) == 0u { - return; - } - let c = cells_of(row); - for (var y = c.y; y <= c.w; y++) { - for (var x = c.x; x <= c.z; x++) { - put_label(cell(x, y), OCCUPIED); - } - } -} - -@compute @workgroup_size(256) -fn ask(@builtin(global_invocation_id) id: vec3u) { - let row = labeled(id); - if row == NONE { - return; - } - let s = word(candidate(row) + CANDIDATE_STATE); - if (s & PLACED_LABEL) == 0u { - return; - } - let code = u32(bitcast(word(extra_at(row, mark.label + LABEL_CODE)))); - if (s & UNSET) != 0u { - want(code); - } else { - seen(code); - } -} - -@compute @workgroup_size(256) -fn fade(@builtin(global_invocation_id) id: vec3u) { - let row = labeled(id); - if row == NONE { - return; - } - let at = candidate(row); - // A label placed before its text is set waits for it to fade in. - let s = word(at + CANDIDATE_STATE); - let placed = (s & PLACED_LABEL) != 0u && (s & UNSET) == 0u; - let alpha = bitcast(word(at + CANDIDATE_ALPHA)); - let next = clamp(select(alpha - frame.label_fade, alpha + frame.label_fade, placed), 0.0, 1.0); - put(at + CANDIDATE_ALPHA, bitcast(select(next, 0.0, missing_bits(bitcast(next))))); -} diff --git a/crates/latkit/shaders/compute/texts.wesl b/crates/latkit/shaders/compute/texts.wesl new file mode 100644 index 00000000..30cc2177 --- /dev/null +++ b/crates/latkit/shaders/compute/texts.wesl @@ -0,0 +1,72 @@ +// The texts labels need. Each labeled row a mark draws, with a text code, while its row shows and +// its tile is not leaving, asks the host to set its text while the atlas lacks it and its anchor is +// near the view (and, in a diagram, its text is large enough to read), and marks a set text in view +// as seen, so the atlas keeps it. It writes nothing per row: `draw/glyphs.wesl` decides from the +// same anchor whether each label shows. Each pipeline is made for one primitive and compiles only +// that primitive's anchors. + +import package::lib::types::{VISIBLE, LABEL_CODE, WANTED, item}; +import package::lib::bind::{eye, mark, word}; +import package::lib::state::{row_at, extra_at}; +import package::lib::read::missing_bits; +import package::lib::label::{anchor, length_px, legibility, text_at, in_view}; +import package::lib::drawn::drawn_row; + +// Each labeled mark's words: its run table, read here, and the codes it asks for and has seen, +// written atomically by every row. +@group(2) @binding(13) var labels: array>; + +// Asks the host to set the text of `code`, once however many rows show it. A text is either set or +// not for the whole pass, so the seen bit of a text not set marks it asked for. +fn want(code: u32) { + let bit = 1u << (code % 32u); + if (atomicOr(&labels[mark.seen + code / 32u], bit) & bit) != 0u { + return; + } + let asked = atomicAdd(&labels[mark.wanted], 1u); + if asked < WANTED { + atomicStore(&labels[mark.wanted + 1u + asked], code); + } +} + +// Marks the text of `code` as shown, so the atlas keeps it. +fn seen(code: u32) { + atomicOr(&labels[mark.seen + code / 32u], 1u << (code % 32u)); +} + +@compute @workgroup_size(256) +fn texts(@builtin(global_invocation_id) id: vec3u) { + let i = item(id); + if i >= mark.rows { + return; + } + let drawn = drawn_row(i); + let row = drawn.row; + let code_bits = word(extra_at(row, mark.label + LABEL_CODE)); + let flags = word(row_at(row) + mark.stride - 1u); + // A row whose tile is leaving needs no text. + if missing_bits(code_bits) || (flags & VISIBLE) == 0u || drawn.leaving { + return; + } + let a = anchor(i); + if !a.shown { + return; + } + let code = u32(bitcast(code_bits)); + let uv0 = atomicLoad(&labels[mark.runs + 2u * code]); + let uv1 = atomicLoad(&labels[mark.runs + 2u * code + 1u]); + let size = length_px(mark.label_size); + if uv0 == 0u && uv1 == 0u { + // The atlas lacks this text. Ask for it if the label could show soon: legible and near the + // view. + let near = 0.25 * eye.size; + if legibility(size) > 0.0 && in_view(a.at - near, a.at + near) { + want(code); + } + return; + } + let t = text_at(uv0, uv1, size, a.at, a.align); + if in_view(t.lo, t.hi) { + seen(code); + } +} diff --git a/crates/latkit/shaders/draw/backdrop.wesl b/crates/latkit/shaders/draw/backdrop.wesl index f84a1035..fe58973e 100644 --- a/crates/latkit/shaders/draw/backdrop.wesl +++ b/crates/latkit/shaders/draw/backdrop.wesl @@ -103,12 +103,18 @@ fn globe_degrees(w: vec3f, radius: f32) -> vec2f { return vec2f(lon, degrees(asin(clamp(v.y, -1.0, 1.0)))); } -// The earth: the Mercator band or the globe's disc, blended as the projection morphs, lit by +// The earth: the flat map's square or the globe's disc, blended as the projection morphs, lit by // daylight, with the graticule on it and the atmosphere around the globe. fn earth(o: vec3f, d: vec3f, g: vec3f, ahead: bool) -> Ground { - // The flat map ends where Web Mercator does, at y = ±π. + // The flat map is one world. It ends where Web Mercator does, at y = ±π, and at the + // antimeridian, x = ±π from the prime meridian. The distance to the antimeridian starts from + // the center's longitude part by part, so the edge holds still at any zoom. let band = PI - abs(g.y + frame.mercator_y); - let flat = saturate(band / max(fwidth(band), 1e-9) + 0.5) * f32(ahead); + let east = radians(180.0 - frame.center_hi.x - frame.center_lo.x) - g.x; + let west = radians(180.0 + frame.center_hi.x + frame.center_lo.x) + g.x; + let side = min(east, west); + let across = saturate(side / max(fwidth(side), 1e-9) + 0.5); + let flat = saturate(band / max(fwidth(band), 1e-9) + 0.5) * across * f32(ahead); let center = globe_center(); let radius = -center.z; let to_center = center - o; diff --git a/crates/latkit/shaders/draw/glyphs.wesl b/crates/latkit/shaders/draw/glyphs.wesl index 97995abe..b3bd353a 100644 --- a/crates/latkit/shaders/draw/glyphs.wesl +++ b/crates/latkit/shaders/draw/glyphs.wesl @@ -1,14 +1,16 @@ -// Glyphs: every labeled row's text, placed from its anchor each frame and shown as placement -// decided. A halo of the background color keeps text readable over anything below it. The ports -// variant draws each port's label beside it, inside its marker. Text is a distance field, so edges -// stay crisp at any size, and the halo is a wider threshold of the same field. +// Glyphs: every labeled row's text, placed from its anchor each frame. A label shows where its text +// is set, its row shows, and it fits the room its anchor gives, fading with its row's tile and, in a +// diagram, once too small to read. Nothing else decides: no label depends on another, or on an +// earlier frame. A halo of the background color keeps text readable over anything below it. The +// ports variant draws each port's label beside it, inside its marker. Text is a distance field, so +// edges stay crisp at any size, and the halo is a wider threshold of the same field. import package::lib::types::{ - CANDIDATE_WORDS, CANDIDATE_ALPHA, LABEL_CODE, LABEL_COLOR, PORT_WORDS, PORT_CODE, PORT_UV, SIDE_LEFT, - SIDE_RIGHT, SIDE_TOP, TEXT_SPREAD, TEXT_UNSET, SPACE_DIAGRAM, THEME_TEXT, + LABEL_CODE, LABEL_COLOR, PORT_WORDS, PORT_CODE, PORT_UV, SIDE_LEFT, SIDE_RIGHT, SIDE_TOP, TEXT_SPREAD, + SPACE_DIAGRAM, THEME_TEXT, }; import package::lib::bind::{frame, eye, mark, word, program, ramp}; -import package::lib::state::{extra_at, record_at}; +import package::lib::state::extra_at; import package::lib::read::missing_bits; import package::lib::points::{shown_point, port_at, port_inward}; import package::lib::marker::theme_port_size; @@ -66,19 +68,15 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: let uv0 = labels[mark.runs + 2u * code]; let uv1 = labels[mark.runs + 2u * code + 1u]; // Nothing to draw until the text is set. - if (uv0 == 0u && uv1 == 0u) || (uv0 & TEXT_UNSET) != 0u { + if uv0 == 0u && uv1 == 0u { return unshown(); } let a = anchor(instance); let size = length_px(mark.label_size); let t = text_at(uv0, uv1, size, a.at, a.align); - // A diagram's labels show while they fit and are legible. Elsewhere, placement decides. - var alpha = bitcast(word(record_at(mark.candidates, CANDIDATE_WORDS, row) + CANDIDATE_ALPHA)); - if frame.space == SPACE_DIAGRAM { - alpha = f32(a.shown && fits(t.hi - t.lo, a.room)) * legibility(size); - } - // A label fades with its row's tile. - alpha *= drawn.presence; + // A label shows while its row shows and it fits, as legible as its size allows, and fades with + // its row's tile. + let alpha = f32(a.shown && fits(t.hi - t.lo, a.room)) * legibility(size) * drawn.presence; if !(alpha > 0.0) { return unshown(); } diff --git a/crates/latkit/shaders/lib/label.wesl b/crates/latkit/shaders/lib/label.wesl index 7b81397d..bfdf50c1 100644 --- a/crates/latkit/shaders/lib/label.wesl +++ b/crates/latkit/shaders/lib/label.wesl @@ -1,8 +1,8 @@ // Labels: where a labeled row's text is anchored, its size, and whether it can show. Draws place -// every label from its anchor each frame, so labels track the camera and their rows exactly. -// Placement measures the same box only to decide which labels are dropped. Text is a distance field -// set at `TEXT_BASE` and drawn at any size: in CSS pixels, or in a diagram in length units, where -// text scales with the sheet and fades out once too small to read. +// every label from its anchor each frame, so labels track the camera and their rows exactly. The +// texts pass measures the same box only to find the texts in view. Text is a distance field set at +// `TEXT_BASE` and drawn at any size: in CSS pixels, or in a diagram in length units, where text +// scales with the sheet and fades out once too small to read. import package::lib::types::{ POINTS, MARKER_BLOCK, SIZED, GROUP, PATH_TRACE, TEXT_BASE, TEXT_SPREAD, HEADER_BAND, SPACE_DIAGRAM, diff --git a/crates/latkit/shaders/lib/placement.wesl b/crates/latkit/shaders/lib/placement.wesl deleted file mode 100644 index a0b96434..00000000 --- a/crates/latkit/shaders/lib/placement.wesl +++ /dev/null @@ -1,40 +0,0 @@ -// Label placement storage. Each labeled row's candidate lives in pages listed by the page table at -// `mark.candidates`. The labels buffer holds each mark's requested and shown text codes, run table, -// and the select that caps its labels, plus the grid of screen cells that labels claim. - -import package::lib::types::{CANDIDATE_WORDS, WANTED}; -import package::lib::bind::{mark, pages}; -import package::lib::state::record_at; - -@group(2) @binding(13) var labels: array>; - -fn label_word(at: u32) -> u32 { - return atomicLoad(&labels[at]); -} - -fn put_label(at: u32, value: u32) { - atomicStore(&labels[at], value); -} - -// Where held row `row`'s candidate starts among the pages. -fn candidate(row: u32) -> u32 { - return record_at(mark.candidates, CANDIDATE_WORDS, row); -} - -// Writes word `at` of the pages, such as a candidate word. -fn put(at: u32, value: u32) { - pages[at] = value; -} - -// Asks the host to set the text of `code`. -fn want(code: u32) { - let asked = atomicAdd(&labels[mark.wanted], 1u); - if asked < WANTED { - put_label(mark.wanted + 1u + asked, code); - } -} - -// Marks the text of `code` as shown, so the atlas keeps it. -fn seen(code: u32) { - atomicOr(&labels[mark.seen + code / 32u], 1u << (code % 32u)); -} diff --git a/crates/latkit/shaders/lib/types.wesl b/crates/latkit/shaders/lib/types.wesl index 735649f0..e9085c7f 100644 --- a/crates/latkit/shaders/lib/types.wesl +++ b/crates/latkit/shaders/lib/types.wesl @@ -219,32 +219,15 @@ const AREA_WORDS: u32 = 2u; const AREA_COLOR: u32 = 0u; const AREA_FLAGS: u32 = 1u; -// Label words among a labeled mark's extra words: text code, priority (0 to 255), and color. -const LABEL_WORDS: u32 = 3u; +// Label words among a labeled mark's extra words: text code (missing for no label) and color. +const LABEL_WORDS: u32 = 2u; const LABEL_CODE: u32 = 0u; -const LABEL_PRIORITY: u32 = 1u; -const LABEL_COLOR: u32 = 2u; - -// Words of a labeled row's placement candidate, in pages listed by the page table at -// `mark.candidates`: the label's box in CSS pixels (four words), key, state, and opacity. Draws -// place each label from its anchor every frame; placement only decides whether it shows. -const CANDIDATE_WORDS: u32 = 7u; -const CANDIDATE_BOX: u32 = 0u; -const CANDIDATE_KEY: u32 = 4u; -const CANDIDATE_STATE: u32 = 5u; -const CANDIDATE_ALPHA: u32 = 6u; +const LABEL_COLOR: u32 = 1u; + // Each text run is set once, at a size of `TEXT_BASE` atlas texels, as a distance field reaching // `TEXT_SPREAD` texels past the text's edge. const TEXT_BASE: f32 = 32.0; const TEXT_SPREAD: f32 = 8.0; -// Candidate state bits. WANTS: in view and needs room. PLACED_LABEL: placed. DROPPED: a placed -// label occupies some of its cells. STICKY: placed in the previous frame. UNSET: its text is not -// set yet. -const WANTS: u32 = 1u; -const PLACED_LABEL: u32 = 2u; -const DROPPED: u32 = 4u; -const STICKY: u32 = 8u; -const UNSET: u32 = 16u; // Words per text run at the start of the fields buffer, for runs whose distance field the GPU // computes: where its coverage starts, the coverage's width and height, where its column minima // start, where its field starts, and the field's row pitch in words. @@ -256,15 +239,7 @@ const FIELD_AT: u32 = 4u; const FIELD_ROW: u32 = 5u; const FIELD_WORDS: u32 = 6u; -// Flag on a run table entry's first corner word while its text is not set. The second word then -// holds the run's measured size, so its label can be placed before the text is set. -const TEXT_UNSET: u32 = 0x80000000u; -// Value of a label grid cell occupied by a placed label. Other cells hold NONE or the least key -// claiming them. -const OCCUPIED: u32 = 0u; -// Side of a label grid cell, in CSS pixels. -const CELL: f32 = 8.0; -// Most text codes a mark asks the host to set per frame. +// Most text codes a mark asks the host to set per texts pass. const WANTED: u32 = 64u; // Row state flags, in the last word of each row's kind words. @@ -491,12 +466,6 @@ struct Frame { // above are already in CSS pixels, except comet spacing and length. unit: f32, zoom: f32, - // The label grid: where its cells start in the labels buffer, and its column and row counts. - // `label_fade` is how much a label's opacity changes per frame. - cells: u32, - cell_columns: u32, - cell_rows: u32, - label_fade: f32, // A plane's grid: kind, pitch in position units, and the world distance from the center at // which it has faded out (0 for no fade). `graticule` is 1 if the earth draws a graticule. grid_kind: u32, @@ -615,23 +584,16 @@ struct Mark { // Where the program lists the mark's ports (`PORT_WORDS` each), and how many each row has. port_info: u32, ports: u32, - // `label`: the extra word holding the label's code, or NONE. `candidates`: the page table of - // each row's candidate. In the labels buffer: `runs`, the run table (two corner words per - // code); `wanted`, a count followed by the codes to set; `seen`, a bit per code shown in view. - // `label_id`: the id of the mark's first candidate, unique across marks. The two sizes are in - // length units. + // `label`: the extra word holding the label's code, or NONE. In the labels buffer: `runs`, the + // run table (two corner words per code, both 0 until the text is set); `wanted`, a count + // followed by the codes to set, each once; `seen`, a bit per code the texts pass met: a set + // text shown in view, or one not set that it asked for. The two sizes are in length units. label: u32, - candidates: u32, runs: u32, wanted: u32, seen: u32, - label_id: u32, label_size: f32, port_label_size: f32, - // The most labels shown, or NONE for no cap; and where the labels buffer holds the select that - // applies the cap. - label_max: u32, - label_select: u32, // The range of CSS pixels the mark's size is clamped to at any zoom: a vertex's half size, or a // stroke's width. size_min: f32, diff --git a/crates/latkit/src/data/tests.rs b/crates/latkit/src/data/tests.rs index fda7f272..680f2110 100644 --- a/crates/latkit/src/data/tests.rs +++ b/crates/latkit/src/data/tests.rs @@ -336,10 +336,12 @@ fn queries_filter_sort_page_and_select() { let ids = data.query(&Query::of("Branch").limit(2).ids_alone()).expect("a query").table; assert_eq!((ids.len(), ids.fields().count()), (2, 0), "ids alone"); let json: Query = serde_json::from_value(serde_json::json!({ - "of": "Branch", "filter": { "op": "lt", "args": ["loading", 100] }, "sort": [["loading", "descending"]], "limit": 2, + "of": "Branch", "filter": { "lt": ["$loading", 100] }, "sort": [["$loading", "descending"]], "limit": 2, + "select": { "name": "$name" }, })) .expect("a query"); - assert_eq!(names(&data.query(&json).expect("a query")), [text("b"), text("d")]); + let read = data.query(&json).expect("a query"); + assert_eq!(names(&read), [text("b"), text("d")], "a selected field comes back as stored"); let named = data.query(&Query::of("Branch").ids([3_u64, 1, 9])).expect("by ids"); assert_eq!(names(&named), [text("d"), text("b")], "in the order given, unknown ids skipped"); let lake = data.query(&Query::of("Branch").filter(field("name").contains("C"))).expect("contains"); diff --git a/crates/latkit/src/derive/mod.rs b/crates/latkit/src/derive/mod.rs index fda9659f..8a9ff1b6 100644 --- a/crates/latkit/src/derive/mod.rs +++ b/crates/latkit/src/derive/mod.rs @@ -4,8 +4,8 @@ //! changes. Small work runs on the frame, within one budget ([`FRAME_WORK`]) that all derivations //! share. Larger work runs as a host job, and its result is kept only while its inputs are //! unchanged. Results are tables beside the tables marks draw, row for row, which marks read like -//! data. Values the GPU computes (normals, segment counts, label placement) use the same stamps and -//! are read back. +//! data. Values the GPU computes (normals, segment counts, the texts labels need) use the same +//! stamps and are read back. //! //! The derivations: a diagram's sheet (places, sizes, group boxes, wires). Outside diagrams: //! lanes of edges sharing both endpoints, stars of nets, and, when the scene has a layout, places diff --git a/crates/latkit/src/derive/order.rs b/crates/latkit/src/derive/order.rs index 32518780..48fb5232 100644 --- a/crates/latkit/src/derive/order.rs +++ b/crates/latkit/src/derive/order.rs @@ -7,11 +7,15 @@ //! edges end at come before the sample, as a pyramid would show them with those edges. A pyramid //! serves while the rows it was made from are unchanged; rows appended since draw after them, in //! table order. +//! +//! A table of vector tiles draws in table order, its tiles' rows appended as they arrive, so its +//! tiles are already ranges of its order and appending moves no row. use std::sync::Arc; use super::pyramid::Pyramid; use super::stamp_of; +use crate::data::Version; use crate::gpu::shaders::PAGE_ROWS; /// A paged table's draw order: which row draws at each place. @@ -23,6 +27,10 @@ pub(crate) enum Order { /// A pyramid's order of the table's first rows, then the rows appended since, in table order, /// up to `rows` rows. Pyramid { pyramid: Arc, rows: u32 }, + /// The table's `rows` rows in table order, while its row order version (`Table::order`) is + /// `order`. Rows appended draw after the rest; removing rows changes the version, and so the + /// order. + Table { rows: u32, order: Version }, } /// The rows a sample draws before its own: the rows the sampled edges end at, just as a pyramid @@ -104,7 +112,7 @@ impl Order { /// The number of rows it orders: every row of its table. pub(crate) fn len(&self) -> u32 { match self { - Order::Sampled { rows, .. } | Order::Pyramid { rows, .. } => *rows, + Order::Sampled { rows, .. } | Order::Pyramid { rows, .. } | Order::Table { rows, .. } => *rows, } } @@ -113,14 +121,15 @@ impl Order { self.len().div_ceil(PAGE_ROWS) } - /// How many rows a sample draws first: the lifted rows plus the sample's own. 0 for a pyramid. + /// How many rows a sample draws first: the lifted rows plus the sample's own. 0 for the other + /// orders. pub(crate) fn sampled(&self) -> u32 { match self { Order::Sampled { rows, step, lifted } => { let first = rows.div_ceil(*step); lifted.len() + first - lifted.places.partition_point(|&p| p < first) as u32 } - Order::Pyramid { .. } => 0, + Order::Pyramid { .. } | Order::Table { .. } => 0, } } @@ -132,6 +141,7 @@ impl Order { None => sample_source(unlisted(&lifted.places, d - lifted.len()), *rows, *step), }, Order::Pyramid { pyramid, .. } => pyramid.order.get(d as usize).copied().unwrap_or(d), + Order::Table { .. } => d, } } @@ -146,6 +156,7 @@ impl Order { lifted.len() + place - lifted.places.partition_point(|&p| p < place) as u32 } Order::Pyramid { pyramid, .. } => pyramid.rank.get(r as usize).copied().unwrap_or(r), + Order::Table { .. } => r, } } @@ -173,11 +184,13 @@ impl Order { } /// The key that identifies pages packed in this order. A pyramid's id ignores rows appended - /// since; a sample's depends on its row count, step, and lifted rows. + /// since, and so does table order's; a sample's depends on its row count, step, and lifted + /// rows. pub(crate) fn id(&self) -> u64 { match self { Order::Sampled { rows, step, lifted } => stamp_of(("sampled", rows, step, lifted.stamp)), Order::Pyramid { pyramid, .. } => stamp_of(pyramid.made), + Order::Table { order, .. } => stamp_of(("table", order)), } } } @@ -237,4 +250,17 @@ mod tests { assert_eq!(order.page(0), [2, 0, 1, 3, 4]); assert_eq!((order.rank(2), order.rank(4)), (0, 4)); } + + #[test] + fn table_order_keeps_its_id_while_rows_are_only_appended() { + let version = Version::next(); + let order = Order::Table { rows: 3, order: version }; + assert_eq!(order.page(0), [0, 1, 2]); + assert_eq!((order.source(2), order.rank(1), order.sampled()), (2, 1, 0)); + let grown = Order::Table { rows: PAGE_ROWS + 5, order: version }; + assert_eq!(grown.id(), order.id(), "pages packed before rows were appended stay valid"); + assert_eq!(grown.page(1), (PAGE_ROWS..PAGE_ROWS + 5).collect::>()); + let removed = Order::Table { rows: 2, order: Version::next() }; + assert_ne!(removed.id(), order.id(), "removing rows makes another order"); + } } diff --git a/crates/latkit/src/derive/pyramid.rs b/crates/latkit/src/derive/pyramid.rs index b7cf09ab..9c99d514 100644 --- a/crates/latkit/src/derive/pyramid.rs +++ b/crates/latkit/src/derive/pyramid.rs @@ -20,7 +20,7 @@ use std::sync::Arc; use rustc_hash::{FxHashMap, FxHashSet}; -use super::sight::{Domain, Region, Sight, TILE_PX, meets}; +use super::sight::{Domain, Region, SPLIT_PX, Sight, meets}; use crate::data::{UNWIRED, Version}; use crate::gpu::shaders::PAGE_ROWS; use crate::tiles::TileKey; @@ -32,7 +32,8 @@ pub(crate) const GRID_BITS: u32 = 7; pub(crate) const MAX_LEVEL: u8 = 24; /// Marks a row not yet assigned a level. const NEVER: u8 = u8::MAX; -/// The most tiles a camera request splits into per level; past it, tiles stop splitting. +/// The most tiles a camera request splits into per level; past it, the tiles drawn largest split +/// and the rest stop splitting. const MAX_TILES: usize = 1024; /// Log2 of the sub-cells across a large tile. Each sub-cell is a range of the order that the camera /// can request separately, so only the visible part of a large tile draws. @@ -624,11 +625,12 @@ impl Pyramid { /// /// At each level it takes every tile whose rows reach into the region in sight (for a large /// tile, only the sub-cells that do), and splits a tile into its children while it draws larger - /// than `TILE_PX` (with 25% slack) near the camera's focus. A drawing holds whole pages of the - /// order, so once a level's pages would exceed `rows` rows, that level and all finer ones are - /// dropped and the coarser levels draw instead. If the first level in sight exceeds the budget - /// alone (for example, edges spanning the square, which cannot go finer), as many of its first - /// rows as fit are drawn. + /// than `SPLIT_PX` near the camera's focus. When more tiles would split than a level allows, + /// those drawn largest split first, and the next level lists their children in that order. A + /// drawing holds whole pages of the order, so once a level's pages would exceed `rows` rows, + /// that level and all finer ones are dropped and the coarser levels draw instead. If the first + /// level in sight exceeds the budget alone (for example, edges spanning the square, which + /// cannot go finer), as many of its first rows as fit are drawn. pub(crate) fn wanted(&self, sight: &Sight<'_>, domain: &Domain, rows: u32) -> (Vec<(u8, Range)>, bool) { let fit = rows.div_ceil(PAGE_ROWS); if let [tile] = &self.tiles[..] @@ -639,13 +641,14 @@ impl Pyramid { } let region: Region = sight.region(); let focus = sight.focus(); - let limit = TILE_PX * 1.25; let mut out: Vec<(u8, Range)> = Vec::new(); let (mut held, mut last) = (0_u32, None); let mut level = vec![TileKey { z: 0, x: 0, y: 0 }]; while !level.is_empty() { let mut found = Vec::new(); - let mut next = Vec::new(); + // The level's tiles with tiles below them that draw larger than `SPLIT_PX`, with their + // sizes. + let mut large: Vec<(TileKey, f64)> = Vec::new(); for key in level { let grow = 0.5 / f64::from(1_u32 << key.z); if !meets(®ion, domain.bounds(key, grow)) { @@ -673,12 +676,16 @@ impl Pyramid { } } } - let splits = - self.above.contains(&key) && next.len() < MAX_TILES && sight.measure(&focus, domain, key).0 > limit; - if splits { - next.extend(key.children()); + if self.above.contains(&key) { + let (size, _) = sight.measure(&focus, domain, key); + if size > SPLIT_PX { + large.push((key, size)); + } } } + // The tiles drawn largest split first, a quarter of `MAX_TILES` at most. + crate::order::by_key(&mut large, |&(_, size)| !crate::order::number(size)); + let next: Vec = large.iter().take(MAX_TILES / 4).flat_map(|(key, _)| key.children()).collect(); // If this level's pages exceed the budget, drop it; coarser levels draw instead. let (pages, reached) = pages_past(&found, last); if held + pages > fit { diff --git a/crates/latkit/src/derive/shapes.rs b/crates/latkit/src/derive/shapes.rs index 4ddc65bd..972d3652 100644 --- a/crates/latkit/src/derive/shapes.rs +++ b/crates/latkit/src/derive/shapes.rs @@ -12,7 +12,8 @@ //! Triangulation that does not fit the frame's budget runs in a job; a page whose every row needs //! shaping goes to the job whole. Rows waiting on a job draw nothing until it finishes. Bending a //! path is cheap and done at once. Vector tiles bring their own shapes, made in the job that -//! decodes them. +//! decodes them, as a field beside their rows. Their tables are paged, and read that field one +//! page of their order at a time like any other, so nothing shapes those rows again. use rustc_hash::{FxHashMap, FxHashSet}; diff --git a/crates/latkit/src/derive/sight.rs b/crates/latkit/src/derive/sight.rs index 054a5124..49acf05c 100644 --- a/crates/latkit/src/derive/sight.rs +++ b/crates/latkit/src/derive/sight.rs @@ -1,8 +1,12 @@ //! The camera's view as tiles; the only place derivations read the camera. //! //! Tiles split a square of the space four ways per level: the earth's Web Mercator square, or a -//! square of a plane. A tile is needed where it meets the region in sight, and splits while it -//! draws larger than its nominal size (`TILE_PX`) where it lies nearest the camera's focus. +//! square of a plane. The earth's square wraps at the antimeridian on the globe, and ends there on +//! a flat map, which is one world. A tile is needed where it meets the region in sight, and splits +//! while it draws larger than its nominal size (`TILE_PX`) where it lies nearest the camera's +//! focus. How large a tile draws is the area it covers on screen there, as a side, so a tile +//! foreshortened toward the globe's limb or a tilted horizon stays coarse. When a budget limits how +//! many tiles split, the tiles drawn largest split first. use glam::DVec3; @@ -12,6 +16,8 @@ use crate::view::Projected; /// CSS pixels a tile spans when drawn at its own level. pub(crate) const TILE_PX: f64 = 512.0; +/// CSS pixels past which a tile splits: its nominal size with 25% slack. +pub(crate) const SPLIT_PX: f64 = TILE_PX * 1.25; /// The square a space's tiles divide: the earth's Web Mercator square, or a square of a plane. #[derive(Clone, Copy, PartialEq, Debug)] @@ -92,7 +98,7 @@ pub(crate) struct Sight<'a> { } /// The region in sight as west, south, east, north boxes in position units: one box, or on the -/// earth two where the region crosses the antimeridian. +/// globe two where the region crosses the antimeridian. pub(crate) type Region = Vec<[f64; 4]>; impl Sight<'_> { @@ -150,7 +156,8 @@ impl Sight<'_> { std::iter::once(center).chain(near).collect() } - /// The ground in sight, sampled on a grid of points across the view. + /// The ground in sight, sampled on a grid of points across the view. On a flat map it ends at + /// the world's edges; on the globe it wraps the antimeridian. pub(crate) fn region(&self) -> Region { const SAMPLES: usize = 7; let globe = self.globe(); @@ -200,17 +207,19 @@ impl Sight<'_> { if w > e { return vec![[-180.0, -90.0, 180.0, 90.0]]; } - if missed { - // Some samples miss a tilted map, which shows its horizon: grow the region by four - // times its size on each side to cover the ground toward it. + // Some samples miss a tilted map, which shows its horizon: grow the region by four times + // its size on each side to cover the ground toward it. Otherwise pad it by 2%. + let (grow_x, grow_y) = if missed { let size = (e - w).max(n - s) * 4.0; - return boxes(w - size, e + size, s - size, n + size); - } - let (pad_x, pad_y) = ((e - w) * 0.02, (n - s) * 0.02); + (size, size) + } else { + ((e - w) * 0.02, (n - s) * 0.02) + }; if globe { - return boxes(w - pad_x, e + pad_x, s - pad_y, n + pad_y); + return boxes(w - grow_x, e + grow_x, s - grow_y, n + grow_y); } - vec![[(w - pad_x).max(-180.0), (s - pad_y).max(-90.0), (e + pad_x).min(180.0), (n + pad_y).min(90.0)]] + // A flat map is one world: the region ends at its edges rather than wrapping. + vec![[(w - grow_x).max(-180.0), (s - grow_y).max(-90.0), (e + grow_x).min(180.0), (n + grow_y).min(90.0)]] } /// The cap of the globe the camera can see, out to its horizon. @@ -231,10 +240,13 @@ impl Sight<'_> { } /// How large tile `key` of `domain`'s square draws, in CSS pixels, at its point nearest each - /// focus, and how far that point is from the view's center. It scales up how far a step of at - /// most a pixel moves on screen there. A longer step from a large tile could pass the globe's - /// horizon, where nothing projects, once the camera is close to the ground. On a flat map every - /// tile of a level measures the same wherever it lies. + /// focus (the largest of those), and how far that point is from the view's center. Its size is + /// the area it would cover at the scale there, as a side: the area that steps of at most a + /// pixel along its two edges span on screen, scaled up to the whole tile. So a tile + /// foreshortened toward the globe's limb or a tilted horizon measures less than its longer + /// side. A longer step from a large tile could pass the globe's horizon, where nothing + /// projects, once the camera is close to the ground. On a flat map seen from above, every tile + /// of a level measures the same wherever it lies. pub(crate) fn measure(&self, focus: &[[f64; 2]], domain: &Domain, key: TileKey) -> (f64, f64) { let side = 1.0 / f64::from(1_u32 << key.z); let (west, north) = (f64::from(key.x) * side, f64::from(key.y) * side); @@ -242,14 +254,20 @@ impl Sight<'_> { let (mut size, mut distance) = (0.0_f64, f64::INFINITY); for &f in focus { let [fx, fy] = domain.unit(f); - // The earth's square wraps: shift the focus a turn either way, whichever is nearer. - let fx = if *domain == Domain::Earth { fx + (west + side / 2.0 - fx).round() } else { fx }; + let fx = match domain { + // The globe wraps: shift the focus a turn either way, whichever is nearer. + Domain::Earth if self.globe() => fx + (west + side / 2.0 - fx).round(), + // A flat map is one world: a focus past its edge stays there, far from the tiles + // across the antimeridian. + Domain::Earth => (f[0] + 180.0) / 360.0, + Domain::Plane { .. } => fx, + }; let at = [fx.clamp(west, west + side - step), fy.clamp(north, north + side - step)]; let Some(p) = self.project(domain.position(at)) else { continue }; - let reach = |q: [f64; 2]| { - self.project(domain.position(q)).map_or(0.0, |q| side / step * (q[0] - p[0]).hypot(q[1] - p[1])) - }; - size = size.max(reach([at[0] + step, at[1]])).max(reach([at[0], at[1] + step])); + // Where a step along each edge moves on screen; nowhere past the globe's horizon. + let moved = |q: [f64; 2]| self.project(domain.position(q)).map_or([0.0; 2], |q| [q[0] - p[0], q[1] - p[1]]); + let ([ax, ay], [bx, by]) = (moved([at[0] + step, at[1]]), moved([at[0], at[1] + step])); + size = size.max(side / step * (ax * by - ay * bx).abs().sqrt()); distance = distance.min((p[0] - self.size[0] / 2.0).hypot(p[1] - self.size[1] / 2.0)); } (size, distance) @@ -311,11 +329,57 @@ mod tests { let n = f64::from(1_u32 << z); let key = TileKey { z, x: (u * n) as u32, y: (v * n) as u32 }; let (px, _) = sight.measure(&sight.focus(), &Domain::Earth, key); - // A large tile measures where it is largest: south of the center, a mercator step - // covers more of the globe. + // Under the center the globe is seen square on, so a tile measures its size there. + // A large tile measures where it is largest, which may lie south of the center, + // where a mercator step covers more of the globe. let nominal = 256.0 * 2_f64.powf(zoom) / n; assert!((0.95..1.3).contains(&(px / nominal)), "zoom {zoom}, level {z}: {px} px, not {nominal}"); } } } + + #[test] + fn tiles_foreshortened_by_a_tilted_camera_measure_the_area_they_cover() { + let map = crate::Space::Earth(crate::Earth::default()); + let size = [800.0, 600.0]; + let center = [-74.0, 40.7]; + let [u, v] = Domain::Earth.unit(center); + let key = TileKey { z: 12, x: (u * 4096.0) as u32, y: (v * 4096.0) as u32 }; + let measured = |pitch: f64| { + let camera = crate::Camera::Map { center: [center[0], center[1], 0.0], zoom: 8.0, pitch, bearing: 0.0 }; + let projected = Projected::new(&map, &camera, size, [0.0, 0.0, size[0], size[1]], 0.0); + let sight = Sight { projected: &projected, size, earth: true }; + sight.measure(&sight.focus(), &Domain::Earth, key).0 + }; + // Tilted 60°, a tile under the center keeps its width on screen and draws half as tall, so + // its area as a side is about √½ of what it measures seen from above. + let ratio = measured(60.0) / measured(0.0); + assert!((0.65..0.8).contains(&ratio), "{ratio}"); + } + + #[test] + fn a_flat_map_ends_at_the_antimeridian() { + let map = crate::Space::Earth(crate::Earth::default()); + let size = [800.0, 600.0]; + let tile = |lon: f64| { + let [u, v] = Domain::Earth.unit([lon, 0.0]); + TileKey { z: 10, x: (u * 1024.0) as u32, y: (v * 1024.0) as u32 } + }; + let (near, across) = (tile(179.0), tile(-179.0)); + // Seen from above, and tilted to show the horizon, which grows the region several times + // over: on a globe that region would wrap past 180° onto the tiles across it. + for pitch in [0.0, 75.0] { + let camera = crate::Camera::Map { center: [179.0, 0.0, 0.0], zoom: 10.0, pitch, bearing: 0.0 }; + let projected = Projected::new(&map, &camera, size, [0.0, 0.0, size[0], size[1]], 0.0); + let sight = Sight { projected: &projected, size, earth: true }; + let region = sight.region(); + assert!(region.iter().all(|r| r[0] >= -180.0 && r[2] <= 180.0), "pitch {pitch}: {region:?}"); + assert!(meets(®ion, near.bounds()), "pitch {pitch}: {region:?}"); + assert!(!meets(®ion, across.bounds()), "pitch {pitch}: {region:?}"); + let focus = sight.focus(); + let ((_, beside), (_, beyond)) = + (sight.measure(&focus, &Domain::Earth, near), sight.measure(&focus, &Domain::Earth, across)); + assert!(beside < 1.0 && beyond > size[0], "pitch {pitch}: {beside} px and {beyond} px from the center"); + } + } } diff --git a/crates/latkit/src/derive/tiled.rs b/crates/latkit/src/derive/tiled.rs index e62d33f6..08bdb721 100644 --- a/crates/latkit/src/derive/tiled.rs +++ b/crates/latkit/src/derive/tiled.rs @@ -12,6 +12,9 @@ //! draw after them, so appending never thins the drawn rows; once the appended rows reach an //! eighth of the pyramid's rows (at least a page), the pyramid is rebuilt. What the camera needs of //! a table is ranges of its order, so nothing is gathered as it moves. +//! +//! A table of vector tiles needs no pyramid: its tiles are the vector tiles themselves, whose rows +//! its set appends as they arrive, and the set chooses what it draws (`TileSets::choose`). use std::ops::Range; use std::sync::Arc; @@ -25,6 +28,7 @@ use crate::expr::Expr; use crate::expr::cpu::Rows; use crate::gpu::shaders::PAGE_ROWS; use crate::host::Job; +use crate::tiles::TileSets; /// Estimated microseconds to place one row in a pyramid. const PLACE_COST: f64 = 1.0; @@ -78,14 +82,18 @@ pub(crate) type Plans = Keyed<(Name, Name), Planned>; pub(crate) struct Ask<'a> { pub(crate) data: &'a [Snapshot], pub(crate) plans: &'a Plans, + /// The tile sets: datasets of vector tiles have no pyramids. + pub(crate) tiles: &'a TileSets, /// The camera's view, on a map or a plane. pub(crate) sight: Option<&'a Sight<'a>>, pub(crate) earth: bool, } -/// What a view draws of a paged table: its order, and the ranges of it to draw, finest tiles first -/// so more important rows draw on top. `capped` is whether the plan's row budget dropped some of -/// what the camera needs; only then does a smaller budget draw less of the table. +/// What a view draws of a paged table: its order, and the ranges of it to draw, in the order they +/// draw, so later ranges draw on top. A pyramid's tiles draw finest first, so more important rows +/// draw on top; vector tiles draw coarsest first, so finer tiles draw over the ancestors standing +/// in for them. `capped` is whether the plan's row budget dropped some of what the camera needs; +/// only then does a smaller budget draw less of the table. #[derive(Clone, Debug, PartialEq)] pub(crate) struct Chosen { pub(crate) order: Order, @@ -103,11 +111,12 @@ pub(crate) struct Tiled { } impl Tiled { - /// Updates each dataset's pyramids to `ask`, and chooses what each paged table draws. + /// Updates each dataset's pyramids to `ask`, and chooses what each paged table draws, except + /// the tables of vector tiles. pub(crate) fn update(&mut self, ask: &Ask<'_>, jobs: &mut Jobs<'_>) -> Choices { let mut wanted: Vec<&Name> = Vec::new(); for ((data, _), planned) in ask.plans { - if !planned.as_is() && !wanted.contains(&data) { + if !planned.as_is() && !wanted.contains(&data) && !ask.tiles.vector(data) { wanted.push(data); } } diff --git a/crates/latkit/src/expr/compile.rs b/crates/latkit/src/expr/compile.rs index d85280c2..92205a80 100644 --- a/crates/latkit/src/expr/compile.rs +++ b/crates/latkit/src/expr/compile.rs @@ -346,16 +346,20 @@ impl<'a> Compiler<'a> { self.program.ends.push(EndUse { table: table.clone(), column: wgsl::NONE }); } - /// Compiles channel `name`, storing the dictionary code of text field `expr` at `word`. Returns + /// Compiles channel `name`, storing at `word` the dictionary code of text field `expr`, or of a + /// `when` whose values are each that field or missing. A missing code draws no label. Returns /// the program column whose dictionary decodes the codes. pub(crate) fn text(&mut self, name: &str, expr: &Expr, word: u32) -> Result { - let Node::Field(f) = &*expr.0 else { return Err(invalid("a label's text must be a text field").at(name)) }; + let Some(f) = text_field(expr, None).map_err(|e| e.at(name))? else { + return Err(invalid("a label's text needs a text field").at(name)); + }; if self.fields.kind(f.from.as_ref(), &f.name) != Some(Kind::Text) { return Err(invalid("not a text field").at(&f.name).at(name)); } let start = self.begin(); + // The values compile as any conditional's do, a missing one as the missing code. + self.expect(expr, Ty::Text).map_err(|e| e.at(name))?; let column = self.field_column(f, Words::Codes, false); - self.op(wgsl::OP_FIELD, column, 0, 0, 0, 1)?; self.op(wgsl::OP_STORE_CODE, word, 0, 0, 1, 0)?; self.close(name, start); Ok(column) @@ -462,7 +466,14 @@ impl<'a> Compiler<'a> { match (self.emit(e)?, want) { (got, want) if got == want => Ok(()), (Ty::Bool, Ty::Number) => Ok(()), - (got, want) => Err(invalid(format!("expected {}, found {}", ty_name(want), ty_name(got)))), + (got, want) => { + let hint = match (got, want) { + (Ty::Number, Ty::Color) => "; map a number to colors with colormap", + (Ty::Bool, Ty::Color) => "; choose between colors with when", + _ => "", + }; + Err(invalid(format!("expected {}, found {}{hint}", ty_name(want), ty_name(got)))) + } } } @@ -753,6 +764,23 @@ fn binary_name(op: Binary) -> &'static str { } } +/// The one text field a label's text shows: `expr` itself, or the field each value of a `when` +/// gives, where every value is that field (the same name and table) or missing. `seen` is the field +/// found so far; `None` when every value is missing. +fn text_field<'e>(expr: &'e Expr, seen: Option<&'e FieldRef>) -> Result> { + match &*expr.0 { + Node::Missing => Ok(seen), + Node::Field(f) => match seen { + Some(g) if g.name != f.name || g.from != f.from => { + Err(invalid("a label's text chooses between one text field and missing, not two fields")) + } + _ => Ok(Some(f)), + }, + Node::When(_, a, b) => text_field(b, text_field(a, seen)?), + _ => Err(invalid("a label's text must be a text field, or a when choosing between one and missing")), + } +} + fn mentions_pointer(e: &Expr) -> bool { e.any(&|e| matches!(&*e.0, Node::State(State::Pointer))) } diff --git a/crates/latkit/src/expr/cpu.rs b/crates/latkit/src/expr/cpu.rs index bd2889df..b29f1527 100644 --- a/crates/latkit/src/expr/cpu.rs +++ b/crates/latkit/src/expr/cpu.rs @@ -86,11 +86,15 @@ impl<'a> Rows<'a> { out } - /// The value of text field `expr` in `row`. + /// The text `expr` gives `row`: a text field's value, or the value a `when` chooses; `None` + /// where it is missing. pub(crate) fn text(&self, expr: &Expr, row: usize) -> Option> { - let Node::Field(f) = &*expr.0 else { return None }; - match self.value(f, row)? { - Value::Text(text) => Some(text), + match &*expr.0 { + Node::Field(f) => match self.value(f, row)? { + Value::Text(text) => Some(text), + _ => None, + }, + Node::When(c, a, b) => self.text(if self.eval(c, row) > 0.5 { a } else { b }, row), _ => None, } } @@ -234,4 +238,13 @@ mod tests { let scaled: Expr = field("w").scale(0.0..=1.0).into(); assert_eq!(rows.number(&scaled, 2), Some(1.0), "a fitted domain spans the values"); } + + #[test] + fn a_conditional_text_gives_its_field_or_nothing() { + let table = Table::new("T", Ids::count(2)).field("name", vec!["a", "b"]).field("on", vec![true, false]); + let rows = Rows::new(&table, &[]); + let text = when(field("on")).then(field("name")).otherwise(Expr::missing()); + assert_eq!(rows.text(&text, 0).as_deref(), Some("a")); + assert_eq!(rows.text(&text, 1), None, "a missing text is no text"); + } } diff --git a/crates/latkit/src/expr/form.rs b/crates/latkit/src/expr/form.rs index ea89e7f0..e2171721 100644 --- a/crates/latkit/src/expr/form.rs +++ b/crates/latkit/src/expr/form.rs @@ -1,24 +1,30 @@ -//! JSON forms of expressions, colors, sizes, and colormaps. A number or boolean is a constant, a -//! string is a field name, and an object is one node whose key names its kind. In a color option (a -//! [`Paint`]), a string is a CSS color instead, and a conditional's values are colors: +//! JSON forms of expressions, sizes, and colormaps. Every value reads by one rule: a number or +//! boolean is a constant, `null` is a missing value, a string starting with `$` reads the field it +//! names, any other string is a CSS color, and an object is one node whose key names its kind. A +//! call's one key names its operation and holds its arguments: //! -//! | Form | Builder | -//! | ------------------------------------------------------------- | --------------------------------- | -//! | `{ "field": "x", "from": ["data", "Table"], "step": true }` | `field("x").from_table(..).step()` | -//! | `{ "color": "#ffb000" }` | a color | -//! | `{ "state": "selected" }` | `selected()`, `hovered()`, … | -//! | `{ "missing": true }` | `Expr::missing()` | -//! | `{ "op": "add", "args": [a, b] }` | `a + b`, `a.min(b)`, `!a`, … | -//! | `{ "when": [[c, v], …], "otherwise": e }` | `when(c).then(v)…otherwise(e)` | -//! | `{ "is": [a, "text"] }`, `{ "contains": [a, "text"] }` | `a.is("text")`, `a.contains(..)` | -//! | `{ "scale": [0, 1], "value": a, "domain": [lo, hi] }` | `a.scale(..).domain(..)` | -//! | `{ "colormap": "viridis", "value": a, "domain": [lo, hi] }` | `a.colormap(..).domain(..)` | -//! | `{ "px": a }` (sizes only) | `px(a)` | -//! | `{ "units": a, "atLeast": 2, "atMost": 20 }` (sizes only) | `units(a).at_least(..)` | +//! | Form | Builder | +//! | ------------------------------------------------------------ | ---------------------------------- | +//! | `"$x"` | `field("x")` | +//! | `{ "field": "x", "from": ["data", "Table"], "step": true }` | `field("x").from_table(..).step()` | +//! | `"#ffb000"`, `"rebeccapurple"` | a [`Color`] | +//! | `null` | `Expr::missing()` | +//! | `{ "state": "selected" }` | `selected()`, `hovered()`, … | +//! | `{ "add": [a, b] }`, `{ "not": [a] }` | `a + b`, `!a`, `a.min(b)`, … | +//! | `{ "is": [a, "text"] }`, `{ "contains": [a, "text"] }` | `a.is("text")`, `a.contains(..)` | +//! | `{ "when": [[c, v], …], "otherwise": e }` | `when(c).then(v)…otherwise(e)` | +//! | `{ "scale": [0, 1], "value": a, "domain": [lo, hi] }` | `a.scale(..).domain(..)` | +//! | `{ "colormap": "viridis", "value": a, "domain": [lo, hi] }` | `a.colormap(..).domain(..)` | +//! | `{ "px": a }` (sizes only) | `px(a)` | +//! | `{ "units": a, "atLeast": 2, "atMost": 20 }` (sizes only) | `units(a).at_least(..)` | //! -//! An object may have only its kind's keys; a key of another kind is an error. A colormap is a -//! catalog name or `{ "kind": "sequential", "colors": ["black", [0.2, "red"], "white"] }`, where a -//! `[place, color]` pair places a color and `categories` names a categorical map's colors. +//! `neg`, `not`, `abs`, `sqrt`, `sin`, `cos`, `floor`, `ln`, and `exp` take one argument; `add`, +//! `sub`, `mul`, `div`, `rem`, `pow`, `min`, `max`, `lt`, `le`, `gt`, `ge`, `eq`, `ne`, `and`, and +//! `or` take two; `is` and `contains` take a value and a plain text, which is neither a field nor a +//! color. A field whose name starts with `$` is written `{ "field": "$x" }`. An object may have +//! only its kind's keys; a key of another kind is an error. A colormap is a catalog name or +//! `{ "kind": "sequential", "colors": ["black", [0.2, "red"], "white"] }`, where a `[place, color]` +//! pair places a color and `categories` names a categorical map's colors. use std::borrow::Cow; use std::fmt; @@ -27,9 +33,9 @@ use std::sync::Arc; use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema}; use serde::Deserialize; use serde::de::value::MapAccessDeserializer; -use serde::de::{self, Deserializer, IntoDeserializer, MapAccess, SeqAccess, Visitor}; +use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor}; -use super::{Binary, Color, Colormap, ColormapKind, Expr, Node, Paint, Size, State, Stop, Unary, Unit, node}; +use super::{Binary, Color, Colormap, ColormapKind, Expr, Node, Size, State, Stop, Unary, Unit, node}; use crate::Name; impl<'de> Deserialize<'de> for Expr { @@ -38,42 +44,21 @@ impl<'de> Deserialize<'de> for Expr { } } -impl<'de> Deserialize<'de> for Paint { - fn deserialize>(deserializer: D) -> Result { - Written::deserialize(deserializer)?.paint().map(Paint).map_err(de::Error::custom) - } -} - -/// The JSON of an expression, color, size, or label, before its meaning is known: a constant, a -/// string, or an object whose keys decide its kind once all are read. The caller decides what a -/// string means: a field name, or in a color option a CSS color. +/// The JSON of an expression, size, or label, before its kind is known: an expression already +/// read, or an object whose keys decide its kind once all are read. pub(crate) enum Written { - Constant(Expr), - Text(String), + Expr(Expr), Object(Box), } impl Written { - /// The expression this JSON describes; a string names a field. + /// The expression this JSON describes. pub(crate) fn expr(self) -> Result { match self { - Written::Constant(expr) => Ok(expr), - Written::Text(name) => Ok(super::field(name)), + Written::Expr(expr) => Ok(expr), Written::Object(form) => form.expr(), } } - - /// The color this JSON describes, as a color option reads it: a string is a CSS color, and a - /// conditional's values are colors. - pub(crate) fn paint(self) -> Result { - match self { - Written::Constant(expr) => Ok(expr), - Written::Text(css) => Color::css(&css) - .map(Expr::from) - .ok_or_else(|| format!("not a CSS color: {css:?}; for a field, use field({css:?})")), - Written::Object(form) => form.paint(), - } - } } impl<'de> Deserialize<'de> for Written { @@ -88,43 +73,72 @@ impl<'de> Visitor<'de> for Reading { type Value = Written; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("a number, a boolean, a text, or an expression object") + f.write_str("a number, a boolean, null, a field or a color, or an expression object") } fn visit_bool(self, b: bool) -> Result { - Ok(Written::Constant(b.into())) + Ok(Written::Expr(b.into())) } fn visit_i64(self, n: i64) -> Result { - Ok(Written::Constant((n as f64).into())) + Ok(Written::Expr((n as f64).into())) } fn visit_u64(self, n: u64) -> Result { - Ok(Written::Constant((n as f64).into())) + Ok(Written::Expr((n as f64).into())) } fn visit_f64(self, n: f64) -> Result { - Ok(Written::Constant(n.into())) + Ok(Written::Expr(n.into())) + } + + fn visit_unit(self) -> Result { + Ok(Written::Expr(Expr::missing())) } fn visit_str(self, text: &str) -> Result { - Ok(Written::Text(text.to_owned())) + string(text).map(Written::Expr).map_err(E::custom) } - fn visit_map>(self, map: A) -> Result { - Form::deserialize(MapAccessDeserializer::new(map)).map(|form| Written::Object(Box::new(form))) + fn visit_map>(self, mut map: A) -> Result { + let mut form = Form::default(); + while let Some(key) = map.next_key::()? { + form.read(&key, &mut map)?; + } + Ok(Written::Object(Box::new(form))) } } -/// Declares `Form` and `Form::held` from one list of keys, each with its JSON spelling. +/// The expression a string writes: `$name` reads field `name`, and any other string is a CSS +/// color. +fn string(text: &str) -> Result { + match text.strip_prefix('$') { + Some("") => Err("\"$\" names no field".to_owned()), + Some(name) => Ok(super::field(name)), + None => Color::css(text) + .map(Expr::from) + .ok_or_else(|| format!("{text:?} is not a CSS color; \"${text}\" reads field {text}")), + } +} + +/// Declares `Form` and how it reads, from one list of keys, each with its JSON spelling. `null` +/// leaves a key unset, except a key marked `as value`, which holds an expression and reads `null` +/// as a missing value. macro_rules! form { - ($($vis:vis $key:ident $json:literal: $ty:ty),+ $(,)?) => { - /// Every key that an expression object, a size, or label options can have; each kind - /// accepts only its own keys. A conditional's values are read as the caller decides. - #[derive(Deserialize)] - #[serde(deny_unknown_fields)] + (@read $map:ident, $ty:ty) => { + $map.next_value::>()? + }; + (@read $map:ident, $ty:ty, value) => { + Some($map.next_value::<$ty>()?) + }; + ($($vis:vis $key:ident $json:literal: $ty:ty $(as $how:ident)?),+ $(,)?) => { + /// Every key that an expression object, a size, or label options can have, and the call + /// that a key naming an operation makes; each kind accepts only its own keys. + #[derive(Default)] pub(crate) struct Form { - $(#[serde(rename = $json)] $vis $key: Option<$ty>,)+ + $($vis $key: Option<$ty>,)+ + /// The operation a key names, and the call it makes with the key's arguments. + call: Option<(&'static str, Expr)>, } impl Form { @@ -134,39 +148,50 @@ macro_rules! form { $(if self.$key.is_some() { held.push($json); })+ + if let Some((operation, _)) = &self.call { + held.push(*operation); + } held } + + /// Reads `key`'s value from `map`. Any other key names an operation, whose call it + /// makes. + fn read<'de, A: MapAccess<'de>>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> { + match key { + $($json => { + if self.$key.is_some() { + return Err(de::Error::custom(format!("{key} is given twice"))); + } + self.$key = form!(@read map, $ty $(, $how)?); + Ok(()) + })+ + _ => self.read_call(key, map), + } + } } }; } form! { field "field": Name, - color "color": Written, state "state": State, - missing "missing": bool, - op "op": String, - when "when": Vec<(Expr, Written)>, - is "is": (Expr, String), - contains "contains": (Expr, String), + when "when": Vec<(Expr, Expr)>, scale "scale": [f64; 2], colormap "colormap": Colormap, from "from": (Name, Name), step "step": bool, - args "args": Vec, - otherwise "otherwise": Written, - value "value": Expr, + otherwise "otherwise": Expr as value, + value "value": Expr as value, domain "domain": [f64; 2], - pub(crate) px "px": Expr, - pub(crate) units "units": Expr, + pub(crate) px "px": Expr as value, + pub(crate) units "units": Expr as value, pub(crate) at_least "atLeast": f32, pub(crate) at_most "atMost": f32, - pub(crate) text "text": Expr, + pub(crate) text "text": Expr as value, pub(crate) size "size": f32, - pub(crate) priority "priority": Expr, + pub(crate) color "color": Expr as value, pub(crate) max_width "maxWidth": f32, pub(crate) font "font": Arc, - pub(crate) max "max": u32, } impl Form { @@ -178,95 +203,78 @@ impl Form { } } - /// A label's color, as a color option reads it. - pub(crate) fn color(&mut self) -> Result, String> { - self.color.take().map(Written::paint).transpose() - } - - /// The color this object describes, as a color option reads it: a conditional's values are - /// colors, and any other object is an expression, which must produce a color. - pub(crate) fn paint(self) -> Result { - if self.when.is_none() { - return self.expr(); + /// Reads the arguments of the operation `key` names from `map`, and makes the call: values, or + /// for `is` and `contains`, a value and a text. + fn read_call<'de, A: MapAccess<'de>>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> { + let Some(&(name, operation, _)) = OPERATIONS.iter().find(|(name, ..)| *name == key) else { + return Err(de::Error::custom(format!("{key:?} names no option or operation"))); + }; + if let Some((first, _)) = &self.call { + return Err(de::Error::custom(format!("an object names one operation, not {first} and {name}"))); } - self.conditional(Written::paint) - } - - /// The conditional this object describes, each value read by `value`. - fn conditional(self, value: fn(Written) -> Result) -> Result { - self.only("when", &["when", "otherwise"])?; - let otherwise = value(self.otherwise.ok_or("when needs otherwise")?)?; - let cases = self.when.expect("present"); - cases.into_iter().rev().try_fold(otherwise, |rest, (c, v)| Ok(node(Node::When(c, value(v)?, rest)))) + let count = |want: usize, got: usize| -> A::Error { + let plural = if want == 1 { "" } else { "s" }; + de::Error::custom(format!("{name} takes {want} argument{plural}, not {got}")) + }; + let call = match operation { + Operation::Unary(op) => { + let [a] = <[Expr; 1]>::try_from(map.next_value::>()?).map_err(|a| count(1, a.len()))?; + node(Node::Unary(op, a)) + } + Operation::Binary(op) => { + let [a, b] = <[Expr; 2]>::try_from(map.next_value::>()?).map_err(|a| count(2, a.len()))?; + node(Node::Binary(op, a, b)) + } + Operation::Is => { + let (a, text): (Expr, String) = map.next_value()?; + a.is(&text) + } + Operation::Contains => { + let (a, text): (Expr, String) = map.next_value()?; + a.contains(&text) + } + }; + self.call = Some((name, call)); + Ok(()) } - /// The expression this object describes. Its kind is that of its first kind key, and it may - /// have only that kind's keys. - pub(crate) fn expr(self) -> Result { + /// The expression this object describes: the call its operation makes, or the node of its + /// first kind key. It may have only that kind's keys. + pub(crate) fn expr(mut self) -> Result { + if let Some((operation, call)) = self.call.take() { + self.only(operation, &[])?; + return Ok(call); + } let held = self.held(); let Some(own) = held.iter().find_map(|key| KINDS.iter().find(|keys| keys[0] == *key)) else { - let mut kinds: Vec<&str> = KINDS.iter().map(|keys| keys[0]).collect(); - kinds.dedup(); - return Err(format!("an expression object needs one of {}", kinds.join(", "))); + let kinds: Vec<&str> = KINDS.iter().map(|keys| keys[0]).collect(); + let found = if held.is_empty() { String::new() } else { format!("; found {}", held.join(", ")) }; + return Err(format!("an expression object needs an operation or one of {}{found}", kinds.join(", "))); }; let kind = own[0]; self.only(kind, own)?; - if kind == "when" { - return self.conditional(Written::expr); - } - let Form { field, color, state, missing, op, is, contains, scale, colormap, .. } = self; + let Form { field, state, when, scale, colormap, from, step, otherwise, value, domain, .. } = self; let needs = |key: &str| format!("{kind} needs {key}"); Ok(match kind { "field" => { let expr = super::field(field.expect("present")); - let expr = match self.from { + let expr = match from { Some((data, table)) => expr.from_table(data, table), None => expr, }; - if self.step == Some(true) { expr.step() } else { expr } + if step == Some(true) { expr.step() } else { expr } } - "color" => match color { - Some(Written::Text(css)) => Color::css(&css).ok_or_else(|| format!("not a CSS color: {css:?}"))?.into(), - _ => return Err("color must be a CSS color".to_owned()), - }, "state" => node(Node::State(state.expect("present"))), - "missing" => { - if missing != Some(true) { - return Err("missing must be true".to_owned()); - } - Expr::missing() - } - "op" => { - let op = op.expect("present"); - let args = self.args.ok_or_else(|| needs("args"))?; - let name = || IntoDeserializer::::into_deserializer(op.as_str()); - if let Ok(unary) = Unary::deserialize(name()) { - let [a] = - <[Expr; 1]>::try_from(args).map_err(|a| format!("{op} takes 1 argument, not {}", a.len()))?; - node(Node::Unary(unary, a)) - } else { - let binary = Binary::deserialize(name()).map_err(|_| format!("no op {op:?}"))?; - let [a, b] = - <[Expr; 2]>::try_from(args).map_err(|a| format!("{op} takes 2 arguments, not {}", a.len()))?; - node(Node::Binary(binary, a, b)) - } - } - "is" => { - let (a, text) = is.expect("present"); - a.is(&text) - } - "contains" => { - let (a, text) = contains.expect("present"); - a.contains(&text) + "when" => { + let otherwise = otherwise.ok_or_else(|| needs("otherwise"))?; + let cases = when.expect("present"); + cases.into_iter().rev().fold(otherwise, |rest, (c, v)| node(Node::When(c, v, rest))) } "scale" => { let [lo, hi] = scale.expect("present"); - domained(self.value.ok_or_else(|| needs("a value"))?.scale(lo..=hi), self.domain) - } - _ => { - let value = self.value.ok_or_else(|| needs("a value"))?; - domained(value.colormap(colormap.expect("present")), self.domain) + domained(value.ok_or_else(|| needs("a value"))?.scale(lo..=hi), domain) } + _ => domained(value.ok_or_else(|| needs("a value"))?.colormap(colormap.expect("present")), domain), }) } } @@ -279,6 +287,58 @@ fn domained(mapped: super::Mapped, domain: Option<[f64; 2]>) -> Expr { } } +/// What a call computes from its arguments. +#[derive(Clone, Copy)] +enum Operation { + Unary(Unary), + Binary(Binary), + Is, + Contains, +} + +/// Each operation a call can name: the key that names it, what it computes, and its description. +const OPERATIONS: [(&str, Operation, &str); 27] = [ + ("neg", Operation::Unary(Unary::Neg), "The value negated."), + ("not", Operation::Unary(Unary::Not), "1 where the value is at most 0.5, else 0."), + ("abs", Operation::Unary(Unary::Abs), "The absolute value."), + ("sqrt", Operation::Unary(Unary::Sqrt), "The square root. Negative values give a missing value."), + ("sin", Operation::Unary(Unary::Sin), "The sine of the value, an angle in radians."), + ("cos", Operation::Unary(Unary::Cos), "The cosine of the value, an angle in radians."), + ("floor", Operation::Unary(Unary::Floor), "The largest whole number at most the value."), + ("ln", Operation::Unary(Unary::Ln), "The natural logarithm. Values at most 0 give a missing value."), + ("exp", Operation::Unary(Unary::Exp), "e raised to the value."), + ("add", Operation::Binary(Binary::Add), "The sum of the two values."), + ("sub", Operation::Binary(Binary::Sub), "The first value minus the second."), + ("mul", Operation::Binary(Binary::Mul), "The product of the two values."), + ( + "div", + Operation::Binary(Binary::Div), + "The first value divided by the second. Dividing by 0 gives a missing value.", + ), + ( + "rem", + Operation::Binary(Binary::Rem), + "The remainder of the first value divided by the second, with the sign of the second: -1 and 3 give 2.", + ), + ("pow", Operation::Binary(Binary::Pow), "The first value raised to the power of the second."), + ("min", Operation::Binary(Binary::Min), "The smaller of the two values."), + ("max", Operation::Binary(Binary::Max), "The larger of the two values."), + ("lt", Operation::Binary(Binary::Lt), "1 where the first value is less than the second, else 0."), + ("le", Operation::Binary(Binary::Le), "1 where the first value is at most the second, else 0."), + ("gt", Operation::Binary(Binary::Gt), "1 where the first value is greater than the second, else 0."), + ("ge", Operation::Binary(Binary::Ge), "1 where the first value is at least the second, else 0."), + ("eq", Operation::Binary(Binary::Eq), "1 where the two values are equal, else 0. Colors compare too."), + ("ne", Operation::Binary(Binary::Ne), "1 where the two values differ, else 0. Colors compare too."), + ("and", Operation::Binary(Binary::And), "1 where both values are above 0.5, else 0."), + ("or", Operation::Binary(Binary::Or), "1 where either value is above 0.5, else 0."), + ("is", Operation::Is, "Whether a text field equals the plain text exactly."), + ( + "contains", + Operation::Contains, + "Whether a text field contains the plain text, ignoring case. In a mark, the field must belong to the mark's own table.", + ), +]; + /// Declares each kind of expression object once: a struct for its JSON schema, never built because /// reading goes through `Form`, and the keys an object of that kind may have, its own key first. macro_rules! kinds { @@ -305,7 +365,7 @@ macro_rules! kinds { kinds! { /// A field's value in each row: from the mark's own table, or with `from`, from another - /// dataset's table joined by row id. + /// dataset's table joined by row id. `"$name"` is the short form of `{ "field": "name" }`. FieldNode { /// The field's name. field: Name, @@ -318,35 +378,11 @@ kinds! { #[serde(default)] step: bool, } - /// A constant color. - ColorNode { - color: Color, - } /// View state of each row, such as whether it is selected. StateNode { /// The state read. state: State, } - /// A missing value. An option that evaluates to missing uses the theme's value. - MissingNode { - /// Always `true`. - #[schemars(extend("const" = true))] - missing: bool, - } - /// A one-argument operation, such as `{ "op": "abs", "args": ["x"] }`. - UnaryNode { - /// The operation. - op: Unary, - /// Its argument. - args: [Expr; 1], - } - /// A two-argument operation, such as `{ "op": "add", "args": ["a", 1] }`. - BinaryNode { - /// The operation. - op: Binary, - /// Its arguments, left and right. - args: [Expr; 2], - } /// The value of the first case whose condition holds, else `otherwise`. WhenNode { /// The cases, as `[condition, value]` pairs tried in order. @@ -355,19 +391,6 @@ kinds! { /// The value where no condition holds. otherwise: Expr, } - /// Whether a text field equals a text exactly. - IsNode { - /// The text field, and the text it must equal. - #[schemars(extend("tsType" = "[field: ExprInput, text: string]"))] - is: (Expr, String), - } - /// Whether a text field contains a text, ignoring case. In a mark, the field must belong to the - /// mark's own table. - ContainsNode { - /// The text field, and the text it must contain. - #[schemars(extend("tsType" = "[field: ExprInput, text: string]"))] - contains: (Expr, String), - } /// A value mapped linearly onto a range, clamped to the range. ScaleNode { /// The range. @@ -444,64 +467,67 @@ impl JsonSchema for ExprNode { } fn json_schema(generator: &mut SchemaGenerator) -> Schema { - let forms = kinds(generator); let mut any = vec![ json_schema!({ "type": "number" }), json_schema!({ "type": "boolean" }), - json_schema!({ "type": "string", "description": "A field's name." }), + json_schema!({ + "type": "null", + "description": "A missing value. An option that evaluates to missing uses the theme's value.", + }), + json_schema!({ + "type": "string", + "description": "A field's value in each row, or a color: `\"$kv\"` reads field `kv`, and any other string is a CSS color, such as `#ffb000` or `rebeccapurple`. A field whose name starts with `$` is written `{ \"field\": \"$x\" }`.", + }), ]; - any.extend(forms); + any.extend(kinds(generator)); + any.push(generator.subschema_for::()); json_schema!({ - "description": "A value for each row: a number, a boolean, a field's name, or a node that computes one.", + "description": "A value for each row. A number or boolean is a constant, `null` is a missing value, a string starting with `$` reads the field it names, any other string is a CSS color, and an object is a node that computes a value.", "anyOf": any, }) } } -/// A conditional color: the color of the first case whose condition holds, else `otherwise`. -#[derive(JsonSchema)] -#[expect(dead_code, reason = "used for its schema only; `Form` reads colors")] -#[serde(deny_unknown_fields)] -struct WhenPaintNode { - /// The cases, as `[condition, color]` pairs tried in order. - #[schemars(extend("tsType" = "[condition: ExprInput, color: PaintInput][]"))] - when: Vec<(Expr, Paint)>, - /// The color where no condition holds. - otherwise: Paint, -} - -/// A color option's schema is `PaintNode`; its TypeScript type also accepts the builders. -impl JsonSchema for Paint { - fn schema_name() -> Cow<'static, str> { - "Paint".into() - } - - fn json_schema(generator: &mut SchemaGenerator) -> Schema { - let mut schema = generator.subschema_for::(); - schema.insert("tsType".into(), "PaintInput".into()); - schema - } -} - -/// The JSON form of a color option. -struct PaintNode; +/// The JSON form of a call: an object whose one key names an operation and holds its arguments. +struct CallNode; -impl JsonSchema for PaintNode { +impl JsonSchema for CallNode { fn schema_name() -> Cow<'static, str> { - "PaintNode".into() + "CallNode".into() } fn json_schema(generator: &mut SchemaGenerator) -> Schema { - let any = vec![ - generator.subschema_for::(), - generator.subschema_for::(), - generator.subschema_for::(), - generator.subschema_for::(), - generator.subschema_for::(), - ]; + let value = generator.subschema_for::(); + let calls: Vec = OPERATIONS + .iter() + .map(|&(name, operation, description)| { + let mut arguments = match operation { + Operation::Unary(_) => { + json_schema!({ "type": "array", "prefixItems": [value], "minItems": 1, "maxItems": 1 }) + } + Operation::Binary(_) => { + json_schema!({ "type": "array", "prefixItems": [value, value], "minItems": 2, "maxItems": 2 }) + } + Operation::Is | Operation::Contains => json_schema!({ + "type": "array", + "prefixItems": [value, { "type": "string" }], + "minItems": 2, + "maxItems": 2, + "tsType": "[field: ExprInput, text: string]", + }), + }; + arguments.insert("description".into(), description.into()); + json_schema!({ + "type": "object", + "properties": { name: arguments }, + "required": [name], + "additionalProperties": false, + }) + }) + .collect(); json_schema!({ - "description": "A color for each row: a CSS color, a value through a colormap, or a conditional choosing between colors.", - "anyOf": any, + "description": "An operation on values: an object whose one key names the operation and holds its arguments, such as `{ \"add\": [\"$a\", 1] }`. `is` and `contains` take a plain text as their second argument, which is neither a field nor a color.", + "anyOf": calls, }) } } @@ -708,33 +734,41 @@ mod tests { serde_json::from_str(json).expect("an expression") } - fn paint(json: &str) -> Result { - serde_json::from_str::(json).map(|paint| paint.0).map_err(|e| e.to_string()) + fn error(json: &str) -> String { + serde_json::from_str::(json).expect_err("an error").to_string() } #[test] fn forms_match_builders() { - assert_eq!(parse("\"vm\""), field("vm")); - assert_eq!(parse("\"#ff0000\""), field("#ff0000"), "a text in a value is a field's name"); + assert_eq!(parse("\"$vm\""), field("vm")); assert_eq!(parse("2.5"), Expr::from(2.5)); - assert_eq!(parse(r#"{"op": "add", "args": ["a", 1]}"#), field("a") + 1.0); - assert_eq!(parse(r#"{"op": "not", "args": ["on"]}"#), !field("on")); + assert_eq!(parse("null"), Expr::missing()); + assert_eq!(parse(r#"{"add": ["$a", 1]}"#), field("a") + 1.0); + assert_eq!(parse(r#"{"not": ["$on"]}"#), !field("on")); + assert_eq!(parse(r#"{"min": ["$a", {"abs": ["$b"]}]}"#), field("a").min(field("b").abs())); assert_eq!( - parse(r##"{"when": [[{"state": "selected"}, {"color": "#ff0000"}]], "otherwise": "c"}"##), + parse(r##"{"when": [[{"state": "selected"}, "#ff0000"]], "otherwise": "$c"}"##), when(selected()).then(Color::rgb(1.0, 0.0, 0.0)).otherwise(field("c")) ); + assert_eq!( + parse(r#"{"when": [["$on", "$name"]], "otherwise": null}"#), + when(field("on")).then(field("name")).otherwise(Expr::missing()), + "a label's text where `on` holds, and none elsewhere" + ); assert_eq!( parse(r#"{"field": "vm", "from": ["weather", "Station"], "step": true}"#), field("vm").from_table("weather", "Station").step() ); assert_eq!( - parse(r#"{"colormap": "balance", "value": "vm", "domain": [0.9, 1.1]}"#), + parse(r#"{"colormap": "balance", "value": "$vm", "domain": [0.9, 1.1]}"#), field("vm").colormap(Colormap::BALANCE).domain(0.9..=1.1).into() ); - assert_eq!(parse(r#"{"scale": [2, 8], "value": "kv"}"#), field("kv").scale(2.0..=8.0).into()); - assert_eq!(parse(r#"{"contains": ["name", "lake"]}"#), field("name").contains("lake")); + assert_eq!(parse(r#"{"scale": [2, 8], "value": "$kv"}"#), field("kv").scale(2.0..=8.0).into()); + assert_eq!(parse(r#"{"is": ["$kind", "substation"]}"#), field("kind").is("substation")); + assert_eq!(parse(r#"{"contains": ["$name", "lake"]}"#), field("name").contains("lake")); let size = |json: &str| serde_json::from_str::(json).expect("a size"); assert_eq!(size("3"), Size::from(3.0)); + assert_eq!(size("\"$r\""), Size::from(field("r"))); assert_eq!(size(r#"{"px": 3}"#), crate::px(3.0)); assert_eq!(size(r#"{"units": 3}"#), crate::units(3.0)); assert_eq!(size(r#"{"units": 3, "atLeast": 2, "atMost": 20}"#), crate::units(3.0).at_least(2.0).at_most(20.0)); @@ -747,30 +781,47 @@ mod tests { } #[test] - fn a_text_in_a_color_is_a_css_color() { + fn a_string_with_a_dollar_reads_a_field_and_any_other_is_a_color() { let red = Expr::from(Color::rgb(1.0, 0.0, 0.0)); - assert_eq!(paint("\"red\""), Ok(red.clone())); - assert_eq!(paint("\"#ff0000\""), Ok(red.clone())); - let error = paint("\"kv\"").expect_err("a field's name"); - assert!(error.starts_with(r#"not a CSS color: "kv"; for a field, use field("kv")"#), "{error}"); - // A conditional's values are colors and its conditions are values; a colormap's input is a - // value. - let chosen = paint(r#"{"when": [["on", "red"]], "otherwise": {"colormap": "viridis", "value": "kv"}}"#); - let mapped = field("kv").colormap(Colormap::VIRIDIS); - assert_eq!(chosen, Ok(when(field("on")).then(Color::rgb(1.0, 0.0, 0.0)).otherwise(mapped))); - assert_eq!(paint(r#"{"field": "kv"}"#), Ok(field("kv")), "a field parses here and must compile to a color"); + assert_eq!(parse("\"red\""), red); + assert_eq!(parse("\"#ff0000\""), red); + assert_eq!(parse("\"$#ff0000\""), field("#ff0000"), "after the `$`, any text names a field"); + assert_eq!(parse(r#"{"field": "$x"}"#), field("$x"), "the long form names any field"); + let wrong = error("\"level\""); + assert!(wrong.starts_with(r#""level" is not a CSS color; "$level" reads field level"#), "{wrong}"); + assert!(error("\"$\"").contains("names no field")); + // A call's text is plain: neither a field nor a color. + assert_eq!(parse(r#"{"is": ["$kind", "$red"]}"#), field("kind").is("$red")); + assert_eq!(parse(r#"{"contains": ["$name", "level"]}"#), field("name").contains("level")); + } + + #[test] + fn null_is_a_missing_value_wherever_a_value_goes() { + let chosen = parse(r#"{"when": [["$on", null]], "otherwise": "$v"}"#); + assert_eq!(chosen, when(field("on")).then(Expr::missing()).otherwise(field("v"))); + assert_eq!(parse(r#"{"max": ["$v", null]}"#), field("v").max(Expr::missing())); + assert_eq!( + parse(r#"{"colormap": "viridis", "value": null}"#), + Expr::missing().colormap(Colormap::VIRIDIS).into() + ); + let size = serde_json::from_str::(r#"{"px": null}"#).expect("a size"); + assert_eq!(size, crate::px(Expr::missing())); + // An option of a node that is no value stays unset. + assert_eq!(parse(r#"{"field": "v", "from": null}"#), field("v")); } #[test] fn errors_name_the_offending_part() { - let error = serde_json::from_str::(r#"{"field": "a", "color": "red"}"#).expect_err("two kinds"); - assert!(error.to_string().contains("color"), "{error}"); - let error = serde_json::from_str::(r#"{"field": "a", "map": "viridis"}"#).expect_err("a stray key"); - assert!(error.to_string().contains("map"), "{error}"); - let error = serde_json::from_str::(r#"{"op": "frob", "args": []}"#).expect_err("unknown op"); - assert!(error.to_string().contains("frob"), "{error}"); - let error = serde_json::from_str::(r#"{"op": "add", "args": ["a"]}"#).expect_err("two arguments"); - assert!(error.to_string().contains('2'), "{error}"); + assert!(error(r#"{"field": "a", "state": "selected"}"#).contains("field takes no state")); + assert!(error(r#"{"field": "a", "map": "viridis"}"#).contains("map")); + assert!(error(r#"{"frob": []}"#).contains("frob")); + let arguments = error(r#"{"add": ["$a"]}"#); + assert!(arguments.contains("add takes 2 arguments, not 1"), "{arguments}"); + assert!(error(r#"{"add": ["$a", 1], "field": "b"}"#).contains("add takes no field")); + assert!(error(r#"{"add": [1, 2], "mul": [3, 4]}"#).contains("one operation")); + assert!(error(r#"{"color": "red"}"#).contains("found color"), "a color is a string, not a node"); + assert!(error(r#"{"missing": true}"#).contains("missing")); + assert!(error(r#"{"op": "add", "args": ["$a", 1]}"#).contains("op")); let error = serde_json::from_str::(r#"{"kind": "cyclic", "colors": [[0.5, "red"], [0.2, "blue"]]}"#) .expect_err("descending places"); assert!(error.to_string().contains("ascend"), "{error}"); diff --git a/crates/latkit/src/expr/mod.rs b/crates/latkit/src/expr/mod.rs index dd10ffb5..09181d29 100644 --- a/crates/latkit/src/expr/mod.rs +++ b/crates/latkit/src/expr/mod.rs @@ -19,9 +19,9 @@ pub use colormap::{Colormap, ColormapKind, Stop}; use crate::Name; -/// A per-row value of a mark: a constant, a field, view state, or arithmetic on them. -/// -/// A `&str` converts to the field of that name, so options can take field names directly: +/// A per-row value of a mark: a constant, a color, a field, view state, or arithmetic on them. +/// Color options take an expression that computes a color: a [`Color`], a value through a +/// colormap, or a conditional choosing between colors. /// /// ``` /// # use latkit::{field, when, selected, Colormap, Color}; @@ -31,22 +31,11 @@ use crate::Name; /// let high_voltage = field("kv").ge(230.0) & field("kind").is("substation"); /// ``` /// -/// Expressions compile to bytecode, so changing one never recompiles a shader. +/// Expressions compile to bytecode, so changing one never recompiles a shader. In JSON, a string +/// starting with `$` reads the field it names, and any other string is a CSS color. #[derive(Clone, PartialEq, Debug)] pub struct Expr(pub(crate) Arc); -/// A per-row color of a mark: a fixed color, or an expression that yields one, such as a value -/// through a colormap or a conditional choosing between colors. Color options take a `Paint`. In -/// JSON a string is a CSS color, and a field is written `{"field": name}`. -/// -/// ``` -/// # use latkit::{field, Color, Colormap, Paint}; -/// let fixed = Paint::from(Color::css("rebeccapurple").unwrap()); -/// let mapped = Paint::from(field("vm").colormap(Colormap::BALANCE)); -/// ``` -#[derive(Clone, PartialEq, Debug)] -pub struct Paint(pub(crate) Expr); - #[derive(PartialEq, Debug)] pub(crate) enum Node { Number(f64), @@ -89,8 +78,7 @@ pub(crate) enum State { /// An operation on one value. `sin` and `cos` take radians; `not` treats values above 0.5 as /// true. -#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Deserialize, schemars::JsonSchema)] -#[serde(rename_all = "lowercase")] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) enum Unary { Neg, Not, @@ -106,8 +94,7 @@ pub(crate) enum Unary { /// An operation on two values: arithmetic, `min` and `max`, comparisons (1 for true, 0 for /// false), and logic. `rem` is a floored remainder, which takes the sign of the divisor. `and` and /// `or` treat values above 0.5 as true. -#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Deserialize, schemars::JsonSchema)] -#[serde(rename_all = "lowercase")] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) enum Binary { Add, Sub, @@ -150,7 +137,7 @@ pub(crate) fn holds(text: &str, needle: &str) -> bool { text.to_lowercase().contains(needle) } -/// The value of field `name` in each row. +/// The value of field `name` in each row, written `"$name"` in JSON. pub fn field(name: impl Into) -> Expr { node(Node::Field(FieldRef { name: name.into(), from: None, step: false, component: None })) } @@ -219,7 +206,7 @@ macro_rules! binary { } impl Expr { - /// A missing value. An option that evaluates to missing uses the theme's value. + /// A missing value, `null` in JSON. An option that evaluates to missing uses the theme's value. pub fn missing() -> Expr { node(Node::Missing) } @@ -437,24 +424,6 @@ impl From for Expr { } } -impl From for Paint { - fn from(mapped: Mapped) -> Paint { - Paint(mapped.0) - } -} - -impl From for Paint { - fn from(expr: Expr) -> Paint { - Paint(expr) - } -} - -impl From for Paint { - fn from(color: Color) -> Paint { - Paint(color.into()) - } -} - /// A conditional started by [`when`], waiting for [`then`](When::then) to give the value for its /// latest condition. pub struct When { @@ -543,24 +512,6 @@ impl> From for Size { } } -impl From<&str> for Expr { - fn from(name: &str) -> Expr { - field(name) - } -} - -impl From for Expr { - fn from(name: String) -> Expr { - field(name) - } -} - -impl From<&Name> for Expr { - fn from(name: &Name) -> Expr { - field(name) - } -} - impl From for Expr { fn from(value: f64) -> Expr { node(Node::Number(value)) diff --git a/crates/latkit/src/gpu/atlas.rs b/crates/latkit/src/gpu/atlas.rs index f32b74d6..66a50256 100644 --- a/crates/latkit/src/gpu/atlas.rs +++ b/crates/latkit/src/gpu/atlas.rs @@ -1,11 +1,8 @@ //! The text atlas. The host rasterizes each run once at `TEXT_BASE`, and the GPU turns it into a //! distance field reaching `TEXT_SPREAD` texels past the text's edge, packed on shelves of one //! texture. A distance field draws crisp at any size and also yields halos, so a run is rasterized -//! once regardless of the size it shows at. -//! -//! Until a run is rasterized, its size is estimated from its font's character advances, each -//! measured by the host once, so its label is placed where it will show. The GPU builds the fields -//! of every run rasterized in a frame in one batch before the frame draws (`compute/fields.wesl`). +//! once regardless of the size it shows at. The GPU builds the fields of every run rasterized in a +//! frame in one batch before the frame draws (`compute/fields.wesl`). //! //! When no shelf has room, the least recently used shelf of a fitting height is cleared for the new //! run; only when none fits does packing start over. A run counts as used when it is rasterized and @@ -15,8 +12,6 @@ use std::collections::HashMap; use std::sync::Arc; -use rustc_hash::FxHashMap; - use crate::gpu::grown::Grown; use crate::gpu::pipelines::{Pipelines, Variant}; use crate::gpu::shaders::{FIELD_AT, FIELD_COLUMNS, FIELD_COVERAGE, FIELD_HEIGHT, FIELD_ROW, FIELD_WIDTH, FIELD_WORDS}; @@ -54,37 +49,6 @@ impl Glyphs { } } -/// A font's text metrics at the base size, as the host measures them: the size of an empty run, -/// the height each extra line adds, and each character's advance, measured once per character. -struct Metrics { - empty: [f32; 2], - line: f32, - advances: FxHashMap, -} - -impl Metrics { - /// Measures `font` with `host`. - fn of(host: &dyn Host, font: &Arc) -> Metrics { - let measure = |text: &str, max_width: Option| { - host.measure(&TextRun { text: text.into(), font: font.clone(), size_px: TEXT_BASE as u32, max_width }) - .unwrap_or([0.0, 1.25 * TEXT_BASE]) - }; - let empty = measure("", None); - let line = measure("M M", Some(1))[1] - measure("M", None)[1]; - Metrics { empty, line, advances: FxHashMap::default() } - } - - /// The advance of `c`, measured by `host` on first use. - fn advance(&mut self, host: &dyn Host, font: &Arc, c: char) -> f32 { - let empty = self.empty[0]; - *self.advances.entry(c).or_insert_with(|| { - let run = - TextRun { text: c.to_string().into(), font: font.clone(), size_px: TEXT_BASE as u32, max_width: None }; - host.measure(&run).map_or(0.58 * TEXT_BASE, |[w, _]| w - empty) - }) - } -} - /// The run the atlas rasterizes for `text` shown at `size`, wrapping at `max_width`, both in length /// units. The run is set at the base size, with `max_width` scaled to match. A sheet measures the /// same run it draws. @@ -128,8 +92,6 @@ pub(crate) struct Atlas { pub(crate) evictions: u64, /// Counts the texts the host is asked to rasterize. counters: Arc, - /// Text metrics by font. - metrics: HashMap, Metrics>, } impl Atlas { @@ -157,41 +119,9 @@ impl Atlas { clock: 0, evictions: 0, counters, - metrics: HashMap::new(), } } - /// The run table entry for `run` before it is rasterized: the `TEXT_UNSET` flag, then its - /// estimated size as a corner word (width in the low 16 bits, height in the high). Lines break - /// at spaces as the host breaks them, each as wide as its characters' advances, so the label is - /// placed where it will show while only visible labels have their text rasterized. `[0, 0]` - /// without a host. - pub(crate) fn unset(&mut self, host: Option<&dyn Host>, run: &TextRun) -> [u32; 2] { - let Some(host) = host else { return [0, 0] }; - let font = &run.font; - let metrics = self.metrics.entry(font.clone()).or_insert_with(|| Metrics::of(host, font)); - let mut width = |text: &str| text.chars().map(|c| metrics.advance(host, font, c)).sum::(); - let (widest, lines) = match run.max_width.map(|w| w as f32) { - None => (width(&run.text), 1), - Some(max) => { - let space = width(" "); - let mut lines: Vec = Vec::new(); - for word in run.text.split(' ') { - let w = width(word); - match lines.last_mut() { - Some(line) if *line + space + w <= max => *line += space + w, - _ => lines.push(w), - } - } - (lines.iter().copied().fold(0.0, f32::max), lines.len()) - } - }; - let [pad, height] = metrics.empty; - let w = ((widest + pad).ceil() as u32 + 2 * PAD).min(0xffff); - let h = ((height + (lines - 1) as f32 * metrics.line).ceil() as u32 + 2 * PAD).min(0xffff); - [crate::gpu::shaders::TEXT_UNSET, w | h << 16] - } - /// Starts a frame. Runs requested from now on count as used in it. pub(crate) fn tick(&mut self) { self.clock += 1; @@ -219,13 +149,12 @@ impl Atlas { placed.map(|(glyphs, _)| glyphs) } - /// Forgets everything the host made: runs, failures, and font metrics. A new host rasterizes - /// texts again in its own fonts. + /// Forgets everything the host made: runs and failures. A new host rasterizes texts again in its + /// own fonts. pub(crate) fn rehost(&mut self) { self.runs.clear(); self.waiting.clear(); self.shelves.clear(); - self.metrics.clear(); self.evictions += 1; } diff --git a/crates/latkit/src/gpu/mod.rs b/crates/latkit/src/gpu/mod.rs index 5a584523..29c45984 100644 --- a/crates/latkit/src/gpu/mod.rs +++ b/crates/latkit/src/gpu/mod.rs @@ -151,7 +151,7 @@ pub(crate) struct Ledger { /// any view drawing the same data. #[derive(Clone, Default, PartialEq, Debug)] pub(crate) struct Ask { - /// Bytes no other view holds: its marks' state, focus, labels, traces, and derived columns. + /// Bytes no other view holds: its marks' state, focus, traces, and derived columns. pub(crate) own: f64, /// Each shared piece, keyed by content, with its bytes. pub(crate) shared: Vec<(u64, f64)>, diff --git a/crates/latkit/src/gpu/work.rs b/crates/latkit/src/gpu/work.rs index cbab5e5c..5f6e1fd0 100644 --- a/crates/latkit/src/gpu/work.rs +++ b/crates/latkit/src/gpu/work.rs @@ -75,8 +75,8 @@ work! { /// Rows packed into pages of a paged table's draw order. Each page is packed once, the first /// time a view draws it, and again only when a write changes its rows. packed: Packed, - /// Readbacks from the GPU: probes, queries, fits, label placements, segment counts, and - /// pictures. Frame timer readbacks, which only some devices support, are not counted. + /// Readbacks from the GPU: probes, queries, fits, label texts, segment counts, and pictures. + /// Frame timer readbacks, which only some devices support, are not counted. readbacks: Readbacks, /// Jobs started: layouts, routes, lanes, triangulations, pyramids, and tiles. They run on the /// host's workers, or immediately without a host. diff --git a/crates/latkit/src/host/job.rs b/crates/latkit/src/host/job.rs index ab672f14..c7345538 100644 --- a/crates/latkit/src/host/job.rs +++ b/crates/latkit/src/host/job.rs @@ -215,7 +215,7 @@ impl Job { } Layout::Layered(layered) => { let mut out = job(Kind::Layered); - out.u32(layered.flow.code()); + out.u32(layered.direction.code()); out.f64(layered.gap); out.f64(layered.rank_gap); out @@ -401,8 +401,8 @@ fn run_layout(kind: Kind, input: &mut In<'_>) -> Option { let options = if kind == Kind::Stress { Layout::Stress(crate::Stress { gap: input.f64()? }) } else { - let flow = Direction::from_code(input.u32()?)?; - Layout::Layered(Layered { flow, gap: input.f64()?, rank_gap: input.f64()? }) + let direction = Direction::from_code(input.u32()?)?; + Layout::Layered(Layered { direction, gap: input.f64()?, rank_gap: input.f64()? }) }; let count = input.u32()? as usize; let (mut sizes, mut pinned, mut groups, mut pads, mut keys) = diff --git a/crates/latkit/src/layout/layered.rs b/crates/latkit/src/layout/layered.rs index c345018d..a87c962e 100644 --- a/crates/latkit/src/layout/layered.rs +++ b/crates/latkit/src/layout/layered.rs @@ -11,14 +11,14 @@ const ASPECT: f64 = 16.0 / 9.0; const SWEEPS: usize = 8; /// Every vertex's center from a layered layout. Pinned vertices stay put. The others go in ranks -/// along `options.flow` (`Direction::Up` is toward +y), `options.gap` apart within a rank and +/// along `options.direction` (`Direction::Up` is toward +y), `options.gap` apart within a rank and /// `options.rank_gap` between ranks, ordered to reduce crossings. Links are directed; cycles are /// broken. /// /// Each connected part is laid out separately, and parts with no pinned vertex pack into rows -/// (aspect 16:9) below the pinned ones. Across the flow, each vertex lines up with its sources; in -/// a part with no pinned vertex, each source then centers among the vertices it fans out to. Groups -/// nest as [`nested`] lays them out. +/// (aspect 16:9) below the pinned ones. Across the direction, each vertex lines up with its +/// sources; in a part with no pinned vertex, each source then centers among the vertices it fans +/// out to. Groups nest as [`nested`] lays them out. pub(crate) fn layered(graph: &Graph<'_>, options: &Layered) -> Vec<[f64; 2]> { nested(graph, |level| arrange(level, options)) } @@ -39,7 +39,7 @@ fn arrange(level: &Level, options: &Layered) -> Vec<[f64; 2]> { } } let vertex = |i: u32| i as usize; - let flow = options.flow; + let direction = options.direction; let mut anchored: Option<[f64; 4]> = None; let mut loose: Vec<(usize, [f64; 4])> = Vec::new(); for p in 0..count { @@ -48,14 +48,14 @@ fn arrange(level: &Level, options: &Layered) -> Vec<[f64; 2]> { if free > 0 && part_members.len() == 1 { centers[vertex(part_members[0])] = [0.0; 2]; } else if free > 0 { - let extents: Vec<[f64; 2]> = part_members.iter().map(|&i| extent(flow, sizes[vertex(i)])).collect(); + let extents: Vec<[f64; 2]> = part_members.iter().map(|&i| extent(direction, sizes[vertex(i)])).collect(); let fixed: Vec> = - part_members.iter().map(|&i| pins[vertex(i)].map(|pin| to_flow(flow, pin))).collect(); + part_members.iter().map(|&i| pins[vertex(i)].map(|pin| to_ranks(direction, pin))).collect(); let part_links: Vec<[u32; 2]> = joins.get(p).iter().map(|&[from, to]| [slot[from as usize], slot[to as usize]]).collect(); let placed = ranked(&extents, &fixed, &part_links, options.gap, options.rank_gap); for (&i, &at) in part_members.iter().zip(&placed) { - centers[vertex(i)] = from_flow(flow, at); + centers[vertex(i)] = from_ranks(direction, at); } } for &i in part_members { @@ -124,10 +124,11 @@ fn parts(count: usize, links: &[[u32; 2]]) -> (Vec, usize) { (part, parts) } -/// Plane coordinates rotated so `flow` runs along the first axis: `[along, across]`. Across runs -/// down for a sideways flow, and right for an upward or downward one. -fn to_flow(flow: Direction, [x, y]: [f64; 2]) -> [f64; 2] { - match flow { +/// Rank coordinates of plane coordinates: rotated so `direction` runs along the first axis, +/// `[along, across]`. Across runs down for a sideways direction, and right for an upward or +/// downward one. +fn to_ranks(direction: Direction, [x, y]: [f64; 2]) -> [f64; 2] { + match direction { Direction::Right => [x, -y], Direction::Left => [-x, -y], Direction::Down => [-y, x], @@ -135,9 +136,9 @@ fn to_flow(flow: Direction, [x, y]: [f64; 2]) -> [f64; 2] { } } -/// Plane coordinates of flow coordinates `[along, across]`; undoes [`to_flow`]. -fn from_flow(flow: Direction, [along, across]: [f64; 2]) -> [f64; 2] { - match flow { +/// Plane coordinates of rank coordinates `[along, across]`; undoes [`to_ranks`]. +fn from_ranks(direction: Direction, [along, across]: [f64; 2]) -> [f64; 2] { + match direction { Direction::Right => [along, -across], Direction::Left => [-along, -across], Direction::Down => [across, -along], @@ -145,16 +146,16 @@ fn from_flow(flow: Direction, [along, across]: [f64; 2]) -> [f64; 2] { } } -/// A width and height as a length along `flow` and a span across it. -fn extent(flow: Direction, [width, height]: [f64; 2]) -> [f64; 2] { - match flow { +/// A width and height as a length along `direction` and a span across it. +fn extent(direction: Direction, [width, height]: [f64; 2]) -> [f64; 2] { + match direction { Direction::Right | Direction::Left => [width, height], Direction::Down | Direction::Up => [height, width], } } -/// One part's centers in flow coordinates (see [`to_flow`]). `extents` are each vertex's length -/// along the flow and span across it, `pins` the centers of pinned vertices, and `links` are +/// One part's centers in rank coordinates (see [`to_ranks`]). `extents` are each vertex's length +/// along the direction and span across it, `pins` the centers of pinned vertices, and `links` are /// directed. fn ranked( extents: &[[f64; 2]], @@ -342,11 +343,11 @@ impl Crossings { } } -/// Each vertex's center in flow coordinates. Ranks run `rank_gap` apart along the flow, each -/// vertex centered in its rank's band. Across the flow, each vertex lines up with the median of -/// its neighbors in earlier ranks, unless that brings it within `gap` of the previous vertex in its -/// rank. A rank holding pinned vertices starts where they do, and vertices step past pinned ones -/// they would come within `gap` of. +/// Each vertex's center in rank coordinates. Ranks run `rank_gap` apart along the direction, each +/// vertex centered in its rank's band. Across the direction, each vertex lines up with the median +/// of its neighbors in earlier ranks, unless that brings it within `gap` of the previous vertex in +/// its rank. A rank holding pinned vertices starts where they do, and vertices step past pinned +/// ones they would come within `gap` of. fn coordinates( extents: &[[f64; 2]], pins: &[Option<[f64; 2]>], @@ -359,7 +360,7 @@ fn coordinates( let length_of = |vertex: u32| extents[vertex as usize][0]; let bands: Vec = (0..layers.len()).map(|k| layers.get(k).iter().map(|&i| length_of(i)).fold(0.0, f64::max)).collect(); - // Where a rank's pinned vertices begin along the flow. + // Where a rank's pinned vertices begin along the direction. let lead = |k: usize| { layers.get(k).iter().filter_map(|&i| pins[i as usize].map(|pin| pin[0] - length_of(i) / 2.0)).reduce(f64::min) }; @@ -409,9 +410,9 @@ fn coordinates( centers } -/// Moves each vertex across the flow toward the median of its neighbors in later ranks, last ranks -/// first, as far as its neighbors in its rank allow at `gap` apart. A source centers among the -/// vertices it fans out to, and a chain stays in line. +/// Moves each vertex across the direction toward the median of its neighbors in later ranks, last +/// ranks first, as far as its neighbors in its rank allow at `gap` apart. A source centers among +/// the vertices it fans out to, and a chain stays in line. fn balance(centers: &mut [[f64; 2]], extents: &[[f64; 2]], layers: &Runs, below: &Runs, gap: f64) { let mut across: Vec = Vec::new(); let mut goals: Vec> = Vec::new(); @@ -457,11 +458,11 @@ fn balance(centers: &mut [[f64; 2]], extents: &[[f64; 2]], layers: &Runs, b } } -/// Placed boxes in flow coordinates, bucketed in square cells to find those near a box quickly. +/// Placed boxes in rank coordinates, bucketed in square cells to find those near a box quickly. struct Obstacles { /// A cell's side. cell: f64, - /// The clearance boxes keep between them, along the flow and across it. + /// The clearance boxes keep between them, along the direction and across it. room: [f64; 2], /// Each box: its least along and across, then its greatest. boxes: Vec<[f64; 4]>, @@ -533,8 +534,8 @@ mod tests { const SIZE: [f64; 2] = [20.0, 10.0]; - fn options(flow: Direction) -> Layered { - Layered { flow, gap: 10.0, rank_gap: 40.0 } + fn options(direction: Direction) -> Layered { + Layered { direction, gap: 10.0, rank_gap: 40.0 } } /// A graph's columns, owned. @@ -591,7 +592,7 @@ mod tests { (a - b).abs() < 1e-9 } - /// How many edge pairs cross between the same two ranks of a rightward flow. + /// How many edge pairs cross between the same two ranks of a rightward layout. fn crossings(edges: &[[u32; 2]], centers: &[[f64; 2]]) -> usize { let mut count = 0; for (k, first) in edges.iter().enumerate() { @@ -608,11 +609,11 @@ mod tests { } #[test] - fn a_chain_ranks_along_the_flow_in_order() { + fn a_chain_ranks_along_the_direction_in_order() { let graph = Columns::new(4, &[[2, 3], [0, 1], [1, 2]]); - for flow in [Direction::Right, Direction::Left, Direction::Down, Direction::Up] { - let centers = graph.layout(&options(flow)); - let (along, across, sign) = match flow { + for direction in [Direction::Right, Direction::Left, Direction::Down, Direction::Up] { + let centers = graph.layout(&options(direction)); + let (along, across, sign) = match direction { Direction::Right => (0, 1, 1.0), Direction::Left => (0, 1, -1.0), Direction::Down => (1, 0, -1.0), @@ -621,8 +622,8 @@ mod tests { let length = SIZE[along]; for pair in centers.windows(2) { let step = (pair[1][along] - pair[0][along]) * sign; - assert!(near(step, length + 40.0), "{flow:?}: ranks a rank gap apart: {centers:?}"); - assert!(near(pair[1][across], pair[0][across]), "{flow:?}: in line with its source: {centers:?}"); + assert!(near(step, length + 40.0), "{direction:?}: ranks a rank gap apart: {centers:?}"); + assert!(near(pair[1][across], pair[0][across]), "{direction:?}: in line with its source: {centers:?}"); } } } @@ -733,7 +734,7 @@ mod tests { #[test] fn large_graphs_lay_out_without_overlaps() { - // Fifty groups of twenty, and edges joining random vertices, many against the flow. + // Fifty groups of twenty, and edges joining random vertices, many against the direction. let count = 10_000; let mut seed = 0x9e37_79b9_u32; let mut random = move |below: usize| { diff --git a/crates/latkit/src/lib.rs b/crates/latkit/src/lib.rs index 58ce704e..e17f9e78 100644 --- a/crates/latkit/src/lib.rs +++ b/crates/latkit/src/lib.rs @@ -61,7 +61,7 @@ pub use data::{ pub use data::{Order, Patches, Query, QueryResult, Reading}; pub use error::{Error, Path, Result, Step}; pub use expr::{ - Color, Colormap, ColormapKind, Expr, Mapped, Paint, Size, Stop, Then, When, elapsed, field, hovered, pointer, px, + Color, Colormap, ColormapKind, Expr, Mapped, Size, Stop, Then, When, elapsed, field, hovered, pointer, px, selected, units, when, zoom, }; pub use image::{Image, ImageFormat}; diff --git a/crates/latkit/src/scene/areas.rs b/crates/latkit/src/scene/areas.rs index 36a01353..4fe426a7 100644 --- a/crates/latkit/src/scene/areas.rs +++ b/crates/latkit/src/scene/areas.rs @@ -48,7 +48,7 @@ impl Areas { } let color = Store::Color { word: wgsl::AREA_COLOR, fallback: wgsl::THEME_AREA }; match &self.style.color { - Some(paint) => c.channel("color", &paint.0, color)?, + Some(expr) => c.channel("color", expr, color)?, None => c.theme("color", wgsl::THEME_AREA, color)?, } c.channel("visible", &self.style.visible, Store::Flag { word: wgsl::AREA_FLAGS, bit: wgsl::VISIBLE })?; diff --git a/crates/latkit/src/scene/controls.rs b/crates/latkit/src/scene/controls.rs index 10b7b6d2..726af22f 100644 --- a/crates/latkit/src/scene/controls.rs +++ b/crates/latkit/src/scene/controls.rs @@ -52,10 +52,9 @@ pub struct Controls { #[serde(deserialize_with = "seconds")] #[schemars(with = "f64")] pub press: Duration, - /// How much the view animates on its own: eases, camera flights, coasting, orbits, label fades, - /// comets, and options that read `elapsed()`. `system` (the default) follows the user's - /// reduced-motion setting, `full` animates everything, and `reduced` animates only motion the - /// user drives. + /// How much the view animates on its own: eases, camera flights, coasting, orbits, comets, and + /// options that read `elapsed()`. `system` (the default) follows the user's reduced-motion + /// setting, `full` animates everything, and `reduced` animates only motion the user drives. pub motion: Motion, } @@ -83,7 +82,7 @@ pub enum Motion { /// All motion. Full, /// Only motion the user drives: changes apply at once, flights jump, nothing coasts or orbits, - /// labels appear without fading, and comets and options that read `elapsed()` stay still. + /// and comets and options that read `elapsed()` stay still. Reduced, } diff --git a/crates/latkit/src/scene/edges.rs b/crates/latkit/src/scene/edges.rs index 27ad0794..3b6ad050 100644 --- a/crates/latkit/src/scene/edges.rs +++ b/crates/latkit/src/scene/edges.rs @@ -123,7 +123,7 @@ impl Edges { let mut flags = if self.arrows { wgsl::ARROWS } else { 0 }; let color = Store::Color { word: wgsl::EDGE_COLOR, fallback: wgsl::THEME_EDGE }; match (&self.style.color, context.theme.edge.color) { - (Some(paint), _) => c.channel("color", &paint.0, color)?, + (Some(expr), _) => c.channel("color", expr, color)?, (None, Some(_)) => c.theme("color", wgsl::THEME_EDGE, color)?, // A diagram's wires, and edges without ends to blend (nets and poles), take the text // color. diff --git a/crates/latkit/src/scene/faces.rs b/crates/latkit/src/scene/faces.rs index 179bd150..b49bc674 100644 --- a/crates/latkit/src/scene/faces.rs +++ b/crates/latkit/src/scene/faces.rs @@ -41,8 +41,8 @@ impl Faces { } let mut flags = 0; match &self.style.color { - Some(paint) => { - c.channel("color", &paint.0, Store::Color { word: wgsl::FACE_COLOR, fallback: wgsl::THEME_VERTEX })?; + Some(expr) => { + c.channel("color", expr, Store::Color { word: wgsl::FACE_COLOR, fallback: wgsl::THEME_VERTEX })?; } None => flags |= wgsl::ENDS_COLOR, } diff --git a/crates/latkit/src/scene/label.rs b/crates/latkit/src/scene/label.rs index 65158074..eaedaf1a 100644 --- a/crates/latkit/src/scene/label.rs +++ b/crates/latkit/src/scene/label.rs @@ -4,23 +4,29 @@ use schemars::JsonSchema; use serde::Deserialize; use serde::de::{self, Deserializer}; +use crate::Expr; use crate::expr::form::Written; -use crate::{Expr, Paint}; /// Text for each row, placed beside, inside, or along the row's shape and following it every frame: -/// a text field, or options. Where labels would overlap, the higher priority shows; among equals, -/// labels already shown stay. In a diagram a label is part of the drawing: it scales with the zoom, -/// fades out when too small to read, and shows wherever it fits its vertex. +/// a text field, or options. A label shows wherever its row shows and its text fits the room the +/// row gives it, such as inside a block, whatever other labels it meets. A row whose text is +/// missing has no label, so a `when` choosing between the text field and missing labels only the +/// rows it names. In a diagram a label is part of the drawing: it scales with the zoom, fades out +/// when too small to read, and shows wherever it fits its vertex. /// /// ``` -/// # use latkit::{field, Label, Vertices}; -/// let buses = Vertices::of("grid", "Bus").position("lon", "lat").label(Label::new("name").priority(field("kv"))); +/// # use latkit::{field, when, zoom, Expr, Label, Vertices}; +/// let named = when(zoom().ge(6.0)).then(field("name")).otherwise(Expr::missing()); +/// let buses = Vertices::of("grid", "Bus").position(field("lon"), field("lat")); +/// let labeled = buses.label(Label::new(named)); /// ``` #[derive(Clone, PartialEq, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = text_or_options)] pub struct Label { - /// The text field to show. + /// The text field to show, or a `when` whose values are each that field or missing. A row + /// whose text is missing has no label. In a diagram the text reads only data, since the sheet + /// sizes blocks around it. pub(crate) text: Expr, /// Font size in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's /// `text.size` unless set. @@ -28,10 +34,7 @@ pub struct Label { pub(crate) size: Option, /// Each row's text color; the theme's text color unless set. #[serde(default)] - pub(crate) color: Option, - /// Where labels would overlap, the higher priority shows; the mark's `priority` unless set. - #[serde(default)] - pub(crate) priority: Option, + pub(crate) color: Option, /// The width at which lines wrap, in the space's unit (units in a diagram, CSS pixels /// elsewhere). #[serde(default)] @@ -39,26 +42,20 @@ pub struct Label { /// The CSS font family list for the text; the theme's `text.font` unless set. #[serde(default)] pub(crate) font: Option>, - /// The most labels shown at once: those of highest priority among the rows in view, or fewer - /// where they would overlap. No limit unless set. - #[serde(default)] - pub(crate) max: Option, } /// Reads a label from a text field alone, or from an options object with `text`. impl<'de> Deserialize<'de> for Label { fn deserialize>(deserializer: D) -> Result { let label = |written: Written| match written { - Written::Object(mut form) if form.text.is_some() => { - form.only("a label", &["text", "size", "color", "priority", "maxWidth", "font", "max"])?; + Written::Object(form) if form.text.is_some() => { + form.only("a label", &["text", "size", "color", "maxWidth", "font"])?; Ok(Label { - color: form.color()?.map(Paint), text: form.text.expect("present"), + color: form.color, size: form.size, - priority: form.priority, max_width: form.max_width, font: form.font, - max: form.max, }) } written => written.expr().map(Label::new), @@ -78,9 +75,10 @@ fn text_or_options(schema: &mut schemars::Schema) { } impl Label { - /// Labels each row with a text field's value. + /// Labels each row with a text field's value, or with a `when` choosing between that field and + /// missing; a row whose text is missing has no label. pub fn new(text: impl Into) -> Label { - Label { text: text.into(), size: None, color: None, priority: None, max_width: None, font: None, max: None } + Label { text: text.into(), size: None, color: None, max_width: None, font: None } } /// Sets the font size in the space's unit (units in a diagram, CSS pixels elsewhere); the @@ -93,19 +91,11 @@ impl Label { /// Sets each row's text color; the theme's text color unless set. #[must_use] - pub fn color(mut self, color: impl Into) -> Label { + pub fn color(mut self, color: impl Into) -> Label { self.color = Some(color.into()); self } - /// Sets each row's label priority: where labels would overlap, the higher priority shows. The - /// mark's priority unless set. - #[must_use] - pub fn priority(mut self, priority: impl Into) -> Label { - self.priority = Some(priority.into()); - self - } - /// Sets the width at which lines wrap, in the space's unit (units in a diagram, CSS pixels /// elsewhere). #[must_use] @@ -120,12 +110,4 @@ impl Label { self.font = Some(font.into()); self } - - /// Shows at most `max` labels at once: those of highest priority among the rows in view, or - /// fewer where they would overlap. - #[must_use] - pub fn max(mut self, max: u32) -> Label { - self.max = Some(max); - self - } } diff --git a/crates/latkit/src/scene/layout.rs b/crates/latkit/src/scene/layout.rs index 77f5ee46..b9fbd9a8 100644 --- a/crates/latkit/src/scene/layout.rs +++ b/crates/latkit/src/scene/layout.rs @@ -19,7 +19,7 @@ pub enum Layout { None, /// Drawn distances follow graph distances. Stress(Stress), - /// Ranks along a direction, for flows and hierarchies. + /// Ranks along a direction, for processes and hierarchies. Layered(Layered), } @@ -45,13 +45,13 @@ impl Default for Stress { } } -/// Layered drawing: vertices in ranks, edges pointing along `flow`, and nets ranked from their +/// Layered drawing: vertices in ranks, edges pointing along `direction`, and nets ranked from their /// outputs. #[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct Layered { /// The direction edges point; `right` by default. - pub flow: Direction, + pub direction: Direction, /// Distance between vertices within a rank, in position units. Default 30. pub gap: f64, /// Distance between ranks, in position units. Default 80. @@ -60,7 +60,7 @@ pub struct Layered { impl Default for Layered { fn default() -> Layered { - Layered { flow: Direction::Right, gap: 30.0, rank_gap: 80.0 } + Layered { direction: Direction::Right, gap: 30.0, rank_gap: 80.0 } } } diff --git a/crates/latkit/src/scene/lines.rs b/crates/latkit/src/scene/lines.rs index 7f4e36cc..2f3b0e76 100644 --- a/crates/latkit/src/scene/lines.rs +++ b/crates/latkit/src/scene/lines.rs @@ -111,7 +111,7 @@ impl Lines { } let color = Store::Color { word: wgsl::LINE_COLOR, fallback: wgsl::THEME_LINE }; match &self.style.color { - Some(paint) => c.channel("color", &paint.0, color)?, + Some(expr) => c.channel("color", expr, color)?, None => c.theme("color", wgsl::THEME_LINE, color)?, } c.channel("visible", &self.style.visible, Store::Flag { word: wgsl::LINE_FLAGS, bit: wgsl::VISIBLE })?; diff --git a/crates/latkit/src/scene/mark.rs b/crates/latkit/src/scene/mark.rs index 2473c8e3..1271a744 100644 --- a/crates/latkit/src/scene/mark.rs +++ b/crates/latkit/src/scene/mark.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use super::{Areas, Edges, Faces, Label, Lines, Vertices}; use crate::expr::compile::{Compiler, Store}; use crate::gpu::shaders as wgsl; -use crate::{Error, Expr, Name, Paint, Result, Size, Space}; +use crate::{Error, Expr, Name, Result, Size, Space}; /// One table a scene draws. In JSON: `{ vertices | edges | faces | lines | areas: options }`. #[derive(Clone, PartialEq, Debug, serde::Deserialize, schemars::JsonSchema)] @@ -60,7 +60,7 @@ pub(crate) const GEOMETRY: &str = "geometry"; pub(crate) struct Style { pub(crate) data: Name, pub(crate) table: Name, - pub(crate) color: Option, + pub(crate) color: Option, pub(crate) visible: Expr, /// Each row's priority, read from its data alone. pub(crate) priority: Option, @@ -289,8 +289,6 @@ pub(crate) struct Labeling { pub(crate) max_width: Option, /// The font for the text: the label's, else the theme's. pub(crate) font: std::sync::Arc, - /// The most labels shown at once; `None` for no limit. - pub(crate) max: Option, } /// The context a mark compiles in: the theme it falls back on, the space, and the layout. @@ -483,13 +481,9 @@ impl Mark { /// Calls `f` with every expression in the mark's options. pub(crate) fn for_each_expr<'a>(&'a self, f: &mut impl FnMut(&'a Expr)) { let style = self.style(); - style.color.iter().map(|paint| &paint.0).chain([&style.visible]).chain(&style.priority).for_each(&mut *f); + style.color.iter().chain([&style.visible]).chain(&style.priority).for_each(&mut *f); if let Some(label) = &style.label { - [&label.text] - .into_iter() - .chain(label.color.iter().map(|paint| &paint.0)) - .chain(&label.priority) - .for_each(&mut *f); + [&label.text].into_iter().chain(&label.color).for_each(&mut *f); } let stroke = match self { Mark::Edges(m) => Some(&m.stroke), @@ -502,7 +496,7 @@ impl Mark { if let Mark::Vertices(v) = self { v.position.iter().flatten().chain([&v.elevation]).for_each(&mut *f); v.radius.iter().chain(v.size.iter().flatten()).map(|size| &size.expr).for_each(&mut *f); - v.ring.iter().map(|paint| &paint.0).chain(v.marker.inputs()).for_each(&mut *f); + v.ring.iter().chain(v.marker.inputs()).for_each(&mut *f); } } @@ -538,30 +532,25 @@ impl Mark { compiler.check_data("priority", priority)?; } if let Some(label) = &style.label { - let priority = label.priority.as_ref().or(style.priority.as_ref()); - compile_label(label, priority, compiler, context.theme, &mut drawing).map_err(|e| e.at("label"))?; + compile_label(label, compiler, context, &mut drawing).map_err(|e| e.at("label"))?; } Ok(drawing) } } -/// Compiles `label`'s text, `priority` (the label's, else the mark's), and color into extra words -/// after the mark's own. -fn compile_label( - label: &Label, - priority: Option<&Expr>, - c: &mut Compiler<'_>, - theme: &super::Theme, - drawing: &mut Drawing, -) -> Result<()> { +/// Compiles `label`'s text and color into extra words after the mark's own. In a diagram the text +/// reads only data, since the sheet sizes blocks around it. +fn compile_label(label: &Label, c: &mut Compiler<'_>, context: Context<'_>, drawing: &mut Drawing) -> Result<()> { + if context.diagram() { + c.check_structure("text", [&label.text])?; + } + let theme = context.theme; let extra = drawing.extras; let word = |k: u32| drawing.kind.stride() + extra + k; let column = c.text("text", &label.text, word(wgsl::LABEL_CODE))?; - let priority: Expr = priority.cloned().unwrap_or_else(|| 0.0.into()).scale(0.0..=255.0).into(); - c.channel("priority", &priority, Store::Number { word: word(wgsl::LABEL_PRIORITY), fallback: None })?; let color = Store::Color { word: word(wgsl::LABEL_COLOR), fallback: wgsl::THEME_TEXT }; match &label.color { - Some(paint) => c.channel("color", &paint.0, color)?, + Some(expr) => c.channel("color", expr, color)?, None => c.theme("color", wgsl::THEME_TEXT, color)?, } drawing.extras += wgsl::LABEL_WORDS; @@ -572,7 +561,6 @@ fn compile_label( size: label.size.unwrap_or(theme.text.size), max_width: label.max_width, font: label.font.clone().unwrap_or_else(|| theme.text.font.clone()), - max: label.max, }); Ok(()) } @@ -727,15 +715,16 @@ macro_rules! mark { of: ($crate::Name, $crate::Name), $($(#[$doc])* $(#[schemars(extend("tsType" = $ts))])? #[serde(default $(= $default)?)] $field: $ty,)* $($stroke)* - /// Each row's color. Unset, rows take the theme's default for the mark; faces instead + /// Each row's color: a CSS color, a value through a colormap, or a conditional choosing + /// between colors. Unset, rows take the theme's default for the mark; faces instead /// blend their corners' colors. - color: Option<$crate::Paint>, + color: Option<$crate::Expr>, /// Whether each row is drawn and can be picked. Default `true`. #[serde(default = "crate::scene::mark::yes")] visible: $crate::Expr, /// Each row's priority, from its data alone (no view state). Higher-priority rows draw - /// on top, are the last dropped when a table too large to draw whole is thinned to the - /// tiles in view, and are labeled first. + /// on top, and are the last dropped when a table too large to draw whole is thinned to + /// the tiles in view. priority: Option<$crate::Expr>, /// Distinguishes two marks of the same table across scenes, and names the mark in hits. name: Option<$crate::Name>, @@ -804,10 +793,11 @@ pub(crate) fn zero() -> Expr { macro_rules! style_methods { ($mark:ty) => { impl $mark { - /// Sets each row's color. Unset, rows take the theme's default for the mark; faces - /// instead blend their corners' colors. + /// Sets each row's color: a [`Color`](crate::Color), a value through a colormap, or a + /// conditional choosing between colors. Unset, rows take the theme's default for the + /// mark; faces instead blend their corners' colors. #[must_use] - pub fn color(mut self, color: impl Into<$crate::Paint>) -> Self { + pub fn color(mut self, color: impl Into<$crate::Expr>) -> Self { self.style.color = Some(color.into()); self } @@ -820,8 +810,8 @@ macro_rules! style_methods { } /// Sets each row's priority, from its data alone (no view state). Higher-priority rows - /// draw on top, are the last dropped when a table too large to draw whole is thinned to - /// the tiles in view, and are labeled first. + /// draw on top, and are the last dropped when a table too large to draw whole is thinned + /// to the tiles in view. #[must_use] pub fn priority(mut self, priority: impl Into<$crate::Expr>) -> Self { self.style.priority = Some(priority.into()); diff --git a/crates/latkit/src/scene/mod.rs b/crates/latkit/src/scene/mod.rs index 24bb9d06..8817718f 100644 --- a/crates/latkit/src/scene/mod.rs +++ b/crates/latkit/src/scene/mod.rs @@ -52,9 +52,10 @@ use crate::{Error, Result, Snapshot, Source}; /// ``` /// # use latkit::{Colormap, Edges, Earth, Projection, Scene, Vertices, field}; /// let voltage = field("vm").colormap(Colormap::BALANCE).domain(0.95..=1.05); +/// let buses = Vertices::of("grid", "Bus").position(field("lon"), field("lat")).color(voltage); /// let scene = Scene::new(Earth::default().projection(Projection::Globe)) /// .draw(Edges::of("grid", "Branch").ends("from", "to")) -/// .draw(Vertices::of("grid", "Bus").position("lon", "lat").color(voltage).legend("Bus voltage · pu")) +/// .draw(buses.legend("Bus voltage · pu")) /// .title("Voltage"); /// ``` #[derive(Clone, PartialEq, Debug, serde::Deserialize, schemars::JsonSchema)] @@ -227,7 +228,7 @@ impl Scene { return Err(mark.within(i, Error::invalid(reason).at(option))); } let style = mark.style(); - let shown = match style.color.as_ref().and_then(|paint| paint.0.colormap_of()) { + let shown = match style.color.as_ref().and_then(crate::Expr::colormap_of) { Some((colormap, domain)) => domain.is_some() || !colormap.named_categories().is_empty(), None => false, }; diff --git a/crates/latkit/src/scene/space.rs b/crates/latkit/src/scene/space.rs index 8620f538..ea135fb7 100644 --- a/crates/latkit/src/scene/space.rs +++ b/crates/latkit/src/scene/space.rs @@ -62,6 +62,11 @@ impl From for Space { /// The earth: positions are longitude and latitude in degrees. The ground takes the theme's `earth` /// colors. +/// +/// A flat map is one world, drawn once. Its ground ends at the antimeridian and at Web Mercator's +/// latitude limit, and the camera zooms out until the whole world fits the view and pans until the +/// world's edges meet the view's. On a globe, longitude wraps, and the camera zooms out until the +/// globe spans a third of the view's shorter side. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct Earth { diff --git a/crates/latkit/src/scene/tests.rs b/crates/latkit/src/scene/tests.rs index 8bbc7b02..0cb0bd18 100644 --- a/crates/latkit/src/scene/tests.rs +++ b/crates/latkit/src/scene/tests.rs @@ -15,8 +15,8 @@ fn scenes_match_builders() { "space": { "earth": { "projection": "globe" } }, "marks": [ { "edges": { "of": ["grid", "Branch"], "ends": ["from", "to"], "width": { "px": 2 } } }, - { "vertices": { "of": ["grid", "Bus"], "position": ["lon", "lat"], "radius": { "units": 3 }, - "color": { "colormap": "balance", "value": "vm", "domain": [0.95, 1.05] } } } + { "vertices": { "of": ["grid", "Bus"], "position": ["$lon", "$lat"], "radius": { "units": 3 }, + "color": { "colormap": "balance", "value": "$vm", "domain": [0.95, 1.05] } } } ], "theme": { "base": "light", "vertex": { "radius": 5 } }, "ease": 0 @@ -28,7 +28,7 @@ fn scenes_match_builders() { .draw(Edges::of("grid", "Branch").ends("from", "to").width(px(2.0))) .draw( Vertices::of("grid", "Bus") - .position("lon", "lat") + .position(field("lon"), field("lat")) .radius(units(3.0)) .color(field("vm").colormap(Colormap::BALANCE).domain(0.95..=1.05)), ); @@ -88,38 +88,41 @@ fn faces_lines_and_controls_match_builders() { } #[test] -fn a_text_in_a_color_option_is_a_color_and_elsewhere_a_field() { +fn a_string_with_a_dollar_reads_a_field_in_every_option_and_any_other_is_a_color() { let json = r##"{ "space": "plane", - "marks": [{ "vertices": { "of": ["grid", "Bus"], "position": ["x", "y"], "radius": "r", - "color": { "when": [[{ "field": "on" }, "#ffb000"]], "otherwise": "gray" }, - "label": { "text": "name", "color": "white" } } }] + "marks": [{ "vertices": { "of": ["grid", "Bus"], "position": ["$x", "$y"], "radius": "$r", + "color": { "when": [["$on", "#ffb000"]], "otherwise": "gray" }, + "label": { "text": "$name", "color": "white" } } }] }"##; let css = |css: &str| Color::css(css).expect("a CSS color"); let built = Scene::new(Plane::default()).draw( Vertices::of("grid", "Bus") - .position("x", "y") - .radius("r") + .position(field("x"), field("y")) + .radius(field("r")) .color(crate::when(field("on")).then(css("#ffb000")).otherwise(css("gray"))) - .label(Label::new("name").color(css("white"))), + .label(Label::new(field("name")).color(css("white"))), ); assert_eq!(scene(json), built); let error = serde_json::from_str::( - r#"{ "space": "plane", "marks": [{ "vertices": { "of": ["grid", "Bus"], "color": "kv" } }] }"#, + r#"{ "space": "plane", "marks": [{ "vertices": { "of": ["grid", "Bus"], "radius": "kv" } }] }"#, ) - .expect_err("a field's name where a color goes"); - assert!(error.to_string().contains(r#"not a CSS color: "kv"; for a field, use field("kv")"#), "{error}"); + .expect_err("a field's name without its $"); + assert!(error.to_string().contains(r#""kv" is not a CSS color; "$kv" reads field kv"#), "{error}"); } #[test] fn joins_read_from_another_table() { let json = r#"{ "space": "plane", - "marks": [{ "vertices": { "of": ["grid", "Bus"], "position": ["x", "y"], + "marks": [{ "vertices": { "of": ["grid", "Bus"], "position": ["$x", "$y"], "radius": { "field": "r", "from": ["weather", "Station"] } } }] }"#; - let built = Scene::new(Plane::default()) - .draw(Vertices::of("grid", "Bus").position("x", "y").radius(field("r").from_table("weather", "Station"))); + let built = Scene::new(Plane::default()).draw( + Vertices::of("grid", "Bus") + .position(field("x"), field("y")) + .radius(field("r").from_table("weather", "Station")), + ); assert_eq!(scene(json), built); } @@ -153,7 +156,7 @@ fn comets_and_named_categories_match_builders() { "marks": [ { "vertices": { "of": ["grid", "Bus"], "legend": "Kind", "color": { "colormap": { "kind": "categorical", "colors": ["#ff0000", "#0000ff"], - "categories": ["red", "blue"] }, "value": "k" } } } + "categories": ["red", "blue"] }, "value": "$k" } } } ], "theme": { "flow": { "color": "#00ff00", "width": 6, "spacing": { "units": 40 }, "length": { "units": 30 } } } }"##; @@ -169,7 +172,7 @@ fn comets_and_named_categories_match_builders() { .draw(Vertices::of("grid", "Bus").legend("Kind").color(field("k").colormap(map.categories(["red", "blue"])))); assert_eq!(scene(json), built); let continuous = - r#"{ "colormap": { "kind": "sequential", "colors": ["red"], "categories": ["a"] }, "value": "k" }"#; + r#"{ "colormap": { "kind": "sequential", "colors": ["red"], "categories": ["a"] }, "value": "$k" }"#; assert!(serde_json::from_str::(continuous).is_err(), "only a categorical map has categories"); let mixed = Scene::new(Plane::default()) .theme(Theme { flow: FlowTheme { spacing: Length::Units(40.0), ..Theme::default().flow }, ..Theme::default() }); @@ -206,18 +209,19 @@ fn presets_equal_their_values() { assert_eq!(options, expected); assert!(serde_json::from_str::(r#"{ "zoom": true }"#).is_err(), "zoom names an input, not a boolean"); assert_eq!(serde_json::from_str::(r#""layered""#).expect("a layout"), Layout::Layered(Layered::default())); - let layered = r#"{ "layered": { "flow": "down", "rankGap": 40 } }"#; + let layered = r#"{ "layered": { "direction": "down", "rankGap": 40 } }"#; let layered = serde_json::from_str::(layered).expect("a layout"); - assert_eq!(layered, Layout::Layered(Layered { flow: Direction::Down, rank_gap: 40.0, ..Layered::default() })); + let down = Layered { direction: Direction::Down, rank_gap: 40.0, ..Layered::default() }; + assert_eq!(layered, Layout::Layered(down)); assert_eq!(serde_json::from_str::(r#""block""#).expect("a marker"), Marker::block()); - let gauge = serde_json::from_str::(r#"{ "gauge": { "fill": "f" } }"#).expect("a marker"); + let gauge = serde_json::from_str::(r#"{ "gauge": { "fill": "$f" } }"#).expect("a marker"); assert_eq!(gauge, Marker::Gauge { fill: field("f"), shape: Shape::Ellipse }); assert_eq!(serde_json::from_str::(r#""left""#).expect("a port"), Port::new(Side::Left)); let output = serde_json::from_str::(r#"{ "side": "right", "output": true }"#).expect("a port"); assert_eq!(output, Port::output(Side::Right)); - assert_eq!(serde_json::from_str::
    x?v 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 <{H_}kwr8hw_rxB2COr;{eo!A!eDq5Vvr{9lU&o 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

    8cv#RICfL6S*T{GH^ho=gSMBO-{4n%Zc zNmt-a*8hLK^nWN!^mQRC)1}!~yWhT(=t+(K3YQ=d+tLvf}MQA|$N@j6MTltxadDHPfWO z3VBl1jUC*d<4B*f{JS`R#Q?mvC&px(Q)bK591??=EkKYo7ZP6sFVXMI@&S}NJVE~g zTotTGWrbJzem0H>?^~pdc&^3G?MSza^eDZf|KdsfM51oZeN8n)W_E(!6xzqbg%KRF_tG~2! zC?HPAspG`+rS5z|LdGCR8qR9pYs1Y=QEoHXS${M|sJSd_V3ixy+}5VUe&j2L(|eeH^sl|;n<@NKXrQ&-~R4#t(N=u zoyNWVSfvW3JPm%E@KA{KF}JR<6ucNV%`90DEH5idj}nEvIUqU;>C|Zy@3yKPaw{NZ0xc50Z&3^Y zM}-s6_38K5OG7cqK2Tk&Jjm*)}lZhq}#ouJ5E%wT@zNt z`#i_+ymmb%yj$c{>o{7|0cDK%-ZM6#1WoUBmuK;*`2qDyyAX>$B`4;w1(1Hdxo=c) zinSF~txq#b%jje8R&0Wv$bBODe%I{AlW-I73Iq=lrv=`z&`|FQCopm7cR3Zl*z62U z$$r^(lszNUz0VlNv*kM4tHoqy=A;|$v!uI00o!}4PqoI6V}|r)r(9rJ-gIEhvLvbT zDL>V#R|ekPE-K`_Skj%STqE^1-7cxty(ENFe{HCP5^Nxw7hc|y^#rPI6j%{*^d7Z>cGow6 zWBI0hN8Aswl=r~e9YFmRyGy)k*Q%P9*<&-CT~2Y@0NOHprk>J8Fr}!5xkVqWafZdU zOfz)oYB2QtbVxR^h;#nBcLaQfdvcYfkx>>nxz#dxcI;tv3n1<tCmc~4(?&Q|z zlP$PP#d31&{ca1^NTjSfzXM_XN%;eF6K5aSOyMiigw-v81+tnAtZ=AG7Lzy+fsaA%sAP?~yaNggH2`i&#&w9;l0Z4Xk-3<|hAP=iR79AKw?} z!NI>GATrz`rf3*|JdQCbd*9i`nu-%SUwnd4?PSG~gzGh?K5yJj2ch=&56k!W4|$|m z8^}yAmLXJIJZYRI1N!<%bWi!loSCyxA#M&w|9nt-?7gv1E`++6#r46|vbV}-H zDE$*Xb>K&kAipR>FE+24L@C7pRlNU6t!(va^+tSFig+cRIv?Erv;rMcNs(>PjKtrY z{vC+!RnV6-bCG;v7o;S~QuL?7*iuJBt;M zVq2H9Paq!J>+6i&Clvho;RgqY!7b+7Kd&tT$^9f6X}@Q^V0P@DV3E#k@?-hmrNq|e zO)MM59v?4DQyg&{uD@*eB%e_H^Ry@SeHRP*YUIYXD^+Kl&BKNDOY4}5lgu{}xXBjp zw?4 z#X7?rth*pkSD9byi~M5Qveh`%1pZ;3XOXC}eLKR_|Ek^iK=!GoLX6?t!)nGvJt*+< zF=S7mWIkB}?h*{~6Z$~7yvUuRu{Hu}s%GR&H7L~bZ0Pepuds(A%6$C+doRVlHe%xP zuh)Vp3%d|Xg+tc!2tnc*hO<$GbbV$4?+YbF%Tx(OU zCW8Cn6KLD-=w)&N9m5yv6Y_al=P8WizS(Zjuxk^Z$%m#w;F2OkJQZ+1XG)=5UTqirR-Bl z+^XY~q?+le6lJ61b39EqX*D-51KfBb6p;_*SlW$gsJ^<#4D3qI((Fc@bnyxwD*hq} z4I?|VXT+A3YQ8 z0IPaB>xfOh+<@Q1|M}TR*DUn?g!}3{&IQuBLZUi~3%xG?0jG2aEc{d&BsATv5-hz? zga4oSa&C_*u07>txZ~<4=eLD6osU@oVCO^9cT7%K2WK>1mZG{|#R4h%CE3aNrXvXH zR%UXBvw11)ndVRtW1@N^Erpj^{22?Lndf}rS@-VXv`0#jG6*}ql{PST4zKfKY9{%L zNV(ysSYPU1n+MNH)sVx^q76Lnho}%zc(mid<}H5hs?me+cyO$=q~5d!4fpg^a%(4{ zhbG^i#BvHqe^EZY)&7TjV4+}Jpp#S}DFQ}ul&iHZSY6&xvqm~zI$U)?QddO9)@Rgq zcsR5;8k_R|a?W*wvh1Qj1Jzz$V|i(wPj*thM+87RI|zgbV&e2xqrdfCU{!+gTZ7jP zN10QCsNPbLI6NX)`ZEe7)zUW5%xbQ5$r+QL2tF-Iz1iDWIYco!!(AoKFQdGYOfNy^ z-6FoD7}3y#pV>iD6eC++0?8{V&77MZh>ujSI((z)Baho&RQ*c6|AI)Mz0S=1BTkBk zp-md}VwQl&8+mqoH@y8qOsuL6oUQqO+KOkbR=E2Yq z&PR3{57?ZW-2r54bkcdC1>T_c#xdI#L05OasOqoTDa1uv^QKI$arl{eM&6#j{Wy){ zlL={wB>M-Lg+rTAx_!ww!?|lYo53e>0;pHs*dNftK3pBql2jG6NAC?`AI46+f6-(4 zbL8Eo68!kok{|5mEz0HAH)D+N(-s`A25`G~;lxLoBJE-#FaX@_qe?Kt!&HT9_vnb$ z#PIvy80Y`(+on}dH?S06GZnw!#P3*f#?D=V3+;P-75ATSbgs=>Y_Swj&bMEHW{XUN zYk)WIyRuv@vohs<-uiMGdT`iT>(E#C0 z%{ZsKVF|Y1y;IDQ$?;PEJ9*5dpE4+#6C-sz=+9Tb3Ut22N1p+h^ii~`D^{$h`yOQ- ztHysAYhXg#DJ*2dbblHI`#nhl5Z@6Ak}BDze%_c}CyU3)_6C9>)KZQc5?YI8p(xs19hKNJ!lf@;_!nRr| zsvJ5`D^5QM)**C9Sw26t*c+w!)d>_JSJ-v$K>h8@V2^JU&Vd^7gT3%1%LQ<0aW_Tf zN=U)4U%e*QP-V&af?wZx9=vnGclJBbeANU3ByH$2s_ThxT3hbFjvWt+_lfO~2eH`tt$_n7aC>M7(i8GR31 zig6h&IX08^7@ZTsoX>3!f|ugai*iKI2-qxF16`E7aYuj0j`h&h`8;Q@_{lASz`V%E zjQ@_FdPft9P>bJyH%ew=2Vgnm&`6&O$^m$Kpnmd9OhNtC zmaN$a^98>TgSU%E*3ide9xBG~48W8~d{`Lad(pW}B4}+Qi~Y8#tqN`MA=lgW0YhEvUO>evu<(0 zx|;ayu-_PTz4L5zbaq$eSa**4+01LeiPk30VW!dMjX-x-0ylgI#7C!IZNo02^)Ioi zwemQpW9h!;GYlZ%xwdUo)|8t#oOcL(5as@#bBf0+54LDBpRKz^8-L=!oOpq%fDFCb zd<+~EoAy4adNB(tB2Xp#YLhH+4?=JM3SoNxB0Vibtx@66vFAPh<2{5!Hlg5fZ~~Kz z5Y@=7C*N}yib)4>vQ`K3+;-&kUIA=_6&Hax9VWE6mMkOmxDG!JGl+TQ1Kpf~?~4=?tOU~tboM-Rw+jny`Petg2PaO-RsSs~Jsip! zHpACE9ze{+_r?kN9bSC z!U}#^?c_O}Y??C;9U1xl3Hgm}9#mSqU0a3mz$_hX|Mdb+e=eO*H8q0$!S9R)TgK_u zTXsN8y%wV^Zg0sJ@4)+jEcv?8shsrm6(m(&r)5HqpP~wL*U_nQmQzpa%wE+)yHuq) z?mt~@QN#c-vgWIa?a_7bt^|_{H)FN6w-PlVHxAg{5Jh?jHko?Bx3g8ryrf=`w0!RY;n@jXU+XPkJ82ceE0oF=r3Aig>+%gO;t>8B{8B^5GD z2@=6bfRT`JacP5rSTNC?jk$B&@|9P7cs?+zZgreT#DOewYXmY7vDe!Wj2+)hqpZ%( z#N0n)@bYvlj4-r1uvszj>f>kXeNGP&Do9GZs6{q(nk}(LOpQm^yCg+rR&kB$w_8x1 z#_`RusdcQ=Nia^P@4}B$2ca#cP1~1%HODxjiPQ8l8r+H$-xjGnp^kZM$~S=3Yf&2| zm&6fh@h<_IEYpo8_$Q+F=d#)ddHm6~eq~Unhey`W=X<}6l;BJ6 zH0IcVkRw3)Da!*eSHQo~X=OwCobHn2IL9PY69kg;kj{2p08|TTE){~h>?TWA1+?@u ztj4frZd6GO10J3P*?FjO(An${u6~xu1T%ojFOrtzX=?+dgmcvtyVAO<8=18)M4#?? z6LWx@l%Z2b18>;nE;pNSLrrGlTq`eb?f~#vXVyEGAe3(k$eqz~sxzpb3X8gxzMhzS z^Rr;1(E7l_6+>Q86%jSX$_L6JEW&G#Sw}mMgvI z;C$=KL|xAq=3&XL=|=D4;Kr5CHCg5&G(>>GYHIq2U()r#y#TLwjCKYPUmgq|L>%&? zezL92o6*F}l`l^( zzkK>j>+Gv6PG#~}NYZMJ#8AK}XTRtHgm!Yo4w?n3(~k@9Uv8j5k4=Wv2u-xqOS7;& z(C-&8FOsfAnIif7tR`hVqzZXqHbk*g{<0~^91B?Al=lBgXV5)|S8RZ)d`@0z2C8qb zF2w>+x4|$9&fprAxOIz8K<79ioP9W0kO@RZ1;Qy1-^?!Z^hxuZ_+V`q_G6J-(rA>g$uA}` z!+-!?X^859PD@%P1u5WlB;H6klf>JRk(g-=NErv0rtZC=oG(TWor^%X&eUI?5Un?_ zPnK`i=Hu(z(y({{WZR5V)&S)U%jbKCykYa1cIY=sY`!*!;kB}9H&=8em?Bxl9}OYR zU~Yirp1P`{qG2zM^j&Tt`9xr0P2t%NZB+;zau zZPf}^_4C7y5a+=FT+96EnyvFRGSv;zYzND%$J>oJGvnv?kms7p&cJ^KR0Mm9zNbZy zHFW}J5xPB#PjbpF0u_6t*~WOJjMC)^_T|?pkR~_;lHe^6Sd$rKm+vT8`c_7HBLP#{+Rd4_07r@!i0@DWHgji}%>)?ugSe9#9Bj(%Bp*eh0k0@I^syT&R5f_){oo(9z(T+{|zSI&mk)I%xq0& zMOtsM_tGOfsrDc~BafEy9TbR}?DGQd)$xIYK&Kpv=;+U(omGPsxL2Aj_;26K3%~)@ z@H0mh&D{cijt8qvr(5eN?Ut47rA+|$j*4^8_1u|tU#744PkiqPLY9@Os=WIyxYyqA z@I=2V3%~ozm>MAS{<<@^{y}A()%hXV-@IvFuX_;KTf z-SGP20`?PyUe4*Vg~Pjq=6BbZ6@{CjhdW&ghOcJUf5P&I*BBR3CPxrQ3EJGh;-gp9 zLXy%k0Goy$$?A>qSG6pIQZo#)Cwpyl%$pR-Go<<3nptLqQVb?;U7JCfZtYpn#s!-b z)eJ4o6(?@Zzt2E-$HWvMZqLeVUT=4K)@Ir+OA`Z>R5gWd7+O`uCqb_FZ!ZSP#hBrX z=Ub|U);4FR`*dOgB7>-&_eH@cf^bawn zrvUu=D_Z;51MpG(T94F^S_2lSANCZmhbbX%g)_Ha{Nd%*U~-c;)0}MCe?)zUizLUI zQUUhVa5(T3WB@Y99r(gN{mIzc#T2mH^UyV7tJ@gH(Pls5XjgBM;w-FX!Iv3^cfQ&| zB*SaK&4x@;{K`BLnLgVEsM1z&*b-PI=%?&cMCRdSL zgLcNI{UC7{{?Z*?uUrijW8&_pA30wYDVz;x)kYTiDc`RFe5dhCOCU>sHPi;YwoWze zWs`O4WptCo=VHPLTA3oTJX z9T&h@qbd2*M zCET!?n7|{L*3)Ba)1lJoMlg9>JZo*9+0_;aj!un}M=*EhgVXlB+t^Pm%6zI6Zgm^v zE`CxK?J<}#wWbK^g@>gv!UWpz8m|-~S(Pz0KdwWm{uO`&p(5Mx4+V&h6iVI&2+n z%`j@dy3!TEu@9xCogWhUH&s(~%&{}4bu1{r`&E;12kyq;c0?=5-~{e+ka<(~bF0`C zMHhMs%+#A=>$5WPC##eQ`Ih#2Zs&HECj_U%LsXIm#kk4JM%HZrOP!C!~im;LqSClqYz$Ytwn=`Cc|j z_Q`U_5PW%N8Z18ALj#xhZ*)rJebP)`SNK*4EFvuliYHh3P+nu|)>y9liRW5RvJrD1 zX%LN<`HJ+jEzw+@qFavhN_@_qwN3=0xIe%((j;`_nj9tv;+1-WJ!jr`WeFB6yVh7+ zldPF)eM35O8}7c)_;MVSlTn?8tU$Cbemm1RWQmjHObXH{l2;{x%cn1>aXrfXq=Yok zAyW{!bsaP1N&!Np*vuFHG?+TDW_0XmGKtO#0|{PsdphEqsOtTYTAFE;q42+16rSz5 zi!`#iqNmuC2>_S5!cW_UY&CwkqDdWDMI%43y{LXOV%K%5q`UmV3hR(VJXk2~u%)Wn zjrfXDj945b8y|<7V$Z&sIuSb`C&&BUa`P+@lN<(WCQ@Xu8hYe}sMP(<{4MjAD7Q8Q z7=kNPQ~*>$GjG96?)d7|W3))lhJW{r2NiO$2xYKeVnX)k6J>PhGSr?!Sh#m5=P1w& zF`hiXXN>-CC5V-y7haCun4cwx1bOCMU!YZEUxec6=L<-WVsaK$H`7PdbE8D(C;|lD zUyDe<8h}89*Z;Dm2GH&;HI+W>X0K<{`c=cu%n0cRD}YEtb!T(MyVY%U>Z(AY@hcDP z$hV+l;uP1EWEog6DjGWt)^MtkfkT%_feyUbv8nf`)^(hMYX^6hxcAQ7R4TU3LPw%b zoMLAeSM`i3WF??RD5<_UE37o!;f^)hn(wP=hO}gco*tG0y!3HATdx|;bb;V=e!<{Q zv{Hlh2mC&$m!o59)WH{C&G>tqKI z^w3kkuZd=UXIjTR4i-bu*=q)?6z!tro>!Krr5a~a zH4EL+|MZO?_GBdxa6OMr1=E)L=}izE8h|#*EtZ|D_RyoQP(%x8j!E8QjB6yLt8kx# zU`_)G#!Z3`iktxddzBR}s?Ast_l^F0>}uz1I>{BRFGHSGMV*(TR803h{5xfprUcZX z&J>xhm@8fbsWoVr0AH>Gw3;;V1#W|27d9=koHNUWk4_Gx>H^9azotEziz%lL`9Y)t zZO{G!-|>1pv?1wTJeV+bCK0rU6TB2yh3v7%IAUfnsaFzacm$@_u5ob{O8MdqXRNUmyXOH4-0q4u zGd0H(Jv>^A3J@_toUN5$ z`n3|*uq>2cJ}t2{(`;GBY5DDHO)N;%0PX-ouN*&*bua0;wtU5N8m*2~r#BB$MD>PF z?zoYxUBn*+s4!8@Z1XJljG|1w{?6AhOz8Z;o_h@8BLRu%&s+ca<}jMY)W2^X1qUpO z>=!i5w7`knCpoPSF6=lDvL1{9cUagtFLmc|KFk)4b?yZqYL<&?$}V3y!>sEHt!r`} zl^?{aTM58ci1Ny}2*l16JlXA|V-aA7Y&m z{|%yyt@v};Th|sEvd70O@irH}j6Y+z=iv2nimiK$Xc1Ls|H#J$c$2U6Ns8LUkMXxf z(*WlcZ{bVOW~?KQv> zNZvX)%xFG^H95%AT7~Z_db*R#2d188@BJ6$tnKUW3J^5)Iob>rIAf+Sm0%_umm7bh z)g=n{KL_l=3h1b8BM1mH%7LEdBudCd}tRL-*Iq|t=#%8#+hz0UOvgan>Q`~ zuAk*!SPc4~)@D6wv%ESpZ0}QF-vt}$K6QZKm9F5FRhwwk&leZY|7-WNC)ZQxG49Y~Ug2Zk`UX(f5fS|1VD@~bId`Iv;?*_CWz(0t zD+32xkOXUl)JTIj}<4#;YW?ZMW}U zCb5ih8{>unL&^n^d6((Dc1@)_z}7;|bUkWo=Yk724WW9RXb^Dp*>aonV0ew=+`Kw< z^KmS@uumxb!LP2VzXNk+J{WRMsE%aM?=cU$n7U3uj{fuLVVYOhbTG6p)W)y9A+N{P z$=Vp&*&m9YdW`%naDNdaUxVm~jB&&4our}v9KG25PWG^__MQ=_cX!gvrZj}v!Xl<{ z4|s-TK*C5Y=qhZS$A-89h!Ssq1KunGJjtrd4uq$iA%`W< zEl2h9R|3!{!@O*W!gKHr;^-OBz`GqKSZhQ63kcuWjZV&6_O3*kRSkx;N9(`%$EA#&QH*eJ)r`LO9aVYyA&gp_RihA**)p@cv@vu zqw{!t@oauvppo!`Tg!WVgw8gUOW*%p4BagP5>A=*Js`e{Z%QBJ=+4Y>26k7n;Y}Yj zFPc^)IBMmS@n?OAxr;$XAg^s))d6WO0QXn1@8_O^$fcVWuFzZrd%_Aw4^Ax%1{@!y zeQx1^$g^bpQ>9?fdN2cDW^CTDHNWfcVbkh_CQ62H&#_mVV(JzS4w4st0guu^63P7= zK~)FzI2vzWA0tQwaYZRZvWxr(#7W4lD0I#@y9Em;J(Z6Fh(UeA1~2x$K@ z5EuPQ?I`vvvtFLqgEYH0rHtX^45v5jM(XfUJ?}ho{Vg?z;fHT{mWE!oT@cgteuwJP;bl&_$b5oM|gU*soDwr zJiTEN&3w7G+aFMC<6DbI6+W?~tcYheJCg}`72}Lt(2O?^Ay^n{%Tos*oPO~YDTjs0 zy;K4(-iDY}Kv|%4+r1o84Ml9=d|^W+N!n>#sHnKg z_2Fw3x$@&)@!xn+kYSwD@XJ_A#&U257WZ`ZdvLmER}+-S8;;apEN5A;{F(qiTYpv^ zfhm}qZ|JCx0j2|c@Uplj4fyDaToosI+P9CE= z2d+k`Rc@nGT^?k*#=q4fRl5c^yXaA^J1e=g*>J8EZyH5lRL`SyO9ZbTCT#SF4D=)5 zRy8hz1|-J_sVn&YdN0Tv#8L`|?x5$okexaFWDeiAVEi5G<-y;84%x4RmOAU^wRGLH zmjUi4{QX4>c6K2>kZ(pbHtusj9`Q=nSiEqNI469?GynU-eYS?lXa+*xFY|r#WBjI4 zVxjki&k)AbsZubjdD->!e9K>NNL@+mSpMl>-PIK%T2xHhnQf)mk`ZHnXw-(200Iv(xV(fe=fB-GAL2o<(zXT<_E~fTm0VrjZ zL;MTqMNjavNk0fKopoQvix<9+O;vP-K^+I#B3EX!au(6Z_8+S+fvu#m&^DsKB)Le> zb_NeT=E3Rkigy)ZM+J#J&6Ytm-ucdia4E>NfNYq!^C12~kSBM)FW4&g{58f(ua89xj=SwPY0pn{Ztyw zD9`eblSMALQx`TQi-zd>u~2dN67 zZd+K9B}n6)LIcj7m;ft)8;OzP$buWZYfdQ2`Cd)MIQf=XYPIc+0NZ5{A(Tdj2zo*h$#Ld0)CR zAXD)Il=<613}5#S`2!fWrsp}gSZ*70^Cv^$5Ch7q8yQELC#WSbWH+D?S1k3HxD`?){v39$!^6zu7Bq z#L%5MEGcSCV~t!5O@L-vX9vh{n?e{h4-(iNiaXGl0!+)dygVM8S{*DJqNjXkEw9Zl zF}C)UqrJWaa=5=w@g77oMK$@cW32(4&pqq4rSDO%*cW?xs|`x1bI_LnhBfHIVFtX; zdTHqd^LSVdyJNKF$Q})<^4F-30JXwIpx&-x!D}+@2ocarDA&XB(N~?svNIBGP7H(m zDUU#M1sD4XZ?z&AB*AEBKyKxMJnV?4aO)3`U#^!nLnUq~wY~rhw71r5?g_uS>XZ1Z zd+z*aF%#sO5ND8tFu0jbC77ZkW0DzmWKg)v)-#KtYOvDIrCs%L*(oZuq`@= zXk36yr+J0oaqB>3>@(NCWW{ zxQc9l!qg?S^&Q@W6Sv&=Q=PfNf`kVU2qkKelpeez_d=pV!d>fOK8bL?2T^vHmW&>S zOyH#~63uZ`+t1UjH_K|;peMz{$=-%qY!o#zJsM{^5M~$x&=1+o;$aQx*g$)Cg%d(o zL;HudOkYJIsf3tY&hWfsnI*T3QtD3=oka zRg^F!gb+w%N=O13NV0Qop5J-jbKdu?vsSxW`$y|qW$%68*Y*8=J{Uj(zA!4CI5ZU9 zwokMa^D}pAyngL<`Ey*YwAFi+CAdrk;2TAj7K9gTrtY+7N)R;tVzTkD?2z>Dit6Q5 zvAutq<*}HGwkX)H5Kj~N{dmhHHyT9g*K-wHpp(Ejpeu| zxg$oy;KjGdZOpEEu%TWUE!cBgCRro1DGJLpIRqN6FJfr@vguU?fb{9i(hO2H`^kx^ z2IEWteNKdL9gmE_hVd!}28PYRgG6szhoFsi6b_&8gAZe8TeB&Xe@8rVF4f%#CTf>J zut)70jP;Xn=iln(wPfWGzCOc-d=rv1f1;3HFL+wt_|=qZ?@$cD%Qcf?j1Q5eWXQ!8 z8!O3|KWkma*HiS&Y0>j#!+kq>JFNMUb;bf-pzr?COGnfE#pdh2^a+w}@z_mFx`U(m zXMntxArz@;cz`de65~&AUPioob+{pqc04-iis)oDgH>vs7sfoR^$*votB>uf1A_26 zl_I##w2gE-*9)I>u5knqQz0LN-<-M*6r$^_=Y+$Se%&)bEd-Itw=)ZdJrkfw^vwB_@IR4#%>1+%?P^j^ z!#=>^;}Lw1Y0%TnYF#Y2d4H;88peSM!8+}uvg*uz_4JGaP>_wLmpOVjsIOXm5hnHC za$ser6$`AYZ8cd`?&u^+{)<$#HKXjsbawFG@~)&k%DzYPsHo_f8G#>=H&q9Xo&uT! zU)LIMZ(i$ppIG4Qtt6KH=am9AO#E#-^S|CHHE7F52QTdJ5}qxr&6;6h(4Bk|wesfn zHW$+ME+q7MY;|az!42NIijcJ8S({}W>3VBG8TdR z4-tkwTce=4uP?Da?WuJIA|iqReQ#}%3qoXxfX+QmkArBA{QDash-2H!lQZr1`1%2k znRxLz;@z>=YqRnvrNf(9njnHw$kc=TILYd)RMlrA!>fH`C($hBa$X{43(XkGQmkRq zulaL(KaV}AVran&gr)ZiVA@JChac1WMS~+7q-Vi504KK;xo{#J0fII8wCpv6IZQiM z&NEfl!w-%`MWHm$T*a^I&GsPUZ}#Y9+hVmHc>mJUBoOJjt9n#60hZZTjg*w_Y<-$@U~n)|I-cB0N%}SRjPai6~x6K zuX+3H_ai0(}(y{Fc@y@T6V zSU6nE)Hq3PnYD4}R+ZDoJqpbn9kG%ayPh(6g#Q5Y`<{ciKkc<5nZ33 z#)XU-Rmt;P`#WVSH{qeRBA}nDAA3hCmYvKlg$IPOF*0n8$#A1ZU+Elpg1XwvZ{mI( z08bRO_AYk=$78C+3JSCr2+Hn36-d#j&KxVFGoyNbmCw#0BASvHdkWWX*@kI7TZy)e))PJ zGK(I&ggx{`DB(9Bfmv2pU}$T0JM8P=g}4B$ie zL(ie#;n8JY+;?I1vj{`1MyMJFewx~mUQ%O`Q*JQP4Bj3^j(5>&AG1i=9u9*v(Ouw) zt!r`4CdGPdQ(1Y5jn2fm3xta5_YEU zbJq|`<7A`v5AE7^st)*hKSz3-(`u_IR(lawky5As1tN#j4Ig$e>}jV0c@y7BY=^a3IUWktAOk#rufSdAUj5|wXBiq=NOh5 zgY!%pFRq1bnc9L#JS}_-N{XfI#~>Swp`T(|A{YR+l&u+I$K_NBk5TkB8$Mvq{we?k zCJrlz(Yn--P%xE!MQUtw)tw+Qd~eWP$-!DwnQnOuS;gW-^prO z2akM;fl=9)rhSl1`6J6jE%r*D>5sfWpqbA%Zf{%ZZ?V?GyKT~4>irqinm(>rbwvL#xwzLH;e&-783ydlj9TjkB}*wbU_KHhZ~yEZh`Vm^^aiQ7Pdt8^_n>G8jPm5g}g~ z5ZpTgKG>ki13xZfQL-dbrqkqL_Yu#CDBe_x4}32>Tz6BHJ9$%9egeihcsa`lyl6`} zP=Xx;f-tsUl`te*M1^INC}~A=3Q@TXHQV6z6w5iSt~mhv7IBh?g-q3_j0jiM{8|CM z+ADDz(G*X*(i0IqV$2`j!k$fw3Nd(nA!&}KEz9U3{1F{8Vt^EWy^r?=orqsRmCf;1 z-7h95dH&Zgq0aU^#);VY78TyI$M*U`0K{>^$<*UnMGH-WGa%Nyr;w9f!xKO?<4tj^ z2#vSlX7GM_gmiI;OLfI(Dm|fkWC^1$#d|&zzg?)-qPJ!Df@t0maIbkj@>N71y)~_J z?k7a(e(G~tLbh#J4}6aSDUZgU4KZ&y;eKv`~D&u`7iHduJzg<#I<0ioUwaK|>!Y@Nb)2!5p!73jyxf;9CAUc-dUg zE>k}}@Mz>5RQq5b@!?M^YeB8tCmAFa6$7PPG)}4}-4aNbM}9s{@Z#AbYk`_e>*Yjj z$PG%&eDSy`K?a?WvXxs9m`}hKBFFX-ft~}Z{NKaF)I**ib-J1;uTG&RvtQ-+j9BP# zs$H0;V{3q6;w~C)ZaaU5sJNdZqFccx_4B)3!|4QfvMsAwl$1QylAij(hl|`lVYNDv zTQU)e8>{GoCsO%s(wTN&o_QI1R^|x0iwWGm0O;rP#-rf6w2o&8UW|ub{FCIODcuo# z;`3sQYz={-y_b@b3LwA65)yu-&5p`_5891CY-VrAoYV)Z+sZM`*sD2&<;&)XHhAS6Q_JwA0` zXb04qlUY2=>Q#SRUisVH21UDR`H19Ay%TqDj{>Rm?J6i4Z9`Av$B#N>Cw^wtZ`b`$ zlTD88+g)+_U4Mp?Bxa=<9ApKxNKnyFdzvh%eiB|#Xnsf9k_M7;``GjMqJ}J=sLUnO zQ*@Ct_8<64hIsB)X{@0KVzt$w`h4PZ7G66b@EYobk6WFQ8&N0wg@i1nPq5)#45|dc zJ#q8L0sqjoZu};u`2ZO0^NR_!Sj>;B!bCqJgCG7JyV*N#`W#VHP0IYw^`LKe z_Gq`&$F?SR^D%(ahPU}VISxovb_9!~JxhytRW?FI(plD?$ddE0Be@>M=*L6Pv|)rP63 z27iKL2Ce$Cx#`Y!p-(S-2m4A)EgNhz!R;<+-J1om^T=rYl^V^uuc0hu zOO-zU#n$vqv@^#7m7)|Mg4Tji-I1Z#%dS9UNAh>;mA-r=I1Xv+k0lq+N%5>i$-Yj~L?zJfgh8}7Jtxm!L^NLM55T3GI(SPe>zl!n zWbbJZ?gu6s4$nk==#W4hKjp-D=l>HSsv_wXj&{}(Q9o;p3o&MJ`jh_IQ_jc?_O`}? zY4YH+B%%`9l+1#JW-gWsogx=82vKM%x|-SjGrq{3(IrEHnUmrI(zxT6qX7JUqm(@ow0oPTiujcR#2Jc1$GDCaaw!g)M3Kt znrx^3ffWEbyq#(GTE?7ByOU%Mn$~6Dlb;vEnhy!k1e8;fa;dY$6Y~={(WF@|>g%AaGehbesQc z95lPi<@P23R&j;|6ECjW*mtNBL1iAa+(#M?Xv-#Dk#lzl zY6v(&66%^OsnIzc`0aOsu0Qa&rm#;tGhXj_0U>l6? z@r`4W`K@k#@^eq}{K09!)4>U!3|&iW6i3Ecf&Su~7^NJ(J_xI_eW=355N`$xcpko~ zu8ebzK+Rs`x>rkw#TTG9XlX>)ug(=ioa_ausJh%WM$^k15$D3c1yC)xg;f1J74Et} z=f*lo+a3p3@@d`d?kVxiXMrFy?W4OJB;Q7|R)eH4v@{T;*dangWLLdppKZ-SFXfvf zwj3xk1an*(=W7S}vstY9C9(AH`o*D|Khi2SFOd=EZN#kDDeq@(&uYPHbxvOV11>o= zQSpTxPs4G72SBnt4O(`jWO}-4zQEOeC;>TVv_dlCU)7aI8q$S^n3pgK zTO`3J?uEXnnp^8trHe%W<=4iUi$RLb-Wi({_aMi^-&-5gsEy6rfw zQf(W)Dyho+|C;%r1wH*dK2+D1MwwcJ>6n_Ga~hiWzGIMdLl0!-zQ?3=JDp>6sQfkN zTO1|lod?Ul-zXfVoSyTt{Z>;8LV{uWIE8E$GxwS2(D1u)wB}@J*oUc!y1DCw`V9yd zV6s9%u4#~NRYT996XSjwWuzK)$j$#~Eo%(lG5-R};Xu>pz7DEW?E~0tucXgFkS$Wl zeWT>~DfV|MJhqJogAn+L3Dm8XIB}fd#1ygayP72@tABU3B+K)l@DXrr9eF{Io47dh z3cN>Bye7eJjbF*&77Gjm|^z6!uuzkm(Ci zk-b|X;X&CZ-dqRA^2#9PpU?l`dPiFnM}8-$KGl&8scB{d_flACB2yWml8sY;C`{D! zh;{x3FyT+k5it#R6yXsBec*T{h3_d}r;=?L(ybBK-urX&u43jTS9n-%vPJFp4g~{L z-5x~A_npCEd>8-nioVk}FQ`B>{98#;?yWSx!q)k#lPLJ(n!w`IwxPJ|lb6gV3=QP( zAZTQ(0gSQTiGayYp`FWoB6aOgGz}}AF01MD5XF$Aa=h}3i~WHZ2;=MVhC^7lVgV)U53(LwR?@JF+LW6 z8OUoeqS!ATeId7f90l(N=l6;7L2m$`2{p~SzCIcN@I)DQxa(d1JnsUGe(&2NIc@Vf zbDoi1VS0rjbSkId=h-=CJ8W$NGp~eaWIQmt{U|J4sNzBGipNds7o`TPM|lTO zWCm~{cPj1zP>k^YJ9PfPMC*Bjb+SLjGl}ZXN`D@<>WIh%m}9obhE(}ck+%GSA-A+_ zv+qP@*nhO^K-RlSSX?rF0@Pb(T1rPK{{}erp8)!z{`L{qhh8M%ywH{fE+wip3~2WB zK&y)uI7KwC&%5xT!Pj2;Bqc=%&CZ*1f7ha!2BU>T_xsw$7(8in!Cs_@DsJDEghT zj9SBeGr61ARVyBD2;RZgCpxQ*@XEjw>u!bq`}(tltg>|k1(hj7!73rEnb&HHQ()k~ zH0$u*poo6+NuJ_0h6t<(7;cv@6~wGIKs7b3_2%K>=ayh+)N>MCZ~9Xm5S`r6FJ8|! z4Kx>Uj{}s+*6>E!e9x(4I!t1=;qRy@?Z1+(BE996P%4naigB@Kc5tD9l5OT}6{oiAX?aDEdG-!27_E+J$+Qa8tTTjcJ%52g1+ z$mi{LUyZY~^dbE&Wf-0;9H~w%Jiq&ITKMGR66oc*6(Am($_vGAtY4uoPTDCTT5jIz zS{ksy^J(QHD7|q|wbi^-K+;uvCtBqF<}{wNsT7w|BEbcMvVsUV^=A|*Zh;oEYJL+H zWY~cC6?5(3ZGUcia1~{Kv&xbS0KPo)&JVKm`Z{$@X`EmkT2S%p-a)Zq7T`tZ({QP| zUg95ay?T8=!#me*BEne&R?az&}E!<1k<5E$aQSf`6tEti+4Z_8FNjO#qhB-@_zq3A3x!+@H9JMG%>OD zp$$r`?Zi=`x3E>3xae`P@UUlMR-+}w+w^Pp@Y?{~RRPxWnmd)j+X{}HNM%FXP|Hw9 zmnm&hs2P1%WZ|1iDP%}eI$d5rjbXI_#2)m!EM{aAFAn2xaI4ph1!@-_=OYNohqo*O~pDrkrn-X^LCJ z`TKT*I|ud_5KVtNLDgv<)4PvLDK~Aa97PwjFR4sm*6N)b4B=7N<MYF;HEnfp=?Bn2G3a^oOAByQ^Gne$|K zxh#9VkRaM78uHc(c>DPI&w7ou4N5niKTl-*Q;h=3f}E(Cr|e_&ccPDmG`JlY&70<^ z?J921sz|ttrC+ZLz!zJ3$y?>05#bFjkxdCREcLn&#)%5fO3=Iq5%B17+O$Zbc-#JA zx-t@~{#`gfo9xEowM?9C|Kd>JGH~pdny1r!qY1=`GdtP3WjF-%GiaZar_}k->ZL&B zP1ZG`nW=g7{?%VT`xuGY*EHgx8o|KBH|Abp{{euLM)NY_R5c-v?@~kS&l?=sC$+3X zc4t|-5t8QdAfWQ81Q~-~$p>QX%R^9fVo|}SpN-YR`IV~Vfh`|+al2Fc>L>-O7j6V& z4=#{98~=nwxmgPLt*f8t&x+<`9KjgYep0DqT_6-PJ6-N7f?M(<>u1e8Ax*aE13M)q zRw*z3ns+Ql!biWVbn~v8J_;b)JmoiY6<+k6HDqo&$EF1+cZ}_KW$yzhJs?6o*6oVR zk}%YZhTqQB#!R%?#dU-c@0k?#J>F0K_Mh}D*8GzP#iQo0u@4nph!M%*!cxm?as@-R z3qb$Dn)4pnNvTL{-1zt4g8RWwPN-9Ff?m}P$j=%AL8f|1w`4wIYRHP%g^}=|4Y(c@ zD%ykluydaTijB|+FvQ9_F=!0;m0`yqhop5$OE z&1r-)HnQr=ePBn=UC*A~%x*bA8oAl&AZO8%^Y$3<+7|#3i z|D|_*3?qTtZ)nNwEaYFaaSeRNan*Eafi3ESdM$8n*4`>|_5>5#U8uQ@z;}BtFTo`I z2jpWf){396)ZGAk`|-%~Q`I4{s-Y@NgriUur=hZbOKStu9d}po>~@m(H)}U3&)G!+ zlv@Y~YEBuN+QN0;F)~&|N^fmj#!+@x)p7*T9Y*ofQ4CYzq-0f&^o_HSjOj2iODa~h z#N#+r#KJB%J9YslXnmEF2fceS9+`q19o=+SNXlm>IH%dAEzn^N4bw+;9NufAOzm7h zee}o*?yJc43sp?DrpXxref>!@R7UXdv~5Rr^U>C?v{q@DiX$wvIo9Z>LkeGIgrN1?r$BZ5}rVqytoop-MbzM(c ztot203&;?hO6?sr?Rxh7Xe8k3h)4R;Uyxy~pHx?^tdsX%%1fSxb`$4+sReN_xB81s z8Zju_uR$<79lV-a_5@13m27C*Njld`kn08Ns$}ajFW#$avB7}!VRE%u#qejT%(lbh zCbJq^+Y`HJxKa(DeTGxEOHEWKXv}U8Cn19aX+AHsnd3s{qIzU&C=*)?M1?JD#5!cQ zd~{9}V}Uv;y_4w-XgrS&+L7R_)}NRk^jD;dAfX08G^?kI@xr0k>rz_8)=o0=_gFbh%9@n z!^+s|t$F_7q6FXd*Zj>kY{n{nAzqp!b1%tzURFTQL9psj!Dh_UN0rPs7o4p9_&>^@ zYTf|0#TfbkRlaG!gd4cO80K6@BKpm`h>AE8kXrfWltmI|Xwk~vIUM&BU2qmfAH;eI zJv;Y~)}}5Vd*hr2h1hZg^u94-~W1IeN8{H(?6x1B5MD1eSfzOL?ZIrp@tuh66kB|iIxBN0ov);y<)nr z?KM!|^k?96kc#`|5RT8UsRg*;y2%>fjlGL)8?snQ7!EYEexh=97EgP?N0*v?<#V5; zkw%OEIsbLpv^;;!i>bHHsO0IA@q-Jjwk{xTZ-r{itfmnR6UI{U=h{K9Go!KBo^A0g z^^OY=4)ue#ygyvRon~PA{7_tX>(rV40p_kA_gDx3KKC2~g_yGDBgMBY^T1ZF_5W5? z-?2*pW?gHzD#!IhV~zWxnLoN8Qe+Kw^1j)Q1;|f=?Q0+eyZ0GL4*Aj~qSb!JloZ7L z@Gu{brD^g${$SC^!f!rEGEhC6G?Tf=){0cTMRGGjJV2|=n*v|oV$DOr&n(MdZ9`Bw zJsP815jQk#4V?K9O}XZeO91A&b$D}Fp0NSr*HH|^TacJ9Sipb8QEmpe`9bbfWR&i$ zpbXsmTC6qMK|>}`Og=-3&Ny@J&&L)OLCS9=;1&(fi0}DxUmuDp&e#1)PReOb7!yJ| zYykq_dIi+phV_ff8MAv(kiI7Sbd<#J@54iADt=BgRn(YU<{{b(`NpLV2(O0gRdq!YfWL<{ zSR1NZIy!p@(-DtIj*m`inT(xRq~8)=c(vUk0p$|sRs^vh3Q6jM-?&MU8571cVjKT^f&2cyK02siud#oc)p?3sC5F= z{7)AD&!EA2&K=R(Dt=g|G}H{Tp3Fjkl$Sa97S$*9`H(QH>B=o^DEy4X&%);+Z8 z4`=7$ch~HGx(YrO)+eq9XYS@DQc%{Xm~=wn$)?8d8)@n9v{!wLnZEvM{+7tG*sci* zA#Bb-xF5Bvk2bzg?!|nCtLM{%bnV$z2-Lj1&#Vu;J;Xl((WD=F|DJjO&x!&m3XHsucu{FzE#bl)(l4bD6%2d)K;YZN-aC|xoQ%L0!SjyCu=>i2 zq7eRkO~f!jb06fG{XB`P)>C_wrvCgcT1Fn@`*2a==*a6W1Zxj36v${v_nlXV%vY6G zHOLn^BKEgxf|#EPh$@G+IJ+lmiBh_=puZsjC{LZ`OX+X*F7@N7j(zl(m&c(_Kc$Zb1EMs=fuV7T^XKF3J0Pdz4;UW%nAIZ9AbX-ntlG!;r@kas4+J6mg%sSDebL`grB`Diur~9%NNAF6e zW^qR*r)RXFgzUk3HZpa>1!K4;pxX#^cSPdoG6DQ*ht^=AU(qHbEWBHZm#i^;8)+n#cq6-?1%)beWl$m^Cl!KR;m%#2!!E*N*M zhNpOhw|JK)5-aca@LueElN(j&*Y?i}xyGY2)ukIdFI#1o<}^gL|E&vX86t`Def1rnTR4$7k4zC=mxgKXJgO1#|wPobT0J@h5t_ttT~zAh0R7$o$60^_W{zxbf2H; zIi~6@Kd?Wg>@hg2r z_-nZcIz71{(ryVxmN%?pO_$v-h|y$4VM zZ?jIRv7ltM^AHkKkvNgtXY0X-VyO;$$H{beU7Fvmvb^M_;SeIx{azPnTqyR@Is+QL zF>}{29S2ZzG2tNCq;f&EqHBjSDjz{HelEd2^=7}yjR3vKJqEJ(8AGAD}O zh809{XI=W3U3uFbaC~Qj?9>QvAhG__^Y_)hss`pasq|BBaQpNVb-xlH0O)G)dnY6- z@nyb0MAlsuft3VNyHQ!3@bdMQOSTo0_vx)(gW)xYjsG^(f#4pq>;C}ef~=Tt$CZx7 z|NTpHcokjeLjMZTbG>FN(n1Kqn0np0+W#DVgU*}HPT;WgBlx;v6|KjxippmzmHBSO zQT)HYo*51kyx#5QepLxoEa$$C%5`4)_#2_Ko;{EapcSg_s&Z7AmvxevFxnRD@*js3;vEWw9dEE;t- z)(U-|>ECor5ZMS{7cp{|s%KfQ0JJi6GOlXoF&=ten>4qS3A^g+b^4n~MRzGeYrk=?z;zb_fj}&d8!xFqoHiK3cg<)v48)f;j`2{ zU$l+Z(0@G=t&fSOO=aBNU4K!H6VF^@cgBOZdqSucn;-nXbs=R&?s%ca9=U@G3i`kN%&$5vhDiYFVa)EkO$cTyIx} zXsD$P1YV2*+!gVY8v#*&zgW&2xG2LucE#W1gA)_D4}Kh~zmet+z2}1hZO}+1+p+>Q z$+%A0NuvjjhftlN_^B`KbUucF0&`niqXW`*_C4XOyoDFUsSwDD@hnYw+}j?(e?HXK zdzj2csio4@p0;0r?bXs4QLkp1-;<`c4vf=Jso-?pnE{fn_=2Iv1!dDw_YVGz9f@bl zTBX`$;v5bPVrVPKx-}7X>MZb`9>?WLb}rcn?ZIGTE|huXw@uN!B8UwlAeH{uTVC|HF8vMqFLb6t zIlpB!!(Tl6SHwTg<#jQGC$K6n_%~NS>(K{k(&r=h(d`zQR}LfX2c;X3D-EMnp^zFA zU4pObY7e5jmy!%OpN+kI-P5fhuelGdOxT>fM%8Wa1j+KdUzH!KKUSv>Qia*5h6PV$ zErEKGbGkMMp$A;%|BI)|R7@>1J^h~GhEVg|QbXndSBy{IL742l} zH#h5Rys4z8JfUu`hP=~nvTB9RVK3Iy-VVG3CPRY?O3lhGWklCKdRn#Ey33@)hiKE$ z%sY59e;Wsxi+qQxMcf!!gQ;%Lcic(Ep6L=L50qiY$%r~>e{lAD-l@FGv=jJe18F_0m&G z<`r4vBKrN3QzOyy?&ZlXCH)|A>{Dkzv9v4k86GCluTkomtD?lk;BgPA^czsNLo)?p zmpKiJgfIH247t*!XLx>O4WiJ{nc~$w{MQ2Pcf;eD){TCHqzJ_{cRY!eQF@mI5(&s6DJOv&LtBL0s#1a+u=3_CI$?*2%I|MRGUW>x0BzW z_zo#3+XG?cMELO!@)ngTsFvZGk4JmDks+_r%z85QgfUL*hqbbIN@`j;J`n+?0twt=ig6#wRA3-?2i_{2e3u8yVUhd|XjP`<$oM<3}dP*cfFG-RcpnTdR z(STxX=xmxlZ)DkHK0r94X@_t9O79H7-;ujY@AimR-%3Oqd>Pc4IDADH$@+S`LCkc8 zOlgpiZ5fs7;_%!Kq3z|pQoPUc-Ccataaw;+Ik&mMz_l{EX`^8)3W8(CKXpRgwS8w0 zpMhr#IcTFQ-PO2(cWVCTp{QfT^wIa#lf0|!?|y1V9%j;6Zk-8 z)JoeRUT=JM;;!uE6mUgu`COH(T}KnMZHw%bs>KD!(f4oY-9rn^Ud`4Al+%;OO@Tzi zkL8S)0dW!lhrLgfwbCC2(I}|7wb`foQ7wsLnRLgeeZXhoN~SGesQw;{A_cr$IZ$!S{F}xfTi)AHHfQ76nF2Ld*YxAkSv8FS|W zK&{_?KD+d0VK4Tdw%+Xs`Y68+o?RxN_{g(-%bS;3%J-Xhh zAL|feYe0m@>#y=7LPmJ6?)&ChBtr2wwI8A88X{Q0Sgms zw|Le}QrIDZ;sClsUqZST;=HGjmuEXaeX@7~uoN1Z(HaH{ec(@^#OS%Q(h}@_sHSbI z&(jxUI@Y$5Bc6|dAU@tQCI#d!Y!QG$fz&{R)3Of$MQ9E|2s0T7B~hLIU`X+F?mQ+C zSKmbwE?pQ>+%5?g#;LrDzm~lJk8-ec5|^`=J!RwLPU`;s>Q0oiRPhyC=YAAqVCN$U zV-#qPDxM!L-TTBYb?zR(-bEu>4Z>=B+8ZBB`&_Sq7sU{mYC#1^Xzoy3e5~jQD5`!$=}95W6ls2!{hL}k+)3O; z*|x`NGfEzm!?qRrnsTL0DI6BXw-#p_7n`mR9>)ZI0KDL{u3qqrmn+i(F?ucU(1b#s zFe;kg(Rgs|7a~VKEiF;rCRN3D&3#n>x@cX{UxTbuAi0{WwyD}YqW2vhen)hnWr|h| znUhy$({5e;jha|GEJGk|C=m(6v>7^+4Vq6GeJ=Zl#&EU97NK|ZPVTG1rggj-kF+F7 zo*I0l=`R-m1Q^IVd#$T$cpr(}?Z|t#?80TpI0)_)uQ847X-REe+?Jiuy1#h1(O^GY z0zB(wpiS)r9yxXu|20#-i4das5~DfjJ0<3}Hl&m0Q%foObVi77#I@e^OS!yP+E`!; z9P8XU^qeNUDxLo~5B$BfMSmdc$g0mZf2oa*v1b>~AK}^b_0|;B*Q)6?jw^RHtK57M z&_|Esls;S?>QyyJXe(A#EjqjcfIy4DLc;~!Jifpp;$N$Qp63JCOuA#V;;iA*$&v%` zHc4o+3rQXifM!IN=PK+Nf>|&KPY7T3(heWuUkivJE6YnQ8`2oct3T1-1~jh1K!(5B z&krH!-exW; zswnPTeqU04S{1Ex_h@sDrs)K{GsMCVZr{_N*tzfFlIfFB*A+Z%8NvLwR0gaY-qveg zv69H4e@Uk5W&lTuG_J=>$GUm_@>#wo?d@cd>`!*$Wr^?6DM=NGh`W%YQ}UQayW!zd zL*3-(i~oO*A75m>{k7jGZnq$s_HjpslKx6+*#}wup=LZ}c_)n(U7*IOiwqTFseEQU zyVN-(6o|tCg)?K1Z+xn3N-|mjW*76H6PnZPI??814q&AF$H}_sO_tz=_XX4&XZ$Ca zHi3pWLADNFYZ-yL8MGfhsi(fwvWq+CA5nkGy71KWCi|qhnso0jbycl2EiK))VnOyb z0om(|V=y|$goXEr;%%hgb)YRAo91vovo|3AnY&UJRKUxvG3tv75xq;WDKqjfPnm8bt00QDkr z^XNspiTe23vnBP`^hg%wz%;ZUWeP0?SORa*Q~#MYQV2hmUrypffeiv}RmW`3_&$N*x8c(%YSK+@bV>JaXJign6xVe0x zz*9{+$ec=Zq)L`&_;3fnn$NcSvOliSws9@bI;dEhbM+&l6{=Py4X3GRV01&53t9qH zoP>p@pY}{K^(NjX-^bvzTldN^@M(~mLDRt-g=n2~;Er~5)mg{aDTUjF(`C}hRciej zQC0F2ZIzUI2W@QN^z+`3gX`B zYE7}21Gu==^ov&=3T}D6iQ3=XeE{7u@d8!4AY|uF5@QY6QuN$+^O;MLAnX&lx%&+KSfX^KD-0MUH+Ypbsi)Kg~r}`dh0XXeb)zmvq zcP{EKfR60dvPg6r$cOzxyX{58p)6O^s>0jQ@U$D|>TjB}+2D)Paz$X*4tPmJbbPTFT9huKu{ zV8}f#W(;qFnCZ69FuKHGtmO>SGGPcAh2He1|MzAf>wmzDNYREDZj*f{`?xGu7zEnA zcm|Fbej8kwc>#5{E(T*qfPG(u=5~RYLvS{IQ|vJiA-?&bmPa*oem(bho&t6EUjOX%ISU$D$(Gu)vS+f{9;Y0 z^%@p%*|X~>lfW+C2HaoT!X8#`~v=ydX0>5CW;`(Fx5n+Ew31C6%^`z z-ZLSu_4S{?u#+RTRs$(#_a&aWV{~L`+{n*h!2c6HQ&h&)F{nUy5!4;BGxfu6Nd)Wy zZWKny9dD~zpbbE=xjc&&n_6vTz3Z-#7_YkMc*kkEX4OkVmts7T>M73^J zRo5#iJmnl>dm2$Uew(dK4c?!Sy-0y0m>-qS{NcsesGF?$aR~<)2pCL*XL%xAL$toT z+O_+OeWAS`C&c%CYxvE8W7<0>dOG({guqTII`9(qZFO+JO(tE;XePajjoKBFQp{8}aE zqy-qT0ClTRb2`zG>*+QgF$hH!Cw?SgCx0Qr&fZ=S@x|9%8GQN7Zr*I%_4@-Au(^V!%_#jc!G9X(ZSi{$F&x zc~n#9{_kJwz_ALcBU&J7MWl)n6=jfww8vU%Q8>z}s6c|EQe_MXNeEAP*Iuhv6=eowI>X`K`=&~qPI2a(9+86RkOpEwG9!L$N*P0^Ut z6JP`2ovYb1+iy&;hk+K%iERhLFCNUzX6XNWHuJzMu3@sNkM_c`Y0KIKF4MNjE@W4S zQIff7pkiSeeyTA-zk%^;OCMy(>yIPK?s5jEOl8R*oseX%aw0~*uILB;ARj;?bz~9wG=JU zoGT$-iA~(xwL9 zhvT3$-ha?C<$<6aej^;3`i_{^CFP= z>Cb?sW8$>W(2{vTuvI(a!TXg_VtWV}@ueEJCJgdc8-TQ;t;rv6Oz2?Nx z(t)G8`zFEkc1Z@GIndIQvO*}F-fHZ}iGh@LU}zMt@I|CgdWV?lt_?OV>vA`k&5>%y zD)_+cif>Y4J(QX60zhyl^(Q&>5K3ps7y&JjJgOanR!KitrDy{Nyqf$a5ET`Mh58c> zrY|piB#{q)zw(uRm(Oiw1HENu+2Il_Ka*!RN2MsULr%#?_6jiL%i@ZB+l^(Io4MU( zl;-dTx04?kZUj5g=` zBo9}`sA~w;J!vXVmhB(cKbcs?`9RevD_tYorQ{|`lO&u^%}cs3x-kU-*$Qvxsb`RK ztcYht_i^=_rGbvyo~DjBf<6B~z_fBL2-7X^tO{OsU=5QAqBvjp*#jL`w}T)#qyFhm zACfrbCD#!G-;l%?LU@WH!yPOS1Ff^0T%L8s18(H;hPi_qr3F3T)-2Syb!*T?3*Bu; z;=CN@1F^+cKqmc{2rwd*dyXSF<1JvUHt_PS&Y6pmeDq1RHmxf5*Ch|eK<0R)CS@TT5_rb7I^3Ht8~-irHs~n2AwjaQPxI7|{~$x#UI| z^80-=T0O%N0U*{%-Re%zoca)hI5JQvOz8&+&cFs$P!h~m%9N6ZT-#3*S*@VA!tEf~ zdW$CqD^64$q4Yi~9;pCX?>YF-8FzuC?$2B~uBpa5#FXR4n{{Qb36oz#V-=vrCg|WK zkAmbt$4Zu3A^P%dCBFP=7jz>FOP1o+^NbxUaB7$N44b+2eooRrD)|2Xc{p|O=`>O$ z(!4{5`KJC1gb1%FS_o_uR9}hD<{KE(;k?M&8h?fTAUPiaUy2_zTYbm|*CoJS)r_F2 zuA@ywSXdfzXfpDQC47Jy26}MvOQE8F<2CPBW2O(>%o@mM!;@4%2<0~OP8_~Nt5jS= z*YD2-t)fc4#5#=B*J~G3F}l?9(PQk4i|=b)g9GHy%14_qMrNX-fXU0_mZ^xcSjj(s z{N}fQkdkx8L^B?-!mud?U11puzx#d8IyZhN_AuNfc}DKd%C5Wn=mISkFh3W5PBM4kRQITl#R2V>=NpeSW(A zLcNXU_`p6W&vT$fIbeHP8*aM|d`@k|Jv_?!scyt@?<`}ufX>@Ahr`f0JwJWdKF|^a z{A%qVuAaZQP~oryy)PW~jMNX|e(QoR4!_ED23wUQgYds6PXNbxAAoZ8(jJEWnGn~V z=kP0_yv4May3XN`ty_GSg@=DO;`_`5*zn-|TrLFzvEvm{Kr?e(whI5%%_#E6cuUx? zjtE~%C0NC`W}bB?^=8z9TIVeOi!;Z_CTxwH?LaxDgL#*iRtWj?WQ<1eiN}d&nBgG@ z@5H`glc?@OH=1fs4QY7!v|i48H&?X`wGFLvPb>h7TpYO8!W(rem$@zm1TZ0=y2aL+ zYdE+T`~N(KD;Sw4$Kx7nd2l@Z%h71#6?ZQdkI}bN1XUW<{O!EqQx})SoDQ@Dfp!&_ z`97cZ16O$zSSJsg3KNfWtPaU9q!%tnO>B`|9a~F!22VgkxL2kzQl-rw%8K_UnC``K z!E2K|v*me~Yb%vxWIwIbF4tS8R~%R@;C#&T)gDD0OFNTgbuZq1K9Dxlp_Z1+{4JjL zW${pTPa^g(u-X>ZQ^kY#zX z>?JgP^^Xms69|JK(QgPi?g8i?HEb`D`zES&F}I&3C7mAW*$7DUn@9rzMBQNP676<2 z^?J2QUFb{IH)BJv;P#P9&ld>{X58TXsLZXzF&2Q5$Y#r1#-iW1`x&xu$KkfXq?u}HZshuTCSO@%S?DAx zK4!j$08Orvec?~$)fk?ab*mCUuWo9Eh7_GLbGdAK`A=~rw5|OBbOm0iNAd`^HKyGBZ=*0DM{!y4 zo<&$8$cr{92ZUb-2dvyPx^QDZ-xt0FF#!gzj*LDb!|eu@5K4b&uelyqx&`sjA1uA+ zPGJS}WSVtS$9_n`v>#F#v)Z-&uBq z1J;q`?7(gv;*Nl&;JqSCJk5~7w>5?NDbct6LkA#Qh|-B|CuW~h4G<%#iON|wLDl=& zJQxa~Nw^}mhF2fhHTNKV)s<+70w9G+?9_7GO$Q;vVnvrkM{HnFxcL~}Y;Z-&16dVs zuq;=;C)KiV)PB&sKE%@P3M)y7|57&R|IY%*{9ta+#8wx)^j#BLxF~h`DkVJeo2t+;(lC3@)ISMlg%tBY^K`2K%JNY|Lbk8Hs<^Nn%Z#bc}hkSjE zcThY!+Il{cHXP$nV_oY3@X@XjDZBY*$nt^%hr1{yu`49-NxdeXKsk4?`4LySh;99` zum^9k63Qtw{Nyir8qX5T*AAc#qOU9gQjq2&BLuv^)->V%8Sx_7T@YPLF_vJ>5JUoHd3V3@J?_I)jC4kOyLQi|4>gpmxVJ$dlTT% zC})thzP_}=%!ImzLs_jFKIXD>%by+hg1B9i! zA!k3b_uL5s6m(hJHa{p;w%adN+jMw}a2(uFc`dCKE5H&QEi5G%bA3~~o5tjYAXv%Q z_V8er1jthIjH*L|CB{DA6m5qVHJSwa9L1T-ZHKy(+JLpWm#bCzktRwiKGrioERy$^ z_3g5LnI};nF9KJar@l1o{qU{1LCFysREu|@9X$dAkyyd5+E`?(Hbtj`3Y(~`*ONW7 zagrIpX&4_pFtsL4C(bhffh9$B z;`D|qBuUd-lx?vvr2c8g+Y+n&9MlonBy~gWA(_^9N9wOW+G$&L(O*!h?}q~YBcN?i z?~A!BYA@wx;4lz0SEX3OQ{8=PbyI`_E3Y=6i=!?=|E+~mZdt>@_4J)6863=Ji{XkH zo&=lo0i)gyGhFSNWVyH&tuwh(vVHto8f&jrhJ;W5L4K1HuYR4Fc72#}b|WNinP!*BX(g#U0cQw`S7I5Cpc7_PZwAY+xsj_W4cye=m0k}`zij%Y4 zoz>Mi5{hK$3OF#d3>?Hju6!|G`GkFp_&8~7@Z5X64EHXF_wEWEQzyGnEC0x!i_TmM zM|uVq)jDxHhVLD5xnuhx6y)}>#wJj4>_zOez==DRwM))ifWp#m4*i1Xs&%S^lq{c3 z;EaCV2tX`~H0!K-oQ=fzIIZ9MHGW6Cw@rP8G#m!Gfw6K;ZW(SY;b^QSkW%b3FkBj- zNKeT4mrwEY zuc(WqRQRON2N9R1KAtK7X#=;}NQT)?nK>?-mty*!F;qau6da|dscLf_-#>Ihf~Se; zJ-NAq>+N60jya4>R^-D4?iLL@N#(_Adpqd)#I0)HuiclseYl$s>|#Ote=}6C5t*H* z2KE#lvQYh;CbCjMp5=r9YZcC6-&)@z5A3QEU~W%=Lpnz1VCK@NG@0+B*3wAWwFr}P znU0-FPv8tM7|@+Q)UorL z1}WM>3$x=nRNe75wB+UR3iy`uhnbl2Xz&FeWxh|irW`q)?0nlZazqlFrtOO3Om;jo z(Sdk+_7MK|@LJMSOFuu9RCR>rMkBw)s?f<^@olyAk=p z5p+~`eJnQ63I4+AgR%o`Vv!@n%`f6qD0DnrRKx~h$i@qWC}5c|KOq$G0qV&}mk?4W zWa#Tph;zs{FiNs@955MMV^aS6+pfHl;oc^~#VlYNnjVZ%t9C+>b!{-t;`TN*F2GU| zht=KxuF)ijQ4>zr=>tB5ke3gL`y3EJV5eh@+rwXr%R^PV~(}PF4T_$pMV$(lr#ThcZ`Id7U8HU@J%{K_ayf`LMc{B;G^K@?)>SSMZC-|9^ZpAyeM&+ z1z=61KA);RR@{8>(&5VmavI$cZ^wx;8~lJXGDiJ(;#Re{s-iKLH+ChEL*}=8Ep(6Y zWm>{~A;DZkm1n5TMOn|a=CiJXFVK4s0)f~3lI26uzM^sxN4KFjq@^_vmiI~M13~Hp z)oQU2lTUV#5^h;Lp8D#(_4erm>$OE8&~&6F(3keNnuemhs57Vxi6Wr5%mU5ML(Cu`4*x^& z=zW5D0fI8QLByThSXF*U>p{@0PoO=_kI)Y7*Xi?(*?!SsM_7b|j4i!ZbEVj9M@j-` z-de}re|FWwtw6Ut0VJGVf!O86n0ED_?!=iZ{k`NK*EK5tjte-OM-{-tMuSx|Ke6_X ztXkZh328}U2(IS|2a8G44ylV!Us3W0iahK5OUlD3{qFZb3Wkofe!ttn8r~3myV*E(o|CI1}QO2kO*>*oNESt7G~Ljx?6{ z1y~cK`OzqI)G51=(T6~gB-Sx`9RTcxcpd=x)Q6ase0&>V73{4_psj@5k><-cXpJ?b zbIymrSMIScQ+a={tq^w-yzOSNS~}+X98JXu0qxjyQ}fF2*IXzUfM1Zh+kw-^6KB#R z^lEytnhCWGPLb4T&D-mv670D#@xr>o@)*r^#wlGJ{f#I7k4R24w776IUwK?bvdz00 zH8F~66?kBaTA(I&^hfN8UDZO~pFrV9Ys(^Qs>wUcq0TH{TVcov{Rgu_RFLFD+z{0s9_v{P?7J@d+VRm`RAVaWM-xf z%J+{UEnj1T$+oMSDnMrNj`STG3IQRbwJ~GC@s+dF2pN#dt>4sWI<1!i&DDWRbe;qX z(xNHZE6}Yw`Q^i()K|^QIxu-|+Fol-)ETv?dM3EdyhDG%pv^{enw^S<-0-#q1DnC; ze*v+W^A~=qtJ?4s?cWs3oAR#4Jd26;|7>2=59_}AKK~z`Qz7KV308Wzp&rM}LBY3p zQb(GB2nao~4*GmA(JH)jbQd0iccu>-reZ)FZ3}rUY_evo@q#hJdD3lSmF?lhotUYO zauW1NlO^%j@S&?>68p&JP7m(S;C`@*(@`G=s-a5?3$NNIa1Pvd4CW+N7eh|oRg4tt z3np-BW$Z#wdp)~Pu!ynKiL+XX@B%Y(zzgV&9SjA+20B!RJ=V2ETf;k|^?|CS z7YnKQ(Nu5BP0$GkQjC?>G$A6hJ>JrUQi7`Puuy+z%NJww%Pr@pM3gP-eqa2+tc2=~EeK5MW&XJrp8Ew|5)=Xq5jB9Vv#ro%b**TKsAEX~} z@czt+AFRIX2pvJ1&m^{u6(Z-TDb^bh1OTkMey|efji}!(jMJj;T-+TzU^r-@P7RtD zC^Jo{b=k$;h-gJ;lb?1d>{LaH{SQwAIG=gjmOaaTtW!p-SnfpSY3NmBZOqnkhm8Y8 z;6ZW9TnCs8$JlMU7l_hu=cc|jVN?umSz|^(SjCX8&%cD|AFQ>^0Y~n;+uUT0o~bExa5!2j_?*vAMi-7nF{^e-aoyzM&2%6+2KBV% z&ri^_S%)z*%K+}x<3Lqf+vs(GVB!5)GRqn9XxKc5NdlI$508kl7crOo6Jqp}6cDc< zANUWIB?N}Pc>fxFA+)r1aMzrS&6<5edevh9=3MC~R00qQvwlB??MK_Trs1t`I}w2#OG9tb#^apn3C<$4`A7EaT+!fn0Td=z>-fJnqedQ%XZQV;IKF9AFH6bSjtKV`MI1 z&>lL|aGzlMH}>GJggM~IMMz7<$mqx*g`01mmGr!z2iOcA6* zM96;m+;gLlLEGCydYl05vfaE`7Eqi9e_k0FP-F}WxYg&uSvj!Z=WhA@@0QIVs9R@0 zdOfxQ6MEPh)xb#=0x1OC*uGi!UYe1fYvE``Ngj6U)?p^2D-o_sP(@p>NR87xVJ7D*oLQrnHO# zc)^t*5JanU0u|_k=HdlotjmrZ;jWlq3cxUZs3m(J=EwM?HGpU;qo|-F=!2_wc2uq) z)=?x~QPSAb4;Kvd!?$J|{fgRKvz>Z(X`_P0)7OhK+B&vLYA^of(mnoJb@r5u57PR) z1#|a@_Wg*43w`aNxY8DzOv^l9Ch=2OLy$W&4dMG@Cz0QsIGR@`$%DKo$1V<-d#SGp zUX!fSgt*Go4p%?jCuV7aOUy3L58=q^l7}uX&+4Xt_hzHTy66qpt?e2YSJx zTIJI4e*d_*c=bW{%rlJqKA}j)sACOYfDg*vwFHSX&BeZ5yq3mdJF(H9^enY|gAY%) zKf?A}L>zm_Jj^11^QCm(Vk{)oz0{56F)2-GC$g19wnu`|sqCT?vN0F@t=aL!%lIhM zf>xs><#|Ka=s92Wm2=@GmK`4+tzz~cT(B0ZpmTSCrC1VJimj;5wk@3i2H$aAS$BzV zORFoYqqPaOzrViu?ASWc^DzsarHfYp%WL%8;{t6c+b=gP=z+gd&U>B2rvMX(>NMSB zHG-En#e~)X63jC4?ZX3UdA46GL!F$dou~0vC&o`BsokzeitczpV|im2cRD~RNuqC9 zK-gD0kX;6kA%P1}UqMQX(jFdt%dThwmz%vv&ClV#HxGp1&ruvy);ne7spSv}A`XC1W&*w*==W2Z385?uT+ztICY-~;1>#yb(Gs6Y=MYwl)!B-2(l zdT?7oc%@bZs?Y^saCf#jEK|<4G!eEKMdOZ*v0&vl+Zl`oqNxHp?hlZ(P06X$2a8h_ zZCdSDs+1)n-ppdww7VnaQ?xf&@VO`+Le!TlAlqgHb;_xCaOsuXro7b`!l6mcWAx*i zaZ1-*ITHb`p`1o^t$q&L!g4ockiTzK5&G-=QHc%PeLpCU*BMkvv8G1o-xT^mHC6}D z0OOc5b*41?2*2@3wdOm&a$jP1>Wwa!2ODdqgtnqG+glXSua@W4B=JK55nkKHX|o0(vuCFk4*x zO}242svSTlPH5c-d51eSZ@)oM)cZl=fQMnw+WE=j1>MiXk)0R7d`05y7P5*j;rwjs}Z?S!rEiINtmta=Nv8>!f`|#yd~j`->;1Yb1mBJG?lSN8IHI+DO-A z5i7LZ8tC)K^8Uite$&H8Y6 zkoMc}tPJMB^>JFcih2cl#3a98Xj8j;c#8WC%-$p=Yh*FxuQBty)!h;vB=@1EfsVY$ z7b+P`*IY*vC=Z!z&S2pzM{5?4yL>)Tnxntqhg?3rvfdGFXX{nlA)PxwJugQHh+}&Sfg;8FxMC2!|YFhn5 z_mG94QMbhL%6iCjWHd9tvQnk{-u-agB8fiN(Eul6gu^tML~6z9S0i=T!!6T@qR~Q} z-m|rl^DZ`C`x^A+brm3YcWSRUYqWr#pnia|{XWc@nygE*sU_a1nV-Nl>tJVaWXE&NiCs0#002nJ z9t|yN9+ucv;n8Ek#i!t>fzxUc#Ra@e2zMN*n!sdhsz}`vElLt*$NC;TR(uMol~)DP z#YAukP{AzVvTx$OC-bp@N+Vh3l;VvaKM>dOI_Tcf!?;mz2=>SYCMk24`?H3Lb>TiN z=_p?H3*ZxH{!(5mU)uD5EG~6YFx;sphoq+8{6&tGA9?)YeC4Q;k9Fy{P>X^4Xzt9% z;HB#k=~GU9oU}$PL|a{EWFP8ZTxGVaOgWxYWi>BN|6C}17`{j_*dP_S|8GmyoFofs z`JX$%Q2|JPZl5fPQ9Xp3pbJ3JS(#x1-kOpVvSo~ufQ1)~oZN=T2%#$F#fdw-nH=viAqMGU%jK2&m&GW4~7gh4Np;G6A2sdFy?nze>_HwpL`{E3@5`zO|Ue{*}JmTwE*4rH=@Be+Mn4kx;%I~FvlzeM!9*v`B?sAg zqV?sgNSLupT&*j5_GlSdBl4YB=3P4riqz6N`(1_icO_Ualx?3vfG>))LG|o`u-cg= zyiV|O>uh}-lVR$HYE;!UX1ZyIbxZYA+IXn%?A5|r_&(vv{Y`fIB7iuvIs8^yuar0E zU~feO>t-Nn5JX~St0-#l$=IDfv=|JYF;fWmb`~&WjqFA?J`BVZyuTlL%Tb^`AECWV zSGR>_tU?+uvtJ3uVtvKjm)$lU8-(o5bId8iTd4J1s1Npfs1H~qRJ08C`6qe9TjYhn zHE|F9eUA%%ji$%}J<#X{4=ZZUO7iEbGt20X2{KDO)D9cN+r77fNn|m2CiHZnn*=)% zWob)VXXnL(rQ(xZk3;pON|Hi$ctF1(fuepdCP>oqKF_Xs-ayJ4D`nxF? zc@lz#saH;0oR>{LvAY$LzTAdHx*uXtH_75jZzEGKw*+A?gV5@Zc-z&n!6hKjqU8Jp z&$?ht_YeSyhrOMg{$oDD&tR9ghLRAvt!a#v!kZz1_hQY&)qa{bxGJF+xHdEA`*1&| zP6k+)2gZb1!p2RkZV^7{{i0pkSkBgdcMr%?$z*N|2mSf`c{s~SU(M~XW3=h?VH4XPRy z%iN2FuG!8YH#pefG#&hDV>3tA>w&oeF4sTFeOOU7s^#7H-E4;n!wWHB%{Wvc3b@Dw z>B%r@>-(KWlvmzD!>IeTf(O$vvW+2%v5<6-M@h4jZQYLCA3=WQSq+a*VPrlkfN$T! z3$4|x5~%Nr8d&@WYgS(^WyQP60b(p_zWJdSsCsRS8FaVdV-g+% zV(R0Xq(qs-!%0jcO(UQILooqbYt*PBwG%ww>1OEH2;W$_lXUJlZ)g9DT6i{K5gzC4sw| zfq3e&JEpN`IY$bb@1|{yW3pT@Cu~7vOzdG)sGJx);GZ zk#ac7rKsUpaNx9o&`=DrNPjCqpQnm#<@?K�KU7vYN;oYjTlOMG+sHTpO&miDq^9t; zV_oDijBVA&mL)}P=L*?=jgM7^uCl?Fw_8DECANH29Rjb}4hT5{f)xDg>Z8x*TAhFv z_Xj@Zx<+7b8wI727ZWuJ=9TWp1)&ej_XqJBZl`X46`F1d}?>cBV`XnnPGWY z%gNg={`0?!j3Z7SLpLRjIXA%z;YUmL;`X=sCEpGB3FJUa4N%ADF_C|T1yk)k^&WWl~%$M)C;fj5Juaq+Ic^^b3wt)7wR5!Rtr$Bgf%Q*g1ciQP4Q3G&+8 zDfbE{)VFwW1jMS~Ej9YVktmY%8-)F!dcPw`p?7^sC-UQ0$l7uy<8dQkvoO`g(E9MI zg@}yyV$Rng6w76t**aekA<|xE*ER+5UadaT%*YrFJ>s$h`Drj_Go`IR;EL?z$Tjqf zrhFR=^1~&^<=J&!3LUPF{)pABd(^PbnuTg6^NTwd%r0a62?p=VMA-W?Ji9NwCG5n+ z$L`LHZXs|xVMr~g?}O4cj_zkhKkW^9aMlGuxf58C+O_u3c#Zl>J^7W;x;(y!e0Mmi zgj=7W=mm_6o+jk}aQ4G}KxId`$V?v)4bSZN`DUn!Z=G*bQ+XXOA!O%3Dwv{or?(sX zOJ45S>_@RS`K5N3j``$OKyWZH7rKc8Mt-T$eZy{@zZU~BQb-oJ5L2ZJh}}V93u8sB z`9KAT3&93F8g+8lM}>NA$cVbi{ocp9m+G_G zHxU0BQMSANbQFi2XN$YPz@MMiJMICIi7mVnv(4^T>2yorS{BW{gO`puNzKV+($*KU zbqBZZ1R?|}@0IVBo^s&&ZQ=I2GELVUPXR?_qXc|FJWqk3b197awmXiv_T9kOR>hVK zDRHw_SI>w zO=7Jou)yT~?8)#=m*TEIa?7D(ujdA0FQ!i}Xu^`D0QOekkoSoo-G;%5KD>4UZ}{87 z-r`horuKB6cY3nzYTDRuHqY*s-8y*W^yZQbw6%Tz++p;c{Sp0r=cd3Ae>%T4=kNWw zS=>xW&hY(_@@50Wc9SP%Ls}vE;|kV3z_9##_Wn6D3=X03LVv9lp*)!iXafaneYn!o zNO+M*^T~&It$4!m`8R#M7*IdgTW;ik3py3Fsv$6IzA|@^uL0EN0=@VS*s*)lw9E}O zL%kjNr#s7l$X%l7`Y;C`MnHJ@<`;>kTQ~Klmw92-u2)T{jcIvykM8$LWR#3f{;B}E z?8mC=yf4Gnta;$a)7FbLZ&4NeeAW$1Am}21#p~cOH(2vMid3}1eF;w6M9*lCDN?vY zGr#eykCr*%m8(txx=<&jpEM{fnAj9Zmp4$qLU$H6=-1*^#leu~cN}h{A2*Q(yP26^ zL6&fHg!a=G~LO!nw|k=%q~4d}B>luv*Zx*}q;(+{uiI5}cu zLcue!OAR0&17GAHUHflAny?7%Cics%{ZTdA;6paG?`Ktlq7$QHI&ckmQn-sWG1YOU}>D8QSw(gcB&!ui#YJLYtSD>B8dQq zOw>OC3iI*RujBF^|4dMB^-p!_9`@``F0p+ZPicD_A0iz(%>XzElm+^>3OW6&H*)4D z%>Jd0N*S)B5uOKfcpx*Gq#1efXY-O2bs2Oe%ma2EeS3LSg2~o2ESaf};I{_ht{bC~ zug)H0q`}K=eQD^)XVl?!vRtR-0~x%+CSHBMZK)jzMVw$u+CQW&L0alW+Pk8f zTR9;Tg}PyttuJnMin4q@9^TKWs0by056}h(L!m^hV0zjE?|@@pjq)2{oe5JcJg{

    - The ACTIVSg70k grid on the globe as one 765 kV line opens: buses raised by their frequency, branches that pick up its power glow amber, those it leaves glow cyan + The ACTIVSg70k grid on a globe as one 765 kV line opens. Buses rise with their frequency, branches that pick up the line's power glow amber, and branches that lose flow glow cyan

    @@ -56,12 +56,12 @@ const view = await View.create(document.querySelector('canvas')!, { grid.set('Bus', [102], { kv: [345] }); // drawn the next frame ``` -A scene is plain data, typed from latkit's schema: `JSON.stringify(scene)` writes the JSON that the +A scene is plain data, with types from latkit's schema. `JSON.stringify(scene)` writes the JSON the command line reads. ## On the command line -The same scene and data, written as JSON, render to a picture, or to video with `--frames`: +Write the same scene and data as JSON to render a picture, or a video with `--frames`: ```sh latkit render scene.json --data grid.json --output grid.png @@ -110,14 +110,14 @@ latkit render scene.json --data grid.json --frames frames.jsonl --fps 60 --outpu

    Two thousand buses of a synthetic Texas grid: generators as amber discs sized by output, comets along its highest-voltage lines - The speed of 1,456 machines over ten seconds after a fault, near ones bright and swinging hardest, far ones dark - Routes between forty of the world's great cities, along great circles on a globe, comets flowing on them - A microgrid controller as blocks in nested groups, wired port to port with routed wires and a feedback loop + The speed of 1,456 machines over ten seconds after a fault. Machines near the fault are bright and swing hardest, and far ones are dark + Great-circle routes between forty of the world's great cities on a globe, with comets flowing along them + A microgrid controller as blocks in nested groups, with wires routed port to port and a feedback loop Sixty-four filaments in cyan, amber, and coral, twisting around a three-dimensional torus knot - A Clifford strange attractor of 260,000 translucent points, colored by the way each step turns + A Clifford strange attractor of 260,000 translucent points, colored by the direction of each step

    -latkit drew every picture here; [the examples](https://latkit.readthedocs.io/en/latest/examples/) show each one with the code that draws it. +latkit drew every picture here. [The examples](https://latkit.readthedocs.io/en/latest/examples/) show the code for each one. ## Documentation diff --git a/RELEASING.md b/RELEASING.md index 95c0e139..b65b7220 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -5,18 +5,18 @@ Changesets records changes and opens the version PR. `cargo xtask version` copie into the Cargo workspace (its package version and its `latkit` dependency) and updates `Cargo.lock`. 1. Merge the version PR and let CI pass on that commit. -2. The release workflow builds the npm tarball with `cargo xtask package` and runs - `cargo xtask package-test` against that exact tarball, including strict TypeScript consumers and - browser rendering. +2. The release workflow builds the npm tarball with `cargo xtask package` and tests that exact + tarball with `cargo xtask package-test`, including strict TypeScript consumers and browser + rendering. 3. `cargo xtask release` verifies the clean commit, the tarball hash, and the test receipt, and - packages both Cargo crates. It then publishes, or verifies already published, npm, the core - crate, and the CLI, in that order. + packages both Cargo crates. It then publishes npm, the core crate, and the CLI, in that order. A + package already published is verified instead. 4. Tags are created only after every registry artifact matches the build. A retry skips identical artifacts and completes any missing publication. Different content at an existing version stops the release, and a registry error never counts as a missing artifact. Once a version is tagged, later commits at that version publish nothing. -To recover, rerun the failed workflow, or dispatch **Release** on `main`; a manual dispatch needs a +To recover, rerun the failed workflow or dispatch **Release** on `main`. A manual dispatch needs a successful CI run for the exact commit. Do not bump versions just to recover a partial release. Rebuild and retest after changing or committing source files. @@ -26,7 +26,7 @@ account settings, not files in this repository. Set up package ownership and aut the first release. See [npm trusted publishers](https://docs.npmjs.com/trusted-publishers/). Users install the CLI with `cargo install latkit-cli`. Adding `--locked` reproduces the dependency -versions a release was tested with; CI and release builds always use it. +versions a release was tested with. CI and release builds always use it. latkit's binary format carries no version. A release that changes its layout says in its changeset that files written before it no longer open, and updates `crates/latkit/tests/fixtures/patch.hex`. diff --git a/benchmark/README.md b/benchmark/README.md index 9cc169d6..9db42cfc 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -1,10 +1,9 @@ # Benchmark -The benchmark runs each scenario in Chrome at increasing sizes and measures each phase's time, GPU -time, and GPU work, checked against committed baselines. The page steps every frame itself, starting -each one when the GPU has finished the last rather than on the display's clock, so a run draws the -same frames on any device. Frame pacing is judged by the browser gate (`npm/examples/gate/gate.ts`) -instead. +The benchmark runs each scenario in Chrome at increasing sizes. It measures each phase's time, GPU +time, and GPU work, and checks them against committed baselines. The page steps every frame itself: +each frame starts when the GPU has finished the last one, not on the display's clock, so a run draws +the same frames on any device. The browser gate (`npm/examples/gate/gate.ts`) checks frame pacing. ```sh cargo xtask bench --quick # each scenario once at its smallest size: phases and work @@ -16,20 +15,21 @@ cargo xtask bench --update # lay this run's results over the baselines A run fails on: -- An error, a frame drawn while nothing changed, a pipeline compiled where it was first used - (`late`), or resident data over its budget. -- A phase whose time or uploaded bytes (and, for loading, resident bytes) grow with size faster - than its declared order. Each upload (`uploads`) counts as at least a page of bytes. -- More work than `baselines/work.json`. In a phase that acts once, the counts the data alone sets +- An error, a frame drawn when nothing changed, a pipeline compiled when first used (`late`), or + resident data over its budget. +- A phase whose time or uploaded bytes grow with size faster than its declared order. For loading, + resident bytes count too. Each upload (`uploads`) counts as at least one page of bytes. +- More work than `baselines/work.json`. In a phase that acts once, counts set by the data alone (`uploaded`, `uploads`, `evaluated`, `packed`, `jobs`, `pipelines`, `late`, `resident`) may not - exceed the baseline. The other counts (`written`, `resolved`, `cleared`, `readbacks`, `texts`), - every count of a phase that acts each frame, and every count of a scenario fed by fetches may - reach 2× the baseline, since they depend on which frame a job or fetch finishes in. + exceed the baseline. All other counts may reach 2× the baseline, because they depend on which + frame a job or fetch finishes in. These are `written`, `resolved`, `cleared`, `readbacks`, and + `texts`, every count in a phase that acts each frame, and every count in a scenario fed by + fetches. - In a full sweep on the machine that recorded `baselines/.json`, a phase whose fastest - repeat is more than 1.25× slower than its baseline, plus one timer tick (16 ms) for a phase that - acts once. Judging the fastest repeat means a stall of the machine, which slows one repeat, does - not fail the run; a regression slows every repeat. + repeat takes more than 1.25× its baseline, plus one timer tick (16 ms) for a phase that acts + once. Only the fastest repeat counts, so a machine stall that slows one repeat does not fail the + run. A real regression slows every repeat. -On a machine with two GPUs, Chrome is made to draw on the faster one. Reports and results are -written to `target/bench/`. Scenarios are defined in `page/scenarios.ts`, the data they draw in -`page/data.ts`, and how a phase is measured in `page/stage.ts`. +On a machine with two GPUs, the runner makes Chrome draw on the faster one. Reports and results go +to `target/bench/`. Scenarios are in `page/scenarios.ts`, the data they draw in `page/data.ts`, and +how a phase is measured in `page/stage.ts`. diff --git a/benchmark/run.ts b/benchmark/run.ts index 07321062..c38cfc0f 100644 --- a/benchmark/run.ts +++ b/benchmark/run.ts @@ -7,7 +7,7 @@ * `--quick` runs each scenario once at its smallest size. It checks every phase and the work * baseline in under a minute on a GPU. Growth and times are judged only in a full sweep. * `--repeats` sets the runs per size (3 by default). `--update` merges this run's results into the - * committed baselines, keeping entries it did not run; only a full sweep on the GPU writes times. + * committed baselines, keeping entries it did not run. Only a full sweep on the GPU writes times. * `--software` draws with SwiftShader on the CPU, as Linux always does. Work counts the same there, * times are only reported, and no shader can hang the GPU. * @@ -52,7 +52,7 @@ const software = flags.software || process.platform === 'linux'; const here = path.dirname(fileURLToPath(import.meta.url)); const baselines = path.join(here, 'baselines'); const results = path.resolve(here, '../target/bench'); -/** Gives WebGPU timestamps, which measure each frame's GPU time, full precision; Chrome otherwise +/** Gives WebGPU timestamps, which measure each frame's GPU time, full precision. Chrome otherwise * rounds them to 100 µs. The page steps each frame as soon as the GPU finishes the last, so the * display's frame rate never affects a run. */ const FLAGS = ['--enable-webgpu-developer-features']; diff --git a/crates/latkit-cli/README.md b/crates/latkit-cli/README.md index ef1fb576..1c39f14b 100644 --- a/crates/latkit-cli/README.md +++ b/crates/latkit-cli/README.md @@ -2,7 +2,7 @@ Renders latkit scenes to images, image sequences, raw frames, and video on the machine's GPU, with the same renderer as the browser package. Inputs are latkit's JSON, so any language can write them. -The package is `latkit-cli`; the executable is `latkit`. +The package is `latkit-cli` and the executable is `latkit`. ```sh cargo install latkit-cli @@ -15,8 +15,7 @@ latkit schema scene > scene.schema.json ``` [The documentation](https://latkit.readthedocs.io/en/latest/cli.html) covers commands, inputs, -frames, outputs, and diagnostics, and this CLI draws all of -[its examples](https://latkit.readthedocs.io/en/latest/examples/). -[The 70,000-bus fault](examples/bus-fault/README.md) and -[the WECC240 fault](examples/wecc240-fault/README.md) videos are made with Python and this CLI -alone. +frames, outputs, and diagnostics. This CLI draws all of +[its examples](https://latkit.readthedocs.io/en/latest/examples/). The +[70,000-bus fault](examples/bus-fault/README.md) and +[WECC240 fault](examples/wecc240-fault/README.md) videos are made with only Python and this CLI. diff --git a/crates/latkit-cli/examples/branch-opening-orbit/README.md b/crates/latkit-cli/examples/branch-opening-orbit/README.md index f8367fe7..117532e1 100644 --- a/crates/latkit-cli/examples/branch-opening-orbit/README.md +++ b/crates/latkit-cli/examples/branch-opening-orbit/README.md @@ -3,15 +3,15 @@ [Preview](preview.png) · The video is not in Git: [reproduce it](#reproduce) to write `animation.mp4` here. -A close orbit around the network: the bearing turns 108°, from −70° to +38°, while the center +A close orbit around the network. The bearing turns 108°, from −70° to +38°, while the center drifts west from 84.5° W to 87° W at 37.5° N. Zoom stays nearly constant and pitch stays between 46° and 50°, so the view never pulls back or dips. Center and bearing change smoothly between keyframes. The saved simulation and visual scales come from the [original example](../branch-opening/README.md): -70,000 buses, the Little Rock–Newark 765 kV line opening at 1 s, 0.4× playback, -reduced height, no plot, 1920 × 1080 at 60 fps. The video is 23.517 seconds (1,411 frames). -Rendering the orbit leaves the original example and its video untouched. +70,000 buses, the Little Rock–Newark 765 kV line opening at 1 s, 0.4× playback, reduced height, no +plot, 1920 × 1080 at 60 fps. The video is 23.517 seconds (1,411 frames). Rendering the orbit leaves +the original example and its video untouched. ## Reproduce @@ -22,27 +22,26 @@ python crates/latkit-cli/examples/branch-opening-orbit/render.py --cli target/cl python crates/latkit-cli/examples/branch-opening-orbit/render.py --cli target/cli/latkit.exe ``` -Python writes the CLI inputs; the latkit CLI draws and encodes the video. The -[data recipe](../branch-opening/render.py) and state boundaries come from the -original example. Generated inputs go to `target/branch-opening-orbit/`. -Nothing is simulated again, the frequency recording is not copied, and no image sequence is written. +Python writes the CLI inputs, and the latkit CLI draws and encodes the video. The +[data recipe](../branch-opening/render.py) and state boundaries come from the original example. +Generated inputs go to `target/branch-opening-orbit/`. The recipe does not rerun the simulation, +copy the frequency recording, or write an image sequence. -Edit [camera.json](camera.json) to change the orbit. -`at` is simulation time in seconds, `center` is longitude/latitude, -`scale` is the plane's pixels per coordinate unit, and `pitch`/`bearing` are degrees. -The recipe writes a `camera` value into every line of the CLI's frames file. Keyframes are +Edit [camera.json](camera.json) to change the orbit. `at` is simulation time in seconds, `center` is +longitude/latitude, `scale` is the plane's pixels per coordinate unit, and `pitch`/`bearing` are +degrees. The recipe writes a `camera` value into every line of the CLI's frames file. Keyframes are interpolated with monotone cubic Hermite splines, so camera velocity is continuous, no value overshoots a keyframe, and the path starts and stops gently. Zoom is interpolated in log space. `--preview --at 5.8` renders the frame at that simulation time, with its camera. `--prepare-only` writes the inputs and prints the complete CLI command. `--source`, `--work`, and `--output` choose -other data, scratch, and output directories; `--limit N` renders only the first N frames. -Each output's JSON records its camera path, source checksum, timing, and render command. +other data, scratch, and output directories. `--limit N` renders only the first N frames. Each +output's JSON records its camera path, source checksum, timing, and render command. -Frequency and height mean the same as in the original example: frequency is estimated from -filtered voltage angles, and height exaggerates it on a nonlinear scale. The close framing shows -the network's response rather than the outer state boundaries. +Frequency and height mean the same as in the original example. Frequency is estimated from filtered +voltage angles, and height exaggerates it on a nonlinear scale. The close framing shows the +network's response, not the outer state boundaries. -The delivered video was decoded end to end (1,411 frames). The samples bracketing each frame's time -were checked at every frame, and the framing was inspected around the opening and along the camera +The finished video was decoded end to end (1,411 frames). At every frame, the samples on either +side of its time were checked. The framing was inspected around the opening and along the camera path. diff --git a/crates/latkit-cli/examples/branch-opening/README.md b/crates/latkit-cli/examples/branch-opening/README.md index e87d1b6c..ed029c81 100644 --- a/crates/latkit-cli/examples/branch-opening/README.md +++ b/crates/latkit-cli/examples/branch-opening/README.md @@ -1,18 +1,17 @@ -# ACTIVSg70k line opening — Python + latkit CLI +# ACTIVSg70k line opening with Python and the latkit CLI [Preview](preview.png) · The video is not in Git: [render the saved study](#render-the-saved-study) to write `animation.mp4` here. The synthetic 70,000-bus network responds as the Little Rock–Newark 765 kV line -(`branch_59757_59774_1`) opens at 1 s and stays open. The video shows simulation time -0.6–10 s at a constant 0.4× speed: **1,411 frames, 60 fps, 1920 × 1080, -23.517 seconds**. The map fills the frame, and a dashed gold line marks the open -branch. The camera's bearing sweeps 30° to show the response from changing angles. +(`branch_59757_59774_1`) opens at 1 s and stays open. The video shows simulation time 0.6–10 s at a +constant 0.4× speed: **1,411 frames, 60 fps, 1920 × 1080, 23.517 seconds**. The map fills the +frame, and a dashed gold line marks the open branch. The camera's bearing sweeps 30° to show the +response from changing angles. -[`render.py`](render.py) uses only Python's standard library and the latkit CLI. latkit -draws the network, state boundaries, labels, and frequency legend, and encodes H.264 -with its native Windows MP4 output. The recipe needs no Rust code, browser, Python -imaging library, or external encoder. +[`render.py`](render.py) uses only Python's standard library and the latkit CLI. latkit draws the +network, state boundaries, labels, and frequency legend, and encodes H.264 with its native Windows +MP4 output. The recipe needs no Rust code, browser, Python imaging library, or external encoder. ## Render the saved study @@ -24,12 +23,11 @@ python crates/latkit-cli/examples/branch-opening/render.py --cli target/cli/latk python crates/latkit-cli/examples/branch-opening/render.py --cli target/cli/latkit.exe ``` -An installed CLI works too: `python crates/latkit-cli/examples/branch-opening/render.py`. -Use `--preview --at 2.5` for a still at another time, `--limit 120 --output target/preview` -for a short movie, or `--prepare-only` to write the inputs and print the complete -CLI command. The scene and camera JSON are written to `target/branch-opening/render/`; -to experiment with the view, edit them and run the printed CLI command directly. -Rerunning the recipe overwrites them. +An installed CLI works too: `python crates/latkit-cli/examples/branch-opening/render.py`. Use +`--preview --at 2.5` for a still at another time, `--limit 120 --output target/preview` for a short +movie, or `--prepare-only` to write the inputs and print the complete CLI command. The scene and +camera JSON are written to `target/branch-opening/render/`. To experiment with the view, edit them +and run the printed CLI command. Rerunning the recipe overwrites them. The optional positional argument selects another saved-study directory, which holds: @@ -43,42 +41,39 @@ target/branch-opening/ ``` The recording is local data, **not included in Git**. The small state-boundary file, -`states.geojson`, comes from the bus-fault recording and is included here. -`animation.json` and `preview.json` record the command, source checksum, scales, -dimensions, and render timing. Native MP4 output currently requires Windows. +`states.geojson`, comes from the bus-fault recording and is included here. `animation.json` and +`preview.json` record the command, source checksum, scales, dimensions, and render timing. Native +MP4 output requires Windows. ## Data and display The study used `ghcr.io/lukelowry/gridkit:develop-perf`, image ID -`sha256:befba9e16edce341888e73c05b9d2c35aedf1357fa18b54c56114dbe25ccd565`, -and the original ACTIVSg70k case, SHA-256 -`8c3d1edd9deaf409436b1f38e55f41c963d08eae7709474bcfb2c1dffadbdb15`. -The full simulation manifest is stored beside the recording. +`sha256:befba9e16edce341888e73c05b9d2c35aedf1357fa18b54c56114dbe25ccd565`, and the original +ACTIVSg70k case, SHA-256 `8c3d1edd9deaf409436b1f38e55f41c963d08eae7709474bcfb2c1dffadbdb15`. The +full simulation manifest is stored beside the recording. -Only bus voltage angles were monitored, at 60 Hz over 0–10 s, with both sides of -the event retained. Frequency is the causal 20 ms filtered derivative of unwrapped -voltage angle, expressed as Hz deviation from 60 Hz. The raw angles were streamed -and discarded. This is a bus voltage-frequency estimate, not generator rotor speed. -The switching angle jump produces a brief impulse (about −1.647 to +0.683 Hz). +Only bus voltage angles were monitored, at 60 Hz over 0–10 s, keeping samples on both sides of the +event. Frequency is the causal 20 ms filtered derivative of unwrapped voltage angle, expressed as +Hz deviation from 60 Hz. The raw angles were streamed and discarded. This is a bus +voltage-frequency estimate, not generator rotor speed. The switching angle jump produces a brief +impulse (about −1.647 to +0.683 Hz). Colors saturate at ±0.20 Hz. Signed height is exaggerated by -`height_scale * asinh(df / 0.02) / asinh(0.35 / 0.02)`; it is a visual encoding of -frequency, not physical displacement. `height_scale` includes a gain of 0.45, which -scales every height down by 55% while keeping its sign and nonlinear shape. The -camera keeps a fixed center, zoom, and 48° pitch, with bearing easing from −10° to -+20° over the full recording. This keeps the initial peak in view and makes the -geography easier to follow. There is no bottom plot or trace legend. - -`HEIGHT_GAIN` and `BEARINGS` near the top of `render.py` control the exaggeration -and camera sweep. The camera path follows simulation time, so a preview at any time -matches that moment in the movie. - -For slow motion, latkit interpolates between the recorded samples and computes the -height expression on the GPU. The recipe appends only the samples the next picture -needs and forgets older ones. It selects samples with the same comparison latkit uses: -the lower sample must be **at or before** the requested time. The samples on either -side of the event stay distinct, so no picture before the opening shows data from after it. -The recipe writes no interpolated or height cache and no image sequence. - -The CLI validates the generated documents before rendering. The delivered MP4 was -also decoded end to end and checked around the event and sample boundaries. +`height_scale * asinh(df / 0.02) / asinh(0.35 / 0.02)`. Height is a visual encoding of frequency, +not physical displacement. `height_scale` includes a gain of 0.45, which scales every height down by +55% while keeping its sign and nonlinear shape. The camera keeps a fixed center, zoom, and 48° +pitch, with bearing easing from −10° to +20° over the full recording. This keeps the initial peak +in view and makes the geography easier to follow. There is no bottom plot or trace legend. + +`HEIGHT_GAIN` and `BEARINGS` near the top of `render.py` control the exaggeration and camera sweep. +The camera path follows simulation time, so a preview at any time matches that moment in the movie. + +For slow motion, latkit interpolates between the recorded samples and computes the height +expression on the GPU. The recipe appends only the samples the next picture needs and forgets older +ones. It selects samples with the same comparison latkit uses: the lower sample must be **at or +before** the requested time. The samples on either side of the event stay distinct, so no picture +before the opening shows data from after it. The recipe writes no interpolated or height cache and +no image sequence. + +The CLI validates the generated documents before rendering. The finished MP4 was also decoded end +to end and checked around the event and sample boundaries. diff --git a/crates/latkit-cli/examples/bus-fault/README.md b/crates/latkit-cli/examples/bus-fault/README.md index 6c3b1435..28a8dac5 100644 --- a/crates/latkit-cli/examples/bus-fault/README.md +++ b/crates/latkit-cli/examples/bus-fault/README.md @@ -1,18 +1,18 @@ -# 70,000-bus fault — Python + latkit CLI +# 70,000-bus fault with Python and the latkit CLI -[Preview](preview.png) · The animation is not in Git: [`render.py`](render.py), run as below, -writes `animation.mp4` here. +[Preview](preview.png) · The animation is not in Git. Run [`render.py`](render.py) as below to +write `animation.mp4` here. The saved ACTIVSg70k fault recording: simulation time 0.6–1.4 s at 0.1× speed, then 1.4–10 s at 1×, as 997 frames at 60 fps and 1920 × 1080. The fault is on bus 56923 at 1 s and clears at 1.15 s. -Each bus is colored and raised or lowered by its frequency deviation; a plot below traces six buses, +Each bus is colored and raised or lowered by its frequency deviation. A plot below traces six buses, with a playhead. [`render.py`](render.py) uses only Python's standard library. It writes the grid and the traced -buses as latkit JSON, whose frame columns are slices of the saved float32 caches, and one line per -output frame that appends that frame's samples. latkit reads the states' GeoJSON, draws the map -with its title and legend above the plot, and encodes the MP4: 5.5 s for all 997 frames on an RTX -2080 SUPER. +buses as latkit JSON, with frame columns that are slices of the saved float32 caches. It also +writes one line per output frame that appends that frame's samples. latkit reads the states' +GeoJSON, draws the map with its title and legend above the plot, and encodes the MP4. All 997 +frames take 5.5 s on an RTX 2080 SUPER. ```sh python crates/latkit-cli/examples/bus-fault/render.py --cli target/cli/latkit --preview @@ -22,9 +22,10 @@ python crates/latkit-cli/examples/bus-fault/render.py --cli target/cli `` is the recording's directory: `buses.json`, `topology.json`, `usa-states.geojson`, `depth-metadata.json`, and the frame-major caches `depth-frequency.f32` and `depth-height.f32`, in the metadata's bus order. These files are not in this repository. Frequency is a causal 20 ms -filtered derivative of bus voltage angle; height is `asinh(df / 0.02) / asinh(0.35 / 0.02)`; colors +filtered derivative of bus voltage angle. Height is `asinh(df / 0.02) / asinh(0.35 / 0.02)`. Colors saturate at ±0.05 Hz. The recipe writes its JSON to `target/bus-fault/` and keeps any scenes and cameras already there, so -your edits survive a rerun. `--prepare-only` prints the command without rendering; `--limit N` -renders only the first N frames. `animation.json` and `preview.json` record the command and timings. +your edits survive a rerun. `--prepare-only` prints the command without rendering, and `--limit N` +renders only the first N frames. `animation.json` and `preview.json` record the command and +timings. diff --git a/crates/latkit-cli/examples/wecc240-fault/README.md b/crates/latkit-cli/examples/wecc240-fault/README.md index 158f51dc..50c475d7 100644 --- a/crates/latkit-cli/examples/wecc240-fault/README.md +++ b/crates/latkit-cli/examples/wecc240-fault/README.md @@ -1,17 +1,16 @@ -# WECC240 fault — GridKit + latkit CLI +# WECC240 fault with GridKit and the latkit CLI [Preview](preview.png) · The video is not in Git: [reproduce it](#reproduce) to write `animation.mp4` here. -The reference WECC240 fault is applied at **John Day, bus 4005**, at 1.00 s and -cleared at 1.15 s, with fault impedance R = 0, X = 0.01 p.u. The model contains -243 buses, 447 branches, 103 synchronous machines, and 37 inverter groups. -GridKit simulates 0–10 s; latkit renders **1920 × 1080, 60 fps, 1,561 frames** -into a 26.017-second H.264 MP4. Playback slows to 0.1× around the fault and runs -at 0.5× elsewhere. +The reference WECC240 fault is applied at **John Day, bus 4005**, at 1.00 s and cleared at 1.15 s, +with fault impedance R = 0, X = 0.01 p.u. The model has 243 buses, 447 branches, 103 synchronous +machines, and 37 inverter groups. GridKit simulates 0–10 s, and latkit renders +**1920 × 1080, 60 fps, 1,561 frames** into a 26.017-second H.264 MP4. Playback slows to 0.1× around +the fault and runs at 0.5× elsewhere. -The preview, case, compact recordings, and reproduction scripts are in Git. -Intermediate CSV files and generated CLI inputs stay in `target/`. +The preview, case, compact recordings, and reproduction scripts are in Git. Intermediate CSV files +and generated CLI inputs stay in `target/`. ## Reproduce @@ -24,14 +23,13 @@ python crates/latkit-cli/examples/wecc240-fault/render.py --preview python crates/latkit-cli/examples/wecc240-fault/render.py ``` -Skip the first command to render the included recording without Docker. -`render.py` uses `target/cli/latkit.exe` (or `target/cli/latkit`) if present, and otherwise -an installed `latkit`. Use `--cli PATH` to select another executable, -`--preview --at 1.12` for a still, or `--limit 120 --output target/wecc-preview` -for a short movie. `--prepare-only` writes the scene, data, camera, and frame inputs under -`target/wecc240-fault/render/` and prints the CLI command. -Native MP4 encoding currently requires Windows; the Python recipes use only -the standard library. The state boundaries come from [`../branch-opening/states.geojson`](../branch-opening/states.geojson). +Skip the first command to render the included recording without Docker. `render.py` uses +`target/cli/latkit.exe` (or `target/cli/latkit`) if present, and otherwise an installed `latkit`. +Use `--cli PATH` to select another executable, `--preview --at 1.12` for a still, or +`--limit 120 --output target/wecc-preview` for a short movie. `--prepare-only` writes the scene, +data, camera, and frame inputs under `target/wecc240-fault/render/` and prints the CLI command. +Native MP4 encoding requires Windows. The Python recipes use only the standard library. The state +boundaries come from [`../branch-opening/states.geojson`](../branch-opening/states.geojson). ## Simulation and display @@ -41,35 +39,34 @@ The run used `ghcr.io/lukelowry/gridkit:develop-perf`, pinned for execution to: sha256:befba9e16edce341888e73c05b9d2c35aedf1357fa18b54c56114dbe25ccd565 ``` -`simulate.py` resolves the requested tag to its local image ID before running, -and runs it with two CPUs and networking disabled. To reproduce with this exact image, pass -that ID with `--image`. The saved [metadata](recording/metadata.json) includes -the image labels, case hashes, solver settings, sample times, and recording hashes. +`simulate.py` resolves the requested tag to its local image ID before running, and runs it with two +CPUs and networking disabled. To reproduce with this exact image, pass that ID with `--image`. The +saved [metadata](recording/metadata.json) includes the image labels, case hashes, solver settings, +sample times, and recording hashes. -Monitors sample at 240 Hz and keep both sides of the fault's application and clearing: -2,403 samples in total. The map shows bus frequency deviation, estimated with a -20 ms causal filtered derivative of unwrapped voltage angle; the angle jumps at switching -cause brief impulses. Colors saturate at ±0.1 Hz. Height exaggerates the same quantity -on an `asinh` scale; it is a visual encoding, not a physical displacement. +Monitors sample at 240 Hz and keep both sides of the fault's application and clearing, 2,403 +samples in total. The map shows bus frequency deviation, estimated with a 20 ms causal filtered +derivative of unwrapped voltage angle. The angle jumps at switching cause brief impulses. Colors +saturate at ±0.1 Hz. Height exaggerates the same quantity on an `asinh` scale. It is a visual +encoding, not a physical displacement. -The upper plot shows actual GENROU rotor-speed deviation (`omega × 60 Hz`) for -4039_G, the machine with the largest absolute excursion. The lower plot shows -faulted-bus voltage. A white playhead follows simulation time. These are distinct -measurements: estimated bus frequency, rotor speed, and voltage. +The upper plot shows actual GENROU rotor-speed deviation (`omega × 60 Hz`) for 4039_G, the machine +with the largest absolute excursion. The lower plot shows faulted-bus voltage. A white playhead +follows simulation time. These are distinct measurements: estimated bus frequency, rotor speed, and +voltage. -The runtime case differs from the source case only in its monitors and their names. -GridKit reports initialization adjustments to small Hygov time constants and -Repca reactive limits; the complete messages are in [solver.txt](solver.txt). -`animation.json` and `preview.json` record render settings, timing, and hashes. +The runtime case differs from the source case only in its monitors and their names. GridKit reports +initialization adjustments to small Hygov time constants and Repca reactive limits. The complete +messages are in [solver.txt](solver.txt). `animation.json` and `preview.json` record render +settings, timing, and hashes. ## Source -`source.case.json` comes from GridKit's -`cases/PhasorDynamics/WECC240/WECC240.case.json`; only line endings are normalized -to LF. Fault parameters match its -`examples/PhasorDynamics/Validation/WECC240/WECC240.solver.json`. -The upstream case README credits the National Laboratory of the Rockies test -case repository. The GridKit notice is retained in [SOURCE-LICENSE.txt](SOURCE-LICENSE.txt). +`source.case.json` comes from GridKit's `cases/PhasorDynamics/WECC240/WECC240.case.json`, with only +line endings normalized to LF. Fault parameters match its +`examples/PhasorDynamics/Validation/WECC240/WECC240.solver.json`. The upstream case README credits +the National Laboratory of the Rockies test case repository. The GridKit notice is retained in +[SOURCE-LICENSE.txt](SOURCE-LICENSE.txt). This product includes software produced by UT-Battelle, LLC under Contract No. DE-AC05-00OR22725 with the Department of Energy. diff --git a/crates/latkit-cli/src/args.rs b/crates/latkit-cli/src/args.rs index 4ede7dc9..774e375d 100644 --- a/crates/latkit-cli/src/args.rs +++ b/crates/latkit-cli/src/args.rs @@ -59,8 +59,8 @@ pub(crate) enum Command { #[arg(long, default_value = "30", value_parser = deadline)] deadline: Duration, }, - /// Write a dataset as time tiles: `frames.json` (which `--tiles` opens), the dataset without its - /// sampled fields' histories, and those histories in spans at every level of detail + /// Write a dataset as time tiles: `frames.json` (which `--tiles` opens), the dataset without + /// the histories of its sampled fields, and those histories in spans at every level of detail Tile { /// The dataset, `[NAME=]FILE` (`-` reads stdin) #[arg(value_name = "[NAME=]FILE")] @@ -94,7 +94,7 @@ pub(crate) struct Job { /// Output file, `%d` pattern for an image sequence, or `-` for stdout #[arg(short, long, value_name = "FILE")] pub(crate) output: Option, - /// Output format. Defaults to the one the output's extension names, else PNG + /// Output format. Defaults to the format of the output's extension, or PNG #[arg(long, value_enum)] pub(crate) format: Option, /// JPEG quality, from 1 (smallest) to 100 (sharpest) diff --git a/crates/latkit-cli/src/main.rs b/crates/latkit-cli/src/main.rs index c55b7bd5..0d30f409 100644 --- a/crates/latkit-cli/src/main.rs +++ b/crates/latkit-cli/src/main.rs @@ -140,8 +140,8 @@ fn query(source: &Source, query: &str, deadline: Duration) -> Result<(), Failure Ok(out.commit()?) } -/// Writes the dataset as time tiles in directory `output`: the manifest, the dataset without its -/// sampled fields' histories, and those histories in spans of `chunk` entries. +/// Writes the dataset as time tiles in directory `output`: the manifest, the dataset without the +/// histories of its sampled fields, and those histories in spans of `chunk` entries. fn tile(source: &Source, output: &Path, chunk: u32, deadline: Duration) -> Result<(), Failure> { let data = inputs::dataset(source, &latkit::host::Native::new(), deadline)?.snapshot(); let tiled = data.to_tiles(chunk, |path, bytes| { diff --git a/crates/latkit-cli/src/render.rs b/crates/latkit-cli/src/render.rs index f44c6c9d..96c20c91 100644 --- a/crates/latkit-cli/src/render.rs +++ b/crates/latkit-cli/src/render.rs @@ -146,7 +146,7 @@ impl Sequence<'_> { let at = |error: Error| Failure::at(error.at(index).at("frames"), path); apply(&self.inputs.data, &self.inputs.scenes, &frame, patches).map_err(at)?; steer(self.views, &frame); - // Only invalid input is the frame's fault; an output that fails names itself. + // Only invalid input is the frame's fault. An output that fails names itself. let drawn = block(job.deadline, video.frame(&mut self.views.iter_mut().collect::>())); drawn.map_err(|error| if let Error::Invalid { .. } = error { at(error) } else { error.into() })?; self.report.progress(video.frames(), video.time()); diff --git a/crates/latkit-cli/src/sinks.rs b/crates/latkit-cli/src/sinks.rs index f9f8c718..3b185ce6 100644 --- a/crates/latkit-cli/src/sinks.rs +++ b/crates/latkit-cli/src/sinks.rs @@ -1,6 +1,6 @@ -//! Output destinations: an output written whole or not at all, and the video sinks besides the -//! library's MP4: image files, raw RGBA, and YUV4MPEG2. An output's errors name it in their message, -//! whoever writes it, since they reach the command through the library's video. +//! Output destinations: an output written all at once or not at all, and the video sinks other than +//! the library's MP4: image files, raw RGBA, and YUV4MPEG2. An output's errors name the output in +//! their message, whoever writes it, since they reach the command through the library's video. use std::collections::VecDeque; use std::fs::File; @@ -117,8 +117,8 @@ impl Commit for Mp4 { } /// Pictures as image files, encoded a few at a time on the host's workers. Either one file per -/// frame, numbered by the output's pattern and each written whole and in order, or one file for a -/// single frame. A held frame (where nothing moved) repeats the previous frame's file. +/// frame, numbered by the output's pattern and each written complete and in order, or one file for +/// a single frame. A held frame (where nothing moved) repeats the previous frame's file. pub(crate) struct Images { to: To, format: ImageFormat, diff --git a/crates/latkit-js/src/canvas.rs b/crates/latkit-js/src/canvas.rs index a8c8ae19..973afea8 100644 --- a/crates/latkit-js/src/canvas.rs +++ b/crates/latkit-js/src/canvas.rs @@ -9,7 +9,7 @@ use crate::Error; #[wasm_bindgen] pub struct Handle(pub(crate) PageView); -/// Pairs each of `views` with its panel, the one encoding of a canvas's or a shot's layout. `at` +/// Pairs each of `views` with its panel. Canvases and shots both encode their layout this way. `at` /// holds four fractions per view, `[x, y, width, height]`, in order. Fails unless `at` has four /// values per view. pub(crate) fn layout(views: Vec, at: &[f32]) -> Result, Error> { diff --git a/crates/latkit-js/src/gpu.rs b/crates/latkit-js/src/gpu.rs index 63d10a84..5986f46d 100644 --- a/crates/latkit-js/src/gpu.rs +++ b/crates/latkit-js/src/gpu.rs @@ -1,4 +1,4 @@ -//! The GPU: it creates views and canvases, and reports its frames, budget, and work. +//! The GPU, which creates views and canvases and reports its frames, budget, and work. use wasm_bindgen::JsCast; use wasm_bindgen::prelude::*; diff --git a/crates/latkit-js/src/shot.rs b/crates/latkit-js/src/shot.rs index 8f165b14..7e706f4b 100644 --- a/crates/latkit-js/src/shot.rs +++ b/crates/latkit-js/src/shot.rs @@ -1,8 +1,8 @@ //! Pictures and videos of views. A shot crosses as JSON, `{ size, scale, transparent, capture, -//! elapsed }` with times in seconds, and its layout as a canvas's does: views, and four fractions -//! per view. Canvases showing those views keep their last frames meanwhile. The page decides how -//! long to wait. An image's pixels cross once, and its files are encoded in workers. A video returns -//! the bytes each frame produced, for the page to write. +//! elapsed }` with times in seconds. Its layout crosses as a canvas's does: views, and four +//! fractions per view. Canvases showing those views keep their last frames meanwhile. The page +//! decides how long to wait. An image's pixels cross once, and its files are encoded in workers. A +//! video returns the bytes each frame produced, for the page to write. use std::cell::RefCell; use std::io::Write; @@ -63,16 +63,16 @@ impl Image { } } -/// Takes `shot`'s (JSON) picture of `views`, each at its four fractions of `at`, once they are -/// complete as its capture says. +/// Takes a picture of `views` as `shot` (JSON) describes, each view at its four fractions of `at`, +/// once the views are complete as the shot's `capture` says. #[wasm_bindgen] pub fn image(shot: &str, views: Vec, at: &[f32]) -> Result { let (shot, views) = shot_of(shot, views, at)?; Ok(task(async move { Ok(Image(latkit::web::image(&shot, &views).await?)) })) } -/// The shot `json` describes, with its panels from `views` and `at`, and no deadline, since the -/// page decides how long to wait; and its views. +/// The shot `json` describes, with its panels from `views` and `at` and no deadline (the page +/// decides how long to wait), and its views. fn shot_of(json: &str, views: Vec, at: &[f32]) -> Result<(latkit::Shot, Vec), Error> { let panels = layout(views, at)?; let shot = crate::json::("shot", json)?.deadline(None); @@ -96,7 +96,7 @@ impl Write for Written { } /// A video of a shot's pictures of its views, encoded with WebCodecs as frames are drawn. One -/// operation runs at a time; cancelling its task drops the video with it. +/// operation runs at a time. Cancelling its task drops the video with it. #[wasm_bindgen] pub struct Video { /// The video. `None` while a frame is drawing it, and after it is finished or cancelled. @@ -114,8 +114,8 @@ impl Video { *self.frames.borrow() as f64 } - /// Draws the next frame of its views. Resolves to the file bytes ready to write, once there is - /// room for another frame. + /// Draws the next frame of its views. Resolves to the file bytes ready to write, once the + /// encoder can take another frame. pub fn frame(&self) -> Task { let (inner, written, frames) = (self.inner.clone(), self.written.clone(), self.frames.clone()); let views = self.views.clone(); @@ -154,9 +154,9 @@ impl Written { } } -/// A video of `shot`'s (JSON) pictures of `views`, each at its four fractions of `at`, at `fps` -/// frames per second. It encodes MP4 at `bitrate` bits per second, by default about 0.15 bits per -/// pixel per frame. +/// A video of pictures of `views` as `shot` (JSON) describes, each view at its four fractions of +/// `at`, at `fps` frames per second. It encodes MP4 at `bitrate` bits per second. Defaults to about +/// 0.15 bits per pixel per frame. #[wasm_bindgen] pub fn video(shot: &str, views: Vec, at: &[f32], fps: f64, bitrate: Option) -> Result { let (shot, views) = shot_of(shot, views, at)?; diff --git a/crates/latkit-js/src/view.rs b/crates/latkit-js/src/view.rs index 93b923b4..22615f28 100644 --- a/crates/latkit-js/src/view.rs +++ b/crates/latkit-js/src/view.rs @@ -74,8 +74,8 @@ impl View { Ok(()) } - /// Lays out every vertex row without a position again, using the scene's layout. Resolves to - /// one patch per dataset, as bytes, that writes the computed positions into the data. + /// Runs the scene's layout again on every vertex row without a position. Resolves to one patch + /// per dataset, as bytes, that writes the computed positions into the data. pub fn arrange(&self) -> Task { let pending = self.view.with(latkit::View::arrange); task(async move { diff --git a/crates/latkit/README.md b/crates/latkit/README.md index 36093ef6..2d1723cb 100644 --- a/crates/latkit/README.md +++ b/crates/latkit/README.md @@ -20,7 +20,7 @@ fn main() -> Result<(), latkit::Error> { ``` - `default-features = false`: data, patches, LATK, JSON reading, queries, expressions, colormaps, - GeoJSON, and images; no GPU dependency. + GeoJSON, and images, with no GPU dependency. - `gpu` (default): scenes, views, picking, layouts, and rendering into wgpu targets. - `native`: system fonts, file loading, jobs, and Windows Media Foundation video. - `web`: browser integration on `wasm32-unknown-unknown`. @@ -28,8 +28,7 @@ fn main() -> Result<(), latkit::Error> { `cargo install latkit-cli` installs the `latkit` executable. `npm install latkit` installs the browser runtime and plain-data builders. -The binary format uses 64-bit absolute frame indexes. Sampled values are `f32`; coordinates are -`f64`. -See the [format specification](https://latkit.readthedocs.io/en/latest/guide.html#bytes). +The binary format uses 64-bit absolute frame indexes. Sampled values are `f32` and coordinates are +`f64`. See the [format specification](https://latkit.readthedocs.io/en/latest/guide.html#bytes). MIT licensed. Third-party notices are in `THIRD_PARTY.md` and `colormaps/NOTICES.md`. diff --git a/crates/latkit/shaders/compute/evaluate.wesl b/crates/latkit/shaders/compute/evaluate.wesl index a69249c8..3cafc7ad 100644 --- a/crates/latkit/shaders/compute/evaluate.wesl +++ b/crates/latkit/shaders/compute/evaluate.wesl @@ -1,7 +1,7 @@ -// The expression machine: each row of a mark runs the mark's channels, which store into the row's -// state words. All rows run the same instructions, so control flow stays uniform across a -// workgroup. A dispatch covers the row pages the mark lists: only the changed pages, when only some -// rows changed. +// The expression machine, a stack interpreter. Each row of a mark runs the mark's channels, which +// store into the row's state words. All rows run the same instructions, so control flow stays +// uniform across a workgroup. A dispatch covers the row pages the mark lists, which are only the +// changed pages when only some rows changed. // // Evaluation ignores transitions. During a transition, channels store into the settled words, and // each frame `ease` computes what every row shows from its frozen and settled words by the mark's diff --git a/crates/latkit/shaders/compute/fields.wesl b/crates/latkit/shaders/compute/fields.wesl index 4e736587..a1596fdf 100644 --- a/crates/latkit/shaders/compute/fields.wesl +++ b/crates/latkit/shaders/compute/fields.wesl @@ -1,17 +1,17 @@ -// Text distance fields. For each run, converts the coverage a host set into each texel's signed -// distance to the text's edge (positive inside), stored as the byte +// Text distance fields. For each run, converts the coverage the host rasterized into each texel's +// signed distance to the text's edge (positive inside), stored as the byte // `0.5 + distance / (2 TEXT_SPREAD)`. The field covers the run plus `TEXT_SPREAD` texels on each // side. Partial coverage places the edge within its texel, so antialiased edges keep subtexel // precision. // // A texel's squared distance to the text is the minimum, over the field's texels, of each texel's -// own squared distance (0 inside the text, a partial texel's subtexel distance squared, effectively -// infinite outside) plus the squared distance to that texel. The distance to the outside works the -// same way. The minimum splits into a pass down each column, then a pass along each row. Only -// texels within the spread can bring a texel within the spread, so each pass looks only that far, -// which is exact wherever the field is not clamped. After the column pass, one of a texel's two -// distances is always 0, so one signed word holds both: the distance to the outside above zero, and -// to the text below. +// own squared distance plus the squared distance to that texel. A texel's own squared distance is 0 +// inside the text, its subtexel distance squared where coverage is partial, and effectively +// infinite outside. The distance to the outside works the same way. The minimum splits into a pass +// down each column, then a pass along each row. Only texels within the spread can bring a texel +// within the spread, so each pass looks only that far, which is exact wherever the field is not +// clamped. After the column pass, one of a texel's two distances is always 0, so one signed word +// holds both: the distance to the outside above zero, and to the text below. import package::lib::types::{ TEXT_SPREAD, FIELD_COVERAGE, FIELD_WIDTH, FIELD_HEIGHT, FIELD_COLUMNS, FIELD_AT, FIELD_ROW, FIELD_WORDS, diff --git a/crates/latkit/shaders/compute/probe.wesl b/crates/latkit/shaders/compute/probe.wesl index f4df4746..da44a2ed 100644 --- a/crates/latkit/shaders/compute/probe.wesl +++ b/crates/latkit/shaders/compute/probe.wesl @@ -4,16 +4,16 @@ // the step needs. // // The hover takes three steps over every mark it reaches. `nearest` keeps the least key over all -// instances within reach of the probe: the distance in 1/64 pixel, then the mark drawn last. +// instances within the probe's radius: the distance in 1/64 pixel, then the mark drawn last. // `topmost` keeps the last instance of that mark at that key, and `describe` writes where that hit -// lies. Rows hidden behind other drawn marks are not reached. `under` writes the nearest depth -// drawn around the probe, which gestures rotate and zoom about. +// lies. Rows hidden behind other drawn marks are not hit. `under` writes the nearest depth drawn +// around the probe, which gestures rotate and zoom about. // // Queries write after the probe's words, each mark from the word its `query` names. `hits` appends -// every instance within reach of the query's point while the mark has room. `boxed` sets a bit for -// every row with an instance in the query's box, whether or not something drawn covers it. `locate` -// writes where one drawn row lies on screen. Every row an answer names is a held row, which the -// view maps to a table row using its record of that frame. +// every instance within the query's radius of its point while the mark has space for hits. `boxed` +// sets a bit for every row with an instance in the query's box, whether or not something drawn +// covers it. `locate` writes where one drawn row lies on screen. Every row an answer names is a +// held row, which the view maps to a table row using its record of that frame. import package::lib::types::{ NONE, FADE_OUT, POINTS, STROKES, MARK_EDGES, PROBE_KEY, PROBE_INSTANCE, PROBE_DEPTH, PROBE_X, PROBE_Y, @@ -191,7 +191,7 @@ fn reach(s: Seen, p: vec2f) -> Reach { } case STROKES: { let q = nearest_on(p, s.a, s.b); - // The nearer end gives the sample a trace's hit is at; for an edge, which of its ends. + // The nearer end gives the sample a trace's hit is at. For an edge, it gives which end. var end = select(s.part, s.part + 1u, length(q.xy - s.b.xy) < length(q.xy - s.a.xy)); if mark.kind == MARK_EDGES { end = u32(2u * end >= path_count(s.item)); diff --git a/crates/latkit/shaders/compute/texts.wesl b/crates/latkit/shaders/compute/texts.wesl index d34cea3c..b88e6511 100644 --- a/crates/latkit/shaders/compute/texts.wesl +++ b/crates/latkit/shaders/compute/texts.wesl @@ -1,10 +1,10 @@ -// The labels that show, and the texts they need. `claim` writes each labeled row's claim into the -// cells its label covers, each cell keeping the greatest; `texts` then asks the host to set the -// text of each label that keeps its room, while the atlas lacks it and its anchor is near the view -// (and, in a diagram, its text is large enough to read), and marks a set text in view as seen, so -// the atlas keeps it. Rows whose tile is leaving claim nothing. `draw/glyphs.wesl` decides from the -// same claims whether each label shows. Each pipeline is made for one primitive and compiles only -// that primitive's anchors. +// The labels that show, and the texts they need. `claim` writes each labeled row's key into the +// grid cells its label covers. Each cell keeps the largest key. `texts` then asks the host to set +// the text of each label that is not crowded out, when the atlas lacks the text and the label's +// anchor is near the view (and, in a diagram, its text is large enough to read). It also marks a +// set text in view as seen, so the atlas keeps it. Rows whose tile is leaving claim nothing. +// `draw/glyphs.wesl` decides from the same claims whether each label shows. Each pipeline is made +// for one primitive and compiles only that primitive's anchors. import package::lib::types::{NONE, VISIBLE, LABEL_CODE, WANTED, item}; import package::lib::bind::{eye, mark, word}; @@ -22,7 +22,8 @@ import package::lib::drawn::drawn_row; @group(2) @binding(14) var cells: array>; // A labeled row as the texts passes see it: its text code (NONE without a label to show), its -// anchor, and its run table entry: both words 0 until the text is set, NONE when it draws nothing. +// anchor, and its run table entry. The entry's words are both 0 until the text is set, and NONE +// when it draws nothing. struct Labeled { code: u32, a: Anchor, diff --git a/crates/latkit/shaders/draw/glyphs.wesl b/crates/latkit/shaders/draw/glyphs.wesl index ad22bee3..6429801d 100644 --- a/crates/latkit/shaders/draw/glyphs.wesl +++ b/crates/latkit/shaders/draw/glyphs.wesl @@ -1,11 +1,10 @@ -// Glyphs: every labeled row's text, placed from its anchor each frame. A label shows where its text -// is set, its row shows, and it fits the room its anchor gives, fading with its row's tile and, in a -// diagram, once too small to read. A label beside its row also shows only where it kept its claim -// on the cells it covers this frame (`compute/texts.wesl`), so labels never overlap; nothing -// depends on an earlier frame. A halo of the background color keeps text readable over anything -// below it. The ports variant draws each port's label beside it, inside its marker. Text is a -// distance field, so edges stay crisp at any size, and the halo is a wider threshold of the same -// field. +// Glyphs: every labeled row's text, placed from its anchor each frame. A label shows when its text +// is set, its row shows, and it fits its anchor's `room`. It fades with its row's tile and, in a +// diagram, once too small to read. A label beside its row also shows only if it kept its claim on +// every cell it covers this frame (`compute/texts.wesl`), so labels never overlap. Nothing depends +// on an earlier frame. A halo of the background color keeps text readable over anything below it. +// The ports variant draws each port's label beside it, inside its marker. Text is a distance field, +// so edges stay crisp at any size, and the halo is a wider threshold of the same field. import package::lib::types::{ NONE, LABEL_CODE, LABEL_COLOR, PORT_WORDS, PORT_CODE, PORT_UV, SIDE_LEFT, SIDE_RIGHT, SIDE_TOP, TEXT_SPREAD, @@ -51,7 +50,7 @@ struct Varyings { fn quad(t: Text, corner: u32, color: vec4f) -> Varyings { var out: Varyings; let q = vec2f(f32(corner & 1u), f32(corner >> 1u)); - // Outside a diagram, text has a fixed size; snapping to whole target pixels keeps it crisp. + // Outside a diagram, text has a fixed size. Snapping to whole target pixels keeps it crisp. var p = mix(t.quad_lo, t.quad_hi, q); if frame.space != SPACE_DIAGRAM { p = round(p * eye.pixel_ratio) / eye.pixel_ratio; @@ -91,8 +90,8 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: let a = anchor(instance); let size = length_px(mark.label_size); let t = text_at(uv0, uv1, size, a.at, a.align); - // A label shows while its row shows, it fits, and it kept its room, as legible as its size - // allows, and fades with its row's tile. + // A label shows while its row shows, it fits, and it kept its claimed cells. Its opacity + // follows its legibility at this size and its row's tile fade. let room = fits(t.hi - t.lo, a.room) && (!claims_room(a) || keeps(cells_of(t), claim_key(instance))); let alpha = f32(a.shown && room) * legibility(size) * drawn.presence; if !(alpha > 0.0) { diff --git a/crates/latkit/shaders/draw/points.wesl b/crates/latkit/shaders/draw/points.wesl index e73fbd5c..606ac96a 100644 --- a/crates/latkit/shaders/draw/points.wesl +++ b/crates/latkit/shaders/draw/points.wesl @@ -1,7 +1,7 @@ // Points: one instanced quad per vertex row, shaded by its marker. A group's rows draw as // containers. A quad covers its marker and any status ring or shadow, plus one pixel of antialiased -// edge. The focus variant redraws the lit rows over everything, with quads enlarged for their glow; -// a group glows only around its border, so its contents stay visible. The ports variant draws one +// edge. The focus variant redraws the lit rows over everything, with quads enlarged for their glow. +// A group glows only around its border, so its contents stay visible. The ports variant draws one // quad per port of every row, on its side of the row's marker. import package::lib::types::{STATUS, MARKER_ICON, MARKER_SHAPE, MARKER_BLOCK, GROUP, SHADOW, SPACE_DIAGRAM}; @@ -27,7 +27,7 @@ struct Varyings { @location(3) @interpolate(flat) half: vec2f, @location(4) @interpolate(flat) status: vec4f, @location(5) @interpolate(flat) shape: u32, - // A group's depth; for a port, 1 when its reference names a net. + // A group's depth. For a port, 1 when its reference names a net. @location(6) @interpolate(flat) extra: f32, // The row's focus bits, which choose the focus draw's glow. @location(7) @interpolate(flat) focus: u32, diff --git a/crates/latkit/shaders/draw/strokes.wesl b/crates/latkit/shaders/draw/strokes.wesl index d89bdbf6..0776e57b 100644 --- a/crates/latkit/shaders/draw/strokes.wesl +++ b/crates/latkit/shaders/draw/strokes.wesl @@ -19,7 +19,7 @@ import package::lib::daylight::daylight; import package::lib::color::output; struct Varyings { - // The corner in clip space; in the fragment, the target pixel, which `pixel` reads. + // The corner in clip space. In the fragment, the target pixel, which `pixel` reads. @builtin(position) clip: vec4f, @location(0) @interpolate(flat) a: vec2f, @location(1) @interpolate(flat) b: vec2f, @@ -106,9 +106,9 @@ fn vertex(@builtin(vertex_index) corner: u32, @builtin(instance_index) instance: } // The fragment's position in CSS pixels, in the same screen space as `a` and `b`. It comes from the -// fragment's own position, never an interpolated value: interpolation skews across a quad whose -// ends lie at very different depths, and a renderer that clips the quad at the screen's edge, as -// lavapipe does, shifts it. +// fragment's own position, never an interpolated value. Interpolation skews across a quad whose +// ends lie at very different depths, and a renderer that clips the quad at the screen's edge (as +// lavapipe does) shifts it. fn pixel(v: Varyings) -> vec2f { return v.clip.xy / eye.pixel_ratio; } diff --git a/crates/latkit/shaders/lib/center.wesl b/crates/latkit/shaders/lib/center.wesl index 943a5774..04312d8b 100644 --- a/crates/latkit/shaders/lib/center.wesl +++ b/crates/latkit/shaders/lib/center.wesl @@ -27,9 +27,9 @@ fn corner_center(c: Corner) -> Center { return Center(to_screen(clip), c.shown && c.ahead && clip.w > 0.0); } -// Where drawn row `item` lies on screen: a point's center; the middle of a path's middle segment, -// or a trace's last sample; an area's label anchor, inside its largest polygon; or a face's -// centroid. The row shows there when it is visible and that point is on screen. +// Where drawn row `item` lies on screen: a point's center, the middle of a path's middle segment, a +// trace's last sample, an area's label anchor (inside its largest polygon), or a face's centroid. +// The row shows there when it is visible and that point is on screen. fn row_center(item: u32) -> Center { switch PRIMITIVE { case POINTS: { diff --git a/crates/latkit/shaders/lib/color.wesl b/crates/latkit/shaders/lib/color.wesl index b74595ba..27a2587f 100644 --- a/crates/latkit/shaders/lib/color.wesl +++ b/crates/latkit/shaders/lib/color.wesl @@ -14,7 +14,7 @@ fn scale(x: f32, domain: vec2f, range: vec2f) -> f32 { return mix(range.x, range.y, clamp(t, 0.0, 1.0)); } -// `x` through colormap `layer` over `domain`, packed RGBA8; the theme's missing color if `x` is +// `x` through colormap `layer` over `domain`, packed RGBA8. The theme's missing color if `x` is // missing. fn colormap(layer: u32, kind: u32, x: f32, domain: vec2f) -> u32 { if missing(x) { diff --git a/crates/latkit/shaders/lib/drawn.wesl b/crates/latkit/shaders/lib/drawn.wesl index 8b873218..ff569ee3 100644 --- a/crates/latkit/shaders/lib/drawn.wesl +++ b/crates/latkit/shaders/lib/drawn.wesl @@ -9,7 +9,7 @@ import package::lib::lists::lists; struct DrawnRow { row: u32, - // Opacity and scale factors while its tile fades in or out; 1 otherwise. + // Opacity and scale factors while its tile fades in or out. 1 otherwise. presence: f32, growth: f32, // Whether its tile is leaving. A leaving row ignores the pointer. diff --git a/crates/latkit/shaders/lib/ends.wesl b/crates/latkit/shaders/lib/ends.wesl index 0c60344d..45bcf9fa 100644 --- a/crates/latkit/shaders/lib/ends.wesl +++ b/crates/latkit/shaders/lib/ends.wesl @@ -1,7 +1,7 @@ // Resolves each end of an edge or face row to a held row of the vertex mark that draws the end's // table. A row's ends name places in that table's order, and places stay fixed as the camera moves. -// Their held rows change, because the vertex mark's drawing makes pages and kept rows resident in -// its slots as the camera moves. So no channel stores an end's held row. The expression machine's +// Their held rows change as the camera moves, because the vertex mark makes other pages and kept +// rows resident in its slots. So no channel stores an end's held row. The expression machine's // end step resolves it for the rows whose ends changed, and for every row once the vertices moved. // It writes end `k` into the row's kind word `k`, which draws, probes, and normals read. diff --git a/crates/latkit/shaders/lib/focus.wesl b/crates/latkit/shaders/lib/focus.wesl index 4c929988..d38c8941 100644 --- a/crates/latkit/shaders/lib/focus.wesl +++ b/crates/latkit/shaders/lib/focus.wesl @@ -5,7 +5,7 @@ import package::lib::types::{SELECTED, HOVERED, FADE_IN, FADE_OUT}; import package::lib::bind::{frame, mark, word}; import package::lib::state::record_at; -// The focus bits of held row `row`; none for a row that left. +// The focus bits of held row `row`. None for a row that left. fn focus_of(row: u32) -> u32 { if row >= mark.held - mark.exits { return 0u; diff --git a/crates/latkit/shaders/lib/label.wesl b/crates/latkit/shaders/lib/label.wesl index 30c7ded1..54703a2a 100644 --- a/crates/latkit/shaders/lib/label.wesl +++ b/crates/latkit/shaders/lib/label.wesl @@ -1,8 +1,8 @@ -// Labels: where a labeled row's text is anchored, its size, the room it claims, and whether it can +// Labels: where a labeled row's text is anchored, its size, the space it claims, and whether it can // show. Draws place every label from its anchor each frame, so labels track the camera and their -// rows exactly; the label passes measure the same boxes to claim room and find the texts in view. Text is a distance field set at -// `TEXT_BASE` and drawn at any size: in CSS pixels, or in a diagram in length units, where text -// scales with the sheet and fades out once too small to read. +// rows exactly. The label passes measure the same boxes to claim space and find the visible texts. +// Text is a distance field set at `TEXT_BASE` and drawn at any size: in CSS pixels, or in a diagram +// in length units. In a diagram, text scales with the sheet and fades out once too small to read. import package::lib::types::{ POINTS, MARKER_BLOCK, SIZED, GROUP, PATH_TRACE, TEXT_BASE, TEXT_SPREAD, HEADER_BAND, SPACE_DIAGRAM, @@ -75,7 +75,7 @@ fn legibility(px: f32) -> f32 { } // A run of text placed on screen: the text's box and its quad, which extends over the distance -// field's spread (both in CSS pixels); its atlas corners in texels; and CSS pixels per texel. +// field's spread (both in CSS pixels), its atlas corners in texels, and CSS pixels per texel. struct Text { lo: vec2f, hi: vec2f, @@ -117,23 +117,24 @@ fn in_view(lo: vec2f, hi: vec2f) -> bool { return all(hi > vec2f(0.0)) && all(lo < eye.size); } -// Labels beside their rows compete for room. Each claims the cells its box covers, each cell keeps -// the greatest claim, and a label shows only where it keeps every cell it covers: where labels -// would overlap, the label of the row drawn on top shows. The claims depend on this frame alone. -// A label inside its row (a block's, or a group's title) or in a diagram has its own room. +// Labels beside their rows compete for space. Each label writes its claim key into the cells its +// box covers, and each cell keeps the largest key. A label shows only if it keeps every cell it +// covers, so where labels would overlap, the label of the row drawn on top shows. The claims depend +// on this frame alone. A label inside its row (a block's label, or a group's title) or in a diagram +// does not compete. -// Whether the label anchored at `a` competes for room with other labels. +// Whether the label anchored at `a` competes for space with other labels. fn claims_room(a: Anchor) -> bool { return a.room.x <= 0.0 && frame.space != SPACE_DIAGRAM; } -// The claim of drawn row `i`: later marks, then later rows, draw on top, so they claim more. Never -// 0, which marks a cell no label claimed. +// The claim key of drawn row `i`. Later marks, then later rows, draw on top, so their keys are +// larger. Never 0, which marks a cell no label claimed. fn claim_key(i: u32) -> u32 { return (min(mark.index, 255u) << 24u) | min(i + 1u, 0xffffffu); } -// The box a label claims: its text's box once set, else room for a short text at its anchor. +// The box a label claims: its text's box once set, else space for a short text at its anchor. fn claimed(a: Anchor, uv0: u32, uv1: u32, size: f32) -> Text { if uv0 != 0u || uv1 != 0u { return text_at(uv0, uv1, size, a.at, a.align); diff --git a/crates/latkit/shaders/lib/marker.wesl b/crates/latkit/shaders/lib/marker.wesl index 516f5f8c..73624f41 100644 --- a/crates/latkit/shaders/lib/marker.wesl +++ b/crates/latkit/shaders/lib/marker.wesl @@ -41,7 +41,7 @@ fn theme_text() -> vec3f { return unpack4x8unorm(theme_word(THEME_TEXT)).rgb; } -// The border color of a diagram's blocks and groups; clear for none. +// The border color of a diagram's blocks and groups. Clear for none. fn theme_outline() -> vec4f { return unpack4x8unorm(theme_word(THEME_OUTLINE)); } diff --git a/crates/latkit/shaders/lib/path.wesl b/crates/latkit/shaders/lib/path.wesl index 5c0e5b5b..b40b635f 100644 --- a/crates/latkit/shaders/lib/path.wesl +++ b/crates/latkit/shaders/lib/path.wesl @@ -26,7 +26,7 @@ import package::lib::drawn::drawn_row; override PATH: u32 = PATH_ENDS; // One segment of a row's path. `along` is the distance along the path where it starts, in position -// units (degrees on the earth). The segment list stores it; without a list it is 0. `row` is the +// units (degrees on the earth). The segment list stores it. Without a list it is 0. `row` is the // held row, whose state and columns it reads. `item` is the drawn row, which the mark's list // numbers. While its tile fades in or out, the segment shows with opacity `presence` and scale // `growth`, and a leaving segment ignores the pointer. @@ -46,8 +46,9 @@ fn path_segment(item: u32, index: u32, along: f32) -> Segment { return Segment(d.row, item, index, along, d.presence, d.growth, d.leaving); } -// A point on a path: its position as high and low parts plus elevation, set by its source; the -// same point in world and position units, set by `placed_point`; its color; and whether it shows. +// A point on a path. Its source sets its position as high and low parts plus elevation, and +// `placed_point` sets the same point in world and position units. It also has a color and whether +// it shows. struct PathPoint { hi: vec2f, lo: vec2f, @@ -55,7 +56,7 @@ struct PathPoint { world: vec3f, position: vec3f, color: vec4f, - // For a vertex, its half size in CSS pixels and whether it is a box (has its own height); zero + // For a vertex, its half size in CSS pixels and whether it is a box (has its own height). Zero // elsewhere. half: vec2f, boxed: bool, @@ -468,8 +469,8 @@ fn ends_part(s: Segment) -> bool { return s.index + 1u == path_count(s.item); } -// A row's lane in its family, the edges joining the same two vertices: its offset from the middle, -// in lanes of the theme's bow; its index in the family, counted from one side; and the step between +// A row's lane in its family (the edges joining the same two vertices): its offset from the middle +// in lanes of the theme's bow, its index in the family counted from one side, and the step between // lanes. A family spreads at most `FAMILY_LANES` lanes wide, so a larger family packs its lanes // closer, `step` apart. struct Lane { @@ -517,7 +518,7 @@ fn segment_screen(s: Segment) -> OnScreen { let ca = clip_of(ends[0].world); let cb = clip_of(ends[1].world); let cut = in_front(ca, cb); - // A segment reaching past the globe's edge still draws; the globe's depth clips it there. + // A segment reaching past the globe's edge still draws. The globe's depth clips it there. let ahead = ends[0].ahead || ends[1].ahead; out.shown = ends[0].shown && ends[1].shown && ahead && cut.x < cut.y; out.ca = mix(ca, cb, cut.x); @@ -548,7 +549,7 @@ fn stroke_style(row: u32) -> Style { if mark.kind == MARK_EDGES { let e = edge_at(row_at(row)); s.width = clamp(px(e.width, EDGE_WIDTH), mark.size_min, mark.size_max) * growth(e.flags); - // An edge colored by its ends gets their colors in the draw; here it only fades. + // An edge colored by its ends gets their colors in the draw. Here it only fades. s.color = select(unpack4x8unorm(e.color), vec4f(1.0), ends_colored()); s.color.a *= faded(row, e.flags); s.flow = e.flow; diff --git a/crates/latkit/shaders/lib/read.wesl b/crates/latkit/shaders/lib/read.wesl index 956e363b..25b8f9cd 100644 --- a/crates/latkit/shaders/lib/read.wesl +++ b/crates/latkit/shaders/lib/read.wesl @@ -28,8 +28,8 @@ fn column_word(column: u32, word: u32) -> u32 { return program[mark.columns + column * COLUMN_WORDS + word]; } -// Binary search: where the last of `count` items of `stride` words at `at` in `program` whose word -// `key` is at most `value` starts; the first item when none is. +// Binary search over `count` items of `stride` words at `at` in `program`. Returns where the last +// item whose word `key` is at most `value` starts, or the first item when none is. fn last_at_most(at: u32, count: u32, stride: u32, key: u32, value: u32) -> u32 { var lo = 0u; var hi = count; @@ -93,7 +93,7 @@ fn sample_value(column: u32, row: u32) -> u32 { return bitcast(mix(bitcast(a), bitcast(b), t)); } -// A bits column's word, such as a reference's row index; NONE where unwired. +// A bits column's word, such as a reference's row index. NONE where unwired. fn bits_value(column: u32, row: u32) -> u32 { if row >= column_word(column, COLUMN_ROWS) { return NONE; diff --git a/crates/latkit/shaders/lib/state.wesl b/crates/latkit/shaders/lib/state.wesl index bff2c8a5..a128f2ba 100644 --- a/crates/latkit/shaders/lib/state.wesl +++ b/crates/latkit/shaders/lib/state.wesl @@ -150,7 +150,7 @@ fn line_at(at: u32) -> Line { ); } -// The current mark's word indices for a stroke row's comet travel and its flow; NONE for a mark +// The current mark's word indices for a stroke row's comet travel and its flow. NONE for a mark // that draws no strokes. fn travel_words() -> vec2u { switch mark.kind { diff --git a/crates/latkit/shaders/lib/triangles.wesl b/crates/latkit/shaders/lib/triangles.wesl index d4eb2d96..ef7a8e99 100644 --- a/crates/latkit/shaders/lib/triangles.wesl +++ b/crates/latkit/shaders/lib/triangles.wesl @@ -89,8 +89,8 @@ fn face_corner(row: u32, corner: u32) -> Corner { return c; } -// Point `offset` of triangle `t.index` of area row `t.row`. Offset 0 is the area's label anchor; -// 1 to 3 are the triangle's corners. +// Point `offset` of triangle `t.index` of area row `t.row`. Offset 0 is the area's label anchor. +// Offsets 1 to 3 are the triangle's corners. fn area_point(t: Segment, offset: u32) -> Corner { var c: Corner; c.row = t.row; diff --git a/crates/latkit/shaders/lib/types.wesl b/crates/latkit/shaders/lib/types.wesl index ef36fda2..96693f68 100644 --- a/crates/latkit/shaders/lib/types.wesl +++ b/crates/latkit/shaders/lib/types.wesl @@ -63,9 +63,9 @@ const COLUMN_FLAGS: u32 = 0u; const COLUMN_ROWS: u32 = 1u; // Where the column's page table starts in the program. A sampled column has cursors here instead. const COLUMN_TABLE: u32 = 2u; -// Where the page table of an f64 column's low words starts; NONE for an f32 column. +// Where the page table of an f64 column's low words starts. NONE for an f32 column. const COLUMN_LOW: u32 = 3u; -// The bits column mapping each row to a row of another table; NONE when it reads its own table. +// The bits column mapping each row to a row of another table. NONE when it reads its own table. const COLUMN_JOIN: u32 = 4u; // A sampled column's f32 interpolation fraction from the frame at the shown coordinate to the next. const COLUMN_T: u32 = 5u; @@ -131,7 +131,7 @@ const PATH_PAGE_WORDS: u32 = 4u; // its words per row. // SLOTS: the end table, which maps a place to a held row (see `held_of`). NONE unless the table is // paged. -// NORMALS: the page table of the face normals summed at its rows; NONE when no face sums them. +// NORMALS: the page table of the face normals summed at its rows. NONE when no face sums them. // COLUMN: the program column of the reference field naming the end's rows; NONE when the end is the // row's own vertex, as for a pole. // The end pass writes the held row of end `k` into the row's kind word `k`. @@ -242,7 +242,7 @@ const FIELD_WORDS: u32 = 6u; // Most text codes a mark asks the host to set per texts pass. const WANTED: u32 = 64u; // Labels claim the screen cells their text covers, `LABEL_CELL` CSS pixels square, over the view -// and `LABEL_MARGIN` of its size beyond each side. Until its text is set, a label claims room for +// and `LABEL_MARGIN` of its size beyond each side. Until its text is set, a label claims space for // text `LABEL_GUESS` times its size wide. const LABEL_CELL: f32 = 8.0; const LABEL_MARGIN: f32 = 0.25; @@ -277,7 +277,7 @@ const TRIANGLES: u32 = 2u; // Path kinds, the source of a stroke's points. ENDS: two vertices, joined straight, along a great // circle, or through bends. TRACE: samples across the axis. GEOMETRY: a geometry column's points. -// Faces use ENDS for their three vertices; areas use GEOMETRY, three points per triangle. +// Faces use ENDS for their three vertices. Areas use GEOMETRY, three points per triangle. const PATH_ENDS: u32 = 0u; const PATH_TRACE: u32 = 1u; const PATH_GEOMETRY: u32 = 2u; @@ -488,7 +488,7 @@ struct Frame { // Direction toward the sun in earth-fixed coordinates (x toward 0° longitude on the equator, z // toward the north pole), with night's brightness in w. All zero when daylight is off. sun: vec4f, - // Color of the atmosphere around the globe; clear when the earth has none. + // Color of the atmosphere around the globe. Clear when the earth has none. atmosphere: vec4f, // Shadow color of a diagram's blocks and groups. shadow: vec4f, @@ -533,7 +533,7 @@ struct Mark { stride: u32, extras: u32, // During a transition, the page tables of each row's words as shown when it began (`frozen`) - // and as its channels now evaluate them (`settled`); NONE otherwise. `motion` is its eased + // and as its channels now evaluate them (`settled`). NONE otherwise. `motion` is its eased // progress, from 0 to 1. frozen: u32, settled: u32, @@ -568,7 +568,7 @@ struct Mark { palette: u32, // The program column a line traces, or NONE. trace: u32, - // Segments per row when every row has the same count; NONE when the segment list numbers them. + // Segments per row when every row has the same count. NONE when the segment list numbers them. per_row: u32, // Where the mark's segment list header starts in the lists buffer, and where the running // query's results for the mark start in the probe buffer. @@ -590,10 +590,11 @@ struct Mark { // Where the program lists the mark's ports (`PORT_WORDS` each), and how many each row has. port_info: u32, ports: u32, - // `label`: the extra word holding the label's code, or NONE. In the labels buffer: `runs`, the - // run table (two corner words per code, both 0 until the text is set); `wanted`, a count - // followed by the codes to set, each once; `seen`, a bit per code the texts pass met: a set - // text shown in view, or one not set that it asked for. The two sizes are in length units. + // `label`: the extra word holding the label's code, or NONE. The next three are offsets in the + // labels buffer. `runs` is the run table: two corner words per code, both 0 until the text is + // set. `wanted` is a count followed by the codes to set, each once. `seen` holds a bit per code + // the texts pass met: a set text shown in view, or an unset text it asked for. The two sizes + // are in length units. label: u32, runs: u32, wanted: u32, @@ -604,7 +605,7 @@ struct Mark { // stroke's width. size_min: f32, size_max: f32, - // A vertex mark's page table of the face normals summed at its rows; NONE when no face reads + // A vertex mark's page table of the face normals summed at its rows. NONE when no face reads // them. normals: u32, } diff --git a/crates/latkit/src/data/bytes.rs b/crates/latkit/src/data/bytes.rs index 9010a71c..26783c4a 100644 --- a/crates/latkit/src/data/bytes.rs +++ b/crates/latkit/src/data/bytes.rs @@ -1,8 +1,8 @@ //! The binary encoding shared by patches and datasets. //! -//! A patch encodes as one frame: a header (magic and total length), the name of the dataset the -//! patch changes (empty when it names none), and the patch's operations. Everything is little -//! endian. Because the header states the frame's length, back-to-back frames split without +//! A patch encodes as one frame. The frame holds a header (magic and total length), the name of the +//! dataset the patch changes (empty if it names none), and the patch's operations. Everything is +//! little endian. Because the header states the frame's length, back-to-back frames split without //! decoding, and a reader knows as soon as the header arrives whether it has a whole frame. A //! dataset encodes as the patch that builds it from an empty dataset. //! @@ -59,10 +59,11 @@ const HEADER: usize = 12; /// afterwards, to apply the patch or keep the dataset, is not counted. #[derive(Clone, Copy, Debug)] pub struct DecodeLimits { - /// The largest encoded frame, in bytes. Checked as soon as the header arrives. Default 256 MiB. + /// The largest encoded frame, in bytes. Checked as soon as the header arrives. Defaults to + /// 256 MiB. pub frame_bytes: usize, /// The budget, in bytes, for decoded vectors, names, operations, pages, and derived sample - /// buckets. Default 512 MiB. + /// buckets. Defaults to 512 MiB. pub decoded_bytes: usize, } @@ -203,7 +204,7 @@ pub(crate) fn stream(mut read: R, limits: DecodeLimits) -> impl Iterato }) } -/// Reads the next frame from `read`. Returns `None` if the input ends between frames; an end +/// Reads the next frame from `read`. Returns `None` if the input ends between frames. An end /// inside a frame is a truncation error. fn next(read: &mut impl Read, limits: DecodeLimits) -> Result> { let failed = |error: std::io::Error| match error.kind() { @@ -519,7 +520,7 @@ impl Reader<'_> { } /// Reads ids as values. `u32` ids, sent by writers without 64-bit integers, are widened to - /// `u64`, the storage of every numeric id. + /// `u64`, which stores every numeric id. fn id_values(&mut self) -> Result { let values = self.values()?; Ok(match values.array { diff --git a/crates/latkit/src/data/column.rs b/crates/latkit/src/data/column.rs index 4c27c20a..819b581f 100644 --- a/crates/latkit/src/data/column.rs +++ b/crates/latkit/src/data/column.rs @@ -14,20 +14,21 @@ pub(crate) const PAGE_MASK: usize = PAGE_ROWS - 1; /// entirely rewritten. const REMEMBERED: u8 = 8; -/// One page of a column: up to [`PAGE_ROWS`] values and an id unique to these contents. A page -/// made by writing over another records which page and which rows (`written`). It also caches its -/// geometry's point count. +/// One page of a column. It holds up to [`PAGE_ROWS`] values and an id unique to these contents. A +/// page made by writing over another records that page and the rows written (`written`). It also +/// caches its geometry's point count. #[derive(Clone, Debug)] pub(crate) struct Page { pub(crate) id: PageId, pub(crate) values: Values, pub(crate) written: Option>, - /// Computed on first use and cached; pages are immutable. + /// Computed on first use and cached, since pages are immutable. points: OnceLock, } -/// How a page was written: the page it copied (`from`), the rows written or added as offsets into -/// the page, and how `from` was written in turn (`before`), up to [`REMEMBERED`] writes back. +/// How a page was written. It records the page it copied (`from`), the rows written or added as +/// offsets into the page, and how `from` was written in turn (`before`), up to [`REMEMBERED`] +/// writes back. #[derive(Debug)] pub(crate) struct Written { pub(crate) from: PageId, @@ -49,12 +50,12 @@ impl Page { Page { id: PageId::next(), values, written: Some(Arc::new(written)), points: OnceLock::new() } } - /// Its geometry's point count as shaders read it; see [`Values::points`]. + /// The page's geometry point count as shaders read it. See [`Values::points`]. pub(crate) fn points(&self) -> usize { *self.points.get_or_init(|| self.values.points()) } - /// The rows written since page `before`, as sorted offsets into the page; empty if this page is + /// The rows written since page `before`, as sorted offsets into the page. Empty if this page is /// `before`. `None` unless this page was written from `before` and still remembers it (see /// [`REMEMBERED`]). pub(crate) fn rows_since(&self, before: PageId) -> Option> { @@ -76,8 +77,8 @@ impl Page { } } -/// A field's values in pages of [`PAGE_ROWS`] rows. Writes replace only the pages they touch; -/// the others keep their ids. +/// A field's values in pages of [`PAGE_ROWS`] rows. Writes replace only the pages they touch. The +/// others keep their ids. #[derive(Clone, Debug)] pub(crate) struct Column { len: usize, @@ -129,7 +130,7 @@ impl Column { &self.pages } - /// Whether `other` has the same length and page ids, which means equal values without + /// Whether `other` has the same length and page ids. That means the values are equal, without /// comparing any. pub(crate) fn shares(&self, other: &Column) -> bool { self.len == other.len && self.pages.iter().zip(other.pages.iter()).all(|(a, b)| a.id == b.id) @@ -143,7 +144,7 @@ impl Column { self.pages.get(row >> PAGE_BITS)?.values.get(row & PAGE_MASK) } - /// The values of the page holding row `row` and the row's index in them; `None` past the + /// The values of the page holding row `row`, and the row's index in them. `None` past the /// column's end. pub(crate) fn at(&self, row: usize) -> Option<(&Values, usize)> { let values = &self.pages.get(row >> PAGE_BITS)?.values; @@ -152,8 +153,8 @@ impl Column { } /// The values of rows `rows`, in that order, for GPU upload. Rows past the column's end come - /// back missing: a placeholder derived table, shown until its job finishes, can be shorter than - /// the rows requested. + /// back missing, because a placeholder derived table, shown until its job finishes, can be + /// shorter than the rows requested. pub(crate) fn pick(&self, rows: &[u32]) -> Values { let from: Vec> = rows.iter().map(|&r| Some(r as usize).filter(|&r| r < self.len)).collect(); let sources: Vec<&Values> = std::iter::once(&self.like).chain(self.pages.iter().map(|p| &p.values)).collect(); @@ -163,8 +164,8 @@ impl Column { /// A copy with `len` rows (at least the current count) and `writes` applied. /// /// `writes` are `(row, source row)` pairs sorted by row, each row at most once. Pages that no - /// write touches and whose length is unchanged keep their ids. New rows not written are - /// missing. Errors if `source` holds another kind of value. + /// write touches and that keep their length keep their ids. New rows not written are missing. + /// Returns an error if `source` holds another kind of value. pub(crate) fn write(&self, len: usize, writes: &[(usize, usize)], source: &Column) -> Result { debug_assert!(len >= self.len); debug_assert!(writes.windows(2).all(|w| w[0].0 < w[1].0)); @@ -227,8 +228,8 @@ impl Column { } /// Whether the first `rows` rows are unchanged since `before`, which this column was written - /// from a few writes back: every write since is at row `rows` or later, as with appends. - /// Writes a page no longer remembers count as changes. + /// from a few writes back. That holds when every write since is at row `rows` or later, as with + /// appends. Writes that a page no longer remembers count as changes. pub(crate) fn kept_below(&self, before: &Column, rows: usize) -> bool { (0..rows.div_ceil(PAGE_ROWS)).all(|p| { let (Some(now), Some(then)) = (self.pages.get(p), before.pages.get(p)) else { return false }; @@ -240,8 +241,8 @@ impl Column { /// The rows written or added since `before`, in order. For each page that differs from /// `before`'s, these are the rows it remembers writing since, or all its rows if it does not - /// remember `before`'s page. A page `before` lacks counts whole. Rows of `before` past this - /// column's end are not included. + /// remember `before`'s page. A page that `before` lacks counts in full. Rows of `before` past + /// this column's end are not included. pub(crate) fn written_since(&self, before: &Column) -> Vec { let mut rows = Vec::new(); for (p, now) in self.pages.iter().enumerate() { @@ -259,7 +260,7 @@ impl Column { } /// All values in row order as one [`Values`]. A single-page column returns its page without - /// copying; otherwise the pages are concatenated. + /// copying. Otherwise the pages are concatenated. pub(crate) fn values(&self) -> Values { match &self.pages[..] { [] => self.like.clone(), @@ -269,8 +270,8 @@ impl Column { } /// The rows whose values differ from `before`'s, in order. When the pages remember the rows - /// written since, only those rows are compared; otherwise every row of each changed page is - /// (see [`differing`](Column::differing)). + /// written since, only those rows are compared. Otherwise every row of each changed page is + /// compared (see [`differing`](Column::differing)). pub(crate) fn changed_since(&self, before: &Column) -> Vec { let Some(written) = self.rows_since(before) else { return self.differing(before) }; written @@ -284,8 +285,8 @@ impl Column { self.pages[row / PAGE_ROWS].values.same_row(row % PAGE_ROWS, values, at) } - /// The rows whose values differ from `before`'s, in order. Pages with the same id are skipped; - /// the rest are compared row by row. Rows past the shorter column's end count as different. + /// The rows whose values differ from `before`'s, in order. Pages with the same id are skipped, + /// and the rest are compared row by row. Rows past the shorter column's end count as different. pub(crate) fn differing(&self, before: &Column) -> Vec { let mut rows = Vec::new(); for p in 0..self.pages.len().max(before.pages.len()) { @@ -351,7 +352,7 @@ mod tests { fn differing_rows_are_found_in_the_pages_that_changed() { let column = Column::new(&numbers(PAGE_ROWS * 2 + 10)); let source = Column::new(&Values::from(vec![-1.0_f64, (PAGE_ROWS + 4) as f64])); - // Row 3 of the second page changes; its row 4 is rewritten with the value it already had. + // Row 3 of the second page changes. Its row 4 is rewritten with the value it already had. let written = column.write(column.len(), &[(PAGE_ROWS + 3, 0), (PAGE_ROWS + 4, 1)], &source); let written = written.expect("numbers"); assert_eq!(written.differing(&column), [PAGE_ROWS + 3]); @@ -373,7 +374,7 @@ mod tests { assert_eq!(Column::new(&numbers(PAGE_ROWS + 10)).rows_since(&column), None, "nor from another column"); let grown = column.write(column.len() + 2, &[], &source).expect("numbers"); assert_eq!(grown.rows_since(&column), None, "a changed row count lists no rows"); - // A page remembers a few writes back; beyond that it counts as entirely rewritten. + // A page remembers a few writes back. Beyond that it counts as entirely rewritten. let mut near = once.clone(); for _ in 1..REMEMBERED { near = near.write(near.len(), &[(7, 0)], &source).expect("numbers"); diff --git a/crates/latkit/src/data/describe.rs b/crates/latkit/src/data/describe.rs index d81bb70f..73809696 100644 --- a/crates/latkit/src/data/describe.rs +++ b/crates/latkit/src/data/describe.rs @@ -1,5 +1,5 @@ -//! Summaries of a dataset: a [`Description`] of its tables and fields, and an [`Envelope`] of each -//! row's least and greatest value of a sampled field over spans of the axis. +//! Summaries of a dataset. A [`Description`] lists its tables and fields. An [`Envelope`] holds +//! each row's least and greatest value of a sampled field over spans of the axis. use super::column::{PAGE_BITS, PAGE_MASK}; use super::ids::Ids; @@ -21,7 +21,8 @@ pub struct Description { pub name: Name, /// The axis name and unit, if set. pub axis: Option<[Name; 2]>, - /// The first and last axis coordinates held by any sampled field; `None` when none holds any. + /// The first and last axis coordinates held by any sampled field, or `None` if no field holds + /// any. pub span: Option<[f64; 2]>, /// Its tables, sorted by name. pub tables: Vec, @@ -54,9 +55,9 @@ pub struct FieldDescription { } /// A request for each row's least and greatest value of a sampled field over equal spans of the -/// axis: field `field` of table `table`, over `spans` equal spans from `range[0]` to `range[1]`, -/// for every row or for the rows `ids` names, in that order. Plain data that deserializes from -/// JSON, like a query. +/// axis. It reads field `field` of table `table` over `spans` equal spans from `range[0]` to +/// `range[1]`, for every row or for the rows in `ids`, in that order. Like a query, it is plain +/// data that deserializes from JSON. #[derive(Clone, PartialEq, Debug, serde::Deserialize, schemars::JsonSchema)] #[serde(deny_unknown_fields)] pub struct EnvelopeRequest { @@ -64,11 +65,11 @@ pub struct EnvelopeRequest { pub table: Name, /// The sampled field to read. pub field: Name, - /// The axis range the spans cover, `[first, last]`: finite, with `first <= last`. + /// The axis range the spans cover, `[first, last]`. Both are finite, with `first <= last`. pub range: [f64; 2], - /// The number of equal spans to divide the range into; at least 1. + /// The number of equal spans to divide the range into. At least 1. pub spans: usize, - /// The rows to read, in this order; every row when unset. + /// The rows to read, in this order. Defaults to every row. #[serde(default)] pub ids: Option, } @@ -89,12 +90,11 @@ impl Snapshot { /// `request` describes. Values are laid out row by row, with NaN where a span holds no frame. /// /// Each span reads the coarsest stored levels of detail that fit inside it, so it costs a few - /// reads regardless of how many frames it covers. Where only coarser levels are present, as - /// while time tiles are loading, those are used instead and may include frames just outside - /// the span. + /// reads however many frames it covers. Where only coarser levels are present, such as while + /// time tiles load, those are used and may include frames just outside the span. /// - /// Errors if the table, the field, or a requested row does not exist, the field is not - /// sampled, the range is not finite and ordered, `spans` is 0, or rows times spans exceeds + /// Returns an error if the table, the field, or a requested row does not exist, the field is + /// not sampled, the range is not finite and ordered, `spans` is 0, or rows times spans exceeds /// 2^26. /// /// ``` @@ -155,7 +155,7 @@ impl Table { let width = (range[1] - range[0]) / spans as f64; let at: Vec = (0..=spans).map(|s| if s == spans { range[1] } else { range[0] + width * s as f64 }).collect(); - // Each span covers frames from its start up to the next span's start; the last span also + // Each span covers frames from its start up to the next span's start. The last span also // includes its end. let bounds: Vec = at.iter().enumerate().map(|(s, &t)| samples.bound(t, s == spans)).collect(); let pieces: Vec> = bounds.windows(2).map(|b| samples.pieces(b[0]..b[1])).collect(); @@ -188,12 +188,13 @@ impl Table { /// [`Snapshot::envelope`] returns them. #[derive(Clone, PartialEq, Debug, serde::Serialize)] pub struct Envelope { - /// Each span's start coordinate, then the last span's end: one more value than spans. + /// Each span's start coordinate, then the last span's end. It has one more value than there + /// are spans. pub at: Vec, - /// The least value of each requested row in each span, row-major (`row * spans + span`); NaN + /// The least value of each requested row in each span, row-major (`row * spans + span`). NaN /// where the span has no value. pub least: Vec, - /// The greatest value of each requested row in each span, row-major (`row * spans + span`); + /// The greatest value of each requested row in each span, row-major (`row * spans + span`). /// NaN where the span has no value. pub greatest: Vec, } diff --git a/crates/latkit/src/data/forms.rs b/crates/latkit/src/data/forms.rs index 090b7c9c..f39f1c28 100644 --- a/crates/latkit/src/data/forms.rs +++ b/crates/latkit/src/data/forms.rs @@ -1,6 +1,6 @@ //! Serde forms for data written as JSON: a patch's changes, a table's fields, values, and ids. -//! Everything decodes in one pass except a slice of a binary file, which [`Patch::read`] fetches -//! through a host afterwards. JavaScript sends the binary frames instead of JSON. +//! Everything decodes in one pass except slices of binary files, which [`Patch::read`] fetches +//! through a host afterwards. JavaScript sends binary frames, not JSON. use std::fmt; use std::sync::Arc; @@ -42,11 +42,11 @@ pub(crate) enum OpForm { /// Writes fields for these rows. Rows with existing ids are updated, new ids add rows, and /// fields left out keep their values. A missing table is added. Set(TableForm), - /// Removes the rows with these ids; ids that match no row are ignored. + /// Removes the rows with these ids. Ids that match no row are ignored. Remove { table: String, ids: IdsForm }, /// Records a frame at each axis coordinate in `at`. Each field gives one value per row per - /// coordinate, frame-major. Covers every row, or only the rows `ids` names; other rows keep - /// their last values. + /// coordinate, frame-major. Covers every row, or only the rows in `ids`. Other rows keep their + /// last values. Append { at: At, table: String, @@ -57,10 +57,10 @@ pub(crate) enum OpForm { /// Forgets every sampled field's frames before axis coordinate `before`. Forget { before: f64 }, /// Inserts a run of entries of sampled field `field` at level `level`, numbered from `first`, - /// as time tiles store them. At level 0 each entry is a frame: one coordinate in `at` and one - /// value per row in `values`. At level `k` each entry is a bucket of `8^k` frames: its first - /// and last frame's coordinates in `at`, and in `values` every bucket's least value per row, - /// followed by every bucket's greatest. Null is missing. + /// in the form time tiles store them. At level 0 each entry is a frame, with one coordinate in + /// `at` and one value per row in `values`. At level `k` each entry is a bucket of `8^k` frames. + /// `at` holds the coordinates of each bucket's first and last frame. `values` holds every + /// bucket's least value per row, followed by every bucket's greatest. Null is missing. Frames { table: String, field: String, @@ -217,7 +217,7 @@ impl JsonSchema for Geometry { } /// Deserializes a patch from its JSON form. A field given as a slice of a binary file is an error -/// here; [`Patch::read`] fetches those. +/// here, since only [`Patch::read`] fetches slices. impl<'de> Deserialize<'de> for Patch { fn deserialize>(deserializer: D) -> std::result::Result { patch(Vec::::deserialize(deserializer)?).map_err(de::Error::custom) @@ -254,7 +254,7 @@ impl<'de> Deserialize<'de> for Id { } impl OpForm { - /// The field columns of a replace, set, or append; empty for other changes. + /// The field columns of a replace, set, or append, or empty for other changes. pub(crate) fn columns(&mut self) -> &mut [(Name, Column)] { match self { OpForm::Replace(TableForm { fields, .. }) @@ -266,7 +266,7 @@ impl OpForm { } /// Builds the patch for `ops`. Every slice of a file must already be replaced by its values. Errors -/// name the part as JSON reading does, such as `[2].set.fields.x`. +/// name the part like JSON reading, such as `[2].set.fields.x`. pub(crate) fn patch(ops: Vec) -> Result { let mut patch = Patch::new(); for (i, op) in ops.into_iter().enumerate() { @@ -345,7 +345,7 @@ impl Slice { self.offset.checked_add(length).ok_or_else(|| Error::invalid("the slice's end offset overflows")) } - /// Decodes the slice's fetched `bytes`. Errors unless exactly the slice's length arrived. + /// Decodes the slice's fetched `bytes`. Errors if their length is not the slice's length. pub(crate) fn read(&self, bytes: &[u8]) -> Result { let length = self.end()? - self.offset; if bytes.len() as u64 != length { @@ -356,7 +356,7 @@ impl Slice { } } -/// The value type of a slice's bytes. Numbers are little endian; `bool` is one byte each, nonzero +/// The value type of a slice's bytes. Numbers are little endian. `bool` is one byte each, nonzero /// meaning true. #[derive(Clone, Copy, Deserialize, JsonSchema)] enum Dtype { @@ -417,7 +417,7 @@ impl Column { } } -/// A table's fields in the order written, each name once. +/// A table's fields in the order written. Each name appears once. #[derive(Default, Deserialize)] #[serde(transparent)] pub(crate) struct Fields(Ordered); @@ -432,7 +432,7 @@ impl Fields { } } -/// Reads a column. Errors name only what is wrong; reading names the field in the error's path. +/// Reads a column. Errors say only what is wrong. The caller adds the field to the error's path. impl<'de> Deserialize<'de> for Column { fn deserialize>(deserializer: D) -> std::result::Result { deserializer.deserialize_any(ColumnVisitor) @@ -561,7 +561,7 @@ impl Geometry { } } - /// The geometry as a [`Value`]; `None` when it is empty. Errors when nested deeper than + /// The geometry as a [`Value`], or `None` when it is empty. Errors when nested deeper than /// polygons. fn value(self) -> std::result::Result, &'static str> { if matches!(&self, Geometry::List(items) if items.is_empty()) { @@ -678,7 +678,7 @@ impl<'de> Deserialize<'de> for IdsForm { } } -/// One id as written in JSON; `null` is `None`. +/// One id as written in JSON, with `null` as `None`. struct Key(Option); impl<'de> Deserialize<'de> for Key { diff --git a/crates/latkit/src/data/frames.rs b/crates/latkit/src/data/frames.rs index bbae7a6c..4b3e7da1 100644 --- a/crates/latkit/src/data/frames.rs +++ b/crates/latkit/src/data/frames.rs @@ -1,8 +1,8 @@ -//! Time tiles: a dataset written as files from which a view fetches its sampled fields' histories -//! in spans, on demand. A span of level `k` holds `chunk` entries of that level: frames at level 0, -//! buckets of `8^k` frames above. One span covers the same frames as eight spans of the level -//! below, as one map tile covers four tiles one zoom level in. While a span loads, the view draws -//! the nearest coarser span that has arrived. +//! Time tiles: a dataset written as files, from which a view fetches the history of sampled fields +//! in spans, on demand. A span at level `k` holds `chunk` entries of that level. The entries are +//! frames at level 0 and buckets of `8^k` frames above it. One span covers the same frames as eight +//! spans of the level below, as one map tile covers four tiles one zoom level in. While a span +//! loads, the view draws the nearest coarser span that has arrived. //! //! | File | Contents | //! | ------------- | ---------------------------------------------------------------------------- | @@ -71,7 +71,8 @@ mod range { impl Manifest { /// Parses a manifest. Returns `None` when `bytes` are not a JSON object with a `frames` list, - /// so other tile formats can be tried, and an error when they are one but invalid. + /// so other tile formats can be tried. Returns an error when they are such an object but it is + /// invalid. pub(crate) fn from_bytes(bytes: &[u8]) -> Option> { /// The key every manifest has, used to recognize one. #[derive(serde::Deserialize)] @@ -103,8 +104,8 @@ impl Manifest { } } -/// Whether `chunk` is a valid number of entries per span: from 8 to 2^20 and a multiple of 8, so a -/// bucket one level up never straddles two spans. +/// Whether `chunk` is a valid number of entries per span. It must be from 8 to 2^20 and a multiple +/// of 8, so a bucket one level up never straddles two spans. fn chunks(chunk: u32) -> bool { (8..=1 << 20).contains(&chunk) && chunk.is_multiple_of(8) } @@ -112,16 +113,17 @@ fn chunks(chunk: u32) -> bool { impl Snapshot { /// Writes the dataset as time tiles, passing each file's path and bytes to `write`: /// - /// - `frames.json`, the manifest; - /// - `base.latk`, the dataset with each sampled field's frame range but none of its entries; + /// - `frames.json`, the manifest. + /// - `base.latk`, the dataset with each sampled field's frame range but none of its entries. /// - `{field}/{level}/{index}.latk`, a patch for each span of `chunk` entries, for every level /// of every sampled field that has entries there. /// /// A view given the manifest as [`Tiles`](crate::Tiles) fetches the spans its lines draw, at - /// the level of detail its plot window needs, and the frames around the coordinate it shows. - /// Until a span arrives, the coarser span above it is drawn. + /// the level of detail the plot window needs. It also fetches the frames around the coordinate + /// it shows. Until a span arrives, the view draws the coarser span above it. /// - /// The manifest is written last, so a reader that finds it finds every file it names. Errors if + /// The manifest is written last, so a reader that finds it finds every file it names. Returns + /// an error if /// `chunk` is not a multiple of 8 from 8 to 2^20, or if `write` fails. pub fn to_tiles(&self, chunk: u32, mut write: impl FnMut(&str, &[u8]) -> Result<()>) -> Result<()> { if !chunks(chunk) { @@ -205,8 +207,8 @@ mod tests { .expect("tiles"); let manifest = Manifest::from_bytes(&files[MANIFEST]).expect("a manifest").expect("valid"); assert_eq!(manifest.fields, [Listed { table: "Bus".into(), field: "vm".into(), levels: 2, frames: 0..100 }]); - // 100 frames: 7 spans of 16 at level 0; 12 buckets of 8 in 1 span at level 1; 1 bucket of - // 64 at level 2. + // 100 frames: 7 spans of 16 at level 0, 12 buckets of 8 in 1 span at level 1, and 1 bucket + // of 64 at level 2. let spans: Vec<&str> = files .keys() .map(String::as_str) diff --git a/crates/latkit/src/data/geometry.rs b/crates/latkit/src/data/geometry.rs index 59f811ad..b0dc2c68 100644 --- a/crates/latkit/src/data/geometry.rs +++ b/crates/latkit/src/data/geometry.rs @@ -6,7 +6,7 @@ use super::values::Value; /// Shared, immutable geometries of one depth: nested offsets over `[x, y]` coordinates. /// /// Depth 0 is a point per row, 1 a path, 2 parts (rings of a polygon, or the paths of a -/// multi-path), and 3 polygons. `levels[0]` is indexed by row; each level's offsets index the next +/// multi-path), and 3 polygons. `levels[0]` is indexed by row. Each level's offsets index the next /// level, and the last level indexes `coords`. Offsets are absolute, so slicing rows slices only /// `levels[0]`. #[derive(Clone)] @@ -54,7 +54,7 @@ impl Geometry { &self.coords } - /// The number of coordinates its rows cover; for a slice, only its own rows'. + /// The number of coordinates its rows cover. For a slice, only its own rows count. pub(crate) fn points(&self) -> usize { let Some(rows) = self.levels.first() else { return self.coords.len() }; let mut span = rows[0] as usize..rows[rows.len() - 1] as usize; @@ -212,7 +212,7 @@ impl GeometryBuilder { for level in 0..self.levels.len() { let offsets = &source.levels[level]; let (first, last) = (offsets[span.start], offsets[span.end]); - // Items already pushed at the next level; this run's offsets continue from there. + // Items already pushed at the next level. This run's offsets continue from there. let next = match self.levels.get(level + 1) { Some(below) => below.len() - 1, None => self.coords.len(), diff --git a/crates/latkit/src/data/ids.rs b/crates/latkit/src/data/ids.rs index 4d303a25..d23bde52 100644 --- a/crates/latkit/src/data/ids.rs +++ b/crates/latkit/src/data/ids.rs @@ -14,7 +14,7 @@ use crate::{Error, Result}; /// [`Patch::remove`](crate::Patch::remove), or the target ids of a reference. /// /// [`Ids::count`] names rows `0..n`. Lists convert from vectors, arrays, and slices of numbers or -/// texts; for a reference, `None` leaves a row unwired. A bare number is never a count: use +/// texts. For a reference, `None` leaves a row unwired. A bare number is never a count. Use /// [`Ids::count`], or `{"count": n}` in JSON. /// /// ``` @@ -172,7 +172,7 @@ impl RowIds { } } - /// The id of row `row`, `None` past the rows. + /// The id of row `row`, or `None` past the last row. pub(crate) fn key(&self, row: usize) -> Option> { match self { RowIds::Count(n) => (row < *n as usize).then_some(IdRef::Number(row as u64)), @@ -292,8 +292,8 @@ impl RowIds { } } -/// Finds a row by id without allocating: the identity for counted ids, else a hash table of row -/// numbers. +/// Finds a row by id without allocating. Counted ids are their own row numbers. Other ids use a +/// hash table of row numbers. #[derive(Debug)] pub(crate) enum Index { Identity(u32), @@ -340,8 +340,8 @@ impl Index { } /// The row number each id in `ids` names, or `unwired` where the id is missing or names no row, - /// plus whether any present id named no row. Chooses one loop for the ids' and rows' storage - /// up front instead of matching per id. + /// plus whether any present id named no row. Picks one loop for the storage of the ids and rows + /// up front, so it does not match storage per id. pub(crate) fn resolve(&self, rows: &RowIds, ids: &Values, unwired: u32) -> (Vec, bool) { let valid = |i: usize| ids.valid.as_ref().is_none_or(|v| v.get(i)); let mut dangling = false; @@ -415,7 +415,7 @@ mod tests { assert_eq!(extended.get(&more, IdRef::Number(50)), Some(4)); assert_eq!(extended.get(&more, IdRef::Number(20)), Some(1)); assert!(index.extended(&more.extend(&[Id::Number(10)]).expect("numbers"), 5).is_err(), "repeated id"); - // Remove rows 1 and 3; the others move up to close the gaps. + // Remove rows 1 and 3. The others move up to close the gaps. let doomed = [false, true, false, true, false]; let kept = more.gather(&[0, 2, 4]); let fewer = extended.without(&doomed, &kept).expect("unique"); diff --git a/crates/latkit/src/data/json.rs b/crates/latkit/src/data/json.rs index f567606a..846a3973 100644 --- a/crates/latkit/src/data/json.rs +++ b/crates/latkit/src/data/json.rs @@ -1,6 +1,6 @@ -//! Reading datasets and patches from latkit's binary format, GeoJSON, or JSON, detected from the -//! first bytes. The JSON format is documented on [`Patch::read`]; its serde forms are in -//! `forms.rs`. Slices of binary files are fetched through a host. +//! Reads datasets and patches from latkit's binary format, GeoJSON, or JSON. The format is detected +//! from the first bytes. [`Patch::read`] documents the JSON format, and `forms.rs` holds its serde +//! forms. A host fetches slices of binary files. use serde::Deserialize; @@ -13,8 +13,8 @@ use super::source::Data; use crate::resource::{Fetch, Request}; use crate::{Error, Result}; -/// Options for [`Data::read`] and [`Patch::read`]: the dataset's name, and the document's location -/// and the host that fetches the file slices it references. +/// Options for [`Data::read`] and [`Patch::read`]. They give the dataset's name, the document's +/// location, and the host that fetches the file slices it references. #[derive(Clone, Copy, Default)] pub struct Reading<'a> { /// The dataset's name. Required for GeoJSON. latkit's bytes and JSON documents carry their own @@ -28,10 +28,10 @@ pub struct Reading<'a> { } impl Patch { - /// Reads a patch from `bytes`: either latkit's binary format, one frame as + /// Reads a patch from `bytes`. The bytes are either one frame of latkit's binary format, as /// [`Patch::to_bytes`] writes it, or JSON, which any language can write without latkit. The - /// JSON is a list of changes, each an object keyed by the [`Patch`] method that makes it, with - /// that method's parameters by name. + /// JSON is a list of changes. Each change is an object keyed by the [`Patch`] method that makes + /// it, with that method's parameters by name. /// /// ```json /// [ @@ -42,22 +42,22 @@ impl Patch { /// ] /// ``` /// - /// - `replace` and `set` take `{table, ids, fields}`; `remove` takes `{table, ids}`; `append` - /// takes `{table, at, fields, ids?}`; `forget` takes `{before}`; `axis` takes - /// `{name, unit}`; and `frames` takes `{table, field, level?, first, at, values}`, as - /// [`Patch::frames`] does. - /// - `ids` is `{"count": n}` for rows `0..n`, or a list of numbers or texts; `null` leaves a + /// - `replace` and `set` take `{table, ids, fields}`. `remove` takes `{table, ids}`. `append` + /// takes `{table, at, fields, ids?}`. `forget` takes `{before}`. `axis` takes + /// `{name, unit}`. `frames` takes `{table, field, level?, first, at, values}`, like + /// [`Patch::frames`]. + /// - `ids` is `{"count": n}` for rows `0..n`, or a list of numbers or texts. `null` leaves a /// reference unwired. - /// - A field is an array of one kind of value, as [`Values`](crate::Values) accepts; - /// `{"reference": table, "ids": ids}`; or a slice of a binary file, - /// `{"file", "dtype", "offset", "count"}`: `count` values starting at byte `offset` - /// (default 0), with dtype `f32le`, `f64le`, `i32le`, `u32le`, `u64le`, `u8` (all read as - /// numbers), or `bool` (one byte each, nonzero meaning true). - /// - `append`'s `at` is one axis coordinate or a list, as [`Patch::append`] takes. + /// - A field is an array of one kind of value, as [`Values`](crate::Values) accepts, a + /// reference `{"reference": table, "ids": ids}`, or a slice of a binary file + /// `{"file", "dtype", "offset", "count"}`. A slice holds `count` values starting at byte + /// `offset` (default 0). Its dtype is `f32le`, `f64le`, `i32le`, `u32le`, `u64le`, or `u8`, + /// all read as numbers, or `bool`, one byte each with nonzero meaning true. + /// - `append`'s `at` is one axis coordinate or a list, like [`Patch::append`]. /// - Errors name the path to the part that could not be read, such as `[2].set.fields.x`. /// - /// Slices are fetched through `reading.host`, all requests issued together, with paths - /// relative to `reading.base`. Slices with no host are an + /// `reading.host` fetches the slices, with all requests issued together and paths relative to + /// `reading.base`. Slices without a host are an /// [`Error::Unsupported`](crate::Error::Unsupported). /// /// ```no_run @@ -79,12 +79,12 @@ impl Patch { impl Data { /// Reads a dataset from `bytes`, detecting the format from the first bytes: /// - /// - latkit's binary format, as [`Snapshot::to_bytes`] writes it; - /// - GeoJSON, gzip-compressed or not: features become rows of tables `points`, `lines`, and + /// - latkit's binary format, as [`Snapshot::to_bytes`] writes it. + /// - GeoJSON, gzip-compressed or not. Features become rows of tables `points`, `lines`, and /// `polygons` by geometry type, with the geometry in field `geometry` and each property as a - /// field; - /// - a JSON document `{"name": ..., "patch": [...]}`: the dataset's name and the patch that - /// builds it, read as [`Patch::read`] reads one. + /// field. + /// - A JSON document `{"name": ..., "patch": [...]}` with the dataset's name and the patch that + /// builds it, read like [`Patch::read`]. /// /// `reading.name` names the dataset and is required for GeoJSON. The other formats carry their /// own name, which `reading.name` overrides when set. @@ -121,8 +121,8 @@ impl Data { } } -/// Whether `bytes` look like GeoJSON rather than latkit's binary format or a dataset document: -/// gzip-compressed, or a JSON object whose first key is neither `name` nor `patch`. +/// Whether `bytes` look like GeoJSON, not latkit's binary format or a dataset document. That is, +/// they are gzip-compressed, or a JSON object whose first key is neither `name` nor `patch`. pub(crate) fn geojson(bytes: &[u8]) -> bool { if bytes.starts_with(&[0x1f, 0x8b]) { return true; @@ -133,8 +133,8 @@ pub(crate) fn geojson(bytes: &[u8]) -> bool { key != b"name" && key != b"patch" } -/// `bytes` as JSON text: UTF-8 that opens an object or an array. Anything else, such as an HTML -/// page served in place of a missing file, is an error stating the `expected` format. +/// `bytes` as JSON text, which must be UTF-8 that opens an object or an array. Anything else, such +/// as an HTML page served for a missing file, is an error that names the `expected` format. fn text<'a>(bytes: &'a [u8], expected: &'static str) -> Result<&'a str> { let text = std::str::from_utf8(bytes).map_err(|_| Error::invalid(expected))?; match text.trim_start().as_bytes().first() { @@ -143,8 +143,9 @@ fn text<'a>(bytes: &'a [u8], expected: &'static str) -> Result<&'a str> { } } -/// Patches keyed by dataset name, in the order written, such as one message of a stream of -/// changes: JSON `{"grid": [...], "weather": [...]}`, each patch in [`Patch::read`]'s JSON form. +/// Patches keyed by dataset name, in the order written, such as one message in a stream of +/// changes. The JSON is `{"grid": [...], "weather": [...]}`, with each patch in the JSON form that +/// [`Patch::read`] reads. #[derive(Default, Deserialize)] pub struct Patches(Ordered>); @@ -173,8 +174,8 @@ impl Patches { } } -/// Builds the patch for `ops`. Issues every file slice request through `reading.host` before -/// awaiting any, relative to `reading.base`, then builds each change in order. +/// Builds the patch for `ops`. Issues every file slice request through `reading.host`, relative to +/// `reading.base`, before awaiting any. Then builds each change in order. async fn build(mut ops: Vec, reading: Reading<'_>) -> Result { let mut slices: Vec<&mut Column> = ops .iter_mut() @@ -284,7 +285,7 @@ mod tests { let renamed = pollster::block_on(Data::read(&read.snapshot().to_bytes(), named(Some("town")))).unwrap(); assert_eq!(renamed.snapshot().table("points").map(crate::Table::len), Some(1), "renaming keeps the tables"); assert_eq!(renamed.name().as_str(), "town"); - // A page served in place of a missing file is none of them, and the error says what is read. + // An HTML page served for a missing file is none of these, and the error names the format. let page = b""; let error = pollster::block_on(Data::read(page, named(None))).unwrap_err().to_string(); assert_eq!(error, "input: a dataset must be latkit's bytes, GeoJSON, or a JSON document"); diff --git a/crates/latkit/src/data/name.rs b/crates/latkit/src/data/name.rs index 1b7bf3d1..fe84d196 100644 --- a/crates/latkit/src/data/name.rs +++ b/crates/latkit/src/data/name.rs @@ -7,7 +7,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use rustc_hash::FxHashMap; -/// The name of a dataset, table, or field: cheap to clone, compared and ordered by its text. +/// The name of a dataset, table, or field. Cheap to clone, and compared and ordered by its text. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)] #[serde(transparent)] pub struct Name(Arc); @@ -108,8 +108,9 @@ impl fmt::Debug for Name { } } -/// A row's id within its table: a number or a text. A number above 2^53 − 1 serializes as decimal -/// text, since JSON readers such as JavaScript's would round it; the text names the same row. +/// A row's id within its table, either a number or a text. A number above 2^53 − 1 serializes as +/// decimal text, since JSON readers such as JavaScript's would round it. The text names the same +/// row. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum Id { /// A numeric id. @@ -126,7 +127,7 @@ impl Id { } } - /// The id as a number every JSON reader represents exactly, as it serializes; `None` for a + /// The id as a number that every JSON reader represents exactly, as it serializes. `None` for a /// text id or a number above 2^53 − 1. pub fn exact_number(&self) -> Option { match self { @@ -135,8 +136,8 @@ impl Id { } } - /// The id a JSON or JavaScript number names: a whole number from 0 to 2^53 − 1. `None` for any - /// other number, which may already be rounded. + /// The id that a JSON or JavaScript number names, if it is a whole number from 0 to 2^53 − 1. + /// `None` for any other number, which may already be rounded. pub fn from_number(number: f64) -> Option { (number.fract() == 0.0 && (0.0..=SAFE as f64).contains(&number)).then_some(Id::Number(number as u64)) } @@ -323,7 +324,7 @@ macro_rules! counter { counter!( /// Identifies one state of something that changes: a dataset, a table's rows, or a field. Version, - /// Identifies the contents of one page; a written page gets a new id. + /// Identifies the contents of one page. A written page gets a new id. PageId ); diff --git a/crates/latkit/src/data/ordered.rs b/crates/latkit/src/data/ordered.rs index 0a0d8a09..89b527b9 100644 --- a/crates/latkit/src/data/ordered.rs +++ b/crates/latkit/src/data/ordered.rs @@ -1,5 +1,5 @@ -//! JSON objects read as named entries in the order written, for objects whose key order matters, -//! such as a query's selected fields or a message's patches by dataset. +//! JSON objects read as named entries in the order written. Used where key order matters, such as +//! a query's selected fields or a message's patches by dataset. use std::fmt; use std::marker::PhantomData; @@ -8,7 +8,7 @@ use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor}; use super::name::Name; -/// Entries in the order written, each name once. +/// Entries in the order written. Each name appears once. #[derive(Clone, PartialEq, Debug)] pub(crate) struct Ordered(pub(crate) Vec<(Name, V)>); diff --git a/crates/latkit/src/data/patch.rs b/crates/latkit/src/data/patch.rs index c2bbbe2c..e8e1073b 100644 --- a/crates/latkit/src/data/patch.rs +++ b/crates/latkit/src/data/patch.rs @@ -10,12 +10,12 @@ use super::table::{Field, Reference, Table}; use super::values::Values; use crate::{Error, Result}; -/// A list of changes to a dataset, applied in order and atomically by -/// [`Data::apply`](crate::Data::apply): every change applies, or none does. +/// A list of changes to a dataset. [`Data::apply`](crate::Data::apply) applies them in order, all +/// at once or not at all. /// -/// Patches are built in code or decoded from bytes and JSON; every change reaches a dataset this -/// way. A patch made by [`Patch::to`] names its dataset, as patches in a stream of several -/// datasets do, and applying it to another dataset is an error. +/// Patches are built in code or decoded from bytes or JSON. They are the only way to change a +/// dataset. A patch made by [`Patch::to`] names its dataset, as patches in a stream of several +/// datasets do. Applying it to another dataset is an error. #[derive(Clone, Default, Debug)] pub struct Patch { pub(crate) data: Option, @@ -28,12 +28,13 @@ pub(crate) enum Op { Axis { name: Name, unit: Name }, /// Replaces a whole table, sampled fields included. Replace(Table), - /// Writes the given fields of the given rows. Unknown ids add rows; an unknown table is added. + /// Writes the given fields of the given rows. Unknown ids add rows, and an unknown table is + /// added. Set(Table), - /// Removes rows by id; ids that name no row are ignored. + /// Removes rows by id. Ids that name no row are ignored. Remove { table: Name, ids: Ids }, - /// Records frames of sampled fields, values frame-major. Covers every row, or the rows `ids` - /// names; other rows keep their last values. + /// Records frames of sampled fields, with values frame-major. Covers every row, or only the + /// rows in `ids`. Other rows keep their last values. Append { table: Name, at: Vec, ids: Option, fields: Vec<(Name, Values)> }, /// Forgets every sampled field's frames before a coordinate. Forget { before: f64 }, @@ -47,7 +48,7 @@ impl Patch { Patch::default() } - /// An empty patch for dataset `data` only; applying it to another dataset is an error. + /// An empty patch for dataset `data` only. Applying it to another dataset is an error. pub fn to(data: impl Into) -> Patch { Patch { data: Some(data.into()), ops: Vec::new() } } @@ -71,16 +72,16 @@ impl Patch { self } - /// Writes `table`'s fields for its rows. Rows with existing ids are updated, new ids add rows, - /// and fields `table` leaves out keep their values (missing for added rows). Adds the table if - /// none has its name. + /// Writes the fields of `table` for its rows. Rows with existing ids are updated and new ids + /// add rows. Fields that `table` leaves out keep their values, or are missing for added rows. + /// Adds the table if no table has its name. #[must_use] pub fn set(mut self, table: Table) -> Patch { self.ops.push(Op::Set(table)); self } - /// Removes the rows of `table` with `ids`. Ids that match no row are ignored; a missing table + /// Removes the rows of `table` with `ids`. Ids that match no row are ignored. A missing table /// is an error. #[must_use] pub fn remove(mut self, table: impl Into, ids: impl Into) -> Patch { @@ -91,7 +92,7 @@ impl Patch { /// Records a frame of `table` at each axis coordinate in `at`. Each field gives a value per row /// per coordinate, frame-major: every row's value at the first coordinate, then at the next. /// - /// A field's first frame makes it sampled. Coordinates must be finite and must not decrease; at + /// A field's first frame makes it sampled. Coordinates must be finite and must not decrease. At /// equal coordinates, reads return the last frame. #[must_use] pub fn append( @@ -109,9 +110,9 @@ impl Patch { self } - /// Like [`append`](Patch::append), but records values only for the rows `ids` names: one per - /// named row per coordinate, frame-major. Other rows keep their last value. An id that names no - /// row is an error. + /// Like [`append`](Patch::append), but records values only for the rows in `ids`, one per row + /// per coordinate, frame-major. Other rows keep their last value. An id that names no row is an + /// error. #[must_use] pub fn append_rows( mut self, @@ -139,11 +140,11 @@ impl Patch { } /// Inserts a run of entries of sampled field `field` at level `level`, numbered from `first`, - /// as time tiles (such as `latkit tile` writes) store them. At level 0 the entries are frames: - /// one coordinate each in `at` and one value per row of `table` in `values`. At level `k` they - /// are buckets of `8^k` frames: each bucket's first and last frame's coordinates in `at`, and - /// in `values` every bucket's least value per row, followed by every bucket's greatest. NaN is - /// missing. + /// in the form time tiles store them (such as the tiles `latkit tile` writes). At level 0 the + /// entries are frames, each with one coordinate in `at` and one value per row of `table` in + /// `values`. At level `k` they are buckets of `8^k` frames. `at` holds the coordinates of each + /// bucket's first and last frame. `values` holds every bucket's least value per row, followed + /// by every bucket's greatest. NaN is missing. /// /// None of the entries may already be present. Entries outside the field's frame range extend /// it, and frames in any gap stay missing. @@ -190,17 +191,17 @@ impl Patch { } /// Decodes one whole frame written by [`Patch::to_bytes`] or - /// [`Snapshot::to_bytes`](crate::Snapshot::to_bytes), with default [`DecodeLimits`]. Errors if - /// `bytes` are malformed, truncated, or extend past the frame. + /// [`Snapshot::to_bytes`](crate::Snapshot::to_bytes), with default [`DecodeLimits`]. Returns an + /// error if `bytes` are malformed, truncated, or extend past the frame. pub fn from_bytes(bytes: &[u8]) -> Result { super::bytes::decode(bytes) } - /// Decodes the back-to-back frames `read` carries, as from a file, pipe, or socket, yielding - /// each patch once its frame has fully arrived. Each frame states its length up front, so one - /// longer than `limits` allows fails with [`Error::Limit`](crate::Error::Limit) before its body - /// is read, and an untrusted stream buffers only one frame at a time. The iterator ends when - /// the input ends between frames, or after the first error. + /// Decodes back-to-back frames from `read`, such as a file, pipe, or socket. Yields each patch + /// once its whole frame has arrived. Each frame states its length up front, so a frame longer + /// than `limits` allows fails with [`Error::Limit`](crate::Error::Limit) before its body is + /// read. An untrusted stream buffers at most one frame at a time. The iterator ends when the + /// input ends between frames, or after the first error. /// /// ``` /// # use latkit::{DecodeLimits, Ids, Patch, Table}; @@ -224,8 +225,8 @@ impl From for Patch { } } -/// Consecutive appends to the same fields of every row of one table, merged into one append so -/// each field is written once, however many changes carried its frames. +/// Consecutive appends to the same fields of every row of one table, merged into one append. Each +/// field is then written once, however many changes carried its frames. pub(crate) struct Appends { table: Name, at: Vec, @@ -280,8 +281,8 @@ impl Appends { } impl Op { - /// An [`Op::Frames`] of entries of `level` from `first`; the entry count follows from the - /// number of coordinates in `at`. + /// An [`Op::Frames`] of entries at `level`, numbered from `first`. The number of coordinates in + /// `at` sets the entry count. pub(crate) fn frames(table: Name, field: Name, level: u32, first: u64, at: Vec, values: Vec) -> Op { let entries = (at.len() / super::samples::coordinates(level)) as u64; let run = Run { level, entries: first..first.saturating_add(entries) }; diff --git a/crates/latkit/src/data/query.rs b/crates/latkit/src/data/query.rs index 189a1e44..00ad1e6f 100644 --- a/crates/latkit/src/data/query.rs +++ b/crates/latkit/src/data/query.rs @@ -1,6 +1,6 @@ -//! Queries: reading a page of a table's rows, filtered and sorted, with their fields or computed -//! values. Queries evaluate on the CPU with the same expressions marks draw with (a plain field is -//! read page by page) and return a table, so the result has the same shape as the input. +//! Queries read a page of a table's rows, filtered and sorted, with their fields or computed +//! values. They evaluate on the CPU with the same expressions marks draw with, reading a plain +//! field a page at a time. The result is a table, so it has the same shape as the input. use std::cmp::Ordering; @@ -14,11 +14,11 @@ use crate::expr::Expr; use crate::expr::cpu::Rows; use crate::{Error, Result}; -/// A request to read rows of a table. Starts from the rows `ids` names (every row by default), -/// keeps those `filter` accepts, sorts them by `sort`, and returns up to `limit` of them starting -/// at `offset`. Each row comes with the fields `select` names or computes (every field by default). -/// Sampled fields are read at axis coordinate `at`, or at their last frame by default. Plain data -/// that deserializes from JSON, like a scene. +/// A request to read rows of a table. It starts from the rows in `ids` (default: every row), keeps +/// the rows `filter` accepts, sorts them by `sort`, and returns up to `limit` of them starting at +/// `offset`. Each row comes with the fields `select` names or computes (default: every field). +/// Sampled fields are read at axis coordinate `at` (default: their last frame). Like a scene, it is +/// plain data that deserializes from JSON. /// /// ``` /// use latkit::{Data, Ids, Order, Query, Table, field}; @@ -35,15 +35,15 @@ use crate::{Error, Result}; pub struct Query { /// The table to read. of: Name, - /// The rows to read, in this order unless sorted; every row when unset. Ids that name no row + /// The rows to read, in this order unless sorted. Defaults to every row. Ids that name no row /// are skipped. #[serde(default)] ids: Option, /// Keeps only the rows for which this is true. #[serde(default)] filter: Option, - /// Keys to sort by, each breaking ties left by the keys before it. Rows where a key has no - /// value sort last; rows equal on every key keep their order. + /// Keys to sort by. Each key breaks ties left by the keys before it. Rows where a key has no + /// value sort last, and rows equal on every key keep their order. #[serde(default)] sort: Vec<(Expr, Order)>, /// How many of the kept rows to skip. @@ -52,11 +52,11 @@ pub struct Query { /// The most rows to return. When sorting with a limit, the first `offset + limit` rows are /// selected in linear time and only those are sorted. limit: Option, - /// Fields to return, by output name. A plain field returns that field as stored; any other - /// expression returns numbers. Every field when unset; an empty object returns the ids alone. + /// Fields to return, by output name. A plain field returns that field as stored, and any other + /// expression returns numbers. Defaults to every field. An empty object returns the ids alone. #[serde(default)] select: Option>, - /// The axis coordinate at which sampled fields are read; their last frame when unset. + /// The axis coordinate at which sampled fields are read. Defaults to their last frame. at: Option, } @@ -102,7 +102,7 @@ impl Query { } /// Sorts by `key`, breaking ties left by earlier keys. Rows equal on every key keep their - /// order; rows where `key` has no value sort last. + /// order, and rows where `key` has no value sort last. #[must_use] pub fn sort(mut self, key: impl Into, order: Order) -> Query { self.sort.push((key.into(), order)); @@ -123,8 +123,8 @@ impl Query { self } - /// Adds output field `name`, computed by `value`. A plain field returns that field as stored; - /// any other expression returns numbers. Without any `select`, every field is returned. + /// Adds output field `name`, computed by `value`. A plain field returns that field as stored, + /// and any other expression returns numbers. Without any `select`, every field is returned. #[must_use] pub fn select(mut self, name: impl Into, value: impl Into) -> Query { self.select.get_or_insert_with(Ordered::default).0.push((name.into(), value.into())); @@ -150,17 +150,17 @@ impl Query { /// passed the filter. #[derive(Clone, Debug)] pub struct QueryResult { - /// The page of rows, as a table with the source table's name: their ids, and their selected - /// fields or computed values. Sampled fields come back as plain values read at the query's - /// coordinate. + /// The page of rows, as a table with the source table's name. It holds their ids and their + /// selected fields or computed values. Sampled fields come back as plain values read at the + /// query's coordinate. pub table: Table, /// The number of rows that passed the filter, before `offset` and `limit`. pub total: usize, } impl Snapshot { - /// Runs `query`, returning the page of rows it describes as a table and the number of rows - /// that passed the filter. When sorting with a limit, the rows are selected in linear time and + /// Returns the page of rows `query` describes, as a table, and the number of rows that passed + /// the filter. When sorting with a limit, the rows are selected in linear time and /// only those are sorted. Errors if the table or a selected field does not exist. pub fn query(&self, query: &Query) -> Result { let table = self.table(&query.of).ok_or_else(|| Error::invalid("no such table").at(query.of.as_str()))?; diff --git a/crates/latkit/src/data/samples.rs b/crates/latkit/src/data/samples.rs index beedbbb8..4dbe1e15 100644 --- a/crates/latkit/src/data/samples.rs +++ b/crates/latkit/src/data/samples.rs @@ -1,17 +1,17 @@ //! Samples: a field's values over the dataset's axis, stored as levels of detail. //! -//! Level 0 holds frames: each row's value at one axis coordinate. Each level `k` above holds -//! buckets of `8^k` frames: each row's least and greatest value over the bucket. A long history can -//! then be drawn from about as many entries as there are pixels. Each level stores its entries in -//! pages together with their coordinates (a frame's own; a bucket's first and last frame's), so a -//! level can be drawn without the levels below it. Frames are numbered from the first ever -//! written, and a version reads frames `start..end`. +//! Level 0 holds frames, each row's value at one axis coordinate. Each level `k` above holds +//! buckets of `8^k` frames, with each row's least and greatest value over the bucket. A long +//! history can then be drawn from about as many entries as there are pixels. Each level stores its +//! entries in pages together with their coordinates (a frame's own, or a bucket's first and last +//! frame's), so a level can be drawn without the levels below it. Frames are numbered from the +//! first ever written, and a version reads frames `start..end`. //! //! Any entry may be missing. Appended frames grow the newest pages in place and write each bucket //! whose eight entries one level below are all present. Runs of entries decoded from bytes or -//! fetched as time tiles can fill any range, regardless of what surrounds it. An entry is present -//! when its level's coordinate pages contain it: every write stores an entry's coordinates and -//! every row block's values together. +//! fetched as time tiles can fill any range, whatever surrounds it. An entry is present when its +//! level's coordinate pages contain it, because every write stores an entry's coordinates and every +//! row block's values together. use std::ops::Range; use std::sync::{Arc, PoisonError, RwLock}; @@ -35,9 +35,9 @@ pub(crate) fn bucket_frames(level: u32) -> u64 { 1 << (3 * level) } -/// The level of detail for drawing `frames` frames across `columns` pixel columns: frames (level -/// 0) while there are at most two per column, else the finest level with at most two buckets per -/// column. +/// The level of detail for drawing `frames` frames across `columns` pixel columns. Uses frames +/// (level 0) while there are at most two per column, else the finest level with at most two +/// buckets per column. pub(crate) fn detail(frames: u64, columns: u32) -> u32 { let needed = frames.div_ceil(2 * u64::from(columns.max(1))); if needed <= 1 { 0 } else { (1..=MAX_LEVEL).find(|&k| bucket_frames(k) >= needed).unwrap_or(MAX_LEVEL) } @@ -79,7 +79,7 @@ impl Run { self.entries.start.saturating_mul(span)..self.entries.end.saturating_mul(span) } - /// Its coordinate count, and its value count for `rows` rows; `None` on overflow. + /// Its coordinate count and its value count for `rows` rows, or `None` on overflow. pub(crate) fn sizes(&self, rows: usize) -> Option<(usize, usize)> { let n = usize::try_from(self.entries.end.checked_sub(self.entries.start)?).ok()?; Some((n.checked_mul(coordinates(self.level))?, n.checked_mul(rows)?.checked_mul(sides(self.level))?)) @@ -155,7 +155,7 @@ impl Log { } /// Stops reading items before absolute index `start`. Shared storage is left to the other - /// versions; once the forgotten prefix exceeds a quarter of the kept items (plus 16), this + /// versions. Once the forgotten prefix exceeds a quarter of the kept items (plus 16), this /// version copies the items it keeps. pub(crate) fn forget(&mut self, start: usize) { self.start = start.clamp(self.start, self.end); @@ -187,9 +187,11 @@ pub(crate) struct Page { store: Arc>, } -/// A page of a row block's frames, or of a level's least or greatest values: `per` rows per entry. +/// A page of a row block's frames, or of a level's least or greatest values, with `per` rows per +/// entry. pub(crate) type FramePage = Page; -/// A page of a level's coordinates, `per` per entry: a frame's own, or a bucket's first and last. +/// A page of a level's coordinates, `per` per entry. A frame has its own coordinate, and a bucket +/// has its first and last frame's. pub(crate) type AxisPage = Page; impl Page { @@ -370,8 +372,8 @@ fn forget_before(log: &mut Log>, index: u64) { log.forget(start + dropped); } -/// Writes `entry`'s `items` after the last entry of `log`: into the last page if it can grow and -/// has room within `limit` items, else into a new growable page. +/// Writes `entry`'s `items` after the last entry of `log`. They go into the last page if it can +/// grow and has room within `limit` items, else into a new growable page. fn grow(log: &mut Log>, entry: u64, items: &[T], limit: usize) { if !log.last().is_some_and(|page| page.extend(entry, items, limit)) { log.push(Page::new(entry, items.len(), items.to_vec(), true)); @@ -394,8 +396,8 @@ struct Logs { blocks: Vec>>, } -/// A sampled field's values over the axis: frames and the levels of detail above them, any of -/// which may have gaps. Values are stored per row block of [`PAGE_ROWS`] rows. +/// A sampled field's values over the axis, as frames and the levels of detail above them. Any level +/// may have gaps. Values are stored per row block of [`PAGE_ROWS`] rows. #[derive(Clone, Default)] pub(crate) struct Samples { /// The frames this version reads, `start..end`, numbered from the first ever written. @@ -541,7 +543,7 @@ impl Samples { stored.push(run.clone()); continue; } - // Buckets derivable from the runs one level below, in order; this run omits them. + // Buckets derivable from the runs one level below, in order. This run omits them. let derived = runs .iter() .filter(|r| r.level + 1 == run.level) @@ -645,10 +647,10 @@ impl Samples { self.append(at, &Values::from(dense), total) } - /// Inserts `run`, none of whose entries may be present yet: its `coordinates` ([`coordinates`] - /// per entry) and its values for `rows` rows, entry-major. At level 0 that is one value per - /// row; above, every entry's least values come first, then every entry's greatest. Frames the - /// run covers outside this version's range extend the range. + /// Inserts `run`, none of whose entries may be present yet. `coordinates` holds + /// [`coordinates`] per entry, and `values` holds its values for `rows` rows, entry-major. At + /// level 0 that is one value per row. Above it, every entry's least values come first, then + /// every entry's greatest. Frames the run covers outside this version's range extend the range. /// /// Errors if the level is above [`MAX_LEVEL`], the sizes do not match, the coordinates are not /// finite and non-decreasing, the run starts before forgotten frames, or an entry is present. @@ -855,10 +857,10 @@ impl Samples { true } - /// Counts the entries of `level` before `at` (at or before it, with `past`), comparing each - /// entry's coordinate `which`: its last (1) counts entries wholly before, its first (0) entries - /// begun before. Returns `Ok(count)` when the present entries determine it, or `Err(start)` - /// with the first entry of the gap containing `at`. + /// Counts the entries of `level` before `at`, or at or before it with `past`. Compares each + /// entry's coordinate `which`. The last coordinate (1) counts entries wholly before `at`, and + /// the first (0) counts entries begun before it. Returns `Ok(count)` when the present entries + /// determine it, or `Err(start)` with the first entry of the gap containing `at`. fn locate(&self, level: u32, at: f64, past: bool, which: u32) -> std::result::Result { let before = |t: f64| if past { t <= at } else { t < at }; let visible = self.visible(level); @@ -889,8 +891,9 @@ impl Samples { } /// The number of frames before `at` (at or before it, with `past`), as an absolute frame index - /// clamped to `start..=end`. Counts frames where present, elsewhere whole buckets of the finest - /// level present. If no level determines it, returns where the gap containing `at` starts. + /// clamped to `start..=end`. Counts frames where present, and elsewhere whole buckets of the + /// finest level present. If no level determines it, returns where the gap containing `at` + /// starts. pub(crate) fn bound(&self, at: f64, past: bool) -> u64 { let mut hole = None; for level in 0..self.axes.len() as u32 { @@ -906,9 +909,9 @@ impl Samples { } /// The fractional number of frames at or before `at`, as precise as the finest level that - /// determines it. Exact where frames are present; elsewhere, the whole buckets at or before - /// `at`, plus the fraction of the next bucket given by where `at` lies between its first and - /// last frame's coordinates. + /// determines it. Exact where frames are present. Elsewhere it is the whole buckets at or + /// before `at`, plus a fraction of the next bucket set by where `at` lies between that bucket's + /// first and last frame coordinates. fn position(&self, at: f64) -> Option { (0..self.axes.len() as u32).find_map(|level| { let n = self.locate(level, at, true, 1).ok()?; @@ -920,9 +923,9 @@ impl Samples { }) } - /// The frames a trace over coordinates `across` draws: from the last frame at or before its - /// start through the first frame after its end, as precisely as the present levels allow. All - /// frames if either bound cannot be determined. + /// The frames a trace over coordinates `across` draws. They run from the last frame at or + /// before its start through the first frame after its end, as precisely as the present levels + /// allow. Returns all frames if either bound cannot be determined. pub(crate) fn across(&self, [x0, x1]: [f64; 2]) -> Range { let (Some(a), Some(b)) = (self.position(x0), self.position(x1)) else { return self.start..self.end }; let first = (a.floor() as u64).saturating_sub(1).max(self.start); @@ -930,7 +933,7 @@ impl Samples { first..last.max(first) } - /// The first frame from `frame` on that any level has present: where a gap ends. + /// The first frame from `frame` on that any level has present, which is where a gap ends. fn next_held(&self, frame: u64) -> Option { let mut next: Option = None; for (level, log) in self.axes.iter().enumerate() { @@ -973,9 +976,10 @@ impl Samples { pieces } - /// The runs that draw frames `frames` at `level`, left to right: that level's entries where - /// present; else, as a placeholder, the entry of the nearest coarser level that contains them; - /// else finer levels' entries. Frames no level has are not drawn. + /// The runs that draw frames `frames` at `level`, left to right. Uses that level's entries + /// where present. Elsewhere it uses, as a placeholder, the entry of the nearest coarser level + /// that contains them, and failing that, entries of finer levels. Frames no level has are not + /// drawn. pub(crate) fn cover(&self, frames: Range, level: u32) -> Vec { let mut runs = Vec::new(); let frames = frames.start.max(self.start)..frames.end.min(self.end); @@ -1044,7 +1048,7 @@ impl Samples { self.held(0, frame).then_some(frame) } - /// How far `at` lies from `frame` toward the next frame, in `0..=1`; 0 if the next frame is + /// How far `at` lies from `frame` toward the next frame, in `0..=1`. 0 if the next frame is /// missing. pub(crate) fn t(&self, frame: u64, at: f64) -> f32 { match (self.at(frame), self.at(frame + 1)) { @@ -1092,8 +1096,8 @@ impl Samples { self.entry(row, 0, 0, frame) } - /// The value of `row` at entry `index` of `level`: a frame's, or a bucket's least (`side` 0) or - /// greatest (1); `None` when missing. + /// The value of `row` at entry `index` of `level`. That is a frame's value, or a bucket's least + /// (`side` 0) or greatest (1). `None` when missing. pub(crate) fn entry(&self, row: usize, level: u32, side: usize, index: u64) -> Option { if !self.held(level, index) { return None; @@ -1105,7 +1109,7 @@ impl Samples { (!value.is_nan()).then_some(f64::from(value)) } - /// Writes `frame`'s values for the first `out.len()` rows into `out`; rows without a value are + /// Writes `frame`'s values for the first `out.len()` rows into `out`. Rows without a value are /// NaN. fn read_frame(&self, frame: u64, out: &mut [f32]) { out.fill(f32::NAN); @@ -1237,7 +1241,7 @@ mod tests { newest.append(&[0.0, 1.0], &Values::from(vec![1.0_f64, 2.0]), 1).expect("frames"); let mut older = newest.clone(); newest.append(&[2.0, 3.0], &Values::from(vec![3.0_f64, 4.0]), 1).expect("frames"); - // The older version has frames 0 and 1; inserting frames 4 and 5 extends it, leaving a gap + // The older version has frames 0 and 1. Inserting frames 4 and 5 extends it, leaving a gap // at 2 and 3. older.insert(&Run { level: 0, entries: 4..6 }, &[4.0, 5.0], &[5.0, 6.0], 1).expect("a run"); assert_eq!(older.value(0, 2), None, "the newer version's frame 2 is not the older one's"); diff --git a/crates/latkit/src/data/snapshot.rs b/crates/latkit/src/data/snapshot.rs index fe92838c..a7226aa4 100644 --- a/crates/latkit/src/data/snapshot.rs +++ b/crates/latkit/src/data/snapshot.rs @@ -10,12 +10,12 @@ use super::table::{Field, Table}; use super::values::Value; use crate::{Error, Result}; -/// One immutable version of a dataset. Taking one is O(1), and it shares unchanged pages with the -/// versions before and after it. [`Data::snapshot`](crate::Data::snapshot) takes one; a view given -/// one draws that version unchanged. +/// One immutable version of a dataset, taken with [`Data::snapshot`](crate::Data::snapshot). +/// Taking one is O(1), and it shares unchanged pages with the versions before and after it. A view +/// given a snapshot draws that version. /// -/// Later changes to the dataset never affect it: its description, values, queries, and envelopes -/// stay the same. +/// Later changes to the dataset never affect a snapshot. Its description, values, queries, and +/// envelopes stay the same. /// /// ``` /// # use latkit::{Data, Patch, Table, Value}; @@ -37,9 +37,9 @@ pub(crate) struct Inner { /// Sorted by name. pub(crate) tables: Vec
    , pub(crate) version: Version, - /// The version the dataset started from, shared by every version patches derive from it. A - /// view matches rows across versions with the same origin to animate them; for a same-named - /// dataset with another origin, every old row leaves and every new row enters. + /// The version the dataset started from. Every version that patches derive from it shares it. + /// A view matches rows across versions with the same origin to animate them. For a same-named + /// dataset with a different origin, every old row leaves and every new row enters. pub(crate) origin: Version, /// A hash of the table names and the fields' names, kinds, and targets. Marks recompile only /// when it changes. @@ -105,9 +105,9 @@ impl Snapshot { } } - /// The version `patch` makes from this one. Applies every change or, if one is invalid, none, - /// returning an error whose path names the change and part, such as `patch[1].Bus.kv`. A patch - /// for another dataset is an error. + /// Returns the version `patch` makes from this one. Applies all changes or none. If a change is + /// invalid, the error's path names the change and part, such as `patch[1].Bus.kv`. A patch for + /// another dataset is an error. pub(crate) fn applied(&self, patch: Patch) -> Result { if let Some(data) = &patch.data && *data != self.0.name @@ -152,13 +152,13 @@ impl Snapshot { self.0.tables.iter().fold(patch, |patch, table| patch.replace(table.clone())) } - /// Encodes the dataset as bytes: its [`to_patch`](Snapshot::to_patch), encoded. + /// Encodes the dataset's [`to_patch`](Snapshot::to_patch) as bytes. pub fn to_bytes(&self) -> Vec { self.to_patch().to_bytes() } - /// Decodes a dataset from bytes written by [`to_bytes`](Snapshot::to_bytes). Errors unless the - /// bytes hold a patch that names a dataset. + /// Decodes a dataset from bytes written by [`to_bytes`](Snapshot::to_bytes). Returns an error + /// if the bytes do not hold a patch that names a dataset. pub(crate) fn from_bytes(bytes: &[u8]) -> Result { let patch = Patch::from_bytes(bytes)?; let Some(name) = patch.data.clone() else { diff --git a/crates/latkit/src/data/source.rs b/crates/latkit/src/data/source.rs index 31a2c4c7..e23f7731 100644 --- a/crates/latkit/src/data/source.rs +++ b/crates/latkit/src/data/source.rs @@ -9,10 +9,10 @@ use super::snapshot::Snapshot; use crate::pending::Watchers; use crate::{Error, Result}; -/// A dataset that changes in place: named tables of columns, changed only by patches. Clones share -/// the same dataset. A view drawing it shows the latest version from the frame after a patch -/// applies. A [`Snapshot`] keeps one version unchanged, costing only the pages that later versions -/// rewrite. +/// A dataset that changes in place. It holds named tables of columns, and only patches change it. +/// Clones share the same dataset. A view drawing it shows the latest version from the frame after a +/// patch applies. A [`Snapshot`] keeps one version unchanged and costs only the pages that later +/// versions rewrite. /// /// ``` /// # use latkit::{Data, Ids, Patch, Table}; @@ -67,7 +67,7 @@ impl Data { self.current().clone() } - /// Makes `snapshot`, a version of this dataset, the current version, as an undo does. Views + /// Makes `snapshot`, a version of this dataset, the current version, like an undo. Views /// drawing the dataset show it from their next frame. A snapshot of another dataset (by name) /// is an error. pub fn restore(&self, snapshot: &Snapshot) -> Result<()> { @@ -91,7 +91,7 @@ impl Data { } impl From for Data { - /// A dataset whose current version is `snapshot`; patches apply from there. + /// A dataset whose current version is `snapshot`. Patches apply from there. fn from(snapshot: Snapshot) -> Data { Data(Arc::new(Shared { name: snapshot.name().clone(), @@ -107,9 +107,9 @@ impl std::fmt::Debug for Data { } } -/// Where a view gets a dataset, matched by the dataset's name: a [`Data`], drawn as it changes; a -/// [`Snapshot`], drawn unchanged; or [`Tiles`](crate::Tiles), fetched in pieces as the view needs -/// them. +/// Where a view gets a dataset, matched by the dataset's name. A [`Data`] is drawn as it changes, a +/// [`Snapshot`] is drawn unchanged, and [`Tiles`](crate::Tiles) are fetched in pieces as the view +/// needs them. #[cfg(feature = "gpu")] #[derive(Clone, Debug)] pub enum Source { diff --git a/crates/latkit/src/data/table.rs b/crates/latkit/src/data/table.rs index eb22731f..a5f8d8a8 100644 --- a/crates/latkit/src/data/table.rs +++ b/crates/latkit/src/data/table.rs @@ -8,7 +8,8 @@ use super::samples::Samples; use super::values::{Array, Kind, Value, Values}; use crate::{Error, Result}; -/// A named set of rows with fields: the rows of one type in a dataset, or rows to write into one. +/// A named set of rows with fields. It holds the rows of one type in a dataset, or rows to write +/// into one. /// /// ``` /// # use latkit::{Ids, Table}; @@ -43,8 +44,8 @@ pub(crate) enum Field { pub(crate) struct Reference { pub(crate) to: Name, pub(crate) ids: Column, - /// Target row of each row as `u32`; missing where unwired or the id names no row. Its pages - /// align with the pages of `ids`. + /// Each row's target row as a `u32`, missing where the row is unwired or its id names no row. + /// Its pages line up with the pages of `ids`. pub(crate) rows: Column, /// The inputs each page of `rows` was resolved from, used to detect stale pages. pub(crate) resolved: Arc<[Resolved]>, @@ -57,7 +58,7 @@ pub(crate) struct Reference { /// The target row of a reference row that names no row. pub(crate) const UNWIRED: u32 = u32::MAX; -/// The rows that reference each target row: target `t`'s are `rows[starts[t]..starts[t + 1]]`, in +/// The rows that reference each target row. Target `t` has `rows[starts[t]..starts[t + 1]]`, in /// row order. #[derive(Default)] pub(crate) struct Inverse { @@ -103,7 +104,7 @@ impl Field { } /// The field for rows `rows`, in that order. A reference keeps its target ids, and its target - /// rows are resolved again when the result is applied; samples keep every frame. + /// rows are resolved again when the result is applied. Samples keep every frame. pub(crate) fn gather(&self, rows: &[usize]) -> Field { match self { Field::Values(column, _) => Field::Values(column.gather(rows), Version::next()), @@ -132,8 +133,8 @@ impl Reference { } } - /// The target row that row `row` references, or [`UNWIRED`] if none or past the rows. Reads the - /// page's `u32` storage directly, as [`targets`](Reference::targets) does for every row. + /// The target row that row `row` references, or [`UNWIRED`] if it has none or `row` is past the + /// end. Reads the page's `u32` storage directly, like [`targets`](Reference::targets). pub(crate) fn target_row(&self, row: usize) -> u32 { let Some((values, i)) = self.rows.at(row) else { return UNWIRED }; match (&values.array, &values.valid) { @@ -149,8 +150,8 @@ impl Reference { } } - /// Every row's target row, or [`UNWIRED`] where it names none. Reads page by page rather than - /// value by value. + /// Every row's target row, or [`UNWIRED`] where it names none. Reads a page at a time, not a + /// value at a time. pub(crate) fn targets(&self) -> Vec { let mut out = Vec::with_capacity(self.rows.len()); for page in self.rows.pages() { @@ -239,8 +240,8 @@ impl Reference { } impl Table { - /// A table named `name` with rows `ids`: a count for rows `0..n`, or a list of numbers or - /// texts. + /// A table named `name` with rows `ids`. `ids` is a count for rows `0..n`, or a list of numbers + /// or texts. pub fn new(name: impl Into, ids: impl Into) -> Table { let version = Version::next(); Table { @@ -263,7 +264,7 @@ impl Table { } /// Adds or replaces field `name`, referencing rows of table `to` by id, one id per row. `None` - /// leaves a row unwired. A missing table `to` is an error when the table is applied; an id that + /// leaves a row unwired. A missing table `to` is an error when the table is applied. An id that /// matches no row stays unwired until such a row exists. #[must_use] pub fn reference(mut self, name: impl Into, to: impl Into, ids: impl Into) -> Table { @@ -306,14 +307,14 @@ impl Table { } /// The value of `field` for the row with `id`, or `None` if missing. A sampled field reads its - /// last frame; a reference returns the target's id. + /// last frame. A reference returns the target's id. pub fn get(&self, id: impl Into, field: &str) -> Option { let row = self.row(id.into().as_ref())?; self.value(row, field, f64::INFINITY) } - /// Every row's value of `field`, in row order: target ids for a reference, the last frame for - /// a sampled field. + /// Every row's value of `field`, in row order. A reference gives target ids and a sampled field + /// gives its last frame. pub fn values(&self, field: &str) -> Option { Some(match self.field_named(field)? { Field::Values(column, _) => column.values(), @@ -333,7 +334,8 @@ impl Table { } } - /// A table of rows `rows` of this one, in that order, with every field. Removing rows uses it. + /// A table of the given `rows` of this one, in that order, with every field. Used to remove + /// rows. pub(crate) fn rows(&self, rows: &[usize]) -> Table { let version = Version::next(); Table { diff --git a/crates/latkit/src/data/values.rs b/crates/latkit/src/data/values.rs index da4c2a01..c30dde23 100644 --- a/crates/latkit/src/data/values.rs +++ b/crates/latkit/src/data/values.rs @@ -67,7 +67,7 @@ impl Kind { } } - /// A geometry kind's nesting depth; `None` for other kinds. + /// A geometry kind's nesting depth, or `None` for other kinds. pub(crate) fn depth(self) -> Option { match self { Kind::Point => Some(0), @@ -100,8 +100,8 @@ pub enum Value { Row(Row), } -/// Serializes as JSON: a number (`null` when not finite), boolean, or text; a geometry as nested -/// `[x, y]` points; a reference as its target row's id. +/// Serializes as JSON. Numbers, booleans, and texts are written as is, with non-finite numbers as +/// `null`. A geometry is nested `[x, y]` points, and a reference is its target row's id. impl serde::Serialize for Value { fn serialize(&self, serializer: S) -> Result { match self { @@ -136,9 +136,9 @@ impl schemars::JsonSchema for Value { } } -/// Where each output value of [`Values::assemble`] comes from. Source 0 is the first source; the -/// sources after it are pages of [`PAGE_ROWS`](super::column::PAGE_ROWS) rows, addressed by row. A -/// concrete type rather than a closure, so [`Values::assemble`] is compiled only once. +/// Where each output value of [`Values::assemble`] comes from. Source 0 is the first source. The +/// sources after it are pages of [`PAGE_ROWS`](super::column::PAGE_ROWS) rows, addressed by row. +/// It is a concrete type, not a closure, so [`Values::assemble`] is compiled only once. #[derive(Clone, Copy)] pub(crate) enum Picks<'a> { /// Value `i` is row `rows[i]` of the pages. @@ -148,8 +148,8 @@ pub(crate) enum Picks<'a> { Writes { from: &'a [Option], kept: usize }, } -/// A run of values to copy: a source and its first row (consecutive rows follow), or `None` for -/// missing values, with the run's length. +/// A run of values to copy, and its length. The run is a source and its first row, with +/// consecutive rows following, or `None` for missing values. type Run = (Option<(usize, usize)>, usize); impl Picks<'_> { @@ -236,13 +236,13 @@ impl Values { }) } - /// Value `i` as a row id: `None` when missing, or when the values are not stored as `u64` + /// Value `i` as a row id. `None` when missing, or when the values are not stored as `u64` /// numbers or texts. pub fn id(&self, i: usize) -> Option { super::ids::key(self, i).map(super::name::IdRef::to_id) } - /// Every value as a number, missing as NaN: `None` unless the values are numbers. + /// Every value as a number, with NaN for missing values. `None` if the values are not numbers. pub fn numbers(&self) -> Option> { (self.kind() == Kind::Number).then(|| self.map_numbers(|x| x.unwrap_or(f64::NAN))) } @@ -263,15 +263,16 @@ impl Values { Values { array, valid: self.valid.as_ref().map(|v| v.slice(range)) } } - /// Maps every value, read as by `number` (`None` where missing), through `f`. + /// Maps every value through `f`. Values are read like `number`, with `None` where missing. pub(crate) fn map_numbers(&self, f: impl FnMut(Option) -> T) -> Vec { let mut out = Vec::with_capacity(self.len()); self.extend_numbers(&mut out, f); out } - /// Appends every value to `out`, read as by `number` (`None` where missing) and mapped through - /// `f`. One loop per storage type, so the storage is matched once rather than per value. + /// Appends every value to `out`, mapped through `f`. Values are read like `number`, with `None` + /// where missing. Each storage type has its own loop, so the storage is matched once, not per + /// value. pub(crate) fn extend_numbers(&self, out: &mut Vec, mut f: impl FnMut(Option) -> T) { macro_rules! each { ($v:expr, $number:expr) => { @@ -291,7 +292,7 @@ impl Values { } } - /// Value `i` as a number; booleans are 0 or 1, texts and geometries NaN. + /// Value `i` as a number. Booleans are 0 or 1, and texts and geometries are NaN. pub(crate) fn number(&self, i: usize) -> f64 { match &self.array { Array::F64(v) => v[i], @@ -318,8 +319,8 @@ impl Values { } } - /// The number of points shaders read for the present geometries: every coordinate, plus one - /// break between a row's parts or rings. 0 for values that are not geometries. + /// The number of points shaders read for the present geometries. That is every coordinate, + /// plus one break between a row's parts or rings. 0 for values that are not geometries. pub(crate) fn points(&self) -> usize { let Some(geometry) = self.geometry() else { return 0 }; (0..self.len()).filter(|&row| self.is_valid(row)).map(|row| geometry.row_points(row)).sum() @@ -330,8 +331,8 @@ impl Values { Values { array: Array::Geometry(geometry), valid: None } } - /// Geometry of `depth` copying each of `rows`: a row of a geometry, or `None` for an empty row, - /// which draws nothing. + /// Geometry of `depth` that copies each of `rows`. Each is a row of a geometry, or `None` for + /// an empty row, which draws nothing. pub(crate) fn of_rows<'g>(depth: u8, rows: impl IntoIterator>) -> Values { let mut builder = GeometryBuilder::new(depth); for row in rows { @@ -343,9 +344,9 @@ impl Values { Values::of_geometry(builder.finish()) } - /// A field of `values` typed by what is present: numbers if every present value is a number, - /// booleans if every one is a boolean, and texts otherwise, with other values written as text. - /// GeoJSON and vector tiles type their properties so. + /// A field of `values`, typed by the values present. It is numbers if every present value is a + /// number, booleans if every one is a boolean, and texts otherwise, with other values written + /// as text. GeoJSON and vector tile properties are typed this way. pub(crate) fn infer(values: &[Option]) -> Values { let present = || values.iter().flatten(); if present().all(|v| matches!(v, Value::Number(_))) { @@ -367,8 +368,8 @@ impl Values { /// The values `picks` names among `sources`, missing where it names none. /// - /// The result keeps the first source's storage; numbers from other sources convert to it. A - /// source of another kind is an error. + /// The result keeps the first source's storage, and numbers from other sources convert to it. + /// A source of another kind is an error. pub(crate) fn assemble(sources: &[&Values], picks: Picks<'_>) -> Result { let kind = sources.first().expect("a source").kind(); if let Some(other) = sources.iter().find(|s| s.kind() != kind) { @@ -379,14 +380,15 @@ impl Values { } /// `parts` concatenated in the first part's storage, converting numbers from other storages. - /// Joins a column's pages by copying each part whole. Every part must have the first's kind. + /// Joins a column's pages by copying each part whole. Every part must have the same kind as the + /// first. pub(crate) fn concat(parts: &[&Values]) -> Values { debug_assert!(parts.iter().all(|p| p.kind() == parts[0].kind())); let runs: Vec = parts.iter().enumerate().map(|(s, part)| (Some((s, 0)), part.len())).collect(); Values::of_runs(parts, &runs) } - /// The values `runs` copy from `sources`, in the first source's storage; numbers from other + /// The values `runs` copy from `sources`, in the first source's storage. Numbers from other /// sources convert to it. fn of_runs(sources: &[&Values], runs: &[Run]) -> Values { let len = runs.iter().map(|&(_, n)| n).sum(); @@ -462,8 +464,8 @@ impl Values { Values { array, valid: (!all).then(|| bits.finish()) } } - /// Whether `other` holds exactly these values in the same storage. Compares the stored - /// representation without building any [`Value`]: floats by their bits, so NaN equals itself. + /// Whether `other` holds exactly these values in the same storage. Compares the stored form + /// without building any [`Value`]. Floats compare by their bits, so NaN equals itself. pub(crate) fn same(&self, other: &Values) -> bool { let len = self.len(); if other.len() != len || (0..len).any(|i| self.is_valid(i) != other.is_valid(i)) { @@ -483,8 +485,8 @@ impl Values { } } - /// Whether value `i` equals `other`'s value `j` as stored: floats by their bits, so NaN equals - /// itself. Two missing values are equal; values in different storages differ. + /// Whether value `i` equals value `j` of `other` as stored. Floats compare by their bits, so + /// NaN equals itself. Two missing values are equal, and values in different storages differ. pub(crate) fn same_row(&self, i: usize, other: &Values, j: usize) -> bool { let valid = self.is_valid(i); if valid != other.is_valid(j) { @@ -506,7 +508,7 @@ impl Values { } } - /// Numbers converted to `store`; other kinds unchanged. + /// Numbers converted to `store`. Other kinds are unchanged. fn cast(&self, convert: impl Fn(f64) -> T, store: Store) -> Values where Buffer: Into, diff --git a/crates/latkit/src/derive/graph.rs b/crates/latkit/src/derive/graph.rs index 565e9e00..873013a7 100644 --- a/crates/latkit/src/derive/graph.rs +++ b/crates/latkit/src/derive/graph.rs @@ -76,8 +76,8 @@ pub(crate) fn drawing(vertex_marks: &[VertexMark<'_>], data: &Name, table: &Name vertex_marks.iter().position(|vm| vm.data.name() == data && vm.table.name() == table) } -/// The position row `row`'s data gives it: from the mark's position fields, else its point geometry -/// (finite points only). `None` where the data gives none. +/// Row `row`'s position from its data: the mark's position fields, else its point geometry (finite +/// points only). `None` where the data gives none. pub(crate) fn position(mark: &Vertices, rows: &Rows<'_>, row: usize) -> Option<[f64; 2]> { match &mark.position { Some([x, y]) => Some([rows.number(x, row)?, rows.number(y, row)?]), @@ -115,8 +115,9 @@ pub(crate) fn links(marks: &[VertexMark<'_>], edge_marks: &[EdgeMark<'_>]) -> Ve links } -/// How an edge mark's rows meet vertices: the two endpoint references, each with the first vertex -/// index of the table it references; or, for a net table, every port reference to its rows. +/// How an edge mark's rows meet vertices. For an edge table, the two endpoint references, each with +/// the first vertex index of the table it references. For a net table, every port reference to its +/// rows. pub(crate) struct Joins<'a> { ends: Option<[(&'a Reference, usize); 2]>, ports: Vec>, diff --git a/crates/latkit/src/derive/lanes.rs b/crates/latkit/src/derive/lanes.rs index b3ab6d37..cc97026c 100644 --- a/crates/latkit/src/derive/lanes.rs +++ b/crates/latkit/src/derive/lanes.rs @@ -21,7 +21,7 @@ use crate::host::Job; const LANE_COST: f64 = 0.06; /// The offset added to twice the lane, so the high half of a lane word is never negative. const HALF: u32 = 1 << 15; -/// The largest family size a lane word stores; larger families store this. +/// The largest family size a lane word stores. Larger families store this. const MOST: u32 = (1 << 16) - 1; /// The stamp a table's lanes were made from, and any pending job. @@ -172,7 +172,7 @@ pub(crate) fn lanes(from: &[u32], to: &[u32]) -> Option> { ) } -/// Each of `rows` rows' lane among the rows joining the same two vertices, where `ends` gives each +/// The lane of each of `rows` rows among the rows joining the same two vertices. `ends` gives each /// row's endpoints. The sheet spreads straight wires by lane. `None` if no rows share endpoints. pub(crate) fn spread(rows: usize, ends: impl Fn(usize) -> Option<[usize; 2]>) -> Option> { let (from, to): (Vec, Vec) = diff --git a/crates/latkit/src/derive/matches.rs b/crates/latkit/src/derive/matches.rs index bae9d9dd..e4753780 100644 --- a/crates/latkit/src/derive/matches.rs +++ b/crates/latkit/src/derive/matches.rs @@ -1,8 +1,8 @@ -//! Text matches: for each `field.contains(text)` a mark uses, whether each row's text field -//! contains `text`, ignoring case. The results are boolean fields of a table beside the mark's own -//! table, which the GPU reads. Texts stay on the CPU, so matches are computed here, again only when -//! the field or the rows change. A page whose matches are unchanged keeps its column page, so only -//! changed pages upload. +//! Text matches. For each `field.contains(text)` a mark uses, computes whether each row's text +//! field contains `text`, ignoring case. The results are boolean fields of a table beside the +//! mark's own table, which the GPU reads. Texts stay on the CPU, so matches are computed here, and +//! again only when the field or the rows change. A page whose matches are unchanged keeps its +//! column page, so only changed pages upload. use std::sync::Arc; diff --git a/crates/latkit/src/derive/mod.rs b/crates/latkit/src/derive/mod.rs index 36ee5086..e35dcd84 100644 --- a/crates/latkit/src/derive/mod.rs +++ b/crates/latkit/src/derive/mod.rs @@ -1,17 +1,17 @@ -//! Derived data: tables the view computes from the scene and data instead of receiving them. +//! Derived data: tables the view computes from the scene and data. //! -//! Every derivation follows one lifecycle. It stamps its inputs and recomputes only when the stamp +//! Every derivation works the same way. It stamps its inputs and recomputes only when the stamp //! changes. Small work runs on the frame, within one budget ([`FRAME_WORK`]) that all derivations //! share. Larger work runs as a host job, and its result is kept only while its inputs are -//! unchanged. Results are tables beside the tables marks draw, row for row, which marks read like -//! data. Values the GPU computes (normals, segment counts, the texts labels need) use the same -//! stamps and are read back. +//! unchanged. Results are tables that stand beside the drawn tables, row for row, and marks read +//! them like data. Values the GPU computes (normals, segment counts, the texts labels need) use the +//! same stamps and are read back. //! -//! The derivations: a diagram's sheet (places, sizes, group boxes, wires). Outside diagrams: -//! lanes of edges sharing both endpoints, stars of nets, and, when the scene has a layout, places -//! of vertex rows without positions. Everywhere: shapes of geometry fields (every area's -//! triangles), text matches for `contains`, the pieces of given tile sets the camera needs, and -//! what each paged table draws. +//! In a diagram, the sheet derives places, sizes, group boxes, and wires. Outside diagrams, the +//! derivations are lanes of edges that share both endpoints, stars of nets, and, when the scene has +//! a layout, places of vertex rows without positions. In every space they also derive shapes of +//! geometry fields (every area's triangles), text matches for `contains`, the pieces of given tile +//! sets the camera needs, and what each paged table draws. pub(crate) mod graph; mod lanes; @@ -55,7 +55,7 @@ pub(crate) fn stamp_with(hash: impl FnOnce(&mut FxHasher)) -> u64 { /// Work whose bytes arrive later: a GPU readback, or a host job or fetch. pub(crate) trait Answer { - /// Takes the bytes once they arrive; `None` until then. + /// Takes the bytes once they arrive. `None` until then. fn take(&self) -> Option>>; } @@ -73,11 +73,11 @@ impl Answer for Pending> { /// Work under way: its answer, the stamp of the inputs it was started for, and the context its /// answer is read with. Work that lands counts as done for its stamp, whether it succeeded or -/// failed, so no work for that stamp starts again: a job runs once for each set of inputs. Stamp 0 -/// is for work started on request rather than for inputs, which this does not track. +/// failed, so no work for that stamp starts again. A job runs once for each set of inputs. Stamp 0 +/// is for work started on request, not for inputs, and is not tracked. pub(crate) struct Working>> { pending: Option<(u64, A, C)>, - /// The stamp of the work that landed last; 0 for none. + /// The stamp of the work that landed last, or 0 for none. done: u64, } @@ -113,7 +113,7 @@ impl Working { self.pending = Some((stamp, answer, context)); } - /// Drops the pending work; its answer is ignored. + /// Drops the pending work. Its answer is ignored. pub(crate) fn cancel(&mut self) { self.pending = None; } @@ -141,7 +141,7 @@ impl Default for Derived { } impl Derived { - /// The stamp the value was made from; 0 before the first value. + /// The stamp the value was made from, or 0 before the first value. pub(crate) fn stamp(&self) -> u64 { self.stamp } @@ -213,7 +213,7 @@ impl<'a> Jobs<'a> { Jobs { errors, host, waker, counters, left } } - /// Whether `cost` microseconds fit the rest of the frame's budget; spends them if so. + /// Whether `cost` microseconds fit the rest of the frame's budget. Spends them if so. pub(crate) fn fits(&mut self, cost: u64) -> bool { let fits = cost <= u64::from(self.left); if fits { @@ -244,7 +244,7 @@ impl<'a> Jobs<'a> { self.left = self.left.saturating_sub(cost.min(u64::from(u32::MAX)) as u32); } - /// Runs `job` on the host, off the frame; without a host, runs it immediately. + /// Runs `job` on the host, off the frame. Without a host, runs it immediately. pub(crate) fn spawn(&self, job: Job) -> Pending> { self.counters.add(Count::Jobs, 1); let pending = match self.host { @@ -350,9 +350,9 @@ impl Input<'_> { } } - /// Calls `read` with every field that `expr`, of a mark drawing `table`, reads. When `own` is - /// set, a field of `table` itself is reported as `own` (it sizes or places each row by that - /// row's value), unless a domain fits `expr` to every row's values. + /// Calls `read` with every field that `expr` reads, for a mark drawing `table`. When `own` is + /// set, a field of `table` itself is reported as `own`, since it sizes or places each row by + /// that row's value. The exception is when a domain fits `expr` to every row's values. fn read_expr<'a>( &'a self, table: (&'a Name, &'a Name), @@ -435,8 +435,8 @@ pub(crate) struct Derivations { pub(crate) tiled: tiled::Tiled, } -/// The derived tables beside one drawn table: the first of the sheet's, places', stars', or lanes', -/// then the shapes', then the matches'. +/// The derived tables beside one drawn table: the first found of the sheet's, places', stars', or +/// lanes' tables, then the shapes' table, then the matches' table. pub(crate) type Beside<'a> = [Option<&'a Table>; 3]; impl Derivations { @@ -509,8 +509,8 @@ impl Derivations { [first, self.shapes.tables().get(&key), self.matches.tables().get(&key)] } - /// The shapes of paged table `table` of dataset `data`, made per page of its order, by the - /// derived field each is written as. + /// The shapes of paged table `table` of dataset `data`, made per page of its order, keyed by + /// the derived field each is written as. pub(crate) fn paged(&self, data: &Name, table: &Name) -> Vec<(Name, &shapes::PagedShapes)> { self.shapes.paged(data, table) } diff --git a/crates/latkit/src/derive/order.rs b/crates/latkit/src/derive/order.rs index 48fb5232..05c83dcc 100644 --- a/crates/latkit/src/derive/order.rs +++ b/crates/latkit/src/derive/order.rs @@ -3,13 +3,13 @@ //! A mark lays out every per-row input and every per-row GPU word of a paged table in pages of its //! order. Choosing tiles then chooses ranges of pages, so nothing is gathered as the camera moves. //! -//! Until its pyramid arrives, a table draws an even sample of its rows first. The rows that sampled -//! edges end at come before the sample, as a pyramid would show them with those edges. A pyramid -//! serves while the rows it was made from are unchanged; rows appended since draw after them, in +//! Until its pyramid arrives, a table draws an even sample of its rows first. The endpoint rows of +//! sampled edges come before the sample, as a pyramid would show them with those edges. A pyramid +//! is used while the rows it was made from are unchanged. Rows appended since draw after them, in //! table order. //! -//! A table of vector tiles draws in table order, its tiles' rows appended as they arrive, so its -//! tiles are already ranges of its order and appending moves no row. +//! A table of vector tiles draws in table order, with each tile's rows appended as they arrive. So +//! its tiles are already ranges of its order, and appending moves no row. use std::sync::Arc; @@ -21,21 +21,23 @@ use crate::gpu::shaders::PAGE_ROWS; /// A paged table's draw order: which row draws at each place. #[derive(Clone, Debug)] pub(crate) enum Order { - /// The rows in `lifted` first; then every `step`th row of the table's `rows` rows (those at - /// multiples of `step`) not lifted; then the remaining rows. Each group is in table order. + /// The rows in `lifted` first. Then every `step`th row of the table's `rows` rows (those at + /// multiples of `step`) that is not lifted. Then the remaining rows. Each group is in table + /// order. Sampled { rows: u32, step: u32, lifted: Lifted }, /// A pyramid's order of the table's first rows, then the rows appended since, in table order, /// up to `rows` rows. Pyramid { pyramid: Arc, rows: u32 }, /// The table's `rows` rows in table order, while its row order version (`Table::order`) is - /// `order`. Rows appended draw after the rest; removing rows changes the version, and so the + /// `order`. Rows appended draw after the rest. Removing rows changes the version, and so the /// order. Table { rows: u32, order: Version }, } -/// The rows a sample draws before its own: the rows the sampled edges end at, just as a pyramid -/// shows a vertex at least as coarse as the coarsest edge ending at it. Holds the rows ascending, -/// each row's place in the plain sample (without lifted rows) ascending, and a stamp of the rows. +/// The rows a sample draws before its own: the endpoint rows of the sampled edges. A pyramid +/// likewise shows a vertex at a level at least as coarse as the coarsest edge ending at it. Holds +/// the rows ascending, each row's place in the plain sample (without lifted rows) ascending, and a +/// stamp of the rows. #[derive(Clone, Debug, Default)] pub(crate) struct Lifted { rows: Arc<[u32]>, @@ -45,7 +47,7 @@ pub(crate) struct Lifted { impl Lifted { /// Lifts `rows` of a table of `len` rows into a sample of `sample` rows. The lifted rows count - /// toward the sample; returns them with the step of the even sample that fills the rest. + /// toward the sample. Returns them with the step of the even sample that fills the rest. pub(crate) fn within(rows: Vec, len: u32, sample: u32) -> (Lifted, u32) { let rows = ascending(rows, len); let step = len.div_ceil(sample.saturating_sub(rows.len() as u32).max(1)).max(1); @@ -80,7 +82,7 @@ fn sample_rank(r: u32, rows: u32, step: u32) -> u32 { if r.is_multiple_of(step) { r / step } else { rows.div_ceil(step) + r - r / step - 1 } } -/// The row at place `d` in an even sample of `rows` rows taking every `step`th; the inverse of +/// The row at place `d` in an even sample of `rows` rows taking every `step`th. The inverse of /// `sample_rank`. fn sample_source(d: u32, rows: u32, step: u32) -> u32 { let first = rows.div_ceil(step); @@ -145,7 +147,7 @@ impl Order { } } - /// The place where row `r` draws; the inverse of `source`. + /// The place where row `r` draws. The inverse of `source`. pub(crate) fn rank(&self, r: u32) -> u32 { match self { Order::Sampled { rows, step, lifted } => { @@ -183,9 +185,8 @@ impl Order { out } - /// The key that identifies pages packed in this order. A pyramid's id ignores rows appended - /// since, and so does table order's; a sample's depends on its row count, step, and lifted - /// rows. + /// The key that identifies pages packed in this order. The ids of a pyramid and of table order + /// ignore rows appended since. A sample's id depends on its row count, step, and lifted rows. pub(crate) fn id(&self) -> u64 { match self { Order::Sampled { rows, step, lifted } => stamp_of(("sampled", rows, step, lifted.stamp)), diff --git a/crates/latkit/src/derive/places.rs b/crates/latkit/src/derive/places.rs index 565802f0..a9db01d5 100644 --- a/crates/latkit/src/derive/places.rs +++ b/crates/latkit/src/derive/places.rs @@ -3,9 +3,9 @@ //! On a plane or the earth, the scene's layout places the vertex rows that have no position. Rows //! with positions are pinned, and edges are the layout's links, so an unpositioned row ends up //! among its neighbors. Gaps are in the space's units. As on a diagram's sheet, places persist by -//! row, and the layout (a host job) places only rows without a place until `arrange` lays out -//! every unpositioned row again. The result is a table beside each vertex table, holding its rows' -//! positions, given or laid out, which the vertex marks read. +//! row. The layout (a host job) places only rows without a place, until `arrange` lays out every +//! unpositioned row again. The result is a table beside each vertex table that holds its rows' +//! positions, given or laid out. The vertex marks read it. use std::hash::Hash; diff --git a/crates/latkit/src/derive/pyramid.rs b/crates/latkit/src/derive/pyramid.rs index 9c99d514..2ee001bc 100644 --- a/crates/latkit/src/derive/pyramid.rs +++ b/crates/latkit/src/derive/pyramid.rs @@ -32,7 +32,7 @@ pub(crate) const GRID_BITS: u32 = 7; pub(crate) const MAX_LEVEL: u8 = 24; /// Marks a row not yet assigned a level. const NEVER: u8 = u8::MAX; -/// The most tiles a camera request splits into per level; past it, the tiles drawn largest split +/// The most tiles a camera request splits into per level. Past it, the tiles drawn largest split /// and the rest stop splitting. const MAX_TILES: usize = 1024; /// Log2 of the sub-cells across a large tile. Each sub-cell is a range of the order that the camera @@ -64,13 +64,14 @@ pub(crate) enum Places { #[derive(Clone, Debug, Default)] pub(crate) struct Placing { pub(crate) rows: u32, - /// Whether its rows are assigned to tiles by level; else they form one tile in priority order. + /// Whether its rows are assigned to tiles by level. If not, they form one tile in priority + /// order. pub(crate) leveled: bool, /// Whether the answer includes its pyramid. A table placed only to anchor the edges ending at /// it is not answered. pub(crate) answered: bool, pub(crate) places: Places, - /// Each row's priority, higher first; empty when all rows are equal. + /// Each row's priority, higher first. Empty when all rows are equal. pub(crate) priority: Arc<[f32]>, /// For an edge table placed by its endpoints: the index of the table they reference, and each /// endpoint reference's target rows. @@ -79,7 +80,7 @@ pub(crate) struct Placing { /// Each row's anchor in `domain`'s square in `u32` fixed point (the full `u32` range spans the /// square, from the west and from the north), and for boxes, half the box's larger side in the same -/// units. Unplaced rows anchor at the origin with no extent; `Places::None` gives nothing. +/// units. Unplaced rows anchor at the origin with no extent. `Places::None` gives nothing. fn anchored(places: &Places, domain: &Domain) -> (Vec<[u32; 2]>, Vec) { let fixed = |u: f64| (u.clamp(0.0, 1.0) * 4_294_967_295.0) as u32; match places { @@ -235,7 +236,7 @@ fn by_ends(ends: &[Arc<[u32]>], at: &[[u32; 2]], wraps: bool, n: usize) -> (Vec< (anchors, extents) } -/// For each row, the next row joining the same two vertices, forming one cycle per pair; a row +/// For each row, the next row joining the same two vertices, so each pair forms one cycle. A row /// with no parallel rows maps to itself. Parallel rows share an anchor, so they are adjacent in /// curve order. fn mates(sorted: &[(u64, u32)], pairs: &[u64]) -> Vec { @@ -274,8 +275,8 @@ fn tie(row: u32) -> u32 { (u64::from(row).wrapping_mul(0x9E37_79B9_7F4A_7C15) >> 32) as u32 } -/// Each of `n` rows' rank, higher first: its priority (missing or NaN lowest), then a hash of its -/// row index, so equal priorities spread evenly. +/// The rank of each of `n` rows, higher first: its priority (missing or NaN lowest), then a hash +/// of its row index, so equal priorities spread evenly. fn ranks(priority: &[f32], n: usize) -> Vec { (0..n) .map(|r| { @@ -333,11 +334,11 @@ fn unpack(key: u64) -> TileKey { /// The level each row is assigned. At each level, coarsest first, each cell takes the /// highest-ranked row left in it, so a row is assigned at the first level where it outranks every /// other row left in its cell. Rows with a nonzero `floor` are never chosen by rank. A row is never -/// assigned finer than its size or `ceiling` allows; if no cell took it by then, it is forced -/// in at that level. Rows `pairs` marks as parallel are assigned together. Rows left over go to the +/// assigned finer than its size or `ceiling` allows. If no cell took it by then, it is forced in at +/// that level. Rows `pairs` marks as parallel are assigned together. Rows left over go to the /// finest level. /// -/// Rows are processed in runs along the curve, each run the rows left in one cell. While a run +/// Rows are processed in runs along the curve. Each run is the rows left in one cell. While a run /// stays within one cell for several levels, those levels take its top rows in rank order in one /// pass, using a heap. Where a run spreads over several cells, each cell's rows continue as a run /// of their own. So a crowd in one cell for many levels costs its size once, not once per level. @@ -411,7 +412,7 @@ fn place( ); let mut heap = std::collections::BinaryHeap::from(std::mem::take(&mut scratch)); for l in z..=deepest { - // Skip rows too large for this level; the forcing step below assigns them. + // Skip rows too large for this level. The forcing step below assigns them. while let Some((_, r)) = heap.pop() { if level[r as usize] == NEVER && fits[r as usize] >= l { land(r, l, &mut level); @@ -453,7 +454,7 @@ fn place( } /// Orders rows by tile, coarsest level first, and returns the order with the tiles. Each tile's -/// rows are in priority order, highest last; a large tile's rows are grouped by sub-cell, each +/// rows are in priority order, highest last. A large tile's rows are grouped by sub-cell, each /// sub-cell in priority order. `sorted` holds the rows along the curve. fn tiled( sorted: &[(u64, u32)], @@ -493,7 +494,7 @@ fn tiled( } let start = order.len(); let members = &rows[i..j]; - // Sort by priority only, without the tie hash; equal rows keep their curve order. + // Sort by priority only, without the tie hash. Equal rows keep their curve order. let by_priority = |order: &mut Vec, from: usize| { crate::order::by_key(&mut order[from..], |&r| ranks[r as usize] >> 32); }; @@ -586,7 +587,7 @@ pub(crate) struct Pyramid { pub(crate) order: Vec, pub(crate) rank: Vec, pub(crate) tiles: Vec, - /// Identifies this pyramid among all others; pages packed in its order are keyed by it. + /// Identifies this pyramid among all others. Pages packed in its order are keyed by it. pub(crate) made: Version, /// Each tile's index in `tiles`, by key. index: FxHashMap, @@ -623,14 +624,14 @@ impl Pyramid { /// The ranges of the order the camera needs, coarsest level first, each with its level, and /// whether the `rows` budget dropped any of them. /// - /// At each level it takes every tile whose rows reach into the region in sight (for a large - /// tile, only the sub-cells that do), and splits a tile into its children while it draws larger - /// than `SPLIT_PX` near the camera's focus. When more tiles would split than a level allows, - /// those drawn largest split first, and the next level lists their children in that order. A - /// drawing holds whole pages of the order, so once a level's pages would exceed `rows` rows, - /// that level and all finer ones are dropped and the coarser levels draw instead. If the first - /// level in sight exceeds the budget alone (for example, edges spanning the square, which - /// cannot go finer), as many of its first rows as fit are drawn. + /// At each level it takes every tile whose rows reach into the visible region (for a large + /// tile, only the sub-cells that do). It splits a tile into its children while the tile draws + /// larger than `SPLIT_PX` near the camera's focus. When more tiles would split than a level + /// allows, those drawn largest split first, and the next level lists their children in that + /// order. A drawing holds whole pages of the order. So once a level's pages would exceed `rows` + /// rows, that level and all finer ones are dropped and the coarser levels draw. If the first + /// visible level exceeds the budget alone (for example, edges spanning the square, which cannot + /// go finer), as many of its first rows as fit are drawn. pub(crate) fn wanted(&self, sight: &Sight<'_>, domain: &Domain, rows: u32) -> (Vec<(u8, Range)>, bool) { let fit = rows.div_ceil(PAGE_ROWS); if let [tile] = &self.tiles[..] @@ -686,7 +687,7 @@ impl Pyramid { // The tiles drawn largest split first, a quarter of `MAX_TILES` at most. crate::order::by_key(&mut large, |&(_, size)| !crate::order::number(size)); let next: Vec = large.iter().take(MAX_TILES / 4).flat_map(|(key, _)| key.children()).collect(); - // If this level's pages exceed the budget, drop it; coarser levels draw instead. + // If this level's pages exceed the budget, drop it, and the coarser levels draw. let (pages, reached) = pages_past(&found, last); if held + pages > fit { if out.is_empty() { @@ -732,9 +733,9 @@ impl Pyramid { } /// Encodes `pyramids` as a job's answer, in little-endian `u32` words. Per pyramid: its table's -/// index among those asked, its row count, its order, and its tile count; then per tile: its packed -/// key (two words), the end of its rows, its reach, its sub-cell count, and where each sub-cell's -/// rows end. +/// index among those asked, its row count, its order, and its tile count. Then per tile: its +/// packed key (two words), the end of its rows, its reach, its sub-cell count, and where each +/// sub-cell's rows end. pub(crate) fn encode(pyramids: &[(u32, Pyramid)]) -> Vec { let mut words: Vec = Vec::new(); for (table, pyramid) in pyramids { diff --git a/crates/latkit/src/derive/shapes.rs b/crates/latkit/src/derive/shapes.rs index 972d3652..6326f788 100644 --- a/crates/latkit/src/derive/shapes.rs +++ b/crates/latkit/src/derive/shapes.rs @@ -3,17 +3,17 @@ //! Each area becomes triangles, preceded by its label point. On the earth, each line's path is //! unwrapped across the antimeridian and bent to follow the globe. //! -//! A table drawn whole is shaped page by page of its field, into a field beside it. A page keeps -//! the shapes made from it before, and a page written over another keeps the shapes of its rows -//! not written, so a write reshapes only the rows it wrote. A paged table is shaped page by page of -//! its order, the first time each page draws. A row whose geometry did not change keeps its shape -//! as its pages come and go. +//! A table drawn whole is shaped one page of its field at a time, into a field beside it. A page +//! keeps the shapes made from it before. A page written over another keeps the shapes of the rows +//! not written, so a write reshapes only the rows it wrote. A paged table is shaped one page of its +//! order at a time, the first time each page draws. A row whose geometry did not change keeps its +//! shape as its pages come and go. //! -//! Triangulation that does not fit the frame's budget runs in a job; a page whose every row needs +//! Triangulation that does not fit the frame's budget runs in a job. A page where every row needs //! shaping goes to the job whole. Rows waiting on a job draw nothing until it finishes. Bending a //! path is cheap and done at once. Vector tiles bring their own shapes, made in the job that -//! decodes them, as a field beside their rows. Their tables are paged, and read that field one -//! page of their order at a time like any other, so nothing shapes those rows again. +//! decodes them, as a field beside their rows. Their tables are paged and read that field one page +//! of their order at a time, like any other, so nothing shapes those rows again. use rustc_hash::{FxHashMap, FxHashSet}; @@ -69,8 +69,8 @@ impl Missing { } } -/// What one part of a job shapes: a whole page of the field, by page id, with its row count; or -/// rows, by id, each with its geometry digest. +/// What one part of a job shapes: a whole page of the field (by page id, with its row count), or +/// rows (by id, each with its geometry digest). #[derive(Hash)] enum Asked { Page(PageId, usize), @@ -95,9 +95,9 @@ fn empty_shapes(depth: u8, rows: usize) -> Values { Values::of_rows(depth, std::iter::repeat_n(None, rows)) } -/// Shapes made since the last complete set: whole pages, by the id of the field page each was made -/// from; and rows, by id, each with the geometry digest it was made from and its part and index in -/// `parts`. +/// Shapes made since the last complete set. Whole pages are keyed by the id of the field page each +/// was made from. Rows are keyed by id, each with the geometry digest it was made from and its part +/// and index in `parts`. #[derive(Default)] struct Landed { pages: FxHashMap, @@ -194,8 +194,9 @@ impl Paging { } } -/// One field's shapes and any pending job. Drawn whole: the last complete set, the derived -/// field written, and shapes made but not yet written. Paged: its pages and the previous order's. +/// One field's shapes and any pending job. For a table drawn whole, it holds the last complete set, +/// the derived field written, and shapes made but not yet written. For a paged table, it holds its +/// pages and the previous order's pages. #[derive(Default)] struct Shaped { /// The source table, its field column, and the derived column from the last time every row @@ -374,7 +375,7 @@ impl Shaped { had } - /// Updates the shapes of a table drawn whole to `source`; returns whether its field changed. + /// Updates the shapes of a table drawn whole to `source`. Returns whether its field changed. fn update_whole( &mut self, want: &Want, @@ -390,7 +391,7 @@ impl Shaped { } self.stamp = stamp; let (mut derived, mut missing) = self.build(want, source, column, depth); - // Shapes made on the frame apply at once; rebuild the column with them. + // Shapes made on the frame apply at once. Rebuild the column with them. if !missing.is_empty() { match self.shape_now(want, column, missing, jobs) { Ok(()) => (derived, missing) = self.build(want, source, column, depth), @@ -450,8 +451,8 @@ impl Shaped { (paging.source, paging.column) = (source.clone(), column.clone()); } paging.shapes.kept.retain(|d, _| paged.kept.binary_search(d).is_ok()); - // Make drawn pages not made yet, or waiting on rows while no job runs; and, while no job - // runs, shape kept rows not shaped yet. + // Make drawn pages that are not made yet, or that wait on rows while no job runs. While no + // job runs, also shape kept rows not shaped yet. let running = self.working.busy(); let made = &paging.shapes.pages; let make: Vec = @@ -466,7 +467,7 @@ impl Shaped { return false; } let (mut made, mut kept, mut missing) = self.make_paged(want, order, &make, &keep, source, column, depth); - // Shapes made on the frame apply at once; remake the pages with them. + // Shapes made on the frame apply at once. Remake the pages with them. if !missing.rows.is_empty() { match self.shape_now(want, column, missing, jobs) { Ok(()) => (made, kept, missing) = self.make_paged(want, order, &make, &keep, source, column, depth), @@ -522,7 +523,7 @@ impl Shaped { (made, kept, missing) } - /// Shapes `missing` on the frame if it fits the rest of the budget; else returns it. + /// Shapes `missing` on the frame if it fits the rest of the budget. Otherwise returns it. fn shape_now( &mut self, want: &Want, @@ -539,8 +540,8 @@ impl Shaped { Ok(()) } - /// Starts a job to shape `missing`, unless one is pending, in which case a later update asks - /// again, or the same job already ran. + /// Starts a job to shape `missing`, unless the same job already ran. While a job is pending it + /// does nothing, and a later update asks again. fn ask(&mut self, want: &Want, column: &Column, missing: Missing, jobs: &mut Jobs<'_>) { if self.working.busy() { return; @@ -554,8 +555,8 @@ impl Shaped { /// Builds the derived column for `column` of `source`, page by page, and returns what is left /// to shape. A page reuses the shapes made from it before, or that arrived for it. A page - /// written over a shaped page keeps the shapes of its rows not written, and a page at the same - /// place in the last complete set keeps those of its unchanged rows. A page none of whose rows + /// written over a shaped page keeps the shapes of its rows not written. A page at the same + /// place in the last complete set keeps the shapes of its unchanged rows. A page where no row /// can reuse a shape is shaped whole. Otherwise each row reuses a shape made from the same /// geometry, or is left to shape. fn build(&self, want: &Want, source: &Table, column: &Column, depth: u8) -> (Column, Missing) { @@ -587,7 +588,7 @@ impl Shaped { pages.push(Page::new(empty_shapes(depth, 0))); continue; }; - // A path page needing no bending is its own shape; sharing it means no new upload. + // A path page needing no bending is its own shape. Sharing it means no new upload. if want.shaping == Shaping::Path && !bends(geometry) { pages.push(page.clone()); continue; @@ -601,7 +602,7 @@ impl Shaped { Some((&derived.pages().get(p)?.values, same)) }); if fresh && kept.is_none() && !found(start) { - // Every row needs shaping: shape the page whole; it draws nothing until then. + // Every row needs shaping: shape the page whole. It draws nothing until then. pages.push(Page::new(empty_shapes(depth, len))); missing.pages.push((page.id, page.values.clone())); continue; @@ -627,7 +628,7 @@ impl Shaped { } /// The shapes of the page that `page` was written over, searching back through its recorded - /// writes (a few at most), with whether each row keeps its shape: those not written since. + /// writes (a few at most), with whether each row keeps its shape (rows not written since do). fn over<'a>(&'a self, page: &Page, made: &FxHashMap) -> Option<(&'a Values, Vec)> { let mut written: Vec = Vec::new(); let mut at = page.written.as_deref(); @@ -664,8 +665,8 @@ impl Shaped { } /// Writes the shape of row `row` (with `id`), whose geometry is value `i` of `values`. Reuses a - /// shape that arrived or was made before from the same geometry; bends a path at once; else - /// writes an empty shape and adds the row to `missing`. + /// shape that arrived or was made before from the same geometry. Bends a path at once. + /// Otherwise writes an empty shape and adds the row to `missing`. fn shape( &self, want: &Want, @@ -683,7 +684,7 @@ impl Shaped { if let Some((landed, at)) = self.landed.row(&id, digest).or_else(|| self.kept(&id, digest)) { shapes.push_from(landed, at); } else if want.shaping == Shaping::Path { - // Bending is cheap, so it runs here; rows needing none keep their geometry. + // Bending is cheap, so it runs here. Rows needing none keep their geometry. match values.get(i).and_then(|v| want.shaping.shape(&v)) { Some(shape) => { shapes.push_value(&shape); diff --git a/crates/latkit/src/derive/sight.rs b/crates/latkit/src/derive/sight.rs index 49acf05c..be1f1908 100644 --- a/crates/latkit/src/derive/sight.rs +++ b/crates/latkit/src/derive/sight.rs @@ -1,10 +1,10 @@ -//! The camera's view as tiles; the only place derivations read the camera. +//! The camera's view as tiles. This is the only place derivations read the camera. //! //! Tiles split a square of the space four ways per level: the earth's Web Mercator square, or a -//! square of a plane. The earth's square wraps at the antimeridian on the globe, and ends there on -//! a flat map, which is one world. A tile is needed where it meets the region in sight, and splits -//! while it draws larger than its nominal size (`TILE_PX`) where it lies nearest the camera's -//! focus. How large a tile draws is the area it covers on screen there, as a side, so a tile +//! square of a plane. On the globe, the earth's square wraps at the antimeridian. On a flat map it +//! ends there, so the map is one world. A tile is needed where it meets the visible region. It +//! splits while it draws larger than its nominal size (`TILE_PX`) at its point nearest the camera's +//! focus. A tile's drawn size is the square root of the screen area it covers there, so a tile //! foreshortened toward the globe's limb or a tilted horizon stays coarse. When a budget limits how //! many tiles split, the tiles drawn largest split first. @@ -66,7 +66,7 @@ impl Domain { } } - /// The position at `u` in the square; the inverse of `unit`. + /// The position at `u` in the square. The inverse of `unit`. pub(crate) fn position(&self, u: [f64; 2]) -> [f64; 2] { match *self { Domain::Earth => [u[0] * 360.0 - 180.0, mercator::latitude_down(u[1])], @@ -97,12 +97,12 @@ pub(crate) struct Sight<'a> { pub(crate) earth: bool, } -/// The region in sight as west, south, east, north boxes in position units: one box, or on the -/// globe two where the region crosses the antimeridian. +/// The visible region as west, south, east, north boxes in position units: one box, or on the +/// globe two boxes where the region crosses the antimeridian. pub(crate) type Region = Vec<[f64; 4]>; impl Sight<'_> { - /// Where position `p` appears on screen; `None` when it does not show. + /// Where position `p` appears on screen. `None` when it does not show. fn project(&self, p: [f64; 2]) -> Option<[f64; 2]> { let w = self.projected.world([p[0], p[1], 0.0]); if !self.facing(w) { @@ -128,8 +128,8 @@ impl Sight<'_> { self.earth && self.projected.globe >= 0.5 } - /// Whether world point `w` faces the camera: always on a flat map; on the globe, only on the - /// near side. + /// Whether world point `w` faces the camera: always on a flat map, and only on the near side on + /// the globe. fn facing(&self, w: DVec3) -> bool { if !self.globe() { return true; @@ -156,8 +156,8 @@ impl Sight<'_> { std::iter::once(center).chain(near).collect() } - /// The ground in sight, sampled on a grid of points across the view. On a flat map it ends at - /// the world's edges; on the globe it wraps the antimeridian. + /// The visible ground, sampled on a grid of points across the view. On a flat map it ends at + /// the world's edges. On the globe it wraps the antimeridian. pub(crate) fn region(&self) -> Region { const SAMPLES: usize = 7; let globe = self.globe(); @@ -239,14 +239,15 @@ impl Sight<'_> { boxes(lon - half, lon + half, south, north) } - /// How large tile `key` of `domain`'s square draws, in CSS pixels, at its point nearest each - /// focus (the largest of those), and how far that point is from the view's center. Its size is - /// the area it would cover at the scale there, as a side: the area that steps of at most a - /// pixel along its two edges span on screen, scaled up to the whole tile. So a tile - /// foreshortened toward the globe's limb or a tilted horizon measures less than its longer - /// side. A longer step from a large tile could pass the globe's horizon, where nothing - /// projects, once the camera is close to the ground. On a flat map seen from above, every tile - /// of a level measures the same wherever it lies. + /// How large tile `key` of `domain`'s square draws, in CSS pixels, and how far from the view's + /// center the measured point is. The size is measured at the tile's point nearest each focus, + /// and the largest is kept. It is the square root of the screen area the tile would cover at + /// the scale there: the area spanned by steps of at most a pixel along its two edges, scaled up + /// to the whole tile. So a tile foreshortened toward the globe's limb or a tilted horizon + /// measures less than its longer side. Steps are short because a longer step from a large tile + /// could pass the globe's horizon, where nothing projects, once the camera is close to the + /// ground. On a flat map seen from above, every tile of a level measures the same wherever it + /// lies. pub(crate) fn measure(&self, focus: &[[f64; 2]], domain: &Domain, key: TileKey) -> (f64, f64) { let side = 1.0 / f64::from(1_u32 << key.z); let (west, north) = (f64::from(key.x) * side, f64::from(key.y) * side); @@ -264,7 +265,8 @@ impl Sight<'_> { }; let at = [fx.clamp(west, west + side - step), fy.clamp(north, north + side - step)]; let Some(p) = self.project(domain.position(at)) else { continue }; - // Where a step along each edge moves on screen; nowhere past the globe's horizon. + // Where a step along each edge moves on screen. A step past the globe's horizon counts + // as no move. let moved = |q: [f64; 2]| self.project(domain.position(q)).map_or([0.0; 2], |q| [q[0] - p[0], q[1] - p[1]]); let ([ax, ay], [bx, by]) = (moved([at[0] + step, at[1]]), moved([at[0], at[1] + step])); size = size.max(side / step * (ax * by - ay * bx).abs().sqrt()); @@ -291,7 +293,7 @@ fn boxes(west: f64, east: f64, south: f64, north: f64) -> Region { if e <= 180.0 { vec![[w, s, e, n]] } else { vec![[w, s, 180.0, n], [-180.0, s, e - 360.0, n]] } } -/// Whether box `b` meets the region in sight. +/// Whether box `b` meets the visible region. pub(crate) fn meets(region: &Region, b: [f64; 4]) -> bool { region.iter().any(|r| b[0] <= r[2] && b[2] >= r[0] && b[1] <= r[3] && b[3] >= r[1]) } diff --git a/crates/latkit/src/derive/stars.rs b/crates/latkit/src/derive/stars.rs index d72f271a..c1945350 100644 --- a/crates/latkit/src/derive/stars.rs +++ b/crates/latkit/src/derive/stars.rs @@ -1,10 +1,10 @@ //! Stars of nets, outside diagrams. //! -//! Each row of an edge mark with `joins` is a net joining the vertex rows whose reference field -//! names it. The net draws as a star: a segment from the members' centroid to each member, at the -//! places their vertex mark gives them. Stars are a `route` path field beside the net table, which -//! the mark draws like any geometry. They are recomputed only when the data, the scene, or the -//! layout's places change. +//! Each row of an edge mark with `joins` is a net. Its members are the vertex rows whose reference +//! field names it. The net draws as a star: a segment from the members' centroid to each member, at +//! the places their vertex mark gives them. Stars are a `route` path field beside the net table, +//! and the mark draws them like any geometry. They are recomputed only when the data, the scene, or +//! the layout's places change. use std::hash::Hash; @@ -98,7 +98,8 @@ fn place(vm: &VertexMark<'_>, rows: &Rows<'_>, row: usize, placed: Option<&crate }) } -/// A net's star: a path from the centroid of `members` to each member; empty for fewer than two. +/// A net's star: a path from the centroid of `members` to each member. Empty for fewer than two +/// members. fn star(members: &[[f64; 2]]) -> Vec> { if members.len() < 2 { return Vec::new(); diff --git a/crates/latkit/src/derive/tiled.rs b/crates/latkit/src/derive/tiled.rs index 08bdb721..b25292b1 100644 --- a/crates/latkit/src/derive/tiled.rs +++ b/crates/latkit/src/derive/tiled.rs @@ -1,20 +1,20 @@ -//! Tables drawn in a computed order: by priority, or by the tiles in sight. +//! Tables drawn in a computed order: by priority, or by the visible tiles. //! //! A table with a priority draws its rows in priority order, highest on top. A paged table (too -//! large to draw whole) draws the rows of the tiles the camera needs, or, where nothing places its -//! rows, the rows that matter most. Each draws in an [`Order`]. The order is its pyramid's, -//! computed from where its rows lie and their priority: on the frame when that fits the budget, -//! else in one job per dataset. Until the pyramid arrives, the table draws an even sample of its -//! rows first, enough for a first look, after the rows that sampled edges end at. +//! large to draw whole) draws the rows of the tiles the camera needs. Where nothing places its +//! rows, it draws the rows that matter most. Each draws in an [`Order`]: its pyramid's order, +//! computed from where its rows lie and their priority. The pyramid is computed on the frame when +//! that fits the budget, else in one job per dataset. Until it arrives, the table draws an even +//! sample of its rows first, enough for a first look, after the endpoint rows of sampled edges. //! -//! A table and the table its edges end at draw from their pyramids together, or both from their -//! samples. A pyramid serves while the rows it was made from are unchanged. Rows appended since -//! draw after them, so appending never thins the drawn rows; once the appended rows reach an -//! eighth of the pyramid's rows (at least a page), the pyramid is rebuilt. What the camera needs of -//! a table is ranges of its order, so nothing is gathered as it moves. +//! A table and the table its edges end at both draw from their pyramids, or both from their +//! samples. A pyramid is used while the rows it was made from are unchanged. Rows appended since +//! draw after them, so appending never thins the drawn rows. Once the appended rows reach an eighth +//! of the pyramid's rows (at least a page), the pyramid is rebuilt. The camera needs ranges of a +//! table's order, so nothing is gathered as it moves. //! -//! A table of vector tiles needs no pyramid: its tiles are the vector tiles themselves, whose rows -//! its set appends as they arrive, and the set chooses what it draws (`TileSets::choose`). +//! A table of vector tiles needs no pyramid. Its tiles are the vector tiles themselves. Its tile +//! set appends their rows as they arrive and chooses what it draws (`TileSets::choose`). use std::ops::Range; use std::sync::Arc; @@ -89,11 +89,11 @@ pub(crate) struct Ask<'a> { pub(crate) earth: bool, } -/// What a view draws of a paged table: its order, and the ranges of it to draw, in the order they -/// draw, so later ranges draw on top. A pyramid's tiles draw finest first, so more important rows -/// draw on top; vector tiles draw coarsest first, so finer tiles draw over the ancestors standing -/// in for them. `capped` is whether the plan's row budget dropped some of what the camera needs; -/// only then does a smaller budget draw less of the table. +/// What a view draws of a paged table: its order, and the ranges of it to draw. Ranges are listed +/// in draw order, so later ranges draw on top. A pyramid's tiles draw finest first, so more +/// important rows draw on top. Vector tiles draw coarsest first, so finer tiles draw over the +/// ancestors standing in for them. `capped` is whether the plan's row budget dropped some of what +/// the camera needs. Only then does a smaller budget draw less of the table. #[derive(Clone, Debug, PartialEq)] pub(crate) struct Chosen { pub(crate) order: Order, @@ -164,8 +164,8 @@ impl Placed { } } -/// What a dataset's pyramid job answers: the tables, each with its index among those asked, its -/// order version when asked, and its bounds; and the square the pyramids tile. +/// What a dataset's pyramid job answers: the tables (each with its index among those asked, its +/// order version when asked, and its bounds) and the square the pyramids tile. struct Pyramids { tables: Vec<(u32, Name, Version, Option<[f64; 4]>)>, domain: Domain, @@ -221,8 +221,8 @@ struct Set { working: Working, /// The inputs of the last pyramid request, by table. read: Named, - /// Per table: the stamp of its inputs, the rows that sampled edges ending at it end at (lifted - /// into its sample), and the step of its own sample. + /// Per table: the stamp of its inputs, the endpoint rows of the sampled edges ending at it + /// (lifted into its sample), and the step of its own sample. lifted: Named<(u64, Lifted, u32)>, /// Whether any table draws its sample because no pyramid serves it yet. sampling: bool, @@ -292,7 +292,7 @@ impl Set { (None, Draw::Whole) => continue, (None, Draw::Tiles { .. } | Draw::First { .. }) => { self.sampling = true; - // The rows sampled edges end at draw first, as in a pyramid. + // The endpoint rows of sampled edges draw first, as in a pyramid. let edges: Vec = (0..drawn.len()).filter(|&e| ends[e] == Some(i)).collect(); let (rows, step) = if edges.is_empty() { (Lifted::default(), steps[i]) @@ -312,7 +312,7 @@ impl Set { let made = pyramid.rows(); let (ranges, capped) = match draw { Draw::Whole => (std::iter::once(0..len).collect(), false), - // The rows that matter most are last in the order; appended rows draw too. + // The rows that matter most are last in the order. Appended rows draw too. Draw::First { rows } => (std::iter::once(made - rows.min(made)..len).collect(), rows < made), Draw::Tiles { rows } => { let (mut wanted, capped) = @@ -340,7 +340,8 @@ impl Set { /// no rows written since except appended ones. With no job pending, each table's pyramid must /// also serve it, with fewer rows appended since than an eighth of the rows it was made from /// (at least a page). A pending job keeps running while only rows were appended, so appends - /// never starve the pyramids; once it finishes, it serves until that many rows are appended. + /// never starve the pyramids. Once it finishes, its pyramids serve until that many rows are + /// appended. fn holds(&self, placing: &[(&Table, &Planned)], earth: bool) -> bool { placing.len() == self.read.len() && placing.iter().all(|(table, planned)| { @@ -371,12 +372,13 @@ impl Set { /// Requests the pyramids of `placing`: computed at once when they fit the frame's budget, else /// in one job for the dataset. fn level(&mut self, placing: &[(&Table, &Planned)], ask: &Ask<'_>, jobs: &mut Jobs<'_>) { - // Drop any pending job; its answer would no longer serve. + // Drop any pending job. Its answer would no longer serve. self.working.cancel(); self.read = placing.iter().map(|(table, planned)| (table.name.clone(), Read::new(table, planned, ask.earth))).collect(); let (leveling, bounds) = leveling(ask, placing); - // Placing a row takes about a microsecond; ordering one by priority alone, a twentieth. + // Placing a row takes about a microsecond. Ordering one by priority alone takes a + // twentieth of that. let cost: f64 = leveling .tables .iter() @@ -405,7 +407,7 @@ impl Set { /// Each table's sample size until its pyramid arrives (its plan's row budget, at most `SAMPLE`), /// and its sample step. The endpoints of sampled edges count toward their target table's sample /// (per `ends`), so edges sample sparsely enough that their endpoints fill at most half the -/// target's sample; the target's own sample fills the rest. +/// target's sample. The target's own sample fills the rest. fn samples(drawn: &[(&Table, &Planned)], ends: &[Option]) -> (Vec, Vec) { let sample: Vec = drawn .iter() @@ -432,10 +434,10 @@ fn samples(drawn: &[(&Table, &Planned)], ends: &[Option]) -> (Vec, V (sample, steps) } -/// The rows of table `drawn[at]` that the sampled rows of edge tables `edges` end at (each edge -/// table sampling every `steps[e]`th row), lifted into its sample of `sample` rows, with the step -/// of its own even sample of the rest. Returns `was` when its input stamp is unchanged, else -/// recomputes; the result starts with that stamp. +/// The rows of table `drawn[at]` where the sampled rows of edge tables `edges` end, lifted into its +/// sample of `sample` rows. Each edge table samples every `steps[e]`th row. Also returns the step +/// of the target's own even sample of the rest. Returns `was` when its input stamp is unchanged, +/// else recomputes. The result starts with that stamp. fn lift( drawn: &[(&Table, &Planned)], edges: &[usize], @@ -565,7 +567,8 @@ fn fields_of(expr: &Expr, out: &mut Vec) { }); } -/// The box (west, south, east, north) around every placed row of `places`; inverted when none is. +/// The box (west, south, east, north) around every placed row of `places`. Inverted when no row +/// is placed. fn bounds_of(places: &Places) -> [f64; 4] { let mut b = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY]; let mut take = |r: [f64; 4]| { diff --git a/crates/latkit/src/error.rs b/crates/latkit/src/error.rs index 91d396e1..08c703bc 100644 --- a/crates/latkit/src/error.rs +++ b/crates/latkit/src/error.rs @@ -73,8 +73,8 @@ impl Error { } } - /// This error, of the same kind, naming `source` as where it happened: the first step of an - /// invalid input's path, or the start of another error's message. + /// This error, of the same kind, with `source` named as where it happened. `source` becomes the + /// first step of an invalid input's path, or the start of another error's message. #[cfg(feature = "gpu")] #[must_use] pub(crate) fn within(self, source: &str) -> Error { @@ -111,16 +111,16 @@ impl schemars::JsonSchema for Error { fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { schemars::json_schema!({ - "description": "What failed, as JSON: the form of the npm package's `LatkitError`.", + "description": "A failure as JSON, in the same form as the npm package's `LatkitError`.", "type": "object", "properties": { "code": { - "description": "The kind of failure, as `LatkitError.code` describes it.", + "description": "The kind of failure. See `LatkitError.code`.", "enum": Error::CODES, }, - "message": { "description": "What failed, in words.", "type": "string" }, + "message": { "description": "A description of what failed.", "type": "string" }, "path": { - "description": "The invalid part of the input, such as `marks[2].color`, on `invalid` failures.", + "description": "For `invalid` failures, the path of the invalid input, such as `marks[2].color`.", "type": "string", }, }, diff --git a/crates/latkit/src/expr/color.rs b/crates/latkit/src/expr/color.rs index f930b1e3..b3501acf 100644 --- a/crates/latkit/src/expr/color.rs +++ b/crates/latkit/src/expr/color.rs @@ -31,7 +31,7 @@ impl Color { Color { r, g, b, a: 1.0 } } - /// A color from `#rgb`, `#rgba`, `#rrggbb`, or `#rrggbbaa`; `None` for any other text. + /// A color from `#rgb`, `#rgba`, `#rrggbb`, or `#rrggbbaa`. `None` for any other text. pub fn hex(hex: &str) -> Option { let digits = hex.strip_prefix('#')?; if !digits.is_ascii() { @@ -51,7 +51,7 @@ impl Color { } /// A color from CSS: hex, `rgb(…)` or `rgba(…)` (channels 0 to 255 or percentages, alpha 0 to 1 - /// or a percentage), a named color, or `transparent`. Case-insensitive; `None` if unrecognized. + /// or a percentage), a named color, or `transparent`. Case-insensitive. `None` if unrecognized. pub fn css(css: &str) -> Option { let css = css.trim(); if css.starts_with('#') { diff --git a/crates/latkit/src/expr/colormap.rs b/crates/latkit/src/expr/colormap.rs index 666a8227..d4e5b58c 100644 --- a/crates/latkit/src/expr/colormap.rs +++ b/crates/latkit/src/expr/colormap.rs @@ -106,14 +106,14 @@ impl Colormap { (0..CATALOG.len()).map(|i| Colormap(Map::Catalog(i as u8))) } - /// The catalog colormap named `name`, such as `"viridis"`; `None` if the catalog has none. + /// The catalog colormap named `name`, such as `"viridis"`. `None` if the catalog has none. pub fn named(name: &str) -> Option { CATALOG.iter().position(|e| e.name == name).map(|i| Colormap(Map::Catalog(i as u8))) } /// A colormap through `stops`, each a color or a `(place, color)` pair. Colors blend in OKLab, - /// so lightness changes evenly; a color without a place sits evenly between its neighbors, as - /// in a CSS gradient. A categorical map takes one color per category instead. + /// so lightness changes evenly. A color without a place sits evenly between its neighbors, like + /// a CSS gradient. A categorical map takes one color per category. /// /// Errors when `stops` is empty, when places lie outside `0..=1` or out of order, and when a /// categorical map has places or more than 256 colors. @@ -152,7 +152,7 @@ impl Colormap { }))) } - /// The names of the categories, from category 0; none unless named. + /// The names of the categories, from category 0. Empty unless named. pub(crate) fn named_categories(&self) -> &[Arc] { match &self.0 { Map::Catalog(_) => &[], @@ -176,7 +176,7 @@ impl Colormap { } } - /// A human-readable title, such as `"Red / Blue"`; `"Custom"` for a custom colormap. + /// A human-readable title, such as `"Red / Blue"`. `"Custom"` for a custom colormap. pub fn label(&self) -> &str { match &self.0 { Map::Catalog(i) => CATALOG[*i as usize].label, @@ -218,7 +218,7 @@ impl Colormap { Color::rgba(red, green, blue, alpha) } - /// The number of colors the map was made from; for a categorical map, its category count. + /// The number of colors the map was made from. For a categorical map, its category count. pub fn count(&self) -> u32 { match &self.0 { Map::Catalog(i) => CATALOG[*i as usize].count, diff --git a/crates/latkit/src/expr/compile.rs b/crates/latkit/src/expr/compile.rs index 92205a80..b4324d0a 100644 --- a/crates/latkit/src/expr/compile.rs +++ b/crates/latkit/src/expr/compile.rs @@ -72,7 +72,7 @@ pub(crate) struct ColumnUse { pub(crate) step: bool, /// Also read the low part that `f32` loses from an `f64`, for full-precision positions. pub(crate) low: bool, - /// Read at every frame in view, as a line tracing the field does. + /// Read at every frame in view, like a line that traces the field. pub(crate) trace: bool, } @@ -98,14 +98,14 @@ impl ColumnUse { } /// One end of each row of a mark: the vertex table it references, and the program column of the -/// reference field, or `NONE` when the end is the row's own vertex, as for a pole. +/// reference field. The column is `NONE` when the end is the row's own vertex, as for a pole. #[derive(Clone, PartialEq, Debug)] pub(crate) struct EndUse { pub(crate) table: Name, pub(crate) column: u32, } -/// A constant that holds `text`'s dictionary code in `column`; the view fills it in. +/// A constant that holds `text`'s dictionary code in `column`. The view fills it in. #[derive(Clone, PartialEq, Debug)] pub(crate) struct TextUse { pub(crate) constant: u32, @@ -128,7 +128,7 @@ pub(crate) struct Program { pub(crate) fitted: Vec, pub(crate) columns: Vec, pub(crate) texts: Vec, - /// Each colormap op's index in `code`, with its colormap; the view writes the op's texture + /// Each colormap op's index in `code`, with its colormap. The view writes the op's texture /// layer. pub(crate) colormaps: Vec<(u32, Colormap)>, /// Each row's ends, in order. The end pass writes the GPU row holding end `i` into kind word @@ -142,7 +142,7 @@ pub(crate) struct Program { #[derive(Clone, Copy, Default, PartialEq, Eq, Debug)] pub(crate) struct Reads { pub(crate) deps: Deps, - /// Bit `i` for column `i`; bit 63 also covers every column after it. + /// Bit `i` for column `i`. Bit 63 also covers every column after it. pub(crate) columns: u64, } @@ -340,15 +340,15 @@ impl<'a> Compiler<'a> { Ok(()) } - /// Adds the next end of each row: the row's own vertex, as placed by the vertex mark that draws - /// `table`, the mark's own table. A pole stands on it. + /// Adds the next end of each row: the row's own vertex, where the vertex mark of `table` (the + /// mark's own table) places it. A pole stands on it. pub(crate) fn own_end(&mut self, table: &Name) { self.program.ends.push(EndUse { table: table.clone(), column: wgsl::NONE }); } - /// Compiles channel `name`, storing at `word` the dictionary code of text field `expr`, or of a - /// `when` whose values are each that field or missing. A missing code draws no label. Returns - /// the program column whose dictionary decodes the codes. + /// Compiles channel `name`, which stores at `word` the dictionary code of text field `expr`. + /// `expr` can also be a `when` that returns that field or missing. A missing code draws no + /// label. Returns the program column whose dictionary decodes the codes. pub(crate) fn text(&mut self, name: &str, expr: &Expr, word: u32) -> Result { let Some(f) = text_field(expr, None).map_err(|e| e.at(name))? else { return Err(invalid("a label's text needs a text field").at(name)); @@ -357,7 +357,7 @@ impl<'a> Compiler<'a> { return Err(invalid("not a text field").at(&f.name).at(name)); } let start = self.begin(); - // The values compile as any conditional's do, a missing one as the missing code. + // The values compile like any conditional's, and a missing value as the missing code. self.expect(expr, Ty::Text).map_err(|e| e.at(name))?; let column = self.field_column(f, Words::Codes, false); self.op(wgsl::OP_STORE_CODE, word, 0, 0, 1, 0)?; @@ -662,7 +662,7 @@ impl<'a> Compiler<'a> { self.program.domains.push(Reads::default()); self.program.fitted.push(input.clone()); self.program.constants.extend([1, 0, 0, index]); - // Compile the input again as a reduction; `finish` places it after the channels. + // Compile the input again as a reduction. `finish` places it after the channels. let saved = (std::mem::take(&mut self.program.code), self.depth, self.deps, self.columns_read); self.depth = 0; self.deps = Deps::default(); @@ -764,9 +764,9 @@ fn binary_name(op: Binary) -> &'static str { } } -/// The one text field a label's text shows: `expr` itself, or the field each value of a `when` -/// gives, where every value is that field (the same name and table) or missing. `seen` is the field -/// found so far; `None` when every value is missing. +/// The one text field a label's text shows. It is `expr` itself, or the field a `when` returns when +/// every value is that field (same name and table) or missing. `seen` is the field found so far. +/// Returns `None` when every value is missing. fn text_field<'e>(expr: &'e Expr, seen: Option<&'e FieldRef>) -> Result> { match &*expr.0 { Node::Missing => Ok(seen), diff --git a/crates/latkit/src/expr/cpu.rs b/crates/latkit/src/expr/cpu.rs index b29f1527..602104ac 100644 --- a/crates/latkit/src/expr/cpu.rs +++ b/crates/latkit/src/expr/cpu.rs @@ -1,4 +1,4 @@ -//! Evaluates expressions on the CPU, for work done from the data alone: a diagram's sheet +//! Evaluates expressions on the CPU, for work that needs only data: a diagram's sheet //! (positions, sizes, and block label texts), layouts, row priorities, fitted domains of paged //! tables, and queries (filters, sort keys, and selections). The GPU evaluates the same expressions //! for drawing. Here they read only fields, with sampled fields read at one axis coordinate. @@ -16,8 +16,8 @@ use crate::data::{Field, Geometry, Snapshot, Table, Value}; pub(crate) struct Rows<'a> { pub(crate) table: &'a Table, pub(crate) all: &'a [Snapshot], - /// The axis coordinate at which sampled fields are read; infinity, their last frame, by - /// default. + /// The axis coordinate at which sampled fields are read. Defaults to infinity, which reads + /// their last frame. at: f64, /// Fitted domains already computed, keyed by their input node. fitted: RefCell>, @@ -41,15 +41,15 @@ impl<'a> Rows<'a> { self } - /// `expr` evaluated for `row`; `None` where the result is missing or not a finite number. + /// `expr` evaluated for `row`. `None` where the result is missing or not a finite number. pub(crate) fn number(&self, expr: &Expr, row: usize) -> Option { let v = self.eval(expr, row); v.is_finite().then_some(v) } /// `expr` evaluated for every row and converted by `make`, with NaN for missing values. A - /// constant fills every row, and a plain field of the table is read page by page; anything else - /// is evaluated for each row individually. + /// constant fills every row, and a plain field of the table is read page by page. Anything else + /// is evaluated row by row. pub(crate) fn numbers(&self, expr: &Expr, mut make: impl FnMut(f64) -> T) -> Vec { let n = self.table.len(); let mut out = Vec::with_capacity(n); @@ -86,8 +86,8 @@ impl<'a> Rows<'a> { out } - /// The text `expr` gives `row`: a text field's value, or the value a `when` chooses; `None` - /// where it is missing. + /// The text `expr` gives `row`: a text field's value, or the value a `when` picks. `None` where + /// it is missing. pub(crate) fn text(&self, expr: &Expr, row: usize) -> Option> { match &*expr.0 { Node::Field(f) => match self.value(f, row)? { @@ -161,7 +161,7 @@ impl<'a> Rows<'a> { } } - /// The extent of `input` over every row, as a fitted domain computes it; `[0, 1]` when no row + /// The extent of `input` over every row, as a fitted domain computes it. `[0, 1]` when no row /// has a value. fn fit(&self, input: &Expr) -> [f64; 2] { let key = Arc::as_ptr(&input.0); diff --git a/crates/latkit/src/expr/form.rs b/crates/latkit/src/expr/form.rs index 792eef39..18bbf588 100644 --- a/crates/latkit/src/expr/form.rs +++ b/crates/latkit/src/expr/form.rs @@ -1,7 +1,7 @@ -//! JSON forms of expressions, sizes, and colormaps. Every value reads by one rule: a number or +//! JSON forms of expressions, sizes, and colormaps. Every value follows one rule: a number or //! boolean is a constant, `null` is a missing value, a string starting with `$` reads the field it //! names, any other string is a CSS color, and an object is one node whose key names its kind. A -//! call's one key names its operation and holds its arguments: +//! call has one key, which names its operation and holds its arguments: //! //! | Form | Builder | //! | ------------------------------------------------------------ | ---------------------------------- | @@ -18,13 +18,13 @@ //! | `{ "px": a }` (sizes only) | `px(a)` | //! | `{ "units": a, "atLeast": 2, "atMost": 20 }` (sizes only) | `units(a).at_least(..)` | //! -//! `neg`, `not`, `abs`, `sqrt`, `sin`, `cos`, `floor`, `ln`, and `exp` take one argument; `add`, +//! `neg`, `not`, `abs`, `sqrt`, `sin`, `cos`, `floor`, `ln`, and `exp` take one argument. `add`, //! `sub`, `mul`, `div`, `rem`, `pow`, `min`, `max`, `lt`, `le`, `gt`, `ge`, `eq`, `ne`, `and`, and -//! `or` take two; `is` and `contains` take a value and a plain text, which is neither a field nor a +//! `or` take two. `is` and `contains` take a value and a plain text, which is neither a field nor a //! color. A field whose name starts with `$` is written `{ "field": "$x" }`. An object may have -//! only its kind's keys; a key of another kind is an error. A colormap is a catalog name or -//! `{ "kind": "sequential", "colors": ["black", [0.2, "red"], "white"] }`, where a `[place, color]` -//! pair places a color and `categories` names a categorical map's colors. +//! only its kind's keys, and a key of another kind is an error. A colormap is a catalog name or +//! `{ "kind": "sequential", "colors": ["black", [0.2, "red"], "white"] }`. A `[place, color]` pair +//! places a color, and `categories` names a categorical map's colors. use std::borrow::Cow; use std::fmt; @@ -133,7 +133,7 @@ macro_rules! form { }; ($($vis:vis $key:ident $json:literal: $ty:ty $(as $how:ident)?),+ $(,)?) => { /// Every key that an expression object, a size, or label options can have, and the call - /// that a key naming an operation makes; each kind accepts only its own keys. + /// made by a key that names an operation. Each kind accepts only its own keys. #[derive(Default)] pub(crate) struct Form { $($vis $key: Option<$ty>,)+ @@ -154,8 +154,8 @@ macro_rules! form { held } - /// Reads `key`'s value from `map`. Any other key names an operation, whose call it - /// makes. + /// Reads `key`'s value from `map`. Any other key names an operation, and this makes + /// its call. fn read<'de, A: MapAccess<'de>>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> { match key { $($json => { @@ -203,8 +203,8 @@ impl Form { } } - /// Reads the arguments of the operation `key` names from `map`, and makes the call: values, or - /// for `is` and `contains`, a value and a text. + /// Reads from `map` the arguments of the operation `key` names, and makes the call. The + /// arguments are values, or a value and a text for `is` and `contains`. fn read_call<'de, A: MapAccess<'de>>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> { let Some(&(name, operation, _)) = OPERATIONS.iter().find(|(name, ..)| *name == key) else { return Err(de::Error::custom(format!("{key:?} names no option or operation"))); @@ -339,8 +339,9 @@ const OPERATIONS: [(&str, Operation, &str); 27] = [ ), ]; -/// Declares each kind of expression object once: a struct for its JSON schema, never built because -/// reading goes through `Form`, and the keys an object of that kind may have, its own key first. +/// Declares each kind of expression object once. Each kind gets a struct for its JSON schema (never +/// built, since reading goes through `Form`) and the list of keys an object of that kind may have, +/// its own key first. macro_rules! kinds { ($($(#[$meta:meta])* $node:ident { $($(#[$attr:meta])* $key:ident: $ty:ty),+ $(,)? })+) => { $( @@ -364,8 +365,8 @@ macro_rules! kinds { } kinds! { - /// A field's value in each row: from the mark's own table, or with `from`, from another - /// dataset's table joined by row id. `"$name"` is the short form of `{ "field": "name" }`. + /// A field's value in each row, from the mark's own table. With `from`, it reads another + /// dataset's table, joined by row id. `"$name"` is the short form of `{ "field": "name" }`. FieldNode { /// The field's name. field: Name, @@ -374,7 +375,7 @@ kinds! { #[schemars(extend("tsType" = "TableInput"))] from: Option<(Name, Name)>, /// For a sampled field, reads the last frame at or before the shown coordinate instead of - /// interpolating. Default `false`. + /// interpolating. Defaults to `false`. #[serde(default)] step: bool, } @@ -398,7 +399,7 @@ kinds! { scale: [f64; 2], /// The value mapped. value: Expr, - /// The values mapped to the range's ends; the extent of the values shown when unset. + /// The values mapped to the range's ends. Defaults to the extent of the values shown. #[serde(default)] #[schemars(extend("tsType" = "[low: number, high: number]"))] domain: Option<[f64; 2]>, @@ -409,7 +410,7 @@ kinds! { colormap: Colormap, /// The value mapped. A categorical map takes each value's integer part as its category. value: Expr, - /// The values mapped to the colormap's ends; the extent of the values shown when unset. A + /// The values mapped to the colormap's ends. Defaults to the extent of the values shown. A /// categorical map ignores it. #[serde(default)] #[schemars(extend("tsType" = "[low: number, high: number]"))] @@ -426,8 +427,8 @@ struct Px { px: Expr, } -/// A size in position units (degrees on the earth), regardless of the space's unit, so it scales -/// as the view zooms. `atLeast` and `atMost` bound it in CSS pixels. +/// A size in position units (degrees on the earth), regardless of the space's unit. It scales with +/// the zoom. `atLeast` and `atMost` bound it in CSS pixels. #[derive(JsonSchema)] #[expect(dead_code, reason = "used for its schema only; `Form` reads sizes")] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -445,7 +446,7 @@ struct Units { at_most: Option, } -/// An expression's schema is `ExprNode`; its TypeScript type also accepts the builders. +/// An expression's schema is `ExprNode`. Its TypeScript type also accepts the builders. impl JsonSchema for Expr { fn schema_name() -> Cow<'static, str> { "Expr".into() @@ -472,17 +473,17 @@ impl JsonSchema for ExprNode { json_schema!({ "type": "boolean" }), json_schema!({ "type": "null", - "description": "A missing value. An option that evaluates to missing uses the theme's value.", + "description": "A missing value. An option that is missing uses the theme's value.", }), json_schema!({ "type": "string", - "description": "A field's value in each row, or a color: `\"$kv\"` reads field `kv`, and any other string is a CSS color, such as `#ffb000` or `rebeccapurple`. A field whose name starts with `$` is written `{ \"field\": \"$x\" }`.", + "description": "A field or a color. `\"$kv\"` reads field `kv`. Any other string is a CSS color, such as `#ffb000` or `rebeccapurple`. Write a field named `$x` as `{ \"field\": \"$x\" }`.", }), ]; any.extend(kinds(generator)); any.push(generator.subschema_for::()); json_schema!({ - "description": "A value for each row. A number or boolean is a constant, `null` is a missing value, a string starting with `$` reads the field it names, any other string is a CSS color, and an object is a node that computes a value.", + "description": "A value for each row. A number or boolean is a constant and `null` is missing. A string starting with `$` reads that field, and any other string is a CSS color. An object computes a value.", "anyOf": any, }) } @@ -526,7 +527,7 @@ impl JsonSchema for CallNode { }) .collect(); json_schema!({ - "description": "An operation on values: an object whose one key names the operation and holds its arguments, such as `{ \"add\": [\"$a\", 1] }`. `is` and `contains` take a plain text as their second argument, which is neither a field nor a color.", + "description": "An operation, written as an object with one key: the operation's name, holding its arguments. For example `{ \"add\": [\"$a\", 1] }`. The second argument of `is` and `contains` is plain text, not a field or a color.", "anyOf": calls, }) } @@ -560,7 +561,7 @@ impl JsonSchema for Size { let px = generator.subschema_for::(); let units = generator.subschema_for::(); json_schema!({ - "description": "A size: a value in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in CSS pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the view zooms in.", + "description": "A size: a value in CSS pixels, or units in a diagram. `{ px }` is always CSS pixels. `{ units }` is position units (degrees on the earth), which grow as the view zooms in.", "anyOf": [expr, px, units], }) } @@ -610,8 +611,8 @@ impl JsonSchema for Color { struct CustomColormap { /// How the colors map values. kind: ColormapKind, - /// The colors, blended in OKLab. Each is a color, spaced evenly between its neighbors as in a - /// CSS gradient, or a `[place, color]` pair with a place from 0 to 1, in ascending order. A + /// The colors, blended in OKLab. Each is a color, spaced evenly between its neighbors like a + /// CSS gradient, or a `[place, color]` pair with a place from 0 to 1. Places must ascend. A /// categorical map takes one color per category, without places, at most 256. colors: Vec, /// The category names in category order, shown in legends. Categorical maps only. @@ -661,7 +662,7 @@ impl JsonSchema for Colormap { let names: Vec<&str> = Colormap::catalog().filter_map(|map| map.name()).collect(); generator.definitions_mut().insert( "ColormapName".into(), - serde_json::json!({ "description": "A colormap of the catalog.", "type": "string", "enum": names }), + serde_json::json!({ "description": "A colormap from the catalog.", "type": "string", "enum": names }), ); let colors = generator.subschema_for::(); json_schema!({ diff --git a/crates/latkit/src/expr/mod.rs b/crates/latkit/src/expr/mod.rs index 09181d29..b6eac5a7 100644 --- a/crates/latkit/src/expr/mod.rs +++ b/crates/latkit/src/expr/mod.rs @@ -21,7 +21,7 @@ use crate::Name; /// A per-row value of a mark: a constant, a color, a field, view state, or arithmetic on them. /// Color options take an expression that computes a color: a [`Color`], a value through a -/// colormap, or a conditional choosing between colors. +/// colormap, or a conditional that picks between colors. /// /// ``` /// # use latkit::{field, when, selected, Colormap, Color}; @@ -76,7 +76,7 @@ pub(crate) enum State { Zoom, } -/// An operation on one value. `sin` and `cos` take radians; `not` treats values above 0.5 as +/// An operation on one value. `sin` and `cos` take radians. `not` treats values above 0.5 as /// true. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) enum Unary { @@ -162,24 +162,24 @@ pub fn pointer() -> Expr { node(Node::State(State::Pointer)) } -/// Seconds since the view was created; a mark that reads it redraws every frame. +/// Seconds since the view was created. A mark that reads it redraws every frame. pub fn elapsed() -> Expr { node(Node::State(State::Elapsed)) } -/// The camera's zoom: the web mercator zoom level on the earth, log₂ of CSS pixels per unit -/// elsewhere. Options that read it update as the camera zooms, as levels of detail do. +/// The camera's zoom: the web mercator zoom level on the earth, and log₂ of CSS pixels per unit +/// elsewhere. Options that read it update as the camera zooms, like levels of detail. pub fn zoom() -> Expr { node(Node::State(State::Zoom)) } -/// A size in position units (degrees on the earth), regardless of the space's unit. It scales as -/// the view zooms; [`Size::at_least`] and [`Size::at_most`] bound it in CSS pixels. +/// A size in position units (degrees on the earth), regardless of the space's unit. It scales with +/// the zoom. [`Size::at_least`] and [`Size::at_most`] bound it in CSS pixels. pub fn units(size: impl Into) -> Size { Size { expr: size.into(), unit: Some(Unit::Units), px: Size::ANY } } -/// A size in CSS pixels, regardless of the space's unit, so it keeps its on-screen size as the view +/// A size in CSS pixels, regardless of the space's unit. It keeps its size on screen as the view /// zooms. In a diagram, this keeps a stroke's width constant on screen. pub fn px(size: impl Into) -> Size { Size { expr: size.into(), unit: Some(Unit::Px), px: Size::ANY } @@ -270,7 +270,7 @@ impl Expr { } /// Whether a text field contains `text`, ignoring case. In a mark, the field must belong to the - /// mark's own table; the view recomputes matches on the CPU when the field changes. + /// mark's own table. The view recomputes matches on the CPU when the field changes. #[must_use] pub fn contains(self, text: &str) -> Expr { node(Node::Contains(self, text.into())) @@ -290,14 +290,15 @@ impl Expr { self.map_fields(&|f| FieldRef { step: true, ..f.clone() }) } - /// The value mapped through `colormap`. The domain fits the values shown unless set with - /// [`Mapped::domain`]; a categorical map ignores it and bins values by their integer part. + /// The value mapped through `colormap`. The domain defaults to the extent of the values shown. + /// Set it with [`Mapped::domain`]. A categorical map ignores the domain and bins values by + /// their integer part. pub fn colormap(self, colormap: impl Into) -> Mapped { Mapped(node(Node::Colormap { input: self, colormap: colormap.into(), domain: Domain::Fit })) } - /// The value mapped linearly onto `range`, clamped. The domain fits the values shown unless set - /// with [`Mapped::domain`]. + /// The value mapped linearly onto `range`, clamped. The domain defaults to the extent of the + /// values shown. Set it with [`Mapped::domain`]. pub fn scale(self, range: RangeInclusive) -> Mapped { let range = [*range.start(), *range.end()]; Mapped(node(Node::Scale { input: self, domain: Domain::Fit, range })) @@ -381,7 +382,7 @@ impl Expr { } } - /// The first colormap the expression maps through, with its domain when fixed; a legend shows + /// The first colormap the expression maps through, with its domain when fixed. A legend shows /// it. A conditional's `otherwise` is searched before its cases, since it is the general case. pub(crate) fn colormap_of(&self) -> Option<(&Colormap, Option<[f64; 2]>)> { match &*self.0 { @@ -397,7 +398,7 @@ impl Expr { } } -/// A value mapped through a colormap or onto a range, whose domain can be set. +/// A value mapped through a colormap or onto a range. Its domain can be set. #[derive(Clone, PartialEq, Debug)] pub struct Mapped(Expr); @@ -457,13 +458,13 @@ impl Then { } } -/// A size. A bare value is in the space's unit: units in a diagram, CSS pixels elsewhere. [`px`] -/// makes it CSS pixels and [`units`] makes it position units, which scale as the view zooms. +/// A size. A bare value is in the space's unit: CSS pixels, or units in a diagram. [`px`] makes it +/// CSS pixels, and [`units`] makes it position units, which scale with the zoom. /// [`Size::at_least`] and [`Size::at_most`] bound it in CSS pixels. #[derive(Clone, PartialEq, Debug)] pub struct Size { pub(crate) expr: Expr, - /// Its explicit unit; `None` for the space's unit. + /// Its explicit unit. `None` means the space's unit. pub(crate) unit: Option, /// The CSS pixel bounds it stays within at any zoom. pub(crate) px: [f32; 2], @@ -488,9 +489,10 @@ impl Size { self.unit.map_or(space_units, |unit| unit == Unit::Units) } - /// Sets a minimum on-screen size of `px` CSS pixels, however far the view zooms out: - /// `units(field("r")).at_least(2.0)` scales with the map but never shrinks below 2 pixels. A - /// marker is bounded on its larger side and keeps its proportions. Negative values count as 0. + /// Sets a minimum size of `px` CSS pixels on screen, however far the view zooms out. For + /// example, `units(field("r")).at_least(2.0)` scales with the map but never shrinks below 2 + /// pixels. A marker is bounded on its larger side and keeps its proportions. Negative values + /// count as 0. #[must_use] pub fn at_least(mut self, px: f32) -> Size { self.px[0] = px.max(0.0); diff --git a/crates/latkit/src/expr/ramp.rs b/crates/latkit/src/expr/ramp.rs index 26f77f44..d7272c8a 100644 --- a/crates/latkit/src/expr/ramp.rs +++ b/crates/latkit/src/expr/ramp.rs @@ -1,5 +1,5 @@ //! Builds a colormap's 256 texels from its stops, both for the catalog baked at build time and for -//! maps made at run time. Colors blend in OKLab so lightness changes evenly; cyclic ramps wrap, and +//! maps made at run time. Colors blend in OKLab so lightness changes evenly. Cyclic ramps wrap, and //! categorical ramps repeat their stops texel by texel. The file uses nothing from the crate, so //! the build script includes it unchanged. @@ -16,8 +16,8 @@ pub(crate) enum Spread { /// The 256 texels through `stops`: each an optional place from 0 to 1, ascending, and a /// straight-alpha sRGB color with channels from 0 to 1. A stop without a place sits evenly between -/// its neighbors' places, as in a CSS gradient. An unplaced first stop sits at 0. An unplaced last -/// stop sits at 1, except in a cyclic ramp, whose unplaced stops spread evenly up to one turn past +/// its neighbors' places, like a CSS gradient. An unplaced first stop sits at 0. An unplaced last +/// stop sits at 1, except in a cyclic ramp. There, unplaced stops spread evenly up to one turn past /// the first stop. A categorical ramp's texel `i` takes stop `i` modulo the stop count. pub(crate) fn texels(spread: Spread, stops: &[(Option, [f32; 4])]) -> [[u8; 4]; 256] { assert!(!stops.is_empty(), "a ramp needs at least one stop"); diff --git a/crates/latkit/src/geo/earcut.rs b/crates/latkit/src/geo/earcut.rs index fe3cf45c..59f8fdec 100644 --- a/crates/latkit/src/geo/earcut.rs +++ b/crates/latkit/src/geo/earcut.rs @@ -1,9 +1,9 @@ //! Polygon triangulation by ear clipping, ported from Mapbox's earcut 3.2. //! -//! Each hole is joined to the outer ring by a bridge. Ears are clipped from a doubly linked ring; -//! large polygons use a z-order index to find points that block an ear. When no ear can be clipped, -//! the ring is filtered while that removes points, then cured of local self-intersections once, -//! then split in two. +//! Each hole is joined to the outer ring by a bridge. Ears are clipped from a doubly linked ring. +//! Large polygons use a z-order index to find points that block an ear. When no ear can be +//! clipped, the ring is filtered while that removes points, then cured of local self-intersections +//! once, then split in two. /// The null node index. const NIL: u32 = u32::MAX; @@ -32,7 +32,7 @@ struct Rings<'a> { filtered: bool, } -/// Triangulates a polygon. `points` holds the outer ring, then each hole; `holes` lists the index +/// Triangulates a polygon. `points` holds the outer ring, then each hole. `holes` lists the index /// where each hole starts. Returns triangles as index triples into `points`. pub(crate) fn earcut(points: &[[f64; 2]], holes: &[usize]) -> Vec<[u32; 3]> { let nodes = Vec::with_capacity(points.len() * 3 / 2); diff --git a/crates/latkit/src/geo/mercator.rs b/crates/latkit/src/geo/mercator.rs index da3fa33d..7b33c59f 100644 --- a/crates/latkit/src/geo/mercator.rs +++ b/crates/latkit/src/geo/mercator.rs @@ -12,7 +12,7 @@ pub(crate) fn y(latitude: f64) -> f64 { (FRAC_PI_4 + lat / 2.0).tan().ln() } -/// The latitude in degrees at projected y `y`; the inverse of [`y`]. +/// The latitude in degrees at projected y `y`, the inverse of [`y`]. pub(crate) fn latitude(y: f64) -> f64 { y.sinh().atan().to_degrees() } @@ -22,7 +22,7 @@ pub(crate) fn down(latitude: f64) -> f64 { 0.5 - y(latitude) / TAU } -/// The latitude `t` of the way down the square; the inverse of [`down`]. +/// The latitude `t` of the way down the square, the inverse of [`down`]. pub(crate) fn latitude_down(t: f64) -> f64 { latitude(PI * (1.0 - 2.0 * t)) } diff --git a/crates/latkit/src/geo/mvt.rs b/crates/latkit/src/geo/mvt.rs index c2ea7ed6..250f02e4 100644 --- a/crates/latkit/src/geo/mvt.rs +++ b/crates/latkit/src/geo/mvt.rs @@ -35,8 +35,8 @@ impl Shape { } } -/// One feature: its shape, and its properties as pairs of key index and value: a text, number, or -/// boolean. +/// One feature: its shape, and its properties as pairs of key index and value. A value is a text, +/// number, or boolean. #[derive(Clone, Debug)] pub(crate) struct Feature { pub(crate) shape: Shape, diff --git a/crates/latkit/src/geo/pmtiles.rs b/crates/latkit/src/geo/pmtiles.rs index 3c7f16ff..806ba894 100644 --- a/crates/latkit/src/geo/pmtiles.rs +++ b/crates/latkit/src/geo/pmtiles.rs @@ -32,8 +32,8 @@ pub(crate) fn is_archive(bytes: &[u8]) -> bool { bytes.starts_with(MAGIC) } -/// Parses the header at the start of `bytes`. Errors unless it is a version 3 archive whose -/// directories and tiles are uncompressed or gzip-compressed. +/// Parses the header at the start of `bytes`. Errors if it is not a version 3 archive with +/// uncompressed or gzip-compressed directories and tiles. pub(crate) fn header(bytes: &[u8]) -> Result { if !is_archive(bytes) || bytes.len() < HEADER { return Err("these bytes are not a PMTiles archive".into()); diff --git a/crates/latkit/src/geo/tiles.rs b/crates/latkit/src/geo/tiles.rs index a328c653..f24b0b43 100644 --- a/crates/latkit/src/geo/tiles.rs +++ b/crates/latkit/src/geo/tiles.rs @@ -75,8 +75,8 @@ pub(crate) struct Decoded { pub(crate) shapes: Vec, } -/// Most rows one tile adds to a table; further rows are dropped. Row ids are the tile's id times -/// this plus the row's index, so ids from different tiles never collide. +/// The most rows one tile adds to a table. Further rows are dropped. Row ids are the tile's id +/// times this plus the row's index, so ids from different tiles never collide. pub(crate) const FEATURES: u64 = 1 << 20; /// Name of the field holding a tile row's geometry. diff --git a/crates/latkit/src/gpu/arena.rs b/crates/latkit/src/gpu/arena.rs index ad728425..be75ff49 100644 --- a/crates/latkit/src/gpu/arena.rs +++ b/crates/latkit/src/gpu/arena.rs @@ -1,25 +1,26 @@ -//! GPU memory for pages: one pool of 128 Ki-word chunks in a single storage buffer, which doubles -//! as it fills, up to the budget. It holds uploaded data pages and the per-row word pages each -//! view's marks keep, which the GPU writes. +//! GPU memory for pages: one pool of 128 Ki-word chunks in a single storage buffer. The buffer +//! doubles as it fills, up to the budget. It holds uploaded data pages, and the per-row word pages +//! that each view's marks keep and the GPU writes. //! -//! Each chunk is carved into blocks of one power-of-two size, and no page is larger than a chunk; a -//! geometry's points are split into pieces of one row group each. So space any page frees can serve -//! any other: a chunk whose blocks are all freed can be carved again for another size. Few sizes in -//! few chunks waste little space in partly used chunks, which matters with a small budget. +//! Each chunk is carved into blocks of one power-of-two size. No page is larger than a chunk, and a +//! geometry's points are split into pieces of one row group each. So space that any page frees can +//! serve any other: once all of a chunk's blocks are free, it can be carved again for another size. +//! With few sizes in few chunks, partly used chunks waste little space, which matters with a small +//! budget. //! //! A data page is keyed by its id and the words a program reads from it. Geometry pieces are keyed //! by content, so a rewritten page finds the pieces of its unchanged rows already resident. A view -//! reserves all the pages its next draw reads at once, or none: resident pages are pinned, and +//! reserves all the pages its next draw reads at once, or none. Resident pages are pinned, and //! missing ones get blocks, evicting the least recently used unpinned pages. So nothing is partly -//! pinned and no page waits for room. Page tables pin the pages they reference, so a page in use is -//! never evicted, and unpinned pages stay cached. Uploads stream into the reserved blocks within a -//! per-frame byte budget. Views plan their drawing to fit the pool, so a reservation rarely finds -//! no room; when it does, it waits for the next frame, which grows the pool by the shortfall. +//! pinned and no page waits for space. Page tables pin the pages they reference, so a page in use +//! is never evicted, and unpinned pages stay cached. Uploads stream into the reserved blocks within +//! a per-frame byte budget. Views plan their drawing to fit the pool, so a reservation rarely finds +//! no space. When it does, it waits for the next frame, which grows the pool by the shortfall. //! //! A per-row word page is a block of its own. It is cleared when handed out if it is read before //! anything writes it, and freed only once the frame that released it is done. The pool grows only -//! before a frame records writes to the pages, since growing copies the buffer; a block that finds -//! no room after that waits for the next frame. +//! before a frame records writes to the pages, since growing copies the buffer. A block that finds +//! no space after that waits for the next frame. use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque}; use std::hash::{Hash, Hasher}; @@ -117,7 +118,7 @@ pub(crate) struct Ask { pub(crate) enum Content { /// Static values, converted to words as `words` reads them. Values { values: Values, words: Words, low: bool }, - /// A page of samples. A page an append opened grows, and only its new tail uploads. + /// A page of samples. A page that an append opened grows, and only its new tail uploads. Frames(FramePage), /// A page of axis positions for a level's entries. It grows like a page of samples. Axis(AxisPage), @@ -211,9 +212,10 @@ pub(crate) fn page_words(values: &Values, words: Words, low: bool) -> Vec { } } -/// A geometry page as `lib/read.wesl` reads it. The row table has two words per row: the index of -/// the piece where its points begin, shifted left by `POINT_BITS` over its first point's index in -/// that piece; and its point count. Then come the pieces, each point an x and a y word. +/// A geometry page as `lib/read.wesl` reads it. The row table has two words per row. The first is +/// the index of the piece where the row's points begin, shifted left by `POINT_BITS`, bitwise-or +/// the index of its first point in that piece. The second is its point count. Then come the pieces, +/// with each point stored as an x and a y word. pub(crate) struct PathPage { pub(crate) rows: Vec, pub(crate) pieces: Vec>, @@ -291,7 +293,8 @@ struct Block { } /// A chunk of the pool: free, or carved into blocks of one class with a bit per free block. The -/// chunk does not store its class; `Arena::partial` lists it under its class while it has room. +/// chunk does not store its class. `Arena::partial` lists it under its class while it has a free +/// block. #[derive(Clone, Copy)] struct Chunk { live: u32, @@ -335,7 +338,7 @@ pub(crate) struct Arena { cleared: Vec<(u32, usize)>, freed: Vec<(u32, usize)>, /// Whether this frame recorded writes to the pages, after which the pool keeps its buffer. - /// `short` counts the words of blocks that found no room; the next frame grows the pool by it. + /// `short` counts the words of blocks that found no space. The next frame grows the pool by it. writing: bool, short: u64, } @@ -383,8 +386,8 @@ impl Arena { /// Reserves every page in `asks`, or none. Pins the resident ones and gives the missing ones /// blocks, evicting least recently used unpinned pages but never these. Queues uploads for new /// pages and for resident pages holding fewer words than asked. Returns `false`, pinning - /// nothing, if a missing page has no content to upload or finds no room; for no room, the next - /// frame grows the pool by the shortfall. + /// nothing, if a missing page has no content to upload or finds no space. When it finds no + /// space, the next frame grows the pool by the shortfall. pub(crate) fn reserve(&mut self, device: &wgpu::Device, queue: &Queue, asks: &mut [Ask]) -> bool { // Pin resident pages first, so placing the rest evicts none of them. let held: Vec = asks.iter().map(|ask| self.blocks.contains_key(&ask.key)).collect(); @@ -432,8 +435,8 @@ impl Arena { } /// Allocates a separate block of `words` for a per-row word page. With `cleared`, the next - /// [`clear`](Arena::clear) zeroes it. `None` if the pool has no room now; the next frame grows - /// it. + /// [`clear`](Arena::clear) zeroes it. Returns `None` if the pool has no space now. The next + /// frame then grows it. pub(crate) fn block(&mut self, device: &wgpu::Device, queue: &Queue, words: u32, cleared: bool) -> Option { let class = class_of(words); let offset = self.allocate(device, queue, class)?; @@ -603,8 +606,8 @@ impl Arena { /// Allocates a block of `class`. Tries, in order: a free block of that class, a newly carved /// free chunk, growing the pool (only before this frame writes the pages), then evicting least - /// recently used unpinned blocks of any size. `None` when every remaining block is pinned or a - /// per-row block; the next frame then grows the pool by the shortfall. + /// recently used unpinned blocks of any size. Returns `None` when every remaining block is + /// pinned or a per-row block. The next frame then grows the pool by the shortfall. fn allocate(&mut self, device: &wgpu::Device, queue: &Queue, class: usize) -> Option { loop { if let Some(&k) = self.partial[class].first() { diff --git a/crates/latkit/src/gpu/atlas.rs b/crates/latkit/src/gpu/atlas.rs index 5fd2c807..29724674 100644 --- a/crates/latkit/src/gpu/atlas.rs +++ b/crates/latkit/src/gpu/atlas.rs @@ -1,14 +1,14 @@ -//! The text atlas. The host rasterizes each run once at `TEXT_BASE`, and the GPU turns it into a -//! distance field reaching `TEXT_SPREAD` texels past the text's edge, packed on shelves of one -//! texture. A distance field draws crisp at any size and also yields halos, so a run is rasterized -//! once regardless of the size it shows at. The GPU builds the fields of every run rasterized in a -//! frame in one batch before the frame draws (`compute/fields.wesl`). +//! The text atlas. The host rasterizes each text run once at `TEXT_BASE`. The GPU turns the raster +//! into a distance field that extends `TEXT_SPREAD` texels past the text's edge, and packs it on a +//! shelf of one texture. A distance field draws crisp at any size and also gives halos, so each run +//! is rasterized once whatever size it shows at. Before a frame draws, the GPU builds the fields of +//! every run rasterized in that frame in one batch (`compute/fields.wesl`). //! -//! When no shelf has room, the least recently used shelf tall enough is cleared for the new run. A -//! run counts as used when it is rasterized and whenever labels in view show it, and a shelf used -//! this frame is never cleared, so visible text is not evicted. When every shelf is in use, the -//! atlas is full for the rest of the frame and takes no more runs; it starts over only when nothing -//! in it was used this frame. Views track evictions to drop stale references. +//! When no shelf has space, the least recently used shelf that is tall enough is cleared for the +//! new run. A run counts as used when it is rasterized and whenever labels in view show it. A shelf +//! used this frame is never cleared, so visible text is not evicted. When every shelf is in use, +//! the atlas is full for the rest of the frame and takes no more runs. It starts over only when +//! nothing in it was used this frame. Views track evictions to drop stale references. use std::collections::HashMap; use std::sync::Arc; @@ -30,7 +30,7 @@ const SIZE: u32 = 4096; const GAP: u32 = 1; /// Texels of field around a run's coverage. const PAD: u32 = TEXT_SPREAD as u32; -/// Most runs whose fields one dispatch builds, one workgroup layer each. +/// The most runs one dispatch builds fields for, one workgroup layer each. const RUNS: usize = 65_535; /// Where `Atlas::hold` put a raster: at its glyphs on a shelf, nowhere because it has nothing to @@ -170,8 +170,8 @@ impl Atlas { placed.map(|(glyphs, _)| glyphs) } - /// Forgets everything the host made: runs and failures. A new host rasterizes texts again in its - /// own fonts. + /// Forgets the runs and failures the host made. A new host rasterizes texts again in its own + /// fonts. pub(crate) fn rehost(&mut self) { self.runs.clear(); self.waiting.clear(); @@ -179,7 +179,7 @@ impl Atlas { self.evictions += 1; } - /// Places `raster`'s field, queued to be built, and returns where. + /// Places `raster`'s field and queues it to be built. Returns where it was placed. fn hold(&mut self, raster: Raster) -> Held { let [w, h] = raster.size; if w == 0 || h == 0 { @@ -313,8 +313,8 @@ impl Atlas { (self.work.buffer.clone(), self.work_group.clone().expect("built above")) } - /// Finds room for `width` by `height` texels, returning the shelf and top-left corner; `None` - /// when every shelf that could hold it was used this frame. + /// Finds space for `width` by `height` texels and returns the shelf and top-left corner. + /// Returns `None` when every shelf that could hold it was used this frame. fn place(&mut self, width: u32, height: u32) -> Option<(usize, [u32; 2])> { let fits = |s: &Shelf| s.height >= height && s.height <= height + height / 2 + 4; let at = self.shelves.iter().position(|s| fits(s) && s.used + width <= SIZE); @@ -326,8 +326,8 @@ impl Atlas { }) }); let at = at.or_else(|| { - // Clear the shelf tall enough that was used least recently, a fitting height first, - // unless it was used this frame. + // Clear the least recently used shelf that is tall enough, preferring a fitting height. + // Shelves used this frame are skipped. let clock = self.clock; let oldest = (0..self.shelves.len()) .filter(|&i| self.shelves[i].height >= height && self.shelves[i].tick < clock) @@ -366,7 +366,7 @@ mod tests { use super::*; /// The exact field of `width` by `height` coverage bytes, computed texel by texel: the least - /// squared distance to inside and to outside over every texel within reach. + /// squared distance to inside and to outside over every texel within `PAD` texels. fn exact(coverage: &[u8], width: usize, height: usize) -> Vec { let pad = i64::from(PAD); let (w, h) = (width as i64 + 2 * pad, height as i64 + 2 * pad); diff --git a/crates/latkit/src/gpu/dictionary.rs b/crates/latkit/src/gpu/dictionary.rs index 5e752a48..66886cb8 100644 --- a/crates/latkit/src/gpu/dictionary.rs +++ b/crates/latkit/src/gpu/dictionary.rs @@ -1,6 +1,6 @@ -//! Text dictionaries: each text field's distinct texts get codes in the order they are first seen. +//! Text dictionaries. Each text field's distinct texts get codes in the order they are first seen. //! Codes are never reassigned, so a page's codes never change and upload once. A page is coded -//! once, and a page written over a coded page reuses its codes and codes only the rows written. +//! once. A page written over a coded page reuses that page's codes and codes only the rows written. use std::collections::HashMap; use std::sync::Arc; @@ -37,13 +37,14 @@ impl Dictionaries { impl Dictionary { /// The codes of `page` as `f32` words, `MISSING` where missing, adding its new texts. A page - /// coded before needs no work; one written over a coded page codes only the rows written. + /// coded before needs no work. A page written over a coded page codes only the rows written. pub(crate) fn codes(&mut self, page: &Page) -> Arc<[u32]> { if let Some(codes) = self.pages.get(&page.id) { return codes.clone(); } let len = page.values.len(); - // The nearest page it was written over whose codes are kept, and the rows written since. + // Find the nearest page this one was written over that still has codes, and the rows + // written since. let mut reused = None; let mut at = page.written.as_deref(); while let Some(written) = at { @@ -72,9 +73,8 @@ impl Dictionary { codes } - /// The codes of `column`'s rows `rows`, in that order, adding new texts: the contents of a - /// packed page of a paged table. Each row costs a lookup, regardless of how its pages were - /// coded. + /// The codes of `column`'s rows `rows`, in that order, adding new texts. This fills a packed + /// page of a paged table. Each row costs one lookup, however its pages were coded. pub(crate) fn rows(&mut self, column: &Column, rows: &[u32]) -> Vec { rows.iter().map(|&r| column.at(r as usize).map_or(MISSING, |(values, i)| self.code(values, i))).collect() } @@ -105,7 +105,8 @@ impl Dictionary { self.texts.get(code as usize) } - /// The code of `text` as an `f32` word, or NaN, which matches no text, if it was never seen. + /// The code of `text` as an `f32` word. Returns NaN, which matches no text, if `text` was never + /// seen. pub(crate) fn code_of(&self, text: &str) -> u32 { self.codes.get(text).map_or(f32::NAN.to_bits(), |&code| (code as f32).to_bits()) } diff --git a/crates/latkit/src/gpu/grown.rs b/crates/latkit/src/gpu/grown.rs index 3c139adb..56ec14c8 100644 --- a/crates/latkit/src/gpu/grown.rs +++ b/crates/latkit/src/gpu/grown.rs @@ -1,7 +1,7 @@ -//! Buffers that grow: when one runs out of room, it is replaced by one half again as large as -//! needed, so a buffer that keeps growing is replaced only a few times. +//! Buffers that grow. A buffer that runs out of space is replaced by one 1.5 times the size needed, +//! so a buffer that keeps growing is replaced only a few times. -/// A buffer replaced by one 1.5 times the size needed when it runs out of room. +/// A buffer that is replaced by one 1.5 times the size needed when it runs out of space. pub(crate) struct Grown { pub(crate) buffer: wgpu::Buffer, label: &'static str, @@ -13,7 +13,7 @@ impl Grown { Grown { buffer: buffer(device, label, usage, 256), label, usage } } - /// Makes room for `bytes`; true when the buffer was replaced and its contents lost. + /// Makes room for `bytes`. Returns true when it replaced the buffer, losing its contents. pub(crate) fn ensure(&mut self, device: &wgpu::Device, bytes: u64) -> bool { if bytes <= self.buffer.size() { return false; @@ -23,8 +23,8 @@ impl Grown { } } -/// A buffer of at least `bytes` (at least 256, a whole number of words) for `usage`, which writes -/// can copy into. +/// Creates a buffer for `usage` that writes can copy into. Its size is `bytes`, rounded up to at +/// least 256 and to whole words. pub(crate) fn buffer(device: &wgpu::Device, label: &str, usage: wgpu::BufferUsages, bytes: u64) -> wgpu::Buffer { device.create_buffer(&wgpu::BufferDescriptor { label: Some(label), diff --git a/crates/latkit/src/gpu/icons.rs b/crates/latkit/src/gpu/icons.rs index 55a8cb93..0c594b44 100644 --- a/crates/latkit/src/gpu/icons.rs +++ b/crates/latkit/src/gpu/icons.rs @@ -1,6 +1,5 @@ -//! Icons on the GPU: each image an icon marker shows is a layer of one texture array, scaled to fit -//! a common size. Markers reference their images, and markers showing the same image share its -//! layer. +//! Icons on the GPU. Each image an icon marker shows is scaled to a common size and stored as one +//! layer of a texture array. Markers showing the same image share its layer. use std::hash::{Hash, Hasher}; use std::sync::Arc; @@ -12,8 +11,8 @@ use crate::gpu::work::Queue; /// Texels across an icon layer. const SIZE: u32 = 64; -/// An image's pixels, the same when shared: markers showing one image share its layer. Holding the -/// pixels keeps their address, which identifies them, from being reused. +/// An image's pixels, compared by address so that markers showing one image share its layer. +/// Holding the pixels keeps the address from being reused. pub(crate) struct Pixels(Arc<[u8]>); impl PartialEq for Pixels { diff --git a/crates/latkit/src/gpu/mod.rs b/crates/latkit/src/gpu/mod.rs index 29c45984..66c67435 100644 --- a/crates/latkit/src/gpu/mod.rs +++ b/crates/latkit/src/gpu/mod.rs @@ -1,5 +1,5 @@ -//! The GPU: one device shared by every view, with the page arena, pipelines, colormaps, and text -//! dictionaries all views draw from. +//! The GPU device that every view shares, and the page arena, pipelines, colormaps, and text +//! dictionaries the views draw from. pub(crate) mod arena; pub(crate) mod atlas; @@ -34,7 +34,7 @@ use crate::{Error, Result}; /// canvas supports it. pub(crate) const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8Unorm; -/// The GPU device that views draw with. Cheap to clone; views of one `Gpu` share uploaded data. +/// The GPU device that views draw with. Cheap to clone. Views of one `Gpu` share uploaded data. #[derive(Clone)] pub struct Gpu(pub(crate) Arc); @@ -105,7 +105,7 @@ pub(crate) struct Inner { pub(crate) budget: Budget, /// Each view's share of the budget, which it plans within. pub(crate) ledger: Ledger, - /// What the current frame, shared by views drawn together, may still upload and compute. + /// What the current frame may still upload and compute. Views drawn together share it. pub(crate) frame: Allowance, } @@ -134,13 +134,13 @@ impl Inner { } } -/// Fraction of the budget that a GPU's views together plan their drawing within. The rest holds the -/// pages a new data version uploads while the old one is still drawn, and cached pages. +/// Fraction of the budget that all of a GPU's views plan their drawing within. The rest holds +/// cached pages, and the pages of a new data version that upload while the old version still draws. const PLANNED: f64 = 0.5; -/// Each view's share of the planned part of the budget, granted max-min fair, so views together -/// never plan past it however many there are. Pages several views draw are resident once, so they -/// count once, split between those views. +/// Each view's share of the planned part of the budget. Shares are max-min fair, so all views +/// together never plan past it, however many there are. A page that several views draw is resident +/// once, so it counts once, split between those views. #[derive(Default)] pub(crate) struct Ledger { /// Each view's request for drawing everything, by view. @@ -197,13 +197,13 @@ impl Ledger { #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Budget { /// GPU memory in bytes for the data all views of the `Gpu` draw: its pages and each view's - /// per-row state. A view draws a table too large for its share as a paged table, drawing only - /// the pages the camera needs, so it always fits. Pages no view draws stay cached in the - /// remaining room, least recently drawn evicted first. Defaults to half the device's largest - /// storage buffer. The page pool grows as data needs it and never shrinks, so set the budget - /// before drawing data. + /// per-row state. A table too large for a view's share draws as a paged table, with only the + /// pages the camera needs, so it always fits. Pages no view draws stay cached in the rest of + /// the budget, and the least recently drawn are evicted first. Defaults to half the device's + /// largest storage buffer. The page pool grows as data needs it and never shrinks, so set the + /// budget before drawing data. pub bytes: u64, - /// Most bytes uploaded per frame; larger data streams in over several frames. Defaults to + /// The most bytes uploaded per frame. Larger data streams in over several frames. Defaults to /// 32 MiB. pub upload_per_frame: u64, } @@ -255,8 +255,8 @@ impl Gpu { } /// Queues, once, the pipelines nearly every scene uses to draw into the page's canvases, so - /// views opened later find them compiled. In a browser this happens as the first view opens, - /// and they compile after that view's draw pipelines finish. A GPU no view uses compiles + /// views opened later find them compiled. In a browser this runs when the first view opens, and + /// these pipelines compile after that view's own draw pipelines. A GPU no view uses compiles /// nothing. #[cfg(all(feature = "web", target_arch = "wasm32"))] pub(crate) fn warm(&self) { @@ -288,7 +288,7 @@ impl Gpu { Ok(Gpu::create(None, None, device, queue)) } - /// A lightweight sink for the application's existing device callbacks; it does not own the GPU. + /// A handle that the application's own device callbacks report to. It does not own the GPU. pub fn device_events(&self) -> DeviceEvents { DeviceEvents { lost: self.0.lost.clone(), errors: self.0.errors.clone() } } @@ -351,8 +351,8 @@ impl Gpu { inner: Mutex::new(inner), } }); - // In a page, set the multisample count now from the screen, which every view and canvas of - // the GPU draws for. + // In a page, set the multisample count now from the screen's pixel ratio. Every view and + // canvas of the GPU draws for that screen. #[cfg(all(feature = "web", target_arch = "wasm32"))] if let Some(window) = web_sys::window() { let _ = shared.samples.set(samples_at(window.device_pixel_ratio() as f32)); @@ -411,7 +411,7 @@ impl Gpu { } /// Gives views the platform's services: text, background jobs, fetching, and video encoding. - /// Texts the last host set are set again by this one. + /// Text the previous host rasterized is rasterized again by this one. pub fn set_host(&self, host: impl Host) { let mut inner = self.0.lock(); inner.host = Some(Arc::new(host)); diff --git a/crates/latkit/src/gpu/packed.rs b/crates/latkit/src/gpu/packed.rs index c54615ad..361e241a 100644 --- a/crates/latkit/src/gpu/packed.rs +++ b/crates/latkit/src/gpu/packed.rs @@ -1,9 +1,9 @@ -//! Packed pages of paged tables. A column of a paged table is packed one page of its draw order at -//! a time, the first time a view draws that page, as the words its marks read. Packed pages are -//! keyed by their source, so views drawing a table the same way share its pages, a page the camera -//! returns to is the same page, and a write repacks only the pages its rows draw in. A table drawn -//! in its own order reads its data's pages directly, except for a reference to a table drawn in -//! another order, whose pages are packed through that order's ranks. +//! Packed pages of paged tables. A paged table's column is packed into the words its marks read, +//! one page of its draw order at a time, the first time a view draws that page. Packed pages are +//! keyed by their source. So views that draw a table the same way share its pages, a page the +//! camera returns to is the same page, and a write repacks only the pages its rows draw in. A table +//! drawn in its own order reads its data's pages directly. The exception is a reference to a table +//! drawn in another order: its pages are packed through that order's ranks. use std::sync::Arc; @@ -47,8 +47,8 @@ struct Pages { pub(crate) struct Packed { counters: Arc, columns: FxHashMap, - /// Reference pages of tables drawn in their own order, packed through the ranks of the order - /// their target table draws in, keyed by page and that order. + /// Reference pages of tables drawn in their own order, packed through the ranks of the target + /// table's draw order. Keyed by page and that order. ranked: FxHashMap<(PageId, u64), (PageId, u64)>, uses: u64, } @@ -69,7 +69,7 @@ impl Packed { } /// The pages of `packing`, packed from the current `source`. A page is kept while none of its - /// rows was written since; `page_of` maps a row to its page. Without a source, pages are kept + /// rows was written since. `page_of` maps a row to its page. Without a source, pages are kept /// as long as `packing` matches. fn pages( &mut self, @@ -164,7 +164,7 @@ impl Packed { } /// The id of reference page `page` of a table drawn in its own order, packed through the ranks - /// of order `order`, which its target draws in. + /// of `order`, the target table's draw order. pub(crate) fn ranked(&mut self, page: PageId, order: u64) -> PageId { self.uses += 1; if self.ranked.len() >= KEPT && !self.ranked.contains_key(&(page, order)) { @@ -192,9 +192,9 @@ pub(crate) fn values(column: &Column, rows: &[u32], words: Words, low: bool) -> out } -/// A page of the target rows `reference` gives for rows `rows`: each target's place in `rank`, its -/// table's draw order, or the row itself when that table is drawn whole. `NONE` where a row -/// references nothing. +/// A page of the target rows that `reference` gives for rows `rows`. Each target is its place in +/// `rank` (its table's draw order), or the row itself when that table is drawn whole. `NONE` where +/// a row references nothing. pub(crate) fn targets(reference: &Reference, rows: &[u32], rank: Option<&Order>) -> Vec { let mut out: Vec = rows .iter() @@ -207,8 +207,8 @@ pub(crate) fn targets(reference: &Reference, rows: &[u32], rank: Option<&Order>) out } -/// The words of reference page `page` of a table drawn in its own order: each target row's place -/// in `rank`, its table's draw order. +/// The words of reference page `page` of a table drawn in its own order. Each word is the target +/// row's place in `rank`, the target table's draw order. pub(crate) fn ranked_page(reference: &Reference, page: usize, rank: &Order) -> Vec { let start = page * PAGE_ROWS as usize; let len = reference.rows.pages().get(page).map_or(0, |p| p.values.len()); diff --git a/crates/latkit/src/gpu/pipelines.rs b/crates/latkit/src/gpu/pipelines.rs index c1d20522..fe2f0843 100644 --- a/crates/latkit/src/gpu/pipelines.rs +++ b/crates/latkit/src/gpu/pipelines.rs @@ -1,15 +1,15 @@ //! Pipelines, cached by [`Variant`]. //! -//! Everything compiles through one queue per GPU ([`Pipelines::ask`]), most urgent first: the -//! pipelines frames draw with, then their text, then those that changes, lit rows, and input need, -//! then pipelines warmed for future views once the queue would otherwise sit idle. +//! Every pipeline compiles through one queue per GPU ([`Pipelines::ask`]), most urgent first: draw +//! pipelines, then text pipelines, then the pipelines that changes, lit rows, and input need. Last +//! come pipelines warmed for future views, once the queue would otherwise sit idle. //! -//! In a browser, draw pipelines start compiling at once, text as the browser's compile workers free -//! up, and the rest a few at a time, all in the background and in parallel. Anything that uses a -//! pipeline waits until its compile finishes, so no frame waits on a compile, and creating the -//! pipeline afterwards finds it compiled. Natively a pipeline is created in process when first -//! used, which is just as cheap. Every pipeline shares the view and mark group layouts; each -//! shader's pass group comes from its WGSL. +//! In a browser, compiles run in the background and in parallel. Draw pipelines start at once, +//! text pipelines as the browser's compile workers free up, and the rest a few at a time. Anything +//! that uses a pipeline waits until its compile finishes. So no frame waits on a compile, and +//! creating the pipeline afterwards finds it compiled. Natively, a pipeline is created in process +//! when first used, which is just as cheap. Every pipeline shares the view and mark group layouts. +//! Each shader's pass group layout comes from its WGSL. use std::collections::HashMap; use std::future::Future; @@ -24,9 +24,9 @@ use super::work::{Count, Counters}; use crate::Error; use crate::view::DEPTH; -/// How soon a requested pipeline is needed; sooner compiles first. `Draw`: pipelines frames draw -/// with. `Text`: their text. `Answer`: pipelines that changes, lit rows, and input need. `Warm`: -/// pipelines warmed for future views. +/// How soon a requested pipeline is needed. Sooner compiles first. `Draw`: pipelines that frames +/// draw with. `Text`: pipelines for the frames' text. `Answer`: pipelines that changes, lit rows, +/// and input need. `Warm`: pipelines warmed for future views. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] pub(crate) enum Urgency { Draw, @@ -35,10 +35,10 @@ pub(crate) enum Urgency { Warm, } -/// How many pipelines a browser compiles in parallel. Text starts only while fewer than this are -/// compiling, or while no draw pipeline is, since starting it sooner would delay the draw -/// pipelines. The rest compile at most half as many at once, so frames drawn meanwhile keep the -/// CPU; each finished compile starts the next. +/// How many pipelines a browser compiles in parallel. Text pipelines start only while fewer than +/// this are compiling, or while no draw pipeline is, since starting them sooner would delay the +/// draw pipelines. The rest compile at most half as many at once, so frames drawn meanwhile keep +/// the CPU. Each finished compile starts the next. const WORKERS: u64 = 8; /// A pending check of a custom marker's shader, resolving to its error if any. It is `Send` @@ -102,8 +102,8 @@ impl Variant { Variant { shader, entry, primitive: 0, path: PATH_ENDS, reads: Reads::ALL, target: None } } - /// The probe program for marks of `primitive`. It answers every query in steps, each dispatch's - /// uniform selecting the step. + /// The probe program for marks of `primitive`. It answers every query in steps, and each + /// dispatch's uniform selects the step. pub(crate) fn probe(shader: Shader, primitive: u32) -> Variant { Variant::compute(shader, "answer").primitive(primitive) } @@ -150,8 +150,8 @@ impl Variant { } } -/// A pipeline requested ahead of use: a variant of a latkit shader, or of a custom marker, whose -/// WGSL is appended to its shader's. +/// A pipeline requested ahead of use: a variant of a latkit shader, or of a custom marker. A custom +/// marker's WGSL is appended to its shader's WGSL. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub(crate) enum Asked { Shader(Variant), @@ -208,7 +208,7 @@ pub(crate) struct Pipelines { render: HashMap, compute: HashMap, custom: HashMap<(Arc, Variant), Custom>, - /// Pipelines views, shots, and the GPU requested ahead, each with its compile state. + /// Pipelines requested ahead by views, shots, and the GPU, each with its compile state. asked: HashMap>, /// Requested pipelines waiting to start compiling: most urgent first, and largest first within /// an urgency, since a browser compiles in request order. @@ -308,10 +308,10 @@ impl Pipelines { } /// Requests each of `needs` at `urgency`, waking `waker` as each compile finishes. Draw - /// pipelines start at once, text as workers free up, and the rest in turn, at most half of - /// `WORKERS` at a time. Requesting a queued pipeline again at a higher urgency moves it up. - /// Each shader module is created now, because a browser parses a module's WGSL on its GPU - /// thread at creation; this way modules that compile later are parsed before anything draws. + /// pipelines start at once, text pipelines as workers free up, and the rest in turn, at most + /// half of `WORKERS` at a time. Requesting a queued pipeline again at a higher urgency moves it + /// up. Each shader module is created now, because a browser parses a module's WGSL on its GPU + /// thread at creation. This way, modules that compile later are parsed before anything draws. pub(crate) fn ask(&mut self, device: &wgpu::Device, needs: &[Asked], urgency: Urgency, waker: &Waker) { self.queue(device, needs, urgency, waker); self.pump(device); @@ -355,8 +355,9 @@ impl Pipelines { } } - /// Starts queued compiles in order: every draw pipeline; text while fewer than `WORKERS` are - /// compiling or no draw pipeline is; the rest while fewer than half of `WORKERS` are compiling. + /// Starts queued compiles in order. Draw pipelines always start. Text pipelines start while + /// fewer than `WORKERS` are compiling or no draw pipeline is. The rest start while fewer than + /// half of `WORKERS` are compiling. pub(crate) fn pump(&mut self, device: &wgpu::Device) { // Warm pipelines wait until the first scene's draw pipelines have finished. self.warming |= self.drew && self.drawing() == 0; @@ -440,7 +441,7 @@ impl Pipelines { drawing.fetch_add(1, Ordering::AcqRel); } wasm_bindgen_futures::spawn_local(async move { - // A descriptor the browser rejects also finishes; creating the pipeline later + // A descriptor the browser rejects also finishes. Creating the pipeline later // reports why, as it would without compiling ahead. let _ = wasm_bindgen_futures::JsFuture::from(promise).await; compiling.fetch_sub(1, Ordering::AcqRel); @@ -496,7 +497,7 @@ impl Pipelines { } } - /// The layout of `shader`'s pass group; shaders with the same bindings share one. + /// The layout of `shader`'s pass group. Shaders with the same bindings share one. pub(crate) fn pass_layout(&mut self, device: &wgpu::Device, shader: Shader) -> wgpu::BindGroupLayout { let entries: Vec<_> = shader.source().pass.iter().map(Binding::layout_entry).collect(); self.pass_layouts @@ -540,8 +541,8 @@ impl Pipelines { }) } - /// The pipeline `variant` of a custom marker's shader draws a marker of `source` with, or - /// `None` once its WGSL failed to compile. + /// The pipeline that draws a custom marker of `source`: `variant`, with `source` appended to + /// its shader. `None` once that WGSL failed to compile. pub(crate) fn custom( &mut self, device: &wgpu::Device, @@ -682,10 +683,10 @@ mod compiled { ("createComputePipeline", "createComputePipelineAsync"), ]; - /// Has the browser compile, in the background, the pipeline `make` creates through wgpu. While - /// `make` runs, the device's creation methods are swapped for their async variants and receive - /// the exact descriptor wgpu builds, so what compiles is exactly what wgpu creates later. - /// `None` if the device is not a browser's or `make` created nothing. + /// Has the browser compile, in the background, the pipeline that `make` creates through wgpu. + /// While `make` runs, the device's creation methods are swapped for their async variants. These + /// receive the exact descriptor wgpu builds, so what compiles is exactly what wgpu creates + /// later. Returns `None` if the device is not a browser's or `make` created nothing. pub(super) fn capture(device: &wgpu::Device, make: impl FnOnce()) -> Option { let raw: &JsValue = device.as_webgpu()?.as_ref(); let twins: Vec = MAKERS diff --git a/crates/latkit/src/gpu/readback.rs b/crates/latkit/src/gpu/readback.rs index 9b7950b2..e4b075c7 100644 --- a/crates/latkit/src/gpu/readback.rs +++ b/crates/latkit/src/gpu/readback.rs @@ -9,9 +9,8 @@ use std::task::Waker; use super::work::{Count, Counters}; use crate::{Error, Result}; -/// A pool of free map buffers, with the counters for readbacks made from it and how many are still -/// mapping. Each pool is created explicitly as counted or uncounted, so none skips counting by -/// default. +/// A pool of free map buffers. It also holds the counters for its readbacks and the number still +/// mapping. Each pool is created as counted or uncounted, so no pool skips counting by default. #[derive(Clone)] pub(crate) struct Readbacks { free: Arc>>, @@ -20,7 +19,7 @@ pub(crate) struct Readbacks { } impl Readbacks { - /// A pool whose readbacks `counters` counts, as the `Gpu`'s pool does. + /// A pool whose readbacks are counted in `counters`, like the `Gpu`'s pool. pub(crate) fn counted(counters: Arc) -> Readbacks { Readbacks { free: Arc::default(), counters: Some(counters), mapping: Arc::default() } } diff --git a/crates/latkit/src/gpu/shaders.rs b/crates/latkit/src/gpu/shaders.rs index f49d5859..7bfb2d43 100644 --- a/crates/latkit/src/gpu/shaders.rs +++ b/crates/latkit/src/gpu/shaders.rs @@ -1,5 +1,5 @@ -//! The GPU contract generated from the WESL shaders by `build/shaders.rs`: constants, `#[repr(C)]` -//! mirrors of shared structs, bind group layouts, and each shader's text. +//! Code that `build/shaders.rs` generates from the WESL shaders: constants, `#[repr(C)]` mirrors of +//! shared structs, bind group layouts, and each shader's text. #![allow(clippy::unreadable_literal)] @@ -41,8 +41,8 @@ impl ShaderSource { out } - /// Bytes of its WGSL, which a browser compiles for each of its pipelines: a rough measure of - /// compile time. + /// The size of its WGSL in bytes, a rough measure of compile time. A browser compiles the WGSL + /// once per pipeline. pub(crate) fn size(&self) -> usize { self.pieces .iter() @@ -116,6 +116,6 @@ impl Binding { include!(concat!(env!("OUT_DIR"), "/shaders.rs")); -// The data, a table's draw order, and a mark's evaluation all use the same page of rows. +// Data, draw order, and mark evaluation all use the same number of rows per page. const _: () = assert!(1 << PAGE_BITS == PAGE_ROWS && PAGE_MASK + 1 == PAGE_ROWS && PAGE_ROWS as usize == crate::data::PAGE_ROWS); diff --git a/crates/latkit/src/gpu/work.rs b/crates/latkit/src/gpu/work.rs index 5f6e1fd0..af0ffba4 100644 --- a/crates/latkit/src/gpu/work.rs +++ b/crates/latkit/src/gpu/work.rs @@ -1,10 +1,10 @@ //! Exact counts of the work a `Gpu` does. Unlike timings, the counts repeat across runs and -//! machines for the same budget, device limits, and frames drawn, so the benchmark gates on them -//! and tests assert them. +//! machines for the same budget, device limits, and frames drawn. The benchmark gates on them and +//! tests assert them. //! -//! Every write to the GPU goes through [`Queue`], which counts it; clippy forbids the raw queue's -//! writes anywhere else. Other counted parts hold the counters themselves: the pipelines, the -//! readback pool, the atlas, and each frame's jobs and evaluations through the `Gpu`. +//! Every write to the GPU goes through [`Queue`], which counts it. Clippy forbids the raw queue's +//! writes anywhere else. The pipelines, the readback pool, and the atlas hold the counters +//! themselves, and views count each frame's jobs and evaluations through the `Gpu`. use std::ops::{Deref, Sub}; use std::sync::Arc; @@ -66,7 +66,7 @@ work! { written: Written, /// Rows whose channels the GPU evaluated. evaluated: Evaluated, - /// Edge and face rows whose endpoints the GPU resolved to their vertices' resident rows: rows + /// Edge and face rows whose endpoints the GPU resolved to resident vertex rows. These are rows /// that changed, plus every row of a page that references a vertex now resident elsewhere. resolved: Resolved, /// Bytes of per-row word pages the GPU cleared: pages read before anything writes them, and the @@ -111,9 +111,9 @@ impl Queue { self.raw.write_buffer(buffer, offset, bytes); } - /// Writes `bytes` into `buffer` at `offset` in command order, through a copy recorded in - /// `encoder`, counted as written. So a reused scratch buffer keeps earlier passes' inputs - /// intact, and the write takes effect after any copy recorded before it. + /// Writes `bytes` into `buffer` at `offset` through a copy recorded in `encoder`, counted as + /// written. The write lands in command order, so a reused scratch buffer keeps earlier passes' + /// inputs intact, and the write takes effect after any copy recorded before it. pub(crate) fn write_buffer_in( &self, device: &wgpu::Device, @@ -144,8 +144,8 @@ impl Queue { encoder.clear_buffer(buffer, offset, Some(size)); } - /// Writes `bytes`, the words of `pages` adjacent data pages or pieces of pages, into `buffer` - /// at `offset`, counted as uploaded. + /// Writes `bytes` into `buffer` at `offset`, counted as uploaded. The bytes are the words of + /// `pages` adjacent data pages or pieces of pages. #[expect(clippy::disallowed_methods, reason = "the counted upload")] pub(crate) fn upload(&self, buffer: &wgpu::Buffer, offset: u64, bytes: &[u8], pages: u64) { self.counters.add(Count::Uploaded, bytes.len() as u64); diff --git a/crates/latkit/src/host/browser.rs b/crates/latkit/src/host/browser.rs index cc39d284..4e4833e4 100644 --- a/crates/latkit/src/host/browser.rs +++ b/crates/latkit/src/host/browser.rs @@ -23,8 +23,8 @@ thread_local! { static ENCODERS: RefCell> = const { RefCell::new(None) }; } -/// How many jobs the page's workers run at once: one fewer than the cores, from 1 to 4. The page -/// keeps a worker for each, so it never queues. +/// How many jobs the page's workers run at once: one fewer than the number of cores, from 1 to 4. +/// The page keeps a worker for each, so it never queues. fn workers() -> usize { let cores = web_sys::window() .map(|window| window.navigator().hardware_concurrency()) @@ -39,7 +39,8 @@ fn workers() -> usize { } /// The latkit error for a value the page's script threw or rejected with. An error whose `code` is -/// `unsupported` or `timeout` keeps it; anything else is an [`Error::Io`] saying `what` failed. +/// `unsupported` or `timeout` keeps that code. Anything else becomes an [`Error::Io`] saying `what` +/// failed. fn failure(error: &JsValue, what: &str) -> Error { let field = |key: &str| js_sys::Reflect::get(error, &key.into()).ok().and_then(|v| v.as_string()); let message = field("message").or_else(|| error.as_string()).unwrap_or_default(); diff --git a/crates/latkit/src/host/job.rs b/crates/latkit/src/host/job.rs index c7345538..e9687725 100644 --- a/crates/latkit/src/host/job.rs +++ b/crates/latkit/src/host/job.rs @@ -280,10 +280,10 @@ impl Job { Job(out.0) } - /// A job reading GeoJSON `bytes` as dataset `name` in the background, as - /// [`Data::read`](crate::Data::read) does; GeoJSON can be large. `None` if `bytes` are latkit's - /// own bytes or a dataset document, which are read on the calling thread. Its result is the - /// patch that builds the dataset, or no bytes if the GeoJSON is invalid. + /// A job that reads GeoJSON `bytes` as dataset `name` in the background, like + /// [`Data::read`](crate::Data::read), since GeoJSON can be large. `None` if `bytes` are + /// latkit's own bytes or a dataset document, which are read on the calling thread. Its result + /// is the patch that builds the dataset, or no bytes if the GeoJSON is invalid. pub fn geojson(name: &str, bytes: &[u8]) -> Option { if !crate::data::geojson(bytes) { return None; diff --git a/crates/latkit/src/host/mod.rs b/crates/latkit/src/host/mod.rs index 7e1fc947..55922444 100644 --- a/crates/latkit/src/host/mod.rs +++ b/crates/latkit/src/host/mod.rs @@ -5,8 +5,8 @@ //! A [`Gpu`](crate::Gpu) gets its host from [`Gpu::set_host`](crate::Gpu::set_host). Without one, //! views draw no text, run jobs immediately, fetch nothing, and encode no video. In a browser the //! host is `Browser` (the `web` feature on `wasm32`). Natively, the `native` feature supplies -//! `Native`: fonts loaded on first use, a worker pool that runs jobs and reads files, and Media -//! Foundation H.264 on Windows. It needs no window or network client. +//! `Native`. It loads fonts on first use, runs jobs and reads files on a worker pool, and encodes +//! H.264 with Media Foundation on Windows. It needs no window or network client. #[cfg(all(feature = "web", target_arch = "wasm32"))] mod browser; @@ -37,8 +37,8 @@ pub const TIMESCALE: u32 = 90_000; /// A platform's H.264 encoder: NV12 pictures (BT.709, limited range) in, Annex B units out. It uses /// no B-frames, so every platform's encoder can comply and the muxer never reorders frames. pub trait Encoder { - /// Encodes one NV12 picture at `time` ticks, as a keyframe when `key`. Resolves, once the - /// encoder can take another picture, to the units finished since the last call, in order. + /// Encodes one NV12 picture at `time` ticks, as a keyframe when `key`. Once the encoder can + /// take another picture, resolves to the units finished since the last call, in order. fn encode(&mut self, nv12: Vec, time: u64, key: bool) -> Pending>; /// Resolves to the remaining units once every picture given has been encoded. @@ -63,27 +63,27 @@ pub trait Host: Send + Sync + 'static { /// fails. fn rasterize(&self, run: &TextRun) -> Result; - /// The width and height of `run` once rasterized, in pixels. By default, rasterizes it to find - /// out. + /// The width and height of `run` once rasterized, in pixels. The default implementation + /// rasterizes it. fn measure(&self, run: &TextRun) -> Result<[f32; 2]> { self.rasterize(run).map(|raster| raster.size.map(|v| v as f32)) } - /// Runs `job` in the background, resolving to the result of [`Job::run`]. By default it runs - /// immediately on the calling thread. + /// Runs `job` in the background, resolving to the result of [`Job::run`]. The default + /// implementation runs it immediately on the calling thread. fn spawn(&self, job: Job) -> Pending> { Pending::ready(Ok(job.run())) } /// Fetches `request`, resolving to its bytes. Resolves to no bytes if nothing exists at the - /// URL, and to an error if it cannot be read. By default, fails as unsupported. + /// URL, and to an error if it cannot be read. The default implementation fails as unsupported. fn fetch(&self, request: &Request) -> Pending> { let _ = request; Pending::ready(Err(Error::Unsupported("this host cannot fetch".into()))) } /// An H.264 encoder for `encoding`, in hardware where the platform has one. [`Mp4`](crate::Mp4) - /// encodes with it. By default, fails as unsupported. + /// encodes with it. The default implementation fails as unsupported. fn encoder(&self, encoding: &Encoding) -> crate::Result> { let _ = encoding; Err(Error::Unsupported("this host cannot encode video".into())) diff --git a/crates/latkit/src/host/native.rs b/crates/latkit/src/host/native.rs index c0c993d4..0c7ff020 100644 --- a/crates/latkit/src/host/native.rs +++ b/crates/latkit/src/host/native.rs @@ -18,9 +18,9 @@ use crate::{Error, Pending, Resolver, Result}; /// Native platform services: shaped text, background jobs, local files, and video. /// -/// `Native` is a handle; clones share its fonts and workers. System fonts are discovered on the -/// first text request, while [`from_fonts`](Self::from_fonts) uses only the supplied font bytes. A -/// pool of at most four workers starts on the first job or fetch. A fetch reads a file path or a +/// `Native` is a handle, and clones share its fonts and workers. System fonts are discovered on the +/// first text request. [`from_fonts`](Self::from_fonts) uses only the supplied font bytes instead. +/// A pool of at most four workers starts on the first job or fetch. A fetch reads a file path or a /// `file://` URL on a worker. Video is encoded with Media Foundation on Windows, in hardware where /// the machine has it. Other platforms encode no video. #[derive(Clone, Default)] diff --git a/crates/latkit/src/host/native/media_foundation.rs b/crates/latkit/src/host/native/media_foundation.rs index c5f5d985..66e48b23 100644 --- a/crates/latkit/src/host/native/media_foundation.rs +++ b/crates/latkit/src/host/native/media_foundation.rs @@ -46,7 +46,7 @@ struct MediaFoundation { } /// Work for the encoder's thread: a picture, whose `room` resolves once the encoder can take -/// another; or the request to finish. +/// another, or the request to finish. enum Work { Picture { nv12: Vec, time: u64, key: bool, room: Resolver> }, Finish(Resolver>), @@ -263,7 +263,7 @@ impl Transform { (transform.input, transform.output) = (inputs[0], outputs[0]); } if let Some(codec) = &transform.codec { - // Settings an encoder rejects are skipped; they only tune its output. + // Settings an encoder rejects are skipped. They only tune its output. let gop = (encoding.fps * KEYFRAMES).round().max(1.0) as u32; let rate = eAVEncCommonRateControlMode_UnconstrainedVBR.0 as u32; for (api, value) in [ diff --git a/crates/latkit/src/host/queue.rs b/crates/latkit/src/host/queue.rs index e25bf007..717cdde9 100644 --- a/crates/latkit/src/host/queue.rs +++ b/crates/latkit/src/host/queue.rs @@ -14,7 +14,7 @@ pub(crate) struct Queue { } impl Queue { - /// Queues `job`; `resolver` receives its result. + /// Queues `job`. `resolver` receives its result. pub(crate) fn push(&mut self, job: Job, resolver: Resolver>) { self.waiting.push_back((job, resolver)); } diff --git a/crates/latkit/src/json.rs b/crates/latkit/src/json.rs index ba69f647..25ab2ca5 100644 --- a/crates/latkit/src/json.rs +++ b/crates/latkit/src/json.rs @@ -26,8 +26,7 @@ pub fn from_json(text: &str) -> Result { Ok(value) } -/// Deserializes JSON `bytes` as `T`, as [`from_json`] reads text. Bytes that are not UTF-8 are -/// invalid too. +/// Deserializes JSON `bytes` as `T`, like [`from_json`]. Bytes that are not UTF-8 are invalid. pub(crate) fn from_bytes(bytes: &[u8]) -> Result { from_json(std::str::from_utf8(bytes).map_err(|e| Error::invalid(format!("not UTF-8: {e}")))?) } @@ -60,9 +59,9 @@ thread_local! { static WITHIN: std::cell::Cell> = const { std::cell::Cell::new(None) }; } -/// Deserializes `T` from `value`, a part of the input [`read`] is reading that was buffered first, -/// such as a mark, whose key naming its kind may come after its options. An error's path continues -/// from the value's own. +/// Deserializes `T` from `value`, a buffered part of the input [`read`] is reading. Marks are +/// buffered, since the key naming a mark's kind may come after its options. An error's path +/// continues from the value's own path. pub(crate) fn nested(value: serde_json::Value) -> std::result::Result { read(value).map_err(|error| match error { Error::Invalid { path, reason } => { diff --git a/crates/latkit/src/layout/boxes.rs b/crates/latkit/src/layout/boxes.rs index 4dac2de7..598b6cf9 100644 --- a/crates/latkit/src/layout/boxes.rs +++ b/crates/latkit/src/layout/boxes.rs @@ -1,7 +1,7 @@ -//! A spatial index of boxes that change: a packed R-tree whose leaves refit as their boxes change, -//! plus a list of items given boxes since packing, which every query also scans until there are -//! enough to repack. Queries and changes take about O(log n), and the root holds the extent of all -//! boxes, so keeping the index current never walks every box (except when repacking). +//! A spatial index of boxes that change. It is a packed R-tree whose leaves refit as their boxes +//! change, plus a list of items given boxes since packing. Every query also scans that list until +//! it is long enough to repack. Queries and changes take about O(log n). The root holds the extent +//! of all boxes, so keeping the index current never walks every box, except when repacking. use super::rect::{EMPTY, Rect, meets, union, valid}; @@ -15,9 +15,9 @@ const FRESH: usize = 64; pub(crate) struct Boxes { rects: Vec, index: Index, - /// Each item's leaf; `usize::MAX` for an item that had no box when packed. + /// Each item's leaf, or `usize::MAX` for an item that had no box when packed. leaves: Vec, - /// Items given a box since packing; they have no leaf. + /// Items given a box since packing. They have no leaf. fresh: Vec, } @@ -90,7 +90,7 @@ struct Index { items: Vec, /// Each node's box: the leaves, then each level up to the root. nodes: Vec, - /// Where each level's nodes start, leaves first; the last is the node count. + /// Where each level's nodes start, leaves first. The last is the node count. starts: Vec, } diff --git a/crates/latkit/src/layout/layered.rs b/crates/latkit/src/layout/layered.rs index a87c962e..1ad60f06 100644 --- a/crates/latkit/src/layout/layered.rs +++ b/crates/latkit/src/layout/layered.rs @@ -12,12 +12,12 @@ const SWEEPS: usize = 8; /// Every vertex's center from a layered layout. Pinned vertices stay put. The others go in ranks /// along `options.direction` (`Direction::Up` is toward +y), `options.gap` apart within a rank and -/// `options.rank_gap` between ranks, ordered to reduce crossings. Links are directed; cycles are -/// broken. +/// `options.rank_gap` between ranks, ordered to reduce crossings. Links are directed, and cycles +/// are broken. /// /// Each connected part is laid out separately, and parts with no pinned vertex pack into rows /// (aspect 16:9) below the pinned ones. Across the direction, each vertex lines up with its -/// sources; in a part with no pinned vertex, each source then centers among the vertices it fans +/// sources. In a part with no pinned vertex, each source then centers among the vertices it fans /// out to. Groups nest as [`nested`] lays them out. pub(crate) fn layered(graph: &Graph<'_>, options: &Layered) -> Vec<[f64; 2]> { nested(graph, |level| arrange(level, options)) @@ -31,7 +31,7 @@ fn arrange(level: &Level, options: &Layered) -> Vec<[f64; 2]> { let (part, count) = parts(sizes.len(), &level.links); let inside = Runs::new(count, (0..sizes.len()).map(|i| (part[i] as usize, i as u32))); let joins = Runs::new(count, level.links.iter().map(|&link| (part[link[0] as usize] as usize, link))); - // Each member's index within its part; a part's members are in index order. + // Each member's index within its part. A part's members are in index order. let mut slot = vec![0_u32; sizes.len()]; for p in 0..count { for (s, &i) in inside.get(p).iter().enumerate() { @@ -124,8 +124,8 @@ fn parts(count: usize, links: &[[u32; 2]]) -> (Vec, usize) { (part, parts) } -/// Rank coordinates of plane coordinates: rotated so `direction` runs along the first axis, -/// `[along, across]`. Across runs down for a sideways direction, and right for an upward or +/// Rank coordinates `[along, across]` of plane coordinates, rotated so `direction` runs along the +/// first axis. Across runs down for a sideways direction, and right for an upward or /// downward one. fn to_ranks(direction: Direction, [x, y]: [f64; 2]) -> [f64; 2] { match direction { @@ -136,7 +136,7 @@ fn to_ranks(direction: Direction, [x, y]: [f64; 2]) -> [f64; 2] { } } -/// Plane coordinates of rank coordinates `[along, across]`; undoes [`to_ranks`]. +/// Plane coordinates of rank coordinates `[along, across]`. Undoes [`to_ranks`]. fn from_ranks(direction: Direction, [along, across]: [f64; 2]) -> [f64; 2] { match direction { Direction::Right => [along, -across], @@ -204,7 +204,7 @@ fn ranks(count: usize, links: &[[u32; 2]]) -> Vec { } let mut roots: Vec = (0..count as u32).collect(); crate::order::by_key(&mut roots, |&vertex| u64::from(pointed[vertex as usize])); - // A link into a vertex still on the walk closes a cycle; every other link keeps its direction. + // A link into a vertex still on the walk closes a cycle. Every other link keeps its direction. let mut state = vec![NEW; count]; let mut forward: Vec<[u32; 2]> = Vec::with_capacity(links.len()); let mut walk: Vec<(u32, usize)> = Vec::new(); diff --git a/crates/latkit/src/layout/levels.rs b/crates/latkit/src/layout/levels.rs index c5f9dd97..49eab447 100644 --- a/crates/latkit/src/layout/levels.rs +++ b/crates/latkit/src/layout/levels.rs @@ -5,10 +5,10 @@ //! the level holding it. Links between members of different groups are lifted to the groups that //! share a level. Every layout places one level at a time, so all layouts nest the same way. -/// A graph to lay out, in plane units with y up: each vertex's width and height, its pinned center -/// if any, the group holding it, and its padding as a group (how far its box reaches past its -/// members: left, bottom, right, top); and links between vertices, directed where a layout reads -/// direction. +/// A graph to lay out, in plane units with y up. It holds each vertex's width and height, its +/// pinned center if any, the group holding it, and its padding as a group (how far its box reaches +/// past its members: left, bottom, right, top). It also holds the links between vertices, directed +/// where a layout reads direction. pub(crate) struct Graph<'a> { pub(crate) sizes: &'a [[f64; 2]], pub(crate) links: &'a [[u32; 2]], @@ -26,7 +26,7 @@ pub(crate) struct Level { /// Every vertex's center, with `place` laying out each level. A group's center is the middle of /// its box: its members' bounds grown by its pads. A group with a pinned member is pinned where its -/// members settled; a group's own pin moves its members only when none of them is pinned. Group +/// members settled. A group's own pin moves its members only when none of them is pinned. Group /// references out of range, to the vertex itself, or closing a cycle are ignored. Panics unless /// `pinned`, `groups`, and `pads` have an entry per vertex. pub(crate) fn nested(graph: &Graph<'_>, mut place: impl FnMut(&Level) -> Vec<[f64; 2]>) -> Vec<[f64; 2]> { @@ -194,7 +194,7 @@ pub(crate) fn frame(center: [f64; 2], size: [f64; 2]) -> [f64; 4] { [center[0] - half[0], center[1] - half[1], center[0] + half[0], center[1] + half[1]] } -/// The box holding `boxes`; a point at the origin for none. +/// The box holding `boxes`, or a point at the origin for none. pub(crate) fn bounds(boxes: impl Iterator) -> [f64; 4] { boxes.reduce(super::rect::union).unwrap_or([0.0; 4]) } @@ -208,7 +208,7 @@ pub(crate) struct Runs { } impl Runs { - /// Buckets `keyed` items into `buckets` buckets; each key must be below `buckets`. + /// Buckets `keyed` items into `buckets` buckets. Each key must be below `buckets`. pub(crate) fn new(buckets: usize, keyed: impl Iterator + Clone) -> Runs { let mut offsets = vec![0; buckets + 1]; for (key, _) in keyed.clone() { diff --git a/crates/latkit/src/layout/overlap.rs b/crates/latkit/src/layout/overlap.rs index a85e8ae9..9d6c36f6 100644 --- a/crates/latkit/src/layout/overlap.rs +++ b/crates/latkit/src/layout/overlap.rs @@ -9,10 +9,10 @@ const ROUNDS: usize = 40; /// within `clearance` of each other. Pinned boxes never move, and two pinned boxes may still /// overlap. /// -/// First come rounds of pushing: a pair too close parts along the axis where it overlaps least, -/// each box moving half the overlap, or the free box all of it when the other is pinned. This +/// First come rounds of pushing. A pair too close parts along the axis where it overlaps least. +/// Each box moves half the overlap, or the free box moves all of it when the other is pinned. This /// moves boxes least, but a crowd can jam. If overlaps remain after `ROUNDS`, a sweep places the -/// free boxes in order of x after the pinned ones, each pushed right past any placed box it +/// free boxes in order of x after the pinned ones. Each is pushed right past any placed box it /// overlaps. Pairs are found in a grid of cells as wide as the widest box, so each round and the /// sweep are linear in the number of boxes when their sizes are similar. pub(crate) fn separate(boxes: &[[f64; 4]], pinned: &[bool], clearance: f64) -> Vec<[f64; 2]> { diff --git a/crates/latkit/src/layout/pack.rs b/crates/latkit/src/layout/pack.rs index be0614fd..09d3502b 100644 --- a/crates/latkit/src/layout/pack.rs +++ b/crates/latkit/src/layout/pack.rs @@ -1,6 +1,6 @@ /// Packs boxes (each `[width, height]`) in the given order into rows `gap` apart, so the result's /// width over height approaches `aspect`. Returns each box's top-left corner, with y up (top is the -/// greater y); the first box's top left is at the origin. +/// greater y). The first box's top left is at the origin. pub(crate) fn pack(boxes: &[[f64; 2]], gap: f64, aspect: f64) -> Vec<[f64; 2]> { rows(boxes, gap, span(boxes, gap, aspect)) } diff --git a/crates/latkit/src/layout/route.rs b/crates/latkit/src/layout/route.rs index 427be3c1..90f55e50 100644 --- a/crates/latkit/src/layout/route.rs +++ b/crates/latkit/src/layout/route.rs @@ -21,8 +21,8 @@ use crate::scene::Side; pub(crate) struct Terminal { /// The point on the vertex's boundary. pub(crate) at: [f64; 2], - /// The side the wire leaves by. With none, the owner's side nearest `at`; without an owner, the - /// axis toward the rest of the net. + /// The side the wire leaves by. With none, the wire leaves by the owner's side nearest `at`, or + /// without an owner, along the axis toward the rest of the net. pub(crate) side: Option, /// The obstacle the terminal belongs to. pub(crate) owner: Option, @@ -31,7 +31,7 @@ pub(crate) struct Terminal { /// A net: every terminal it joins. Two terminals make a plain edge. #[derive(Clone, PartialEq, Debug, Default)] pub(crate) struct Net { - /// The terminals; the route grows from the first. + /// The terminals. The route grows from the first. pub(crate) terminals: Vec, } @@ -226,8 +226,8 @@ impl<'a> Router<'a> { Some(tree) } - /// Resolves a terminal for routing. Its outward direction is its side's normal; else that of - /// its owner's side nearest it; else the axis toward `toward`. + /// Resolves a terminal for routing. Its outward direction is its side's normal, else the normal + /// of its owner's side nearest it, else the axis toward `toward`. fn end(&self, terminal: Terminal, toward: Point) -> End { let owner = self.obstacles.owned(terminal.owner); let at = terminal.at; @@ -270,9 +270,9 @@ impl<'a> Router<'a> { point } - /// The cheapest clear path from the tree to a reader's stub end `end`: an elbow from one of the - /// nearest attach points; else a search around the obstacles at decreasing clearance; else a - /// plain elbow through them. + /// The cheapest clear path from the tree to a reader's stub end `end`. It is an elbow from one + /// of the nearest attach points, else a search around the obstacles at decreasing clearance, + /// else a plain elbow through them. fn branch(&mut self, tree: &Tree, end: Point, reader: &End, root: &End) -> Branch { let mut attaches = tree.attaches(end); attaches.truncate(ATTACHES); @@ -315,7 +315,7 @@ impl<'a> Router<'a> { /// Spreads runs of different nets that share a line onto separate tracks, `spacing` apart where /// the corridor between obstacles has room. Tracks are ordered by where the runs' ends turn, so - /// runs nest rather than cross. A run moves its joints, so branches stay connected; terminals + /// runs nest rather than cross. A run moves its joints, so branches stay connected. Terminals /// never move, and stubs only grow. fn separate(&self, trees: &mut [Option]) { let tolerance = self.spacing / 16.0; @@ -805,7 +805,7 @@ impl Search { return false; } } - // Index each vertical span's crossings; they come bottom to top, as the rows do. + // Index each vertical span's crossings. They come bottom to top, as the rows do. self.starts.clear(); self.starts.resize(vertical.len() + 1, 0); for crossing in &self.crossings { @@ -980,13 +980,13 @@ impl Search { /// The free spans of lines running along axis `along` (0: x, 1: y) within `window`, among `boxes` /// (the grown obstacles that meet it). Lines pass through each box's corners (clamped into the -/// window) and through each of `ends`, taking the free span at that point; lines along the +/// window) and through each of `ends`, taking the free span at that point. Lines along the /// window's edges take every free span. Sorted by line, then start. fn spans(boxes: &[Rect], window: Rect, ends: &[Point], along: usize) -> Vec { let held = 1 - along; let (start, end) = (window[along], window[along + 2]); let (low, high) = (window[held], window[held + 2]); - // A seed is a line and a point along it, which selects the free span holding that point; NaN + // A seed is a line and a point along it, which selects the free span holding that point. NaN // selects every free span of the line. let mut seeds: Vec = boxes .iter() @@ -1159,7 +1159,7 @@ fn inside(p: Point, r: &Rect) -> bool { /// Whether the axis-aligned segment from `a` to `b` passes through a box's interior. fn crosses(a: Point, b: Point, r: &Rect) -> bool { - // A vertical segment has a fixed x and runs along y; a horizontal one has a fixed y. + // A vertical segment has a fixed x and runs along y. A horizontal one has a fixed y. let held = usize::from(a[0] != b[0]); let runs = 1 - held; a[held] > r[held] + EPSILON @@ -1222,7 +1222,7 @@ struct Run { along: [f64; 2], /// Which way its ends turn: negative toward lower lines, positive toward higher. turn: f64, - /// The lines tracks may move it between; a single line for a run holding a terminal. + /// The lines tracks may move it between, or a single line for a run holding a terminal. limits: [f64; 2], } diff --git a/crates/latkit/src/layout/stress.rs b/crates/latkit/src/layout/stress.rs index bbe1bb29..2cca902a 100644 --- a/crates/latkit/src/layout/stress.rs +++ b/crates/latkit/src/layout/stress.rs @@ -72,8 +72,8 @@ fn place(level: &Level, gap: f64) -> Vec<[f64; 2]> { separate(&boxes, &pinned, gap * CLEARANCE) } -/// Each vertex's terms as (other vertex, target distance, weight): its neighbors one `gap` away, -/// weighted by the inverse square of that distance; and pivots at their graph distance times +/// Each vertex's terms as (other vertex, target distance, weight). Its neighbors are one `gap` +/// away, weighted by the inverse square of that distance. Pivots are at their graph distance times /// `gap`, weighted by the inverse square times how many vertices of the pivot's region lie within /// half that distance of it. fn terms(n: usize, gap: f64, offsets: &[u32], targets: &[u32]) -> Vec> { @@ -225,8 +225,8 @@ fn start(level: &Level, gap: f64, offsets: &[u32], targets: &[u32]) -> Vec<[f64; start } -/// Each vertex's neighbors, each once, ignoring direction, self-links, and links out of range: as -/// offsets into one list of targets. +/// Each vertex's neighbors, each once, as offsets into one list of targets. Ignores direction, +/// self-links, and links out of range. fn neighbors(n: usize, links: &[[u32; 2]]) -> (Vec, Vec) { let mut pairs: Vec<(u32, u32)> = links .iter() @@ -245,7 +245,7 @@ fn neighbors(n: usize, links: &[[u32; 2]]) -> (Vec, Vec) { (offsets, pairs.into_iter().map(|(_, b)| b).collect()) } -/// Hop counts from `from` to every vertex; `u32::MAX` where unreachable. +/// Hop counts from `from` to every vertex, or `u32::MAX` where unreachable. fn hops(from: usize, offsets: &[u32], targets: &[u32]) -> Vec { let mut distance = vec![u32::MAX; offsets.len() - 1]; let mut queue = VecDeque::from([from]); diff --git a/crates/latkit/src/order.rs b/crates/latkit/src/order.rs index 2f04c32d..0c18e6e3 100644 --- a/crates/latkit/src/order.rs +++ b/crates/latkit/src/order.rs @@ -65,7 +65,7 @@ pub(crate) fn number32(x: f32) -> u32 { if bits >> 31 == 1 { !bits } else { bits | 1 << 31 } } -/// The value whose key is `key`; the inverse of [`number32`]. +/// The value whose key is `key`. The inverse of [`number32`]. #[cfg(feature = "gpu")] pub(crate) fn from_number32(key: u32) -> f32 { f32::from_bits(if key >> 31 == 1 { key & !(1 << 31) } else { !key }) diff --git a/crates/latkit/src/pending.rs b/crates/latkit/src/pending.rs index 57c794ce..29a1732f 100644 --- a/crates/latkit/src/pending.rs +++ b/crates/latkit/src/pending.rs @@ -140,8 +140,8 @@ impl Future for Pending { } } -/// The wakers of whatever watches something, by the watcher's id, each the watcher's latest. Waking -/// wakes every watcher. +/// The wakers of everything watching one thing, keyed by watcher id. Each watcher keeps only its +/// latest waker. Waking wakes every watcher. #[derive(Default)] pub(crate) struct Watchers(Mutex>); diff --git a/crates/latkit/src/scene/areas.rs b/crates/latkit/src/scene/areas.rs index e6a0d667..a3a5a13c 100644 --- a/crates/latkit/src/scene/areas.rs +++ b/crates/latkit/src/scene/areas.rs @@ -6,16 +6,16 @@ use crate::{Error, Kind, Name, Result}; mark!( /// Rows drawn as the areas of a polygon field: parcels, states, lakes, the land and water of a map. /// The view triangulates each row's polygons once, in the background when they are large. On the -/// earth, areas follow the globe's curve and stay whole across the antimeridian. To outline areas, -/// add a [`Lines`](crate::Lines) mark of the same table, which draws their rings. +/// earth, areas follow the globe's curve and stay unbroken across the antimeridian. To outline +/// areas, add a [`Lines`](crate::Lines) mark of the same table. It draws their rings. /// /// ``` /// # use latkit::{field, Areas, Colormap}; /// let states = Areas::of("map", "states").color(field("load").colormap(Colormap::MAKO)); /// ``` Areas("areas") { - /// The polygon field each row fills, holding one polygon's rings or several polygons; the field - /// named `geometry` by default. + /// The polygon field each row fills. It holds one polygon's rings or several polygons. Defaults + /// to the field named `geometry`. geometry: Option, }); @@ -25,8 +25,8 @@ impl Areas { Areas { style: Style::new(data.into(), table.into()), geometry: None } } - /// Sets the polygon field each row fills, holding one polygon's rings or several polygons; the - /// field named `geometry` by default. + /// Sets the polygon field each row fills. It holds one polygon's rings or several polygons. + /// Defaults to the field named `geometry`. #[must_use] pub fn geometry(mut self, field: impl Into) -> Areas { self.geometry = Some(field.into()); @@ -58,8 +58,8 @@ impl Areas { } } -/// The name of the derived field holding polygon field `field`'s triangles: for each row, the point -/// its label anchors to, then three points per triangle. +/// The name of the derived field that holds the triangles of polygon field `field`. Each row holds +/// its label's anchor point, then three points per triangle. pub(crate) fn triangles_of(field: &str) -> Name { format!("{field}.triangles").into() } diff --git a/crates/latkit/src/scene/compile.rs b/crates/latkit/src/scene/compile.rs index 859767b0..a7c5be51 100644 --- a/crates/latkit/src/scene/compile.rs +++ b/crates/latkit/src/scene/compile.rs @@ -14,8 +14,9 @@ pub(crate) struct Compiled { } /// Compiles `mark` against the schemas in `all`. Returns `None` while the mark's dataset or table -/// is missing: a view keeps such a mark pending, and validation reports the absence before calling -/// this. Errors when an option fails to compile or the mark compiles to more than 32 channels. +/// is missing. A view keeps such a mark pending, and validation reports the missing table before +/// calling this. Errors when an option fails to compile or the mark compiles to more than 32 +/// channels. pub(crate) fn compile(mark: &Mark, all: &[Snapshot], context: Context<'_>) -> Result> { let style = mark.style(); let Some(data) = all.iter().find(|d| *d.name() == style.data) else { return Ok(None) }; diff --git a/crates/latkit/src/scene/controls.rs b/crates/latkit/src/scene/controls.rs index 726af22f..d03d4d66 100644 --- a/crates/latkit/src/scene/controls.rs +++ b/crates/latkit/src/scene/controls.rs @@ -6,55 +6,58 @@ use serde::Deserialize; use super::form::{preset_schema, presets, seconds}; /// How a view responds to the pointer and keyboard, and how much it animates on its own: a preset -/// (`"none"` or `"inspect"`), or options over the defaults. Each mark controls whether its own rows -/// pick, select, and drag; these options control the view itself. +/// (`"none"` or `"inspect"`), or options that change the defaults. Each mark sets whether its own +/// rows pick, select, and drag. These options control the view itself. #[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(remote = "Self", default, rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = preset_schema::)] pub struct Controls { - /// Whether dragging the background pans the camera. Default `true`. + /// Whether dragging the background pans the camera. Defaults to `true`. pub pan: bool, - /// Which input zooms the view: the wheel and pinching (`wheel`, the default); pinching, and the - /// wheel only with Control or Meta held, as trackpad pinches send it, so the plain wheel - /// scrolls the page (`modifier`); or nothing (`off`). + /// Which input zooms the view. `wheel` zooms with the wheel and pinching. `modifier` zooms with + /// pinching, and with the wheel only while Control or Meta is held (as trackpad pinches send + /// it), so the plain wheel scrolls the page. `off` turns zooming off. Defaults to `wheel`. pub zoom: Zoom, /// Whether dragging with the secondary button, or with Shift, rotates and tilts a map or plane. - /// Default `true`. + /// Defaults to `true`. pub turn: bool, - /// Whether the keyboard controls the view. Default `true`. Tab and Shift+Tab step through the - /// rows nearest the view's center, and arrow keys step to the nearest row in their direction. - /// Enter opens the stepped-to row and Space selects it. With no row stepped to, arrow keys pan + /// Whether the keyboard controls the view. Defaults to `true`. Tab and Shift+Tab step through + /// the rows nearest the view's center. Arrow keys step to the nearest row in their direction. + /// Enter opens the row stepped to, and Space selects it. With no row stepped to, arrow keys pan /// (Shift+arrows turn), `+` and `-` zoom, Home fits everything, and Escape clears the /// selection. pub keyboard: bool, - /// Whether the hovered row glows, and a hovered vertex sized in CSS pixels grows. Default - /// `true`. Rows are hovered either way: [`Event::Hovered`](crate::Event) still fires and - /// `hovered()` still reads them. + /// Whether the hovered row glows and a hovered vertex sized in CSS pixels grows. Defaults to + /// `true`. Hover is tracked either way: [`Event::Hovered`](crate::Event) still fires and + /// `hovered()` still returns the row. pub hover: bool, - /// How far from the pointer, in CSS pixels, a row can be and still be picked; touch adds 8. - /// Default 6. + /// How far a row can be from the pointer and still be picked, in CSS pixels. Touch adds 8. + /// Defaults to 6. pub pick_radius: f32, /// Whether Shift+drag selects the rows inside a box instead of turning the camera. Shift+click - /// still toggles one row. Default `false`. + /// still toggles one row. Defaults to `false`. pub select_box: bool, - /// Whether the user can draw connections, reported as [`Event::Connect`](crate::Event): by - /// dragging from a port, from a vertex with Alt held, or an edge's end off its vertex. Default - /// `false`. + /// Whether the user can draw connections by dragging from a port, from a vertex with Alt held, + /// or an edge's end off its vertex. Connections are reported as + /// [`Event::Connect`](crate::Event). Defaults to `false`. pub connect: bool, /// Whether Delete and Backspace report the selection as [`Event::Delete`](crate::Event). - /// Default `false`. + /// Defaults to `false`. pub delete: bool, /// Whether clicking again at the same point, with the camera unmoved, selects the next row - /// under it, nearest first, so overlapping rows can each be selected. Default `false`. + /// under the pointer, nearest first. This lets overlapping rows each be selected. Defaults to + /// `false`. pub cycle: bool, - /// Seconds a touch or pen must hold still to request a menu ([`Event::Menu`](crate::Event)), - /// as the secondary button does; 0 turns this off. Default 0.5. + /// How many seconds a touch or pen must hold still to request a menu + /// ([`Event::Menu`](crate::Event)), like the secondary button. 0 turns this off. Defaults to + /// 0.5. #[serde(deserialize_with = "seconds")] #[schemars(with = "f64")] pub press: Duration, /// How much the view animates on its own: eases, camera flights, coasting, orbits, comets, and - /// options that read `elapsed()`. `system` (the default) follows the user's reduced-motion - /// setting, `full` animates everything, and `reduced` animates only motion the user drives. + /// options that read `elapsed()`. `system` follows the user's reduced-motion setting, `full` + /// animates everything, and `reduced` animates only motion the user drives. Defaults to + /// `system`. pub motion: Motion, } @@ -76,7 +79,7 @@ pub enum Zoom { #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub enum Motion { - /// Follows the system: reduced when the user asked their system for less motion. + /// Follows the system setting: reduced when the user has asked their system for less motion. #[default] System, /// All motion. @@ -123,7 +126,7 @@ impl Controls { } /// Controls for inspecting rows: rows glow under the pointer, select on click, and step with - /// the keys, while the camera moves only when the application moves it. + /// the keys. Only the application moves the camera. pub fn inspect() -> Controls { Controls { pan: false, zoom: Zoom::Off, turn: false, ..Controls::default() } } diff --git a/crates/latkit/src/scene/edges.rs b/crates/latkit/src/scene/edges.rs index 0a7b9bcd..c8d1303c 100644 --- a/crates/latkit/src/scene/edges.rs +++ b/crates/latkit/src/scene/edges.rs @@ -5,9 +5,9 @@ use crate::gpu::shaders as wgsl; use crate::{Error, Kind, Name, Result}; mark!( -/// Rows drawn as strokes between the vertices named by two reference fields: branches, links, -/// wires. In a diagram, a row without ends is a net, joining every vertex port whose reference -/// field names it. +/// Rows drawn as strokes between two vertices named by reference fields: branches, links, wires. +/// In a diagram, a row without ends is a net. It joins every vertex port whose reference field +/// names it. /// /// ``` /// # use latkit::{field, Edges}; @@ -18,17 +18,17 @@ Edges("edges") + stroke { ends: Option<(Name, Name)> as "[from: string, to: string]", /// A path field whose points each row passes through between its ends. bends: Option, - /// How each row runs between its ends: geodesic on the earth and straight elsewhere unless set. - /// `orthogonal` routes around blocks with rounded bends, in diagrams only. + /// How each row runs between its ends. Defaults to geodesic on the earth and straight + /// elsewhere. `orthogonal` routes around blocks with rounded bends, in diagrams only. route: Option, - /// Whether each row has an arrowhead at its second end; a net has one at each input it reaches. - /// Default `false`. + /// Whether each row has an arrowhead at its second end. A net has one at each input it + /// reaches. Defaults to `false`. arrows: bool, /// Draws each row as a pole from its own vertex down to the ground, on a plane or the earth. - /// Poles take no `ends`, `bends`, or `joins`. Default `false`. + /// Poles take no `ends`, `bends`, or `joins`. Defaults to `false`. poles: bool, - /// Draws each row as a net on a plane or the earth, joining the vertex rows of `table` whose - /// `reference` field names it: a star from their center to each member. + /// Draws each row as a net on a plane or the earth. The net joins the vertex rows of `table` + /// whose `reference` field names it, as a star from their center to each member. joins: Option<(Name, Name)> as "[table: string, reference: string]", }); @@ -47,17 +47,17 @@ impl Edges { } } - /// Draws each row as a net on a plane or the earth, joining the vertex rows of `table` whose - /// field `reference` names it: a star from their center to each member. In a diagram, nets join - /// vertex ports instead. + /// Draws each row as a net on a plane or the earth. The net joins the vertex rows of `table` + /// whose field `reference` names it, as a star from their center to each member. In a diagram, + /// nets join vertex ports. #[must_use] pub fn joins(mut self, table: impl Into, reference: impl Into) -> Edges { self.joins = Some((table.into(), reference.into())); self } - /// Draws each row as a pole: a stroke from the row's own vertex, as placed by the vertex mark - /// that draws the same table, down to the ground, on a plane or the earth. Dashes, comets, + /// Draws each row as a pole on a plane or the earth: a stroke from the row's own vertex down to + /// the ground. The vertex is where the vertex mark of the same table places it. Dashes, comets, /// picking, and labels work as on any edge. #[must_use] pub fn poles(mut self) -> Edges { @@ -72,15 +72,15 @@ impl Edges { self } - /// Sets how each row runs between its ends: geodesic on the earth and straight elsewhere unless - /// set. + /// Sets how each row runs between its ends. Defaults to geodesic on the earth and straight + /// elsewhere. #[must_use] pub fn route(mut self, route: Route) -> Edges { self.route = Some(route); self } - /// Sets whether each row has an arrowhead at its second end; a net has one at each input it + /// Sets whether each row has an arrowhead at its second end. A net has one at each input it /// reaches. #[must_use] pub fn arrows(mut self, on: bool) -> Edges { diff --git a/crates/latkit/src/scene/faces.rs b/crates/latkit/src/scene/faces.rs index cff23895..09b09892 100644 --- a/crates/latkit/src/scene/faces.rs +++ b/crates/latkit/src/scene/faces.rs @@ -4,8 +4,8 @@ use crate::gpu::shaders as wgsl; use crate::{Error, Name, Result}; mark!( -/// Rows drawn as triangles between the vertices named by three reference fields: meshes, surfaces, -/// and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen +/// Rows drawn as triangles between three vertices named by reference fields: meshes, surfaces, and +/// fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen /// from above shows its exact colors. /// /// ``` @@ -13,7 +13,7 @@ mark!( /// let surface = Faces::of("mesh", "Face").corners("a", "b", "c"); /// ``` Faces("faces") { - /// The reference fields naming each row's three vertices; required. Without a color, a face + /// The reference fields naming each row's three vertices. Required. Without a color, a face /// blends the colors of its corners. corners: Option<[Name; 3]> as "[a: string, b: string, c: string]", }); @@ -24,7 +24,7 @@ impl Faces { Faces { style: Style::new(data.into(), table.into()), corners: None } } - /// Sets the reference fields naming each row's three vertices; required. Without a color, a + /// Sets the reference fields naming each row's three vertices. Required. Without a color, a /// face blends the colors of its corners. #[must_use] pub fn corners(mut self, a: impl Into, b: impl Into, c: impl Into) -> Faces { diff --git a/crates/latkit/src/scene/form.rs b/crates/latkit/src/scene/form.rs index 2293e9e9..b439d207 100644 --- a/crates/latkit/src/scene/form.rs +++ b/crates/latkit/src/scene/form.rs @@ -1,5 +1,5 @@ -//! JSON reading for scenes. Each type deserializes and derives its schema from its own definition; -//! a type with presets also accepts a preset's name, such as `"inspect"`, `"earth"`, or `"stress"`. +//! JSON reading for scenes. Each type deserializes and takes its schema from its own definition. A +//! type with presets also accepts a preset's name, such as `"inspect"`, `"earth"`, or `"stress"`. use std::fmt; use std::marker::PhantomData; @@ -80,12 +80,12 @@ macro_rules! presets { pub(crate) use presets; -/// Rewrites `T`'s derived schema to accept a preset name or the derived form, moving the -/// description to the whole. +/// Rewrites `T`'s derived schema to accept a preset name or the derived form. The description +/// moves to the combined schema. pub(crate) fn preset_schema(schema: &mut Schema) { let mut derived = std::mem::replace(schema, json_schema!({})); let description = derived.remove("description"); - // Drop unit variants that a preset already names; the preset's name covers them. + // Drop unit variants that a preset already names, since the preset's name covers them. for key in ["oneOf", "anyOf"] { if let Some(variants) = derived.get_mut(key).and_then(Value::as_array_mut) { variants.retain(|v| !v.get("const").and_then(Value::as_str).is_some_and(|name| T::NAMES.contains(&name))); @@ -97,7 +97,7 @@ pub(crate) fn preset_schema(schema: &mut Schema) { } } -/// Reads a duration in seconds; errors on negative or out-of-range values. +/// Reads a duration in seconds. Errors on negative or out-of-range values. pub(crate) fn seconds<'de, D: Deserializer<'de>>(deserializer: D) -> Result { let seconds = f64::deserialize(deserializer)?; Duration::try_from_secs_f64(seconds).map_err(|_| de::Error::custom("seconds must be finite and not negative")) diff --git a/crates/latkit/src/scene/label.rs b/crates/latkit/src/scene/label.rs index 82ef8acf..7a8b2c07 100644 --- a/crates/latkit/src/scene/label.rs +++ b/crates/latkit/src/scene/label.rs @@ -7,14 +7,15 @@ use serde::de::{self, Deserializer}; use crate::Expr; use crate::expr::form::Written; -/// Text for each row, placed beside, inside, or along the row's shape and following it every frame: -/// a text field, or options. A label shows wherever its row shows and its text fits the room the -/// row gives it, such as inside a block. Labels beside or along their rows never overlap: where they -/// would, the label of the row drawn on top shows, so a mark's `priority` decides, and a dense view -/// labels as many rows as fit. A row whose text is missing or empty has no label, so a `when` choosing -/// between the text field and missing labels only the rows it names. In a diagram a label is part -/// of the drawing: it scales with the zoom, fades out when too small to read, and shows wherever it -/// fits its vertex. +/// Text for each row, placed beside, inside, or along the row's shape and moving with it: a text +/// field, or options. A label shows when its row is visible and its text fits, for example inside +/// a block. +/// +/// Labels beside or along their rows never overlap. Where they would, the row drawn on top keeps +/// its label (so `priority` decides), and a dense view labels as many rows as fit. A row whose text +/// is missing or empty has no label, so a `when` that returns the field or missing labels only +/// some rows. In a diagram, a label scales with the zoom, fades out when too small to read, and +/// shows wherever it fits its vertex. /// /// ``` /// # use latkit::{field, when, zoom, Expr, Label, Vertices}; @@ -26,22 +27,21 @@ use crate::expr::form::Written; #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = text_or_options)] pub struct Label { - /// The text field to show, or a `when` whose values are each that field or missing. A row - /// whose text is missing or empty has no label. In a diagram the text reads only data, since the sheet + /// The text field to show, or a `when` that returns that field or missing. A row whose text is + /// missing or empty has no label. In a diagram, the text may read only data, since the sheet /// sizes blocks around it. pub(crate) text: Expr, - /// Font size in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's - /// `text.size` unless set. + /// Font size in CSS pixels, or diagram units in a diagram. Defaults to the theme's + /// `text.size`. #[serde(default)] pub(crate) size: Option, - /// Each row's text color; the theme's text color unless set. + /// Each row's text color. Defaults to the theme's text color. #[serde(default)] pub(crate) color: Option, - /// The width at which lines wrap, in the space's unit (units in a diagram, CSS pixels - /// elsewhere). + /// The width at which lines wrap, in CSS pixels, or diagram units in a diagram. #[serde(default)] pub(crate) max_width: Option, - /// The CSS font family list for the text; the theme's `text.font` unless set. + /// The CSS font family list for the text. Defaults to the theme's `text.font`. #[serde(default)] pub(crate) font: Option>, } @@ -66,7 +66,7 @@ impl<'de> Deserialize<'de> for Label { } } -/// A label's schema: a text field, or its options, with the description moved to the whole. +/// A label's schema: a text field or its options. The description moves to the combined schema. fn text_or_options(schema: &mut schemars::Schema) { let mut options = std::mem::replace(schema, schemars::json_schema!({})); let description = options.remove("description"); @@ -77,36 +77,35 @@ fn text_or_options(schema: &mut schemars::Schema) { } impl Label { - /// Labels each row with a text field's value, or with a `when` choosing between that field and - /// missing; a row whose text is missing or empty has no label. + /// Labels each row with a text field's value, or with a `when` that returns that field or + /// missing. A row whose text is missing or empty has no label. pub fn new(text: impl Into) -> Label { Label { text: text.into(), size: None, color: None, max_width: None, font: None } } - /// Sets the font size in the space's unit (units in a diagram, CSS pixels elsewhere); the - /// theme's text size unless set. + /// Sets the font size in CSS pixels, or diagram units in a diagram. Defaults to the theme's + /// text size. #[must_use] pub fn size(mut self, size: f32) -> Label { self.size = Some(size); self } - /// Sets each row's text color; the theme's text color unless set. + /// Sets each row's text color. Defaults to the theme's text color. #[must_use] pub fn color(mut self, color: impl Into) -> Label { self.color = Some(color.into()); self } - /// Sets the width at which lines wrap, in the space's unit (units in a diagram, CSS pixels - /// elsewhere). + /// Sets the width at which lines wrap, in CSS pixels, or diagram units in a diagram. #[must_use] pub fn max_width(mut self, width: f32) -> Label { self.max_width = Some(width); self } - /// Sets the CSS font family list for the text; the theme's font unless set. + /// Sets the CSS font family list for the text. Defaults to the theme's font. #[must_use] pub fn font(mut self, font: impl Into>) -> Label { self.font = Some(font.into()); diff --git a/crates/latkit/src/scene/layout.rs b/crates/latkit/src/scene/layout.rs index b9fbd9a8..8ab92636 100644 --- a/crates/latkit/src/scene/layout.rs +++ b/crates/latkit/src/scene/layout.rs @@ -4,11 +4,11 @@ use serde::Deserialize; use super::form::{preset_schema, presets}; /// How the view places vertex rows that have no position: a name, or -/// `{ stress | layered: options }`. In a diagram it places blocks; on a plane or the earth it +/// `{ stress | layered: options }`. In a diagram it places blocks. On a plane or the earth it /// places points among the rows they link to. Gaps are in position units: diagram units, plane /// units, or degrees on the earth. /// -/// A row keeps its place once placed: rows added later are placed around the others, and +/// A placed row keeps its place. Rows added later are placed around the others, and /// [`View::arrange`](crate::View::arrange) lays out every row again. #[derive(Clone, Copy, PartialEq, Debug, Default, Deserialize, JsonSchema)] #[serde(remote = "Self", rename_all = "camelCase", deny_unknown_fields)] @@ -29,12 +29,12 @@ presets!(Layout { "layered" => Layout::Layered(Layered::default()), }); -/// Stress majorization: vertices move until drawn distances match graph distances, then overlapping -/// boxes separate, keeping a quarter of `gap` between them. +/// Stress majorization: vertices move until drawn distances match graph distances. Then +/// overlapping boxes separate, keeping a quarter of `gap` between them. #[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct Stress { - /// Distance between the centers of linked vertices, in position units. Default 40. + /// Distance between the centers of linked vertices, in position units. Defaults to 40. #[schemars(range(min = 0))] pub gap: f64, } @@ -50,11 +50,11 @@ impl Default for Stress { #[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct Layered { - /// The direction edges point; `right` by default. + /// The direction edges point. Defaults to `right`. pub direction: Direction, - /// Distance between vertices within a rank, in position units. Default 30. + /// Distance between vertices within a rank, in position units. Defaults to 30. pub gap: f64, - /// Distance between ranks, in position units. Default 80. + /// Distance between ranks, in position units. Defaults to 80. pub rank_gap: f64, } diff --git a/crates/latkit/src/scene/lines.rs b/crates/latkit/src/scene/lines.rs index e760057e..834df07d 100644 --- a/crates/latkit/src/scene/lines.rs +++ b/crates/latkit/src/scene/lines.rs @@ -8,9 +8,8 @@ use crate::{Error, Kind, Name, Result, Space}; mark!( /// Rows drawn as lines along a path field (roads, rivers, borders) or a polygon field's rings (area -/// outlines). On a plot, each row instead traces a sampled field's history across the data's axis, -/// as a monitor does. On the earth, paths follow the globe's curve and stay whole across the -/// antimeridian. +/// outlines). On a plot, each row traces a sampled field's history across the data's axis, like a +/// monitor. On the earth, paths follow the globe's curve and stay unbroken across the antimeridian. /// /// ``` /// # use latkit::Lines; @@ -18,13 +17,13 @@ mark!( /// let height = Lines::of("body", "Body").trace("height"); /// ``` Lines("lines") + stroke { - /// The path or polygon field each row runs along; the field named `geometry` by default. Not + /// The path or polygon field each row runs along. Defaults to the field named `geometry`. Not /// allowed on a plot. geometry: Option, - /// The sampled field each row traces across the data's axis; plots only, where it is required. - /// A field with no frames yet draws nothing until its first frame arrives. + /// The sampled field each row traces across the data's axis. Required on a plot, and allowed + /// only there. A field with no frames yet draws nothing until its first frame arrives. trace: Option, - /// How each row runs between its samples on a plot; `linear` by default. + /// How each row runs between its samples on a plot. Defaults to `linear`. interpolation: Interpolation, }); @@ -40,7 +39,7 @@ impl Lines { } } - /// Sets the path or polygon field each row runs along; the field named `geometry` by default. + /// Sets the path or polygon field each row runs along. Defaults to the field named `geometry`. /// Not allowed on a plot. #[must_use] pub fn geometry(mut self, field: impl Into) -> Lines { @@ -48,7 +47,7 @@ impl Lines { self } - /// Sets the sampled field each row traces across the data's axis; plots only. Unless the mark + /// Sets the sampled field each row traces across the data's axis. Plots only. Unless the mark /// is [named](Lines::named), the field's name identifies it across scenes. #[must_use] pub fn trace(mut self, field: impl Into) -> Lines { @@ -56,7 +55,7 @@ impl Lines { self } - /// Sets how each row runs between its samples on a plot; linear by default. + /// Sets how each row runs between its samples on a plot. Defaults to linear. #[must_use] pub fn interpolation(mut self, interpolation: Interpolation) -> Lines { self.interpolation = interpolation; @@ -79,7 +78,7 @@ impl Lines { return Err(Error::invalid("name the sampled field each row traces").at("trace")); }; drawing.trace = match c.kind(field) { - // Streamed data may not have its first frame yet; the mark recompiles when it + // Streamed data may not have its first frame yet. The mark recompiles when it // arrives. None => wgsl::NONE, _ => c.trace(field).map_err(|e| e.at("trace"))?, diff --git a/crates/latkit/src/scene/mark.rs b/crates/latkit/src/scene/mark.rs index 63dc328e..41a41be5 100644 --- a/crates/latkit/src/scene/mark.rs +++ b/crates/latkit/src/scene/mark.rs @@ -5,8 +5,8 @@ use crate::expr::compile::{Compiler, Store}; use crate::gpu::shaders as wgsl; use crate::{Error, Expr, Name, Result, Size, Space}; -/// One table a scene draws. In JSON, the key naming the mark's kind holds its dataset and table, -/// beside its options: `{ "vertices": ["grid", "Bus"], "radius": 4 }`. +/// One table a scene draws. In JSON, the mark's kind is the key that holds its dataset and table, +/// next to its options: `{ "vertices": ["grid", "Bus"], "radius": 4 }`. #[derive(Clone, PartialEq, Debug)] #[non_exhaustive] pub enum Mark { @@ -23,7 +23,7 @@ pub enum Mark { } impl<'de> serde::Deserialize<'de> for Mark { - /// Reads the options whole, since the key naming the kind can come after them. + /// Reads all the options first, since the key naming the kind can come after them. fn deserialize>(deserializer: D) -> std::result::Result { use serde::de::Error as _; let form = serde_json::Map::deserialize(deserializer)?; @@ -58,7 +58,7 @@ impl schemars::JsonSchema for Mark { generator.subschema_for::(), ]; schemars::json_schema!({ - "description": "One table a scene draws. The key naming the mark's kind holds its dataset and table, beside its options: `{ vertices: [data, table], radius: 4 }`.", + "description": "One table a scene draws. The mark's kind is the key that holds its table, next to its options: `{ vertices: [data, table], radius: 4 }`.", "oneOf": kinds, }) } @@ -94,7 +94,7 @@ impl From for Mark { } } -/// The default name of the field a mark reads geometry from, as geographic tables name it. +/// The default name of the field a mark reads geometry from, the name geographic tables use. pub(crate) const GEOMETRY: &str = "geometry"; /// Options every mark has. @@ -143,7 +143,7 @@ impl Stroke { } /// Compiles the dash, flow, and width channels. A width is in the space's unit unless it names - /// its own; the theme's width is always in the space's unit. + /// its own unit. The theme's width is always in the space's unit. pub(crate) fn compile(&self, c: &mut Compiler<'_>, words: &StrokeWords, diagram: bool) -> Result<()> { c.channel("dash", &self.dash, Store::Flag { word: words.flags, bit: wgsl::DASH })?; c.channel("flow", &self.flow, Store::Number { word: words.flow, fallback: None })?; @@ -158,7 +158,7 @@ impl Stroke { /// How a mark's rows respond to the pointer. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) struct Interaction { - /// Whether rows respond to hover, clicks, and picks; unset, the mark's kind decides (see + /// Whether rows respond to hover, clicks, and picks. When unset, the mark's kind decides (see /// [`Mark::picks`]). pub(crate) pick: Option, pub(crate) select: bool, @@ -210,7 +210,7 @@ impl MarkKind { } } - /// The code shaders tell it by. + /// The code shaders identify it by. pub(crate) fn code(self) -> u32 { match self { MarkKind::Vertices => wgsl::MARK_VERTICES, @@ -232,8 +232,9 @@ impl MarkKind { } } - /// The shaders that draw its rows: every row, and the rows its focus lights, which faces lack: - /// a face shows focus by its color alone. With `custom`, a vertex mark's custom marker's. + /// The shader that draws its rows, and the shader that draws its focused rows. Faces have no + /// focus shader, since a face shows focus by its color alone. With `custom`, the shaders for a + /// vertex mark's custom marker. pub(crate) fn shaders(self, custom: bool) -> (wgsl::Shader, Option) { use wgsl::Shader; match self { @@ -245,8 +246,8 @@ impl MarkKind { } } - /// The bounds shader's entry point that measures its extents for a fit; `None` for edges and - /// faces, whose rows lie between the vertices that frame them. + /// The bounds shader entry point that measures its extents for a fit. `None` for edges and + /// faces, since their rows lie between their vertices. pub(crate) fn bounds(self) -> Option<&'static str> { match self { MarkKind::Vertices => Some("points"), @@ -268,7 +269,7 @@ impl MarkKind { } } -/// Identifies a mark across scenes: a new scene's mark with the same key eases from the old one. +/// Identifies a mark across scenes. A mark in a new scene with the same key eases from the old one. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub(crate) struct Key { pub(crate) kind: MarkKind, @@ -314,7 +315,7 @@ pub(crate) struct Markers { /// The number of inputs, and the extra word holding the first. pub(crate) inputs: u32, pub(crate) input: u32, - /// A pie's slice colors, packed; for an icon marker, its image layers, filled in when the icons + /// A pie's packed slice colors. For an icon marker, its image layers, filled in when the icons /// are packed. pub(crate) palette: Vec, /// An icon marker's images. @@ -332,7 +333,7 @@ pub(crate) struct Labeling { pub(crate) column: u32, pub(crate) size: f32, pub(crate) max_width: Option, - /// The font for the text: the label's, else the theme's. + /// The label's font, or the theme's when the label sets none. pub(crate) font: std::sync::Arc, } @@ -351,8 +352,8 @@ impl Context<'_> { matches!(self.space, Space::Diagram(_)) } - /// Whether the scene's layout places vertex rows without a position outside a diagram, on a - /// plane or the earth. + /// Whether the scene's layout places vertex rows without a position on a plane or the earth + /// (diagrams are not counted). pub(crate) fn placed(&self) -> bool { matches!(self.space, Space::Plane(_) | Space::Earth(_)) && !matches!(self.layout, super::Layout::None) } @@ -371,9 +372,9 @@ pub(crate) struct Drawing { pub(crate) markers: Markers, /// The program column a line traces, or `NONE`. pub(crate) trace: u32, - /// Program columns, `NONE` when absent: the geometry a stroke's path runs through (a diagram - /// route, bends, or a line's path) or an area's triangles; and each row's lane among edges - /// joining the same two vertices. + /// Program columns, `NONE` when absent. `geometry` holds the points a stroke's path runs + /// through (a diagram route, bends, or a line's path) or an area's triangles. `lane` holds each + /// row's lane among edges joining the same two vertices. pub(crate) geometry: u32, pub(crate) lane: u32, pub(crate) label: Option, @@ -448,7 +449,7 @@ impl Mark { }) } - /// The error when `data`, the dataset this mark names, lacks the mark's table; `None` when the + /// The error when `data`, the dataset this mark names, lacks the mark's table. `None` when the /// table exists. pub(crate) fn absent_from(&self, data: &crate::Snapshot) -> Option { let style = self.style(); @@ -469,8 +470,8 @@ impl Mark { } /// The path a stroke mark's rows follow, or where a triangle's corners come from: one of the - /// `PATH_` codes, which its pipelines are built for. A net's star and every wire in a diagram - /// follow the route the view computes. On a plot a line traces its samples, and elsewhere it + /// `PATH_` codes its pipelines are built for. A net's star and every wire in a diagram follow + /// the route the view computes. On a plot a line traces its samples, and elsewhere it /// follows its path field. Faces take their corners from the vertices they join, and vertices /// have no path, so both read `PATH_ENDS`. pub(crate) fn path(&self, space: &Space) -> u32 { @@ -619,9 +620,9 @@ pub(crate) fn described(lines: &[&str]) -> String { const MARK_OPTIONS: [&str; 9] = ["color", "visible", "priority", "name", "pick", "select", "drag", "label", "legend"]; const STROKE_OPTIONS: [&str; 3] = ["width", "dash", "flow"]; -/// A mark's schema from its form's: the options every mark has, and when `stroked` the stroke -/// options, moved to the `MarkOptions` and `StrokeOptions` definitions it extends, so each is -/// described once. +/// Builds a mark's schema from its form's schema. The options every mark has move to a +/// `MarkOptions` definition and, when `stroked`, the stroke options move to `StrokeOptions`. The +/// mark's schema extends both, so each option is described once. pub(crate) fn extending( generator: &mut schemars::SchemaGenerator, mut form: schemars::Schema, @@ -632,14 +633,15 @@ pub(crate) fn extending( let about = "The options of marks drawn as strokes: edges and lines."; parts.push(shared(generator, &mut form, "StrokeOptions", about, &STROKE_OPTIONS)); } - // The whole refuses keys no part has; a part alone cannot, since it lacks the others' keys. + // The combined schema rejects keys no part has. A part alone cannot, since it lacks the other + // parts' keys. form.remove("additionalProperties"); parts.push(form.into()); schemars::json_schema!({ "allOf": parts, "unevaluatedProperties": false }) } -/// Moves `keys` from `form`'s properties into definition `name`, described as `about`, which the -/// first mark to share it defines. Returns a reference to it. +/// Moves `keys` from `form`'s properties into the definition `name`, described as `about`. The +/// first mark to share the definition creates it. Returns a reference to the definition. fn shared( generator: &mut schemars::SchemaGenerator, form: &mut schemars::Schema, @@ -676,10 +678,10 @@ fn shared( json!({ "$ref": format!("#/$defs/{name}") }) } -/// Declares a mark type: a struct holding the options every mark has, the stroke options when it -/// draws strokes, and its own options; and its flat JSON form, with the key `$key` naming the -/// kind, dataset, and table, which it deserializes from and derives its schema from. Each option is -/// declared once, here. +/// Declares a mark type and its flat JSON form. The struct holds the options every mark has, the +/// stroke options when it draws strokes, and its own options. In the JSON form, the key `$key` +/// names the kind, dataset, and table. The type deserializes from the form and takes its schema +/// from it. Each option is declared once, here. macro_rules! mark { ($(#[doc = $line:literal])* $name:ident($key:literal) $(+ $stroke:ident)? { $($(#[$doc:meta])* $field:ident: $ty:ty $(as $ts:literal)? $(= $default:literal)?),* $(,)? @@ -724,15 +726,15 @@ macro_rules! mark { }; (@form $key:literal + stroke { $($(#[$doc:meta])* $field:ident: $ty:ty $(as $ts:literal)? $(= $default:literal)?),* }) => { $crate::scene::mark::mark!(@struct $key { $($(#[$doc])* $field: $ty $(as $ts)? $(= $default)?),* } { - /// Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) - /// unless given with `px` or `units`. The theme's width unless set. + /// Each row's width in CSS pixels, or units in a diagram, unless given with `px` or + /// `units`. Defaults to the theme's width. width: Option<$crate::Size>, - /// Whether each row is dashed, with the theme's `dash` period. Default `false`. + /// Whether each row is dashed, using the theme's `dash` period. Defaults to `false`. #[serde(default = "crate::scene::mark::no")] dash: $crate::Expr, - /// The speed of comets flowing along each row, per second, in the unit of the theme's - /// `flow` lengths. Negative speeds flow backward; 0, the default, draws no comets. When - /// the speed changes, comets continue from where they are. + /// The speed of comets along each row, per second, in the unit of the theme's `flow` + /// lengths. Negative speeds flow backward. 0 draws no comets. Defaults to 0. When the + /// speed changes, comets continue from where they are. #[serde(default = "crate::scene::mark::still")] flow: $crate::Expr, }); @@ -755,36 +757,37 @@ macro_rules! mark { of: ($crate::Name, $crate::Name), $($(#[$doc])* $(#[schemars(extend("tsType" = $ts))])? #[serde(default $(= $default)?)] $field: $ty,)* $($stroke)* - /// Each row's color: a CSS color, a value through a colormap, or a conditional choosing - /// between colors. Unset, rows take the theme's default for the mark; faces instead - /// blend their corners' colors. + /// Each row's color: a CSS color, a value through a colormap, or a conditional that + /// picks between colors. Defaults to the theme's color for the mark. Faces default to a + /// blend of their corners' colors. color: Option<$crate::Expr>, - /// Whether each row is drawn and can be picked. Default `true`. + /// Whether each row is drawn and can be picked. Defaults to `true`. #[serde(default = "crate::scene::mark::yes")] visible: $crate::Expr, - /// Each row's priority, from its data alone (no view state). Higher-priority rows draw - /// on top, their labels win where labels would overlap, and they are the last dropped - /// when a table too large to draw whole is thinned to the tiles in view. + /// Each row's priority. It reads only data, not view state. Higher rows draw on top, + /// keep their labels where labels overlap, and are dropped last when a large table is + /// thinned. priority: Option<$crate::Expr>, /// Distinguishes two marks of the same table across scenes, and names the mark in hits. name: Option<$crate::Name>, - /// Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and - /// plot traces do, and geometry (areas, and lines along paths such as coasts, borders, - /// and roads) does not. + /// Whether rows respond to hover, clicks, and picks. Defaults to on for vertices, + /// edges, faces, and plot traces, and off for geometry: areas, and lines along paths + /// such as coasts, borders, and roads. pick: Option, - /// Whether clicking a row selects it. Default `true`. + /// Whether clicking a row selects it. Defaults to `true`. #[serde(default = "crate::scene::mark::on")] select: bool, - /// Whether pressing and moving a row drags it, reported as - /// [`Event::Drag`](crate::Event), instead of panning the camera. Default `false`. + /// Whether pressing and moving a row drags it instead of panning the camera. Drags are + /// reported as [`Event::Drag`](crate::Event). Defaults to `false`. #[serde(default)] drag: bool, /// Each row's label: a text field, or label options. label: Option<$crate::Label>, - /// A legend of the mark's colors at the view's bottom right, under this title: a ticked - /// colormap across its domain, a cell for each category, or a swatch beside each name - /// of a map's `categories`. `color` must map through a colormap with a fixed domain or - /// named categories; in a conditional, `otherwise` is searched before the cases. + /// A legend of the mark's colors at the view's bottom right, with this title. It shows + /// a ticked colormap across its domain, a cell for each category, or a swatch for each + /// name in the colormap's `categories`. `color` must map through a colormap with a + /// fixed domain or named categories. In a conditional, `otherwise` is searched before + /// the cases. legend: Option>, } @@ -834,24 +837,24 @@ macro_rules! style_methods { ($mark:ty) => { impl $mark { /// Sets each row's color: a [`Color`](crate::Color), a value through a colormap, or a - /// conditional choosing between colors. Unset, rows take the theme's default for the - /// mark; faces instead blend their corners' colors. + /// conditional that picks between colors. Defaults to the theme's color for the mark. + /// Faces default to a blend of their corners' colors. #[must_use] pub fn color(mut self, color: impl Into<$crate::Expr>) -> Self { self.style.color = Some(color.into()); self } - /// Sets whether each row is drawn and can be picked; all rows by default. + /// Sets whether each row is drawn and can be picked. Defaults to all rows. #[must_use] pub fn visible(mut self, visible: impl Into<$crate::Expr>) -> Self { self.style.visible = visible.into(); self } - /// Sets each row's priority, from its data alone (no view state). Higher-priority rows - /// draw on top, their labels win where labels would overlap, and they are the last - /// dropped when a table too large to draw whole is thinned to the tiles in view. + /// Sets each row's priority. It reads only data, not view state. Higher rows draw on + /// top, keep their labels where labels overlap, and are dropped last when a large table + /// is thinned. #[must_use] pub fn priority(mut self, priority: impl Into<$crate::Expr>) -> Self { self.style.priority = Some(priority.into()); @@ -866,24 +869,24 @@ macro_rules! style_methods { self } - /// Sets whether rows respond to hover, clicks, and picks. Unset, vertices, edges, - /// faces, and plot traces do, and geometry (areas, and lines along paths such as - /// coasts, borders, and roads) does not. + /// Sets whether rows respond to hover, clicks, and picks. Defaults to on for vertices, + /// edges, faces, and plot traces, and off for geometry: areas, and lines along paths + /// such as coasts, borders, and roads. #[must_use] pub fn pick(mut self, on: bool) -> Self { self.style.interaction.pick = Some(on); self } - /// Sets whether clicking a row selects it; on by default. + /// Sets whether clicking a row selects it. Defaults to on. #[must_use] pub fn select(mut self, on: bool) -> Self { self.style.interaction.select = on; self } - /// Sets whether pressing and moving a row drags it instead of panning the camera; off - /// by default. + /// Sets whether pressing and moving a row drags it instead of panning the camera. + /// Defaults to off. #[must_use] pub fn drag(mut self, on: bool) -> Self { self.style.interaction.drag = on; @@ -897,11 +900,11 @@ macro_rules! style_methods { self } - /// Draws a legend of the mark's colors at the view's bottom right, under `title`: a - /// ticked colormap across its domain, a cell for each category, or a swatch beside each - /// of the [named categories](crate::Colormap::categories). The color must map through a - /// colormap with a fixed domain or named categories; in a conditional, `otherwise` is - /// searched before the cases. Legends stack upward in mark order. + /// Draws a legend of the mark's colors at the view's bottom right, with `title`. It + /// shows a ticked colormap across its domain, a cell for each category, or a swatch for + /// each of the [named categories](crate::Colormap::categories). The color must map + /// through a colormap with a fixed domain or named categories. In a conditional, + /// `otherwise` is searched before the cases. Legends stack upward in mark order. #[must_use] pub fn legend(mut self, title: impl Into>) -> Self { self.style.legend = Some(title.into()); @@ -915,24 +918,24 @@ macro_rules! style_methods { macro_rules! stroke_methods { ($mark:ty) => { impl $mark { - /// Sets each row's width, in the space's unit (units in a diagram, CSS pixels - /// elsewhere) unless given with [`px`](crate::px) or [`units`](crate::units). + /// Sets each row's width in CSS pixels, or units in a diagram, unless given with + /// [`px`](crate::px) or [`units`](crate::units). #[must_use] pub fn width(mut self, width: impl Into<$crate::Size>) -> Self { self.stroke.width = Some(width.into()); self } - /// Sets whether each row is dashed, with the theme's dash period. + /// Sets whether each row is dashed, using the theme's dash period. #[must_use] pub fn dash(mut self, dash: impl Into<$crate::Expr>) -> Self { self.stroke.dash = dash.into(); self } - /// Sets the speed of comets flowing along each row, per second, in the unit of the - /// theme's [`flow`](crate::FlowTheme) lengths. Negative speeds flow backward, and 0 - /// draws no comets. When the speed changes, comets continue from where they are. + /// Sets the speed of comets along each row, per second, in the unit of the theme's + /// [`flow`](crate::FlowTheme) lengths. Negative speeds flow backward. 0 draws no + /// comets. When the speed changes, comets continue from where they are. #[must_use] pub fn flow(mut self, speed: impl Into<$crate::Expr>) -> Self { self.stroke.flow = speed.into(); diff --git a/crates/latkit/src/scene/marker.rs b/crates/latkit/src/scene/marker.rs index e5432389..f757d30b 100644 --- a/crates/latkit/src/scene/marker.rs +++ b/crates/latkit/src/scene/marker.rs @@ -9,25 +9,26 @@ use crate::gpu::shaders as wgsl; use crate::{Color, Expr, Image}; /// How a vertex is drawn: a shape's name, `"block"`, or -/// `{ shape | block | gauge | pie | icon | wgsl: options }`. An ellipse by default. +/// `{ shape | block | gauge | pie | icon | wgsl: options }`. Defaults to an ellipse. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(remote = "Self", rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = preset_schema::)] pub enum Marker { /// A filled shape. Shape(Shape), - /// A rectangle with rounded corners that holds its label inside, as a diagram's blocks do. + /// A rectangle with rounded corners that holds its label inside, like a diagram's blocks. Block { - /// Corner radius in the space's unit (units in a diagram, CSS pixels elsewhere); the - /// theme's `diagram.corner` unless set. + /// Corner radius in CSS pixels, or units in a diagram. Defaults to the theme's + /// `diagram.corner`. #[serde(default)] corner: Option, }, - /// A shape's outline, filled to `fill` from 0 to 1: around a circle, or upward in other shapes. + /// A shape's outline, filled to `fill` (0 to 1). A circle fills around its edge, and other + /// shapes fill upward. Gauge { /// How full each row's gauge is, from 0 to 1. fill: Expr, - /// The outline's shape; an ellipse by default. + /// The outline's shape. Defaults to an ellipse. #[serde(default)] shape: Shape, }, @@ -37,11 +38,11 @@ pub enum Marker { slices: Vec, /// Each slice's color. colors: Vec, - /// The hole in the middle, as a fraction of the radius; 0 by default. + /// The hole in the middle, as a fraction of the radius. Defaults to 0. #[serde(default)] hole: f32, }, - /// One of `images`, fitted to the marker's box; `index` picks the image for each row. + /// One of `images`, fitted to the marker's box. `index` picks the image for each row. Icon { /// The images. images: Vec, diff --git a/crates/latkit/src/scene/mod.rs b/crates/latkit/src/scene/mod.rs index 1b7a9c6e..bf364a32 100644 --- a/crates/latkit/src/scene/mod.rs +++ b/crates/latkit/src/scene/mod.rs @@ -1,5 +1,5 @@ -//! Scenes: plain-data descriptions of a view's content. Marks name their datasets, and the view -//! draws them from the sources it is given. +//! Scenes: plain data that describes what a view draws. Each mark names a dataset, and the view +//! draws it from the sources it is given. pub(crate) mod areas; pub(crate) mod compile; @@ -66,17 +66,18 @@ pub struct Scene { /// The marks, in drawing order: each draws over the marks before it. #[serde(default)] pub(crate) marks: Vec, - /// How the view places vertex rows that have no position: a diagram's blocks, or points on a - /// plane or the earth. `none` by default. + /// How the view places vertex rows that have no position, such as a diagram's blocks or points + /// on a plane or the earth. Defaults to `none`. #[serde(default)] pub(crate) layout: Layout, - /// Default colors and sizes, and the look of text, focus, and each space; `dark` by default. + /// Default colors and sizes, and the look of text, focus, and each space. Defaults to `dark`. #[serde(default)] pub(crate) theme: Theme, /// How the view responds to the pointer and keyboard. #[serde(default)] pub(crate) controls: Controls, - /// Seconds over which scene and data changes animate; 0.3 by default, 0 for none. + /// How many seconds scene and data changes take to animate. 0 turns animation off. Defaults to + /// 0.3. #[serde(default = "ease", deserialize_with = "form::seconds")] #[schemars(with = "f64")] pub(crate) ease: Duration, @@ -115,8 +116,8 @@ impl Scene { self } - /// Sets the layout that places vertex rows without a position: a diagram's blocks, or points on - /// a plane or the earth. + /// Sets the layout that places vertex rows without a position, such as a diagram's blocks or + /// points on a plane or the earth. #[must_use] pub fn layout(mut self, layout: impl Into) -> Scene { self.layout = layout.into(); @@ -137,7 +138,8 @@ impl Scene { self } - /// Sets how long scene and data changes animate; zero applies them at once. Default 0.3 s. + /// Sets how long scene and data changes take to animate. Zero applies them at once. Defaults to + /// 0.3 s. #[must_use] pub fn ease(mut self, over: Duration) -> Scene { self.ease = over; @@ -163,12 +165,12 @@ impl Scene { &self.space } - /// Checks the scene against `sources` without a GPU, with the same compiler a view uses. + /// Checks the scene against `sources` without a GPU, using the same compiler as a view. /// - /// Every drawn dataset and table must be present, except datasets that a [`Source::Tiles`] - /// gives: a view fetches those while drawing, so their marks are checked as tiles arrive. - /// Errors on duplicate source names, missing datasets or tables, options a mark cannot compile, - /// and anything else the scene itself gets wrong, such as two marks with one key. + /// Every drawn dataset and table must be present, except datasets from a [`Source::Tiles`]. A + /// view fetches those while drawing, so their marks are checked as tiles arrive. Errors on + /// duplicate source names, missing datasets or tables, options a mark cannot compile, and other + /// mistakes in the scene itself, such as two marks with one key. pub fn validate(&self, sources: &[Source]) -> Result<()> { self.check()?; for (i, source) in sources.iter().enumerate() { @@ -206,10 +208,10 @@ impl Scene { Ok(()) } - /// Checks the scene's own consistency, which needs no data. Errors when a plot has a layout, - /// when the theme's comet spacing and length use different units, when two marks share a key - /// (they would be indistinguishable across scenes), when a mark uses an option its space lacks, - /// and when a legend has no colormap with a fixed domain or named categories to show. + /// Checks the scene on its own, without data. Errors when a plot has a layout, when the theme's + /// comet spacing and length use different units, when two marks share a key (they could not be + /// told apart across scenes), when a mark uses an option its space lacks, and when a legend has + /// no colormap with a fixed domain or named categories to show. pub(crate) fn check(&self) -> Result<()> { let diagram = matches!(self.space, Space::Diagram(_)); if matches!(self.space, Space::Plot(_)) && self.layout != Layout::None { diff --git a/crates/latkit/src/scene/port.rs b/crates/latkit/src/scene/port.rs index 295a89e7..72b04100 100644 --- a/crates/latkit/src/scene/port.rs +++ b/crates/latkit/src/scene/port.rs @@ -26,10 +26,10 @@ pub struct Port { /// Its name, drawn beside it inside the marker. #[serde(default)] pub label: Option, - /// Its shape; an ellipse by default. + /// Its shape. Defaults to an ellipse. #[serde(default)] pub shape: Shape, - /// Whether it is an output, which drives its net. Default `false`, an input. + /// Whether it is an output, which drives its net. Defaults to `false` (an input). #[serde(default)] pub output: bool, } diff --git a/crates/latkit/src/scene/space.rs b/crates/latkit/src/scene/space.rs index ea135fb7..8969bcea 100644 --- a/crates/latkit/src/scene/space.rs +++ b/crates/latkit/src/scene/space.rs @@ -6,16 +6,16 @@ use super::form::{preset_schema, presets}; /// The coordinate space of a scene's positions: a name, or /// `{ plane | diagram | earth | plot: options }`. /// -/// The space sets the unit of lengths that do not name their own: units in a diagram, where a unit -/// is a CSS pixel at zoom 0, and CSS pixels elsewhere. Only a diagram has ports, groups, nets, and -/// orthogonal routes. +/// The space sets the unit of lengths that do not name their own. In a diagram it is the diagram +/// unit, one CSS pixel at zoom 0. Elsewhere it is the CSS pixel. Only a diagram has ports, groups, +/// nets, and orthogonal routes. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(remote = "Self", rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = preset_schema::)] pub enum Space { /// Longitude and latitude in degrees, on a flat map or a globe. Earth(Earth), - /// Plane units, with elevation; the camera can turn and tilt. + /// Plane units, with elevation. The camera can turn and tilt. Plane(Plane), /// The data's axis horizontally and values vertically. Plot(Plot), @@ -60,27 +60,27 @@ impl From for Space { } } -/// The earth: positions are longitude and latitude in degrees. The ground takes the theme's `earth` +/// The earth: positions are longitude and latitude in degrees. The ground uses the theme's `earth` /// colors. /// -/// A flat map is one world, drawn once. Its ground ends at the antimeridian and at Web Mercator's -/// latitude limit, and the camera zooms out until the whole world fits the view and pans until the -/// world's edges meet the view's. On a globe, longitude wraps, and the camera zooms out until the -/// globe spans a third of the view's shorter side. +/// A flat map shows the world once. Its ground ends at the antimeridian and at Web Mercator's +/// latitude limit. The camera zooms out until the whole world fits the view, and pans until the +/// world's edges meet the view's edges. On a globe, longitude wraps, and the camera zooms out until +/// the globe spans a third of the view's shorter side. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct Earth { - /// A flat web mercator map (`mercator`, the default) or a globe (`globe`). + /// A flat web mercator map (`mercator`) or a globe (`globe`). Defaults to `mercator`. pub projection: Projection, /// Whether lines of longitude and latitude cross the ground, in the theme's `grid` color. - /// Default `true`. + /// Defaults to `true`. pub graticule: bool, /// Lights the earth by the sun's position this many seconds after the Unix epoch, plus the /// view's axis coordinate when it has one. The night side darkens the ground and everything on - /// it, down to the theme's `earth.night` brightness. Unset, the earth is lit evenly. + /// it, down to the theme's `earth.night` brightness. When unset, the earth is lit evenly. pub daylight: Option, /// Whether air glows around the globe, in the theme's `earth.atmosphere` color, brighter on the - /// day side. Default `false`. + /// day side. Defaults to `false`. pub atmosphere: bool, } @@ -147,7 +147,7 @@ impl Default for Earth { #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct Plane { - /// The grid drawn under the marks; lines 10 units apart by default. + /// The grid drawn under the marks. Defaults to lines 10 units apart. pub grid: Grid, } @@ -157,16 +157,16 @@ impl Default for Plane { } } -/// A flat sheet for diagrams, seen from straight above; the camera pans and zooms but does not -/// turn. Positions are in units with y down. Lengths are in units too, a unit being a CSS pixel at -/// zoom 0: sizes, padding, ports, strokes, text, and the theme's lengths. Sizes given with `px`, -/// the text halo, the focus glow, and the pick radius stay in CSS pixels. Zooming scales the whole -/// picture. Only a diagram has ports, groups, nets, and orthogonal routes, and the scene's layout -/// places its blocks. +/// A flat sheet for diagrams, seen from straight above. The camera pans and zooms but does not +/// turn. Positions are in units with y down, and a unit is a CSS pixel at zoom 0. Sizes, padding, +/// ports, strokes, text, and the theme's lengths are in units too. Sizes given with `px`, the text +/// halo, the focus glow, and the pick radius stay in CSS pixels. Zooming scales the whole picture. +/// Only a diagram has ports, groups, nets, and orthogonal routes, and the scene's layout places its +/// blocks. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct Diagram { - /// The grid drawn under the marks; dots 20 units apart by default. + /// The grid drawn under the marks. Defaults to dots 20 units apart. pub grid: Grid, } @@ -187,11 +187,11 @@ pub enum Grid { /// Units between lines. pitch: f64, }, - /// Dots `pitch` units apart; with `snap`, a diagram's drags snap to them. + /// Dots `pitch` units apart. With `snap`, drags in a diagram snap to them. Dots { /// Units between dots. pitch: f64, - /// Whether drag positions snap to the dots, in a diagram. Default `false`. + /// Whether drag positions snap to the dots, in a diagram. Defaults to `false`. #[serde(default)] snap: bool, }, @@ -213,19 +213,19 @@ pub struct Plot { pub struct Axis { /// The axis title. pub label: Option, - /// Whether grid lines cross the plot at the ticks. Default `true`. + /// Whether grid lines cross the plot at the ticks. Defaults to `true`. pub grid: bool, - /// Whether the axis is hidden. Default `false`. + /// Whether the axis is hidden. Defaults to `false`. pub hidden: bool, - /// Tick positions, each a value or `[value, label]`; a tick without a label shows its value. - /// Unset, ticks fall at round values. + /// Tick positions, each a value or `[value, label]`. A tick without a label shows its value. + /// When unset, ticks fall at round values. #[serde(deserialize_with = "ticks")] #[schemars(with = "Option>")] pub ticks: Ticks, - /// How tick values are formatted; `auto` by default. + /// How tick values are formatted. Defaults to `auto`. pub format: Format, - /// Decimal places of fixed values, or of the significand in scientific and engineering formats, - /// at most 15. Unset, as many as the tick step needs. + /// Decimal places of fixed values, or of the significand in scientific and engineering formats. + /// At most 15. When unset, as many as the tick step needs. #[schemars(range(max = 15))] pub precision: Option, } diff --git a/crates/latkit/src/scene/theme.rs b/crates/latkit/src/scene/theme.rs index 648a22fa..6fd725b3 100644 --- a/crates/latkit/src/scene/theme.rs +++ b/crates/latkit/src/scene/theme.rs @@ -8,13 +8,12 @@ use serde_json::Value; use super::form::{Presets, preset_schema, presets}; use crate::Color; -/// Default colors and sizes for marks whose options leave them unset or missing, and the look of -/// text, focus, comets, and each space, grouped by subject. Write a base theme's name (`"dark"` or -/// `"light"`), or options over a base (`base`, dark by default); a group's options override only -/// the base values they set. +/// Default colors and sizes for marks, and the look of text, focus, comets, and each space, grouped +/// by subject: a base theme's name (`"dark"` or `"light"`), or options that change a base. `base` +/// names the base and defaults to `dark`. Each option replaces only the base value it sets. /// -/// Lengths are in the space's unit (units in a diagram, CSS pixels elsewhere), except the text halo -/// and the focus glow, which are always in CSS pixels. +/// Lengths are in CSS pixels, or units in a diagram. The text halo and the focus glow are always in +/// CSS pixels. #[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] #[serde(remote = "Self", default, rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = with_base)] @@ -23,7 +22,7 @@ pub struct Theme { pub background: Color, /// The color of a plane's or diagram's grid, a plot's grid lines, and the earth's graticule. pub grid: Color, - /// The color a colormap gives a missing value, such as a row whose field has none. + /// The color a colormap gives a missing value, such as a row with no value in the field. pub missing: Color, /// The dash period: one dash and one gap of equal length. pub dash: f32, @@ -82,19 +81,20 @@ pub struct VertexTheme { #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = merged)] pub struct EdgeTheme { - /// The edge color. `None` blends the colors of each edge's two vertices; a diagram's wires and - /// edges without `ends` (nets and poles) then take the text color. + /// The edge color. `None` blends the colors of each edge's two vertices. With `None`, a + /// diagram's wires and edges without `ends` (nets and poles) use the text color. pub color: Option, /// The edge width, in the space's unit. pub width: f32, /// In a diagram, the distance between edges joining the same two vertices and between parallel /// orthogonal wires. Wires keep twice this distance from blocks where they can. pub spacing: f32, - /// Outside a diagram, how far edges joining the same two vertices bow apart: each lane by this - /// fraction of the edge's length at its middle. A family spreads at most eight lanes wide, and - /// more lanes pack closer. A bow is part of the drawing, so it scales with the zoom. + /// Outside a diagram, how far edges joining the same two vertices bow apart. Each lane bows out + /// by this fraction of the edge's length, measured at its middle. Parallel edges spread at most + /// eight lanes wide, and more lanes pack closer. Bows scale with the zoom. pub bow: f32, - /// How far an arrowhead extends beyond the edge's width: a head is this plus the width long. + /// How far an arrowhead extends beyond the edge's width. A head's length is this plus the + /// width. pub arrow: f32, } @@ -125,36 +125,36 @@ pub struct AreaTheme { pub struct FocusTheme { /// The glow color around hovered rows. pub hover: Color, - /// The glow color around selected rows; `None` glows in each row's own color. + /// The glow color around selected rows. `None` uses each row's own color. pub selected: Option, /// How far the hover and selection glows reach, in CSS pixels. pub glow: f32, /// The opacity of rows outside the selection while any row is selected, from 0 to 1. pub dim: f32, - /// The scale factor for a hovered vertex sized in CSS pixels; vertices sized in units only + /// The scale factor for a hovered vertex sized in CSS pixels. Vertices sized in units only /// glow. pub grow: f32, } -/// How comets flowing along edges and lines look: a round head, brightest at its middle, trailing a +/// How comets along edges and lines look. A comet has a round head, brightest at its middle, and a /// tail that thins and fades. /// -/// `spacing` and `length` must be in the same unit, which also measures each row's `flow` speed. In -/// position units (degrees on the earth), comets are part of the drawing: they scale with the zoom -/// and stay in place along their lines. In CSS pixels, they keep their on-screen size and speed. A -/// plain number is in the space's unit. +/// `spacing` and `length` must use the same unit, and each row's `flow` speed is in that unit too. +/// In position units (degrees on the earth), comets scale with the zoom and stay in place along +/// their lines. In CSS pixels, they keep their size and speed on screen. A plain number is in the +/// space's unit. #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = merged)] pub struct FlowTheme { - /// The comet color. Its alpha sets how strongly a comet covers its stroke; a fully transparent + /// The comet color. Its alpha sets how strongly a comet covers its stroke. A fully transparent /// color turns comets off. pub color: Color, /// The distance between consecutive comets. pub spacing: Length, /// The length of a comet's tail behind its head. pub length: Length, - /// The width of a comet's head, in the space's unit; never narrower than its stroke. + /// The width of a comet's head, in the space's unit. A head is never narrower than its stroke. pub width: f32, } @@ -165,9 +165,9 @@ pub struct FlowTheme { pub struct DiagramTheme { /// The fill color of group containers. pub group: Color, - /// The color of the one-unit border around blocks and groups; `None` draws no border. + /// The color of the one-unit border around blocks and groups. `None` draws no border. pub outline: Option, - /// The corner radius of blocks and groups; a block marker can set its own. + /// The corner radius of blocks and groups. A block marker can set its own. pub corner: f32, /// The width of ports. pub port_size: f32, @@ -184,9 +184,9 @@ pub struct DiagramTheme { #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(transform = merged)] pub struct ShadowTheme { - /// The color a shadow darkens toward; its alpha sets how dark the shadow is under the block. + /// The color a shadow darkens toward. Its alpha sets how dark the shadow is under the block. pub color: Color, - /// How far a shadow falls below its block or group; its edge softens over the same distance. + /// How far a shadow falls below its block or group. Its edge softens over the same distance. pub lift: f32, } @@ -212,9 +212,9 @@ pub struct PlotTheme { pub playhead: Color, } -/// A theme length: a plain number in the space's unit (units in a diagram, CSS pixels elsewhere), +/// A theme length: a plain number in the space's unit (CSS pixels, or units in a diagram), /// `{ "px": n }` in CSS pixels at any zoom, or `{ "units": n }` in position units (degrees on the -/// earth), which scale as the view zooms. +/// earth), which scale with the zoom. #[derive(Clone, Copy, PartialEq, Debug)] pub enum Length { /// In the space's unit. @@ -298,18 +298,18 @@ impl JsonSchema for Length { }) }; schemars::json_schema!({ - "description": "A length: a number in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in CSS pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the view zooms in.", + "description": "A length: a number in CSS pixels, or units in a diagram. `{ px }` is always CSS pixels. `{ units }` is position units (degrees on the earth), which grow as the view zooms in.", "anyOf": [ { "type": "number" }, - exactly("px", "CSS pixels, at any zoom."), - exactly("units", "Position units (degrees on the earth), which grow on screen as the view zooms in."), + exactly("px", "CSS pixels, the same at any zoom."), + exactly("units", "Position units (degrees on the earth), which grow as the view zooms in."), ], }) } } impl Theme { - /// Light marks on a dark background; the default theme. + /// Light marks on a dark background. The default theme. pub fn dark() -> Theme { let hex = |hex: &str| Color::hex(hex).expect("hex"); Theme { @@ -394,7 +394,7 @@ impl Default for Theme { presets!(Theme { "dark" => Theme::dark(), "light" => Theme::light() }; read by hand); -/// Reads a base theme's name, or options merged over the base's serialized form group by group: an +/// Reads a base theme's name, or options merged group by group over the base's serialized form. An /// omitted option keeps the base's value, and `null` clears an optional color. impl<'de> Deserialize<'de> for Theme { fn deserialize>(deserializer: D) -> Result { @@ -423,8 +423,8 @@ impl Serialize for Theme { } } -/// Merges `options` into `base` object by object, except that a length object replaces the base's -/// value whole. +/// Merges `options` into `base` object by object. A length object replaces the base's value +/// outright. fn over(base: &mut Value, options: Value) { match (base, options) { (Value::Object(base), Value::Object(options)) if !is_length(&options) => { @@ -450,7 +450,7 @@ fn merged(schema: &mut schemars::Schema) { fn with_base(schema: &mut schemars::Schema) { merged(schema); if let Some(properties) = schema.get_mut("properties").and_then(Value::as_object_mut) { - let base = serde_json::json!({ "description": "The theme these options change; dark unless set.", "enum": Theme::NAMES }); + let base = serde_json::json!({ "description": "The theme these options change. Defaults to `dark`.", "enum": Theme::NAMES }); properties.insert("base".into(), base); } preset_schema::(schema); diff --git a/crates/latkit/src/scene/vertices.rs b/crates/latkit/src/scene/vertices.rs index d45a51fe..7a61944a 100644 --- a/crates/latkit/src/scene/vertices.rs +++ b/crates/latkit/src/scene/vertices.rs @@ -17,25 +17,25 @@ mark!( /// .color(field("vm").colormap(Colormap::BALANCE).domain(0.95..=1.05)); /// ``` Vertices("vertices") { - /// Each row's x and y. Without a position, the point field that `geometry` names places rows, - /// and the scene's layout places rows that have neither. + /// Each row's x and y. Rows without a position are placed by the point field `geometry` names. + /// The scene's layout places rows that have neither. position: Option<[Expr; 2]>, - /// The point field that places rows when `position` is not set; the field named `geometry` by - /// default. + /// The point field that places rows when `position` is not set. Defaults to the field named + /// `geometry`. geometry: Option, - /// Height above the ground; on the earth, a fraction of the earth's radius. Default 0. Not + /// Height above the ground. On the earth, a fraction of the earth's radius. Defaults to 0. Not /// allowed in a diagram, which is flat. elevation: Expr = "crate::scene::mark::zero", - /// Each row's radius, in the space's unit (units in a diagram, CSS pixels elsewhere) unless - /// given with `px` or `units`; a diagram refuses `px`. The theme's `vertex.radius` unless set. + /// Each row's radius in CSS pixels, or units in a diagram, unless given with `px` or `units`. + /// A diagram does not accept `px`. Defaults to the theme's `vertex.radius`. radius: Option, - /// Each row's width and height, for markers that are not round, in the space's unit unless - /// given with `px` or `units`; a diagram refuses `px`. In a diagram, a block without a size - /// fits its label and ports. + /// Each row's width and height, for markers that are not round. In CSS pixels, or units in a + /// diagram, unless given with `px` or `units`. A diagram does not accept `px`. In a diagram, a + /// block without a size fits its label and ports. size: Option<[Size; 2]>, - /// How each row is drawn; an ellipse by default. + /// How each row is drawn. Defaults to an ellipse. marker: Marker, - /// A ring around each row's marker in this color; a missing or transparent color draws none. + /// A ring around each row's marker in this color. A missing or transparent color draws no ring. ring: Option, /// Ports on each row, one for each reference field that names a net. An `edges` mark of the net /// table, without `ends`, draws the nets. Diagrams only. @@ -45,7 +45,7 @@ Vertices("vertices") { /// Diagrams only. contains: Option<(Name, Name)> as "[table: string, reference: string]", /// Whether each block or group casts a shadow on the sheet, styled by the theme's - /// `diagram.shadow`. Default `false`. Diagrams only. + /// `diagram.shadow`. Defaults to `false`. Diagrams only. shadow: bool, }); @@ -67,9 +67,9 @@ impl Vertices { } } - /// Adds a port on each row for the net that reference field `reference` names, replacing any - /// earlier port for that field. An [`Edges`](crate::Edges) mark of the net table, without ends, - /// draws the nets. Diagrams only. + /// Adds a port on each row for the net named by the reference field `reference`. It replaces + /// any earlier port for that field. An [`Edges`](crate::Edges) mark of the net table, without + /// ends, draws the nets. Diagrams only. #[must_use] pub fn port(mut self, reference: impl Into, port: impl Into) -> Vertices { let reference = reference.into(); @@ -87,7 +87,7 @@ impl Vertices { self } - /// Sets the point field that places rows without a position; `geometry` by default. + /// Sets the point field that places rows without a position. Defaults to `geometry`. #[must_use] pub fn geometry(mut self, field: impl Into) -> Vertices { self.geometry = Some(field.into()); @@ -99,16 +99,16 @@ impl Vertices { self.geometry.as_deref().unwrap_or(GEOMETRY) } - /// Sets each row's x and y. Without a position, the point field named by - /// [`geometry`](Vertices::geometry) places rows, and the scene's layout places rows that have - /// neither, in a diagram or on a plane or the earth. + /// Sets each row's x and y. Rows without a position are placed by the point field named by + /// [`geometry`](Vertices::geometry). The scene's layout places rows that have neither, in a + /// diagram or on a plane or the earth. #[must_use] pub fn position(mut self, x: impl Into, y: impl Into) -> Vertices { self.position = Some([x.into(), y.into()]); self } - /// Sets the height above the ground; on the earth, a fraction of the earth's radius. Not + /// Sets the height above the ground. On the earth, a fraction of the earth's radius. Not /// allowed in a diagram, which is flat. #[must_use] pub fn elevation(mut self, z: impl Into) -> Vertices { @@ -116,32 +116,32 @@ impl Vertices { self } - /// Sets each row's radius, in the space's unit (units in a diagram, CSS pixels elsewhere) - /// unless given with [`px`](crate::px) or [`units`](crate::units); a diagram refuses `px`. + /// Sets each row's radius in CSS pixels, or units in a diagram, unless given with + /// [`px`](crate::px) or [`units`](crate::units). A diagram does not accept `px`. #[must_use] pub fn radius(mut self, radius: impl Into) -> Vertices { self.radius = Some(radius.into()); self } - /// Sets each row's width and height, for markers that are not round, in the space's unit - /// unless given with [`px`](crate::px) or [`units`](crate::units); a diagram refuses `px`. In a - /// diagram, a block without a size fits its label and ports. + /// Sets each row's width and height, for markers that are not round. In CSS pixels, or units + /// in a diagram, unless given with [`px`](crate::px) or [`units`](crate::units). A diagram does + /// not accept `px`. In a diagram, a block without a size fits its label and ports. #[must_use] pub fn size(mut self, width: impl Into, height: impl Into) -> Vertices { self.size = Some([width.into(), height.into()]); self } - /// Sets how each row is drawn; an ellipse by default. + /// Sets how each row is drawn. Defaults to an ellipse. #[must_use] pub fn marker(mut self, marker: impl Into) -> Vertices { self.marker = marker.into(); self } - /// Draws a ring around each row's marker in this color; a missing or transparent color draws - /// none. + /// Draws a ring around each row's marker in this color. A missing or transparent color draws + /// no ring. #[must_use] pub fn ring(mut self, color: impl Into) -> Vertices { self.ring = Some(color.into()); @@ -190,7 +190,7 @@ impl Vertices { ports.push((reference.clone(), port.clone(), column)); } let mut drawing = Drawing::new(MarkKind::Vertices); - // The marker's half-size stays within its size's pixel bounds; a `size` bounds the whole + // The marker's half-size stays within its size's pixel bounds. A `size` bounds the full // width, so its bounds are halved. drawing.size_px = match (&self.size, &self.radius) { (Some([w, _]), _) => w.px.map(|px| px / 2.0), @@ -214,8 +214,8 @@ impl Vertices { Ok(drawing) } - /// Compiles positions and sizes in a diagram, which its sheet computes in units from the data - /// alone. + /// Compiles positions and sizes in a diagram. The diagram's sheet computes them in units, from + /// data only. fn sheet(&self, c: &mut Compiler<'_>, group: bool) -> Result<()> { let mut sizes = self .radius @@ -239,8 +239,8 @@ impl Vertices { Ok(()) } - /// Compiles positions and sizes outside a diagram. Rows stand at their position, or where the - /// scene's layout places them from the data alone, and take their size, their radius, or the + /// Compiles positions and sizes outside a diagram. Rows sit at their position, or where the + /// scene's layout places them from data only. Their size is their `size`, their radius, or the /// theme's radius. Returns the flags: `SIZED` when rows have a width and a height. fn placed(&self, c: &mut Compiler<'_>, context: Context<'_>) -> Result { let at = wgsl::VERTEX_POSITION; diff --git a/crates/latkit/src/sheet/groups.rs b/crates/latkit/src/sheet/groups.rs index 425d5da9..3bc822b0 100644 --- a/crates/latkit/src/sheet/groups.rs +++ b/crates/latkit/src/sheet/groups.rs @@ -1,5 +1,5 @@ //! A diagram's groups. A group's box surrounds its members, padded, with its title band across the -//! top; a group with no placed members is centered at its own place. Each group keeps its members' +//! top. A group with no placed members is centered at its own place. Each group keeps its members' //! boxes indexed, so an edit refits only the groups above the vertices it moved. use std::cmp::Reverse; @@ -108,8 +108,8 @@ impl Groups { } } -/// Group `group`'s box around `held`, its members' extent, y down; at its own place when `held` is -/// empty. `None` when it has neither. +/// Group `group`'s box around `held`, its members' extent, y down. When `held` is empty, the box is +/// at the group's own place. `None` when it has neither. fn stand(group: &Vertex, held: Rect, framing: &Framing<'_>) -> Option { if valid(&held) { let (top, padding) = (framing.header[group.mark], framing.padding); @@ -119,7 +119,7 @@ fn stand(group: &Vertex, held: Rect, framing: &Framing<'_>) -> Option { center.map(|c| around(c, group.half)) } -/// A y-down box flipped to y up for routing; `EMPTY` for none. +/// A y-down box flipped to y up for routing. `EMPTY` for none. fn up(bounds: Option) -> Rect { bounds.map_or(EMPTY, |b| [b[0], -b[3], b[2], -b[1]]) } diff --git a/crates/latkit/src/sheet/mod.rs b/crates/latkit/src/sheet/mod.rs index 200bf6d3..7f39573a 100644 --- a/crates/latkit/src/sheet/mod.rs +++ b/crates/latkit/src/sheet/mod.rs @@ -3,14 +3,14 @@ //! //! The sheet depends only on the data and the scene, never on what was drawn, so the camera, //! hover, and motion never change it. Places persist by row: adding a row moves nothing else, and -//! a row that loses its position stays where it was. Layouts place only rows without a place until +//! a row that loses its position stays where it was. Layouts place only rows without a place, until //! `arrange` lays out every row without a position again. A wire routes again only when its //! terminals move or a box near it changes. Superlinear work too large for the frame (layouts, //! many routes) runs as a host job that answers with a patch. //! -//! The sheet is a derivation: it writes a table beside each table it draws, row for row, which -//! marks read like data. An edit of the values that size and place rows recomputes only the rows -//! it wrote and what they affect: their boxes, the groups around them, and the wires they move. +//! The sheet is a derivation. It writes a table beside each table it draws, row for row, and marks +//! read it like data. An edit of the values that size and place rows recomputes only the rows it +//! wrote and what they affect: their boxes, the groups around them, and the wires they move. mod groups; mod wires; @@ -171,7 +171,7 @@ impl Derive for Sheet { } impl Sheet { - /// Whether a layout job is pending; a fit waits for it. + /// Whether a layout job is pending. A fit waits for it. pub(crate) fn placing(&self) -> bool { self.placing.is_some() } @@ -196,7 +196,7 @@ impl Sheet { self.boxes.get(row).copied() } - /// The box around every placed vertex and every wire, in units with y down; `None` until + /// The box around every placed vertex and every wire, in units with y down. `None` until /// something is placed. Read from the indexes the sheet keeps of blocks, groups, and wires. pub(crate) fn extent(&self) -> Option<[f64; 4]> { let wires = self.drawn.as_ref()?.wired.extent(); @@ -216,7 +216,7 @@ impl Sheet { if let (Some(row), [Some(Value::Number(x)), Some(Value::Number(y))]) = (placing.rows.get(&key), &values[..]) { - // Layouts are y up; the sheet is y down. + // Layouts are y up and the sheet is y down. self.places.insert(row.clone(), [*x, -*y]); placed.push((row.clone(), [*x, -*y])); } @@ -305,7 +305,7 @@ impl Sheet { moved.extend(was.into_iter().chain(vertex.bounds)); } } - // Refit the groups above the touched rows; a group whose box changed counts as touched. + // Refit the groups above the touched rows. A group whose box changed counts as touched. let framing = groups::Framing { header: &header, padding, places: &self.places }; let regrouped = self.groups.refit(&mut drawn.vertices, &touched, &framing); if !regrouped.is_empty() { @@ -396,8 +396,8 @@ impl Sheet { } /// Half the width and height of `vm`'s row `row`, in units: from its size, its radius, or else - /// a box fitting its label and ports. A group's is its size when it holds nothing: room for its - /// title. + /// a box fitting its label and ports. For a group, it is the size when the group holds nothing, + /// with space for its title. fn half(&mut self, vm: &VertexMark<'_>, rows: &Rows<'_>, row: usize, input: &Input<'_>) -> [f64; 2] { let theme = &input.scene.theme; let radius = f64::from(theme.vertex.radius); @@ -472,7 +472,8 @@ impl Sheet { [width / 2.0, height / 2.0] } - /// The width and height of row `row`'s label in units, and its font size; `None` if no label. + /// The width and height of row `row`'s label in units, and its font size. `None` without a + /// label. fn label(&mut self, mark: &Vertices, rows: &Rows<'_>, row: usize, input: &Input<'_>) -> Option<([f64; 2], f64)> { let label = mark.style.label.as_ref()?; let text = rows.text(&label.text, row)?; @@ -509,9 +510,9 @@ impl Sheet { size } - /// Starts a layout job, unless one is pending, to place every block, and every group holding - /// nothing, that has no place (while arranging, no position). Other rows are pinned, and - /// groups lay out around their members. + /// Starts a layout job, unless one is pending, to place every block and empty group that has no + /// place (while arranging, no position). Other rows are pinned, and groups lay out around their + /// members. #[allow(clippy::too_many_arguments, reason = "one layout request")] fn place( &mut self, @@ -654,7 +655,7 @@ fn shape_of(mark: &Vertices) -> Shape { } } -/// The height in units of the band across the top of a group mark's boxes, where titles sit; 0 for +/// The height in units of the band across the top of a group mark's boxes, where titles sit. 0 for /// other marks, or without labels. fn header(mark: &Vertices, theme: &Theme) -> f64 { match (&mark.contains, &mark.style.label) { @@ -719,8 +720,8 @@ const VERTEX_FIELDS: [VertexField; 5] = [ ("depth", |v| Some(f64::from(v.depth))), ]; -/// Writes each drawn table's derived table, and drops those of tables no longer drawn: a vertex -/// table's centers, half sizes, and depths; an edge table's paths. A field equal to the previous +/// Writes each drawn table's derived table (a vertex table's centers, half sizes, and depths, or an +/// edge table's paths), and drops those of tables no longer drawn. A field equal to the previous /// one keeps its column, so nothing uploads again. fn write( vertex_marks: &[VertexMark<'_>], diff --git a/crates/latkit/src/sheet/wires.rs b/crates/latkit/src/sheet/wires.rs index c213775f..cc20f43a 100644 --- a/crates/latkit/src/sheet/wires.rs +++ b/crates/latkit/src/sheet/wires.rs @@ -1,8 +1,8 @@ //! Wires: the path of each edge row of a diagram, in units with y down. //! -//! An edge with endpoints runs between its two vertices; a net joins every port that references +//! An edge with endpoints runs between its two vertices. A net joins every port that references //! it, from its outputs. A straight wire runs between outlines, spread into lanes where edges join -//! the same two vertices; a straight net is a star through its middle. A wire with bends runs +//! the same two vertices. A straight net is a star through its middle. A wire with bends runs //! through the points of a path field. An orthogonal wire routes around every block. //! //! A route stays until its terminals move or a box near it changes. It then routes again on the @@ -33,10 +33,10 @@ use crate::{Pending, Route, Shape, Side}; /// fit routes in a host job. const ROUTE_COST: u64 = 160; /// The most wires routed on the frame per update while rows are dragged, regardless of budget. -/// The rest stay due and route in later updates; once the drag ends, those left route within the +/// The rest stay due and route in later updates. Once the drag ends, those left route within the /// frame's budget or in a host job. const DRAGGED: usize = 256; -/// How far wires keep from blocks, in tracks; the same clearance the router keeps. +/// How far wires keep from blocks, in tracks. The router keeps the same clearance. const CLEARANCE: f64 = 2.0; /// A wire's parts: its polylines. @@ -56,19 +56,19 @@ impl Routed { Routed { lines, terminals, reach } } - /// A wire waiting for its first route; its empty box meets nothing. + /// A wire waiting for its first route. Its empty box meets nothing. fn waiting() -> Routed { Routed { lines: Vec::new(), terminals: 0, reach: EMPTY } } } -/// Every orthogonal wire's route by edge row, each in its own slot; an index of each route's box by -/// slot, to find the routes near a box that moved; and the rows due to route again (near a box that -/// moved, or due before and not routed since). +/// Every orthogonal wire's route by edge row, each in its own slot. Also an index of each route's +/// box by slot, to find the routes near a box that moved, and the rows due to route again (near a +/// box that moved, or due before and not routed since). #[derive(Default)] pub(crate) struct Routes { by_row: FxHashMap, - /// The row each slot holds the route of; `None` where it is free. + /// The row whose route each slot holds. `None` where the slot is free. slots: Vec>, free: Vec, reaches: Boxes, @@ -138,9 +138,10 @@ impl Routes { } } -/// Wiring state kept between updates, so an edit recomputes only the wires it affects: each edge -/// mark's rows' parts and an index of their boxes, the edge and net rows meeting each vertex, each -/// edge mark's lanes, and the sides orthogonal edges with endpoints leave their vertices by. +/// Wiring state kept between updates, so an edit recomputes only the wires it affects. It holds the +/// parts of each edge mark's rows and an index of their boxes, the edge and net rows meeting each +/// vertex, each edge mark's lanes, and the sides by which orthogonal edges with endpoints leave +/// their vertices. #[derive(Default)] pub(crate) struct Wired { pub(crate) parts: Vec>, @@ -277,8 +278,9 @@ pub(crate) struct Change<'a> { pub(crate) touched: Option<&'a [usize]>, } -/// A pending routing job: each row it answers, by key, with its terminals' stamp when requested; -/// the boxes that moved since, which its routes may cross; and how far wires keep from them. +/// A pending routing job. It holds each row it answers, by key, with its terminals' stamp when +/// requested. It also holds the boxes that moved since, which its routes may cross, and how far +/// wires keep from them. pub(crate) struct Routing { pub(crate) pending: Pending>, rows: FxHashMap, @@ -286,8 +288,8 @@ pub(crate) struct Routing { clearance: f64, } -/// The inputs to wiring: the diagram's vertices, vertex marks, and edge marks; the blocks wires -/// route around (by vertex) and the groups they route among, y up; and, in units, the spacing of +/// The inputs to wiring: the diagram's vertices, vertex marks, and edge marks. The blocks wires +/// route around (by vertex) and the groups they route among, y up. In units, the spacing of /// parallel wires, the bend radius, and the port size. pub(crate) struct Context<'a> { pub(crate) vertices: &'a [Vertex], @@ -363,7 +365,7 @@ pub(crate) fn moved(before: &FxHashMap, vertices: &[Vertex]) -> Vec, @@ -453,7 +455,7 @@ pub(crate) fn paths( } } wire(context, input, jobs, &asks, change, (routes, routing), &mut wired.parts); - // Update the box of each computed row; the extent of all wires is kept from these boxes. + // Update the box of each computed row. The extent of all wires is kept from these boxes. if telling { let boxes = (0..edge_marks.len()).flat_map(|e| (0..edge_marks[e].table.len()).map(move |row| (e, row))); wired.boxes = Boxes::new(boxes.map(|(e, row)| wired.bounds(e, row)).collect()); @@ -496,9 +498,9 @@ fn every_row(context: &Context<'_>, joins: &[Joins<'_>], wired: &mut Wired) -> V edge_marks.iter().map(|em| (0..em.table.len()).collect()).collect() } -/// The rows an edit of vertices `touched` affects, by edge mark, sorted: rows meeting them; -/// orthogonal edges with endpoints leaving by a side whose ends changed; and orthogonal wires due -/// to route again or requested from the pending job. +/// The rows an edit of vertices `touched` affects, by edge mark, sorted. These are rows meeting +/// them, orthogonal edges with endpoints leaving by a side whose ends changed, and orthogonal wires +/// due to route again or requested from the pending job. fn reached( context: &Context<'_>, joins: &[Joins<'_>], @@ -513,7 +515,7 @@ fn reached( rows[e as usize].push(row as usize); } // An edge meeting a vertex the edit touched may now leave by other sides, or from other places - // along them; so may the edges leaving by a side it left or joined. + // along them. So may the edges leaving by a side it left or joined. let mut changed = Vec::new(); for (e, rows) in rows.iter_mut().enumerate().filter(|(e, _)| sided(&edge_marks[*e])) { rows.sort_unstable(); @@ -619,7 +621,7 @@ fn wire( let rows = keys.into_iter().zip(&due).map(|(key, &k)| (key, (asks[k].key.clone(), stamps[k]))).collect(); *routing = Some(Routing { pending, rows, moved: Vec::new(), clearance }); } - // Wires still due route later; until then they keep their routes if their terminals are + // Wires still due route later. Until then they keep their routes if their terminals are // unchanged. for &k in &due { routes.due(&asks[k].key); @@ -713,8 +715,8 @@ fn net(ask: &Ask, vertices: &[Vertex]) -> Net { Net { terminals: ask.ends.iter().map(terminal).collect() } } -/// The routes, y up, of wires within `reach` of the ends of the wires `routed`, which keep their -/// tracks while those route; ordered by edge mark, then row. +/// The routes, y up, of wires within `reach` of the ends of the wires `routed`. These keep their +/// tracks while `routed` route. Ordered by edge mark, then row. fn fixed(context: &Context<'_>, asks: &[Ask], routed: &[usize], routes: &Routes, reach: f64) -> Parts { let region = routed.iter().flat_map(|&k| &asks[k].ends).fold(EMPTY, |region, end| union(region, dot(end.at))); let routing: FxHashSet<&Row> = routed.iter().map(|&k| &asks[k].key).collect(); @@ -754,7 +756,7 @@ fn elbow(a: &End, b: &End, stub: f64) -> Vec { simplify(&path) } -/// The outward direction of a side, y down; zero for no side. +/// The outward direction of a side, y down. Zero for no side. fn normal(side: Option) -> Point { match side { Some(Side::Left) => [-1.0, 0.0], @@ -799,7 +801,7 @@ fn outline(v: &Vertex, d: Point) -> f64 { /// A straight net: two ends joined directly, or more joined through their centroid, with parts /// from outputs into it and from it out to inputs. A part into the centroid repeats its end there, -/// a junction without an arrowhead. Parts stop at the rims of the ports they join. +/// so it draws as a junction without an arrowhead. Parts stop at the rims of the ports they join. fn star(ends: &[End], outputs: usize) -> Parts { if let [a, b] = ends { let mut part = vec![rim_point(a, b.at), rim_point(b, a.at)]; @@ -858,7 +860,7 @@ fn center(b: Rect) -> Point { [b[0].midpoint(b[2]), b[1].midpoint(b[3])] } -/// The unit direction from `a` to `b`, and the distance; `None` where they coincide. +/// The unit direction from `a` to `b`, and the distance. `None` where they coincide. fn unit(a: Point, b: Point) -> Option<(Point, f64)> { let d = [b[0] - a[0], b[1] - a[1]]; let length = d[0].hypot(d[1]); diff --git a/crates/latkit/src/shot.rs b/crates/latkit/src/shot.rs index f7f1ae3e..eebb9327 100644 --- a/crates/latkit/src/shot.rs +++ b/crates/latkit/src/shot.rs @@ -29,8 +29,8 @@ pub(crate) const TICK: f64 = 1.0 / 60.0; /// The longest a picture waits for work to arrive before drawing again. const FRAME: Duration = Duration::from_millis(16); -/// A picture of views, taken once they are complete as its [`Capture`] says. Each view draws into -/// its panel of a `size`-pixel picture at `scale` pixels per CSS pixel. A shot is plain data, like +/// A picture of views, taken when they are complete, as its [`Capture`] says. Each view draws into +/// its panel of a `size`-pixel picture, at `scale` pixels per CSS pixel. A shot is plain data, like /// a scene. /// /// ```no_run @@ -116,7 +116,8 @@ impl Shot { self } - /// Sets each view's panel in the picture, one per view, in order. By default one view fills it. + /// Sets each view's panel in the picture, one per view, in order. Defaults to one view filling + /// the picture. #[must_use] pub fn panels(mut self, panels: impl Into) -> Shot { self.panels = panels.into(); @@ -130,8 +131,8 @@ impl Shot { self } - /// Sets when the picture is taken: once its views are complete and still (the default), or as - /// they are now. + /// Sets when the picture is taken: once its views are complete and still, or as they are now. + /// Defaults to [`Capture::Still`]. #[must_use] pub fn capture(mut self, capture: Capture) -> Shot { self.capture = capture; @@ -148,7 +149,7 @@ impl Shot { } /// Fixes elapsed-time expressions and animated drawing effects at `elapsed`, while finite - /// transitions still settle. By default they follow the view's clock. + /// transitions still settle. Defaults to following the view's clock. #[must_use] pub fn elapsed(mut self, elapsed: Duration) -> Shot { self.elapsed = Some(elapsed); diff --git a/crates/latkit/src/tiles/cache.rs b/crates/latkit/src/tiles/cache.rs index db2375fe..90336875 100644 --- a/crates/latkit/src/tiles/cache.rs +++ b/crates/latkit/src/tiles/cache.rs @@ -1,7 +1,7 @@ -//! The tile cache: each tile source a GPU's views draw, by URL. A source is opened once, its pieces -//! are fetched and decoded once for every set drawing them, and it is released once no set draws -//! it. Work finishing for a source wakes every set drawing it, so a piece one view fetched draws in -//! all of them. +//! The tile cache holds each tile source a GPU's views draw, by URL. A source is opened once, and +//! its pieces are fetched and decoded once for every set drawing them. It is released once no set +//! draws it. When work for a source finishes, it wakes every set drawing the source, so a piece one +//! view fetched draws in all of them. use std::collections::hash_map::Entry; use std::sync::Arc; @@ -16,8 +16,8 @@ use crate::host::Request; use crate::pending::Watchers; use crate::{Error, Pending}; -/// Bytes first read from an archive source (a URL ending in `.pmtiles`): its header, and in most -/// archives the root directory after it. Other sources are read whole. +/// Bytes first read from an archive source (a URL ending in `.pmtiles`). They hold its header and, +/// in most archives, the root directory after it. Other sources are read whole. const ARCHIVE_FIRST: u64 = 16_384; /// Every tile source a GPU's views draw, by URL. @@ -26,8 +26,8 @@ pub(crate) struct Cache { sources: FxHashMap, Fetched>, } -/// One tile source: its open state, the sets drawing it, and the waker that wakes them all, which -/// its work wakes as it finishes. +/// One tile source: its open state, the sets drawing it, and the waker that wakes them all as the +/// source's work finishes. pub(crate) struct Fetched { pub(crate) state: State, sets: Arc, @@ -36,7 +36,7 @@ pub(crate) struct Fetched { /// A source's open state. pub(crate) enum State { - /// Its first bytes are being fetched; they determine what it holds. + /// Its first bytes are being fetched. They determine what it holds. Opening(Pending>), Open(Holds), /// It could not be opened, with the reason. @@ -50,9 +50,9 @@ pub(crate) enum Holds { } impl Cache { - /// The source at `url`, registering set `set` as drawing it, to be woken by `jobs`'s waker as - /// the source's work finishes. Starts opening the source if no set drew it before, and opens it - /// as far as finished work allows. + /// The source at `url`, with set `set` registered as drawing it. `jobs`'s waker wakes the set + /// as the source's work finishes. Starts opening the source if no set drew it before, and opens + /// it as far as finished work allows. pub(crate) fn open(&mut self, url: &Arc, set: u64, jobs: &Jobs<'_>) -> &mut Fetched { let fetched = match self.sources.entry(url.clone()) { Entry::Occupied(entry) => entry.into_mut(), diff --git a/crates/latkit/src/tiles/frames.rs b/crates/latkit/src/tiles/frames.rs index 2e18094f..e970b539 100644 --- a/crates/latkit/src/tiles/frames.rs +++ b/crates/latkit/src/tiles/frames.rs @@ -1,9 +1,9 @@ -//! Time tiles: sampled fields' histories fetched in spans as the sets drawing them need, from the -//! files [`Snapshot::to_tiles`](crate::Snapshot::to_tiles) writes. Every field needs its coarsest -//! spans, which are drawn as placeholders for any span still loading. A line tracing a field needs -//! the spans of the level of detail its plot window draws at, middle first. A mark showing a field -//! at the view's coordinate needs the frames around that coordinate. A drawn span adds its entries -//! to the set's dataset, and a span no longer drawn removes them. +//! Time tiles: the histories of sampled fields, fetched in spans as the sets drawing them need, +//! from the files [`Snapshot::to_tiles`](crate::Snapshot::to_tiles) writes. Every field needs its +//! coarsest spans, which are drawn as placeholders for any span still loading. A line tracing a +//! field needs the spans at the level of detail the plot window draws at, middle first. A mark +//! showing a field at the view's coordinate needs the frames around that coordinate. A drawn span +//! adds its entries to the set's dataset, and a span no longer drawn removes them. use std::ops::Range; use std::sync::Arc; @@ -23,8 +23,8 @@ use crate::{Error, Pending}; /// Most spans a line needs, middle first. A plot window normally needs far fewer. const MAX_SPANS: usize = 64; -/// The spans of one manifest's sampled fields, shared by every set drawing them: the base dataset -/// without their entries, and each field's fetched and decoded spans. +/// The spans of one manifest's sampled fields, shared by every set drawing them. Holds the base +/// dataset without their entries, and each field's fetched and decoded spans. pub(crate) struct Fetched { manifest: Manifest, /// The spans' URL template, resolved against the manifest's URL. @@ -205,11 +205,11 @@ fn sampled<'a>(data: &'a Snapshot, listed: &Listed) -> Option<&'a Samples> { } } -/// The spans a view needs of a field covering `frames`, nearest first. These are always the spans -/// of its coarsest level holding any entry; when a mark shows it (`(_, true)`), the frames around -/// the shown coordinate; and when a line traces it (`(true, _)`), the spans of the level of detail -/// the plot window draws at, middle first. The window's position among the frames comes from the -/// samples already held, as precisely as they allow. +/// The spans a view needs of a field covering `frames`, nearest first. It always needs the spans +/// of the field's coarsest level holding any entry. When a mark shows the field (`(_, true)`), it +/// needs the frames around the shown coordinate. When a line traces it (`(true, _)`), it needs the +/// spans of the level of detail the plot window draws at, middle first. The window's position +/// among the frames comes from the samples already held, as precisely as they allow. fn wanted( frames: &Range, samples: Option<&Samples>, diff --git a/crates/latkit/src/tiles/mod.rs b/crates/latkit/src/tiles/mod.rs index bb2ce513..ea5f5fe7 100644 --- a/crates/latkit/src/tiles/mod.rs +++ b/crates/latkit/src/tiles/mod.rs @@ -1,9 +1,9 @@ //! Tiles: data fetched in pieces, by key, as views need them. Each key is covered by its parent, -//! up to a root: an earth tile by the tile one zoom out, and a span of a sampled field's history by -//! the span one level of detail up. Until a piece is fetched, its nearest fetched ancestor is drawn -//! as a placeholder. +//! up to a root. An earth tile's parent is the tile one zoom out. A span of a sampled field's +//! history has the span one level of detail up. Until a piece is fetched, its nearest fetched +//! ancestor is drawn as a placeholder. //! -//! A GPU fetches and decodes each source's pieces once for every view drawing them (`cache`, +//! A GPU fetches and decodes each source's pieces once for all the views drawing them (`cache`, //! `tiling`). Each view draws the pieces its camera needs as a dataset of its own (`sets`). mod cache; @@ -21,26 +21,27 @@ pub(crate) use sets::{Asked, Reading, TileSets}; use crate::data::Name; -/// A dataset fetched in pieces as views need them: a [`Source`](crate::Source) that each view draws -/// as a dataset of its own, holding the pieces its camera needs. All views of a GPU drawing tiles -/// from one URL share what is fetched. The source's first bytes determine the kind of tiles. +/// A dataset fetched in pieces as views need them. Each view draws this +/// [`Source`](crate::Source) as a dataset of its own, holding the pieces its camera needs. Views on +/// one GPU that draw tiles from the same URL share what is fetched. The source's first bytes +/// determine the kind of tiles. /// -/// **Time tiles**: a `frames.json` manifest as -/// [`Snapshot::to_tiles`](crate::Snapshot::to_tiles) writes it, for a dataset whose sampled fields' -/// histories are fetched in spans. A line tracing a field fetches the level of detail its plot -/// window draws at. A mark showing a field fetches the frames around the view's coordinate. A span -/// still loading draws the coarser span above it. +/// **Time tiles**: a `frames.json` manifest, as +/// [`Snapshot::to_tiles`](crate::Snapshot::to_tiles) writes it. The history of each sampled field +/// is fetched in spans. A line tracing a field fetches the level of detail the plot window draws +/// at. A mark showing a field fetches the frames around the view's coordinate. While a span loads, +/// the coarser span above it is drawn. /// /// **Vector tiles**: a PMTiles archive, a TileJSON document, or a URL template with `{z}`, `{x}`, -/// and `{y}`, fetched as the camera needs them. Each layer becomes a table of its features of the -/// geometry type its marks draw: polygons for [`Areas`](crate::Areas), lines for -/// [`Lines`](crate::Lines), and points for [`Vertices`](crate::Vertices). When marks of several -/// kinds draw one layer, polygons come first. A table named `layer:polygons`, `layer:lines`, or -/// `layer:points` holds that geometry type regardless of its marks. Each feature's properties -/// become fields. Row ids are stable, derived from the tile and the feature's index in it, so a -/// refetched tile keeps its rows' selection. A tile arriving appends its rows to the view's -/// dataset, which keeps the rows of tiles it no longer draws until they outnumber the rows it -/// draws. +/// and `{y}`. Tiles are fetched as the camera needs them. Each layer becomes a table of the +/// layer's features that have the geometry type its marks draw: polygons for +/// [`Areas`](crate::Areas), lines for [`Lines`](crate::Lines), and points for +/// [`Vertices`](crate::Vertices). When marks of several kinds draw one layer, polygons come first. +/// A table named `layer:polygons`, `layer:lines`, or `layer:points` holds that geometry type +/// whatever its marks. Each feature's properties become fields. Row ids are stable, derived from +/// the tile and the feature's index in it, so a refetched tile keeps its rows' selection. An +/// arriving tile appends its rows to the view's dataset. The dataset keeps the rows of tiles it no +/// longer draws until they outnumber the rows it draws. /// /// ``` /// # use latkit::Tiles; @@ -72,7 +73,7 @@ impl Tiles { /// A tiling key: one piece of a source, covered by its parent up to a root. pub(crate) trait Key: Copy + Eq + Hash + Ord + Debug { - /// The key covering this one, one level up; `None` at the root. + /// The key covering this one, one level up, or `None` at the root. fn parent(self) -> Option; } diff --git a/crates/latkit/src/tiles/sets.rs b/crates/latkit/src/tiles/sets.rs index 777afcfd..5505682e 100644 --- a/crates/latkit/src/tiles/sets.rs +++ b/crates/latkit/src/tiles/sets.rs @@ -1,7 +1,7 @@ //! Tile sets: for each tile source a view is given, a set whose dataset holds the pieces the view's //! camera and plot window need. Marks draw it like any other dataset, except that a vector set's -//! tables are always paged, and draw the vector tiles drawn as their tiles. The pieces come from -//! the GPU's cache, shared by every view drawing the same source. +//! tables are always paged, and their tiles are the vector tiles drawn. The pieces come from the +//! GPU's cache, shared by every view drawing the same source. use std::sync::atomic::{AtomicU64, Ordering}; @@ -13,9 +13,9 @@ use crate::derive::tiled::{Choices, Draw, Plans}; use crate::derive::{Input, Jobs}; use crate::{Error, Tiles}; -/// What a view reads of sampled fields: the plot window lines trace across (`across`) and its width -/// in pixel columns, the coordinate fields are shown at (`at`), and each field marks read, by -/// dataset, table, and name, with whether a line traces it. +/// What a view reads of sampled fields. It holds the plot window that lines trace across (`across`) +/// and its width in pixel columns, and the coordinate at which fields are shown (`at`). It also +/// holds each field that marks read, by dataset, table, and name, with whether a line traces it. #[derive(Default)] pub(crate) struct Reading { pub(crate) across: Option<[f64; 2]>, @@ -201,8 +201,8 @@ impl TileSets { self.sets.iter().any(|s| *s.tiles.name == *name && matches!(s.drawing, Some(Drawing::Vector(_)))) } - /// Adds to `choices` what each table of vector tiles draws, for the tables `plans` draws by - /// tile: the spans of the vector tiles drawn, within the plan's rows. + /// Adds to `choices` what each table of vector tiles draws, for the tables that `plans` draws + /// by tile. A table draws the spans of the vector tiles drawn, within the plan's rows. pub(crate) fn choose(&self, plans: &Plans, choices: &mut Choices) { for set in &self.sets { let Some(Drawing::Vector(vector)) = &set.drawing else { continue }; diff --git a/crates/latkit/src/tiles/tiling.rs b/crates/latkit/src/tiles/tiling.rs index f713b85e..6e539ceb 100644 --- a/crates/latkit/src/tiles/tiling.rs +++ b/crates/latkit/src/tiles/tiling.rs @@ -1,10 +1,10 @@ -//! Tilings: a source's pieces, fetched by key as the sets drawing it need them, decoded into -//! patches, and kept within a byte budget, evicting the least recently drawn first. Every set -//! drawing one source shares its tiling, so a piece one set fetched draws in another. Each set -//! decodes for a stamp that identifies what its decoding reads. A wanted key draws once it is -//! decoded for that stamp; until then its nearest decoded ancestor is drawn as a placeholder, and -//! missing pieces are fetched, nearest first. Earth tiles and spans of a sampled field's history -//! both use it. +//! Tilings: a source's pieces, fetched by key as the sets drawing the source need them, then +//! decoded into patches. A tiling keeps pieces within a byte budget and evicts the least recently +//! drawn first. Every set drawing one source shares its tiling, so a piece one set fetched draws in +//! another. Each set decodes for a stamp that identifies what its decoding reads. A wanted key +//! draws once it is decoded for that stamp. Until then its nearest decoded ancestor is drawn as a +//! placeholder, and missing pieces are fetched, nearest first. Earth tiles and spans of a sampled +//! field's history both use tilings. use std::sync::Arc; @@ -16,7 +16,7 @@ use crate::derive::Jobs; use crate::host::Request; use crate::{Error, Pending}; -/// Most fetches, and separately most decodes, a tiling runs at once. +/// The most fetches a tiling runs at once, and separately the most decodes. const REQUESTS: usize = 8; /// Bytes of fetched pieces a tiling keeps. Past this, the least recently drawn are evicted. const KEPT_BYTES: usize = 64 << 20; @@ -25,7 +25,7 @@ const KEPT_BYTES: usize = 64 << 20; pub(crate) enum Requested { /// Fetch the bytes `request` returns. Fetch(Request), - /// Not yet: the source must first fetch what locates the piece, such as an archive's + /// Not yet. The source must first fetch what locates the piece, such as an archive's /// directories. Wait, /// The source has no such piece, which draws nothing. @@ -47,8 +47,9 @@ pub(crate) trait Pieces { fn decode(&self, key: K, bytes: &Arc>, jobs: &Jobs<'_>) -> Decoding; } -/// The result of an update: each wanted key if decoded, else its nearest decoded ancestor, each -/// listed once; and whether any wanted key shows a placeholder (an ancestor, or nothing). +/// The result of an update. It lists each wanted key if decoded, else its nearest decoded +/// ancestor, each once. It also says whether any wanted key shows a placeholder (an ancestor, or +/// nothing). pub(crate) struct Drawn { pub(crate) keys: Vec, pub(crate) standing_in: bool, @@ -57,7 +58,7 @@ pub(crate) struct Drawn { } /// One set's claim on a tiling: the stamp it decodes for, and the keys whose decodes it keeps. -/// Those are the keys it draws, the keys it wants, and their ancestors, which serve as +/// Those are the keys it draws, the keys it wants, and their ancestors, which are drawn as /// placeholders. struct Claim { stamp: u64, @@ -109,8 +110,8 @@ impl Tiling { ) -> Drawn { self.tick += 1; self.land(); - // Decode wanted keys once fetched, and the fetched ancestors of keys still loading, as - // placeholders; fetch the rest. A piece decoded immediately draws immediately. + // Decode wanted keys once fetched, and the fetched ancestors of keys still loading as + // placeholders. Fetch the rest. A piece decoded immediately draws immediately. let mut decode: Vec = Vec::new(); for &key in wanted { if self.ready(key, stamp) { @@ -148,7 +149,7 @@ impl Tiling { } } // The set keeps the decodes of what it draws and wants, plus every ancestor of what it - // wants, which serves as a placeholder while zooming. Each walk up stops where an earlier + // wants, which is drawn as a placeholder while zooming. Each walk up stops where an earlier // wanted key's walk passed. let mut keys: FxHashSet = drawn.iter().copied().collect(); let mut walked: FxHashSet = FxHashSet::default(); diff --git a/crates/latkit/src/tiles/vector.rs b/crates/latkit/src/tiles/vector.rs index e8eb9ce3..34cd7a4e 100644 --- a/crates/latkit/src/tiles/vector.rs +++ b/crates/latkit/src/tiles/vector.rs @@ -1,17 +1,18 @@ //! Vector tiles: the tiles the camera needs, fetched from a PMTiles archive or a URL template -//! (given directly or read from a TileJSON document), decoded in jobs, and appended to the dataset -//! a set draws. The camera picks tiles that draw no larger than their own pixel size near its -//! focus, splitting those drawn largest first, and lists them nearest first. Tiles decode only the -//! layers a set's marks draw, and decode again for a set whose scene draws others. The job decoding -//! a tile also computes its rows' shapes for their marks, so a tile arrives with its triangles and -//! paths. A set holds the rows with their shapes, and draws a dataset of the rows as the tiles -//! wrote them. +//! (given directly or read from a TileJSON document). Jobs decode them, and their rows are +//! appended to the dataset a set draws. Near its focus, the camera picks tiles that draw no larger +//! than their own pixel size. It splits the tiles drawn largest first, and lists them nearest +//! first. Tiles decode only the layers a set's marks draw, and decode again for a set whose scene +//! draws other layers. The job decoding a tile also computes its rows' shapes for their marks, so a +//! tile arrives with its triangles and paths. A set holds the rows with their shapes, and draws a +//! dataset of the rows as the tiles wrote them. //! -//! A set's tables hold the rows of the tiles it drew in the order they arrived, so a tile landing -//! appends its rows and the pages earlier tiles filled stay as they are. Each table is paged, in -//! table order: its tiles are the vector tiles drawn, each the span of rows it wrote. A tile that -//! stops drawing keeps its rows, and draws them again if it returns, until the rows no drawn tile -//! holds outnumber the rows drawn by at least a page; then every table drops them at once. +//! A set's tables hold the rows of the tiles it drew, in the order they arrived. A landing tile +//! appends its rows, and the pages earlier tiles filled stay as they are. Each table is paged in +//! table order, and its tiles are the vector tiles drawn, each the span of rows it wrote. A tile +//! that stops drawing keeps its rows, and draws them again if it returns. This lasts until the rows +//! that no drawn tile holds outnumber the rows drawn by at least a page. Then every table drops +//! them at once. use std::collections::BinaryHeap; use std::ops::Range; @@ -35,15 +36,15 @@ use crate::gpu::shaders::PAGE_ROWS; use crate::host::{Job, Request}; use crate::{Error, Expr, Kind, Mark, Pending, Scene}; -/// Most tiles drawn at once. Past this, tiles stop splitting. A view 4K CSS pixels wide, seen from -/// above, draws at most about 100 tiles of 320 to 640 pixels. +/// The most tiles drawn at once. Past this, tiles stop splitting. A view 4K CSS pixels wide, seen +/// from above, draws at most about 100 tiles of 320 to 640 pixels. const MAX_TILES: usize = 128; -/// The tiles to draw within `zooms` and `bounds`, nearest the camera's focus first: those meeting -/// the camera's view, split from the root while they draw larger than their pixel size. The tile -/// drawn largest splits first, so once [`MAX_TILES`] binds, the smaller tiles left stay as they -/// are. Tiles coarser than the first zoom hold nothing: they split before any other, and draw -/// nothing if they cannot. +/// The tiles to draw within `zooms` and `bounds`, nearest the camera's focus first. These are the +/// tiles that meet the camera's view, split from the root while they draw larger than their pixel +/// size. The tile drawn largest splits first, so once [`MAX_TILES`] is reached, the smaller tiles +/// left stay as they are. Tiles coarser than the first zoom hold nothing. They split before any +/// other, and draw nothing if they cannot. fn wanted(sight: &Sight<'_>, zooms: [u8; 2], bounds: [f64; 4]) -> Vec { let region = sight.region(); let focus = sight.focus(); @@ -199,10 +200,10 @@ impl Fetched { /// Each tile whose rows a set's tables hold, with the span of rows it wrote in each table. type Spans = FxHashMap)>>; -/// What one set draws of vector tiles: the tiles whose rows its dataset holds, with their spans; -/// the tiles drawn, nearest the camera's focus first; a stamp of what the rows were made from (the -/// stamp they were decoded for, and their tables' priorities); and `held`, the same rows as the -/// dataset including their derived shape fields. +/// What one set draws of vector tiles. It records the tiles whose rows its dataset holds, with +/// their spans, and the tiles drawn, nearest the camera's focus first. A stamp records what the +/// rows were made from: the stamp they were decoded for, and their tables' priorities. `held` +/// holds the same rows as the dataset, including their derived shape fields. pub(crate) struct Drawing { spans: Spans, drawn: Vec, @@ -225,11 +226,11 @@ impl Drawing { self.drawn.len() } - /// What `table`, one of the dataset's tables, draws within `rows` rows: the span of each tile - /// drawn, nearest the camera's focus first until the rows run out (the tile they run out in - /// draws its last rows, those of highest priority), as ranges of its table order. The ranges - /// draw coarsest tile first, so finer tiles draw over the ancestors standing in for them. - /// `capped` when the budget left out any rows. + /// What `table`, one of the dataset's tables, draws within `rows` rows. It draws the span of + /// each tile drawn, as ranges of its table order, nearest the camera's focus first until the + /// rows run out. The tile in which they run out draws its last rows, which have the highest + /// priority. The ranges draw coarsest tile first, so finer tiles draw over the ancestors + /// standing in for them. `capped` is set when the budget left out any rows. pub(crate) fn chosen(&self, table: &Table, rows: u32) -> Chosen { let len = table.len() as u32; let (mut left, mut capped) = (rows, false); @@ -289,11 +290,11 @@ impl Drawing { } /// Draws tiles `drawn`, decoded for `stamp`. A tile new to the tables appends its rows after - /// every row they hold, as the tile wrote them but in its table's priority order: to `data`, - /// and with their shapes to `held`. Rows of tiles not drawn stay until they outnumber the rows - /// drawn by at least a page; then each table drops them in one removal, and the spans after - /// them move down. Every row goes when the stamp or a priority changes, since each tile then - /// appends its rows anew, with the same ids. Returns whether `data` changed. + /// every row they hold, as the tile wrote them but in its table's priority order. The rows go + /// to `data`, and with their shapes to `held`. Rows of tiles not drawn stay until they + /// outnumber the rows drawn by at least a page. Then each table drops them in one removal, and + /// the spans after them move down. Every row goes when the stamp or a priority changes, since + /// each tile then appends its rows anew, with the same ids. Returns whether `data` changed. fn draw( &mut self, drawn: &[TileKey], @@ -390,8 +391,8 @@ impl Drawing { } } -/// The priority each table of dataset `name` draws its rows by: that of the first mark drawing it -/// that gives one. +/// The priority each table of dataset `name` draws its rows by. It is the priority of the first +/// mark drawing the table that sets one. fn priorities(scene: &Scene, name: &Name) -> Vec<(Name, Expr)> { let mut out: Vec<(Name, Expr)> = Vec::new(); for mark in &scene.marks { @@ -405,7 +406,8 @@ fn priorities(scene: &Scene, name: &Name) -> Vec<(Name, Expr)> { } /// `table` with its rows in `priority` order, highest last, so they draw on top. Rows of equal -/// priority keep their order, and a missing or NaN priority is lowest. As is without a priority. +/// priority keep their order, and a missing or NaN priority is lowest. Without a priority, returns +/// `table` unchanged. fn ranked(table: &Table, priority: Option<&Expr>, all: &[Snapshot]) -> Table { let Some(priority) = priority else { return table.clone() }; let lowest = |p: f64| if p.is_nan() { f64::NEG_INFINITY } else { p }; @@ -535,10 +537,10 @@ fn decoded_tables(scene: &Scene, name: &Name) -> Vec { tables } -/// Converts `table`'s fields to the kinds the same-named fields have where they differ: in the -/// same-named table of `data`, or else in a table set before it in the same patch, as `kinds` -/// records, by table and field. For example, a field a tile reads as numbers but the dataset holds -/// as text is written as text. Records the kinds of fields new to both. +/// Converts `table`'s fields to the kinds the same-named fields have, where they differ. Those +/// fields are in the same-named table of `data`, or else in a table set before it in the same +/// patch, as `kinds` records by table and field. For example, a field a tile reads as numbers but +/// the dataset holds as text is written as text. Records the kinds of fields new to both. fn harmonized(table: &Table, data: &Snapshot, kinds: &mut FxHashMap<(Name, Name), Kind>) -> Table { let existing = data.table(&table.name); let mut out = table.clone(); diff --git a/crates/latkit/src/video/mod.rs b/crates/latkit/src/video/mod.rs index 3a17035f..df3fedac 100644 --- a/crates/latkit/src/video/mod.rs +++ b/crates/latkit/src/video/mod.rs @@ -1,8 +1,7 @@ //! Videos: a shot's pictures over time, passed in order to a [`Sink`] as they are drawn. Three -//! stages overlap, so throughput is set by the slowest stage rather than their sum: the GPU draws -//! and composes one frame while another is read back and the sink takes a third. A frame where -//! nothing moved is neither drawn nor read back; the sink is told it is held and shows the previous -//! frame again. +//! stages overlap, so the slowest stage sets throughput, not their sum. The GPU draws and composes +//! one frame while another is read back and the sink takes a third. A frame where nothing moved is +//! neither drawn nor read back. The sink is told it is held and shows the previous frame again. //! //! [`Mp4`] is the usual sink: H.264 from the host's [`Encoder`](crate::host::Encoder), written as //! fragmented MP4. @@ -22,9 +21,9 @@ use crate::host::{Encoder, Host, TIMESCALE, Unit}; use crate::shot::{Borrowed, Drawing, Step, Views}; use crate::{Error, Gpu, Result, Shot, View}; -/// Most frames in flight at once: one drawing, one reading back, one in the sink. +/// The most frames in flight at once: one drawing, one reading back, one in the sink. const IN_FLIGHT: usize = 3; -/// Most seconds of video between keyframes. +/// The most seconds of video between keyframes. const KEYFRAMES: f64 = 2.0; /// Default bits per pixel per frame, enough to keep lines and text sharp. const BITS_PER_PIXEL: f64 = 0.15; @@ -47,7 +46,8 @@ pub trait Sink { const PIXELS: Pixels; /// Prepares the sink for the video `encoding` describes, before the first frame, using the - /// GPU's `host` services; an MP4 gets its encoder from the host. Does nothing by default. + /// GPU's `host` services. An MP4 gets its encoder from the host. The default implementation + /// does nothing. fn begin(&mut self, encoding: &Encoding, host: Option<&dyn Host>) -> Result<()> { let _ = (encoding, host); Ok(()) diff --git a/crates/latkit/src/video/mp4.rs b/crates/latkit/src/video/mp4.rs index 03fd5292..4526ad09 100644 --- a/crates/latkit/src/video/mp4.rs +++ b/crates/latkit/src/video/mp4.rs @@ -99,8 +99,8 @@ impl Muxer { Ok(out) } - /// Ends the video at `end` ticks; the last frame lasts until then, at least one tick. Returns - /// the last fragment's bytes, or nothing if no frame is pending. + /// Ends the video at `end` ticks. The last frame lasts until then, and at least one tick. + /// Returns the last fragment's bytes, or nothing if no frame is pending. pub(crate) fn finish(&mut self, end: u64) -> Vec { let mut out = Vec::new(); if let Some(last) = self.run.last().map(|sample| sample.time) { diff --git a/crates/latkit/src/view/axes.rs b/crates/latkit/src/view/axes.rs index 95c941e6..2f411fa2 100644 --- a/crates/latkit/src/view/axes.rs +++ b/crates/latkit/src/view/axes.rs @@ -1,6 +1,6 @@ //! A plot's axes, drawn over its marks: grid lines at round values, tick labels and titles in the //! theme's font, and the playhead at the shown coordinate. Rebuilt each frame as overlay sprites, -//! in O(ticks). The room they take around the plot area is measured with the host's text metrics. +//! in O(ticks). The space they take around the plot area is measured with the host's text metrics. use std::sync::Arc; @@ -12,22 +12,22 @@ use crate::gpu::shaders::{self as wgsl, TEXT_SPREAD}; use crate::host::{Host, TextRun}; use crate::{Axis, Camera, Color, Format, Plot, Space}; -/// A tick's length, and the gap between it and its label: CSS pixels. +/// A tick's length, and the gap between it and its label, in CSS pixels. const TICK: f64 = 4.0; const GAP: f64 = 3.0; -/// Room kept on the sides of a hidden axis, in CSS pixels. +/// Space kept on the sides of a hidden axis, in CSS pixels. const HIDDEN: f64 = 8.0; -/// The room a plot's axes take around its area, from their measured text. +/// The space a plot's axes take around the plot area, from their measured text. #[derive(Clone, Copy, Default, PartialEq, Debug)] pub(crate) struct Gutters { - /// Room outside the area: top, right, bottom, left, CSS pixels. + /// Space outside the area in CSS pixels: top, right, bottom, left. pub(crate) room: [f64; 4], - /// How tall a tick label is written, CSS pixels. + /// The height of a tick label in CSS pixels. tick: f64, } -/// The room `plot`'s axes take. `measure` gives the CSS pixel size of a run of the theme's text, +/// The space `plot`'s axes take. `measure` gives the CSS pixel size of a run of the theme's text, /// including the distance field around it. Below the area: a tick, its labels, and the x title. /// Left: a tick and the widest y label. Above: the y title, at least half a label's height. Right: /// half the widest x label. A hidden axis leaves `HIDDEN` on its two sides instead. @@ -43,8 +43,8 @@ pub(crate) fn gutters(plot: &Plot, measure: impl Fn(&str) -> [f64; 2]) -> Gutter Gutters { room: [top, right, bottom, left], tick } } -/// Labels as wide as the widest `axis` writes: its given ticks' labels, else a sample value with a -/// sign, two digits, and three decimals, in its format. +/// Labels as wide as the widest that `axis` writes: its given ticks' labels, or else a sample value +/// with a sign, two digits, and three decimals, in its format. fn written(axis: &Axis) -> Vec { let Some(given) = &axis.ticks else { return vec![label(-88.888, 0.001, axis)] }; let mut values: Vec = given.iter().map(|&(v, _)| v).collect(); @@ -91,7 +91,7 @@ pub(super) fn label(value: f64, step: f64, axis: &Axis) -> String { } } -/// The ticks of `axis` across `range`: the ones it gives, else round values `step` apart. +/// The ticks of `axis` across `range`: the ones it gives, otherwise round values `step` apart. fn axis_ticks(axis: &Axis, range: [f64; 2], step: f64) -> Vec<(f64, String)> { match &axis.ticks { Some(given) => given @@ -148,7 +148,7 @@ pub(super) struct Writer<'a> { } impl Writer<'_> { - /// `text` as a sprite aligned by `align` at `anchor` (CSS pixels), snapped to device pixels; + /// `text` as a sprite aligned by `align` at `anchor` (CSS pixels), snapped to device pixels. /// `None` while the text cannot be set yet. pub(super) fn text(&mut self, text: &str, anchor: [f64; 2], align: Align) -> Option { let run = TextRun { text: text.into(), font: self.font.clone(), size_px: self.size_px, max_width: None }; @@ -218,7 +218,7 @@ impl View { self.axes(inner, plot, self.area(), x, y) } - /// Measures the room the plot's axes take, with the host's metrics for the theme's text at the + /// Measures the space the plot's axes take, with the host's metrics for the theme's text at the /// view's pixel ratio, once per scene and ratio. The plot area is what they leave. pub(super) fn measure_gutters(&mut self) { let Space::Plot(plot) = &self.scene.space else { return }; @@ -292,7 +292,7 @@ impl View { } sprites.push(line([left, top], [left, bottom], 1.0, axis)); if let Some(title) = &plot.y.label { - // Above the area, aligned with the left edge of the room its labels take. + // Above the area, aligned with the left edge of the space its labels take. sprites.extend(writer.text(title, [left - gutters.room[3], top], Align::Above)); } } diff --git a/crates/latkit/src/view/camera.rs b/crates/latkit/src/view/camera.rs index e450152c..f5da396b 100644 --- a/crates/latkit/src/view/camera.rs +++ b/crates/latkit/src/view/camera.rs @@ -13,12 +13,12 @@ pub enum Camera { /// A camera over the earth, a plane, or a diagram: the point it looks at and turns about, its /// zoom, its tilt, and its rotation. A diagram is always seen from straight above, upright. Map { - /// Longitude, latitude, and elevation on the earth; x, y, and elevation on a plane or a - /// diagram. In JSON, `[x, y]` is shorthand for elevation 0. + /// On the earth, longitude, latitude, and elevation. On a plane or a diagram, x, y, and + /// elevation. In JSON, `[x, y]` is shorthand for elevation 0. #[serde(with = "center")] #[schemars(with = "center::Center")] center: [f64; 3], - /// The web mercator zoom level on the earth; elsewhere, log₂ of CSS pixels per unit. + /// The web mercator zoom level on the earth. Elsewhere, log₂ of CSS pixels per unit. zoom: f64, /// Tilt in degrees from looking straight down, up to 75. Ignored in a diagram. pitch: f64, @@ -65,16 +65,16 @@ const FOV: f64 = 0.643_501_108_793_284_4; pub(crate) const MAX_PITCH: f64 = 75.0; const MAX_ZOOM: f64 = 24.0; -/// A rectangle of the view, CSS pixels: left, top, right, bottom. +/// A rectangle of the view in CSS pixels: left, top, right, bottom. pub(crate) type Area = [f64; 4]; -/// The box fits have framed: everything, widened by any rows framed since. It narrows the camera's -/// [`Reach`]. +/// The box fits have framed: the last fit to everything, grown by any rows fitted since. It +/// narrows the camera's [`Reach`]. #[derive(Clone, Copy, PartialEq, Debug)] pub(crate) struct Bounds { pub(crate) min: [f64; 3], pub(crate) max: [f64; 3], - /// The least and greatest zoom the fits framed at. + /// The lowest and highest zoom of those fits. pub(crate) zoom: [f64; 2], } @@ -89,31 +89,31 @@ impl Bounds { } } -/// Where a map camera may go: the zooms it keeps within, and the box its center keeps within at -/// each zoom. The space bounds it, and fits narrow it. +/// A map camera's zoom and pan limits: the zoom range, and the box the center stays in at each +/// zoom. The space sets the limits and fits narrow them. /// -/// A flat map is one world. The camera zooms out until the whole world just fits the room it -/// frames, and its center goes only where, seen from above, the world's edges stay outside that -/// room; along an axis where the world is no wider than the room, the center stays on the world's -/// middle. The globe zooms out until it spans a third of the room's shorter side, and its longitude -/// wraps. A plane, a diagram, and a plot bound nothing. Fits narrow the reach to 3 levels out past -/// the widest framing and 8 in past the closest, the center no farther outside the framed box than -/// its size, and the center's elevation within the box's. Where the two disagree, the space wins. +/// A flat map is one world. The camera zooms out until the whole world just fits the area it +/// frames. Its center stays where, seen from above, the world's edges stay outside that area. +/// Along an axis where the world is no wider than the area, the center stays on the world's middle. +/// The globe zooms out until it spans a third of the area's shorter side, and its longitude wraps. +/// A plane, a diagram, and a plot set no limits. Fits limit the zoom to 3 levels out past the +/// widest fit and 8 in past the closest. They keep the center within one box size of the framed +/// box, and its elevation within the box's. Where the space and the fits disagree, the space wins. #[derive(Clone, Copy, PartialEq, Debug)] pub(crate) struct Reach { - /// The least and greatest zoom. + /// The lowest and highest zoom. pub(crate) zoom: [f64; 2], /// The box fits have framed. framed: Option, - /// On a flat map, the room the camera frames in CSS pixels, which the world's edges stay - /// outside of. + /// On a flat map, the size in CSS pixels of the area the camera frames. The world's edges stay + /// outside it. world: Option<[f64; 2]>, } impl Reach { - /// The reach of a camera looking at `space` with `room` CSS pixels to frame within, narrowed by - /// the box fits `framed`. Without a room, as before a view's first frame, the space bounds - /// nothing. + /// The limits of a camera looking at `space` that frames an area of `room` CSS pixels, narrowed + /// by the box fits `framed`. Without `room`, as before a view's first frame, the space sets no + /// limits. pub(crate) fn new(space: &Space, room: Option<[f64; 2]>, framed: Option) -> Reach { let projection = match space { Space::Earth(earth) => Some(earth.projection), @@ -160,9 +160,9 @@ impl Camera { } /// The camera a fraction `t` (0 to 1) of the way from `self` to `to`, as a view of `space` - /// flies between them. Bearing, and longitude on the globe, turn the short way; a flat map is - /// one world, so its longitude never crosses the antimeridian. Cameras of different kinds give - /// `to`. Scripts that move the camera over time can use it. + /// flies between them. Bearing, and longitude on the globe, turn the short way. A flat map is + /// one world, so its longitude never crosses the antimeridian. Returns `to` if the cameras are + /// of different kinds. Use it in scripts that move the camera over time. #[must_use] pub fn ease(&self, to: &Camera, t: f64, space: &Space) -> Camera { let globe = matches!(space, Space::Earth(earth) if earth.projection == crate::Projection::Globe); @@ -227,7 +227,7 @@ impl Camera { } /// The camera turned by `degrees` of bearing and pitch about `anchor`, which keeps its screen - /// position. Pitch stays within `0..=MAX_PITCH`; a diagram never turns. + /// position. Pitch stays within `0..=MAX_PITCH`. A diagram never turns. pub(crate) fn turned(&self, view: &Projected, anchor: [f64; 3], degrees: [f64; 2]) -> Camera { let Camera::Map { center, zoom, pitch, bearing } = *self else { return *self }; if view.space == wgsl::SPACE_DIAGRAM { @@ -242,7 +242,7 @@ impl Camera { None => turned, }; } - // Rotate the camera rigidly about the anchor; its center moves with it. + // Rotate the camera rigidly about the anchor. Its center moves with it. let rig = |p: f64, b: f64| DMat3::from_rotation_z(-b.to_radians()) * DMat3::from_rotation_x(p.to_radians()); let r = rig(to_pitch, to_bearing) * rig(pitch, bearing).transpose(); let a = view.world(anchor); @@ -253,8 +253,8 @@ impl Camera { } /// The camera zoomed in by `levels` (out when negative) toward `anchor`, which keeps its screen - /// position. A map's zoom is clamped to the zooms of `reach` before the center moves, so a zoom - /// stopped at its limit moves nothing. + /// position. A map's zoom is clamped to the zoom range of `reach` before the center moves, so a + /// zoom at its limit moves nothing. pub(crate) fn zoomed(&self, view: &Projected, anchor: [f64; 3], levels: f64, reach: &Reach) -> Camera { match *self { Camera::Plot { x, y } => { @@ -286,9 +286,9 @@ impl Camera { } } - /// The camera kept within `reach`: its zoom first, then its center, which the fits' box holds - /// and then a flat map's world at that zoom, and pitch within `0..=MAX_PITCH`. A camera within - /// reach comes back unchanged. + /// The camera clamped to `reach`. Clamps the zoom first, then the center to the fits' box and + /// to a flat map's world at that zoom, and pitch to `0..=MAX_PITCH`. A camera already within + /// the limits comes back unchanged. pub(crate) fn within(&self, reach: &Reach) -> Camera { let Camera::Map { mut center, zoom, pitch, bearing } = *self else { return *self }; let zoom = zoom.clamp(reach.zoom[0], reach.zoom[1]); @@ -298,8 +298,8 @@ impl Camera { center = [across(0), across(1), center[2].clamp(framed.min[2], framed.max[2])]; } if let Some([w, h]) = reach.world { - // The world is 256 · 2^zoom CSS pixels across. Its edges stay outside the room while - // the center keeps half the room's width and height from them. + // The world is 256 · 2^zoom CSS pixels across. Its edges stay outside the framed area + // while the center stays half the area's width and height from them. let world = 256.0 * zoom.exp2(); let east = 180.0 * (1.0 - w / world).max(0.0); let north = latitude(PI * (1.0 - h / world).max(0.0)); @@ -399,11 +399,12 @@ fn wrap(lon: f64) -> f64 { (lon + 180.0).rem_euclid(360.0) - 180.0 } -/// A camera's projection of a space into a view of some size, which frames and gestures use to +/// A camera's projection of a space into a view of a given size. Frames and gestures use it to /// convert between positions, world coordinates, and the screen. /// -/// World coordinates are what the shaders project: position units relative to the camera's center -/// on a plane or plot, mercator radians (or the globe) on the earth, with elevation up. +/// World coordinates are what the shaders project. On a plane or plot they are position units +/// relative to the camera's center. On the earth they are mercator radians (or the globe). +/// Elevation is up. #[derive(Clone)] pub(crate) struct Projected { pub(crate) clip_from_world: DMat4, @@ -423,7 +424,7 @@ pub(crate) struct Projected { } impl Projected { - /// How far a space's earth is morphed onto the globe: 1 for the globe projection, else 0. + /// How far the earth is morphed onto the globe: 1 for the globe projection, otherwise 0. pub(crate) fn globe_of(space: &Space) -> f64 { match space { Space::Earth(earth) => f64::from(u8::from(earth.projection == crate::Projection::Globe)), @@ -432,7 +433,7 @@ impl Projected { } /// The projection of `camera` looking at `space` in a view of `size` CSS pixels, with its - /// center in the middle of `area`. A plot's camera spans `area`; one without a value range + /// center in the middle of `area`. A plot's camera spans `area`, and one without a value range /// shows `0..1`. A camera that does not fit `space` is replaced by that space's default. pub(crate) fn new(space: &Space, camera: &Camera, size: [f64; 2], area: Area, globe: f64) -> Projected { let camera = if camera.fits(space) { *camera } else { Camera::default_for(space) }; @@ -619,8 +620,8 @@ impl Projected { } /// `center` moved by world offset `d` across the ground, and up by `raise`. Longitude wraps on - /// the globe. A flat map is one world, drawn once, so its longitude runs on past the - /// antimeridian, for the camera's reach to stop. + /// the globe. A flat map is one world, drawn once, so its longitude runs past the antimeridian + /// and the camera's pan limits stop it. pub(crate) fn shifted(&self, center: [f64; 3], d: DVec3, raise: f64) -> [f64; 3] { if self.space != wgsl::SPACE_EARTH { return [center[0] + d.x, center[1] + d.y, center[2] + raise]; @@ -632,7 +633,7 @@ impl Projected { } /// A right-handed perspective projection onto WebGPU's clip space, depth `0..=1` from `near` to -/// `far`; passing the far distance as `near` reverses depth. +/// `far`. Passing the far distance as `near` reverses depth. fn perspective(fov: f64, aspect: f64, near: f64, far: f64) -> DMat4 { let f = 1.0 / (0.5 * fov).tan(); DMat4::from_cols_array(&[ @@ -858,8 +859,8 @@ mod tests { let view = Projected::new(&flat(), &camera, SIZE, AREA, 0.0); let anchor = view.position(view.ground([400.0, 300.0]).expect("the map")); // Dragged 1000 pixels west, 22 degrees at zoom 6, the center runs on past 180° rather than - // wrapping to the far side of the world, and the reach stops it where the world's east edge - // meets the view's. + // wrapping to the far side of the world, and the pan limit stops it where the world's east + // edge meets the view's. let panned = camera.panned(&view, anchor, [-1000.0, 0.0]); let Camera::Map { center, .. } = panned else { panic!("a map camera") }; assert!(center[0] > 180.0, "{center:?}"); @@ -874,7 +875,7 @@ mod tests { // The globe is 256 · 2^zoom / π pixels across. let across = 256.0 * reach.zoom[0].exp2() / PI; assert!((across - 200.0).abs() < 1e-9, "{across}"); - // Its longitude wraps, so any center is within reach. + // Its longitude wraps, so any center is within the limits. let far = Camera::Map { center: [179.0, 60.0, 0.0], zoom: 3.0, pitch: 0.0, bearing: 0.0 }; assert_eq!(far.within(&reach), far); } @@ -889,7 +890,7 @@ mod tests { let east = Camera::Map { center: [200.0, 0.0, 0.0], zoom: 2.0, pitch: 0.0, bearing: 0.0 }; let Camera::Map { center, .. } = east.within(&reach) else { panic!("a map camera") }; assert!((center[0] - 180.0 * (1.0 - 800.0 / 1024.0)).abs() < 1e-9, "{center:?}"); - // A plane bounds nothing, so only the fit holds its camera. + // A plane sets no limits, so only the fit limits its camera. let framed = Bounds { min: [0.0; 3], max: [10.0, 10.0, 0.0], zoom: [1.0; 2] }; let far = Camera::Map { center: [100.0, -100.0, 5.0], zoom: -10.0, pitch: 90.0, bearing: 0.0 }; let held = far.within(&Reach::new(&plane(), Some(SIZE), Some(framed))); diff --git a/crates/latkit/src/view/drawing.rs b/crates/latkit/src/view/drawing.rs index 8b11d1aa..7d6b3573 100644 --- a/crates/latkit/src/view/drawing.rs +++ b/crates/latkit/src/view/drawing.rs @@ -4,10 +4,10 @@ //! tiles fading out, then the camera's tiles in the order chosen, then pinned rows (selected, //! framed by a fit, or ends of kept edges) that no tile covers. A page keeps its slot while //! resident, so panning never moves rows. Idle pages stay resident while the budget allows, so -//! returning to them is free; a page made resident again only needs its words evaluated again. +//! returning to them is free. A page made resident again only needs its words evaluated again. //! //! Resident edges need their endpoints resident. A vertex page that many edges end in is made -//! resident whole; scattered endpoints become kept rows, so far-apart edges cost one row per +//! resident as a whole. Scattered endpoints become kept rows, so far-apart edges cost one row per //! endpoint rather than one page. Kept rows live in pages of their own, each at a fixed position //! while kept. @@ -25,11 +25,11 @@ use crate::derive::tiled::{Chosen, Draw, Place}; use crate::gpu::shaders as wgsl; use crate::gpu::shaders::PAGE_ROWS; -/// Flag bit of a slot holding a page of kept rows; the low bits are that page's number. +/// Flag bit of a slot holding a page of kept rows. The low bits are that page's number. const KEPT: u32 = 1 << 31; -/// Endpoints in one vertex page at which the page is made resident whole; with fewer, they are kept -/// individually. So endpoints never take more than twice the rows they reference. +/// The number of endpoints in one vertex page at which the whole page is made resident. Fewer are +/// kept individually, so endpoints never take more than twice the rows they reference. const DENSE: u32 = PAGE_ROWS / 2; /// The contents of a slot. @@ -93,7 +93,7 @@ pub(crate) struct Drawing { pub(crate) items: u32, pub(crate) leaving_items: u32, /// The layout count at which each slot's page was last needed. Idle pages stay resident while - /// there is room, most recently needed first. + /// there is space, most recently needed first. asked: Arc<[u64]>, /// How many layouts led to it, its own included. lays: u64, @@ -106,13 +106,13 @@ pub(crate) struct Drawing { /// Unique to this drawing. Lists of its rows are keyed by it. pub(crate) made: Version, /// The endpoint or pinned places the previous drawing held elsewhere, ascending. Edge rows that - /// reference them resolve their endpoints again. The version identifies this set of moves; it + /// reference them resolve their endpoints again. The version identifies this set of moves. It /// carries over from the previous drawing when nothing moved. pub(crate) moved: (Version, Arc<[u32]>), /// A stamp of the kept rows' inputs: pinned rows, endpoint needs, and resident pages. A drawing /// with the same stamp reuses its predecessor's kept rows unchanged. kept_from: u64, - /// A stamp of how it holds rows; see [`Drawing::layout`]. + /// A stamp of how it holds rows. See [`Drawing::layout`]. layout: u64, } @@ -130,10 +130,11 @@ pub(crate) struct Lay<'a> { /// Rows drawn regardless of tiles: selected rows, rows framed by a fit, and endpoints of kept /// edges. pub(crate) pinned: &'a Pinned, - /// What resident edges need resident without drawing: whole pages, and individual rows. + /// The vertex rows resident edges need resident without drawing them: whole pages and + /// individual rows. pub(crate) ends: &'a Ending, - /// How many pages it may keep resident in all. Needed pages are always resident; idle pages - /// fill whatever room is left. + /// The most pages it may keep resident. Needed pages are always resident. Idle pages fill + /// whatever is left. pub(crate) room: u32, /// The scene version its rows' words are evaluated in. Idle pages are released when it changes. pub(crate) scene: u64, @@ -200,7 +201,7 @@ impl Written { } impl Lay<'_> { - /// A stamp of the order, ranges, pinned rows, endpoint needs, and room. Excludes `scene` and + /// A stamp of the order, ranges, pinned rows, endpoint needs, and `room`. Excludes `scene` and /// `written`, which only affect idle pages. pub(crate) fn stamp(&self) -> u64 { let chosen = self.chosen; @@ -226,8 +227,8 @@ impl Drawing { if fading { ranges.iter().filter(|r| !has(was, r)).cloned().collect() } else { Vec::new() }; let leaving: Vec> = if fading { was.iter().filter(|r| !has(&ranges, r)).cloned().collect() } else { Vec::new() }; - // Resident pages: those drawn or fading out, and dense endpoint pages; idle pages of - // `before` while there is room and their words are still valid; and pages of kept rows. + // Resident pages: those drawn or fading out, dense endpoint pages, idle pages of `before` + // while there is space and their words are still valid, and pages of kept rows. let count = order.pages() as usize; let asked_now = asked(count, ranges.iter().chain(&leaving), &lay.ends.pages); let mut held = asked_now.clone(); @@ -302,8 +303,8 @@ impl Drawing { Drawing::lay(Some(self), lay, false) } - /// Builds the runs: tiles fading out first, so they draw underneath; then the drawn ranges in - /// the order chosen, with `entering` fading in; then `pinned` rows no range covers. + /// Builds the runs. Tiles fading out come first, so they draw underneath. Then come the drawn + /// ranges in the order chosen, with `entering` fading in, then `pinned` rows no range covers. fn run(&mut self, entering: &[Range], pinned: &[u32]) { let covered = |list: &[Range], row: u32| list.iter().any(|r| r.contains(&row)); let kept: Vec = pinned @@ -401,7 +402,7 @@ impl Drawing { } } - /// The held row of order place `d`, from its resident page or the kept rows; `None` if not + /// The held row of order place `d`, from its resident page or the kept rows. `None` if not /// resident. pub(crate) fn held_of(&self, d: u32) -> Option { let slot = *self.pages.get((d / PAGE_ROWS) as usize)?; @@ -501,9 +502,9 @@ fn keep_table(before: Option<&Drawing>, kept: &[u32], kept_slots: &[u32]) -> Kep /// The places in `ends` or `pinned` that `before` held and `now` holds elsewhere or not at all, /// ascending. Edge rows that reference them resolve their endpoints again. A page resident in both /// keeps its slot, and a row kept in both keeps its kept position, so only rows of a page resident -/// in just one drawing move: listed rows of pages `before` held and `now` does not, and rows -/// `before` kept whose pages `now` holds. A row `before` did not hold is referenced only by edge -/// rows new to the edge drawing, which resolve their endpoints when they arrive. +/// in just one drawing move. Those are the listed rows of pages `before` held and `now` does not, +/// and the rows `before` kept whose pages `now` holds. A row `before` did not hold is referenced +/// only by edge rows new to the edge drawing, which resolve their endpoints when they arrive. fn moved(before: &Drawing, now: &Drawing, ends: &Ending, pinned: &[u32]) -> Vec { /// The subslice of ascending `places` that lies in page `p`. fn within(places: &[u32], p: u32) -> &[u32] { @@ -532,7 +533,7 @@ fn moved(before: &Drawing, now: &Drawing, ends: &Ending, pinned: &[u32]) -> Vec< } /// Keeps idle pages resident: pages `before` held that are no longer `asked` for and have no -/// `written` row, most recently needed first, up to `room` resident pages in all. Marks them in +/// `written` row, most recently needed first, up to `room` resident pages in total. Marks them in /// `held` and returns them ascending. fn linger(before: &Drawing, asked: &[bool], room: u32, written: &Written, held: &mut [bool]) -> Vec { let wanted = asked.iter().filter(|&&a| a).count() as u32; @@ -555,7 +556,7 @@ fn linger(before: &Drawing, asked: &[bool], room: u32, written: &Written, held: /// The kept rows: the places in `order` from `pinned` and `ends` whose pages are not `held`, /// indexed by kept position, `NONE` for a free position. Rows already kept in `before` keep their -/// positions; the rest take the first free ones. +/// positions, and the rest take the first free ones. fn keep(before: Option<&Drawing>, pinned: &[u32], ends: &[u32], held: &[bool], order: &Order) -> Vec { let mut out: Vec = pinned.iter().chain(ends).copied().filter(|&d| d < order.len() && !held[(d / PAGE_ROWS) as usize]).collect(); @@ -585,7 +586,7 @@ fn keep(before: Option<&Drawing>, pinned: &[u32], ends: &[u32], held: &[bool], o } /// Assigns slots to the `held` pages and to `kept_pages` pages of kept rows. Each keeps its slot -/// from `before`; the rest take the first free slots. Returns each slot's contents, each page's +/// from `before`, and the rest take the first free slots. Returns each slot's contents, each page's /// slot, and each kept page's slot. fn place(before: Option<&Drawing>, held: &[bool], kept_pages: usize) -> (Vec, Vec, Vec) { let mut slots: Vec = before.map_or_else(Vec::new, |b| b.slots.to_vec()); @@ -645,11 +646,12 @@ struct Places { } impl Ends { - /// What the edges resident in `drawing` need of the vertex table they reference through `ends`, - /// which draws in `target`: dense pages, sparse endpoint rows, and the endpoints of kept edges. - /// Recomputed only when the drawing's layout or the vertex table's length changes. Each edge - /// page counts its distinct endpoints once toward their vertex page, so a vertex page that many - /// edge pages share may be made resident whole with fewer than `DENSE` distinct rows in use. + /// What the edges resident in `drawing` need from the vertex table they reference through + /// `ends`, which draws in `target`: dense pages, sparse endpoint rows, and the endpoints of + /// kept edges. Recomputed only when the drawing's layout or the vertex table's length changes. + /// Each edge page counts its distinct endpoints once toward their vertex page, so a vertex page + /// that many edge pages share may be made resident as a whole with fewer than `DENSE` distinct + /// rows in use. fn ending(&mut self, drawing: &Drawing, target: &Order, ends: &[&Reference]) -> Arc { let stamp = stamp_of((drawing.order.id(), target.id(), ends.iter().map(|r| r.version).collect::>())); if stamp != self.stamp { @@ -736,9 +738,9 @@ impl Ends { } } -/// What resident edges need of the vertex table, as places in its order, ascending: dense `pages` -/// to make resident whole and the remaining endpoint `rows` to keep individually, neither drawn; -/// and the endpoints of `kept` edges, drawn regardless of their pages. +/// What resident edges need from the vertex table, as places in its order, ascending. Dense `pages` +/// are made resident as a whole and the remaining endpoint `rows` are kept individually, and +/// neither is drawn. The endpoints of `kept` edges are drawn regardless of their pages. #[derive(Default, Debug)] pub(crate) struct Ending { pages: Vec, @@ -768,7 +770,7 @@ impl Ending { /// The drawing state of one paged table: the `current` drawing, the `next` one that replaces it /// once every page it needs is on the GPU, and the view-clock time `since` the current one first -/// drew. Also caches its edges' needs of their vertex table (`ends`), the merged needs of edge +/// drew. Also caches what its edges need from their vertex table (`ends`), the merged needs of edge /// tables ending at this table (`ending`), and its pinned rows. #[derive(Default)] pub(crate) struct Drawn { @@ -834,8 +836,8 @@ impl Drawn { impl View { /// Lays out each paged table's drawing from the camera's choices. A new drawing replaces the - /// current one when its inputs changed, or when the current one finished fading. A table no - /// longer paged loses its drawing and draws whole, in its own order. + /// current one when its inputs changed, or when the current one finished fading. A table that + /// stops being paged loses its drawing and draws all its rows, in its own order. pub(super) fn lay_out_drawings(&mut self) { let choices = std::mem::take(&mut self.choices); self.drawings.retain(|key, _| choices.contains_key(key)); @@ -902,7 +904,7 @@ impl View { } /// Sets each mark's `reveal` for this frame: how far its paged table's tiles have faded in or - /// out, from 0 to 1; 1 when they are not fading. + /// out, from 0 to 1. 1 when they are not fading. pub(super) fn reveal(&mut self) { let ease = self.ease().as_secs_f64(); for shown in &mut self.marks { @@ -941,8 +943,8 @@ impl View { paged } - /// How many pages `key`'s drawing may keep resident in all: for a table drawn by tiles, as many - /// as the plan's rows fill; otherwise 0, so no idle page stays. + /// The most pages `key`'s drawing may keep resident. For a table drawn by tiles, as many as the + /// plan's rows fill. Otherwise 0, so no idle page stays. fn room(&self, key: &(Name, Name)) -> u32 { match self.planning.plans().get(key).map(|p| p.draw) { Some(Draw::Tiles { rows }) => rows.div_ceil(PAGE_ROWS), diff --git a/crates/latkit/src/view/ends.rs b/crates/latkit/src/view/ends.rs index 5b1397dc..ee1a1664 100644 --- a/crates/latkit/src/view/ends.rs +++ b/crates/latkit/src/view/ends.rs @@ -61,7 +61,7 @@ impl End { self.mark != before.mark || self.order != before.order } - /// The places its drawing moved since `before` was attached; empty if none moved. + /// The places its drawing moved since `before` was attached. Empty if none moved. fn moved_since(&self, before: &End) -> &[u32] { match (&self.moved, &before.moved) { (Some((version, places)), Some((was, _))) if version != was => places, @@ -171,8 +171,8 @@ impl View { } /// The row blocks of mark `i` whose rows end at any of `places` in the attached vertex table, - /// ascending: the slots of its drawing whose endpoints include them. `None` for a table drawn - /// whole, which keeps no per-page endpoints, so every block resolves. + /// ascending: the slots of its drawing whose endpoints include them. `None` for a fully drawn + /// table, which keeps no per-page endpoints, so every block resolves. fn naming(&self, i: usize, places: &[u32]) -> Option> { let shown = &self.marks[i]; let style = shown.mark.style(); @@ -216,8 +216,8 @@ impl View { /// Where each of mark `i`'s ends finds its vertices: the vertex mark it attaches to, that mark's /// state and normals pages, and for a paged table, its drawing's page slots and kept rows. An end -/// attaches to the unnamed vertex mark drawing its table, else the first one, but only while that -/// mark has word pages for all its held rows. +/// attaches to the unnamed vertex mark drawing its table, otherwise the first one, but only while +/// that mark has word pages for all its held rows. fn ends(marks: &[Shown], i: usize) -> Vec { let Some(compiled) = &marks[i].compiled else { return Vec::new() }; let data = &marks[i].mark.style().data; @@ -226,7 +226,7 @@ fn ends(marks: &[Shown], i: usize) -> Vec { .ends .iter() .map(|end| match super::graph::end_mark(marks, data, &end.table) { - // A vertex mark without word pages for its rows draws nothing; nothing attaches to it. + // A vertex mark without word pages for its rows draws nothing. Nothing attaches to it. Some(m) if marks[m].pages_held() => { let (state, normals) = (marks[m].words.pages(Kept::State), marks[m].words.pages(Kept::Normals)); End { diff --git a/crates/latkit/src/view/event.rs b/crates/latkit/src/view/event.rs index 6ddf4db3..85e47be4 100644 --- a/crates/latkit/src/view/event.rs +++ b/crates/latkit/src/view/event.rs @@ -1,10 +1,10 @@ //! Events a view reports to the application. //! //! An intent asks the application to act: a click, open, menu, drag, connection, or deletion. Each -//! is reported once, in the order the user acted. A change reports that view state moved and is +//! is reported once, in the order the user acted. A change reports that view state changed and is //! named after that state: `hovered`, `selection`, `camera`, `waiting`. Only the latest change of //! each is reported, and the state can always be read, so a late listener can catch up. A view -//! reports only what the application did not do itself: its setters change state silently. +//! reports only what the application did not do itself. Its setters change state silently. use std::collections::VecDeque; @@ -47,9 +47,9 @@ pub enum Event { phase: Phase, /// The row dragged, as it was hit. hit: Hit, - /// The row's new position in the space's coordinates: the hit point moved with the pointer - /// since the press, at the depth where the drag started, so a vertex keeps its grab offset. - /// Snapped to a diagram's dots when its grid snaps. + /// The row's new position in the space's coordinates. It is the hit point moved with the + /// pointer since the press, at the depth where the drag started, so a vertex keeps its + /// offset from the pointer. Snapped to a diagram's grid dots when the grid snaps. position: [f64; 3], /// The pointer. trigger: Trigger, @@ -57,11 +57,11 @@ pub enum Event { /// A connection was drawn with [`Controls::connect`](crate::Controls) on: from a port, from a /// vertex with Alt held, or by dragging an edge's end off it. Connect { - /// The end it was drawn from: for an edge, the end that stayed. + /// The end it was drawn from. For a dragged edge end, the edge's other end. from: End, /// The vertex it was released over, if any. to: Option, - /// The edge whose end was dragged off; `to` replaces that end. + /// The edge whose end was dragged off. `to` replaces that end. edge: Option, /// Where it was released. trigger: Trigger, @@ -76,7 +76,7 @@ pub enum Event { }, /// The hovered row changed: [`View::hovered`](crate::View::hovered). Hovered { - /// The row now hovered; `None` once no row is. + /// The row now hovered, or `None` if no row is. hit: Option, /// The pointer, or the keys. trigger: Trigger, @@ -85,22 +85,23 @@ pub enum Event { Selection { /// The rows selected now. rows: Vec, - /// How the user changed it; `None` when the data dropped rows from it. + /// How the user changed it, or `None` when the data dropped rows from it. trigger: Option, }, - /// The camera moved by a gesture, fit, flight, coast, or orbit, or back within reach as the - /// view's size, insets, or space changed: [`View::camera`](crate::View::camera). + /// The camera moved: [`View::camera`](crate::View::camera). A gesture, fit, flight, coast, or + /// orbit moved it, or it moved back within its zoom and pan limits after the view's size, + /// insets, or space changed. Camera(Camera), - /// The work the last frame waited on changed; empty once frames are complete: - /// [`View::waiting`](crate::View::waiting). + /// The work the last frame waited on changed: [`View::waiting`](crate::View::waiting). Empty + /// once frames are complete. Waiting(Waiting), - /// Something failed; reported once. A mark that no longer fits its data is also listed in + /// Something failed. Reported once. A mark that no longer fits its data is also listed in /// [`View::failures`](crate::View::failures). Error(Error), } -/// Where the user acted, with which modifier keys, and with which device. For keyboard input, the -/// point is the row the keys point at, else the pointer, else the view's middle. +/// Where the user acted, with which modifier keys and device. For keyboard input, the point is the +/// row the keys point at, or else the pointer, or else the view's middle. #[derive(Clone, Copy, PartialEq, Debug, Default, serde::Serialize, schemars::JsonSchema)] pub struct Trigger { /// CSS pixels from the top left. @@ -139,8 +140,8 @@ impl Event { } } -/// The events a view has yet to report, in order. An intent waiting on a pick holds its place, and -/// later intents wait behind it; changes and errors never wait. +/// The events a view has yet to report, in order. An intent waiting on a pick keeps its place, and +/// later intents wait behind it. Changes and errors never wait. #[derive(Default)] pub(crate) struct Events(VecDeque); @@ -244,7 +245,8 @@ pub struct End { pub struct Hit { /// The row. pub row: Row, - /// The name of the mark that drew it, if named. It tells two marks of one table apart. + /// The name of the mark that drew it, if the mark has one. It tells apart two marks of one + /// table. #[serde(skip_serializing_if = "Option::is_none")] pub mark: Option, /// The index of the mark in the scene the frame drew. @@ -257,10 +259,10 @@ pub struct Hit { pub position: [f64; 3], /// CSS pixels from the probe to the row. pub distance: f32, - /// The port hit, when a vertex's port was. + /// The port hit, if the hit is on a vertex's port. #[serde(skip_serializing_if = "Option::is_none")] pub port: Option, - /// The sample hit, when a line tracing a sampled field was. + /// The sample hit, if the hit is on a line tracing a sampled field. #[serde(skip_serializing_if = "Option::is_none")] pub sample: Option, } @@ -303,9 +305,9 @@ pub enum Fit { Reveal(Row), /// The box from `min` to `max`: x, y, and elevation. Bounds { - /// The least corner. + /// The minimum corner. min: [f64; 3], - /// The greatest corner. + /// The maximum corner. max: [f64; 3], }, } @@ -334,34 +336,34 @@ pub struct Stats { pub frames: u64, /// Rows of every mark's table. pub rows: u64, - /// Rows every mark draws: every row of a table drawn whole, and the rows of the tiles in view - /// of a paged table. + /// Rows the marks draw: every row of a fully drawn table, and the rows in the tiles in view of + /// a paged table. pub drawn: u64, /// Draw calls the last frame recorded, label draws included. pub draws: u64, /// Instances the last frame's draws drew: points, segments, triangles, and glyphs. An upper /// bound for segments, which the GPU may cull. pub instances: u64, - /// Fetched tiles the view draws, of every tile source. + /// Fetched tiles the view draws, across all tile sources. pub tiles: u64, - /// Tiles being fetched or decoded, of every tile source. + /// Tiles being fetched or decoded, across all tile sources. pub tiles_loading: u64, /// Milliseconds the last frame took to record on the CPU. pub cpu_ms: f32, - /// Milliseconds the GPU takes per frame: the median of recently measured frames; 0 if the - /// device cannot measure. + /// Milliseconds the GPU takes per frame: the median of recently measured frames. 0 if the + /// device cannot measure GPU time. pub gpu_ms: f32, } -/// The work a frame was still waiting on, by kind; empty when the frame was complete. +/// The work a frame was still waiting on, by kind. Empty when the frame was complete. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] pub struct Waiting { - /// Pipelines the view uses are still compiling. Until those it draws with finish, the view + /// Pipelines the view uses are still compiling. Until its draw pipelines finish, the view /// draws nothing new. pub compiling: bool, /// Pages its marks draw are still uploading, or tables are not yet built from them. pub data: bool, - /// Layouts, routes, lanes, shapes, tiles, or pyramids under way. + /// Layouts, routes, lanes, shapes, tiles, or pyramids in progress. pub derived: bool, /// The label texts in view being read back, or texts requested and not yet set. pub labels: bool, diff --git a/crates/latkit/src/view/fit.rs b/crates/latkit/src/view/fit.rs index 7dd77d26..fa711dca 100644 --- a/crates/latkit/src/view/fit.rs +++ b/crates/latkit/src/view/fit.rs @@ -1,9 +1,9 @@ //! Fits: moving the camera to frame what is drawn. //! -//! A box fits at once, as does anything whose extent the CPU already knows: a diagram's sheet holds -//! every box and wire, and tiles hold the bounds of every row they draw. Anything else is measured +//! A box fits at once, as does anything whose extent the CPU already knows. A diagram's sheet has +//! every box and wire, and tiles have the bounds of every row they draw. Anything else is measured //! on the GPU, read back, and framed a frame or two later. A plot without a value range measures -//! its traces whenever they or the camera change, and grows its range with headroom so the axes +//! its traces whenever they or the camera change. It grows its range with headroom so the axes //! stay still. use super::View; @@ -32,7 +32,7 @@ impl Fitting { self.reading.busy() } - /// Whether a fit is waiting to be measured; never while extents are being read back. + /// Whether a fit is waiting to be measured. Always false while extents are being read back. pub(crate) fn due(&self) -> bool { self.wanted.is_some() && !self.reading.busy() } @@ -96,7 +96,8 @@ impl View { return None; } // A fit waits until data has streamed, shapes are triangulated and bent, and any layout has - // placed it; in a diagram, until vertices stand where the sheet puts them. + // placed the rows. In a diagram it also waits until vertices reach where the sheet puts + // them. let easing = matches!(self.scene.space, Space::Diagram(_)) && self .marks @@ -104,7 +105,7 @@ impl View { .any(|m| m.kind() == MarkKind::Vertices && (m.transition.is_some() || m.begin.is_some())); let all = matches!(self.fitting.wanted, Some(Fit::All)); // Fitting everything uses a tiled table's row bounds, regardless of what is drawn. A mark - // with no rows has nothing to frame; it only delays a fit while no mark has rows. + // with no rows has nothing to frame. It only delays a fit while no mark has rows. let compiled = || self.marks.iter().filter(|m| m.compiled.is_some()); let staged = |m: &Shown| self.staged.get(&m.mark.style().data).is_some_and(|s| !s.is_empty()); let drawn = |m: &Shown| (m.drawable() && !staged(m)) || (all && self.tiled_bounds(m).is_some()); @@ -171,9 +172,10 @@ impl View { self.frame_fit(lo, hi, fit); } - /// The box `fit` frames if known without measuring, as `[min x, min y, max x, max y]`. In a - /// diagram whose sheet is placed: the sheet's extent, or the fitted rows' boxes. Otherwise, for - /// `Fit::All` once every mark is `ready` and only tiled tables are measured: their bounds. + /// The box `fit` frames, if known without measuring, as `[min x, min y, max x, max y]`. In a + /// diagram whose sheet is placed, it is the sheet's extent or the union of the fitted rows' + /// boxes. Otherwise, for `Fit::All` once every mark is `ready` and only tiled tables are + /// measured, it is their bounds. fn known(&self, fit: &Fit, ready: bool) -> Option<[f64; 4]> { let union = |a: [f64; 4], b: [f64; 4]| [a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])]; let sheet = &self.derived.sheet; @@ -226,8 +228,9 @@ impl View { self.derived.tiled.bounds(&style.data, &style.table) } - /// Frames the box `min..max` at once, as far as the camera's reach allows. Framing everything - /// sets the box that narrows the reach; framing part of it widens that box, never narrows it. + /// Frames the box `min..max` at once, within the camera's zoom and pan limits. A fit to + /// everything sets the box that narrows the limits. A fit to part of it grows that box and + /// never shrinks it. fn frame_box(&mut self, min: [f64; 3], max: [f64; 3], everything: bool) { let camera = self.camera.framing(&self.scene.space, min, max, self.size, self.area()); self.rig.hold(); @@ -277,7 +280,7 @@ impl View { } impl super::marks::Shown { - /// Whether this mark's extents frame a fit: whether its kind measures them. + /// Whether fits frame this mark's extents, which depends on its kind. pub(crate) fn measured(&self) -> bool { self.kind().bounds().is_some() } diff --git a/crates/latkit/src/view/focus.rs b/crates/latkit/src/view/focus.rs index b6ac6a7a..a94e5703 100644 --- a/crates/latkit/src/view/focus.rs +++ b/crates/latkit/src/view/focus.rs @@ -3,7 +3,7 @@ //! //! The selection keeps only rows a pickable mark shows. The endpoints of a selected or hovered edge //! are lit too. Each mark's lit rows are listed by draw index for its focus draw, which draws them -//! again over everything with a glow. Hover is always state that expressions can read; the controls +//! again over everything with a glow. Hover is always state that expressions can read. The controls //! decide whether it glows. use rustc_hash::FxHashMap; @@ -19,7 +19,7 @@ use crate::gpu::work::Queue; use crate::scene::mark::Primitive; use crate::{Row, Rows}; -/// The row gap at which two changed words of a page are written separately; closer ones share one +/// The row gap at which two changed words of a page are written separately. Closer ones share one /// write, along with the unchanged words between them. const GAP: u32 = 16; @@ -185,9 +185,9 @@ impl View { self.focus.lit = Some(all); } - /// Instances each lit row takes in mark `i`'s focus draw, for lit held rows `lit`: one for a - /// point; for paths and areas, enough for the longest lit row's pieces, past which a shorter - /// row's instances draw nothing; none for faces. + /// Instances each lit row takes in mark `i`'s focus draw, for lit held rows `lit`. One for a + /// point. For paths and areas, enough for the longest lit row's pieces (a shorter row's extra + /// instances draw nothing). None for faces. fn pieces(&self, i: usize, lit: &[u32]) -> u32 { let shown = &self.marks[i]; if shown.compiled.is_none() { diff --git a/crates/latkit/src/view/frame.rs b/crates/latkit/src/view/frame.rs index b114916c..64c99c76 100644 --- a/crates/latkit/src/view/frame.rs +++ b/crates/latkit/src/view/frame.rs @@ -1,12 +1,12 @@ -//! One frame, in order: tick the clock, settle readbacks, advance the camera, derive the tables -//! marks read beside their data, lay out drawings, sync columns, stream uploads, give marks word -//! pages for their rows, start transitions, pack the program buffer, write focus words and -//! uniforms, then record the compute, texts, render, probe, and query passes into the caller's -//! encoder. No pass is recorded before the program buffer is packed, since packing may move a -//! mark's words with copies recorded in the encoder. +//! One frame, in order: tick the clock, settle readbacks, advance the camera, derive tables from +//! the data, lay out drawings, sync columns, stream uploads, give marks word pages for their rows, +//! start transitions, pack the program buffer, write focus words and uniforms, then record the +//! compute, texts, render, probe, and query passes into the caller's encoder. No pass is recorded +//! before the program buffer is packed, since packing may move a mark's words with copies +//! recorded in the encoder. //! //! The frame submits nothing itself except setup that must run before its recorded passes: -//! clearing newly handed-out pages, and copying buffers that grew. +//! clearing newly allocated pages and copying buffers that grew. use web_time::Instant; @@ -25,9 +25,9 @@ use crate::gpu::shaders as wgsl; use crate::scene::mark::MarkKind; use crate::{Error, Name, derive}; -/// Whether a frame's pages found room in the arena. If some did not, the next frame makes room: -/// `Short` grows the arena or plans less; `Starved`, when the arena is full and the plan is already -/// minimal, releases what the view draws of datasets waiting for room. +/// Whether a frame's pages fit in the arena. If some did not, the next frame makes space. `Short` +/// grows the arena or plans less. `Starved` means the arena is full and the plan is already +/// minimal, so the next frame releases the drawn tables of datasets waiting for space. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Debug)] pub(super) enum Room { #[default] @@ -40,7 +40,7 @@ pub(super) enum Room { const TRAVEL: f64 = 0.1; impl View { - /// Records `frame` of the scene into `encoder`; the caller submits it. Until the view is + /// Records `frame` of the scene into `encoder`. The caller submits it. Until the view is /// [ready](View::ready) for the frame's target, it records nothing and the target keeps its /// contents. /// @@ -50,8 +50,8 @@ impl View { self.record(encoder, frame, true); } - /// Records `frame` for a picture. Pictures take no input, so they compile only what they draw - /// and change with, never the pipelines that probes and queries use. + /// Records `frame` for a picture. Pictures take no input, so they compile only the pipelines + /// they draw and update with, never those that probes and queries use. pub(crate) fn render_picture(&mut self, encoder: &mut wgpu::CommandEncoder, frame: &Frame<'_>) { self.record(encoder, frame, false); } @@ -64,9 +64,9 @@ impl View { } return; } - // Set the frame's size first, so the tiles sources bring up are those this frame shows. - // Clear the arrival flag before that, so work they start that finishes at once wakes the - // next frame. + // Set the frame's size first, so the sources load the tiles this frame shows. Clear the + // arrival flag before that, so work they start that finishes immediately wakes the next + // frame. self.pixel_ratio = frame.pixel_ratio.max(0.01); let ratio = f64::from(self.pixel_ratio); self.size = [f64::from(frame.size[0]) / ratio, f64::from(frame.size[1]) / ratio]; @@ -83,7 +83,7 @@ impl View { if let Some(timer) = &mut self.timer { timer.begin(encoder); } - // Runs the callbacks of finished readbacks; a no-op in browsers, which run them themselves. + // Runs the callbacks of finished readbacks. A no-op in browsers, which run them themselves. let _ = device.poll(wgpu::PollType::Poll); self.tell_gpu_errors(); self.measure_gutters(); @@ -109,8 +109,8 @@ impl View { self.resources.as_mut().expect("created").write_lit(device, queue, &rows); } let measure = self.measuring(); - // A picture takes no input. Other frames probe and answer queries once the pipelines those - // use have compiled. + // A picture takes no input. Other frames probe and answer queries once their pipelines + // have compiled. let answers = input && self.answers(); let probe = self.probe_point(answers); let query = self.lay_out_lists(answers); @@ -166,9 +166,9 @@ impl View { } /// The effect time for this frame, and the seconds comets travel to reach it. Channels reading - /// the clock re-evaluate only when it moved, so a frame redrawn at the same instant (as a shot - /// draws while it waits) evaluates none of them. Under reduced motion the effect time stays at - /// its first value. A shot's fixed time overrides both. + /// the clock evaluate again only when it moves, so a frame redrawn at the same instant (as a + /// shot draws while it waits) evaluates none of them. Under reduced motion the effect time + /// stays at its first value. A shot's fixed time overrides both. fn effect_clock(&mut self) -> (f64, f64) { let held = !self.moves() && !self.effect_time.is_nan(); let elapsed = self.signal.elapsed().unwrap_or_else(|| if held { self.effect_time } else { self.clock.now() }); @@ -184,10 +184,10 @@ impl View { (elapsed, travel) } - /// Ends a frame begun at `started`: reports what it still waits on, records its stats, and - /// requests the next frame if work remains, as work arriving between frames does. A frame - /// without `input` (a picture's) requests none for probes and queries, which only input frames - /// run. + /// Ends a frame begun at `started`. Reports what it still waits on, records its stats, and + /// requests the next frame if work remains (as work arriving between frames does). A frame + /// without `input` (a picture's) requests no frame for probes and queries, which only input + /// frames run. fn end(&mut self, started: Instant, input: bool) { let waiting = self.under_way(); if waiting != self.waiting { @@ -227,9 +227,9 @@ impl View { } /// Gives every mark word pages for its held rows: state, focus words, and, for a vertex mark - /// that faces attach to, the normals they sum. Rows in new state pages evaluate from scratch. A - /// mark the arena has no room for draws nothing until it has. Returns whether any focus word - /// pages are new. + /// that faces attach to, the normals the faces sum. Rows in new state pages evaluate from + /// scratch. A mark whose pages do not fit in the arena draws nothing until they do. Returns + /// whether any focus word pages are new. fn fit_words(&mut self, inner: &mut Inner) -> bool { let gpu = self.gpu.clone(); let device = (&gpu.0.device, &gpu.0.queue); @@ -264,10 +264,10 @@ impl View { fresh } - /// Records that some pages found no room; what lacks them draws nothing until the next frame - /// makes room. If the arena can grow, or the plan can shrink (it shrinks here), that is - /// `Short`. Otherwise it is `Starved`, which releases what the view draws of datasets waiting - /// for room, unless an earlier release freed nothing (`limited`). + /// Records that some pages did not fit in the arena. Marks missing pages draw nothing until the + /// next frame makes space. If the arena can grow, or the plan can shrink (it shrinks here), + /// that is `Short`. Otherwise it is `Starved`, which releases the drawn tables of datasets + /// waiting for space, unless an earlier release freed nothing (`limited`). fn found_no_room(&mut self, inner: &Inner) { let room = if !inner.arena.full() || self.planning.overflowed() { Room::Short @@ -279,10 +279,10 @@ impl View { self.room = self.room.max(room); } - /// Releases what the view draws of each dataset waiting for room, when the arena is full and - /// the plan is already minimal. Its marks draw nothing until the staged replacement arrives in - /// the freed room. If nothing was released, what the view draws does not fit the budget: it - /// reports a `Limit` error once and waits for a new data version to publish. + /// Releases the drawn tables of each dataset waiting for space, when the arena is full and the + /// plan is already minimal. Their marks draw nothing until the staged tables upload into the + /// freed space. If nothing was released, the view's data does not fit the budget. It reports a + /// `Limit` error once and waits for a new data version to publish. fn start_over(&mut self, inner: &mut Inner) { let mut freed = false; for shown in &mut self.marks { @@ -307,9 +307,9 @@ impl View { } } - /// Keeps what the view draws within its share of the budget. It counts each page its tables pin - /// once, plus the word pages its marks keep per row, minus idle pages, which stay only while - /// there is room and go first when the plan shrinks. + /// Keeps the view's GPU memory within its budget share. Counts each page its tables pin once, + /// plus the marks' word pages, minus idle pages. Idle pages stay only while there is space and + /// go first when the plan shrinks. fn hold_to_share(&mut self, inner: &Inner) { let mut pins: Vec = self.marks.iter().flat_map(|m| m.tables.pins()).collect(); pins.sort_unstable_by_key(|key| (key.page, key.low, key.words as u8)); @@ -351,10 +351,10 @@ impl View { } } - /// Derives the tables marks read beside their data, from the data and scene alone, handing the - /// host whatever the frame cannot afford. First come the orders of paged tables and the ranges - /// of them the camera needs, from their pyramids or, for tables of vector tiles, from the - /// vector tiles drawn; then every other derivation. + /// Derives tables from the data and scene alone, handing the host any work the frame cannot + /// afford. First come the orders of paged tables and the ranges of them the camera needs. These + /// come from the tables' pyramids or, for tables of vector tiles, from the vector tiles drawn. + /// Every other derivation follows. fn derive(&mut self, inner: &mut Inner) { let host = inner.host.clone(); let projected = self.projected(); @@ -389,8 +389,8 @@ impl View { inner.frame.work = jobs.left() as u32; } - /// Brings every tile set up to date with what the camera and plot window need, from the GPU's - /// tile cache and what arrived since. Tiles fetch and decode as they arrive, so this spends + /// Updates every tile set to what the camera and plot window need, from the GPU's tile cache + /// and the pieces that arrived since. Tiles fetch and decode as they arrive, so this spends /// none of the frame's work budget. pub(super) fn update_tiles(&mut self, inner: &mut Inner) { let host = inner.host.clone(); @@ -512,8 +512,8 @@ impl View { } /// Syncs every mark's columns to its data and, for a paged table, to its latest drawing. Column - /// tables whose inputs changed are rebuilt once and staged with their dataset, which reserves - /// every page they read at once or waits for room. + /// tables whose inputs changed are rebuilt once and staged with their dataset. The dataset + /// reserves every page they read all at once, or waits for space. fn sync(&mut self, inner: &mut Inner) { let at = self.at; let (across, columns) = self.trace_window(); @@ -550,9 +550,9 @@ impl View { } } - /// Swaps in each dataset once all of it is ready and every mark has room for word pages of its - /// new rows. A dataset's rebuilt tables and its paged tables' drawings swap in together, so - /// edges never end at another drawing's vertices and a data version appears whole. Returns + /// Swaps in each dataset once all of it is ready and every mark has word pages for its new + /// rows. A dataset's rebuilt tables and its paged tables' drawings swap in together, so edges + /// never end at another drawing's vertices and a data version appears all at once. Returns /// whether any tables were swapped in. fn publish(&mut self, inner: &mut Inner) -> bool { let mut datasets: Vec = @@ -652,7 +652,7 @@ impl View { }; match &before { // Rows that came, went, or moved evaluate every channel, constants included: over the - // rewritten row blocks if every row kept its place, else over every row. + // rewritten row blocks if every row kept its place, otherwise over every row. Some(old) if old.drawing.is_none() && drawing.is_none() => { if moved.is_some() || shown.rows != table.len() as u32 { let rewritten = old.table.len() <= table.len() && moved.as_ref().is_none_or(|(s, _)| in_place(s)); @@ -676,7 +676,8 @@ impl View { } else if moved.is_some() { shown.exits = 0; } else if shown.transition.is_some() && !bound.is_empty() { - // Rows in slots rebound mid-transition enter; the rest keep easing from what they show. + // Rows in slots rebound mid-transition enter. The rest keep easing from their current + // state. shown.restart(&bound); } if !kept || (shown.begin.is_none() && shown.transition.is_none()) { diff --git a/crates/latkit/src/view/gestures.rs b/crates/latkit/src/view/gestures.rs index 7fb1c40d..868d4f32 100644 --- a/crates/latkit/src/view/gestures.rs +++ b/crates/latkit/src/view/gestures.rs @@ -1,24 +1,25 @@ //! Gestures: how a view interprets pointer and key input. //! //! A press decides what it does once the pointer moves a few pixels. With Shift it selects a box -//! when box selection is on, else turns a map or plane. On a port, an Alt-held vertex, or an edge, -//! it draws a connection when connecting is on. On a row of a draggable mark it drags the row. -//! Elsewhere it pans. A press that does not move clicks; with Shift, Control, or Meta it toggles -//! its row in the selection. With cycling on, clicking again where nothing moved selects the next -//! row under the point. A secondary press turns once it moves and opens a menu when it does not, as -//! a touch or pen held still does. The wheel and pinches zoom, as the controls allow, and keys -//! step. A diagram never turns, and its drags snap to its grid when the grid says so. Every camera -//! move is about the point under the pointer and goes through the rig, which lets a released drag -//! coast and keeps the camera within reach. +//! when box selection is on, otherwise it turns a map or plane. On a port, a vertex with Alt held, +//! or an edge, it draws a connection when connecting is on. On a row of a draggable mark it drags +//! the row. Elsewhere it pans. A press that does not move clicks. With Shift, Control, or Meta it +//! toggles its row in the selection. With cycling on, clicking again where nothing moved selects +//! the next row under the point. A secondary press turns once it moves and opens a menu when it +//! does not, as does a touch or pen held still. The wheel and pinches zoom, as the controls allow, +//! and keys step. A diagram never turns, and its drags snap to its grid when the grid snaps. Every +//! camera move is about the point under the pointer and goes through the rig, which lets a released +//! drag coast and keeps the camera within its zoom and pan limits. //! //! The point under a press comes from the latest probe if it was taken where the press is, -//! otherwise from a probe of its own a frame or two later; the press waits for it, then catches up. -//! Menus and connections report what lies under their point once a pick resolves, in their place +//! otherwise from its own probe a frame or two later. The press waits for it, then catches up. +//! Menus and connections report the rows under their point once a pick resolves, in their place //! among the intents. //! -//! Keys point too. Tab steps through the rows nearest the view's middle, and past the last one lets -//! focus move on. An arrow steps to the nearest row in its direction. Enter opens the row and Space -//! toggles it in the selection. The row the keys point at stays hovered until the pointer acts. +//! Keys can point at rows too. Tab steps through the rows nearest the view's middle, and past the +//! last one lets focus leave the view. An arrow steps to the nearest row in its direction. Enter +//! opens the row and Space toggles it in the selection. The row the keys point at stays hovered +//! until the pointer acts. use super::View; use super::event::{Awaited, Cursor, End, Event, Fit, Hit, Phase, Trigger}; @@ -47,7 +48,7 @@ pub(crate) enum Gesture { #[default] Idle, /// A pointer pressed at `from`, now at `trigger`. `answer` is `None` while its probe is in - /// flight. `earlier` is a previous press released here before its own answer arrived; it clicks + /// flight. `earlier` is a previous press released here before its own answer arrived. It clicks /// first, using this press's answer. Press { pointer: u32, @@ -60,12 +61,12 @@ pub(crate) enum Gesture { /// When it began, on the view's clock. since: f64, }, - /// A touch or pen held still long enough to open a menu; its release does nothing more. + /// A touch or pen held still long enough to open a menu. Its release does nothing more. Held { pointer: u32, }, /// A connection drawn from `from` toward the pointer at `to`. `anchor` is where it starts on - /// screen, once known; `locating` finds it when the press did not give it. + /// screen, once known. `locating` finds it when the press did not give it. Connect { pointer: u32, from: End, @@ -75,7 +76,7 @@ pub(crate) enum Gesture { to: [f32; 2], }, /// A row dragged by `pointer`. `grab` is the hit point minus the press point, in CSS pixels, so - /// the row keeps its grab offset. + /// the row keeps its offset from the pointer. Drag { pointer: u32, probed: Probed, @@ -112,17 +113,17 @@ pub(crate) struct Gestures { time: f64, /// The last click: when, where, on which row, and where the camera was. clicked: Option<(f64, [f32; 2], Option, Camera)>, - /// How fast the pan or turn under way moves, per second, and when it last moved. + /// How fast the current pan or turn moves, per second, and when it last moved. velocity: [f64; 2], moved: Option, } -/// The rows the keys step through: the hits of a pick around the view's middle, nearest first, as -/// drawn in the frame it was taken in; the index stepped to, which keeps the hover until the -/// pointer acts; and keys pressed while the pick was pending. +/// The rows the keys step through. Holds the hits of a pick around the view's middle, nearest +/// first, as drawn in the frame it was taken in. Also holds the index stepped to, which keeps the +/// hover until the pointer acts, and keys pressed while the pick was pending. #[derive(Default)] pub(crate) struct Stepping { - /// The rows last picked; `None` until the keys first step. + /// The rows last picked. `None` until the keys first step. rows: Option>, picking: Option>>, /// Whether the rows match where the current frame draws them. @@ -229,13 +230,14 @@ impl View { } } - /// Where the pointer last acted, the keys it held, and its device; the default once it left. + /// Where the pointer last acted, the modifier keys held, and its device. The default once it + /// leaves. fn pointing(&self) -> Trigger { self.pointer.unwrap_or_default() } - /// Handles the pointer leaving. Unless a gesture is under way, its probes are dropped and the - /// hover clears, unless the keys point at a row. + /// Handles the pointer leaving. If no gesture is in progress, its probes are dropped, and the + /// hover clears unless the keys point at a row. fn left(&mut self) { let trigger = self.pointing(); self.pointer = None; @@ -389,8 +391,8 @@ impl View { true } - /// Handles a probe's answer at `point`: a press waiting for it decides; otherwise the answer - /// sets the hover, unless the keys point at a row. + /// Handles a probe's answer at `point`. A press waiting for it decides what to do. Otherwise + /// the answer sets the hover, unless the keys point at a row. pub(super) fn landed(&mut self, point: [f32; 2], answer: Answer) { if let Gesture::Press { from, answer: waiting @ None, .. } = &mut self.gestures.now && near(*from, point, 0.5) @@ -534,7 +536,7 @@ impl View { } } - /// Selects `row` alone, or with `toggle` adds it to the selection or takes it out. + /// Selects `row` alone, or with `toggle` toggles it in the selection. fn select_row(&mut self, row: Row, toggle: bool, trigger: Trigger) { let mut selection = if toggle { self.focus.selection.clone() } else { Vec::new() }; match selection.iter().position(|r| *r == row) { @@ -559,7 +561,7 @@ impl View { /// Reports where the dragged row goes with the pointer at `trigger`: the hit point moved with /// the pointer since the press, at the depth where the drag started, snapped to a diagram's - /// dots when its grid snaps. + /// grid dots when the grid snaps. fn drag(&mut self, phase: Phase, trigger: Trigger) { let Gesture::Drag { probed, grab, .. } = &self.gestures.now else { return }; let view = self.projected(); @@ -591,9 +593,9 @@ impl View { self.scene.marks.get(mark).is_some_and(|m| m.style().interaction.select) } - /// Where a connection starts from what a press hit: at a port, or at a vertex with Alt held; - /// or, for an edge with ends, from the end that stays, detaching the end pressed nearer. Also - /// gives the edge, and the start's screen point when known. + /// Where a connection starts from what a press hit: at a port, at a vertex with Alt held, or, + /// for an edge with ends, at the end that stays (the end pressed nearer detaches). Also returns + /// the edge, and the start's screen point when known. fn connect_from(&self, probed: &Probed, modifiers: Modifiers) -> Option<(End, Option, Option<[f32; 2]>)> { let hit = &probed.hit; let shown = self.marks.get(hit.index)?; @@ -619,9 +621,9 @@ impl View { self.events.tell_after(hits, Awaited::Menu(trigger)); } - /// Turns intents whose picks resolved into events, in order; takes the keys' rows once picked; - /// anchors a connection being drawn at its start once located; and opens a menu for a long - /// press. + /// Turns intents whose picks resolved into events, in order. Also takes the keys' rows once + /// picked, anchors a connection being drawn at its start once located, and opens a menu for a + /// long press. pub(super) fn settle_gestures(&mut self) { if let Gesture::Connect { anchor, locating: Some(locating), .. } = &mut self.gestures.now && let Some(found) = locating.take() @@ -681,14 +683,14 @@ impl View { } /// Whether a touch or pen is pressed while long presses open menus, so frames keep running to - /// notice the hold. + /// detect a long press. pub(super) fn holding(&self) -> bool { matches!(self.gestures.now, Gesture::Press { released: false, trigger, .. } if matches!(trigger.device, Device::Touch | Device::Pen)) && !self.scene.controls.press.is_zero() } - /// Zooms about the point under the wheel, as the controls allow; otherwise leaves the wheel to + /// Zooms about the point under the wheel, as the controls allow. Otherwise leaves the wheel to /// the page. fn wheel(&mut self, point: [f32; 2], delta: [f32; 2], modifiers: Modifiers) -> bool { let zooms = match self.scene.controls.zoom { @@ -779,8 +781,9 @@ impl View { /// Steps the row the keys point at: Tab to the next row nearest the view's middle, Shift+Tab /// back, an arrow to the nearest row in its direction. Tab uses the rows as last picked and - /// waits only for the first pick; an arrow waits for rows picked where the frame now draws - /// them. Past either end, Tab is not used, so focus moves on. Returns whether the key was used. + /// waits only for the first pick. An arrow waits for rows picked where the current frame draws + /// them. Past either end, Tab is not used, so focus leaves the view. Returns whether the key + /// was used. fn step(&mut self, key: Key, modifiers: Modifiers) -> bool { let tab = key == Key::Tab; if !self.gestures.keys.fresh && self.gestures.keys.picking.is_none() { diff --git a/crates/latkit/src/view/graph.rs b/crates/latkit/src/view/graph.rs index 29c2015d..6ca42698 100644 --- a/crates/latkit/src/view/graph.rs +++ b/crates/latkit/src/view/graph.rs @@ -129,7 +129,7 @@ pub(super) fn summing(marks: &[Shown]) -> Vec { summing } -/// The vertex mark drawing `table` that ends attach to: the unnamed one, else the first. +/// The vertex mark drawing `table` that ends attach to: the unnamed one, or else the first. pub(super) fn end_mark(marks: &[Shown], data: &Name, table: &Name) -> Option { let drawing = |i: &usize| { let m = &marks[*i]; diff --git a/crates/latkit/src/view/input.rs b/crates/latkit/src/view/input.rs index 191d3016..1f7f7eb4 100644 --- a/crates/latkit/src/view/input.rs +++ b/crates/latkit/src/view/input.rs @@ -13,7 +13,7 @@ pub enum Input { button: Button, /// Modifier keys held. modifiers: Modifiers, - /// The device that pressed; a finger's picks reach farther. + /// The device that pressed. A finger picks rows from farther away. device: Device, }, /// A pointer moved. @@ -83,7 +83,7 @@ pub enum Device { Mouse, /// A pen. Pen, - /// A finger: picks reach 8 CSS pixels farther. + /// A finger. Picks find rows up to 8 CSS pixels farther away. Touch, /// The keyboard. Keyboard, @@ -103,7 +103,7 @@ pub struct Modifiers { } impl Modifiers { - /// Whether a click adds its row to the selection or takes it out: Shift, Control, or Meta held. + /// Whether a click toggles its row in the selection: Shift, Control, or Meta is held. pub fn toggles(self) -> bool { self.shift || self.control || self.meta } diff --git a/crates/latkit/src/view/kept.rs b/crates/latkit/src/view/kept.rs index 458bea22..0d726250 100644 --- a/crates/latkit/src/view/kept.rs +++ b/crates/latkit/src/view/kept.rs @@ -23,8 +23,8 @@ pub(crate) struct KeptTable(Arc<[[u32; 2]]>); impl KeptTable { /// The table of `rows`, each `[place, held row]`, updated from `before`. If `before`'s size - /// suits `rows`, its buckets are reused with places `gone` removed and rows `came` inserted; - /// otherwise the table is rebuilt. + /// suits `rows`, its buckets are reused with places `gone` removed and rows `came` inserted. + /// Otherwise the table is rebuilt. pub(crate) fn laid(before: &KeptTable, rows: &[[u32; 2]], gone: &[u32], came: &[[u32; 2]]) -> KeptTable { let buckets = before.0.len(); // A table of the fewest buckets is updated in place however few rows it keeps. diff --git a/crates/latkit/src/view/labels.rs b/crates/latkit/src/view/labels.rs index ec580653..9dcb8022 100644 --- a/crates/latkit/src/view/labels.rs +++ b/crates/latkit/src/view/labels.rs @@ -1,24 +1,24 @@ //! Labels. //! -//! In the labels buffer each labeled mark keeps a run table, mapping its text codes to where the -//! atlas holds each text, both words 0 until the text is set. Draws place every label from its -//! anchor each frame, and a label shows wherever its text is set, its row shows, and it fits its -//! row's room: no label depends on another, or on an earlier frame. A row whose text is missing has -//! no label. +//! In the labels buffer, each labeled mark keeps a run table that maps its text codes to where the +//! atlas holds each text. Both words are 0 until the text is set. Draws place every label from its +//! anchor each frame. A label shows when its text is set, its row is visible, and the text fits, +//! for example inside a block. No label depends on another label or on an earlier frame. A row +//! whose text is missing has no label. //! -//! Labels beside their rows compete for room on the screen. When the camera, a labeled mark's -//! places, or a run table changes, the claim pass writes each such label's claim into the screen -//! cells its text covers (a short text's room until its text is set), each cell keeping the claim of -//! the row drawn on top. A label shows only where it kept every cell it covers, so labels never -//! overlap, and only labels that keep their room need texts: however large the data, the texts in -//! view are as many as fit on the screen. The claims depend on that frame alone. +//! Labels beside their rows compete for screen space. When the camera, a labeled mark's places, or +//! a run table changes, the claim pass writes each such label's claim into the screen cells its +//! text covers. Until its text is set, a label claims the cells a short text would cover. Each cell +//! keeps the claim of the row drawn on top. A label shows only if it kept every cell it covers, so +//! labels never overlap. Only labels that keep their cells need texts, so however large the data, +//! the texts in view are only as many as fit on the screen. The claims depend on that frame alone. //! -//! The texts pass then finds the texts those labels need: each one near the view whose text the +//! The texts pass then finds the texts those labels need. Each label near the view whose text the //! atlas lacks asks for it, and each one in view whose text is set marks it seen. The host reads -//! both back, sets a few of the asked texts each frame within a budget, and touches the seen ones so -//! the atlas keeps them; a text set shows the frame after. When the atlas has no room left that the -//! texts in view do not use, the rest wait for room. A run table grows with its mark's texts, and the -//! new one starts with every text already set, so labels keep showing. +//! both back, sets a few of the asked texts each frame within a budget, and touches the seen ones +//! so the atlas keeps them. A text shows the frame after it is set. When the atlas has no space +//! left beyond what the texts in view use, the rest wait. A run table grows with its mark's texts, +//! and the new one starts with every text already set, so labels keep showing. use std::collections::HashMap; use std::sync::Arc; @@ -51,7 +51,7 @@ pub(crate) struct Slots { pub(crate) runs: u32, pub(crate) wanted: u32, pub(crate) seen: u32, - /// Codes its run table and seen bits have room for. + /// The number of codes its run table and seen bits have space for. codes: u32, } @@ -108,8 +108,8 @@ impl Labels { impl View { /// How mark `i` sets its label texts, wherever its text field is: in the mark's table, a joined - /// one, or a table the view derives beside it. `None` while the mark has no label, or the table - /// holding its text is missing. + /// one, or a table derived beside it. `None` while the mark has no label or the table with its + /// text is missing. fn setting(&self, i: usize) -> Option { let shown = &self.marks[i]; let compiled = shown.compiled.as_ref()?; @@ -133,7 +133,7 @@ impl View { Some(atlas::run(text, setting.font, setting.size, setting.max_width)) } - /// Codes mark `i`'s label text has so far. + /// The number of codes mark `i`'s label text has so far. fn codes_of(&self, i: usize, inner: &mut Inner) -> u32 { self.setting(i).map_or(0, |setting| setting.dictionary(inner).len() as u32) } @@ -181,13 +181,13 @@ impl View { let Some(run) = self.run_of(i, code, inner) else { continue }; let host = inner.host.clone(); let Some(glyphs) = inner.atlas.text(host.as_deref(), &run) else { - // With no room left this frame, the texts left are asked for once the view - // changes. + // With no atlas space left this frame, the remaining texts are asked for again + // once the view changes. if inner.atlas.full() { break 'marks; } // A text that draws nothing, such as an empty one, gives no label, so it - // claims no room. + // claims no screen cells. set += 1; self.write_words(slots, code, [wgsl::NONE; 2]); continue; @@ -218,8 +218,8 @@ impl View { /// Lays out each labeled mark's slots in the labels buffer, and fixes run table entries whose /// texts the atlas evicted. When the layout or a mark's setting changed, it replaces the buffer - /// and writes back every text still set as before, so labels keep showing; a mark no longer - /// labeled, or setting its texts another way, starts over. + /// and writes back every text still set as before, so labels keep showing. A mark that stopped + /// being labeled, or sets its texts another way, starts over. pub(super) fn lay_out_labels(&mut self, inner: &mut Inner) { let mut slots: Vec> = vec![None; self.marks.len()]; let labeled: Vec = (0..self.marks.len()) @@ -227,7 +227,7 @@ impl View { self.marks[i].drawable() && self.marks[i].compiled.as_ref().is_some_and(|c| c.drawing.label.is_some()) }) .collect(); - // Every mark's wanted codes, then every mark's seen bits, so one copy reads them all; then + // Every mark's wanted codes, then every mark's seen bits, so one copy reads them all. Then // each mark's run table. let mut at = 0; let codes: Vec = labeled.iter().map(|&i| self.codes_of(i, inner).next_power_of_two().max(64)).collect(); @@ -293,8 +293,8 @@ impl View { } } - /// Clears run table entries whose texts the atlas no longer holds where they were, so their - /// labels hide until their texts are set again. + /// Clears run table entries whose texts the atlas evicted or moved, so their labels hide until + /// their texts are set again. fn drop_evicted(&mut self, inner: &Inner) { let mut cleared = Vec::new(); for (&i, written) in &mut self.labels.written { @@ -318,10 +318,10 @@ impl View { self.labels.changes += 1; } - /// Claims the room labels cover, and runs the texts pass, when what they read changed. Claims - /// run in every such frame, since its labels show from them; the texts pass waits while its - /// last readback is in flight, and while a fit is about to move the camera. Both wait until the - /// text pipelines have compiled. + /// Claims the screen cells labels cover, and runs the texts pass, when their inputs changed. + /// Claims run in every such frame, since that frame's labels show from them. The texts pass + /// waits while its last readback is in flight, and while a fit is about to move the camera. + /// Both wait until the text pipelines have compiled. pub(super) fn place_labels(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner, eye: &wgsl::Eye) { if !self.texts() { return; @@ -342,7 +342,7 @@ impl View { let gpu = self.gpu.clone(); let device = &gpu.0.device; let resources = self.resources.as_mut().expect("created before labels"); - // The grid spans the view and a margin around it, a word per cell; one more cell each way + // The grid spans the view and a margin around it, a word per cell. One more cell each way // covers rounding. let cells = |side: f32| u64::from(((side + 2.0 * wgsl::LABEL_MARGIN * side) / wgsl::LABEL_CELL).ceil() as u32 + 1); diff --git a/crates/latkit/src/view/legends.rs b/crates/latkit/src/view/legends.rs index f334838a..1bd7bd35 100644 --- a/crates/latkit/src/view/legends.rs +++ b/crates/latkit/src/view/legends.rs @@ -1,9 +1,9 @@ //! A scene's title and its marks' legends, drawn over the marks. //! //! The title and subtitle sit at the top left. Legends stack upward from the bottom right of the -//! drawing area (inside a plot's axes), each under its title: a ticked bar of the mark's colormap across its domain, a color cell per -//! category, or, for named categories, a swatch beside each name. Rebuilt each frame as overlay -//! sprites, in O(legends). +//! drawing area (inside a plot's axes), each under its title. A legend is a ticked bar of the +//! mark's colormap across its domain, a color cell per category, or, for named categories, a swatch +//! beside each name. Rebuilt each frame as overlay sprites, in O(legends). use std::sync::Arc; @@ -15,7 +15,7 @@ use crate::{Axis, Color, Colormap, ColormapKind, Theme}; /// CSS pixels between the view's inset edge and the title or legends. const MARGIN: f64 = 16.0; -/// A legend's bar: width and height, CSS pixels. +/// A legend's bar width and height, in CSS pixels. const BAR: [f64; 2] = [220.0, 8.0]; /// Cells a continuous bar is drawn in. const CELLS: usize = 64; @@ -33,7 +33,7 @@ const LEADING: f64 = 1.35; impl View { /// CSS pixels the title and subtitle take below the top inset, including the margin above them - /// and half a margin below; 0 when the scene has neither. + /// and half a margin below. 0 when the scene has neither. pub(super) fn title_band(&self) -> f64 { let font = f64::from(self.scene.theme.text.size); let lines: f64 = [(self.scene.title.is_some(), TITLE), (self.scene.subtitle.is_some(), SUBTITLE)] @@ -70,8 +70,8 @@ impl View { writer(inner, scale, color, &mut |w| sprites.extend(w.text(text, [area[0], y], Align::BelowRight))); y += font * scale * LEADING; } - // Legends stack up from the drawing area's bottom right: on a plot, inside its axes, clear of - // their labels. + // Legends stack up from the drawing area's bottom right. On a plot that is inside its axes, + // clear of their labels. let drawing = self.area(); let right = drawing[2] - MARGIN; let mut base = drawing[3] - MARGIN; @@ -203,7 +203,7 @@ fn legend( height } -/// A rectangle filled with `color`, corner `a` to corner `b`, CSS pixels. +/// A rectangle filled with `color`, from corner `a` to corner `b`, in CSS pixels. fn fill(a: [f64; 2], b: [f64; 2], color: Color) -> wgsl::Sprite { let mut sprite = line(a, b, 0.0, color); sprite.kind = wgsl::SPRITE_FILL; diff --git a/crates/latkit/src/view/lists.rs b/crates/latkit/src/view/lists.rs index 0071bb1a..239958d3 100644 --- a/crates/latkit/src/view/lists.rs +++ b/crates/latkit/src/view/lists.rs @@ -92,7 +92,7 @@ impl List { impl View { /// Lays out every listed mark's segment list at the front of the lists buffer, using counts - /// read back: a list smaller than its rows were found to need grows, with a quarter to spare. + /// read back. A list smaller than its rows need grows, with a quarter to spare. pub(super) fn lay_out_segments(&mut self) { let mut at = 0; for shown in &mut self.marks { @@ -180,7 +180,7 @@ impl View { /// Reads back how many segments each great-circle mark needs, so a list too small grows. Each /// mark has one readback at a time: a newer list does not cancel it, and is counted after it - /// arrives. A mark only bowed into lanes always has room for every piece of every lane, so it + /// arrives. A mark only bowed into lanes always has space for every piece of every lane, so it /// reads back nothing. pub(super) fn count_found(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &Inner) { let gpu = self.gpu.clone(); diff --git a/crates/latkit/src/view/marks.rs b/crates/latkit/src/view/marks.rs index dcc6a21d..bc89a5da 100644 --- a/crates/latkit/src/view/marks.rs +++ b/crates/latkit/src/view/marks.rs @@ -26,11 +26,11 @@ pub(crate) struct Offsets { pub(crate) palette: u32, pub(crate) port_info: u32, pub(crate) blends: u32, - /// The runs of held rows it draws; `NONE` without a drawing, when its held rows are its table's + /// The runs of held rows it draws. `NONE` without a drawing, when the held rows are the table's /// rows. pub(crate) runs: u32, - /// The page table of each kind of per-row words, indexed by `Kept`; `NONE` for a kind it does - /// not keep. + /// The page table of each kind of per-row words, indexed by `Kept`. `NONE` for a kind the mark + /// does not keep. pub(crate) pages: [u32; 5], } @@ -86,7 +86,7 @@ pub(crate) struct Shown { pub(crate) rows: u32, /// Rows that left and are fading out, held after the live rows. pub(crate) exits: u32, - /// Rows it held when the frame began; a starting transition freezes these. + /// Rows it held when the frame began. A starting transition freezes these. pub(crate) before: u32, /// The rows its state holds, as of the last published tables. pub(crate) holding: Option, @@ -134,15 +134,15 @@ pub(crate) struct Shown { pub(crate) offsets: Offsets, pub(crate) packed: bool, pub(crate) written: Vec, - /// A transition waiting to start. `transition` is the running one, and `remap` where its remap + /// A transition waiting to start. `transition` is the running one. `remap` is where its remap /// words start in the frame it starts, which the compute pass freezes it from (`NONE` in other - /// frames). `carry` is whether this frame froze rows at new places, so their comets' travel - /// follows them. + /// frames). `carry` is whether this frame froze rows at new places, so their comets follow + /// them. pub(crate) begin: Option, pub(crate) transition: Option, pub(crate) remap: u32, pub(crate) carry: bool, - /// The probe-buffer word where this frame's query writes what it finds of this mark. + /// The probe-buffer word where this frame's query writes its result for this mark. pub(crate) query: u32, /// The list that numbers a stroke mark's segments when rows have different segment counts. pub(crate) segments: super::lists::Segments, @@ -150,10 +150,10 @@ pub(crate) struct Shown { pub(crate) drawn_rows: super::lists::DrawnRows, /// Whether an edge mark follows great circles. pub(crate) geodesic: bool, - /// Incremented whenever the mark's state is written, so whatever derives from it updates. - /// `place_version` is incremented whenever what places its rows and labels is written: row - /// positions, sizes, and visibility, and label texts. A color that changes every frame moves no - /// label. + /// Incremented whenever the mark's state is written, so anything derived from it updates. + /// `place_version` is incremented whenever the channels that place its rows and labels are + /// written: row positions, sizes, visibility, and label texts. A color that changes every frame + /// moves no label. pub(crate) version: u64, pub(crate) place_version: u64, /// The rows its focus draw lights. @@ -216,7 +216,7 @@ impl Shown { } /// Live held rows, not counting exits. Every per-row word belongs to a held row. An unpaged - /// table's held rows are its rows; a paged table holds a page of rows per slot. + /// table's held rows are its rows. A paged table holds a page of rows per slot. pub(crate) fn live(&self) -> u32 { self.drawing().map_or(self.rows, |d| d.live()) } @@ -302,7 +302,7 @@ impl Shown { self.words.release(&mut gpu.arena); } - /// Kind words per row. + /// Words per row that the mark's kind uses. pub(crate) fn stride(&self) -> u32 { self.kind().stride() } @@ -317,14 +317,14 @@ impl Shown { self.stride() + self.extras() } - /// Words per row in each kind of word page its held rows take, 0 for a kind it keeps none of: - /// its state, its focus word, and when faces are `summing` their normals at its rows, those. + /// Words per row for each kind of word page: state, focus, and normals (when faces are + /// `summing` normals at its rows). 0 for a kind the mark keeps none of. pub(crate) fn kept_widths(&self, summing: bool) -> [(Kept, u32); 3] { [(Kept::State, self.width()), (Kept::Focus, 1), (Kept::Normals, if summing { wgsl::NORMAL_WORDS } else { 0 })] } - /// What its channels and fitted domains read besides its own table's static fields, which picks - /// their pipeline variant. Before it compiles, neither joins, samples, nor the view. + /// What its channels and fitted domains read besides its own table's static fields. This picks + /// their pipeline variant. Before the mark compiles, it reads no joins, samples, or view state. pub(crate) fn reads(&self) -> crate::gpu::pipelines::Reads { let Some(program) = self.compiled.as_ref().map(|c| &c.program) else { return crate::gpu::pipelines::Reads::NONE; @@ -355,7 +355,7 @@ impl Shown { } /// Whether its edges curve between their endpoints, along great circles or bowed into lanes. - /// The GPU counts each edge's pieces; for great circles, the total is read back to size the + /// The GPU counts each edge's pieces. For great circles, the total is read back to size the /// segment list. pub(crate) fn curved(&self) -> bool { self.path() == wgsl::PATH_ENDS && (self.geodesic || self.laned()) @@ -367,7 +367,7 @@ impl Shown { compiled.drawing.lane != wgsl::NONE && self.tables.holds(compiled.drawing.lane) } - /// Instances its list needs room for: every path's segments plus each curve's pieces (as last + /// Instances its list needs space for: every path's segments plus each curve's pieces (as last /// counted), or every area's triangles. pub(crate) fn segment_capacity(&self) -> u32 { let rows = (self.rows + self.exits) as usize; @@ -427,7 +427,7 @@ impl Shown { } /// Marks the channels and domains that read any of `columns` dirty. They evaluate over `blocks` - /// (the changed pages) when the channels read only their own rows, else over every row. + /// (the changed pages) when the channels read only their own rows, otherwise over every row. pub(crate) fn columns_changed(&mut self, columns: u64, blocks: Option<&[u32]>) { let Some(compiled) = &self.compiled else { return }; let program = &compiled.program; @@ -438,7 +438,7 @@ impl Shown { channels |= 1 << k; } } - // A fitted domain, or a table read through a join, reaches past a row's own block. + // A fitted domain, or a table read through a join, reads beyond a row's own block. let joined = program.columns.iter().enumerate().any(|(i, c)| c.from.is_some() && columns & Reads::column(i) != 0); // Rows whose endpoint references changed resolve their endpoints again. @@ -677,9 +677,9 @@ impl Blocks { } } -/// The row count of each page of `held` rows, the first `live` of them not exits: a full page per -/// slot of a `paged` table, since slots evaluate whole, or an unpaged table's rows split into -/// pages. Exits follow in either case. +/// The row count of each page of `held` rows, of which the first `live` are not exits. A `paged` +/// table gets a full page per slot, since each slot evaluates as a whole page. An unpaged table's +/// rows are split into pages. Exits follow in either case. pub(crate) fn page_rows(live: u32, held: u32, paged: bool) -> Vec { (0..held.div_ceil(wgsl::PAGE_ROWS)) .map(|p| { diff --git a/crates/latkit/src/view/mod.rs b/crates/latkit/src/view/mod.rs index dab77778..76be5825 100644 --- a/crates/latkit/src/view/mod.rs +++ b/crates/latkit/src/view/mod.rs @@ -1,4 +1,4 @@ -//! The view: draws a scene from its sources into any wgpu frame, and handles input. +//! The view. Draws a scene from its sources into any wgpu frame and handles input. mod axes; mod camera; @@ -74,15 +74,15 @@ pub struct Frame<'a> { /// Physical pixels per CSS pixel. pub pixel_ratio: f32, /// Seconds on the caller's clock: an animation frame's timestamp, a video frame's time, or a - /// test's step. The view's motion advances by the change since the previous frame, clamped to - /// between 0 and 0.25 seconds, so it never jumps or runs backward. + /// test's step. Motion advances by the time since the previous frame, clamped to between 0 and + /// 0.25 seconds, so it never jumps or runs backward. pub time: f64, } -/// Draws a scene from its sources, which are named datasets, and handles input. +/// Draws a scene from its sources (named datasets) and handles input. /// /// The application owns the data and the scene. The view owns the camera, the axis coordinate, the -/// selection, and the GPU state its frames need. It has no loop of its own: +/// selection, and the GPU state for drawing. It has no loop of its own: /// [`render`](View::render) records a frame into an encoder the application submits, and /// [`input`](View::input) takes the platform's pointer and key events. /// @@ -102,7 +102,7 @@ pub struct Frame<'a> { /// ``` pub struct View { gpu: Gpu, - /// This view's id among its GPU's views. The GPU's ledger grants budget shares by it. + /// This view's id among the GPU's views. The GPU's ledger keys budget shares by it. id: u64, scene: Scene, /// Incremented whenever the scene changes. @@ -112,43 +112,43 @@ pub struct View { /// The version of each dataset the view draws, sorted by name: a given snapshot, the latest /// version of a `Data` as of the last refresh, or the tiles loaded so far. snapshots: Vec, - /// What the camera chose to draw of each paged table this frame. + /// The parts of each paged table the camera chose to draw this frame. choices: crate::derive::tiled::Choices, /// Each paged table's drawing, by dataset and table. drawings: crate::data::Keyed<(Name, Name), drawing::Drawn>, - /// Per dataset, the marks' rebuilt tables, waiting for their pages before they replace the - /// current ones. + /// The marks' rebuilt tables, per dataset. They replace the current tables once their pages + /// arrive. staged: crate::data::Keyed, - /// Whether this frame's pages found room in the arena, and if not, how the next frame finds it. + /// Whether this frame's pages fit in the arena, and if not, how the next frame makes space. room: frame::Room, - /// Set once the view has reported that what it draws exceeds the GPU budget. Until a new data - /// version publishes, running out of room and a due fit (with nothing drawn to measure) no - /// longer request frames. + /// Set once the view has reported that its data exceeds the GPU budget. Until a new data + /// version publishes, a full arena and a due fit (with nothing drawn to measure) stop + /// requesting frames. limited: bool, - /// Tables the view derives from the data and the scene, row for row beside the tables it draws. - /// Marks read them like data. + /// Tables derived from the data and the scene, row for row with the drawn tables. Marks read + /// them like data. derived: Derivations, marks: Vec, camera: Camera, rig: Rig, /// CSS pixels kept clear at the top, right, bottom, and left. insets: [f32; 4], - /// The room a plot's axes take around its area, with the stamp of the scene and pixel ratio it - /// was measured for. + /// The space a plot's axes take around the plot area, and the stamp of the scene and pixel + /// ratio it was measured for. gutters: (u64, axes::Gutters), fitting: Fitting, - /// The value range a plot without one shows, fitted to its traces. + /// The value range fitted to the traces, shown when the plot camera sets none. plot_y: Option<[f64; 2]>, at: f64, clock: Clock, effect_time: f64, events: Events, - /// The signal that arriving work wakes, and a waker for it, which readbacks, jobs, and fetches - /// hold. + /// The signal that finished work wakes, and a waker for it. Readbacks, jobs, and fetches keep + /// clones of the waker. signal: Arc, waker: Waker, resources: Option, - /// Where each mark's words lie in the program buffer. + /// Each mark's word range in the program buffer. regions: Regions, /// Something visible changed since the last frame. changed: bool, @@ -167,7 +167,7 @@ pub struct View { /// The same, while the text pipelines compile. Meanwhile frames draw without labels, glyphs, /// and the overlay. texting: Option, - /// The callers waiting on the arrangement under way. + /// Callers waiting for the arrangement in progress. arranging: Vec>>, /// Whether the platform requests reduced motion, which `Motion::System` follows. reduced_motion: bool, @@ -178,23 +178,24 @@ pub struct View { focus: Focus, probing: Probing, queries: query::Queries, - /// Words the marks' segment lists take at the front of the lists buffer. + /// The number of words the marks' segment lists use at the start of the lists buffer. list_words: u32, labels: labels::Labels, gestures: Gestures, - /// The pointer over the view: its point, keys held, and device; `None` once it has left. + /// The pointer over the view: its point, pressed modifier keys, and device. `None` once it + /// leaves. pointer: Option, /// The size of the last frame in CSS pixels, and its pixel ratio. size: [f64; 2], pixel_ratio: f32, - /// How each table draws within the view's share of the budget, and the costs behind it. + /// How each table draws within the view's budget share, and the costs the plan used. planning: plan::Planning, - /// The most pixel columns a trace spans, as the plan allows. + /// The most pixel columns a trace may span, set by the plan. trace_columns: u32, stats: Stats, } -/// Views created so far; gives each view its id. +/// The number of views created, used to give each view its id. static VIEWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); impl View { @@ -273,8 +274,8 @@ impl View { /// Replaces the scene, compiling its marks against the latest version of each /// [`Data`](crate::Data) source. A mark that matches a current mark by kind, data, table, and - /// name keeps its state, and eases to its new description. Errors if the scene is invalid or a - /// mark fails to compile; the current scene then stays. + /// name keeps its state and eases to its new options. Errors if the scene is invalid or a mark + /// fails to compile, and the current scene stays. pub fn set_scene(&mut self, scene: Scene) -> Result<()> { scene.check()?; let gpu = self.gpu.clone(); @@ -343,9 +344,9 @@ impl View { self.camera = Camera::default_for(&scene.space); self.plot_y = None; } - // Fits framed the old space's positions, and motion under way moves through them; neither - // carries over to another kind of space. The next frame places the camera within the new - // space's reach. + // Fit bounds and running motion refer to the old space's positions, so they do not carry + // over to another kind of space. The next frame places the camera within the new space's + // zoom and pan limits. if kind_changed { self.rig.bounds = None; self.rig.hold(); @@ -355,13 +356,13 @@ impl View { } self.scene = scene; self.scene_version += 1; - // Request the new scene's pipelines afresh. They compile while its data syncs and its sheet - // is arranged; those it shares with the old scene are compiled already. + // Request the new scene's pipelines again. They compile while its data syncs and its sheet + // is arranged. Pipelines shared with the old scene are already compiled. self.asked.clear(); self.compiling = None; self.answering = None; self.texting = None; - // If the GPU's samples per pixel are known, start compiling now; else the first frame asks. + // If the GPU's sample count is known, start compiling now. Otherwise the first frame asks. if let Some(samples) = gpu.samples() { self.ask(&mut gpu.0.lock(), (crate::gpu::FORMAT, samples), true); } @@ -372,11 +373,11 @@ impl View { } /// Replaces the view's sources, each a named dataset. A [`Data`](crate::Data) draws its latest - /// version, from the frame after each patch applies. A [`Snapshot`] draws as given. - /// [`Tiles`](crate::Tiles) loads the pieces the view needs. + /// version, starting the frame after each patch applies. A [`Snapshot`] draws as given. + /// [`Tiles`](crate::Tiles) loads the pieces in view. /// - /// This is cheap: unchanged pages are shared, and only changed fields evaluate again. A mark - /// whose dataset no longer fits it draws nothing, and the next frame reports why, unless a new + /// This is cheap: unchanged pages are shared and only changed fields evaluate again. A mark + /// whose dataset no longer fits it draws nothing, and the next frame reports why unless a new /// scene replaces that mark first. So the sources and the scene can be set in either order. /// Errors if two sources share a name. pub fn set_sources>(&mut self, sources: impl IntoIterator) -> Result<()> { @@ -440,12 +441,12 @@ impl View { Some(&self.snapshots[i]) } - /// Brings every source up to date before the frame reads it: the tiles the camera and the plot - /// window need, with the pieces that arrived for them, and the latest version of each changed - /// `Data`. A frame compiles marks, requests pipelines, derives, and draws from one version of - /// each dataset, so a newly added field is never read by a pipeline not requested for it. While - /// a picture holds the view, `Data` sources stay at the versions taken when it began, but tiles - /// still arrive. + /// Updates every source before the frame reads it: the tiles the camera and the plot window + /// need, the pieces that arrived for them, and the latest version of each changed `Data`. A + /// frame compiles marks, requests pipelines, derives, and draws from one version of each + /// dataset, so no pipeline reads a field added after the pipeline was requested. While a shot + /// keeps the view's data unchanged, `Data` sources stay at the versions taken when it began, + /// but tiles still arrive. fn refresh(&mut self, inner: &mut crate::gpu::Inner) { self.update_tiles(inner); for data in self.derived.tiles.take_changed() { @@ -499,8 +500,8 @@ impl View { self.fitting.plot_due = true; } - /// Stops drawing dataset `name`, releasing what its marks hold of it, until a source provides - /// it again. + /// Stops drawing dataset `name` until a source provides it again, and releases the GPU tables + /// its marks use. fn forget(&mut self, name: &str, inner: &mut crate::gpu::Inner) { self.derived.tiles.remove(name, &mut inner.tiles); self.snapshots.retain(|d| d.name() != name); @@ -543,12 +544,12 @@ impl View { /// Moves to `camera` at once, stopping any flight or coasting. Like every setter, it emits no /// event. Ignored unless `camera` [fits](Camera::fits) the scene's space. /// - /// Like every camera move, it keeps the camera within reach. The earth bounds the reach, as - /// [`Earth`](crate::Earth) describes, in the part of the view inside the - /// [`insets`](View::insets). Fits narrow it to 3 zoom levels out and 8 in from what they - /// framed, with the center no farther outside the framed box than its size. The view learns its - /// size from its first frame. When its size, insets, or space change, the next frame moves the - /// camera back within reach and emits a [`Camera`](Event::Camera) event. + /// Every camera move stays within the camera's zoom and pan limits. On the earth, the limits + /// are those [`Earth`](crate::Earth) describes, for the part of the view inside the + /// [`insets`](View::insets). After a fit, the camera zooms at most 3 levels out and 8 in from + /// the fit, and its center stays within one box size of the box the fit framed. The view learns + /// its size from its first frame. When its size, insets, or space change, the next frame moves + /// the camera back within the limits and emits a [`Camera`](Event::Camera) event. pub fn set_camera(&mut self, camera: Camera) { if camera.fits(&self.scene.space) { self.rig.hold(); @@ -584,9 +585,9 @@ impl View { if self.moves() { self.scene.ease } else { Duration::ZERO } } - /// Eases to `camera`, kept within reach as [`set_camera`](View::set_camera) keeps it, over - /// twice the scene's ease, on the view's clock. Ignored unless `camera` [fits](Camera::fits) - /// the scene's space. + /// Eases to `camera` over twice the scene's ease, on the view's clock. The target stays within + /// the zoom and pan limits [`set_camera`](View::set_camera) describes. Ignored unless `camera` + /// [fits](Camera::fits) the scene's space. pub fn fly(&mut self, camera: Camera) { if camera.fits(&self.scene.space) { let seconds = (self.ease().max(Duration::from_millis(1)) * 2).as_secs_f64(); @@ -596,27 +597,27 @@ impl View { } } - /// The screen point of `position` (x, y, and elevation), in CSS pixels from the top left; + /// The screen point of `position` (x, y, and elevation), in CSS pixels from the top left. /// `None` behind the camera. Use it to anchor overlays to what the view draws. pub fn screen(&self, position: [f64; 3]) -> Option<[f32; 2]> { let view = self.projected(); view.screen(view.world(position)).map(|s| [s[0] as f32, s[1] as f32]) } - /// The ground position under screen point `point` (CSS pixels from the top left): on a plane, - /// the point at elevation 0; on the earth, the point on the map or globe; on a plot, the values - /// there. `None` where the point misses the ground. + /// The ground position under screen point `point` (CSS pixels from the top left). On a plane + /// it is the point at elevation 0, on the earth the point on the map or globe, and on a plot + /// the values there. `None` where the point misses the ground. pub fn position(&self, point: [f32; 2]) -> Option<[f64; 3]> { let view = self.projected(); view.ground(point.map(f64::from)).map(|w| view.position(w)) } - /// Lays out every vertex row without a position again, by the scene's layout: a diagram's - /// blocks, or the points of a plane or the earth. Without this call, laid-out positions - /// persist, and rows added later are placed around them. + /// Runs the scene's layout again on every vertex row without a position: a diagram's blocks, or + /// the points on a plane or the earth. Without this call, laid-out positions persist and rows + /// added later are placed around them. /// - /// Once the layout finishes, resolves to one patch per dataset that writes each laid-out row's - /// position into its mark's position fields (or its point `geometry`). Applying the patches + /// Once the layout finishes, resolves to one patch per dataset. Each patch writes the laid-out + /// positions into the mark's position fields (or its point `geometry`). Applying the patches /// makes the positions part of the data. Marks whose position is not a pair of fields are /// skipped. pub fn arrange(&mut self) -> Pending> { @@ -667,7 +668,7 @@ impl View { self.rig.orbit } - /// Turns the camera about its center at `degrees_per_second`; zero stops it. A diagram never + /// Turns the camera about its center at `degrees_per_second`. Zero stops it. A diagram never /// turns. pub fn set_orbit(&mut self, degrees_per_second: f64) { let turns = degrees_per_second.is_finite() && !matches!(self.scene.space, Space::Diagram(_)); @@ -728,20 +729,20 @@ impl View { /// a changed page waits to upload, a tile shows its ancestor, a large table shows an even /// sample of its rows, a row keeps its old position until a layout arrives, a wire shows an /// elbow until its route arrives, an area shows its previous triangles, a label shows its - /// estimated size, or a fit is due. Shots and videos wait for it, and + /// estimated size, or a fit is due. Shots and videos wait until it is true. /// [`waiting`](View::waiting) lists what is pending. pub fn complete(&self) -> bool { self.waiting.is_empty() } - /// The kinds of pending work the last frame showed placeholders for; empty once the view is - /// [complete](View::complete). [`Waiting`](Event::Waiting) events report its changes. + /// The kinds of pending work the last frame showed placeholders for. Empty once the view is + /// [complete](View::complete). [`Waiting`](Event::Waiting) events report changes to it. pub fn waiting(&self) -> Waiting { self.waiting } - /// The events since the last call, in order: see [`Event`]. An intent whose details depend on a - /// pick is held until the pick finishes, and later intents wait behind it. + /// The events since the last call, in order. See [`Event`]. An intent that needs a pick result + /// waits until the pick finishes, and later intents wait behind it. pub fn events(&mut self) -> impl Iterator + use<> { self.events.drain() } @@ -758,7 +759,7 @@ impl View { } /// Whether a [`Shot`](crate::Shot) or a [`Video`](crate::Video) is drawing the view. Nothing - /// else should draw it meanwhile, since a view draws at one size at a time; a canvas or window + /// else should draw it meanwhile, since a view draws at one size at a time. A canvas or window /// keeps showing its last frame. pub fn lent(&self) -> bool { self.signal.lent() @@ -776,15 +777,15 @@ impl View { self.events.error().cloned() } - /// Whether the view can draw frames into `format` at `pixel_ratio` now, with every pipeline its - /// scene draws with compiled. + /// Whether the view can draw frames into `format` at `pixel_ratio` now, because the scene's + /// draw pipelines for that target are compiled. /// /// The first call for a target requests every pipeline the scene uses, draw pipelines first. In /// a browser they compile in parallel off the GPU thread, and each one that finishes wakes the - /// view. Until the draw pipelines are ready, the view draws nothing new, so whatever shows its - /// frames keeps the last one, as a canvas does. Until the rest are ready, frames omit text and - /// lit rows, and probes and queries wait. Natively the view can always draw. - /// [`render`](View::render) checks this itself and records nothing until it is true. + /// view. Until the draw pipelines are ready, the view draws nothing new, so a canvas keeps + /// showing the last frame. Until the rest are ready, frames omit text and lit rows, and probes + /// and queries wait. Natively the view can always draw. [`render`](View::render) checks this + /// itself and records nothing until it is true. pub fn ready(&mut self, format: wgpu::TextureFormat, pixel_ratio: f32) -> bool { self.ready_for(format, pixel_ratio, true) } @@ -830,11 +831,11 @@ impl View { } /// Whether a new frame would differ from the last one: something changed, animates, or streams - /// in, or pending work arrived. While the view only waits, it needs no frame; the work it waits + /// in, or pending work arrived. While the view only waits, it needs no frame. The work it waits /// on wakes it. pub fn needs_render(&self) -> bool { let landed = || self.gpu.0.lock().pipelines.landed(); - // While draw pipelines compile, the view draws nothing new; each finished compile wakes it. + // While draw pipelines compile, the view draws nothing new. Each finished compile wakes it. if let Some(seen) = self.compiling { return landed() != seen; } @@ -863,8 +864,8 @@ impl View { /// Whether the view wants a frame: one that would differ /// ([`needs_render`](View::needs_render)), or one that only checks whether a touch has been - /// held long enough to open a menu. A loop that draws on demand draws while this is true, as a - /// canvas does. A video repeats its last frame instead of drawing frames that only check. + /// pressed long enough to open a menu. A loop that draws on demand, like a canvas, draws while + /// this is true. A video repeats its last frame rather than draw frames that only check. pub fn wants_frame(&self) -> bool { self.needs_render() || (self.compiling.is_none() && self.holding()) } @@ -875,8 +876,8 @@ impl View { self.scene.theme.flow.color.a > 0.0 && self.marks.iter().any(|m| m.drawable() && m.mark.flows()) } - /// Whether the next frame, taking `input` or not, has work this one left: pages to upload past - /// the frame's budget, pages that found no room this frame, a due fit, probe, or query, tile + /// Whether this frame left work for the next one (taking `input` or not): pages over the + /// frame's upload budget, pages that did not fit in the arena, a due fit, probe, or query, tile /// pieces to take, or a transition to begin. Nothing else would wake the view for these. Jobs, /// texts, fetches, and readbacks wake it themselves when they arrive. fn due(&self, input: bool) -> bool { @@ -893,10 +894,10 @@ impl View { self.signal.ask(); } - /// The pending work a frame drawn now would show placeholders for: see + /// The pending work a frame drawn now would show placeholders for. See /// [`complete`](View::complete). A mark waits for its dataset until a source provides it. A - /// mark that failed, or whose dataset comes from tiles (which may never hold its table), waits - /// for nothing. + /// mark that failed, or whose dataset comes from tiles (which may never contain its table), + /// waits for nothing. fn under_way(&self) -> Waiting { let marks = || self.marks.iter().filter(|m| m.compiled.is_some()); let given = |m: &Shown| { @@ -921,9 +922,8 @@ impl View { } } - /// Whether something is easing to a rest: a transition, a flight, coasting, or a tile fade. - /// Orbits, comets, and other motion that lasts as long as it is shown never settle, and do not - /// count. + /// Whether something is easing to rest: a transition, a flight, coasting, or a tile fade. + /// Orbits, comets, and other continuous motion never settle and do not count. pub(crate) fn easing(&self) -> bool { self.rig.easing() || self.fading() || self.marks.iter().any(|m| m.begin.is_some() || m.transition.is_some()) } @@ -971,8 +971,8 @@ impl View { self.projected_with(&self.drawn_camera()) } - /// The projection through `camera`, with a plot's missing value range filled in as the frame - /// fills it. + /// The projection through `camera`. A plot camera without a value range uses the fitted one, + /// as the frame does. fn projected_with(&self, camera: &Camera) -> Projected { let camera = match *camera { Camera::Plot { x, y: None } => Camera::Plot { x, y: Some(self.plot_y.unwrap_or([0.0, 1.0])) }, diff --git a/crates/latkit/src/view/passes.rs b/crates/latkit/src/view/passes.rs index 456474a7..97e4e2da 100644 --- a/crates/latkit/src/view/passes.rs +++ b/crates/latkit/src/view/passes.rs @@ -1,11 +1,13 @@ //! A frame's GPU passes. //! -//! The compute pass freezes what rows showed for transitions starting this frame, expands paged -//! marks' drawn rows from their runs, evaluates dirty channels over the changed row blocks +//! The compute pass freezes the rows' current state for transitions starting this frame, expands +//! paged marks' drawn rows from their runs, evaluates dirty channels over the changed row blocks //! (reducing fitted domains first), resolves endpoints, eases marks in transition, moves comets, -//! numbers segments, sums face normals when their corners moved, and measures extents when asked. The render pass draws the backdrop, then every mark's primitive and -//! ports in scene order with depth, then the labels and the overlay. A pending probe then reads -//! what the frame drew, through the same geometry. +//! numbers segments, sums face normals when their corners moved, and measures extents when asked. +//! +//! The render pass draws the backdrop, then every mark's primitive and ports in scene order with +//! depth, then the labels and the overlay. A pending probe then reads what the frame drew, through +//! the same geometry. use super::resources::MARK_STRIDE; use super::sync::Source; @@ -177,7 +179,7 @@ impl Marks<'_> { /// The marks each step of a frame's compute pass covers. struct Work { - /// Marks whose transitions start this frame, freezing what their rows showed. + /// Marks whose transitions start this frame, freezing their rows' current state. frozen: Vec, evaluated: Vec, /// Marks whose rows resolve changed endpoints. @@ -187,8 +189,8 @@ struct Work { segments: Vec, faces: Vec, measured: Vec, - /// Marks whose comets carry their travel over to rows a transition moved; `travelled`, marks - /// whose comets move this frame. + /// Marks whose comets carry their travel over to rows a transition moved. `travelled` lists + /// marks whose comets move this frame. carried: Vec, travelled: Vec, /// Marks whose drawn rows' words are rebuilt from their runs. @@ -240,9 +242,9 @@ pub(super) fn offset(index: usize) -> u32 { (index as u64 * MARK_STRIDE) as u32 } -/// The least and greatest finite value of `input` over `rows`, encoded as `lib/reduce.wesl` keeps -/// them: the complement of the least's key, then the greatest's. `[0, 0]` when no row has a finite -/// value. +/// The minimum and maximum finite value of `input` over `rows`, encoded as `lib/reduce.wesl` keeps +/// them: the complement of the minimum's key, then the maximum's key. `[0, 0]` when no row has a +/// finite value. fn fit(rows: &Rows<'_>, input: &crate::Expr) -> [u32; 2] { use crate::order::number32; let (mut least, mut most) = (f32::INFINITY, f32::NEG_INFINITY); @@ -373,10 +375,10 @@ impl View { pass.set_bind_group(2, state, &[step(wgsl::STEP_CHANNELS)]); } - /// Records the frame's compute pass: freezes starting transitions, evaluates dirty channels, and - /// sums face normals when their inputs changed. With `measure`, it also measures the extents of - /// every measured mark and returns their readback. With `travel`, comets move by the frame's - /// step. + /// Records the frame's compute pass: freezes starting transitions, evaluates dirty channels, + /// and sums face normals when their inputs changed. With `measure`, it also measures the + /// extents of every measured mark and returns their readback. With `travel`, comets move by the + /// frame's step. pub(super) fn compute( &mut self, encoder: &mut wgpu::CommandEncoder, @@ -430,8 +432,8 @@ impl View { }); pass.set_bind_group(0, &groups.view, &[]); let held = super::marks::Shown::held; - // Starting transitions freeze what their rows showed as the frame began, before any - // pass writes it, and after packing placed their page tables. + // Starting transitions freeze their rows' state as the frame began, before any pass + // writes it, and after packing placed their page tables. if let Some(group) = &freeze_group { pass.set_bind_group(2, group, &[]); self.over_marks(&mut pass, inner, &groups.mark, GATHER, &frozen, held); @@ -475,7 +477,7 @@ impl View { } /// Prepares the fitted domains that marks `work` reduce. Domains computed on the CPU over a - /// paged table's rows are written; the rest are cleared for the GPU to fit. + /// paged table's rows are written. The rest are cleared for the GPU to fit. fn start_domains(&mut self, encoder: &mut wgpu::CommandEncoder, work: &[usize]) { let mut fitted: Vec<(usize, Vec)> = Vec::new(); for &i in work { @@ -550,8 +552,8 @@ impl View { } /// Writes the blocks buffer, which each mark's uniform points into. For each mark, it lists the - /// row blocks it evaluates this frame (every block when a channel changed for every row, else - /// the changed pages), then the row blocks whose endpoints it resolves. + /// row blocks it evaluates this frame (every block when a channel changed for every row, + /// otherwise the changed pages), then the row blocks whose endpoints it resolves. pub(super) fn lay_out_blocks(&mut self) { let mut words = Vec::new(); for shown in &mut self.marks { @@ -747,7 +749,7 @@ impl View { marks.draw(&mut pass, drawn, inner, i, shown, false); marks.ports(&mut pass, drawn, inner, i, shown); } - // Lit rows draw again, glowing, over every mark but labels; only their own ports cover + // Lit rows draw again, glowing, over every mark but labels. Only their own ports cover // them. They draw once their pipelines have compiled. for (i, shown) in self.marks.iter().enumerate() { if self.answers() && shown.drawable() && shown.lit.rows > 0 { @@ -771,7 +773,7 @@ impl View { /// drawn there, and returns their readback. pub(super) fn probe(&mut self, encoder: &mut wgpu::CommandEncoder, inner: &mut Inner) -> Readback { let probed: Vec = (0..self.marks.len()).filter(|&i| self.marks[i].probed()).collect(); - // Any drawn mark's probe program can read the depth under the probe; the first one does. + // Any drawn mark's probe program can read the depth under the probe. The first one does. let under: Vec = self.marks.iter().position(super::marks::Shown::drawable).into_iter().collect(); let mut reset = [0_u32; wgsl::PROBE_WORDS as usize]; reset[wgsl::PROBE_KEY as usize] = wgsl::NONE; @@ -810,7 +812,7 @@ impl View { let shown = &self.marks[i]; pass.set_pipeline(&inner.pipelines.compute(device, MarkVariants::shown(shown).probe(shader))); pass.set_bind_group(1, &groups.mark, &[offset(i)]); - // A step that reads one result runs once; the rest run over every instance probes + // A step that reads one result runs once. The rest run over every instance probes // cover. if matches!(step, wgsl::STEP_DESCRIBE | wgsl::STEP_LOCATE | wgsl::STEP_UNDER) { pass.dispatch_workgroups(1, 1, 1); diff --git a/crates/latkit/src/view/plan.rs b/crates/latkit/src/view/plan.rs index 6c946ed3..dc43216e 100644 --- a/crates/latkit/src/view/plan.rs +++ b/crates/latkit/src/view/plan.rs @@ -1,24 +1,25 @@ //! The plan: how each drawn table fits the GPU budget. //! -//! A table draws whole (in priority order when a mark gives one) unless it is too large. A table a -//! mark places on a map or a plane becomes a paged table when it is denser than a screen can show, -//! and the largest such tables are paged until the rest fit the view's budget share. A table of -//! vector tiles is always paged, its tiles the vector tiles drawn; other tables drawn from tiles +//! A table draws all its rows (in priority order when a mark gives one) unless it is too large. A +//! table a mark places on a map or a plane is paged when it is denser than a screen can show. The +//! largest such tables are paged until the rest fit the view's budget share. A table of vector +//! tiles is always paged, with the vector tiles drawn as its tiles. Other tables drawn from tiles //! never are. A paged table draws the rows of the tiles the camera needs, as many as its share -//! holds, and edges ending at it are paged too, so they show with their endpoints. If the whole -//! tables still do not fit, a table a line traces keeps drawing whole, its traces spanning only as -//! many pixel columns as fit, and any other table draws the rows that matter most, as many as fit. -//! What a view draws always fits. +//! holds. Edges ending at it are paged too, so they show with their endpoints. If the unpaged +//! tables still do not fit, a table a line traces still draws all its rows, with its traces +//! spanning only as many pixel columns as fit. Any other table draws the rows that matter most, as +//! many as fit. What a view draws always fits. //! //! A table costs what the arena allocates for it: its columns' pages, the arena blocks of its -//! marks' per-row words, and its geometry. The GPU's ledger grants each view a share based on what -//! it would draw whole. Costs are recomputed only when the scene, or a drawn table's rows or -//! geometry, change, and the plan only when the costs or the share change. +//! marks' per-row words, and its geometry. The GPU's ledger grants each view a share based on its +//! cost with every table drawn in full. Costs are recomputed only when the scene, or a drawn +//! table's rows or geometry, change. The plan is recomputed only when the costs or the share +//! change. //! //! Paged tables can need more than the costs predict, such as the kept rows that drawn edges end -//! at. When what the view holds exceeds its share, or finds no room in a full arena, paged tables -//! are planned within a smaller part of the share until the share changes. That way the current -//! drawing and its replacement both fit, with room left to keep idle pages. +//! at. When the view's memory exceeds its share, or its pages do not fit in a full arena, paged +//! tables are planned within a smaller part of the share until the share changes. That way the +//! current drawing and its replacement both fit, with space left to keep idle pages. use std::hash::Hash; @@ -37,13 +38,13 @@ use crate::{Expr, Kind, Mark, Space}; /// Rows past which a table a mark places is paged: more than a screen shows at one row per cell. pub(crate) const DENSE: usize = 1 << 18; -/// Pixel columns a view requests trace room for: a wide screen's width. +/// Pixel columns a view requests trace space for: a wide screen's width. const ASKED_COLUMNS: f64 = 4096.0; /// The fewest rows a paged table draws: one tile's worth. const LEAST: u32 = 1 << 14; -/// Bytes a row's trace takes per pixel column: the least and greatest values of two buckets. +/// Bytes a row's trace takes per pixel column: the minimum and maximum values of two buckets. const TRACE_COLUMN_BYTES: f64 = 16.0; -/// The fewest pixel columns a trace spans, however little room is left. +/// The fewest pixel columns a trace spans, however little budget is left. const TRACE_COLUMNS: u32 = 32; /// A table's cost as the arena allocates it: its row count, bytes per page of rows, bytes of @@ -66,7 +67,7 @@ impl Cost { } } -/// Each table the scene draws, planned as drawn whole, and its cost. +/// Each table the scene draws, planned as drawn in full, and its cost. #[derive(Default)] struct Costs { whole: Plans, @@ -81,8 +82,8 @@ pub(crate) struct Planning { costs: Derived, /// How each table draws, and the most pixel columns a trace spans. plan: Derived<(Plans, u32)>, - /// The fraction of the remaining room the plan gives paged tables. Lowered each time they find - /// no room in a full arena or exceed the share; reset when the share changes. + /// The fraction of the remaining budget the plan gives paged tables. Lowered each time their + /// pages do not fit in a full arena or exceed the share. Reset when the share changes. fit: f64, share: f64, } @@ -109,23 +110,23 @@ impl Planning { self.plan.value.1 } - /// Records that paged tables found no room in a full arena, so they draw within less while any + /// Records that paged tables did not fit in a full arena, so they draw within less while any /// draws more than its minimum. Returns whether the plan shrank. pub(crate) fn overflowed(&mut self) -> bool { self.tighten(0.75) } - /// Records that the view holds `bytes` for what it draws. If that exceeds the share and the - /// plan held back some of what the camera needs of a paged table (`capped`), paged tables draw - /// within less, in proportion to the excess. If nothing was held back, a smaller plan would - /// draw no less and only release idle pages, so the plan stays. + /// Records that the view uses `bytes` for what it draws. If that exceeds the share and the plan + /// left out some of the rows the camera needs from a paged table (`capped`), paged tables draw + /// within less, in proportion to the excess. If nothing was left out, a smaller plan would draw + /// no less and only release idle pages, so the plan stays. pub(crate) fn held(&mut self, bytes: u64, capped: bool) { if capped && self.share > 0.0 && bytes as f64 > self.share { self.tighten(self.share / bytes as f64); } } - /// Scales the room for paged tables by `part`, if any draws more than its minimum. Returns + /// Scales the budget for paged tables by `part`, if any draws more than its minimum. Returns /// whether it did. fn tighten(&mut self, part: f64) -> bool { let shrinks = self.plans().values().any(|p| matches!(p.draw, Draw::Tiles { rows } if rows > LEAST)); @@ -138,7 +139,7 @@ impl Planning { impl View { /// Updates the plan for the scene, the data, and the share of `budget` that the GPU's `ledger` - /// grants the view based on what it would draw whole. + /// grants the view based on its cost with every table drawn in full. pub(super) fn update_plan(&mut self, ledger: &mut Ledger, budget: u64) { let stamp = self.costs_stamp(); if self.planning.costs.due(stamp) { @@ -182,7 +183,7 @@ impl View { } /// Plans how each table the scene draws fits in `share` bytes given `costs`, giving paged - /// tables `fit` of the remaining room. Also returns the most pixel columns a trace spans. + /// tables `fit` of the remaining budget. Also returns the most pixel columns a trace spans. fn plan(&self, costs: &Costs, share: f64, fit: f64) -> (Plans, u32) { let (mut plans, costs) = (costs.whole.clone(), &costs.tables); let vector = |key: &(Name, Name)| self.derived.tiles.vector(&key.0); @@ -197,7 +198,7 @@ impl View { tiled.push(key.clone()); } }; - // Page tables of vector tiles and dense tables, then the largest ones until the whole + // Page tables of vector tiles and dense tables, then the largest ones until the unpaged // tables fit. let keys: Vec<(Name, Name)> = plans.keys().cloned().collect(); for key in &keys { @@ -219,8 +220,9 @@ impl View { tile(key, &mut plans, &mut whole); } } - // If the whole tables still do not fit, each draws the rows that matter most, as many as - // fit. A table a line traces still draws whole; its traces get coarser in time instead. + // If the unpaged tables still do not fit, each draws the rows that matter most, as many as + // fit. A table a line traces still draws all its rows, and its traces get coarser in time + // instead. if whole > share { let kept: f64 = keys .iter() @@ -237,7 +239,7 @@ impl View { } whole = share.max(kept); } - // Paged tables and traces share the remaining room. + // Paged tables and traces share the remaining budget. let traced: f64 = costs.values().map(|c| c.traced).sum(); let left = (share - whole).max(share / 4.0) * fit; let each = left / (tiled.len() + usize::from(traced > 0.0)).max(1) as f64; @@ -251,8 +253,8 @@ impl View { (plans, columns) } - /// Plans each table the scene draws as whole, with the place and priority of the first mark - /// that gives each, and computes each table's cost from the fields its marks read. + /// Plans each table the scene draws as drawn in full, with the place and priority of the first + /// mark that gives each, and computes each table's cost from the fields its marks read. fn costs(&self) -> Costs { let space = &self.scene.space; let placing = matches!(space, Space::Earth(_) | Space::Plane(_)); diff --git a/crates/latkit/src/view/probe.rs b/crates/latkit/src/view/probe.rs index 0bc68a65..8469d6c2 100644 --- a/crates/latkit/src/view/probe.rs +++ b/crates/latkit/src/view/probe.rs @@ -1,8 +1,8 @@ //! Probes: the nearest row and the nearest drawn depth under a point. //! //! A compute pass runs over what the frame drew, and its result is read back a frame or two later. -//! One probe is in flight at a time; a newer point waits for it. The answer is decoded against the -//! frame that drew it (its marks' held rows and its camera), regardless of changes since. +//! One probe is in flight at a time, and a newer point waits for it. The answer is decoded against +//! the frame that drew it (its marks' held rows and its camera), regardless of changes since. use std::sync::Arc; @@ -31,7 +31,7 @@ pub(crate) struct Probed { #[derive(Clone, Debug, Default)] pub(crate) struct Answer { pub(crate) hit: Option, - /// The nearest depth drawn around the point; 0 where nothing was drawn. + /// The nearest depth drawn around the point. 0 where nothing was drawn. pub(crate) under: f32, } @@ -43,8 +43,8 @@ pub(crate) struct Drew { projected: Projected, } -/// One mark as its frame drew it: its dataset, table, and name; whether its rows are vertices and -/// whether selection picks them; the rows its state held; its port names; and for a line tracing a +/// One mark as its frame drew it: its dataset, table, and name, whether its rows are vertices, +/// whether selection picks them, the rows its state held, its port names, and, for a line tracing a /// field, the field and its trace plan. struct Held { data: Name, @@ -97,7 +97,7 @@ impl Drew { self.marks.get(i).is_some_and(|m| m.selects) } - /// The name of port `number` of vertex mark `i`, numbered from 1; `None` for 0, the marker. + /// The name of port `number` of vertex mark `i`, numbered from 1. `None` for 0, the marker. fn port(&self, i: usize, number: u32) -> Option { let mark = self.marks.get(i)?; if !mark.vertices || number == 0 { @@ -113,7 +113,7 @@ impl Drew { let (level, index, side) = plan.entry(point)?; let table = &mark.holding.as_ref()?.table; let crate::data::Field::Sampled(samples, _) = table.field_named(field)? else { return None }; - // A bucket's two points show its middle frame, with its least or greatest value. + // A bucket's two points show its middle frame, with its minimum or maximum value. let span = crate::data::bucket_frames(level); let frame = index * span + span / 2; let value = samples.entry(row, level, side, index)?; @@ -218,8 +218,9 @@ impl View { self.landed(point, answer); } - /// The position gestures pivot on under screen point `point`: what the frame drew there, else - /// the ground level the camera looks at, else a point as far away as the camera's center. + /// The position gestures pivot on under screen point `point`: what the frame drew there, or + /// else the ground level the camera looks at, or else a point as far away as the camera's + /// center. pub(super) fn anchor(&self, point: [f32; 2], answer: Option<&Answer>) -> [f64; 3] { let view = self.projected(); let p = point.map(f64::from); @@ -260,7 +261,7 @@ fn hit(word: &impl Fn(u32) -> u32, drew: &Drew) -> Option { } impl super::marks::Shown { - /// Whether probes reach this mark's rows. + /// Whether probes cover this mark's rows. pub(crate) fn probed(&self) -> bool { self.drawable() && self.mark.picks() && self.rows > 0 } diff --git a/crates/latkit/src/view/program.rs b/crates/latkit/src/view/program.rs index 489c988a..d48d0afe 100644 --- a/crates/latkit/src/view/program.rs +++ b/crates/latkit/src/view/program.rs @@ -1,11 +1,11 @@ //! The program buffer: each mark's column descriptors and tables, ranges, code, constants, end //! tables, and ports, in a region of its own. //! -//! When a mark changes, only its region is rewritten, and only the words that differ. A region that -//! outgrows its room moves to the end of the buffer. If the buffer is too small, it grows and the -//! GPU copies its contents over, so no other region is rewritten. Once moved regions leave more of -//! the buffer unused than the regions take, every region is laid out again from the start, each -//! writing only its own words. +//! When a mark changes, only its region is rewritten, and only the words that differ. A region +//! that outgrows its capacity moves to the end of the buffer. If the buffer is too small, it grows +//! and the GPU copies its contents over, so no other region is rewritten. Once moved regions leave +//! more of the buffer unused than the regions take, every region is laid out again from the start, +//! each writing only its own words. use super::{Event, View}; use crate::gpu::Inner; @@ -20,7 +20,7 @@ pub(crate) struct Regions { /// Changed words separated by fewer unchanged words than this are written in one span. const GAP: usize = 16; -/// The room given to a region of `len` words, with slack to grow in place. +/// The capacity given to a region of `len` words, with slack to grow in place. fn room(len: u32) -> u32 { len + len / 4 + 16 } @@ -75,7 +75,8 @@ impl View { } }; if !self.regions.relayout { - // The room the regions take. Moved regions left the rest of the buffer, up to `end`. + // The space the regions take. Moved regions left the rest of the buffer unused, up to + // `end`. let mut live: u32 = self.marks.iter().filter_map(|m| m.region).map(|[_, room]| room).sum(); for shown in self.marks.iter_mut().filter(|m| !m.packed && m.compiled.is_some()) { let len = shown.packed_len(); @@ -119,8 +120,8 @@ impl View { if std::mem::take(&mut self.regions.relayout) { let total: u64 = self.marks.iter().map(|m| u64::from(room(m.packed_len()))).sum(); resources.fit_program(device, total); - // Write each region's words at the start of its room. The slack after them is not - // written: nothing reads it until the region grows into it and writes it. + // Write each region's words at its start. The slack after them is not written: nothing + // reads it until the region grows into it and writes it. let mut start = 0; for shown in self.marks.iter_mut().filter(|m| m.compiled.is_some()) { let len = shown.packed_len(); diff --git a/crates/latkit/src/view/query.rs b/crates/latkit/src/view/query.rs index b2b3e38d..0c78b75a 100644 --- a/crates/latkit/src/view/query.rs +++ b/crates/latkit/src/view/query.rs @@ -1,11 +1,12 @@ -//! Queries: every row within reach of a point, every row in a box, and where a row lies on screen. +//! Queries: every row within a radius of a point, every row in a box, and where a row lies on +//! screen. //! //! Each mark's probe program answers on the GPU, through the geometry the frame drew. The result is //! read back a frame or two later and decoded against that frame. A query's results for each mark //! follow the probe's words in the probe buffer: a hit count and capacity followed by the hits, or //! a bit per row. Hits arrive in any order and are sorted before they are returned. A mark that -//! found more hits than it kept is queried again with room for all of them. One query runs at a -//! time; the rest wait in order, and a query nobody awaits any more is skipped. +//! found more hits than it kept is queried again with space for all of them. One query runs at a +//! time. The rest wait in order, and a query nobody awaits any more is skipped. use std::collections::VecDeque; @@ -21,7 +22,7 @@ use crate::gpu::shaders as wgsl; use crate::{Pending, Resolver, Row}; /// Hits kept per mark on the first try. Hits past these are counted but not kept, and the query -/// runs again with room for them. +/// runs again with space for them. const HITS: u32 = 1024; /// A query for the GPU. @@ -80,7 +81,7 @@ impl Ask { } } -/// Where a query's results for mark `mark` lie: from probe-buffer word `at`, with room for `room` +/// Where a query's results for mark `mark` lie: from probe-buffer word `at`, with space for `room` /// hits or rows. #[derive(Clone, Copy)] struct Found { @@ -130,7 +131,7 @@ impl View { answer } - /// Where `row` lies on screen, in CSS pixels from the top left, as the next frames draw it; + /// Where `row` lies on screen, in CSS pixels from the top left, as the next frames draw it. /// `None` when no mark draws it in view. A diagram's vertices resolve at once from its sheet. pub fn screen_of(&mut self, row: &Row) -> Pending> { let diagram = matches!(self.scene.space, crate::Space::Diagram(_)); @@ -162,9 +163,9 @@ impl View { shown.item_of(shown.held_row(index as u32)?).filter(|&item| item < shown.rows) } - /// Starts the next query this frame if none is in progress, skipping those nobody awaits. Makes - /// room for its results after the probe's words, and returns its query words and whether it - /// reads depth. + /// Starts the next query this frame if none is in progress, skipping those nobody awaits. + /// Reserves space for its results after the probe's words, and returns its query words and + /// whether it reads depth. pub(super) fn next_query(&mut self) -> Option<([f32; 4], bool)> { if self.queries.running.is_some() || self.queries.reading.busy() { return None; @@ -244,7 +245,7 @@ impl View { }; match ask { Ask::Hits { point, radius, room, reply } => { - // If a mark found more hits than it kept, run again with room for all of them. + // If a mark found more hits than it kept, run again with space for all of them. let most = found.iter().map(|f| words_at(&words, f.at, 1).first().copied().unwrap_or(0)).max(); match most.filter(|&most| most > room) { Some(room) => self.queries.waiting.push_front(Ask::Hits { point, radius, room, reply }), @@ -282,9 +283,9 @@ impl View { /// The hits in `words` for `found`, decoded against `drew`, the frame they came from. fn hits(found: &[Found], words: &[u32], drew: &Drew) -> Vec { - // Nearest first; among equals, the mark drawn last first, and a port before its marker; then - // by row and position, so the order never depends on which hit arrived first. Each row appears - // once, at its nearest hit. + // Nearest first. Among equals, the mark drawn last comes first, and a port before its marker. + // Then by row and position, so the order never depends on which hit arrived first. Each row + // appears once, at its nearest hit. let mut kept: Vec<(usize, &[u32; wgsl::HIT_WORDS as usize])> = found .iter() .flat_map(|f| { diff --git a/crates/latkit/src/view/resources.rs b/crates/latkit/src/view/resources.rs index 26f35f1a..8c732de2 100644 --- a/crates/latkit/src/view/resources.rs +++ b/crates/latkit/src/view/resources.rs @@ -18,7 +18,7 @@ use crate::gpu::{Inner, Shared}; /// Bytes between marks' uniforms: the dynamic offset alignment every device supports. pub(crate) const MARK_STRIDE: u64 = 256; const _: () = assert!(size_of::() as u64 <= MARK_STRIDE, "a mark's uniform fits its stride"); -/// The depth format; depth is reversed, nearer is greater. +/// The depth format. Depth is reversed: nearer is greater. pub(crate) const DEPTH: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; /// The dynamic offset of the probe's step `step`, one of the `STEP_` codes. pub(crate) fn step(step: u32) -> u32 { @@ -168,7 +168,8 @@ impl Resources { } } - /// Makes room for `words` of program; true when the buffer was replaced and must be rewritten. + /// Makes room for `words` of program. Returns true when the buffer was replaced and must be + /// rewritten. pub(crate) fn fit_program(&mut self, device: &wgpu::Device, words: u64) -> bool { let replaced = self.program.ensure(device, (words + 1) * 4); if replaced { diff --git a/crates/latkit/src/view/rig.rs b/crates/latkit/src/view/rig.rs index d249519f..62a894b2 100644 --- a/crates/latkit/src/view/rig.rs +++ b/crates/latkit/src/view/rig.rs @@ -1,12 +1,12 @@ //! Camera motion. A gesture moves the camera at once, about the point under the pointer. The wheel, -//! the keys, and a released drag add pending motion about such a point, which the following frames -//! apply with exponential decay, so a fling coasts and a wheel notch glides by the same rule. A -//! flight eases over a set time, and an orbit turns steadily. All of it runs on the view's clock. +//! the keys, and a released drag add pending motion about such a point. The following frames apply +//! it with exponential decay, so a fling coasts and a wheel notch glides by the same rule. A flight +//! eases over a set time, and an orbit turns steadily. All of it runs on the view's clock. //! -//! The camera keeps within its reach: where the space lets it go in the room the camera frames, -//! narrowed by fits. Every move places it through the reach: gestures, pending motion, flights, -//! orbits, fits, and the setter. The reach changes with the view's size and insets, its space, and -//! fits, so each frame places the camera again before it moves. +//! The camera stays within its zoom and pan limits (`Reach`): what the space allows for the area +//! the camera frames, narrowed by fits. Every move is clamped to these limits: gestures, pending +//! motion, flights, orbits, fits, and the setter. The limits change with the view's size and +//! insets, its space, and fits, so each frame clamps the camera again before it moves. use super::camera::{Bounds, Camera, Reach}; use super::{Event, View}; @@ -27,7 +27,7 @@ pub(crate) struct Rig { flight: Option, /// Degrees of bearing per second. pub(crate) orbit: f64, - /// The box fits have framed, which narrows the reach. + /// The box fits have framed, which narrows the zoom and pan limits. pub(crate) bounds: Option, } @@ -40,7 +40,7 @@ struct Flight { } impl Rig { - /// Whether the camera is easing to a rest: a flight or pending motion. An orbit never settles. + /// Whether the camera is easing to rest: a flight or pending motion. An orbit never settles. pub(crate) fn easing(&self) -> bool { self.flight.is_some() || self.owed() } @@ -67,8 +67,8 @@ impl Rig { self.zoom += zoom; } - /// Adds the motion a drag released with velocities `pan` and `turn` coasts: each velocity times - /// the coasting time constant. + /// Adds the coasting motion of a drag released with velocities `pan` and `turn`: each velocity + /// times the coasting time constant. pub(crate) fn fling(&mut self, anchor: [f64; 3], pan: [f64; 2], turn: [f64; 2]) { self.owe(anchor, pan.map(|v| v * COAST), turn.map(|v| v * COAST), 0.0); } @@ -81,8 +81,9 @@ impl Rig { } impl View { - /// Where the camera may go: the space's bounds in the room the camera frames, narrowed by the - /// box fits have framed. Until a frame gives the view its size, only fits narrow it. + /// The camera's zoom and pan limits: the space's limits for the area the camera frames, + /// narrowed by the box fits have framed. Until a frame gives the view its size, only fits set + /// limits. pub(super) fn reach(&self) -> Reach { let area = self.area(); let sized = self.size[0] > 0.0 && self.size[1] > 0.0; @@ -90,9 +91,9 @@ impl View { Reach::new(&self.scene.space, room, self.rig.bounds) } - /// Places the camera within its reach, which may have changed since the last frame; applies the - /// share of pending motion due for the `dt` seconds the clock moved; advances a flight; and - /// orbits. + /// Clamps the camera to its zoom and pan limits, which may have changed since the last frame. + /// Then applies the share of pending motion due for the `dt` seconds the clock moved, advances + /// a flight, and orbits. pub(super) fn advance(&mut self, dt: f64) { self.move_camera(self.camera); if let Some(flight) = &self.rig.flight { @@ -132,16 +133,16 @@ impl View { } } - /// Moves the camera for a gesture, a fit, or an animation, or into a reach that changed, and - /// emits a `Camera` event if it moved. + /// Moves the camera for a gesture, a fit, or an animation, or back within limits that changed, + /// and emits a `Camera` event if it moved. pub(super) fn move_camera(&mut self, camera: Camera) { if self.place_camera(camera) { self.events.tell(Event::Camera(self.camera)); } } - /// Moves the camera to `camera` kept within reach, and dirties the channels that read the - /// camera or the pointer. Returns whether it moved. + /// Moves the camera to `camera`, clamped to its zoom and pan limits, and dirties the channels + /// that read the camera or the pointer. Returns whether it moved. pub(super) fn place_camera(&mut self, camera: Camera) -> bool { let camera = camera.within(&self.reach()); if camera == self.camera { diff --git a/crates/latkit/src/view/sync.rs b/crates/latkit/src/view/sync.rs index d7d44044..60f86e0f 100644 --- a/crates/latkit/src/view/sync.rs +++ b/crates/latkit/src/view/sync.rs @@ -1,21 +1,21 @@ -//! A mark's columns on the GPU. For every column its program reads: a descriptor and a page table -//! of arena offsets, or for a sampled column, a cursor per page of rows at the shown coordinate. +//! A mark's columns on the GPU. Each column the program reads gets a descriptor and a page table of +//! arena offsets. A sampled column instead gets a cursor per page of rows at the shown coordinate. //! //! An unpaged table's rows are paged as its data is, and its columns read the data's pages. A paged -//! table's rows are paged by slot, one page of its order per slot, and its columns read pages -//! packed in that order, cached on the GPU by what they were packed from. A reference column to a -//! paged table holds the referenced rows' places in that table's order. +//! table's rows are paged by slot, one page of its order per slot. Its columns read pages packed in +//! that order, cached on the GPU by what they were packed from. A reference column to a paged table +//! holds the referenced rows' places in that table's order. //! //! A column a line traces holds only what is in view: runs of entries, as raw frames while there -//! are at most two per pixel column, else as buckets of the level of detail that fits. Where the -//! samples lack those, coarser buckets are shown as placeholders, or else the finer ones available. -//! Each level drawn lists its axis pages and, per page of rows, its pages of values. +//! are at most two per pixel column, otherwise as buckets of the level of detail that fits. Where +//! the samples lack those, coarser buckets are shown as placeholders, or else the finer ones +//! available. Each level drawn lists its axis pages and, per page of rows, its pages of values. //! //! Only columns whose inputs changed are rebuilt, once each. Moving the shown coordinate rebuilds //! only sampled columns' cursors, and a paged table rebuilds only where its marks' resident pages //! changed. Rebuilt tables are staged together with those of every other mark of the dataset. The -//! dataset reserves every page they read at once, or none and waits for room. Once every page has -//! arrived, the view swaps them all in, so a data version, and what a camera move draws, appear +//! dataset reserves every page they read all at once, or none and waits for space. Once every page +//! has arrived, the view swaps them all in, so a data version, and what a camera move draws, appear //! complete. Staged tables are rebuilt while they wait only if their inputs change again. A rebuilt //! column that lists pages reports which pages of rows read changed pages, so only those rows //! evaluate again. While a mark eases, its geometry column keeps the table it had when the @@ -45,8 +45,8 @@ const WORDS: usize = wgsl::COLUMN_WORDS as usize; #[derive(Clone, PartialEq, Debug)] enum Stamp { Missing, - /// A field's version, and the id of the order it depends on: a reference's target table's - /// order, or the order paged shapes were made in; else 0. + /// A field's version, and the id of the order it depends on: the order of a reference's target + /// table, or the order paged shapes were made in. Otherwise 0. Field(Version, u64), Sampled(Version, Option, u32), Trace(Version, Plan), @@ -62,8 +62,8 @@ pub(crate) struct Window { pub(crate) steps: u32, } -/// What a traced column draws: runs of entries left to right, each with its first point; the level -/// of detail the window needs; and the total point count. +/// What a traced column draws: runs of entries left to right, each with its first point, the level +/// of detail the window needs, and the total point count. #[derive(Clone, PartialEq, Eq, Debug)] pub(crate) struct Plan { pub(crate) level: u32, @@ -73,8 +73,8 @@ pub(crate) struct Plan { } impl Plan { - /// The entry that point `point` draws: its level, its index, and its side, a bucket's least (0) - /// or greatest (1). `None` for a point that breaks the line. + /// The entry that point `point` draws: its level, its index, and its side, a bucket's minimum + /// (0) or maximum (1). `None` for a point that breaks the line. pub(crate) fn entry(&self, point: u32) -> Option<(u32, u64, usize)> { let i = self.runs.partition_point(|(_, first)| *first <= point).checked_sub(1)?; let (run, first) = &self.runs[i]; @@ -115,7 +115,7 @@ impl Rows<'_> { /// its pinned pages, and `missing` the words still waiting for an offset. struct ColumnTable { stamp: Stamp, - /// How it pages its rows: 0 for the data's pages, else the stamp of its drawing's slots. + /// How it pages its rows: 0 for the data's pages, otherwise the stamp of its drawing's slots. held: u64, descriptor: [u32; WORDS], words: Vec, @@ -219,7 +219,7 @@ impl Staged { } /// Reserves every page the staged tables need, all or none. Returns `false` when the arena has - /// no room for them now. A table that needs a page the arena has since dropped, and has no + /// no space for them now. A table that needs a page the arena has since dropped, and has no /// content to upload for it, is discarded and rebuilt later. pub(crate) fn reserve(&mut self, gpu: &mut Inner, device: &wgpu::Device, queue: &crate::gpu::work::Queue) -> bool { let gone = |table: &ColumnTable| { @@ -301,16 +301,16 @@ pub(crate) struct Tables { } /// What publishing rebuilt tables changed. Bit `i` of `columns` is set for each program column -/// whose table changed, and of `data` for each whose data changed, not only its paging. `blocks` -/// lists the row blocks whose pages changed; `None` when every row changed. +/// whose table changed. Bit `i` of `data` is set for each whose data changed, not only its paging. +/// `blocks` lists the row blocks whose pages changed, or is `None` when every row changed. pub(crate) struct Published { pub(crate) columns: u64, pub(crate) data: u64, pub(crate) blocks: Option>, } -/// A mark's own dataset and table, the tables the view derives beside it, and for a paged table, -/// the shapes made one page of its order at a time, by the field each is written as. +/// A mark's own dataset and table, the tables derived beside it, and, for a paged table, the shapes +/// made one page of its order at a time, keyed by the field each is written as. pub(crate) struct Source<'a> { pub(crate) data: &'a Snapshot, pub(crate) table: &'a Table, @@ -375,8 +375,8 @@ enum Use<'a> { impl Tables { /// Rebuilds the tables whose inputs changed, for `source` paged as `rows`, with `inputs`. - /// Changes are measured against the `staged` tables if any, else the current ones. Each table - /// is built once and requests its pages. `None` when nothing changed. + /// Changes are measured against the `staged` tables if any, otherwise the current ones. Each + /// table is built once and requests its pages. `None` when nothing changed. pub(crate) fn rebuild( &mut self, program: &Program, @@ -394,8 +394,8 @@ impl Tables { Use::Field { column, join } }); let uses: Vec> = fields.chain(joins.iter().map(|from| Use::Join(from))).collect(); - // Rebuild every table when there are no tables of this shape yet, unless a reshape is - // already staged; then compare against it. + // Rebuild every table when there are no tables of this shape yet. If a reshape is already + // staged, compare against it instead. let reshaped = staged.is_some_and(|s| s.reshape && s.tables.len() == uses.len()); let reshape = !reshaped && (!self.synced @@ -490,7 +490,7 @@ impl Tables { published } - /// Drops the frozen geometry table: the mark no longer eases from it. + /// Drops the frozen geometry table, so the mark stops easing from it. pub(crate) fn thaw(&mut self, gpu: &mut Inner) { if let Some(table) = self.frozen.take() { unpin(&table.pins, gpu); @@ -507,7 +507,7 @@ impl Tables { self.generation } - /// The most points the paths of geometry column `column` take; 0 for other columns. + /// The most points the paths of geometry column `column` take. 0 for other columns. pub(crate) fn points(&self, column: u32) -> u32 { self.current.get(column as usize).map_or(0, |t| t.points) } @@ -664,7 +664,7 @@ fn plan(samples: &Samples, window: Window) -> Plan { } /// Where sampled `column` reads `samples` at axis coordinate `at`: the frame shown there, and how -/// far toward the next one it is; 0 for a column that steps. +/// far toward the next one it is (0 for a column that steps). fn shown_frame(samples: &Samples, column: &ColumnUse, at: f64) -> (Option, f32) { let frame = samples.frame_at(at); let t = if column.step { 0.0 } else { frame.map_or(0.0, |f| samples.t(f, at)) }; @@ -919,8 +919,8 @@ impl<'g> Builder<'g> { } /// Appends the page of `packing` in each slot of `drawing`, packed from `source` by `pack` the - /// first time it is needed; `NONE` for a free slot. The order's pages are looked up in one pass - /// over the column, and each page of kept rows separately. + /// first time it is needed, or `NONE` for a free slot. The order's pages are looked up in one + /// pass over the column, and each page of kept rows separately. fn held_pages( &mut self, drawing: &Drawing, @@ -1001,7 +1001,8 @@ impl<'g> Builder<'g> { /// A geometry column's table, in the layout `lib/read.wesl` reads: per page, the block of its /// row table (each row's first point), where its blocks of points are listed, its point count, - /// and its row count; then those lists. A page's points take as many blocks as they need. + /// and its row count, followed by those lists. A page's points take as many blocks as they + /// need. fn path(&mut self, column: &Column) { let pages = column.pages(); self.words.resize(wgsl::PATH_PAGE_WORDS as usize * pages.len(), 0); @@ -1118,8 +1119,8 @@ impl<'g> Builder<'g> { /// - a header: the run count, pages of rows, level count, and total points; /// - per run: its level's index, first entry, entry count, and first point; /// - per level: the level, its axis page count and list offset, and its page lists' start; - /// - per level and page of rows: the count and list offset of its pages of frames or least - /// values, then the same for greatest values; + /// - per level and page of rows: the count and list offset of its pages of frames or minimum + /// values, then the same for maximum values; /// - the lists: axis pages as (offset, first entry), and pages of values as (offset, first /// entry, words per entry). /// @@ -1200,8 +1201,8 @@ impl<'g> Builder<'g> { at } - /// Sets frame `k` (0 the frame shown, 1 the next) of the cursor at `at` to `rows` rows from word - /// `base` of page `key`, built by `content` when the arena lacks it. + /// Sets frame `k` (0 the frame shown, 1 the next) of the cursor at `at` to `rows` rows from + /// word `base` of page `key`, built by `content` when the arena lacks it. fn cursor(&mut self, at: usize, k: usize, key: PageKey, base: u32, rows: u32, content: impl FnOnce() -> Content) { self.place(at + 2 * k, key, base + rows, content); self.words[at + 2 * k + 1] = base; @@ -1225,7 +1226,7 @@ fn kept_packing(packing: &Packing, places: &[u32]) -> Packing { } /// The id of the page of `packing` in slot `s` of `drawing`, packed from `source`: a page of its -/// order, or of its kept rows, each cached until one of its rows is written; `None` if free. +/// order, or of its kept rows, each cached until one of its rows is written. `None` if free. fn slot_page( packed: &mut crate::gpu::packed::Packed, packing: &Packing, diff --git a/crates/latkit/src/view/transition.rs b/crates/latkit/src/view/transition.rs index 9b735a38..2aff47dc 100644 --- a/crates/latkit/src/view/transition.rs +++ b/crates/latkit/src/view/transition.rs @@ -1,4 +1,4 @@ -//! Transitions: a mark whose description, static data, or rows change eases from what it showed. +//! Transitions: a mark whose options, static data, or rows change eases from its previous state. //! //! When a transition starts, each row's shown state is frozen into the mark's frozen words at the //! row's new index: from the same index when no row moved, or gathered by id when rows were added, @@ -38,7 +38,7 @@ pub(crate) struct Transition { /// A transition waiting for the frame that freezes it. pub(crate) struct Begin { - /// Each new row's old row, then each exit's old row, when rows moved; `None` when every row + /// Each new row's old row, then each exit's old row, when rows moved. `None` when every row /// kept its index. sources: Option>, /// Whether no new row matches one shown before, so rows only fade in and out. @@ -150,7 +150,7 @@ fn drawn(holding: &Holding) -> Vec { } /// How rows moved between holdings with different orders: each new held row is traced to its old -/// held row, by index while the table's row order is unchanged, else by id. +/// held row, by index while the table's row order is unchanged, otherwise by id. fn follow(old: &Holding, new: &Holding) -> (Vec, Vec) { let same = old.table.order == new.table.order; let source = |h: u32| match &new.drawing { @@ -215,7 +215,7 @@ pub(crate) fn holds_alike(a: &Holding, b: &Holding) -> bool { } impl Shown { - /// Takes over `before`'s held rows and their words, as a changed description of the same mark. + /// Takes over `before`'s held rows and their words, as a changed version of the same mark. pub(crate) fn inherit(&mut self, before: &mut Shown) { self.words = std::mem::take(&mut before.words); self.before = before.before; @@ -225,7 +225,7 @@ impl Shown { self.place_version = before.place_version + 1; } - /// Begins a transition from what the mark shows, with rows moved as `moved` says, if they + /// Begins a transition from the mark's current state, with rows moved as `moved` says, if they /// moved. Does nothing when the mark showed no rows. pub(crate) fn begin(&mut self, moved: Option<(Vec, Vec)>) { if self.before == 0 { @@ -244,9 +244,9 @@ impl Shown { self.all_dirty(); } - /// Restarts the running transition from what the mark shows, as slots `bound` receive pages of - /// entering rows. Other rows keep easing toward their settled state, which their channels - /// already evaluated, and exits keep fading out after the live rows. + /// Restarts the running transition from the mark's current state, as slots `bound` receive + /// pages of entering rows. Other rows keep easing toward their settled state, which their + /// channels already evaluated, and exits keep fading out after the live rows. pub(crate) fn restart(&mut self, bound: &[u32]) { let mut sources: Vec = (0..self.live()).collect(); for &slot in bound { @@ -300,7 +300,7 @@ impl View { shown.tables.thaw(inner); } } else { - // Rows ease only when the clock moves; their channels evaluated once already. + // Rows ease only when the clock moves. Their channels evaluated once already. shown.easing = self.clock.moved(); } } @@ -378,8 +378,8 @@ impl View { } /// Allocates frozen and settled words for every held row of each mark whose transition starts - /// or runs, and frees them once it ends. A transition without room for them is cancelled, and - /// its rows show their evaluated state at once. + /// or runs, and frees them once it ends. A transition whose words do not fit in the arena is + /// cancelled, and its rows show their evaluated state at once. pub(super) fn fit_easing(&mut self, inner: &mut Inner) { let gpu = self.gpu.clone(); let device = (&gpu.0.device, &gpu.0.queue); diff --git a/crates/latkit/src/view/uniforms.rs b/crates/latkit/src/view/uniforms.rs index 35716135..e6b3f70f 100644 --- a/crates/latkit/src/view/uniforms.rs +++ b/crates/latkit/src/view/uniforms.rs @@ -67,7 +67,7 @@ pub(crate) fn frame( frame.glow = theme.focus.glow; frame.dim = theme.focus.dim; frame.dash = theme.dash * unit; - // Comet spacing and length keep their own unit; a stroke converts them to pixels as it draws. + // Comet spacing and length keep their own unit. A stroke converts them to pixels as it draws. frame.flow_spacing = theme.flow.spacing.value(); frame.flow_length = theme.flow.length.value(); frame.flow_width = theme.flow.width * unit; @@ -77,7 +77,7 @@ pub(crate) fn frame( Length::Px(_) => wgsl::FLOW_PX, }; frame.bow = theme.edge.bow; - // Hover is tracked either way; the controls only decide whether it glows. + // Hover is tracked either way. The controls only decide whether it glows. let glows = scene.controls.hover; frame.grow = if glows { theme.focus.grow } else { 1.0 }; frame.shading = theme.shading; diff --git a/crates/latkit/src/view/variants.rs b/crates/latkit/src/view/variants.rs index 60ba8bcf..1a017f6e 100644 --- a/crates/latkit/src/view/variants.rs +++ b/crates/latkit/src/view/variants.rs @@ -1,21 +1,22 @@ //! The pipelines a scene uses, requested when the scene is shown, in three urgency tiers. //! -//! Draw: for each kind of mark, its draw pipeline and the compute passes it needs (a stroke mark's -//! for the path it follows), and each mark's channels for what they read. Text: labels, glyphs, the -//! distance fields text is set in, and the overlay. Answer: the passes that transitions and comet -//! carry use, the lit rows' draws, and for frames that take input, each kind of mark's probe -//! program, which answers hover, picks, box selection, and row location. +//! Draw: for each kind of mark, its draw pipeline and the compute passes it needs (for a stroke +//! mark, those for the path it follows), and each mark's channel pipeline for what its channels +//! read. Text: labels, glyphs, the distance fields text is set in, and the overlay. Answer: the +//! passes that transitions and comet carry use, the lit rows' draws, and for frames that take +//! input, each kind of mark's probe program, which answers hover, picks, box selection, and row +//! location. //! //! The GPU compiles them through its queue, most urgent first ([`crate::gpu::pipelines`]). The view //! draws once its draw pipelines are ready, adds text once those are, and lights rows, probes, and //! answers queries once the rest are. So no frame waits on a compile, and nothing compiles at first -//! use. Pipelines the scene never uses are never requested. A picture takes no input: it requests +//! use. Pipelines the scene never uses are never requested. A picture takes no input. It requests //! everything it draws with, text, transitions, and lit rows at draw urgency, and draws once all of //! them are ready. //! -//! Each pipeline is named once, here: the passes that serve every mark alike as constants, and a -//! mark's own through [`MarkVariants`]. The view requests and runs them by these same names, so it -//! runs only what it requested. +//! Each pipeline is named once, here: the passes shared by every mark as constants, and each mark's +//! own through [`MarkVariants`]. The view requests and runs them by these same names, so it runs +//! only what it requested. use std::sync::Arc; @@ -85,7 +86,7 @@ impl<'a> MarkVariants<'a> { self.kind.primitive().code() } - /// The shader drawing its rows, or with `lit`, the rows its focus lights; `None` for a face's + /// The shader drawing its rows, or with `lit`, the rows its focus lights. `None` for a face's /// lit rows. pub(super) fn shader(self, lit: bool) -> Option { let (every, focus) = self.kind.shaders(self.custom.is_some()); @@ -102,13 +103,13 @@ impl<'a> MarkVariants<'a> { }) } - /// The pass measuring its rows' extents for a fit; `None` for edges and faces. + /// The pass measuring its rows' extents for a fit. `None` for edges and faces. pub(super) fn bounds(self) -> Option { Some(Variant::compute(Shader::Bounds, self.kind.bounds()?).path(self.path)) } - /// Whether a list may number its instances, for rows whose counts differ: edges between ends, - /// which may pass through bends, follow great circles, or bow into lanes; and every path + /// Whether a list may number its instances, for rows whose counts differ: edges between ends + /// (which may pass through bends, follow great circles, or bow into lanes), and every path /// through geometry, including areas' triangles. fn listed(self) -> bool { self.path == PATH_GEOMETRY || (self.kind == MarkKind::Edges && self.path == PATH_ENDS) @@ -126,8 +127,8 @@ impl<'a> MarkVariants<'a> { Variant::probe(shader, self.primitive()).path(self.path) } - /// The label passes: `claim`, which claims the room its labels would cover, then `texts`, which - /// finds the texts the labels that kept their room need. + /// The label passes: `claim`, which claims the screen cells its labels would cover, then + /// `texts`, which finds the texts needed by the labels that kept their cells. pub(super) fn label_passes(self) -> [Variant; 2] { ["claim", "texts"] .map(|entry| Variant::compute(Shader::Texts, entry).path(self.path).primitive(self.primitive())) @@ -191,7 +192,7 @@ impl View { } else { ([draw, text, changes, lit].concat(), Vec::new(), Vec::new()) }; - // Several marks can need the same pipeline; request each once. + // Several marks can need the same pipeline. Request each once. for asked in [&mut drawing, &mut texts, &mut answering] { let mut seen = rustc_hash::FxHashSet::default(); asked.retain(|need| seen.insert(need.clone())); @@ -206,8 +207,8 @@ impl View { at } - /// Each mark's channel pipeline, built for what its channels read. Before its data is known, - /// they read neither joins, samples, nor the view. + /// Each mark's channel pipeline, built for what its channels read. Before the mark's data is + /// known, it reads no joins, samples, or view state. fn channels(&self) -> Vec { let mut channels: Vec = Vec::new(); for shown in &self.marks { @@ -298,9 +299,9 @@ fn stages(scene: &Scene, format: wgpu::TextureFormat, samples: u32) -> Stages { stages } -/// Adds `mark`'s pipelines to `stages`: its draw and compute passes, for the path a stroke follows -/// or an area's triangle geometry; its lit-row draw, for the rows that hover, selection, and edge -/// endpoints light; its probe; and its labels. +/// Adds `mark`'s pipelines to `stages`: its draw and compute passes (for the path a stroke follows +/// or an area's triangle geometry), its lit-row draw (for the rows that hover, selection, and edge +/// endpoints light), its probe, and its labels. fn mark_stages(mark: &Mark, scene: &Scene, format: wgpu::TextureFormat, samples: u32, stages: &mut Stages) { let variants = MarkVariants::of(mark, &scene.space); stages.draw.extend(variants.draw(false, format, samples)); diff --git a/crates/latkit/src/view/words.rs b/crates/latkit/src/view/words.rs index b569f914..d623ca90 100644 --- a/crates/latkit/src/view/words.rs +++ b/crates/latkit/src/view/words.rs @@ -1,10 +1,10 @@ //! A mark's per-row words, in pages of the arena. For each page of held rows, the mark has one //! arena block per kind of words, sized for the page's rows. A row's state is its kind words, then -//! its extra words. Beside the state are the row's focus word, the normals faces sum at a vertex, -//! and while the mark eases, its frozen state (as shown when the transition began) and its settled -//! state. A page keeps its blocks while the mark holds rows there, so rows keep their words as the -//! camera moves, and holding more rows moves none. Every kind counts toward the GPU budget along -//! with the data. +//! its extra words. Alongside the state are the row's focus word, the normals faces sum at a +//! vertex, and while the mark eases, its frozen state (as shown when the transition began) and its +//! settled state. A page keeps its blocks while the mark holds rows there, so rows keep their words +//! as the camera moves, and adding held rows moves none. Every kind counts toward the GPU budget +//! along with the data. use std::sync::Arc; @@ -77,10 +77,10 @@ impl Words { self.before = None; } - /// Fits `kept`'s pages to hold `rows[p]` rows on page `p`, at `width` words per row; `width` 0 - /// frees them all. A page whose block is too small gets a new block, as does every page when - /// the width changes. Blocks no longer used are freed. Returns what changed, or `None` (with - /// nothing changed) when the arena has no room now. + /// Fits `kept`'s pages to hold `rows[p]` rows on page `p`, at `width` words per row. A `width` + /// of 0 frees them all. A page whose block is too small gets a new block, as does every page + /// when the width changes. Unused blocks are freed. Returns what changed, or `None` (with + /// nothing changed) when the arena has no space now. pub(crate) fn fit( &mut self, kept: Kept, diff --git a/crates/latkit/src/web.rs b/crates/latkit/src/web.rs index d5932aff..27ba61fc 100644 --- a/crates/latkit/src/web.rs +++ b/crates/latkit/src/web.rs @@ -879,7 +879,7 @@ impl Views for Lending<'_> { } } -/// Takes `shot`'s picture of `views`, one per panel, once they are complete as its +/// Takes `shot`'s picture of `views`, one per panel, when they are complete, as its /// [`Capture`](crate::Capture) says. Canvases showing them keep their last frames meanwhile. pub async fn image(shot: &Shot, views: &[PageView]) -> Result { shot.image_of(&mut Lending::of(views)?).await diff --git a/crates/latkit/src/wire.rs b/crates/latkit/src/wire.rs index c21df0d0..666d6117 100644 --- a/crates/latkit/src/wire.rs +++ b/crates/latkit/src/wire.rs @@ -1,5 +1,5 @@ //! Little-endian bytes: the encoding of latkit's byte format, of the jobs it hands its host, and of -//! the binary formats it reads. [`Out`] writes values; [`In`] reads them back, returning `None` +//! the binary formats it reads. [`Out`] writes values. [`In`] reads them back, returning `None` //! past the end. #[cfg(target_endian = "big")] @@ -61,7 +61,7 @@ macro_rules! codes { } } - /// The value written as `code`; `None` for a code no value has. + /// The value written as `code`. `None` for a code no value has. pub(crate) fn from_code(code: u32) -> Option { match code { $($code => Some(Self::$variant),)+ @@ -125,7 +125,7 @@ impl<'a> In<'a> { self.take().map(f64::from_le_bytes) } - /// A text written by [`Out::str`]; `None` unless it is UTF-8. + /// A text written by [`Out::str`]. `None` unless it is UTF-8. pub(crate) fn str(&mut self) -> Option<&'a str> { let n = self.u32()? as usize; std::str::from_utf8(self.bytes(n)?).ok() diff --git a/crates/latkit/tests/areas.rs b/crates/latkit/tests/areas.rs index 42ca4b92..6e6c179c 100644 --- a/crates/latkit/tests/areas.rs +++ b/crates/latkit/tests/areas.rs @@ -85,7 +85,7 @@ fn areas_triangulated_in_a_job_ease_in_with_each_row_s_color() { let mut view = view(&gpu, Scene::new(plane()).draw(Areas::of("map", "Zone").color(color)), &data); let middle = f64::from(side) / 2.0; view.set_camera(Camera::Map { center: [middle, middle, 0.0], zoom: 0.75, pitch: 0.0, bearing: 0.0 }); - // The triangles landing start a transition; once it settles, each row shows its own color. + // The triangles landing start a transition. Once it settles, each row shows its own color. let pixels = frame(&gpu, &mut view); let cell = |x: f64, y: f64| { let [px, py] = view.screen([x + 0.5, y + 0.5, 0.0]).expect("in view"); diff --git a/crates/latkit/tests/gestures.rs b/crates/latkit/tests/gestures.rs index d88fac63..434dbb88 100644 --- a/crates/latkit/tests/gestures.rs +++ b/crates/latkit/tests/gestures.rs @@ -488,15 +488,15 @@ fn reduced_motion_applies_every_move_at_once() { fn a_flat_map_keeps_the_camera_over_its_one_world() { let Some(gpu) = gpu() else { return }; let mut view = View::new(&gpu, Scene::new(latkit::Earth::default()).theme(plain_theme())).expect("valid scene"); - // Set before the first frame gives the view its size, the camera moves within reach on that - // frame, which reports it. At zoom 4 the world is 4096 pixels across, and the center keeps half - // the view's width from the antimeridian. + // Set before the first frame gives the view its size, the camera moves within its limits on + // that frame, which reports it. At zoom 4 the world is 4096 pixels across, and the center keeps + // half the view's width from the antimeridian. view.set_camera(Camera::Map { center: [180.0, 0.0, 0.0], zoom: 4.0, pitch: 0.0, bearing: 0.0 }); frame(&gpu, &mut view); let Camera::Map { center, .. } = *view.camera() else { panic!("a map camera") }; assert!((center[0] - 180.0 * (1.0 - f64::from(SIZE) / 4096.0)).abs() < 1e-9, "{center:?}"); assert!(view.events().any(|e| matches!(e, Event::Camera(_))), "the frame reports the move"); - // Once the view has its size, a camera set past the reach is kept within it at once: zoomed + // Once the view has its size, a camera set past its limits is kept within them at once. Zoomed // all the way out, the whole world just fits the view, centered. view.set_camera(Camera::Map { center: [90.0, 60.0, 0.0], zoom: -3.0, pitch: 0.0, bearing: 0.0 }); let Camera::Map { center, zoom, .. } = *view.camera() else { panic!("a map camera") }; diff --git a/crates/latkit/tests/labels.rs b/crates/latkit/tests/labels.rs index 841f0ece..7f770d1c 100644 --- a/crates/latkit/tests/labels.rs +++ b/crates/latkit/tests/labels.rs @@ -1,9 +1,8 @@ //! Labels show beside their rows once the host rasterizes their text, or inside a block while they -//! fit. Where labels would overlap, the label of the row drawn on top shows, an empty text gives no -//! label, and only labels that -//! show near the view have their text rasterized, so a dense view settles. A conditional text labels -//! only the rows it names, and a label keeps showing while its mark gains texts. A diagram's labels -//! scale with the sheet and fade out once too small to read. +//! fit. Where labels would overlap, the label of the row drawn on top shows. An empty text gives no +//! label. Only labels that show near the view have their text rasterized, so a dense view settles. +//! A conditional text labels only the rows it names, and a label keeps showing while its mark gains +//! texts. A diagram's labels scale with the sheet and fade out once too small to read. #![cfg(feature = "gpu")] diff --git a/crates/latkit/tests/picks.rs b/crates/latkit/tests/picks.rs index aa8cb428..f94b1bb6 100644 --- a/crates/latkit/tests/picks.rs +++ b/crates/latkit/tests/picks.rs @@ -1,5 +1,5 @@ -//! Queries answered from what the frame drew: every row within reach of a point, nearest first, the -//! rows in a Shift-drag box, a row's screen position, and the sample a traced line passes through. +//! Queries answered from what the frame drew: every row near a point, nearest first, the rows in a +//! Shift-drag box, a row's screen position, and the sample a traced line passes through. #![cfg(feature = "gpu")] @@ -141,8 +141,8 @@ fn every_hit_of_geodesic_edges_lies_in_reach() { let hits = answer(&gpu, &mut view, pending); assert!(!hits.is_empty(), "the middle of the lattice is in reach"); for hit in &hits { - // A vertex is in reach at its outline, but its point is its center, a marker radius - // further. + // A vertex is within the radius at its outline, but its point is its center, a marker + // radius further. let reach = (hit.point[0] - c).hypot(hit.point[1] - c); let what = format!("{} {} at {:?}, {reach} away", hit.row.table, hit.row.id, hit.point); assert!(hit.distance <= radius && reach <= radius + 8.0, "{what}"); diff --git a/crates/latkit/tests/scale.rs b/crates/latkit/tests/scale.rs index 79cc1c2d..1f5e64b4 100644 --- a/crates/latkit/tests/scale.rs +++ b/crates/latkit/tests/scale.rs @@ -1,6 +1,6 @@ -//! Scale: a view's drawing always fits its budget. Paged tables draw the tiles in view, traces too -//! large thin in time, higher-priority rows draw on top, a path longer than an arena chunk still -//! draws whole, and a view sleeps while it waits. +//! Scale: a view's drawing always fits its budget. Paged tables draw the tiles in view, traces that +//! are too large thin in time, and higher-priority rows draw on top. A path longer than an arena +//! chunk still draws in full, and a view sleeps while it waits. #![cfg(feature = "gpu")] @@ -577,7 +577,7 @@ fn a_change_to_one_row_of_a_paged_table_updates_only_its_page() { let work = gpu.work() - before; let center = SIZE / 2; assert!(close(at(&pixels, center, center), [255, 255, 255, 255]), "{:?}", at(&pixels, center, center)); - // A page is 4,096 rows; the field takes 8 bytes a row. + // A page is 4,096 rows, and the field takes 8 bytes a row. assert!(work.evaluated <= 4096, "only one page evaluates: {}", work.evaluated); assert!(work.uploaded <= 8 * 4096, "only one page of the field uploads: {} bytes", work.uploaded); } diff --git a/docs/cli.md b/docs/cli.md index 66d3dcd8..65562921 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -2,7 +2,7 @@ The `latkit` executable draws [scenes](guide.md#scenes) of datasets to images, image sequences, raw frames, and video on the machine's GPU, with the same renderer as the browser. Its inputs are -latkit's JSON, so any language can write them: a dataset is a [JSON document](guide.md#json), +latkit's JSON, so any language can write them. A dataset can be a [JSON document](guide.md#json), latkit's [bytes](guide.md#bytes), or GeoJSON named with `NAME=`. The [command reference](reference/cli.md) documents every command and option. @@ -22,11 +22,11 @@ latkit render plot.json --tiles grid=history/frames.json -o year.png latkit schema scene > scene.schema.json ``` -At most one input reads stdin (`-`). +At most one input can read stdin (`-`). ## Frames -`--frames` is a JSON Lines file with one line per output frame; line `n` shows at `n / --fps` +`--frames` is a JSON Lines file with one line per output frame. Line `n` shows at `n / --fps` seconds. Every key is optional: ```json @@ -59,9 +59,10 @@ seconds. Every key is optional: | `insets` | CSS pixels each view keeps clear | | `orbit` | degrees per second a map or plane turns | -`camera`, `fly`, and `fit` take one value for every view, or a list with one per view (`null` -leaves that view unchanged). The CLI reads and loads up to two frames ahead of the one drawing. -Changes ease over each scene's `ease`; set `"ease": 0` to show each frame's data without easing. +`camera`, `fly`, and `fit` take one value for every view, or a list with one value per view (`null` +leaves that view unchanged). The CLI reads and loads up to two frames ahead of the one it is +drawing. Changes ease over each scene's `ease`. Set `"ease": 0` to show each frame's data without +easing. ## Outputs @@ -73,17 +74,17 @@ Changes ease over each scene's `ease`; set `"ease": 0` to show each frame's data | `.y4m` | YUV4MPEG2, 4:2:0 BT.709, for any encoder (`--frames`) | | `.mp4` | H.264 in fragmented MP4 from the platform's encoder (`--frames`) | -A still waits until every view is complete and motion has settled; `--elapsed` sets the time its -animated effects show. A video frame where nothing moved repeats the frame before. An output file is -replaced only once it is completely written. MP4 needs Windows (Media Foundation); elsewhere, pipe -Y4M to an encoder. `--deadline` limits how long each picture or video frame may take (30 seconds by -default). +A still picture waits until every view is complete and motion has settled. `--elapsed` sets the +time its animated effects show. A video frame where nothing moved repeats the frame before. An +output file is replaced only once the new file is completely written. MP4 needs Windows (Media +Foundation). On other systems, pipe Y4M to an encoder. `--deadline` limits how long each picture or +video frame may take, and defaults to 30 seconds. ## Diagnostics -Exit codes: 0 success, 1 failure, 2 usage error. Errors print as `latkit: SOURCE: message`, where -`SOURCE` is the input file at fault; for invalid input, the message starts with the path to the -invalid part, such as `frames[3].patch.grid[0].Bus.x`. An output that cannot be written is named in -the message. `--diagnostics json` writes one JSON object per line to stderr, `{ type, detail }`: -`progress` after each frame, `stats` when done, and `error` with its `code`, `message`, `path`, and -`source`. +Exit codes are 0 for success, 1 for failure, and 2 for a usage error. Errors print as +`latkit: SOURCE: message`, where `SOURCE` is the input file at fault. For invalid input, the +message starts with the path to the invalid part, such as `frames[3].patch.grid[0].Bus.x`. When an +output cannot be written, the message names it. `--diagnostics json` writes one JSON object per +line to stderr, `{ type, detail }`. The `type` is `progress` after each frame, `stats` when done, +or `error` with its `code`, `message`, `path`, and `source`. diff --git a/docs/examples/bloom.md b/docs/examples/bloom.md index 3d0ad600..9c61122c 100644 --- a/docs/examples/bloom.md +++ b/docs/examples/bloom.md @@ -8,7 +8,7 @@ Thirty-two thousand seeds placed a golden angle apart, sized and colored by a spiral wave through a custom colormap. The `latkit` command line draws this picture from the module below, which builds its datasets -with `Data` and returns its scene. `node docs/gallery/render.ts bloom` draws it again. +with `Data` and returns its scene. Run `node docs/gallery/render.ts bloom` to draw it again. ```{literalinclude} ../gallery/bloom.ts :language: typescript diff --git a/docs/examples/clifford.md b/docs/examples/clifford.md index 64b6927d..709a2309 100644 --- a/docs/examples/clifford.md +++ b/docs/examples/clifford.md @@ -1,14 +1,14 @@ # Clifford ```{image} ../images/clifford.png -:alt: A Clifford strange attractor of 260,000 translucent points, colored by the way each step turns +:alt: A Clifford strange attractor of 260,000 translucent points, colored by the direction of each step :class: picture ``` A strange attractor of 260,000 translucent points, colored by the direction of each step through a cyclic colormap, so both density and flow show. The `latkit` command line draws this picture from the module below, which builds its datasets -with `Data` and returns its scene. `node docs/gallery/render.ts clifford` draws it again. +with `Data` and returns its scene. Run `node docs/gallery/render.ts clifford` to draw it again. ```{literalinclude} ../gallery/clifford.ts :language: typescript diff --git a/docs/examples/currents.md b/docs/examples/currents.md index dccb0b9a..6917fc9c 100644 --- a/docs/examples/currents.md +++ b/docs/examples/currents.md @@ -8,7 +8,7 @@ Streamlines traced through a field of seven vortices, as on a wind map. Each streamline is a path geometry, colored by its speed. The `latkit` command line draws this picture from the module below, which builds its datasets -with `Data` and returns its scene. `node docs/gallery/render.ts currents` draws it again. +with `Data` and returns its scene. Run `node docs/gallery/render.ts currents` to draw it again. ```{literalinclude} ../gallery/currents.ts :language: typescript diff --git a/docs/examples/diagram.md b/docs/examples/diagram.md index ea922a97..81bfdf34 100644 --- a/docs/examples/diagram.md +++ b/docs/examples/diagram.md @@ -1,14 +1,14 @@ # Diagram ```{image} ../images/diagram.png -:alt: A microgrid controller as blocks in nested groups, wired port to port with routed wires and a feedback loop +:alt: A microgrid controller as blocks in nested groups, with wires routed port to port and a feedback loop :class: picture ``` -Blocks in nested groups, placed in layers by latkit's layered layout, with wires routed at right angles between their ports, a feedback loop, and a status ring. +Blocks in nested groups, placed by latkit's layered layout. Wires run at right angles between their ports, with a feedback loop and a status ring. The `latkit` command line draws this picture from the module below, which builds its datasets -with `Data` and returns its scene. `node docs/gallery/render.ts diagram` draws it again. +with `Data` and returns its scene. Run `node docs/gallery/render.ts diagram` to draw it again. ```{literalinclude} ../gallery/diagram.ts :language: typescript diff --git a/docs/examples/frequency.md b/docs/examples/frequency.md index 15a91bf0..c0b0b6cf 100644 --- a/docs/examples/frequency.md +++ b/docs/examples/frequency.md @@ -1,14 +1,14 @@ # ACTIVSg10k ```{image} ../images/frequency.png -:alt: The speed of 1,456 machines over ten seconds after a fault, near ones bright and swinging hardest, far ones dark +:alt: The speed of 1,456 machines over ten seconds after a fault. Machines near the fault are bright and swing hardest, and far ones are dark :class: picture ``` A simulated fault on the ACTIVSg10k case: every machine's speed over ten seconds, traced directly from the recording's sampled field and colored by distance from the fault. The `latkit` command line draws this picture from the module below, which builds its datasets -with `Data` and returns its scene. `node docs/gallery/render.ts frequency` draws it again. +with `Data` and returns its scene. Run `node docs/gallery/render.ts frequency` to draw it again. ```{literalinclude} ../gallery/frequency.ts :language: typescript diff --git a/docs/examples/globe.md b/docs/examples/globe.md index d346cc42..cef2e776 100644 --- a/docs/examples/globe.md +++ b/docs/examples/globe.md @@ -1,14 +1,14 @@ # Globe ```{image} ../images/globe.png -:alt: Routes between forty of the world's great cities, along great circles on a globe, comets flowing on them +:alt: Great-circle routes between forty of the world's great cities on a globe, with comets flowing along them :class: picture ``` -Routes between forty cities along great circles, with comets flowing along them, over coastlines and borders read from GeoJSON, on a globe with an atmosphere. +Great-circle routes between forty cities, with comets flowing along them. Coastlines and borders come from GeoJSON, and the globe has an atmosphere. The `latkit` command line draws this picture from the module below, which builds its datasets -with `Data` and returns its scene. `node docs/gallery/render.ts globe` draws it again. +with `Data` and returns its scene. Run `node docs/gallery/render.ts globe` to draw it again. ```{literalinclude} ../gallery/globe.ts :language: typescript diff --git a/docs/examples/grid.md b/docs/examples/grid.md index ce7be513..413419d7 100644 --- a/docs/examples/grid.md +++ b/docs/examples/grid.md @@ -5,10 +5,10 @@ :class: picture ``` -The example suite's ACTIVSg70k case, with 70,000 buses and 88,207 branches, over the US states on a Mercator map tilted toward the Great Lakes. Each line's color and width follow its voltage level, and `priority` draws the high-voltage backbone on top. +The ACTIVSg70k case from the example suite: 70,000 buses and 88,207 branches over the US states, on a Mercator map tilted toward the Great Lakes. Each line's color and width follow its voltage level, and `priority` draws the high-voltage backbone on top. The `latkit` command line draws this picture from the module below, which builds its datasets -with `Data` and returns its scene. `node docs/gallery/render.ts grid` draws it again. +with `Data` and returns its scene. Run `node docs/gallery/render.ts grid` to draw it again. ```{literalinclude} ../gallery/grid.ts :language: typescript diff --git a/docs/examples/helios.md b/docs/examples/helios.md index e12a4e31..06702d21 100644 --- a/docs/examples/helios.md +++ b/docs/examples/helios.md @@ -8,7 +8,7 @@ A network in five rings, with vertices colored through a colormap, labeled hubs, and comets flowing along every branch. The `latkit` command line draws this picture from the module below, which builds its datasets -with `Data` and returns its scene. `node docs/gallery/render.ts helios` draws it again. +with `Data` and returns its scene. Run `node docs/gallery/render.ts helios` to draw it again. ```{literalinclude} ../gallery/helios.ts :language: typescript diff --git a/docs/examples/index.md b/docs/examples/index.md index 5e14bc1c..b406155f 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -1,22 +1,20 @@ # Examples -The `latkit` command line draws every picture here from the module on its page, which builds its -datasets with `Data` and returns its scene. The +The `latkit` command line draws every picture here. Each page shows the module that builds the +picture's datasets with `Data` and returns its scene. The [example suite](https://github.com/lukelowry/latkit/tree/main/npm/examples) shows the same features live in the browser. :::{container} cards -[![The ACTIVSg70k grid on the globe as one 765 kV line opens: buses raised by their frequency, branches that pick up its power glow amber, those it leaves glow cyan](../images/opening.png) Line opening](opening.md) - [![Seventy thousand buses of a synthetic grid on the United States, its lines colored and widened by voltage](../images/grid.png) ACTIVSg70k](grid.md) [![Two thousand buses of a synthetic Texas grid: generators as amber discs sized by output, comets along its highest-voltage lines](../images/texas.png) ACTIVSg2000](texas.md) -[![The speed of 1,456 machines over ten seconds after a fault, near ones bright and swinging hardest, far ones dark](../images/frequency.png) ACTIVSg10k](frequency.md) +[![The speed of 1,456 machines over ten seconds after a fault. Machines near the fault are bright and swing hardest, and far ones are dark](../images/frequency.png) ACTIVSg10k](frequency.md) -[![Routes between forty of the world's great cities, along great circles on a globe, comets flowing on them](../images/globe.png) Globe](globe.md) +[![Great-circle routes between forty of the world's great cities on a globe, with comets flowing along them](../images/globe.png) Globe](globe.md) -[![A microgrid controller as blocks in nested groups, wired port to port with routed wires and a feedback loop](../images/diagram.png) Diagram](diagram.md) +[![A microgrid controller as blocks in nested groups, with wires routed port to port and a feedback loop](../images/diagram.png) Diagram](diagram.md) [![A synthetic network of 480 buses in five concentric rings, comets flowing along its 896 branches](../images/helios.png) Helios](helios.md) @@ -24,7 +22,7 @@ live in the browser. [![Ridged mountains of 112,000 lit triangles, from deep teal valleys to pale summits, seen from above at an angle](../images/terrain.png) Terrain](terrain.md) -[![A Clifford strange attractor of 260,000 translucent points, colored by the way each step turns](../images/clifford.png) Clifford](clifford.md) +[![A Clifford strange attractor of 260,000 translucent points, colored by the direction of each step](../images/clifford.png) Clifford](clifford.md) [![Thirty-two thousand seeds a golden angle apart, in arms of sapphire, rose, and amber](../images/bloom.png) Golden bloom](bloom.md) @@ -36,7 +34,6 @@ live in the browser. ```{toctree} :hidden: -opening grid texas frequency diff --git a/docs/examples/opening.md b/docs/examples/opening.md deleted file mode 100644 index cb2cfefc..00000000 --- a/docs/examples/opening.md +++ /dev/null @@ -1,32 +0,0 @@ -# Line opening - -```{image} ../images/opening.png -:alt: The ACTIVSg70k grid on the globe as one 765 kV line opens: buses raised by their frequency, branches that pick up its power glow amber, those it leaves glow cyan -:class: picture banner -``` - -The ACTIVSg70k case on the globe as its Little Rock to Newark 765 kV line, in red, opens. A DC -power flow of all 70,000 buses computes the bus angles before the opening and how far each shifts -after it. Each of the 88,207 branches is colored by the flow it picks up (amber) or sheds (cyan). -Changes under 10 MW stay grey, and branches below 230 kV with such small changes form a dim web -beneath the backbone. Each bus's height is its frequency 2.5 s after the opening, recorded from a -simulation of the same event, and poles drop to the ground from a sample of buses at 345 kV and up. - -The `latkit` command line draws this picture from the module below, which builds its datasets -with `Data` and returns its scene. `node docs/gallery/render.ts opening` draws it again. - -```{literalinclude} ../gallery/opening.ts -:language: typescript -``` - -The power flow solves the grid's susceptance matrix with conjugate gradients in a few seconds: - -```{literalinclude} ../gallery/flow.ts -:language: typescript -``` - -The recording is too large for the repository, so `record.ts` saves only the instant drawn: - -```{literalinclude} ../gallery/record.ts -:language: typescript -``` diff --git a/docs/examples/terrain.md b/docs/examples/terrain.md index 702fee11..12e56380 100644 --- a/docs/examples/terrain.md +++ b/docs/examples/terrain.md @@ -8,7 +8,7 @@ Ridged mountains in three dimensions, as faces over a grid of vertices. Each face blends its corners' colors and is lit by normals the GPU computes. The `latkit` command line draws this picture from the module below, which builds its datasets -with `Data` and returns its scene. `node docs/gallery/render.ts terrain` draws it again. +with `Data` and returns its scene. Run `node docs/gallery/render.ts terrain` to draw it again. ```{literalinclude} ../gallery/terrain.ts :language: typescript diff --git a/docs/examples/texas.md b/docs/examples/texas.md index b8b16399..8195f785 100644 --- a/docs/examples/texas.md +++ b/docs/examples/texas.md @@ -8,7 +8,7 @@ A synthetic Texas grid. Generators are sized by the square root of their output, and the ten largest are labeled. Comets flow along the lines of 345 kV and up, spaced in map units so they keep their place as the view zooms. The `latkit` command line draws this picture from the module below, which builds its datasets -with `Data` and returns its scene. `node docs/gallery/render.ts texas` draws it again. +with `Data` and returns its scene. Run `node docs/gallery/render.ts texas` to draw it again. ```{literalinclude} ../gallery/texas.ts :language: typescript diff --git a/docs/examples/tides.md b/docs/examples/tides.md index 454c5ed1..003844cc 100644 --- a/docs/examples/tides.md +++ b/docs/examples/tides.md @@ -5,10 +5,10 @@ :class: picture ``` -Closed contours swelling outward, each rippled by harmonics whose phases shift with its level, colored from the center out. +Closed contours swelling outward, colored from the center out. Harmonics ripple each contour, and their phases shift from level to level. The `latkit` command line draws this picture from the module below, which builds its datasets -with `Data` and returns its scene. `node docs/gallery/render.ts tides` draws it again. +with `Data` and returns its scene. Run `node docs/gallery/render.ts tides` to draw it again. ```{literalinclude} ../gallery/tides.ts :language: typescript diff --git a/docs/examples/torus.md b/docs/examples/torus.md index 82dc6c01..b3a4524b 100644 --- a/docs/examples/torus.md +++ b/docs/examples/torus.md @@ -8,7 +8,7 @@ Sixty-four filaments around a (2, 3) torus knot, drawn as edges between points with elevation and seen from above at an angle. The `latkit` command line draws this picture from the module below, which builds its datasets -with `Data` and returns its scene. `node docs/gallery/render.ts torus` draws it again. +with `Data` and returns its scene. Run `node docs/gallery/render.ts torus` to draw it again. ```{literalinclude} ../gallery/torus.ts :language: typescript diff --git a/docs/gallery/clifford.ts b/docs/gallery/clifford.ts index ebea4203..eace9aeb 100644 --- a/docs/gallery/clifford.ts +++ b/docs/gallery/clifford.ts @@ -4,7 +4,7 @@ import { framed } from './look.ts'; import type { Picture } from './render.ts'; export default function (): Picture { - // Just under 2^18 = 262,144 rows, past which a view pages a table instead of drawing it whole. + // Just under 2^18 = 262,144 rows. Past that, a view pages a table rather than drawing all of it. const n = 260_000; const [a, b, c, d] = [-1.4, 1.6, 1.0, 0.7]; const x = new Float32Array(n); diff --git a/docs/gallery/render.ts b/docs/gallery/render.ts index aa1d2173..44e0d8c3 100644 --- a/docs/gallery/render.ts +++ b/docs/gallery/render.ts @@ -1,9 +1,8 @@ /** * Draws the documentation's gallery with latkit. Each piece's default export builds its datasets - * with `Data` and returns its picture, or a promise of it: a scene, and how to look at it. The - * `latkit` CLI draws each piece to - * `docs/images/NAME.png`, from the datasets the scene's tables belong to, with no editing of the - * picture afterwards. + * with `Data` and returns its picture, or a promise of it: a scene and how to look at it. The + * `latkit` CLI draws each piece to `docs/images/NAME.png` from the datasets the scene uses. The + * pictures are not edited afterwards. * * cargo build --locked -p latkit-cli --profile cli * cargo xtask wasm # builds the `latkit` package the pieces import @@ -64,7 +63,7 @@ async function render(name: string): Promise { }; const picture = await draw(); const file = (suffix: string) => path.join(work, `${name}.${suffix}`); - // The datasets of the tables the scene names, collected as a view collects them. + // The datasets the scene's tables belong to, collected the way a view collects them. const datasets = new Set(); const scene = JSON.stringify( picture.scene, diff --git a/docs/guide.md b/docs/guide.md index 5e17ab98..9a7b7040 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -5,8 +5,8 @@ pictures and video. The [API reference](reference/api.md) documents every method ## Data -A dataset is a name and a set of tables. A table has rows, each with an id, and fields, each with -one value per row. +A dataset is a name and a set of tables. A table has rows and fields. Each row has an id, and each +field has one value per row. ### Write a dataset @@ -32,19 +32,19 @@ const grid = new Data('grid') ); ``` -- **Ids** are nonnegative safe integers or strings, or `{ count: n }` for rows `0` to `n - 1`. -- **Values** are one column per field: typed arrays, or arrays of numbers, booleans, strings, or +- Ids are nonnegative safe integers or strings, or `{ count: n }` for rows `0` to `n - 1`. +- Values are one column per field: typed arrays, or arrays of numbers, booleans, strings, or geometries, with `null` for missing. A `Uint8Array` holds booleans. -- **Geometries** are `[x, y]` points, paths of points, parts (a polygon's rings), or polygons. -- **References** name rows of another table by id: `reference(table, ids)`. +- Geometries are `[x, y]` points, paths of points, parts (a polygon's rings), or polygons. +- References name rows of another table by id: `reference(table, ids)`. ### Change it -`set` writes rows, adding new ids; `replace` replaces a whole table; `remove` removes rows. Each -change applies at once, whole or not at all, and every view drawing the dataset shows it on the next -frame. A [`Patch`](reference/api.md#patch) holds several changes that apply together, all or none: -an undo step, or one message from a server. `snapshot()` keeps the current version in O(1) by -sharing pages, so an undo history costs only the pages that changed: +`set` writes rows and adds any new ids. `replace` replaces a whole table, and `remove` removes rows. +Each change applies immediately, all or nothing, and every view of the dataset shows it on the next +frame. A [`Patch`](reference/api.md#patch) holds several changes that apply together, all or +nothing, such as an undo step or one message from a server. `snapshot()` saves the current version +in O(1). Snapshots share unchanged pages, so an undo history costs only the pages that changed: ```ts const before = grid.snapshot(); @@ -55,10 +55,10 @@ grid.apply(new Patch().set('Bus', [101], { name: ['Northgate'] }).remove('Branch ### Record over time -A field becomes sampled with its first frame. A view shows the axis coordinate `view.at`: marks -interpolate each sampled field between the frames on either side of it, or show the last frame at -or before it when read with `field(name).step()`. Sampled values are stored as `f32` for drawing; -axis coordinates stay `f64`. +A field becomes sampled when you append its first frame. A view shows the axis coordinate +`view.at`. Marks interpolate each sampled field between the frames on either side of `view.at`. A +field read with `field(name).step()` shows the last frame at or before it instead. Sampled values +are stored as `f32` for drawing, and axis coordinates as `f64`. ```ts grid.axis('time', 's'); @@ -69,13 +69,13 @@ grid.forget(1); // a rolling window view.at = 1.2; ``` -Each sampled field keeps levels of detail as it records: level `k` holds each row's minimum and -maximum over buckets of `8^k` frames. A line tracing a field on a plot reads the level that gives -each pixel column one or two buckets, so a long history draws as fast as a short one. +Each sampled field keeps levels of detail as it records. Level `k` holds each row's minimum and +maximum over buckets of `8^k` frames. A line that traces a field on a plot reads the level with one +or two buckets per pixel column, so a long history draws as fast as a short one. ### Fetch long histories in spans -Write a history too large to load at once as time tiles; a view then fetches only what it draws: +Write a history too large to load at once as time tiles. A view then fetches only what it draws: ```sh latkit tile history.latk -o history/ @@ -85,21 +85,21 @@ latkit tile history.latk -o history/ view.sources = [new Tiles('grid', 'history/frames.json')]; // in place of the dataset named grid ``` -A line tracing a field fetches the spans its plot window shows, at the detail it draws; other marks -fetch the frames around `view.at`. While a span loads, the view draws the coarser span above it. In -Rust, `Snapshot::to_tiles` writes the same files. +A line that traces a field fetches the spans in its plot window, at the detail it draws. Other +marks fetch the frames around `view.at`. While a span loads, the view draws the coarser span that +covers it. In Rust, `Snapshot::to_tiles` writes the same files. ### Stream -`patch.toBytes()` encodes a patch as one frame, and frames written back to back form a stream that -a server in any language can write ([format](#bytes)). `Patch.stream` yields each patch as its last -byte arrives: +`patch.toBytes()` encodes a patch as one frame. Frames written back to back form a stream, which a +server in any language can write ([format](#bytes)). `Patch.stream` yields each patch as soon as +its last byte arrives: ```ts for await (const patch of Patch.stream((await fetch('/feed')).body!)) grid.apply(patch); ``` -`new Patch('grid')` names its dataset; applying it to another dataset fails. +`new Patch('grid')` names its dataset, and applying it to another dataset fails. ### Read files @@ -128,14 +128,14 @@ const hot = grid.query({ // { table, ids, fields: { name: string[], loading: Float64Array }, total } ``` -[`Dataset`](reference/api.md#dataset) lists the reads, `describe()` and `envelope()` among them. A -`Snapshot` reads its own version the same way. +[`Dataset`](reference/api.md#dataset) lists every read, including `describe()` and `envelope()`. A +`Snapshot` has the same reads, for its own version. ## Scenes A scene is plain data: a `space`, a list of `marks` drawn in order, and optional `layout`, `theme`, -`controls`, `ease` (the seconds a change eases over), `title`, and `subtitle`. Setting -`view.scene = next` replaces the scene, and each row eases from where it was drawn. +`controls`, `ease` (how many seconds a change eases over), `title`, and `subtitle`. Set +`view.scene = next` to replace the scene. Each row eases from where it was drawn. ```ts const scene: Scene = { @@ -167,15 +167,15 @@ The [scene types](reference/types.md#scene) document every option and its defaul | `'plot'` | the axis coordinate across, values up | | `'diagram'` | a flat sheet, y down, sizes in position units; ports, groups, layouts | -Written as an object, a space takes options: `{ earth: { projection: 'globe' } }`, +Write a space as an object to give it options: `{ earth: { projection: 'globe' } }`, `{ plane: { grid: { lines: { pitch: 10 } } } }`, or a plot's `{ plot: { x, y } }` axes. ### Marks -A mark's kind holds the table it draws: `{ vertices: grid.table('Bus') }`. A view draws the -datasets its marks' tables belong to. `JSON.stringify` writes a table as `[data, table]`, the form -the command line reads, which names one of a view's `sources`. The pointer hovers and clicks -vertices, edges, faces, and a plot's traces; set `pick: true` to make areas and lines respond too. +A mark's kind key holds the table it draws: `{ vertices: grid.table('Bus') }`. A view loads every +dataset its marks draw from. `JSON.stringify` writes a table as `[data, table]`, the form the +command line reads, where `data` names one of the view's `sources`. The pointer hovers and clicks +vertices, edges, faces, and a plot's traces. Set `pick: true` to make areas and lines respond too. | Mark | Draws each row as | | ---------- | ------------------------------------------------------------------------ | @@ -185,22 +185,22 @@ vertices, edges, faces, and a plot's traces; set `pick: true` to make areas and | `areas` | the area a polygon field covers | | `faces` | a triangle between three vertices | -`flow` animates comets along a stroke. In a diagram, vertices are blocks with `ports`, a vertex -mark with `contains` draws groups around the rows that reference them, and an edges mark of a table -without `ends` draws nets joining the ports that reference it. +`flow` animates comets along a stroke. In a diagram, vertices are blocks with `ports`. A vertices +mark with `contains` draws each row as a group around the rows that reference it. An edges mark +without `ends` draws each row as a net that joins the ports that reference it. -`label` shows a text field beside each row, inside a block, or along a stroke, wherever the row -shows and the text fits. Labels never overlap: where they would, the label of the row drawn on top -shows, so `priority` decides which rows are named first, and a dense view labels as many as fit. A -row whose text is missing has no label, so a `when` choosing between the field and `null` labels -only some rows, as above. +`label` shows a text field beside each row, inside a block, or along a stroke, when the row is +visible and the text fits. Labels never overlap. Where two would, the row drawn on top keeps its +label, so `priority` decides which rows are labeled first. A dense view labels as many rows as fit. +A row whose text is missing has no label, so a `when` that picks between the field and `null` +labels only some rows, as in the scene above. ### Expressions Options that vary by row take an expression: a number, a boolean, a string, `null`, or a builder. -A string `'$name'` reads field `name`; any other string is a CSS color, and a field sets colors -through a colormap or a conditional. `null` is a missing value: an option that evaluates to missing -takes the theme's value. +A string `'$name'` reads field `name`. Any other string is a CSS color. To color by a field, pass it +through a colormap or a conditional. `null` is a missing value, and an option that evaluates to +missing takes the theme's value. ```ts import { field, when, selected, zoom, units, px } from 'latkit'; @@ -215,25 +215,25 @@ px(3); // a size in CSS pixels, however the view zooms field('vm').from(weather.table('Station')); // read from another dataset, joined by id ``` -Sizes are in CSS pixels on a plane, the earth, and a plot, and in position units in a diagram, -unless wrapped in `px()` or `units()`. State expressions read the view: `selected()`, `hovered()`, -`pointer()`, `elapsed()`, and `zoom()`. `colormaps()` lists the catalog, and `colormapCss(map)` -gives a colormap's colors as a CSS gradient for legends drawn in HTML. +Sizes are in CSS pixels, or in position units in a diagram. Wrap a size in `px()` or `units()` to +choose its unit. State expressions read the view: `selected()`, `hovered()`, `pointer()`, +`elapsed()`, and `zoom()`. `colormaps()` lists the colormap catalog, and `colormapCss(map)` returns +a colormap as a CSS gradient, for legends drawn in HTML. ### Themes, controls, and layouts -- **Theme**: `'dark'`, `'light'`, or overrides of a base theme, grouped by what they style: +- `theme` is `'dark'`, `'light'`, or overrides of a base theme, grouped by what they style: `{ base: 'light', text: { size: 12 }, edge: { width: 2 } }`. -- **Controls**: what the pointer and keys do. By default the pointer pans, zooms, turns, and - hovers; `'inspect'` leaves the camera to the application, and `'none'` suits a still picture. -- **Layout** places vertex rows that have no position: `'stress'` or `'layered'`. A row keeps its +- `controls` sets what the pointer and keys do. By default the pointer pans, zooms, turns, and + hovers. `'inspect'` leaves the camera to your application, and `'none'` suits a still picture. +- `layout` places vertex rows that have no position: `'stress'` or `'layered'`. A row keeps its place once laid out. ## Views -`View.create(canvas, scene)` draws a scene on a ``, and frames everything once it draws. -Views of one canvas each take a region, `[x, y, width, height]` in fractions of the canvas, and the -canvas passes pointer, wheel, and key input to the view under the pointer: +`View.create(canvas, scene)` draws a scene on a `` and fits everything in view once it +draws. Several views can share a canvas. Each takes a region, `[x, y, width, height]` in fractions +of the canvas, and pointer, wheel, and key input goes to the view under the pointer: ```ts const map = await View.create(canvas, mapScene, { region: [0, 0, 1, 0.7] }); @@ -241,12 +241,13 @@ const plot = await View.create(canvas, plotScene, { region: [0, 0.7, 1, 0.3] }); ``` A view with a `null` canvas draws only into [pictures](#pictures-and-video). Views draw with the -first `Gpu` created, which `View.create` creates when there is none; create one first to set its -[budget](#memory), or another for views that should not share its memory, and pass it as `gpu`. +first `Gpu` created, and `View.create` creates one if there is none. Create a `Gpu` first to set its +[budget](#memory), or create another for views that should not share its memory, and pass it as +`gpu`. -A view draws a `Data` at its latest version. `view.sources` gives datasets beside the scene's, or in -place of one of the same name: a `Snapshot` to show a past version, or -[`Tiles`](reference/api.md#tiles) to fetch only the pieces the view needs, from time tiles or vector +A view draws each `Data` at its latest version. `view.sources` adds datasets to the scene's, or +replaces one with the same name. Use a `Snapshot` to show a past version, or +[`Tiles`](reference/api.md#tiles) to fetch only the pieces the view needs from time tiles or vector tiles. ### Camera @@ -260,16 +261,16 @@ view.insets = [48, 0, 0, 320]; // CSS pixels kept clear: top, right, bottom, lef view.orbit = 6; // degrees a second ``` -Every move keeps the camera within reach. A flat map is one world: the camera zooms out until the -whole world fits and pans until the world's edges meet the view's. A globe zooms out until it spans -a third of the view, and after a fit the camera stays near what it framed -([`View.camera`](reference/api.md#view)). +Camera moves stay within the camera's zoom and pan limits. A flat map shows one world. Its camera +zooms out only until the whole world fits, and pans only until the world's edges meet the view's +edges. A globe zooms out until it spans a third of the view. After a fit, the camera stays near the +area the fit framed ([`View.camera`](reference/api.md#view)). ### Events -Intent events (`click`, `open`, `menu`, `drag`, `connect`, `delete`) fire once each, in the order -the user acted. Change events (`hovered`, `selection`, `camera`, `waiting`) carry the latest state, -which the property of the same name holds. Setters fire no events. +Intent events (`click`, `open`, `menu`, `drag`, `connect`, `delete`) fire once per action, in the +order the user acted. Change events (`hovered`, `selection`, `camera`, `waiting`) carry the latest +state, which the property of the same name also holds. Setting a property fires no event. ```ts view.on('click', ({ hit }) => console.log(hit.row, hit.point)); @@ -291,9 +292,9 @@ const at = await view.screenOf({ data: 'grid', table: 'Bus', id: 101 }); // [x, ### Layouts into data -`view.arrange()` lays out every vertex row without a position again, and resolves to one patch per -dataset that writes their places into the mark's position fields. Applied, the places become part -of the data: +`view.arrange()` lays out every vertex row that has no position again. It resolves to one patch per +dataset, which writes the new places into the mark's position fields. Apply the patches to make the +places part of the data: ```ts for (const patch of await view.arrange()) if (patch.data === grid.name) grid.apply(patch); @@ -301,22 +302,21 @@ for (const patch of await view.arrange()) if (patch.data === grid.name) grid.app ### Memory -`gpu.budget = { bytes, uploadPerFrame }` caps the GPU memory data takes and the bytes uploaded per -frame; set it before data draws, on a `Gpu` created before any view, or on `view.gpu`. The views of -one GPU share it: a view that needs less than an even split gets all it needs, and the others -divide the rest. A table too large for its view's share is -paged, and the view draws only the pages its camera needs. Each frame does only the work its -changes need: a patch re-evaluates only the pages it wrote. +`gpu.budget = { bytes, uploadPerFrame }` caps the GPU memory that data uses and the bytes uploaded +per frame. Set it before data draws, on a `Gpu` created before any view or on `view.gpu`. All views +of one GPU share the budget. A view that needs less than an even split gets all it needs, and the +other views divide the rest. A table too large for its view's share is split into pages, and the +view draws only the pages on screen. A patch re-evaluates only the pages it wrote. `view.stats`, `gpu.work`, `gpu.resident`, and `gpu.uploading` report what the views draw and what -the GPU did. A test or benchmark drives frames itself with `gpu.step(now)`, waiting on -`gpu.landed()` between them. +work the GPU did. To drive frames yourself in a test or benchmark, call `gpu.step(now)` and wait on +`gpu.landed()` between frames. ## Pictures and video -A [`Shot`](reference/api.md#shot) is plain data: a size in pixels and the views drawn into it, each -in its region. It draws views whether or not they are on a canvas, and `Shot.of(view)` makes a shot -of exactly what a view's canvas shows. +A [`Shot`](reference/api.md#shot) is plain data: a size in pixels and the views to draw into it, +each in its region. It draws views whether or not they are on a canvas. `Shot.of(view)` makes a +shot of exactly what a view's canvas shows. ```ts const shot = new Shot([3840, 2160], { scale: 2, views: [view] }); // 1920 by 1080 CSS pixels @@ -327,11 +327,10 @@ image.destroy(); ### Video -`shot.video({ fps, output })` encodes H.264 MP4 with WebCodecs as frames are drawn, and writes it -to a `WritableStream`. Each frame draws the views at its own time, `frames / fps` seconds, so motion -follows the video rather than the wall clock, and a frame where nothing moved repeats the one -before. `record(step)` calls `step(time)` before each frame, and finishes the video when it returns -`false`: +`shot.video({ fps, output })` encodes H.264 MP4 with WebCodecs as it draws frames, and writes it to +a `WritableStream`. Frame `n` draws the views at `n / fps` seconds, so motion follows the video's +clock, not the wall clock. A frame where nothing moved repeats the one before. `record(step)` calls +`step(time)` before each frame and finishes the video when `step` returns `false`: ```ts const file = await (await showSaveFilePicker()).createWritable(); @@ -342,25 +341,25 @@ await video.record((time) => { }); ``` -To draw frames one at a time, call `video.frame()`, then `video.finish()`, or `video.abort()` to -stop. +To draw frames one at a time, call `video.frame()` for each frame and `video.finish()` at the end, +or `video.abort()` to stop. ## Formats ### Schema -All JSON latkit reads and writes has a JSON Schema, generated from the same declarations that read -and write it. `latkit schema` prints it, and the npm package's types are generated from it. Its -`$defs` are the inputs (`Scene`, `Data`, `Patch`, `Query`, `Camera`, `Fit`, `Budget`, `Shot`), and -its `outputs` are what latkit returns (`ViewEvent`, `Stats`, `Work`, `Description`, `Rows`, +All JSON that latkit reads and writes has a JSON Schema, generated from the same definitions that +read and write it. `latkit schema` prints it, and the npm package's types are generated from it. +Its `$defs` are the inputs (`Scene`, `Data`, `Patch`, `Query`, `Camera`, `Fit`, `Budget`, `Shot`). +Its `outputs` are what latkit returns (`ViewEvent`, `Stats`, `Work`, `Description`, `Rows`, `Camera`, `Budget`, `Value`, `Failure`, and the types they use). (json)= ### JSON -A dataset's document is its name and the patch that builds it. A patch is a list of changes, each -an object keyed by its method name, with its parameters by name: +A dataset's JSON document holds its name and the patch that builds it. A patch is a list of +changes. Each change is an object keyed by its method name, holding its parameters by name: ```json { @@ -396,17 +395,18 @@ an object keyed by its method name, with its parameters by name: ``` A field is an array of one kind of value, a reference `{ reference, ids }`, or a slice of a binary -file beside the document, `{ file, dtype, offset, count }`: `count` values starting at byte -`offset`, of dtype `f32le`, `f64le`, `i32le`, `u32le`, `u64le`, `u8` (numbers), or `bool` (one byte -each, nonzero is true). A `null` id in a reference leaves that row unconnected. +file next to the document, `{ file, dtype, offset, count }`. A slice reads `count` values starting +at byte `offset`. Its `dtype` is `f32le`, `f64le`, `i32le`, `u32le`, `u64le`, or `u8` for numbers, +or `bool` for booleans (one byte each, nonzero is true). A `null` id in a reference leaves that row +unconnected. (bytes)= ### Bytes -A patch encodes as one frame; a dataset's bytes are the patch that builds it. A frame's header -gives its length, so frames written back to back split without decoding. All values are little -endian. +A patch encodes as one frame, and a dataset's bytes are the patch that builds it. A frame's header +gives its length, so a reader can split frames written back to back without decoding them. All +values are little endian. | Part | Layout | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -425,19 +425,19 @@ endian. | storage | 0 `f64`, 1 `f32`, 2 `i32`, 3 `u32`, 4 `u64`, 5 bits, 6 texts (`u32` offsets, then `u32` byte length and UTF-8), 7 geometry (`u8` depth, each level's `u32` count and offsets, `u32` point count, `f64` x y pairs) | | text | `u32` byte length, UTF-8 | -Bits are packed into `u64` words, low bit first. A sampled field's entries are frames at level 0, -and buckets of `8^k` frames at level `k`, from 0 to 21. A bucket is left out when all eight entries -one level below it are stored, since it can be computed from them. Writers outside latkit should -record frames as `append` changes. +Bits are packed into `u64` words, low bit first. A sampled field's entries are frames at level 0 +and buckets of `8^k` frames at level `k`, for levels 0 to 21. A bucket is left out when all eight +entries one level below it are stored, since it can be computed from them. Writers outside latkit +should record frames as `append` changes. -The header is 12 bytes: the magic `LATK` and the frame's length. Readers reject other bytes, -unknown operation or storage tags, truncated frames, and trailing bytes. The format carries no -version: a release that changes it says so, and files written before it no longer open. Rust and +The header is 12 bytes: the magic `LATK` and the frame's length. Readers reject any other magic, +unknown operation or storage tags, truncated frames, and trailing bytes. The format has no version. +A release that changes it says so, and files written before that release no longer open. Rust and JavaScript tests both check the committed `patch.hex` fixture. -Absolute frame indexes are `u64`; counts within an encoded run are `u32`. JavaScript builders +Absolute frame indexes are `u64`, and counts within an encoded run are `u32`. JavaScript builders accept only safe integers. In Rust, `Patch::from_bytes` decodes with the default `DecodeLimits`: a -256 MiB frame and 512 MiB of decoded storage; `Patch::stream(read, limits)` takes others. +256 MiB frame and 512 MiB of decoded storage. `Patch::stream(read, limits)` takes other limits. ## Deploying @@ -452,14 +452,14 @@ The Rust library is the `latkit` crate. | File/URL slices | Browser fetch and CORS | Supply data directly | Local paths and `file:` URLs | | H.264 MP4 | WebCodecs Constrained Baseline | Use the CLI | Windows Media Foundation | -Serve WASM with `Content-Type: application/wasm`, and the JavaScript, WASM, and worker from the -same package version. WebGPU requires HTTPS or localhost and a supported device. +Serve WASM with `Content-Type: application/wasm`. Serve the JavaScript, WASM, and worker from the +same package version. WebGPU needs HTTPS or localhost, and a supported device. ### Assets and workers By default, `load()` finds the WASM and worker files next to the installed modules. With Vite or -another bundler that supports URL imports, pass their URLs, which also works under nested -deployment paths: +another bundler that supports URL imports, pass their URLs. This also works when the app is +deployed under a nested path: ```ts import { load } from 'latkit'; @@ -469,10 +469,11 @@ import workerUrl from 'latkit/worker?url'; await load({ wasm: wasmUrl, worker: () => new Worker(workerUrl) }); ``` -`latkit/worker` is a standalone classic worker, fine as a Blob URL. Workers start on demand and -stop after 30 seconds idle; where workers are unavailable, jobs run on the calling thread. Your -Content Security Policy must allow the WASM asset and data origins in `connect-src`, WebAssembly -compilation (typically `'wasm-unsafe-eval'` in `script-src`), and the worker URL in `worker-src`. +`latkit/worker` is a standalone classic worker, so it also works from a Blob URL. Workers start when +needed and stop after 30 idle seconds. Where workers are unavailable, jobs run on the calling +thread. Your Content Security Policy must allow the WASM asset and data origins in `connect-src`, +WebAssembly compilation (typically `'wasm-unsafe-eval'` in `script-src`), and the worker URL in +`worker-src`. ### Server-side builders and data @@ -508,8 +509,8 @@ let restored = Data::read(&saved, Reading::default()).await?; `Data::read` and `Patch::read` take a `Reading`: the dataset's name, the base its slices are relative to, and the `resource::Fetch` that fetches them. Every rendering `Host` is also a `Fetch`. -Enable `gpu` for scenes, views, and wgpu rendering; `native` adds system fonts, jobs, files, and -Windows H.264; `web` adds browser integration for the WASM target. +Enable `gpu` for scenes, views, and wgpu rendering. `native` adds system fonts, jobs, files, and +Windows H.264, and `web` adds browser integration for the WASM target. ### Share an existing device @@ -521,6 +522,7 @@ device.set_device_lost_callback(move |_, _| loss.lost()); device.on_uncaptured_error(std::sync::Arc::new(move |error| events.error(&error))); ``` -`from_wgpu` checks the device's limits and leaves its callbacks to you, so forward device loss and -uncaptured errors from your own, as above. Clone one `Gpu` for views that should share caches and a -budget. The [Rust API reference](https://docs.rs/latkit) documents each type, feature, and example. +`from_wgpu` checks the device's limits but leaves its callbacks to you. Forward device loss and +uncaptured errors from your own callbacks, as above. Clone one `Gpu` for views that should share +caches and a budget. The [Rust API reference](https://docs.rs/latkit) documents each type, feature, +and example. diff --git a/docs/index.md b/docs/index.md index 5143017e..dcdc222e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,7 +8,7 @@ renders interactive views in the browser with WebGPU, and images and video from :::{container} plot ```{image} images/opening.png -:alt: The ACTIVSg70k grid on the globe as one 765 kV line opens: buses raised by their frequency, branches that pick up its power glow amber, those it leaves glow cyan +:alt: The ACTIVSg70k grid on a globe as one 765 kV line opens. Buses rise with their frequency, branches that pick up the line's power glow amber, and branches that lose flow glow cyan :class: banner ``` @@ -16,8 +16,8 @@ renders interactive views in the browser with WebGPU, and images and video from :::{container} caption The 70,000-bus ACTIVSg70k grid as one 765 kV line opens. A DC power flow computes where the line's -power goes: branches that pick it up glow amber, and those that lose flow glow cyan. Each bus is -raised by its frequency. Drawn by the `latkit` command line in the {doc}`examples/opening` example. +power goes. Branches that pick it up glow amber, and branches that lose flow glow cyan. Each bus +rises with its frequency. Drawn by the `latkit` command line from `docs/gallery/opening.ts`. ::: ## Install @@ -30,18 +30,18 @@ cargo add latkit # in Rust ## Quickstart -`quickstart(canvas)` draws a dataset on a canvas. When the page is done with them, destroy the -`view` and `grid` it returns. The package's consumer test type-checks and renders this file. +`quickstart(canvas)` draws a dataset on a canvas. Destroy the `view` and `grid` it returns when the +page is done with them. The package's consumer test type-checks and renders this file. ```{literalinclude} ../npm/test/consumer/quickstart.ts :language: typescript ``` -A **dataset** is a set of named tables of columns, which changes in place. A **scene** describes -what to draw: marks of a dataset's tables, in a space, with a theme and controls. A **view** draws a -scene on a canvas and into pictures. Scenes and expressions are plain data, and `JSON.stringify` -writes the JSON the command line reads. `load()` loads the WebAssembly that datasets and drawing -need; `View.create()`, `Data.read()`, and `Patch.read()` call it for you. +A **dataset** is a set of named tables of columns, and you change it in place. A **scene** +describes what to draw: marks that draw a dataset's tables, in a space, with a theme and controls. +A **view** draws a scene on a canvas and into pictures. Scenes and expressions are plain data, and +`JSON.stringify` writes the JSON the command line reads. `load()` loads the WebAssembly that +datasets and drawing need. `View.create()`, `Data.read()`, and `Patch.read()` call it for you. ## Examples @@ -62,9 +62,9 @@ need; `View.create()`, `Data.read()`, and `Patch.read()` call it for you. : 260,000 translucent points of a strange attractor. ::: -latkit draws every picture on this site; the {doc}`examples/index` show each one with its code. The -{doc}`guide` covers data, scenes, views, and pictures; the {doc}`cli` page covers drawing from JSON; -{doc}`performance` reports benchmark results; and the reference lists every option. +latkit draws every picture on this site, and the {doc}`examples/index` show the code for each. The +{doc}`guide` covers data, scenes, views, and pictures. The {doc}`cli` page covers drawing from JSON. +{doc}`performance` reports benchmark results, and the reference lists every option. ```{toctree} :hidden: diff --git a/docs/performance.md b/docs/performance.md index b6203a30..881211d4 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -2,9 +2,10 @@ Each row is a benchmark scenario at the largest size it ran, in Chrome on one GPU. **Open** is the time from creating the view until it has drawn everything. **Frame** and **GPU** are the median CPU -and GPU milliseconds per frame while the view moved, in its slowest moving phase. **Pick** is the +and GPU milliseconds per frame in the scenario's slowest phase with a moving view. **Pick** is the time to answer a pick or hover. `cargo xtask bench` runs every scenario at increasing sizes and -checks it against the committed baselines; `node benchmark/docs.ts` writes this table from them. +checks it against the committed baselines. `node benchmark/docs.ts` writes this table from those +baselines. ```{csv-table} :file: _static/performance.csv @@ -14,42 +15,43 @@ checks it against the committed baselines; `node benchmark/docs.ts` writes this ## What a page is held to -The browser gate first compiles every pipeline cold in Chrome; each must compile within 800 ms. -It then opens each example page in a fresh Chrome without a shader cache, moves the pointer over it +The browser gate first compiles every pipeline cold in Chrome. Each must compile within 800 ms. It +then opens each example page in a fresh Chrome with no shader cache, moves the pointer over it (hovering, zooming, panning, clicking, and stepping through rows with the keys), and exports a picture. A page fails at once on: -- a console or page error; -- a pipeline compiled where it is first used, which holds back the frames that use it; -- no picture exported. +- A console or page error. +- A pipeline compiled when it is first used, which holds back the frames that use it. +- No exported picture. -A page is late if any view has drawn no frame 1,000 ms after the page opens, or if the page misses -more than one display frame in a row while opening or under the pointer. A late page is opened again, -cold, and fails only if it is late both times: a stall of the machine slows one open, while a slow -page slows both. +A page is late if any view has not drawn a frame 1,000 ms after the page opens, or if the page +misses more than one display frame in a row while opening or under the pointer. A late page is +opened again, cold, and fails only if it is late both times. A machine stall slows one open, but a +slow page slows both. -On SwiftShader, which draws on the CPU, the gate only checks that every pipeline compiles; page +On SwiftShader, which draws on the CPU, the gate only checks that every pipeline compiles. Page timings need a GPU. ## What makes it fast -- Every row's drawing is computed on the GPU, so a frame's CPU time stays the same however many rows - draw; only the GPU's work grows. -- Pipelines compile in the background, off the GPU's thread, before any frame needs them: first those - the next frame draws with, then text, then the rest, a few at a time. +- The GPU computes how every row draws, so a frame's CPU time does not grow with the number of + rows. Only the GPU's work grows. +- Pipelines compile in the background, off the GPU's thread, before any frame needs them. The next + frame's pipelines compile first, then text, then the rest, a few at a time. - Each pipeline contains only the code its mark uses: the primitive it draws, and whether it reads joins, sampled fields, or view state. -- A table too large to draw whole is paged: the view draws only the pages the camera needs, within - the GPU memory budget. A page keeps its GPU slot while the camera moves, and stays resident after - the camera leaves while the budget allows, so returning to it needs no upload. -- Data derived for drawing, such as edge lanes, triangles, and fitted domains, is recomputed when the - data changes, never when the camera moves. A page's triangles are built the first time it draws. -- Map tiles are decoded and shaped in workers. A tile arriving appends its rows to its tables, which - are paged like any large table, so only the pages its rows fill upload and evaluate. Tiles split - by the area they cover on screen, so the globe's limb and a tilted horizon stay coarse, and when - the tile budget binds, the tiles drawn largest split first. -- A write updates only the pages holding the rows it changed, and an append only the page the new - rows go into. A paged table's spatial tiles are rebuilt only once the rows appended since the last - build reach an eighth of the rows it was built from. +- A table too large to draw at once is split into pages. The view draws only the pages on screen, + within the GPU memory budget. A page stays on the GPU while the camera moves, and after the + camera leaves while the budget allows, so coming back to it needs no upload. +- Data derived for drawing, such as edge lanes, triangles, and fitted domains, is recomputed when + the data changes, never when the camera moves. A page's triangles are built the first time it + draws. +- Map tiles are decoded and shaped in workers. An arriving tile appends its rows to its tables, + which are paged like any large table, so only the pages those rows fill upload and evaluate. + Tiles split by the area they cover on screen, so the globe's edge and a tilted horizon stay + coarse. When the tile budget runs out, the tiles drawn largest split first. +- A write updates only the pages that hold the rows it changed, and an append updates only the page + the new rows go into. A paged table rebuilds its spatial tiles only once the rows appended since + the last build reach an eighth of the rows that build used. diff --git a/docs/reference/api.md b/docs/reference/api.md index 3993f9fd..e6e59adb 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -1,18 +1,18 @@ # JavaScript API -Every class, function, and type the `latkit` package exports. Scenes and their options take the -[scene and result types](types.md). Four rules hold throughout, so the entries below leave them -out: - -- **Loading.** Scenes, expressions, and patches are plain data. Datasets, views, pictures, and the - colormap functions need latkit's WebAssembly: until [`load()`](#load) finishes, they throw an - `unloaded` error. [`View.create()`](#view-create), [`Gpu.create()`](#gpu-create), [`Data.read()`](#data-read), and [`Patch.read()`](#patch-read) of - JSON load it themselves. -- **Errors.** Everything latkit throws or rejects with is a [`LatkitError`](#latkiterror). Input it cannot use - fails with an `invalid` error whose `path` names the part, such as `marks[2].color`, and - changes nothing. -- **Waits.** Every promise waits within its [`Wait`](#wait): until its `signal` aborts, or 30 seconds. -- **Lifetimes.** A [`Handle`](#handle) holds memory until `destroy()`, or a `using` declaration, frees it. +Every class, function, and type the `latkit` package exports. Scenes and their options use the +[scene and result types](types.md). These rules apply everywhere, so the entries below don't +repeat them: + +- Scenes, expressions, and patches are plain data. Datasets, views, pictures, and the colormap + functions need latkit's WebAssembly, and throw an `unloaded` error until [`load()`](#load) finishes. + [`View.create()`](#view-create), [`Gpu.create()`](#gpu-create), [`Data.read()`](#data-read), and [`Patch.read()`](#patch-read) of JSON call it + themselves. +- Everything latkit throws or rejects with is a [`LatkitError`](#latkiterror). Input it can't use fails with an + `invalid` error and changes nothing. The error's `path` names the bad part, such as + `marks[2].color`. +- Every promise waits until its [`Wait`](#wait) signal aborts, or for 30 seconds without one. +- A [`Handle`](#handle) holds memory until `destroy()` or the end of a `using` block frees it. ## Loading, errors, and lifetimes @@ -26,18 +26,20 @@ What every part of the package shares. `function load(options?: { wasm?: string | URL | WebAssembly.Module; worker?: () => Worker }): Promise` Loads latkit's WebAssembly, once per page. Later and concurrent calls share the first call's -load; options they pass must equal the first call's, or they reject with an `io` error. A failed -load rejects with an `io` error and can be retried. `load()` also requests a GPU adapter while -the WebAssembly compiles, so the browser starts its GPU process early, then the first device for -[`Gpu.create()`](#gpu-create). +load. Options they pass must equal the first call's, or they reject with an `io` error. A failed +load rejects with an `io` error and can be retried. While the WebAssembly compiles, `load()` +requests a GPU adapter so the browser starts its GPU process early. Then it requests the first +device for [`Gpu.create()`](#gpu-create). {#load-wasm .member} **`wasm`**`?: string | URL | WebAssembly.Module` [¶](#load-wasm){.headerlink} -: The URL of this version's `latkit/wasm` asset, or that asset compiled; bundled by default. +: The URL of this version's `latkit/wasm` asset, or that asset compiled. Defaults to the + bundled asset. {#load-worker .member} **`worker`**`?: () => Worker` [¶](#load-worker){.headerlink} -: Creates a worker running the standalone `latkit/worker` bundle; bundled by default. +: Creates a worker that runs the standalone `latkit/worker` bundle. Defaults to the bundled + worker. (latkiterror)= @@ -46,25 +48,25 @@ the WebAssembly compiles, so the browser starts its GPU process early, then the {.signature} `interface LatkitError extends Error` -The error latkit throws: an `Error` whose `code` says what kind of failure it is. +The error latkit throws: an `Error` with a `code` that names the kind of failure. {#latkiterror-code .member} **`code`**`:` [`Failure`](#failure)`['code'] | 'unloaded' | 'destroyed'` [¶](#latkiterror-code){.headerlink} : The kind of failure: - - `invalid`: the input is malformed or does not fit; `path` says where. + - `invalid`: the input is malformed or does not fit. `path` says where. - `unsupported`: the browser or device cannot do what was asked. - `limit`: a budget or device limit would be exceeded. - `io`: reading or writing external bytes failed, such as loading latkit or fetching a file. - - `device-lost`: the GPU device was lost; create a new [`Gpu`](#gpu). + - `device-lost`: the GPU device was lost. Create a new [`Gpu`](#gpu). - `cancelled`: the wait's signal aborted, or the work was cancelled. - `timeout`: the wait's signal timed out. - `unloaded`: the call needs WebAssembly, and [`load()`](#load) has not finished. - - `destroyed`: the object was destroyed, so it can no longer be used. + - `destroyed`: the object was destroyed and can't be used. {#latkiterror-path .member} **`path`**`?: string` [¶](#latkiterror-path){.headerlink} -: The invalid part of the input, such as `marks[2].color`, on `invalid` errors. +: On `invalid` errors, the invalid part of the input, such as `marks[2].color`. (wait)= @@ -77,8 +79,8 @@ Options that bound how long a promise waits. {#wait-signal .member} **`signal`**`?: AbortSignal` [¶](#wait-signal){.headerlink} -: Aborts the wait. A timeout rejects with a `timeout` error; any other abort rejects with a - `cancelled` error whose `cause` is the abort reason. Either way, latkit cancels the call's +: Aborts the wait. A timeout rejects with a `timeout` error. Any other abort rejects with a + `cancelled` error, with the abort reason as its `cause`. Either way, latkit cancels the call's pending work where it can. Without a signal, each wait times out after 30 seconds. (handle)= @@ -88,9 +90,9 @@ Options that bound how long a promise waits. {.signature} `abstract class Handle` -An object that holds WebAssembly memory, and GPU memory for a [`Gpu`](#gpu), until it is destroyed: -`destroy()` frees it, as does a `using` declaration when its block ends. Once it is destroyed, -its other methods throw a `destroyed` error. +An object that holds WebAssembly memory (and GPU memory, for a [`Gpu`](#gpu)) until it is destroyed. +`destroy()` frees it, as does the end of a `using` block. After that, its other methods throw a +`destroyed` error. ```{rubric} Methods ``` @@ -101,7 +103,7 @@ its other methods throw a `destroyed` error. ## Data and patches -Datasets of named tables, their tables that marks draw, and the patches that change them, written without WebAssembly. +Datasets, the tables in them that marks draw, and the patches that change them. Patches are written without WebAssembly. (dataset)= @@ -110,32 +112,32 @@ Datasets of named tables, their tables that marks draw, and the patches that cha {.signature} `abstract class Dataset extends` [`Handle`](#handle) -Named tables of columns, read the same way from a [`Data`](#data), which reads its current version, -and a [`Snapshot`](#snapshot), which reads its own. +Named tables of columns. A [`Data`](#data) reads its current version and a [`Snapshot`](#snapshot) reads +the version it holds, through the same methods. ```{rubric} Properties ``` {#dataset-name .member} `readonly` **`name`**`: string` [¶](#dataset-name){.headerlink} -: The dataset's name: the first part of its tables' JSON form. +: The dataset's name: the `data` in its tables' JSON form `[data, table]`. ```{rubric} Methods ``` {#dataset-table .member} **`table`**`(name: string):` [`Table`](#table) [¶](#dataset-table){.headerlink} -: The dataset's table `name`, for a mark to draw or a field to read from. +: Returns table `name`, for a mark to draw or a field to read. {#dataset-describe .member} **`describe`**`():` [`Description`](#description) [¶](#dataset-describe){.headerlink} -: The tables and their row counts, each field and its kind, and the axis and its span. +: Describes the tables and row counts, each field and its kind, and the axis and its span. {#dataset-get .member} **`get`**`(table: string, id:` [`Id`](#id)`, field: string, options?: { at?: number }):` [`Value`](#value) [¶](#dataset-get){.headerlink} -: Reads `field` of the row with `id` in `table`: for a sampled field, its last frame at or before - `options.at`, or its last frame; for a reference, the referenced row's id. `null` when the - table, row, field, or value is missing. +: Reads `field` of row `id` in `table`. A sampled field gives its last frame at or before + `options.at`, or its last frame without `at`. A reference gives the referenced row's id. + Returns `null` when the table, row, field, or value is missing. {#dataset-get-at .member} **`at`**`?: number` [¶](#dataset-get-at){.headerlink} @@ -143,19 +145,19 @@ and a [`Snapshot`](#snapshot), which reads its own. {#dataset-query .member} **`query`**`(query:` [`Query`](#query)`):` [`QueryResult`](#queryresult) [¶](#dataset-query){.headerlink} -: Reads a table's rows, filtered, sorted, and paged, through the same expressions marks use. - Sampled fields are read at [`query.at`](#query-at). +: Reads a table's rows, filtered, sorted, and paged with the same expressions marks use. Sampled + fields are read at [`query.at`](#query-at). {#dataset-envelope .member} **`envelope`**`(request:` [`EnvelopeRequest`](#enveloperequest)`):` [`Envelope`](#envelope) [¶](#dataset-envelope){.headerlink} -: Each row's minimum and maximum of a sampled field over equal spans of an axis range, for every - row or the rows `request.ids` lists. Each span reads the coarsest stored levels of detail that - fit in it, so it costs a few reads however many frames it covers. Throws a `limit` error when - rows times spans exceeds 2^26. +: Returns the minimum and maximum of a sampled field over equal spans of an axis range, for every + row or for the rows in `request.ids`. Each span reads the coarsest stored levels of detail that + fit inside it, so it costs a few reads however many frames it covers. Throws a `limit` error + when rows times spans exceeds 2^26. {#dataset-tobytes .member} **`toBytes`**`(): Uint8Array` [¶](#dataset-tobytes){.headerlink} -: The dataset as latkit's bytes: a patch that builds it from an empty dataset. +: Returns the dataset as latkit's bytes: a patch that builds it from an empty dataset. (data)= @@ -164,11 +166,12 @@ and a [`Snapshot`](#snapshot), which reads its own. {.signature} `class Data extends` [`Dataset`](#dataset) -A dataset that changes in place, each change applied whole or not at all. Each method of -[`Patch`](#patch) is also a method here that applies that one change at once; `apply` applies a patch -of several. Views draw its latest version from the frame after each change. A [`Snapshot`](#snapshot) -keeps one version and shares its pages with the dataset, so an undo history costs only the pages -that differ. Destroying it leaves views drawing it and its snapshots usable. +A dataset that changes in place. Each change applies all at once or not at all. Every +[`Patch`](#patch) method is also a method here that applies that one change right away, and `apply` +applies a patch of several. Views draw the latest version from the frame after each change. A +[`Snapshot`](#snapshot) keeps one version and shares its pages with the dataset, so an undo history +costs only the pages that differ. Destroying a `Data` leaves the views that draw it, and its +snapshots, usable. ```ts const grid = new Data('grid').set('Bus', [1, 2], { kv: [230, 115] }); @@ -193,16 +196,16 @@ grid.set('Bus', [2], { kv: [69] }); // drawn the next frame - GeoJSON, plain or gzip-compressed, read in a worker. Points, line strings, and polygons become rows of tables `points`, `lines`, and `polygons`, with the geometry in field `geometry` and each property as a field. - - A dataset document, `{"name": ..., "patch": [...]}`, whose patch is read as - [`Patch.read()`](#patch-read) reads one. + - A dataset document, `{"name": ..., "patch": [...]}`. Its patch is read like + [`Patch.read()`](#patch-read). {#data-read-name .member} **`name`**`?: string` [¶](#data-read-name){.headerlink} - : Names the dataset, replacing the name its bytes or document give; GeoJSON requires it. + : The dataset's name, overriding the name in its bytes or document. Required for GeoJSON. {#data-read-base .member} **`base`**`?: string` [¶](#data-read-base){.headerlink} - : The URL a document's file slices are relative to; by default, the one it came from. + : The URL that a document's file slices are relative to. Defaults to the document's URL. {#data-apply .member} **`apply`**`(patch:` [`Patch`](#patch) `| Uint8Array): this` [¶](#data-apply){.headerlink} @@ -215,23 +218,23 @@ grid.set('Bus', [2], { kv: [69] }); // drawn the next frame {#data-replace .member} **`replace`**`(table: string, fields:` [`Fields`](#fields)`): this` [¶](#data-replace){.headerlink} -: Applies [`Patch.replace()`](#patch-replace): replaces table `table` with rows `ids`, or `0` to `n - 1`, and - `fields`. +: Applies [`Patch.replace()`](#patch-replace): replaces table `table` with `fields` for rows `ids` (or `0` to + `n - 1`). {.member} **`replace`**`(table: string, ids:` [`RowIds`](#rowids)`, fields?:` [`Fields`](#fields)`): this` -: Applies [`Patch.replace()`](#patch-replace): replaces table `table` with rows `ids`, or `0` to `n - 1`, and - `fields`. +: Applies [`Patch.replace()`](#patch-replace): replaces table `table` with `fields` for rows `ids` (or `0` to + `n - 1`). {#data-set .member} **`set`**`(table: string, fields:` [`Fields`](#fields)`): this` [¶](#data-set){.headerlink} -: Applies [`Patch.set()`](#patch-set): writes `fields` for rows `ids` of `table`, or `0` to `n - 1`, adding - new rows. +: Applies [`Patch.set()`](#patch-set): writes `fields` for rows `ids` (or `0` to `n - 1`) of `table`, + adding new rows. {.member} **`set`**`(table: string, ids:` [`RowIds`](#rowids)`, fields?:` [`Fields`](#fields)`): this` -: Applies [`Patch.set()`](#patch-set): writes `fields` for rows `ids` of `table`, or `0` to `n - 1`, adding - new rows. +: Applies [`Patch.set()`](#patch-set): writes `fields` for rows `ids` (or `0` to `n - 1`) of `table`, + adding new rows. {#data-remove .member} **`remove`**`(table: string, ids:` [`RowIds`](#rowids)`): this` [¶](#data-remove){.headerlink} @@ -243,7 +246,7 @@ grid.set('Bus', [2], { kv: [69] }); // drawn the next frame {#data-append-ids .member} **`ids`**`?:` [`RowIds`](#rowids) [¶](#data-append-ids){.headerlink} - : The rows the frames cover, every row by default; other rows keep their last values. + : The rows the frames cover. Defaults to every row. Other rows keep their last values. {#data-forget .member} **`forget`**`(before: number): this` [¶](#data-forget){.headerlink} @@ -251,15 +254,15 @@ grid.set('Bus', [2], { kv: [69] }); // drawn the next frame {#data-frames .member} **`frames`**`(table: string, field: string, run:` [`Run`](#run)`): this` [¶](#data-frames){.headerlink} -: Applies [`Patch.frames()`](#patch-frames): stores a run of a sampled field's entries, as a time tile holds. +: Applies [`Patch.frames()`](#patch-frames): stores a run of a sampled field's entries, as in a time tile. {#data-snapshot .member} **`snapshot`**`():` [`Snapshot`](#snapshot) [¶](#data-snapshot){.headerlink} -: The current version as a [`Snapshot`](#snapshot), which later patches leave unchanged. O(1). +: Returns the current version as a [`Snapshot`](#snapshot). Later patches don't change it. O(1). {#data-restore .member} **`restore`**`(snapshot:` [`Snapshot`](#snapshot)`): this` [¶](#data-restore){.headerlink} -: Makes `snapshot`, a version of this dataset, the current version, as an undo does. +: Makes `snapshot`, a version of this dataset, the current version. This is how to undo. (snapshot)= @@ -269,7 +272,7 @@ grid.set('Bus', [2], { kv: [69] }); // drawn the next frame `class Snapshot extends` [`Dataset`](#dataset) One immutable version of a dataset, taken in O(1) by [`Data.snapshot()`](#data-snapshot). A view given a -snapshot draws that version, and keeps drawing it once the snapshot is destroyed. +snapshot draws that version, and keeps drawing it after the snapshot is destroyed. (tiles)= @@ -278,18 +281,18 @@ snapshot draws that version, and keeps drawing it once the snapshot is destroyed {.signature} `class Tiles` -A dataset fetched in pieces as views need them: plain data, a dataset name and a URL. Each view -draws the pieces its camera and plot window need, and the views of a GPU share them. The first -bytes at `url` decide the kind: +A dataset fetched in pieces as views need them. It is plain data: a dataset name and a URL. Each +view fetches the pieces its camera and plot window need, and views on one GPU share them. The +first bytes at `url` decide the kind: -- Time tiles: a `frames.json` manifest as `latkit tile` writes it. A line tracing a sampled field - fetches the detail its plot window shows; other marks fetch the frames around the view's `at`. - A span still loading shows the coarser span that contains it. +- Time tiles: a `frames.json` manifest written by `latkit tile`. A line that traces a sampled + field fetches the detail its plot window shows. Other marks fetch the frames around the view's + `at`. While a span loads, the coarser span that contains it shows. - Vector tiles: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` URL template. Each layer becomes a table of the features its marks draw: polygons for areas, lines for lines, points for vertices. A table named `layer:polygons`, `layer:lines`, or `layer:points` holds - that kind whatever the marks. A tile arriving appends its rows to the view's dataset, which - keeps the rows of tiles it no longer draws until they outnumber the rows it draws. + that kind regardless of the marks. Each tile that arrives appends its rows to the view's + dataset. Rows of tiles no longer drawn are kept until they outnumber the rows drawn. ```{rubric} Constructor ``` @@ -307,14 +310,14 @@ bytes at `url` decide the kind: {#tiles-url .member} `readonly` **`url`**`: string` [¶](#tiles-url){.headerlink} -: Where the tiles are. +: The tiles' URL. ```{rubric} Methods ``` {#tiles-table .member} **`table`**`(name: string):` [`Table`](#table) [¶](#tiles-table){.headerlink} -: The tiles' table `name`, such as a vector tile layer, for a mark to draw. +: Returns table `name`, such as a vector tile layer, for a mark to draw. (source)= @@ -332,15 +335,15 @@ A dataset a view draws: the one a [`Table`](#table) belongs to, or one of the vi {.signature} `class Table` -A table of a dataset, which a mark draws or a field reads from: `grid.table('Bus')`. A view draws -the datasets its scene's tables belong to. Its JSON form is `[data, table]`. +A table in a dataset, such as `grid.table('Bus')`. Marks draw its rows and `field().from()` reads +its fields. A view loads every dataset its scene uses. In JSON it is `[data, table]`. ```{rubric} Constructor ``` {#table-new .member} **`new Table`**`(source:` [`Source`](#source)`, name: string)` [¶](#table-new){.headerlink} -: Table `name` of `source`. +: The table `name` in `source`. ```{rubric} Properties ``` @@ -358,7 +361,7 @@ the datasets its scene's tables belong to. Its JSON form is `[data, table]`. {#table-tojson .member} **`toJSON`**`(): [data: string, table: string]` [¶](#table-tojson){.headerlink} -: The JSON form, `[data, table]`, which `JSON.stringify` writes for the table. +: Returns the JSON form, `[data, table]`. `JSON.stringify` calls it. (tableinput)= @@ -367,8 +370,8 @@ the datasets its scene's tables belong to. Its JSON form is `[data, table]`. {.signature} `type TableInput =` [`Table`](#table) `| readonly [data: string, table: string]` -A table that a mark draws or a field reads from: a dataset's [`Table`](#table), or its JSON form -`[data, table]`, which names a dataset the view's `sources` give. +A table for a mark to draw or a field to read: a [`Table`](#table), or its JSON form `[data, table]`, +where `data` names a dataset in the view's `sources`. (queryresult)= @@ -389,13 +392,13 @@ The rows a query returns, as columns in the forms a patch takes. {#queryresult-fields .member} **`fields`**`: Record` [¶](#queryresult-fields){.headerlink} -: Each selected field's column, by output name: numbers as a `Float64Array` with NaN for missing - values, other values as an array with `null` for missing ones, and a reference as the table it - references and the id of the row each row references. +: Each selected field's column, keyed by output name. Numbers come as a `Float64Array` with NaN + for missing values. Other values come as an array with `null` for missing ones. A reference + comes as the referenced table and, for each row, the id of the row it references. {#queryresult-total .member} **`total`**`: number` [¶](#queryresult-total){.headerlink} -: How many rows matched the query before `offset` and `limit` applied. +: The number of rows that matched, before `offset` and `limit`. (envelope)= @@ -404,7 +407,7 @@ The rows a query returns, as columns in the forms a patch takes. {.signature} `interface Envelope` -Each row's minimum and maximum of a sampled field over equal spans of the axis. +The minimum and maximum of a sampled field for each row, over equal spans of the axis. {#envelope-at .member} **`at`**`: Float64Array` [¶](#envelope-at){.headerlink} @@ -412,7 +415,7 @@ Each row's minimum and maximum of a sampled field over equal spans of the axis. {#envelope-least .member} **`least`**`: Float32Array` [¶](#envelope-least){.headerlink} -: Each row's minimum in each span, `spans` values per row, row after row; NaN without frames. +: Each row's minimum in each span, `spans` values per row, row after row. NaN for no value. {#envelope-greatest .member} **`greatest`**`: Float32Array` [¶](#envelope-greatest){.headerlink} @@ -425,9 +428,9 @@ Each row's minimum and maximum of a sampled field over equal spans of the axis. {.signature} `class Patch` -A list of changes to a dataset, which [`Data.apply`](#data-apply) applies in order, all or none. It is written -as latkit's bytes as it is built, without WebAssembly; each method adds a change and returns the -patch. +A list of changes to a dataset. [`Data.apply`](#data-apply) applies them in order, all or none. The patch is +written as latkit's bytes as it is built, without WebAssembly. Each method adds a change and +returns the patch. ```ts const patch = new Patch('grid') @@ -455,27 +458,27 @@ const patch = new Patch('grid') {#patch-read .member} `static` **`read`**`(source:` [`ByteSource`](#bytesource) `| object, options?: { base?: string; limit?: number } &` [`Wait`](#wait)`): Promise` [¶](#patch-read){.headerlink} -: Reads a patch from `source`: latkit's bytes, one frame as `toBytes()` writes it, or JSON that +: Reads a patch from `source`: latkit's bytes (one frame, as `toBytes()` writes it), or JSON that any language can write without latkit. The JSON is a list of changes. Each change is keyed by its method name and holds the method's parameters by name, such as `{"set": {"table": "Bus", "ids": [1], "fields": {"kv": [345]}}}`. A field may be a slice of a - binary file, `{file, dtype, offset, count}`, fetched at once. Bytes are read without - WebAssembly. + binary file, `{file, dtype, offset, count}`, which is fetched right away. Bytes are read + without WebAssembly. {#patch-read-base .member} **`base`**`?: string` [¶](#patch-read-base){.headerlink} - : The URL the JSON's file slices are relative to; by default, the one it came from. + : The URL that the JSON's file slices are relative to. Defaults to the JSON's URL. {#patch-read-limit .member} **`limit`**`?: number` [¶](#patch-read-limit){.headerlink} - : The longest frame in bytes, 256 MiB by default; a longer one fails with a `limit` error. + : The longest frame in bytes. Defaults to 256 MiB. A longer frame fails with a `limit` error. {#patch-stream .member} `static` **`stream`**`(stream:` [`ReadableBytes`](#readablebytes) `| AsyncIterable, options?: { limit?: number }): AsyncGenerator` [¶](#patch-stream){.headerlink} : Yields each patch in `stream` as soon as its last byte arrives. The stream carries frames back - to back, such as a response body, a file, or a socket's messages. Besides the current chunk - and the patches the caller keeps, at most one frame is buffered. Breaking out of the loop - cancels the stream. Throws an `invalid` error for a stream that ends inside a frame. + to back, such as a response body, a file, or a socket's messages. At most one frame is + buffered, besides the current chunk and any patches the caller keeps. Breaking out of the loop + cancels the stream. Throws an `invalid` error if the stream ends inside a frame. ```ts for await (const patch of Patch.stream((await fetch('/feed')).body!)) data.apply(patch); @@ -483,8 +486,8 @@ const patch = new Patch('grid') {#patch-stream-limit .member} **`limit`**`?: number` [¶](#patch-stream-limit){.headerlink} - : The longest frame in bytes, at least 12; 256 MiB by default. Each frame states its length - up front, so a longer one fails with a `limit` error before its body is allocated. + : The longest frame in bytes, at least 12. Defaults to 256 MiB. Each frame states its length + up front, so a longer frame fails with a `limit` error before its body is allocated. {#patch-axis .member} **`axis`**`(name: string, unit: string): this` [¶](#patch-axis){.headerlink} @@ -494,24 +497,24 @@ const patch = new Patch('grid') {#patch-replace .member} **`replace`**`(table: string, fields:` [`Fields`](#fields)`): this` [¶](#patch-replace){.headerlink} : Replaces table `table` with rows `ids` and `fields`, adding the table if it does not exist. - Without ids, the rows are `0` to `n - 1`, `n` being the length of each field's values. + Without ids, the rows are `0` to `n - 1`, where `n` is the length of each field's values. {.member} **`replace`**`(table: string, ids:` [`RowIds`](#rowids)`, fields?:` [`Fields`](#fields)`): this` : Replaces table `table` with rows `ids` and `fields`, adding the table if it does not exist. - Without ids, the rows are `0` to `n - 1`, `n` being the length of each field's values. + Without ids, the rows are `0` to `n - 1`, where `n` is the length of each field's values. {#patch-set .member} **`set`**`(table: string, fields:` [`Fields`](#fields)`): this` [¶](#patch-set){.headerlink} : Writes `fields` for rows `ids` of `table`. Rows with existing ids change, new ids add rows, and omitted fields keep their values. Adds the table if it does not exist. Without ids, the rows - are `0` to `n - 1`, `n` being the length of each field's values. + are `0` to `n - 1`, where `n` is the length of each field's values. {.member} **`set`**`(table: string, ids:` [`RowIds`](#rowids)`, fields?:` [`Fields`](#fields)`): this` : Writes `fields` for rows `ids` of `table`. Rows with existing ids change, new ids add rows, and omitted fields keep their values. Adds the table if it does not exist. Without ids, the rows - are `0` to `n - 1`, `n` being the length of each field's values. + are `0` to `n - 1`, where `n` is the length of each field's values. {#patch-remove .member} **`remove`**`(table: string, ids:` [`RowIds`](#rowids)`): this` [¶](#patch-remove){.headerlink} @@ -519,14 +522,14 @@ const patch = new Patch('grid') {#patch-append .member} **`append`**`(table: string, at: number | readonly number[], fields: Readonly>, options?: { ids?:` [`RowIds`](#rowids) `}): this` [¶](#patch-append){.headerlink} -: Records a frame of `table` at each axis coordinate in `at`. Each field gives a value per row - per frame, frame by frame. A field's first frame makes it a sampled field. Consecutive appends - of typed float arrays to the same fields of every row write as one change, so a frame per call - costs no more than one call with every frame. +: Records a frame of `table` at each axis coordinate in `at`. Each field gives one value per row + for each frame, frame by frame. A field becomes a sampled field at its first frame. Consecutive + appends of typed float arrays to the same fields of every row are written as one change, so + appending one frame per call costs no more than one call with every frame. {#patch-append-ids .member} **`ids`**`?:` [`RowIds`](#rowids) [¶](#patch-append-ids){.headerlink} - : The rows the frames cover, every row by default; other rows keep their last values. + : The rows the frames cover. Defaults to every row. Other rows keep their last values. {#patch-forget .member} **`forget`**`(before: number): this` [¶](#patch-forget){.headerlink} @@ -534,9 +537,9 @@ const patch = new Patch('grid') {#patch-frames .member} **`frames`**`(table: string, field: string, run:` [`Run`](#run)`): this` [¶](#patch-frames){.headerlink} -: Stores a run of entries of sampled field `field` of `table`, at one level of detail. This is - what a time tile holds, as `latkit tile` writes it. At level 0 each entry is a frame; at level - `k`, a bucket of `8^k` frames. None of the entries may be stored already. Entries past the +: Stores a run of entries of sampled field `field` in `table`, at one level of detail. This is + what a time tile from `latkit tile` holds. At level 0 each entry is a frame. At level `k` each + entry is a bucket of `8^k` frames. None of the entries may already be stored. Entries past the field's last frame extend it. {#patch-extend .member} @@ -545,7 +548,7 @@ const patch = new Patch('grid') {#patch-tobytes .member} **`toBytes`**`(): Uint8Array` [¶](#patch-tobytes){.headerlink} -: A copy of the patch as latkit's bytes: one frame, naming the dataset if the patch does. +: Returns a copy of the patch as latkit's bytes: one frame, naming the dataset if it has one. (bytesource)= @@ -554,9 +557,9 @@ const patch = new Patch('grid') {.signature} `type ByteSource = Uint8Array | ArrayBuffer | Blob | Response |` [`ReadableBytes`](#readablebytes) `| AsyncIterable | string` -Bytes to read: an array or buffer, a `Blob`, a `Response`, a byte stream, or a URL to fetch as a -string. Reading waits within its [`Wait`](#wait), and fails with an `io` error when a fetch fails or -answers an error status, or the bytes cannot be read. +Bytes to read: an array or buffer, a `Blob`, a `Response`, a byte stream, or a URL string to +fetch. Reading is bounded by the call's [`Wait`](#wait). It fails with an `io` error when a fetch +fails or returns an error status, or when the bytes can't be read. (readablebytes)= @@ -569,7 +572,7 @@ A byte stream: anything shaped like a browser or Node.js `ReadableStream` of `Ui {#readablebytes-getreader .member} **`getReader`**`(): { read(): Promise<{ done: boolean; value?: Uint8Array }>; cancel(reason?: unknown): Promise; releaseLock(): void }` [¶](#readablebytes-getreader){.headerlink} -: A reader of the stream's chunks, which latkit reads to the end, or cancels. +: Returns a reader of the stream's chunks. latkit reads it to the end or cancels it. (rowids)= @@ -589,8 +592,9 @@ or `Float64Array` of numbers. `type Values = Float64Array | Float32Array | Int32Array | Uint32Array | Uint8Array | readonly (number | null)[] | readonly (boolean | null)[] | readonly (string | null)[] | readonly (`[`Geometry`](#geometry) `| null)[]` One field's values, one per row, with `null` for a missing value. Numbers come as a typed array -or an array. A `Uint8Array` holds booleans, nonzero being true. Booleans, strings, and geometries -also come as arrays. Values are copied when passed, so the arrays can be reused at once. +or an array. A `Uint8Array` holds booleans, where nonzero is true. Booleans, strings, and +geometries also come as arrays. Values are copied when passed, so the arrays can be reused right +away. (geometry)= @@ -626,7 +630,7 @@ A reference field: for each row, a row of another table. {.signature} `function reference(table: string, ids:` [`RowIds`](#rowids)`):` [`Reference`](#reference) -Builds a [`Reference`](#reference) field: for each row, the id of the row of `table` it references. +Builds a [`Reference`](#reference) field: for each row, the id of the row it references in `table`. (fields)= @@ -644,28 +648,29 @@ A table's fields by name: values, or a [`Reference`](#reference). {.signature} `interface Run` -A run of a sampled field's entries at one level of detail, as [`Patch.frames()`](#patch-frames) stores. +A run of entries of a sampled field at one level of detail, for [`Patch.frames()`](#patch-frames). {#run-level .member} **`level`**`?: number` [¶](#run-level){.headerlink} -: The level of detail, from 0 to 21; 0 by default. +: The level of detail, from 0 to 21. Defaults to 0. {#run-first .member} **`first`**`: number` [¶](#run-first){.headerlink} -: The first entry's number: a safe nonnegative integer. +: The index of the first entry, a safe nonnegative integer. {#run-at .member} **`at`**`: Float64Array | readonly number[]` [¶](#run-at){.headerlink} -: Each frame's coordinate, or each bucket's first and last frames' coordinates. +: Each frame's coordinate. At a bucket level, the coordinates of each bucket's first and last + frames. {#run-values .member} **`values`**`: Float32Array | readonly number[]` [¶](#run-values){.headerlink} -: A value per row, entry by entry; at a bucket level, every bucket's per-row minimums, then every - bucket's per-row maximums. NaN marks a missing value. +: One value per row for each entry, entry by entry. At a bucket level, the per-row minimums of + every bucket, then the per-row maximums of every bucket. NaN marks a missing value. ## Expressions -Builders for the per-row expressions that scene options take, colors among them. +Builders for the per-row expressions that scene options take, including colors. (expr)= @@ -675,8 +680,8 @@ Builders for the per-row expressions that scene options take, colors among them. `class Expr` A value for every row of a mark: a constant, a color, a field, view state, or arithmetic on them. -Color options take one that computes a color: a CSS color, a value through a colormap, or a -conditional choosing between colors. +Color options take one that computes a color: a CSS color, a value mapped through a colormap, or +a conditional that chooses between colors. ```{rubric} Constructor ``` @@ -697,17 +702,17 @@ conditional choosing between colors. {#expr-tojson .member} **`toJSON`**`():` [`ExprNode`](#exprnode) [¶](#expr-tojson){.headerlink} -: The JSON form, which `JSON.stringify` writes for the expression. +: Returns the JSON form. `JSON.stringify` calls it. {#expr-from .member} **`from`**`(table:` [`TableInput`](#tableinput)`): Expr` [¶](#expr-from){.headerlink} : Reads this expression's fields from `table`, such as `weather.table('Station')`, joined by row - id. A view draws the table's dataset too. + id. The view also loads that table's dataset. {#expr-step .member} **`step`**`(): Expr` [¶](#expr-step){.headerlink} -: Reads this expression's sampled fields at the last frame at or before the view's `at`, instead - of interpolating toward the next frame. +: Reads this expression's sampled fields at the last frame at or before the view's `at`, without + interpolating toward the next frame. {#expr-add .member} **`add`**`(other:` [`ExprInput`](#exprinput)`): Expr` [¶](#expr-add){.headerlink} @@ -815,17 +820,17 @@ conditional choosing between colors. {#expr-is .member} **`is`**`(text: string): Expr` [¶](#expr-is){.headerlink} -: Whether a text field equals `text`, a plain text that is neither a field nor a color. +: Whether a text field equals `text`, plain text read as neither a field nor a color. {#expr-contains .member} **`contains`**`(text: string): Expr` [¶](#expr-contains){.headerlink} -: Whether a text field contains `text`, a plain text, ignoring case. In a mark, the field must be +: Whether a text field contains `text` (plain text), ignoring case. In a mark, the field must be in the mark's own table. {#expr-colormap .member} **`colormap`**`(map:` [`Colormap`](#colormap)`):` [`Mapped`](#mapped) [¶](#expr-colormap){.headerlink} -: Maps the value to a color for every row through `map`, a catalog name or custom colors. The - domain fits the values shown unless set with [`Mapped.domain()`](#mapped-domain). +: Maps the value to a color through `map`, a catalog name or custom colors. The domain fits the + values shown unless set with [`Mapped.domain()`](#mapped-domain). {#expr-scale .member} **`scale`**`(low: number, high: number):` [`Mapped`](#mapped) [¶](#expr-scale){.headerlink} @@ -839,9 +844,9 @@ conditional choosing between colors. {.signature} `type ExprInput =` [`Expr`](#expr) `|` [`ExprNode`](#exprnode) -An option's value: an [`Expr`](#expr), or its JSON form. In the JSON form, `null` is a missing -value, a string starting with `$` reads a field (`'$kv'` reads field `kv`), and any other string -is a CSS color. Color options take a value that computes a color. +An option's value: an [`Expr`](#expr) or its JSON form. In JSON, `null` is a missing value, a string +starting with `$` reads a field (`'$kv'` reads field `kv`), and any other string is a CSS color. +Color options take a value that computes a color. (mapped)= @@ -850,15 +855,15 @@ is a CSS color. Color options take a value that computes a color. {.signature} `class Mapped extends` [`Expr`](#expr) -A value mapped through a colormap or onto a range, with an input domain that can be set. +A value mapped through a colormap or onto a range. Its input domain can be set. ```{rubric} Methods ``` {#mapped-domain .member} **`domain`**`(low: number, high: number): Mapped` [¶](#mapped-domain){.headerlink} -: Sets the input values that map to the colormap's or the range's ends, instead of fitting the - values shown. +: Sets the input values that map to the ends of the colormap or range. Without it, the domain + fits the values shown. (color)= @@ -877,8 +882,8 @@ start with `$` is a color. {.signature} `function field(name: string):` [`Expr`](#expr) -Reads field `name` of each row, written `'$name'` in JSON. `from()` and `step()` give it its -`{ field }` form. +Reads field `name` of each row. In JSON it is `'$name'`, or the `{ field }` form after `from()` +or `step()`. (when-function)= @@ -889,7 +894,7 @@ Reads field `name` of each row, written `'$name'` in JSON. `from()` and `step()` Starts a conditional. `when(c).then(a).when(d).then(b).otherwise(e)` takes the value of the first case whose condition holds, else `e`. The values may be numbers, colors, or a text field and -`null`, as the option takes. +`null`, whichever the option takes. (when)= @@ -898,7 +903,7 @@ case whose condition holds, else `e`. The values may be numbers, colors, or a te {.signature} `class When` -A conditional's first condition, awaiting its value from `then`. +A conditional's first condition. `then` gives its value. ```{rubric} Methods ``` @@ -914,7 +919,7 @@ A conditional's first condition, awaiting its value from `then`. {.signature} `class Then` -A conditional's cases so far, awaiting another `when` case or the `otherwise` fallback. +A conditional's cases so far. Add a case with `when`, or finish with `otherwise`. ```{rubric} Methods ``` @@ -934,7 +939,7 @@ A conditional's cases so far, awaiting another `when` case or the `otherwise` fa {.signature} `class Next` -A conditional's next condition, awaiting its value from `then`. +A conditional's next condition. `then` gives its value. ```{rubric} Methods ``` @@ -953,9 +958,9 @@ A conditional's next condition, awaiting its value from `then`. {.signature} `function px(size:` [`ExprInput`](#exprinput)`):` [`Px`](#px) -A size in CSS pixels regardless of the space's unit, so it stays the same on screen as the view -zooms: `px(1)` keeps a diagram's stroke 1 pixel wide at any zoom. Given a number, the result also -works as a theme length. +A size in CSS pixels in any space, so it stays the same on screen as the view zooms. `px(1)` +keeps a diagram's stroke 1 pixel wide at any zoom. Given a number, the result also works as a +theme length. (units-function)= @@ -967,10 +972,10 @@ works as a theme length. {.signature} `function units(size:` [`ExprInput`](#exprinput)`, limits?: Omit<`[`Units`](#units)`, 'units'>):` [`Units`](#units) -A size in position units (degrees on the earth) regardless of the space's unit, so it scales as -the view zooms. `limits` bound its on-screen size in CSS pixels: `units('$r', { atLeast: 2 })` -grows with the map and never shrinks below 2 pixels. A marker keeps its proportions, with the -limits applied to its larger side. Given only a number, the result also works as a theme length. +A size in position units (degrees on the earth) in any space, so it scales as the view zooms. +`limits` bound its size on screen in CSS pixels: `units('$r', { atLeast: 2 })` grows with the +map and never shrinks below 2 pixels. A marker keeps its proportions, and the limits apply to its +larger side. Given only a number, the result also works as a theme length. (selected)= @@ -1055,8 +1060,8 @@ RGBA pixels with straight alpha, rows top to bottom. A browser `ImageData` has t {.signature} `function colormaps(): { name:` [`ColormapName`](#colormapname)`; label: string; kind:` [`ColormapKind`](#colormapkind) `}[]` -The catalog's colormaps, in order: each one's name, its title for people, such as -`'Red / Blue'`, and its kind. +Returns the catalog's colormaps in order, each with its name, its label such as `'Red / Blue'`, +and its kind. (colormapcss)= @@ -1066,12 +1071,12 @@ The catalog's colormaps, in order: each one's name, its title for people, such a `function colormapCss(map:` [`Colormap`](#colormap)`, direction?: string): string` Returns a CSS `linear-gradient(...)` with the colors the GPU draws for `map`, a catalog name or a -custom colormap. `direction` is the gradient's CSS direction, `'to right'` by default. A +custom colormap. `direction` is the CSS gradient direction and defaults to `'to right'`. A categorical map gives one solid band per category. ## Views -Views draw scenes on canvases, with the GPU they share. +Views, which draw scenes on canvases, and the GPU they draw with. (view)= @@ -1080,9 +1085,10 @@ Views draw scenes on canvases, with the GPU they share. {.signature} `class View extends` [`Handle`](#handle) -A scene drawn from its datasets, on a canvas and into pictures; created by [`View.create()`](#view-create). -The canvas passes it pointer, wheel, and key input, and a [`Shot`](#shot) draws it into pictures. -Destroying it rejects its unanswered queries; its canvas keeps its last frame. +A scene drawn from its datasets on a canvas and into pictures. Create one with +[`View.create()`](#view-create). The canvas passes it pointer, wheel, and key input, and a `Shot` draws it +into pictures. Destroying a view rejects its unanswered queries, and its canvas keeps the last +frame. ```ts const grid = new Data('grid').set('Bus', [1, 2], { x: [0, 30], y: [0, 20], kv: [230, 115] }); @@ -1101,21 +1107,22 @@ const view = await View.create(canvas, { {#view-region .member} **`region`**`:` [`Rect`](#rect) [¶](#view-region){.headerlink} -: Where the view draws on its canvas and in pictures: `[x, y, width, height]` in fractions of - them, from the top left. Views of one canvas draw in the order they were created. +: The part of its canvas, and of pictures, that the view draws in: `[x, y, width, height]` in + fractions, from the top left. Views on one canvas draw in the order they were created. {#view-scene .member} **`scene`**`:` [`Scene`](#scene) [¶](#view-scene){.headerlink} -: The scene drawn. Marks that match a mark of the previous scene by kind, dataset, table, and - name keep their state and ease to their new options. An invalid scene leaves the current one. +: The scene drawn. A mark that matches one in the previous scene by kind, dataset, table, and + name keeps its state and eases to its new options. An invalid scene leaves the current one in + place. {#view-sources .member} **`sources`**`: readonly` [`Source`](#source)`[]` [¶](#view-sources){.headerlink} -: Datasets the view draws besides those of its scene's tables, and in their place where they - share a name: a mark whose table is written `[data, table]` draws the source named `data`. The - view draws a [`Data`](#data) at its latest version, from the frame after each change; a [`Snapshot`](#snapshot) - unchanged; and [`Tiles`](#tiles) as the pieces it needs. A mark whose dataset no longer fits it draws - nothing; it is listed in `failures`, and the next frame emits an `error` event. +: Datasets the view draws besides those its scene uses. A source replaces a scene dataset with + the same name. A mark whose table is written `[data, table]` draws the source named `data`. The + view draws a [`Data`](#data) at its latest version from the frame after each change, a [`Snapshot`](#snapshot) as it + is, and [`Tiles`](#tiles) as the pieces it needs. A mark whose dataset no longer fits it draws nothing. + It is listed in `failures`, and the next frame emits an `error` event. {#view-at .member} **`at`**`: number` [¶](#view-at){.headerlink} @@ -1124,13 +1131,13 @@ const view = await View.create(canvas, { {#view-camera .member} **`camera`**`:` [`Camera`](#camera) [¶](#view-camera){.headerlink} -: Where the view looks. Setting it moves the camera at once; use [`View.fly()`](#view-fly) to ease. A +: Where the view looks. Setting it moves the camera at once. Use [`View.fly()`](#view-fly) to ease. A camera of the wrong kind for the space, such as a plot camera on a map, is ignored. - Every move keeps the camera within reach. On the earth, the space bounds it, as [`Earth`](#earth) - describes, inside the insets. Fits narrow it to 3 zoom levels out and 8 in from what they - framed, with the center near the framed box. When a resize, new insets, or a new space moves - the camera back within reach, a `camera` event reports it. + Camera moves stay within the camera's zoom and pan limits. On the earth, the space sets the + limits inside the insets, as [`Earth`](#earth) describes. A fit narrows them to 3 zoom levels out and 8 + in from the box it framed, with the center near that box. When a resize, new insets, or a new + space moves the camera back within its limits, a `camera` event reports it. {#view-insets .member} **`insets`**`:` [`Insets`](#insets) [¶](#view-insets){.headerlink} @@ -1139,7 +1146,7 @@ const view = await View.create(canvas, { {#view-orbit .member} **`orbit`**`: number` [¶](#view-orbit){.headerlink} -: Degrees per second the camera turns about its center on a map or plane; 0 stops it. A diagram +: Degrees per second the camera turns about its center on a map or plane. 0 stops it. A diagram never turns. {#view-selection .member} @@ -1148,16 +1155,16 @@ const view = await View.create(canvas, { {#view-hovered .member} `readonly` **`hovered`**`:` [`Hit`](#hit) `| null` [¶](#view-hovered){.headerlink} -: The row under the pointer, or the row keyboard navigation moved to; `null` if none. +: The row under the pointer or the row keyboard navigation moved to, or `null`. {#view-waiting .member} `readonly` **`waiting`**`:` [`Waiting`](#waiting) [¶](#view-waiting){.headerlink} -: The kinds of pending work that kept the last frame from being complete; empty when it was +: The kinds of pending work that kept the last frame from being complete. Empty when it was complete. See [`Waiting`](#waiting). {#view-complete .member} `readonly` **`complete`**`: boolean` [¶](#view-complete){.headerlink} -: Whether the last frame was complete, with nothing in `waiting` pending. Shots wait for it. +: Whether the last frame was complete, with nothing in `waiting`. Shots wait for this. {#view-failures .member} `readonly` **`failures`**`: readonly` [`LatkitError`](#latkiterror)`[]` [¶](#view-failures){.headerlink} @@ -1177,13 +1184,13 @@ const view = await View.create(canvas, { {#view-create .member} `static` **`create`**`(canvas: HTMLCanvasElement | OffscreenCanvas | null, scene:` [`Scene`](#scene)`, options?: { region?:` [`Rect`](#rect)`; camera?:` [`Camera`](#camera)`; sources?: readonly` [`Source`](#source)`[]; gpu?:` [`Gpu`](#gpu)`; pixelRatio?: number }): Promise` [¶](#view-create){.headerlink} -: Creates a view that draws `scene` on `canvas`, in `options.region`, over any views already on - it; with a `null` canvas, the view draws only into pictures. It draws the datasets its scene's - tables belong to, and frames everything once it draws, unless given a camera. +: Creates a view that draws `scene` on `canvas`, in `options.region`, over any views already + there. With a `null` canvas, the view draws only into pictures. The view loads every dataset + its scene uses. Unless given a camera, it frames everything when it first draws. {#view-create-region .member} **`region`**`?:` [`Rect`](#rect) [¶](#view-create-region){.headerlink} - : Where the view draws on the canvas and in pictures; the whole of them by default. + : The part of the canvas, and of pictures, that the view draws in. Defaults to all of it. {#view-create-camera .member} **`camera`**`?:` [`Camera`](#camera) [¶](#view-create-camera){.headerlink} @@ -1191,41 +1198,43 @@ const view = await View.create(canvas, { {#view-create-sources .member} **`sources`**`?: readonly` [`Source`](#source)`[]` [¶](#view-create-sources){.headerlink} - : Datasets beside those of the scene's tables; see [`View.sources`](#view-sources). + : Datasets to draw besides those the scene uses. See [`View.sources`](#view-sources). {#view-create-gpu .member} **`gpu`**`?:` [`Gpu`](#gpu) [¶](#view-create-gpu){.headerlink} - : The GPU that draws the view; the first [`Gpu`](#gpu) created by default, created if need be. + : The GPU that draws the view. Defaults to the first [`Gpu`](#gpu) created, creating one if needed. {#view-create-pixelratio .member} **`pixelRatio`**`?: number` [¶](#view-create-pixelratio){.headerlink} - : Pixels per CSS pixel of an `OffscreenCanvas`, 1 by default; the first view on it sets it. + : Pixels per CSS pixel of an `OffscreenCanvas`. Defaults to 1. The first view on the canvas + sets it. {#view-fit .member} **`fit`**`(fit?:` [`Fit`](#fit)`): void` [¶](#view-fit){.headerlink} -: Frames what `fit` names, every row by default, keeping pitch and bearing. It takes effect on - the next frame, or a frame or two later when the extent must be measured on the GPU. +: Frames what `fit` names, keeping pitch and bearing. Defaults to every row. It takes effect on + the next frame, or a frame or two later when the GPU must measure the extent. {#view-fly .member} **`fly`**`(camera:` [`Camera`](#camera)`): void` [¶](#view-fly){.headerlink} -: Eases the camera to `camera`, kept within reach as [`View.camera`](#view-camera) describes, over twice - the scene's ease time, or at once when motion is reduced. A camera of the wrong kind for the - space is ignored. +: Eases the camera to `camera` over twice the scene's ease time, or moves it at once when motion + is reduced. The camera stays within its limits, as [`View.camera`](#view-camera) describes. A camera of + the wrong kind for the space is ignored. {#view-screen .member} **`screen`**`(position:` [`Position`](#position)`):` [`Point`](#point) `| null` [¶](#view-screen){.headerlink} -: Projects `position` to a point on screen with the current camera, for anchoring HTML overlays; - `null` when it is behind the camera. +: Projects `position` to a point on screen with the current camera, for anchoring HTML overlays. + Returns `null` when the position is behind the camera. {#view-position .member} **`position`**`(point:` [`Point`](#point)`):` [`Position`](#position) `| null` [¶](#view-position){.headerlink} -: The position on the ground under screen point `point`: at elevation 0 on a plane, on the map or - globe on the earth, the values there on a plot. `null` where the point looks past the ground. +: Returns the position on the ground under screen point `point`: at elevation 0 on a plane, on + the map or globe on the earth, and the values there on a plot. Returns `null` where the point + looks past the ground. {#view-screenof .member} **`screenOf`**`(row:` [`Row`](#row)`, wait?:` [`Wait`](#wait)`): Promise<`[`Point`](#point) `| null>` [¶](#view-screenof){.headerlink} -: Resolves to where `row` is on screen once a frame draws the view, or `null` when no mark draws - it in view. A diagram's vertices resolve at once. +: Resolves to where `row` is on screen once a frame draws the view, or to `null` when no mark + draws it in view. A diagram's vertices resolve at once. {#view-pick .member} **`pick`**`(point:` [`Point`](#point)`, options?: { radius?: number } &` [`Wait`](#wait)`): Promise<`[`Hit`](#hit)`[]>` [¶](#view-pick){.headerlink} @@ -1234,18 +1243,18 @@ const view = await View.create(canvas, { {#view-pick-radius .member} **`radius`**`?: number` [¶](#view-pick-radius){.headerlink} - : How near, in CSS pixels; 0 by default. + : How near, in CSS pixels. Defaults to 0. {#view-neighbors .member} **`neighbors`**`(row:` [`Row`](#row)`):` [`Rows`](#rows)`[]` [¶](#view-neighbors){.headerlink} -: The rows connected to `row`, one set per table: a vertex's neighbors across edges and nets, or - an edge's or a net's vertices. The cost is proportional to the row's degree. +: Returns the rows connected to `row`, one set per table: a vertex's neighbors across edges and + nets, or the vertices of an edge or net. The cost is proportional to the row's degree. {#view-arrange .member} **`arrange`**`(wait?:` [`Wait`](#wait)`): Promise<`[`Patch`](#patch)`[]>` [¶](#view-arrange){.headerlink} -: Lays out every vertex row without a position again, and resolves to one patch per dataset that - writes each row's place into its mark's position fields; applied, the places become part of - the data. Marks whose position is not read from fields are left out. +: Lays out again every vertex row that has no position, and resolves to one patch per dataset. + Each patch writes each row's place into its mark's position fields, so applying it makes the + places part of the data. Marks that don't read their position from fields are left out. ```ts for (const patch of await view.arrange()) if (patch.data === grid.name) grid.apply(patch); @@ -1253,15 +1262,15 @@ const view = await View.create(canvas, { {#view-snapshot .member} **`snapshot`**`(name: string):` [`Snapshot`](#snapshot) `| undefined` [¶](#view-snapshot){.headerlink} -: The version of dataset `name` the view draws, or `undefined` if it draws none of that name: the - latest version of a [`Data`](#data) the view has taken, a given snapshot, or the pieces of [`Tiles`](#tiles) - fetched for its camera. +: Returns the version of dataset `name` that the view draws, or `undefined` if it draws no + dataset by that name. That is the latest version of a [`Data`](#data) the view has taken, the snapshot + it was given, or the pieces of [`Tiles`](#tiles) fetched for its camera. {#view-on .member} **`on`**`(type: T, listener: (event:` [`ViewEvents`](#viewevents)`[T]) => void): () => void` [¶](#view-on){.headerlink} -: Calls `listener` with each event of `type` emitted while it is registered, and returns a - function that removes it. Past events are not replayed; read the current state from the - property a change event is named after. +: Calls `listener` with each event of `type` emitted while it is registered. Returns a function + that removes it. Past events are not replayed. Read the current state from the property that a + change event is named after. (rect)= @@ -1279,8 +1288,8 @@ A region `[x, y, width, height]` in fractions of a canvas or picture, from its t {.signature} `type ViewEvents = { [E in` [`ViewEvent`](#viewevent) `as E['type']]: E extends { type: 'error' } ?` [`LatkitError`](#latkiterror) `: E['detail'] }` -The events a view emits, by type, mapped to each event's detail. Intents (`click`, `open`, -`menu`, `drag`, `connect`, `delete`) are emitted once each, in the order the user acted. Changes +The events a view emits, mapping each type to its detail. Intents (`click`, `open`, `menu`, +`drag`, `connect`, `delete`) are emitted once each, in the order the user acted. Changes (`hovered`, `selection`, `camera`, `waiting`) are named after the property that changed and carry only its latest value. An `error` event's detail is a [`LatkitError`](#latkiterror). Setters emit nothing, so events report only what the application did not do itself. @@ -1320,68 +1329,69 @@ and latitude; on a plot, the axis coordinate and the value. {.signature} `class Gpu extends` [`Handle`](#handle) -A WebGPU device that views draw with. The views of one `Gpu` share its memory and draw together -in each animation frame, so linked views always show the same moment. Views use the first `Gpu` -created unless given another, and [`View.create`](#view-create) creates it when there is none. Destroying it -destroys its views and frame listeners; other GPUs and datasets stay usable. +A WebGPU device that views draw with. Views on one `Gpu` share its memory and draw together in +each animation frame, so linked views always show the same moment. Views use the first `Gpu` +created unless given another, and [`View.create`](#view-create) creates one if none exists. Destroying a `Gpu` +destroys its views and frame listeners. Other GPUs and datasets stay usable. ```{rubric} Properties ``` {#gpu-lost .member} `readonly` **`lost`**`: boolean` [¶](#gpu-lost){.headerlink} -: Whether the device was lost. Drawing then does nothing; create a new [`Gpu`](#gpu). +: Whether the device was lost. Drawing then does nothing, so create a new [`Gpu`](#gpu). {#gpu-budget .member} **`budget`**`:` [`Budget`](#budget) [¶](#gpu-budget){.headerlink} -: The GPU memory budget for data, shared by every view of this GPU; see [`Budget`](#budget). Defaults - to half the device's largest storage buffer, and 32 MiB uploaded per frame. +: The GPU memory budget for data, shared by every view on this GPU. See [`Budget`](#budget). Defaults + to half the device's largest storage buffer, with 32 MiB uploaded per frame. {#gpu-work .member} `readonly` **`work`**`:` [`Work`](#work) [¶](#gpu-work){.headerlink} -: Exact counts of the work this GPU has done since it was created; see [`Work`](#work). The counts - repeat for the same budget, device limits, and frames, so tests and benchmarks can assert them. +: Exact counts of the work this GPU has done since it was created. See [`Work`](#work). The same + budget, device limits, and frames give the same counts, so tests and benchmarks can assert + them. {#gpu-resident .member} `readonly` **`resident`**`: number` [¶](#gpu-resident){.headerlink} -: Bytes of GPU memory its views draw from: data pages, and each view's per-row data. +: Bytes of GPU memory the views draw from: data pages and each view's per-row data. {#gpu-uploading .member} `readonly` **`uploading`**`: number` [¶](#gpu-uploading){.headerlink} -: Bytes waiting to upload to the GPU, across its views. +: Bytes waiting to upload to the GPU, across all its views. ```{rubric} Methods ``` {#gpu-create .member} `static` **`create`**`(): Promise` [¶](#gpu-create){.headerlink} -: Requests a WebGPU device; the first call takes the one [`load()`](#load) requested. Create one before - any view to set its `budget` first, or another for views that should not share memory with the - rest. Rejects with an `unsupported` error when the browser has no WebGPU device. +: Requests a WebGPU device. The first call takes the device [`load()`](#load) requested. Create one before + any view to set its `budget` first, or create another for views that should not share memory + with the rest. Rejects with an `unsupported` error when the browser has no WebGPU device. {#gpu-on .member} **`on`**`(type: 'frame', listener: (now: number) => void): () => void` [¶](#gpu-on){.headerlink} -: Calls `listener` every animation frame with the frame's timestamp in milliseconds, before the - views draw, so changes it makes show in that frame. While any listener is registered, a frame - runs every animation frame. Returns a function that removes the listener. +: Calls `listener` every animation frame with the frame's timestamp in milliseconds. It runs + before the views draw, so its changes show in that frame. While any listener is registered, a + frame runs on every animation frame. Returns a function that removes the listener. {#gpu-step .member} **`step`**`(now: number): void` [¶](#gpu-step){.headerlink} -: Draws one frame now, at `now` milliseconds on the `requestAnimationFrame` clock: it runs the - frame listeners, then draws every canvas that needs a frame. After the first call, the GPU - requests no more animation frames, so a test or benchmark that steps every frame draws the +: Draws one frame now, at `now` milliseconds on the `requestAnimationFrame` clock. It runs the + frame listeners, then draws every canvas that needs a frame. After the first call the GPU + stops requesting animation frames, so a test or benchmark that steps every frame draws the same frames on any device. {#gpu-done .member} **`done`**`(wait?:` [`Wait`](#wait)`): Promise` [¶](#gpu-done){.headerlink} : Resolves once the GPU has finished its submitted work, every readback has arrived, and every - pipeline a view or shot requested has compiled. A frame stepped after that uses all of it. + pipeline a view or shot requested has compiled. A frame stepped after that uses all of them. {#gpu-landed .member} **`landed`**`(wait?:` [`Wait`](#wait)`): Promise` [¶](#gpu-landed){.headerlink} -: Resolves when, after this call, work a view is waiting on arrives (a readback, a pipeline - compile, a job, a label text, or a fetch), or a view requests a frame. A harness that steps - frames awaits it to know when to step again. +: Resolves when work a view waits on arrives after this call (a readback, a pipeline compile, a + job, a label text, or a fetch), or when a view requests a frame. A harness that steps frames + awaits it to know when to step again. ## Pictures and video @@ -1394,9 +1404,9 @@ Shots draw views into images and video. {.signature} `class Shot` -A picture of views, `size` pixels in all, each view drawn in its region. While a shot draws a -view, its canvas keeps its last frame. An image draws each view's data as it was when the -picture began. `Shot.of(view)` makes a shot of what a view's canvas shows. +A picture of views, `size` pixels in all, with each view drawn in its region. While a shot draws +a view, the view's canvas keeps its last frame. An image draws each view's data as it was when +the picture began. `Shot.of(view)` makes a shot of what a view's canvas shows. ```ts const png = await (await Shot.of(view, { scale: 2 }).image()).encode('png'); @@ -1411,8 +1421,8 @@ const png = await (await Shot.of(view, { scale: 2 }).image()).encode('png'); {#shot-new-views .member} **`views`**`?: readonly` [`Placed`](#placed)`[]` [¶](#shot-new-views){.headerlink} - : The views drawn, later views over earlier ones: each in its own region, or, given as - `{ view, region }`, in that region of the picture. + : The views drawn, later views over earlier ones. Each draws in its own region, or in + `region` when given as `{ view, region }`. ```{rubric} Properties ``` @@ -1430,9 +1440,9 @@ const png = await (await Shot.of(view, { scale: 2 }).image()).encode('png'); {#shot-of .member} `static` **`of`**`(view:` [`View`](#view)`, options?:` [`ShotOptions`](#shotoptions)`): Shot` [¶](#shot-of){.headerlink} -: A shot of exactly what the canvas showing `view` shows: its views in their regions, at the - canvas's CSS size times `options.scale` in pixels. Throws an `invalid` error for a view on no - canvas, which a shot must be given a size for. +: Returns a shot of exactly what the canvas showing `view` shows: its views in their regions, at + the canvas's CSS size times `options.scale` in pixels. Throws an `invalid` error for a view on + no canvas. Give such a shot a size with the constructor. {#shot-image .member} **`image`**`(wait?:` [`Wait`](#wait)`): Promise<`[`Image`](#image)`>` [¶](#shot-image){.headerlink} @@ -1444,7 +1454,7 @@ const png = await (await Shot.of(view, { scale: 2 }).image()).encode('png'); : Starts a video of this shot: H.264 in MP4, written to `options.output` as frames are drawn. The width is rounded down to a multiple of 4 and the height to an even number, as the encoder requires. Canvases showing the views keep their last frame until the video finishes or aborts. - Aborting `options.signal` aborts the video and its output; frames have no other time limit. + Aborting `options.signal` aborts the video and its output. Frames have no other time limit. {#shot-video-fps .member} **`fps`**`: number` [¶](#shot-video-fps){.headerlink} @@ -1456,7 +1466,7 @@ const png = await (await Shot.of(view, { scale: 2 }).image()).encode('png'); {#shot-video-bitrate .member} **`bitrate`**`?: number` [¶](#shot-video-bitrate){.headerlink} - : Bits per second; about 0.15 bits per pixel per frame by default. + : Bits per second. Defaults to about 0.15 bits per pixel per frame. (shotoptions)= @@ -1469,7 +1479,7 @@ Options for a [`Shot`](#shot). {#shotoptions-scale .member} **`scale`**`?: number` [¶](#shotoptions-scale){.headerlink} -: Pixels per CSS pixel, 1 by default. Views lay out at `size / scale` CSS pixels, so 2 draws the +: Pixels per CSS pixel. Defaults to 1. Views lay out at `size / scale` CSS pixels, so 2 draws the same layout twice as sharp. {#shotoptions-transparent .member} @@ -1484,8 +1494,8 @@ Options for a [`Shot`](#shot). {#shotoptions-elapsed .member} **`elapsed`**`?: number` [¶](#shotoptions-elapsed){.headerlink} -: The time in seconds that [`elapsed()`](#elapsed) expressions and flowing comets show in a still picture. By - default they follow the view's clock. +: The time in seconds that [`elapsed()`](#elapsed) expressions and flowing comets show in a still picture. + Defaults to the view's clock. (placed)= @@ -1502,7 +1512,7 @@ A view drawn into a picture, in its own region or in `region`. {#placed-region .member} `readonly` **`region`**`:` [`Rect`](#rect) [¶](#placed-region){.headerlink} -: Where it draws in the picture, in place of its own region. +: Where it draws in the picture, overriding its own region. (image)= @@ -1538,7 +1548,7 @@ A picture's RGBA pixels, with straight alpha, rows top to bottom. {#image-encode-quality .member} **`quality`**`?: number` [¶](#image-encode-quality){.headerlink} - : JPEG quality, from 1 (smallest) to 100 (sharpest); 90 by default. + : JPEG quality, from 1 (smallest) to 100 (sharpest). Defaults to 90. (imageformat)= @@ -1556,7 +1566,7 @@ An image file format for [`Image.encode()`](#image-encode). {.signature} `class Video` -A video being drawn, encoded, and written; created by [`Shot.video()`](#shot-video). Draw frames with +A video being drawn, encoded, and written. Create one with [`Shot.video()`](#shot-video). Draw frames with [`Video.frame()`](#video-frame) or [`Video.record()`](#video-record), then call [`Video.finish()`](#video-finish). ```{rubric} Properties @@ -1576,9 +1586,9 @@ A video being drawn, encoded, and written; created by [`Shot.video()`](#shot-vid {#video-frame .member} **`frame`**`(): Promise` [¶](#video-frame){.headerlink} -: Draws the next frame once its views are complete, and writes its bytes, waiting on the - output's backpressure. Calls must not overlap: a call made before the previous one resolves, - or after `finish()`, rejects with an `invalid` error, and a call after `abort()` with a +: Draws the next frame once its views are complete, then writes its bytes, waiting on the + output's backpressure. Calls must not overlap. A call made before the previous one resolves, + or after `finish()`, rejects with an `invalid` error. A call after `abort()` rejects with a `cancelled` error. A failure aborts the video. {#video-record .member} diff --git a/docs/reference/cli.json b/docs/reference/cli.json index ad981de8..c5aa9e89 100644 --- a/docs/reference/cli.json +++ b/docs/reference/cli.json @@ -96,7 +96,7 @@ "required": false, "many": false, "global": false, - "help": "Output format. Defaults to the one the output's extension names, else PNG", + "help": "Output format. Defaults to the format of the output's extension, or PNG", "defaults": [], "values": [ { @@ -400,7 +400,7 @@ "required": false, "many": false, "global": false, - "help": "Output format. Defaults to the one the output's extension names, else PNG", + "help": "Output format. Defaults to the format of the output's extension, or PNG", "defaults": [], "values": [ { @@ -773,7 +773,7 @@ { "name": "latkit tile", "usage": "latkit tile [OPTIONS] --output <[NAME=]FILE>", - "about": "Write a dataset as time tiles: `frames.json` (which `--tiles` opens), the dataset without its sampled fields' histories, and those histories in spans at every level of detail", + "about": "Write a dataset as time tiles: `frames.json` (which `--tiles` opens), the dataset without the histories of its sampled fields, and those histories in spans at every level of detail", "args": [ { "id": "data", diff --git a/docs/reference/cli.md b/docs/reference/cli.md index d6827e38..0b21dfa0 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1,6 +1,6 @@ # Command reference -Every command and option of the `latkit` command line, written from its help. The [command-line guide](../cli.md) shows them at work. `--help` prints a command's help in the terminal, and `latkit --version` prints the version. +Every command and option of the `latkit` command line, generated from its help. The [command-line guide](../cli.md) shows them in use. `--help` prints a command's help, and `latkit --version` prints the version. (cli-latkit)= @@ -36,7 +36,7 @@ Render scenes of JSON or binary data to images and videos. {.member} [`latkit tile`](#cli-tile) -: Write a dataset as time tiles: `frames.json` (which [`--tiles`](#cli-render-tiles) opens), the dataset without its sampled fields' histories, and those histories in spans at every level of detail. +: Write a dataset as time tiles: `frames.json` (which [`--tiles`](#cli-render-tiles) opens), the dataset without the histories of its sampled fields, and those histories in spans at every level of detail. ```{rubric} Options ``` @@ -86,7 +86,7 @@ Draw scenes of datasets into an image, image sequence, raw frames, or a video. {#cli-render-format .member} **`--format`** `` [¶](#cli-render-format){.headerlink} -: Output format. Defaults to the one the output's extension names, else PNG. +: Output format. Defaults to the format of the output's extension, or PNG. {#cli-render-format-png .member} **`png`** [¶](#cli-render-format-png){.headerlink} @@ -284,7 +284,7 @@ Print the rows a query selects, sorted and paged, as JSON Lines with one object {.signature} `latkit tile [OPTIONS] --output <[NAME=]FILE>` -Write a dataset as time tiles: `frames.json` (which [`--tiles`](#cli-render-tiles) opens), the dataset without its sampled fields' histories, and those histories in spans at every level of detail. +Write a dataset as time tiles: `frames.json` (which [`--tiles`](#cli-render-tiles) opens), the dataset without the histories of its sampled fields, and those histories in spans at every level of detail. ```{rubric} Arguments ``` diff --git a/docs/reference/types.md b/docs/reference/types.md index 6e6aad07..7eba473b 100644 --- a/docs/reference/types.md +++ b/docs/reference/types.md @@ -1,7 +1,7 @@ # Scene and result types -The types of the scenes latkit draws and of what it reports, written from the same schema as the -package's types. Each property gives its meaning and default. +The types of the scenes latkit draws and the results it reports, generated from the same schema as +the package's types. Each property gives its meaning and default. ## Scenes @@ -30,12 +30,12 @@ its dataset and table: `{ "vertices": ["grid", "Bus"], "radius": 4 }`. {#scene-layout .member} **`layout`**`?:` [`Layout`](#layout) [¶](#scene-layout){.headerlink} -: How the view places vertex rows that have no position: a diagram's blocks, or points on a plane - or the earth. `none` by default. +: How the view places vertex rows that have no position, such as a diagram's blocks or points on a + plane or the earth. Defaults to `none`. {#scene-theme .member} **`theme`**`?:` [`Theme`](#theme) [¶](#scene-theme){.headerlink} -: Default colors and sizes, and the look of text, focus, and each space; `dark` by default. +: Default colors and sizes, and the look of text, focus, and each space. Defaults to `dark`. {#scene-controls .member} **`controls`**`?:` [`Controls`](#controls) [¶](#scene-controls){.headerlink} @@ -43,7 +43,7 @@ its dataset and table: `{ "vertices": ["grid", "Bus"], "radius": 4 }`. {#scene-ease .member} **`ease`**`?: number` [¶](#scene-ease){.headerlink} -: Seconds over which scene and data changes animate; 0.3 by default, 0 for none. +: How many seconds scene and data changes take to animate. 0 turns animation off. Defaults to 0.3. {#scene-title .member} **`title`**`?: string | null` [¶](#scene-title){.headerlink} @@ -63,9 +63,9 @@ its dataset and table: `{ "vertices": ["grid", "Bus"], "radius": 4 }`. The coordinate space of a scene's positions: a name, or `{ plane | diagram | earth | plot: options }`. -The space sets the unit of lengths that do not name their own: units in a diagram, where a unit -is a CSS pixel at zoom 0, and CSS pixels elsewhere. Only a diagram has ports, groups, nets, and -orthogonal routes. +The space sets the unit of lengths that do not name their own. In a diagram it is the diagram +unit, one CSS pixel at zoom 0. Elsewhere it is the CSS pixel. Only a diagram has ports, groups, +nets, and orthogonal routes. {#space-earth .member} `{` **`earth`**`:` [`Earth`](#earth) `}` [¶](#space-earth){.headerlink} @@ -73,7 +73,7 @@ orthogonal routes. {#space-plane .member} `{` **`plane`**`:` [`Plane`](#plane) `}` [¶](#space-plane){.headerlink} -: Plane units, with elevation; the camera can turn and tilt. +: Plane units, with elevation. The camera can turn and tilt. {#space-plot .member} `{` **`plot`**`:` [`Plot`](#plot) `}` [¶](#space-plot){.headerlink} @@ -94,7 +94,7 @@ A plane: positions are in plane units. {#plane-grid .member} **`grid`**`?:` [`Grid`](#grid) [¶](#plane-grid){.headerlink} -: The grid drawn under the marks; lines 10 units apart by default. +: The grid drawn under the marks. Defaults to lines 10 units apart. (earth)= @@ -103,33 +103,33 @@ A plane: positions are in plane units. {.signature} `interface Earth` -The earth: positions are longitude and latitude in degrees. The ground takes the theme's `earth` +The earth: positions are longitude and latitude in degrees. The ground uses the theme's `earth` colors. -A flat map is one world, drawn once. Its ground ends at the antimeridian and at Web Mercator's -latitude limit, and the camera zooms out until the whole world fits the view and pans until the -world's edges meet the view's. On a globe, longitude wraps, and the camera zooms out until the -globe spans a third of the view's shorter side. +A flat map shows the world once. Its ground ends at the antimeridian and at Web Mercator's +latitude limit. The camera zooms out until the whole world fits the view, and pans until the +world's edges meet the view's edges. On a globe, longitude wraps, and the camera zooms out until +the globe spans a third of the view's shorter side. {#earth-projection .member} **`projection`**`?:` [`Projection`](#projection) [¶](#earth-projection){.headerlink} -: A flat web mercator map (`mercator`, the default) or a globe (`globe`). +: A flat web mercator map (`mercator`) or a globe (`globe`). Defaults to `mercator`. {#earth-graticule .member} **`graticule`**`?: boolean` [¶](#earth-graticule){.headerlink} -: Whether lines of longitude and latitude cross the ground, in the theme's `grid` color. Default - `true`. +: Whether lines of longitude and latitude cross the ground, in the theme's `grid` color. Defaults + to `true`. {#earth-daylight .member} **`daylight`**`?: number | null` [¶](#earth-daylight){.headerlink} : Lights the earth by the sun's position this many seconds after the Unix epoch, plus the view's axis coordinate when it has one. The night side darkens the ground and everything on it, down to - the theme's `earth.night` brightness. Unset, the earth is lit evenly. + the theme's `earth.night` brightness. When unset, the earth is lit evenly. {#earth-atmosphere .member} **`atmosphere`**`?: boolean` [¶](#earth-atmosphere){.headerlink} : Whether air glows around the globe, in the theme's `earth.atmosphere` color, brighter on the day - side. Default `false`. + side. Defaults to `false`. (projection)= @@ -155,16 +155,16 @@ How the earth is projected. {.signature} `interface Diagram` -A flat sheet for diagrams, seen from straight above; the camera pans and zooms but does not -turn. Positions are in units with y down. Lengths are in units too, a unit being a CSS pixel at -zoom 0: sizes, padding, ports, strokes, text, and the theme's lengths. Sizes given with `px`, -the text halo, the focus glow, and the pick radius stay in CSS pixels. Zooming scales the whole -picture. Only a diagram has ports, groups, nets, and orthogonal routes, and the scene's layout -places its blocks. +A flat sheet for diagrams, seen from straight above. The camera pans and zooms but does not +turn. Positions are in units with y down, and a unit is a CSS pixel at zoom 0. Sizes, padding, +ports, strokes, text, and the theme's lengths are in units too. Sizes given with `px`, the text +halo, the focus glow, and the pick radius stay in CSS pixels. Zooming scales the whole picture. +Only a diagram has ports, groups, nets, and orthogonal routes, and the scene's layout places its +blocks. {#diagram-grid .member} **`grid`**`?:` [`Grid`](#grid) [¶](#diagram-grid){.headerlink} -: The grid drawn under the marks; dots 20 units apart by default. +: The grid drawn under the marks. Defaults to dots 20 units apart. (plot)= @@ -198,25 +198,25 @@ A plot axis. {#axis-grid .member} **`grid`**`?: boolean` [¶](#axis-grid){.headerlink} -: Whether grid lines cross the plot at the ticks. Default `true`. +: Whether grid lines cross the plot at the ticks. Defaults to `true`. {#axis-hidden .member} **`hidden`**`?: boolean` [¶](#axis-hidden){.headerlink} -: Whether the axis is hidden. Default `false`. +: Whether the axis is hidden. Defaults to `false`. {#axis-ticks .member} **`ticks`**`?:` [`Tick`](#tick)`[] | null` [¶](#axis-ticks){.headerlink} -: Tick positions, each a value or `[value, label]`; a tick without a label shows its value. Unset, - ticks fall at round values. +: Tick positions, each a value or `[value, label]`. A tick without a label shows its value. When + unset, ticks fall at round values. {#axis-format .member} **`format`**`?:` [`Format`](#format) [¶](#axis-format){.headerlink} -: How tick values are formatted; `auto` by default. +: How tick values are formatted. Defaults to `auto`. {#axis-precision .member} **`precision`**`?: number | null` [¶](#axis-precision){.headerlink} -: Decimal places of fixed values, or of the significand in scientific and engineering formats, at - most 15. Unset, as many as the tick step needs. +: Decimal places of fixed values, or of the significand in scientific and engineering formats. At + most 15. When unset, as many as the tick step needs. (tick)= @@ -275,7 +275,7 @@ A plane's or a diagram's grid: `"off"`, `{ lines: { pitch } }`, or `{ dots: { pi {#grid-dots .member} `{` **`dots`**`: { pitch: number; snap?: boolean } }` [¶](#grid-dots){.headerlink} -: Dots `pitch` units apart; with `snap`, a diagram's drags snap to them. +: Dots `pitch` units apart. With `snap`, drags in a diagram snap to them. {#grid-dots-pitch .member} **`pitch`**`: number` [¶](#grid-dots-pitch){.headerlink} @@ -283,7 +283,7 @@ A plane's or a diagram's grid: `"off"`, `{ lines: { pitch } }`, or `{ dots: { pi {#grid-dots-snap .member} **`snap`**`?: boolean` [¶](#grid-dots-snap){.headerlink} - : Whether drag positions snap to the dots, in a diagram. Default `false`. + : Whether drag positions snap to the dots, in a diagram. Defaults to `false`. (controls)= @@ -293,70 +293,72 @@ A plane's or a diagram's grid: `"off"`, `{ lines: { pitch } }`, or `{ dots: { pi `type Controls = 'none' | 'inspect' | { … }` How a view responds to the pointer and keyboard, and how much it animates on its own: a preset -(`"none"` or `"inspect"`), or options over the defaults. Each mark controls whether its own rows -pick, select, and drag; these options control the view itself. +(`"none"` or `"inspect"`), or options that change the defaults. Each mark sets whether its own +rows pick, select, and drag. These options control the view itself. {#controls-pan .member} **`pan`**`?: boolean` [¶](#controls-pan){.headerlink} -: Whether dragging the background pans the camera. Default `true`. +: Whether dragging the background pans the camera. Defaults to `true`. {#controls-zoom .member} **`zoom`**`?:` [`Zoom`](#zoom) [¶](#controls-zoom){.headerlink} -: Which input zooms the view: the wheel and pinching (`wheel`, the default); pinching, and the - wheel only with Control or Meta held, as trackpad pinches send it, so the plain wheel scrolls - the page (`modifier`); or nothing (`off`). +: Which input zooms the view. `wheel` zooms with the wheel and pinching. `modifier` zooms with + pinching, and with the wheel only while Control or Meta is held (as trackpad pinches send it), + so the plain wheel scrolls the page. `off` turns zooming off. Defaults to `wheel`. {#controls-turn .member} **`turn`**`?: boolean` [¶](#controls-turn){.headerlink} : Whether dragging with the secondary button, or with Shift, rotates and tilts a map or plane. - Default `true`. + Defaults to `true`. {#controls-keyboard .member} **`keyboard`**`?: boolean` [¶](#controls-keyboard){.headerlink} -: Whether the keyboard controls the view. Default `true`. Tab and Shift+Tab step through the rows - nearest the view's center, and arrow keys step to the nearest row in their direction. Enter - opens the stepped-to row and Space selects it. With no row stepped to, arrow keys pan +: Whether the keyboard controls the view. Defaults to `true`. Tab and Shift+Tab step through the + rows nearest the view's center. Arrow keys step to the nearest row in their direction. Enter + opens the row stepped to, and Space selects it. With no row stepped to, arrow keys pan (Shift+arrows turn), `+` and `-` zoom, Home fits everything, and Escape clears the selection. {#controls-hover .member} **`hover`**`?: boolean` [¶](#controls-hover){.headerlink} -: Whether the hovered row glows, and a hovered vertex sized in CSS pixels grows. Default `true`. - Rows are hovered either way: a `'hovered'` event still fires and `hovered` still reads them. +: Whether the hovered row glows and a hovered vertex sized in CSS pixels grows. Defaults to + `true`. Hover is tracked either way: a `'hovered'` event still fires and `hovered` still returns + the row. {#controls-pickradius .member} **`pickRadius`**`?: number` [¶](#controls-pickradius){.headerlink} -: How far from the pointer, in CSS pixels, a row can be and still be picked; touch adds 8. Default - 6. +: How far a row can be from the pointer and still be picked, in CSS pixels. Touch adds 8. Defaults + to 6. {#controls-selectbox .member} **`selectBox`**`?: boolean` [¶](#controls-selectbox){.headerlink} : Whether Shift+drag selects the rows inside a box instead of turning the camera. Shift+click - still toggles one row. Default `false`. + still toggles one row. Defaults to `false`. {#controls-connect .member} **`connect`**`?: boolean` [¶](#controls-connect){.headerlink} -: Whether the user can draw connections, reported as a `'connect'` event: by dragging from a port, - from a vertex with Alt held, or an edge's end off its vertex. Default `false`. +: Whether the user can draw connections by dragging from a port, from a vertex with Alt held, or + an edge's end off its vertex. Connections are reported as a `'connect'` event. Defaults to + `false`. {#controls-delete .member} **`delete`**`?: boolean` [¶](#controls-delete){.headerlink} -: Whether Delete and Backspace report the selection as a `'delete'` event. Default `false`. +: Whether Delete and Backspace report the selection as a `'delete'` event. Defaults to `false`. {#controls-cycle .member} **`cycle`**`?: boolean` [¶](#controls-cycle){.headerlink} : Whether clicking again at the same point, with the camera unmoved, selects the next row under - it, nearest first, so overlapping rows can each be selected. Default `false`. + the pointer, nearest first. This lets overlapping rows each be selected. Defaults to `false`. {#controls-press .member} **`press`**`?: number` [¶](#controls-press){.headerlink} -: Seconds a touch or pen must hold still to request a menu (a `'menu'` event), as the secondary - button does; 0 turns this off. Default 0.5. +: How many seconds a touch or pen must hold still to request a menu (a `'menu'` event), like the + secondary button. 0 turns this off. Defaults to 0.5. {#controls-motion .member} **`motion`**`?:` [`Motion`](#motion) [¶](#controls-motion){.headerlink} : How much the view animates on its own: eases, camera flights, coasting, orbits, comets, and - options that read `elapsed`. `system` (the default) follows the user's reduced-motion setting, - `full` animates everything, and `reduced` animates only motion the user drives. + options that read `elapsed`. `system` follows the user's reduced-motion setting, `full` animates + everything, and `reduced` animates only motion the user drives. Defaults to `system`. (motion)= @@ -369,7 +371,7 @@ How much a view animates on its own. {#motion-system .member} **`'system'`** [¶](#motion-system){.headerlink} -: Follows the system: reduced when the user asked their system for less motion. +: Follows the system setting: reduced when the user has asked their system for less motion. {#motion-full .member} **`'full'`** [¶](#motion-full){.headerlink} @@ -410,12 +412,12 @@ Which input zooms a view. `type Layout = 'none' | 'stress' | 'layered' | { stress:` [`Stress`](#stress) `} | { layered:` [`Layered`](#layered) `}` How the view places vertex rows that have no position: a name, or -`{ stress | layered: options }`. In a diagram it places blocks; on a plane or the earth it +`{ stress | layered: options }`. In a diagram it places blocks. On a plane or the earth it places points among the rows they link to. Gaps are in position units: diagram units, plane units, or degrees on the earth. -A row keeps its place once placed: rows added later are placed around the others, and -[`view.arrange`](#view-arrange) lays out every row again. +A placed row keeps its place. Rows added later are placed around the others, and [`view.arrange`](#view-arrange) +lays out every row again. {#layout-stress .member} `{` **`stress`**`:` [`Stress`](#stress) `}` [¶](#layout-stress){.headerlink} @@ -437,15 +439,15 @@ outputs. {#layered-direction .member} **`direction`**`?:` [`Direction`](#direction) [¶](#layered-direction){.headerlink} -: The direction edges point; `right` by default. +: The direction edges point. Defaults to `right`. {#layered-gap .member} **`gap`**`?: number` [¶](#layered-gap){.headerlink} -: Distance between vertices within a rank, in position units. Default 30. +: Distance between vertices within a rank, in position units. Defaults to 30. {#layered-rankgap .member} **`rankGap`**`?: number` [¶](#layered-rankgap){.headerlink} -: Distance between ranks, in position units. Default 80. +: Distance between ranks, in position units. Defaults to 80. (stress)= @@ -454,12 +456,12 @@ outputs. {.signature} `interface Stress` -Stress majorization: vertices move until drawn distances match graph distances, then overlapping +Stress majorization: vertices move until drawn distances match graph distances. Then overlapping boxes separate, keeping a quarter of `gap` between them. {#stress-gap .member} **`gap`**`?: number` [¶](#stress-gap){.headerlink} -: Distance between the centers of linked vertices, in position units. Default 40. +: Distance between the centers of linked vertices, in position units. Defaults to 40. (direction)= @@ -515,18 +517,20 @@ How much GPU memory latkit uses. {#budget-bytes .member} **`bytes`**`: number` [¶](#budget-bytes){.headerlink} : GPU memory in bytes for the data all views of the [`Gpu`](#gpu) draw: its pages and each view's per-row - state. A view draws a table too large for its share as a paged table, drawing only the pages the - camera needs, so it always fits. Pages no view draws stay cached in the remaining room, least - recently drawn evicted first. Defaults to half the device's largest storage buffer. The page - pool grows as data needs it and never shrinks, so set the budget before drawing data. + state. A table too large for a view's share draws as a paged table, with only the pages the + camera needs, so it always fits. Pages no view draws stay cached in the rest of the budget, and + the least recently drawn are evicted first. Defaults to half the device's largest storage + buffer. The page pool grows as data needs it and never shrinks, so set the budget before drawing + data. {#budget-uploadperframe .member} **`uploadPerFrame`**`: number` [¶](#budget-uploadperframe){.headerlink} -: Most bytes uploaded per frame; larger data streams in over several frames. Defaults to 32 MiB. +: The most bytes uploaded per frame. Larger data streams in over several frames. Defaults to 32 + MiB. ## Themes -How a scene looks: a group for each kind of thing it draws, and the lengths its options take. +How a scene looks: a group of options for each kind of thing it draws, and the lengths those options take. (theme)= @@ -535,13 +539,12 @@ How a scene looks: a group for each kind of thing it draws, and the lengths its {.signature} `type Theme = 'dark' | 'light' | { … }` -Default colors and sizes for marks whose options leave them unset or missing, and the look of -text, focus, comets, and each space, grouped by subject. Write a base theme's name (`"dark"` or -`"light"`), or options over a base (`base`, dark by default); a group's options override only -the base values they set. +Default colors and sizes for marks, and the look of text, focus, comets, and each space, grouped +by subject: a base theme's name (`"dark"` or `"light"`), or options that change a base. `base` +names the base and defaults to `dark`. Each option replaces only the base value it sets. -Lengths are in the space's unit (units in a diagram, CSS pixels elsewhere), except the text halo -and the focus glow, which are always in CSS pixels. +Lengths are in CSS pixels, or units in a diagram. The text halo and the focus glow are always in +CSS pixels. {#theme-background .member} **`background`**`?:` [`Color`](#color) [¶](#theme-background){.headerlink} @@ -553,7 +556,7 @@ and the focus glow, which are always in CSS pixels. {#theme-missing .member} **`missing`**`?:` [`Color`](#color) [¶](#theme-missing){.headerlink} -: The color a colormap gives a missing value, such as a row whose field has none. +: The color a colormap gives a missing value, such as a row with no value in the field. {#theme-dash .member} **`dash`**`?: number` [¶](#theme-dash){.headerlink} @@ -605,7 +608,7 @@ and the focus glow, which are always in CSS pixels. {#theme-base .member} **`base`**`?: 'dark' | 'light'` [¶](#theme-base){.headerlink} -: The theme these options change; dark unless set. +: The theme these options change. Defaults to `dark`. (texttheme)= @@ -660,8 +663,8 @@ Defaults for edges, and how parallel edges and arrowheads look. {#edgetheme-color .member} **`color`**`?:` [`Color`](#color) `| null` [¶](#edgetheme-color){.headerlink} -: The edge color. `null` blends the colors of each edge's two vertices; a diagram's wires and - edges without `ends` (nets and poles) then take the text color. +: The edge color. `null` blends the colors of each edge's two vertices. With `null`, a diagram's + wires and edges without `ends` (nets and poles) use the text color. {#edgetheme-width .member} **`width`**`?: number` [¶](#edgetheme-width){.headerlink} @@ -674,13 +677,13 @@ Defaults for edges, and how parallel edges and arrowheads look. {#edgetheme-bow .member} **`bow`**`?: number` [¶](#edgetheme-bow){.headerlink} -: Outside a diagram, how far edges joining the same two vertices bow apart: each lane by this - fraction of the edge's length at its middle. A family spreads at most eight lanes wide, and more - lanes pack closer. A bow is part of the drawing, so it scales with the zoom. +: Outside a diagram, how far edges joining the same two vertices bow apart. Each lane bows out by + this fraction of the edge's length, measured at its middle. Parallel edges spread at most eight + lanes wide, and more lanes pack closer. Bows scale with the zoom. {#edgetheme-arrow .member} **`arrow`**`?: number` [¶](#edgetheme-arrow){.headerlink} -: How far an arrowhead extends beyond the edge's width: a head is this plus the width long. +: How far an arrowhead extends beyond the edge's width. A head's length is this plus the width. (linetheme)= @@ -719,17 +722,17 @@ Defaults for areas. {.signature} `interface FlowTheme` -How comets flowing along edges and lines look: a round head, brightest at its middle, trailing a +How comets along edges and lines look. A comet has a round head, brightest at its middle, and a tail that thins and fades. -`spacing` and `length` must be in the same unit, which also measures each row's `flow` speed. In -position units (degrees on the earth), comets are part of the drawing: they scale with the zoom -and stay in place along their lines. In CSS pixels, they keep their on-screen size and speed. A -plain number is in the space's unit. +`spacing` and `length` must use the same unit, and each row's `flow` speed is in that unit too. +In position units (degrees on the earth), comets scale with the zoom and stay in place along +their lines. In CSS pixels, they keep their size and speed on screen. A plain number is in the +space's unit. {#flowtheme-color .member} **`color`**`?:` [`Color`](#color) [¶](#flowtheme-color){.headerlink} -: The comet color. Its alpha sets how strongly a comet covers its stroke; a fully transparent +: The comet color. Its alpha sets how strongly a comet covers its stroke. A fully transparent color turns comets off. {#flowtheme-spacing .member} @@ -742,7 +745,7 @@ plain number is in the space's unit. {#flowtheme-width .member} **`width`**`?: number` [¶](#flowtheme-width){.headerlink} -: The width of a comet's head, in the space's unit; never narrower than its stroke. +: The width of a comet's head, in the space's unit. A head is never narrower than its stroke. (focustheme)= @@ -759,7 +762,7 @@ How hovered and selected rows glow, and how rows outside a selection dim. {#focustheme-selected .member} **`selected`**`?:` [`Color`](#color) `| null` [¶](#focustheme-selected){.headerlink} -: The glow color around selected rows; `null` glows in each row's own color. +: The glow color around selected rows. `null` uses each row's own color. {#focustheme-glow .member} **`glow`**`?: number` [¶](#focustheme-glow){.headerlink} @@ -771,7 +774,7 @@ How hovered and selected rows glow, and how rows outside a selection dim. {#focustheme-grow .member} **`grow`**`?: number` [¶](#focustheme-grow){.headerlink} -: The scale factor for a hovered vertex sized in CSS pixels; vertices sized in units only glow. +: The scale factor for a hovered vertex sized in CSS pixels. Vertices sized in units only glow. (diagramtheme)= @@ -788,11 +791,11 @@ How a diagram's blocks, groups, ports, and wires look. {#diagramtheme-outline .member} **`outline`**`?:` [`Color`](#color) `| null` [¶](#diagramtheme-outline){.headerlink} -: The color of the one-unit border around blocks and groups; `null` draws no border. +: The color of the one-unit border around blocks and groups. `null` draws no border. {#diagramtheme-corner .member} **`corner`**`?: number` [¶](#diagramtheme-corner){.headerlink} -: The corner radius of blocks and groups; a block marker can set its own. +: The corner radius of blocks and groups. A block marker can set its own. {#diagramtheme-portsize .member} **`portSize`**`?: number` [¶](#diagramtheme-portsize){.headerlink} @@ -821,11 +824,11 @@ How a diagram's blocks and groups cast shadows on the sheet, for vertex marks wi {#shadowtheme-color .member} **`color`**`?:` [`Color`](#color) [¶](#shadowtheme-color){.headerlink} -: The color a shadow darkens toward; its alpha sets how dark the shadow is under the block. +: The color a shadow darkens toward. Its alpha sets how dark the shadow is under the block. {#shadowtheme-lift .member} **`lift`**`?: number` [¶](#shadowtheme-lift){.headerlink} -: How far a shadow falls below its block or group; its edge softens over the same distance. +: How far a shadow falls below its block or group. Its edge softens over the same distance. (earththeme)= @@ -868,21 +871,20 @@ How a plot looks. {.signature} `type Length = number | { px: number } | { units: number }` -A length: a number in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in -CSS pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the -view zooms in. +A length: a number in CSS pixels, or units in a diagram. `{ px }` is always CSS pixels. +`{ units }` is position units (degrees on the earth), which grow as the view zooms in. {#length-px .member} `{` **`px`**`: number }` [¶](#length-px){.headerlink} -: CSS pixels, at any zoom. +: CSS pixels, the same at any zoom. {#length-units .member} `{` **`units`**`: number }` [¶](#length-units){.headerlink} -: Position units (degrees on the earth), which grow on screen as the view zooms in. +: Position units (degrees on the earth), which grow as the view zooms in. ## Marks -The marks that draw the tables of a scene. +The marks that draw a scene's tables. (mark)= @@ -891,8 +893,8 @@ The marks that draw the tables of a scene. {.signature} `type Mark =` [`Vertices`](#vertices) `|` [`Edges`](#edges) `|` [`Faces`](#faces) `|` [`Lines`](#lines) `|` [`Areas`](#areas) -One table a scene draws. The key naming the mark's kind holds its dataset and table, beside its -options: `{ vertices: [data, table], radius: 4 }`. +One table a scene draws. The mark's kind is the key that holds its table, next to its options: +`{ vertices: [data, table], radius: 4 }`. (markoptions)= @@ -905,19 +907,18 @@ The options every mark has. {#markoptions-color .member} **`color`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#markoptions-color){.headerlink} -: Each row's color: a CSS color, a value through a colormap, or a conditional choosing between - colors. Unset, rows take the theme's default for the mark; faces instead blend their corners' +: Each row's color: a CSS color, a value through a colormap, or a conditional that picks between + colors. Defaults to the theme's color for the mark. Faces default to a blend of their corners' colors. {#markoptions-visible .member} **`visible`**`?:` [`ExprInput`](#exprinput) [¶](#markoptions-visible){.headerlink} -: Whether each row is drawn and can be picked. Default `true`. +: Whether each row is drawn and can be picked. Defaults to `true`. {#markoptions-priority .member} **`priority`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#markoptions-priority){.headerlink} -: Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, - their labels win where labels would overlap, and they are the last dropped when a table too - large to draw whole is thinned to the tiles in view. +: Each row's priority. It reads only data, not view state. Higher rows draw on top, keep their + labels where labels overlap, and are dropped last when a large table is thinned. {#markoptions-name .member} **`name`**`?: string | null` [¶](#markoptions-name){.headerlink} @@ -925,17 +926,18 @@ The options every mark has. {#markoptions-pick .member} **`pick`**`?: boolean | null` [¶](#markoptions-pick){.headerlink} -: Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces - do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. +: Whether rows respond to hover, clicks, and picks. Defaults to on for vertices, edges, faces, and + plot traces, and off for geometry: areas, and lines along paths such as coasts, borders, and + roads. {#markoptions-select .member} **`select`**`?: boolean` [¶](#markoptions-select){.headerlink} -: Whether clicking a row selects it. Default `true`. +: Whether clicking a row selects it. Defaults to `true`. {#markoptions-drag .member} **`drag`**`?: boolean` [¶](#markoptions-drag){.headerlink} -: Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the - camera. Default `false`. +: Whether pressing and moving a row drags it instead of panning the camera. Drags are reported as + a `'drag'` event. Defaults to `false`. {#markoptions-label .member} **`label`**`?:` [`Label`](#label) `| null` [¶](#markoptions-label){.headerlink} @@ -943,10 +945,10 @@ The options every mark has. {#markoptions-legend .member} **`legend`**`?: string | null` [¶](#markoptions-legend){.headerlink} -: A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap - across its domain, a cell for each category, or a swatch beside each name of a map's - `categories`. `color` must map through a colormap with a fixed domain or named categories; in a - conditional, `otherwise` is searched before the cases. +: A legend of the mark's colors at the view's bottom right, with this title. It shows a ticked + colormap across its domain, a cell for each category, or a swatch for each name in the + colormap's `categories`. `color` must map through a colormap with a fixed domain or named + categories. In a conditional, `otherwise` is searched before the cases. (strokeoptions)= @@ -959,18 +961,18 @@ The options of marks drawn as strokes: edges and lines. {#strokeoptions-width .member} **`width`**`?:` [`Size`](#size) `| null` [¶](#strokeoptions-width){.headerlink} -: Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given - with `px` or `units`. The theme's width unless set. +: Each row's width in CSS pixels, or units in a diagram, unless given with `px` or `units`. + Defaults to the theme's width. {#strokeoptions-dash .member} **`dash`**`?:` [`ExprInput`](#exprinput) [¶](#strokeoptions-dash){.headerlink} -: Whether each row is dashed, with the theme's `dash` period. Default `false`. +: Whether each row is dashed, using the theme's `dash` period. Defaults to `false`. {#strokeoptions-flow .member} **`flow`**`?:` [`ExprInput`](#exprinput) [¶](#strokeoptions-flow){.headerlink} -: The speed of comets flowing along each row, per second, in the unit of the theme's `flow` - lengths. Negative speeds flow backward; 0, the default, draws no comets. When the speed changes, - comets continue from where they are. +: The speed of comets along each row, per second, in the unit of the theme's `flow` lengths. + Negative speeds flow backward. 0 draws no comets. Defaults to 0. When the speed changes, comets + continue from where they are. (vertices)= @@ -987,37 +989,37 @@ Rows drawn as markers: points of a map, nodes of a graph, blocks of a diagram. {#vertices-position .member} **`position`**`?: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] | null` [¶](#vertices-position){.headerlink} -: Each row's x and y. Without a position, the point field that `geometry` names places rows, and - the scene's layout places rows that have neither. +: Each row's x and y. Rows without a position are placed by the point field `geometry` names. The + scene's layout places rows that have neither. {#vertices-geometry .member} **`geometry`**`?: string | null` [¶](#vertices-geometry){.headerlink} -: The point field that places rows when `position` is not set; the field named `geometry` by - default. +: The point field that places rows when `position` is not set. Defaults to the field named + `geometry`. {#vertices-elevation .member} **`elevation`**`?:` [`ExprInput`](#exprinput) [¶](#vertices-elevation){.headerlink} -: Height above the ground; on the earth, a fraction of the earth's radius. Default 0. Not allowed - in a diagram, which is flat. +: Height above the ground. On the earth, a fraction of the earth's radius. Defaults to 0. Not + allowed in a diagram, which is flat. {#vertices-radius .member} **`radius`**`?:` [`Size`](#size) `| null` [¶](#vertices-radius){.headerlink} -: Each row's radius, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given - with `px` or `units`; a diagram refuses `px`. The theme's `vertex.radius` unless set. +: Each row's radius in CSS pixels, or units in a diagram, unless given with `px` or `units`. A + diagram does not accept `px`. Defaults to the theme's `vertex.radius`. {#vertices-size .member} **`size`**`?: [`[`Size`](#size)`,` [`Size`](#size)`] | null` [¶](#vertices-size){.headerlink} -: Each row's width and height, for markers that are not round, in the space's unit unless given - with `px` or `units`; a diagram refuses `px`. In a diagram, a block without a size fits its - label and ports. +: Each row's width and height, for markers that are not round. In CSS pixels, or units in a + diagram, unless given with `px` or `units`. A diagram does not accept `px`. In a diagram, a + block without a size fits its label and ports. {#vertices-marker .member} **`marker`**`?:` [`Marker`](#marker) [¶](#vertices-marker){.headerlink} -: How each row is drawn; an ellipse by default. +: How each row is drawn. Defaults to an ellipse. {#vertices-ring .member} **`ring`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#vertices-ring){.headerlink} -: A ring around each row's marker in this color; a missing or transparent color draws none. +: A ring around each row's marker in this color. A missing or transparent color draws no ring. {#vertices-ports .member} **`ports`**`?: [reference: string, port:` [`Port`](#port)`][]` [¶](#vertices-ports){.headerlink} @@ -1032,7 +1034,7 @@ Rows drawn as markers: points of a map, nodes of a graph, blocks of a diagram. {#vertices-shadow .member} **`shadow`**`?: boolean` [¶](#vertices-shadow){.headerlink} : Whether each block or group casts a shadow on the sheet, styled by the theme's `diagram.shadow`. - Default `false`. Diagrams only. + Defaults to `false`. Diagrams only. (edges)= @@ -1041,9 +1043,9 @@ Rows drawn as markers: points of a map, nodes of a graph, blocks of a diagram. {.signature} `interface Edges extends` [`MarkOptions`](#markoptions)`,` [`StrokeOptions`](#strokeoptions) -Rows drawn as strokes between the vertices named by two reference fields: branches, links, -wires. In a diagram, a row without ends is a net, joining every vertex port whose reference -field names it. +Rows drawn as strokes between two vertices named by reference fields: branches, links, wires. In +a diagram, a row without ends is a net. It joins every vertex port whose reference field names +it. {#edges-edges .member} **`edges`**`:` [`TableInput`](#tableinput) [¶](#edges-edges){.headerlink} @@ -1059,23 +1061,23 @@ field names it. {#edges-route .member} **`route`**`?:` [`Route`](#route) `| null` [¶](#edges-route){.headerlink} -: How each row runs between its ends: geodesic on the earth and straight elsewhere unless set. +: How each row runs between its ends. Defaults to geodesic on the earth and straight elsewhere. `orthogonal` routes around blocks with rounded bends, in diagrams only. {#edges-arrows .member} **`arrows`**`?: boolean` [¶](#edges-arrows){.headerlink} -: Whether each row has an arrowhead at its second end; a net has one at each input it reaches. - Default `false`. +: Whether each row has an arrowhead at its second end. A net has one at each input it reaches. + Defaults to `false`. {#edges-poles .member} **`poles`**`?: boolean` [¶](#edges-poles){.headerlink} : Draws each row as a pole from its own vertex down to the ground, on a plane or the earth. Poles - take no `ends`, `bends`, or `joins`. Default `false`. + take no `ends`, `bends`, or `joins`. Defaults to `false`. {#edges-joins .member} **`joins`**`?: [table: string, reference: string] | null` [¶](#edges-joins){.headerlink} -: Draws each row as a net on a plane or the earth, joining the vertex rows of `table` whose - `reference` field names it: a star from their center to each member. +: Draws each row as a net on a plane or the earth. The net joins the vertex rows of `table` whose + `reference` field names it, as a star from their center to each member. (lines)= @@ -1085,9 +1087,8 @@ field names it. `interface Lines extends` [`MarkOptions`](#markoptions)`,` [`StrokeOptions`](#strokeoptions) Rows drawn as lines along a path field (roads, rivers, borders) or a polygon field's rings (area -outlines). On a plot, each row instead traces a sampled field's history across the data's axis, -as a monitor does. On the earth, paths follow the globe's curve and stay whole across the -antimeridian. +outlines). On a plot, each row traces a sampled field's history across the data's axis, like a +monitor. On the earth, paths follow the globe's curve and stay unbroken across the antimeridian. {#lines-lines .member} **`lines`**`:` [`TableInput`](#tableinput) [¶](#lines-lines){.headerlink} @@ -1095,17 +1096,17 @@ antimeridian. {#lines-geometry .member} **`geometry`**`?: string | null` [¶](#lines-geometry){.headerlink} -: The path or polygon field each row runs along; the field named `geometry` by default. Not +: The path or polygon field each row runs along. Defaults to the field named `geometry`. Not allowed on a plot. {#lines-trace .member} **`trace`**`?: string | null` [¶](#lines-trace){.headerlink} -: The sampled field each row traces across the data's axis; plots only, where it is required. A - field with no frames yet draws nothing until its first frame arrives. +: The sampled field each row traces across the data's axis. Required on a plot, and allowed only + there. A field with no frames yet draws nothing until its first frame arrives. {#lines-interpolation .member} **`interpolation`**`?:` [`Interpolation`](#interpolation) [¶](#lines-interpolation){.headerlink} -: How each row runs between its samples on a plot; `linear` by default. +: How each row runs between its samples on a plot. Defaults to `linear`. (areas)= @@ -1116,8 +1117,8 @@ antimeridian. Rows drawn as the areas of a polygon field: parcels, states, lakes, the land and water of a map. The view triangulates each row's polygons once, in the background when they are large. On the -earth, areas follow the globe's curve and stay whole across the antimeridian. To outline areas, -add a [`Lines`](#lines) mark of the same table, which draws their rings. +earth, areas follow the globe's curve and stay unbroken across the antimeridian. To outline +areas, add a [`Lines`](#lines) mark of the same table. It draws their rings. {#areas-areas .member} **`areas`**`:` [`TableInput`](#tableinput) [¶](#areas-areas){.headerlink} @@ -1125,8 +1126,8 @@ add a [`Lines`](#lines) mark of the same table, which draws their rings. {#areas-geometry .member} **`geometry`**`?: string | null` [¶](#areas-geometry){.headerlink} -: The polygon field each row fills, holding one polygon's rings or several polygons; the field - named `geometry` by default. +: The polygon field each row fills. It holds one polygon's rings or several polygons. Defaults to + the field named `geometry`. (faces)= @@ -1135,9 +1136,9 @@ add a [`Lines`](#lines) mark of the same table, which draws their rings. {.signature} `interface Faces extends` [`MarkOptions`](#markoptions) -Rows drawn as triangles between the vertices named by three reference fields: meshes, surfaces, -and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen -from above shows its exact colors. +Rows drawn as triangles between three vertices named by reference fields: meshes, surfaces, and +fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen from +above shows its exact colors. {#faces-faces .member} **`faces`**`:` [`TableInput`](#tableinput) [¶](#faces-faces){.headerlink} @@ -1145,7 +1146,7 @@ from above shows its exact colors. {#faces-corners .member} **`corners`**`?: [a: string, b: string, c: string] | null` [¶](#faces-corners){.headerlink} -: The reference fields naming each row's three vertices; required. Without a color, a face blends +: The reference fields naming each row's three vertices. Required. Without a color, a face blends the colors of its corners. (marker)= @@ -1156,7 +1157,7 @@ from above shows its exact colors. `type Marker = 'ellipse' | 'rectangle' | 'rounded' | 'diamond' | 'block' | { shape:` [`Shape`](#shape) `} | { block: { … } } | { gauge: { … } } | { pie: { … } } | { icon: { … } } | { wgsl: { … } }` How a vertex is drawn: a shape's name, `"block"`, or -`{ shape | block | gauge | pie | icon | wgsl: options }`. An ellipse by default. +`{ shape | block | gauge | pie | icon | wgsl: options }`. Defaults to an ellipse. {#marker-shape .member} `{` **`shape`**`:` [`Shape`](#shape) `}` [¶](#marker-shape){.headerlink} @@ -1164,16 +1165,16 @@ How a vertex is drawn: a shape's name, `"block"`, or {#marker-block .member} `{` **`block`**`: { corner?: number | null } }` [¶](#marker-block){.headerlink} -: A rectangle with rounded corners that holds its label inside, as a diagram's blocks do. +: A rectangle with rounded corners that holds its label inside, like a diagram's blocks. {#marker-block-corner .member} **`corner`**`?: number | null` [¶](#marker-block-corner){.headerlink} - : Corner radius in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's - `diagram.corner` unless set. + : Corner radius in CSS pixels, or units in a diagram. Defaults to the theme's `diagram.corner`. {#marker-gauge .member} `{` **`gauge`**`: { fill:` [`ExprInput`](#exprinput)`; shape?:` [`Shape`](#shape) `} }` [¶](#marker-gauge){.headerlink} -: A shape's outline, filled to `fill` from 0 to 1: around a circle, or upward in other shapes. +: A shape's outline, filled to `fill` (0 to 1). A circle fills around its edge, and other shapes + fill upward. {#marker-gauge-fill .member} **`fill`**`:` [`ExprInput`](#exprinput) [¶](#marker-gauge-fill){.headerlink} @@ -1181,7 +1182,7 @@ How a vertex is drawn: a shape's name, `"block"`, or {#marker-gauge-shape .member} **`shape`**`?:` [`Shape`](#shape) [¶](#marker-gauge-shape){.headerlink} - : The outline's shape; an ellipse by default. + : The outline's shape. Defaults to an ellipse. {#marker-pie .member} `{` **`pie`**`: { slices:` [`ExprInput`](#exprinput)`[]; colors:` [`Color`](#color)`[]; hole?: number } }` [¶](#marker-pie){.headerlink} @@ -1197,11 +1198,11 @@ How a vertex is drawn: a shape's name, `"block"`, or {#marker-pie-hole .member} **`hole`**`?: number` [¶](#marker-pie-hole){.headerlink} - : The hole in the middle, as a fraction of the radius; 0 by default. + : The hole in the middle, as a fraction of the radius. Defaults to 0. {#marker-icon .member} `{` **`icon`**`: { images:` [`Bitmap`](#bitmap)`[]; index:` [`ExprInput`](#exprinput) `} }` [¶](#marker-icon){.headerlink} -: One of `images`, fitted to the marker's box; `index` picks the image for each row. +: One of `images`, fitted to the marker's box. `index` picks the image for each row. {#marker-icon-images .member} **`images`**`:` [`Bitmap`](#bitmap)`[]` [¶](#marker-icon-images){.headerlink} @@ -1292,11 +1293,11 @@ outputs, and the net's arrows point away from them. {#port-shape .member} **`shape`**`?:` [`Shape`](#shape) [¶](#port-shape){.headerlink} -: Its shape; an ellipse by default. +: Its shape. Defaults to an ellipse. {#port-output .member} **`output`**`?: boolean` [¶](#port-output){.headerlink} -: Whether it is an output, which drives its net. Default `false`, an input. +: Whether it is an output, which drives its net. Defaults to `false` (an input). (side)= @@ -1374,37 +1375,37 @@ How a line runs between its samples. {.signature} `type Label =` [`ExprInput`](#exprinput) `| { … }` -Text for each row, placed beside, inside, or along the row's shape and following it every frame: -a text field, or options. A label shows wherever its row shows and its text fits the room the -row gives it, such as inside a block. Labels beside or along their rows never overlap: where -they would, the label of the row drawn on top shows, so a mark's `priority` decides, and a dense -view labels as many rows as fit. A row whose text is missing or empty has no label, so a `when` -choosing between the text field and missing labels only the rows it names. In a diagram a label -is part of the drawing: it scales with the zoom, fades out when too small to read, and shows +Text for each row, placed beside, inside, or along the row's shape and moving with it: a text +field, or options. A label shows when its row is visible and its text fits, for example inside a +block. + +Labels beside or along their rows never overlap. Where they would, the row drawn on top keeps +its label (so `priority` decides), and a dense view labels as many rows as fit. A row whose text +is missing or empty has no label, so a `when` that returns the field or missing labels only some +rows. In a diagram, a label scales with the zoom, fades out when too small to read, and shows wherever it fits its vertex. {#label-text .member} **`text`**`:` [`ExprInput`](#exprinput) [¶](#label-text){.headerlink} -: The text field to show, or a `when` whose values are each that field or missing. A row whose - text is missing or empty has no label. In a diagram the text reads only data, since the sheet - sizes blocks around it. +: The text field to show, or a `when` that returns that field or missing. A row whose text is + missing or empty has no label. In a diagram, the text may read only data, since the sheet sizes + blocks around it. {#label-size .member} **`size`**`?: number | null` [¶](#label-size){.headerlink} -: Font size in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's - `text.size` unless set. +: Font size in CSS pixels, or diagram units in a diagram. Defaults to the theme's `text.size`. {#label-color .member} **`color`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#label-color){.headerlink} -: Each row's text color; the theme's text color unless set. +: Each row's text color. Defaults to the theme's text color. {#label-maxwidth .member} **`maxWidth`**`?: number | null` [¶](#label-maxwidth){.headerlink} -: The width at which lines wrap, in the space's unit (units in a diagram, CSS pixels elsewhere). +: The width at which lines wrap, in CSS pixels, or diagram units in a diagram. {#label-font .member} **`font`**`?: string | null` [¶](#label-font){.headerlink} -: The CSS font family list for the text; the theme's `text.font` unless set. +: The CSS font family list for the text. Defaults to the theme's `text.font`. (size)= @@ -1413,9 +1414,8 @@ wherever it fits its vertex. {.signature} `type Size =` [`ExprInput`](#exprinput) `|` [`Px`](#px) `|` [`Units`](#units) -A size: a value in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in CSS -pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the -view zooms in. +A size: a value in CSS pixels, or units in a diagram. `{ px }` is always CSS pixels. `{ units }` +is position units (degrees on the earth), which grow as the view zooms in. (px)= @@ -1437,8 +1437,8 @@ A size in CSS pixels, regardless of the space's unit. {.signature} `interface Units` -A size in position units (degrees on the earth), regardless of the space's unit, so it scales as -the view zooms. `atLeast` and `atMost` bound it in CSS pixels. +A size in position units (degrees on the earth), regardless of the space's unit. It scales with +the zoom. `atLeast` and `atMost` bound it in CSS pixels. {#units-units .member} **`units`**`:` [`ExprInput`](#exprinput) [¶](#units-units){.headerlink} @@ -1463,13 +1463,12 @@ Expressions as JSON: a number, a boolean, `null`, a field as `"$name"`, a CSS co {.signature} `type ExprNode = number | boolean | null | string |` [`FieldNode`](#fieldnode) `|` [`StateNode`](#statenode) `|` [`WhenNode`](#whennode) `|` [`ScaleNode`](#scalenode) `|` [`ColormapNode`](#colormapnode) `|` [`CallNode`](#callnode) -A value for each row. A number or boolean is a constant, `null` is a missing value, a string -starting with `$` reads the field it names, any other string is a CSS color, and an object is a -node that computes a value. +A value for each row. A number or boolean is a constant and `null` is missing. A string starting +with `$` reads that field, and any other string is a CSS color. An object computes a value. {#exprnode-null .member} **`null`** [¶](#exprnode-null){.headerlink} -: A missing value. An option that evaluates to missing uses the theme's value. +: A missing value. An option that is missing uses the theme's value. (fieldnode)= @@ -1478,8 +1477,8 @@ node that computes a value. {.signature} `interface FieldNode` -A field's value in each row: from the mark's own table, or with `from`, from another dataset's -table joined by row id. `"$name"` is the short form of `{ "field": "name" }`. +A field's value in each row, from the mark's own table. With `from`, it reads another dataset's +table, joined by row id. `"$name"` is the short form of `{ "field": "name" }`. {#fieldnode-field .member} **`field`**`: string` [¶](#fieldnode-field){.headerlink} @@ -1492,7 +1491,7 @@ table joined by row id. `"$name"` is the short form of `{ "field": "name" }`. {#fieldnode-step .member} **`step`**`?: boolean` [¶](#fieldnode-step){.headerlink} : For a sampled field, reads the last frame at or before the shown coordinate instead of - interpolating. Default `false`. + interpolating. Defaults to `false`. (statenode)= @@ -1514,9 +1513,9 @@ View state of each row, such as whether it is selected. {.signature} `type CallNode = { neg: [`[`ExprInput`](#exprinput)`] } | { not: [`[`ExprInput`](#exprinput)`] } | { abs: [`[`ExprInput`](#exprinput)`] } | { sqrt: [`[`ExprInput`](#exprinput)`] } | { sin: [`[`ExprInput`](#exprinput)`] } | { cos: [`[`ExprInput`](#exprinput)`] } | { floor: [`[`ExprInput`](#exprinput)`] } | { ln: [`[`ExprInput`](#exprinput)`] } | { exp: [`[`ExprInput`](#exprinput)`] } | { add: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] } | { sub: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] } | { mul: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] } | { div: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] } | { rem: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] } | { pow: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] } | { min: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] } | { max: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] } | { lt: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] } | { le: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] } | { gt: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] } | { ge: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] } | { eq: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] } | { ne: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] } | { and: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] } | { or: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] } | { is: [field:` [`ExprInput`](#exprinput)`, text: string] } | { contains: [field:` [`ExprInput`](#exprinput)`, text: string] }` -An operation on values: an object whose one key names the operation and holds its arguments, -such as `{ "add": ["$a", 1] }`. `is` and `contains` take a plain text as their second argument, -which is neither a field nor a color. +An operation, written as an object with one key: the operation's name, holding its arguments. +For example `{ "add": ["$a", 1] }`. The second argument of `is` and `contains` is plain text, +not a field or a color. {#callnode-neg .member} `{` **`neg`**`: [`[`ExprInput`](#exprinput)`] }` [¶](#callnode-neg){.headerlink} @@ -1664,7 +1663,7 @@ A value mapped linearly onto a range, clamped to the range. {#scalenode-domain .member} **`domain`**`?: [low: number, high: number] | null` [¶](#scalenode-domain){.headerlink} -: The values mapped to the range's ends; the extent of the values shown when unset. +: The values mapped to the range's ends. Defaults to the extent of the values shown. (colormapnode)= @@ -1685,7 +1684,7 @@ A value mapped through a colormap. {#colormapnode-domain .member} **`domain`**`?: [low: number, high: number] | null` [¶](#colormapnode-domain){.headerlink} -: The values mapped to the colormap's ends; the extent of the values shown when unset. A +: The values mapped to the colormap's ends. Defaults to the extent of the values shown. A categorical map ignores it. (state)= @@ -1714,7 +1713,7 @@ Colors for a range of values: a catalog name, or your own colors. {.signature} `type ColormapName = 'amber' | 'amp' | 'batlow' | 'cividis' | 'crest' | 'deep' | 'devon' | 'flare' | 'grays' | 'haline' | 'inferno' | 'lajolla' | 'magma' | 'mako' | 'oslo' | 'plasma' | 'rocket' | 'thermal' | 'turku' | 'viridis' | 'balance' | 'berlin' | 'brbg' | 'broc' | 'coolwarm' | 'cork' | 'icefire' | 'managua' | 'piyg' | 'puor' | 'rdbu' | 'spectral' | 'vanimo' | 'vik' | 'vlag' | 'phase' | 'romao' | 'twilight' | 'dark2' | 'okabeito' | 'paired' | 'set2' | 'tab10' | 'tab20' | 'tableau10' | 'turbo'` -A colormap of the catalog. +A colormap from the catalog. (colormapkind)= @@ -1757,8 +1756,8 @@ A custom colormap: its kind, its colors, and for a categorical map, its category {#customcolormap-colors .member} **`colors`**`: (`[`Color`](#color) `| [number,` [`Color`](#color)`])[]` [¶](#customcolormap-colors){.headerlink} -: The colors, blended in OKLab. Each is a color, spaced evenly between its neighbors as in a CSS - gradient, or a `[place, color]` pair with a place from 0 to 1, in ascending order. A categorical +: The colors, blended in OKLab. Each is a color, spaced evenly between its neighbors like a CSS + gradient, or a `[place, color]` pair with a place from 0 to 1. Places must ascend. A categorical map takes one color per category, without places, at most 256. {#customcolormap-categories .member} @@ -1851,12 +1850,12 @@ its tilt, and its rotation. A diagram is always seen from straight above, uprigh {#camera-center .member} **`center`**`: [number, number] | [number, number, number]` [¶](#camera-center){.headerlink} -: Longitude, latitude, and elevation on the earth; x, y, and elevation on a plane or a diagram. In - JSON, `[x, y]` is shorthand for elevation 0. +: On the earth, longitude, latitude, and elevation. On a plane or a diagram, x, y, and elevation. + In JSON, `[x, y]` is shorthand for elevation 0. {#camera-zoom .member} **`zoom`**`: number` [¶](#camera-zoom){.headerlink} -: The web mercator zoom level on the earth; elsewhere, log₂ of CSS pixels per unit. +: The web mercator zoom level on the earth. Elsewhere, log₂ of CSS pixels per unit. {#camera-pitch .member} **`pitch`**`: number` [¶](#camera-pitch){.headerlink} @@ -1903,11 +1902,11 @@ The target of a view's fit. {#fit-bounds-min .member} **`min`**`: [number, number, number]` [¶](#fit-bounds-min){.headerlink} - : The least corner. + : The minimum corner. {#fit-bounds-max .member} **`max`**`: [number, number, number]` [¶](#fit-bounds-max){.headerlink} - : The greatest corner. + : The maximum corner. (query)= @@ -1924,7 +1923,7 @@ The rows of a table to read back, run through the expressions marks draw with. {#query-ids .member} **`ids`**`?:` [`Ids`](#ids) `| null` [¶](#query-ids){.headerlink} -: The rows to read, in this order unless sorted; every row when unset. Ids that name no row are +: The rows to read, in this order unless sorted. Defaults to every row. Ids that name no row are skipped. {#query-filter .member} @@ -1933,8 +1932,8 @@ The rows of a table to read back, run through the expressions marks draw with. {#query-sort .member} **`sort`**`?: [`[`ExprInput`](#exprinput)`,` [`Order`](#order)`][]` [¶](#query-sort){.headerlink} -: Keys to sort by, each breaking ties left by the keys before it. Rows where a key has no value - sort last; rows equal on every key keep their order. +: Keys to sort by. Each key breaks ties left by the keys before it. Rows where a key has no value + sort last, and rows equal on every key keep their order. {#query-offset .member} **`offset`**`?: number` [¶](#query-offset){.headerlink} @@ -1947,12 +1946,12 @@ The rows of a table to read back, run through the expressions marks draw with. {#query-select .member} **`select`**`?: { [name: string]:` [`ExprInput`](#exprinput) `} | null` [¶](#query-select){.headerlink} -: Fields to return, by output name. A plain field returns that field as stored; any other - expression returns numbers. Every field when unset; an empty object returns the ids alone. +: Fields to return, by output name. A plain field returns that field as stored, and any other + expression returns numbers. Defaults to every field. An empty object returns the ids alone. {#query-at .member} **`at`**`?: number | null` [¶](#query-at){.headerlink} -: The axis coordinate at which sampled fields are read; their last frame when unset. +: The axis coordinate at which sampled fields are read. Defaults to their last frame. (enveloperequest)= @@ -1962,9 +1961,9 @@ The rows of a table to read back, run through the expressions marks draw with. `interface EnvelopeRequest` A request for each row's least and greatest value of a sampled field over equal spans of the -axis: field `field` of table `table`, over `spans` equal spans from `range[0]` to `range[1]`, -for every row or for the rows `ids` names, in that order. Plain data that deserializes from -JSON, like a query. +axis. It reads field `field` of table `table` over `spans` equal spans from `range[0]` to +`range[1]`, for every row or for the rows in `ids`, in that order. Like a query, it is plain +data that deserializes from JSON. {#enveloperequest-table .member} **`table`**`: string` [¶](#enveloperequest-table){.headerlink} @@ -1976,15 +1975,15 @@ JSON, like a query. {#enveloperequest-range .member} **`range`**`: [number, number]` [¶](#enveloperequest-range){.headerlink} -: The axis range the spans cover, `[first, last]`: finite, with `first <= last`. +: The axis range the spans cover, `[first, last]`. Both are finite, with `first <= last`. {#enveloperequest-spans .member} **`spans`**`: number` [¶](#enveloperequest-spans){.headerlink} -: The number of equal spans to divide the range into; at least 1. +: The number of equal spans to divide the range into. At least 1. {#enveloperequest-ids .member} **`ids`**`?:` [`Ids`](#ids) `| null` [¶](#enveloperequest-ids){.headerlink} -: The rows to read, in this order; every row when unset. +: The rows to read, in this order. Defaults to every row. ## Results @@ -2051,9 +2050,9 @@ Something that happened in a view, as [`view.on`](#view-on) reports it. {#viewevent-drag-position .member} **`position`**`: [number, number, number]` [¶](#viewevent-drag-position){.headerlink} - : The row's new position in the space's coordinates: the hit point moved with the pointer since - the press, at the depth where the drag started, so a vertex keeps its grab offset. Snapped to a - diagram's dots when its grid snaps. + : The row's new position in the space's coordinates. It is the hit point moved with the pointer + since the press, at the depth where the drag started, so a vertex keeps its offset from the + pointer. Snapped to a diagram's grid dots when the grid snaps. {#viewevent-drag-trigger .member} **`trigger`**`:` [`Trigger`](#trigger) [¶](#viewevent-drag-trigger){.headerlink} @@ -2066,7 +2065,7 @@ Something that happened in a view, as [`view.on`](#view-on) reports it. {#viewevent-connect-from .member} **`from`**`:` [`End`](#end) [¶](#viewevent-connect-from){.headerlink} - : The end it was drawn from: for an edge, the end that stayed. + : The end it was drawn from. For a dragged edge end, the edge's other end. {#viewevent-connect-to .member} **`to`**`:` [`End`](#end) `| null` [¶](#viewevent-connect-to){.headerlink} @@ -2074,7 +2073,7 @@ Something that happened in a view, as [`view.on`](#view-on) reports it. {#viewevent-connect-edge .member} **`edge`**`:` [`Row`](#row) `| null` [¶](#viewevent-connect-edge){.headerlink} - : The edge whose end was dragged off; `to` replaces that end. + : The edge whose end was dragged off. `to` replaces that end. {#viewevent-connect-trigger .member} **`trigger`**`:` [`Trigger`](#trigger) [¶](#viewevent-connect-trigger){.headerlink} @@ -2098,7 +2097,7 @@ Something that happened in a view, as [`view.on`](#view-on) reports it. {#viewevent-hovered-hit .member} **`hit`**`:` [`Hit`](#hit) `| null` [¶](#viewevent-hovered-hit){.headerlink} - : The row now hovered; `null` once no row is. + : The row now hovered, or `null` if no row is. {#viewevent-hovered-trigger .member} **`trigger`**`:` [`Trigger`](#trigger) [¶](#viewevent-hovered-trigger){.headerlink} @@ -2114,20 +2113,20 @@ Something that happened in a view, as [`view.on`](#view-on) reports it. {#viewevent-selection-trigger .member} **`trigger`**`:` [`Trigger`](#trigger) `| null` [¶](#viewevent-selection-trigger){.headerlink} - : How the user changed it; `null` when the data dropped rows from it. + : How the user changed it, or `null` when the data dropped rows from it. {#viewevent-camera .member} **`'camera'`**`:` [`Camera`](#camera) [¶](#viewevent-camera){.headerlink} -: The camera moved by a gesture, fit, flight, coast, or orbit, or back within reach as the view's - size, insets, or space changed: [`view.camera`](#view-camera). +: The camera moved: [`view.camera`](#view-camera). A gesture, fit, flight, coast, or orbit moved it, or it moved + back within its zoom and pan limits after the view's size, insets, or space changed. {#viewevent-waiting .member} **`'waiting'`**`:` [`Waiting`](#waiting) [¶](#viewevent-waiting){.headerlink} -: The work the last frame waited on changed; empty once frames are complete: [`view.waiting`](#view-waiting). +: The work the last frame waited on changed: [`view.waiting`](#view-waiting). Empty once frames are complete. {#viewevent-error .member} **`'error'`**`:` [`Failure`](#failure) [¶](#viewevent-error){.headerlink} -: Something failed; reported once. A mark that no longer fits its data is also listed in +: Something failed. Reported once. A mark that no longer fits its data is also listed in [`view.failures`](#view-failures). (hit)= @@ -2145,7 +2144,7 @@ A row under a point, as the frame drew it. {#hit-mark .member} **`mark`**`?: string` [¶](#hit-mark){.headerlink} -: The name of the mark that drew it, if named. It tells two marks of one table apart. +: The name of the mark that drew it, if the mark has one. It tells apart two marks of one table. {#hit-point .member} **`point`**`: [number, number]` [¶](#hit-point){.headerlink} @@ -2162,11 +2161,11 @@ A row under a point, as the frame drew it. {#hit-port .member} **`port`**`?: string` [¶](#hit-port){.headerlink} -: The port hit, when a vertex's port was. +: The port hit, if the hit is on a vertex's port. {#hit-sample .member} **`sample`**`?:` [`Sample`](#sample) [¶](#hit-sample){.headerlink} -: The sample hit, when a line tracing a sampled field was. +: The sample hit, if the hit is on a line tracing a sampled field. (end)= @@ -2192,19 +2191,19 @@ A vertex a connection ends at, and the port the connection meets, if any. {.signature} `interface Failure` -What failed, as JSON: the form of the npm package's [`LatkitError`](#latkiterror). +A failure as JSON, in the same form as the npm package's [`LatkitError`](#latkiterror). {#failure-code .member} **`code`**`: 'invalid' | 'unsupported' | 'limit' | 'io' | 'device-lost' | 'cancelled' | 'timeout'` [¶](#failure-code){.headerlink} -: The kind of failure, as [`LatkitError.code`](#latkiterror-code) describes it. +: The kind of failure. See [`LatkitError.code`](#latkiterror-code). {#failure-message .member} **`message`**`: string` [¶](#failure-message){.headerlink} -: What failed, in words. +: A description of what failed. {#failure-path .member} **`path`**`?: string` [¶](#failure-path){.headerlink} -: The invalid part of the input, such as `marks[2].color`, on `invalid` failures. +: For `invalid` failures, the path of the invalid input, such as `marks[2].color`. (phase)= @@ -2234,8 +2233,8 @@ The phase of a `Drag`. {.signature} `interface Trigger` -Where the user acted, with which modifier keys, and with which device. For keyboard input, the -point is the row the keys point at, else the pointer, else the view's middle. +Where the user acted, with which modifier keys and device. For keyboard input, the point is the +row the keys point at, or else the pointer, or else the view's middle. {#trigger-point .member} **`point`**`: [number, number]` [¶](#trigger-point){.headerlink} @@ -2268,7 +2267,7 @@ The input device. {#device-touch .member} **`'touch'`** [¶](#device-touch){.headerlink} -: A finger: picks reach 8 CSS pixels farther. +: A finger. Picks find rows up to 8 CSS pixels farther away. {#device-keyboard .member} **`'keyboard'`** [¶](#device-keyboard){.headerlink} @@ -2319,7 +2318,7 @@ recorded over, and the part of the axis they cover. {#description-span .member} **`span`**`: [number, number] | null` [¶](#description-span){.headerlink} -: The first and last axis coordinates held by any sampled field; `null` when none holds any. +: The first and last axis coordinates held by any sampled field, or `null` if no field holds any. {#description-tables .member} **`tables`**`:` [`TableDescription`](#tabledescription)`[]` [¶](#description-tables){.headerlink} @@ -2466,7 +2465,7 @@ Counters describing a view's frames. {#stats-drawn .member} **`drawn`**`: number` [¶](#stats-drawn){.headerlink} -: Rows every mark draws: every row of a table drawn whole, and the rows of the tiles in view of a +: Rows the marks draw: every row of a fully drawn table, and the rows in the tiles in view of a paged table. {#stats-draws .member} @@ -2480,11 +2479,11 @@ Counters describing a view's frames. {#stats-tiles .member} **`tiles`**`: number` [¶](#stats-tiles){.headerlink} -: Fetched tiles the view draws, of every tile source. +: Fetched tiles the view draws, across all tile sources. {#stats-tilesloading .member} **`tilesLoading`**`: number` [¶](#stats-tilesloading){.headerlink} -: Tiles being fetched or decoded, of every tile source. +: Tiles being fetched or decoded, across all tile sources. {#stats-cpums .member} **`cpuMs`**`: number` [¶](#stats-cpums){.headerlink} @@ -2492,8 +2491,8 @@ Counters describing a view's frames. {#stats-gpums .member} **`gpuMs`**`: number` [¶](#stats-gpums){.headerlink} -: Milliseconds the GPU takes per frame: the median of recently measured frames; 0 if the device - cannot measure. +: Milliseconds the GPU takes per frame: the median of recently measured frames. 0 if the device + cannot measure GPU time. (waiting)= @@ -2537,7 +2536,7 @@ between them. {#work-resolved .member} **`resolved`**`: number` [¶](#work-resolved){.headerlink} -: Edge and face rows whose endpoints the GPU resolved to their vertices' resident rows: rows that +: Edge and face rows whose endpoints the GPU resolved to resident vertex rows. These are rows that changed, plus every row of a page that references a vertex now resident elsewhere. {#work-cleared .member} diff --git a/npm/README.md b/npm/README.md index f7630780..e7c8ffd3 100644 --- a/npm/README.md +++ b/npm/README.md @@ -1,7 +1,7 @@ # latkit -Large networks, such as power grids, drawn with WebGPU: datasets of tables, scenes of vertices, -edges, lines, and areas over them, and pictures and video of the views. +Draw large networks, such as power grids, with WebGPU. Write datasets of tables, describe scenes of +vertices, edges, lines, and areas over them, and export pictures and video of the views. ```ts import { Data, View, field, load } from 'latkit'; @@ -24,7 +24,7 @@ const view = await View.create(document.querySelector('canvas')!, { grid.set('Bus', [102], { kv: [345] }); // drawn the next frame ``` -A scene is plain data, typed from latkit's schema; scenes, expressions, and patches need no -WebAssembly, so a worker or a server builds them too. The +A scene is plain data, with types from latkit's schema. Scenes, expressions, and patches need no +WebAssembly, so you can build them in a worker or on a server too. The [guide](https://latkit.readthedocs.io/en/latest/guide.html) shows each task, and the [API reference](https://latkit.readthedocs.io/en/latest/reference/api.html) documents every export. diff --git a/npm/build.mjs b/npm/build.mjs index 31d16ae8..f777aeaa 100644 --- a/npm/build.mjs +++ b/npm/build.mjs @@ -1,5 +1,7 @@ -/** Build steps of the npm package, run with the workspace's pinned tools. Release and consumer - * outputs all stay under target/npm. */ +/** + * Build steps for the npm package, run with the workspace's pinned tools. Release and consumer + * outputs all stay under target/npm. + */ import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { createHash } from 'node:crypto'; diff --git a/npm/docs.mjs b/npm/docs.mjs index aa077ead..3a4381ae 100644 --- a/npm/docs.mjs +++ b/npm/docs.mjs @@ -1,12 +1,16 @@ /** - * Writes the reference pages in `docs/reference`: the JavaScript API from the package's shipped - * declarations, the scene and result types from the types the schema writes, and the command - * reference from the command tree the CLI's test writes to `cli.json`. Each entry has a heading, - * its declaration, and its documentation, then its members or a union's forms: each a signature - * and its definition, with an anchor of its own. Types in signatures, and code spans in the - * documentation that name an entry or a member, link to it. With `--check`, asserts that the - * checked-in pages are current instead. The pages are checked in so Read the Docs can build them - * with Python alone. + * Writes the reference pages in `docs/reference`: + * + * - `api.md`, the JavaScript API, from the package's shipped declarations. + * - `types.md`, the scene and result types, from the `inputs.ts` and `outputs.ts` built from the + * schema. + * - `cli.md`, the command reference, from the command tree the CLI's test writes to `cli.json`. + * + * Each entry has a heading, its declaration, and its documentation, then its members or the forms + * of its union. Each member or form is a signature over its definition, with its own anchor. Types + * in signatures link to their entries, as do code spans in the documentation that name an entry or + * member. With `--check`, asserts that the checked-in pages are current and writes nothing. The + * pages are checked in so Read the Docs can build them with Python alone. */ import assert from 'node:assert/strict'; import fs from 'node:fs'; @@ -21,8 +25,9 @@ const prettierConfig = await prettier.resolveConfig(path.join(root, 'npm/src/ind // Reading declarations /** - * `text` with its first comment blanked out. Every module opens with a comment about itself, and - * where the declarations drop the imports below it, it would read as the first declaration's doc. + * Returns `text` with its first comment blanked out. Every module opens with a comment about + * itself. Where the declarations drop the imports below it, it would read as the first + * declaration's doc. */ function withoutModuleDoc(text) { const comments = ts.getLeadingCommentRanges(text, 0) ?? []; @@ -31,7 +36,7 @@ function withoutModuleDoc(text) { return text.slice(0, first.pos) + ' '.repeat(first.end - first.pos) + text.slice(first.end); } -/** `text` with every comment blanked out and positions kept, so code read from it has none. */ +/** Returns `text` with every comment blanked out and its positions kept. */ function uncommented(text) { const scanner = ts.createScanner( ts.ScriptTarget.Latest, @@ -55,8 +60,9 @@ function uncommented(text) { } /** - * The doc comment just before position `pos` of `text`, as Markdown; '' when there is none. It may - * sit on the line before, or on the same line, as before a union member: `| /** … *\/ { … }`. + * Returns the doc comment just before position `pos` of `text` as Markdown, or '' if there is none. + * The comment may sit on the line before, or on the same line, as before a union member: + * `| /** … *\/ { … }`. */ function docBefore(text, pos) { const ranges = [ @@ -76,8 +82,9 @@ function docBefore(text, pos) { const has = (node, kind) => (ts.getModifiers?.(node) ?? []).some((m) => m.kind === kind); /** - * The declarations of `file`, by name. Each has a `kind` (function, const, class, interface, or - * type), its doc, and, by kind: its overloads, its members, or its union's branches. + * Returns the declarations of `file` by name. Each has a `kind` (function, const, class, interface, + * or type) and its doc. Depending on the kind, it also has its overloads, its members, or its + * union's branches. */ function read(file) { const raw = withoutModuleDoc(fs.readFileSync(path.join(root, file), 'utf8')); @@ -103,10 +110,10 @@ function read(file) { }); /** - * The members of a class, an interface, or an object type, in order. A getter and its setter are - * one property, and a method's overloads one method. A property of an object type lists that - * type's members as its `fields`, and a method its options' members. `Symbol.dispose` is left to - * `Handle`'s documentation. + * The members of a class, interface, or object type, in order. A getter and its setter form one + * property, and a method's overloads form one method. A property with an object type lists that + * type's members as its `fields`, and a method lists its options' members. `Symbol.dispose` is + * left to `Handle`'s documentation. */ const members = (node) => { const out = []; @@ -165,8 +172,8 @@ function read(file) { }; /** - * A union's branches, each with its doc, or the one type of a type that is no union. The first - * branch's doc may come before the union's leading `|`. + * A union's branches, each with its doc, or the single type of a type that is not a union. The + * first branch's doc may come before the union's leading `|`. */ const branches = (node) => (ts.isUnionTypeNode(node) ? node.types : [node]).map((t, i) => { @@ -246,21 +253,21 @@ const anchor = (...parts) => .replace(/^-|-$/g, ''); /** - * What each documented name links to: its anchor, how a link to it reads, and its doc. Anchors are - * labels for the whole site, so a link needs no page. + * What each documented name links to: its anchor, its link label, and its doc. Anchors are labels + * for the whole site, so a link needs no page. */ const targets = new Map(); -/** The Markdown link to `name`, an entry or `Owner.member`; undefined if the reference lacks it. */ +/** Returns the Markdown link to `name`, an entry or `Owner.member`, or undefined if it has none. */ function link(name, label) { const target = targets.get(name); return target && `[${label ?? `\`${target.label}\``}](#${target.anchor})`; } /** - * The entry or member a code span names, if the reference documents it: `Name`, `Name.member`, - * `instance.member` for a class's `Name.member`, or a call of one of these or of a function, - * `name()`. + * Returns the entry or member a code span would name: `Name`, `Name.member`, `instance.member` for + * a class's `Name.member`, or a call of any of these or of a function, `name()`. Returns undefined + * for other spans. */ function named(span) { const call = span.endsWith('()'); @@ -272,9 +279,9 @@ function named(span) { } /** - * `doc` with each `{@link Name}` made a link, and the first code span that names an entry or a - * member other than `self` made one too. Fenced code is left as it is. Throws for a `{@link}` to - * an unknown name. + * Returns `doc` with each `{@link Name}` made a link, and the first code span naming each other + * documented entry or member (not `self`) made one too. Fenced code is left alone. Throws for a + * `{@link}` to an unknown name. */ function linked(doc, self) { const done = new Set([self]); @@ -301,14 +308,14 @@ function linked(doc, self) { /** What lacks documentation, reported together once the pages are written. */ const undocumented = []; -/** Notes `what` as undocumented unless `doc` says something. */ +/** Notes `what` as undocumented if `doc` is empty. */ const documented = (doc, what) => doc || undocumented.push(what); // Signatures /** - * `code` on one line, as Prettier formats it, without a final semicolon. `code` is a declaration; - * with `as` set to `member`, a member of a class or an interface, and to `type`, a type. + * Formats `code` on one line with Prettier, without a final semicolon. `code` is a declaration by + * default, a class or interface member when `as` is `member`, and a type when `as` is `type`. */ async function oneLine(code, as = 'declaration') { const [before, after] = { @@ -329,8 +336,8 @@ async function oneLine(code, as = 'declaration') { } /** - * A code span of `text`, with the spaces at its ends outside it: Markdown would trim a span that - * starts and ends with a space. + * Wraps `text` in a code span, keeping the spaces at its ends outside the span. Markdown trims a + * span that starts and ends with a space. */ function codeSpan(text) { const [, lead, inner, trail] = text.match(/^( *)(.*?)( *)$/); @@ -340,7 +347,7 @@ function codeSpan(text) { return `${lead}${ticks}${pad}${inner}${pad}${ticks}${trail}`; } -/** `code` as code spans, with each documented type other than `self` a link. */ +/** Returns `code` as code spans, linking each documented type other than `self`. */ function linkedCode(code, self) { const scanner = ts.createScanner( ts.ScriptTarget.Latest, @@ -363,8 +370,8 @@ function linkedCode(code, self) { } /** - * A signature as Markdown: `code`, a one-line declaration, with each documented type other than - * `self` a link, and the name it declares, `code.slice(...name)`, in bold. + * Returns a signature as Markdown: `code`, a one-line declaration, with each documented type other + * than `self` linked and the declared name, `code.slice(...name)`, in bold. */ function signature(code, self, name) { if (!name) return linkedCode(code, self); @@ -380,8 +387,8 @@ function signature(code, self, name) { const nameAt = (code) => code.match(/^((static|readonly|abstract|override|async) )*/)[0].length; /** - * A union as its declaration shows it, on one line, its forms' details left to the items below - * it: an object of more than two keys reads `{ … }`, and so does an object type within a form. + * Returns a union's declaration on one line, leaving the details of its forms to the items below. + * An object with more than two keys reads `{ … }`, as does an object type inside a form. */ async function unionDeclaration(entry) { const shown = entry.branches.map((b) => { @@ -416,9 +423,9 @@ function item(id, term, definition) { } /** - * The items of `member` of entry `owner`, at `path` below it: one for each overload of a method, - * the first with the member's anchor, and one for a property, with an item for each of its fields - * in its definition. + * Returns the items for `member` of entry `owner`, at `path` below it. A method gets one item per + * overload, and the first carries the member's anchor. A property gets one item, with an item for + * each of its fields inside its definition. */ async function memberItems(owner, member, path = []) { const id = anchor(owner, ...path, member.name); @@ -458,7 +465,7 @@ async function optionItems(owner, path, options = []) { return items; } -/** The items of a class's or an interface's members: properties, then methods, each under a rubric. */ +/** The items for a class's or interface's members: constructor, properties, then methods. */ async function membersOf(entry) { const groups = [ ['Constructor', entry.members.filter((m) => m.kind === 'constructor')], @@ -474,8 +481,9 @@ async function membersOf(entry) { } /** - * The key an event union's branches share, as `type` in `{ type: 'name'; detail: … }`: every - * branch is an object with this literal key and at most one other. Undefined for other unions. + * The key an event union's branches share, such as `type` in `{ type: 'name'; detail: … }`. Every + * branch is an object with this key as a literal and at most one other key. Undefined for other + * unions. */ function eventKey(entry) { const objects = entry.branches.filter((b) => b.kind === 'object'); @@ -489,11 +497,11 @@ function eventKey(entry) { } /** - * The items of a union's branches: each documented value or named type, and each form. A named - * type without a doc of its own needs no item: the declaration links it. An event, - * `{ type: 'name'; detail: … }`, is one item named by its tag, its detail's fields in its - * definition. A tagged form, `{ key: options }`, is one item, its fields in its definition. Any - * other form lists its keys, after its doc when the union has several such forms. + * Returns the items for a union's branches: each documented value or named type, and each form. A + * named type with no doc of its own gets no item, since the declaration links it. An event, + * `{ type: 'name'; detail: … }`, is one item named by its tag, with its detail's fields inside its + * definition. A tagged form, `{ key: options }`, is one item with its fields inside its + * definition. Any other form lists its keys, after its doc when the union has several such forms. */ async function branchesOf(entry) { const owner = entry.name; @@ -517,7 +525,7 @@ async function branchesOf(entry) { fields.push(...(await memberItems(owner, field, [name]))); let term; if (key) { - // An event reads as its tag, and the type of a detail that has no fields of its own. + // An event shows its tag, and the type of its detail when the detail has no fields. const detail = payload && !payload.fields?.length ? `: ${payload.type}` : ''; term = `**${codeSpan(tag)}**${detail && linkedCode(detail, owner)}`; } else { @@ -538,8 +546,8 @@ async function branchesOf(entry) { } /** - * An entry's heading text: a function's name with parentheses, any other name with its type - * parameters' names. + * An entry's heading text: a function's name with parentheses, or any other name with the names + * of its type parameters. */ const heading = (entry) => entry.kind === 'function' ? `${entry.name}()` : `${entry.name}${entry.parameters ?? ''}`; @@ -555,7 +563,7 @@ async function declaration(entry) { return [[`${kind} ${entry.name}${entry.generics}`, ...entry.heritage].join(' ')]; } -/** An entry, as its page shows it. */ +/** Renders an entry as Markdown. */ async function render(entry) { anchors.push(entry.anchor); const lines = (await declaration(entry)).map((line) => signature(line, entry.name)); @@ -574,8 +582,8 @@ async function render(entry) { } /** - * A reference page: `head`, then each section of `sections` as `[title, description, names]`, its - * entries in the order listed. + * Renders a reference page: `head`, then each of `sections`, given as `[title, description, + * names]`, with its entries in the order listed. */ async function page(head, entries, sections) { const out = [linked(head)]; @@ -589,16 +597,17 @@ async function page(head, entries, sections) { // The command reference /** - * The command reference, from the command tree the CLI's test writes to `cli.json`: each command's - * usage and description, then its arguments and options, each a term over its definition with an - * anchor of its own. Code spans that name a command or an option link to it. A command that takes - * `render`'s arguments says so instead of listing them again. + * Renders the command reference from `cli.json`, the command tree the CLI's test writes. Each + * command shows its usage and description, then its arguments and options. Each argument or option + * is a term over its definition, with its own anchor. Code spans that name a command or option link + * to it. A command with the same arguments as `render` says so and does not list them again. */ function cliPage() { const commands = JSON.parse(fs.readFileSync(path.join(root, 'docs/reference/cli.json'), 'utf8')); const anchorOf = (command) => `cli-${command.name.split(' ')[1] ?? 'latkit'}`; const idOf = (command, arg) => `${anchorOf(command)}-${arg.long ?? arg.id}`; - // What code spans link to: each command, and each option, first in its own command. + // What code spans link to: each command, and each option at the first command that has it. A + // command's own options override this below. const links = new Map(commands.map((command) => [command.name, anchorOf(command)])); for (const command of commands) for (const arg of command.args) @@ -618,7 +627,7 @@ function cliPage() { const render = commands.find((command) => command.name === 'latkit render'); const out = [ '# Command reference', - "Every command and option of the `latkit` command line, written from its help. The [command-line guide](../cli.md) shows them at work. `--help` prints a command's help in the terminal, and `latkit --version` prints the version.", + "Every command and option of the `latkit` command line, generated from its help. The [command-line guide](../cli.md) shows them in use. `--help` prints a command's help, and `latkit --version` prints the version.", ]; for (const command of commands) { const top = command.name === 'latkit'; @@ -700,7 +709,7 @@ const sections = { ], [ 'Data and patches', - 'Datasets of named tables, their tables that marks draw, and the patches that change them, written without WebAssembly.', + 'Datasets, the tables in them that marks draw, and the patches that change them. Patches are written without WebAssembly.', [ 'Dataset', 'Data', @@ -725,7 +734,7 @@ const sections = { ], [ 'Expressions', - 'Builders for the per-row expressions that scene options take, colors among them.', + 'Builders for the per-row expressions that scene options take, including colors.', [ 'Expr', 'ExprInput', @@ -751,7 +760,7 @@ const sections = { ], [ 'Views', - 'Views draw scenes on canvases, with the GPU they share.', + 'Views, which draw scenes on canvases, and the GPU they draw with.', ['View', 'Rect', 'ViewEvents', 'Insets', 'Point', 'Position', 'Gpu'], ], [ @@ -789,7 +798,7 @@ const sections = { ], [ 'Themes', - 'How a scene looks: a group for each kind of thing it draws, and the lengths its options take.', + 'How a scene looks: a group of options for each kind of thing it draws, and the lengths those options take.', [ 'Theme', 'TextTheme', @@ -808,7 +817,7 @@ const sections = { ], [ 'Marks', - 'The marks that draw the tables of a scene.', + "The marks that draw a scene's tables.", [ 'Mark', 'MarkOptions', @@ -883,23 +892,23 @@ const sections = { const heads = { 'api.md': `# JavaScript API -Every class, function, and type the \`latkit\` package exports. Scenes and their options take the -[scene and result types](types.md). Four rules hold throughout, so the entries below leave them -out: - -- **Loading.** Scenes, expressions, and patches are plain data. Datasets, views, pictures, and the - colormap functions need latkit's WebAssembly: until \`load()\` finishes, they throw an - \`unloaded\` error. \`View.create()\`, \`Gpu.create()\`, \`Data.read()\`, and \`Patch.read()\` of - JSON load it themselves. -- **Errors.** Everything latkit throws or rejects with is a \`LatkitError\`. Input it cannot use - fails with an \`invalid\` error whose \`path\` names the part, such as \`marks[2].color\`, and - changes nothing. -- **Waits.** Every promise waits within its \`Wait\`: until its \`signal\` aborts, or 30 seconds. -- **Lifetimes.** A \`Handle\` holds memory until \`destroy()\`, or a \`using\` declaration, frees it.`, +Every class, function, and type the \`latkit\` package exports. Scenes and their options use the +[scene and result types](types.md). These rules apply everywhere, so the entries below don't +repeat them: + +- Scenes, expressions, and patches are plain data. Datasets, views, pictures, and the colormap + functions need latkit's WebAssembly, and throw an \`unloaded\` error until \`load()\` finishes. + \`View.create()\`, \`Gpu.create()\`, \`Data.read()\`, and \`Patch.read()\` of JSON call it + themselves. +- Everything latkit throws or rejects with is a \`LatkitError\`. Input it can't use fails with an + \`invalid\` error and changes nothing. The error's \`path\` names the bad part, such as + \`marks[2].color\`. +- Every promise waits until its \`Wait\` signal aborts, or for 30 seconds without one. +- A \`Handle\` holds memory until \`destroy()\` or the end of a \`using\` block frees it.`, 'types.md': `# Scene and result types -The types of the scenes latkit draws and of what it reports, written from the same schema as the -package's types. Each property gives its meaning and default.`, +The types of the scenes latkit draws and the results it reports, generated from the same schema as +the package's types. Each property gives its meaning and default.`, }; /** The declarations of every file in `files`, read from `dir`. */ @@ -909,8 +918,8 @@ const entries = { 'types.md': readAll(TYPES, 'npm/src', '.ts'), }; -// Anchors ignore case, so a builder named like a type on either page, such as `zoom()` beside -// `Zoom`, takes `-function` after its name. +// Anchors ignore case, so a builder named like a type on either page, such as `zoom()` and `Zoom`, +// gets `-function` appended to its anchor. const types = new Set( Object.values(entries) .flatMap((all) => [...all.values()]) @@ -939,7 +948,7 @@ for (const [name, all] of Object.entries(entries)) { doc: member.doc, }); } - // A union's forms list their keys as members do, except an event's, which nest in the event. + // A union's forms list their keys like members, except an event's, which nest in the event. const forms = entry.branches && !eventKey(entry) ? entry.branches : []; for (const member of forms.flatMap((b) => (b.kind === 'object' ? b.members : []))) { targets.set(`${entry.name}.${member.name}`, { @@ -955,8 +964,8 @@ const written = new Map(); for (const name of Object.keys(entries)) { const file = path.join(root, 'docs/reference', name); const content = await page(heads[name], entries[name], sections[name]); - // The reference must use npm's names. No line may contain a Rust path (`::`), and types.md, - // which the schema writes from Rust's documentation, may not contain a Rust call (`name()`). + // The reference must use npm's names. No line may contain a Rust path (`::`). types.md, which the + // schema writes from Rust's documentation, may not contain a Rust call (`name()`) either. const rust = name === 'types.md' ? /::|\w\(\)/ : /::/; const leftover = content.split('\n').filter((line) => rust.test(line)); assert.deepEqual(leftover, [], `${file}: Rust's names in the TypeScript reference`); diff --git a/npm/examples/gate/gate.ts b/npm/examples/gate/gate.ts index 84384a8d..f3a5abe3 100644 --- a/npm/examples/gate/gate.ts +++ b/npm/examples/gate/gate.ts @@ -24,7 +24,7 @@ import { STEP, deadline, errorsOf, launch, serve, type Chrome } from './browser. import { quantile } from './quantile.ts'; /** Maximum milliseconds Chrome may take to compile one pipeline cold. The slowest takes about - * 0.35 s. A view opens no sooner than its slowest pipeline; the others compile in parallel. */ + * 0.35 s. A view opens no sooner than its slowest pipeline. The others compile in parallel. */ const COMPILE_BUDGET = 800; /** Maximum milliseconds from opening a page cold to its views' first frames. A first frame draws * everything its scene uses. Every pipeline compiles in parallel, off Chrome's GPU thread. */ @@ -286,8 +286,8 @@ async function open(url: string, id: string): Promise { const page = await deadline(context.newPage(), STEP, 'a page'); const opened: Opened = { wrong: errorsOf(page), slow: [] }; const { wrong, slow } = opened; - // Tracks the longest gap between animation frames since the last reset, the last 120 gaps, - // whose median is the display's frame interval, and each gap longer than `LONG` with what the + // Tracks the longest gap between animation frames since the last reset, the last 120 gaps + // (their median is the display's frame interval), and each gap longer than `LONG` with what the // views were doing. await page.addInitScript((long: number) => { interface Stage { diff --git a/npm/examples/src/pages/bunny/index.ts b/npm/examples/src/pages/bunny/index.ts index 48c8dbb7..d1731eb8 100644 --- a/npm/examples/src/pages/bunny/index.ts +++ b/npm/examples/src/pages/bunny/index.ts @@ -47,7 +47,7 @@ export default page({ const body = new Body(mesh); const { burst } = body; const pieces = burst.chunks.length; - // The mesh, its pieces once popped, and the room are set once; each recorded frame then + // The mesh, its pieces once popped, and the room are set once. Each recorded frame then // appends every vertex, the shadows, and the body as a whole. const data = new Data('bunny') .axis('time', 's') diff --git a/npm/examples/src/pages/diagram.ts b/npm/examples/src/pages/diagram.ts index d9b8e6e6..92fa6d12 100644 --- a/npm/examples/src/pages/diagram.ts +++ b/npm/examples/src/pages/diagram.ts @@ -1,7 +1,7 @@ /** * A block diagram on a sheet: blocks with ports, wires routed around them, and groups drawn around - * their members. Each block kind is a table whose reference fields name the signal on each port, and - * an edges mark of the signals draws them as wires. Drag blocks, draw wires from an output to an + * their members. Each block kind is a table. Its reference fields name the signal on each port, and + * an edges mark over the signals draws them as wires. Drag blocks, draw wires from an output to an * input, delete with the Delete key, arrange, and undo. */ import { @@ -169,8 +169,8 @@ export default page({ ], })); - // Every edit can be undone: each one keeps the version before it, which shares its pages with - // the data, so the history costs only what changed. + // Every edit can be undone. Each edit keeps the version before it, which shares pages with the + // data, so the history costs only what changed. const undo: Snapshot[] = []; const redo: Snapshot[] = []; const edit = (change: () => void) => { diff --git a/npm/examples/src/pages/grids/maps.ts b/npm/examples/src/pages/grids/maps.ts index a8a6a3b4..551a13d9 100644 --- a/npm/examples/src/pages/grids/maps.ts +++ b/npm/examples/src/pages/grids/maps.ts @@ -1,8 +1,11 @@ /** - * The maps drawn beneath a grid: Natural Earth's coasts and borders, shipped with the page; Natural - * Earth's states and provinces, downloaded from its repository; and OpenStreetMap's planet as - * OpenFreeMap's vector tiles, fetched as the camera needs them. GeoJSON, plain or gzip-compressed, - * is read in a worker. + * The maps drawn beneath a grid: + * + * - Natural Earth's coasts and borders, shipped with the page. + * - Natural Earth's states and provinces, downloaded from its repository. + * - OpenStreetMap's planet as OpenFreeMap's vector tiles, fetched as the camera needs them. + * + * GeoJSON, plain or gzip-compressed, is read in a worker. */ import { Data, Tiles, field, when, zoom, type Mark } from 'latkit'; import { download } from './cases.js'; diff --git a/npm/examples/src/pages/telemetry.ts b/npm/examples/src/pages/telemetry.ts index 83a0f68f..2cb5d8c2 100644 --- a/npm/examples/src/pages/telemetry.ts +++ b/npm/examples/src/pages/telemetry.ts @@ -1,9 +1,8 @@ /** * Telemetry: thousands of streaming voltages traced over a scrolling window. Frames are appended as - * they arrive and the oldest are forgotten; pause or scrub the timeline to read back what passed. - * Every 20 seconds one area dips and recovers, or strike one yourself, and query for the signals - * hit hardest. Or draw a recorded day instead, fetched span by span as time tiles at the detail the - * plot needs. + * they arrive and the oldest are forgotten. Pause or scrub the timeline to read back what passed. + * Every 20 seconds one area dips and recovers. Strike one yourself, and query for the signals hit + * hardest. Or draw a recorded day, fetched span by span as time tiles at the detail the plot needs. */ import { Data, Tiles, field, selected, when, type CustomColormap } from 'latkit'; import { choice, page } from '../shell/page.js'; @@ -21,7 +20,7 @@ const KEEP = 60; const WINDOW = 20; const EVERY = 20; -/** How far a dip has pulled voltages down, `since` seconds after it struck; 0 after six seconds. */ +/** How far a dip has pulled voltages down, `since` seconds after it struck. 0 after six seconds. */ const dip = (since: number) => since < 0 || since > 6 ? 0 diff --git a/npm/examples/src/shell/dom.ts b/npm/examples/src/shell/dom.ts index 6f233d41..f4ede5e0 100644 --- a/npm/examples/src/shell/dom.ts +++ b/npm/examples/src/shell/dom.ts @@ -1,8 +1,10 @@ type Child = Node | string | null | undefined | false; type Attributes = Record; -/** Creates an element with `attributes` and `children`. Attributes that are `false` or `undefined` - * are omitted, `true` sets an empty attribute, and falsy children are skipped. */ +/** + * Creates an element with `attributes` and `children`. Attributes that are `false` or `undefined` + * are omitted, `true` sets an empty attribute, and falsy children are skipped. + */ export function h( tag: K, attributes: Attributes = {}, diff --git a/npm/examples/src/shell/page.ts b/npm/examples/src/shell/page.ts index 729d2612..8c3e3e58 100644 --- a/npm/examples/src/shell/page.ts +++ b/npm/examples/src/shell/page.ts @@ -1,7 +1,7 @@ /** - * What an example page declares. A page is its settings, which the shell shows as controls, and an - * `open` that builds its data and views. Each view's scene is a function of the settings, which the - * shell draws again whenever one changes, so a page holds no code that restyles its views. + * What an example page declares: its settings, which the shell shows as controls, and an `open` + * function that builds its data and views. Each view's scene is a function of the settings. The + * shell redraws it whenever a setting changes, so a page has no code that restyles its views. */ import type { Camera, Data, Rect, Scene, View } from 'latkit'; @@ -9,7 +9,7 @@ import type { Camera, Data, Rect, Scene, View } from 'latkit'; export type Setting = { readonly label: string; readonly value: T; - /** The side panel section it shows in; `Settings` by default. */ + /** The side panel section it shows in. Defaults to `Settings`. */ readonly section?: string; /** A key that changes it: a toggle flips, and a choice steps to its next option. */ readonly key?: string; @@ -27,7 +27,7 @@ export type Setting = { type Where = { section?: string; key?: string }; -/** A dropdown of `options`, keyed by their labels; the first option by default. */ +/** A dropdown of `options`, keyed by their labels. Defaults to the first option. */ export function choice( label: string, options: Readonly>, @@ -43,7 +43,7 @@ export function choice( }; } -/** A button that toggles; off by default. */ +/** A toggle button. Defaults to off. */ export function toggle(label: string, where: Where & { value?: boolean } = {}): Setting { return { kind: 'toggle', label, ...where, value: where.value ?? false }; } @@ -79,15 +79,15 @@ export interface Timeline { play(): void; } -/** What a page can do while it is open. Everything it makes through it closes with it. */ +/** What a page can do while it is open. Everything made through it closes with the page. */ export interface Context { /** The current settings. */ readonly settings: Readonly; /** Aborted when the page closes. */ readonly signal: AbortSignal; /** - * Creates a view on the page's canvas, in `region`, drawing `scene` of the current settings and - * again after each change. Without a camera, it frames everything drawn. + * Creates a view on the page's canvas, in `region`, that draws `scene` for the current settings + * and redraws it after each change. Without a camera, it frames everything drawn. */ view( scene: (settings: V) => Scene, @@ -97,8 +97,8 @@ export interface Context { actions(title: string, actions: readonly Action[]): void; /** * Plays `data`'s axis in a timeline under the canvas. Live, it follows the end of the data and - * forgets frames older than `keep` seconds; paused or replaying, it shows earlier frames. Every - * view of the page shows the timeline's coordinate. + * forgets frames older than `keep` seconds. Paused or replaying, it shows earlier frames. Every + * view on the page shows the timeline's coordinate. */ timeline(options: { data: Data; @@ -137,7 +137,7 @@ export interface Opened { export interface Page { readonly title: string; readonly settings?: S; - /** Whether hovering or clicking a row shows its fields in the side panel; true by default. */ + /** Whether hovering or clicking a row shows its fields in the side panel. Defaults to true. */ readonly inspect?: boolean; open(context: Context>): Promise> | void>; } diff --git a/npm/examples/src/shell/shell.ts b/npm/examples/src/shell/shell.ts index 07c5880e..f71ef426 100644 --- a/npm/examples/src/shell/shell.ts +++ b/npm/examples/src/shell/shell.ts @@ -1,8 +1,8 @@ /** * The examples' shell: the layout every page shares, and everything a page does not declare - * itself. It shows a page's settings as controls and draws each view's scene again when one - * changes, runs the page's frames and timeline, and gives every page an inspector, a performance - * readout, and picture and video export. + * itself. It shows a page's settings as controls and redraws each view's scene when one changes. + * It runs the page's frames and timeline, and gives every page an inspector, a performance readout, + * and picture and video export. */ import { Shot, @@ -108,8 +108,8 @@ export function start(pages: readonly Entry[]): void { ); console.error(error); }; - // Settings keep the URL in step without firing this, so a change here comes from a link or the - // address bar: the page opens again with the settings it gives. + // Settings update the URL without firing this, so a change here comes from a link or the address + // bar. The page reopens with the settings in the URL. addEventListener('hashchange', () => void show()); void show(); } @@ -344,7 +344,7 @@ class OpenPage implements Context> { ); } - /** Sets `name` to `value`, keeps the URL in step, and draws every view's scene again. */ + /** Sets `name` to `value`, updates the URL, and redraws every view's scene. */ async #change(name: string, value: unknown): Promise { this.settings[name] = value; const query = new URLSearchParams(); @@ -361,7 +361,7 @@ class OpenPage implements Context> { } } - /** A view's region, its axes swapped on narrow screens so side-by-side views stack. */ + /** A view's region, with its axes swapped on narrow screens so side-by-side views stack. */ #fitted(region: Rect): Rect { const [x, y, width, height] = region; return matchMedia('(max-width: 700px)').matches ? [y, x, height, width] : region; @@ -529,8 +529,10 @@ function valueFrom(setting: Setting, given: string | null): unknown { return setting.options.find(([, v]) => String(v) === given)?.[1]; } -/** A field's value for the inspector: a number, a text, a point, or the word for a longer geometry. A - * reference reads as the id of the row it references. */ +/** + * A field's value for the inspector: a number, a text, a point, or the word `geometry` for a longer + * geometry. A reference shows as the id of the row it references. + */ function format(value: Value): string { if (typeof value === 'number') return Number.isInteger(value) ? value.toLocaleString() : value.toPrecision(4); diff --git a/npm/examples/src/shell/timeline.ts b/npm/examples/src/shell/timeline.ts index aab7ebb1..d50437b3 100644 --- a/npm/examples/src/shell/timeline.ts +++ b/npm/examples/src/shell/timeline.ts @@ -1,7 +1,7 @@ /** * A page's timeline: the recorded span of a dataset with an axis, played live, paused, or - * replayed, with its transport under the canvas. Live, it follows the end of the data and forgets - * frames older than the span it keeps. + * replayed, with playback controls under the canvas. When live, it follows the end of the data and + * forgets frames older than the span it keeps. */ import type { Data } from 'latkit'; import { h } from './dom.js'; diff --git a/npm/schema.json b/npm/schema.json index 6acc7c27..ae4730b9 100644 --- a/npm/schema.json +++ b/npm/schema.json @@ -18,11 +18,11 @@ }, "layout": { "$ref": "#/$defs/Layout", - "description": "How the view places vertex rows that have no position: a diagram's blocks, or points on a\nplane or the earth. `none` by default." + "description": "How the view places vertex rows that have no position, such as a diagram's blocks or points\non a plane or the earth. Defaults to `none`." }, "theme": { "$ref": "#/$defs/Theme", - "description": "Default colors and sizes, and the look of text, focus, and each space; `dark` by default." + "description": "Default colors and sizes, and the look of text, focus, and each space. Defaults to `dark`." }, "controls": { "$ref": "#/$defs/Controls", @@ -31,7 +31,7 @@ "ease": { "type": "number", "format": "double", - "description": "Seconds over which scene and data changes animate; 0.3 by default, 0 for none." + "description": "How many seconds scene and data changes take to animate. 0 turns animation off. Defaults to\n0.3." }, "title": { "type": [ @@ -80,7 +80,7 @@ "additionalProperties": false }, { - "description": "Plane units, with elevation; the camera can turn and tilt.", + "description": "Plane units, with elevation. The camera can turn and tilt.", "type": "object", "properties": { "plane": { @@ -121,7 +121,7 @@ ] } ], - "description": "The coordinate space of a scene's positions: a name, or\n`{ plane | diagram | earth | plot: options }`.\n\nThe space sets the unit of lengths that do not name their own: units in a diagram, where a unit\nis a CSS pixel at zoom 0, and CSS pixels elsewhere. Only a diagram has ports, groups, nets, and\northogonal routes." + "description": "The coordinate space of a scene's positions: a name, or\n`{ plane | diagram | earth | plot: options }`.\n\nThe space sets the unit of lengths that do not name their own. In a diagram it is the diagram\nunit, one CSS pixel at zoom 0. Elsewhere it is the CSS pixel. Only a diagram has ports, groups,\nnets, and orthogonal routes." }, "Earth": { "type": "object", @@ -129,11 +129,11 @@ "properties": { "projection": { "$ref": "#/$defs/Projection", - "description": "A flat web mercator map (`mercator`, the default) or a globe (`globe`)." + "description": "A flat web mercator map (`mercator`) or a globe (`globe`). Defaults to `mercator`." }, "graticule": { "type": "boolean", - "description": "Whether lines of longitude and latitude cross the ground, in the theme's `grid` color.\nDefault `true`." + "description": "Whether lines of longitude and latitude cross the ground, in the theme's `grid` color.\nDefaults to `true`." }, "daylight": { "type": [ @@ -141,14 +141,14 @@ "null" ], "format": "double", - "description": "Lights the earth by the sun's position this many seconds after the Unix epoch, plus the\nview's axis coordinate when it has one. The night side darkens the ground and everything on\nit, down to the theme's `earth.night` brightness. Unset, the earth is lit evenly." + "description": "Lights the earth by the sun's position this many seconds after the Unix epoch, plus the\nview's axis coordinate when it has one. The night side darkens the ground and everything on\nit, down to the theme's `earth.night` brightness. When unset, the earth is lit evenly." }, "atmosphere": { "type": "boolean", - "description": "Whether air glows around the globe, in the theme's `earth.atmosphere` color, brighter on the\nday side. Default `false`." + "description": "Whether air glows around the globe, in the theme's `earth.atmosphere` color, brighter on the\nday side. Defaults to `false`." } }, - "description": "The earth: positions are longitude and latitude in degrees. The ground takes the theme's `earth`\ncolors.\n\nA flat map is one world, drawn once. Its ground ends at the antimeridian and at Web Mercator's\nlatitude limit, and the camera zooms out until the whole world fits the view and pans until the\nworld's edges meet the view's. On a globe, longitude wraps, and the camera zooms out until the\nglobe spans a third of the view's shorter side." + "description": "The earth: positions are longitude and latitude in degrees. The ground uses the theme's `earth`\ncolors.\n\nA flat map shows the world once. Its ground ends at the antimeridian and at Web Mercator's\nlatitude limit. The camera zooms out until the whole world fits the view, and pans until the\nworld's edges meet the view's edges. On a globe, longitude wraps, and the camera zooms out until\nthe globe spans a third of the view's shorter side." }, "Projection": { "oneOf": [ @@ -171,7 +171,7 @@ "properties": { "grid": { "$ref": "#/$defs/Grid", - "description": "The grid drawn under the marks; lines 10 units apart by default." + "description": "The grid drawn under the marks. Defaults to lines 10 units apart." } }, "description": "A plane: positions are in plane units." @@ -219,7 +219,7 @@ "format": "double" }, "snap": { - "description": "Whether drag positions snap to the dots, in a diagram. Default `false`.", + "description": "Whether drag positions snap to the dots, in a diagram. Defaults to `false`.", "type": "boolean" } }, @@ -233,7 +233,7 @@ "dots" ], "additionalProperties": false, - "description": "Dots `pitch` units apart; with `snap`, a diagram's drags snap to them." + "description": "Dots `pitch` units apart. With `snap`, drags in a diagram snap to them." } ], "description": "A plane's or a diagram's grid: `\"off\"`, `{ lines: { pitch } }`, or `{ dots: { pitch, snap } }`." @@ -266,11 +266,11 @@ }, "grid": { "type": "boolean", - "description": "Whether grid lines cross the plot at the ticks. Default `true`." + "description": "Whether grid lines cross the plot at the ticks. Defaults to `true`." }, "hidden": { "type": "boolean", - "description": "Whether the axis is hidden. Default `false`." + "description": "Whether the axis is hidden. Defaults to `false`." }, "ticks": { "type": [ @@ -280,11 +280,11 @@ "items": { "$ref": "#/$defs/Tick" }, - "description": "Tick positions, each a value or `[value, label]`; a tick without a label shows its value.\nUnset, ticks fall at round values." + "description": "Tick positions, each a value or `[value, label]`. A tick without a label shows its value.\nWhen unset, ticks fall at round values." }, "format": { "$ref": "#/$defs/Format", - "description": "How tick values are formatted; `auto` by default." + "description": "How tick values are formatted. Defaults to `auto`." }, "precision": { "type": [ @@ -294,7 +294,7 @@ "format": "uint8", "minimum": 0, "maximum": 15, - "description": "Decimal places of fixed values, or of the significand in scientific and engineering formats,\nat most 15. Unset, as many as the tick step needs." + "description": "Decimal places of fixed values, or of the significand in scientific and engineering formats.\nAt most 15. When unset, as many as the tick step needs." } }, "description": "A plot axis." @@ -356,13 +356,13 @@ "properties": { "grid": { "$ref": "#/$defs/Grid", - "description": "The grid drawn under the marks; dots 20 units apart by default." + "description": "The grid drawn under the marks. Defaults to dots 20 units apart." } }, - "description": "A flat sheet for diagrams, seen from straight above; the camera pans and zooms but does not\nturn. Positions are in units with y down. Lengths are in units too, a unit being a CSS pixel at\nzoom 0: sizes, padding, ports, strokes, text, and the theme's lengths. Sizes given with `px`,\nthe text halo, the focus glow, and the pick radius stay in CSS pixels. Zooming scales the whole\npicture. Only a diagram has ports, groups, nets, and orthogonal routes, and the scene's layout\nplaces its blocks." + "description": "A flat sheet for diagrams, seen from straight above. The camera pans and zooms but does not\nturn. Positions are in units with y down, and a unit is a CSS pixel at zoom 0. Sizes, padding,\nports, strokes, text, and the theme's lengths are in units too. Sizes given with `px`, the text\nhalo, the focus glow, and the pick radius stay in CSS pixels. Zooming scales the whole picture.\nOnly a diagram has ports, groups, nets, and orthogonal routes, and the scene's layout places its\nblocks." }, "Mark": { - "description": "One table a scene draws. The key naming the mark's kind holds its dataset and table, beside its options: `{ vertices: [data, table], radius: 4 }`.", + "description": "One table a scene draws. The mark's kind is the key that holds its table, next to its options: `{ vertices: [data, table], radius: 4 }`.", "oneOf": [ { "$ref": "#/$defs/Vertices" @@ -415,18 +415,18 @@ }, "minItems": 2, "maxItems": 2, - "description": "Each row's x and y. Without a position, the point field that `geometry` names places rows,\nand the scene's layout places rows that have neither." + "description": "Each row's x and y. Rows without a position are placed by the point field `geometry` names.\nThe scene's layout places rows that have neither." }, "geometry": { "type": [ "string", "null" ], - "description": "The point field that places rows when `position` is not set; the field named `geometry` by\ndefault." + "description": "The point field that places rows when `position` is not set. Defaults to the field named\n`geometry`." }, "elevation": { "$ref": "#/$defs/Expr", - "description": "Height above the ground; on the earth, a fraction of the earth's radius. Default 0. Not\nallowed in a diagram, which is flat." + "description": "Height above the ground. On the earth, a fraction of the earth's radius. Defaults to 0. Not\nallowed in a diagram, which is flat." }, "radius": { "anyOf": [ @@ -437,7 +437,7 @@ "type": "null" } ], - "description": "Each row's radius, in the space's unit (units in a diagram, CSS pixels elsewhere) unless\ngiven with `px` or `units`; a diagram refuses `px`. The theme's `vertex.radius` unless set." + "description": "Each row's radius in CSS pixels, or units in a diagram, unless given with `px` or `units`.\nA diagram does not accept `px`. Defaults to the theme's `vertex.radius`." }, "size": { "type": [ @@ -449,11 +449,11 @@ }, "minItems": 2, "maxItems": 2, - "description": "Each row's width and height, for markers that are not round, in the space's unit unless\ngiven with `px` or `units`; a diagram refuses `px`. In a diagram, a block without a size\nfits its label and ports." + "description": "Each row's width and height, for markers that are not round. In CSS pixels, or units in a\ndiagram, unless given with `px` or `units`. A diagram does not accept `px`. In a diagram, a\nblock without a size fits its label and ports." }, "marker": { "$ref": "#/$defs/Marker", - "description": "How each row is drawn; an ellipse by default." + "description": "How each row is drawn. Defaults to an ellipse." }, "ring": { "anyOf": [ @@ -464,7 +464,7 @@ "type": "null" } ], - "description": "A ring around each row's marker in this color; a missing or transparent color draws none." + "description": "A ring around each row's marker in this color. A missing or transparent color draws no ring." }, "ports": { "type": "array", @@ -504,7 +504,7 @@ }, "shadow": { "type": "boolean", - "description": "Whether each block or group casts a shadow on the sheet, styled by the theme's\n`diagram.shadow`. Default `false`. Diagrams only." + "description": "Whether each block or group casts a shadow on the sheet, styled by the theme's\n`diagram.shadow`. Defaults to `false`. Diagrams only." } }, "required": [ @@ -520,7 +520,7 @@ "tsType": "ExprInput" }, "ExprNode": { - "description": "A value for each row. A number or boolean is a constant, `null` is a missing value, a string starting with `$` reads the field it names, any other string is a CSS color, and an object is a node that computes a value.", + "description": "A value for each row. A number or boolean is a constant and `null` is missing. A string starting with `$` reads that field, and any other string is a CSS color. An object computes a value.", "anyOf": [ { "type": "number" @@ -529,11 +529,11 @@ "type": "boolean" }, { - "description": "A missing value. An option that evaluates to missing uses the theme's value.", + "description": "A missing value. An option that is missing uses the theme's value.", "type": "null" }, { - "description": "A field's value in each row, or a color: `\"$kv\"` reads field `kv`, and any other string is a CSS color, such as `#ffb000` or `rebeccapurple`. A field whose name starts with `$` is written `{ \"field\": \"$x\" }`.", + "description": "A field or a color. `\"$kv\"` reads field `kv`. Any other string is a CSS color, such as `#ffb000` or `rebeccapurple`. Write a field named `$x` as `{ \"field\": \"$x\" }`.", "type": "string" }, { @@ -584,13 +584,13 @@ }, "step": { "type": "boolean", - "description": "For a sampled field, reads the last frame at or before the shown coordinate instead of\ninterpolating. Default `false`." + "description": "For a sampled field, reads the last frame at or before the shown coordinate instead of\ninterpolating. Defaults to `false`." } }, "required": [ "field" ], - "description": "A field's value in each row: from the mark's own table, or with `from`, from another\ndataset's table joined by row id. `\"$name\"` is the short form of `{ \"field\": \"name\" }`." + "description": "A field's value in each row, from the mark's own table. With `from`, it reads another\ndataset's table, joined by row id. `\"$name\"` is the short form of `{ \"field\": \"name\" }`." }, "StateNode": { "type": "object", @@ -680,7 +680,7 @@ }, "minItems": 2, "maxItems": 2, - "description": "The values mapped to the range's ends; the extent of the values shown when unset.", + "description": "The values mapped to the range's ends. Defaults to the extent of the values shown.", "tsType": "[low: number, high: number]" } }, @@ -713,7 +713,7 @@ }, "minItems": 2, "maxItems": 2, - "description": "The values mapped to the colormap's ends; the extent of the values shown when unset. A\ncategorical map ignores it.", + "description": "The values mapped to the colormap's ends. Defaults to the extent of the values shown. A\ncategorical map ignores it.", "tsType": "[low: number, high: number]" } }, @@ -735,7 +735,7 @@ ] }, "ColormapName": { - "description": "A colormap of the catalog.", + "description": "A colormap from the catalog.", "type": "string", "enum": [ "amber", @@ -822,7 +822,7 @@ } ] }, - "description": "The colors, blended in OKLab. Each is a color, spaced evenly between its neighbors as in a\nCSS gradient, or a `[place, color]` pair with a place from 0 to 1, in ascending order. A\ncategorical map takes one color per category, without places, at most 256." + "description": "The colors, blended in OKLab. Each is a color, spaced evenly between its neighbors like a\nCSS gradient, or a `[place, color]` pair with a place from 0 to 1. Places must ascend. A\ncategorical map takes one color per category, without places, at most 256." }, "categories": { "type": "array", @@ -864,7 +864,7 @@ "description": "How a colormap maps values: `sequential` and `diverging` maps clamp, `cyclic` maps wrap, and\n`categorical` maps give each integer its own color." }, "CallNode": { - "description": "An operation on values: an object whose one key names the operation and holds its arguments, such as `{ \"add\": [\"$a\", 1] }`. `is` and `contains` take a plain text as their second argument, which is neither a field nor a color.", + "description": "An operation, written as an object with one key: the operation's name, holding its arguments. For example `{ \"add\": [\"$a\", 1] }`. The second argument of `is` and `contains` is plain text, not a field or a color.", "anyOf": [ { "type": "object", @@ -1465,7 +1465,7 @@ ] }, "Size": { - "description": "A size: a value in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in CSS pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the view zooms in.", + "description": "A size: a value in CSS pixels, or units in a diagram. `{ px }` is always CSS pixels. `{ units }` is position units (degrees on the earth), which grow as the view zooms in.", "anyOf": [ { "$ref": "#/$defs/Expr" @@ -1522,7 +1522,7 @@ "required": [ "units" ], - "description": "A size in position units (degrees on the earth), regardless of the space's unit, so it scales\nas the view zooms. `atLeast` and `atMost` bound it in CSS pixels." + "description": "A size in position units (degrees on the earth), regardless of the space's unit. It scales with\nthe zoom. `atLeast` and `atMost` bound it in CSS pixels." }, "Marker": { "anyOf": [ @@ -1552,14 +1552,14 @@ "additionalProperties": false }, { - "description": "A rectangle with rounded corners that holds its label inside, as a diagram's blocks do.", + "description": "A rectangle with rounded corners that holds its label inside, like a diagram's blocks.", "type": "object", "properties": { "block": { "type": "object", "properties": { "corner": { - "description": "Corner radius in the space's unit (units in a diagram, CSS pixels elsewhere); the\ntheme's `diagram.corner` unless set.", + "description": "Corner radius in CSS pixels, or units in a diagram. Defaults to the theme's\n`diagram.corner`.", "type": [ "number", "null" @@ -1576,7 +1576,7 @@ "additionalProperties": false }, { - "description": "A shape's outline, filled to `fill` from 0 to 1: around a circle, or upward in other shapes.", + "description": "A shape's outline, filled to `fill` (0 to 1). A circle fills around its edge, and other\nshapes fill upward.", "type": "object", "properties": { "gauge": { @@ -1587,7 +1587,7 @@ "$ref": "#/$defs/Expr" }, "shape": { - "description": "The outline's shape; an ellipse by default.", + "description": "The outline's shape. Defaults to an ellipse.", "$ref": "#/$defs/Shape" } }, @@ -1626,7 +1626,7 @@ } }, "hole": { - "description": "The hole in the middle, as a fraction of the radius; 0 by default.", + "description": "The hole in the middle, as a fraction of the radius. Defaults to 0.", "type": "number", "format": "float" } @@ -1644,7 +1644,7 @@ "additionalProperties": false }, { - "description": "One of `images`, fitted to the marker's box; `index` picks the image for each row.", + "description": "One of `images`, fitted to the marker's box. `index` picks the image for each row.", "type": "object", "properties": { "icon": { @@ -1707,7 +1707,7 @@ ] } ], - "description": "How a vertex is drawn: a shape's name, `\"block\"`, or\n`{ shape | block | gauge | pie | icon | wgsl: options }`. An ellipse by default." + "description": "How a vertex is drawn: a shape's name, `\"block\"`, or\n`{ shape | block | gauge | pie | icon | wgsl: options }`. Defaults to an ellipse." }, "Shape": { "oneOf": [ @@ -1801,11 +1801,11 @@ ] }, "shape": { - "description": "Its shape; an ellipse by default.", + "description": "Its shape. Defaults to an ellipse.", "$ref": "#/$defs/Shape" }, "output": { - "description": "Whether it is an output, which drives its net. Default `false`, an input.", + "description": "Whether it is an output, which drives its net. Defaults to `false` (an input).", "type": "boolean" } }, @@ -1851,11 +1851,11 @@ "type": "object", "properties": { "text": { - "description": "The text field to show, or a `when` whose values are each that field or missing. A row\nwhose text is missing or empty has no label. In a diagram the text reads only data, since the sheet\nsizes blocks around it.", + "description": "The text field to show, or a `when` that returns that field or missing. A row whose text is\nmissing or empty has no label. In a diagram, the text may read only data, since the sheet\nsizes blocks around it.", "$ref": "#/$defs/Expr" }, "size": { - "description": "Font size in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's\n`text.size` unless set.", + "description": "Font size in CSS pixels, or diagram units in a diagram. Defaults to the theme's\n`text.size`.", "type": [ "number", "null" @@ -1863,7 +1863,7 @@ "format": "float" }, "color": { - "description": "Each row's text color; the theme's text color unless set.", + "description": "Each row's text color. Defaults to the theme's text color.", "anyOf": [ { "$ref": "#/$defs/Expr" @@ -1874,7 +1874,7 @@ ] }, "maxWidth": { - "description": "The width at which lines wrap, in the space's unit (units in a diagram, CSS pixels\nelsewhere).", + "description": "The width at which lines wrap, in CSS pixels, or diagram units in a diagram.", "type": [ "number", "null" @@ -1882,7 +1882,7 @@ "format": "float" }, "font": { - "description": "The CSS font family list for the text; the theme's `text.font` unless set.", + "description": "The CSS font family list for the text. Defaults to the theme's `text.font`.", "type": [ "string", "null" @@ -1895,7 +1895,7 @@ ] } ], - "description": "Text for each row, placed beside, inside, or along the row's shape and following it every frame:\na text field, or options. A label shows wherever its row shows and its text fits the room the\nrow gives it, such as inside a block. Labels beside or along their rows never overlap: where they\nwould, the label of the row drawn on top shows, so a mark's `priority` decides, and a dense view\nlabels as many rows as fit. A row whose text is missing or empty has no label, so a `when` choosing\nbetween the text field and missing labels only the rows it names. In a diagram a label is part\nof the drawing: it scales with the zoom, fades out when too small to read, and shows wherever it\nfits its vertex.\n\n```\n# use latkit::{field, when, zoom, Expr, Label, Vertices};\nlet named = when(zoom().ge(6.0)).then(field(\"name\")).otherwise(Expr::missing());\nlet buses = Vertices::of(\"grid\", \"Bus\").position(field(\"lon\"), field(\"lat\"));\nlet labeled = buses.label(Label::new(named));\n```" + "description": "Text for each row, placed beside, inside, or along the row's shape and moving with it: a text\nfield, or options. A label shows when its row is visible and its text fits, for example inside\na block.\n\nLabels beside or along their rows never overlap. Where they would, the row drawn on top keeps\nits label (so `priority` decides), and a dense view labels as many rows as fit. A row whose text\nis missing or empty has no label, so a `when` that returns the field or missing labels only\nsome rows. In a diagram, a label scales with the zoom, fades out when too small to read, and\nshows wherever it fits its vertex.\n\n```\n# use latkit::{field, when, zoom, Expr, Label, Vertices};\nlet named = when(zoom().ge(6.0)).then(field(\"name\")).otherwise(Expr::missing());\nlet buses = Vertices::of(\"grid\", \"Bus\").position(field(\"lon\"), field(\"lat\"));\nlet labeled = buses.label(Label::new(named));\n```" }, "MarkOptions": { "type": "object", @@ -1910,11 +1910,11 @@ "type": "null" } ], - "description": "Each row's color: a CSS color, a value through a colormap, or a conditional choosing\nbetween colors. Unset, rows take the theme's default for the mark; faces instead\nblend their corners' colors." + "description": "Each row's color: a CSS color, a value through a colormap, or a conditional that\npicks between colors. Defaults to the theme's color for the mark. Faces default to a\nblend of their corners' colors." }, "visible": { "$ref": "#/$defs/Expr", - "description": "Whether each row is drawn and can be picked. Default `true`." + "description": "Whether each row is drawn and can be picked. Defaults to `true`." }, "priority": { "anyOf": [ @@ -1925,7 +1925,7 @@ "type": "null" } ], - "description": "Each row's priority, from its data alone (no view state). Higher-priority rows draw\non top, their labels win where labels would overlap, and they are the last dropped\nwhen a table too large to draw whole is thinned to the tiles in view." + "description": "Each row's priority. It reads only data, not view state. Higher rows draw on top,\nkeep their labels where labels overlap, and are dropped last when a large table is\nthinned." }, "name": { "type": [ @@ -1939,15 +1939,15 @@ "boolean", "null" ], - "description": "Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and\nplot traces do, and geometry (areas, and lines along paths such as coasts, borders,\nand roads) does not." + "description": "Whether rows respond to hover, clicks, and picks. Defaults to on for vertices,\nedges, faces, and plot traces, and off for geometry: areas, and lines along paths\nsuch as coasts, borders, and roads." }, "select": { "type": "boolean", - "description": "Whether clicking a row selects it. Default `true`." + "description": "Whether clicking a row selects it. Defaults to `true`." }, "drag": { "type": "boolean", - "description": "Whether pressing and moving a row drags it, reported as\n[`Event::Drag`](crate::Event), instead of panning the camera. Default `false`." + "description": "Whether pressing and moving a row drags it instead of panning the camera. Drags are\nreported as [`Event::Drag`](crate::Event). Defaults to `false`." }, "label": { "anyOf": [ @@ -1965,7 +1965,7 @@ "string", "null" ], - "description": "A legend of the mark's colors at the view's bottom right, under this title: a ticked\ncolormap across its domain, a cell for each category, or a swatch beside each name\nof a map's `categories`. `color` must map through a colormap with a fixed domain or\nnamed categories; in a conditional, `otherwise` is searched before the cases." + "description": "A legend of the mark's colors at the view's bottom right, with this title. It shows\na ticked colormap across its domain, a cell for each category, or a swatch for each\nname in the colormap's `categories`. `color` must map through a colormap with a\nfixed domain or named categories. In a conditional, `otherwise` is searched before\nthe cases." } } }, @@ -2030,15 +2030,15 @@ "type": "null" } ], - "description": "How each row runs between its ends: geodesic on the earth and straight elsewhere unless set.\n`orthogonal` routes around blocks with rounded bends, in diagrams only." + "description": "How each row runs between its ends. Defaults to geodesic on the earth and straight\nelsewhere. `orthogonal` routes around blocks with rounded bends, in diagrams only." }, "arrows": { "type": "boolean", - "description": "Whether each row has an arrowhead at its second end; a net has one at each input it reaches.\nDefault `false`." + "description": "Whether each row has an arrowhead at its second end. A net has one at each input it\nreaches. Defaults to `false`." }, "poles": { "type": "boolean", - "description": "Draws each row as a pole from its own vertex down to the ground, on a plane or the earth.\nPoles take no `ends`, `bends`, or `joins`. Default `false`." + "description": "Draws each row as a pole from its own vertex down to the ground, on a plane or the earth.\nPoles take no `ends`, `bends`, or `joins`. Defaults to `false`." }, "joins": { "type": [ @@ -2055,7 +2055,7 @@ ], "minItems": 2, "maxItems": 2, - "description": "Draws each row as a net on a plane or the earth, joining the vertex rows of `table` whose\n`reference` field names it: a star from their center to each member.", + "description": "Draws each row as a net on a plane or the earth. The net joins the vertex rows of `table`\nwhose `reference` field names it, as a star from their center to each member.", "tsType": "[table: string, reference: string]" } }, @@ -2065,7 +2065,7 @@ } ], "unevaluatedProperties": false, - "description": "Rows drawn as strokes between the vertices named by two reference fields: branches, links, wires. In a diagram, a row without ends is a net, joining every vertex port whose reference field names it." + "description": "Rows drawn as strokes between two vertices named by reference fields: branches, links, wires. In a diagram, a row without ends is a net. It joins every vertex port whose reference field names it." }, "Route": { "oneOf": [ @@ -2100,15 +2100,15 @@ "type": "null" } ], - "description": "Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere)\nunless given with `px` or `units`. The theme's width unless set." + "description": "Each row's width in CSS pixels, or units in a diagram, unless given with `px` or\n`units`. Defaults to the theme's width." }, "dash": { "$ref": "#/$defs/Expr", - "description": "Whether each row is dashed, with the theme's `dash` period. Default `false`." + "description": "Whether each row is dashed, using the theme's `dash` period. Defaults to `false`." }, "flow": { "$ref": "#/$defs/Expr", - "description": "The speed of comets flowing along each row, per second, in the unit of the theme's\n`flow` lengths. Negative speeds flow backward; 0, the default, draws no comets. When\nthe speed changes, comets continue from where they are." + "description": "The speed of comets along each row, per second, in the unit of the theme's `flow`\nlengths. Negative speeds flow backward. 0 draws no comets. Defaults to 0. When the\nspeed changes, comets continue from where they are." } } }, @@ -2146,7 +2146,7 @@ }, "minItems": 3, "maxItems": 3, - "description": "The reference fields naming each row's three vertices; required. Without a color, a face\nblends the colors of its corners.", + "description": "The reference fields naming each row's three vertices. Required. Without a color, a face\nblends the colors of its corners.", "tsType": "[a: string, b: string, c: string]" } }, @@ -2156,7 +2156,7 @@ } ], "unevaluatedProperties": false, - "description": "Rows drawn as triangles between the vertices named by three reference fields: meshes, surfaces, and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen from above shows its exact colors." + "description": "Rows drawn as triangles between three vertices named by reference fields: meshes, surfaces, and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen from above shows its exact colors." }, "Lines": { "allOf": [ @@ -2190,18 +2190,18 @@ "string", "null" ], - "description": "The path or polygon field each row runs along; the field named `geometry` by default. Not\nallowed on a plot." + "description": "The path or polygon field each row runs along. Defaults to the field named `geometry`. Not\nallowed on a plot." }, "trace": { "type": [ "string", "null" ], - "description": "The sampled field each row traces across the data's axis; plots only, where it is required.\nA field with no frames yet draws nothing until its first frame arrives." + "description": "The sampled field each row traces across the data's axis. Required on a plot, and allowed\nonly there. A field with no frames yet draws nothing until its first frame arrives." }, "interpolation": { "$ref": "#/$defs/Interpolation", - "description": "How each row runs between its samples on a plot; `linear` by default." + "description": "How each row runs between its samples on a plot. Defaults to `linear`." } }, "required": [ @@ -2210,7 +2210,7 @@ } ], "unevaluatedProperties": false, - "description": "Rows drawn as lines along a path field (roads, rivers, borders) or a polygon field's rings (area outlines). On a plot, each row instead traces a sampled field's history across the data's axis, as a monitor does. On the earth, paths follow the globe's curve and stay whole across the antimeridian." + "description": "Rows drawn as lines along a path field (roads, rivers, borders) or a polygon field's rings (area outlines). On a plot, each row traces a sampled field's history across the data's axis, like a monitor. On the earth, paths follow the globe's curve and stay unbroken across the antimeridian." }, "Interpolation": { "oneOf": [ @@ -2261,7 +2261,7 @@ "string", "null" ], - "description": "The polygon field each row fills, holding one polygon's rings or several polygons; the field\nnamed `geometry` by default." + "description": "The polygon field each row fills. It holds one polygon's rings or several polygons. Defaults\nto the field named `geometry`." } }, "required": [ @@ -2270,7 +2270,7 @@ } ], "unevaluatedProperties": false, - "description": "Rows drawn as the areas of a polygon field: parcels, states, lakes, the land and water of a map. The view triangulates each row's polygons once, in the background when they are large. On the earth, areas follow the globe's curve and stay whole across the antimeridian. To outline areas, add a [`Lines`](crate::Lines) mark of the same table, which draws their rings." + "description": "Rows drawn as the areas of a polygon field: parcels, states, lakes, the land and water of a map. The view triangulates each row's polygons once, in the background when they are large. On the earth, areas follow the globe's curve and stay unbroken across the antimeridian. To outline areas, add a [`Lines`](crate::Lines) mark of the same table. It draws their rings." }, "Layout": { "anyOf": [ @@ -2313,7 +2313,7 @@ ] } ], - "description": "How the view places vertex rows that have no position: a name, or\n`{ stress | layered: options }`. In a diagram it places blocks; on a plane or the earth it\nplaces points among the rows they link to. Gaps are in position units: diagram units, plane\nunits, or degrees on the earth.\n\nA row keeps its place once placed: rows added later are placed around the others, and\n[`View::arrange`](crate::View::arrange) lays out every row again." + "description": "How the view places vertex rows that have no position: a name, or\n`{ stress | layered: options }`. In a diagram it places blocks. On a plane or the earth it\nplaces points among the rows they link to. Gaps are in position units: diagram units, plane\nunits, or degrees on the earth.\n\nA placed row keeps its place. Rows added later are placed around the others, and\n[`View::arrange`](crate::View::arrange) lays out every row again." }, "Stress": { "type": "object", @@ -2322,11 +2322,11 @@ "gap": { "type": "number", "format": "double", - "description": "Distance between the centers of linked vertices, in position units. Default 40.", + "description": "Distance between the centers of linked vertices, in position units. Defaults to 40.", "minimum": 0 } }, - "description": "Stress majorization: vertices move until drawn distances match graph distances, then overlapping\nboxes separate, keeping a quarter of `gap` between them." + "description": "Stress majorization: vertices move until drawn distances match graph distances. Then\noverlapping boxes separate, keeping a quarter of `gap` between them." }, "Layered": { "type": "object", @@ -2334,17 +2334,17 @@ "properties": { "direction": { "$ref": "#/$defs/Direction", - "description": "The direction edges point; `right` by default." + "description": "The direction edges point. Defaults to `right`." }, "gap": { "type": "number", "format": "double", - "description": "Distance between vertices within a rank, in position units. Default 30." + "description": "Distance between vertices within a rank, in position units. Defaults to 30." }, "rankGap": { "type": "number", "format": "double", - "description": "Distance between ranks, in position units. Default 80." + "description": "Distance between ranks, in position units. Defaults to 80." } }, "description": "Layered drawing: vertices in ranks, edges pointing along `direction`, and nets ranked from their\noutputs." @@ -2397,7 +2397,7 @@ "tsType": "Color" }, "missing": { - "description": "The color a colormap gives a missing value, such as a row whose field has none.", + "description": "The color a colormap gives a missing value, such as a row with no value in the field.", "type": "string", "tsType": "Color" }, @@ -2452,7 +2452,7 @@ "$ref": "#/$defs/PlotTheme" }, "base": { - "description": "The theme these options change; dark unless set.", + "description": "The theme these options change. Defaults to `dark`.", "enum": [ "dark", "light" @@ -2462,7 +2462,7 @@ "additionalProperties": false } ], - "description": "Default colors and sizes for marks whose options leave them unset or missing, and the look of\ntext, focus, comets, and each space, grouped by subject. Write a base theme's name (`\"dark\"` or\n`\"light\"`), or options over a base (`base`, dark by default); a group's options override only\nthe base values they set.\n\nLengths are in the space's unit (units in a diagram, CSS pixels elsewhere), except the text halo\nand the focus glow, which are always in CSS pixels." + "description": "Default colors and sizes for marks, and the look of text, focus, comets, and each space, grouped\nby subject: a base theme's name (`\"dark\"` or `\"light\"`), or options that change a base. `base`\nnames the base and defaults to `dark`. Each option replaces only the base value it sets.\n\nLengths are in CSS pixels, or units in a diagram. The text halo and the focus glow are always in\nCSS pixels." }, "TextTheme": { "type": "object", @@ -2512,7 +2512,7 @@ "additionalProperties": false, "properties": { "color": { - "description": "The edge color. `None` blends the colors of each edge's two vertices; a diagram's wires and\nedges without `ends` (nets and poles) then take the text color.", + "description": "The edge color. `None` blends the colors of each edge's two vertices. With `None`, a\ndiagram's wires and edges without `ends` (nets and poles) use the text color.", "tsType": "Color", "type": [ "string", @@ -2532,12 +2532,12 @@ "bow": { "type": "number", "format": "float", - "description": "Outside a diagram, how far edges joining the same two vertices bow apart: each lane by this\nfraction of the edge's length at its middle. A family spreads at most eight lanes wide, and\nmore lanes pack closer. A bow is part of the drawing, so it scales with the zoom." + "description": "Outside a diagram, how far edges joining the same two vertices bow apart. Each lane bows out\nby this fraction of the edge's length, measured at its middle. Parallel edges spread at most\neight lanes wide, and more lanes pack closer. Bows scale with the zoom." }, "arrow": { "type": "number", "format": "float", - "description": "How far an arrowhead extends beyond the edge's width: a head is this plus the width long." + "description": "How far an arrowhead extends beyond the edge's width. A head's length is this plus the\nwidth." } }, "description": "Defaults for edges, and how parallel edges and arrowheads look." @@ -2581,7 +2581,7 @@ "type": "string" }, "selected": { - "description": "The glow color around selected rows; `None` glows in each row's own color.", + "description": "The glow color around selected rows. `None` uses each row's own color.", "tsType": "Color", "type": [ "string", @@ -2601,7 +2601,7 @@ "grow": { "type": "number", "format": "float", - "description": "The scale factor for a hovered vertex sized in CSS pixels; vertices sized in units only\nglow." + "description": "The scale factor for a hovered vertex sized in CSS pixels. Vertices sized in units only\nglow." } }, "description": "How hovered and selected rows glow, and how rows outside a selection dim." @@ -2611,7 +2611,7 @@ "additionalProperties": false, "properties": { "color": { - "description": "The comet color. Its alpha sets how strongly a comet covers its stroke; a fully transparent\ncolor turns comets off.", + "description": "The comet color. Its alpha sets how strongly a comet covers its stroke. A fully transparent\ncolor turns comets off.", "tsType": "Color", "type": "string" }, @@ -2626,13 +2626,13 @@ "width": { "type": "number", "format": "float", - "description": "The width of a comet's head, in the space's unit; never narrower than its stroke." + "description": "The width of a comet's head, in the space's unit. A head is never narrower than its stroke." } }, - "description": "How comets flowing along edges and lines look: a round head, brightest at its middle, trailing a\ntail that thins and fades.\n\n`spacing` and `length` must be in the same unit, which also measures each row's `flow` speed. In\nposition units (degrees on the earth), comets are part of the drawing: they scale with the zoom\nand stay in place along their lines. In CSS pixels, they keep their on-screen size and speed. A\nplain number is in the space's unit." + "description": "How comets along edges and lines look. A comet has a round head, brightest at its middle, and a\ntail that thins and fades.\n\n`spacing` and `length` must use the same unit, and each row's `flow` speed is in that unit too.\nIn position units (degrees on the earth), comets scale with the zoom and stay in place along\ntheir lines. In CSS pixels, they keep their size and speed on screen. A plain number is in the\nspace's unit." }, "Length": { - "description": "A length: a number in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in CSS pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the view zooms in.", + "description": "A length: a number in CSS pixels, or units in a diagram. `{ px }` is always CSS pixels. `{ units }` is position units (degrees on the earth), which grow as the view zooms in.", "anyOf": [ { "type": "number" @@ -2642,7 +2642,7 @@ "properties": { "px": { "type": "number", - "description": "CSS pixels, at any zoom." + "description": "CSS pixels, the same at any zoom." } }, "required": [ @@ -2655,7 +2655,7 @@ "properties": { "units": { "type": "number", - "description": "Position units (degrees on the earth), which grow on screen as the view zooms in." + "description": "Position units (degrees on the earth), which grow as the view zooms in." } }, "required": [ @@ -2675,7 +2675,7 @@ "type": "string" }, "outline": { - "description": "The color of the one-unit border around blocks and groups; `None` draws no border.", + "description": "The color of the one-unit border around blocks and groups. `None` draws no border.", "tsType": "Color", "type": [ "string", @@ -2685,7 +2685,7 @@ "corner": { "type": "number", "format": "float", - "description": "The corner radius of blocks and groups; a block marker can set its own." + "description": "The corner radius of blocks and groups. A block marker can set its own." }, "portSize": { "type": "number", @@ -2714,14 +2714,14 @@ "additionalProperties": false, "properties": { "color": { - "description": "The color a shadow darkens toward; its alpha sets how dark the shadow is under the block.", + "description": "The color a shadow darkens toward. Its alpha sets how dark the shadow is under the block.", "tsType": "Color", "type": "string" }, "lift": { "type": "number", "format": "float", - "description": "How far a shadow falls below its block or group; its edge softens over the same distance." + "description": "How far a shadow falls below its block or group. Its edge softens over the same distance." } }, "description": "How a diagram's blocks and groups cast shadows on the sheet, for vertex marks with `shadow` on." @@ -2773,60 +2773,60 @@ "type": "object", "properties": { "pan": { - "description": "Whether dragging the background pans the camera. Default `true`.", + "description": "Whether dragging the background pans the camera. Defaults to `true`.", "type": "boolean" }, "zoom": { - "description": "Which input zooms the view: the wheel and pinching (`wheel`, the default); pinching, and the\nwheel only with Control or Meta held, as trackpad pinches send it, so the plain wheel\nscrolls the page (`modifier`); or nothing (`off`).", + "description": "Which input zooms the view. `wheel` zooms with the wheel and pinching. `modifier` zooms with\npinching, and with the wheel only while Control or Meta is held (as trackpad pinches send\nit), so the plain wheel scrolls the page. `off` turns zooming off. Defaults to `wheel`.", "$ref": "#/$defs/Zoom" }, "turn": { - "description": "Whether dragging with the secondary button, or with Shift, rotates and tilts a map or plane.\nDefault `true`.", + "description": "Whether dragging with the secondary button, or with Shift, rotates and tilts a map or plane.\nDefaults to `true`.", "type": "boolean" }, "keyboard": { - "description": "Whether the keyboard controls the view. Default `true`. Tab and Shift+Tab step through the\nrows nearest the view's center, and arrow keys step to the nearest row in their direction.\nEnter opens the stepped-to row and Space selects it. With no row stepped to, arrow keys pan\n(Shift+arrows turn), `+` and `-` zoom, Home fits everything, and Escape clears the\nselection.", + "description": "Whether the keyboard controls the view. Defaults to `true`. Tab and Shift+Tab step through\nthe rows nearest the view's center. Arrow keys step to the nearest row in their direction.\nEnter opens the row stepped to, and Space selects it. With no row stepped to, arrow keys pan\n(Shift+arrows turn), `+` and `-` zoom, Home fits everything, and Escape clears the\nselection.", "type": "boolean" }, "hover": { - "description": "Whether the hovered row glows, and a hovered vertex sized in CSS pixels grows. Default\n`true`. Rows are hovered either way: [`Event::Hovered`](crate::Event) still fires and\n`hovered()` still reads them.", + "description": "Whether the hovered row glows and a hovered vertex sized in CSS pixels grows. Defaults to\n`true`. Hover is tracked either way: [`Event::Hovered`](crate::Event) still fires and\n`hovered()` still returns the row.", "type": "boolean" }, "pickRadius": { - "description": "How far from the pointer, in CSS pixels, a row can be and still be picked; touch adds 8.\nDefault 6.", + "description": "How far a row can be from the pointer and still be picked, in CSS pixels. Touch adds 8.\nDefaults to 6.", "type": "number", "format": "float" }, "selectBox": { - "description": "Whether Shift+drag selects the rows inside a box instead of turning the camera. Shift+click\nstill toggles one row. Default `false`.", + "description": "Whether Shift+drag selects the rows inside a box instead of turning the camera. Shift+click\nstill toggles one row. Defaults to `false`.", "type": "boolean" }, "connect": { - "description": "Whether the user can draw connections, reported as [`Event::Connect`](crate::Event): by\ndragging from a port, from a vertex with Alt held, or an edge's end off its vertex. Default\n`false`.", + "description": "Whether the user can draw connections by dragging from a port, from a vertex with Alt held,\nor an edge's end off its vertex. Connections are reported as\n[`Event::Connect`](crate::Event). Defaults to `false`.", "type": "boolean" }, "delete": { - "description": "Whether Delete and Backspace report the selection as [`Event::Delete`](crate::Event).\nDefault `false`.", + "description": "Whether Delete and Backspace report the selection as [`Event::Delete`](crate::Event).\nDefaults to `false`.", "type": "boolean" }, "cycle": { - "description": "Whether clicking again at the same point, with the camera unmoved, selects the next row\nunder it, nearest first, so overlapping rows can each be selected. Default `false`.", + "description": "Whether clicking again at the same point, with the camera unmoved, selects the next row\nunder the pointer, nearest first. This lets overlapping rows each be selected. Defaults to\n`false`.", "type": "boolean" }, "press": { - "description": "Seconds a touch or pen must hold still to request a menu ([`Event::Menu`](crate::Event)),\nas the secondary button does; 0 turns this off. Default 0.5.", + "description": "How many seconds a touch or pen must hold still to request a menu\n([`Event::Menu`](crate::Event)), like the secondary button. 0 turns this off. Defaults to\n0.5.", "type": "number", "format": "double" }, "motion": { - "description": "How much the view animates on its own: eases, camera flights, coasting, orbits, comets, and\noptions that read `elapsed()`. `system` (the default) follows the user's reduced-motion\nsetting, `full` animates everything, and `reduced` animates only motion the user drives.", + "description": "How much the view animates on its own: eases, camera flights, coasting, orbits, comets, and\noptions that read `elapsed()`. `system` follows the user's reduced-motion setting, `full`\nanimates everything, and `reduced` animates only motion the user drives. Defaults to\n`system`.", "$ref": "#/$defs/Motion" } }, "additionalProperties": false } ], - "description": "How a view responds to the pointer and keyboard, and how much it animates on its own: a preset\n(`\"none\"` or `\"inspect\"`), or options over the defaults. Each mark controls whether its own rows\npick, select, and drag; these options control the view itself." + "description": "How a view responds to the pointer and keyboard, and how much it animates on its own: a preset\n(`\"none\"` or `\"inspect\"`), or options that change the defaults. Each mark sets whether its own\nrows pick, select, and drag. These options control the view itself." }, "Zoom": { "oneOf": [ @@ -2853,7 +2853,7 @@ { "type": "string", "const": "system", - "description": "Follows the system: reduced when the user asked their system for less motion." + "description": "Follows the system setting: reduced when the user has asked their system for less motion." }, { "type": "string", @@ -3103,7 +3103,7 @@ "remove" ], "additionalProperties": false, - "description": "Removes the rows with these ids; ids that match no row are ignored." + "description": "Removes the rows with these ids. Ids that match no row are ignored." }, { "type": "object", @@ -3209,7 +3209,7 @@ "append" ], "additionalProperties": false, - "description": "Records a frame at each axis coordinate in `at`. Each field gives one value per row per\ncoordinate, frame-major. Covers every row, or only the rows `ids` names; other rows keep\ntheir last values." + "description": "Records a frame at each axis coordinate in `at`. Each field gives one value per row per\ncoordinate, frame-major. Covers every row, or only the rows in `ids`. Other rows keep their\nlast values." }, { "type": "object", @@ -3288,7 +3288,7 @@ "frames" ], "additionalProperties": false, - "description": "Inserts a run of entries of sampled field `field` at level `level`, numbered from `first`,\nas time tiles store them. At level 0 each entry is a frame: one coordinate in `at` and one\nvalue per row in `values`. At level `k` each entry is a bucket of `8^k` frames: its first\nand last frame's coordinates in `at`, and in `values` every bucket's least value per row,\nfollowed by every bucket's greatest. Null is missing." + "description": "Inserts a run of entries of sampled field `field` at level `level`, numbered from `first`,\nin the form time tiles store them. At level 0 each entry is a frame, with one coordinate in\n`at` and one value per row in `values`. At level `k` each entry is a bucket of `8^k` frames.\n`at` holds the coordinates of each bucket's first and last frame. `values` holds every\nbucket's least value per row, followed by every bucket's greatest. Null is missing." } ], "description": "One change in a patch, keyed by the name of the `Patch` method that makes it." @@ -3396,7 +3396,7 @@ "u8", "bool" ], - "description": "The value type of a slice's bytes. Numbers are little endian; `bool` is one byte each, nonzero\nmeaning true." + "description": "The value type of a slice's bytes. Numbers are little endian. `bool` is one byte each, nonzero\nmeaning true." }, "Patch": { "type": "array", @@ -3421,7 +3421,7 @@ "type": "null" } ], - "description": "The rows to read, in this order unless sorted; every row when unset. Ids that name no row\nare skipped." + "description": "The rows to read, in this order unless sorted. Defaults to every row. Ids that name no row\nare skipped." }, "filter": { "anyOf": [ @@ -3449,7 +3449,7 @@ "minItems": 2, "maxItems": 2 }, - "description": "Keys to sort by, each breaking ties left by the keys before it. Rows where a key has no\nvalue sort last; rows equal on every key keep their order." + "description": "Keys to sort by. Each key breaks ties left by the keys before it. Rows where a key has no\nvalue sort last, and rows equal on every key keep their order." }, "offset": { "type": "integer", @@ -3474,7 +3474,7 @@ "additionalProperties": { "$ref": "#/$defs/Expr" }, - "description": "Fields to return, by output name. A plain field returns that field as stored; any other\nexpression returns numbers. Every field when unset; an empty object returns the ids alone." + "description": "Fields to return, by output name. A plain field returns that field as stored, and any other\nexpression returns numbers. Defaults to every field. An empty object returns the ids alone." }, "at": { "type": [ @@ -3482,7 +3482,7 @@ "null" ], "format": "double", - "description": "The axis coordinate at which sampled fields are read; their last frame when unset." + "description": "The axis coordinate at which sampled fields are read. Defaults to their last frame." } }, "required": [ @@ -3525,13 +3525,13 @@ }, "minItems": 2, "maxItems": 2, - "description": "The axis range the spans cover, `[first, last]`: finite, with `first <= last`." + "description": "The axis range the spans cover, `[first, last]`. Both are finite, with `first <= last`." }, "spans": { "type": "integer", "format": "uint", "minimum": 0, - "description": "The number of equal spans to divide the range into; at least 1." + "description": "The number of equal spans to divide the range into. At least 1." }, "ids": { "anyOf": [ @@ -3542,7 +3542,7 @@ "type": "null" } ], - "description": "The rows to read, in this order; every row when unset." + "description": "The rows to read, in this order. Defaults to every row." } }, "required": [ @@ -3551,7 +3551,7 @@ "range", "spans" ], - "description": "A request for each row's least and greatest value of a sampled field over equal spans of the\naxis: field `field` of table `table`, over `spans` equal spans from `range[0]` to `range[1]`,\nfor every row or for the rows `ids` names, in that order. Plain data that deserializes from\nJSON, like a query." + "description": "A request for each row's least and greatest value of a sampled field over equal spans of the\naxis. It reads field `field` of table `table` over `spans` equal spans from `range[0]` to\n`range[1]`, for every row or for the rows in `ids`, in that order. Like a query, it is plain\ndata that deserializes from JSON." }, "Camera": { "anyOf": [ @@ -3580,12 +3580,12 @@ "maxItems": 3 } ], - "description": "Longitude, latitude, and elevation on the earth; x, y, and elevation on a plane or a\ndiagram. In JSON, `[x, y]` is shorthand for elevation 0." + "description": "On the earth, longitude, latitude, and elevation. On a plane or a diagram, x, y, and\nelevation. In JSON, `[x, y]` is shorthand for elevation 0." }, "zoom": { "type": "number", "format": "double", - "description": "The web mercator zoom level on the earth; elsewhere, log₂ of CSS pixels per unit." + "description": "The web mercator zoom level on the earth. Elsewhere, log₂ of CSS pixels per unit." }, "pitch": { "type": "number", @@ -3685,7 +3685,7 @@ "type": "object", "properties": { "min": { - "description": "The least corner.", + "description": "The minimum corner.", "type": "array", "items": { "type": "number", @@ -3695,7 +3695,7 @@ "maxItems": 3 }, "max": { - "description": "The greatest corner.", + "description": "The maximum corner.", "type": "array", "items": { "type": "number", @@ -3777,13 +3777,13 @@ "type": "integer", "format": "uint64", "minimum": 0, - "description": "GPU memory in bytes for the data all views of the `Gpu` draw: its pages and each view's\nper-row state. A view draws a table too large for its share as a paged table, drawing only\nthe pages the camera needs, so it always fits. Pages no view draws stay cached in the\nremaining room, least recently drawn evicted first. Defaults to half the device's largest\nstorage buffer. The page pool grows as data needs it and never shrinks, so set the budget\nbefore drawing data." + "description": "GPU memory in bytes for the data all views of the `Gpu` draw: its pages and each view's\nper-row state. A table too large for a view's share draws as a paged table, with only the\npages the camera needs, so it always fits. Pages no view draws stay cached in the rest of\nthe budget, and the least recently drawn are evicted first. Defaults to half the device's\nlargest storage buffer. The page pool grows as data needs it and never shrinks, so set the\nbudget before drawing data." }, "uploadPerFrame": { "type": "integer", "format": "uint64", "minimum": 0, - "description": "Most bytes uploaded per frame; larger data streams in over several frames. Defaults to\n32 MiB." + "description": "The most bytes uploaded per frame. Larger data streams in over several frames. Defaults to\n32 MiB." } }, "required": [ @@ -3836,7 +3836,7 @@ "required": [ "size" ], - "description": "A picture of views, taken once they are complete as its [`Capture`] says. Each view draws into\nits panel of a `size`-pixel picture at `scale` pixels per CSS pixel. A shot is plain data, like\na scene.\n\n```no_run\n# async fn example(view: &mut latkit::View) -> latkit::Result<()> {\nuse latkit::Shot;\nlet image = Shot::new([3840, 2160]).scale(2.0).image(&mut [view]).await?;\nstd::fs::write(\"view.png\", image.encode(latkit::ImageFormat::Png)?).expect(\"written\");\n# Ok(()) }\n```" + "description": "A picture of views, taken when they are complete, as its [`Capture`] says. Each view draws into\nits panel of a `size`-pixel picture, at `scale` pixels per CSS pixel. A shot is plain data, like\na scene.\n\n```no_run\n# async fn example(view: &mut latkit::View) -> latkit::Result<()> {\nuse latkit::Shot;\nlet image = Shot::new([3840, 2160]).scale(2.0).image(&mut [view]).await?;\nstd::fs::write(\"view.png\", image.encode(latkit::ImageFormat::Png)?).expect(\"written\");\n# Ok(()) }\n```" }, "Panels": { "type": "array", @@ -3985,7 +3985,7 @@ "$ref": "#/outputs/Hit" }, "position": { - "description": "The row's new position in the space's coordinates: the hit point moved with the pointer\nsince the press, at the depth where the drag started, so a vertex keeps its grab offset.\nSnapped to a diagram's dots when its grid snaps.", + "description": "The row's new position in the space's coordinates. It is the hit point moved with the\npointer since the press, at the depth where the drag started, so a vertex keeps its\noffset from the pointer. Snapped to a diagram's grid dots when the grid snaps.", "type": "array", "items": { "type": "number", @@ -4024,7 +4024,7 @@ "type": "object", "properties": { "from": { - "description": "The end it was drawn from: for an edge, the end that stayed.", + "description": "The end it was drawn from. For a dragged edge end, the edge's other end.", "$ref": "#/outputs/End" }, "to": { @@ -4039,7 +4039,7 @@ ] }, "edge": { - "description": "The edge whose end was dragged off; `to` replaces that end.", + "description": "The edge whose end was dragged off. `to` replaces that end.", "anyOf": [ { "$ref": "#/outputs/Row" @@ -4113,7 +4113,7 @@ "type": "object", "properties": { "hit": { - "description": "The row now hovered; `None` once no row is.", + "description": "The row now hovered, or `None` if no row is.", "anyOf": [ { "$ref": "#/outputs/Hit" @@ -4158,7 +4158,7 @@ } }, "trigger": { - "description": "How the user changed it; `None` when the data dropped rows from it.", + "description": "How the user changed it, or `None` when the data dropped rows from it.", "anyOf": [ { "$ref": "#/outputs/Trigger" @@ -4196,7 +4196,7 @@ "type", "detail" ], - "description": "The camera moved by a gesture, fit, flight, coast, or orbit, or back within reach as the\nview's size, insets, or space changed: [`View::camera`](crate::View::camera)." + "description": "The camera moved: [`View::camera`](crate::View::camera). A gesture, fit, flight, coast, or\norbit moved it, or it moved back within its zoom and pan limits after the view's size,\ninsets, or space changed." }, { "type": "object", @@ -4213,7 +4213,7 @@ "type", "detail" ], - "description": "The work the last frame waited on changed; empty once frames are complete:\n[`View::waiting`](crate::View::waiting)." + "description": "The work the last frame waited on changed: [`View::waiting`](crate::View::waiting). Empty\nonce frames are complete." }, { "type": "object", @@ -4230,7 +4230,7 @@ "type", "detail" ], - "description": "Something failed; reported once. A mark that no longer fits its data is also listed in\n[`View::failures`](crate::View::failures)." + "description": "Something failed. Reported once. A mark that no longer fits its data is also listed in\n[`View::failures`](crate::View::failures)." } ], "description": "Something that happened in a view, as [`View::events`](crate::View::events) reports it." @@ -4247,7 +4247,7 @@ "string", "null" ], - "description": "The name of the mark that drew it, if named. It tells two marks of one table apart." + "description": "The name of the mark that drew it, if the mark has one. It tells apart two marks of one\ntable." }, "point": { "type": "array", @@ -4279,7 +4279,7 @@ "string", "null" ], - "description": "The port hit, when a vertex's port was." + "description": "The port hit, if the hit is on a vertex's port." }, "sample": { "anyOf": [ @@ -4290,7 +4290,7 @@ "type": "null" } ], - "description": "The sample hit, when a line tracing a sampled field was." + "description": "The sample hit, if the hit is on a line tracing a sampled field." } }, "required": [ @@ -4388,7 +4388,7 @@ "modifiers", "device" ], - "description": "Where the user acted, with which modifier keys, and with which device. For keyboard input, the\npoint is the row the keys point at, else the pointer, else the view's middle." + "description": "Where the user acted, with which modifier keys and device. For keyboard input, the point is the\nrow the keys point at, or else the pointer, or else the view's middle." }, "Modifiers": { "type": "object", @@ -4433,7 +4433,7 @@ { "type": "string", "const": "touch", - "description": "A finger: picks reach 8 CSS pixels farther." + "description": "A finger. Picks find rows up to 8 CSS pixels farther away." }, { "type": "string", @@ -4536,12 +4536,12 @@ "maxItems": 3 } ], - "description": "Longitude, latitude, and elevation on the earth; x, y, and elevation on a plane or a\ndiagram. In JSON, `[x, y]` is shorthand for elevation 0." + "description": "On the earth, longitude, latitude, and elevation. On a plane or a diagram, x, y, and\nelevation. In JSON, `[x, y]` is shorthand for elevation 0." }, "zoom": { "type": "number", "format": "double", - "description": "The web mercator zoom level on the earth; elsewhere, log₂ of CSS pixels per unit." + "description": "The web mercator zoom level on the earth. Elsewhere, log₂ of CSS pixels per unit." }, "pitch": { "type": "number", @@ -4614,11 +4614,11 @@ } }, "Failure": { - "description": "What failed, as JSON: the form of the npm package's `LatkitError`.", + "description": "A failure as JSON, in the same form as the npm package's `LatkitError`.", "type": "object", "properties": { "code": { - "description": "The kind of failure, as `LatkitError.code` describes it.", + "description": "The kind of failure. See `LatkitError.code`.", "enum": [ "invalid", "unsupported", @@ -4630,11 +4630,11 @@ ] }, "message": { - "description": "What failed, in words.", + "description": "A description of what failed.", "type": "string" }, "path": { - "description": "The invalid part of the input, such as `marks[2].color`, on `invalid` failures.", + "description": "For `invalid` failures, the path of the invalid input, such as `marks[2].color`.", "type": "string" } }, @@ -4662,7 +4662,7 @@ "type": "integer", "format": "uint64", "minimum": 0, - "description": "Rows every mark draws: every row of a table drawn whole, and the rows of the tiles in view\nof a paged table." + "description": "Rows the marks draw: every row of a fully drawn table, and the rows in the tiles in view of\na paged table." }, "draws": { "type": "integer", @@ -4680,13 +4680,13 @@ "type": "integer", "format": "uint64", "minimum": 0, - "description": "Fetched tiles the view draws, of every tile source." + "description": "Fetched tiles the view draws, across all tile sources." }, "tilesLoading": { "type": "integer", "format": "uint64", "minimum": 0, - "description": "Tiles being fetched or decoded, of every tile source." + "description": "Tiles being fetched or decoded, across all tile sources." }, "cpuMs": { "type": "number", @@ -4696,7 +4696,7 @@ "gpuMs": { "type": "number", "format": "float", - "description": "Milliseconds the GPU takes per frame: the median of recently measured frames; 0 if the\ndevice cannot measure." + "description": "Milliseconds the GPU takes per frame: the median of recently measured frames. 0 if the\ndevice cannot measure GPU time." } }, "required": [ @@ -4743,7 +4743,7 @@ "type": "integer", "format": "uint64", "minimum": 0, - "description": "Edge and face rows whose endpoints the GPU resolved to their vertices' resident rows: rows\nthat changed, plus every row of a page that references a vertex now resident elsewhere." + "description": "Edge and face rows whose endpoints the GPU resolved to resident vertex rows. These are rows\nthat changed, plus every row of a page that references a vertex now resident elsewhere." }, "cleared": { "type": "integer", @@ -4834,7 +4834,7 @@ }, "minItems": 2, "maxItems": 2, - "description": "The first and last axis coordinates held by any sampled field; `None` when none holds any." + "description": "The first and last axis coordinates held by any sampled field, or `None` if no field holds\nany." }, "tables": { "type": "array", @@ -4976,13 +4976,13 @@ "type": "integer", "format": "uint64", "minimum": 0, - "description": "GPU memory in bytes for the data all views of the `Gpu` draw: its pages and each view's\nper-row state. A view draws a table too large for its share as a paged table, drawing only\nthe pages the camera needs, so it always fits. Pages no view draws stay cached in the\nremaining room, least recently drawn evicted first. Defaults to half the device's largest\nstorage buffer. The page pool grows as data needs it and never shrinks, so set the budget\nbefore drawing data." + "description": "GPU memory in bytes for the data all views of the `Gpu` draw: its pages and each view's\nper-row state. A table too large for a view's share draws as a paged table, with only the\npages the camera needs, so it always fits. Pages no view draws stay cached in the rest of\nthe budget, and the least recently drawn are evicted first. Defaults to half the device's\nlargest storage buffer. The page pool grows as data needs it and never shrinks, so set the\nbudget before drawing data." }, "uploadPerFrame": { "type": "integer", "format": "uint64", "minimum": 0, - "description": "Most bytes uploaded per frame; larger data streams in over several frames. Defaults to\n32 MiB." + "description": "The most bytes uploaded per frame. Larger data streams in over several frames. Defaults to\n32 MiB." } }, "required": [ diff --git a/npm/schema.mjs b/npm/schema.mjs index 6a6bd22c..6ca4a887 100644 --- a/npm/schema.mjs +++ b/npm/schema.mjs @@ -1,15 +1,16 @@ /** * Generates npm's types from `schema.json`. The types latkit reads (`$defs`) become `inputs.ts`, * and the types it writes (`outputs`) become `outputs.ts`. Each definition reachable from the roots - * becomes a type, documented with its schema description; one that extends shared options becomes - * an interface extending them. An output that an input already declares uses that input's type. A - * schema's `tsType` names a hand-written type to use instead, such as `ExprInput`. + * becomes a type, documented with its schema description. A definition that extends shared options + * becomes an interface that extends them. An output that an input already declares uses the + * input's type. A schema's `tsType` names a hand-written type to use in its place, such as + * `ExprInput`. */ import fs from 'node:fs'; import path from 'node:path'; import prettier from 'prettier'; -/** Root types npm takes as plain data. Every type they are built from is generated too. */ +/** Root types npm takes as plain data. The types they are built from are generated too. */ const INPUTS = ['Scene', 'Query', 'EnvelopeRequest', 'Camera', 'Fit', 'Budget', 'ColormapKind']; /** Root types npm reads back from latkit. */ @@ -28,8 +29,10 @@ const OUTPUTS = [ /** Hand-written types that a `tsType` names, keyed by the module that declares them. */ const IMPORTS = { './data.js': ['TableInput'], './scene.js': ['Color', 'ExprInput'] }; -/** Returns the sources of `inputs.ts` and `outputs.ts` for the schema at `file`, formatted with the - * repository's Prettier config. */ +/** + * Returns the sources of `inputs.ts` and `outputs.ts` for the schema at `file`, formatted with the + * repository's Prettier config. + */ export async function typescript(file) { const schema = JSON.parse(fs.readFileSync(file, 'utf8')); const inputs = declare(schema.$defs, '#/$defs/', INPUTS, new Set()); @@ -60,9 +63,9 @@ export async function typescript(file) { } /** - * Declares the definitions in `defs` that `roots` reach, where refs to them start with `prefix`. - * Names in `declared` are declared by another module. They are not declared again, and those used - * are returned as `borrowed` for import. + * Declares the definitions in `defs` that `roots` reach. Refs to them start with `prefix`. Names + * in `declared` belong to another module. They are not declared again, and the ones used are + * returned as `borrowed` for import. */ function declare(defs, prefix, roots, declared) { const output = prefix === '#/outputs/'; @@ -115,8 +118,9 @@ function declare(defs, prefix, roots, declared) { } }; // A union member's description moves onto the single key of a tagged variant, where editors show - // it. Any other member keeps its description as a doc comment before it, which the reference - // pages read: what each value of an enumeration means, and each form of a type with several. + // it. Any other member keeps its description as a doc comment before it, for the reference pages + // to read. That covers what each value of an enumeration means, and each form of a type with + // several. const members = (schemas) => { const parts = schemas.map((s) => { const keys = Object.keys(s.properties ?? {}); @@ -148,8 +152,8 @@ function declare(defs, prefix, roots, declared) { } return /[|&]/.test(item) ? `(${item})[]` : `${item}[]`; }; - // latkit omits absent values from its output rather than writing null, so optional output - // fields drop their null. + // latkit leaves absent values out of its output and never writes null for them, so optional + // output fields drop their null. const absent = (schema) => { if (Array.isArray(schema.type)) return { ...schema, type: schema.type.filter((t) => t !== 'null') }; @@ -174,8 +178,8 @@ function declare(defs, prefix, roots, declared) { return `{ ${lines.join('\n')} }`; }; - // An object that extends shared options, `allOf` references and its own object, is an interface - // extending theirs. + // An object that extends shared options (`allOf` of references and its own object) becomes an + // interface that extends theirs. const extending = (schema) => { const parts = schema.allOf ?? []; const own = parts.filter((p) => ref(p) === undefined); @@ -227,8 +231,8 @@ function named(code) { /** * Converts a schema description into a doc comment. It keeps paragraphs, rewraps lines to at most - * 96 columns, turns Rust's intra-doc links into plain code spans, renames Rust names to npm's, - * writes `None` as `null` (as JSON does), and drops code blocks, which are Rust examples. + * 96 columns, turns Rust's intra-doc links into plain code spans, renames Rust names to npm's, and + * writes `None` as `null` (as JSON does). It drops code blocks, since they are Rust examples. */ function doc(text) { const lines = []; diff --git a/npm/src/canvas.ts b/npm/src/canvas.ts index ba0488eb..2a5b3129 100644 --- a/npm/src/canvas.ts +++ b/npm/src/canvas.ts @@ -1,4 +1,4 @@ -/** Canvases, which views create: the views shown in one `` or `OffscreenCanvas`. */ +/** Canvases. A canvas shows views in one `` or `OffscreenCanvas`. Views create them. */ import type * as bindings from '../pkg/latkit.js'; import { Handle, inner } from './internal.js'; @@ -11,11 +11,11 @@ export type Rect = readonly [x: number, y: number, width: number, height: number export type Target = HTMLCanvasElement | OffscreenCanvas; /** - * @internal Shows views in a `` element or an `OffscreenCanvas`, each in its region, later - * views over earlier ones. A `` element sizes itself to its CSS box at the device pixel - * ratio and passes pointer, wheel, and key input to the view under the pointer. A frame redraws - * only the views that need it, or every view after a resize or a change of views. It is destroyed - * with its last view, and the element keeps its last frame. + * @internal Shows views in a `` element or an `OffscreenCanvas`, each in its region, with + * later views over earlier ones. A `` element sizes itself to its CSS box at the device + * pixel ratio and passes pointer, wheel, and key input to the view under the pointer. A frame + * redraws only the views that need it, or every view after a resize or a change of views. The + * canvas is destroyed with its last view, and the element keeps its last frame. */ export class Canvas extends Handle { #views: View[] = []; @@ -31,7 +31,7 @@ export class Canvas extends Handle { this.#released = released; } - /** Creates a canvas of `device` in `target`; `released` runs once it is destroyed. */ + /** Creates a canvas on `device` in `target`. `released` runs when the canvas is destroyed. */ static made( device: bindings.Gpu, target: Target, @@ -58,7 +58,7 @@ export class Canvas extends Handle { this.relayout(); } - /** Stops showing `view`; with no view left, destroys the canvas. */ + /** Stops showing `view`. Destroys the canvas when no view is left. */ remove(view: View): void { this.#views = this.#views.filter((shown) => shown !== view); if (this.#views.length) this.relayout(); @@ -82,13 +82,13 @@ export type Placed = | { /** The view drawn. */ readonly view: View; - /** Where it draws in the picture, in place of its own region. */ + /** Where it draws in the picture, overriding its own region. */ readonly region: Rect; }; /** - * @internal Views as the bindings take them: each view's handle, and its region's four fractions - * in one array. + * @internal Encodes views for the bindings: their handles, and their regions as four fractions + * each in one array. */ export function encoded(views: readonly Placed[]): [bindings.Handle[], Float32Array] { const placed = views.map((v) => ('view' in v ? v : { view: v, region: v.region })); diff --git a/npm/src/colormap.ts b/npm/src/colormap.ts index 2b79caae..1f7eebb9 100644 --- a/npm/src/colormap.ts +++ b/npm/src/colormap.ts @@ -5,7 +5,7 @@ import { wasm } from './internal.js'; /** * Returns a CSS `linear-gradient(...)` with the colors the GPU draws for `map`, a catalog name or a - * custom colormap. `direction` is the gradient's CSS direction, `'to right'` by default. A + * custom colormap. `direction` is the CSS gradient direction and defaults to `'to right'`. A * categorical map gives one solid band per category. */ export function colormapCss(map: Colormap, direction = 'to right'): string { @@ -13,8 +13,8 @@ export function colormapCss(map: Colormap, direction = 'to right'): string { } /** - * The catalog's colormaps, in order: each one's name, its title for people, such as - * `'Red / Blue'`, and its kind. + * Returns the catalog's colormaps in order, each with its name, its label such as `'Red / Blue'`, + * and its kind. */ export function colormaps(): { name: ColormapName; label: string; kind: ColormapKind }[] { return JSON.parse(wasm().colormaps()) as ReturnType; diff --git a/npm/src/data.ts b/npm/src/data.ts index 4b72e2c6..c7515975 100644 --- a/npm/src/data.ts +++ b/npm/src/data.ts @@ -8,8 +8,8 @@ import type { Description, Value } from './outputs.js'; import { framed, Patch, type Fields, type RowIds, type Run, type Values } from './patch.js'; /** - * A table of a dataset, which a mark draws or a field reads from: `grid.table('Bus')`. A view draws - * the datasets its scene's tables belong to. Its JSON form is `[data, table]`. + * A table in a dataset, such as `grid.table('Bus')`. Marks draw its rows and `field().from()` reads + * its fields. A view loads every dataset its scene uses. In JSON it is `[data, table]`. */ export class Table { /** The dataset the table belongs to. */ @@ -17,21 +17,21 @@ export class Table { /** The table's name. */ readonly name: string; - /** Table `name` of `source`. */ + /** The table `name` in `source`. */ constructor(source: Source, name: string) { this.source = source; this.name = name; } - /** The JSON form, `[data, table]`, which `JSON.stringify` writes for the table. */ + /** Returns the JSON form, `[data, table]`. `JSON.stringify` calls it. */ toJSON(): [data: string, table: string] { return [this.source.name, this.name]; } } /** - * A table that a mark draws or a field reads from: a dataset's {@link Table}, or its JSON form - * `[data, table]`, which names a dataset the view's `sources` give. + * A table for a mark to draw or a field to read: a {@link Table}, or its JSON form `[data, table]`, + * where `data` names a dataset in the view's `sources`. */ export type TableInput = Table | readonly [data: string, table: string]; @@ -42,20 +42,20 @@ export interface QueryResult { /** The ids of the rows returned, in order. */ ids: Id[]; /** - * Each selected field's column, by output name: numbers as a `Float64Array` with NaN for missing - * values, other values as an array with `null` for missing ones, and a reference as the table it - * references and the id of the row each row references. + * Each selected field's column, keyed by output name. Numbers come as a `Float64Array` with NaN + * for missing values. Other values come as an array with `null` for missing ones. A reference + * comes as the referenced table and, for each row, the id of the row it references. */ fields: Record; - /** How many rows matched the query before `offset` and `limit` applied. */ + /** The number of rows that matched, before `offset` and `limit`. */ total: number; } -/** Each row's minimum and maximum of a sampled field over equal spans of the axis. */ +/** The minimum and maximum of a sampled field for each row, over equal spans of the axis. */ export interface Envelope { /** The span boundaries on the axis: one more than the number of spans. */ at: Float64Array; - /** Each row's minimum in each span, `spans` values per row, row after row; NaN without frames. */ + /** Each row's minimum in each span, `spans` values per row, row after row. NaN for no value. */ least: Float32Array; /** Each row's maximum in each span, laid out like `least`. */ greatest: Float32Array; @@ -65,29 +65,29 @@ export interface Envelope { type Reads = bindings.Data | bindings.Snapshot; /** - * Named tables of columns, read the same way from a {@link Data}, which reads its current version, - * and a {@link Snapshot}, which reads its own. + * Named tables of columns. A {@link Data} reads its current version and a {@link Snapshot} reads + * the version it holds, through the same methods. */ export abstract class Dataset extends Handle { - /** The dataset's name: the first part of its tables' JSON form. */ + /** The dataset's name: the `data` in its tables' JSON form `[data, table]`. */ get name(): string { return inner(this).name; } - /** The dataset's table `name`, for a mark to draw or a field to read from. */ + /** Returns table `name`, for a mark to draw or a field to read. */ table(name: string): Table { return new Table(this as Dataset as Data | Snapshot, name); } - /** The tables and their row counts, each field and its kind, and the axis and its span. */ + /** Describes the tables and row counts, each field and its kind, and the axis and its span. */ describe(): Description { return JSON.parse(inner(this).describe()) as Description; } /** - * Reads `field` of the row with `id` in `table`: for a sampled field, its last frame at or before - * `options.at`, or its last frame; for a reference, the referenced row's id. `null` when the - * table, row, field, or value is missing. + * Reads `field` of row `id` in `table`. A sampled field gives its last frame at or before + * `options.at`, or its last frame without `at`. A reference gives the referenced row's id. + * Returns `null` when the table, row, field, or value is missing. */ get( table: string, @@ -102,35 +102,36 @@ export abstract class Dataset extends Handle { } /** - * Reads a table's rows, filtered, sorted, and paged, through the same expressions marks use. - * Sampled fields are read at `query.at`. + * Reads a table's rows, filtered, sorted, and paged with the same expressions marks use. Sampled + * fields are read at `query.at`. */ query(query: Query): QueryResult { return inner(this).query(JSON.stringify(query)) as QueryResult; } /** - * Each row's minimum and maximum of a sampled field over equal spans of an axis range, for every - * row or the rows `request.ids` lists. Each span reads the coarsest stored levels of detail that - * fit in it, so it costs a few reads however many frames it covers. Throws a `limit` error when - * rows times spans exceeds 2^26. + * Returns the minimum and maximum of a sampled field over equal spans of an axis range, for every + * row or for the rows in `request.ids`. Each span reads the coarsest stored levels of detail that + * fit inside it, so it costs a few reads however many frames it covers. Throws a `limit` error + * when rows times spans exceeds 2^26. */ envelope(request: EnvelopeRequest): Envelope { return inner(this).envelope(JSON.stringify(request)) as Envelope; } - /** The dataset as latkit's bytes: a patch that builds it from an empty dataset. */ + /** Returns the dataset as latkit's bytes: a patch that builds it from an empty dataset. */ toBytes(): Uint8Array { return inner(this).toBytes(); } } /** - * A dataset that changes in place, each change applied whole or not at all. Each method of - * {@link Patch} is also a method here that applies that one change at once; `apply` applies a patch - * of several. Views draw its latest version from the frame after each change. A {@link Snapshot} - * keeps one version and shares its pages with the dataset, so an undo history costs only the pages - * that differ. Destroying it leaves views drawing it and its snapshots usable. + * A dataset that changes in place. Each change applies all at once or not at all. Every + * {@link Patch} method is also a method here that applies that one change right away, and `apply` + * applies a patch of several. Views draw the latest version from the frame after each change. A + * {@link Snapshot} keeps one version and shares its pages with the dataset, so an undo history + * costs only the pages that differ. Destroying a `Data` leaves the views that draw it, and its + * snapshots, usable. * * ```ts * const grid = new Data('grid').set('Bus', [1, 2], { kv: [230, 115] }); @@ -153,15 +154,15 @@ export class Data extends Dataset { * - GeoJSON, plain or gzip-compressed, read in a worker. Points, line strings, and polygons * become rows of tables `points`, `lines`, and `polygons`, with the geometry in field * `geometry` and each property as a field. - * - A dataset document, `{"name": ..., "patch": [...]}`, whose patch is read as - * {@link Patch.read} reads one. + * - A dataset document, `{"name": ..., "patch": [...]}`. Its patch is read like + * {@link Patch.read}. */ static async read( source: ByteSource | object, options: { - /** Names the dataset, replacing the name its bytes or document give; GeoJSON requires it. */ + /** The dataset's name, overriding the name in its bytes or document. Required for GeoJSON. */ name?: string; - /** The URL a document's file slices are relative to; by default, the one it came from. */ + /** The URL that a document's file slices are relative to. Defaults to the document's URL. */ base?: string; } & Wait = {}, ): Promise { @@ -186,8 +187,8 @@ export class Data extends Dataset { } /** - * Applies {@link Patch.replace}: replaces table `table` with rows `ids`, or `0` to `n - 1`, and - * `fields`. + * Applies {@link Patch.replace}: replaces table `table` with `fields` for rows `ids` (or `0` to + * `n - 1`). */ replace(table: string, fields: Fields): this; replace(table: string, ids: RowIds, fields?: Fields): this; @@ -197,8 +198,8 @@ export class Data extends Dataset { } /** - * Applies {@link Patch.set}: writes `fields` for rows `ids` of `table`, or `0` to `n - 1`, adding - * new rows. + * Applies {@link Patch.set}: writes `fields` for rows `ids` (or `0` to `n - 1`) of `table`, + * adding new rows. */ set(table: string, fields: Fields): this; set(table: string, ids: RowIds, fields?: Fields): this; @@ -217,7 +218,7 @@ export class Data extends Dataset { at: number | readonly number[], fields: Readonly>, options?: { - /** The rows the frames cover, every row by default; other rows keep their last values. */ + /** The rows the frames cover. Defaults to every row. Other rows keep their last values. */ ids?: RowIds; }, ): this { @@ -229,17 +230,17 @@ export class Data extends Dataset { return this.apply(new Patch().forget(before)); } - /** Applies {@link Patch.frames}: stores a run of a sampled field's entries, as a time tile holds. */ + /** Applies {@link Patch.frames}: stores a run of a sampled field's entries, as in a time tile. */ frames(table: string, field: string, run: Run): this { return this.apply(new Patch().frames(table, field, run)); } - /** The current version as a {@link Snapshot}, which later patches leave unchanged. O(1). */ + /** Returns the current version as a {@link Snapshot}. Later patches don't change it. O(1). */ snapshot(): Snapshot { return Snapshot.made(inner(this).snapshot()); } - /** Makes `snapshot`, a version of this dataset, the current version, as an undo does. */ + /** Makes `snapshot`, a version of this dataset, the current version. This is how to undo. */ restore(snapshot: Snapshot): this { inner(this).restore(inner(snapshot)); return this; @@ -248,7 +249,7 @@ export class Data extends Dataset { /** * One immutable version of a dataset, taken in O(1) by {@link Data.snapshot}. A view given a - * snapshot draws that version, and keeps drawing it once the snapshot is destroyed. + * snapshot draws that version, and keeps drawing it after the snapshot is destroyed. */ export class Snapshot extends Dataset { private constructor(bound: bindings.Snapshot) { @@ -262,23 +263,23 @@ export class Snapshot extends Dataset { } /** - * A dataset fetched in pieces as views need them: plain data, a dataset name and a URL. Each view - * draws the pieces its camera and plot window need, and the views of a GPU share them. The first - * bytes at `url` decide the kind: + * A dataset fetched in pieces as views need them. It is plain data: a dataset name and a URL. Each + * view fetches the pieces its camera and plot window need, and views on one GPU share them. The + * first bytes at `url` decide the kind: * - * - Time tiles: a `frames.json` manifest as `latkit tile` writes it. A line tracing a sampled field - * fetches the detail its plot window shows; other marks fetch the frames around the view's `at`. - * A span still loading shows the coarser span that contains it. + * - Time tiles: a `frames.json` manifest written by `latkit tile`. A line that traces a sampled + * field fetches the detail its plot window shows. Other marks fetch the frames around the view's + * `at`. While a span loads, the coarser span that contains it shows. * - Vector tiles: a PMTiles archive, a TileJSON document, or a `{z}/{x}/{y}` URL template. Each * layer becomes a table of the features its marks draw: polygons for areas, lines for lines, * points for vertices. A table named `layer:polygons`, `layer:lines`, or `layer:points` holds - * that kind whatever the marks. A tile arriving appends its rows to the view's dataset, which - * keeps the rows of tiles it no longer draws until they outnumber the rows it draws. + * that kind regardless of the marks. Each tile that arrives appends its rows to the view's + * dataset. Rows of tiles no longer drawn are kept until they outnumber the rows drawn. */ export class Tiles { /** The name of the dataset the tiles are drawn as. */ readonly name: string; - /** Where the tiles are. */ + /** The tiles' URL. */ readonly url: string; /** Tiles at `url`, drawn as the dataset named `name`. */ @@ -287,7 +288,7 @@ export class Tiles { this.url = url; } - /** The tiles' table `name`, such as a vector tile layer, for a mark to draw. */ + /** Returns table `name`, such as a vector tile layer, for a mark to draw. */ table(name: string): Table { return new Table(this, name); } diff --git a/npm/src/encoder.ts b/npm/src/encoder.ts index 87b549e1..c3ef8a9d 100644 --- a/npm/src/encoder.ts +++ b/npm/src/encoder.ts @@ -1,5 +1,5 @@ /** - * The H.264 encoder latkit's videos use in a browser, built on WebCodecs. It takes NV12 pictures + * The H.264 encoder for latkit's videos in a browser, built on WebCodecs. It takes NV12 pictures * (BT.709, limited range) and returns Annex B units in order, each `[time, key, bytes]`, with * `time` in 90 kHz ticks. It prefers hardware encoding and uses the lowest H.264 level that fits * the size and frame rate. @@ -60,9 +60,9 @@ function configurations( /** * Creates an encoder for `width`×`height` pictures at `fps` frames per second and `bitrate` bits - * per second. Throws an `unsupported` error when the browser has no `VideoEncoder`. Its calls - * reject with an `unsupported` error when the browser encodes no H.264 at that size and rate, and - * with an `io` error when encoding fails. + * per second. Throws an `unsupported` error when the browser has no `VideoEncoder`. Its methods + * reject with an `unsupported` error when the browser cannot encode H.264 at that size and rate, + * and with an `io` error when encoding fails. */ export function encoder(width: number, height: number, fps: number, bitrate: number) { if (typeof VideoEncoder !== 'function') diff --git a/npm/src/gpu.ts b/npm/src/gpu.ts index 66d0a080..671aeadc 100644 --- a/npm/src/gpu.ts +++ b/npm/src/gpu.ts @@ -1,4 +1,4 @@ -/** The GPU device: it draws views on their canvases, owns their shared memory, and runs frames. */ +/** The GPU device. It draws views on their canvases, holds their shared memory, and runs frames. */ import type * as bindings from '../pkg/latkit.js'; import { Canvas, type Target } from './canvas.js'; @@ -9,13 +9,13 @@ import type { Work } from './outputs.js'; import type { View } from './view.js'; /** - * A WebGPU device that views draw with. The views of one `Gpu` share its memory and draw together - * in each animation frame, so linked views always show the same moment. Views use the first `Gpu` - * created unless given another, and `View.create` creates it when there is none. Destroying it - * destroys its views and frame listeners; other GPUs and datasets stay usable. + * A WebGPU device that views draw with. Views on one `Gpu` share its memory and draw together in + * each animation frame, so linked views always show the same moment. Views use the first `Gpu` + * created unless given another, and `View.create` creates one if none exists. Destroying a `Gpu` + * destroys its views and frame listeners. Other GPUs and datasets stay usable. */ export class Gpu extends Handle { - /** The GPU views use when given none, and the request for it while one is being created. */ + /** The default GPU for views, and the pending request while it is being created. */ static #shared: Gpu | undefined; static #creating: Promise | undefined; @@ -28,9 +28,9 @@ export class Gpu extends Handle { } /** - * Requests a WebGPU device; the first call takes the one `load()` requested. Create one before - * any view to set its `budget` first, or another for views that should not share memory with the - * rest. Rejects with an `unsupported` error when the browser has no WebGPU device. + * Requests a WebGPU device. The first call takes the device `load()` requested. Create one before + * any view to set its `budget` first, or create another for views that should not share memory + * with the rest. Rejects with an `unsupported` error when the browser has no WebGPU device. */ static async create(): Promise { await load(); @@ -39,7 +39,7 @@ export class Gpu extends Handle { return gpu; } - /** @internal The GPU views use when given none, created when there is none. */ + /** @internal Returns the default GPU for views, creating it if there is none. */ static shared(): Promise { const shared = Gpu.#shared; if (shared && !shared.lost) return Promise.resolve(shared); @@ -47,9 +47,9 @@ export class Gpu extends Handle { } /** - * Calls `listener` every animation frame with the frame's timestamp in milliseconds, before the - * views draw, so changes it makes show in that frame. While any listener is registered, a frame - * runs every animation frame. Returns a function that removes the listener. + * Calls `listener` every animation frame with the frame's timestamp in milliseconds. It runs + * before the views draw, so its changes show in that frame. While any listener is registered, a + * frame runs on every animation frame. Returns a function that removes the listener. */ on(type: 'frame', listener: (now: number) => void): () => void { const handle = device(this).onFrame(listener); @@ -60,14 +60,14 @@ export class Gpu extends Handle { return stop; } - /** Whether the device was lost. Drawing then does nothing; create a new `Gpu`. */ + /** Whether the device was lost. Drawing then does nothing, so create a new `Gpu`. */ get lost(): boolean { return device(this).lost; } /** - * The GPU memory budget for data, shared by every view of this GPU; see {@link Budget}. Defaults - * to half the device's largest storage buffer, and 32 MiB uploaded per frame. + * The GPU memory budget for data, shared by every view on this GPU. See {@link Budget}. Defaults + * to half the device's largest storage buffer, with 32 MiB uploaded per frame. */ get budget(): Budget { return JSON.parse(device(this).budget) as Budget; @@ -78,27 +78,28 @@ export class Gpu extends Handle { } /** - * Exact counts of the work this GPU has done since it was created; see {@link Work}. The counts - * repeat for the same budget, device limits, and frames, so tests and benchmarks can assert them. + * Exact counts of the work this GPU has done since it was created. See {@link Work}. The same + * budget, device limits, and frames give the same counts, so tests and benchmarks can assert + * them. */ get work(): Work { return JSON.parse(device(this).work) as Work; } - /** Bytes of GPU memory its views draw from: data pages, and each view's per-row data. */ + /** Bytes of GPU memory the views draw from: data pages and each view's per-row data. */ get resident(): number { return device(this).resident; } - /** Bytes waiting to upload to the GPU, across its views. */ + /** Bytes waiting to upload to the GPU, across all its views. */ get uploading(): number { return device(this).uploading; } /** - * Draws one frame now, at `now` milliseconds on the `requestAnimationFrame` clock: it runs the - * frame listeners, then draws every canvas that needs a frame. After the first call, the GPU - * requests no more animation frames, so a test or benchmark that steps every frame draws the + * Draws one frame now, at `now` milliseconds on the `requestAnimationFrame` clock. It runs the + * frame listeners, then draws every canvas that needs a frame. After the first call the GPU + * stops requesting animation frames, so a test or benchmark that steps every frame draws the * same frames on any device. */ step(now: number): void { @@ -107,16 +108,16 @@ export class Gpu extends Handle { /** * Resolves once the GPU has finished its submitted work, every readback has arrived, and every - * pipeline a view or shot requested has compiled. A frame stepped after that uses all of it. + * pipeline a view or shot requested has compiled. A frame stepped after that uses all of them. */ async done(wait: Wait = {}): Promise { await waited(device(this).done(), wait, 'waiting for the GPU'); } /** - * Resolves when, after this call, work a view is waiting on arrives (a readback, a pipeline - * compile, a job, a label text, or a fetch), or a view requests a frame. A harness that steps - * frames awaits it to know when to step again. + * Resolves when work a view waits on arrives after this call (a readback, a pipeline compile, a + * job, a label text, or a fetch), or when a view requests a frame. A harness that steps frames + * awaits it to know when to step again. */ async landed(wait: Wait = {}): Promise { await waited(device(this).landed(), wait, 'waiting for work'); diff --git a/npm/src/index.ts b/npm/src/index.ts index 15d08115..f41a45f3 100644 --- a/npm/src/index.ts +++ b/npm/src/index.ts @@ -4,10 +4,9 @@ * latkit: columnar data drawn with WebGPU. * * Scenes, expressions, and patches are plain data and need no WebAssembly. Datasets, views, - * pictures, and the colormap functions need it, so `await load()` first. Every `Handle` also works - * with `using`. The references above make the browser types latkit names (canvases, `Blob`, - * `Response`, `WebAssembly.Module`) and `Symbol.dispose` typecheck whatever a project's `lib` - * setting, in Node.js too. + * pictures, and the colormap functions do, so call `await load()` first. Every `Handle` works with + * `using`. The references above let the browser types latkit uses (canvases, `Blob`, `Response`, + * `WebAssembly.Module`) and `Symbol.dispose` typecheck under any `lib` setting, in Node.js too. */ export type * from './inputs.js'; export type * from './outputs.js'; diff --git a/npm/src/inputs.ts b/npm/src/inputs.ts index db7ada86..30a99d23 100644 --- a/npm/src/inputs.ts +++ b/npm/src/inputs.ts @@ -12,15 +12,15 @@ export interface AreaTheme { /** * Rows drawn as the areas of a polygon field: parcels, states, lakes, the land and water of a map. * The view triangulates each row's polygons once, in the background when they are large. On the - * earth, areas follow the globe's curve and stay whole across the antimeridian. To outline areas, - * add a `Lines` mark of the same table, which draws their rings. + * earth, areas follow the globe's curve and stay unbroken across the antimeridian. To outline + * areas, add a `Lines` mark of the same table. It draws their rings. */ export interface Areas extends MarkOptions { /** The dataset and table drawn, `[data, table]`. */ areas: TableInput; /** - * The polygon field each row fills, holding one polygon's rings or several polygons; the field - * named `geometry` by default. + * The polygon field each row fills. It holds one polygon's rings or several polygons. Defaults to + * the field named `geometry`. */ geometry?: string | null; } @@ -29,20 +29,20 @@ export interface Areas extends MarkOptions { export interface Axis { /** The axis title. */ label?: string | null; - /** Whether grid lines cross the plot at the ticks. Default `true`. */ + /** Whether grid lines cross the plot at the ticks. Defaults to `true`. */ grid?: boolean; - /** Whether the axis is hidden. Default `false`. */ + /** Whether the axis is hidden. Defaults to `false`. */ hidden?: boolean; /** - * Tick positions, each a value or `[value, label]`; a tick without a label shows its value. Unset, - * ticks fall at round values. + * Tick positions, each a value or `[value, label]`. A tick without a label shows its value. When + * unset, ticks fall at round values. */ ticks?: Tick[] | null; - /** How tick values are formatted; `auto` by default. */ + /** How tick values are formatted. Defaults to `auto`. */ format?: Format; /** - * Decimal places of fixed values, or of the significand in scientific and engineering formats, at - * most 15. Unset, as many as the tick step needs. + * Decimal places of fixed values, or of the significand in scientific and engineering formats. At + * most 15. When unset, as many as the tick step needs. */ precision?: number | null; } @@ -59,20 +59,24 @@ export interface Bitmap { export interface Budget { /** * GPU memory in bytes for the data all views of the `Gpu` draw: its pages and each view's per-row - * state. A view draws a table too large for its share as a paged table, drawing only the pages the - * camera needs, so it always fits. Pages no view draws stay cached in the remaining room, least - * recently drawn evicted first. Defaults to half the device's largest storage buffer. The page - * pool grows as data needs it and never shrinks, so set the budget before drawing data. + * state. A table too large for a view's share draws as a paged table, with only the pages the + * camera needs, so it always fits. Pages no view draws stay cached in the rest of the budget, and + * the least recently drawn are evicted first. Defaults to half the device's largest storage + * buffer. The page pool grows as data needs it and never shrinks, so set the budget before drawing + * data. */ bytes: number; - /** Most bytes uploaded per frame; larger data streams in over several frames. Defaults to 32 MiB. */ + /** + * The most bytes uploaded per frame. Larger data streams in over several frames. Defaults to 32 + * MiB. + */ uploadPerFrame: number; } /** - * An operation on values: an object whose one key names the operation and holds its arguments, - * such as `{ "add": ["$a", 1] }`. `is` and `contains` take a plain text as their second argument, - * which is neither a field nor a color. + * An operation, written as an object with one key: the operation's name, holding its arguments. + * For example `{ "add": ["$a", 1] }`. The second argument of `is` and `contains` is plain text, + * not a field or a color. */ export type CallNode = | { @@ -197,11 +201,11 @@ export type Camera = * its tilt, and its rotation. A diagram is always seen from straight above, upright. */ | { /** - * Longitude, latitude, and elevation on the earth; x, y, and elevation on a plane or a diagram. In - * JSON, `[x, y]` is shorthand for elevation 0. + * On the earth, longitude, latitude, and elevation. On a plane or a diagram, x, y, and elevation. + * In JSON, `[x, y]` is shorthand for elevation 0. */ center: [number, number] | [number, number, number]; - /** The web mercator zoom level on the earth; elsewhere, log₂ of CSS pixels per unit. */ + /** The web mercator zoom level on the earth. Elsewhere, log₂ of CSS pixels per unit. */ zoom: number; /** Tilt in degrees from looking straight down, up to 75. Ignored in a diagram. */ pitch: number; @@ -228,7 +232,7 @@ export type ColormapKind = | /** Around a circle: values wrap. */ 'cyclic' | /** Distinct colors for distinct integers. */ 'categorical'; -/** A colormap of the catalog. */ +/** A colormap from the catalog. */ export type ColormapName = | 'amber' | 'amp' @@ -284,7 +288,7 @@ export interface ColormapNode { /** The value mapped. A categorical map takes each value's integer part as its category. */ value: ExprInput; /** - * The values mapped to the colormap's ends; the extent of the values shown when unset. A + * The values mapped to the colormap's ends. Defaults to the extent of the values shown. A * categorical map ignores it. */ domain?: [low: number, high: number] | null; @@ -292,69 +296,71 @@ export interface ColormapNode { /** * How a view responds to the pointer and keyboard, and how much it animates on its own: a preset - * (`"none"` or `"inspect"`), or options over the defaults. Each mark controls whether its own rows - * pick, select, and drag; these options control the view itself. + * (`"none"` or `"inspect"`), or options that change the defaults. Each mark sets whether its own + * rows pick, select, and drag. These options control the view itself. */ export type Controls = | 'none' | 'inspect' | { - /** Whether dragging the background pans the camera. Default `true`. */ + /** Whether dragging the background pans the camera. Defaults to `true`. */ pan?: boolean; /** - * Which input zooms the view: the wheel and pinching (`wheel`, the default); pinching, and the - * wheel only with Control or Meta held, as trackpad pinches send it, so the plain wheel scrolls - * the page (`modifier`); or nothing (`off`). + * Which input zooms the view. `wheel` zooms with the wheel and pinching. `modifier` zooms with + * pinching, and with the wheel only while Control or Meta is held (as trackpad pinches send it), + * so the plain wheel scrolls the page. `off` turns zooming off. Defaults to `wheel`. */ zoom?: Zoom; /** * Whether dragging with the secondary button, or with Shift, rotates and tilts a map or plane. - * Default `true`. + * Defaults to `true`. */ turn?: boolean; /** - * Whether the keyboard controls the view. Default `true`. Tab and Shift+Tab step through the rows - * nearest the view's center, and arrow keys step to the nearest row in their direction. Enter - * opens the stepped-to row and Space selects it. With no row stepped to, arrow keys pan + * Whether the keyboard controls the view. Defaults to `true`. Tab and Shift+Tab step through the + * rows nearest the view's center. Arrow keys step to the nearest row in their direction. Enter + * opens the row stepped to, and Space selects it. With no row stepped to, arrow keys pan * (Shift+arrows turn), `+` and `-` zoom, Home fits everything, and Escape clears the selection. */ keyboard?: boolean; /** - * Whether the hovered row glows, and a hovered vertex sized in CSS pixels grows. Default `true`. - * Rows are hovered either way: a `'hovered'` event still fires and `hovered` still reads them. + * Whether the hovered row glows and a hovered vertex sized in CSS pixels grows. Defaults to + * `true`. Hover is tracked either way: a `'hovered'` event still fires and `hovered` still returns + * the row. */ hover?: boolean; /** - * How far from the pointer, in CSS pixels, a row can be and still be picked; touch adds 8. Default - * 6. + * How far a row can be from the pointer and still be picked, in CSS pixels. Touch adds 8. Defaults + * to 6. */ pickRadius?: number; /** * Whether Shift+drag selects the rows inside a box instead of turning the camera. Shift+click - * still toggles one row. Default `false`. + * still toggles one row. Defaults to `false`. */ selectBox?: boolean; /** - * Whether the user can draw connections, reported as a `'connect'` event: by dragging from a port, - * from a vertex with Alt held, or an edge's end off its vertex. Default `false`. + * Whether the user can draw connections by dragging from a port, from a vertex with Alt held, or + * an edge's end off its vertex. Connections are reported as a `'connect'` event. Defaults to + * `false`. */ connect?: boolean; - /** Whether Delete and Backspace report the selection as a `'delete'` event. Default `false`. */ + /** Whether Delete and Backspace report the selection as a `'delete'` event. Defaults to `false`. */ delete?: boolean; /** * Whether clicking again at the same point, with the camera unmoved, selects the next row under - * it, nearest first, so overlapping rows can each be selected. Default `false`. + * the pointer, nearest first. This lets overlapping rows each be selected. Defaults to `false`. */ cycle?: boolean; /** - * Seconds a touch or pen must hold still to request a menu (a `'menu'` event), as the secondary - * button does; 0 turns this off. Default 0.5. + * How many seconds a touch or pen must hold still to request a menu (a `'menu'` event), like the + * secondary button. 0 turns this off. Defaults to 0.5. */ press?: number; /** * How much the view animates on its own: eases, camera flights, coasting, orbits, comets, and - * options that read `elapsed`. `system` (the default) follows the user's reduced-motion setting, - * `full` animates everything, and `reduced` animates only motion the user drives. + * options that read `elapsed`. `system` follows the user's reduced-motion setting, `full` animates + * everything, and `reduced` animates only motion the user drives. Defaults to `system`. */ motion?: Motion; }; @@ -364,8 +370,8 @@ export interface CustomColormap { /** How the colors map values. */ kind: ColormapKind; /** - * The colors, blended in OKLab. Each is a color, spaced evenly between its neighbors as in a CSS - * gradient, or a `[place, color]` pair with a place from 0 to 1, in ascending order. A categorical + * The colors, blended in OKLab. Each is a color, spaced evenly between its neighbors like a CSS + * gradient, or a `[place, color]` pair with a place from 0 to 1. Places must ascend. A categorical * map takes one color per category, without places, at most 256. */ colors: (/** A CSS color, such as `#ffb000` or `rebeccapurple`. */ Color | [number, Color])[]; @@ -374,15 +380,15 @@ export interface CustomColormap { } /** - * A flat sheet for diagrams, seen from straight above; the camera pans and zooms but does not - * turn. Positions are in units with y down. Lengths are in units too, a unit being a CSS pixel at - * zoom 0: sizes, padding, ports, strokes, text, and the theme's lengths. Sizes given with `px`, - * the text halo, the focus glow, and the pick radius stay in CSS pixels. Zooming scales the whole - * picture. Only a diagram has ports, groups, nets, and orthogonal routes, and the scene's layout - * places its blocks. + * A flat sheet for diagrams, seen from straight above. The camera pans and zooms but does not + * turn. Positions are in units with y down, and a unit is a CSS pixel at zoom 0. Sizes, padding, + * ports, strokes, text, and the theme's lengths are in units too. Sizes given with `px`, the text + * halo, the focus glow, and the pick radius stay in CSS pixels. Zooming scales the whole picture. + * Only a diagram has ports, groups, nets, and orthogonal routes, and the scene's layout places its + * blocks. */ export interface Diagram { - /** The grid drawn under the marks; dots 20 units apart by default. */ + /** The grid drawn under the marks. Defaults to dots 20 units apart. */ grid?: Grid; } @@ -390,9 +396,9 @@ export interface Diagram { export interface DiagramTheme { /** The fill color of group containers. */ group?: Color; - /** The color of the one-unit border around blocks and groups; `null` draws no border. */ + /** The color of the one-unit border around blocks and groups. `null` draws no border. */ outline?: Color | null; - /** The corner radius of blocks and groups; a block marker can set its own. */ + /** The corner radius of blocks and groups. A block marker can set its own. */ corner?: number; /** The width of ports. */ portSize?: number; @@ -412,31 +418,31 @@ export type Direction = | /** Toward the top. */ 'up'; /** - * The earth: positions are longitude and latitude in degrees. The ground takes the theme's `earth` + * The earth: positions are longitude and latitude in degrees. The ground uses the theme's `earth` * colors. * - * A flat map is one world, drawn once. Its ground ends at the antimeridian and at Web Mercator's - * latitude limit, and the camera zooms out until the whole world fits the view and pans until the - * world's edges meet the view's. On a globe, longitude wraps, and the camera zooms out until the - * globe spans a third of the view's shorter side. + * A flat map shows the world once. Its ground ends at the antimeridian and at Web Mercator's + * latitude limit. The camera zooms out until the whole world fits the view, and pans until the + * world's edges meet the view's edges. On a globe, longitude wraps, and the camera zooms out until + * the globe spans a third of the view's shorter side. */ export interface Earth { - /** A flat web mercator map (`mercator`, the default) or a globe (`globe`). */ + /** A flat web mercator map (`mercator`) or a globe (`globe`). Defaults to `mercator`. */ projection?: Projection; /** - * Whether lines of longitude and latitude cross the ground, in the theme's `grid` color. Default - * `true`. + * Whether lines of longitude and latitude cross the ground, in the theme's `grid` color. Defaults + * to `true`. */ graticule?: boolean; /** * Lights the earth by the sun's position this many seconds after the Unix epoch, plus the view's * axis coordinate when it has one. The night side darkens the ground and everything on it, down to - * the theme's `earth.night` brightness. Unset, the earth is lit evenly. + * the theme's `earth.night` brightness. When unset, the earth is lit evenly. */ daylight?: number | null; /** * Whether air glows around the globe, in the theme's `earth.atmosphere` color, brighter on the day - * side. Default `false`. + * side. Defaults to `false`. */ atmosphere?: boolean; } @@ -454,8 +460,8 @@ export interface EarthTheme { /** Defaults for edges, and how parallel edges and arrowheads look. */ export interface EdgeTheme { /** - * The edge color. `null` blends the colors of each edge's two vertices; a diagram's wires and - * edges without `ends` (nets and poles) then take the text color. + * The edge color. `null` blends the colors of each edge's two vertices. With `null`, a diagram's + * wires and edges without `ends` (nets and poles) use the text color. */ color?: Color | null; /** The edge width, in the space's unit. */ @@ -466,19 +472,19 @@ export interface EdgeTheme { */ spacing?: number; /** - * Outside a diagram, how far edges joining the same two vertices bow apart: each lane by this - * fraction of the edge's length at its middle. A family spreads at most eight lanes wide, and more - * lanes pack closer. A bow is part of the drawing, so it scales with the zoom. + * Outside a diagram, how far edges joining the same two vertices bow apart. Each lane bows out by + * this fraction of the edge's length, measured at its middle. Parallel edges spread at most eight + * lanes wide, and more lanes pack closer. Bows scale with the zoom. */ bow?: number; - /** How far an arrowhead extends beyond the edge's width: a head is this plus the width long. */ + /** How far an arrowhead extends beyond the edge's width. A head's length is this plus the width. */ arrow?: number; } /** - * Rows drawn as strokes between the vertices named by two reference fields: branches, links, - * wires. In a diagram, a row without ends is a net, joining every vertex port whose reference - * field names it. + * Rows drawn as strokes between two vertices named by reference fields: branches, links, wires. In + * a diagram, a row without ends is a net. It joins every vertex port whose reference field names + * it. */ export interface Edges extends MarkOptions, StrokeOptions { /** The dataset and table drawn, `[data, table]`. */ @@ -488,59 +494,57 @@ export interface Edges extends MarkOptions, StrokeOptions { /** A path field whose points each row passes through between its ends. */ bends?: string | null; /** - * How each row runs between its ends: geodesic on the earth and straight elsewhere unless set. + * How each row runs between its ends. Defaults to geodesic on the earth and straight elsewhere. * `orthogonal` routes around blocks with rounded bends, in diagrams only. */ route?: Route | null; /** - * Whether each row has an arrowhead at its second end; a net has one at each input it reaches. - * Default `false`. + * Whether each row has an arrowhead at its second end. A net has one at each input it reaches. + * Defaults to `false`. */ arrows?: boolean; /** * Draws each row as a pole from its own vertex down to the ground, on a plane or the earth. Poles - * take no `ends`, `bends`, or `joins`. Default `false`. + * take no `ends`, `bends`, or `joins`. Defaults to `false`. */ poles?: boolean; /** - * Draws each row as a net on a plane or the earth, joining the vertex rows of `table` whose - * `reference` field names it: a star from their center to each member. + * Draws each row as a net on a plane or the earth. The net joins the vertex rows of `table` whose + * `reference` field names it, as a star from their center to each member. */ joins?: [table: string, reference: string] | null; } /** * A request for each row's least and greatest value of a sampled field over equal spans of the - * axis: field `field` of table `table`, over `spans` equal spans from `range[0]` to `range[1]`, - * for every row or for the rows `ids` names, in that order. Plain data that deserializes from - * JSON, like a query. + * axis. It reads field `field` of table `table` over `spans` equal spans from `range[0]` to + * `range[1]`, for every row or for the rows in `ids`, in that order. Like a query, it is plain + * data that deserializes from JSON. */ export interface EnvelopeRequest { /** The table to read. */ table: string; /** The sampled field to read. */ field: string; - /** The axis range the spans cover, `[first, last]`: finite, with `first <= last`. */ + /** The axis range the spans cover, `[first, last]`. Both are finite, with `first <= last`. */ range: [number, number]; - /** The number of equal spans to divide the range into; at least 1. */ + /** The number of equal spans to divide the range into. At least 1. */ spans: number; - /** The rows to read, in this order; every row when unset. */ + /** The rows to read, in this order. Defaults to every row. */ ids?: Ids | null; } /** - * A value for each row. A number or boolean is a constant, `null` is a missing value, a string - * starting with `$` reads the field it names, any other string is a CSS color, and an object is a - * node that computes a value. + * A value for each row. A number or boolean is a constant and `null` is missing. A string starting + * with `$` reads that field, and any other string is a CSS color. An object computes a value. */ export type ExprNode = | number | boolean - | /** A missing value. An option that evaluates to missing uses the theme's value. */ null + | /** A missing value. An option that is missing uses the theme's value. */ null | /** - * A field's value in each row, or a color: `"$kv"` reads field `kv`, and any other string is a CSS - * color, such as `#ffb000` or `rebeccapurple`. A field whose name starts with `$` is written - * `{ "field": "$x" }`. + * A field or a color. `"$kv"` reads field `kv`. Any other string is a CSS color, such as `#ffb000` + * or `rebeccapurple`. Write a field named `$x` as `{ "field": "$x" }`. */ string | FieldNode | StateNode @@ -550,23 +554,23 @@ export type ExprNode = | CallNode; /** - * Rows drawn as triangles between the vertices named by three reference fields: meshes, surfaces, - * and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen - * from above shows its exact colors. + * Rows drawn as triangles between three vertices named by reference fields: meshes, surfaces, and + * fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen from + * above shows its exact colors. */ export interface Faces extends MarkOptions { /** The dataset and table drawn, `[data, table]`. */ faces: TableInput; /** - * The reference fields naming each row's three vertices; required. Without a color, a face blends + * The reference fields naming each row's three vertices. Required. Without a color, a face blends * the colors of its corners. */ corners?: [a: string, b: string, c: string] | null; } /** - * A field's value in each row: from the mark's own table, or with `from`, from another dataset's - * table joined by row id. `"$name"` is the short form of `{ "field": "name" }`. + * A field's value in each row, from the mark's own table. With `from`, it reads another dataset's + * table, joined by row id. `"$name"` is the short form of `{ "field": "name" }`. */ export interface FieldNode { /** The field's name. */ @@ -575,7 +579,7 @@ export interface FieldNode { from?: TableInput | null; /** * For a sampled field, reads the last frame at or before the shown coordinate instead of - * interpolating. Default `false`. + * interpolating. Defaults to `false`. */ step?: boolean; } @@ -594,25 +598,25 @@ export type Fit = | { /** The box from `min` to `max`: x, y, and elevation. */ bounds: { - /** The least corner. */ + /** The minimum corner. */ min: [number, number, number]; - /** The greatest corner. */ + /** The maximum corner. */ max: [number, number, number]; }; }; /** - * How comets flowing along edges and lines look: a round head, brightest at its middle, trailing a + * How comets along edges and lines look. A comet has a round head, brightest at its middle, and a * tail that thins and fades. * - * `spacing` and `length` must be in the same unit, which also measures each row's `flow` speed. In - * position units (degrees on the earth), comets are part of the drawing: they scale with the zoom - * and stay in place along their lines. In CSS pixels, they keep their on-screen size and speed. A - * plain number is in the space's unit. + * `spacing` and `length` must use the same unit, and each row's `flow` speed is in that unit too. + * In position units (degrees on the earth), comets scale with the zoom and stay in place along + * their lines. In CSS pixels, they keep their size and speed on screen. A plain number is in the + * space's unit. */ export interface FlowTheme { /** - * The comet color. Its alpha sets how strongly a comet covers its stroke; a fully transparent + * The comet color. Its alpha sets how strongly a comet covers its stroke. A fully transparent * color turns comets off. */ color?: Color; @@ -620,7 +624,7 @@ export interface FlowTheme { spacing?: Length; /** The length of a comet's tail behind its head. */ length?: Length; - /** The width of a comet's head, in the space's unit; never narrower than its stroke. */ + /** The width of a comet's head, in the space's unit. A head is never narrower than its stroke. */ width?: number; } @@ -628,13 +632,13 @@ export interface FlowTheme { export interface FocusTheme { /** The glow color around hovered rows. */ hover?: Color; - /** The glow color around selected rows; `null` glows in each row's own color. */ + /** The glow color around selected rows. `null` uses each row's own color. */ selected?: Color | null; /** How far the hover and selection glows reach, in CSS pixels. */ glow?: number; /** The opacity of rows outside the selection while any row is selected, from 0 to 1. */ dim?: number; - /** The scale factor for a hovered vertex sized in CSS pixels; vertices sized in units only glow. */ + /** The scale factor for a hovered vertex sized in CSS pixels. Vertices sized in units only glow. */ grow?: number; } @@ -656,11 +660,11 @@ export type Grid = }; } | { - /** Dots `pitch` units apart; with `snap`, a diagram's drags snap to them. */ + /** Dots `pitch` units apart. With `snap`, drags in a diagram snap to them. */ dots: { /** Units between dots. */ pitch: number; - /** Whether drag positions snap to the dots, in a diagram. Default `false`. */ + /** Whether drag positions snap to the dots, in a diagram. Defaults to `false`. */ snap?: boolean; }; }; @@ -692,34 +696,32 @@ export type Interpolation = | /** Each value holds until the next sample, where the line steps. */ 'stepAfter'; /** - * Text for each row, placed beside, inside, or along the row's shape and following it every frame: - * a text field, or options. A label shows wherever its row shows and its text fits the room the - * row gives it, such as inside a block. Labels beside or along their rows never overlap: where - * they would, the label of the row drawn on top shows, so a mark's `priority` decides, and a dense - * view labels as many rows as fit. A row whose text is missing or empty has no label, so a `when` - * choosing between the text field and missing labels only the rows it names. In a diagram a label - * is part of the drawing: it scales with the zoom, fades out when too small to read, and shows + * Text for each row, placed beside, inside, or along the row's shape and moving with it: a text + * field, or options. A label shows when its row is visible and its text fits, for example inside a + * block. + * + * Labels beside or along their rows never overlap. Where they would, the row drawn on top keeps + * its label (so `priority` decides), and a dense view labels as many rows as fit. A row whose text + * is missing or empty has no label, so a `when` that returns the field or missing labels only some + * rows. In a diagram, a label scales with the zoom, fades out when too small to read, and shows * wherever it fits its vertex. */ export type Label = | ExprInput | { /** - * The text field to show, or a `when` whose values are each that field or missing. A row whose - * text is missing or empty has no label. In a diagram the text reads only data, since the sheet - * sizes blocks around it. + * The text field to show, or a `when` that returns that field or missing. A row whose text is + * missing or empty has no label. In a diagram, the text may read only data, since the sheet sizes + * blocks around it. */ text: ExprInput; - /** - * Font size in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's - * `text.size` unless set. - */ + /** Font size in CSS pixels, or diagram units in a diagram. Defaults to the theme's `text.size`. */ size?: number | null; - /** Each row's text color; the theme's text color unless set. */ + /** Each row's text color. Defaults to the theme's text color. */ color?: ExprInput | null; - /** The width at which lines wrap, in the space's unit (units in a diagram, CSS pixels elsewhere). */ + /** The width at which lines wrap, in CSS pixels, or diagram units in a diagram. */ maxWidth?: number | null; - /** The CSS font family list for the text; the theme's `text.font` unless set. */ + /** The CSS font family list for the text. Defaults to the theme's `text.font`. */ font?: string | null; }; @@ -728,22 +730,22 @@ export type Label = * outputs. */ export interface Layered { - /** The direction edges point; `right` by default. */ + /** The direction edges point. Defaults to `right`. */ direction?: Direction; - /** Distance between vertices within a rank, in position units. Default 30. */ + /** Distance between vertices within a rank, in position units. Defaults to 30. */ gap?: number; - /** Distance between ranks, in position units. Default 80. */ + /** Distance between ranks, in position units. Defaults to 80. */ rankGap?: number; } /** * How the view places vertex rows that have no position: a name, or - * `{ stress | layered: options }`. In a diagram it places blocks; on a plane or the earth it + * `{ stress | layered: options }`. In a diagram it places blocks. On a plane or the earth it * places points among the rows they link to. Gaps are in position units: diagram units, plane * units, or degrees on the earth. * - * A row keeps its place once placed: rows added later are placed around the others, and - * `view.arrange` lays out every row again. + * A placed row keeps its place. Rows added later are placed around the others, and `view.arrange` + * lays out every row again. */ export type Layout = | 'none' @@ -759,18 +761,17 @@ export type Layout = }; /** - * A length: a number in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in - * CSS pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the - * view zooms in. + * A length: a number in CSS pixels, or units in a diagram. `{ px }` is always CSS pixels. + * `{ units }` is position units (degrees on the earth), which grow as the view zooms in. */ export type Length = | number | { - /** CSS pixels, at any zoom. */ + /** CSS pixels, the same at any zoom. */ px: number; } | { - /** Position units (degrees on the earth), which grow on screen as the view zooms in. */ + /** Position units (degrees on the earth), which grow as the view zooms in. */ units: number; }; @@ -784,77 +785,76 @@ export interface LineTheme { /** * Rows drawn as lines along a path field (roads, rivers, borders) or a polygon field's rings (area - * outlines). On a plot, each row instead traces a sampled field's history across the data's axis, - * as a monitor does. On the earth, paths follow the globe's curve and stay whole across the - * antimeridian. + * outlines). On a plot, each row traces a sampled field's history across the data's axis, like a + * monitor. On the earth, paths follow the globe's curve and stay unbroken across the antimeridian. */ export interface Lines extends MarkOptions, StrokeOptions { /** The dataset and table drawn, `[data, table]`. */ lines: TableInput; /** - * The path or polygon field each row runs along; the field named `geometry` by default. Not + * The path or polygon field each row runs along. Defaults to the field named `geometry`. Not * allowed on a plot. */ geometry?: string | null; /** - * The sampled field each row traces across the data's axis; plots only, where it is required. A - * field with no frames yet draws nothing until its first frame arrives. + * The sampled field each row traces across the data's axis. Required on a plot, and allowed only + * there. A field with no frames yet draws nothing until its first frame arrives. */ trace?: string | null; - /** How each row runs between its samples on a plot; `linear` by default. */ + /** How each row runs between its samples on a plot. Defaults to `linear`. */ interpolation?: Interpolation; } /** - * One table a scene draws. The key naming the mark's kind holds its dataset and table, beside its - * options: `{ vertices: [data, table], radius: 4 }`. + * One table a scene draws. The mark's kind is the key that holds its table, next to its options: + * `{ vertices: [data, table], radius: 4 }`. */ export type Mark = Vertices | Edges | Faces | Lines | Areas; /** The options every mark has. */ export interface MarkOptions { /** - * Each row's color: a CSS color, a value through a colormap, or a conditional choosing between - * colors. Unset, rows take the theme's default for the mark; faces instead blend their corners' + * Each row's color: a CSS color, a value through a colormap, or a conditional that picks between + * colors. Defaults to the theme's color for the mark. Faces default to a blend of their corners' * colors. */ color?: ExprInput | null; - /** Whether each row is drawn and can be picked. Default `true`. */ + /** Whether each row is drawn and can be picked. Defaults to `true`. */ visible?: ExprInput; /** - * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, - * their labels win where labels would overlap, and they are the last dropped when a table too - * large to draw whole is thinned to the tiles in view. + * Each row's priority. It reads only data, not view state. Higher rows draw on top, keep their + * labels where labels overlap, and are dropped last when a large table is thinned. */ priority?: ExprInput | null; /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ name?: string | null; /** - * Whether rows respond to hover, clicks, and picks. Unset, vertices, edges, faces, and plot traces - * do, and geometry (areas, and lines along paths such as coasts, borders, and roads) does not. + * Whether rows respond to hover, clicks, and picks. Defaults to on for vertices, edges, faces, and + * plot traces, and off for geometry: areas, and lines along paths such as coasts, borders, and + * roads. */ pick?: boolean | null; - /** Whether clicking a row selects it. Default `true`. */ + /** Whether clicking a row selects it. Defaults to `true`. */ select?: boolean; /** - * Whether pressing and moving a row drags it, reported as a `'drag'` event, instead of panning the - * camera. Default `false`. + * Whether pressing and moving a row drags it instead of panning the camera. Drags are reported as + * a `'drag'` event. Defaults to `false`. */ drag?: boolean; /** Each row's label: a text field, or label options. */ label?: Label | null; /** - * A legend of the mark's colors at the view's bottom right, under this title: a ticked colormap - * across its domain, a cell for each category, or a swatch beside each name of a map's - * `categories`. `color` must map through a colormap with a fixed domain or named categories; in a - * conditional, `otherwise` is searched before the cases. + * A legend of the mark's colors at the view's bottom right, with this title. It shows a ticked + * colormap across its domain, a cell for each category, or a swatch for each name in the + * colormap's `categories`. `color` must map through a colormap with a fixed domain or named + * categories. In a conditional, `otherwise` is searched before the cases. */ legend?: string | null; } /** * How a vertex is drawn: a shape's name, `"block"`, or - * `{ shape | block | gauge | pie | icon | wgsl: options }`. An ellipse by default. + * `{ shape | block | gauge | pie | icon | wgsl: options }`. Defaults to an ellipse. */ export type Marker = | 'ellipse' @@ -867,21 +867,21 @@ export type Marker = shape: Shape; } | { - /** A rectangle with rounded corners that holds its label inside, as a diagram's blocks do. */ + /** A rectangle with rounded corners that holds its label inside, like a diagram's blocks. */ block: { - /** - * Corner radius in the space's unit (units in a diagram, CSS pixels elsewhere); the theme's - * `diagram.corner` unless set. - */ + /** Corner radius in CSS pixels, or units in a diagram. Defaults to the theme's `diagram.corner`. */ corner?: number | null; }; } | { - /** A shape's outline, filled to `fill` from 0 to 1: around a circle, or upward in other shapes. */ + /** + * A shape's outline, filled to `fill` (0 to 1). A circle fills around its edge, and other shapes + * fill upward. + */ gauge: { /** How full each row's gauge is, from 0 to 1. */ fill: ExprInput; - /** The outline's shape; an ellipse by default. */ + /** The outline's shape. Defaults to an ellipse. */ shape?: Shape; }; } @@ -892,12 +892,12 @@ export type Marker = slices: ExprInput[]; /** Each slice's color. */ colors: Color[]; - /** The hole in the middle, as a fraction of the radius; 0 by default. */ + /** The hole in the middle, as a fraction of the radius. Defaults to 0. */ hole?: number; }; } | { - /** One of `images`, fitted to the marker's box; `index` picks the image for each row. */ + /** One of `images`, fitted to the marker's box. `index` picks the image for each row. */ icon: { /** The images. */ images: Bitmap[]; @@ -920,7 +920,7 @@ export type Marker = /** How much a view animates on its own. */ export type Motion = - | /** Follows the system: reduced when the user asked their system for less motion. */ 'system' + | /** Follows the system setting: reduced when the user has asked their system for less motion. */ 'system' | /** All motion. */ 'full' | /** * Only motion the user drives: changes apply at once, flights jump, nothing coasts or orbits, and @@ -932,7 +932,7 @@ export type Order = /** Smallest first. */ 'ascending' | /** Largest first. */ ' /** A plane: positions are in plane units. */ export interface Plane { - /** The grid drawn under the marks; lines 10 units apart by default. */ + /** The grid drawn under the marks. Defaults to lines 10 units apart. */ grid?: Grid; } @@ -970,9 +970,9 @@ export type Port = order?: number | null; /** Its name, drawn beside it inside the marker. */ label?: string | null; - /** Its shape; an ellipse by default. */ + /** Its shape. Defaults to an ellipse. */ shape?: Shape; - /** Whether it is an output, which drives its net. Default `false`, an input. */ + /** Whether it is an output, which drives its net. Defaults to `false` (an input). */ output?: boolean; }; @@ -990,15 +990,15 @@ export interface Query { /** The table to read. */ of: string; /** - * The rows to read, in this order unless sorted; every row when unset. Ids that name no row are + * The rows to read, in this order unless sorted. Defaults to every row. Ids that name no row are * skipped. */ ids?: Ids | null; /** Keeps only the rows for which this is true. */ filter?: ExprInput | null; /** - * Keys to sort by, each breaking ties left by the keys before it. Rows where a key has no value - * sort last; rows equal on every key keep their order. + * Keys to sort by. Each key breaks ties left by the keys before it. Rows where a key has no value + * sort last, and rows equal on every key keep their order. */ sort?: [ExprInput, Order][]; /** How many of the kept rows to skip. */ @@ -1009,11 +1009,11 @@ export interface Query { */ limit?: number | null; /** - * Fields to return, by output name. A plain field returns that field as stored; any other - * expression returns numbers. Every field when unset; an empty object returns the ids alone. + * Fields to return, by output name. A plain field returns that field as stored, and any other + * expression returns numbers. Defaults to every field. An empty object returns the ids alone. */ select?: { [name: string]: ExprInput } | null; - /** The axis coordinate at which sampled fields are read; their last frame when unset. */ + /** The axis coordinate at which sampled fields are read. Defaults to their last frame. */ at?: number | null; } @@ -1058,7 +1058,7 @@ export interface ScaleNode { scale: [low: number, high: number]; /** The value mapped. */ value: ExprInput; - /** The values mapped to the range's ends; the extent of the values shown when unset. */ + /** The values mapped to the range's ends. Defaults to the extent of the values shown. */ domain?: [low: number, high: number] | null; } @@ -1075,15 +1075,15 @@ export interface Scene { /** The marks, in drawing order: each draws over the marks before it. */ marks?: Mark[]; /** - * How the view places vertex rows that have no position: a diagram's blocks, or points on a plane - * or the earth. `none` by default. + * How the view places vertex rows that have no position, such as a diagram's blocks or points on a + * plane or the earth. Defaults to `none`. */ layout?: Layout; - /** Default colors and sizes, and the look of text, focus, and each space; `dark` by default. */ + /** Default colors and sizes, and the look of text, focus, and each space. Defaults to `dark`. */ theme?: Theme; /** How the view responds to the pointer and keyboard. */ controls?: Controls; - /** Seconds over which scene and data changes animate; 0.3 by default, 0 for none. */ + /** How many seconds scene and data changes take to animate. 0 turns animation off. Defaults to 0.3. */ ease?: number; /** A title drawn at the view's top left, larger than the theme's text. */ title?: string | null; @@ -1093,9 +1093,9 @@ export interface Scene { /** How a diagram's blocks and groups cast shadows on the sheet, for vertex marks with `shadow` on. */ export interface ShadowTheme { - /** The color a shadow darkens toward; its alpha sets how dark the shadow is under the block. */ + /** The color a shadow darkens toward. Its alpha sets how dark the shadow is under the block. */ color?: Color; - /** How far a shadow falls below its block or group; its edge softens over the same distance. */ + /** How far a shadow falls below its block or group. Its edge softens over the same distance. */ lift?: number; } @@ -1114,9 +1114,8 @@ export type Side = | /** The bottom. */ 'bottom'; /** - * A size: a value in the space's unit (units in a diagram, CSS pixels elsewhere), `{ px }` in CSS - * pixels, or `{ units }` in position units (degrees on the earth), which grow on screen as the - * view zooms in. + * A size: a value in CSS pixels, or units in a diagram. `{ px }` is always CSS pixels. `{ units }` + * is position units (degrees on the earth), which grow as the view zooms in. */ export type Size = ExprInput | Px | Units; @@ -1124,9 +1123,9 @@ export type Size = ExprInput | Px | Units; * The coordinate space of a scene's positions: a name, or * `{ plane | diagram | earth | plot: options }`. * - * The space sets the unit of lengths that do not name their own: units in a diagram, where a unit - * is a CSS pixel at zoom 0, and CSS pixels elsewhere. Only a diagram has ports, groups, nets, and - * orthogonal routes. + * The space sets the unit of lengths that do not name their own. In a diagram it is the diagram + * unit, one CSS pixel at zoom 0. Elsewhere it is the CSS pixel. Only a diagram has ports, groups, + * nets, and orthogonal routes. */ export type Space = | 'plane' @@ -1138,7 +1137,7 @@ export type Space = earth: Earth; } | { - /** Plane units, with elevation; the camera can turn and tilt. */ + /** Plane units, with elevation. The camera can turn and tilt. */ plane: Plane; } | { @@ -1163,27 +1162,27 @@ export interface StateNode { } /** - * Stress majorization: vertices move until drawn distances match graph distances, then overlapping + * Stress majorization: vertices move until drawn distances match graph distances. Then overlapping * boxes separate, keeping a quarter of `gap` between them. */ export interface Stress { - /** Distance between the centers of linked vertices, in position units. Default 40. */ + /** Distance between the centers of linked vertices, in position units. Defaults to 40. */ gap?: number; } /** The options of marks drawn as strokes: edges and lines. */ export interface StrokeOptions { /** - * Each row's width, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given - * with `px` or `units`. The theme's width unless set. + * Each row's width in CSS pixels, or units in a diagram, unless given with `px` or `units`. + * Defaults to the theme's width. */ width?: Size | null; - /** Whether each row is dashed, with the theme's `dash` period. Default `false`. */ + /** Whether each row is dashed, using the theme's `dash` period. Defaults to `false`. */ dash?: ExprInput; /** - * The speed of comets flowing along each row, per second, in the unit of the theme's `flow` - * lengths. Negative speeds flow backward; 0, the default, draws no comets. When the speed changes, - * comets continue from where they are. + * The speed of comets along each row, per second, in the unit of the theme's `flow` lengths. + * Negative speeds flow backward. 0 draws no comets. Defaults to 0. When the speed changes, comets + * continue from where they are. */ flow?: ExprInput; } @@ -1201,13 +1200,12 @@ export interface TextTheme { } /** - * Default colors and sizes for marks whose options leave them unset or missing, and the look of - * text, focus, comets, and each space, grouped by subject. Write a base theme's name (`"dark"` or - * `"light"`), or options over a base (`base`, dark by default); a group's options override only - * the base values they set. + * Default colors and sizes for marks, and the look of text, focus, comets, and each space, grouped + * by subject: a base theme's name (`"dark"` or `"light"`), or options that change a base. `base` + * names the base and defaults to `dark`. Each option replaces only the base value it sets. * - * Lengths are in the space's unit (units in a diagram, CSS pixels elsewhere), except the text halo - * and the focus glow, which are always in CSS pixels. + * Lengths are in CSS pixels, or units in a diagram. The text halo and the focus glow are always in + * CSS pixels. */ export type Theme = | 'dark' @@ -1217,7 +1215,7 @@ export type Theme = background?: Color; /** The color of a plane's or diagram's grid, a plot's grid lines, and the earth's graticule. */ grid?: Color; - /** The color a colormap gives a missing value, such as a row whose field has none. */ + /** The color a colormap gives a missing value, such as a row with no value in the field. */ missing?: Color; /** The dash period: one dash and one gap of equal length. */ dash?: number; @@ -1243,7 +1241,7 @@ export type Theme = earth?: EarthTheme; /** How a plot looks. */ plot?: PlotTheme; - /** The theme these options change; dark unless set. */ + /** The theme these options change. Defaults to `dark`. */ base?: 'dark' | 'light'; }; @@ -1251,8 +1249,8 @@ export type Theme = export type Tick = number | [number, string | null]; /** - * A size in position units (degrees on the earth), regardless of the space's unit, so it scales as - * the view zooms. `atLeast` and `atMost` bound it in CSS pixels. + * A size in position units (degrees on the earth), regardless of the space's unit. It scales with + * the zoom. `atLeast` and `atMost` bound it in CSS pixels. */ export interface Units { /** The size, in position units. */ @@ -1276,34 +1274,34 @@ export interface Vertices extends MarkOptions { /** The dataset and table drawn, `[data, table]`. */ vertices: TableInput; /** - * Each row's x and y. Without a position, the point field that `geometry` names places rows, and - * the scene's layout places rows that have neither. + * Each row's x and y. Rows without a position are placed by the point field `geometry` names. The + * scene's layout places rows that have neither. */ position?: [ExprInput, ExprInput] | null; /** - * The point field that places rows when `position` is not set; the field named `geometry` by - * default. + * The point field that places rows when `position` is not set. Defaults to the field named + * `geometry`. */ geometry?: string | null; /** - * Height above the ground; on the earth, a fraction of the earth's radius. Default 0. Not allowed - * in a diagram, which is flat. + * Height above the ground. On the earth, a fraction of the earth's radius. Defaults to 0. Not + * allowed in a diagram, which is flat. */ elevation?: ExprInput; /** - * Each row's radius, in the space's unit (units in a diagram, CSS pixels elsewhere) unless given - * with `px` or `units`; a diagram refuses `px`. The theme's `vertex.radius` unless set. + * Each row's radius in CSS pixels, or units in a diagram, unless given with `px` or `units`. A + * diagram does not accept `px`. Defaults to the theme's `vertex.radius`. */ radius?: Size | null; /** - * Each row's width and height, for markers that are not round, in the space's unit unless given - * with `px` or `units`; a diagram refuses `px`. In a diagram, a block without a size fits its - * label and ports. + * Each row's width and height, for markers that are not round. In CSS pixels, or units in a + * diagram, unless given with `px` or `units`. A diagram does not accept `px`. In a diagram, a + * block without a size fits its label and ports. */ size?: [Size, Size] | null; - /** How each row is drawn; an ellipse by default. */ + /** How each row is drawn. Defaults to an ellipse. */ marker?: Marker; - /** A ring around each row's marker in this color; a missing or transparent color draws none. */ + /** A ring around each row's marker in this color. A missing or transparent color draws no ring. */ ring?: ExprInput | null; /** * Ports on each row, one for each reference field that names a net. An `edges` mark of the net @@ -1317,7 +1315,7 @@ export interface Vertices extends MarkOptions { contains?: [table: string, reference: string] | null; /** * Whether each block or group casts a shadow on the sheet, styled by the theme's `diagram.shadow`. - * Default `false`. Diagrams only. + * Defaults to `false`. Diagrams only. */ shadow?: boolean; } diff --git a/npm/src/internal.ts b/npm/src/internal.ts index 109bc775..3555ad87 100644 --- a/npm/src/internal.ts +++ b/npm/src/internal.ts @@ -1,37 +1,37 @@ /** - * What the package's classes share: access to the WebAssembly bindings, the `Handle` base class, - * latkit's errors and bounded waits, and reading bytes. Of these, `Handle`, `LatkitError`, `Wait`, - * `ByteSource`, and `ReadableBytes` are public. + * Code the package's classes share: access to the WebAssembly bindings, the `Handle` base class, + * errors, bounded waits, and byte reading. `Handle`, `LatkitError`, `Wait`, `ByteSource`, and + * `ReadableBytes` are public. */ import * as bindings from '../pkg/latkit.js'; import type { Failure } from './outputs.js'; -/** The error latkit throws: an `Error` whose `code` says what kind of failure it is. */ +/** The error latkit throws: an `Error` with a `code` that names the kind of failure. */ export interface LatkitError extends Error { /** * The kind of failure: * - * - `invalid`: the input is malformed or does not fit; `path` says where. + * - `invalid`: the input is malformed or does not fit. `path` says where. * - `unsupported`: the browser or device cannot do what was asked. * - `limit`: a budget or device limit would be exceeded. * - `io`: reading or writing external bytes failed, such as loading latkit or fetching a file. - * - `device-lost`: the GPU device was lost; create a new `Gpu`. + * - `device-lost`: the GPU device was lost. Create a new `Gpu`. * - `cancelled`: the wait's signal aborted, or the work was cancelled. * - `timeout`: the wait's signal timed out. * - `unloaded`: the call needs WebAssembly, and `load()` has not finished. - * - `destroyed`: the object was destroyed, so it can no longer be used. + * - `destroyed`: the object was destroyed and can't be used. */ code: Failure['code'] | 'unloaded' | 'destroyed'; - /** The invalid part of the input, such as `marks[2].color`, on `invalid` errors. */ + /** On `invalid` errors, the invalid part of the input, such as `marks[2].color`. */ path?: string; } /** Options that bound how long a promise waits. */ export interface Wait { /** - * Aborts the wait. A timeout rejects with a `timeout` error; any other abort rejects with a - * `cancelled` error whose `cause` is the abort reason. Either way, latkit cancels the call's + * Aborts the wait. A timeout rejects with a `timeout` error. Any other abort rejects with a + * `cancelled` error, with the abort reason as its `cause`. Either way, latkit cancels the call's * pending work where it can. Without a signal, each wait times out after 30 seconds. */ signal?: AbortSignal; @@ -39,7 +39,7 @@ export interface Wait { let ready = false; -/** @internal Marks the bindings as loaded, so `wasm()` returns them from now on. */ +/** @internal Marks the bindings as loaded, so `wasm()` returns them. */ export function loaded(): void { ready = true; } @@ -65,16 +65,16 @@ export function thrown({ return Object.assign(error, path === undefined ? { code } : { code, path }); } -/** The bindings object behind each handle, until it is destroyed, and the name its errors use. */ +/** Each handle's bindings object, until the handle is destroyed, and the name its errors use. */ const held = new WeakMap(); /** - * An object that holds WebAssembly memory, and GPU memory for a `Gpu`, until it is destroyed: - * `destroy()` frees it, as does a `using` declaration when its block ends. Once it is destroyed, - * its other methods throw a `destroyed` error. + * An object that holds WebAssembly memory (and GPU memory, for a `Gpu`) until it is destroyed. + * `destroy()` frees it, as does the end of a `using` block. After that, its other methods throw a + * `destroyed` error. */ export abstract class Handle { - /** @internal Holds `bound`, the bindings object it frees; errors name the object `what`. */ + /** @internal Holds `bound`, the bindings object to free. Errors call the object `what`. */ constructor(bound: { free(): void }, what: string) { held.set(this, { bound, what }); } @@ -89,7 +89,7 @@ export abstract class Handle { bound.free(); } - /** @internal Runs once, as the object is destroyed, before its memory is freed. */ + /** @internal Runs once when the object is destroyed, before its memory is freed. */ protected freeing(): void {} /** Calls `destroy()`, so a `using` declaration destroys the object when its block ends. */ @@ -108,7 +108,7 @@ export function inner(owner: Handle): T { /** A byte stream: anything shaped like a browser or Node.js `ReadableStream` of `Uint8Array`. */ export interface ReadableBytes { - /** A reader of the stream's chunks, which latkit reads to the end, or cancels. */ + /** Returns a reader of the stream's chunks. latkit reads it to the end or cancels it. */ getReader(): { read(): Promise<{ done: boolean; value?: Uint8Array }>; cancel(reason?: unknown): Promise; @@ -117,17 +117,17 @@ export interface ReadableBytes { } /** - * Bytes to read: an array or buffer, a `Blob`, a `Response`, a byte stream, or a URL to fetch as a - * string. Reading waits within its {@link Wait}, and fails with an `io` error when a fetch fails or - * answers an error status, or the bytes cannot be read. + * Bytes to read: an array or buffer, a `Blob`, a `Response`, a byte stream, or a URL string to + * fetch. Reading is bounded by the call's {@link Wait}. It fails with an `io` error when a fetch + * fails or returns an error status, or when the bytes can't be read. */ export type ByteSource = Uint8Array | ArrayBuffer | Blob | Response | ReadableBytes | AsyncIterable | string; /** - * @internal Reads `source` into bytes, with the URL they came from as `base` (else empty). Any - * object that is not a {@link ByteSource} is written as JSON text. Waits within `wait`, cancelling - * a fetch or stream it gives up on. + * @internal Reads `source` into bytes, with the URL they came from as `base` (empty if none). An + * object that is not a {@link ByteSource} is encoded as JSON text. Waits within `wait`, and + * cancels a fetch or stream it gives up on. */ export async function bytesOf( source: ByteSource | object, @@ -179,9 +179,9 @@ async function collect( } /** - * @internal Yields the chunks of `stream`, through `getReader()` when it has one, else by async - * iteration, until the stream ends or `signal` aborts. Stopping early cancels the stream, with the - * abort's reason. A failed read throws an `io` error. + * @internal Yields the chunks of `stream` until it ends or `signal` aborts. Reads through + * `getReader()` when the stream has one, else by async iteration. Stopping early cancels the + * stream with the abort reason. A failed read throws an `io` error. */ export async function* chunks( stream: ReadableBytes | AsyncIterable, @@ -219,11 +219,11 @@ export async function* chunks( } /** - * @internal Awaits `pending`, a promise or a bindings task, until `signal` aborts (30 seconds by - * default). If the signal aborts first, a task is cancelled so its work is skipped, and the call - * rejects with an error naming `what`: `timeout` for a timeout, else `cancelled`. Work that fails - * once the signal has aborted, such as a fetch given the same signal, rejects the same way. A task - * is freed either way. + * @internal Awaits `pending`, a promise or a bindings task, until `signal` aborts. The signal + * defaults to a 30-second timeout. If the signal aborts first, a task is cancelled so its work is + * skipped, and the call rejects with an error naming `what`: `timeout` for a timeout, else + * `cancelled`. Work that fails after the signal aborted, such as a fetch given the same signal, + * rejects the same way. A task is always freed. */ export async function waited( pending: Promise | bindings.Task, diff --git a/npm/src/jobs.ts b/npm/src/jobs.ts index 1f211d17..d0bc6595 100644 --- a/npm/src/jobs.ts +++ b/npm/src/jobs.ts @@ -25,7 +25,7 @@ interface Slot { timer?: ReturnType; } -/** The error for a failed job: `cause` itself if it is one, else an `io` error saying it. */ +/** The error for a failed job: `cause` if it is a latkit error, else an `io` error from it. */ function failure(cause: unknown): LatkitError { if (cause instanceof Error && 'code' in cause) return cause as LatkitError; const message = cause instanceof Error ? cause.message : String(cause); @@ -35,9 +35,9 @@ function failure(cause: unknown): LatkitError { /** * Returns the function latkit hands its jobs to. It runs each job on a pooled worker, one job per * worker at a time. latkit queues jobs itself and hands one over only when it can run, so the pool - * grows only to the number of jobs running at once, and nothing queues here. Idle workers are - * terminated after 30 seconds. A job fails after one minute, including a new worker's startup. A - * failure rejects that job and terminates its worker; later jobs start a new one. + * grows only to the number of jobs running at once and nothing queues here. Idle workers are + * terminated after 30 seconds. A job times out after one minute, counting a new worker's startup. + * A failure rejects that job and terminates its worker. Later jobs start a new one. */ export function jobs( module: WebAssembly.Module, diff --git a/npm/src/load.ts b/npm/src/load.ts index c8548ecc..20575f94 100644 --- a/npm/src/load.ts +++ b/npm/src/load.ts @@ -13,16 +13,22 @@ let first: Promise | undefined; /** * Loads latkit's WebAssembly, once per page. Later and concurrent calls share the first call's - * load; options they pass must equal the first call's, or they reject with an `io` error. A failed - * load rejects with an `io` error and can be retried. `load()` also requests a GPU adapter while - * the WebAssembly compiles, so the browser starts its GPU process early, then the first device for - * `Gpu.create()`. + * load. Options they pass must equal the first call's, or they reject with an `io` error. A failed + * load rejects with an `io` error and can be retried. While the WebAssembly compiles, `load()` + * requests a GPU adapter so the browser starts its GPU process early. Then it requests the first + * device for `Gpu.create()`. */ export function load( options: { - /** The URL of this version's `latkit/wasm` asset, or that asset compiled; bundled by default. */ + /** + * The URL of this version's `latkit/wasm` asset, or that asset compiled. Defaults to the + * bundled asset. + */ wasm?: string | URL | WebAssembly.Module; - /** Creates a worker running the standalone `latkit/worker` bundle; bundled by default. */ + /** + * Creates a worker that runs the standalone `latkit/worker` bundle. Defaults to the bundled + * worker. + */ worker?: () => Worker; } = {}, ): Promise { @@ -77,9 +83,9 @@ export function firstDevice(): Promise | undefined { } /** - * Requests a high-performance GPU adapter, as latkit's own request does, while the WebAssembly - * loads. This starts the browser's GPU process early. Starting it is most of a cold device's - * 280 ms, so latkit's own request later finds the process running. + * Requests a high-performance GPU adapter, like latkit's own request, while the WebAssembly loads. + * This starts the browser's GPU process early. Starting it takes most of a cold device's 280 ms, so + * latkit's later request finds the process running. */ function startGpuProcess(): void { const gpu = ( diff --git a/npm/src/outputs.ts b/npm/src/outputs.ts index 44a117d5..0fa44eb1 100644 --- a/npm/src/outputs.ts +++ b/npm/src/outputs.ts @@ -11,7 +11,7 @@ export interface Description { name: string; /** The axis name and unit, if set. */ axis: [string, string] | null; - /** The first and last axis coordinates held by any sampled field; `null` when none holds any. */ + /** The first and last axis coordinates held by any sampled field, or `null` if no field holds any. */ span: [number, number] | null; /** Its tables, sorted by name. */ tables: TableDescription[]; @@ -21,7 +21,7 @@ export interface Description { export type Device = | /** A mouse or a trackpad. */ 'mouse' | /** A pen. */ 'pen' - | /** A finger: picks reach 8 CSS pixels farther. */ 'touch' + | /** A finger. Picks find rows up to 8 CSS pixels farther away. */ 'touch' | /** The keyboard. */ 'keyboard'; /** A vertex a connection ends at, and the port the connection meets, if any. */ @@ -32,13 +32,13 @@ export interface End { port?: string; } -/** What failed, as JSON: the form of the npm package's `LatkitError`. */ +/** A failure as JSON, in the same form as the npm package's `LatkitError`. */ export interface Failure { - /** The kind of failure, as `LatkitError.code` describes it. */ + /** The kind of failure. See `LatkitError.code`. */ code: 'invalid' | 'unsupported' | 'limit' | 'io' | 'device-lost' | 'cancelled' | 'timeout'; - /** What failed, in words. */ + /** A description of what failed. */ message: string; - /** The invalid part of the input, such as `marks[2].color`, on `invalid` failures. */ + /** For `invalid` failures, the path of the invalid input, such as `marks[2].color`. */ path?: string; } @@ -58,7 +58,7 @@ export interface FieldDescription { export interface Hit { /** The row. */ row: Row; - /** The name of the mark that drew it, if named. It tells two marks of one table apart. */ + /** The name of the mark that drew it, if the mark has one. It tells apart two marks of one table. */ mark?: string; /** * The row's point nearest the probe, in CSS pixels from the top left: a vertex's or port's middle, @@ -69,9 +69,9 @@ export interface Hit { position: [number, number, number]; /** CSS pixels from the probe to the row. */ distance: number; - /** The port hit, when a vertex's port was. */ + /** The port hit, if the hit is on a vertex's port. */ port?: string; - /** The sample hit, when a line tracing a sampled field was. */ + /** The sample hit, if the hit is on a line tracing a sampled field. */ sample?: Sample; } @@ -122,7 +122,7 @@ export interface Stats { /** Rows of every mark's table. */ rows: number; /** - * Rows every mark draws: every row of a table drawn whole, and the rows of the tiles in view of a + * Rows the marks draw: every row of a fully drawn table, and the rows in the tiles in view of a * paged table. */ drawn: number; @@ -133,15 +133,15 @@ export interface Stats { * for segments, which the GPU may cull. */ instances: number; - /** Fetched tiles the view draws, of every tile source. */ + /** Fetched tiles the view draws, across all tile sources. */ tiles: number; - /** Tiles being fetched or decoded, of every tile source. */ + /** Tiles being fetched or decoded, across all tile sources. */ tilesLoading: number; /** Milliseconds the last frame took to record on the CPU. */ cpuMs: number; /** - * Milliseconds the GPU takes per frame: the median of recently measured frames; 0 if the device - * cannot measure. + * Milliseconds the GPU takes per frame: the median of recently measured frames. 0 if the device + * cannot measure GPU time. */ gpuMs: number; } @@ -157,8 +157,8 @@ export interface TableDescription { } /** - * Where the user acted, with which modifier keys, and with which device. For keyboard input, the - * point is the row the keys point at, else the pointer, else the view's middle. + * Where the user acted, with which modifier keys and device. For keyboard input, the point is the + * row the keys point at, or else the pointer, or else the view's middle. */ export interface Trigger { /** CSS pixels from the top left. */ @@ -218,9 +218,9 @@ export type ViewEvent = /** The row dragged, as it was hit. */ hit: Hit; /** - * The row's new position in the space's coordinates: the hit point moved with the pointer since - * the press, at the depth where the drag started, so a vertex keeps its grab offset. Snapped to a - * diagram's dots when its grid snaps. + * The row's new position in the space's coordinates. It is the hit point moved with the pointer + * since the press, at the depth where the drag started, so a vertex keeps its offset from the + * pointer. Snapped to a diagram's grid dots when the grid snaps. */ position: [number, number, number]; /** The pointer. */ @@ -233,11 +233,11 @@ export type ViewEvent = */ { type: 'connect'; detail: { - /** The end it was drawn from: for an edge, the end that stayed. */ + /** The end it was drawn from. For a dragged edge end, the edge's other end. */ from: End; /** The vertex it was released over, if any. */ to: End | null; - /** The edge whose end was dragged off; `to` replaces that end. */ + /** The edge whose end was dragged off. `to` replaces that end. */ edge: Row | null; /** Where it was released. */ trigger: Trigger; @@ -255,7 +255,7 @@ export type ViewEvent = | /** The hovered row changed: `view.hovered`. */ { type: 'hovered'; detail: { - /** The row now hovered; `null` once no row is. */ + /** The row now hovered, or `null` if no row is. */ hit: Hit | null; /** The pointer, or the keys. */ trigger: Trigger; @@ -266,20 +266,20 @@ export type ViewEvent = detail: { /** The rows selected now. */ rows: Rows[]; - /** How the user changed it; `null` when the data dropped rows from it. */ + /** How the user changed it, or `null` when the data dropped rows from it. */ trigger: Trigger | null; }; } | /** - * The camera moved by a gesture, fit, flight, coast, or orbit, or back within reach as the view's - * size, insets, or space changed: `view.camera`. + * The camera moved: `view.camera`. A gesture, fit, flight, coast, or orbit moved it, or it moved + * back within its zoom and pan limits after the view's size, insets, or space changed. */ { type: 'camera'; detail: Camera } - | /** The work the last frame waited on changed; empty once frames are complete: `view.waiting`. */ { + | /** The work the last frame waited on changed: `view.waiting`. Empty once frames are complete. */ { type: 'waiting'; detail: Waiting; } | /** - * Something failed; reported once. A mark that no longer fits its data is also listed in + * Something failed. Reported once. A mark that no longer fits its data is also listed in * `view.failures`. */ { type: 'error'; detail: Failure }; @@ -309,7 +309,7 @@ export interface Work { /** Rows whose channels the GPU evaluated. */ evaluated: number; /** - * Edge and face rows whose endpoints the GPU resolved to their vertices' resident rows: rows that + * Edge and face rows whose endpoints the GPU resolved to resident vertex rows. These are rows that * changed, plus every row of a page that references a vertex now resident elsewhere. */ resolved: number; diff --git a/npm/src/patch.ts b/npm/src/patch.ts index 58d8b392..284a3c65 100644 --- a/npm/src/patch.ts +++ b/npm/src/patch.ts @@ -1,6 +1,6 @@ /** - * Patches written directly as latkit's bytes, without WebAssembly, so a page, a worker, or a - * server can build them. The format matches `crates/latkit/src/data/bytes.rs`: a frame with a + * Patches, written directly as latkit's bytes without WebAssembly so a page, worker, or server can + * build them. The format matches `crates/latkit/src/data/bytes.rs`: a frame with a * 12-byte header (the magic `LATK` and the frame's length), the name of the dataset it changes, an * operation count, and the operations, all little endian. */ @@ -32,8 +32,9 @@ export type Geometry = readonly number[] | readonly Geometry[]; /** * One field's values, one per row, with `null` for a missing value. Numbers come as a typed array - * or an array. A `Uint8Array` holds booleans, nonzero being true. Booleans, strings, and geometries - * also come as arrays. Values are copied when passed, so the arrays can be reused at once. + * or an array. A `Uint8Array` holds booleans, where nonzero is true. Booleans, strings, and + * geometries also come as arrays. Values are copied when passed, so the arrays can be reused right + * away. */ export type Values = | Float64Array @@ -54,7 +55,7 @@ export interface Reference { ids: RowIds; } -/** Builds a {@link Reference} field: for each row, the id of the row of `table` it references. */ +/** Builds a {@link Reference} field: for each row, the id of the row it references in `table`. */ export function reference(table: string, ids: RowIds): Reference { return { reference: table, ids }; } @@ -62,17 +63,20 @@ export function reference(table: string, ids: RowIds): Reference { /** A table's fields by name: values, or a {@link Reference}. */ export type Fields = Readonly>; -/** A run of a sampled field's entries at one level of detail, as {@link Patch.frames} stores. */ +/** A run of entries of a sampled field at one level of detail, for {@link Patch.frames}. */ export interface Run { - /** The level of detail, from 0 to 21; 0 by default. */ + /** The level of detail, from 0 to 21. Defaults to 0. */ level?: number; - /** The first entry's number: a safe nonnegative integer. */ + /** The index of the first entry, a safe nonnegative integer. */ first: number; - /** Each frame's coordinate, or each bucket's first and last frames' coordinates. */ + /** + * Each frame's coordinate. At a bucket level, the coordinates of each bucket's first and last + * frames. + */ at: Float64Array | readonly number[]; /** - * A value per row, entry by entry; at a bucket level, every bucket's per-row minimums, then every - * bucket's per-row maximums. NaN marks a missing value. + * One value per row for each entry, entry by entry. At a bucket level, the per-row minimums of + * every bucket, then the per-row maximums of every bucket. NaN marks a missing value. */ values: Float32Array | readonly number[]; } @@ -96,9 +100,9 @@ const utf8 = new TextEncoder(); export let framed: (patch: Patch) => Uint8Array; /** - * A list of changes to a dataset, which `Data.apply` applies in order, all or none. It is written - * as latkit's bytes as it is built, without WebAssembly; each method adds a change and returns the - * patch. + * A list of changes to a dataset. `Data.apply` applies them in order, all or none. The patch is + * written as latkit's bytes as it is built, without WebAssembly. Each method adds a change and + * returns the patch. * * ```ts * const patch = new Patch('grid') @@ -117,8 +121,8 @@ export class Patch { /** Byte offset of the first operation: after the header, dataset name, and operation count. */ #ops: number; /** - * Consecutive appends of float frames to the same fields of every row of one table, not yet - * written. They are written as one append, so each field records all their frames in one write. + * Unwritten consecutive appends of float frames to the same fields of every row of one table. + * They are written as one append, so each field records all their frames in one write. */ #run: Appends | undefined; @@ -142,19 +146,21 @@ export class Patch { } /** - * Reads a patch from `source`: latkit's bytes, one frame as `toBytes()` writes it, or JSON that + * Reads a patch from `source`: latkit's bytes (one frame, as `toBytes()` writes it), or JSON that * any language can write without latkit. The JSON is a list of changes. Each change is keyed by * its method name and holds the method's parameters by name, such as * `{"set": {"table": "Bus", "ids": [1], "fields": {"kv": [345]}}}`. A field may be a slice of a - * binary file, `{file, dtype, offset, count}`, fetched at once. Bytes are read without - * WebAssembly. + * binary file, `{file, dtype, offset, count}`, which is fetched right away. Bytes are read + * without WebAssembly. */ static async read( source: ByteSource | object, options: { - /** The URL the JSON's file slices are relative to; by default, the one it came from. */ + /** The URL that the JSON's file slices are relative to. Defaults to the JSON's URL. */ base?: string; - /** The longest frame in bytes, 256 MiB by default; a longer one fails with a `limit` error. */ + /** + * The longest frame in bytes. Defaults to 256 MiB. A longer frame fails with a `limit` error. + */ limit?: number; } & Wait = {}, ): Promise { @@ -195,9 +201,9 @@ export class Patch { /** * Yields each patch in `stream` as soon as its last byte arrives. The stream carries frames back - * to back, such as a response body, a file, or a socket's messages. Besides the current chunk - * and the patches the caller keeps, at most one frame is buffered. Breaking out of the loop - * cancels the stream. Throws an `invalid` error for a stream that ends inside a frame. + * to back, such as a response body, a file, or a socket's messages. At most one frame is + * buffered, besides the current chunk and any patches the caller keeps. Breaking out of the loop + * cancels the stream. Throws an `invalid` error if the stream ends inside a frame. * * ```ts * for await (const patch of Patch.stream((await fetch('/feed')).body!)) data.apply(patch); @@ -207,8 +213,8 @@ export class Patch { stream: ReadableBytes | AsyncIterable, options: { /** - * The longest frame in bytes, at least 12; 256 MiB by default. Each frame states its length - * up front, so a longer one fails with a `limit` error before its body is allocated. + * The longest frame in bytes, at least 12. Defaults to 256 MiB. Each frame states its length + * up front, so a longer frame fails with a `limit` error before its body is allocated. */ limit?: number; } = {}, @@ -257,7 +263,7 @@ export class Patch { /** * Replaces table `table` with rows `ids` and `fields`, adding the table if it does not exist. - * Without ids, the rows are `0` to `n - 1`, `n` being the length of each field's values. + * Without ids, the rows are `0` to `n - 1`, where `n` is the length of each field's values. */ replace(table: string, fields: Fields): this; replace(table: string, ids: RowIds, fields?: Fields): this; @@ -268,7 +274,7 @@ export class Patch { /** * Writes `fields` for rows `ids` of `table`. Rows with existing ids change, new ids add rows, and * omitted fields keep their values. Adds the table if it does not exist. Without ids, the rows - * are `0` to `n - 1`, `n` being the length of each field's values. + * are `0` to `n - 1`, where `n` is the length of each field's values. */ set(table: string, fields: Fields): this; set(table: string, ids: RowIds, fields?: Fields): this; @@ -285,17 +291,17 @@ export class Patch { } /** - * Records a frame of `table` at each axis coordinate in `at`. Each field gives a value per row - * per frame, frame by frame. A field's first frame makes it a sampled field. Consecutive appends - * of typed float arrays to the same fields of every row write as one change, so a frame per call - * costs no more than one call with every frame. + * Records a frame of `table` at each axis coordinate in `at`. Each field gives one value per row + * for each frame, frame by frame. A field becomes a sampled field at its first frame. Consecutive + * appends of typed float arrays to the same fields of every row are written as one change, so + * appending one frame per call costs no more than one call with every frame. */ append( table: string, at: number | readonly number[], fields: Readonly>, options: { - /** The rows the frames cover, every row by default; other rows keep their last values. */ + /** The rows the frames cover. Defaults to every row. Other rows keep their last values. */ ids?: RowIds; } = {}, ): this { @@ -337,9 +343,9 @@ export class Patch { } /** - * Stores a run of entries of sampled field `field` of `table`, at one level of detail. This is - * what a time tile holds, as `latkit tile` writes it. At level 0 each entry is a frame; at level - * `k`, a bucket of `8^k` frames. None of the entries may be stored already. Entries past the + * Stores a run of entries of sampled field `field` in `table`, at one level of detail. This is + * what a time tile from `latkit tile` holds. At level 0 each entry is a frame. At level `k` each + * entry is a bucket of `8^k` frames. None of the entries may already be stored. Entries past the * field's last frame extend it. */ frames(table: string, field: string, run: Run): this { @@ -384,7 +390,7 @@ export class Patch { return this; } - /** A copy of the patch as latkit's bytes: one frame, naming the dataset if the patch does. */ + /** Returns a copy of the patch as latkit's bytes: one frame, naming the dataset if it has one. */ toBytes(): Uint8Array { return this.#frame().slice(); } @@ -501,8 +507,8 @@ function isReference(field: Values | Reference): field is Reference { } /** - * The ids and fields of `replace` or `set`, given as `(ids, fields)`, or as `(fields)` whose rows - * are `0` to `n - 1`, `n` being the length of the first field's values. + * Returns the ids and fields of `replace` or `set`, given as `(ids, fields)` or as `(fields)`. With + * `(fields)` the rows are `0` to `n - 1`, where `n` is the length of the first field's values. */ function rows(ids: RowIds | Fields, fields: Fields | undefined): [RowIds, Fields] { if (fields !== undefined || isIds(ids)) return [ids as RowIds, fields ?? {}]; diff --git a/npm/src/scene.ts b/npm/src/scene.ts index c6a7ee30..19e97dd1 100644 --- a/npm/src/scene.ts +++ b/npm/src/scene.ts @@ -1,6 +1,6 @@ /** * Builders for the expressions that scene options take. A scene is plain data, typed in - * `inputs.ts`; nothing here needs WebAssembly. + * `inputs.ts`. Nothing here needs WebAssembly. */ import type { TableInput } from './data.js'; @@ -23,9 +23,9 @@ import type { export type Color = string; /** - * An option's value: an {@link Expr}, or its JSON form. In the JSON form, `null` is a missing - * value, a string starting with `$` reads a field (`'$kv'` reads field `kv`), and any other string - * is a CSS color. Color options take a value that computes a color. + * An option's value: an {@link Expr} or its JSON form. In JSON, `null` is a missing value, a string + * starting with `$` reads a field (`'$kv'` reads field `kv`), and any other string is a CSS color. + * Color options take a value that computes a color. */ export type ExprInput = Expr | ExprNode; @@ -33,8 +33,8 @@ const nodeOf = (input: ExprInput): ExprNode => (input instanceof Expr ? input.no /** * A value for every row of a mark: a constant, a color, a field, view state, or arithmetic on them. - * Color options take one that computes a color: a CSS color, a value through a colormap, or a - * conditional choosing between colors. + * Color options take one that computes a color: a CSS color, a value mapped through a colormap, or + * a conditional that chooses between colors. */ export class Expr { /** The expression's JSON form. */ @@ -45,22 +45,22 @@ export class Expr { this.node = node; } - /** The JSON form, which `JSON.stringify` writes for the expression. */ + /** Returns the JSON form. `JSON.stringify` calls it. */ toJSON(): ExprNode { return this.node; } /** * Reads this expression's fields from `table`, such as `weather.table('Station')`, joined by row - * id. A view draws the table's dataset too. + * id. The view also loads that table's dataset. */ from(table: TableInput): Expr { return new Expr(mapFields(this.node, (f) => ({ ...f, from: table }))); } /** - * Reads this expression's sampled fields at the last frame at or before the view's `at`, instead - * of interpolating toward the next frame. + * Reads this expression's sampled fields at the last frame at or before the view's `at`, without + * interpolating toward the next frame. */ step(): Expr { return new Expr(mapFields(this.node, (f) => ({ ...f, step: true }))); @@ -171,13 +171,13 @@ export class Expr { return new Expr({ exp: [this.node] }); } - /** Whether a text field equals `text`, a plain text that is neither a field nor a color. */ + /** Whether a text field equals `text`, plain text read as neither a field nor a color. */ is(text: string): Expr { return new Expr({ is: [this.node, text] }); } /** - * Whether a text field contains `text`, a plain text, ignoring case. In a mark, the field must be + * Whether a text field contains `text` (plain text), ignoring case. In a mark, the field must be * in the mark's own table. */ contains(text: string): Expr { @@ -185,8 +185,8 @@ export class Expr { } /** - * Maps the value to a color for every row through `map`, a catalog name or custom colors. The - * domain fits the values shown unless set with {@link Mapped.domain}. + * Maps the value to a color through `map`, a catalog name or custom colors. The domain fits the + * values shown unless set with {@link Mapped.domain}. */ colormap(map: Colormap): Mapped { return new Mapped({ colormap: map, value: this.node }); @@ -201,11 +201,11 @@ export class Expr { } } -/** A value mapped through a colormap or onto a range, with an input domain that can be set. */ +/** A value mapped through a colormap or onto a range. Its input domain can be set. */ export class Mapped extends Expr { /** - * Sets the input values that map to the colormap's or the range's ends, instead of fitting the - * values shown. + * Sets the input values that map to the ends of the colormap or range. Without it, the domain + * fits the values shown. */ domain(low: number, high: number): Mapped { return new Mapped({ ...(this.node as ScaleNode | ColormapNode), domain: [low, high] }); @@ -234,8 +234,8 @@ function mapFields(input: ExprInput, f: (field: FieldNode) => FieldNode): ExprNo const state = (name: State): Expr => new Expr({ state: name }); /** - * Reads field `name` of each row, written `'$name'` in JSON. `from()` and `step()` give it its - * `{ field }` form. + * Reads field `name` of each row. In JSON it is `'$name'`, or the `{ field }` form after `from()` + * or `step()`. */ export const field = (name: string): Expr => new Expr(`$${name}`); /** Whether the row is selected. */ @@ -250,9 +250,9 @@ export const elapsed = (): Expr => state('elapsed'); export const zoom = (): Expr => state('zoom'); /** - * A size in CSS pixels regardless of the space's unit, so it stays the same on screen as the view - * zooms: `px(1)` keeps a diagram's stroke 1 pixel wide at any zoom. Given a number, the result also - * works as a theme length. + * A size in CSS pixels in any space, so it stays the same on screen as the view zooms. `px(1)` + * keeps a diagram's stroke 1 pixel wide at any zoom. Given a number, the result also works as a + * theme length. */ export function px(size: number): { px: number }; export function px(size: ExprInput): Px; @@ -261,10 +261,10 @@ export function px(size: ExprInput): Px { } /** - * A size in position units (degrees on the earth) regardless of the space's unit, so it scales as - * the view zooms. `limits` bound its on-screen size in CSS pixels: `units('$r', { atLeast: 2 })` - * grows with the map and never shrinks below 2 pixels. A marker keeps its proportions, with the - * limits applied to its larger side. Given only a number, the result also works as a theme length. + * A size in position units (degrees on the earth) in any space, so it scales as the view zooms. + * `limits` bound its size on screen in CSS pixels: `units('$r', { atLeast: 2 })` grows with the + * map and never shrinks below 2 pixels. A marker keeps its proportions, and the limits apply to its + * larger side. Given only a number, the result also works as a theme length. */ export function units(size: number): { units: number }; export function units(size: ExprInput, limits?: Omit): Units; @@ -278,13 +278,13 @@ type Case = [condition: ExprNode, value: ExprNode]; /** * Starts a conditional. `when(c).then(a).when(d).then(b).otherwise(e)` takes the value of the first * case whose condition holds, else `e`. The values may be numbers, colors, or a text field and - * `null`, as the option takes. + * `null`, whichever the option takes. */ export function when(condition: ExprInput): When { return new When(nodeOf(condition)); } -/** A conditional's first condition, awaiting its value from `then`. */ +/** A conditional's first condition. `then` gives its value. */ class When { /** @internal */ constructor(private readonly condition: ExprNode) {} @@ -295,7 +295,7 @@ class When { } } -/** A conditional's cases so far, awaiting another `when` case or the `otherwise` fallback. */ +/** A conditional's cases so far. Add a case with `when`, or finish with `otherwise`. */ class Then { /** @internal */ constructor(private readonly cases: readonly Case[]) {} @@ -311,7 +311,7 @@ class Then { } } -/** A conditional's next condition, awaiting its value from `then`. */ +/** A conditional's next condition. `then` gives its value. */ class Next { /** @internal */ constructor( diff --git a/npm/src/shot.ts b/npm/src/shot.ts index 8016ffe8..0a31854d 100644 --- a/npm/src/shot.ts +++ b/npm/src/shot.ts @@ -8,7 +8,7 @@ import { canvasOf, type View } from './view.js'; /** Options for a {@link Shot}. */ export interface ShotOptions { /** - * Pixels per CSS pixel, 1 by default. Views lay out at `size / scale` CSS pixels, so 2 draws the + * Pixels per CSS pixel. Defaults to 1. Views lay out at `size / scale` CSS pixels, so 2 draws the * same layout twice as sharp. */ scale?: number; @@ -21,16 +21,16 @@ export interface ShotOptions { */ capture?: 'still' | 'now'; /** - * The time in seconds that `elapsed()` expressions and flowing comets show in a still picture. By - * default they follow the view's clock. + * The time in seconds that `elapsed()` expressions and flowing comets show in a still picture. + * Defaults to the view's clock. */ elapsed?: number; } /** - * A picture of views, `size` pixels in all, each view drawn in its region. While a shot draws a - * view, its canvas keeps its last frame. An image draws each view's data as it was when the - * picture began. `Shot.of(view)` makes a shot of what a view's canvas shows. + * A picture of views, `size` pixels in all, with each view drawn in its region. While a shot draws + * a view, the view's canvas keeps its last frame. An image draws each view's data as it was when + * the picture began. `Shot.of(view)` makes a shot of what a view's canvas shows. * * ```ts * const png = await (await Shot.of(view, { scale: 2 }).image()).encode('png'); @@ -47,8 +47,8 @@ export class Shot { size: readonly [number, number], options: ShotOptions & { /** - * The views drawn, later views over earlier ones: each in its own region, or, given as - * `{ view, region }`, in that region of the picture. + * The views drawn, later views over earlier ones. Each draws in its own region, or in + * `region` when given as `{ view, region }`. */ views?: readonly Placed[]; } = {}, @@ -58,9 +58,9 @@ export class Shot { } /** - * A shot of exactly what the canvas showing `view` shows: its views in their regions, at the - * canvas's CSS size times `options.scale` in pixels. Throws an `invalid` error for a view on no - * canvas, which a shot must be given a size for. + * Returns a shot of exactly what the canvas showing `view` shows: its views in their regions, at + * the canvas's CSS size times `options.scale` in pixels. Throws an `invalid` error for a view on + * no canvas. Give such a shot a size with the constructor. */ static of(view: View, options: ShotOptions = {}): Shot { const canvas = canvasOf(view); @@ -87,7 +87,7 @@ export class Shot { * Starts a video of this shot: H.264 in MP4, written to `options.output` as frames are drawn. The * width is rounded down to a multiple of 4 and the height to an even number, as the encoder * requires. Canvases showing the views keep their last frame until the video finishes or aborts. - * Aborting `options.signal` aborts the video and its output; frames have no other time limit. + * Aborting `options.signal` aborts the video and its output. Frames have no other time limit. */ video( options: { @@ -95,7 +95,7 @@ export class Shot { fps: number; /** Where the file's bytes go, locked until the video finishes or aborts. */ output: WritableStream; - /** Bits per second; about 0.15 bits per pixel per frame by default. */ + /** Bits per second. Defaults to about 0.15 bits per pixel per frame. */ bitrate?: number; } & Wait, ): Video { @@ -111,7 +111,7 @@ export class Shot { } } - /** The shot as JSON for the bindings, with times in seconds; the views cross on their own. */ + /** The shot as JSON for the bindings, with times in seconds. The views are passed separately. */ #json(): string { const { scale = 1, transparent = false, capture = 'still', elapsed } = this.options; return JSON.stringify({ size: [...this.size], scale, transparent, capture, elapsed }); @@ -151,7 +151,7 @@ export class Image extends Handle { encode( format: ImageFormat, options: { - /** JPEG quality, from 1 (smallest) to 100 (sharpest); 90 by default. */ + /** JPEG quality, from 1 (smallest) to 100 (sharpest). Defaults to 90. */ quality?: number; } & Wait = {}, ): Promise { @@ -164,7 +164,7 @@ export class Image extends Handle { export type ImageFormat = 'png' | 'jpeg' | 'webp'; /** - * A video being drawn, encoded, and written; created by {@link Shot.video}. Draw frames with + * A video being drawn, encoded, and written. Create one with {@link Shot.video}. Draw frames with * {@link Video.frame} or {@link Video.record}, then call {@link Video.finish}. */ export class Video { @@ -222,9 +222,9 @@ export class Video { } /** - * Draws the next frame once its views are complete, and writes its bytes, waiting on the - * output's backpressure. Calls must not overlap: a call made before the previous one resolves, - * or after `finish()`, rejects with an `invalid` error, and a call after `abort()` with a + * Draws the next frame once its views are complete, then writes its bytes, waiting on the + * output's backpressure. Calls must not overlap. A call made before the previous one resolves, + * or after `finish()`, rejects with an `invalid` error. A call after `abort()` rejects with a * `cancelled` error. A failure aborts the video. */ frame(): Promise { @@ -329,7 +329,7 @@ export class Video { } } - /** The bindings video while the video is open or finishing; else throws why it ended. */ + /** Returns the bindings video while it is open or finishing. Otherwise throws why it ended. */ #open(): bindings.Video { const open = this.#state === 'open' || this.#state === 'finishing'; if (!open || !this.#video) throw this.#ended(); diff --git a/npm/src/view.ts b/npm/src/view.ts index b61c920b..d00ff52e 100644 --- a/npm/src/view.ts +++ b/npm/src/view.ts @@ -1,7 +1,7 @@ /** - * Views: a scene drawn on a canvas from its datasets. A view's state is a set of properties, and - * setting one emits no event. When the user changes that state, the view emits an event named after - * the property. Queries are methods, answered by the frames that draw the view. + * Views: a scene drawn from its datasets on a canvas. A view's state is a set of properties. + * Setting one emits no event. When the user changes one, the view emits an event named after the + * property. Queries are methods, answered by the frames that draw the view. */ import type * as bindings from '../pkg/latkit.js'; @@ -20,8 +20,8 @@ const WHOLE: Rect = [0, 0, 1, 1]; export let canvasOf: (view: View) => Canvas | undefined; /** - * The events a view emits, by type, mapped to each event's detail. Intents (`click`, `open`, - * `menu`, `drag`, `connect`, `delete`) are emitted once each, in the order the user acted. Changes + * The events a view emits, mapping each type to its detail. Intents (`click`, `open`, `menu`, + * `drag`, `connect`, `delete`) are emitted once each, in the order the user acted. Changes * (`hovered`, `selection`, `camera`, `waiting`) are named after the property that changed and carry * only its latest value. An `error` event's detail is a {@link LatkitError}. Setters emit nothing, * so events report only what the application did not do itself. @@ -43,9 +43,10 @@ export type Point = [x: number, y: number]; export type Position = [x: number, y: number, elevation: number]; /** - * A scene drawn from its datasets, on a canvas and into pictures; created by {@link View.create}. - * The canvas passes it pointer, wheel, and key input, and a `Shot` draws it into pictures. - * Destroying it rejects its unanswered queries; its canvas keeps its last frame. + * A scene drawn from its datasets on a canvas and into pictures. Create one with + * {@link View.create}. The canvas passes it pointer, wheel, and key input, and a `Shot` draws it + * into pictures. Destroying a view rejects its unanswered queries, and its canvas keeps the last + * frame. * * ```ts * const grid = new Data('grid').set('Bus', [1, 2], { x: [0, 30], y: [0, 20], kv: [230, 115] }); @@ -85,23 +86,28 @@ export class View extends Handle { } /** - * Creates a view that draws `scene` on `canvas`, in `options.region`, over any views already on - * it; with a `null` canvas, the view draws only into pictures. It draws the datasets its scene's - * tables belong to, and frames everything once it draws, unless given a camera. + * Creates a view that draws `scene` on `canvas`, in `options.region`, over any views already + * there. With a `null` canvas, the view draws only into pictures. The view loads every dataset + * its scene uses. Unless given a camera, it frames everything when it first draws. */ static async create( canvas: HTMLCanvasElement | OffscreenCanvas | null, scene: Scene, options: { - /** Where the view draws on the canvas and in pictures; the whole of them by default. */ + /** The part of the canvas, and of pictures, that the view draws in. Defaults to all of it. */ region?: Rect; /** Where the view looks first. Without one, it frames everything drawn. */ camera?: Camera; - /** Datasets beside those of the scene's tables; see {@link View.sources}. */ + /** Datasets to draw besides those the scene uses. See {@link View.sources}. */ sources?: readonly Source[]; - /** The GPU that draws the view; the first `Gpu` created by default, created if need be. */ + /** + * The GPU that draws the view. Defaults to the first `Gpu` created, creating one if needed. + */ gpu?: Gpu; - /** Pixels per CSS pixel of an `OffscreenCanvas`, 1 by default; the first view on it sets it. */ + /** + * Pixels per CSS pixel of an `OffscreenCanvas`. Defaults to 1. The first view on the canvas + * sets it. + */ pixelRatio?: number; } = {}, ): Promise { @@ -125,8 +131,8 @@ export class View extends Handle { } /** - * Where the view draws on its canvas and in pictures: `[x, y, width, height]` in fractions of - * them, from the top left. Views of one canvas draw in the order they were created. + * The part of its canvas, and of pictures, that the view draws in: `[x, y, width, height]` in + * fractions, from the top left. Views on one canvas draw in the order they were created. */ get region(): Rect { return this.#region; @@ -138,8 +144,9 @@ export class View extends Handle { } /** - * The scene drawn. Marks that match a mark of the previous scene by kind, dataset, table, and - * name keep their state and ease to their new options. An invalid scene leaves the current one. + * The scene drawn. A mark that matches one in the previous scene by kind, dataset, table, and + * name keeps its state and eases to its new options. An invalid scene leaves the current one in + * place. */ get scene(): Scene { return this.#scene; @@ -157,11 +164,11 @@ export class View extends Handle { } /** - * Datasets the view draws besides those of its scene's tables, and in their place where they - * share a name: a mark whose table is written `[data, table]` draws the source named `data`. The - * view draws a `Data` at its latest version, from the frame after each change; a `Snapshot` - * unchanged; and `Tiles` as the pieces it needs. A mark whose dataset no longer fits it draws - * nothing; it is listed in `failures`, and the next frame emits an `error` event. + * Datasets the view draws besides those its scene uses. A source replaces a scene dataset with + * the same name. A mark whose table is written `[data, table]` draws the source named `data`. The + * view draws a `Data` at its latest version from the frame after each change, a `Snapshot` as it + * is, and `Tiles` as the pieces it needs. A mark whose dataset no longer fits it draws nothing. + * It is listed in `failures`, and the next frame emits an `error` event. */ get sources(): readonly Source[] { return this.#sources; @@ -174,7 +181,7 @@ export class View extends Handle { /** * The datasets drawn, as the bindings take them: `sources`, then those of the scene's `tables` - * that no source replaces by name. Throws an `invalid` error for two datasets of one name. + * that no source replaces by name. Throws an `invalid` error when two datasets share a name. */ #drawn(tables: readonly Source[], sources: readonly Source[]): bindings.Source[] { const named = new Set(sources.map((source) => source.name)); @@ -202,13 +209,13 @@ export class View extends Handle { } /** - * Where the view looks. Setting it moves the camera at once; use {@link View.fly} to ease. A + * Where the view looks. Setting it moves the camera at once. Use {@link View.fly} to ease. A * camera of the wrong kind for the space, such as a plot camera on a map, is ignored. * - * Every move keeps the camera within reach. On the earth, the space bounds it, as `Earth` - * describes, inside the insets. Fits narrow it to 3 zoom levels out and 8 in from what they - * framed, with the center near the framed box. When a resize, new insets, or a new space moves - * the camera back within reach, a `camera` event reports it. + * Camera moves stay within the camera's zoom and pan limits. On the earth, the space sets the + * limits inside the insets, as `Earth` describes. A fit narrows them to 3 zoom levels out and 8 + * in from the box it framed, with the center near that box. When a resize, new insets, or a new + * space moves the camera back within its limits, a `camera` event reports it. */ get camera(): Camera { return JSON.parse(view(this).camera) as Camera; @@ -232,7 +239,7 @@ export class View extends Handle { } /** - * Degrees per second the camera turns about its center on a map or plane; 0 stops it. A diagram + * Degrees per second the camera turns about its center on a map or plane. 0 stops it. A diagram * never turns. */ get orbit(): number { @@ -252,20 +259,20 @@ export class View extends Handle { view(this).setSelection(JSON.stringify(rows)); } - /** The row under the pointer, or the row keyboard navigation moved to; `null` if none. */ + /** The row under the pointer or the row keyboard navigation moved to, or `null`. */ get hovered(): Hit | null { return JSON.parse(view(this).hovered) as Hit | null; } /** - * The kinds of pending work that kept the last frame from being complete; empty when it was + * The kinds of pending work that kept the last frame from being complete. Empty when it was * complete. See {@link Waiting}. */ get waiting(): Waiting { return JSON.parse(view(this).waiting) as Waiting; } - /** Whether the last frame was complete, with nothing in `waiting` pending. Shots wait for it. */ + /** Whether the last frame was complete, with nothing in `waiting`. Shots wait for this. */ get complete(): boolean { return view(this).complete; } @@ -289,25 +296,25 @@ export class View extends Handle { } /** - * Frames what `fit` names, every row by default, keeping pitch and bearing. It takes effect on - * the next frame, or a frame or two later when the extent must be measured on the GPU. + * Frames what `fit` names, keeping pitch and bearing. Defaults to every row. It takes effect on + * the next frame, or a frame or two later when the GPU must measure the extent. */ fit(fit: Fit = 'all'): void { view(this).fit(JSON.stringify(fit)); } /** - * Eases the camera to `camera`, kept within reach as {@link View.camera} describes, over twice - * the scene's ease time, or at once when motion is reduced. A camera of the wrong kind for the - * space is ignored. + * Eases the camera to `camera` over twice the scene's ease time, or moves it at once when motion + * is reduced. The camera stays within its limits, as {@link View.camera} describes. A camera of + * the wrong kind for the space is ignored. */ fly(camera: Camera): void { view(this).fly(JSON.stringify(camera)); } /** - * Projects `position` to a point on screen with the current camera, for anchoring HTML overlays; - * `null` when it is behind the camera. + * Projects `position` to a point on screen with the current camera, for anchoring HTML overlays. + * Returns `null` when the position is behind the camera. */ screen(position: Position): Point | null { const point = view(this).screen(Float64Array.from(position)); @@ -315,8 +322,9 @@ export class View extends Handle { } /** - * The position on the ground under screen point `point`: at elevation 0 on a plane, on the map or - * globe on the earth, the values there on a plot. `null` where the point looks past the ground. + * Returns the position on the ground under screen point `point`: at elevation 0 on a plane, on + * the map or globe on the earth, and the values there on a plot. Returns `null` where the point + * looks past the ground. */ position(point: Point): Position | null { const position = view(this).position(Float32Array.from(point)); @@ -324,8 +332,8 @@ export class View extends Handle { } /** - * Resolves to where `row` is on screen once a frame draws the view, or `null` when no mark draws - * it in view. A diagram's vertices resolve at once. + * Resolves to where `row` is on screen once a frame draws the view, or to `null` when no mark + * draws it in view. A diagram's vertices resolve at once. */ screenOf(row: Row, wait: Wait = {}): Promise { return waited(view(this).screenOf(JSON.stringify(row)), wait, 'locating a row'); @@ -338,7 +346,7 @@ export class View extends Handle { async pick( point: Point, options: { - /** How near, in CSS pixels; 0 by default. */ + /** How near, in CSS pixels. Defaults to 0. */ radius?: number; } & Wait = {}, ): Promise { @@ -347,17 +355,17 @@ export class View extends Handle { } /** - * The rows connected to `row`, one set per table: a vertex's neighbors across edges and nets, or - * an edge's or a net's vertices. The cost is proportional to the row's degree. + * Returns the rows connected to `row`, one set per table: a vertex's neighbors across edges and + * nets, or the vertices of an edge or net. The cost is proportional to the row's degree. */ neighbors(row: Row): Rows[] { return JSON.parse(view(this).neighbors(JSON.stringify(row))) as Rows[]; } /** - * Lays out every vertex row without a position again, and resolves to one patch per dataset that - * writes each row's place into its mark's position fields; applied, the places become part of - * the data. Marks whose position is not read from fields are left out. + * Lays out again every vertex row that has no position, and resolves to one patch per dataset. + * Each patch writes each row's place into its mark's position fields, so applying it makes the + * places part of the data. Marks that don't read their position from fields are left out. * * ```ts * for (const patch of await view.arrange()) if (patch.data === grid.name) grid.apply(patch); @@ -369,9 +377,9 @@ export class View extends Handle { } /** - * The version of dataset `name` the view draws, or `undefined` if it draws none of that name: the - * latest version of a `Data` the view has taken, a given snapshot, or the pieces of `Tiles` - * fetched for its camera. + * Returns the version of dataset `name` that the view draws, or `undefined` if it draws no + * dataset by that name. That is the latest version of a `Data` the view has taken, the snapshot + * it was given, or the pieces of `Tiles` fetched for its camera. */ snapshot(name: string): Snapshot | undefined { const snapshot = view(this).snapshot(name); @@ -379,9 +387,9 @@ export class View extends Handle { } /** - * Calls `listener` with each event of `type` emitted while it is registered, and returns a - * function that removes it. Past events are not replayed; read the current state from the - * property a change event is named after. + * Calls `listener` with each event of `type` emitted while it is registered. Returns a function + * that removes it. Past events are not replayed. Read the current state from the property that a + * change event is named after. */ on(type: T, listener: (event: ViewEvents[T]) => void): () => void { let set = this.#listeners.get(type); @@ -419,7 +427,7 @@ function view(owner: View): bindings.View { function encoded(scene: Scene): { json: string; tables: Source[] } { const tables = new Set(); const json = JSON.stringify(scene, function (this: Record, key, value: unknown) { - // `value` is what `toJSON` returned; the holder still has the table itself. + // `value` is what `toJSON` returned. The holder still has the table itself. const held = this[key]; if (held instanceof Table) tables.add(held.source); return value; diff --git a/npm/test/consumer/main.ts b/npm/test/consumer/main.ts index d43b544c..9ba2ff80 100644 --- a/npm/test/consumer/main.ts +++ b/npm/test/consumer/main.ts @@ -28,7 +28,7 @@ async function until(condition: () => boolean): Promise { } } -// The scene names its dataset by name, so each view draws the `grid` its sources give. +// The scene names its dataset, so each view draws the `grid` in its own sources. const scene: Scene = { space: { plane: { grid: 'off' } }, theme: 'dark', diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 42e661f7..04887b7f 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -8,8 +8,8 @@ //! - `rust`: formats, lints, tests (with GPU tests required), documents, and packages the crates, //! and checks that `native` stays out of browser builds. //! - `native-cli`: tests the CLI's native code, which CI runs on macOS and Windows. -//! - `npm`: builds the npm package with `wasm --dev`; lints, documents, type-checks, and tests -//! it; runs the browser gate over that build; then `package` and `package-test`. +//! - `npm`: builds the npm package with `wasm --dev`, then lints, documents, type-checks, and +//! tests it. Runs the browser gate over that build, then `package` and `package-test`. //! - `browser`: the browser gate (`npm/examples/gate/gate.ts`) over a fresh `wasm --dev`. Checks //! that every shader variant's pipelines compile in Chrome within budget, and that every example //! page draws and responds to the pointer without errors or stalls. On `SwiftShader`, which Linux @@ -23,8 +23,8 @@ //! - `wasm [--dev]`: builds `latkit-js`, binds it with the `wasm-bindgen` CLI version in //! `Cargo.lock`, and compiles the npm wrapper. `--dev` builds faster, without link-time //! optimization. -//! - `bindgen-version`: prints the `wasm-bindgen` version in `Cargo.lock`, which CI installs the -//! CLI at. +//! - `bindgen-version`: prints the `wasm-bindgen` version in `Cargo.lock`. CI installs that version +//! of the CLI. //! - `version`: sets the crates' version to the npm package's, as changesets set it. //! - `package`: builds an isolated, optimized npm tarball, the one `pnpm release` publishes, and //! reports its sizes and hashes. From 41de6e17525e59583b1ccb385da98d87c619bbb4 Mon Sep 17 00:00:00 2001 From: Luke Lowery Date: Tue, 27 Oct 2026 09:18:36 -0500 Subject: [PATCH 13/14] polished cli and camera api --- .changeset/spans.md | 10 ++ crates/latkit-cli/src/args.rs | 52 +++++++-- crates/latkit-cli/src/inputs.rs | 36 +++++- crates/latkit-cli/src/render.rs | 197 ++++++++++++++++++++++++++------ crates/latkit-cli/tests/cli.rs | 82 ++++++++++++- crates/latkit-js/src/view.rs | 12 +- crates/latkit/src/view/fit.rs | 16 ++- crates/latkit/src/view/mod.rs | 19 +-- crates/latkit/tests/gestures.rs | 33 +++++- crates/latkit/tests/render.rs | 20 ++++ docs/cli.md | 40 +++++-- docs/reference/api.md | 14 ++- docs/reference/cli.json | 56 ++++++++- docs/reference/cli.md | 10 +- npm/src/view.ts | 16 +-- 15 files changed, 518 insertions(+), 95 deletions(-) create mode 100644 .changeset/spans.md diff --git a/.changeset/spans.md b/.changeset/spans.md new file mode 100644 index 00000000..692f9f2a --- /dev/null +++ b/.changeset/spans.md @@ -0,0 +1,10 @@ +--- +'latkit': minor +--- + +The command line plays an axis span. + +- Added: `--span FROM,TO` and `--rate`. A video or image sequence without `--frames` plays the datasets' span. +- Added: with `--span`, each `--frames` line applies at its `at`, and the camera flies between lines that set it. +- Added: `view.fly(camera, seconds)`. +- Changed: setting or flying the camera replaces a pending fit. diff --git a/crates/latkit-cli/src/args.rs b/crates/latkit-cli/src/args.rs index 774e375d..6d88a516 100644 --- a/crates/latkit-cli/src/args.rs +++ b/crates/latkit-cli/src/args.rs @@ -105,7 +105,7 @@ pub(crate) struct Job { #[arg(long, default_value = "1600x900", value_parser = size)] pub(crate) size: [u32; 2], /// Pixels per CSS pixel. Views lay out size / scale CSS pixels, so 2 draws twice as sharp - #[arg(long, default_value = "1", value_parser = positive)] + #[arg(long, default_value = "1", value_parser = scale)] pub(crate) scale: f32, /// A scene's panel of the picture: its x, y, width, and height, as fractions. Give one per /// scene, in order @@ -136,9 +136,16 @@ pub(crate) struct Job { /// A still's fixed effect time in seconds, for elapsed-time expressions and flowing comets #[arg(long, value_parser = seconds, conflicts_with = "frames")] pub(crate) elapsed: Option, - /// Frames as JSON Lines, one line per output frame (`-` reads stdin) + /// Frames as JSON Lines (`-` reads stdin): one per output frame, or with `--span`, at their `at` #[arg(long, value_name = "FILE")] pub(crate) frames: Option, + /// The axis interval to play. A video or image sequence without `--frames` plays the datasets' + /// span + #[arg(long, value_name = "FROM,TO", value_parser = span, allow_hyphen_values = true)] + pub(crate) span: Option<[f64; 2]>, + /// Axis units played per second. Defaults to 1 + #[arg(long, value_parser = positive)] + pub(crate) rate: Option, /// Output frames per second #[arg(long, default_value = "60", value_parser = fps)] pub(crate) fps: f64, @@ -205,6 +212,11 @@ impl std::str::FromStr for Source { } impl Format { + /// Whether it is a video: MP4 or YUV4MPEG2. + pub(crate) fn video(self) -> bool { + matches!(self, Format::Mp4 | Format::Y4m) + } + /// The pixel layout its pictures compose into. pub(crate) fn pixels(self) -> Pixels { match self { @@ -250,6 +262,12 @@ impl Job { pub(crate) fn sequence(&self) -> bool { self.output.as_deref().is_some_and(|p| pattern(p).is_some()) } + + /// Whether the render plays a span: `--span`, or a video or image sequence without `--frames`, + /// which plays the datasets' span. + pub(crate) fn plays(&self) -> bool { + self.span.is_some() || (self.frames.is_none() && (self.format().video() || self.sequence())) + } } /// The byte range of the `%d` or `%0Nd` at the last `%` in `path`, and its zero-padded width (0 @@ -317,9 +335,17 @@ impl Cli { "--output must end in .png, .jpg, .webp, .mp4, .y4m, or .rgba, unless --format is set", )); } - let video = matches!(format, Format::Mp4 | Format::Y4m); - if (video || job.sequence()) && job.frames.is_none() { - return Err(invalid("a video or an image sequence takes --frames")); + if job.span.is_some() && !(format.video() || job.sequence() || format == Format::Rgba) { + return Err(invalid("--span plays a video, an image sequence, or raw frames")); + } + if job.plays() && (job.at.is_some() || job.elapsed.is_some()) { + return Err(Self::command().error( + ErrorKind::ArgumentConflict, + "a span sets each frame's axis coordinate and time, so it takes no --at or --elapsed", + )); + } + if job.rate.is_some() && !job.plays() { + return Err(invalid("--rate sets how fast a span plays: give --span, or no --frames")); } if job.sequence() && (format.image(job.quality).is_none() || is_stdio(output)) { return Err(invalid("a %d pattern needs an image format: png, jpeg, or webp")); @@ -336,9 +362,19 @@ fn finite(value: &str) -> Result { value.parse::().ok().filter(|n| n.is_finite()).ok_or_else(|| "expected a finite number".into()) } -fn positive(value: &str) -> Result { - let n = finite(value)? as f32; - if n.is_finite() && n > 0.0 { Ok(n) } else { Err("expected a positive number".into()) } +fn positive(value: &str) -> Result { + finite(value).and_then(|n| if n > 0.0 { Ok(n) } else { Err("expected a positive number".into()) }) +} + +fn scale(value: &str) -> Result { + let n = positive(value)? as f32; + if n.is_finite() { Ok(n) } else { Err("expected a positive number".into()) } +} + +fn span(value: &str) -> Result<[f64; 2], String> { + let (from, to) = value.split_once(',').ok_or("expected FROM,TO")?; + let [from, to] = [finite(from.trim())?, finite(to.trim())?]; + if to > from { Ok([from, to]) } else { Err("expected TO greater than FROM".into()) } } fn seconds(value: &str) -> Result { diff --git a/crates/latkit-cli/src/inputs.rs b/crates/latkit-cli/src/inputs.rs index 3aa69375..358cb6ef 100644 --- a/crates/latkit-cli/src/inputs.rs +++ b/crates/latkit-cli/src/inputs.rs @@ -73,11 +73,11 @@ pub(crate) fn dataset(source: &Source, host: &Native, deadline: Duration) -> Res block(deadline, Data::read(&bytes, reading)).map_err(|e| Failure::at(e, path)) } -/// One output frame's changes, from one line of a frames file. +/// One line of a frames file: an output frame's changes, or with `--span`, changes at `at`. #[derive(Deserialize, schemars::JsonSchema)] #[serde(deny_unknown_fields)] pub(crate) struct Frame { - /// The axis coordinate every view shows. + /// The axis coordinate every view shows. With `--span`, where the line applies (required). pub(crate) at: Option, /// Patches by dataset name, applied in the order written. #[serde(default)] @@ -94,6 +94,13 @@ pub(crate) struct Frame { pub(crate) orbit: Option, } +impl Frame { + /// A frame that shows axis coordinate `at` and changes nothing else. + pub(crate) fn showing(at: f64) -> Frame { + Frame { at: Some(at), patch: Patches::default(), camera: None, fly: None, fit: None, insets: None, orbit: None } + } +} + /// One value for every view, or one per view in order. `null` leaves a view unchanged. #[derive(Deserialize, schemars::JsonSchema)] #[serde(untagged)] @@ -145,7 +152,7 @@ impl Frames { } let index = self.index; self.index += 1; - let at = |error: Error| Failure::at(error.at(index).at("frames"), &self.path); + let at = |error: Error| blame(&self.path, index, error); let mut frame: Frame = latkit::from_json(&line).map_err(at)?; if frame.at.is_some_and(|a| !a.is_finite()) { return Err(at(Error::invalid("expected a finite number").at("at"))); @@ -158,6 +165,24 @@ impl Frames { } } + /// Every frame, with its patches loaded, for a span. Each needs `at`, in order. + pub(crate) fn ordered(mut self) -> Result, Failure> { + let mut frames: Vec = Vec::new(); + while let Some(next) = self.next()? { + let before = frames.last().and_then(|(_, frame, _)| frame.at).unwrap_or(f64::NEG_INFINITY); + let error = match next.1.at { + None => Error::invalid("expected `at`, where the span applies the frame"), + Some(at) if at < before => Error::invalid("expected `at` at or after the frame before"), + Some(_) => { + frames.push(next); + continue; + } + }; + return Err(blame(&self.path, next.0, error.at("at"))); + } + Ok(frames) + } + /// Reads and loads frames on a separate thread, a few ahead of the one taken, so their files /// load while frames draw. pub(crate) fn ahead(mut self) -> Ahead { @@ -174,6 +199,11 @@ impl Frames { } } +/// `error` in frame `index` of the frames file at `path`. +pub(crate) fn blame(path: &Path, index: usize, error: Error) -> Failure { + Failure::at(error.at(index).at("frames"), path) +} + /// Frames loaded ahead of drawing. pub(crate) struct Ahead(Receiver>); diff --git a/crates/latkit-cli/src/render.rs b/crates/latkit-cli/src/render.rs index 96c20c91..dc083cae 100644 --- a/crates/latkit-cli/src/render.rs +++ b/crates/latkit-cli/src/render.rs @@ -1,15 +1,16 @@ //! Rendering and validating a job. Rendering creates a view of each scene over the datasets, then -//! takes a still picture, or draws a video of the frames into a sink. Validating reads, loads, and -//! checks every input a render does, including frames, without a GPU. Both apply frames with -//! [`apply`]. +//! takes a still picture, or draws a video into a sink. Validating reads, loads, and checks every +//! input a render does, including frames, without a GPU. Both read what plays with [`playback`] and +//! apply frames with [`apply`]. use std::path::{Path, PathBuf}; +use std::time::Duration; use latkit::host::Native; use latkit::{Camera, Data, Error, Fit, Gpu, Mp4, Name, Patch, Scene, Sink, Tiles, View}; use crate::args::{Format, Job, Source}; -use crate::inputs::{self, Ahead, Each, Frame, Frames}; +use crate::inputs::{self, Each, Frame, Frames, Next, blame}; use crate::report::Report; use crate::sinks::{Commit, Images, Output, Raw, Y4m}; use crate::{Failure, block}; @@ -55,6 +56,11 @@ impl Inputs { self.data.iter().map(latkit::Source::from).chain(self.tiles.iter().map(latkit::Source::from)).collect() } + /// The datasets' span: from the first axis coordinate any of them holds to the last. + fn span(&self) -> Option<[f64; 2]> { + self.data.iter().filter_map(|d| d.snapshot().describe().span).reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])]) + } + /// Creates a view of each scene over every dataset and the tiles, with the job's insets, axis /// coordinate, and camera or fit. fn views(&self, gpu: &Gpu, job: &Job) -> Result, Failure> { @@ -84,7 +90,7 @@ pub(crate) fn render(job: &Job, report: &mut Report) -> Result<(), Failure> { gpu.set_host(inputs.host.clone()); let mut views = inputs.views(&gpu, job)?; let output = job.output.as_deref().expect("render requires --output"); - let Some(path) = &job.frames else { + let Some(playback) = playback(job, &inputs)? else { let mut shot = job.shot(); if let Some(elapsed) = job.elapsed { shot = shot.elapsed(elapsed); @@ -99,9 +105,8 @@ pub(crate) fn render(job: &Job, report: &mut Report) -> Result<(), Failure> { report.done(1, &views, &gpu); return Ok(()); }; - let frames = Frames::open(path, inputs.host.clone(), job.deadline)?.ahead(); let host = inputs.host.clone(); - let mut sequence = Sequence { job, path, gpu: &gpu, views: &mut views, inputs: &inputs, report }; + let sequence = Sequence { job, playback, gpu: &gpu, views: &mut views, inputs: &inputs, report }; let format = job.format(); if let Some(image) = format.image(job.quality) { let images = if job.sequence() { @@ -109,26 +114,135 @@ pub(crate) fn render(job: &Job, report: &mut Report) -> Result<(), Failure> { } else { Images::one(Output::create(output)?, image, host) }; - return sequence.run(frames, images); + return sequence.run(images); } match format { Format::Png | Format::Jpeg | Format::Webp => unreachable!("image formats are handled above"), - Format::Rgba => sequence.run(frames, Raw::new(Output::create(output)?)), - Format::Y4m => sequence.run(frames, Y4m::new(Output::create(output)?)), + Format::Rgba => sequence.run(Raw::new(Output::create(output)?)), + Format::Y4m => sequence.run(Y4m::new(Output::create(output)?)), Format::Mp4 => { let mut mp4 = Mp4::new(Output::create(output)?); if let Some(bitrate) = job.bitrate { mp4 = mp4.bitrate(bitrate); } - sequence.run(frames, mp4) + sequence.run(mp4) + } + } +} + +/// What a render plays: the frames file's lines, in order, and with a span, how its output frames +/// step through the axis. +struct Playback { + lines: Box>>, + span: Option, + /// What errors in the lines name: the frames file, or `--span` without one. + file: PathBuf, +} + +/// An axis interval played at `rate` units per second, and the flights between its lines. +struct Span { + from: f64, + to: f64, + rate: f64, + flights: Flights, +} + +/// What `job` plays, or `None` for a still. +fn playback(job: &Job, inputs: &Inputs) -> Result, Failure> { + let file = job.frames.clone().unwrap_or_else(|| PathBuf::from("--span")); + let frames = job.frames.as_deref().map(|path| Frames::open(path, inputs.host.clone(), job.deadline)).transpose()?; + if !job.plays() { + let Some(frames) = frames else { return Ok(None) }; + let mut lines = frames.ahead().peekable(); + if lines.peek().is_none() { + return Err(Failure::at(Error::invalid("the frames file has no frames"), &file)); } + return Ok(Some(Playback { lines: Box::new(lines), span: None, file })); } + let lines = frames.map(Frames::ordered).transpose()?.unwrap_or_default(); + let [from, to] = job.span.or_else(|| inputs.span()).ok_or_else(|| { + Failure::from(Error::invalid("nothing to play: the datasets record no axis span, so give --span or --frames")) + })?; + let rate = job.rate.unwrap_or(1.0); + let flights = Flights::plan(&lines, inputs.scenes.len(), rate); + let span = Span { from, to, rate, flights }; + Ok(Some(Playback { lines: Box::new(lines.into_iter().map(Ok)), span: Some(span), file })) +} + +/// For each line of a span, the camera each view it sets flies on to, and the axis coordinate of +/// the next line that sets that view's camera. A line that also flies or fits the view starts none. +#[derive(Default)] +struct Flights { + onward: Vec>>, + rate: f64, } -/// A video of the frames file at `path`, one output frame per line. +impl Flights { + fn plan(lines: &[Next], views: usize, rate: f64) -> Flights { + // From the end back: the next camera each view is set to. + let mut later: Vec> = vec![None; views]; + let mut onward = vec![Vec::new(); lines.len()]; + for (j, (_, frame, _)) in lines.iter().enumerate().rev() { + let set = |i: usize| frame.camera.as_ref().and_then(|c| c.of(i)); + let steered = |i: usize| { + frame.fly.as_ref().and_then(|c| c.of(i)).is_some() || frame.fit.as_ref().and_then(|f| f.of(i)).is_some() + }; + onward[j] = (0..views).map(|i| later[i].filter(|_| set(i).is_some() && !steered(i))).collect(); + for (i, later) in later.iter_mut().enumerate() { + if let (Some(camera), Some(at)) = (set(i), frame.at) { + *later = Some((at, camera)); + } + } + } + Flights { onward, rate } + } + + /// Flies each view that line `j` set on to its next camera, from axis coordinate `at`. + fn start(&self, views: &mut [View], j: usize, at: f64) { + let Some(onward) = self.onward.get(j) else { return }; + for (view, onward) in views.iter_mut().zip(onward) { + if let Some((arrive, camera)) = *onward { + view.fly(camera, Some(Duration::from_secs_f64(((arrive - at) / self.rate).max(0.0)))); + } + } + } +} + +/// One output frame: the lines it applies, in order, and in a span, the axis coordinate it shows. +struct Shown { + lines: Vec, + at: Option, +} + +/// The output frames of `lines`: one per line, or with `span`, one per step through it, each +/// applying the lines it reached. +fn shown<'a>( + lines: Box>>, + span: Option<&'a Span>, + fps: f64, +) -> Box> + 'a> { + let Some(span) = span else { + return Box::new(lines.map(|line| line.map(|line| Shown { lines: vec![line], at: None }))); + }; + // A little slack keeps the last frame when the span divides evenly into frames. + let count = ((span.to - span.from) * fps / span.rate + 1e-9).floor() as usize + 1; + let mut lines = lines.peekable(); + Box::new((0..count).map(move |n| { + let at = span.from + n as f64 * span.rate / fps; + let mut due = Vec::new(); + while let Some(line) = + lines.next_if(|line| line.as_ref().is_ok_and(|(_, f, _)| f.at.is_some_and(|key| key <= at))) + { + due.push(line?); + } + Ok(Shown { lines: due, at: Some(at) }) + })) +} + +/// A video of what plays, drawn into a sink. struct Sequence<'a> { job: &'a Job, - path: &'a Path, + playback: Playback, gpu: &'a Gpu, views: &'a mut Vec, inputs: &'a Inputs, @@ -136,27 +250,36 @@ struct Sequence<'a> { } impl Sequence<'_> { - fn run(&mut self, frames: Ahead, sink: S) -> Result<(), Failure> { - let (job, path) = (self.job, self.path); - let mut video = job.shot().video(self.gpu, job.fps, sink)?; - let mut any = false; - for next in frames { - let (index, frame, patches) = next?; - any = true; - let at = |error: Error| Failure::at(error.at(index).at("frames"), path); - apply(&self.inputs.data, &self.inputs.scenes, &frame, patches).map_err(at)?; - steer(self.views, &frame); + fn run(self, sink: S) -> Result<(), Failure> { + let Sequence { job, playback: Playback { lines, span, file }, gpu, views, inputs, report } = self; + // An output frame's own errors name the frames file, or `--span` in a span. + let source = if span.is_some() { Path::new("--span") } else { file.as_path() }; + let mut video = job.shot().video(gpu, job.fps, sink)?; + for next in shown(lines, span.as_ref(), job.fps) { + let Shown { lines, at } = next?; + for (index, mut frame, patches) in lines { + apply(&inputs.data, &inputs.scenes, &frame, patches).map_err(|e| blame(&file, index, e))?; + // A span shows its own axis coordinate, and flies on from each line that sets a camera. + if at.is_some() { + frame.at = None; + } + steer(views, &frame); + if let (Some(at), Some(span)) = (at, &span) { + span.flights.start(views, index, at); + } + } + if let Some(at) = at { + steer(views, &Frame::showing(at)); + } // Only invalid input is the frame's fault. An output that fails names itself. - let drawn = block(job.deadline, video.frame(&mut self.views.iter_mut().collect::>())); - drawn.map_err(|error| if let Error::Invalid { .. } = error { at(error) } else { error.into() })?; - self.report.progress(video.frames(), video.time()); - } - if !any { - return Err(Failure::at(Error::invalid("the frames file has no frames"), path)); + let index = usize::try_from(video.frames()).unwrap_or(usize::MAX); + let drawn = block(job.deadline, video.frame(&mut views.iter_mut().collect::>())); + drawn.map_err(|e| if let Error::Invalid { .. } = e { blame(source, index, e) } else { e.into() })?; + report.progress(video.frames(), video.time()); } let frames = video.frames(); block(job.deadline, video.finish())?.commit()?; - self.report.done(frames, self.views, self.gpu); + report.done(frames, views, gpu); Ok(()) } } @@ -211,7 +334,7 @@ fn steer(views: &mut [View], frame: &Frame) { view.set_camera(camera); } if let Some(camera) = frame.fly.as_ref().and_then(|c| c.of(i)) { - view.fly(camera); + view.fly(camera, None); } if let Some(fit) = frame.fit.as_ref().and_then(|f| f.of(i)) { view.fit(fit); @@ -223,17 +346,15 @@ fn steer(views: &mut [View], frame: &Frame) { /// datasets, cameras, fonts, and every frame applied in turn. pub(crate) fn validate(job: &Job) -> Result<(), Failure> { let inputs = Inputs::read(job)?; - let Some(path) = &job.frames else { return Ok(()) }; - let mut frames = Frames::open(path, inputs.host.clone(), job.deadline)?; - let mut any = false; - while let Some((index, frame, patches)) = frames.next()? { - any = true; - let at = |error: Error| Failure::at(error.at(index).at("frames"), path); + let Some(Playback { lines, file, .. }) = playback(job, &inputs)? else { return Ok(()) }; + for next in lines { + let (index, frame, patches) = next?; + let at = |error: Error| blame(&file, index, error); if apply(&inputs.data, &inputs.scenes, &frame, patches).map_err(at)? { for (_, scene) in &inputs.scenes { scene.validate(&inputs.sources()).map_err(at)?; } } } - if any { Ok(()) } else { Err(Failure::at(Error::invalid("the frames file has no frames"), path)) } + Ok(()) } diff --git a/crates/latkit-cli/tests/cli.rs b/crates/latkit-cli/tests/cli.rs index ec7b52b5..6b9f8102 100644 --- a/crates/latkit-cli/tests/cli.rs +++ b/crates/latkit-cli/tests/cli.rs @@ -90,12 +90,16 @@ fn usage_errors_exit_2_before_reading_or_drawing() { vec!["render", "missing.json", "-o", "out.png", "--elapsed", "1e100"], vec!["render", "missing.json", "-o", "out.png", "--deadline", "0"], vec!["render", "missing.json", "-o", "out.gif"], - vec!["render", "missing.json", "-o", "out.mp4"], + vec!["render", "missing.json", "-o", "out.mp4", "--span", "2,1"], + vec!["render", "missing.json", "-o", "out.png", "--span", "0,1"], + vec!["render", "missing.json", "-o", "out.mp4", "--at", "1"], + vec!["render", "missing.json", "-o", "out.mp4", "--span", "0,1", "--elapsed", "1"], + vec!["render", "missing.json", "--frames", "f.jsonl", "-o", "out.mp4", "--rate", "2"], + vec!["render", "missing.json", "-o", "out.mp4", "--rate", "0"], vec!["render", "missing.json", "--frames", "f.jsonl", "-o", "out.mp4", "--size", "3x64"], vec!["render", "missing.json", "--frames", "f.jsonl", "-o", "out.mp4", "--transparent"], vec!["render", "missing.json", "--frames", "f.jsonl", "-o", "out.mp4", "--fps", "0"], vec!["render", "missing.json", "--frames", "f.jsonl", "-o", "out/%04d.y4m"], - vec!["render", "missing.json", "-o", "out/%04d.png"], vec!["render", "a.json", "b.json", "-o", "out.png"], vec!["render", "a.json", "-o", "out.png", "--tiles", "base.pmtiles"], vec!["validate", "-", "-d", "-"], @@ -387,3 +391,77 @@ fn a_frame_stream_writes_every_output_and_repeats_unchanged_frames() { assert_eq!(std::fs::read(dir.path().join("out.mp4")).unwrap(), mp4, "a failed video leaves the file intact"); } } + +#[test] +fn a_span_plays_frames_at_their_at_and_flies_between_their_cameras() { + let dir = fixture(); + let camera = |x: f64| format!(r#"{{"center":[{x},0],"zoom":2,"pitch":0,"bearing":0}}"#); + let lines = + [format!(r#"{{"at":0,"camera":{}}}"#, camera(0.0)), format!(r#"{{"at":0.05,"camera":{}}}"#, camera(8.0))]; + std::fs::write(dir.path().join("span.jsonl"), lines.join("\n")).unwrap(); + let common = ["scene.json", "-d", "grid.json", "--size=64x64", "--format", "rgba", "-o", "-"]; + let args = [&["render", "--frames", "span.jsonl", "--span", "0,0.05"][..], &common].concat(); + let output = run(dir.path(), &args, None); + if no_gpu(&output) { + return; + } + ok(&output); + // 0.05 axis units at the default rate of 1 and 60 fps: frames at 0, 1/60, 2/60, and 3/60. + let size = 64 * 64 * 4; + let frames: Vec<&[u8]> = output.stdout.chunks(size).collect(); + assert_eq!(frames.len(), 4); + assert!(frames[0] != frames[1] && frames[1] != frames[2], "the camera flies between the lines"); + // The last frame reaches the last line, so it shows that line's camera, as a still with it does. + let last = camera(8.0); + let still = run(dir.path(), &[&["render", "--camera", last.as_str()][..], &common].concat(), None); + ok(&still); + assert_eq!(frames[3], &still.stdout[..], "arrives on the last line"); + + // Each line needs `at`, in order, and its patches name datasets that exist. + for (lines, path) in [ + ([r#"{"at":0}"#, r#"{"orbit":1}"#], "frames[1].at"), + ([r#"{"at":0.02}"#, r#"{"at":0.01}"#], "frames[1].at"), + ([r#"{"at":0}"#, r#"{"at":0.01,"patch":{"absent":[]}}"#], "frames[1].patch"), + ] { + std::fs::write(dir.path().join("bad.jsonl"), lines.join("\n")).unwrap(); + let args = + ["validate", "scene.json", "-d", "grid.json", "--frames", "bad.jsonl", "--span", "0,1", "-o", "x.y4m"]; + let output = run(dir.path(), &args, None); + assert_eq!(output.status.code(), Some(1)); + assert!( + String::from_utf8_lossy(&output.stderr).contains(path), + "{path}: {}", + String::from_utf8_lossy(&output.stderr) + ); + } +} + +#[test] +fn a_video_without_frames_plays_the_datasets_span_at_its_rate() { + let dir = fixture(); + let sampled = r#"{"name":"grid","patch":[ + {"replace":{"table":"Bus","ids":[10,20],"fields":{"x":[-10,10],"y":[0,0]}}}, + {"axis":{"name":"time","unit":"s"}}, + {"append":{"table":"Bus","at":[0,0.05],"fields":{"v":[1,2,3,4]}}}]}"#; + std::fs::write(dir.path().join("sampled.json"), sampled).unwrap(); + let header = b"YUV4MPEG2 W64 H64 F60:1 Ip A1:1 C420jpeg XCOLORRANGE=LIMITED\n"; + let frames = |extra: &[&str]| { + let args = + [&["render", "scene.json", "-d", "sampled.json", "--size=64x64", "-o", "out.y4m"][..], extra].concat(); + let output = run(dir.path(), &args, None); + (!no_gpu(&output)).then(|| { + ok(&output); + let y4m = std::fs::read(dir.path().join("out.y4m")).unwrap(); + (y4m.len() - header.len()) / (6 + 64 * 64 * 3 / 2) + }) + }; + // The span is 0 to 0.05: four frames at a rate of 1, and seven at half that. + if let Some(count) = frames(&[]) { + assert_eq!(count, 4); + assert_eq!(frames(&["--rate", "0.5"]), Some(7)); + } + // Without a span, a video has nothing to play. + let output = run(dir.path(), &["render", "scene.json", "-d", "grid.json", "--size=64x64", "-o", "none.y4m"], None); + assert_eq!(output.status.code(), Some(1)); + assert!(String::from_utf8_lossy(&output.stderr).contains("nothing to play")); +} diff --git a/crates/latkit-js/src/view.rs b/crates/latkit-js/src/view.rs index 22615f28..92f99b98 100644 --- a/crates/latkit-js/src/view.rs +++ b/crates/latkit-js/src/view.rs @@ -60,10 +60,16 @@ impl View { Ok(()) } - /// Eases to `camera` (JSON). - pub fn fly(&self, camera: &str) -> Result<(), Error> { + /// Eases to `camera` (JSON) over `seconds`, or over twice the scene's ease without it. + pub fn fly(&self, camera: &str, seconds: Option) -> Result<(), Error> { let camera = json("camera", camera)?; - self.view.with(|v| v.fly(camera)); + let seconds = seconds + .map(|s| { + std::time::Duration::try_from_secs_f64(s) + .map_err(|_| latkit::Error::invalid("expected nonnegative seconds").at("seconds")) + }) + .transpose()?; + self.view.with(|v| v.fly(camera, seconds)); Ok(()) } diff --git a/crates/latkit/src/view/fit.rs b/crates/latkit/src/view/fit.rs index fa711dca..7839cf02 100644 --- a/crates/latkit/src/view/fit.rs +++ b/crates/latkit/src/view/fit.rs @@ -41,6 +41,14 @@ impl Fitting { pub(crate) fn framing(&self) -> bool { self.wanted.is_some() || self.reading.context().is_some_and(Option::is_some) } + + /// Drops the fit about to move the camera, keeping a plot's measure of its traces. + fn cancel(&mut self) { + self.wanted = None; + if self.reading.context().is_some_and(Option::is_some) { + self.reading.cancel(); + } + } } /// What this frame measures: whether only rows marked for a fit count, and the fit it serves. @@ -76,6 +84,12 @@ impl View { self.fitted(); } + /// Drops a fit not yet applied, as a camera set or flown afterwards replaces it. + pub(super) fn cancel_fit(&mut self) { + self.fitting.cancel(); + self.fitted(); + } + /// Clears the rows marked for a fit once it framed them. fn fitted(&mut self) { if !self.focus.fitting.is_empty() { @@ -210,7 +224,7 @@ impl View { .is_some_and(|s| s[0] >= area[0] && s[0] <= area[2] && s[1] >= area[1] && s[1] <= area[3]); if !inside { let camera = self.camera.centered(middle); - self.fly(camera); + self.fly(camera, None); } } Some(fit) => { diff --git a/crates/latkit/src/view/mod.rs b/crates/latkit/src/view/mod.rs index 76be5825..17c61b5f 100644 --- a/crates/latkit/src/view/mod.rs +++ b/crates/latkit/src/view/mod.rs @@ -541,8 +541,9 @@ impl View { &self.camera } - /// Moves to `camera` at once, stopping any flight or coasting. Like every setter, it emits no - /// event. Ignored unless `camera` [fits](Camera::fits) the scene's space. + /// Moves to `camera` at once, stopping any flight or coasting and replacing a fit not yet + /// applied. Like every setter, it emits no event. Ignored unless `camera` + /// [fits](Camera::fits) the scene's space. /// /// Every camera move stays within the camera's zoom and pan limits. On the earth, the limits /// are those [`Earth`](crate::Earth) describes, for the part of the view inside the @@ -552,6 +553,7 @@ impl View { /// the camera back within the limits and emits a [`Camera`](Event::Camera) event. pub fn set_camera(&mut self, camera: Camera) { if camera.fits(&self.scene.space) { + self.cancel_fit(); self.rig.hold(); self.place_camera(camera); } @@ -585,12 +587,15 @@ impl View { if self.moves() { self.scene.ease } else { Duration::ZERO } } - /// Eases to `camera` over twice the scene's ease, on the view's clock. The target stays within - /// the zoom and pan limits [`set_camera`](View::set_camera) describes. Ignored unless `camera` - /// [fits](Camera::fits) the scene's space. - pub fn fly(&mut self, camera: Camera) { + /// Eases to `camera` on the view's clock, over `seconds`, or over twice the scene's ease when + /// `None`, replacing a fit not yet applied. The target stays within the zoom and pan limits + /// [`set_camera`](View::set_camera) describes. Ignored unless `camera` [fits](Camera::fits) the + /// scene's space. + pub fn fly(&mut self, camera: Camera, seconds: Option) { if camera.fits(&self.scene.space) { - let seconds = (self.ease().max(Duration::from_millis(1)) * 2).as_secs_f64(); + self.cancel_fit(); + let least = Duration::from_millis(1); + let seconds = seconds.unwrap_or_else(|| self.ease().max(least) * 2).max(least).as_secs_f64(); let to = camera.within(&self.reach()); self.rig.fly(self.camera, to, self.clock.now(), seconds); self.wake(); diff --git a/crates/latkit/tests/gestures.rs b/crates/latkit/tests/gestures.rs index 434dbb88..92dd4d71 100644 --- a/crates/latkit/tests/gestures.rs +++ b/crates/latkit/tests/gestures.rs @@ -6,14 +6,17 @@ //! //! Hover is reported whether or not it glows, and follows the frame. A lit row keeps its ports on //! top. Setters emit no events. A scene's first frame requests every pipeline its input can use. -//! The wheel zooms as the controls say, and reduced motion applies every move at once. A flat map -//! keeps the camera over its one world, and a frame reports moving it there. +//! The wheel zooms as the controls say, and reduced motion applies every move at once. A flight +//! takes the seconds it is given. A flat map keeps the camera over its one world, and a frame +//! reports moving it there. #![cfg(feature = "gpu")] mod common; -use common::{SIZE, camera, diagram, frame, gpu, plain_theme, step}; +use std::time::Duration; + +use common::{SIZE, TICK, camera, diagram, frame, gpu, plain_theme, step}; use latkit::{ Button, Camera, Controls, Data, Device, Edges, End, Event, Ids, Input, Key, Modifiers, Motion, Patch, Phase, Row, Rows, Scene, Side, Table, Vertices, View, Zoom, field, @@ -350,11 +353,31 @@ fn setters_emit_no_events_but_a_flight_reports_the_camera() { frame(&gpu, &mut view); let told: Vec = view.events().filter(|e| matches!(e, Event::Camera(_) | Event::Selection { .. })).collect(); assert_eq!(told, Vec::new(), "setters emit no events"); - view.fly(Camera::Map { center: [-10.0, 0.0, 0.0], zoom: 0.0, pitch: 0.0, bearing: 0.0 }); + view.fly(Camera::Map { center: [-10.0, 0.0, 0.0], zoom: 0.0, pitch: 0.0, bearing: 0.0 }, None); let flown = after(&gpu, &mut view, &[], |e| if let Event::Camera(camera) = e { Some(*camera) } else { None }); assert_ne!(flown, set, "a flight reports where it moved"); } +#[test] +fn a_flight_takes_the_seconds_it_is_given() { + let Some(gpu) = gpu() else { return }; + let mut view = view(&gpu, editing()); + let Camera::Map { center, zoom, pitch, bearing } = *view.camera() else { unreachable!("a plane's camera") }; + let x = |view: &View| if let Camera::Map { center, .. } = view.camera() { center[0] } else { f64::NAN }; + let to = Camera::Map { center: [center[0] + 1.0, center[1], center[2]], zoom, pitch, bearing }; + view.fly(to, Some(Duration::from_secs(2))); + // One second of frames in, halfway through the two seconds, smoothstep puts the camera halfway. + let second = (1.0 / TICK).round() as usize; + for _ in 0..second { + step(&gpu, &mut view); + } + assert!((x(&view) - (center[0] + 0.5)).abs() < 1e-9, "halfway: {}", x(&view)); + for _ in 0..second { + step(&gpu, &mut view); + } + assert_eq!(*view.camera(), to, "arrives on time"); +} + #[test] fn waiting_reports_when_the_frame_is_complete() { let Some(gpu) = gpu() else { return }; @@ -477,7 +500,7 @@ fn reduced_motion_applies_every_move_at_once() { let mut system = view(&gpu, Controls::default()); system.set_reduced_motion(true); let to = Camera::Map { center: [-10.0, 5.0, 0.0], zoom: 1.0, pitch: 0.0, bearing: 0.0 }; - system.fly(to); + system.fly(to, None); step(&gpu, &mut system); step(&gpu, &mut system); assert_eq!(*system.camera(), to); diff --git a/crates/latkit/tests/render.rs b/crates/latkit/tests/render.rs index 10bc8974..d7f36941 100644 --- a/crates/latkit/tests/render.rs +++ b/crates/latkit/tests/render.rs @@ -310,6 +310,26 @@ fn new_data_versions_show_and_fit_frames_them() { assert!(close(at(&pixels, center, center), [0, 0, 0, 255]), "the vertex moved away"); } +#[test] +fn a_camera_set_after_a_fit_replaces_it() { + let Some(gpu) = gpu() else { return }; + let data = Data::new("graph"); + data.apply(Table::new("Node", Ids::count(1)).field("x", vec![1000.0]).field("y", vec![1000.0])).expect("valid"); + let scene = Scene::new(plane()) + .theme(plain_theme()) + .draw(Vertices::of("graph", "Node").position(field("x"), field("y")).radius(8.0).color(Color::WHITE)); + let mut view = View::new(&gpu, scene).expect("valid scene"); + view.set_sources([&data]).expect("a source"); + // The fit has not framed the vertex yet when the camera is set, so the camera stays. + view.fit(latkit::Fit::All); + let set = camera(); + view.set_camera(set); + let pixels = frame(&gpu, &mut view); + assert_eq!(*view.camera(), set, "the later camera wins"); + let center = SIZE / 2; + assert!(close(at(&pixels, center, center), [0, 0, 0, 255]), "the vertex stays out of view"); +} + #[test] fn a_mark_with_no_rows_does_not_delay_a_fit() { let Some(gpu) = gpu() else { return }; diff --git a/docs/cli.md b/docs/cli.md index 65562921..b155b443 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -15,6 +15,8 @@ latkit render map.json plot.json -d grid.json -d states=usa.geojson \ --panel 0,0,1,0.8 --panel 0,0.8,1,0.2 --frames frames.jsonl --fps 60 -o film.mp4 latkit render scene.json -d grid.json --frames - --format y4m -o - | ffmpeg -i - film.webm latkit render scene.json -d grid.json --frames frames.jsonl -o frames/%04d.png +latkit render scene.json -d run.latk -o run.mp4 # plays the recording's span +latkit render scene.json -d run.latk --frames frames.jsonl --span 0.6,10 --rate 0.4 -o run.mp4 latkit inspect grid.bin # tables, fields, and the axis, as JSON latkit query grid.bin '{"of":"Branch","filter":{"gt":["$loading",90]},"limit":20}' latkit tile history.bin -o history/ # time tiles, which --tiles opens @@ -26,8 +28,8 @@ At most one input can read stdin (`-`). ## Frames -`--frames` is a JSON Lines file with one line per output frame. Line `n` shows at `n / --fps` -seconds. Every key is optional: +`--frames` is a JSON Lines file with one line per output frame, or with [`--span`](#spans), lines +at axis coordinates. Line `n` shows at `n / --fps` seconds. Every key is optional: ```json { @@ -60,19 +62,33 @@ seconds. Every key is optional: | `orbit` | degrees per second a map or plane turns | `camera`, `fly`, and `fit` take one value for every view, or a list with one value per view (`null` -leaves that view unchanged). The CLI reads and loads up to two frames ahead of the one it is -drawing. Changes ease over each scene's `ease`. Set `"ease": 0` to show each frame's data without -easing. +leaves that view unchanged). A camera or flight replaces a pending fit. The CLI loads up to two +frames ahead of the one it draws. Changes ease over each scene's `ease`; `"ease": 0` shows each +frame's data as is. + +## Spans + +`--span FROM,TO` plays an axis interval at `--rate` units per second (default 1). A video or image +sequence without `--frames` plays the datasets' span. + +With `--span`, each line needs `at`, in order, and applies when playback reaches it. Between lines +that set a view's camera, the view flies from one to the next: + +```text +{"at": 0.6, "camera": {"center": [-92, 36.5], "zoom": 5.09, "pitch": 48, "bearing": -10}} +{"at": 1, "patch": {"grid": [{"set": {"table": "Branch", "ids": ["b1"], "fields": {"open": [true]}}}]}} +{"at": 10, "camera": {"center": [-92, 36.5], "zoom": 5.09, "pitch": 48, "bearing": 20}} +``` ## Outputs -| Output | Writes | -| ------------------------ | ---------------------------------------------------------------- | -| `.png`, `.jpg`, `.webp` | one picture; JPEG at `--quality` | -| `frame-%04d.png` | one image per frame (`--frames`) | -| `.rgba`, `--format rgba` | straight-alpha RGBA, frames back to back | -| `.y4m` | YUV4MPEG2, 4:2:0 BT.709, for any encoder (`--frames`) | -| `.mp4` | H.264 in fragmented MP4 from the platform's encoder (`--frames`) | +| Output | Writes | +| ------------------------ | --------------------------------------------------- | +| `.png`, `.jpg`, `.webp` | one picture; JPEG at `--quality` | +| `frame-%04d.png` | one image per frame | +| `.rgba`, `--format rgba` | straight-alpha RGBA, frames back to back | +| `.y4m` | YUV4MPEG2, 4:2:0 BT.709, for any encoder | +| `.mp4` | H.264 in fragmented MP4 from the platform's encoder | A still picture waits until every view is complete and motion has settled. `--elapsed` sets the time its animated effects show. A video frame where nothing moved repeats the frame before. An diff --git a/docs/reference/api.md b/docs/reference/api.md index e6e59adb..46c016fc 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -1131,8 +1131,9 @@ const view = await View.create(canvas, { {#view-camera .member} **`camera`**`:` [`Camera`](#camera) [¶](#view-camera){.headerlink} -: Where the view looks. Setting it moves the camera at once. Use [`View.fly()`](#view-fly) to ease. A - camera of the wrong kind for the space, such as a plot camera on a map, is ignored. +: Where the view looks. Setting it moves the camera at once, replacing a flight or a fit not yet + applied. Use [`View.fly()`](#view-fly) to ease. A camera of the wrong kind for the space, such as a plot + camera on a map, is ignored. Camera moves stay within the camera's zoom and pan limits. On the earth, the space sets the limits inside the insets, as [`Earth`](#earth) describes. A fit narrows them to 3 zoom levels out and 8 @@ -1215,10 +1216,11 @@ const view = await View.create(canvas, { the next frame, or a frame or two later when the GPU must measure the extent. {#view-fly .member} -**`fly`**`(camera:` [`Camera`](#camera)`): void` [¶](#view-fly){.headerlink} -: Eases the camera to `camera` over twice the scene's ease time, or moves it at once when motion - is reduced. The camera stays within its limits, as [`View.camera`](#view-camera) describes. A camera of - the wrong kind for the space is ignored. +**`fly`**`(camera:` [`Camera`](#camera)`, seconds?: number): void` [¶](#view-fly){.headerlink} +: Eases the camera to `camera` over `seconds`, or over twice the scene's ease time without it, + replacing a fit not yet applied. When motion is reduced, it moves at once. The camera stays + within its limits, as [`View.camera`](#view-camera) describes. A camera of the wrong kind for the space is + ignored. {#view-screen .member} **`screen`**`(position:` [`Position`](#position)`):` [`Point`](#point) `| null` [¶](#view-screen){.headerlink} diff --git a/docs/reference/cli.json b/docs/reference/cli.json index c5aa9e89..508e1b9b 100644 --- a/docs/reference/cli.json +++ b/docs/reference/cli.json @@ -300,7 +300,33 @@ "required": false, "many": false, "global": false, - "help": "Frames as JSON Lines, one line per output frame (`-` reads stdin)", + "help": "Frames as JSON Lines (`-` reads stdin): one per output frame, or with `--span`, at their `at`", + "defaults": [], + "values": [] + }, + { + "id": "span", + "long": "span", + "short": null, + "value": "FROM,TO", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "The axis interval to play. A video or image sequence without `--frames` plays the datasets' span", + "defaults": [], + "values": [] + }, + { + "id": "rate", + "long": "rate", + "short": null, + "value": "RATE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Axis units played per second. Defaults to 1", "defaults": [], "values": [] }, @@ -604,7 +630,33 @@ "required": false, "many": false, "global": false, - "help": "Frames as JSON Lines, one line per output frame (`-` reads stdin)", + "help": "Frames as JSON Lines (`-` reads stdin): one per output frame, or with `--span`, at their `at`", + "defaults": [], + "values": [] + }, + { + "id": "span", + "long": "span", + "short": null, + "value": "FROM,TO", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "The axis interval to play. A video or image sequence without `--frames` plays the datasets' span", + "defaults": [], + "values": [] + }, + { + "id": "rate", + "long": "rate", + "short": null, + "value": "RATE", + "positional": false, + "required": false, + "many": false, + "global": false, + "help": "Axis units played per second. Defaults to 1", "defaults": [], "values": [] }, diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 0b21dfa0..679ffed5 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -162,7 +162,15 @@ Draw scenes of datasets into an image, image sequence, raw frames, or a video. {#cli-render-frames .member} **`--frames`** `` [¶](#cli-render-frames){.headerlink} -: Frames as JSON Lines, one line per output frame (`-` reads stdin). +: Frames as JSON Lines (`-` reads stdin): one per output frame, or with [`--span`](#cli-render-span), at their `at`. + +{#cli-render-span .member} +**`--span`** `` [¶](#cli-render-span){.headerlink} +: The axis interval to play. A video or image sequence without [`--frames`](#cli-render-frames) plays the datasets' span. + +{#cli-render-rate .member} +**`--rate`** `` [¶](#cli-render-rate){.headerlink} +: Axis units played per second. Defaults to 1. {#cli-render-fps .member} **`--fps`** `` [¶](#cli-render-fps){.headerlink} diff --git a/npm/src/view.ts b/npm/src/view.ts index d00ff52e..1da4891b 100644 --- a/npm/src/view.ts +++ b/npm/src/view.ts @@ -209,8 +209,9 @@ export class View extends Handle { } /** - * Where the view looks. Setting it moves the camera at once. Use {@link View.fly} to ease. A - * camera of the wrong kind for the space, such as a plot camera on a map, is ignored. + * Where the view looks. Setting it moves the camera at once, replacing a flight or a fit not yet + * applied. Use {@link View.fly} to ease. A camera of the wrong kind for the space, such as a plot + * camera on a map, is ignored. * * Camera moves stay within the camera's zoom and pan limits. On the earth, the space sets the * limits inside the insets, as `Earth` describes. A fit narrows them to 3 zoom levels out and 8 @@ -304,12 +305,13 @@ export class View extends Handle { } /** - * Eases the camera to `camera` over twice the scene's ease time, or moves it at once when motion - * is reduced. The camera stays within its limits, as {@link View.camera} describes. A camera of - * the wrong kind for the space is ignored. + * Eases the camera to `camera` over `seconds`, or over twice the scene's ease time without it, + * replacing a fit not yet applied. When motion is reduced, it moves at once. The camera stays + * within its limits, as {@link View.camera} describes. A camera of the wrong kind for the space is + * ignored. */ - fly(camera: Camera): void { - view(this).fly(JSON.stringify(camera)); + fly(camera: Camera, seconds?: number): void { + view(this).fly(JSON.stringify(camera), seconds); } /** From 1c59831af937f4e69bf2e1c22cfb967469fca3d6 Mon Sep 17 00:00:00 2001 From: Luke Lowery Date: Tue, 27 Oct 2026 09:49:08 -0500 Subject: [PATCH 14/14] simplify CI --- .github/workflows/ci.yml | 15 +++-- crates/latkit/src/view/labels.rs | 27 +++++---- crates/latkit/tests/labels.rs | 19 +++++- xtask/src/main.rs | 100 +++++++++++++++++-------------- 4 files changed, 96 insertions(+), 65 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c62e1e57..a7c8ff32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,8 +91,11 @@ jobs: with: save-if: ${{ github.ref == 'refs/heads/main' }} # Lavapipe, Vulkan on the CPU, runs the GPU tests instead of skipping; text tests need fonts. - - name: Install a software Vulkan driver and fonts for the GPU tests - run: sudo apt-get update && sudo apt-get install -y mesa-vulkan-drivers fonts-dejavu-core + # Cached, since apt took up to 7 minutes. + - uses: awalsh128/cache-apt-pkgs-action@v1 + with: + packages: mesa-vulkan-drivers fonts-dejavu-core + version: 1 - run: cargo xtask ci rust # The CLI's native code on the other platforms it installs on; Linux runs in `rust`. @@ -113,8 +116,8 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - run: cargo xtask ci native-cli - # The npm package: built from the crate, checked, tested, every pipeline compiled in Chrome on - # SwiftShader, and the packed tarball installed and drawn in a consumer. + # The npm package: built once from the crate in release, checked, tested, every pipeline compiled + # in Chrome on SwiftShader, and that build packed, installed, and drawn in a consumer. npm: needs: changes if: needs.changes.outputs.npm == 'true' @@ -147,8 +150,10 @@ jobs: - name: Install the matching WASM bindings generator if: steps.bindgen-cache.outputs.cache-hit != 'true' run: cargo install wasm-bindgen-cli --version "${{ steps.bindgen.outputs.version }}" --locked - - run: pnpm --dir npm/examples exec playwright-core install --with-deps chromium + # The runner's Chrome drives the browser gate. - run: cargo xtask ci npm + env: + LATKIT_CHROME: /usr/bin/google-chrome # The docs as Read the Docs builds them, from the locked environment: a warning fails. docs: diff --git a/crates/latkit/src/view/labels.rs b/crates/latkit/src/view/labels.rs index 9dcb8022..a84562f7 100644 --- a/crates/latkit/src/view/labels.rs +++ b/crates/latkit/src/view/labels.rs @@ -179,21 +179,22 @@ impl View { break 'marks; } let Some(run) = self.run_of(i, code, inner) else { continue }; + // An empty text gives no label, so it claims no screen cells. A host pads even an + // empty run, so it is never rasterized. + let blank = run.text.trim().is_empty(); let host = inner.host.clone(); - let Some(glyphs) = inner.atlas.text(host.as_deref(), &run) else { - // With no atlas space left this frame, the remaining texts are asked for again - // once the view changes. - if inner.atlas.full() { - break 'marks; - } - // A text that draws nothing, such as an empty one, gives no label, so it - // claims no screen cells. - set += 1; - self.write_words(slots, code, [wgsl::NONE; 2]); - continue; - }; + let glyphs = if blank { None } else { inner.atlas.text(host.as_deref(), &run) }; + // With no atlas space left this frame, the remaining texts are asked for again once + // the view changes. + if glyphs.is_none() && !blank && inner.atlas.full() { + break 'marks; + } set += 1; - self.write_run(i, slots, code, run, glyphs); + match glyphs { + Some(glyphs) => self.write_run(i, slots, code, run, glyphs), + // A text that draws nothing gives no label too. + None => self.write_words(slots, code, [wgsl::NONE; 2]), + } } } self.labels.wanting = set > 0 || left; diff --git a/crates/latkit/tests/labels.rs b/crates/latkit/tests/labels.rs index 7f770d1c..c1ca6a2d 100644 --- a/crates/latkit/tests/labels.rs +++ b/crates/latkit/tests/labels.rs @@ -26,6 +26,23 @@ impl Host for Boxes { } } +/// `Boxes` with 2 blank pixels on each side, as a font host pads every run, so an empty text +/// rasterizes to a blank box. +struct Padded; + +impl Host for Padded { + fn rasterize(&self, run: &TextRun) -> latkit::Result { + let inner = Boxes.rasterize(run)?; + let size = [inner.size[0] + 4, inner.size[1]]; + let mut coverage = vec![0; (size[0] * size[1]) as usize]; + for (row, line) in inner.coverage.chunks(inner.size[0].max(1) as usize).enumerate() { + let at = row * size[0] as usize + 2; + coverage[at..at + line.len()].copy_from_slice(line); + } + Ok(Raster { size, coverage, baseline: inner.baseline }) + } +} + /// `Boxes`, counting the texts it rasterizes. struct Counted(Arc); @@ -128,7 +145,7 @@ fn where_labels_would_overlap_the_row_drawn_on_top_shows_its_label() { #[test] fn an_empty_text_gives_no_label_and_claims_no_room() { let Some(gpu) = gpu() else { return }; - gpu.set_host(Boxes); + gpu.set_host(Padded); let c = SIZE / 2; // The rows of the overlap above, with the second row's text empty: it draws on top but has no // label, so the first row's label shows. diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 04887b7f..e34c24f7 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -44,19 +44,19 @@ fn main() -> ExitCode { "check" => check(), "ci" => ci(&std::env::args().nth(2).unwrap_or_default()), "browser" => browser(), - "bench" => wasm(false).and_then(|()| { + "bench" => wasm(false).and_then(|_| { let flags: Vec = std::env::args().skip(2).collect(); let mut args = vec!["benchmark/run.ts"]; args.extend(flags.iter().map(String::as_str)); run("node", &args) }), - "wasm" => wasm(dev), + "wasm" => wasm(dev).map(drop), "bindgen-version" => bindgen_version().map(|version| println!("{version}")), "version" => version(), "package" => package(), "package-test" => package_test(), "release" => run("node", &["npm/release.mjs"]), - "examples" => wasm(true).and_then(|()| { + "examples" => wasm(true).and_then(|_| { let mut vite = vec!["--dir", "npm/examples", "exec", "vite"]; if std::env::args().any(|a| a == "--open") { vite.push("--open"); @@ -134,68 +134,84 @@ fn rust() -> Result { cargo(&["clippy", "--locked", "-p", "latkit", "--no-default-features", "--all-targets", "--", "-D", "warnings"])?; cargo(&["clippy", "--locked", "-p", "latkit-js", "--target", "wasm32-unknown-unknown", "--", "-D", "warnings"])?; run_with(env!("CARGO"), &["test", "--locked", "--workspace"], &gpu)?; - run_with(env!("CARGO"), &["test", "--locked", "-p", "latkit", "--no-default-features"], &gpu)?; run_with( env!("CARGO"), &["doc", "--locked", "-p", "latkit", "--no-deps", "--all-features"], &[("RUSTDOCFLAGS", "-D warnings")], )?; - let package = |target: &[&str]| { - let mut args = vec!["package", "--locked"]; - // CI's tree is clean. Locally, package uncommitted work as is. - if std::env::var_os("CI").is_none() { - args.push("--allow-dirty"); - } - args.extend(["-p", "latkit"]); - args.extend(target); - cargo(&args) - }; - package(&[])?; - package(&["--target", "wasm32-unknown-unknown", "--features", "web"])?; - cargo(&["check", "--locked", "-p", "latkit", "--target", "wasm32-unknown-unknown", "--features", "native,web"]) + // The published package builds from its own files. The wasm clippy above builds the browser + // target. + let mut package = vec!["package", "--locked", "-p", "latkit"]; + // CI's tree is clean. Locally, package uncommitted work as is. + if std::env::var_os("CI").is_none() { + package.push("--allow-dirty"); + } + cargo(&package) } fn native_cli() -> Result { - cargo(&["test", "--locked", "--package", "latkit", "--features", "native", "--lib", "--test", "validation"])?; - cargo(&["test", "--locked", "--package", "latkit-cli"]) + // One run, so latkit builds once per platform. + cargo(&[ + "test", + "--locked", + "-p", + "latkit", + "-p", + "latkit-cli", + "--features", + "latkit/native", + "--lib", + "--bins", + "--test", + "validation", + "--test", + "cli", + ]) } fn npm() -> Result { - wasm(true)?; + // The release build, which `package` then packs without compiling again. + let shaders = wasm(false)?; pnpm(&["lint"])?; run("node", &["npm/docs.mjs", "--check"])?; pnpm(&["typecheck"])?; pnpm(&["test"])?; - browser_gate()?; + browser_gate(&shaders)?; package()?; package_test() } /// Builds the wasm package, then runs the browser gate over the shaders the build linked. fn browser() -> Result { - wasm(true)?; - browser_gate() + browser_gate(&wasm(true)?) } -fn browser_gate() -> Result { - let shaders = shaders_dir()?; - run("node", &["npm/examples/gate/gate.ts", &shaders]) +/// Runs the browser gate over the linked shaders and `shaders.json` in `shaders`. +fn browser_gate(shaders: &str) -> Result { + run("node", &["npm/examples/gate/gate.ts", shaders]) } -/// The directory where `latkit`'s build script wrote the linked shaders and `shaders.json`. -fn shaders_dir() -> Result { - eprintln!("$ cargo check --locked -p latkit --message-format=json-render-diagnostics"); - let output = Command::new(env!("CARGO")) - .args(["check", "--locked", "-p", "latkit", "--message-format=json-render-diagnostics"]) +fn wasm(dev: bool) -> Result { + build_wasm(dev, "npm") +} + +/// Builds the wasm package into `output`. Returns where latkit's build script wrote the linked +/// shaders and `shaders.json`, the same for every target, which the browser gate reads. +fn build_wasm(dev: bool, output: &str) -> Result { + let profile = if dev { "wasm-dev" } else { "wasm" }; + let args = ["build", "--locked", "-p", "latkit-js", "--profile", profile, "--target", "wasm32-unknown-unknown"]; + eprintln!("$ cargo {}", args.join(" ")); + let built = Command::new(env!("CARGO")) + .args(args) + .arg("--message-format=json-render-diagnostics") .current_dir(root()) .stderr(Stdio::inherit()) .output() .map_err(|e| format!("cargo: {e}"))?; - if !output.status.success() { - return Err(format!("cargo check exited with {}", output.status)); + if !built.status.success() { + return Err(format!("cargo build exited with {}", built.status)); } - let stdout = String::from_utf8_lossy(&output.stdout); - stdout + let shaders = String::from_utf8_lossy(&built.stdout) .lines() .filter_map(|line| serde_json::from_str::(line).ok()) .find(|message| { @@ -203,16 +219,7 @@ fn shaders_dir() -> Result { && message["package_id"].as_str().is_some_and(|id| id.contains("/crates/latkit#")) }) .and_then(|message| message["out_dir"].as_str().map(str::to_owned)) - .ok_or_else(|| "cargo did not report latkit's build output".to_owned()) -} - -fn wasm(dev: bool) -> Result { - build_wasm(dev, "npm") -} - -fn build_wasm(dev: bool, output: &str) -> Result { - let profile = if dev { "wasm-dev" } else { "wasm" }; - cargo(&["build", "--locked", "-p", "latkit-js", "--profile", profile, "--target", "wasm32-unknown-unknown"])?; + .ok_or("cargo did not report latkit's build output")?; let version = bindgen_version()?; let installed = Command::new("wasm-bindgen").arg("--version").output().ok(); let installed = installed.and_then(|o| String::from_utf8(o.stdout).ok()).unwrap_or_default(); @@ -226,7 +233,8 @@ fn build_wasm(dev: bool, output: &str) -> Result { let pkg = format!("{output}/pkg"); let bind = [wasm.as_str(), "--target", "web", "--weak-refs", "--out-dir", &pkg, "--out-name", "latkit"]; run("wasm-bindgen", &bind)?; - run("node", &["npm/build.mjs", "compile", output]) + run("node", &["npm/build.mjs", "compile", output])?; + Ok(shaders) } fn package() -> Result {

    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 X*6zZ8nm-Ht`F?-!H!uEi^3wkSy;OMh diff --git a/docs/index.md b/docs/index.md index 95bbbdce..5143017e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,17 +31,17 @@ cargo add latkit # in Rust ## Quickstart `quickstart(canvas)` draws a dataset on a canvas. When the page is done with them, destroy the -`gpu` and `grid` it returns. The package's consumer test type-checks and renders this file. +`view` and `grid` it returns. The package's consumer test type-checks and renders this file. ```{literalinclude} ../npm/test/consumer/quickstart.ts :language: typescript ``` -A **dataset** is a set of named tables of columns, changed by **patches**. A **scene** describes -what to draw: marks that name tables, in a space, with a theme and controls. A **view** draws a -scene from its **sources** (the datasets it reads) on a canvas or into pictures. Scenes, -expressions, and patches are plain data that need no WebAssembly. `load()` loads the WebAssembly -that datasets and drawing need; `Gpu.create()`, `Data.read()`, and `Patch.read()` call it for you. +A **dataset** is a set of named tables of columns, which changes in place. A **scene** describes +what to draw: marks of a dataset's tables, in a space, with a theme and controls. A **view** draws a +scene on a canvas and into pictures. Scenes and expressions are plain data, and `JSON.stringify` +writes the JSON the command line reads. `load()` loads the WebAssembly that datasets and drawing +need; `View.create()`, `Data.read()`, and `Patch.read()` call it for you. ## Examples diff --git a/docs/reference/api.md b/docs/reference/api.md index aa75d0bd..3993f9fd 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -6,11 +6,11 @@ out: - **Loading.** Scenes, expressions, and patches are plain data. Datasets, views, pictures, and the colormap functions need latkit's WebAssembly: until [`load()`](#load) finishes, they throw an - `unloaded` error. [`Gpu.create()`](#gpu-create), [`Data.read()`](#data-read), and [`Patch.read()`](#patch-read) of JSON load it - themselves. + `unloaded` error. [`View.create()`](#view-create), [`Gpu.create()`](#gpu-create), [`Data.read()`](#data-read), and [`Patch.read()`](#patch-read) of + JSON load it themselves. - **Errors.** Everything latkit throws or rejects with is a [`LatkitError`](#latkiterror). Input it cannot use - fails with an `invalid` error whose `path` names the part, such as - `marks[2].vertices.color`, and changes nothing. + fails with an `invalid` error whose `path` names the part, such as `marks[2].color`, and + changes nothing. - **Waits.** Every promise waits within its [`Wait`](#wait): until its `signal` aborts, or 30 seconds. - **Lifetimes.** A [`Handle`](#handle) holds memory until `destroy()`, or a `using` declaration, frees it. @@ -64,7 +64,7 @@ The error latkit throws: an `Error` whose `code` says what kind of failure it is {#latkiterror-path .member} **`path`**`?: string` [¶](#latkiterror-path){.headerlink} -: The invalid part of the input, such as `marks[2].vertices.color`, on `invalid` errors. +: The invalid part of the input, such as `marks[2].color`, on `invalid` errors. (wait)= @@ -101,7 +101,7 @@ its other methods throw a `destroyed` error. ## Data and patches -Datasets of named tables, the patches that change them, written without WebAssembly, and the sources views draw. +Datasets of named tables, their tables that marks draw, and the patches that change them, written without WebAssembly. (dataset)= @@ -118,11 +118,15 @@ and a [`Snapshot`](#snapshot), which reads its own. {#dataset-name .member} `readonly` **`name`**`: string` [¶](#dataset-name){.headerlink} -: The dataset's name, by which a mark's `data` option draws it. +: The dataset's name: the first part of its tables' JSON form. ```{rubric} Methods ``` +{#dataset-table .member} +**`table`**`(name: string):` [`Table`](#table) [¶](#dataset-table){.headerlink} +: The dataset's table `name`, for a mark to draw or a field to read from. + {#dataset-describe .member} **`describe`**`():` [`Description`](#description) [¶](#dataset-describe){.headerlink} : The tables and their row counts, each field and its kind, and the axis and its span. @@ -160,15 +164,15 @@ and a [`Snapshot`](#snapshot), which reads its own. {.signature} `class Data extends` [`Dataset`](#dataset) -A dataset that changes in place, only by patches, each applied whole or not at all. Views draw -its latest version from the frame after each patch. A [`Snapshot`](#snapshot) keeps one version and -shares its pages with the dataset, so an undo history costs only the pages that differ. -Destroying it leaves views drawing it and its snapshots usable. +A dataset that changes in place, each change applied whole or not at all. Each method of +[`Patch`](#patch) is also a method here that applies that one change at once; `apply` applies a patch +of several. Views draw its latest version from the frame after each change. A [`Snapshot`](#snapshot) +keeps one version and shares its pages with the dataset, so an undo history costs only the pages +that differ. Destroying it leaves views drawing it and its snapshots usable. ```ts -const grid = new Data('grid').apply(new Patch().replace('Bus', [1, 2], { kv: [230, 115] })); -view.sources = [grid]; -grid.apply(new Patch().set('Bus', [2], { kv: [69] })); // drawn next frame +const grid = new Data('grid').set('Bus', [1, 2], { kv: [230, 115] }); +grid.set('Bus', [2], { kv: [69] }); // drawn the next frame ``` ```{rubric} Constructor @@ -205,6 +209,50 @@ grid.apply(new Patch().set('Bus', [2], { kv: [69] })); // drawn next frame : Applies every change in `patch`, or none when one is invalid, and returns this dataset. An `invalid` error's `path` names the failing change, such as `patch[1].Bus.kv`. +{#data-axis .member} +**`axis`**`(name: string, unit: string): this` [¶](#data-axis){.headerlink} +: Applies [`Patch.axis()`](#patch-axis): names the axis that sampled fields are recorded over. + +{#data-replace .member} +**`replace`**`(table: string, fields:` [`Fields`](#fields)`): this` [¶](#data-replace){.headerlink} +: Applies [`Patch.replace()`](#patch-replace): replaces table `table` with rows `ids`, or `0` to `n - 1`, and + `fields`. + +{.member} +**`replace`**`(table: string, ids:` [`RowIds`](#rowids)`, fields?:` [`Fields`](#fields)`): this` +: Applies [`Patch.replace()`](#patch-replace): replaces table `table` with rows `ids`, or `0` to `n - 1`, and + `fields`. + +{#data-set .member} +**`set`**`(table: string, fields:` [`Fields`](#fields)`): this` [¶](#data-set){.headerlink} +: Applies [`Patch.set()`](#patch-set): writes `fields` for rows `ids` of `table`, or `0` to `n - 1`, adding + new rows. + +{.member} +**`set`**`(table: string, ids:` [`RowIds`](#rowids)`, fields?:` [`Fields`](#fields)`): this` +: Applies [`Patch.set()`](#patch-set): writes `fields` for rows `ids` of `table`, or `0` to `n - 1`, adding + new rows. + +{#data-remove .member} +**`remove`**`(table: string, ids:` [`RowIds`](#rowids)`): this` [¶](#data-remove){.headerlink} +: Applies [`Patch.remove()`](#patch-remove): removes the rows of `table` with `ids`. + +{#data-append .member} +**`append`**`(table: string, at: number | readonly number[], fields: Readonly>, options?: { ids?:` [`RowIds`](#rowids) `}): this` [¶](#data-append){.headerlink} +: Applies [`Patch.append()`](#patch-append): records frames of `table` at the axis coordinates `at`. + + {#data-append-ids .member} + **`ids`**`?:` [`RowIds`](#rowids) [¶](#data-append-ids){.headerlink} + : The rows the frames cover, every row by default; other rows keep their last values. + +{#data-forget .member} +**`forget`**`(before: number): this` [¶](#data-forget){.headerlink} +: Applies [`Patch.forget()`](#patch-forget): drops every sampled field's frames before `before`. + +{#data-frames .member} +**`frames`**`(table: string, field: string, run:` [`Run`](#run)`): this` [¶](#data-frames){.headerlink} +: Applies [`Patch.frames()`](#patch-frames): stores a run of a sampled field's entries, as a time tile holds. + {#data-snapshot .member} **`snapshot`**`():` [`Snapshot`](#snapshot) [¶](#data-snapshot){.headerlink} : The current version as a [`Snapshot`](#snapshot), which later patches leave unchanged. O(1). @@ -261,6 +309,13 @@ bytes at `url` decide the kind: `readonly` **`url`**`: string` [¶](#tiles-url){.headerlink} : Where the tiles are. +```{rubric} Methods +``` + +{#tiles-table .member} +**`table`**`(name: string):` [`Table`](#table) [¶](#tiles-table){.headerlink} +: The tiles' table `name`, such as a vector tile layer, for a mark to draw. + (source)= ### `Source` @@ -268,7 +323,52 @@ bytes at `url` decide the kind: {.signature} `type Source =` [`Data`](#data) `|` [`Snapshot`](#snapshot) `|` [`Tiles`](#tiles) -A dataset a view draws; views match it to the scene's marks by the dataset's name. +A dataset a view draws: the one a [`Table`](#table) belongs to, or one of the view's `sources`. + +(table)= + +### `Table` + +{.signature} +`class Table` + +A table of a dataset, which a mark draws or a field reads from: `grid.table('Bus')`. A view draws +the datasets its scene's tables belong to. Its JSON form is `[data, table]`. + +```{rubric} Constructor +``` + +{#table-new .member} +**`new Table`**`(source:` [`Source`](#source)`, name: string)` [¶](#table-new){.headerlink} +: Table `name` of `source`. + +```{rubric} Properties +``` + +{#table-source .member} +`readonly` **`source`**`:` [`Source`](#source) [¶](#table-source){.headerlink} +: The dataset the table belongs to. + +{#table-name .member} +`readonly` **`name`**`: string` [¶](#table-name){.headerlink} +: The table's name. + +```{rubric} Methods +``` + +{#table-tojson .member} +**`toJSON`**`(): [data: string, table: string]` [¶](#table-tojson){.headerlink} +: The JSON form, `[data, table]`, which `JSON.stringify` writes for the table. + +(tableinput)= + +### `TableInput` + +{.signature} +`type TableInput =` [`Table`](#table) `| readonly [data: string, table: string]` + +A table that a mark draws or a field reads from: a dataset's [`Table`](#table), or its JSON form +`[data, table]`, which names a dataset the view's `sources` give. (queryresult)= @@ -392,13 +492,26 @@ const patch = new Patch('grid') `axis('time', 's')`. {#patch-replace .member} -**`replace`**`(table: string, ids:` [`RowIds`](#rowids)`, fields?:` [`Fields`](#fields)`): this` [¶](#patch-replace){.headerlink} +**`replace`**`(table: string, fields:` [`Fields`](#fields)`): this` [¶](#patch-replace){.headerlink} +: Replaces table `table` with rows `ids` and `fields`, adding the table if it does not exist. + Without ids, the rows are `0` to `n - 1`, `n` being the length of each field's values. + +{.member} +**`replace`**`(table: string, ids:` [`RowIds`](#rowids)`, fields?:` [`Fields`](#fields)`): this` : Replaces table `table` with rows `ids` and `fields`, adding the table if it does not exist. + Without ids, the rows are `0` to `n - 1`, `n` being the length of each field's values. {#patch-set .member} -**`set`**`(table: string, ids:` [`RowIds`](#rowids)`, fields?:` [`Fields`](#fields)`): this` [¶](#patch-set){.headerlink} +**`set`**`(table: string, fields:` [`Fields`](#fields)`): this` [¶](#patch-set){.headerlink} +: Writes `fields` for rows `ids` of `table`. Rows with existing ids change, new ids add rows, and + omitted fields keep their values. Adds the table if it does not exist. Without ids, the rows + are `0` to `n - 1`, `n` being the length of each field's values. + +{.member} +**`set`**`(table: string, ids:` [`RowIds`](#rowids)`, fields?:` [`Fields`](#fields)`): this` : Writes `fields` for rows `ids` of `table`. Rows with existing ids change, new ids add rows, and - omitted fields keep their values. Adds the table if it does not exist. + omitted fields keep their values. Adds the table if it does not exist. Without ids, the rows + are `0` to `n - 1`, `n` being the length of each field's values. {#patch-remove .member} **`remove`**`(table: string, ids:` [`RowIds`](#rowids)`): this` [¶](#patch-remove){.headerlink} @@ -420,29 +533,12 @@ const patch = new Patch('grid') : Drops every sampled field's frames before axis coordinate `before`. {#patch-frames .member} -**`frames`**`(table: string, field: string, run: { level?: number; first: number; at: Float64Array | readonly number[]; values: Float32Array | readonly number[] }): this` [¶](#patch-frames){.headerlink} +**`frames`**`(table: string, field: string, run:` [`Run`](#run)`): this` [¶](#patch-frames){.headerlink} : Stores a run of entries of sampled field `field` of `table`, at one level of detail. This is what a time tile holds, as `latkit tile` writes it. At level 0 each entry is a frame; at level `k`, a bucket of `8^k` frames. None of the entries may be stored already. Entries past the field's last frame extend it. - {#patch-frames-level .member} - **`level`**`?: number` [¶](#patch-frames-level){.headerlink} - : The level of detail, from 0 to 21; 0 by default. - - {#patch-frames-first .member} - **`first`**`: number` [¶](#patch-frames-first){.headerlink} - : The first entry's number: a safe nonnegative integer. - - {#patch-frames-at .member} - **`at`**`: Float64Array | readonly number[]` [¶](#patch-frames-at){.headerlink} - : Each frame's coordinate, or each bucket's first and last frames' coordinates. - - {#patch-frames-values .member} - **`values`**`: Float32Array | readonly number[]` [¶](#patch-frames-values){.headerlink} - : A value per row, entry by entry; at a bucket level, every bucket's per-row minimums, then - every bucket's per-row maximums. NaN marks a missing value. - {#patch-extend .member} **`extend`**`(next: Patch): this` [¶](#patch-extend){.headerlink} : Appends `next`'s changes to this patch, which keeps its own dataset name. @@ -541,6 +637,32 @@ Builds a [`Reference`](#reference) field: for each row, the id of the row of `ta A table's fields by name: values, or a [`Reference`](#reference). +(run)= + +### `Run` + +{.signature} +`interface Run` + +A run of a sampled field's entries at one level of detail, as [`Patch.frames()`](#patch-frames) stores. + +{#run-level .member} +**`level`**`?: number` [¶](#run-level){.headerlink} +: The level of detail, from 0 to 21; 0 by default. + +{#run-first .member} +**`first`**`: number` [¶](#run-first){.headerlink} +: The first entry's number: a safe nonnegative integer. + +{#run-at .member} +**`at`**`: Float64Array | readonly number[]` [¶](#run-at){.headerlink} +: Each frame's coordinate, or each bucket's first and last frames' coordinates. + +{#run-values .member} +**`values`**`: Float32Array | readonly number[]` [¶](#run-values){.headerlink} +: A value per row, entry by entry; at a bucket level, every bucket's per-row minimums, then every + bucket's per-row maximums. NaN marks a missing value. + ## Expressions Builders for the per-row expressions that scene options take, colors among them. @@ -578,8 +700,9 @@ conditional choosing between colors. : The JSON form, which `JSON.stringify` writes for the expression. {#expr-from .member} -**`from`**`(data: string, table: string): Expr` [¶](#expr-from){.headerlink} -: Reads this expression's fields from table `table` of dataset `data`, joined by row id. +**`from`**`(table:` [`TableInput`](#tableinput)`): Expr` [¶](#expr-from){.headerlink} +: Reads this expression's fields from `table`, such as `weather.table('Station')`, joined by row + id. A view draws the table's dataset too. {#expr-step .member} **`step`**`(): Expr` [¶](#expr-step){.headerlink} @@ -948,98 +1071,7 @@ categorical map gives one solid band per category. ## Views -A GPU makes views, and canvases that show them. - -(gpu)= - -### `Gpu` - -{.signature} -`class Gpu extends` [`Handle`](#handle) - -A WebGPU device that views draw with. The views and canvases of one `Gpu` share its memory and -draw together in each animation frame, so linked views always show the same moment. Destroying -it destroys its canvases, views, and frame listeners; other GPUs and datasets stay usable. - -```{rubric} Properties -``` - -{#gpu-lost .member} -`readonly` **`lost`**`: boolean` [¶](#gpu-lost){.headerlink} -: Whether the device was lost. Drawing then does nothing; create a new [`Gpu`](#gpu). - -{#gpu-budget .member} -**`budget`**`:` [`Budget`](#budget) [¶](#gpu-budget){.headerlink} -: The GPU memory budget for data, shared by every view of this GPU; see [`Budget`](#budget). Defaults - to half the device's largest storage buffer, and 32 MiB uploaded per frame. - -{#gpu-work .member} -`readonly` **`work`**`:` [`Work`](#work) [¶](#gpu-work){.headerlink} -: Exact counts of the work this GPU has done since it was created; see [`Work`](#work). The counts - repeat for the same budget, device limits, and frames, so tests and benchmarks can assert them. - -{#gpu-resident .member} -`readonly` **`resident`**`: number` [¶](#gpu-resident){.headerlink} -: Bytes of GPU memory its views draw from: data pages, and each view's per-row data. - -{#gpu-uploading .member} -`readonly` **`uploading`**`: number` [¶](#gpu-uploading){.headerlink} -: Bytes waiting to upload to the GPU, across its views. - -```{rubric} Methods -``` - -{#gpu-create .member} -`static` **`create`**`(): Promise` [¶](#gpu-create){.headerlink} -: Requests a WebGPU device; the first call takes the one [`load()`](#load) requested. Rejects with an - `unsupported` error when the browser has no WebGPU device. - -{#gpu-view .member} -**`view`**`(scene:` [`Scene`](#scene)`, options?: { sources?: readonly` [`Source`](#source)`[] }):` [`View`](#view) [¶](#gpu-view){.headerlink} -: Creates a view that draws `scene`. Show it in a [`Canvas`](#canvas), or draw it into pictures with a - [`Shot`](#shot), with or without a canvas. - - {#gpu-view-sources .member} - **`sources`**`?: readonly` [`Source`](#source)`[]` [¶](#gpu-view-sources){.headerlink} - : The datasets the scene draws; see [`View.sources`](#view-sources). - -{#gpu-canvas .member} -**`canvas`**`(target: HTMLCanvasElement | OffscreenCanvas, options?: { layout?: readonly` [`Panel`](#panel)`[]; pixelRatio?: number }):` [`Canvas`](#canvas) [¶](#gpu-canvas){.headerlink} -: Creates a canvas that shows views in `target`. A `` element sizes itself to its CSS box - at the device pixel ratio, and passes pointer, wheel, and key input to the views in its panels. - An `OffscreenCanvas`, as in a worker, keeps the size it is given and takes no input. - - {#gpu-canvas-layout .member} - **`layout`**`?: readonly` [`Panel`](#panel)`[]` [¶](#gpu-canvas-layout){.headerlink} - : The views shown and their regions; see [`Canvas.layout`](#canvas-layout). None by default. - - {#gpu-canvas-pixelratio .member} - **`pixelRatio`**`?: number` [¶](#gpu-canvas-pixelratio){.headerlink} - : Pixels per CSS pixel of an `OffscreenCanvas`, 1 by default. - -{#gpu-on .member} -**`on`**`(type: 'frame', listener: (now: number) => void): () => void` [¶](#gpu-on){.headerlink} -: Calls `listener` every animation frame with the frame's timestamp in milliseconds, before the - views draw, so changes it makes show in that frame. While any listener is registered, a frame - runs every animation frame. Returns a function that removes the listener. - -{#gpu-step .member} -**`step`**`(now: number): void` [¶](#gpu-step){.headerlink} -: Draws one frame now, at `now` milliseconds on the `requestAnimationFrame` clock: it runs the - frame listeners, then draws every canvas that needs a frame. After the first call, the GPU - requests no more animation frames, so a test or benchmark that steps every frame draws the - same frames on any device. - -{#gpu-done .member} -**`done`**`(wait?:` [`Wait`](#wait)`): Promise` [¶](#gpu-done){.headerlink} -: Resolves once the GPU has finished its submitted work, every readback has arrived, and every - pipeline a view or shot requested has compiled. A frame stepped after that uses all of it. - -{#gpu-landed .member} -**`landed`**`(wait?:` [`Wait`](#wait)`): Promise` [¶](#gpu-landed){.headerlink} -: Resolves when, after this call, work a view is waiting on arrives (a readback, a pipeline - compile, a job, a label text, or a fetch), or a view requests a frame. A harness that steps - frames awaits it to know when to step again. +Views draw scenes on canvases, with the GPU they share. (view)= @@ -1048,13 +1080,30 @@ it destroys its canvases, views, and frame listeners; other GPUs and datasets st {.signature} `class View extends` [`Handle`](#handle) -A scene drawn from its sources; created by [`Gpu.view()`](#gpu-view). A canvas shows it and passes it input, -and a [`Shot`](#shot) draws it into pictures. Destroying it rejects its unanswered queries; a canvas -showing it keeps its last frame. +A scene drawn from its datasets, on a canvas and into pictures; created by [`View.create()`](#view-create). +The canvas passes it pointer, wheel, and key input, and a [`Shot`](#shot) draws it into pictures. +Destroying it rejects its unanswered queries; its canvas keeps its last frame. + +```ts +const grid = new Data('grid').set('Bus', [1, 2], { x: [0, 30], y: [0, 20], kv: [230, 115] }); +const view = await View.create(canvas, { + space: 'plane', + marks: [{ vertices: grid.table('Bus'), position: ['$x', '$y'], color: field('kv').colormap('viridis') }], +}); +``` ```{rubric} Properties ``` +{#view-gpu .member} +`readonly` **`gpu`**`:` [`Gpu`](#gpu) [¶](#view-gpu){.headerlink} +: The GPU that draws the view. + +{#view-region .member} +**`region`**`:` [`Rect`](#rect) [¶](#view-region){.headerlink} +: Where the view draws on its canvas and in pictures: `[x, y, width, height]` in fractions of + them, from the top left. Views of one canvas draw in the order they were created. + {#view-scene .member} **`scene`**`:` [`Scene`](#scene) [¶](#view-scene){.headerlink} : The scene drawn. Marks that match a mark of the previous scene by kind, dataset, table, and @@ -1062,10 +1111,11 @@ showing it keeps its last frame. {#view-sources .member} **`sources`**`: readonly` [`Source`](#source)`[]` [¶](#view-sources){.headerlink} -: The datasets the view draws, matched to marks by dataset name, which must be unique. The view - draws a [`Data`](#data) at its latest version, from the frame after each patch; a [`Snapshot`](#snapshot) unchanged; - and [`Tiles`](#tiles) as the pieces it needs. A mark whose dataset no longer fits it draws nothing; it is - listed in `failures`, and the next frame emits an `error` event. +: Datasets the view draws besides those of its scene's tables, and in their place where they + share a name: a mark whose table is written `[data, table]` draws the source named `data`. The + view draws a [`Data`](#data) at its latest version, from the frame after each change; a [`Snapshot`](#snapshot) + unchanged; and [`Tiles`](#tiles) as the pieces it needs. A mark whose dataset no longer fits it draws + nothing; it is listed in `failures`, and the next frame emits an `error` event. {#view-at .member} **`at`**`: number` [¶](#view-at){.headerlink} @@ -1125,6 +1175,32 @@ showing it keeps its last frame. ```{rubric} Methods ``` +{#view-create .member} +`static` **`create`**`(canvas: HTMLCanvasElement | OffscreenCanvas | null, scene:` [`Scene`](#scene)`, options?: { region?:` [`Rect`](#rect)`; camera?:` [`Camera`](#camera)`; sources?: readonly` [`Source`](#source)`[]; gpu?:` [`Gpu`](#gpu)`; pixelRatio?: number }): Promise` [¶](#view-create){.headerlink} +: Creates a view that draws `scene` on `canvas`, in `options.region`, over any views already on + it; with a `null` canvas, the view draws only into pictures. It draws the datasets its scene's + tables belong to, and frames everything once it draws, unless given a camera. + + {#view-create-region .member} + **`region`**`?:` [`Rect`](#rect) [¶](#view-create-region){.headerlink} + : Where the view draws on the canvas and in pictures; the whole of them by default. + + {#view-create-camera .member} + **`camera`**`?:` [`Camera`](#camera) [¶](#view-create-camera){.headerlink} + : Where the view looks first. Without one, it frames everything drawn. + + {#view-create-sources .member} + **`sources`**`?: readonly` [`Source`](#source)`[]` [¶](#view-create-sources){.headerlink} + : Datasets beside those of the scene's tables; see [`View.sources`](#view-sources). + + {#view-create-gpu .member} + **`gpu`**`?:` [`Gpu`](#gpu) [¶](#view-create-gpu){.headerlink} + : The GPU that draws the view; the first [`Gpu`](#gpu) created by default, created if need be. + + {#view-create-pixelratio .member} + **`pixelRatio`**`?: number` [¶](#view-create-pixelratio){.headerlink} + : Pixels per CSS pixel of an `OffscreenCanvas`, 1 by default; the first view on it sets it. + {#view-fit .member} **`fit`**`(fit?:` [`Fit`](#fit)`): void` [¶](#view-fit){.headerlink} : Frames what `fit` names, every row by default, keeping pitch and bearing. It takes effect on @@ -1187,6 +1263,15 @@ showing it keeps its last frame. function that removes it. Past events are not replayed; read the current state from the property a change event is named after. +(rect)= + +### `Rect` + +{.signature} +`type Rect = readonly [x: number, y: number, width: number, height: number]` + +A region `[x, y, width, height]` in fractions of a canvas or picture, from its top left. + (viewevents)= ### `ViewEvents` @@ -1228,63 +1313,75 @@ A point on screen, in CSS pixels from the view's top left corner. A position in the space's coordinates: x, y, and elevation. On the earth, x and y are longitude and latitude; on a plot, the axis coordinate and the value. -(canvas)= +(gpu)= -### `Canvas` +### `Gpu` {.signature} -`class Canvas extends` [`Handle`](#handle) +`class Gpu extends` [`Handle`](#handle) -Shows views in a `` element or an `OffscreenCanvas`, each in its layout panel; created by -[`Gpu.canvas()`](#gpu-canvas). A frame redraws only the views that need it, or every view after a resize or a -layout change. Destroying it stops drawing; the views stay usable, and the element keeps its last -frame. +A WebGPU device that views draw with. The views of one `Gpu` share its memory and draw together +in each animation frame, so linked views always show the same moment. Views use the first `Gpu` +created unless given another, and [`View.create`](#view-create) creates it when there is none. Destroying it +destroys its views and frame listeners; other GPUs and datasets stay usable. ```{rubric} Properties ``` -{#canvas-element .member} -`readonly` **`element`**`: HTMLCanvasElement | OffscreenCanvas` [¶](#canvas-element){.headerlink} -: The `` element or `OffscreenCanvas` it draws into. - -{#canvas-layout .member} -**`layout`**`: readonly` [`Panel`](#panel)`[]` [¶](#canvas-layout){.headerlink} -: The views shown and their regions; later panels draw over earlier ones. Setting it replaces the - shown views. A view shows in at most one panel of one canvas at a time. - -```{rubric} Methods -``` - -{#canvas-shot .member} -**`shot`**`(options?:` [`ShotOptions`](#shotoptions)`):` [`Shot`](#shot) [¶](#canvas-shot){.headerlink} -: A [`Shot`](#shot) of exactly what the canvas shows: its views in its layout, at the canvas's CSS - size times `options.scale` in pixels. +{#gpu-lost .member} +`readonly` **`lost`**`: boolean` [¶](#gpu-lost){.headerlink} +: Whether the device was lost. Drawing then does nothing; create a new [`Gpu`](#gpu). -(panel)= +{#gpu-budget .member} +**`budget`**`:` [`Budget`](#budget) [¶](#gpu-budget){.headerlink} +: The GPU memory budget for data, shared by every view of this GPU; see [`Budget`](#budget). Defaults + to half the device's largest storage buffer, and 32 MiB uploaded per frame. -### `Panel` +{#gpu-work .member} +`readonly` **`work`**`:` [`Work`](#work) [¶](#gpu-work){.headerlink} +: Exact counts of the work this GPU has done since it was created; see [`Work`](#work). The counts + repeat for the same budget, device limits, and frames, so tests and benchmarks can assert them. -{.signature} -`interface Panel` +{#gpu-resident .member} +`readonly` **`resident`**`: number` [¶](#gpu-resident){.headerlink} +: Bytes of GPU memory its views draw from: data pages, and each view's per-row data. -A view and the region of a canvas or picture it draws in. +{#gpu-uploading .member} +`readonly` **`uploading`**`: number` [¶](#gpu-uploading){.headerlink} +: Bytes waiting to upload to the GPU, across its views. -{#panel-view .member} -**`view`**`:` [`View`](#view) [¶](#panel-view){.headerlink} -: The view drawn. +```{rubric} Methods +``` -{#panel-at .member} -**`at`**`?:` [`Rect`](#rect) [¶](#panel-at){.headerlink} -: Where the view draws; the whole canvas or picture by default. +{#gpu-create .member} +`static` **`create`**`(): Promise` [¶](#gpu-create){.headerlink} +: Requests a WebGPU device; the first call takes the one [`load()`](#load) requested. Create one before + any view to set its `budget` first, or another for views that should not share memory with the + rest. Rejects with an `unsupported` error when the browser has no WebGPU device. -(rect)= +{#gpu-on .member} +**`on`**`(type: 'frame', listener: (now: number) => void): () => void` [¶](#gpu-on){.headerlink} +: Calls `listener` every animation frame with the frame's timestamp in milliseconds, before the + views draw, so changes it makes show in that frame. While any listener is registered, a frame + runs every animation frame. Returns a function that removes the listener. -### `Rect` +{#gpu-step .member} +**`step`**`(now: number): void` [¶](#gpu-step){.headerlink} +: Draws one frame now, at `now` milliseconds on the `requestAnimationFrame` clock: it runs the + frame listeners, then draws every canvas that needs a frame. After the first call, the GPU + requests no more animation frames, so a test or benchmark that steps every frame draws the + same frames on any device. -{.signature} -`type Rect = readonly [x: number, y: number, width: number, height: number]` +{#gpu-done .member} +**`done`**`(wait?:` [`Wait`](#wait)`): Promise` [¶](#gpu-done){.headerlink} +: Resolves once the GPU has finished its submitted work, every readback has arrived, and every + pipeline a view or shot requested has compiled. A frame stepped after that uses all of it. -A region `[x, y, width, height]` in fractions of a canvas or picture, from its top left. +{#gpu-landed .member} +**`landed`**`(wait?:` [`Wait`](#wait)`): Promise` [¶](#gpu-landed){.headerlink} +: Resolves when, after this call, work a view is waiting on arrives (a readback, a pipeline + compile, a job, a label text, or a fetch), or a view requests a frame. A harness that steps + frames awaits it to know when to step again. ## Pictures and video @@ -1297,24 +1394,25 @@ Shots draw views into images and video. {.signature} `class Shot` -A picture of views, `size` pixels in all, each view drawn into its layout panel. While a shot -draws a view, a canvas showing that view keeps its last frame. An image draws each view's data as -it was when the picture began. A canvas's `shot()` makes a shot of what the canvas shows. +A picture of views, `size` pixels in all, each view drawn in its region. While a shot draws a +view, its canvas keeps its last frame. An image draws each view's data as it was when the +picture began. `Shot.of(view)` makes a shot of what a view's canvas shows. ```ts -const png = await (await canvas.shot({ scale: 2 }).image()).encode('png'); +const png = await (await Shot.of(view, { scale: 2 }).image()).encode('png'); ``` ```{rubric} Constructor ``` {#shot-new .member} -**`new Shot`**`(size: readonly [number, number], options?:` [`ShotOptions`](#shotoptions) `& { layout?: readonly` [`Panel`](#panel)`[] })` [¶](#shot-new){.headerlink} +**`new Shot`**`(size: readonly [number, number], options?:` [`ShotOptions`](#shotoptions) `& { views?: readonly` [`Placed`](#placed)`[] })` [¶](#shot-new){.headerlink} : Creates a shot of `size`, `[width, height]` in pixels. - {#shot-new-layout .member} - **`layout`**`?: readonly` [`Panel`](#panel)`[]` [¶](#shot-new-layout){.headerlink} - : The views drawn, each in its panel of the picture. + {#shot-new-views .member} + **`views`**`?: readonly` [`Placed`](#placed)`[]` [¶](#shot-new-views){.headerlink} + : The views drawn, later views over earlier ones: each in its own region, or, given as + `{ view, region }`, in that region of the picture. ```{rubric} Properties ``` @@ -1324,12 +1422,18 @@ const png = await (await canvas.shot({ scale: 2 }).image()).encode('png'); : The picture's size, `[width, height]` in pixels. {#shot-options .member} -`readonly` **`options`**`:` [`ShotOptions`](#shotoptions) `& { layout?: readonly` [`Panel`](#panel)`[] }` [¶](#shot-options){.headerlink} -: How the picture is taken, and in `layout`, the panel each view is drawn into. +`readonly` **`options`**`:` [`ShotOptions`](#shotoptions) `& { views?: readonly` [`Placed`](#placed)`[] }` [¶](#shot-options){.headerlink} +: How the picture is taken, and in `views`, the views drawn into it. ```{rubric} Methods ``` +{#shot-of .member} +`static` **`of`**`(view:` [`View`](#view)`, options?:` [`ShotOptions`](#shotoptions)`): Shot` [¶](#shot-of){.headerlink} +: A shot of exactly what the canvas showing `view` shows: its views in their regions, at the + canvas's CSS size times `options.scale` in pixels. Throws an `invalid` error for a view on no + canvas, which a shot must be given a size for. + {#shot-image .member} **`image`**`(wait?:` [`Wait`](#wait)`): Promise<`[`Image`](#image)`>` [¶](#shot-image){.headerlink} : Draws the picture once every view is complete, as `capture` says, and resolves to the @@ -1383,6 +1487,23 @@ Options for a [`Shot`](#shot). : The time in seconds that [`elapsed()`](#elapsed) expressions and flowing comets show in a still picture. By default they follow the view's clock. +(placed)= + +### `Placed` + +{.signature} +`type Placed =` [`View`](#view) `| { view:` [`View`](#view)`; region:` [`Rect`](#rect) `}` + +A view drawn into a picture, in its own region or in `region`. + +{#placed-view .member} +`readonly` **`view`**`:` [`View`](#view) [¶](#placed-view){.headerlink} +: The view drawn. + +{#placed-region .member} +`readonly` **`region`**`:` [`Rect`](#rect) [¶](#placed-region){.headerlink} +: Where it draws in the picture, in place of its own region. + (image)= ### `Image` diff --git a/docs/reference/types.md b/docs/reference/types.md index 7083b353..6e6aad07 100644 --- a/docs/reference/types.md +++ b/docs/reference/types.md @@ -17,8 +17,8 @@ A scene: its space, how it responds to input, and how it is laid out. A view's content and behavior: a space, marks drawn in order, a layout, a theme, controls, an ease duration, and an optional title. -A scene is plain data. In JSON, fields are camelCase and each mark is -`{ vertices | edges | faces | lines | areas: options }`. +A scene is plain data. In JSON, fields are camelCase, and the key naming each mark's kind holds +its dataset and table: `{ "vertices": ["grid", "Bus"], "radius": 4 }`. {#scene-space .member} **`space`**`:` [`Space`](#space) [¶](#scene-space){.headerlink} @@ -889,29 +889,10 @@ The marks that draw the tables of a scene. ### `Mark` {.signature} -`type Mark = { vertices:` [`Vertices`](#vertices) `} | { edges:` [`Edges`](#edges) `} | { faces:` [`Faces`](#faces) `} | { lines:` [`Lines`](#lines) `} | { areas:` [`Areas`](#areas) `}` +`type Mark =` [`Vertices`](#vertices) `|` [`Edges`](#edges) `|` [`Faces`](#faces) `|` [`Lines`](#lines) `|` [`Areas`](#areas) -One table a scene draws. In JSON: `{ vertices | edges | faces | lines | areas: options }`. - -{#mark-vertices .member} -`{` **`vertices`**`:` [`Vertices`](#vertices) `}` [¶](#mark-vertices){.headerlink} -: Rows drawn as markers. - -{#mark-edges .member} -`{` **`edges`**`:` [`Edges`](#edges) `}` [¶](#mark-edges){.headerlink} -: Rows drawn as strokes between two vertices. - -{#mark-faces .member} -`{` **`faces`**`:` [`Faces`](#faces) `}` [¶](#mark-faces){.headerlink} -: Rows drawn as triangles between three vertices. - -{#mark-lines .member} -`{` **`lines`**`:` [`Lines`](#lines) `}` [¶](#mark-lines){.headerlink} -: Rows drawn as lines along their paths, or on a plot through their samples. - -{#mark-areas .member} -`{` **`areas`**`:` [`Areas`](#areas) `}` [¶](#mark-areas){.headerlink} -: Rows drawn as the areas their polygons cover. +One table a scene draws. The key naming the mark's kind holds its dataset and table, beside its +options: `{ vertices: [data, table], radius: 4 }`. (markoptions)= @@ -922,10 +903,6 @@ One table a scene draws. In JSON: `{ vertices | edges | faces | lines | areas: o The options every mark has. -{#markoptions-of .member} -**`of`**`: [data: string, table: string]` [¶](#markoptions-of){.headerlink} -: The dataset and table drawn. - {#markoptions-color .member} **`color`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#markoptions-color){.headerlink} : Each row's color: a CSS color, a value through a colormap, or a conditional choosing between @@ -938,8 +915,9 @@ The options every mark has. {#markoptions-priority .member} **`priority`**`?:` [`ExprInput`](#exprinput) `| null` [¶](#markoptions-priority){.headerlink} -: Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, and - are the last dropped when a table too large to draw whole is thinned to the tiles in view. +: Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, + their labels win where labels would overlap, and they are the last dropped when a table too + large to draw whole is thinned to the tiles in view. {#markoptions-name .member} **`name`**`?: string | null` [¶](#markoptions-name){.headerlink} @@ -1003,6 +981,10 @@ The options of marks drawn as strokes: edges and lines. Rows drawn as markers: points of a map, nodes of a graph, blocks of a diagram. +{#vertices-vertices .member} +**`vertices`**`:` [`TableInput`](#tableinput) [¶](#vertices-vertices){.headerlink} +: The dataset and table drawn, `[data, table]`. + {#vertices-position .member} **`position`**`?: [`[`ExprInput`](#exprinput)`,` [`ExprInput`](#exprinput)`] | null` [¶](#vertices-position){.headerlink} : Each row's x and y. Without a position, the point field that `geometry` names places rows, and @@ -1063,6 +1045,10 @@ Rows drawn as strokes between the vertices named by two reference fields: branch wires. In a diagram, a row without ends is a net, joining every vertex port whose reference field names it. +{#edges-edges .member} +**`edges`**`:` [`TableInput`](#tableinput) [¶](#edges-edges){.headerlink} +: The dataset and table drawn, `[data, table]`. + {#edges-ends .member} **`ends`**`?: [from: string, to: string] | null` [¶](#edges-ends){.headerlink} : The reference fields naming each row's two vertices. @@ -1103,6 +1089,10 @@ outlines). On a plot, each row instead traces a sampled field's history across t as a monitor does. On the earth, paths follow the globe's curve and stay whole across the antimeridian. +{#lines-lines .member} +**`lines`**`:` [`TableInput`](#tableinput) [¶](#lines-lines){.headerlink} +: The dataset and table drawn, `[data, table]`. + {#lines-geometry .member} **`geometry`**`?: string | null` [¶](#lines-geometry){.headerlink} : The path or polygon field each row runs along; the field named `geometry` by default. Not @@ -1129,6 +1119,10 @@ The view triangulates each row's polygons once, in the background when they are earth, areas follow the globe's curve and stay whole across the antimeridian. To outline areas, add a [`Lines`](#lines) mark of the same table, which draws their rings. +{#areas-areas .member} +**`areas`**`:` [`TableInput`](#tableinput) [¶](#areas-areas){.headerlink} +: The dataset and table drawn, `[data, table]`. + {#areas-geometry .member} **`geometry`**`?: string | null` [¶](#areas-geometry){.headerlink} : The polygon field each row fills, holding one polygon's rings or several polygons; the field @@ -1145,6 +1139,10 @@ Rows drawn as triangles between the vertices named by three reference fields: me and fields interpolated across a network. Faces are lit from the camera, so a flat mesh seen from above shows its exact colors. +{#faces-faces .member} +**`faces`**`:` [`TableInput`](#tableinput) [¶](#faces-faces){.headerlink} +: The dataset and table drawn, `[data, table]`. + {#faces-corners .member} **`corners`**`?: [a: string, b: string, c: string] | null` [¶](#faces-corners){.headerlink} : The reference fields naming each row's three vertices; required. Without a color, a face blends @@ -1378,16 +1376,18 @@ How a line runs between its samples. Text for each row, placed beside, inside, or along the row's shape and following it every frame: a text field, or options. A label shows wherever its row shows and its text fits the room the -row gives it, such as inside a block, whatever other labels it meets. A row whose text is -missing has no label, so a `when` choosing between the text field and missing labels only the -rows it names. In a diagram a label is part of the drawing: it scales with the zoom, fades out -when too small to read, and shows wherever it fits its vertex. +row gives it, such as inside a block. Labels beside or along their rows never overlap: where +they would, the label of the row drawn on top shows, so a mark's `priority` decides, and a dense +view labels as many rows as fit. A row whose text is missing or empty has no label, so a `when` +choosing between the text field and missing labels only the rows it names. In a diagram a label +is part of the drawing: it scales with the zoom, fades out when too small to read, and shows +wherever it fits its vertex. {#label-text .member} **`text`**`:` [`ExprInput`](#exprinput) [¶](#label-text){.headerlink} : The text field to show, or a `when` whose values are each that field or missing. A row whose - text is missing has no label. In a diagram the text reads only data, since the sheet sizes - blocks around it. + text is missing or empty has no label. In a diagram the text reads only data, since the sheet + sizes blocks around it. {#label-size .member} **`size`**`?: number | null` [¶](#label-size){.headerlink} @@ -1486,7 +1486,7 @@ table joined by row id. `"$name"` is the short form of `{ "field": "name" }`. : The field's name. {#fieldnode-from .member} -**`from`**`?: [data: string, table: string] | null` [¶](#fieldnode-from){.headerlink} +**`from`**`?:` [`TableInput`](#tableinput) `| null` [¶](#fieldnode-from){.headerlink} : The dataset and table the field is read from, joined by row id. {#fieldnode-step .member} @@ -2204,7 +2204,7 @@ What failed, as JSON: the form of the npm package's [`LatkitError`](#latkiterror {#failure-path .member} **`path`**`?: string` [¶](#failure-path){.headerlink} -: The invalid part of the input, such as `marks[2].vertices.color`, on `invalid` failures. +: The invalid part of the input, such as `marks[2].color`, on `invalid` failures. (phase)= diff --git a/npm/README.md b/npm/README.md index 23990eb7..f7630780 100644 --- a/npm/README.md +++ b/npm/README.md @@ -4,35 +4,27 @@ Large networks, such as power grids, drawn with WebGPU: datasets of tables, scen edges, lines, and areas over them, and pictures and video of the views. ```ts -import { Data, Gpu, Patch, field, load, type Scene } from 'latkit'; +import { Data, View, field, load } from 'latkit'; await load(); -const grid = new Data('grid').apply( - new Patch().replace('Bus', [101, 102], { x: [0, 30], y: [0, 20], kv: [230, 115] }), -); +const grid = new Data('grid').set('Bus', [101, 102], { x: [0, 30], y: [0, 20], kv: [230, 115] }); -const scene: Scene = { +const view = await View.create(document.querySelector('canvas')!, { space: 'plane', marks: [ { - vertices: { - of: ['grid', 'Bus'], - position: ['$x', '$y'], - radius: 6, - color: field('kv').colormap('viridis'), - }, + vertices: grid.table('Bus'), + position: ['$x', '$y'], + radius: 6, + color: field('kv').colormap('viridis'), }, ], -}; -const gpu = await Gpu.create(); -const view = gpu.view(scene, { sources: [grid] }); -gpu.canvas(document.querySelector('canvas')!, { layout: [{ view }] }); -view.fit(); +}); -grid.apply(new Patch().set('Bus', [102], { kv: [345] })); // drawn the next frame +grid.set('Bus', [102], { kv: [345] }); // drawn the next frame ``` -Scenes, expressions, and patches are plain data, typed from latkit's schema, and need no +A scene is plain data, typed from latkit's schema; scenes, expressions, and patches need no WebAssembly, so a worker or a server builds them too. The [guide](https://latkit.readthedocs.io/en/latest/guide.html) shows each task, and the [API reference](https://latkit.readthedocs.io/en/latest/reference/api.html) documents every export. diff --git a/npm/docs.mjs b/npm/docs.mjs index 1b8ddec5..aa077ead 100644 --- a/npm/docs.mjs +++ b/npm/docs.mjs @@ -700,13 +700,15 @@ const sections = { ], [ 'Data and patches', - 'Datasets of named tables, the patches that change them, written without WebAssembly, and the sources views draw.', + 'Datasets of named tables, their tables that marks draw, and the patches that change them, written without WebAssembly.', [ 'Dataset', 'Data', 'Snapshot', 'Tiles', 'Source', + 'Table', + 'TableInput', 'QueryResult', 'Envelope', 'Patch', @@ -718,6 +720,7 @@ const sections = { 'Reference', 'reference', 'Fields', + 'Run', ], ], [ @@ -748,13 +751,13 @@ const sections = { ], [ 'Views', - 'A GPU makes views, and canvases that show them.', - ['Gpu', 'View', 'ViewEvents', 'Insets', 'Point', 'Position', 'Canvas', 'Panel', 'Rect'], + 'Views draw scenes on canvases, with the GPU they share.', + ['View', 'Rect', 'ViewEvents', 'Insets', 'Point', 'Position', 'Gpu'], ], [ 'Pictures and video', 'Shots draw views into images and video.', - ['Shot', 'ShotOptions', 'Image', 'ImageFormat', 'Video'], + ['Shot', 'ShotOptions', 'Placed', 'Image', 'ImageFormat', 'Video'], ], ], 'types.md': [ @@ -886,11 +889,11 @@ out: - **Loading.** Scenes, expressions, and patches are plain data. Datasets, views, pictures, and the colormap functions need latkit's WebAssembly: until \`load()\` finishes, they throw an - \`unloaded\` error. \`Gpu.create()\`, \`Data.read()\`, and \`Patch.read()\` of JSON load it - themselves. + \`unloaded\` error. \`View.create()\`, \`Gpu.create()\`, \`Data.read()\`, and \`Patch.read()\` of + JSON load it themselves. - **Errors.** Everything latkit throws or rejects with is a \`LatkitError\`. Input it cannot use - fails with an \`invalid\` error whose \`path\` names the part, such as - \`marks[2].vertices.color\`, and changes nothing. + fails with an \`invalid\` error whose \`path\` names the part, such as \`marks[2].color\`, and + changes nothing. - **Waits.** Every promise waits within its \`Wait\`: until its \`signal\` aborts, or 30 seconds. - **Lifetimes.** A \`Handle\` holds memory until \`destroy()\`, or a \`using\` declaration, frees it.`, 'types.md': `# Scene and result types diff --git a/npm/examples/scripts/history.ts b/npm/examples/scripts/history.ts index c8534e1e..c23ab3f5 100644 --- a/npm/examples/scripts/history.ts +++ b/npm/examples/scripts/history.ts @@ -1,5 +1,5 @@ /** - * Writes the recorded day that the Live data page reads as time tiles, into `public/history/`. It + * Writes the recorded day that the Telemetry page reads as time tiles, into `public/history/`. It * contains the voltages of 64 buses in six areas, every 15 s for a day. A load curve swings them, * and three disturbances pull areas down before they recover. Every run writes the same bytes. Run * it from the examples with `node scripts/history.ts`. It tiles the day with the repository's diff --git a/npm/examples/src/examples/colormaps/index.ts b/npm/examples/src/examples/colormaps/index.ts deleted file mode 100644 index 0171d029..00000000 --- a/npm/examples/src/examples/colormaps/index.ts +++ /dev/null @@ -1,442 +0,0 @@ -/** - * The colormap catalog on a field of hexagons. Each cell is an area colored on the GPU through the - * chosen colormap, shown beside the same colormap as a CSS ramp for comparison. Choose a colormap, - * the field and number of cells, a transform applied to values before the colormap, and which cells - * show. Hover a cell to read its value, or brush over the field to read the cells under the brush. - */ -import { - Data, - Patch, - colormapCss, - colormaps, - field, - when, - zoom, - type Colormap, - type ColormapName, - type Expr, - type Mark, - type Scene, -} from 'latkit'; -import { h } from '../../ui/dom.js'; -import type { Stage } from '../../ui/stage.js'; -import { theme } from '../../ui/theme.js'; - -/** - * Custom colormaps. `custom` runs from transparent to a semi-transparent blue at a placed stop, - * then to opaque amber, so the plane's grid shows through the cells. `diverging` runs from teal - * through a light middle to rust. `cyclic` ends where it starts. - */ -const CUSTOM = { - custom: { kind: 'sequential', colors: ['#2a6fdb00', [0.55, '#2a6fdb99'], '#ffb000'] }, - diverging: { kind: 'diverging', colors: ['#00727a', '#e8e4d8', '#b5462a'] }, - cyclic: { kind: 'cyclic', colors: ['#3b5bdb', '#e64980', '#fab005', '#3b5bdb'] }, -} as const satisfies Record; -/** A catalog colormap's name, or a key of `CUSTOM`. */ -type Choice = ColormapName | keyof typeof CUSTOM; -const mapOf = (choice: Choice): Colormap => - choice in CUSTOM ? CUSTOM[choice as keyof typeof CUSTOM] : (choice as ColormapName); - -/** Transforms from a cell's value, 0 to 1, to the colormap's input, also 0 to 1, with labels. */ -const TRANSFORMS: Record Expr]> = { - none: ['As is', (v) => v], - sqrt: ['Square root', (v) => v.sqrt()], - squared: ['Squared', (v) => v.pow(2)], - log: ['Logarithm', (v) => v.mul(9).add(1).ln().div(Math.LN10)], - exp: [ - 'Exponential', - (v) => - v - .exp() - .sub(1) - .div(Math.E - 1), - ], - bands: ['Eight bands', (v) => v.mul(8).floor().div(8)], - stripes: ['Stripes', (v) => v.mul(10).rem(1)], - waves: [ - 'Waves', - (v) => - v - .mul(4 * Math.PI) - .cos() - .mul(0.5) - .add(0.5), - ], - middle: ['From the middle', (v) => v.sub(0.5).abs().mul(2)], - inverted: ['Inverted', (v) => v.neg().add(1)], - clamped: ['Middle half', (v) => v.sub(0.25).mul(2).clamp(0, 1)], - edges: ['Nearer edge', (v) => v.min(v.neg().add(1)).mul(2)], - peaks: ['Peaks only', (v) => v.sub(0.6).max(0).div(0.4)], -}; -type Transform = keyof typeof TRANSFORMS; -/** Which cells show: all of them, or those selected by a band of values or by class. */ -type Show = 'all' | 'inside' | 'outside' | 'not' | 'class' | 'others'; -/** A checkerboard behind each ramp, so a colormap's transparent parts are visible. */ -const CHECKS = 'repeating-conic-gradient(#888 0 25%, #ccc 0 50%) 0 0 / 8px 8px'; - -/** The catalog's colormaps grouped by kind, each kind titled, then the custom ones. */ -function catalog(): (readonly [kind: string, names: readonly Choice[]])[] { - const all = colormaps(); - const kinds = [...new Set(all.map((map) => map.kind))]; - const title = (kind: string) => `${kind[0]!.toUpperCase()}${kind.slice(1)}`; - return [ - ...kinds.map( - (kind) => - [title(kind), all.filter((map) => map.kind === kind).map((map) => map.name)] as const, - ), - ['Custom', ['custom', 'diverging', 'cyclic']], - ]; -} - -/** Fields the cells can show, each a value from 0 to 1 at a point. Categorical colormaps show the - * value's class instead. */ -const FIELDS = { - waves: (x: number, y: number) => - 0.5 + 0.3 * Math.sin(x * 0.11) * Math.cos(y * 0.09) + 0.2 * Math.sin((x + y) * 0.035), - peaks: (x: number, y: number) => { - const peak = (cx: number, cy: number, r: number) => - Math.exp(-((x - cx) ** 2 + (y - cy) ** 2) / (2 * r * r)); - return Math.min(1, 0.9 * peak(30, 25, 14) + 0.7 * peak(80, 55, 20) + 0.5 * peak(110, 15, 10)); - }, - ramp: (x: number, _y: number, width: number) => x / width, - angle: (x: number, y: number, width: number, height: number) => - (Math.atan2(y - height / 2, x - width / 2) / (2 * Math.PI) + 1) % 1, -} as const; -type FieldName = keyof typeof FIELDS; - -/** A patch that replaces table `Cell` with `columns` by `rows` pointy-top hexagons of side 1. Each - * cell has its polygon, its value, its class from 0 to 9, and its value as label text. */ -function hexagons(columns: number, rows: number, value: FieldName): Patch { - const count = columns * rows; - const width = columns * Math.sqrt(3); - const height = rows * 1.5; - const geometry: [number, number][][][] = []; - const values = new Float64Array(count); - const classes = new Float64Array(count); - const s = Math.sqrt(3); - for (let r = 0; r < rows; r++) - for (let c = 0; c < columns; c++) { - const x = c * s + (r % 2) * (s / 2); - const y = r * 1.5; - const ring: [number, number][] = []; - for (let k = 0; k <= 6; k++) { - const a = (Math.PI / 3) * (k % 6) + Math.PI / 6; - ring.push([x + Math.cos(a), y + Math.sin(a)]); - } - geometry.push([ring]); - const v = FIELDS[value](x, y, width, height); - values[r * columns + c] = v; - classes[r * columns + c] = Math.min(9, Math.floor(v * 10)); - } - const label = Array.from(values, (v) => v.toFixed(2)); - return new Patch().replace('Cell', { count }, { geometry, value: values, class: classes, label }); -} - -export default function page(stage: Stage): void { - const byKind = catalog(); - const categoricalMaps = new Set(byKind.find(([kind]) => kind === 'Categorical')?.[1]); - let colormap: Choice = 'viridis'; - let value: FieldName = 'peaks'; - let size: [number, number] = [96, 64]; - let outline = false; - let reversed = false; - let light = false; - let transform: Transform = 'none'; - let show: Show = 'all'; - let band: [number, number] = [0.3, 0.7]; - let classed = 5; - let labels = false; - let ownLegend = false; - /** One dataset, replaced whenever the field or cell count changes. The view eases each cell from - * its old color to its new one. */ - const data = new Data('colors'); - stage.signal.addEventListener('abort', () => data.destroy()); - - /** Whether each cell shows, from the Show setting. */ - const visible = (): Expr | boolean => { - const v = field('value'); - const c = field('class'); - switch (show) { - case 'inside': - return v.ge(band[0]).and(v.le(band[1])); - case 'outside': - return v.lt(band[0]).or(v.gt(band[1])); - case 'not': - return v.ge(band[0]).and(v.le(band[1])).not(); - case 'class': - return c.eq(classed); - case 'others': - return c.ne(classed); - default: - return true; - } - }; - - const scene = (): Scene => { - const categorical = categoricalMaps.has(colormap); - const input = categorical ? field('class') : TRANSFORMS[transform]![1](field('value')); - const mapped = input.colormap(mapOf(colormap)); - const outlines: Mark[] = outline - ? [ - { - lines: { - of: ['colors', 'Cell'], - color: light ? '#f7f8fa' : theme.background, - width: 0.75, - }, - }, - ] - : []; - return { - // Grid lines under the cells show through a colormap's transparent parts. - space: { plane: { grid: colormap === 'custom' ? { lines: { pitch: 4 } } : 'off' } }, - // A built-in theme, by name. - theme: light ? 'light' : 'dark', - ease: 0.25, - marks: [ - { - // The cells are pickable, so hovering one reads its value. - areas: { - of: ['colors', 'Cell'], - color: categorical ? mapped : reversed ? mapped.domain(1, 0) : mapped.domain(0, 1), - visible: visible(), - pick: true, - // A cell is labeled once it is wide enough for its value, from zoom 4 (16 pixels per - // unit). Below that its text is missing, and it has no label. - ...(labels - ? { - label: { - text: when(zoom().ge(4)).then(field('label')).otherwise(null), - size: 10, - }, - } - : {}), - ...(ownLegend ? { legend: TRANSFORMS[transform]![0] } : {}), - }, - }, - ...outlines, - ], - }; - }; - const view = stage.view(scene(), { - label: 'A field of hexagons through a colormap: hover a cell to read it', - }); - view.sources = [data]; - const legend = stage.legend(); - - const rebuild = () => { - const [columns, rows] = size; - data.apply(hexagons(columns, rows, value)); - stage.detail(`${(columns * rows).toLocaleString()} cells`); - }; - const restyle = () => { - view.scene = scene(); - legend.ramp( - mapOf(colormap), - `${colormap}${categoricalMaps.has(colormap) ? ', ten classes' : reversed ? ', reversed' : ''}`, - ); - legend.hidden = ownLegend; - for (const [name, swatch] of swatches) - swatch.setAttribute('aria-pressed', String(name === colormap)); - }; - - const palette = stage.section('Colormap'); - const ours: Record = { - custom: 'ours · alpha, a placed stop', - diverging: 'ours · diverging', - cyclic: 'ours · cyclic', - }; - const choose = palette.choice( - 'Colormap', - byKind.flatMap(([kind, names]) => - names.map( - (name) => - [ - name in ours ? ours[name as keyof typeof CUSTOM] : `${name} · ${kind.toLowerCase()}`, - name, - ] as const, - ), - ), - colormap, - (name) => { - colormap = name; - restyle(); - }, - ); - palette.toggle('Reversed', 'r', reversed, (on) => { - reversed = on; - restyle(); - }); - palette.toggle('Outlines', 'o', outline, (on) => { - outline = on; - restyle(); - }); - palette.toggle('Light', 'l', light, (on) => { - light = on; - restyle(); - }); - palette.toggle('Legend in the view', undefined, ownLegend, (on) => { - ownLegend = on; - restyle(); - }); - palette.choice( - 'Transform', - Object.entries(TRANSFORMS).map(([key, [label]]) => [label, key as Transform] as const), - transform, - (next) => { - transform = next; - restyle(); - }, - ); - - const shown = stage.section('Field'); - shown.choice( - 'Value', - [ - ['Peaks', 'peaks'], - ['Waves', 'waves'], - ['A ramp across', 'ramp'], - ['The angle around', 'angle'], - ] as const, - value, - (next) => { - value = next; - rebuild(); - }, - ); - // Sized by column count, with two rows for every three columns. - shown.choice( - 'Cells', - [ - ['1,500', 48], - ['6,000', 96], - ['24,000', 192], - ['96,000', 384], - ] as const, - size[0], - (columns) => { - size = [columns, (columns * 2) / 3]; - rebuild(); - view.fit(); - }, - ); - shown.toggle('Labels', 'b', labels, (on) => { - labels = on; - restyle(); - }); - shown.choice( - 'Show', - [ - ['Every cell', 'all'], - ['Values in the band', 'inside'], - ['Values outside it', 'outside'], - ['All but the band', 'not'], - ['One class', 'class'], - ['All but one class', 'others'], - ] as const, - show, - (next) => { - show = next; - restyle(); - }, - ); - const bound = (k: 0 | 1, label: string) => - shown.range( - label, - { min: 0, max: 1, step: 0.05, value: band[k], format: (v) => v.toFixed(2) }, - (v) => { - band = k ? [band[0], v] : [v, band[1]]; - if (show !== 'all') restyle(); - }, - ); - bound(0, 'Band from'); - bound(1, 'Band to'); - shown.range('Class', { min: 0, max: 9, step: 1, value: classed, format: String }, (v) => { - classed = v; - if (show === 'class' || show === 'others') restyle(); - }); - const reading = shown.note('Hover a cell.'); - view.on('hovered', ({ hit }) => { - if (!hit) return void (reading.textContent = 'Hover a cell.'); - const v = data.get('Cell', hit.row.id, 'value') as number | null; - reading.textContent = `Cell ${hit.row.id}: ${v?.toFixed(3) ?? '–'}`; - }); - - // A brush: a pick finds the cells within `reach` CSS pixels of the pointer, and the note shows - // their mean value. - let reach = 24; - shown.range( - 'Brush', - { min: 4, max: 80, step: 4, value: reach, format: (v) => `${v} px` }, - (v) => (reach = v), - ); - const brushed = shown.note('Brush over the field.'); - let brushing = false; - stage.regions.main.querySelector('canvas')!.addEventListener( - 'pointermove', - (event) => { - if (brushing) return; - brushing = true; - void view - .pick([event.offsetX, event.offsetY], { radius: reach }) - .then((hits) => { - const values = hits.map((hit) => data.get('Cell', hit.row.id, 'value') as number); - const mean = values.reduce((sum, v) => sum + v, 0) / Math.max(1, values.length); - brushed.textContent = hits.length - ? `${hits.length} cells under the brush, mean ${mean.toFixed(3)}` - : 'Brush over the field.'; - }) - .catch(() => {}) - .finally(() => (brushing = false)); - }, - { signal: stage.signal }, - ); - - // The catalog as CSS ramps: the same colors the GPU draws, drawn by the browser over a - // checkerboard so transparent parts show. - const ramps = stage.section('Catalog'); - const groups: [string, HTMLElement[]][] = []; - ramps.choice( - 'Kind', - ['All', ...byKind.map(([kind]) => kind)].map((kind) => [kind, kind] as const), - 'All', - (shown) => { - for (const [kind, elements] of groups) - for (const element of elements) element.hidden = shown !== 'All' && shown !== kind; - }, - ); - const swatches = new Map(); - for (const [kind, names] of byKind) { - const title = h('p', { class: 'note' }, kind); - const grid = h('div', { class: 'swatches' }); - groups.push([kind, [title, grid]]); - ramps.content.append(title); - for (const name of names) { - const swatch = h( - 'button', - { type: 'button', class: 'swatch', title: name, 'aria-pressed': 'false' }, - h('span', { class: 'ramp' }), - name, - ); - (swatch.firstChild as HTMLElement).style.background = - `${colormapCss(mapOf(name))}, ${CHECKS}`; - swatch.addEventListener( - 'click', - () => { - colormap = name; - choose.selectedIndex = byKind.flatMap(([, n]) => n).indexOf(name); - restyle(); - }, - { signal: stage.signal }, - ); - swatches.set(name, swatch); - grid.append(swatch); - } - ramps.content.append(grid); - } - - stage.meter(view); - rebuild(); - restyle(); - view.fit(); -} diff --git a/npm/examples/src/examples/diagram/index.ts b/npm/examples/src/examples/diagram/index.ts deleted file mode 100644 index 6c35b9e2..00000000 --- a/npm/examples/src/examples/diagram/index.ts +++ /dev/null @@ -1,983 +0,0 @@ -/** - * A block diagram on a sheet: blocks with ports, wires routed around them, and groups drawn around - * their members. You can add, drag, connect, arrange, and undo, open a preset or a saved `.latk` - * file, and restyle the sheet, its blocks, and its wires. - */ -import { - Data, - Snapshot, - Patch, - field, - reference, - when, - type Grid, - type Id, - type Layout, - type Mark, - type Port, - type Row, - type Rows, - type Marker, - type Scene, - type Side, -} from 'latkit'; -import { h } from '../../ui/dom.js'; -import { each, one } from '../../ui/rows.js'; -import type { Stage } from '../../ui/stage.js'; -import { theme } from '../../ui/theme.js'; - -/** - * The block kinds. Each has a marker, a fill for the dark and the light sheet, and ports given as - * name, side, and direction. Each port is a reference field naming the signal plugged into it. - */ -const KINDS = { - Input: { marker: 'rounded', fill: ['#173836', '#e2f3ef'], ports: [['out', 'right', 'out']] }, - Process: { - marker: 'block', - fill: ['#1c2b3f', '#ffffff'], - ports: [ - ['in', 'left', 'in'], - ['out', 'right', 'out'], - ], - }, - Control: { - marker: 'block', - fill: ['#3a2e18', '#fbf0da'], - ports: [ - ['in', 'left', 'in'], - ['feedback', 'bottom', 'in'], - ['out', 'right', 'out'], - ], - }, - Output: { marker: 'block', fill: ['#29243f', '#ece8fb'], ports: [['in', 'left', 'in']] }, -} as const satisfies Record< - string, - { - marker: Marker; - fill: readonly [string, string]; - ports: readonly (readonly [string, Side, 'in' | 'out'])[]; - } ->; -type Kind = keyof typeof KINDS; -const kinds = Object.keys(KINDS) as Kind[]; -/** A distinct shape per kind, used when Shapes is on: inputs are ellipses, processes rectangles, - * controls diamonds, and outputs blocks with deeply rounded corners. */ -const SHAPES = { - Input: 'ellipse', - Process: 'rectangle', - Control: { shape: 'diamond' }, - Output: { block: { corner: 18 } }, -} as const satisfies Record; - -/** A block's port. `port` names the reference field that holds the port's signal. */ -interface Plug { - kind: Kind; - id: Id; - port: string; -} - -const direction = (kind: Kind, port: string) => - KINDS[kind].ports.find(([name]) => name === port)?.[2]; - -/** Declares a kind's ports for the scene. Outputs drive their signals, and the feedback port gets - * a label. With dot ports, a plain input is declared by its side alone. With diamond ports, every - * port also gets a shape and an order on its side. */ -const ports = (kind: Kind, diamonds: boolean): [string, Port][] => - KINDS[kind].ports.map(([name, side, way], order): [string, Port] => [ - name, - !diamonds && way === 'in' && name !== 'feedback' - ? side - : { - side, - output: way === 'out', - ...(name === 'feedback' ? { label: 'feedback' } : {}), - ...(diamonds ? { shape: 'diamond', order } : {}), - }, - ]); - -/** - * Builds a diagram's rows: blocks of each kind, the signals their ports plug into, groups of - * Process blocks, and stations that contain groups. Positions (x right, y down) are optional. The - * layout places blocks without one. - */ -class Sketch { - readonly blocks = new Map< - Kind, - { id: number; name: string; at?: [number, number]; group?: number }[] - >(); - readonly plugs = new Map(); - readonly signals: { id: number; name: string }[] = []; - readonly groups: { id: number; name: string; station?: number }[] = []; - readonly stations: { id: number; name: string }[] = []; - /** Blocks whose status ring is shown. */ - readonly alarms = new Set(); - next = 1; - - block(kind: Kind, name: string, at?: [number, number], group?: number): Plug { - const id = this.next++; - const rows = this.blocks.get(kind) ?? []; - rows.push({ id, name, at, group }); - this.blocks.set(kind, rows); - return { kind, id, port: 'out' }; - } - - /** Adds a signal driven by the `out` port of each block in `from` and read by each of `to`, on - * its `in` port or on the port paired with it. */ - wire(name: string, from: Plug | Plug[], ...to: (Plug | [Plug, string])[]): void { - const id = this.next++; - this.signals.push({ id, name }); - for (const driver of [from].flat()) this.plugs.set(`${driver.kind} ${driver.id} out`, id); - for (const reader of to) { - const [plug, port] = Array.isArray(reader) ? reader : [reader, 'in']; - this.plugs.set(`${plug.kind} ${plug.id} ${port}`, id); - } - } - - group(name: string, station?: number): number { - const id = this.next++; - this.groups.push({ id, name, station }); - return id; - } - - station(name: string): number { - const id = this.next++; - this.stations.push({ id, name }); - return id; - } - - /** Builds the dataset from the rows. */ - data(): Data { - const patch = new Patch().set( - 'Signal', - this.signals.map((s) => s.id), - { name: this.signals.map((s) => s.name) }, - ); - if (this.stations.length) { - patch.set( - 'Station', - this.stations.map((s) => s.id), - { name: this.stations.map((s) => s.name) }, - ); - } - if (this.groups.length) { - const stations = this.groups.map((g) => g.station ?? null); - patch.set( - 'Group', - this.groups.map((g) => g.id), - { - name: this.groups.map((g) => g.name), - ...(this.stations.length ? { station: reference('Station', stations) } : {}), - }, - ); - } - for (const [kind, rows] of this.blocks) { - const plugged = Object.fromEntries( - KINDS[kind].ports.map(([port]) => [ - port, - reference( - 'Signal', - rows.map((row) => this.plugs.get(`${kind} ${row.id} ${port}`) ?? null), - ), - ]), - ); - const grouped = kind === 'Process' && this.groups.length; - patch.set( - kind, - rows.map((row) => row.id), - { - name: rows.map((row) => row.name), - x: rows.map((row) => row.at?.[0] ?? null), - y: rows.map((row) => row.at?.[1] ?? null), - status: rows.map((row) => (this.alarms.has(row.id) ? 1 : 0)), - ...plugged, - ...(grouped - ? { - group: reference( - 'Group', - rows.map((row) => row.group ?? null), - ), - } - : {}), - }, - ); - } - return new Data('studio').apply(patch); - } -} - -/** Group colors, for the dark and the light sheet. */ -const GROUP_COLORS = { - slate: ['#1a2230', '#e6ebf2'], - teal: ['#14302f', '#dcefec'], - plum: ['#2a1f33', '#efe6f4'], -} as const; - -/** A small pipeline written as a JSON document for `Data.read`. Each change is an object keyed by - * its method, with the method's parameters by name. */ -const PIPELINE = { - name: 'studio', - patch: [ - { set: { table: 'Signal', ids: [10, 11, 12], fields: { name: ['Raw', 'Cleaned', 'Report'] } } }, - { - set: { - table: 'Input', - ids: [1], - fields: { - name: ['Sensor feed'], - x: [0], - y: [0], - status: [0], - out: { reference: 'Signal', ids: [10] }, - }, - }, - }, - { - set: { - table: 'Process', - ids: [2], - fields: { - name: ['Filter'], - x: [260], - y: [0], - status: [0], - in: { reference: 'Signal', ids: [10] }, - out: { reference: 'Signal', ids: [11] }, - }, - }, - }, - { - set: { - table: 'Output', - ids: [3], - fields: { - name: ['Dashboard'], - x: [520], - y: [0], - status: [0], - in: { reference: 'Signal', ids: [11] }, - }, - }, - }, - ], -}; - -/** The presets: a water plant placed in ranks by the layout, four with fixed positions, and one - * written as a JSON document. */ -const PRESETS: readonly (readonly [string, () => Data | Promise])[] = [ - [ - 'Water plant', - () => { - const s = new Sketch(); - const [intake, treatment, output] = ['Intake', 'Treatment', 'Output'].map((name) => - s.group(name), - ); - const wellA = s.block('Input', 'Well A'); - const wellB = s.block('Input', 'Well B'); - const screen = s.block('Process', 'Screen', undefined, intake); - const dosing = s.block('Input', 'Dosing'); - const mixer = s.block('Process', 'Mixer', undefined, treatment); - const clarifier = s.block('Process', 'Clarifier', undefined, treatment); - const filters = [1, 2].map((n) => s.block('Process', `Filter ${n}`, undefined, treatment)); - const tank = s.block('Process', 'Tank', undefined, output); - const pump = s.block('Process', 'Pump', undefined, output); - const main = s.block('Output', 'Main'); - s.wire('Raw water', [wellA, wellB], screen); - s.wire('To mixer', [screen, dosing], mixer); - s.wire('Mixed', mixer, clarifier); - s.wire('Clarified', clarifier, ...filters); - s.wire('Filtered', filters, tank); - s.wire('Stored', tank, pump); - s.wire('Pumped', pump, main); - return s.data(); - }, - ], - [ - 'Control loop', - () => { - const s = new Sketch(); - const reference = s.block('Input', 'Reference', [0, 0]); - const controller = s.block('Control', 'Controller', [240, 0]); - const actuator = s.block('Process', 'Actuator', [500, 0]); - const plant = s.block('Process', 'Plant', [500, 240]); - const sensor = s.block('Process', 'Sensor', [240, 240]); - const response = s.block('Output', 'Response', [740, 240]); - s.wire('Set point', reference, controller); - s.wire('Command', controller, actuator); - s.wire('Drive', actuator, plant); - s.wire('Measured', plant, response, sensor); - s.wire('Feedback', sensor, [controller, 'feedback']); - s.alarms.add(plant.id as number); - return s.data(); - }, - ], - [ - 'Nested groups', - () => { - const s = new Sketch(); - const station = s.station('Generating station'); - const dispatch = s.block('Input', 'Dispatch', [0, 210]); - const plants: Plug[] = []; - const controls: Plug[] = []; - for (let i = 0; i < 3; i++) { - const unit = s.group(`Unit ${i + 1}`, station); - controls.push(s.block('Process', `Controller ${i + 1}`, [320, i * 210], unit)); - plants.push(s.block('Process', `Plant ${i + 1}`, [620, i * 210], unit)); - s.wire(`Drive ${i + 1}`, controls[i]!, plants[i]!); - } - const meter = s.block('Output', 'Metering', [950, 210]); - s.wire('Dispatch', dispatch, ...controls); - // The meter reads one signal, so the plants share it. - s.wire('Power', plants, meter); - return s.data(); - }, - ], - [ - 'Shape atlas', - () => { - const s = new Sketch(); - const input = s.block('Input', 'Input', [0, 0]); - const process = s.block('Process', 'Process', [300, 0]); - const decision = s.block('Control', 'Decision', [300, 250]); - const result = s.block('Output', 'Result', [640, 250]); - s.wire('Source', input, process); - s.wire('Evaluate', process, decision); - s.wire('Accept', decision, result); - return s.data(); - }, - ], - [ - 'Scale study: 1,024 blocks', - () => { - const s = new Sketch(); - let before: Plug | undefined; - for (let row = 0; row < 32; row++) { - for (let col = 0; col < 32; col++) { - const block = s.block('Process', `Block ${row * 32 + col + 1}`, [col * 240, row * 150]); - if (col && before) s.wire('', before, block); - before = block; - } - } - return s.data(); - }, - ], - ['Written as JSON', () => Data.read(PIPELINE)], -]; - -/** - * Undo and redo history, kept as snapshots of the data. It keeps up to 100 undo steps and destroys - * each snapshot once it can no longer be restored. - */ -class History { - readonly #undo: Snapshot[] = []; - #redo: Snapshot[] = []; - - /** Records `before`, a snapshot taken before an edit, and clears the redo steps. */ - commit(before: Snapshot): void { - this.#undo.push(before); - if (this.#undo.length > 100) this.#undo.shift()!.destroy(); - for (const after of this.#redo) after.destroy(); - this.#redo = []; - } - - /** Returns the snapshot before `current` and keeps `current` for redo. When there is nothing to - * undo, it destroys `current` and returns undefined. */ - undo(current: Snapshot): Snapshot | undefined { - const before = this.#undo.pop(); - if (before) this.#redo.push(current); - else current.destroy(); - return before; - } - - redo(current: Snapshot): Snapshot | undefined { - const after = this.#redo.pop(); - if (after) this.#undo.push(current); - else current.destroy(); - return after; - } - - clear(): void { - for (const version of [...this.#undo, ...this.#redo]) version.destroy(); - this.#undo.length = 0; - this.#redo = []; - } -} - -export default function page(stage: Stage): void { - let preset = PRESETS[0]!; - let data = new Data('studio'); - let layout: Layout = { layered: { direction: 'right', gap: 30, rankGap: 80 } }; - let route: 'orthogonal' | 'straight' = 'orthogonal'; - let comets = false; - type Sheet = 'dots' | 'snap' | 'lines' | 'off'; - let grid: Sheet = 'dots'; - let light = false; - /** Settings for how the sheet, blocks, ports, and wires look, and how the view responds to keys - * and presses. */ - const style = { - shapes: false, - diamonds: false, - names: false, - shadows: true, - corner: 6, - outline: true, - portSize: 7, - bend: 6, - groupPadding: 16, - wireSpacing: 4, - arrow: 7, - lift: 3, - textSize: 12, - dash: 6, - groups: 'slate' as keyof typeof GROUP_COLORS, - keyboard: true, - cycle: false, - press: 0.5, - }; - const history = new History(); - const GRIDS: Record = { - dots: { dots: { pitch: 20 } }, - snap: { dots: { pitch: 20, snap: true } }, - lines: { lines: { pitch: 20 } }, - off: 'off', - }; - - /** The names of the tables the data has now. */ - const held = () => new Set(data.describe().tables.map((t) => t.name)); - /** Every id of `table`. */ - const ids = (table: string) => data.query({ of: table, select: {} }).ids; - - const scene = (): Scene => { - const tables = held(); - const marks: Mark[] = []; - // Containers take the theme's group color, tinted by nesting depth. Their titles sit at the - // top, and they and the blocks inside them cast shadows on the sheet. - const title = { text: '$name', size: 11, color: light ? '#5b6b7d' : '#7f93a8' }; - if (tables.has('Station')) { - marks.push({ - vertices: { - of: ['studio', 'Station'], - contains: ['Group', 'station'], - label: title, - shadow: style.shadows, - }, - }); - } - if (tables.has('Group')) { - marks.push({ - vertices: { - of: ['studio', 'Group'], - contains: ['Process', 'group'], - label: title, - shadow: style.shadows, - }, - }); - } - // With Flow on, comets run from each signal's driver to its readers. A signal named Feedback - // is dashed. - marks.push({ - edges: { - of: ['studio', 'Signal'], - route, - arrows: true, - width: 1.5, - dash: field('name').is('Feedback'), - ...(comets ? { flow: 40 } : {}), - ...(style.names - ? { label: { text: '$name', size: 10, color: light ? '#5b6b7d' : '#7f93a8' } } - : {}), - }, - }); - // One mark per block kind in the data. A mark of a table the data lacks would fail. - for (const kind of kinds.filter((k) => tables.has(k))) { - // Without a size, a block fits its label and ports. - marks.push({ - vertices: { - of: ['studio', kind], - position: ['$x', '$y'], - marker: style.shapes ? SHAPES[kind] : KINDS[kind].marker, - color: KINDS[kind].fill[light ? 1 : 0], - shadow: style.shadows, - ring: when(field('status').gt(0)).then('#ff8c42').otherwise(null), - ports: ports(kind, style.diamonds), - drag: true, - label: { text: '$name', size: 12 }, - }, - }); - } - return { - space: { diagram: { grid: GRIDS[grid] } }, - layout, - theme: { - ...(light ? { base: 'light', background: '#f4f6f9' } : theme), - dash: style.dash, - text: { size: style.textSize }, - edge: { spacing: style.wireSpacing, arrow: style.arrow }, - diagram: { - corner: style.corner, - outline: style.outline ? (light ? '#00000024' : '#ffffff1f') : null, - portSize: style.portSize, - bend: style.bend, - groupPadding: style.groupPadding, - group: GROUP_COLORS[style.groups][light ? 1 : 0], - shadow: { color: light ? '#00000038' : '#00000073', lift: style.lift }, - }, - }, - controls: { - connect: true, - delete: true, - selectBox: true, - keyboard: style.keyboard, - cycle: style.cycle, - press: style.press, - }, - marks, - }; - }; - /** An id that no row in the data uses yet. */ - const fresh = () => - 1 + Math.max(0, ...[...held()].flatMap((t) => ids(t).map((id) => Number(id) || 0))); - const view = stage.view(scene(), { - label: 'A diagram: drag blocks, drag from a port to wire it, Delete removes', - }); - - /** Refreshes after a data change. The scene is rebuilt only if the set of tables changed, so each - * block kind in the data has a mark. The description is always updated. */ - let kindsShown = [...held()].join(); - const changed = () => { - const kindsHeld = [...held()].join(); - if (kindsHeld !== kindsShown) { - kindsShown = kindsHeld; - view.scene = scene(); - } - describe(); - }; - /** Applies `patch` as one undoable edit and shows `said`. */ - const edit = (patch: Patch, said: string) => { - history.commit(data.snapshot()); - data.apply(patch); - changed(); - note(said); - }; - /** Replaces the diagram with `next`, clears the undo history, and fits the view to it. */ - const open = (next: Data, said: string) => { - history.clear(); - data.destroy(); - data = next; - view.sources = [data]; - kindsShown = [...held()].join(); - view.scene = scene(); - changed(); - view.fit(); - note(said); - const tables = held(); - const blocks = kinds.reduce((n, kind) => n + (tables.has(kind) ? ids(kind).length : 0), 0); - stage.detail( - `${blocks.toLocaleString()} blocks · ${ids('Signal').length.toLocaleString()} signals`, - ); - }; - const load = async (choice: (typeof PRESETS)[number]) => { - preset = choice; - open(await choice[1](), 'Drag blocks or connect ports.'); - }; - - const name = (row: Row) => - (data.get(row.table, row.id, 'name') as string | null) ?? `${row.table} ${row.id}`; - const isBlock = (table: string): table is Kind => table in KINDS; - - // Dragging moves a block immediately, and the whole drag is recorded as one undoable edit. - let before: Snapshot | undefined; - view.on('drag', ({ phase, hit, position }) => { - if (!isBlock(hit.row.table)) return; - if (phase === 'start') { - before = data.snapshot(); - return; - } - const at = new Patch().set(hit.row.table, [hit.row.id as number], { - x: [position[0]], - y: [position[1]], - }); - data.apply(at); - changed(); - if (phase === 'end') { - if (before) history.commit(before); - before = undefined; - note(`Moved ${name(hit.row)}.`); - } - }); - - // Wiring joins an output and an input on the output's signal, creating the signal if the output - // has none. An input wired elsewhere leaves its old signal. Dropping a plugged port over empty - // space unplugs it. - view.on('connect', ({ from, to }) => { - if (!to) { - const plugged = - isBlock(from.row.table) && from.port && data.get(from.row.table, from.row.id, from.port); - if (!plugged) return note('Let go over nothing.'); - const patch = new Patch().set(from.row.table, [from.row.id as number], { - [from.port!]: reference('Signal', [null]), - }); - return edit(patch, `Unplugged ${name(from.row)}'s ${from.port}.`); - } - if (!isBlock(from.row.table) || !isBlock(to.row.table)) return note('Wire blocks to blocks.'); - const a: Plug = { kind: from.row.table, id: from.row.id, port: from.port ?? 'out' }; - if (!direction(a.kind, a.port)) a.port = KINDS[a.kind].ports[0]![0]; - const wants = direction(a.kind, a.port) === 'out' ? 'in' : 'out'; - const port = to.port ?? KINDS[to.row.table].ports.find(([, , d]) => d === wants)?.[0]; - if (!port) return note(`${name(to.row)} has no ${wants === 'in' ? 'input' : 'output'}.`); - const b: Plug = { kind: to.row.table, id: to.row.id, port }; - if (direction(b.kind, b.port) !== wants) return note('Wire an output to an input.'); - const [driver, reader] = wants === 'in' ? [a, b] : [b, a]; - const plugged = data.get(driver.kind, driver.id, driver.port) as Row | null; - const patch = new Patch(); - let signal = plugged?.id; - if (signal === undefined) { - signal = fresh(); - patch.set('Signal', [signal], { name: [`Signal ${signal}`] }); - patch.set(driver.kind, [driver.id as number], { - [driver.port]: reference('Signal', [signal as number]), - }); - } - patch.set(reader.kind, [reader.id as number], { - [reader.port]: reference('Signal', [signal as number]), - }); - edit(patch, `Wired ${name(from.row)} and ${name(to.row)}.`); - }); - - /** Removes `rows` as one undoable edit. */ - const remove = (rows: readonly Rows[]) => { - const patch = new Patch(); - for (const { table, ids } of rows) patch.remove(table, ids); - edit(patch, `Removed ${each(rows).map(name).join(', ')}.`); - }; - view.on('delete', ({ rows }) => remove(rows)); - - /** Selects `row` and its neighbors, and fits the view to them. */ - const reveal = (row: Row) => { - const joined = view.neighbors(row); - const near = [one(row), ...joined]; - view.selection = near; - view.fit({ rows: near }); - describe(); - note(`${name(row)} and ${each(joined).length} joined to it.`); - }; - /** Selects `row` and focuses its name for editing. */ - const rename = (row: Row) => { - view.selection = [one(row)]; - describe(); - input.focus(); - input.select(); - }; - const named = (row: Row) => - isBlock(row.table) || row.table === 'Group' || row.table === 'Station'; - - // Opening a row renames it. The context menu lists actions for the row under the pointer. - view.on('open', ({ hit }) => named(hit.row) && rename(hit.row)); - const menu = stage.menu(); - view.on('menu', ({ hits, trigger }) => { - const row = hits[0]?.row; - if (row && !view.selection.some((s) => s.table === row.table && s.ids.includes(row.id))) - view.selection = [one(row)]; - describe(); - const rows = view.selection; - const count = each(rows).length; - menu.open(view, trigger.point, [ - { label: 'Rename', run: () => row && rename(row), disabled: !row || !named(row) }, - { label: 'Reveal neighbors', run: () => row && reveal(row), disabled: !row }, - { - label: count > 1 ? `Delete ${count} rows` : 'Delete', - run: () => remove(rows), - disabled: !count, - }, - { label: 'Fit everything', run: () => view.fit() }, - ]); - }); - - const studio = stage.section('Diagram'); - studio.choice( - 'Preset', - PRESETS.map((p) => [p[0], p] as const), - preset, - (choice) => void load(choice).catch(stage.fail), - ); - studio.actions([ - { - label: 'Undo', - key: 'z', - run: () => { - const before = history.undo(data.snapshot()); - if (!before) return; - data.restore(before); - changed(); - note('Undone.'); - }, - }, - { - label: 'Redo', - key: 'y', - run: () => { - const after = history.redo(data.snapshot()); - if (!after) return; - data.restore(after); - changed(); - note('Redone.'); - }, - }, - { - label: 'Arrange', - key: 'a', - // Clears every block's position so the layout places them all again, then writes the - // computed places back as positions. One undo reverts the whole arrangement. - run: () => { - const patch = new Patch(data.name); - const tables = held(); - for (const kind of kinds.filter((k) => tables.has(k))) { - const rows = ids(kind); - if (rows.length) - patch.set(kind, rows, { x: rows.map(() => null), y: rows.map(() => null) }); - } - history.commit(data.snapshot()); - data.apply(patch); - changed(); - view.fit(); - void view.arrange().then((patches) => { - // Merges the computed places of every table into one patch. - const placed = patches - .filter((p) => p.data === data.name) - .reduce((all, p) => all.extend(p), new Patch(data.name)); - data.apply(placed); - changed(); - note('Arranged.'); - }); - }, - }, - { label: 'Fit', key: 'f', run: () => view.fit() }, - { label: 'Zoom in', key: '=', run: () => zoom(0.5) }, - { label: 'Zoom out', key: '-', run: () => zoom(-0.5) }, - ]); - /** Flies the camera in by `by` zoom levels, where each level doubles the scale. Negative zooms - * out. */ - const zoom = (by: number) => { - const camera = view.camera; - if ('center' in camera) view.fly({ ...camera, zoom: camera.zoom + by }); - }; - const add = stage.section('Add'); - add.actions( - kinds.map((kind, i) => ({ - label: kind, - key: String(i + 1), - run: () => { - // New blocks appear at the camera's center, and nothing else moves. - const camera = view.camera; - const [x, y] = 'center' in camera ? camera.center : [0, 0]; - const id = fresh(); - const plugged = Object.fromEntries( - KINDS[kind].ports.map(([port]) => [port, reference('Signal', [null])]), - ); - edit( - new Patch().set(kind, [id], { - name: [`New ${kind.toLowerCase()}`], - x: [x], - y: [y], - status: [0], - ...plugged, - }), - `Added a ${kind.toLowerCase()} block.`, - ); - }, - })), - ); - - const look = stage.section('Display'); - const layouts: readonly (readonly [string, Layout])[] = [ - ['Left to right', layout], - ['Top to bottom', { layered: { direction: 'down', gap: 30, rankGap: 60 } }], - ['Right to left', { layered: { direction: 'left', gap: 30, rankGap: 80 } }], - ['Bottom to top', { layered: { direction: 'up', gap: 30, rankGap: 60 } }], - ['Force layout', { stress: { gap: 140 } }], - ]; - look.choice('Layout', layouts, layout, (next) => { - layout = next; - view.scene = scene(); - view.fit(); - }); - look.choice( - 'Routing', - [ - ['Around blocks', 'orthogonal'], - ['Straight', 'straight'], - ] as const, - route, - (next) => { - route = next; - view.scene = scene(); - }, - ); - look.toggle('Flow', 'c', comets, (on) => { - comets = on; - view.scene = scene(); - }); - look.toggle('Light', 'l', light, (on) => { - light = on; - view.scene = scene(); - }); - const restyle = () => (view.scene = scene()); - look.toggle('Shapes', 'h', style.shapes, (on) => ((style.shapes = on), restyle())); - look.toggle( - 'Diamond ports', - undefined, - style.diamonds, - (on) => ((style.diamonds = on), restyle()), - ); - look.toggle('Signal names', undefined, style.names, (on) => ((style.names = on), restyle())); - look.choice( - 'Grid', - [ - ['Dots', 'dots'], - ['Dots that snap', 'snap'], - ['Lines', 'lines'], - ['Off', 'off'], - ] as const, - grid, - (next) => ((grid = next), restyle()), - ); - - // Theme settings for blocks, groups, ports, wires, and shadows. - const sheet = stage.section('Sheet', false); - const knob = ( - label: string, - name: - | 'corner' - | 'portSize' - | 'bend' - | 'groupPadding' - | 'wireSpacing' - | 'arrow' - | 'lift' - | 'textSize' - | 'dash', - min: number, - max: number, - step = 1, - ) => - sheet.range( - label, - { min, max, step, value: style[name], format: (v) => `${v} units` }, - (value) => { - style[name] = value; - restyle(); - }, - ); - knob('Corners', 'corner', 0, 16); - knob('Ports', 'portSize', 4, 14); - knob('Wire bends', 'bend', 0, 16); - knob('Group padding', 'groupPadding', 6, 32, 2); - knob('Wire spacing', 'wireSpacing', 2, 12); - knob('Arrowheads', 'arrow', 3, 14); - knob('Text', 'textSize', 9, 16); - knob('Dashes', 'dash', 2, 14); - knob('Shadow lift', 'lift', 0, 10); - sheet.choice( - 'Groups', - [ - ['Slate', 'slate'], - ['Teal', 'teal'], - ['Plum', 'plum'], - ] as const, - style.groups, - (groups) => ((style.groups = groups), restyle()), - ); - sheet.toggle('Shadows', undefined, style.shadows, (on) => ((style.shadows = on), restyle())); - sheet.toggle('Outlines', undefined, style.outline, (on) => ((style.outline = on), restyle())); - - // How the view responds to keys and presses. - const answers = stage.section('Interaction', false); - answers.toggle('Keys', undefined, style.keyboard, (on) => ((style.keyboard = on), restyle())); - answers.toggle('Cycle picks', undefined, style.cycle, (on) => ((style.cycle = on), restyle())); - answers.choice( - 'Long press', - [ - ['Half a second', 0.5], - ['A second', 1], - ['Never', 0], - ] as const, - style.press, - (press) => ((style.press = press), restyle()), - ); - answers.note('Keys: Tab steps through blocks, Enter renames, Space selects, Delete removes.'); - - // Saves the diagram as a `.latk` file, the patch that builds it in latkit's binary format. - // Opening the file restores the diagram unchanged. - const file = stage.section('File', false); - const picker = h('input', { type: 'file', accept: '.latk', hidden: true }); - file.content.append(picker); - picker.addEventListener( - 'change', - () => { - const chosen = picker.files?.[0]; - picker.value = ''; - if (!chosen) return; - void Data.read(chosen, { name: 'studio' }) - .then((read) => open(read, `Opened ${chosen.name}.`)) - .catch((error: unknown) => note(`Could not open ${chosen.name}: ${String(error)}`)); - }, - { signal: stage.signal }, - ); - file.actions([ - { - label: 'Save', - key: 's', - run: () => { - const url = URL.createObjectURL( - new Blob([data.toBytes().slice()], { type: 'application/octet-stream' }), - ); - h('a', { href: url, download: 'diagram.latk' }).click(); - setTimeout(() => URL.revokeObjectURL(url), 10_000); - note('Saved diagram.latk.'); - }, - }, - { label: 'Open', key: 'o', run: () => picker.click() }, - ]); - const described = file.note(); - - // An editable name field for the selected row. - const selected = stage.section('Selection'); - const input = selected.text('Name', (value) => { - const [row] = each(view.selection); - if (row) - edit( - new Patch().set(row.table, [row.id as number], { name: [value] }), - `Renamed to ${value}.`, - ); - }); - input.placeholder = 'Select a block'; - input.disabled = true; - const told = selected.note(); - const note = (text: string) => (told.textContent = text); - // A tag labels the selected block at its screen position. It hides when the camera moves. - const tag = h('p', { class: 'callout', hidden: true }); - stage.regions.main.append(tag); - view.on('camera', () => (tag.hidden = true)); - const describe = () => { - const [row] = each(view.selection); - const editable = !!row && named(row); - input.disabled = !editable; - input.value = editable ? name(row) : ''; - tag.hidden = true; - if (row && isBlock(row.table)) - void view.screenOf(row).then((point) => { - if (!point || each(view.selection)[0]?.id !== row.id) return; - tag.textContent = `${row.table} · ${name(row)}`; - tag.style.translate = `${point[0]}px ${point[1] - 18}px`; - tag.hidden = false; - }); - // Row counts per table, from `data.describe()`. - described.textContent = data - .describe() - .tables.map((t) => `${t.name} ${t.rows.toLocaleString()}`) - .join(' · '); - }; - view.on('selection', describe); - - stage.meter(view); - void load(preset).catch(stage.fail); -} diff --git a/npm/examples/src/examples/index.ts b/npm/examples/src/examples/index.ts deleted file mode 100644 index 09df10e3..00000000 --- a/npm/examples/src/examples/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Page } from '../ui/shell.js'; - -/** The example picker's pages. Their IDs keep older names so existing links still work. */ -export const examples: readonly Page[] = [ - { id: 'bunny', title: 'Soft body', load: () => import('./soft-body/index.js') }, - { id: 'network', title: 'Networks', load: () => import('./networks/index.js') }, - { id: 'operations', title: 'Power flow', load: () => import('./power-flow/index.js') }, - { id: 'telemetry', title: 'Live data', load: () => import('./live-data/index.js') }, - { id: 'signals', title: 'Linked views', load: () => import('./linked-views/index.js') }, - { id: 'diagram', title: 'Diagram', load: () => import('./diagram/index.js') }, - { id: 'colors', title: 'Colormaps', load: () => import('./colormaps/index.js') }, -]; diff --git a/npm/examples/src/examples/linked-views/index.ts b/npm/examples/src/examples/linked-views/index.ts deleted file mode 100644 index a0b2994d..00000000 --- a/npm/examples/src/examples/linked-views/index.ts +++ /dev/null @@ -1,237 +0,0 @@ -/** - * One recording in two views on one canvas: a lattice of nodes on a tilted plane, each on a pole as - * high as its signal, and a plot tracing the signals over time. One playhead moves both views. Play - * at a chosen speed or scrub, and read each view's time and stats. Nodes selected in either view - * are selected in the other too, and their traces stand out. - */ -import { - Data, - Patch, - field, - reference, - selected, - units, - when, - zoom, - type Rows, - type Scene, - type Size, - type View, -} from 'latkit'; -import type { Stage } from '../../ui/stage.js'; -import { theme } from '../../ui/theme.js'; - -/** Nodes per side of the lattice, frames recorded, seconds between frames, and the height of a - * full signal in plane units. */ -const SIDE = 20; -const FRAMES = 1201; -const STEP = 0.05; -const HEIGHT = 3; -/** When only a few traces show, every `TRACED`th node is traced, starting at node 10. */ -const TRACED = 25; - -/** - * Builds the recording: nodes at integer x and y, linked to their neighbors at +x and +y, each with - * a signal rippling across the lattice, `0.5 + 0.45 sin(0.37 x + 1.3 t) cos(0.31 y - 0.7 t)`. - */ -function record(): Data { - const n = SIDE * SIDE; - const x = Float64Array.from({ length: n }, (_, i) => i % SIDE); - const y = Float64Array.from({ length: n }, (_, i) => Math.floor(i / SIDE)); - const traced = Float64Array.from({ length: n }, (_, i) => Number(i % TRACED === 10)); - const [from, to]: [number[], number[]] = [[], []]; - const link = (a: number, b: number) => { - from.push(a); - to.push(b); - }; - for (let i = 0; i < n; i++) { - if (x[i]! < SIDE - 1) link(i, i + 1); - if (y[i]! < SIDE - 1) link(i, i + SIDE); - } - const patch = new Patch() - .axis('time', 's') - .set('Node', { count: n }, { x, y, traced }) - .set( - 'Link', - { count: from.length }, - { from: reference('Node', from), to: reference('Node', to) }, - ); - // One append holds every frame, with the samples laid out frame after frame. - const times = Array.from({ length: FRAMES }, (_, f) => f * STEP); - const signal = new Float32Array(FRAMES * n); - times.forEach((t, f) => { - for (let i = 0; i < n; i++) - signal[f * n + i] = - 0.5 + 0.45 * Math.sin(0.37 * x[i]! + 1.3 * t) * Math.cos(0.31 * y[i]! - 0.7 * t); - }); - return new Data('signals').apply(patch.append('Node', times, { signal })); -} - -export default function page(stage: Stage): void { - const duration = (FRAMES - 1) * STEP; - let few = true; - const look = { - height: HEIGHT, - poles: true, - /** Whether signals hold their last sample instead of easing between samples. */ - held: false, - size: 'pixels' as 'pixels' | 'zoom' | 'units', - }; - /** Each node's signal, eased between samples or held at the last one. */ - const reading = () => (look.held ? field('signal').step() : field('signal')); - const signal = () => reading().colormap('viridis').domain(0, 1); - /** A node's radius: fixed CSS pixels, growing with the zoom, or plane units clamped to a range of - * pixels. */ - const radius = (): Size => - look.size === 'zoom' - ? zoom().mul(0.9).clamp(1.5, 9) - : look.size === 'units' - ? units(0.18, { atLeast: 1.5, atMost: 24 }) - : 3.4; - const lattice = (): Scene => ({ - space: { plane: { grid: 'off' } }, - theme: { ...theme, focus: { dim: 0.25 } }, - marks: [ - { edges: { of: ['signals', 'Link'], ends: ['from', 'to'], width: 1, color: '#3a4556' } }, - ...(look.poles - ? [ - { - edges: { - of: ['signals', 'Node'] as [string, string], - poles: true, - width: 1.5, - color: signal(), - }, - }, - ] - : []), - { - vertices: { - of: ['signals', 'Node'], - position: ['$x', '$y'], - elevation: reading().mul(look.height), - radius: radius(), - color: signal(), - }, - }, - ], - }); - const traces = (): Scene => ({ - space: { - plot: { x: { label: 'Time (s)' }, y: { label: 'Signal', format: 'fixed', precision: 2 } }, - }, - theme: { ...theme, focus: { dim: 0.2 } }, - ease: 0, - marks: [ - { - lines: { - of: ['signals', 'Node'], - trace: 'signal', - color: signal(), - // Selected nodes' traces are drawn wider, and the rest dim. - width: when(selected()) - .then(3) - .otherwise(few ? 1.5 : 0.8), - ...(few ? { visible: field('traced').gt(0.5).or(selected()) } : {}), - }, - }, - ], - }); - const [plane, plot] = stage.panels([lattice(), traces()], { - label: 'A lattice on poles beside a plot of its signals: one playhead moves both', - at: [ - [0, 0, 0.55, 1], - [0.55, 0, 0.45, 1], - ], - }) as [View, View]; - const data = record(); - plane.sources = plot.sources = [data]; - plane.camera = { center: [(SIDE - 1) / 2, (SIDE - 1) / 2, 0], zoom: 3, pitch: 50, bearing: 20 }; - plane.fit(); - plot.camera = { x: [0, duration], y: [0, 1] }; - const restyle = () => { - plane.scene = lattice(); - plot.scene = traces(); - }; - - // Copy each view's selection to the other. - const same = (a: readonly Rows[], b: readonly Rows[]) => JSON.stringify(a) === JSON.stringify(b); - for (const [from, to] of [ - [plane, plot], - [plot, plane], - ] as const) - from.on('selection', () => { - if (!same(from.selection, to.selection)) to.selection = from.selection; - }); - - const views = [ - ['Lattice', plane], - ['Plot', plot], - ] as const; - const playback = stage.playback({ - live: false, - show(at) { - for (const [, view] of views) view.at = at; - }, - }); - playback.record([0, duration]); - - // Each view's time and stats. Both views draw in the canvas's single frame, so their times always - // match. - const where = stage.section('View stats', false); - const lines = views.map(() => where.note()); - let read = 0; - stage.frame(() => { - if (performance.now() - read < 250) return; - read = performance.now(); - views.forEach(([name, view], i) => { - const { frames, cpuMs } = view.stats; - lines[i]!.textContent = - `${name} at ${view.at.toFixed(3)} s · ${frames} frames · ${cpuMs.toFixed(1)} ms`; - }); - }); - - const play = stage.section('Playback'); - play.speed([0.25, 1, 4], 1, (rate) => (playback.rate = rate)); - play.choice( - 'Traces', - [ - [`${(SIDE * SIDE) / TRACED} and the selected`, true], - [`All ${SIDE * SIDE}`, false], - ] as const, - few, - (next) => { - few = next; - plot.scene = traces(); - }, - ); - play.toggle('Hold samples', 'h', look.held, (on) => ((look.held = on), restyle())); - const shape = stage.section('Lattice'); - shape.range( - 'Height', - { min: 0, max: 6, step: 0.25, value: look.height, format: (v) => `${v.toFixed(2)} units` }, - (height) => { - look.height = height; - plane.scene = lattice(); - }, - ); - shape.choice( - 'Node size', - [ - ['Pixels', 'pixels'], - ['Grows with zoom', 'zoom'], - ['Units of the plane', 'units'], - ] as const, - look.size, - (size) => ((look.size = size), (plane.scene = lattice())), - ); - shape.toggle('Poles', 'p', look.poles, (on) => ((look.poles = on), (plane.scene = lattice()))); - shape.note('Select nodes on the lattice or traces on the plot: both views select them.'); - const legend = stage.legend('top'); - legend.ramp('viridis', 'Signal 0–1'); - - stage.meter(plane); - stage.detail( - `${(SIDE * SIDE).toLocaleString()} nodes · ${FRAMES.toLocaleString()} frames over ${duration} s`, - ); -} diff --git a/npm/examples/src/examples/live-data/index.ts b/npm/examples/src/examples/live-data/index.ts deleted file mode 100644 index f7108b4c..00000000 --- a/npm/examples/src/examples/live-data/index.ts +++ /dev/null @@ -1,757 +0,0 @@ -/** - * Thousands of streaming signals traced over a scrolling window. Frames are appended as they - * arrive, and the oldest are forgotten. Read the trace under the pointer, rank the largest changes - * with a query, color traces by area or by their current value, and strike a disturbance to watch - * it spread. Or draw a recorded day instead, fetched span by span as time tiles, at the detail the - * window needs. - */ -import { - Data, - Patch, - Tiles, - colormapCss, - field, - type ColormapName, - type Format, - type Hit, - type Id, - type Interpolation, - type Query, - type Scene, - type Snapshot, -} from 'latkit'; -import { random } from '../../random.js'; -import { h } from '../../ui/dom.js'; -import { each } from '../../ui/rows.js'; -import type { Stage } from '../../ui/stage.js'; -import { theme } from '../../ui/theme.js'; - -/** The signal areas, with the colors tab10 gives them. */ -const AREAS = [ - ['North', '#1f77b4'], - ['South', '#ff7f0e'], - ['East', '#2ca02c'], - ['West', '#d62728'], - ['Coast', '#9467bd'], - ['Valley', '#8c564b'], -] as const; -/** Seconds of history kept. Older frames are forgotten. */ -const HISTORY = 120; -/** Seconds between automatic disturbances, and how long each lasts (long enough to die away). */ -const EVERY = 25; -const LASTS = 6; -/** Signals per page of the hot list, and how often it refreshes, in milliseconds. */ -const HOT = 12; -const HOT_EVERY = 250; -/** The recorded day, as time tiles of 64 buses' voltages every 15 s. `scripts/history.ts` writes - * them. */ -const RECORDED = '/history/frames.json'; -/** Seconds of the recorded day shown per second of playback. */ -const DAY_RATE = 600; - -/** - * The quantities the signals can measure. Each has a unit, a normal range, the decimal places to - * show, and whether its low values (rather than high) are the ones to watch. - */ -const QUANTITIES = { - voltage: { label: 'Voltage', unit: 'pu', range: [0.92, 1.04], digits: 4, low: true }, - frequency: { label: 'Frequency', unit: 'Hz', range: [59.8, 60.06], digits: 3, low: true }, - loading: { label: 'Loading', unit: '%', range: [0, 130], digits: 1, low: false }, - temperature: { label: 'Temperature', unit: '°C', range: [20, 110], digits: 1, low: false }, -} as const; -type Quantity = keyof typeof QUANTITIES; -/** The colormaps offered for coloring traces by current value. */ -const MAPS: readonly ColormapName[] = ['coolwarm', 'viridis', 'turbo', 'magma', 'icefire']; -/** The chance per sample that a signal reports nothing, when dropouts are on. */ -const DROPOUT = 0.002; - -/** - * Sends patches as bytes through a stream, as a socket or a response body would deliver them. Each - * patch is written in two pieces, as a network may split it. `Patch.stream` reads each patch back - * once its last byte arrives. - */ -class Feed { - readonly #writer: WritableStreamDefaultWriter; - - constructor(take: (patch: Patch) => void) { - const { readable, writable } = new TransformStream(); - this.#writer = writable.getWriter(); - void (async () => { - for await (const patch of Patch.stream(readable)) take(patch); - })(); - } - - send(patch: Patch): void { - const bytes = patch.toBytes(); - const half = bytes.length >> 1; - void this.#writer.write(bytes.subarray(0, half)); - void this.#writer.write(bytes.subarray(half)); - } - - close(): void { - void this.#writer.close(); - } -} - -/** - * Synthetic SCADA signals. Each signal has its own offset and noise, and each area swings slowly. - * Every `EVERY` seconds one area is disturbed and recovers, and so does an area struck by hand. A - * disturbance pulls voltages and the system frequency down, and pushes loading and temperature up. - */ -class Signals { - readonly area: Float64Array; - readonly name: string[] = []; - readonly #offset: Float32Array; - readonly #noise: Float32Array; - readonly #random = random(11); - /** The area struck by hand, and when. */ - #struck?: { area: number; at: number }; - - /** Whether signals occasionally report nothing. */ - dropouts = false; - - constructor( - readonly count: number, - readonly quantity: Quantity, - ) { - this.area = new Float64Array(count); - this.#offset = new Float32Array(count); - this.#noise = new Float32Array(count); - for (let i = 0; i < count; i++) { - this.area[i] = i % AREAS.length; - this.#offset[i] = this.#random() - 0.5; - this.#noise[i] = 0.3 + this.#random(); - this.name.push(`${AREAS[i % AREAS.length]![0]} ${Math.floor(i / AREAS.length) + 1}`); - } - } - - /** Strikes area `area` at time `t`. It is disturbed and recovers like a timed disturbance. */ - strike(area: number, t: number): void { - this.#struck = { area, at: t }; - } - - /** Writes every signal's value at `t` into `out`, with NaN for a signal that reports nothing. */ - sample(t: number, out: Float32Array): void { - const struck = Math.floor(t / EVERY) % AREAS.length; - const dip = depth(t - Math.floor(t / EVERY) * EVERY); - const byHand = this.#struck && t >= this.#struck.at ? depth(t - this.#struck.at) : 0; - // The system frequency falls with a disturbance in any area. - const system = 60 + 0.01 * Math.sin(t * 0.21) - 2 * Math.max(dip, byHand); - for (let i = 0; i < this.count; i++) { - const a = this.area[i]!; - const swing = Math.sin(t * (0.6 + a * 0.13) + a); - const noise = (this.#random() - 0.5) * this.#noise[i]!; - const dips = (a === struck ? dip : 0) + (a === this.#struck?.area ? byHand : 0); - const offset = this.#offset[i]!; - if (this.dropouts && this.#random() < DROPOUT) { - out[i] = NaN; - continue; - } - out[i] = - this.quantity === 'voltage' - ? 1 + 0.03 * offset + 0.012 * swing + 0.003 * noise - dips - : this.quantity === 'frequency' - ? system + 0.004 * swing + 0.001 * noise - : this.quantity === 'loading' - ? 60 + 25 * offset + 15 * swing + 2 * noise + 600 * dips - : 45 + 20 * offset + 8 * swing + 0.5 * noise + 400 * dips; - } - } -} - -/** How far a disturbance has pulled values, `since` seconds after it started. It is zero once - * `LASTS` seconds have passed. */ -function depth(since: number): number { - return since < LASTS - ? 0.06 * Math.exp(-since * 1.4) * Math.sin(Math.min(since, 0.5) * Math.PI) - : 0; -} - -/** Formats second `at` of a day as a clock time, hh:mm:ss. */ -function clock(at: number): string { - const s = Math.max(0, Math.round(at)); - const two = (n: number) => String(n).padStart(2, '0'); - return `${two(Math.floor(s / 3600))}:${two(Math.floor(s / 60) % 60)}:${two(s % 60)}`; -} - -export default function page(stage: Stage): void { - let source: 'live' | 'recorded' = 'live'; - let count = 2000; - let quantity: Quantity = 'voltage'; - let window = 20; - let dayWindow = Infinity; - let rate = 50; - let interpolation: Interpolation = 'linear'; - let auto = true; - let colorBy: 'area' | 'value' | 'plain' = 'area'; - let colormap: ColormapName = 'coolwarm'; - let width = 1; - const axes = { format: 'fixed' as Format, grid: true, limits: false, time: true, title: false }; - let dim = 0.15; - let playhead = '#ffb000cc'; - let streamed = false; - let dropouts = false; - let signals = new Signals(count, quantity); - let values = new Float32Array(count); - let data = new Data('scada'); - /** The span of the recorded day that has arrived so far. */ - let day: [number, number] | undefined; - let t = 0; - let carry = 0; - const shown = () => QUANTITIES[quantity]; - - const scene = (): Scene => { - const { label, unit, range, digits } = shown(); - const middle = (range[0] + range[1]) / 2; - return { - space: { - plot: { - x: { label: 'Time (s)', grid: axes.grid, hidden: !axes.time }, - y: { - label: `${label} (${unit})`, - format: axes.format, - precision: axes.format === 'auto' ? null : digits - 1, - grid: axes.grid, - // Limits: ticks at the ends and the middle of the quantity's normal range. - ...(axes.limits - ? { - ticks: [ - [range[0], 'low'], - [middle, null], - [range[1], 'high'], - ] as const, - } - : {}), - }, - }, - }, - ...(axes.title - ? { - title: `${label} ${source === 'live' ? 'live' : 'over a recorded day'}`, - subtitle: - source === 'live' - ? `${count.toLocaleString()} signals · ${rate} samples a second` - : '64 buses · a sample every 15 s', - } - : {}), - // Traces use the theme's line width, and its line color when plain. Samples a signal did not - // report are drawn in the `missing` color. - theme: { - ...theme, - focus: { dim }, - plot: { playhead }, - line: { color: '#7fb3ff', width }, - missing: '#ff4fd8', - }, - ease: 0, - marks: [ - { - lines: { - of: ['scada', 'Signal'], - trace: 'value', - // Colored by value, each trace takes the color of its value at the time shown. - ...(colorBy === 'plain' - ? {} - : { - color: - colorBy === 'area' - ? field('area').colormap('tab10') - : field('value').colormap(colormap).domain(range[0], range[1]), - }), - interpolation, - }, - }, - ], - }; - }; - const view = stage.view(scene(), { - label: 'Live signals: hover a trace to read it, click it to pick it out', - }); - - /** - * Applies `f` to the signal data the view draws now: the live record, or the fetched part of the - * recorded day. Returns `undefined` before any of the day has arrived. - */ - const read = (f: (held: Data | Snapshot) => T): T | undefined => { - if (source === 'live') return f(data); - const held = view.snapshot('scada'); - if (!held) return undefined; - try { - return f(held); - } finally { - held.destroy(); - } - }; - - /** Points the camera at time `at`: the window ending at `at`, or everything kept when the window - * is unlimited. Until the record is as long as the window, the window starts at the record's - * start. With Auto range off, y spans the quantity's normal range. */ - const frame = (at: number) => { - const first = read((held) => held.describe().span?.[0]) ?? at; - const span = source === 'live' ? window : dayWindow; - const x: [number, number] = - span === Infinity - ? [first, Math.max(at, first + 1)] - : [Math.max(first, at - span), Math.max(at, first + span)]; - const range = shown().range; - view.camera = { x, y: auto ? null : [range[0], range[1]] }; - }; - - /** Applies a patch from the feed. The record grows, and the view draws it the next frame. */ - const take = (patch: Patch) => { - data.apply(patch); - playback.record(data.describe().span!); - }; - /** The byte stream that patches go through when they are streamed. It is rebuilt with each - * record. */ - let feed: Feed | undefined; - stage.signal.addEventListener('abort', () => feed?.close()); - - /** Draws the chosen source: a new live record of `n` signals measuring the current quantity, or - * the recorded day, drawn as the tiles the window needs arrive. */ - const restart = (n = count) => { - count = n; - // Stop the previous record's feed. - feed?.close(); - feed = undefined; - data.destroy(); - view.selection = []; - hot.replaceChildren(); - for (const section of [feedSection, disturb]) section.element.hidden = source === 'recorded'; - liveWindows.parentElement!.hidden = source === 'recorded'; - dayWindows.parentElement!.hidden = source === 'live'; - if (source === 'recorded') { - day = undefined; - quantity = 'voltage'; - measure.value = String(Object.keys(QUANTITIES).indexOf(quantity)); - data = new Data('scada'); - view.sources = [new Tiles('scada', RECORDED)]; - playback.live = false; - playback.rate = DAY_RATE; - restyle(); - detail(); - describe(null); - return; - } - signals = new Signals(n, quantity); - signals.dropouts = dropouts; - values = new Float32Array(n); - t = 0; - carry = 0; - // The record starts with its first frame, so the value field exists from the start. - signals.sample(0, values); - data = new Data('scada').apply( - new Patch() - .axis('time', 's') - .set('Signal', { count: n }, { name: signals.name, area: signals.area }) - .append('Signal', 0, { value: values }), - ); - view.sources = [data]; - const current = data; - feed = new Feed((patch) => data === current && take(patch)); - playback.rate = 1; - playback.live = true; - restyle(); - frame(0); - detail(); - describe(null); - }; - - const playback = stage.playback({ - replay: { seconds: 10, rate: 0.25 }, - show(at) { - view.at = at; - frame(at); - }, - format: (at) => (source === 'live' ? `${at.toFixed(2)} s` : clock(at)), - }); - - const meter = stage.meter(view); - meter.note(() => - source === 'live' ? `${((count * rate) / 1000).toFixed(0)}k samples/s` : 'tiles of a day', - ); - let listed = 0; - let spanned = 0; - stage.frame((dt) => { - if (performance.now() - listed > HOT_EVERY) list(); - // The recorded day's span grows as more of it arrives. A playhead at the end follows the new - // end. - if (source === 'recorded' && performance.now() - spanned > HOT_EVERY) { - spanned = performance.now(); - const span = read((held) => held.describe().span); - if (span && (span[0] !== day?.[0] || span[1] !== day?.[1])) { - const end = !day || playback.at >= day[1]; - day = span; - playback.record(span); - if (end) playback.seek(span[1]); - } - } - if (playback.mode !== 'live') return; - meter.measure('append', () => { - carry += dt * rate; - const frames = Math.floor(carry); - if (!frames) return; - carry -= frames; - const patch = new Patch(); - for (let k = 0; k < frames; k++) { - t += 1 / rate; - signals.sample(t, values); - patch.append('Signal', t, { value: values }); - } - patch.forget(t - HISTORY); - if (streamed) feed!.send(patch); - else take(patch); - }); - }); - - const readout = stage.section('Reading'); - const reading = h('p', { class: 'series' }); - readout.content.append(reading); - const name = (id: Id) => - (read((held) => held.get('Signal', id, 'name')) as string | null | undefined) ?? `Signal ${id}`; - const format = (value: number) => { - const { unit, digits } = shown(); - return `${value.toFixed(digits)} ${unit}`; - }; - const when = (at: number) => (source === 'live' ? `${at.toFixed(2)} s` : clock(at)); - /** Signal `id`'s least and greatest values in the window, from its levels of detail. */ - const spread = (id: Id) => { - const camera = view.camera; - if (!('x' in camera)) return ''; - const envelope = read((held) => - held.describe().span - ? held.envelope({ table: 'Signal', field: 'value', range: camera.x, spans: 1, ids: [id] }) - : undefined, - ); - if (!envelope || Number.isNaN(envelope.least[0])) return ''; - return `; ${format(envelope.least[0]!)} to ${format(envelope.greatest[0]!)} in the window`; - }; - const describe = (hit: Hit | null) => { - const picked = each(view.selection) - .map((row) => name(row.id)) - .join(', '); - const sample = hit?.sample; - reading.textContent = sample - ? `${name(hit.row.id)} at ${when(sample.at)}: ${format(sample.value)}${spread(hit.row.id)}` - : picked - ? `Picked ${picked}.` - : 'Hover a trace.'; - }; - view.on('hovered', ({ hit }) => describe(hit)); - view.on('selection', () => { - describe(null); - list(); - }); - - // The signals with the most extreme values at the time shown (lowest or highest, depending on - // the quantity), among all signals or one area's. The query selects them in linear time and - // returns them page by page. - const hottest = stage.section('Largest changes', false); - let inArea = -1; - let page = 0; - hottest.choice( - 'Area', - [['All', -1] as const, ...AREAS.map(([label], i) => [label, i] as const)], - inArea, - (next) => { - inArea = next; - page = 0; - list(); - }, - ); - const turn = (by: number) => () => { - page = Math.max(0, page + by); - list(); - }; - hottest.actions([ - { label: 'Previous', run: turn(-1) }, - { label: 'Next', run: turn(1) }, - ]); - const paged = hottest.note(); - const hot = h('ol', { class: 'hot' }); - hottest.content.append(hot); - const list = () => { - listed = performance.now(); - const { low, range } = shown(); - const query: Query = { - of: 'Signal', - ...(inArea >= 0 ? { filter: field('area').eq(inArea) } : {}), - sort: [['$value', low ? 'ascending' : 'descending']], - limit: HOT, - select: { name: '$name', value: '$value' }, - at: view.at, - }; - const found = read((held) => { - if (!held.describe().span) return undefined; - let rows = held.query({ ...query, offset: page * HOT }); - // Past the last page, show the last page instead. - if (!rows.ids.length && page > 0) { - page = Math.max(0, Math.ceil(rows.total / HOT) - 1); - rows = held.query({ ...query, offset: page * HOT }); - } - return rows; - }); - if (!found) return; - paged.textContent = found.total - ? `${page * HOT + 1}–${page * HOT + found.ids.length} of ${found.total.toLocaleString()}` - : 'None'; - const names = found.fields.name as readonly string[]; - const readings = found.fields.value as Float64Array; - const picked = new Set(each(view.selection).map((row) => String(row.id))); - hot.replaceChildren( - ...found.ids.map((id, k) => { - const value = readings[k]!; - const span = (value - range[0]) / (range[1] - range[0]); - const score = Math.min(1, Math.max(0, low ? 1 - span : span)); - const button = h( - 'button', - { type: 'button', 'aria-pressed': String(picked.has(String(id))) }, - h('span', {}, String(page * HOT + k + 1).padStart(2, '0')), - h('span', {}, names[k] ?? `Signal ${id}`), - h('span', {}, format(value)), - h('span', { class: 'bar', style: `--score: ${(score * 100).toFixed(0)}%` }), - ); - button.addEventListener('click', () => { - view.selection = [{ data: 'scada', table: 'Signal', ids: [id] }]; - describe(null); - list(); - }); - return h('li', {}, button); - }), - ); - }; - - const record = stage.section('Data'); - record.choice( - 'Source', - [ - ['Live feed', 'live'], - ['Recorded day', 'recorded'], - ] as const, - source, - (next) => { - source = next; - restart(); - }, - ); - const feedSection = stage.section('Live feed'); - feedSection.note('Simulated data'); - const measure = feedSection.choice( - 'Measure', - Object.entries(QUANTITIES).map( - ([key, q]) => [`${q.label} (${q.unit})`, key as Quantity] as const, - ), - quantity, - (next) => { - quantity = next; - restart(); - }, - ); - feedSection.choice( - 'Signals', - [ - ['500', 500], - ['2,000', 2000], - ['5,000', 5000], - ], - count, - restart, - ); - feedSection.choice( - 'Samples', - [ - ['20 per second', 20], - ['50 per second', 50], - ['100 per second', 100], - ], - rate, - (r) => { - rate = r; - restyle(); - }, - ); - feedSection.choice( - 'Feed', - [ - ['In the page', false], - ['Streamed bytes', true], - ] as const, - streamed, - (next) => (streamed = next), - ); - feedSection.toggle('Dropouts', 'o', dropouts, (on) => { - dropouts = on; - signals.dropouts = on; - }); - feedSection.actions([{ label: 'Reset', key: 'n', run: () => restart() }]); - - const show = stage.section('Display'); - const liveWindows = show.choice( - 'Window', - [ - ['5 s', 5], - ['20 s', 20], - ['60 s', 60], - [`All ${HISTORY} s kept`, Infinity], - ], - window, - (w) => { - window = w; - frame(view.at); - }, - ); - const dayWindows = show.choice( - 'Window', - [ - ['10 minutes', 600], - ['An hour', 3600], - ['Six hours', 6 * 3600], - ['The day', Infinity], - ], - dayWindow, - (w) => { - dayWindow = w; - frame(view.at); - }, - ); - show.choice( - 'Color by', - [ - ['Area', 'area'], - ['Value now', 'value'], - ['Plain', 'plain'], - ] as const, - colorBy, - (next) => { - colorBy = next; - restyle(); - }, - ); - show.range( - 'Width', - { min: 0.5, max: 3, step: 0.5, value: width, format: (v) => `${v} px` }, - (next) => { - width = next; - view.scene = scene(); - }, - ); - show.choice( - 'Between samples', - [ - ['Lines', 'linear'], - ['Steps after', 'stepAfter'], - ['Steps before', 'stepBefore'], - ] as const, - interpolation, - (next) => { - interpolation = next; - view.scene = scene(); - }, - ); - show.range( - 'Dim the rest', - { min: 0, max: 1, step: 0.05, value: dim, format: (v) => `${Math.round(v * 100)}%` }, - (next) => { - dim = next; - view.scene = scene(); - }, - ); - show.choice( - 'Playhead', - [ - ['Amber', '#ffb000cc'], - ['White', '#ffffffaa'], - ['Hidden', '#00000000'], - ] as const, - playhead, - (next) => { - playhead = next; - view.scene = scene(); - }, - ); - show.toggle('Auto range', 'a', auto, (on) => { - auto = on; - frame(view.at); - }); - show.toggle('Title', undefined, axes.title, (on) => ((axes.title = on), (view.scene = scene()))); - const axesSection = stage.section('Axes', false); - axesSection.choice( - 'Values', - [ - ['Fixed', 'fixed'], - ['Automatic', 'auto'], - ['Scientific', 'scientific'], - ['Engineering', 'engineering'], - ] as const, - axes.format, - (format) => ((axes.format = format), (view.scene = scene())), - ); - axesSection.toggle('Grid', 'g', axes.grid, (on) => ((axes.grid = on), (view.scene = scene()))); - axesSection.toggle( - 'Limits', - 'l', - axes.limits, - (on) => ((axes.limits = on), (view.scene = scene())), - ); - axesSection.toggle( - 'Time axis', - undefined, - axes.time, - (on) => ((axes.time = on), (view.scene = scene())), - ); - const areas = show.series(AREAS.map(([label, color]) => ({ label, color }))); - const maps = h('div', { class: 'swatches' }); - const swatches = MAPS.map((map) => { - const swatch = h( - 'button', - { type: 'button', class: 'swatch', 'aria-pressed': String(map === colormap) }, - h('span', { class: 'ramp', style: `background: ${colormapCss(map)}` }), - map, - ); - swatch.addEventListener('click', () => { - colormap = map; - restyle(); - }); - maps.append(swatch); - return [map, swatch] as const; - }); - show.content.append(maps); - const legend = stage.legend('top'); - const restyle = () => { - view.scene = scene(); - const { label, unit, range } = shown(); - areas.hidden = colorBy !== 'area'; - maps.hidden = legend.hidden = colorBy !== 'value'; - legend.ramp(colormap, `${label} now, ${range[0]} – ${range[1]} ${unit}`); - for (const [map, swatch] of swatches) - swatch.setAttribute('aria-pressed', String(map === colormap)); - }; - - const disturb = stage.section('Disturbance'); - let target = 0; - disturb.choice( - 'Area', - AREAS.map(([label], i) => [label, i] as const), - target, - (next) => (target = next), - ); - disturb.actions([ - { - label: 'Strike it now', - key: 'd', - run: () => { - signals.strike(target, t); - if (playback.mode !== 'live') playback.play(); - }, - }, - ]); - disturb.note(`Auto disturbance: every ${EVERY} s`); - - const detail = () => - stage.detail( - source === 'live' - ? `${count.toLocaleString()} signals · ${shown().label.toLowerCase()} · ${HISTORY} s kept` - : '64 buses · voltage · a recorded day, fetched as the window needs it', - ); - restart(); -} diff --git a/npm/examples/src/examples/networks/index.ts b/npm/examples/src/examples/networks/index.ts deleted file mode 100644 index 6771c17f..00000000 --- a/npm/examples/src/examples/networks/index.ts +++ /dev/null @@ -1,1033 +0,0 @@ -/** - * A large power grid drawn over the earth. Choose a grid: a synthetic case from WECC 240-bus to - * ACTIVSg70k, a two-million-bus continental grid built in the page, or a lattice of up to a million - * buses. Choose the map beneath it: coasts and borders, Natural Earth's states colored by the - * grid's statistics, or OpenStreetMap's planet as vector tiles. - * - * Play a simulated fault and watch the machine speeds swing. Turn the globe, with daylight at noon - * or now. Find a bus by name, and hover or click anything to inspect it. The other panels change - * how buses, lines, and labels draw, how focus and comets look, and how the view responds to the - * pointer and keys. - */ -import { - Patch, - field, - hovered, - pointer, - reference, - selected, - units, - when, - zoom, - type ColormapName, - type Controls, - type Data, - type Expr, - type Fit, - type Mark, - type Marker, - type Row, - type Scene, - type Size, - type Zoom, -} from 'latkit'; -import { h } from '../../ui/dom.js'; -import { each } from '../../ui/rows.js'; -import type { Stage } from '../../ui/stage.js'; -import { theme } from '../../ui/theme.js'; -import { extent, loadCase, megabytes, type Case } from './cases.js'; -import { continent } from './continent.js'; -import { LEVELS } from './format.js'; -import { lattice } from './lattice.js'; -import { LOOK, MAPS, type Loaded, type MapChoice } from './maps.js'; -import { STATE_FIELDS, joinStates, type Joined, type StateField } from './states.js'; - -/** The grid choices as [label, id], case files first. */ -const GRIDS = [ - ['WECC 240', 'wecc240'], - ['ACTIVSg2000', 'activsg2000'], - ['ACTIVSg10k', 'activsg10k'], - ['ACTIVSg25k', 'activsg25k'], - ['ACTIVSg70k', 'activsg70k'], - ['North America · 2M', 'continent-2m'], - ['Lattice · 100K', 'lattice-100k'], - ['Lattice · 1M', 'lattice-1m'], - ['None', 'none'], -] as const; -type GridId = (typeof GRIDS)[number][1]; - -/** Each voltage level's color, from below 100 kV to 765 kV. */ -const LEVEL_COLORS = ['#56657a', '#2fa49b', '#7fc97f', '#e6c84f', '#f08a3c', '#e5486f']; -const BUS_COLORS = { - level: { label: 'voltage level', colormap: undefined }, - plain: { label: 'plain', colormap: undefined }, - load: { label: 'load', colormap: 'mako' }, - generation: { label: 'generation', colormap: 'amber' }, -} as const satisfies Record; -type BusColor = keyof typeof BUS_COLORS; -/** Capitalizes a label's first letter for display as an option. */ -const option = (label: string) => label[0]!.toUpperCase() + label.slice(1); -const STATE_COLORMAPS: Readonly> = { - buses: 'viridis', - load: 'mako', - generation: 'amber', - swing: 'inferno', -}; -/** Bus marker choices, each a shape name or the `{ shape }` form of `Marker`. */ -const MARKERS = { - circle: { shape: 'ellipse' }, - square: 'rectangle', - rounded: 'rounded', - diamond: 'diamond', -} as const satisfies Record; -/** Label font choices, as CSS font families. */ -const FONTS = { - sans: 'system-ui, sans-serif', - serif: 'Georgia, serif', - mono: 'ui-monospace, monospace', -} as const; -/** The side, in degrees, of the squares that group buses into zones. */ -const ZONE = 4; -/** Fields the inspector hides: location fields, which the view already shows, and translations. */ -const HIDDEN = /^(lon|lat|geometry|position|zone|name_.*|name:.*)$/; -/** Noon at midsummer over the western US, where the first grid lies, in seconds since the epoch. */ -const NOON = Date.UTC(2026, 5, 21, 19) / 1000; -const fixed = (value: number) => (value >= 100 ? value.toFixed(0) : value.toPrecision(2)); - -/** The grid being drawn: its dataset, its counts, and its case if it came from one. */ -interface Grid { - readonly id: GridId; - readonly label: string; - /** Its dataset, absent when only the map is drawn. */ - readonly data?: Data; - readonly buses: number; - readonly lines: number; - readonly summary: string; - readonly case?: Case; - readonly peaks: { readonly load: number; readonly generation: number; readonly power: number }; - /** The bounding box of its buses, which framing shows. Absent when only the map is drawn. */ - readonly bounds?: Fit; - /** Whether its buses are grouped into zones by the map square they lie in. */ - readonly zoned: boolean; -} - -/** Groups a case's buses into zones by the `ZONE`-degree square each lies in. The patch adds a - * `Zone` row per square and a `zone` reference from each bus to its zone. An edges mark draws each - * zone as a star joining its buses. */ -function zones(found: Case): Patch { - const ids = found.data.query({ of: 'Bus', select: {} }).ids; - const squares = new Map(); - const zone = new Uint32Array(ids.length); - for (let i = 0; i < ids.length; i++) { - const x = Math.floor(found.positions[i * 2]! / ZONE); - const y = Math.floor(found.positions[i * 2 + 1]! / ZONE); - const key = (x + 1000) * 1000 + y + 1000; - let z = squares.get(key); - if (z === undefined) squares.set(key, (z = squares.size)); - zone[i] = z; - } - const hue = Float64Array.from({ length: squares.size }, (_, z) => z % 20); - return new Patch() - .set('Zone', { count: squares.size }, { hue }) - .set('Bus', ids, { zone: reference('Zone', zone) }); -} - -export default async function page(stage: Stage): Promise { - const state = { - projection: 'globe' as 'mercator' | 'tilt' | 'globe' | 'plane', - daylight: 'noon' as 'off' | 'now' | 'noon', - atmosphere: true, - graticule: false, - color: 'level' as BusColor, - buses: true, - lines: true, - machines: true, - zones: false, - labels: false, - mapLabels: true, - statesBy: 'none' as 'none' | StateField, - flow: false, - }; - /** How buses, lines, labels, focus, and comets look. */ - const look = { - marker: 'circle' as keyof typeof MARKERS, - size: 'pixels' as 'pixels' | 'zoom' | 'units', - emphasis: 'none' as 'none' | 'hovered' | 'selected' | 'pointer', - route: 'geodesic' as 'geodesic' | 'straight', - arrows: false, - width: 1, - lineColor: 'level' as 'level' | 'ends' | 'theme', - bow: 0.12, - labelSize: 10, - /** The zoom from which buses are labeled; 0 labels them at any zoom. */ - labelZoom: 5, - font: 'sans' as keyof typeof FONTS, - wrap: 0, - glow: 5, - dim: 0.35, - halo: 2, - grow: 1.25, - night: 0.28, - comets: 'screen' as 'screen' | 'map', - cometScale: 1, - /** The name of the bus the last search found, which is ringed. */ - found: '', - }; - /** How the view responds to the pointer and keys. */ - const interaction = { - preset: 'explore' as 'explore' | 'inspect' | 'none', - zoom: 'wheel' as Zoom, - pan: true, - turn: true, - keyboard: true, - hover: true, - cycle: false, - selectBox: true, - pickRadius: 6, - }; - let grid: Grid | undefined; - let map: { choice: MapChoice; loaded: Loaded } | undefined; - let joined: Joined | undefined; - - const controls = (): Controls => - interaction.preset !== 'explore' - ? interaction.preset - : { - pan: interaction.pan, - zoom: interaction.zoom, - turn: interaction.turn, - keyboard: interaction.keyboard, - hover: interaction.hover, - cycle: interaction.cycle, - selectBox: interaction.selectBox, - pickRadius: interaction.pickRadius, - }; - - const scene = (): Scene => ({ - space: - state.projection === 'plane' - ? 'plane' - : { - earth: { - projection: state.projection === 'globe' ? 'globe' : 'mercator', - graticule: state.graticule, - atmosphere: state.atmosphere, - ...(state.daylight === 'off' - ? {} - : { daylight: state.daylight === 'now' ? Date.now() / 1000 : NOON }), - }, - }, - theme: { - ...theme, - area: { color: LOOK.fill }, - earth: { ground: LOOK.ground, atmosphere: '#5aa9ff99', night: look.night }, - text: { halo: look.halo }, - focus: { glow: look.glow, dim: look.dim, grow: look.grow }, - // Marks without their own color use the theme's: plain buses and lines. A null edge color - // makes lines blend the colors of the buses they join. - vertex: { color: '#d9e2ec' }, - edge: { color: look.lineColor === 'ends' ? null : '#8fa3b8', bow: look.bow }, - // Comets stay soft, since rivers carry them across the map too. Their length and spacing are - // in pixels on screen, or in degrees on the map, so they scale with the zoom. - flow: - look.comets === 'map' - ? { - color: '#d6e6ffa6', - width: 3 * look.cometScale, - length: units(0.35 * look.cometScale), - spacing: units(1.2 * look.cometScale), - } - : { - color: '#d6e6ffa6', - width: 3 * look.cometScale, - length: 20 * look.cometScale, - spacing: 72 * look.cometScale, - }, - }, - controls: controls(), - marks: [...mapMarks(), ...gridMarks()], - }); - - /** The map's marks, with its states colored by the chosen field. */ - const mapMarks = (): Mark[] => { - if (!map) return []; - const by = state.statesBy; - const states = - by !== 'none' && joined - ? { by, peak: joined.peaks[by], colormap: STATE_COLORMAPS[by] } - : undefined; - return map.loaded.marks({ labels: state.mapLabels, ...(states ? { states } : {}) }); - }; - - /** A bus's radius from `base` CSS pixels, per the Buses panel. Emphasis enlarges hovered, - * selected, or nearby buses. The size mode keeps pixels, grows with the zoom, or sets the size in - * degrees on the map, kept within 1–16 CSS pixels: 0.06° per pixel of `base` matches the pixel - * size at the opening view. */ - const busRadius = (base: Expr | number): Size => { - const emphasis = { - none: 1, - hovered: when(hovered()).then(2.2).otherwise(1), - selected: when(selected()).then(2.2).otherwise(1), - // A lens: 2.6 times as large at the pointer, easing back to normal size 80 pixels away. - pointer: pointer().div(80).neg().add(1).max(0).mul(1.6).add(1), - }[look.emphasis]; - const radius = typeof emphasis === 'number' ? base : emphasis.mul(base); - if (look.size === 'zoom') return zoom().sub(3).mul(0.5).clamp(0.6, 3).mul(radius); - if (look.size === 'units') - return units(typeof radius === 'number' ? radius * 0.06 : radius.mul(0.06), { - atLeast: 1, - atMost: 16, - }); - return radius; - }; - - /** The grid's marks, per the panel settings. */ - const gridMarks = (): Mark[] => { - if (!grid?.buses) return []; - const marks: Mark[] = []; - const big = grid.buses > 20_000; - if (!grid.case) { - if (state.lines) - marks.push({ - edges: { - of: ['grid', 'Line'], - ends: ['from', 'to'], - width: 0.6 * look.width, - pick: false, - route: look.route, - arrows: look.arrows, - }, - }); - if (state.buses) - marks.push({ - vertices: { - of: ['grid', 'Bus'], - position: ['$lon', '$lat'], - color: field('load').colormap('viridis').domain(0, 1), - radius: busRadius(1.2), - marker: MARKERS[look.marker], - }, - }); - return marks; - } - const level = field('level').colormap({ kind: 'categorical', colors: LEVEL_COLORS }); - if (state.zones && grid.zoned) - marks.push({ - edges: { - of: ['grid', 'Zone'], - joins: ['Bus', 'zone'], - color: field('hue').colormap('tab20'), - width: 0.8, - pick: false, - }, - }); - if (state.lines) - marks.push({ - edges: { - of: ['grid', 'Line'], - ends: ['from', 'to'], - // Higher-voltage lines draw on top and stay visible as the camera zooms out. - priority: '$level', - ...(look.lineColor === 'level' ? { color: level } : {}), - width: field('level') - .scale((big ? 0.5 : 0.8) * look.width, (big ? 1.8 : 2.8) * look.width) - .domain(0, 5), - dash: '$transformer', - route: look.route, - arrows: look.arrows, - ...(state.flow ? { flow: field('level').mul(6).add(8) } : {}), - }, - }); - if (state.buses) { - const busColor = - state.color === 'level' - ? level - : state.color === 'plain' - ? undefined - : field(state.color) - .colormap(BUS_COLORS[state.color].colormap) - .domain(0, Math.max(1, grid.peaks[state.color])); - marks.push({ - vertices: { - of: ['grid', 'Bus'], - position: ['$lon', '$lat'], - ...(busColor ? { color: busColor } : {}), - // Where buses crowd, higher-voltage buses draw on top and stay visible from farther out. - priority: '$level', - radius: busRadius( - big ? field('level').scale(0.8, 2.6).domain(0, 5) : grid.buses > 5000 ? 2 : 3, - ), - marker: MARKERS[look.marker], - // The bus a search found is ringed. - ...(look.found - ? { - ring: when(field('name').is(look.found)).then('#ffd166').otherwise(null), - } - : {}), - // Labels never hide one another, so a crowded grid labels its buses only once zoomed in: - // below the chosen zoom a bus's text is missing, and it has no label. - ...(state.labels && grid.case.named - ? { - label: { - text: look.labelZoom - ? when(zoom().ge(look.labelZoom)).then(field('name')).otherwise(null) - : '$name', - size: look.labelSize, - font: FONTS[look.font], - ...(look.wrap ? { maxWidth: look.wrap } : {}), - }, - } - : {}), - }, - }); - } - const simulation = grid.case.simulation; - if (state.machines && grid.case.machines) - marks.push({ - vertices: { - of: ['grid', 'Machine'], - position: ['$lon', '$lat'], - marker: 'diamond', - color: simulation - ? field('speed').colormap('coolwarm').domain(-simulation.largest, simulation.largest) - : '#ebf0f7', - radius: field('power') - .scale(big ? 2 : 3, big ? 5 : 8) - .domain(0, Math.max(1, grid.peaks.power)), - }, - }); - if (simulation?.fault) - marks.push({ - vertices: { - of: ['grid', 'Fault'], - position: ['$lon', '$lat'], - color: '#ff3829', - radius: 9, - marker: 'rounded', - }, - }); - return marks; - }; - - const view = stage.view(scene(), { - label: 'A grid over the earth: drag to pan, Shift-drag to turn, hover or click to inspect', - }); - // Fits leave room for the readout at the top and the credit and legend at the bottom. - view.insets = [32, 16, 32, 16]; - const restyle = () => (view.scene = scene()); - /** Frames the grid regardless of the map's extent, or the whole map when there is no grid. */ - const frame = () => view.fit(grid?.bounds ?? 'all'); - const legend = stage.legend(); - const credit = stage.credit(); - const canvas = stage.regions.main.querySelector('canvas')!; - // A tag labels the first bus a search finds and follows it as the camera moves. - const tag = h('p', { class: 'callout', hidden: true }); - stage.regions.main.append(tag); - let tagged: [number, number, number] | undefined; - stage.frame(() => tagged && pin()); - - // ── Grid ── - const gridSection = stage.section('Grid'); - gridSection.choice( - 'Grid', - GRIDS.map(([label, id]) => [label, id] as const), - GRIDS[0][1] as GridId, - (id) => void loadGrid(id).catch(stage.fail), - ); - const colorBy = gridSection.choice( - 'Bus color', - (Object.keys(BUS_COLORS) as BusColor[]).map( - (key) => [option(BUS_COLORS[key].label), key] as const, - ), - state.color, - (next) => { - state.color = next; - restyle(); - showLegend(); - }, - ); - const levels = gridSection.series(LEVELS.map((label, i) => ({ label, color: LEVEL_COLORS[i]! }))); - const gridSummary = gridSection.note(); - - // ── Simulation ── - const simulationSection = stage.section('Simulation'); - simulationSection.speed([0.1, 0.25, 1], 0.25, (rate) => (playback.rate = rate)); - const simulationSummary = simulationSection.note(); - const playback = stage.playback({ - live: false, - rate: 0.25, - show(at) { - view.at = at; - inspect(); - }, - }); - - // ── Map ── - const mapSection = stage.section('Map'); - mapSection.choice( - 'Map', - [['None', undefined] as const, ...MAPS.map((m) => [m.label, m] as const)], - MAPS[0] as MapChoice | undefined, - (choice) => void loadMap(choice).catch(stage.fail), - ); - const statesBy = mapSection.choice( - 'State color', - [ - ['Nothing', 'none'] as const, - ...(Object.keys(STATE_FIELDS) as StateField[]).map( - (k) => [option(STATE_FIELDS[k].label), k] as const, - ), - ], - 'none' as 'none' | StateField, - (by) => { - state.statesBy = by; - join(); - }, - ); - const mapSummary = mapSection.note(); - - // ── View ── - const viewSection = stage.section('View'); - viewSection.choice( - 'Projection', - [ - ['Globe', 'globe'], - ['Map', 'mercator'], - ['Tilted map', 'tilt'], - ['Plane', 'plane'], - ] as const, - state.projection, - (projection) => { - const flat = state.projection === 'plane' || projection === 'plane'; - state.projection = projection; - restyle(); - // Positions have different units on the earth and on a plane, so switching reframes the grid. - if (flat) return frame(); - const camera = view.camera; - if ('center' in camera) view.fly({ ...camera, pitch: projection === 'tilt' ? 50 : 0 }); - }, - ); - viewSection.choice( - 'Daylight', - [ - ['Noon', 'noon'], - ['Now', 'now'], - ['Off', 'off'], - ] as const, - state.daylight, - (daylight) => { - state.daylight = daylight; - restyle(); - }, - ); - viewSection.toggle('Atmosphere', 'a', state.atmosphere, (on) => { - state.atmosphere = on; - restyle(); - }); - viewSection.toggle('Grid', 'g', state.graticule, (on) => { - state.graticule = on; - restyle(); - }); - viewSection.toggle('Orbit', 't', false, (on) => (view.orbit = on ? 4 : 0)); - viewSection.actions([{ label: 'Fit', key: 'f', run: frame }]); - // Notes showing where the pointer meets the ground, and the camera's zoom, pitch, and bearing. - const where = viewSection.note('Point at the map.'); - const camera = viewSection.note(); - const degrees = (value: number, [plus, minus]: string) => - `${Math.abs(value).toFixed(2)}° ${value >= 0 ? plus : minus}`; - canvas.addEventListener( - 'pointermove', - (event) => { - const ground = view.position([event.offsetX, event.offsetY]); - where.textContent = ground - ? state.projection === 'plane' - ? `Pointer at ${ground[0].toFixed(2)}, ${ground[1].toFixed(2)}` - : `Pointer at ${degrees(ground[1], 'NS')}, ${degrees(ground[0], 'EW')}` - : 'Point at the map.'; - }, - { signal: stage.signal }, - ); - view.on('camera', (at) => { - if ('center' in at) - camera.textContent = `Zoom ${at.zoom.toFixed(1)} · pitch ${at.pitch.toFixed(0)}° · bearing ${at.bearing.toFixed(0)}°`; - }); - - // ── Layers ── - const layers = stage.section('Layers'); - const layer = ( - label: string, - key: string | undefined, - name: 'buses' | 'lines' | 'machines' | 'zones' | 'labels' | 'mapLabels' | 'flow', - ) => - layers.toggle(label, key, state[name], (on) => { - state[name] = on; - restyle(); - }); - layer('Buses', 'b', 'buses'); - layer('Lines', 'l', 'lines'); - layer('Machines', 'm', 'machines'); - layer('Zones', 'z', 'zones'); - layer('Bus labels', 'n', 'labels'); - layer('Map labels', 'p', 'mapLabels'); - layer('Flow', 'c', 'flow'); - - // ── Buses ── - const buses = stage.section('Buses', false); - buses.choice( - 'Marker', - [ - ['Circle', 'circle'], - ['Square', 'square'], - ['Rounded', 'rounded'], - ['Diamond', 'diamond'], - ] as const, - look.marker, - (marker) => ((look.marker = marker), restyle()), - ); - buses.choice( - 'Size', - [ - ['Pixels', 'pixels'], - ['Grows with zoom', 'zoom'], - ['On the map', 'units'], - ] as const, - look.size, - (size) => ((look.size = size), restyle()), - ); - buses.choice( - 'Stand out', - [ - ['None', 'none'], - ['Hovered', 'hovered'], - ['Selected', 'selected'], - ['Near the pointer', 'pointer'], - ] as const, - look.emphasis, - (emphasis) => ((look.emphasis = emphasis), restyle()), - ); - buses.range( - 'Label size', - { min: 8, max: 16, step: 1, value: look.labelSize, format: (v) => `${v} px` }, - (size) => { - look.labelSize = size; - restyle(); - }, - ); - buses.choice( - 'Labels from zoom', - [ - ['Any zoom', 0], - ['5', 5], - ['7', 7], - ] as const, - look.labelZoom, - (from) => ((look.labelZoom = from), restyle()), - ); - buses.choice( - 'Font', - [ - ['Sans', 'sans'], - ['Serif', 'serif'], - ['Mono', 'mono'], - ] as const, - look.font, - (font) => ((look.font = font), restyle()), - ); - buses.choice( - 'Wrap labels', - [ - ['Never', 0], - ['At 60 px', 60], - ] as const, - look.wrap, - (wrap) => ((look.wrap = wrap), restyle()), - ); - - // ── Lines ── - const linesSection = stage.section('Lines', false); - linesSection.choice( - 'Route', - [ - ['Great circles', 'geodesic'], - ['Straight', 'straight'], - ] as const, - look.route, - (route) => ((look.route = route), restyle()), - ); - linesSection.choice( - 'Color', - [ - ['By voltage', 'level'], - ['Blend their buses', 'ends'], - ['Plain', 'theme'], - ] as const, - look.lineColor, - (next) => ((look.lineColor = next), restyle()), - ); - linesSection.range( - 'Width', - { min: 0.5, max: 3, step: 0.25, value: look.width, format: (v) => `${v.toFixed(2)}×` }, - (width) => { - look.width = width; - restyle(); - }, - ); - linesSection.range( - 'Parallel bow', - { min: 0, max: 0.4, step: 0.02, value: look.bow, format: (v) => `${Math.round(v * 100)}%` }, - (bow) => { - look.bow = bow; - restyle(); - }, - ); - linesSection.toggle('Arrows', undefined, look.arrows, (on) => ((look.arrows = on), restyle())); - - // ── Look ── - const lookSection = stage.section('Look', false); - const slider = ( - label: string, - name: 'glow' | 'dim' | 'halo' | 'grow' | 'night' | 'cometScale', - o: { min: number; max: number; step: number; format: (v: number) => string }, - ) => - lookSection.range(label, { ...o, value: look[name] }, (value) => { - look[name] = value; - restyle(); - }); - slider('Glow', 'glow', { min: 0, max: 12, step: 1, format: (v) => `${v} px` }); - slider('Dim the rest', 'dim', { - min: 0, - max: 1, - step: 0.05, - format: (v) => `${Math.round(v * 100)}%`, - }); - slider('Label halo', 'halo', { min: 0, max: 5, step: 0.5, format: (v) => `${v} px` }); - slider('Hover growth', 'grow', { - min: 1, - max: 2, - step: 0.05, - format: (v) => `${v.toFixed(2)}×`, - }); - slider('Night', 'night', { - min: 0, - max: 1, - step: 0.05, - format: (v) => `${Math.round(v * 100)}%`, - }); - lookSection.choice( - 'Comets', - [ - ['On screen', 'screen'], - ['On the map', 'map'], - ] as const, - look.comets, - (comets) => ((look.comets = comets), restyle()), - ); - slider('Comet size', 'cometScale', { - min: 0.5, - max: 2, - step: 0.25, - format: (v) => `${v.toFixed(2)}×`, - }); - - // ── Interaction ── - const reacts = stage.section('Interaction', false); - const answer = () => restyle(); - reacts.choice( - 'Controls', - [ - ['Explore', 'explore'], - ['Inspect', 'inspect'], - ['Still picture', 'none'], - ] as const, - interaction.preset, - (preset) => ((interaction.preset = preset), answer()), - ); - reacts.choice( - 'Zoom', - [ - ['Wheel', 'wheel'], - ['Ctrl + wheel', 'modifier'], - ['Off', 'off'], - ] as const, - interaction.zoom, - (next) => ((interaction.zoom = next), answer()), - ); - for (const [label, name] of [ - ['Pan', 'pan'], - ['Turn', 'turn'], - ['Keys', 'keyboard'], - ['Hover glow', 'hover'], - ['Cycle picks', 'cycle'], - ['Box select', 'selectBox'], - ] as const) - reacts.toggle( - label, - undefined, - interaction[name], - (on) => ((interaction[name] = on), answer()), - ); - reacts.range( - 'Pick reach', - { min: 0, max: 24, step: 2, value: interaction.pickRadius, format: (v) => `${v} px` }, - (radius) => { - interaction.pickRadius = radius; - answer(); - }, - ); - - // ── Inspector ── - const inspector = stage.section('Inspector'); - const search = inspector.text('Find', (text) => find(text.trim())); - search.placeholder = 'A bus by name'; - const described = inspector.note('Select an item.'); - const list = document.createElement('dl'); - list.className = 'fields'; - inspector.content.append(list); - let hoveredRow: Row | null = null; - const inspect = () => { - const row = hoveredRow ?? each(view.selection)[0]; - list.replaceChildren(); - if (!row) return void (described.textContent = 'Select an item.'); - const data = view.snapshot(row.data); - if (!data) return; - const at = view.at; - const value = (name: string) => data.get(row.table, row.id, name, { at }); - const table = data.describe().tables.find((t) => t.name === row.table); - const fields = (table?.fields ?? []).map((f) => f.name).filter((name) => !HIDDEN.test(name)); - const name = fields.find((f) => /^name$/i.test(f)); - described.textContent = `${row.table} · ${name ? format(value(name)) : `#${row.id}`}${hoveredRow ? '' : ' · selected'}`; - // The grid's statistics first, then the other fields, skipping fields without a value. - const first = fields.filter((f) => f in STATE_FIELDS); - let shown = 0; - for (const f of [...first, ...fields.filter((f) => f !== name && !(f in STATE_FIELDS))]) { - const v = value(f); - if (v === null || v === undefined || v === '' || Number.isNaN(v)) continue; - const dt = document.createElement('dt'); - const dd = document.createElement('dd'); - dt.textContent = f in STATE_FIELDS ? STATE_FIELDS[f as StateField].label : f; - dd.textContent = format(v) + (f in STATE_FIELDS ? STATE_FIELDS[f as StateField].unit : ''); - list.append(dt, dd); - if (++shown === 18) break; - } - data.destroy(); - }; - view.on('hovered', ({ hit }) => { - hoveredRow = hit?.row ?? null; - inspect(); - }); - view.on('selection', inspect); - - /** Moves the tag to its bus's screen position, or hides it when nothing is tagged or the bus is - * behind the camera. */ - function pin(): void { - const point = tagged && view.screen(tagged); - tag.hidden = !point; - if (point) tag.style.translate = `${point[0]}px ${point[1]}px`; - } - /** Selects the buses whose names contain `text`, up to 200 of the highest voltage, frames them, - * and tags and rings the first. */ - function find(text: string): void { - const data = grid?.data; - tagged = undefined; - look.found = ''; - if (!text || !data || !grid?.case?.named) { - pin(); - restyle(); - if (text) described.textContent = 'This grid has no named buses.'; - return; - } - const found = data.query({ - of: 'Bus', - filter: field('name').contains(text), - sort: [['$kv', 'descending']], - limit: 200, - select: { name: '$name', lon: '$lon', lat: '$lat' }, - }); - if (!found.ids.length) { - pin(); - restyle(); - described.textContent = `No bus is named like ${text}.`; - return; - } - const rows = [{ data: 'grid', table: 'Bus', ids: found.ids }]; - const first: Row = { data: 'grid', table: 'Bus', id: found.ids[0]! }; - const names = found.fields.name as string[]; - const [lon, lat] = [found.fields.lon as Float64Array, found.fields.lat as Float64Array]; - look.found = names[0]!; - restyle(); - view.selection = rows; - // A single match is brought into view at the current zoom. Several are framed together. - view.fit(found.ids.length === 1 ? { reveal: first } : { rows }); - tagged = [lon[0]!, lat[0]!, 0]; - tag.textContent = names[0]!; - pin(); - described.textContent = `${found.total.toLocaleString()} buses named like ${text}${found.total > found.ids.length ? `, the ${found.ids.length} of highest voltage selected` : ''}.`; - } - - stage.meter(view).note(() => { - const k = (n: number) => `${(n / 1000).toFixed(0)}k`; - return `${k(view.stats.drawn)} of ${k(view.stats.rows)} rows drawn`; - }); - - function showLegend(): void { - levels.hidden = !grid?.case || state.color !== 'level'; - const simulation = grid?.case?.simulation; - if (state.statesBy !== 'none' && joined) - return legend.ramp( - STATE_COLORMAPS[state.statesBy], - `${STATE_FIELDS[state.statesBy].label} 0 – ${fixed(joined.peaks[state.statesBy])}${STATE_FIELDS[state.statesBy].unit}`, - ); - if (simulation && state.machines) - return legend.ramp('coolwarm', `machine speed ±${fixed(simulation.largest)} mHz`); - if (!grid?.case) return legend.ramp('viridis', 'distance from the middle'); - if (state.color === 'level') return legend.ramp('grays', 'buses by voltage level'); - if (state.color === 'plain') return legend.ramp('grays', 'buses'); - const { colormap, label } = BUS_COLORS[state.color]; - legend.ramp(colormap, `${label} 0 – ${fixed(grid.peaks[state.color])} MW`); - } - - /** Rewrites the grid's statistics into the map's states, when the map has states and a state - * color is chosen. Then updates the view's sources, marks, and legend. */ - function join(): void { - if (joined) map?.loaded.states?.restore(joined.before); - joined?.before.destroy(); - joined = undefined; - const states = map?.loaded.states; - if (states && grid?.case && state.statesBy !== 'none') { - joined = joinStates(states, grid.case); - mapSummary.textContent = `${map!.loaded.summary} · the grid reaches ${joined.states} states`; - } - view.sources = [...(map?.loaded.sources ?? []), ...(grid?.data ? [grid.data] : [])]; - restyle(); - showLegend(); - } - - let gridLoad: AbortController | undefined; - async function loadGrid(id: GridId): Promise { - gridLoad?.abort(); - const load = (gridLoad = new AbortController()); - const label = GRIDS.find((g) => g[1] === id)![0]; - stage.detail(`${label}: loading…`); - const started = performance.now(); - let next: Grid; - if (id === 'none') { - next = { - id, - label, - buses: 0, - lines: 0, - peaks: { load: 0, generation: 0, power: 0 }, - summary: 'nothing to draw over the map', - zoned: false, - }; - } else if (id.startsWith('lattice')) { - const built = lattice(id); - next = { - id, - label, - data: built.data, - buses: built.buses, - lines: built.lines, - bounds: built.bounds, - peaks: { load: 1, generation: 0, power: 0 }, - summary: `built in ${(performance.now() - started).toFixed(0)} ms`, - zoned: false, - }; - } else { - const found = id.startsWith('continent') - ? continent(id) - : await loadCase(id, load.signal, (received, total) => - stage.detail(`${label}: downloading ${megabytes(received, total)}`), - ); - if (load.signal.aborted) return; - // Zone case files' buses by location. The continent has too many buses to zone here. - const zoned = found.named; - if (zoned) found.data.apply(zones(found)); - const peak = (values: Float32Array) => values.reduce((top, v) => Math.max(top, v), 0); - const [west, south, east, north] = extent(found.positions); - next = { - id, - label, - data: found.data, - buses: found.buses, - lines: found.lines, - case: found, - bounds: { bounds: { min: [west, south, 0], max: [east, north, 0] } }, - peaks: { - load: peak(found.load), - generation: peak(found.generation), - power: peak(found.machinePower), - }, - summary: found.bytes - ? `${(found.bytes / 1e6).toFixed(2)} MB in ${(performance.now() - started).toFixed(0)} ms` - : `built in ${(performance.now() - started).toFixed(0)} ms`, - zoned, - }; - } - grid = next; - view.selection = []; - hoveredRow = null; - search.value = ''; - tagged = undefined; - look.found = ''; - pin(); - const simulation = next.case?.simulation; - simulationSection.element.hidden = !simulation; - playback.hidden = !simulation; - colorBy.disabled = !next.case; - search.disabled = !next.case?.named; - playback.pause(); - playback.record(simulation ? [0, simulation.duration] : [0, 0]); - view.at = 0; - simulationSummary.textContent = simulation - ? `${simulation.frames} frames over ${simulation.duration.toFixed(0)} s` + - (simulation.fault - ? ` · fault at ${simulation.fault.bus}, ${simulation.fault.on.toFixed(2)}–${simulation.fault.off.toFixed(2)} s` - : '') - : ''; - gridSummary.textContent = - `${next.buses.toLocaleString()} buses · ${next.lines.toLocaleString()} lines` + - (next.case ? ` · ${next.case.machines.toLocaleString()} machines` : '') + - ` · ${next.summary}`; - statesBy.disabled = !next.case || !map?.loaded.states; - join(); - frame(); - stage.detail(`${label} · ${next.summary}`); - inspect(); - } - - let mapLoad: AbortController | undefined; - async function loadMap(choice: MapChoice | undefined): Promise { - mapLoad?.abort(); - const load = (mapLoad = new AbortController()); - if (joined) map?.loaded.states?.restore(joined.before); - joined?.before.destroy(); - joined = undefined; - map = undefined; - credit.textContent = ''; - mapSummary.textContent = choice ? 'loading…' : ''; - join(); - if (!choice) return; - const loaded = await choice.load(load.signal, (status) => { - if (!load.signal.aborted) mapSummary.textContent = status; - }); - if (load.signal.aborted) return; - map = { choice, loaded }; - credit.textContent = choice.attribution; - mapSummary.textContent = loaded.summary; - statesBy.disabled = !grid?.case || !loaded.states; - join(); - } - - showLegend(); - await Promise.all([loadGrid(GRIDS[0][1]), loadMap(MAPS[0])]); -} - -/** Formats a dataset value for the inspector. */ -function format(value: unknown): string { - if (value === null || value === undefined) return '–'; - if (typeof value === 'number') - return Number.isInteger(value) ? value.toLocaleString() : value.toPrecision(4); - if (typeof value === 'object' && 'id' in value) return String((value as Row).id); - return typeof value === 'object' ? JSON.stringify(value) : String(value as string | boolean); -} diff --git a/npm/examples/src/examples/networks/lattice.ts b/npm/examples/src/examples/networks/lattice.ts deleted file mode 100644 index 2bf4c174..00000000 --- a/npm/examples/src/examples/networks/lattice.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Square lattices of buses over most of the globe, used as stress cases larger than any synthetic - * case. Each bus connects to its east and north neighbours, and a few lines span the whole box, - * which the geodesic route draws as great circles. Built in time linear in the bus count. - */ -import { Data, Patch, reference, type Fit } from 'latkit'; - -interface Spec { - readonly cols: number; - readonly rows: number; - readonly lon: readonly [number, number]; - readonly lat: readonly [number, number]; - /** Lines across the box, as [column, row] to [column, row]. */ - readonly spans: readonly (readonly [number, number, number, number])[]; -} - -export const LATTICES: Readonly> = { - 'lattice-100k': { - cols: 400, - rows: 250, - lon: [-160, 160], - lat: [-80, 80], - spans: [ - [0, 0, 399, 249], - [399, 0, 0, 249], - [0, 125, 399, 125], - [200, 0, 200, 249], - ], - }, - 'lattice-1m': { - cols: 1000, - rows: 1000, - lon: [-170, 170], - lat: [-75, 75], - spans: [ - [0, 0, 999, 999], - [999, 0, 0, 999], - ], - }, -}; - -/** Builds lattice `id` as dataset `grid`, with its counts and bounding box. Each bus's `load` is - * its distance from the box's center, scaled to 0–1, for coloring. Throws for an unknown id. */ -export function lattice(id: string): { data: Data; buses: number; lines: number; bounds: Fit } { - const spec = LATTICES[id]; - if (!spec) throw new Error(`No lattice ${id}`); - const { cols, spans } = spec; - const count = cols * spec.rows; - const [west, east] = spec.lon; - const [south, north] = spec.lat; - const lon = new Float64Array(count); - const lat = new Float64Array(count); - const load = new Float32Array(count); - const at = (c: number, r: number) => r * cols + c; - let farthest = 1e-6; - for (let r = 0; r < spec.rows; r++) - for (let c = 0; c < cols; c++) { - const v = at(c, r); - lon[v] = west + ((east - west) * c) / (cols - 1); - lat[v] = south + ((north - south) * r) / (spec.rows - 1); - load[v] = Math.hypot(lon[v]! - (west + east) / 2, lat[v]! - (south + north) / 2); - farthest = Math.max(farthest, load[v]!); - } - for (let v = 0; v < count; v++) load[v]! /= farthest; - const lines = spec.rows * (cols - 1) + cols * (spec.rows - 1) + spans.length; - const from = new Uint32Array(lines); - const to = new Uint32Array(lines); - let e = 0; - for (let r = 0; r < spec.rows; r++) - for (let c = 0; c + 1 < cols; c++) { - from[e] = at(c, r); - to[e++] = at(c + 1, r); - } - for (let r = 0; r + 1 < spec.rows; r++) - for (let c = 0; c < cols; c++) { - from[e] = at(c, r); - to[e++] = at(c, r + 1); - } - for (const [c0, r0, c1, r1] of spans) { - from[e] = at(c0, r0); - to[e++] = at(c1, r1); - } - const data = new Data('grid').apply( - new Patch() - .set('Bus', { count }, { lon, lat, load }) - .set('Line', { count: lines }, { from: reference('Bus', from), to: reference('Bus', to) }), - ); - return { - data, - buses: count, - lines, - bounds: { bounds: { min: [west, south, 0], max: [east, north, 0] } }, - }; -} diff --git a/npm/examples/src/examples/networks/maps.ts b/npm/examples/src/examples/networks/maps.ts deleted file mode 100644 index 94359075..00000000 --- a/npm/examples/src/examples/networks/maps.ts +++ /dev/null @@ -1,168 +0,0 @@ -/** - * The maps drawn beneath the grid: - * - Natural Earth's coasts and borders as lines, shipped with the page; - * - Natural Earth's states and provinces as areas colored by the grid's statistics, downloaded from - * Natural Earth's repository; - * - OpenStreetMap's planet as OpenFreeMap's vector tiles. - * - * GeoJSON, plain or gzip-compressed, is read in a worker. Tiles are fetched as the camera needs them. - */ -import { Data, Tiles, field, when, zoom, type ColormapName, type Mark, type Source } from 'latkit'; -import { download, megabytes } from './cases.js'; - -/** The page's colors, as CSS. */ -export const LOOK = { - background: '#05070a', - ground: '#0e131b', - fill: '#1a212b', - line: '#6b80948c', - coast: '#8ca3b8d9', - landcover: '#1c2924', - park: '#1a3021', - water: '#0d1a2b', - waterway: '#2e5485e6', - road: '#5c616bb3', - place: '#c3ccd8', -} as const; - -/** Map style options: whether areas are labeled, and which field colors the states. */ -export interface Styling { - readonly labels: boolean; - readonly states?: { readonly by: string; readonly peak: number; readonly colormap: ColormapName }; -} - -/** A loaded map: a summary for the status line, its marks (drawn under the grid), its sources, and - * its states dataset if it has one. */ -export interface Loaded { - readonly summary: string; - marks(styling: Styling): Mark[]; - /** The datasets its marks draw. */ - readonly sources: readonly Source[]; - /** The states dataset, colored by the grid's statistics. Its rows are in table `polygons`. */ - readonly states?: Data; -} - -export interface MapChoice { - readonly id: string; - readonly label: string; - readonly attribution: string; - /** Loads the map, reporting status text to `progress`. */ - load(signal: AbortSignal, progress: (status: string) => void): Promise; -} - -const NE = 'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/'; -const NATURAL_EARTH = 'Natural Earth · public domain'; -const seconds = (since: number) => `${((performance.now() - since) / 1000).toFixed(1)} s`; - -/** Natural Earth's coastlines, country borders, and province borders at 1:50M, drawn as lines. */ -const borders: MapChoice = { - id: 'borders', - label: 'Coasts and borders', - attribution: NATURAL_EARTH, - async load(signal) { - const started = performance.now(); - const files = ['coastline', 'countries', 'provinces'] as const; - const sets = await Promise.all( - files.map((name) => - Data.read(`${import.meta.env.BASE_URL}maps/${name}.geojson.gz`, { name, signal }), - ), - ); - return { - sources: sets, - summary: `coasts and borders in ${seconds(started)}`, - marks: () => [ - { - lines: { of: ['provinces', 'lines'], color: LOOK.line, width: 0.6, dash: true }, - }, - { lines: { of: ['countries', 'lines'], color: LOOK.line, width: 1 } }, - { lines: { of: ['coastline', 'lines'], color: LOOK.coast, width: 1 } }, - ], - }; - }, -}; - -/** Natural Earth's states and provinces at `scale`, as areas colored by the grid's statistics. */ -function states(scale: '50m' | '10m', label: string): MapChoice { - return { - id: `states-${scale}`, - label, - attribution: NATURAL_EARTH, - async load(signal, progress) { - const started = performance.now(); - const bytes = await download( - `${NE}ne_${scale}_admin_1_states_provinces.geojson`, - signal, - (got, total) => progress(`downloading ${megabytes(got, total)}`), - ); - const fetched = performance.now(); - const data = await Data.read(bytes, { name: 'states', signal }); - return { - sources: [data], - states: data, - summary: `${(bytes.length / 1e6).toFixed(1)} MB in ${seconds(started)}, read in ${seconds(fetched)}`, - marks: ({ labels, states }) => [ - { - // Each state carries the grid's statistics, which hovering it shows in the inspector. - areas: { - of: ['states', 'polygons'], - color: states - ? field(states.by).colormap(states.colormap).domain(0, Math.max(states.peak, 1e-6)) - : LOOK.fill, - ...(labels ? { label: { text: '$name', size: 10, color: '#8e99aa' } } : {}), - pick: true, - }, - }, - { lines: { of: ['states', 'polygons'], color: LOOK.line, width: 0.7 } }, - ], - }; - }, - }; -} - -/** OpenStreetMap's planet as OpenFreeMap's vector tiles. */ -const planet: MapChoice = { - id: 'planet', - label: 'Planet, vector tiles', - attribution: 'OpenFreeMap · © OpenMapTiles · © OpenStreetMap contributors', - load() { - return Promise.resolve({ - sources: [new Tiles('base', 'https://tiles.openfreemap.org/planet')], - summary: 'tiles fetched as the camera needs them', - marks: ({ labels }) => [ - { areas: { of: ['base', 'landcover'], color: LOOK.landcover } }, - { areas: { of: ['base', 'park'], color: LOOK.park } }, - { areas: { of: ['base', 'water'], color: LOOK.water } }, - // Comets run along rivers in the direction of their OpenStreetMap ways: downstream. - { lines: { of: ['base', 'waterway'], color: LOOK.waterway, width: 0.8, flow: 10 } }, - { lines: { of: ['base', 'transportation'], color: LOOK.road, width: 0.7 } }, - { lines: { of: ['base', 'boundary'], color: LOOK.line, width: 1, dash: true } }, - // Place labels by rank, where 1 is the most important: a place is named once the zoom - // passes its rank, and has no label before. A tile holds only the points inside it, so no - // label repeats. - ...(labels - ? [ - { - vertices: { - of: ['base', 'place'], - color: LOOK.place, - radius: 1.5, - label: { - text: when(field('rank').lt(zoom())).then(field('name')).otherwise(null), - size: 11, - color: LOOK.place, - }, - }, - } satisfies Mark, - ] - : []), - ], - }); - }, -}; - -export const MAPS: readonly MapChoice[] = [ - borders, - states('50m', 'States and provinces, 1:50M'), - states('10m', 'States and provinces, 1:10M (40 MB)'), - planet, -]; diff --git a/npm/examples/src/examples/networks/states.ts b/npm/examples/src/examples/networks/states.ts deleted file mode 100644 index 4a245fad..00000000 --- a/npm/examples/src/examples/networks/states.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Per-state statistics of a grid, written into the states dataset as fields the state areas can be - * colored by. Each bus adds to the bus count, load, and generation of the state it lies in. For a - * simulated grid, each state also gets its machines' largest speed swing, frame by frame. - */ -import { Patch, type Data, type Id, type Snapshot } from 'latkit'; -import { extent, type Case } from './cases.js'; - -/** The per-state fields, with their labels and units. */ -export const STATE_FIELDS = { - buses: { label: 'buses', unit: '' }, - load: { label: 'load', unit: ' MW' }, - generation: { label: 'generation', unit: ' MW' }, - swing: { label: 'largest speed swing', unit: ' mHz' }, -} as const; -export type StateField = keyof typeof STATE_FIELDS; - -export interface Joined { - /** A snapshot of the states from before the grid's statistics were written, for restoring. */ - readonly before: Snapshot; - /** The number of states that contain a bus of the grid. */ - readonly states: number; - /** Each field's largest value in any state. */ - readonly peaks: Readonly>; -} - -type Ring = [number, number][]; - -interface State { - readonly row: number; - /** West, south, east, north. */ - readonly bounds: readonly [number, number, number, number]; - readonly rings: readonly Ring[]; -} - -/** Whether a point lies inside a state, by the even-odd rule: a ray from the point crosses the - * state's rings, holes included, an odd number of times. */ -function inside(state: State, x: number, y: number): boolean { - const [w, s, e, n] = state.bounds; - if (x < w || x > e || y < s || y > n) return false; - let crossings = 0; - for (const ring of state.rings) - for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { - const [xi, yi] = ring[i]!; - const [xj, yj] = ring[j]!; - if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) crossings++; - } - return crossings % 2 === 1; -} - -/** Writes the grid's statistics into `states` for every state the grid reaches. The result keeps a - * snapshot of the states from before, for restoring them. */ -export function joinStates(states: Data, grid: Case): Joined { - const { positions, simulation } = grid; - const box = extent(positions); - const ids: Id[] = states.query({ of: 'polygons', select: {} }).ids; - const found: State[] = []; - ids.forEach((id, row) => { - const polygons = (states.get('polygons', id, 'geometry') ?? []) as Ring[][]; - const rings = polygons.flat(); - const bounds: [number, number, number, number] = [Infinity, Infinity, -Infinity, -Infinity]; - for (const ring of rings) - for (const [x, y] of ring) { - bounds[0] = Math.min(bounds[0], x); - bounds[1] = Math.min(bounds[1], y); - bounds[2] = Math.max(bounds[2], x); - bounds[3] = Math.max(bounds[3], y); - } - const meets = - bounds[0] <= box[2] && bounds[2] >= box[0] && bounds[1] <= box[3] && bounds[3] >= box[1]; - if (meets) found.push({ row, bounds, rings }); - }); - // Index the states by one-degree cell, so each bus tests only the states near it. - const cells = new Map(); - const cell = (x: number, y: number) => Math.floor(x + 180) * 180 + Math.floor(y + 90); - for (const state of found) { - const [w, s, e, n] = state.bounds; - for (let x = Math.floor(Math.max(w, box[0])); x <= Math.min(e, box[2]); x++) - for (let y = Math.floor(Math.max(s, box[1])); y <= Math.min(n, box[3]); y++) { - const key = cell(x, y); - let list = cells.get(key); - if (!list) cells.set(key, (list = [])); - list.push(state); - } - } - const rows = ids.length; - const of = new Int32Array(grid.buses).fill(-1); - const buses = new Float32Array(rows).fill(Number.NaN); - const load = new Float32Array(rows).fill(Number.NaN); - const generation = new Float32Array(rows).fill(Number.NaN); - const reached = new Set(); - for (let b = 0; b < grid.buses; b++) { - const x = positions[b * 2]!; - const y = positions[b * 2 + 1]!; - const state = cells.get(cell(x, y))?.find((candidate) => inside(candidate, x, y)); - if (!state) continue; - const k = state.row; - of[b] = k; - if (!reached.has(k)) { - reached.add(k); - buses[k] = load[k] = generation[k] = 0; - } - buses[k]! += 1; - load[k]! += grid.load[b]!; - generation[k]! += grid.generation[b]!; - } - const peak = (values: Float32Array) => values.reduce((top, v) => (v > top ? v : top), 0); - const patch = new Patch().set('polygons', ids as number[], { buses, load, generation }); - const peaks = { buses: peak(buses), load: peak(load), generation: peak(generation), swing: 0 }; - if (simulation) { - // For each frame, the largest absolute speed deviation among each state's machines. - const g = grid.machines; - const swing = new Float32Array(rows); - for (let f = 0; f < simulation.frames; f++) { - swing.fill(Number.NaN); - for (const k of reached) swing[k] = 0; - for (let m = 0; m < g; m++) { - const k = of[grid.machineBus[m]!]!; - if (k < 0) continue; - swing[k] = Math.max(swing[k]!, Math.abs(simulation.speeds[f * g + m]!)); - } - peaks.swing = Math.max(peaks.swing, peak(swing)); - patch.append('polygons', f * simulation.dt, { swing }); - } - } - const before = states.snapshot(); - states.apply(patch); - return { before, states: reached.size, peaks }; -} diff --git a/npm/examples/src/examples/power-flow/grid.test.ts b/npm/examples/src/examples/power-flow/grid.test.ts deleted file mode 100644 index ca068c9e..00000000 --- a/npm/examples/src/examples/power-flow/grid.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import { Grid } from './grid.js'; -import texas from './texas.json'; - -/** The net MW each substation sends out along its branches, from the solved flows. */ -function sent(grid: Grid): Float64Array { - const out = new Float64Array(grid.n); - texas.branches.forEach((branch, j) => { - out[branch.from]! += grid.flow[j]!; - out[branch.to]! -= grid.flow[j]!; - }); - return out; -} - -/** The largest difference between a substation's injection and the net flow out on its branches. */ -function imbalance(grid: Grid): number { - const out = sent(grid); - return Math.max(...Array.from(out, (mw, i) => Math.abs(mw - grid.injection[i]!))); -} - -describe('the DC power flow', () => { - test('generation matches served load and each bus sends out its injection', () => { - const grid = new Grid(); - const total = grid.injection.reduce((sum, mw) => sum + mw, 0); - expect(Math.abs(total)).toBeLessThan(1e-6); - expect(imbalance(grid)).toBeLessThan(1e-6); - expect(grid.dark).toBe(0); - }); - - test('a tripped branch carries nothing, and the rest take its flow', () => { - const grid = new Grid(); - const before = Float64Array.from(grid.flow); - grid.tripWorst(); - const tripped = grid.out.findIndex((out) => out === 1); - expect(tripped).toBeGreaterThanOrEqual(0); - expect(grid.trips).toBe(1); - expect(grid.flow[tripped]).toBe(0); - expect(imbalance(grid)).toBeLessThan(1e-6); - expect(grid.flow.some((mw, j) => j !== tripped && Math.abs(mw - before[j]!) > 1e-3)).toBe(true); - }); - - test('restoring recloses every branch and solves as before', () => { - const grid = new Grid(); - const before = Float64Array.from(grid.flow); - grid.tripWorst(); - grid.restore(); - expect(grid.out.every((out) => out === 0)).toBe(true); - expect(grid.trips).toBe(0); - grid.flow.forEach((mw, j) => expect(mw).toBeCloseTo(before[j]!, 6)); - }); - - test('more demand loads branches more', () => { - const grid = new Grid(); - const loaded = () => grid.loading.reduce((sum, l) => sum + l, 0); - const light = loaded(); - grid.demand = 1.1; - grid.step(0); - expect(loaded()).toBeGreaterThan(light); - }); -}); diff --git a/npm/examples/src/examples/power-flow/grid.ts b/npm/examples/src/examples/power-flow/grid.ts deleted file mode 100644 index 5293ad38..00000000 --- a/npm/examples/src/examples/power-flow/grid.ts +++ /dev/null @@ -1,187 +0,0 @@ -/** - * A model of the grid a control room watches. Plants dispatch to meet demand, a DC power flow sets - * every branch's flow, protection trips overloaded branches, and each island balances on its own. - */ -import { BASE, angles, flows, islands, type Network } from '../../../../../docs/gallery/flow.js'; -import texas from './texas.json'; - -/** The kinds of substation, in color index order. `fuels` lists the categories of each kind. */ -export const KINDS = [ - { label: 'nuclear', color: '#a181ef', fuels: ['Nuclear Steam'] }, - { - label: 'thermal', - color: '#c08a52', - fuels: ['Gas Turbine', 'Gas Combined Cycle', 'Coal-fired Steam'], - }, - { label: 'wind', color: '#00d1da', fuels: ['Wind'] }, - { label: 'solar', color: '#ffca29', fuels: ['Solar PV'] }, - { label: 'load', color: '#8a93a0', fuels: ['Load'] }, -] as const; -/** Protection trips a branch loaded over `TRIP_AT` times its rating for `TRIP_AFTER` ticks in a - * row. */ -export const TRIP_AT = 1.15; -export const TRIP_AFTER = 3; -/** Seconds a tripped branch stays out before it recloses. */ -export const OUT_FOR = 14; -/** Per unit reactance per kilometre of line. */ -export const REACTANCE = 0.0004; - -type Substation = (typeof texas.substations)[number]; - -/** A substation's kind, as an index into `KINDS`. */ -const kindOf = (s: Substation) => - KINDS.findIndex((k) => (k.fuels as readonly string[]).includes(s.category)); - -/** - * The grid's state and physics. Plants dispatch to meet demand, and a DC power flow sets every - * branch's flow from the injections. Protection trips branches that stay over their rating. Each - * island balances on its own plants, and its loads go dark if it has none. - */ -export class Grid { - readonly n = texas.substations.length; - readonly m = texas.branches.length; - /** Each plant's capacity, or each load's size, in MW. */ - readonly capacity = Float64Array.from(texas.substations, (s) => - s.units.reduce((a, b) => a + b, 0), - ); - readonly load = Uint8Array.from(texas.substations, (s) => (s.category === 'Load' ? 1 : 0)); - readonly kind = Float32Array.from(texas.substations, kindOf); - readonly rating = Float64Array.from(texas.branches, (b) => b.pmax); - /** Each plant's output as a share of its capacity, or each load's share of demand served. - * `injection` holds the MW each substation injects. */ - readonly fill = new Float32Array(this.n); - readonly injection = new Float64Array(this.n); - /** Each branch's flow in MW from its `from` end to its `to` end, its loading as a share of its - * rating, and whether it is out. */ - readonly flow = new Float64Array(this.m); - readonly loading = new Float64Array(this.m); - readonly out = new Uint8Array(this.m); - /** Consecutive ticks each branch has been over its trip setting, and when an out branch - * recloses. */ - readonly #over = new Uint8Array(this.m); - readonly #reclose = new Float64Array(this.m); - /** For each plant, the time until which it is out of service. */ - readonly #plantOut = new Float64Array(this.n); - /** The branches as the power flow sees them, with the out of service ones in `out`. */ - readonly #net: Network; - /** Demand as a share of the loads' size, and the simulated time in seconds. */ - demand = 0.78; - t = 0; - /** Branches tripped since the last restore, and loads dark now. */ - trips = 0; - dark = 0; - - constructor() { - const s = texas.substations; - this.#net = { - buses: this.n, - from: Uint32Array.from(texas.branches, (b) => b.from), - to: Uint32Array.from(texas.branches, (b) => b.to), - reactance: Float64Array.from(texas.branches, (b) => { - const [a, c] = [s[b.from]!, s[b.to]!]; - const km = - Math.hypot((a.lon - c.lon) * Math.cos((a.lat * Math.PI) / 180), a.lat - c.lat) * 111; - return Math.max(0.002, REACTANCE * km); - }), - out: this.out, - }; - this.step(0); - } - - /** Trips the most heavily loaded branch in service, as a fault would. */ - tripWorst(): void { - let worst = -1; - for (let j = 0; j < this.m; j++) - if (!this.out[j] && (worst < 0 || this.loading[j]! > this.loading[worst]!)) worst = j; - if (worst >= 0) this.#trip(worst); - this.step(0); - } - - /** Recloses every branch and plant. */ - restore(): void { - this.out.fill(0); - this.#over.fill(0); - this.#plantOut.fill(0); - this.trips = 0; - this.step(0); - } - - #trip(j: number): void { - this.out[j] = 1; - this.#reclose[j] = this.t + OUT_FOR; - this.trips++; - } - - /** Advances `dt` seconds, then dispatches, solves the flows, and runs protection. A `dt` of 0 - * solves again without tripping anything. */ - step(dt: number): void { - this.t += dt; - const { n, m, t } = this; - for (let j = 0; j < m; j++) - if (this.out[j] && t >= this.#reclose[j]! && this.#reclose[j]! > 0) this.out[j] = 0; - // Each load's demand, which swings through the day, and each plant's available output. - const want = new Float64Array(n); - const can = new Float64Array(n); - for (let i = 0; i < n; i++) { - const wave = 0.08 * Math.sin(t * 0.07 + i * 1.3) + 0.04 * Math.sin(t * 0.31 + i * 0.7); - if (this.load[i]) - want[i] = this.capacity[i]! * Math.min(1.4, Math.max(0, this.demand + wave)); - else if (t >= this.#plantOut[i]!) { - // Wind and solar output vary with the weather. Other plants follow dispatch. - const k = this.kind[i]!; - const weather = - k === 2 - ? 0.55 + 0.4 * Math.sin(t * 0.11 + i) - : k === 3 - ? 0.8 + 0.15 * Math.sin(t * 0.05 + i) - : 1; - can[i] = this.capacity[i]! * weather; - } - } - // Split the grid into islands joined by branches in service, and balance each on its own - // plants. - const island = islands(this.#net); - const groups = new Map(); - for (let i = 0; i < n; i++) { - const members = groups.get(island[i]!); - if (members) members.push(i); - else groups.set(island[i]!, [i]); - } - this.dark = 0; - for (const members of groups.values()) { - const asked = members.reduce((sum, i) => sum + want[i]!, 0); - const able = members.reduce((sum, i) => sum + can[i]!, 0); - const served = able > 0 ? Math.min(1, able / Math.max(asked, 1e-9)) : 0; - const dispatch = asked > 0 ? Math.min(1, (asked * served) / Math.max(able, 1e-9)) : 0; - for (const i of members) { - if (this.load[i]) { - this.fill[i] = served; - this.injection[i] = -want[i]! * served; - if (served < 0.05 && want[i]! > 0) this.dark++; - } else { - this.fill[i] = this.capacity[i]! > 0 ? (can[i]! * dispatch) / this.capacity[i]! : 0; - this.injection[i] = can[i]! * dispatch; - } - } - } - // The flows follow from the bus angles, which the injections in per unit set. - const perUnit = Float64Array.from(this.injection, (mw) => mw / BASE); - this.flow.set(flows(this.#net, angles(this.#net, perUnit))); - // Protection trips a branch that stays over its trip setting. Now and then a plant trips too. - for (let j = 0; j < m; j++) { - this.loading[j] = this.out[j] ? 0 : Math.abs(this.flow[j]!) / this.rating[j]!; - this.#over[j] = this.loading[j]! > TRIP_AT ? this.#over[j]! + 1 : 0; - if (dt > 0 && this.#over[j]! >= TRIP_AFTER) this.#trip(j); - } - if (dt > 0 && Math.random() < 0.004) { - const plants = [...Array(n).keys()].filter((i) => !this.load[i] && t >= this.#plantOut[i]!); - const plant = plants[Math.floor(Math.random() * plants.length)]; - if (plant !== undefined) this.#plantOut[plant] = t + 20; - } - } - - /** Whether plant `i` is out of service. */ - tripped(i: number): boolean { - return !this.load[i] && this.t < this.#plantOut[i]!; - } -} diff --git a/npm/examples/src/examples/power-flow/index.ts b/npm/examples/src/examples/power-flow/index.ts deleted file mode 100644 index f1372c20..00000000 --- a/npm/examples/src/examples/power-flow/index.ts +++ /dev/null @@ -1,431 +0,0 @@ -/** - * Grid operations: a state's transmission grid as a control room watches it. - * - * Each substation is a gauge of its output sized by its capacity (round for a plant, square for a - * load), a pie, or an icon of its kind. A branch's width shows its rating and its color shows its - * loading. Comets travel along each branch's flow, parallel branches bow apart, and branches can - * bend along their corridors. - * - * A DC power flow is solved every tick. Push demand up or trip a line, and overloaded branches trip - * in turn, flows shift onto the remaining branches, and loads cut off from every plant go dark. - * Each tick eases into place, and tripped equipment pulses until it is restored. - */ -import { - Data, - Patch, - elapsed, - field, - icon, - reference, - units, - when, - type Hit, - type Marker, - type Motion, - type RgbaImage, - type Scene, -} from 'latkit'; -import { each } from '../../ui/rows.js'; -import type { Stage } from '../../ui/stage.js'; -import { theme } from '../../ui/theme.js'; -import { Grid, KINDS } from './grid.js'; -import texas from './texas.json'; - -/** The control room's dark palette: a muted network with bright alarms and flows. */ -const LOOK = { - background: '#07090c', - ground: '#10161d', - border: '#3b4552', - text: '#dfe6ee', - hover: '#53a3f2', - comet: '#e8fff4', - warning: '#ffc000', - critical: '#ff6200', - tripped: '#ff1915', -} as const; - -/** A branch's color by its loading in % of its rating: blues up to 80%, then amber at 90%, turning - * red around the trip setting. */ -const LOADING = field('loading') - .colormap({ - kind: 'sequential', - colors: [ - '#2b3e54', - '#2e4760', - '#31506c', - '#345a78', - '#376484', - '#3b6e8f', - '#40799a', - '#4a86a3', - '#6a9eaa', - LOOK.warning, - '#ff8f00', - LOOK.critical, - LOOK.tripped, - ], - }) - .domain(0, 120); - -/** Seconds between ticks of the simulation. */ -const TICK = 0.5; - -/** Draws an icon per substation kind: a disc in the kind's color with its initial. The last icon, - * a red disc marked ×, is for substations that are tripped or dark. */ -function icons(): RgbaImage[] { - const size = 48; - return [ - ...KINDS.map((k) => [k.color, k.label[0]!.toUpperCase()] as const), - [LOOK.tripped, '×'] as const, - ].map(([color, letter]) => { - const canvas = new OffscreenCanvas(size, size); - const context = canvas.getContext('2d')!; - context.fillStyle = color; - context.beginPath(); - context.arc(size / 2, size / 2, size / 2 - 1, 0, 2 * Math.PI); - context.fill(); - context.fillStyle = '#07090c'; - context.font = `bold ${size * 0.55}px system-ui, sans-serif`; - context.textAlign = 'center'; - context.textBaseline = 'middle'; - context.fillText(letter, size / 2, size / 2 + 2); - return context.getImageData(0, 0, size, size); - }); -} - -/** Each branch's corridor bend: one point beside its midpoint, a tenth of its length out, on - * alternating sides, like a corridor routed around obstacles between its substations. */ -function corridors(): number[][][] { - const s = texas.substations; - return texas.branches.map((b, j) => { - const [a, c] = [s[b.from]!, s[b.to]!]; - const [dx, dy] = [c.lon - a.lon, c.lat - a.lat]; - const side = (j % 2 ? 1 : -1) * 0.1; - return [[(a.lon + c.lon) / 2 - dy * side, (a.lat + c.lat) / 2 + dx * side]]; - }); -} -/** A patch of the `grid` dataset's changing fields, from the grid's current state. */ -function patchOf(grid: Grid): Patch { - const kind = Float32Array.from(grid.kind, (k, i) => - grid.tripped(i) || (grid.load[i] && grid.fill[i]! < 0.05) ? KINDS.length : k, - ); - return new Patch() - .set( - 'Substation', - { count: grid.n }, - { - fill: grid.fill, - kind, - alarm: Uint8Array.from(kind, (k) => (k === KINDS.length ? 1 : 0)), - mw: Float64Array.from(grid.injection, Math.abs), - }, - ) - .set( - 'Branch', - { count: grid.m }, - { - out: grid.out, - // Overloaded or out. - alarm: Uint8Array.from(grid.loading, (l, j) => (grid.out[j] || l > 1 ? 1 : 0)), - flow: Float64Array.from(grid.flow, (f, j) => - grid.out[j] ? 0 : Math.max(-1, Math.min(1, f / grid.rating[j]!)), - ), - mw: Float64Array.from(grid.flow, Math.abs), - loading: Float64Array.from(grid.loading, (l) => l * 100), - }, - ); -} - -export default async function page(stage: Stage): Promise { - // Labels use the Jura font, so load it first. - const jura = new FontFace( - 'Jura', - `url(${import.meta.env.BASE_URL}fonts/jura-latin-400-normal.woff2)`, - ); - document.fonts.add(await jura.load()); - - const grid = new Grid(); - const s = texas.substations; - const data = new Data('grid').apply( - new Patch() - .set( - 'Substation', - { count: grid.n }, - { - name: s.map((x) => x.name), - lon: Float64Array.from(s, (x) => x.lon), - lat: Float64Array.from(s, (x) => x.lat), - load: grid.load, - capacity: grid.capacity, - }, - ) - .set( - 'Branch', - { count: grid.m }, - { - from: reference( - 'Substation', - Uint32Array.from(texas.branches, (b) => b.from), - ), - to: reference( - 'Substation', - Uint32Array.from(texas.branches, (b) => b.to), - ), - rating: grid.rating, - corridor: corridors(), - }, - ), - ); - data.apply(patchOf(grid)); - const border = new Data('state').apply( - new Patch().set( - 'Border', - { count: 1 }, - { - geometry: [[...texas.border, texas.border[0]!] as [number, number][]], - }, - ), - ); - - const state = { - comets: true, - labels: true, - playing: true, - markers: 'gauges' as 'gauges' | 'pies' | 'icons', - corridors: false, - legends: true, - labelColor: 'text' as 'text' | 'kind', - ownGlow: false, - ease: 0.45, - }; - let motion: Motion = 'system'; - const ICONS = icons(); - /** The pulse of tripped equipment: red to dark red and back, twice a second. */ - const pulse = elapsed() - .mul(4 * Math.PI) - .sin() - .colormap({ kind: 'sequential', colors: ['#5c0805', LOOK.tripped] }) - .domain(-1, 1); - const kinds = field('kind').colormap({ - kind: 'categorical', - colors: [...KINDS.map((k) => k.color), LOOK.tripped], - categories: [...KINDS.map((k) => k.label), 'tripped or dark'], - }); - // Comet speed in degrees per second. Comets move in the flow's direction, faster as the loading - // rises. A speed of 0 draws none, so an out branch has no comets. - const comets = when(field('out').gt(0.5)) - .then(0) - .when(field('flow').lt(0)) - .then(field('flow').mul(1.8).sub(0.45)) - .otherwise(field('flow').mul(1.8).add(0.45)); - /** A substation's marker: a gauge of its output, a pie of output and headroom, or its kind's - * icon. */ - const marker = (shape: 'ellipse' | 'rounded'): Marker => - state.markers === 'pies' - ? { - pie: { - slices: [field('fill'), field('fill').neg().add(1)], - colors: [LOOK.comet, '#26313d'], - hole: 0.35, - }, - } - : state.markers === 'icons' - ? icon(ICONS, field('kind')) - : { gauge: { fill: field('fill'), shape } }; - /** A mark for the plants (round gauges) or the loads (square ones). Both marks draw the one - * `Substation` table. */ - const substations = (name: string, load: number, shape: 'ellipse' | 'rounded') => ({ - vertices: { - of: ['grid', 'Substation'] as [string, string], - name, - position: ['$lon', '$lat'] as [string, string], - visible: field('load').eq(load), - // Both marks give every row the same radius from its capacity, so branches stop at the - // outline that is shown. - radius: field('capacity').sqrt().scale(8, 17).domain(11, 46), - marker: marker(shape), - color: kinds, - ring: when(field('alarm').gt(0.5)).then(pulse).otherwise('#00000000'), - ...(load || !state.legends ? {} : { legend: 'Substations' }), - ...(state.labels - ? { - label: { - text: '$name', - size: 12, - ...(state.labelColor === 'kind' ? { color: kinds } : {}), - }, - } - : {}), - }, - }); - const scene = (): Scene => ({ - space: { earth: { graticule: false } }, - theme: { - ...theme, - earth: { ground: LOOK.ground }, - text: { color: LOOK.text, font: 'Jura, sans-serif', size: 12, halo: 3 }, - // Selected rows glow blue, or each in its own color. - focus: { hover: LOOK.hover, selected: state.ownGlow ? null : LOOK.hover, glow: 6 }, - // Comet spacing and length are in map units (degrees), so comets zoom with the map and stay - // on their branches. - flow: { color: LOOK.comet, spacing: units(1.35), length: units(0.6), width: 6 }, - edge: { bow: 0.22 }, - }, - ease: state.ease, - // With `motion` at 'system', easing, pulses, and comets stop when the system asks for reduced - // motion. - controls: { motion }, - marks: [ - { lines: { of: ['state', 'Border'], width: 1.25, color: LOOK.border } }, - { - edges: { - of: ['grid', 'Branch'], - ends: ['from', 'to'], - route: 'straight', - ...(state.corridors ? { bends: 'corridor' } : {}), - width: field('rating').scale(1.6, 3.6).domain(300, 800), - color: when(field('alarm').gt(0.5)).then(pulse).otherwise(LOADING), - dash: '$out', - ...(state.legends ? { legend: 'Loading, % of rating' } : {}), - ...(state.comets ? { flow: comets } : {}), - }, - }, - substations('plants', 0, 'ellipse'), - substations('loads', 1, 'rounded'), - ], - }); - - const view = stage.view(scene(), { - label: 'A state’s grid: hover or click a substation or branch to inspect it', - }); - view.sources = [border, data]; - const { xMin, xMax, yMin, yMax } = texas.bounds; - view.fit({ bounds: { min: [xMin, yMin, 0], max: [xMax, yMax, 0] } }); - const restyle = () => (view.scene = scene()); - - // ── Grid ── - const gridSection = stage.section('Grid'); - gridSection.note('Simulated data'); - gridSection.range( - 'Demand', - { - min: 0.5, - max: 1.2, - step: 0.01, - value: grid.demand, - format: (v) => `${Math.round(v * 100)}%`, - }, - (v) => (grid.demand = v), - ); - gridSection.actions([ - { label: 'Trip a line', key: 't', run: () => (grid.tripWorst(), show()) }, - { label: 'Restore', key: 'r', run: () => (grid.restore(), show()) }, - ]); - gridSection.toggle('Run', ' ', state.playing, (on) => (state.playing = on)); - const totals = gridSection.note(); - - // ── Layers ── - const layers = stage.section('Layers'); - layers.toggle('Flow', 'c', state.comets, (on) => ((state.comets = on), restyle())); - layers.toggle('Labels', 'l', state.labels, (on) => ((state.labels = on), restyle())); - layers.toggle('Corridors', 'b', state.corridors, (on) => ((state.corridors = on), restyle())); - layers.toggle('Legends', 'g', state.legends, (on) => ((state.legends = on), restyle())); - layers.choice( - 'Substations', - [ - ['Gauges', 'gauges'], - ['Pies', 'pies'], - ['Icons', 'icons'], - ] as const, - state.markers, - (next) => ((state.markers = next), restyle()), - ); - layers.choice( - 'Label color', - [ - ['Text', 'text'], - ['By kind', 'kind'], - ] as const, - state.labelColor, - (next) => ((state.labelColor = next), restyle()), - ); - layers.choice( - 'Selection glow', - [ - ['Blue', false], - ['Its own color', true], - ] as const, - state.ownGlow, - (next) => ((state.ownGlow = next), restyle()), - ); - layers.choice( - 'Motion', - [ - ['System', 'system'], - ['Full', 'full'], - ['Reduced', 'reduced'], - ] as const, - motion, - (next) => ((motion = next), restyle()), - ); - layers.range( - 'Easing', - { min: 0, max: 1.5, step: 0.05, value: state.ease, format: (v) => `${v.toFixed(2)} s` }, - (ease) => { - state.ease = ease; - restyle(); - }, - ); - - // ── Inspector ── - const inspector = stage.section('Inspector'); - const described = inspector.note('Select a substation or line.'); - let hovered: Hit | null = null; - const inspect = () => { - const row = hovered?.row ?? each(view.selection)[0]; - if (!row) return void (described.textContent = 'Select a substation or line.'); - const value = (name: string) => data.get(row.table, row.id, name) as number; - const i = Number(row.id); - if (row.table === 'Substation') { - const what = grid.load[i] ? 'load' : KINDS[grid.kind[i]!]!.label; - const status = grid.tripped(i) - ? 'tripped' - : grid.load[i] && grid.fill[i]! < 0.05 - ? 'dark' - : `${Math.round(grid.fill[i]! * 100)}%`; - described.textContent = `${s[i]!.name} · ${what} · ${Math.round(value('mw'))} of ${Math.round(grid.capacity[i]!)} MW · ${status}`; - } else { - const b = texas.branches[i]!; - const [from, to] = grid.flow[i]! >= 0 ? [b.from, b.to] : [b.to, b.from]; - described.textContent = grid.out[i] - ? `${s[b.from]!.name}–${s[b.to]!.name} · out` - : `${s[from]!.name} → ${s[to]!.name} · ${Math.round(value('mw'))} MW · ${Math.round(value('loading'))}% of ${b.pmax} MW`; - } - }; - view.on('hovered', ({ hit }) => ((hovered = hit), inspect())); - view.on('selection', inspect); - - const meter = stage.meter(view); - meter.note(() => `${grid.trips} trips`); - - /** Shows the grid's current state in the view, the totals, and the inspector. */ - function show(): void { - data.apply(patchOf(grid)); - const served = grid.injection.reduce((sum, p, i) => sum + (grid.load[i] ? -p : 0), 0); - const out = grid.out.reduce((a, b) => a + b, 0); - totals.textContent = `${Math.round(served).toLocaleString()} MW served · ${out} branches out · ${grid.dark} loads dark`; - inspect(); - } - - let carry = 0; - stage.frame((dt) => { - if (!state.playing) return; - carry += dt; - if (carry < TICK) return; - carry -= TICK; - grid.step(TICK); - show(); - }); - show(); - stage.detail(`${grid.n} substations · ${grid.m} branches, ${texas.name}`); -} diff --git a/npm/examples/src/examples/power-flow/texas.json b/npm/examples/src/examples/power-flow/texas.json deleted file mode 100644 index f7923a2e..00000000 --- a/npm/examples/src/examples/power-flow/texas.json +++ /dev/null @@ -1,948 +0,0 @@ -{ - "name": "Texas", - "bounds": { "xMax": -93, "xMin": -107, "yMax": 37, "yMin": 25.5 }, - "substations": [ - { - "name": "Abilene", - "lon": -99.66, - "lat": 32.51, - "category": "Load", - "units": [104.33, 104.33, 104.33] - }, - { - "name": "Armstrong", - "lon": -97.6, - "lat": 27, - "category": "Wind", - "units": [108.8, 108.8, 108.8, 108.8, 108.8] - }, - { - "name": "Austin", - "lon": -97.78, - "lat": 30.29, - "category": "Load", - "units": [158.75, 158.75, 158.75, 158.75] - }, - { - "name": "Bastrop", - "lon": -97.27, - "lat": 30.15, - "category": "Solar PV", - "units": [240, 240, 240, 240] - }, - { - "name": "Brownsville", - "lon": -97.32, - "lat": 25.94, - "category": "Load", - "units": [88.67, 88.67, 88.67] - }, - { "name": "Bryan", "lon": -96.46, "lat": 30.79, "category": "Gas Turbine", "units": [85, 85] }, - { - "name": "College Station", - "lon": -96.33, - "lat": 30.61, - "category": "Load", - "units": [101.5, 101.5, 101.5, 101.5] - }, - { - "name": "Corpus Christi", - "lon": -97.43, - "lat": 27.81, - "category": "Load", - "units": [85.75, 85.75, 85.75, 85.75] - }, - { - "name": "Dallas", - "lon": -96.68, - "lat": 32.71, - "category": "Load", - "units": [144.29, 144.29, 144.29, 144.29, 144.29, 144.29, 144.29] - }, - { - "name": "El Campo", - "lon": -96.28, - "lat": 29.21, - "category": "Gas Combined Cycle", - "units": [103.75, 103.75, 103.75, 103.75, 103.75, 103.75, 103.75, 103.75] - }, - { - "name": "Fluvanna", - "lon": -101.15, - "lat": 33.05, - "category": "Wind", - "units": [147, 147, 147] - }, - { - "name": "Franklin", - "lon": -96.49, - "lat": 31.18, - "category": "Coal-fired Steam", - "units": [483.33, 483.33, 483.33] - }, - { - "name": "Ft Worth", - "lon": -97.31, - "lat": 32.53, - "category": "Load", - "units": [153, 153, 153, 153, 153, 153] - }, - { - "name": "Galveston", - "lon": -94.78, - "lat": 29.31, - "category": "Load", - "units": [47, 47, 47, 47] - }, - { - "name": "Houston", - "lon": -95.4, - "lat": 29.61, - "category": "Load", - "units": [170.12, 170.12, 170.12, 170.12, 170.12, 170.12, 170.12, 170.12] - }, - { - "name": "Katy", - "lon": -95.83, - "lat": 29.74, - "category": "Load", - "units": [138.2, 138.2, 138.2, 138.2, 138.2] - }, - { - "name": "Kerrville", - "lon": -99.16, - "lat": 30.04, - "category": "Gas Combined Cycle", - "units": [72.5, 72.5, 72.5, 72.5, 72.5, 72.5, 72.5, 72.5] - }, - { "name": "Lubbock", "lon": -101.39, "lat": 33.66, "category": "Load", "units": [88, 88, 88] }, - { - "name": "McCamey", - "lon": -102.24, - "lat": 31.21, - "category": "Solar PV", - "units": [201.67, 201.67, 201.67] - }, - { - "name": "McKinney", - "lon": -96.6, - "lat": 33.17, - "category": "Gas Combined Cycle", - "units": [206, 206, 206, 206, 206] - }, - { - "name": "Mission", - "lon": -98.4, - "lat": 26.21, - "category": "Gas Turbine", - "units": [73.75, 73.75, 73.75, 73.75] - }, - { - "name": "Monahans", - "lon": -102.96, - "lat": 31.58, - "category": "Wind", - "units": [240, 240, 240, 240] - }, - { - "name": "Odessa", - "lon": -102.33, - "lat": 31.84, - "category": "Load", - "units": [59.33, 59.33, 59.33] - }, - { - "name": "O'Donnell", - "lon": -101.65, - "lat": 32.93, - "category": "Wind", - "units": [305, 305, 305, 305, 305, 305] - }, - { - "name": "Pasadena", - "lon": -95.18, - "lat": 29.72, - "category": "Gas Turbine", - "units": [112.5, 112.5, 112.5, 112.5] - }, - { - "name": "Rockdale", - "lon": -97.06, - "lat": 30.57, - "category": "Coal-fired Steam", - "units": [290, 290] - }, - { - "name": "San Antonio", - "lon": -98.61, - "lat": 29.59, - "category": "Load", - "units": [171, 171, 171, 171, 171] - }, - { - "name": "Snyder", - "lon": -100.95, - "lat": 32.75, - "category": "Gas Turbine", - "units": [160, 160] - }, - { - "name": "Tyler", - "lon": -95.32, - "lat": 32.24, - "category": "Load", - "units": [97.33, 97.33, 97.33] - }, - { - "name": "Waco", - "lon": -97.1, - "lat": 31.62, - "category": "Load", - "units": [106, 106, 106, 106] - }, - { - "name": "Wadsworth", - "lon": -96.05, - "lat": 28.8, - "category": "Nuclear Steam", - "units": [1050, 1050] - }, - { - "name": "Wichita Falls", - "lon": -98.49, - "lat": 33.91, - "category": "Load", - "units": [43, 43, 43] - } - ], - "branches": [ - { "from": 0, "to": 18, "pmax": 500 }, - { "from": 0, "to": 22, "pmax": 500 }, - { "from": 0, "to": 27, "pmax": 500 }, - { "from": 0, "to": 27, "pmax": 500 }, - { "from": 0, "to": 29, "pmax": 500 }, - { "from": 1, "to": 7, "pmax": 500 }, - { "from": 2, "to": 3, "pmax": 800 }, - { "from": 2, "to": 11, "pmax": 500 }, - { "from": 2, "to": 25, "pmax": 500 }, - { "from": 2, "to": 25, "pmax": 500 }, - { "from": 2, "to": 29, "pmax": 500 }, - { "from": 4, "to": 1, "pmax": 500 }, - { "from": 4, "to": 7, "pmax": 500 }, - { "from": 5, "to": 28, "pmax": 500 }, - { "from": 6, "to": 5, "pmax": 500 }, - { "from": 6, "to": 5, "pmax": 500 }, - { "from": 6, "to": 11, "pmax": 500 }, - { "from": 7, "to": 9, "pmax": 500 }, - { "from": 7, "to": 30, "pmax": 500 }, - { "from": 8, "to": 12, "pmax": 500 }, - { "from": 8, "to": 12, "pmax": 500 }, - { "from": 8, "to": 19, "pmax": 500 }, - { "from": 8, "to": 29, "pmax": 500 }, - { "from": 9, "to": 15, "pmax": 500 }, - { "from": 9, "to": 26, "pmax": 500 }, - { "from": 10, "to": 17, "pmax": 300 }, - { "from": 10, "to": 23, "pmax": 500 }, - { "from": 11, "to": 5, "pmax": 500 }, - { "from": 11, "to": 8, "pmax": 500 }, - { "from": 11, "to": 8, "pmax": 500 }, - { "from": 12, "to": 0, "pmax": 500 }, - { "from": 12, "to": 0, "pmax": 500 }, - { "from": 12, "to": 27, "pmax": 500 }, - { "from": 12, "to": 29, "pmax": 500 }, - { "from": 13, "to": 15, "pmax": 500 }, - { "from": 13, "to": 24, "pmax": 800 }, - { "from": 14, "to": 6, "pmax": 500 }, - { "from": 14, "to": 9, "pmax": 500 }, - { "from": 14, "to": 9, "pmax": 500 }, - { "from": 14, "to": 24, "pmax": 500 }, - { "from": 15, "to": 3, "pmax": 800 }, - { "from": 15, "to": 14, "pmax": 500 }, - { "from": 15, "to": 14, "pmax": 500 }, - { "from": 15, "to": 25, "pmax": 500 }, - { "from": 16, "to": 2, "pmax": 500 }, - { "from": 16, "to": 18, "pmax": 500 }, - { "from": 17, "to": 23, "pmax": 300 }, - { "from": 17, "to": 31, "pmax": 300 }, - { "from": 19, "to": 12, "pmax": 500 }, - { "from": 19, "to": 31, "pmax": 300 }, - { "from": 20, "to": 4, "pmax": 500 }, - { "from": 20, "to": 7, "pmax": 500 }, - { "from": 20, "to": 26, "pmax": 300 }, - { "from": 21, "to": 18, "pmax": 500 }, - { "from": 22, "to": 18, "pmax": 500 }, - { "from": 22, "to": 21, "pmax": 500 }, - { "from": 22, "to": 23, "pmax": 500 }, - { "from": 24, "to": 6, "pmax": 500 }, - { "from": 25, "to": 5, "pmax": 500 }, - { "from": 25, "to": 11, "pmax": 500 }, - { "from": 25, "to": 29, "pmax": 500 }, - { "from": 26, "to": 2, "pmax": 500 }, - { "from": 26, "to": 2, "pmax": 500 }, - { "from": 26, "to": 16, "pmax": 500 }, - { "from": 26, "to": 16, "pmax": 500 }, - { "from": 27, "to": 10, "pmax": 800 }, - { "from": 27, "to": 23, "pmax": 800 }, - { "from": 28, "to": 8, "pmax": 500 }, - { "from": 30, "to": 9, "pmax": 500 }, - { "from": 30, "to": 9, "pmax": 500 }, - { "from": 30, "to": 13, "pmax": 800 }, - { "from": 30, "to": 26, "pmax": 500 } - ], - "border": [ - [-103.98018, 32.00012], - [-103.06451, 32.00042], - [-103.06463, 32.9999], - [-103.04365, 34.00364], - [-103.0409, 35.8961], - [-103.04171, 36.50044], - [-100.00041, 36.4997], - [-100.00028, 34.56045], - [-99.9589, 34.57127], - [-99.90349, 34.55836], - [-99.86895, 34.52762], - [-99.8329, 34.50007], - [-99.80286, 34.46567], - [-99.7656, 34.43749], - [-99.72703, 34.41055], - [-99.70041, 34.3847], - [-99.65419, 34.37652], - [-99.60003, 34.37469], - [-99.58006, 34.41665], - [-99.52979, 34.41145], - [-99.48722, 34.39796], - [-99.43827, 34.37116], - [-99.39568, 34.39769], - [-99.38923, 34.44686], - [-99.34013, 34.4301], - [-99.30926, 34.4097], - [-99.26128, 34.40351], - [-99.24354, 34.37071], - [-99.2172, 34.34151], - [-99.2097, 34.2989], - [-99.19384, 34.26031], - [-99.18978, 34.21436], - [-99.13808, 34.21923], - [-99.08892, 34.20847], - [-99.04446, 34.19811], - [-99.00224, 34.20937], - [-98.95128, 34.21219], - [-98.91833, 34.18183], - [-98.87292, 34.16658], - [-98.81295, 34.15844], - [-98.77808, 34.13949], - [-98.74197, 34.12553], - [-98.69007, 34.13316], - [-98.64807, 34.16444], - [-98.59979, 34.16057], - [-98.56661, 34.13744], - [-98.53626, 34.10735], - [-98.50415, 34.07229], - [-98.44903, 34.07346], - [-98.40972, 34.09251], - [-98.39403, 34.13191], - [-98.36562, 34.15699], - [-98.30021, 34.13458], - [-98.24795, 34.13072], - [-98.20173, 34.11706], - [-98.15435, 34.12273], - [-98.13082, 34.15053], - [-98.09237, 34.13215], - [-98.11942, 34.08447], - [-98.10189, 34.05097], - [-98.08403, 34.01437], - [-98.04112, 33.99346], - [-97.99086, 33.99887], - [-97.94757, 33.99105], - [-97.96035, 33.95193], - [-97.96875, 33.90602], - [-97.93895, 33.87947], - [-97.89674, 33.85799], - [-97.84378, 33.85681], - [-97.81764, 33.88099], - [-97.78084, 33.89445], - [-97.76277, 33.9344], - [-97.7206, 33.94395], - [-97.69918, 33.9757], - [-97.65621, 33.98949], - [-97.61607, 33.96972], - [-97.59503, 33.93736], - [-97.58108, 33.89968], - [-97.53421, 33.90573], - [-97.48772, 33.91751], - [-97.45106, 33.89168], - [-97.46149, 33.84956], - [-97.4268, 33.81864], - [-97.37469, 33.81855], - [-97.34721, 33.84511], - [-97.33188, 33.88448], - [-97.28539, 33.86824], - [-97.24505, 33.90322], - [-97.18658, 33.90155], - [-97.16663, 33.84731], - [-97.1998, 33.8274], - [-97.1904, 33.78115], - [-97.17463, 33.74504], - [-97.13392, 33.71813], - [-97.08737, 33.74114], - [-97.0951, 33.79217], - [-97.04815, 33.81746], - [-97.0872, 33.84137], - [-97.02144, 33.84638], - [-96.98384, 33.89095], - [-96.99502, 33.93203], - [-96.95178, 33.94474], - [-96.901, 33.93878], - [-96.88301, 33.86802], - [-96.85059, 33.84721], - [-96.79428, 33.86889], - [-96.77067, 33.82962], - [-96.71242, 33.83163], - [-96.68473, 33.86291], - [-96.67345, 33.91228], - [-96.62829, 33.89448], - [-96.59011, 33.88066], - [-96.62902, 33.85241], - [-96.59293, 33.83092], - [-96.55106, 33.81925], - [-96.51569, 33.79516], - [-96.48634, 33.77296], - [-96.43199, 33.7791], - [-96.40822, 33.75054], - [-96.36959, 33.71681], - [-96.34686, 33.68622], - [-96.31223, 33.7063], - [-96.29811, 33.75912], - [-96.22902, 33.74802], - [-96.1786, 33.76043], - [-96.16076, 33.79591], - [-96.14923, 33.83709], - [-96.09936, 33.83047], - [-96.04883, 33.83647], - [-96.00405, 33.84524], - [-95.94733, 33.85892], - [-95.89521, 33.86758], - [-95.85683, 33.85014], - [-95.81557, 33.85985], - [-95.77024, 33.8452], - [-95.75794, 33.89075], - [-95.71218, 33.88552], - [-95.66548, 33.909], - [-95.61637, 33.92763], - [-95.56378, 33.93294], - [-95.55122, 33.88971], - [-95.50662, 33.89672], - [-95.47047, 33.8816], - [-95.42768, 33.87046], - [-95.36257, 33.8669], - [-95.32994, 33.88633], - [-95.28893, 33.87307], - [-95.27596, 33.91128], - [-95.25, 33.93886], - [-95.21695, 33.96267], - [-95.1715, 33.94672], - [-95.13016, 33.9368], - [-95.11039, 33.90541], - [-95.08241, 33.87988], - [-95.06948, 33.91759], - [-95.05057, 33.8783], - [-95.01574, 33.85773], - [-94.9658, 33.8487], - [-94.94787, 33.80836], - [-94.91287, 33.77948], - [-94.87637, 33.75733], - [-94.82795, 33.74088], - [-94.78007, 33.74447], - [-94.74767, 33.72515], - [-94.71953, 33.68886], - [-94.67177, 33.69331], - [-94.65492, 33.65993], - [-94.62204, 33.68253], - [-94.57927, 33.67082], - [-94.54302, 33.65518], - [-94.56997, 33.62847], - [-94.52813, 33.64125], - [-94.48836, 33.62741], - [-94.44864, 33.64277], - [-94.46945, 33.60732], - [-94.43004, 33.59112], - [-94.40067, 33.5698], - [-94.36987, 33.54566], - [-94.33956, 33.56767], - [-94.29497, 33.57912], - [-94.24278, 33.58971], - [-94.21921, 33.5561], - [-94.18391, 33.59469], - [-94.15263, 33.57592], - [-94.12761, 33.55072], - [-94.09744, 33.57374], - [-94.05658, 33.56093], - [-94.04278, 32.02483], - [-94.02289, 31.99045], - [-93.99571, 31.95414], - [-93.97171, 31.92039], - [-93.92865, 31.91265], - [-93.89719, 31.89102], - [-93.88288, 31.85467], - [-93.87084, 31.81378], - [-93.83561, 31.79449], - [-93.8317, 31.74551], - [-93.81669, 31.70792], - [-93.82646, 31.66692], - [-93.81699, 31.62518], - [-93.83492, 31.58621], - [-93.81397, 31.55056], - [-93.78084, 31.52538], - [-93.73524, 31.5135], - [-93.75137, 31.47907], - [-93.72193, 31.45345], - [-93.70468, 31.4189], - [-93.67412, 31.39768], - [-93.64826, 31.37086], - [-93.66902, 31.33883], - [-93.68728, 31.30607], - [-93.65869, 31.28162], - [-93.61992, 31.26518], - [-93.59839, 31.2311], - [-93.60244, 31.18254], - [-93.54998, 31.18663], - [-93.54474, 31.13607], - [-93.55818, 31.09798], - [-93.52765, 31.07451], - [-93.50903, 31.03289], - [-93.53918, 31.00821], - [-93.57336, 30.99183], - [-93.54731, 30.96475], - [-93.52784, 30.93307], - [-93.55856, 30.91324], - [-93.5706, 30.87519], - [-93.55363, 30.83514], - [-93.57468, 30.80137], - [-93.5921, 30.76368], - [-93.61903, 30.74034], - [-93.62665, 30.68836], - [-93.65974, 30.66205], - [-93.68421, 30.63188], - [-93.69222, 30.58571], - [-93.72616, 30.56952], - [-93.72048, 30.52433], - [-93.70976, 30.48265], - [-93.69758, 30.44223], - [-93.72755, 30.41872], - [-93.75261, 30.39275], - [-93.76365, 30.34604], - [-93.74565, 30.31174], - [-93.71227, 30.29233], - [-93.70554, 30.2465], - [-93.72126, 30.20961], - [-93.70424, 30.17418], - [-93.69285, 30.13521], - [-93.71549, 30.10053], - [-93.70058, 30.06368], - [-93.73745, 30.03728], - [-93.76724, 30.00338], - [-93.79444, 29.97477], - [-93.81729, 29.91842], - [-93.83837, 29.88285], - [-93.86357, 29.85718], - [-93.91367, 29.82719], - [-93.9265, 29.78956], - [-93.89386, 29.76729], - [-93.87002, 29.73548], - [-93.83741, 29.68988], - [-93.81435, 29.59658], - [-93.85798, 29.60509], - [-93.90604, 29.625], - [-94.03357, 29.625], - [-94.12518, 29.59556], - [-94.37035, 29.50023], - [-94.41925, 29.48345], - [-94.49945, 29.45234], - [-94.62519, 29.39766], - [-94.6239, 29.34314], - [-94.64242, 29.30014], - [-94.6803, 29.2776], - [-94.74117, 29.25958], - [-94.77269, 29.23172], - [-94.84015, 29.19835], - [-94.91217, 29.15932], - [-94.96366, 29.12524], - [-95.0002, 29.10445], - [-95.0864, 29.06824], - [-95.10653, 29.01493], - [-95.13947, 28.9917], - [-95.18084, 28.94893], - [-95.22723, 28.91038], - [-95.29081, 28.87164], - [-95.31542, 28.84221], - [-95.35347, 28.81786], - [-95.40116, 28.80597], - [-95.49389, 28.76534], - [-95.53378, 28.75026], - [-95.77785, 28.62527], - [-95.87524, 28.58279], - [-96.00024, 28.53207], - [-96.07833, 28.5], - [-96.18224, 28.45204], - [-96.23546, 28.42147], - [-96.26707, 28.38823], - [-96.30353, 28.36502], - [-96.34181, 28.34238], - [-96.37525, 28.29076], - [-96.42581, 28.26869], - [-96.531, 28.22116], - [-96.64075, 28.15539], - [-96.74625, 28.07962], - [-96.8442, 28.0003], - [-96.87526, 27.96992], - [-96.90874, 27.93409], - [-96.93855, 27.9012], - [-96.98428, 27.80378], - [-97.02185, 27.78336], - [-97.05022, 27.75031], - [-97.12527, 27.6406], - [-97.1651, 27.5683], - [-97.19982, 27.50032], - [-97.25026, 27.39231], - [-97.28834, 27.27758], - [-97.30936, 27.19611], - [-97.31982, 27.11002], - [-97.32315, 27.05115], - [-97.32115, 27.00033], - [-97.31388, 26.91313], - [-97.30042, 26.83461], - [-97.28273, 26.76342], - [-97.25171, 26.66187], - [-97.22443, 26.59769], - [-97.18457, 26.47585], - [-97.1565, 26.37535], - [-97.12525, 26.24828], - [-97.1107, 26.14537], - [-97.09739, 26.09351], - [-97.09414, 26.04304], - [-97.09112, 25.97384], - [-97.14665, 25.97097], - [-97.18708, 25.95848], - [-97.23987, 25.95498], - [-97.28964, 25.95403], - [-97.32057, 25.9298], - [-97.3615, 25.91587], - [-97.35669, 25.86735], - [-97.38206, 25.83976], - [-97.41131, 25.86134], - [-97.45669, 25.86834], - [-97.49669, 25.87968], - [-97.52734, 25.90261], - [-97.54974, 25.93622], - [-97.58301, 25.95483], - [-97.61767, 25.97996], - [-97.64938, 26.01086], - [-97.68567, 26.02675], - [-97.7349, 26.03202], - [-97.77683, 26.04397], - [-97.81805, 26.0564], - [-97.85933, 26.06696], - [-97.91124, 26.05645], - [-97.96734, 26.05178], - [-98.00739, 26.06216], - [-98.03924, 26.04128], - [-98.07507, 26.05987], - [-98.12584, 26.06179], - [-98.17213, 26.07114], - [-98.22067, 26.07647], - [-98.26451, 26.08551], - [-98.2906, 26.12066], - [-98.3352, 26.13762], - [-98.35768, 26.16883], - [-98.40225, 26.17952], - [-98.44204, 26.19875], - [-98.47178, 26.22019], - [-98.51485, 26.22722], - [-98.55458, 26.24738], - [-98.59915, 26.25761], - [-98.63418, 26.24261], - [-98.67785, 26.25691], - [-98.71941, 26.2723], - [-98.75097, 26.29632], - [-98.77703, 26.32573], - [-98.79821, 26.36017], - [-98.85342, 26.36502], - [-98.89673, 26.35333], - [-98.9234, 26.38137], - [-98.96189, 26.39837], - [-99.01064, 26.39213], - [-99.04547, 26.40982], - [-99.08513, 26.39878], - [-99.11085, 26.42628], - [-99.09198, 26.47238], - [-99.12778, 26.5252], - [-99.17123, 26.54196], - [-99.17806, 26.62055], - [-99.20052, 26.65644], - [-99.20891, 26.72476], - [-99.24002, 26.74585], - [-99.26221, 26.81567], - [-99.28047, 26.85805], - [-99.32588, 26.87738], - [-99.3373, 26.92276], - [-99.37915, 26.93449], - [-99.38737, 26.9824], - [-99.41548, 27.01724], - [-99.45232, 27.06267], - [-99.42921, 27.09098], - [-99.43827, 27.14479], - [-99.43278, 27.20184], - [-99.44155, 27.24992], - [-99.48906, 27.26148], - [-99.50095, 27.30601], - [-99.50778, 27.35425], - [-99.49271, 27.39195], - [-99.4951, 27.45152], - [-99.48129, 27.48847], - [-99.52614, 27.49659], - [-99.52192, 27.54409], - [-99.5362, 27.59168], - [-99.57863, 27.60229], - [-99.59404, 27.63857], - [-99.63893, 27.62676], - [-99.66184, 27.65575], - [-99.71151, 27.65836], - [-99.75764, 27.71211], - [-99.79634, 27.73559], - [-99.81312, 27.774], - [-99.85088, 27.79397], - [-99.87789, 27.82535], - [-99.90149, 27.86416], - [-99.91746, 27.91797], - [-99.93854, 27.95406], - [-99.96277, 27.98354], - [-100.00863, 28.02396], - [-100.01791, 28.06479], - [-100.05312, 28.08473], - [-100.07547, 28.12488], - [-100.11963, 28.15559], - [-100.16059, 28.16816], - [-100.1965, 28.19022], - [-100.21756, 28.22693], - [-100.2676, 28.25027], - [-100.29411, 28.2804], - [-100.3142, 28.34586], - [-100.34187, 28.38495], - [-100.33619, 28.43018], - [-100.3575, 28.46364], - [-100.33381, 28.49925], - [-100.38826, 28.51486], - [-100.41127, 28.5507], - [-100.42986, 28.59644], - [-100.44553, 28.63714], - [-100.47964, 28.65523], - [-100.51005, 28.69072], - [-100.50761, 28.7406], - [-100.53005, 28.77042], - [-100.54217, 28.80847], - [-100.57473, 28.82879], - [-100.59104, 28.86305], - [-100.61422, 28.90172], - [-100.63886, 28.92762], - [-100.64865, 28.97742], - [-100.66021, 29.0315], - [-100.6752, 29.10074], - [-100.70997, 29.11968], - [-100.73912, 29.14166], - [-100.7726, 29.16837], - [-100.78552, 29.22814], - [-100.81628, 29.25386], - [-100.85584, 29.27555], - [-100.88684, 29.30785], - [-100.9195, 29.32532], - [-100.94897, 29.34725], - [-100.99561, 29.3634], - [-101.01926, 29.39091], - [-101.04336, 29.42988], - [-101.07718, 29.46419], - [-101.13004, 29.47842], - [-101.17166, 29.50395], - [-101.22742, 29.52235], - [-101.25038, 29.55662], - [-101.24733, 29.61848], - [-101.27788, 29.59081], - [-101.31171, 29.61537], - [-101.34602, 29.66224], - [-101.37277, 29.69943], - [-101.39629, 29.72706], - [-101.40232, 29.77119], - [-101.43815, 29.75092], - [-101.4558, 29.78805], - [-101.50322, 29.76458], - [-101.53792, 29.78806], - [-101.57581, 29.76932], - [-101.6269, 29.77076], - [-101.67456, 29.76523], - [-101.71973, 29.77574], - [-101.77383, 29.78868], - [-101.81252, 29.80979], - [-101.8665, 29.79801], - [-101.91572, 29.79521], - [-101.96338, 29.80015], - [-102.01523, 29.79749], - [-102.055, 29.78703], - [-102.10138, 29.79361], - [-102.14357, 29.80332], - [-102.17996, 29.82638], - [-102.22353, 29.84053], - [-102.2615, 29.85324], - [-102.29587, 29.87361], - [-102.34309, 29.86524], - [-102.36293, 29.83149], - [-102.3772, 29.78989], - [-102.40454, 29.76518], - [-102.44738, 29.7775], - [-102.49799, 29.78145], - [-102.53424, 29.75494], - [-102.57057, 29.771], - [-102.61243, 29.74827], - [-102.66024, 29.73563], - [-102.68904, 29.70504], - [-102.70865, 29.66699], - [-102.73657, 29.64183], - [-102.74633, 29.59265], - [-102.77342, 29.56687], - [-102.79201, 29.533], - [-102.80727, 29.49429], - [-102.82564, 29.45794], - [-102.83344, 29.4105], - [-102.84532, 29.36473], - [-102.88437, 29.34795], - [-102.88818, 29.29336], - [-102.90638, 29.26008], - [-102.87175, 29.24178], - [-102.8951, 29.20852], - [-102.93251, 29.19474], - [-102.97553, 29.1863], - [-103.00251, 29.14996], - [-103.02759, 29.11602], - [-103.06418, 29.09148], - [-103.08551, 29.05996], - [-103.1015, 29.0181], - [-103.12358, 28.9843], - [-103.16393, 28.9722], - [-103.20241, 28.9872], - [-103.25042, 28.98043], - [-103.28694, 29.00127], - [-103.31434, 29.02606], - [-103.3596, 29.01864], - [-103.40105, 29.03106], - [-103.43371, 29.0522], - [-103.47135, 29.06673], - [-103.49364, 29.10795], - [-103.5228, 29.13465], - [-103.55369, 29.158], - [-103.60618, 29.16027], - [-103.65363, 29.16342], - [-103.68857, 29.17857], - [-103.72872, 29.19288], - [-103.75287, 29.21949], - [-103.78168, 29.24128], - [-103.80917, 29.26594], - [-103.85431, 29.28147], - [-103.89929, 29.28741], - [-103.95003, 29.29535], - [-103.99166, 29.30381], - [-104.0375, 29.31971], - [-104.06675, 29.341], - [-104.09775, 29.36164], - [-104.13142, 29.37998], - [-104.16744, 29.39474], - [-104.18611, 29.4283], - [-104.2177, 29.45656], - [-104.23382, 29.49365], - [-104.26885, 29.51358], - [-104.31148, 29.5232], - [-104.3625, 29.53588], - [-104.39427, 29.55518], - [-104.45202, 29.60353], - [-104.48614, 29.62866], - [-104.5176, 29.64739], - [-104.5441, 29.68069], - [-104.55245, 29.72371], - [-104.56961, 29.75709], - [-104.589, 29.79566], - [-104.60849, 29.82795], - [-104.63012, 29.86452], - [-104.65585, 29.88921], - [-104.67992, 29.92118], - [-104.6855, 29.97084], - [-104.69325, 30.01913], - [-104.70479, 30.05832], - [-104.68553, 30.09321], - [-104.69671, 30.13446], - [-104.68798, 30.17639], - [-104.70248, 30.21278], - [-104.71895, 30.24709], - [-104.74958, 30.26697], - [-104.76709, 30.30371], - [-104.7948, 30.32696], - [-104.82258, 30.35062], - [-104.84561, 30.37921], - [-104.85663, 30.42152], - [-104.86964, 30.4592], - [-104.87196, 30.51109], - [-104.8877, 30.54673], - [-104.90955, 30.58514], - [-104.95082, 30.60405], - [-104.9795, 30.6286], - [-104.99385, 30.66852], - [-105.03196, 30.68099], - [-105.06688, 30.69926], - [-105.09962, 30.72195], - [-105.12766, 30.74785], - [-105.15619, 30.76977], - [-105.19074, 30.7857], - [-105.22744, 30.79919], - [-105.27169, 30.80938], - [-105.31909, 30.81226], - [-105.34575, 30.84061], - [-105.38732, 30.85373], - [-105.40376, 30.89211], - [-105.44116, 30.90845], - [-105.47079, 30.93124], - [-105.49681, 30.95542], - [-105.52708, 30.97818], - [-105.5701, 31.0082], - [-105.58499, 31.05696], - [-105.6271, 31.09841], - [-105.71053, 31.13697], - [-105.74193, 31.1646], - [-105.78003, 31.18274], - [-105.81838, 31.23015], - [-105.85033, 31.26497], - [-105.87639, 31.29167], - [-105.90739, 31.31221], - [-105.94775, 31.33959], - [-106.00492, 31.39246], - [-106.07531, 31.39762], - [-106.10862, 31.42237], - [-106.15693, 31.43736], - [-106.20012, 31.46241], - [-106.22476, 31.50093], - [-106.24657, 31.54166], - [-106.28079, 31.5621], - [-106.30089, 31.60962], - [-106.33444, 31.66356], - [-106.37237, 31.71257], - [-106.41568, 31.75089], - [-106.46756, 31.75921], - [-106.51369, 31.77071], - [-106.536, 31.79851], - [-106.57724, 31.81041], - [-106.60194, 31.8396], - [-106.63592, 31.86623], - [-106.62557, 31.91243], - [-106.61432, 31.95161], - [-106.63954, 31.98034], - [-106.0829, 32.00265], - [-104.84774, 32.00042], - [-104.77211, 31.99749], - [-103.98018, 32.00012] - ] -} diff --git a/npm/examples/src/examples/soft-body/index.ts b/npm/examples/src/examples/soft-body/index.ts deleted file mode 100644 index aa0a6755..00000000 --- a/npm/examples/src/examples/soft-body/index.ts +++ /dev/null @@ -1,520 +0,0 @@ -/** - * A soft bunny made of springs, in a room. Hop, squash, poke, fling, or pop it, then replay or - * scrub its history. Faces darken as they turn away from the light, above soft contact shadows on - * the floor. The pointer drags the vertices, whose dots stay hidden until the mesh is shown. A plot - * traces the bunny's height, speed, and volume. - */ -import { - Data, - Patch, - field, - reference, - units, - type ColormapName, - type Grid, - type Scene, -} from 'latkit'; -import type { Stage } from '../../ui/stage.js'; -import { theme } from '../../ui/theme.js'; -import { Body, ROOM, STEP, type Controls } from './body.js'; -import { loadMesh } from './mesh.js'; - -/** Seconds of frames kept for replay and scrubbing. */ -const HISTORY = 20; -/** Simulated seconds between recorded frames, regardless of the display's refresh rate. */ -const RECORD = 1 / 60; -/** Seconds the plot shows. */ -const WINDOW = 12; -/** The bunny's color schemes. Each domain starts below the value at rest, so a resting bunny stays - * clear of the colormap's darkest end. */ -const PAINTS: Record< - string, - { label: string; field: string; colormap: ColormapName; domain: [number, number]; legend: string } -> = { - solid: { - label: 'Pearl', - field: 'strain', - colormap: 'turbo', - domain: [-0.05, 0.35], - legend: '', - }, - strain: { - label: 'Strain', - field: 'strain', - colormap: 'turbo', - domain: [-0.05, 0.35], - legend: 'Strain 0 – 0.35 m', - }, - speed: { - label: 'Speed', - field: 'speed', - colormap: 'plasma', - domain: [-1, 9], - legend: 'Speed 0 – 9 m/s', - }, - height: { - label: 'Height', - field: 'z', - colormap: 'viridis', - domain: [-0.5, 4], - legend: 'Height 0 – 4 m', - }, -}; -const SERIES = [ - { field: 'height', label: 'Center height', color: '#66c7ff' }, - { field: 'speed', label: 'Mean speed', color: '#ff8f52' }, - { field: 'volume', label: 'Volume', color: '#cc9eff' }, -]; -const THEME = { - ...theme, - shading: 0.7, -} as const; - -// A soft contact shadow on the floor under the bunny and under each of its pieces, darker the -// lower they are. It is a custom marker in WGSL that reads its darkness from each row's `opacity`. -const CONTACT_SHADOW = `fn marker(f: MarkerFragment) -> MarkerColor { - let p = f.p / max(f.half, vec2f(0.001)); - let d = length(p); - let alpha = (1.0 - smoothstep(0.15, 1.0, d)) * f.input[0]; - return MarkerColor(vec4f(0.0, 0.0, 0.0, alpha), (d - 1.0) * f.half.x); -}`; - -export default async function bunny(stage: Stage): Promise { - const mesh = await loadMesh(new URL('./stanford-bunny.bin', import.meta.url)); - if (stage.signal.aborted) return; - const body = new Body(mesh); - // With reduced motion, the bunny hops lower and pops more gently, and the camera never orbits. - const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)'); - reducedMotion.addEventListener( - 'change', - () => { - if (reducedMotion.matches) space.orbit = 0; - }, - { signal: stage.signal }, - ); - const controls: Controls = { gravity: 9.81, firmness: 0.45, inflate: 1, wind: false }; - /** Wraps a control's setter to also wake the body, so a resting body responds to new gravity or - * a new material. */ - const waking = - (set: (value: T) => void) => - (value: T) => { - set(value); - body.wake(); - }; - let paint = 'solid'; - let inspect = false; - /** Light settings: how much faces turned from the camera darken, the floor's grid, and the - * radius of the mesh's dots in pixels. */ - const lit: { shading: number; grid: 'lines' | 'dots' | 'off'; dots: number } = { - shading: THEME.shading, - grid: 'lines', - dots: 1.5, - }; - const GRIDS: Record = { - lines: { lines: { pitch: 1 } }, - dots: { dots: { pitch: 0.5 } }, - off: 'off', - }; - let fragments = false; - let timeScale = 1; - let carry = 0; - let recordAt = 0; - - // The mesh is set once. Each recorded frame then appends every vertex and the body as a whole. - const data = new Data('bunny').apply( - new Patch() - .axis('time', 's') - .set('Vertex', { count: mesh.count }, {}) - .set( - 'Spring', - { count: mesh.springs.a.length }, - { - a: reference('Vertex', mesh.springs.a), - b: reference('Vertex', mesh.springs.b), - }, - ) - .set( - 'Face', - { count: mesh.faces.a.length }, - { - a: reference('Vertex', mesh.faces.a), - b: reference('Vertex', mesh.faces.b), - c: reference('Vertex', mesh.faces.c), - }, - ) - .set('ChunkVertex', { count: body.burst.count }, {}) - .set( - 'ChunkFace', - { count: body.burst.faces.a.length }, - { - a: reference('ChunkVertex', body.burst.faces.a), - b: reference('ChunkVertex', body.burst.faces.b), - c: reference('ChunkVertex', body.burst.faces.c), - }, - ) - .set('Corner', { count: 4 }, { x: [-ROOM, ROOM, ROOM, -ROOM], y: [-ROOM, -ROOM, ROOM, ROOM] }) - .set( - 'Wall', - { count: 4 }, - { - from: reference('Corner', [0, 1, 2, 3]), - to: reference('Corner', [1, 2, 3, 0]), - }, - ) - .set('Body', { count: 1 }, {}), - ); - - data.apply(new Patch().set('Shadow', { count: body.burst.chunks.length + 1 }, {})); - - // Faces lit from the camera, above soft contact shadows. The vertices stay pickable while their - // dots are hidden. - const room = (live: boolean): Scene => { - const p = PAINTS[paint]!; - return { - space: { plane: { grid: GRIDS[lit.grid] } }, - // The mesh's dots and springs take their sizes from the theme. - theme: { - ...THEME, - shading: lit.shading, - vertex: { radius: inspect ? lit.dots : 0 }, - edge: { width: 1 }, - }, - controls: { pickRadius: 14 }, - marks: [ - { - vertices: { - of: ['bunny', 'Shadow'], - position: ['$x', '$y'], - size: [units(field('radius').mul(2)), units(field('radius'))], - marker: { wgsl: { source: CONTACT_SHADOW, inputs: [field('opacity')] } }, - color: '#000000', - pick: false, - }, - }, - { - vertices: { - of: ['bunny', 'Corner'], - position: ['$x', '$y'], - radius: 3, - color: '#f09478aa', - pick: false, - }, - }, - { - edges: { - of: ['bunny', 'Wall'], - ends: ['from', 'to'], - color: '#f0947866', - width: 1.5, - pick: false, - }, - }, - { - faces: { - of: ['bunny', fragments ? 'ChunkFace' : 'Face'], - corners: ['a', 'b', 'c'], - ...(paint === 'solid' ? { color: '#edccb4' } : {}), - pick: false, - }, - }, - { - edges: { - of: ['bunny', 'Spring'], - ends: ['a', 'b'], - color: '#ffffff14', - pick: false, - visible: inspect && !fragments, - }, - }, - { - vertices: { - of: ['bunny', fragments ? 'ChunkVertex' : 'Vertex'], - position: ['$x', '$y'], - elevation: '$z', - color: - paint === 'solid' - ? inspect - ? '#edccb4' - : '#edccb400' - : field(p.field) - .colormap(p.colormap) - .domain(...p.domain), - select: false, - drag: live && !fragments, - pick: !fragments, - }, - }, - ], - }; - }; - - const space = stage.view(room(true), { - label: 'The bunny: drag it to pull and fling it, click it to poke it', - }); - space.sources = [data]; - space.camera = { center: [0, 0], zoom: 6, pitch: 58, bearing: 30 }; - space.fit({ bounds: { min: [-ROOM, -ROOM, 0], max: [ROOM, ROOM, 2.4] } }); - - const act = (run: () => void) => () => { - playback.play(); - run(); - }; - const actions = stage.section('Actions'); - actions.actions([ - { label: 'Hop', key: ' ', run: act(() => body.hop(reducedMotion.matches)) }, - { label: 'Squash', key: 's', run: act(() => body.squash()) }, - { label: 'Pop', key: 'e', run: act(() => body.pop(reducedMotion.matches)) }, - { label: 'Drop', key: 'd', run: act(() => body.drop()) }, - { - label: 'Poke', - key: 'p', - run: act(() => body.poke(Math.floor(Math.random() * mesh.count), 9)), - }, - ]); - const popButton = actions.element.querySelectorAll('button')[2]!; - popButton.title = 'Pop into soft pieces; press again to gather them'; - const squashButton = actions.element.querySelectorAll('button')[1]!; - squashButton.title = 'Hold to squash; release to rebound'; - squashButton.addEventListener( - 'pointerdown', - (event) => { - if (event.button !== 0) return; - squashButton.setPointerCapture(event.pointerId); - act(() => body.squash(true))(); - }, - { signal: stage.signal }, - ); - for (const event of ['pointerup', 'pointercancel', 'lostpointercapture']) - squashButton.addEventListener(event, () => body.endSquash(), { signal: stage.signal }); - const world = stage.section('Physics'); - world.toggle( - 'Wind', - 'w', - false, - waking((on: boolean) => (controls.wind = on)), - ); - world.toggle('Orbit', 'o', false, (on) => (space.orbit = on && !reducedMotion.matches ? 12 : 0)); - world.choice( - 'Gravity', - [ - ['Space 0', 0], - ['Moon 1.6', 1.62], - ['Earth 9.8', 9.81], - ['Jupiter 24.8', 24.8], - ], - 9.81, - waking((g: number) => (controls.gravity = g)), - ); - world.speed([1, 0.5, 0.2], 1, (rate) => (timeScale = rate)); - const material = stage.section('Material'); - material.range( - 'Firmness', - { - min: 0, - max: 1, - step: 0.01, - value: 0.45, - format: (v) => (v < 0.2 ? 'Jelly' : v < 0.6 ? 'Gummy' : 'Rubber'), - }, - waking((v: number) => (controls.firmness = v)), - ); - material.range( - 'Inflate', - { min: 0.5, max: 1.8, step: 0.01, value: 1, format: (v) => `${Math.round(v * 100)}%` }, - waking((v: number) => (controls.inflate = v)), - ); - const legend = stage.legend(); - const repaint = (p: string) => { - paint = p; - space.scene = room(playback.mode === 'live'); - legend.ramp(PAINTS[p]!.colormap, PAINTS[p]!.legend); - legend.hidden = p === 'solid'; - }; - material.toggle('Mesh', 'i', false, (on) => { - inspect = on; - space.scene = room(playback.mode === 'live'); - }); - material.choice( - 'Color', - Object.entries(PAINTS).map(([key, p]) => [p.label, key] as const), - paint, - repaint, - ); - const light = stage.section('Light', false); - light.range( - 'Shading', - { min: 0, max: 1, step: 0.05, value: lit.shading, format: (v) => `${Math.round(v * 100)}%` }, - (shading) => { - lit.shading = shading; - space.scene = room(playback.mode === 'live'); - }, - ); - light.range( - 'Mesh dots', - { min: 0.5, max: 4, step: 0.25, value: lit.dots, format: (v) => `${v.toFixed(2)} px` }, - (dots) => { - lit.dots = dots; - space.scene = room(playback.mode === 'live'); - }, - ); - light.choice( - 'Floor', - [ - ['Lines', 'lines'], - ['Dots', 'dots'], - ['Bare', 'off'], - ] as const, - lit.grid, - (grid) => { - lit.grid = grid; - space.scene = room(playback.mode === 'live'); - }, - ); - - const monitor = stage.section('History'); - monitor.series(SERIES); - const plot = monitor.view( - { - space: { plot: { x: { label: 'Time (s)' }, y: { label: 'm · m/s · volume' } } }, - theme: THEME, - controls: 'inspect', - marks: SERIES.map((s) => ({ - lines: { of: ['bunny', 'Body'], trace: s.field, color: s.color, width: 2 }, - })), - }, - { label: 'Center height, speed, and volume', height: 220 }, - ); - plot.sources = [data]; - - const playback = stage.playback({ - replay: { seconds: 5, rate: 0.3 }, - show(at) { - const pieces = data.get('Body', 0, 'phase', { at }) === 1; - if (pieces !== fragments) { - fragments = pieces; - space.scene = room(playback.mode === 'live'); - } - space.at = at; - plot.at = at; - const x0 = Math.max(playback.start, at - WINDOW * 0.85); - plot.camera = { x: [x0, x0 + WINDOW], y: null }; - }, - change(live) { - // Outside live mode, a press pans instead of grabbing. - space.scene = room(live); - if (!live) { - body.release(true); - body.endSquash(); - } - }, - }); - - const frame = body.frame(); - const chunkFrame = body.burst.frame(); - const shadow = { - x: new Float32Array(body.burst.chunks.length + 1), - y: new Float32Array(body.burst.chunks.length + 1), - radius: new Float32Array(body.burst.chunks.length + 1), - opacity: new Float32Array(body.burst.chunks.length + 1), - }; - let recordedChunks = false; - let wasBurst = false; - const publish = () => { - body.sample(frame); - const [height, speed, volume] = body.metrics(); - shadow.opacity.fill(0); - if (body.burst.active) { - body.burst.chunks.forEach((chunk, i) => { - shadow.x[i + 1] = chunk.position[0]; - shadow.y[i + 1] = chunk.position[1]; - shadow.radius[i + 1] = chunk.radius + chunk.position[2] * 0.12; - shadow.opacity[i + 1] = 0.32 / (1 + chunk.position[2]); - }); - } else { - shadow.x[0] = frame.x.reduce((sum, v) => sum + v, 0) / mesh.count; - shadow.y[0] = frame.y.reduce((sum, v) => sum + v, 0) / mesh.count; - shadow.radius[0] = 0.85 * Math.cbrt(controls.inflate) + height * 0.15; - shadow.opacity[0] = 0.42 / (1 + height * height * 0.4); - } - const patch = new Patch() - .append('Vertex', body.time, { ...frame }) - .append('Shadow', body.time, shadow) - .append('Body', body.time, { - height: [height], - speed: [speed], - volume: [Number.isFinite(volume) ? volume : null], - phase: [body.burst.active ? 1 : 0], - }) - .forget(body.time - HISTORY); - if (body.burst.active || wasBurst || !recordedChunks) { - body.burst.sample(chunkFrame, STEP); - patch.append('ChunkVertex', body.time, { ...chunkFrame }); - recordedChunks = true; - } - wasBurst = body.burst.active; - data.apply(patch); - playback.record(data.describe().span!); - popButton.firstChild!.textContent = body.burst.active ? 'Gather' : 'Pop'; - }; - - space.on('drag', ({ phase, hit, position }) => { - if (phase === 'start') body.grab(hit.row.id as number, position); - else if (phase === 'move') body.drag(position); - else body.release(); - }); - space.on('click', ({ hit }) => act(() => body.poke(hit.row.id as number))()); - - const canvas = stage.regions.main.querySelector('canvas')!; - // The view's drag event reports release and cancellation alike. Catch cancellation before the - // view's bubbling listener, so a cancelled drag does not fling the bunny. - canvas.addEventListener('pointercancel', () => body.release(true), { - capture: true, - signal: stage.signal, - }); - addEventListener( - 'blur', - () => { - body.release(true); - body.endSquash(); - }, - { signal: stage.signal }, - ); - - const meter = stage.meter(space); - meter.note(() => { - const at = playback.at; - const height = (data.get('Body', 0, 'height', { at }) as number | null) ?? 0; - const speed = (data.get('Body', 0, 'speed', { at }) as number | null) ?? 0; - const volume = data.get('Body', 0, 'volume', { at }) as number | null; - const state = - playback.mode === 'live' ? body.state : playback.mode === 'playing' ? 'replay' : 'paused'; - return `${state} · center ${height.toFixed(2)} m · ${speed.toFixed(1)} m/s · ${volume !== null && Number.isFinite(volume) ? `volume ${(volume * 100).toFixed(0)}%` : '24 pieces'}`; - }); - stage.frame((dt) => { - if (playback.mode !== 'live') return; - // A resting bunny takes no steps and records nothing, so both views sleep until something - // acts on it. - if (body.asleep) { - carry = 0; - return; - } - meter.measure('simulate', () => { - carry = Math.min(carry + dt * timeScale, 16 * STEP); - const steps = body.advance(carry, controls); - carry -= steps * STEP; - // Record at a fixed rate of simulated time, and once more when the body comes to rest. - if (steps && (body.time + STEP / 2 >= recordAt || body.asleep)) { - publish(); - recordAt = body.time + RECORD; - } - }); - }); - - stage.detail( - `${mesh.count.toLocaleString()} vertices · ${mesh.springs.a.length.toLocaleString()} springs · ${mesh.faces.a.length.toLocaleString()} faces`, - ); - legend.ramp(PAINTS[paint]!.colormap, PAINTS[paint]!.legend); - legend.hidden = true; - body.drop(true); - publish(); -} diff --git a/npm/examples/src/main.ts b/npm/examples/src/main.ts index 14ef6329..1be08660 100644 --- a/npm/examples/src/main.ts +++ b/npm/examples/src/main.ts @@ -1,6 +1,10 @@ -import { Gpu } from 'latkit'; -import { examples } from './examples/index.js'; -import { Shell } from './ui/shell.js'; -import './ui/style.css'; +import { start } from './shell/shell.js'; +import './shell/style.css'; -new Shell(examples, Gpu.create()); +start([ + { id: 'grids', title: 'Power grids', load: () => import('./pages/grids/index.js') }, + { id: 'bunny', title: 'Soft body', load: () => import('./pages/bunny/index.js') }, + { id: 'orbits', title: 'Orbits', load: () => import('./pages/orbits.js') }, + { id: 'telemetry', title: 'Telemetry', load: () => import('./pages/telemetry.js') }, + { id: 'diagram', title: 'Diagram', load: () => import('./pages/diagram.js') }, +]); diff --git a/npm/examples/src/examples/soft-body/body.test.ts b/npm/examples/src/pages/bunny/body.test.ts similarity index 100% rename from npm/examples/src/examples/soft-body/body.test.ts rename to npm/examples/src/pages/bunny/body.test.ts diff --git a/npm/examples/src/examples/soft-body/body.ts b/npm/examples/src/pages/bunny/body.ts similarity index 100% rename from npm/examples/src/examples/soft-body/body.ts rename to npm/examples/src/pages/bunny/body.ts diff --git a/npm/examples/src/examples/soft-body/burst.ts b/npm/examples/src/pages/bunny/burst.ts similarity index 100% rename from npm/examples/src/examples/soft-body/burst.ts rename to npm/examples/src/pages/bunny/burst.ts diff --git a/npm/examples/src/examples/soft-body/fracture.ts b/npm/examples/src/pages/bunny/fracture.ts similarity index 100% rename from npm/examples/src/examples/soft-body/fracture.ts rename to npm/examples/src/pages/bunny/fracture.ts diff --git a/npm/examples/src/pages/bunny/index.ts b/npm/examples/src/pages/bunny/index.ts new file mode 100644 index 00000000..48c8dbb7 --- /dev/null +++ b/npm/examples/src/pages/bunny/index.ts @@ -0,0 +1,268 @@ +/** + * A soft bunny made of springs, in a room. Hop, squash, poke, fling, or pop it, then replay or scrub + * its history: every frame of every vertex is recorded, and the views read it back at the + * timeline's time. Faces darken as they turn from the light, above soft contact shadows drawn by a + * custom WGSL marker. A plot traces the bunny's height and speed. + */ +import { Data, field, reference, units, type Scene } from 'latkit'; +import { choice, page } from '../../shell/page.js'; +import { Body, ROOM, STEP } from './body.js'; +import { loadMesh } from './mesh.js'; + +/** Simulated seconds between recorded frames, whatever the display's refresh rate, and the + * seconds the plot shows. */ +const RECORD = 1 / 60; +const WINDOW = 12; +/** The material and the world the bunny lives in. */ +const WORLD = { gravity: 9.81, firmness: 0.45, inflate: 1, wind: false }; +/** Each coloring: the field it reads, through which colormap, over which values. */ +const PAINTS = { + speed: { field: 'speed', colormap: 'plasma', domain: [-1, 9], legend: 'Speed (m/s)' }, + stress: { + field: 'strain', + colormap: 'turbo', + domain: [-0.05, 0.35], + legend: 'Stress (m from rest)', + }, +} as const; +const SERIES = [ + { field: 'height', label: 'Center height (m)', color: '#66c7ff' }, + { field: 'speed', label: 'Mean speed (m/s)', color: '#ff8f52' }, +]; +/** A soft contact shadow, darker the lower the body above it: a custom marker in WGSL that reads + * its darkness from each row's `opacity`. */ +const SHADOW = `fn marker(f: MarkerFragment) -> MarkerColor { + let d = length(f.p / max(f.half, vec2f(0.001))); + let alpha = (1.0 - smoothstep(0.15, 1.0, d)) * f.input[0]; + return MarkerColor(vec4f(0.0, 0.0, 0.0, alpha), (d - 1.0) * f.half.x); +}`; + +export default page({ + title: 'Soft body', + inspect: false, + settings: { paint: choice('Color', { Speed: 'speed', Stress: 'stress' }, { key: 'c' }) }, + + async open({ settings, signal, view, actions, timeline, frame, series, detail }) { + const mesh = await loadMesh(new URL('./stanford-bunny.bin', import.meta.url)); + const body = new Body(mesh); + const { burst } = body; + const pieces = burst.chunks.length; + // The mesh, its pieces once popped, and the room are set once; each recorded frame then + // appends every vertex, the shadows, and the body as a whole. + const data = new Data('bunny') + .axis('time', 's') + .set('Vertex', { count: mesh.count }) + .set('Spring', { + a: reference('Vertex', mesh.springs.a), + b: reference('Vertex', mesh.springs.b), + }) + .set('Face', { + a: reference('Vertex', mesh.faces.a), + b: reference('Vertex', mesh.faces.b), + c: reference('Vertex', mesh.faces.c), + }) + .set('ChunkVertex', { count: burst.count }) + .set('ChunkFace', { + a: reference('ChunkVertex', burst.faces.a), + b: reference('ChunkVertex', burst.faces.b), + c: reference('ChunkVertex', burst.faces.c), + }) + .set('Corner', { x: [-ROOM, ROOM, ROOM, -ROOM], y: [-ROOM, -ROOM, ROOM, ROOM] }) + .set('Wall', { + from: reference('Corner', [0, 1, 2, 3]), + to: reference('Corner', [1, 2, 3, 0]), + }) + .set('Shadow', { count: pieces + 1 }) + .set('Body', { count: 1 }); + + let popped = false; + let live = true; + const room = (s: typeof settings): Scene => { + const paint = PAINTS[s.paint]; + return { + space: { plane: { grid: { lines: { pitch: 1 } } } }, + theme: { + base: 'dark', + background: '#101214', + grid: '#ffffff1c', + shading: 0.7, + vertex: { radius: 0 }, + }, + controls: { pickRadius: 14 }, + marks: [ + { + vertices: data.table('Shadow'), + position: ['$x', '$y'], + size: [units(field('radius').mul(2)), units(field('radius'))], + marker: { wgsl: { source: SHADOW, inputs: [field('opacity')] } }, + color: '#000000', + pick: false, + }, + { + vertices: data.table('Corner'), + position: ['$x', '$y'], + radius: 3, + color: '#f09478aa', + pick: false, + }, + { + edges: data.table('Wall'), + ends: ['from', 'to'], + color: '#f0947866', + width: 1.5, + pick: false, + }, + // Faces without a color blend their corners' colors. + { + faces: data.table(popped ? 'ChunkFace' : 'Face'), + corners: ['a', 'b', 'c'], + pick: false, + }, + { + vertices: data.table(popped ? 'ChunkVertex' : 'Vertex'), + position: ['$x', '$y'], + elevation: '$z', + color: field(paint.field) + .colormap(paint.colormap) + .domain(paint.domain[0], paint.domain[1]), + legend: paint.legend, + select: false, + // Outside live mode, a press pans instead of grabbing. + drag: live && !popped, + pick: !popped, + }, + ], + }; + }; + const space = await view(room, { + region: [0, 0, 1, 0.74], + camera: { center: [0, 0], zoom: 6, pitch: 58, bearing: 30 }, + label: 'The bunny: drag it to fling it, click it to poke it', + }); + // The room fills the view, seen from the camera's angle. + space.fit({ bounds: { min: [-ROOM, -ROOM, 0], max: [ROOM, ROOM, 2.4] } }); + const plot = await view( + () => ({ + space: { plot: { x: { label: 'Time (s)' } } }, + theme: { base: 'dark', background: '#101214' }, + controls: 'inspect', + ease: 0, + marks: SERIES.map((s) => ({ + lines: data.table('Body'), + trace: s.field, + color: s.color, + width: 2, + })), + }), + { region: [0, 0.74, 1, 0.26], camera: { x: [0, WINDOW] } }, + ); + series('History', SERIES); + const redraw = () => (space.scene = room(settings)); + const letGo = () => { + body.release(true); + body.endSquash(); + }; + + const recording = timeline({ + data, + keep: 20, + replay: { seconds: 5, rate: 0.3 }, + // The plot keeps the time shown near its right edge. While a replay passes a pop, the pieces + // stand in for the whole body. + show(at) { + const from = Math.max(0, at - 0.85 * WINDOW); + plot.camera = { x: [from, from + WINDOW] }; + const was = data.get('Body', 0, 'popped', { at }) === 1; + if (was === popped) return; + popped = was; + redraw(); + }, + change(now) { + live = now; + if (!live) letGo(); + redraw(); + }, + }); + + /** Runs an action live, going live first if the timeline was replaying or paused. */ + const act = (run: () => void) => () => { + recording.play(); + run(); + }; + actions('Actions', [ + { label: 'Hop', key: ' ', run: act(() => body.hop()) }, + { + label: 'Squash', + key: 's', + run: act(() => body.squash(true)), + release: () => body.endSquash(), + }, + { label: 'Pop', key: 'e', run: act(() => body.pop()) }, + { label: 'Drop', key: 'd', run: act(() => body.drop()) }, + ]); + space.on('drag', ({ phase, hit, position }) => { + if (phase === 'start') body.grab(Number(hit.row.id), position); + else if (phase === 'move') body.drag(position); + else body.release(); + }); + space.on('click', ({ hit }) => act(() => body.poke(Number(hit.row.id)))()); + addEventListener('blur', letGo, { signal }); + + // Each recorded frame: every vertex, the pieces while popped, a shadow under the body or under + // each piece, and the body's height and speed. + const vertices = body.frame(); + const chunks = burst.frame(); + const shadow = { + x: new Float32Array(pieces + 1), + y: new Float32Array(pieces + 1), + radius: new Float32Array(pieces + 1), + opacity: new Float32Array(pieces + 1), + }; + const record = () => { + body.sample(vertices); + const [height, speed] = body.metrics(); + shadow.opacity.fill(0); + if (burst.active) { + burst.chunks.forEach(({ position: [x, y, z], radius }, i) => { + [shadow.x[i + 1], shadow.y[i + 1]] = [x, y]; + shadow.radius[i + 1] = radius + z * 0.12; + shadow.opacity[i + 1] = 0.32 / (1 + z); + }); + } else { + shadow.x[0] = vertices.x.reduce((sum, v) => sum + v, 0) / mesh.count; + shadow.y[0] = vertices.y.reduce((sum, v) => sum + v, 0) / mesh.count; + shadow.radius[0] = 0.85 + height * 0.15; + shadow.opacity[0] = 0.42 / (1 + height * height * 0.4); + } + burst.sample(chunks, STEP); + data + .append('Vertex', body.time, { ...vertices }) + .append('ChunkVertex', body.time, { ...chunks }) + .append('Shadow', body.time, shadow) + .append('Body', body.time, { + height: [height], + speed: [speed], + popped: [burst.active ? 1 : 0], + }); + }; + + // A resting bunny takes no steps and records nothing, so both views sleep until something acts. + let carry = 0; + let next = 0; + frame((dt) => { + if (body.asleep) return void (carry = 0); + carry = Math.min(carry + dt, 16 * STEP); + const steps = body.advance(carry, WORLD); + carry -= steps * STEP; + if (steps && (body.time + STEP / 2 >= next || body.asleep)) { + record(); + next = body.time + RECORD; + } + }); + detail( + `${mesh.count.toLocaleString()} vertices · ${mesh.springs.a.length.toLocaleString()} springs · ${mesh.faces.a.length.toLocaleString()} faces`, + ); + body.drop(true); + record(); + }, +}); diff --git a/npm/examples/src/examples/soft-body/mesh.ts b/npm/examples/src/pages/bunny/mesh.ts similarity index 100% rename from npm/examples/src/examples/soft-body/mesh.ts rename to npm/examples/src/pages/bunny/mesh.ts diff --git a/npm/examples/src/examples/soft-body/stanford-bunny.bin b/npm/examples/src/pages/bunny/stanford-bunny.bin similarity index 100% rename from npm/examples/src/examples/soft-body/stanford-bunny.bin rename to npm/examples/src/pages/bunny/stanford-bunny.bin diff --git a/npm/examples/src/pages/diagram.ts b/npm/examples/src/pages/diagram.ts new file mode 100644 index 00000000..d9b8e6e6 --- /dev/null +++ b/npm/examples/src/pages/diagram.ts @@ -0,0 +1,267 @@ +/** + * A block diagram on a sheet: blocks with ports, wires routed around them, and groups drawn around + * their members. Each block kind is a table whose reference fields name the signal on each port, and + * an edges mark of the signals draws them as wires. Drag blocks, draw wires from an output to an + * input, delete with the Delete key, arrange, and undo. + */ +import { + Data, + field, + reference, + type Id, + type Mark, + type Port, + type Reference, + type Row, + type Snapshot, + type Values, +} from 'latkit'; +import { choice, page, toggle } from '../shell/page.js'; + +/** The block kinds: each one's marker fill and its ports, as name, side, and whether it drives. */ +const KINDS = { + Input: { fill: '#173836', ports: [['out', 'right', true]] }, + Process: { + fill: '#1c2b3f', + ports: [ + ['in', 'left', false], + ['out', 'right', true], + ], + }, + Control: { + fill: '#3a2e18', + ports: [ + ['in', 'left', false], + ['feedback', 'bottom', false], + ['out', 'right', true], + ], + }, + Output: { fill: '#29243f', ports: [['in', 'left', false]] }, +} as const; +type Kind = keyof typeof KINDS; +const kinds = Object.keys(KINDS) as Kind[]; + +/** A block: its kind, name, group, and the signal on each of its ports. */ +interface Block { + kind: Kind; + name: string; + group?: number; + plugs: Record; +} + +/** Writes blocks, the signals that wire them, and the groups around them into a dataset. Blocks + * have no position, so the layout places them. */ +function sheet(blocks: Block[], signals: string[], groups: string[]): Data { + const data = new Data('sheet').set('Group', { name: groups }).set('Signal', { name: signals }); + for (const kind of kinds) { + const mine = blocks.filter((b) => b.kind === kind); + const fields: Record = { + name: mine.map((b) => b.name), + x: mine.map(() => null), + y: mine.map(() => null), + }; + for (const [port] of KINDS[kind].ports) + fields[port] = reference( + 'Signal', + mine.map((b) => b.plugs[port] ?? null), + ); + if (kind === 'Process') + fields.group = reference( + 'Group', + mine.map((b) => b.group ?? null), + ); + data.set(kind, fields); + } + return data; +} + +/** A control loop: a setpoint, a controller with feedback, and a plant of three stages. */ +function loop(): Data { + return sheet( + [ + { kind: 'Input', name: 'Setpoint', plugs: { out: 0 } }, + { kind: 'Control', name: 'PID', plugs: { in: 0, feedback: 4, out: 1 } }, + { kind: 'Process', name: 'Actuator', group: 0, plugs: { in: 1, out: 2 } }, + { kind: 'Process', name: 'Plant', group: 0, plugs: { in: 2, out: 3 } }, + { kind: 'Process', name: 'Sensor', group: 0, plugs: { in: 3, out: 4 } }, + { kind: 'Output', name: 'Scope', plugs: { in: 3 } }, + ], + ['setpoint', 'command', 'force', 'output', 'measured'], + ['Plant'], + ); +} + +/** Twenty lines of a production floor, each an input through five stages in its own group. */ +function floor(): Data { + const blocks: Block[] = []; + const signals: string[] = []; + const groups: string[] = []; + for (let line = 0; line < 20; line++) { + groups.push(`Line ${line + 1}`); + let signal = signals.push(`feed ${line + 1}`) - 1; + blocks.push({ kind: 'Input', name: `Feed ${line + 1}`, plugs: { out: signal } }); + for (let stage = 0; stage < 5; stage++) { + const out = signals.push(`${line + 1}.${stage + 1}`) - 1; + blocks.push({ + kind: 'Process', + name: `Stage ${stage + 1}`, + group: line, + plugs: { in: signal, out }, + }); + signal = out; + } + blocks.push({ kind: 'Output', name: `Bin ${line + 1}`, plugs: { in: signal } }); + } + return sheet(blocks, signals, groups); +} + +export default page({ + title: 'Diagram', + settings: { + preset: choice('Diagram', { 'Control loop': 'loop', 'Production floor': 'floor' }), + layout: choice('Layout', { + 'Left to right': 'right', + 'Top to bottom': 'down', + Stress: 'stress', + }), + route: choice('Wires', { Orthogonal: 'orthogonal', Straight: 'straight' }), + flow: toggle('Flow', { key: 'f' }), + }, + + async open({ settings, view, actions, detail }) { + let data = settings.preset === 'loop' ? loop() : floor(); + const shown = await view((s) => ({ + space: { diagram: { grid: { dots: { pitch: 20 } } } }, + layout: + s.layout === 'stress' + ? { stress: { gap: 50 } } + : { layered: { direction: s.layout, gap: 30, rankGap: 70 } }, + theme: { base: 'dark', background: '#101214', diagram: { outline: '#ffffff1f' } }, + controls: { connect: true, delete: true }, + marks: [ + { + vertices: data.table('Group'), + contains: ['Process', 'group'], + label: { text: '$name', size: 11, color: '#7f93a8' }, + shadow: true, + }, + { + edges: data.table('Signal'), + route: s.route, + arrows: true, + width: 1.5, + dash: field('name').is('measured'), + flow: s.flow ? 40 : 0, + }, + ...kinds.map((kind): Mark => ({ + vertices: data.table(kind), + position: ['$x', '$y'], + marker: kind === 'Input' ? 'rounded' : 'block', + color: KINDS[kind].fill, + shadow: true, + drag: true, + label: { text: '$name', size: 12 }, + ports: KINDS[kind].ports.map(([name, side, output]): [string, Port] => [ + name, + { side, output }, + ]), + })), + ], + })); + + // Every edit can be undone: each one keeps the version before it, which shares its pages with + // the data, so the history costs only what changed. + const undo: Snapshot[] = []; + const redo: Snapshot[] = []; + const edit = (change: () => void) => { + undo.push(data.snapshot()); + redo.splice(0).forEach((s) => s.destroy()); + change(); + }; + const step = (from: Snapshot[], to: Snapshot[]) => { + const back = from.pop(); + if (!back) return; + to.push(data.snapshot()); + data.restore(back); + back.destroy(); + count(); + }; + const count = () => { + const tables = data.describe().tables; + const rows = (names: string[]) => + tables.filter((t) => names.includes(t.name)).reduce((n, t) => n + t.rows, 0); + detail( + `${rows(kinds).toLocaleString()} blocks · ${rows(['Signal']).toLocaleString()} signals`, + ); + }; + count(); + + // A drag moves a block at once and is undone as one edit. + let dragging = false; + shown.on('drag', ({ phase, hit, position: [x, y] }) => { + const move = () => data.set(hit.row.table, [hit.row.id], { x: [x], y: [y] }); + if (phase === 'start' && !dragging) { + dragging = true; + edit(move); + } else move(); + if (phase === 'end') dragging = false; + }); + + // A wire joins an output to an input on the output's signal, making one if the output has none. + shown.on('connect', ({ from, to }) => { + if (!to || !(from.row.table in KINDS) || !(to.row.table in KINDS)) return; + const port = (row: Row, drives: boolean, named?: string) => + KINDS[row.table as Kind].ports.find( + ([name, , output]) => output === drives && (!named || name === named), + )?.[0]; + const output = port(from.row, true, from.port) ? from : to; + const input = output === from ? to : from; + const out = port(output.row, true, output.port); + const into = port(input.row, false, input.port); + if (!out || !into) return; + edit(() => { + // A reference field reads as the id of the row it references. + let signal = data.get(output.row.table, output.row.id, out) as Id | null; + if (signal === null) { + signal = Math.max(-1, ...data.query({ of: 'Signal', select: {} }).ids.map(Number)) + 1; + data.set('Signal', [signal], { name: [`signal ${signal}`] }); + data.set(output.row.table, [output.row.id], { [out]: reference('Signal', [signal]) }); + } + data.set(input.row.table, [input.row.id], { [into]: reference('Signal', [signal]) }); + }); + count(); + }); + shown.on('delete', ({ rows }) => { + edit(() => rows.forEach(({ table, ids }) => data.remove(table, ids))); + count(); + }); + + actions('Edit', [ + { label: 'Undo', key: 'z', run: () => step(undo, redo) }, + { label: 'Redo', key: 'y', run: () => step(redo, undo) }, + { + label: 'Arrange', + key: 'a', + // The layout places every block again and writes the places into its x and y fields. + run: () => { + void shown.arrange().then((patches) => edit(() => patches.forEach((p) => data.apply(p)))); + }, + }, + { label: 'Fit', key: 'home', run: () => shown.fit() }, + ]); + + return { + change(s, key) { + if (key !== 'preset') return; + const old = data; + data = s.preset === 'loop' ? loop() : floor(); + [...undo.splice(0), ...redo.splice(0)].forEach((snapshot) => snapshot.destroy()); + return () => { + old.destroy(); + shown.fit(); + count(); + }; + }, + }; + }, +}); diff --git a/npm/examples/src/examples/networks/cases.test.ts b/npm/examples/src/pages/grids/cases.test.ts similarity index 100% rename from npm/examples/src/examples/networks/cases.test.ts rename to npm/examples/src/pages/grids/cases.test.ts diff --git a/npm/examples/src/examples/networks/cases.ts b/npm/examples/src/pages/grids/cases.ts similarity index 98% rename from npm/examples/src/examples/networks/cases.ts rename to npm/examples/src/pages/grids/cases.ts index ece0728a..a0de8542 100644 --- a/npm/examples/src/examples/networks/cases.ts +++ b/npm/examples/src/pages/grids/cases.ts @@ -82,6 +82,7 @@ export function readCase(id: string, buffer: ArrayBuffer, downloaded = buffer.by { lon: Float64Array.from(c.machineBus, (b) => c.lon[b]!), lat: Float64Array.from(c.machineBus, (b) => c.lat[b]!), + kv: Float64Array.from(c.machineBus, (b) => c.kv[b]!), name: c.names('machine.name'), power: machinePower, mva: c.array('machine.mva', Float32Array), diff --git a/npm/examples/src/examples/networks/continent.ts b/npm/examples/src/pages/grids/continent.ts similarity index 100% rename from npm/examples/src/examples/networks/continent.ts rename to npm/examples/src/pages/grids/continent.ts diff --git a/npm/examples/src/examples/networks/format.ts b/npm/examples/src/pages/grids/format.ts similarity index 100% rename from npm/examples/src/examples/networks/format.ts rename to npm/examples/src/pages/grids/format.ts diff --git a/npm/examples/src/pages/grids/index.ts b/npm/examples/src/pages/grids/index.ts new file mode 100644 index 00000000..8d9a7637 --- /dev/null +++ b/npm/examples/src/pages/grids/index.ts @@ -0,0 +1,217 @@ +/** + * Power grids on the earth: synthetic cases from WECC 240 buses to ACTIVSg70k, and a grid of North + * America's two million buses built in the page, over coasts and borders, states, or + * OpenStreetMap. Color buses by voltage level, load, or generation, raise them by their voltage, or + * lay the grid out by its topology instead of its geography and watch every bus ease to its place. + */ +import { field, when, zoom, type Mark, type Scene } from 'latkit'; +import { choice, page, toggle } from '../../shell/page.js'; +import { extent, loadCase, megabytes, type Case } from './cases.js'; +import { CONTINENTS, continent } from './continent.js'; +import { LEVELS } from './format.js'; +import { LOOK, MAPS, NO_MAP, type Basemap } from './maps.js'; + +/** Each voltage level's color, from below 100 kV to 765 kV. */ +const LEVEL_COLORS = ['#56657a', '#2fa49b', '#7fc97f', '#e6c84f', '#f08a3c', '#e5486f']; +/** Noon at midsummer over the western US, in seconds since the epoch. */ +const NOON = Date.UTC(2026, 5, 21, 19) / 1000; + +/** A grid's case and its largest load and generation, which their colormaps span. */ +interface Grid extends Case { + readonly peak: { readonly load: number; readonly generation: number }; +} + +/** Loads case `id`, or builds the synthetic continent, reporting progress to `detail`. */ +async function load( + id: string, + signal: AbortSignal, + detail: (text: string) => void, +): Promise { + detail(`Loading ${id}…`); + const found = + id in CONTINENTS + ? continent(id) + : await loadCase(id, signal, (got, total) => + detail(`Loading ${id}, ${megabytes(got, total)}`), + ); + const peak = (values: Float32Array) => values.reduce((most, v) => Math.max(most, v), 1); + detail( + `${found.name} · ${found.buses.toLocaleString()} buses · ${found.lines.toLocaleString()} lines`, + ); + return { ...found, peak: { load: peak(found.load), generation: peak(found.generation) } }; +} + +export default page({ + title: 'Power grids', + settings: { + grid: choice( + 'Grid', + { + 'WECC 240': 'wecc240', + ACTIVSg2000: 'activsg2000', + ACTIVSg10k: 'activsg10k', + ACTIVSg25k: 'activsg25k', + ACTIVSg70k: 'activsg70k', + 'North America · 2M': 'continent-2m', + }, + { value: 'activsg25k' }, + ), + color: choice('Color', { 'Voltage level': 'level', Load: 'load', Generation: 'generation' }), + height: choice('Height', { Flat: 'flat', 'Voltage (kV)': 'kv' }, { key: 'h' }), + layout: choice('Layout', { Geography: 'geography', Topology: 'topology' }, { key: 't' }), + labels: toggle('Labels', { value: true, key: 'l' }), + comets: toggle('Comets', { key: 'c' }), + map: choice( + 'Map', + { + 'Coasts and borders': 'borders', + 'States and provinces': 'states', + OpenStreetMap: 'planet', + None: 'none', + }, + { section: 'Earth' }, + ), + globe: toggle('Globe', { value: true, key: 'g', section: 'Earth' }), + light: choice('Light', { Noon: 'noon', Now: 'now', Even: 'even' }, { section: 'Earth' }), + }, + + async open({ settings, signal, view, detail, credit, fail }) { + let grid = await load(settings.grid, signal, detail); + // The map joins the scene once its datasets are read, so the grid draws first. + let map: Basemap = NO_MAP; + + const scene = (s: typeof settings): Scene => { + const geography = s.layout === 'geography'; + const data = grid.data; + const big = grid.buses > 20_000; + const level = field('level').colormap({ + kind: 'categorical', + colors: LEVEL_COLORS, + categories: [...LEVELS], + }); + // On the earth, height is a fraction of its radius: 765 kV stands about 140 km high. Laid out + // by topology, it is in the layout's units. + const height = s.height === 'kv' ? field('kv').mul(geography ? 2.5e-5 : 0.15) : 0; + const where = geography ? { position: ['$lon', '$lat'] as ['$lon', '$lat'] } : {}; + const marks: Mark[] = [ + ...(geography ? map.marks : []), + { + edges: data.table('Line'), + ends: ['from', 'to'], + // Higher-voltage lines draw on top, and stay visible as the camera zooms out. + priority: '$level', + color: level, + width: field('level') + .scale(big ? 0.5 : 0.8, big ? 1.8 : 2.8) + .domain(0, 5), + dash: '$transformer', + flow: s.comets ? field('level').mul(6).add(8) : 0, + }, + { + vertices: data.table('Bus'), + ...where, + elevation: height, + priority: '$level', + color: + s.color === 'level' + ? level + : field(s.color) + .colormap(s.color === 'load' ? 'mako' : 'amber') + .domain(0, grid.peak[s.color]), + radius: big ? field('level').scale(0.8, 2.6).domain(0, 5) : grid.buses > 5000 ? 2 : 3, + legend: + s.color === 'level' + ? 'Voltage level' + : s.color === 'load' + ? 'Load (MW)' + : 'Generation (MW)', + // A crowded grid names its buses only once zoomed in. + ...(s.labels && grid.named + ? { + label: { + text: when(zoom().ge(geography ? 5.5 : 3)) + .then(field('name')) + .otherwise(null), + size: 10, + }, + } + : {}), + }, + ]; + if (geography && grid.machines) + marks.push({ + vertices: data.table('Machine'), + position: ['$lon', '$lat'], + elevation: height, + marker: 'diamond', + color: '#ebf0f7', + radius: field('power') + .scale(big ? 2 : 3, big ? 5 : 8) + .domain(0, grid.peak.generation), + }); + return { + space: geography + ? { + earth: { + projection: s.globe ? 'globe' : 'mercator', + graticule: false, + atmosphere: s.globe, + ...(s.light === 'even' + ? {} + : { daylight: s.light === 'now' ? Date.now() / 1000 : NOON }), + }, + } + : { plane: { grid: 'off' } }, + layout: geography ? 'none' : 'stress', + theme: { + base: 'dark', + background: LOOK.background, + area: { color: LOOK.fill }, + earth: { ground: LOOK.ground, atmosphere: '#5aa9ff99', night: 0.28 }, + edge: { bow: 0.12 }, + flow: { color: '#d6e6ffa6', width: 3, length: 20, spacing: 72 }, + }, + marks, + }; + }; + + /** Frames the grid's buses. */ + const frame = () => { + if (settings.layout !== 'geography') return shown.fit(); + const [west, south, east, north] = extent(grid.positions); + shown.fit({ bounds: { min: [west, south, 0], max: [east, north, 0] } }); + }; + /** Swaps in map `id` once its datasets are read, unless another map was chosen meanwhile. */ + const showMap = async (id: typeof settings.map) => { + const next = await MAPS[id](signal); + if (id !== settings.map || signal.aborted) { + for (const d of next.data) d.destroy(); + return; + } + const old = map; + map = next; + credit(map.credit); + shown.scene = scene(settings); + for (const d of old.data) d.destroy(); + }; + const shown = await view(scene, { label: 'A power grid: hover or click a bus or line' }); + frame(); + showMap(settings.map).catch(fail); + + return { + async change(s, key) { + if (key === 'grid') { + const next = await load(s.grid, signal, detail); + const old = grid; + grid = next; + return () => { + frame(); + old.data.destroy(); + }; + } + if (key === 'map') showMap(s.map).catch(fail); + if (key === 'layout') return frame; + }, + }; + }, +}); diff --git a/npm/examples/src/pages/grids/maps.ts b/npm/examples/src/pages/grids/maps.ts new file mode 100644 index 00000000..a8a6a3b4 --- /dev/null +++ b/npm/examples/src/pages/grids/maps.ts @@ -0,0 +1,107 @@ +/** + * The maps drawn beneath a grid: Natural Earth's coasts and borders, shipped with the page; Natural + * Earth's states and provinces, downloaded from its repository; and OpenStreetMap's planet as + * OpenFreeMap's vector tiles, fetched as the camera needs them. GeoJSON, plain or gzip-compressed, + * is read in a worker. + */ +import { Data, Tiles, field, when, zoom, type Mark } from 'latkit'; +import { download } from './cases.js'; + +/** The page's colors, as CSS. */ +export const LOOK = { + background: '#05070a', + ground: '#0e131b', + fill: '#1a212b', + line: '#6b80948c', + coast: '#8ca3b8d9', + landcover: '#1c2924', + park: '#1a3021', + water: '#0d1a2b', + waterway: '#2e5485e6', + road: '#5c616bb3', + place: '#c3ccd8', +} as const; + +/** A loaded map: the marks drawn beneath the grid, who to credit, and the datasets to free. */ +export interface Basemap { + readonly marks: readonly Mark[]; + readonly credit: string; + readonly data: readonly Data[]; +} + +/** No map: the earth's ground alone. */ +export const NO_MAP: Basemap = { credit: '', data: [], marks: [] }; + +const NATURAL_EARTH = 'Natural Earth · public domain'; +const NE = 'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/'; + +export const MAPS: Record< + 'borders' | 'states' | 'planet' | 'none', + (signal: AbortSignal) => Basemap | Promise +> = { + /** Coastlines, country borders, and province borders at 1:50M, as lines. */ + async borders(signal) { + const read = (name: string) => + Data.read(`${import.meta.env.BASE_URL}maps/${name}.geojson.gz`, { name, signal }); + const [coast, countries, provinces] = await Promise.all( + ['coastline', 'countries', 'provinces'].map(read), + ); + return { + credit: NATURAL_EARTH, + data: [coast!, countries!, provinces!], + marks: [ + { lines: provinces!.table('lines'), color: LOOK.line, width: 0.6, dash: true }, + { lines: countries!.table('lines'), color: LOOK.line, width: 1 }, + { lines: coast!.table('lines'), color: LOOK.coast, width: 1 }, + ], + }; + }, + + /** States and provinces at 1:50M, as areas named by their labels. */ + async states(signal) { + const bytes = await download(`${NE}ne_50m_admin_1_states_provinces.geojson`, signal); + const states = await Data.read(bytes, { name: 'states', signal }); + return { + credit: NATURAL_EARTH, + data: [states], + marks: [ + { + areas: states.table('polygons'), + color: LOOK.fill, + label: { text: '$name', size: 10, color: '#8e99aa' }, + }, + { lines: states.table('polygons'), color: LOOK.line, width: 0.7 }, + ], + }; + }, + + /** OpenStreetMap's planet, with rivers that carry comets downstream. */ + planet() { + const base = new Tiles('base', 'https://tiles.openfreemap.org/planet'); + return { + credit: 'OpenFreeMap · © OpenMapTiles · © OpenStreetMap contributors', + data: [], + marks: [ + { areas: base.table('landcover'), color: LOOK.landcover }, + { areas: base.table('park'), color: LOOK.park }, + { areas: base.table('water'), color: LOOK.water }, + { lines: base.table('waterway'), color: LOOK.waterway, width: 0.8, flow: 10 }, + { lines: base.table('transportation'), color: LOOK.road, width: 0.7 }, + { lines: base.table('boundary'), color: LOOK.line, width: 1, dash: true }, + // A place is named once the zoom passes its rank, where 1 is the most important. + { + vertices: base.table('place'), + color: LOOK.place, + radius: 1.5, + label: { + text: when(field('rank').lt(zoom())).then(field('name')).otherwise(null), + size: 11, + color: LOOK.place, + }, + }, + ], + }; + }, + + none: () => NO_MAP, +}; diff --git a/npm/examples/src/pages/orbits.ts b/npm/examples/src/pages/orbits.ts new file mode 100644 index 00000000..5d90b21c --- /dev/null +++ b/npm/examples/src/pages/orbits.ts @@ -0,0 +1,85 @@ +/** + * A spiral galaxy whose bodies orbit its center. Each body's position is an expression of the + * elapsed time, its radius, phase, and angular speed, evaluated on the GPU: once the data is + * uploaded, no frame writes a byte, however many bodies move. Inner bodies orbit faster than outer + * ones, so the arms wind as time passes. Near the pointer, bodies swell like a lens. + */ +import { Data, elapsed, field, pointer, type CustomColormap } from 'latkit'; +import { page, choice, toggle } from '../shell/page.js'; +import { random } from '../random.js'; + +/** Glowing colors, translucent where faint, so crowded bodies brighten one another. */ +const GLOW: CustomColormap = { + kind: 'sequential', + colors: ['#2a1b5c66', '#7b2cbf99', '#e0559dbb', '#ff9e64dd', '#fff1c9'], +}; + +/** Places `n` bodies in a disk with four arms: their radius, phase, angular speed, and height. */ +function galaxy(n: number) { + const next = random(7); + const gauss = () => Math.sqrt(-2 * Math.log(next() + 1e-12)) * Math.cos(2 * Math.PI * next()); + const r = new Float64Array(n); + const phase = new Float64Array(n); + const speed = new Float64Array(n); + const z = new Float64Array(n); + for (let i = 0; i < n; i++) { + r[i] = 0.3 + 12 * next() ** 1.6; + // Arms trail along a logarithmic spiral, with bodies scattered about them. + const arm = (i % 4) * (Math.PI / 2); + phase[i] = arm + 1.9 * Math.log(r[i]!) + 0.35 * gauss(); + // A flat rotation curve: the same orbital speed at every radius, so angular speed falls as 1/r. + speed[i] = 0.9 / (r[i]! + 0.6); + z[i] = 0.25 * gauss() * Math.exp(-r[i]! / 5); + } + return { r, phase, speed, z }; +} + +export default page({ + title: 'Orbits', + inspect: false, + settings: { + bodies: choice( + 'Bodies', + { '50 thousand': 50_000, '200 thousand': 200_000, '1 million': 1_000_000 }, + { value: 200_000 }, + ), + color: choice('Color', { 'Orbital speed': 'speed', Distance: 'r' }), + lens: toggle('Lens', { value: true, key: 'l' }), + }, + async open({ settings, view, detail }) { + const data = new Data('galaxy').set('Body', galaxy(settings.bodies)); + const angle = field('phase').add(field('speed').mul(elapsed())); + const shown = await view( + (s) => ({ + space: { plane: { grid: 'off' } }, + theme: { base: 'dark', background: '#05060b' }, + controls: { hover: false }, + marks: [ + { + vertices: data.table('Body'), + position: [field('r').mul(angle.cos()), field('r').mul(angle.sin())], + elevation: '$z', + // Up to three times as large right at the pointer, normal 120 pixels away. + radius: s.lens ? pointer().div(120).neg().add(1).max(0).mul(2).add(1).mul(1.3) : 1.3, + color: + s.color === 'speed' + ? field('speed').colormap(GLOW).domain(0.07, 1.2) + : field('r').neg().colormap(GLOW).domain(-12.3, -0.3), + pick: false, + }, + ], + }), + { camera: { center: [0, 0], zoom: 5, pitch: 52, bearing: 0 } }, + ); + // The disk fills the view, seen from the camera's angle. + shown.fit({ bounds: { min: [-12.5, -12.5, 0], max: [12.5, 12.5, 0] } }); + detail(`${settings.bodies.toLocaleString()} bodies, moved by the GPU`); + return { + change(s, key) { + if (key !== 'bodies') return; + data.replace('Body', galaxy(s.bodies)); + detail(`${s.bodies.toLocaleString()} bodies, moved by the GPU`); + }, + }; + }, +}); diff --git a/npm/examples/src/pages/telemetry.ts b/npm/examples/src/pages/telemetry.ts new file mode 100644 index 00000000..83a0f68f --- /dev/null +++ b/npm/examples/src/pages/telemetry.ts @@ -0,0 +1,161 @@ +/** + * Telemetry: thousands of streaming voltages traced over a scrolling window. Frames are appended as + * they arrive and the oldest are forgotten; pause or scrub the timeline to read back what passed. + * Every 20 seconds one area dips and recovers, or strike one yourself, and query for the signals + * hit hardest. Or draw a recorded day instead, fetched span by span as time tiles at the detail the + * plot needs. + */ +import { Data, Tiles, field, selected, when, type CustomColormap } from 'latkit'; +import { choice, page } from '../shell/page.js'; +import { random } from '../random.js'; + +const AREAS = ['North', 'South', 'East', 'West', 'Coast', 'Valley']; +const BY_AREA: CustomColormap = { + kind: 'categorical', + colors: ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948'], + categories: AREAS, +}; +/** Samples per second, seconds kept, seconds shown, and seconds between automatic dips. */ +const RATE = 50; +const KEEP = 60; +const WINDOW = 20; +const EVERY = 20; + +/** How far a dip has pulled voltages down, `since` seconds after it struck; 0 after six seconds. */ +const dip = (since: number) => + since < 0 || since > 6 + ? 0 + : 0.06 * Math.exp(-since * 1.4) * Math.sin(Math.min(since, 0.5) * Math.PI); + +/** `n` signals' areas, names, offsets, and noise levels. */ +function signals(n: number) { + const next = random(11); + const area = Float64Array.from({ length: n }, (_, i) => i % AREAS.length); + const name = Array.from( + { length: n }, + (_, i) => `${AREAS[i % AREAS.length]} ${Math.floor(i / AREAS.length) + 1}`, + ); + const offset = Float32Array.from({ length: n }, () => next() - 0.5); + const noise = Float32Array.from({ length: n }, () => 0.3 + next()); + return { area, name, offset, noise, next }; +} + +export default page({ + title: 'Telemetry', + settings: { + source: choice('Source', { Live: 'live', 'Recorded day': 'day' }), + count: choice('Signals', { '500': 500, '2,000': 2000, '10,000': 10_000 }, { value: 2000 }), + color: choice('Color', { Area: 'area', Voltage: 'value' }, { key: 'c' }), + }, + + async open({ settings, view, actions, timeline, frame, detail }) { + let s = signals(settings.count); + const live = new Data('live').axis('time', 's').set('Signal', { name: s.name, area: s.area }); + const day = new Tiles('scada', 'history/frames.json'); + let t = 0; + let struck = -Infinity; + let area = 0; + + const plot = await view( + (settings) => { + const table = settings.source === 'live' ? live.table('Signal') : day.table('Signal'); + return { + space: { + plot: { + x: { label: settings.source === 'live' ? 'Time (s)' : 'Time of day (s)' }, + y: { label: 'Voltage (pu)' }, + }, + }, + theme: { base: 'dark', background: '#101214', focus: { dim: 0.15 } }, + ease: 0, + marks: [ + { + lines: table, + trace: 'value', + width: when(selected()) + .then(2.5) + .otherwise(settings.count > 2000 ? 0.6 : 1), + color: + settings.color === 'area' + ? field('area').colormap(BY_AREA) + : field('value').colormap('coolwarm').domain(0.94, 1.04), + legend: settings.color === 'area' ? 'Area' : 'Voltage (pu)', + }, + ], + }; + }, + { camera: { x: [-WINDOW, 0] } }, + ); + timeline({ data: live, keep: KEEP }); + + actions('Actions', [ + { + label: 'Strike an area', + key: 's', + run: () => { + area = (area + 1) % AREAS.length; + struck = t; + }, + }, + { + label: 'Select the lowest', + key: 'q', + // A query reads the latest frame and returns the twelve lowest voltages, which the plot + // then highlights. + run: () => { + const lowest = live.query({ + of: 'Signal', + sort: [['$value', 'ascending']], + limit: 12, + at: t, + }); + plot.selection = [{ data: 'live', table: 'Signal', ids: lowest.ids }]; + }, + }, + ]); + + // Every frame appends the samples that came due since the last, each signal swinging with its + // area, with noise, and dipping when its area is struck. + let carry = 0; + let values = new Float32Array(settings.count); + frame((dt) => { + if (settings.source !== 'live') return; + carry += dt; + while (carry >= 1 / RATE) { + carry -= 1 / RATE; + t += 1 / RATE; + const automatic = Math.floor(t / EVERY) % AREAS.length; + for (let i = 0; i < values.length; i++) { + const a = s.area[i]!; + const dips = (a === automatic ? dip(t % EVERY) : 0) + (a === area ? dip(t - struck) : 0); + const swing = Math.sin(t * (0.6 + a * 0.13) + a); + values[i] = + 1 + 0.03 * s.offset[i]! + 0.012 * swing + 0.003 * (s.next() - 0.5) * s.noise[i]! - dips; + } + live.append('Signal', t, { value: values }); + } + plot.camera = { x: [t - WINDOW, t] }; + }); + detail(`${settings.count.toLocaleString()} signals at ${RATE} Hz, ${KEEP} s kept`); + + return { + change(next, key) { + if (key === 'count') { + s = signals(next.count); + values = new Float32Array(next.count); + live.replace('Signal', { name: s.name, area: s.area }); + detail(`${next.count.toLocaleString()} signals at ${RATE} Hz, ${KEEP} s kept`); + } + if (key === 'source') + return () => { + if (next.source === 'day') plot.camera = { x: [0, 86_400] }; + detail( + next.source === 'day' + ? '64 buses every 15 s for a day, fetched as time tiles' + : `${next.count.toLocaleString()} signals at ${RATE} Hz`, + ); + }; + }, + }; + }, +}); diff --git a/npm/examples/src/ui/dom.ts b/npm/examples/src/shell/dom.ts similarity index 100% rename from npm/examples/src/ui/dom.ts rename to npm/examples/src/shell/dom.ts diff --git a/npm/examples/src/shell/page.ts b/npm/examples/src/shell/page.ts new file mode 100644 index 00000000..729d2612 --- /dev/null +++ b/npm/examples/src/shell/page.ts @@ -0,0 +1,148 @@ +/** + * What an example page declares. A page is its settings, which the shell shows as controls, and an + * `open` that builds its data and views. Each view's scene is a function of the settings, which the + * shell draws again whenever one changes, so a page holds no code that restyles its views. + */ +import type { Camera, Data, Rect, Scene, View } from 'latkit'; + +/** One value of a page's settings and the control that sets it. */ +export type Setting = { + readonly label: string; + readonly value: T; + /** The side panel section it shows in; `Settings` by default. */ + readonly section?: string; + /** A key that changes it: a toggle flips, and a choice steps to its next option. */ + readonly key?: string; +} & ( + | { readonly kind: 'choice'; readonly options: readonly (readonly [string, T])[] } + | { readonly kind: 'toggle' } + | { + readonly kind: 'range'; + readonly min: number; + readonly max: number; + readonly step: number; + readonly format: (value: number) => string; + } +); + +type Where = { section?: string; key?: string }; + +/** A dropdown of `options`, keyed by their labels; the first option by default. */ +export function choice( + label: string, + options: Readonly>, + where: Where & { value?: T } = {}, +): Setting { + const entries = Object.entries(options); + return { + kind: 'choice', + label, + options: entries, + value: where.value ?? entries[0]![1], + ...where, + }; +} + +/** A button that toggles; off by default. */ +export function toggle(label: string, where: Where & { value?: boolean } = {}): Setting { + return { kind: 'toggle', label, ...where, value: where.value ?? false }; +} + +/** A slider. */ +export function range( + label: string, + o: { min: number; max: number; step: number; value: number; format: (value: number) => string }, + where: Where = {}, +): Setting { + return { kind: 'range', label, ...o, ...where }; +} + +export type Settings = Readonly>>; +/** The current values of `S`. */ +export type Values = { -readonly [K in keyof S]: S[K]['value'] }; + +/** A button, also run by its key. */ +export interface Action { + readonly label: string; + readonly key?: string; + readonly run: () => void; + /** Runs `run` on press and `release` on release, for an action held down. */ + readonly release?: () => void; +} + +/** The recorded span of a dataset with an axis, played live, paused, or replayed. */ +export interface Timeline { + readonly mode: 'live' | 'playing' | 'paused'; + /** The axis coordinate every view of the page shows. */ + readonly at: number; + /** Goes live, following the end of the data. */ + play(): void; +} + +/** What a page can do while it is open. Everything it makes through it closes with it. */ +export interface Context { + /** The current settings. */ + readonly settings: Readonly; + /** Aborted when the page closes. */ + readonly signal: AbortSignal; + /** + * Creates a view on the page's canvas, in `region`, drawing `scene` of the current settings and + * again after each change. Without a camera, it frames everything drawn. + */ + view( + scene: (settings: V) => Scene, + options?: { region?: Rect; camera?: Camera; label?: string }, + ): Promise; + /** Adds buttons to the side panel, in section `title`. */ + actions(title: string, actions: readonly Action[]): void; + /** + * Plays `data`'s axis in a timeline under the canvas. Live, it follows the end of the data and + * forgets frames older than `keep` seconds; paused or replaying, it shows earlier frames. Every + * view of the page shows the timeline's coordinate. + */ + timeline(options: { + data: Data; + keep?: number; + /** Plays back the last `seconds` at `rate`. */ + replay?: { seconds: number; rate: number }; + /** Called after the views move to coordinate `at`. */ + show?(at: number): void; + /** Called when the timeline goes live or stops being live. */ + change?(live: boolean): void; + }): Timeline; + /** Calls `step` every animation frame while the page is live, with the seconds since the last. */ + frame(step: (dt: number) => void): void; + /** Lists the colors of a view's series in the side panel. */ + series(title: string, items: readonly { label: string; color: string }[]): void; + /** Shows short text in the header, such as the page's counts or what is loading. */ + detail(text: string): void; + /** Credits the data's source under the canvas. */ + credit(text: string): void; + /** Shows the page as failed with `error`, such as from work it finishes after opening. */ + fail(error: unknown): void; +} + +/** Nothing, or a function to run once the views draw the new settings. */ +export type Changed = void | (() => void); + +/** What a page returns from `open`. */ +export interface Opened { + /** + * Called when setting `key` changes, before the views draw the new settings. It may return a + * function to run once they do, such as one that frames what they now draw. + */ + change?(settings: V, key: keyof V): Changed | Promise; +} + +export interface Page { + readonly title: string; + readonly settings?: S; + /** Whether hovering or clicking a row shows its fields in the side panel; true by default. */ + readonly inspect?: boolean; + open(context: Context>): Promise> | void>; +} + +/** Declares a page, typing its settings' values. */ +export function page(page: Page): Page { + return page; +} diff --git a/npm/examples/src/shell/shell.ts b/npm/examples/src/shell/shell.ts new file mode 100644 index 00000000..07c5880e --- /dev/null +++ b/npm/examples/src/shell/shell.ts @@ -0,0 +1,542 @@ +/** + * The examples' shell: the layout every page shares, and everything a page does not declare + * itself. It shows a page's settings as controls and draws each view's scene again when one + * changes, runs the page's frames and timeline, and gives every page an inspector, a performance + * readout, and picture and video export. + */ +import { + Shot, + View, + load, + type Camera, + type Gpu, + type Rect, + type Row, + type Scene, + type Value, +} from 'latkit'; +import { h } from './dom.js'; +import type { Action, Context, Opened, Page, Setting, Settings } from './page.js'; +import { Timeline, type TimelineOptions } from './timeline.js'; + +declare global { + interface Window { + /** The open page's views and their GPU, for the browser gate. */ + stage?: { views: View[]; gpu?: Gpu }; + } +} + +/** A page in the picker: its id in the URL, and its module. */ +export interface Entry { + readonly id: string; + readonly title: string; + load(): Promise<{ default: Page }>; +} + +/** Fields the inspector leaves out: positions, which the view already shows. */ +const HIDDEN = /^(lon|lat|x|y|z|geometry|position)$/; +const MB = 1 << 20; + +/** Starts the examples: the shared layout, and the page the URL names. */ +export function start(pages: readonly Entry[]): void { + const picker = h('select', { 'aria-label': 'Example' }); + picker.append(...pages.map(({ id, title }) => h('option', { value: id }, title))); + picker.addEventListener('change', () => (location.hash = picker.value)); + const regions = { + detail: h('p', { class: 'detail' }), + error: h('div', { role: 'alert', class: 'error', hidden: true }), + main: h('main', { id: 'example' }), + side: h('aside', { id: 'controls', 'aria-label': 'Controls' }), + footer: h('footer', { 'aria-label': 'Playback' }), + }; + const toggle = h( + 'button', + { type: 'button', class: 'controls-toggle', 'aria-expanded': 'true' }, + 'Controls', + ); + toggle.addEventListener('click', () => { + regions.side.hidden = !regions.side.hidden; + toggle.setAttribute('aria-expanded', String(!regions.side.hidden)); + }); + document.body.append( + h( + 'header', + {}, + h('a', { class: 'brand', href: `#${pages[0]!.id}` }, 'latkit'), + h('label', { class: 'example-picker' }, h('span', {}, 'Examples'), picker), + regions.detail, + toggle, + ), + regions.error, + regions.main, + regions.side, + regions.footer, + ); + + let open: OpenPage | undefined; + let opening = 0; + const show = async () => { + const id = location.hash.slice(1).split('?')[0]; + const entry = pages.find((p) => p.id === id) ?? pages[0]!; + const opened = ++opening; + open?.close(); + open = undefined; + regions.error.hidden = true; + picker.value = entry.id; + document.title = `${entry.title} · latkit`; + regions.detail.textContent = 'Loading…'; + try { + const [{ default: page }] = await Promise.all([entry.load(), load()]); + if (opened !== opening) return; + open = new OpenPage(page, regions, (error) => opened === opening && fail(error)); + await open.start(); + } catch (error) { + if (opened === opening) fail(error); + } + }; + const fail = (error: unknown) => { + const unsupported = (error as { code?: string }).code === 'unsupported'; + regions.error.hidden = false; + regions.error.replaceChildren( + h( + 'p', + {}, + unsupported + ? 'WebGPU is unavailable. Open these examples in a browser with WebGPU enabled.' + : `Could not run this example. ${String(error)}`, + ), + ); + console.error(error); + }; + // Settings keep the URL in step without firing this, so a change here comes from a link or the + // address bar: the page opens again with the settings it gives. + addEventListener('hashchange', () => void show()); + void show(); +} + +type Regions = { + detail: HTMLElement; + error: HTMLElement; + main: HTMLElement; + side: HTMLElement; + footer: HTMLElement; +}; + +/** An open page, and the context it runs in. */ +class OpenPage implements Context> { + readonly id: string; + readonly settings: Record = {}; + readonly #abort = new AbortController(); + readonly signal = this.#abort.signal; + readonly #views: { view: View; scene: (settings: never) => Scene; region: Rect }[] = []; + /** The page's steps, run while it is live, and the shell's, run every frame. */ + readonly #steps: ((dt: number) => void)[] = []; + readonly #ticks: (() => void)[] = []; + readonly #keys = new Map void>(); + readonly #actions = h('div'); + readonly #panel = h('div'); + #canvas?: HTMLCanvasElement; + #timeline?: Timeline; + #opened?: Opened>; + #stepMs = 0; + + constructor( + private readonly page: Page, + private readonly regions: Regions, + readonly fail: (error: unknown) => void, + ) { + this.id = location.hash.slice(1).split('?')[0] ?? ''; + const given = new URLSearchParams(location.hash.split('?')[1]); + for (const [name, setting] of Object.entries(page.settings ?? {})) { + this.settings[name] = valueFrom(setting, given.get(name)) ?? setting.value; + } + regions.side.replaceChildren(this.#actions, this.#panel); + regions.side.hidden = false; + this.#controls(page.settings ?? {}); + addEventListener( + 'keydown', + (event) => { + const typing = (event.target as Element).closest?.('input, select, textarea, button'); + const action = this.#keys.get(event.key.toLowerCase()); + if (!action || typing || event.ctrlKey || event.metaKey || event.altKey) return; + event.preventDefault(); + action(); + }, + { signal: this.signal }, + ); + } + + async start(): Promise { + this.#opened = (await this.page.open(this as never)) ?? undefined; + if (this.signal.aborted) return; + const views = this.#views.map(({ view }) => view); + window.stage = { views, gpu: views[0]?.gpu }; + if (this.page.inspect !== false) this.#inspector(); + if (views[0]) { + this.#meter(views[0]); + this.#export(views[0]); + } + let last: number | undefined; + const stop = views[0]?.gpu.on('frame', (now) => { + const dt = last === undefined ? 0 : Math.min(0.1, (now - last) / 1000); + last = now; + try { + if (!this.#timeline || this.#timeline.mode === 'live') { + const start = performance.now(); + for (const step of this.#steps) step(dt); + this.#stepMs = this.#stepMs * 0.9 + (performance.now() - start) * 0.1; + } + this.#timeline?.tick(dt); + for (const tick of this.#ticks) tick(); + } catch (error) { + this.fail(error); + } + }); + if (stop) this.signal.addEventListener('abort', stop); + if (this.regions.detail.textContent === 'Loading…') this.regions.detail.textContent = ''; + } + + close(): void { + this.#abort.abort(); + for (const { view } of this.#views) view.destroy(); + for (const region of [this.regions.main, this.regions.side, this.regions.footer]) + region.replaceChildren(); + this.regions.detail.textContent = ''; + window.stage = undefined; + } + + // The context a page runs in. Its functions are bound, so a page can destructure them. + + view = async ( + scene: (settings: never) => Scene, + options: { region?: Rect; camera?: Camera; label?: string } = {}, + ): Promise => { + this.#canvas ??= this.regions.main.appendChild( + h('canvas', { tabindex: 0, 'aria-label': options.label ?? this.page.title }), + ); + const region = options.region ?? [0, 0, 1, 1]; + const view = await View.create(this.#canvas, scene(this.settings as never), { + region: this.#fitted(region), + ...(options.camera ? { camera: options.camera } : {}), + }); + view.on('error', (error) => + error.code === 'io' ? this.detail(error.message) : this.fail(error), + ); + this.#views.push({ view, scene, region }); + return view; + }; + + actions = (title: string, actions: readonly Action[]): void => { + const row = h('div', { class: 'actions' }); + for (const action of actions) { + const button = h('button', { type: 'button' }, action.label, ...kbd(action.key)); + if (action.release) { + button.addEventListener('pointerdown', () => action.run(), { signal: this.signal }); + for (const type of ['pointerup', 'pointercancel', 'pointerleave']) + button.addEventListener(type, () => action.release!(), { signal: this.signal }); + } else button.addEventListener('click', () => action.run(), { signal: this.signal }); + if (action.key) this.#keys.set(action.key.toLowerCase(), action.run); + row.append(button); + } + this.#actions.append(section(title, true, row)); + }; + + timeline = (options: TimelineOptions): Timeline => { + this.#timeline = new Timeline( + options, + this.regions.footer, + (key, run) => this.#keys.set(key, run), + (at) => this.#views.forEach(({ view }) => (view.at = at)), + this.signal, + ); + return this.#timeline; + }; + + frame = (step: (dt: number) => void): void => { + this.#steps.push(step); + }; + + series = (title: string, items: readonly { label: string; color: string }[]): void => { + const swatches = items.map(({ label, color }) => + h('span', { style: `--swatch: ${color}` }, label), + ); + this.#panel.append(section(title, true, h('p', { class: 'series' }, ...swatches))); + }; + + detail = (text: string): void => { + this.regions.detail.textContent = text; + }; + + credit = (text: string): void => { + const line = this.regions.main.querySelector('.credit') ?? h('p', { class: 'credit' }); + line.textContent = text; + this.regions.main.append(line); + }; + + // What the shell adds to every page. + + /** Shows each setting as a control in its section, and applies changes. */ + #controls(settings: Settings): void { + const sections = new Map(); + for (const [name, setting] of Object.entries(settings)) { + const title = setting.section ?? 'Settings'; + let body = sections.get(title); + if (!body) { + const group = this.#panel.appendChild(section(title, true)); + sections.set(title, (body = group.lastElementChild as HTMLElement)); + } + const set = (value: unknown) => void this.#change(name, value); + body.append(this.#control(setting, this.settings[name], set)); + } + } + + #control(setting: Setting, value: unknown, set: (value: unknown) => void): HTMLElement { + if (setting.kind === 'toggle') { + const button = h('button', { type: 'button', 'aria-pressed': String(value) }, setting.label); + button.append(...kbd(setting.key)); + const flip = () => { + const on = button.getAttribute('aria-pressed') !== 'true'; + button.setAttribute('aria-pressed', String(on)); + set(on); + }; + button.addEventListener('click', flip, { signal: this.signal }); + if (setting.key) this.#keys.set(setting.key, flip); + return button; + } + if (setting.kind === 'range') { + const input = h('input', { + type: 'range', + min: setting.min, + max: setting.max, + step: setting.step, + }); + input.value = String(value); + const output = h('output', {}, setting.format(value as number)); + input.addEventListener( + 'input', + () => { + output.value = setting.format(input.valueAsNumber); + set(input.valueAsNumber); + }, + { signal: this.signal }, + ); + return h('label', { class: 'field' }, h('span', {}, setting.label, output), input); + } + const options = setting.options; + const select = h( + 'select', + {}, + ...options.map(([label, v], i) => h('option', { value: i, selected: v === value }, label)), + ); + select.addEventListener('change', () => set(options[select.selectedIndex]![1]), { + signal: this.signal, + }); + if (setting.key) + this.#keys.set(setting.key, () => { + select.selectedIndex = (select.selectedIndex + 1) % options.length; + set(options[select.selectedIndex]![1]); + }); + return h( + 'label', + { class: 'field' }, + h('span', {}, setting.label, ...kbd(setting.key)), + select, + ); + } + + /** Sets `name` to `value`, keeps the URL in step, and draws every view's scene again. */ + async #change(name: string, value: unknown): Promise { + this.settings[name] = value; + const query = new URLSearchParams(); + for (const [key, setting] of Object.entries(this.page.settings ?? {})) + if (this.settings[key] !== setting.value) query.set(key, String(this.settings[key])); + history.replaceState(null, '', `#${this.id}${query.size ? `?${query}` : ''}`); + try { + const after = await this.#opened?.change?.(this.settings, name); + if (this.signal.aborted) return; + for (const { view, scene } of this.#views) view.scene = scene(this.settings as never); + after?.(); + } catch (error) { + this.fail(error); + } + } + + /** A view's region, its axes swapped on narrow screens so side-by-side views stack. */ + #fitted(region: Rect): Rect { + const [x, y, width, height] = region; + return matchMedia('(max-width: 700px)').matches ? [y, x, height, width] : region; + } + + /** The Inspect section: the fields of the row under the pointer, or of the last one clicked. */ + #inspector(): void { + const title = h('p', { class: 'note' }, 'Hover or click a row.'); + const fields = h('dl', { class: 'fields' }); + this.#panel.append(section('Inspect', true, title, fields)); + let pinned: { view: View; row: Row } | undefined; + const read = (view: View, row: Row) => { + const data = view.snapshot(row.data); + if (!data) return; + try { + const table = data.describe().tables.find((t) => t.name === row.table); + title.textContent = `${row.table} ${row.id}`; + fields.replaceChildren(); + for (const { name } of table?.fields ?? []) { + if (HIDDEN.test(name)) continue; + const value = data.get(row.table, row.id, name, { at: view.at }); + if (value === null) continue; + fields.append(h('dt', {}, name), h('dd', {}, format(value))); + } + } finally { + data.destroy(); + } + }; + for (const { view } of this.#views) { + view.on('hovered', ({ hit }) => { + if (hit) read(view, hit.row); + else if (pinned) read(pinned.view, pinned.row); + }); + view.on('click', ({ hit }) => { + pinned = { view, row: hit.row }; + read(view, hit.row); + }); + } + } + + /** The Performance section: frame rate and times, GPU work and memory, and the GPU's budget. */ + #meter(view: View): void { + const line = h('p', { class: 'meter', 'aria-live': 'off' }); + const gpu = view.gpu; + const given = gpu.budget; + this.signal.addEventListener('abort', () => (gpu.budget = given)); + const memory = h( + 'select', + {}, + ...[given.bytes, 256 * MB, 64 * MB, 16 * MB].map((bytes, i) => + h('option', { value: bytes }, i ? `${bytes / MB} MB` : 'Default'), + ), + ); + memory.addEventListener( + 'change', + () => (gpu.budget = { ...gpu.budget, bytes: Number(memory.value) }), + ); + this.#panel.append( + section( + 'Performance', + false, + line, + h('label', { class: 'field' }, h('span', {}, 'GPU memory'), memory), + ), + ); + let frames = 0; + let since = performance.now(); + this.#ticks.push(() => { + frames++; + const now = performance.now(); + if (now - since < 500) return; + const stats = view.stats; + line.textContent = [ + `${((frames * 1000) / (now - since)).toFixed(0)} fps`, + `frame ${stats.cpuMs.toFixed(1)} ms${stats.gpuMs ? ` · GPU ${stats.gpuMs.toFixed(2)} ms` : ''}`, + `${stats.drawn.toLocaleString()} rows drawn · ${stats.draws} draws`, + `${(gpu.resident / MB).toFixed(0)} of ${(gpu.budget.bytes / MB).toFixed(0)} MB on the GPU`, + ...(this.#stepMs > 0.05 ? [`page ${this.#stepMs.toFixed(1)} ms a frame`] : []), + ...(view.waiting.length ? [`waiting on ${view.waiting.join(', ')}`] : []), + ].join('\n'); + frames = 0; + since = now; + }); + } + + /** The Export section: a PNG of the canvas, or six seconds of it as MP4. */ + #export(view: View): void { + const status = h('p', { class: 'note', role: 'status' }); + const save = (name: string, file: Blob) => { + const url = URL.createObjectURL(file); + h('a', { href: url, download: name }).click(); + setTimeout(() => URL.revokeObjectURL(url), 10_000); + }; + const scale = () => Math.min(2, devicePixelRatio); + const picture = async () => { + const image = await Shot.of(view, { scale: scale(), capture: 'now' }).image(); + const png = await image.encode('png'); + save('views.png', new Blob([png.slice()], { type: 'image/png' })); + status.textContent = `Saved views.png, ${image.width}×${image.height}`; + image.destroy(); + }; + let recording = false; + const video = async () => { + if (recording || typeof VideoEncoder !== 'function') return; + recording = true; + const chunks: Uint8Array[] = []; + const output = new WritableStream({ + write: (chunk) => void chunks.push(chunk.slice()), + }); + const started = performance.now(); + try { + // Each frame waits for the page to reach its time, so the video plays at the page's speed. + await Shot.of(view, { scale: scale() }) + .video({ fps: 30, output }) + .record(async (time) => { + while (performance.now() - started < time * 1000) + await new Promise((resolve) => requestAnimationFrame(resolve)); + status.textContent = `Recording ${time.toFixed(1)} of 6 s`; + return time < 6; + }); + save('views.mp4', new Blob(chunks, { type: 'video/mp4' })); + status.textContent = 'Saved views.mp4'; + } finally { + recording = false; + } + }; + const run = (act: () => Promise) => () => + void act().catch( + (error: unknown) => (status.textContent = `Could not export: ${String(error)}`), + ); + const row = h('div', { class: 'actions' }); + for (const [label, key, act] of [ + ['Picture', 'x', picture], + ['Video', 'v', video], + ] as const) { + const button = h('button', { type: 'button' }, label, ...kbd(key)); + button.addEventListener('click', run(act), { signal: this.signal }); + this.#keys.set(key, run(act)); + row.append(button); + } + this.#panel.append(section('Export', false, row, status)); + } +} + +/** A collapsible titled group of the side panel. */ +function section(title: string, open: boolean, ...children: Node[]): HTMLElement { + return h( + 'details', + { class: 'section', open }, + h('summary', {}, title), + h('div', { class: 'section-body' }, ...children), + ); +} + +/** The key label of a button, if it has a key. */ +function kbd(key: string | undefined): HTMLElement[] { + return key ? [h('kbd', {}, key === ' ' ? 'Space' : key.toUpperCase())] : []; +} + +/** A setting's value from its URL parameter, or `undefined` when absent or not one of its values. */ +function valueFrom(setting: Setting, given: string | null): unknown { + if (given === null) return undefined; + if (setting.kind === 'toggle') return given === 'true'; + if (setting.kind === 'range') return Number.isFinite(Number(given)) ? Number(given) : undefined; + return setting.options.find(([, v]) => String(v) === given)?.[1]; +} + +/** A field's value for the inspector: a number, a text, a point, or the word for a longer geometry. A + * reference reads as the id of the row it references. */ +function format(value: Value): string { + if (typeof value === 'number') + return Number.isInteger(value) ? value.toLocaleString() : value.toPrecision(4); + if (Array.isArray(value)) + return typeof value[0] === 'number' + ? value.map((v) => format(v as number)).join(', ') + : 'geometry'; + return String(value); +} diff --git a/npm/examples/src/ui/style.css b/npm/examples/src/shell/style.css similarity index 79% rename from npm/examples/src/ui/style.css rename to npm/examples/src/shell/style.css index bde71672..24529c3c 100644 --- a/npm/examples/src/ui/style.css +++ b/npm/examples/src/shell/style.css @@ -140,10 +140,6 @@ aside { gap: 12px; padding: 2px 16px 16px; } -.section canvas { - width: 100%; - height: 180px; -} .actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -254,7 +250,6 @@ input[type='text'] { font: 11px/1.8 var(--mono); white-space: pre-line; } -.legend, .credit { position: absolute; margin: 0; @@ -265,59 +260,11 @@ input[type='text'] { background: var(--bg); padding: 4px 6px; } -.legend { - right: 12px; - bottom: 10px; - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; - max-width: calc(100% - 24px); -} -.legend.top { - top: 10px; - bottom: auto; -} .credit { left: 12px; bottom: 10px; max-width: 48%; } -.callout { - position: absolute; - top: 0; - left: 0; - margin: 0; - padding: 2px 6px; - color: var(--accent-ink); - background: var(--accent); - border-radius: var(--radius); - font-size: 11px; - white-space: nowrap; - pointer-events: none; - /* Above the point it names, centered. */ - transform: translate(-50%, calc(-100% - 10px)); -} -.ramp { - width: 88px; - height: 6px; -} -.menu { - position: absolute; - z-index: 2; - display: grid; - min-width: 180px; - padding: 4px; - background: var(--surface); - border: 1px solid var(--line-strong); - border-radius: var(--radius); -} -.menu button { - justify-content: start; - text-align: left; - background: none; - border-color: transparent; -} .fields { display: grid; grid-template-columns: auto minmax(0, 1fr); @@ -334,50 +281,6 @@ input[type='text'] { font-variant-numeric: tabular-nums; overflow-wrap: anywhere; } -.swatches { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 6px; -} -.swatch { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: 5px; - padding: 6px 8px; - font-size: 11px; - text-align: left; -} -.swatch .ramp { - width: 100%; -} -.hot { - display: grid; - gap: 4px; - margin: 0; - padding: 0; - list-style: none; -} -.hot button { - display: grid; - grid-template-columns: 2ch minmax(0, 1fr) auto; - gap: 3px 8px; - width: 100%; - padding: 5px 8px; - font-size: 12px; - font-variant-numeric: tabular-nums; - text-align: left; -} -.hot .bar { - grid-column: 1 / -1; - height: 2px; - background: linear-gradient(to right, var(--accent) var(--score), var(--raised) var(--score)); -} -.hot button[aria-pressed='true'], -.swatch[aria-pressed='true'] { - color: var(--text); - background: var(--raised); - border-color: var(--accent); -} footer { grid-area: footer; display: flex; @@ -512,8 +415,4 @@ body:has(aside[hidden]) :is(main, footer) { grid-template-columns: minmax(0, 1fr); grid-template-rows: 56px auto minmax(0, 1fr) auto 0; } - .legend { - right: 8px; - bottom: 8px; - } } diff --git a/npm/examples/src/shell/timeline.ts b/npm/examples/src/shell/timeline.ts new file mode 100644 index 00000000..aab7ebb1 --- /dev/null +++ b/npm/examples/src/shell/timeline.ts @@ -0,0 +1,128 @@ +/** + * A page's timeline: the recorded span of a dataset with an axis, played live, paused, or + * replayed, with its transport under the canvas. Live, it follows the end of the data and forgets + * frames older than the span it keeps. + */ +import type { Data } from 'latkit'; +import { h } from './dom.js'; +import type { Timeline as Shown } from './page.js'; + +export interface TimelineOptions { + data: Data; + keep?: number; + replay?: { seconds: number; rate: number }; + show?(at: number): void; + change?(live: boolean): void; +} + +export class Timeline implements Shown { + mode: 'live' | 'playing' | 'paused' = 'live'; + #at = 0; + #span: readonly [number, number] = [0, 0]; + #rate = 1; + readonly #toggle = h('button', { type: 'button', class: 'primary' }); + readonly #slider = h('input', { + type: 'range', + step: 'any', + min: 0, + max: 1, + 'aria-label': 'Time', + }); + readonly #time = h('output', {}, '0.00 s'); + + constructor( + private readonly options: TimelineOptions, + footer: HTMLElement, + key: (key: string, run: () => void) => void, + /** Moves every view of the page to coordinate `at`. */ + private readonly move: (at: number) => void, + signal: AbortSignal, + ) { + const transport = h('div', { class: 'transport' }, this.#toggle); + const flip = () => (this.mode === 'paused' ? this.play() : this.#set('paused')); + this.#toggle.addEventListener('click', flip, { signal }); + key('k', flip); + this.#slider.addEventListener('input', () => this.#seek(this.#slider.valueAsNumber), { + signal, + }); + if (options.replay) { + const replay = h('button', { type: 'button' }, 'Replay', h('kbd', {}, 'R')); + replay.addEventListener('click', () => this.replay(), { signal }); + key('r', () => this.replay()); + transport.append(replay); + } + transport.append(this.#time); + footer.append(transport, this.#slider); + document.addEventListener('visibilitychange', () => document.hidden && this.#set('paused'), { + signal, + }); + this.#label(); + } + + get at(): number { + return this.#at; + } + + /** Goes live: shows the end of the data as it records. */ + play(): void { + this.#rate = 1; + this.#set('live'); + this.#show(this.#span[1]); + } + + /** Plays the last `replay.seconds` at `replay.rate`, then goes live. */ + replay(): void { + const { replay } = this.options; + if (!replay) return; + this.#rate = replay.rate; + this.#set('playing'); + this.#show(Math.max(this.#span[0], this.#span[1] - replay.seconds)); + } + + /** Advances by `dt` seconds of display time. Live, it takes the data's newest span. */ + tick(dt: number): void { + if (this.mode === 'live') { + const span = this.options.data.describe().span; + if (!span || span[1] === this.#span[1]) return; + const { keep } = this.options; + if (keep !== undefined && span[1] - span[0] > keep) this.options.data.forget(span[1] - keep); + this.#extent([Math.max(span[0], span[1] - (keep ?? Infinity)), span[1]]); + this.#show(span[1]); + } else if (this.mode === 'playing') { + const next = this.#at + dt * this.#rate; + if (next < this.#span[1]) this.#show(next); + else this.play(); + } + } + + #extent(span: readonly [number, number]): void { + this.#span = span; + this.#slider.min = String(span[0]); + this.#slider.max = String(Math.max(span[1], span[0] + 1e-3)); + } + + #seek(at: number): void { + this.#set('paused'); + this.#show(at); + } + + #set(mode: Timeline['mode']): void { + const was = this.mode === 'live'; + this.mode = mode; + this.#label(); + if (was !== (mode === 'live')) this.options.change?.(mode === 'live'); + } + + #label(): void { + this.#toggle.textContent = this.mode === 'paused' ? 'Live' : 'Pause'; + this.#toggle.append(h('kbd', {}, 'K')); + } + + #show(at: number): void { + this.#at = at; + this.#slider.value = String(at); + this.#time.value = `${at.toFixed(2)} s`; + this.move(at); + this.options.show?.(at); + } +} diff --git a/npm/examples/src/ui/controls.ts b/npm/examples/src/ui/controls.ts deleted file mode 100644 index feb1ffcd..00000000 --- a/npm/examples/src/ui/controls.ts +++ /dev/null @@ -1,150 +0,0 @@ -import type { Scene, View } from 'latkit'; -import { h } from './dom.js'; -import type { Stage } from './stage.js'; - -/** A button that calls `run`, with an optional `key` that also calls it. */ -export interface Action { - label: string; - key?: string; - run: () => void; -} - -/** The key label shown on a button, if there is a key. */ -const kbd = (key: string | undefined) => - key ? [h('kbd', {}, key === ' ' ? 'Space' : key.toUpperCase())] : []; - -/** A titled group of controls in the side panel. */ -export class Section { - readonly element: HTMLElement; - readonly content = h('div', { class: 'section-body' }); - /** The row that holds the toggles, created on first use. */ - #toggles?: HTMLElement; - - constructor( - private readonly stage: Stage, - title: string, - open = true, - ) { - this.element = h('details', { class: 'section', open }, h('summary', {}, title), this.content); - } - - /** Adds a grid of buttons. Each button's key, if any, also runs it. */ - actions(actions: readonly Action[]): void { - const row = h('div', { class: 'actions' }); - for (const { label, key, run } of actions) { - const button = h('button', { type: 'button' }, label, ...kbd(key)); - button.addEventListener('click', run, { signal: this.stage.signal }); - if (key) this.stage.key(key, run); - row.append(button); - } - this.content.append(row); - } - - /** Adds a toggle button, which its key, if any, also flips. Returns a setter for changes made - * elsewhere. */ - toggle( - label: string, - key: string | undefined, - value: boolean, - change: (on: boolean) => void, - ): (on: boolean) => void { - const pressed = String(value); - const button = h('button', { type: 'button', 'aria-pressed': pressed }, label, ...kbd(key)); - const set = (on: boolean) => { - button.setAttribute('aria-pressed', String(on)); - change(on); - }; - const flip = () => set(button.getAttribute('aria-pressed') !== 'true'); - button.addEventListener('click', flip, { signal: this.stage.signal }); - if (key) this.stage.key(key, flip); - if (!this.#toggles) this.content.append((this.#toggles = h('div', { class: 'actions' }))); - this.#toggles.append(button); - return set; - } - - /** Adds a slider that shows its value through `format` and calls `change` as it moves. Returns - * the input so the page can change it. */ - range( - label: string, - o: { min: number; max: number; step: number; value: number; format: (value: number) => string }, - change: (value: number) => void, - ): HTMLInputElement { - const input = h('input', { - type: 'range', - min: o.min, - max: o.max, - step: o.step, - value: o.value, - }); - const output = h('output', {}, o.format(input.valueAsNumber)); - input.addEventListener( - 'input', - () => { - output.value = o.format(input.valueAsNumber); - change(input.valueAsNumber); - }, - { signal: this.stage.signal }, - ); - this.content.append(h('label', { class: 'field' }, h('span', {}, label, output), input)); - return input; - } - - /** Adds a dropdown of labeled `options`. Returns the select so the page can change it. */ - choice( - label: string, - options: readonly (readonly [string, T])[], - value: T, - change: (value: T) => void, - ): HTMLSelectElement { - const select = h( - 'select', - {}, - ...options.map(([name, v], i) => h('option', { value: i, selected: v === value }, name)), - ); - select.addEventListener('change', () => change(options[select.selectedIndex]![1]), { - signal: this.stage.signal, - }); - this.content.append(h('label', { class: 'field' }, h('span', {}, label), select)); - return select; - } - - /** Adds color swatches that label the series a view draws. */ - series(items: readonly { label: string; color: string }[]): HTMLElement { - const swatches = items.map(({ label, color }) => - h('span', { style: `--swatch: ${color}` }, label), - ); - const line = h('p', { class: 'series' }, ...swatches); - this.content.append(line); - return line; - } - - /** Adds a line of text for the page to keep up to date. */ - note(text = ''): HTMLParagraphElement { - const line = h('p', { class: 'note' }, text); - this.content.append(line); - return line; - } - - /** Creates a view inside this section. */ - view(scene: Scene, options: { label: string; height?: number }): View { - return this.stage.view(scene, { ...options, into: this.content }); - } - - /** Adds the standard playback speed control. */ - speed(rates: readonly number[], value: number, change: (value: number) => void): void { - this.choice( - 'Speed', - rates.map((rate) => [`${rate}×`, rate] as const), - value, - change, - ); - } - - /** Adds a labeled text field, such as for a selected item's name. */ - text(label: string, change: (value: string) => void): HTMLInputElement { - const input = h('input', { type: 'text' }); - input.addEventListener('change', () => change(input.value), { signal: this.stage.signal }); - this.content.append(h('label', { class: 'field' }, h('span', {}, label), input)); - return input; - } -} diff --git a/npm/examples/src/ui/export.ts b/npm/examples/src/ui/export.ts deleted file mode 100644 index d071b872..00000000 --- a/npm/examples/src/ui/export.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { ImageFormat } from 'latkit'; -import { h } from './dom.js'; -import type { Stage } from './stage.js'; - -/** Length of a recorded video, in seconds. */ -const SECONDS = 6; -/** Video frame rate, in frames per second. */ -const FPS = 30; -/** Each picture format's file extension, MIME type, and whether it keeps alpha. */ -const FORMATS: Record = { - png: { extension: 'png', type: 'image/png', alpha: true }, - jpeg: { extension: 'jpg', type: 'image/jpeg', alpha: false }, - webp: { extension: 'webp', type: 'image/webp', alpha: true }, -}; - -/** - * Adds the Export section every page gets, for the page's first canvas. A picture captures the - * canvas as it is now and is encoded in a worker as PNG, JPEG, or WebP, optionally with a - * transparent background. A video records the canvas as MP4 while the page runs, showing its - * progress. Pressing Record again stops it. - */ -export function exportSection(stage: Stage): void { - const [canvas] = stage.canvases; - if (!canvas) return; - const section = stage.section('Export', false); - let format: ImageFormat = 'png'; - /** JPEG quality, from 1 to 100. */ - let quality = 90; - let transparent = false; - /** Video bits per pixel per frame. When unset, the default of about 0.15 applies. */ - let bits: number | undefined; - let recording: AbortController | undefined; - /** Saved pixels per CSS pixel: the screen's device pixel ratio, capped at 2. */ - const scale = () => Math.min(2, devicePixelRatio); - const picture = async () => { - status.textContent = 'Taking a picture…'; - const { extension, type, alpha } = FORMATS[format]; - // `capture: 'now'` pictures this moment while the page keeps running, instead of waiting for - // motion to settle. - const shot = canvas.shot({ scale: scale(), transparent: transparent && alpha, capture: 'now' }); - const image = await shot.image(); - try { - const file = await image.encode(format, { quality }); - const name = `views.${extension}`; - save(name, new Blob([file.slice()], { type })); - status.textContent = `Saved ${name}, ${image.width}×${image.height}, ${kilobytes(file.length)}`; - } finally { - image.destroy(); - } - }; - const video = async () => { - if (recording) return recording.abort(new Error('stopped')); - const chunks: Uint8Array[] = []; - const output = new WritableStream({ - write: (chunk) => void chunks.push(chunk.slice()), - }); - recording = new AbortController(); - const shot = canvas.shot({ scale: scale() }); - const bitrate = bits && Math.round(shot.size[0] * shot.size[1] * FPS * bits); - const started = performance.now(); - try { - const recorder = shot.video({ - fps: FPS, - output, - signal: recording.signal, - ...(bitrate ? { bitrate } : {}), - }); - // Each frame waits until the page reaches that frame's time, so the video plays at the - // page's real speed. - await recorder.record( - async (time) => { - while (performance.now() - started < time * 1000) await nextFrame(); - return time < SECONDS; - }, - ({ time }) => - (status.textContent = `Recording ${time.toFixed(1)} of ${SECONDS} s; V stops`), - ); - const file = new Blob(chunks, { type: 'video/mp4' }); - save('views.mp4', file); - status.textContent = `Saved views.mp4, ${SECONDS} s, ${kilobytes(file.size)}`; - } catch (error) { - status.textContent = recording.signal.aborted - ? 'Stopped' - : `Could not record: ${String(error)}`; - } finally { - recording = undefined; - } - }; - const failing = (run: () => Promise) => () => - void run().catch( - (error: unknown) => (status.textContent = `Could not export: ${String(error)}`), - ); - section.actions([ - { label: 'Save picture', key: 'x', run: failing(picture) }, - { label: 'Record', key: 'v', run: failing(video) }, - ]); - const status = section.note(); - status.setAttribute('role', 'status'); - const quality_ = section.range( - 'Quality', - { min: 50, max: 100, step: 5, value: quality, format: String }, - (q) => (quality = q), - ); - quality_.disabled = true; - const clear = section.toggle('Transparent', undefined, transparent, (on) => (transparent = on)); - section.choice( - 'Format', - [ - ['PNG', 'png'], - ['JPEG', 'jpeg'], - ['WebP', 'webp'], - ] as const, - format, - (next) => { - format = next; - // PNG and WebP are lossless. JPEG trades detail for size and has no alpha. - quality_.disabled = next !== 'jpeg'; - if (!FORMATS[next].alpha && transparent) clear(false); - }, - ); - section.choice( - 'Video', - [ - ['Standard', undefined], - ['Sharp', 0.4], - ['Small', 0.06], - ] as const, - bits, - (next) => (bits = next), - ); -} - -/** Saves `file` as a download named `name`. */ -function save(name: string, file: Blob): void { - const url = URL.createObjectURL(file); - h('a', { href: url, download: name }).click(); - setTimeout(() => URL.revokeObjectURL(url), 10_000); -} - -function kilobytes(bytes: number): string { - return `${Math.max(1, Math.round(bytes / 1024)).toLocaleString()} kB`; -} - -function nextFrame(): Promise { - return new Promise((resolve) => requestAnimationFrame(() => resolve())); -} diff --git a/npm/examples/src/ui/legend.ts b/npm/examples/src/ui/legend.ts deleted file mode 100644 index 5e694ebc..00000000 --- a/npm/examples/src/ui/legend.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { colormapCss, type Colormap } from 'latkit'; -import { h } from './dom.js'; -import type { Stage } from './stage.js'; - -/** A colormap ramp and label over the main view. It sits at the bottom, or at the top (`'top'`) so - * it does not cover a plot's axes. */ -export class Legend { - readonly #ramp = h('span', { class: 'ramp', 'aria-hidden': 'true' }); - readonly #label = h('span'); - readonly #element = h('p', { class: 'legend' }, this.#ramp, this.#label); - - constructor(stage: Stage, place: 'foot' | 'top' = 'foot') { - this.#element.classList.toggle('top', place === 'top'); - stage.regions.main.append(this.#element); - } - - ramp(colormap: Colormap, label: string): void { - this.#ramp.style.background = colormapCss(colormap); - this.#label.textContent = label; - } - - /** Hides the legend, for example while nothing it describes is shown. */ - set hidden(hidden: boolean) { - this.#element.hidden = hidden; - } -} diff --git a/npm/examples/src/ui/menu.ts b/npm/examples/src/ui/menu.ts deleted file mode 100644 index feedce3e..00000000 --- a/npm/examples/src/ui/menu.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { View } from 'latkit'; -import { h } from './dom.js'; -import type { Stage } from './stage.js'; - -/** A menu item. A disabled item is shown but cannot be chosen. */ -export interface Item { - label: string; - run: () => void; - disabled?: boolean; -} - -/** A context menu shown over the main view where a view requested one. It closes when an item runs, - * on Escape, or on a press outside it. */ -export class Menu { - readonly #element = h('div', { class: 'menu', role: 'menu', hidden: true }); - - constructor(private readonly stage: Stage) { - const { signal } = stage; - stage.regions.main.append(this.#element); - addEventListener( - 'pointerdown', - (event) => { - if (!this.#element.contains(event.target as Node)) this.close(); - }, - { signal, capture: true }, - ); - addEventListener('keydown', (event) => event.key === 'Escape' && this.close(), { signal }); - } - - /** Opens `items` at `point`, in CSS pixels from the top left of `view`, kept at least 8 px inside - * the main view. */ - open(view: View, point: [number, number], items: readonly Item[]): void { - const canvas = this.stage.canvases.find((c) => c.layout.some((p) => p.view === view)); - const panel = canvas?.layout.find((p) => p.view === view)?.at ?? [0, 0, 1, 1]; - const element = canvas?.element; - if (!(element instanceof HTMLCanvasElement)) return; - this.#element.replaceChildren( - ...items.map(({ label, run, disabled }) => { - const button = h('button', { type: 'button', role: 'menuitem', disabled }, label); - button.addEventListener('click', () => { - this.close(); - run(); - }); - return button; - }), - ); - this.#element.hidden = false; - const main = this.stage.regions.main; - const [width, height] = [this.#element.offsetWidth, this.#element.offsetHeight]; - const x = element.offsetLeft + panel[0] * element.clientWidth + point[0]; - const y = element.offsetTop + panel[1] * element.clientHeight + point[1]; - const left = Math.min(x, main.clientWidth - width - 8); - const top = Math.min(y, main.clientHeight - height - 8); - this.#element.style.left = `${Math.max(8, left)}px`; - this.#element.style.top = `${Math.max(8, top)}px`; - this.#element.querySelector('button:not([disabled])')?.focus(); - } - - close(): void { - this.#element.hidden = true; - } -} diff --git a/npm/examples/src/ui/meter.ts b/npm/examples/src/ui/meter.ts deleted file mode 100644 index cea71df2..00000000 --- a/npm/examples/src/ui/meter.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { View } from 'latkit'; -import { h } from './dom.js'; -import type { Stage } from './stage.js'; - -const MB = 1 << 20; - -/** - * A performance readout for a view, in the side panel's Performance section. It shows frames per - * second, the view's CPU and GPU frame times, draws, instances, tiles, what the view is waiting on, - * GPU memory, marks that failed, and the page's own measurements. It also sets the GPU's memory and - * upload budgets, which are restored when the page closes. - */ -export class Meter { - readonly #line = h('p', { class: 'meter', 'aria-live': 'off' }); - readonly #times = new Map(); - readonly #notes: (() => string)[] = []; - #frames = 0; - #since = performance.now(); - /** What the view is waiting on, from its latest `waiting` event. */ - #waiting: string[] = []; - - constructor(stage: Stage, view: View) { - const section = stage.section('Performance', false); - section.element.classList.add('performance'); - section.content.append(this.#line); - const { gpu } = stage; - const given = gpu.budget; - stage.signal.addEventListener('abort', () => (gpu.budget = given)); - section.choice( - 'Memory', - [ - ['Default', given.bytes], - ['256 MB', 256 * MB], - ['64 MB', 64 * MB], - ['16 MB', 16 * MB], - ] as const, - given.bytes, - (bytes) => (gpu.budget = { ...gpu.budget, bytes }), - ); - section.choice( - 'Upload', - [ - ['Default', given.uploadPerFrame], - ['4 MB a frame', 4 * MB], - ['1 MB a frame', MB], - ['256 kB a frame', MB / 4], - ] as const, - given.uploadPerFrame, - (uploadPerFrame) => (gpu.budget = { ...gpu.budget, uploadPerFrame }), - ); - view.on('waiting', (waiting) => (this.#waiting = waiting)); - stage.frame(() => { - this.#frames++; - const now = performance.now(); - if (now - this.#since < 500) return; - const stats = view.stats; - const parts = [ - `${((this.#frames * 1000) / (now - this.#since)).toFixed(0)} fps`, - `frame ${stats.cpuMs.toFixed(1)} ms${stats.gpuMs ? ` · GPU ${stats.gpuMs.toFixed(2)} ms` : ''}`, - `${stats.draws} draws · ${stats.instances.toLocaleString()} instances`, - `${(gpu.resident / MB).toFixed(0)} of ${(gpu.budget.bytes / MB).toFixed(0)} MB on the GPU` + - (gpu.uploading ? ` · ${(gpu.uploading / MB).toFixed(1)} MB to upload` : ''), - ]; - if (stats.tiles || stats.tilesLoading) { - parts.push( - `${stats.tiles} tiles${stats.tilesLoading ? ` (${stats.tilesLoading} loading)` : ''}`, - ); - } - if (this.#waiting.length) parts.push(`waiting on ${this.#waiting.join(', ')}`); - const failures = view.failures; - if (failures.length) - parts.push(`${failures.length} marks draw nothing: ${failures[0]!.message}`); - for (const [label, ms] of this.#times) parts.push(`${label} ${ms.toFixed(1)} ms`); - this.#line.textContent = [...parts, ...this.#notes.map((note) => note())].join('\n'); - this.#frames = 0; - this.#since = now; - }); - } - - /** Runs `work`, keeping a moving average of its time under `label`. */ - measure(label: string, work: () => T): T { - const start = performance.now(); - const result = work(); - const ms = performance.now() - start; - this.#times.set(label, (this.#times.get(label) ?? ms) * 0.9 + ms * 0.1); - return result; - } - - /** Adds a line that `read` computes at each refresh, about twice a second. */ - note(read: () => string): void { - this.#notes.push(read); - } -} diff --git a/npm/examples/src/ui/playback.ts b/npm/examples/src/ui/playback.ts deleted file mode 100644 index 8e93c80e..00000000 --- a/npm/examples/src/ui/playback.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { h } from './dom.js'; -import type { Stage } from './stage.js'; - -export interface PlaybackOptions { - /** Shows coordinate `at` in every view. */ - show(at: number): void; - /** Whether the data is recorded while it plays (default true). Live playback follows the end of - * the data. Otherwise playing loops. */ - live?: boolean; - /** Seconds of data shown per second of playback (default 1). */ - rate?: number; - /** Instant replay: how many seconds back from the end to start, and the playback rate. */ - replay?: { seconds: number; rate: number }; - /** Called when playback goes live or stops being live. */ - change?(live: boolean): void; - /** Formats coordinate `at` for the transport. Defaults to seconds. */ - format?(at: number): string; -} - -/** Playback state (live, paused, or playing a recorded span) and the transport controls that drive - * it. */ -export class Playback { - mode: 'live' | 'playing' | 'paused'; - start = 0; - end = 0; - #at = 0; - #rate: number; - #live: boolean; - readonly #toggle = h('button', { type: 'button', class: 'primary' }); - readonly #slider = h('input', { - type: 'range', - step: 'any', - min: 0, - max: 1, - value: 0, - 'aria-label': 'Time', - }); - readonly #time = h('output', {}, '0.00 s'); - readonly #transport = h('div', { class: 'transport' }, this.#toggle); - - constructor( - stage: Stage, - private readonly options: PlaybackOptions, - ) { - const { signal } = stage; - this.#live = options.live ?? true; - this.#rate = options.rate ?? 1; - this.mode = this.#live ? 'live' : 'playing'; - const flip = () => (this.mode === 'paused' ? this.play() : this.pause()); - this.#toggle.addEventListener('click', flip, { signal }); - this.#slider.addEventListener( - 'input', - () => { - this.#set('paused'); - this.#show(this.#slider.valueAsNumber); - }, - { signal }, - ); - stage.key('k', flip); - if (options.replay) { - const replay = h('button', { type: 'button' }, 'Replay', h('kbd', {}, 'R')); - replay.addEventListener('click', () => this.replay(), { signal }); - stage.key('r', () => this.replay()); - this.#transport.append(replay); - } - this.#transport.append(this.#time); - stage.regions.timeline.append(this.#transport, this.#slider); - stage.frame((dt) => { - if (this.mode !== 'playing') return; - const next = this.#at + dt * this.#rate; - if (next < this.end) this.#show(next); - else if (this.#live) this.play(); - else this.#show(this.start + ((next - this.start) % Math.max(this.end - this.start, 1e-9))); - }); - document.addEventListener('visibilitychange', () => document.hidden && this.pause(), { - signal, - }); - this.#label(); - } - - get at(): number { - return this.#at; - } - - /** Hides the transport, for example while there is nothing to play. */ - set hidden(hidden: boolean) { - this.#transport.hidden = hidden; - this.#slider.hidden = hidden; - } - - /** Sets the seconds of data shown per second of playback, from now on. */ - set rate(rate: number) { - this.options.rate = rate; - if (this.mode === 'playing') this.#rate = rate; - } - - /** Sets whether the data is recorded while it plays. Going live follows the end of the data. - * Otherwise playback pauses, and playing then loops. */ - set live(live: boolean) { - this.#live = live; - this.#set(live ? 'live' : 'paused'); - } - - /** Pauses and shows coordinate `at`. */ - seek(at: number): void { - this.#set('paused'); - this.#show(at); - } - - /** Updates the recorded span. Live playback jumps to its end, and a position before its start - * moves to the start. */ - record([start, end]: readonly [number, number]): void { - this.start = start; - this.end = end; - this.#slider.min = String(start); - this.#slider.max = String(Math.max(end, start + 1e-3)); - if (this.mode === 'live') this.#show(end); - else if (this.#at < start) this.#show(start); - } - - /** Goes live, or, for recorded data, plays on from the current position. */ - play(): void { - this.#rate = this.options.rate ?? 1; - this.#set(this.#live ? 'live' : 'playing'); - if (this.#live) this.#show(this.end); - } - - pause(): void { - this.#set('paused'); - } - - /** Plays the last `replay.seconds` again at `replay.rate`, then goes live if the data is live. */ - replay(): void { - const replay = this.options.replay; - if (!replay) return; - this.#rate = replay.rate; - this.#set('playing'); - this.#show(Math.max(this.start, this.end - replay.seconds)); - } - - #set(mode: Playback['mode']): void { - const was = this.mode === 'live'; - this.mode = mode; - this.#label(); - if (was !== (mode === 'live')) this.options.change?.(mode === 'live'); - } - - #label(): void { - this.#toggle.textContent = this.mode !== 'paused' ? 'Pause' : this.#live ? 'Live' : 'Play'; - this.#toggle.append(h('kbd', {}, 'K')); - } - - #show(at: number): void { - this.#at = at; - this.#slider.value = String(at); - this.#time.value = this.options.format?.(at) ?? `${at.toFixed(2)} s`; - this.options.show(at); - } -} diff --git a/npm/examples/src/ui/rows.ts b/npm/examples/src/ui/rows.ts deleted file mode 100644 index 31aff7f6..00000000 --- a/npm/examples/src/ui/rows.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** Conversions between single rows and the row sets that views select and report. */ -import type { Row, Rows } from 'latkit'; - -/** Flattens `sets` into single rows, in order. */ -export const each = (sets: readonly Rows[]): Row[] => - sets.flatMap(({ data, table, ids }) => ids.map((id) => ({ data, table, id }))); - -/** Wraps `row` in a set of one row. */ -export const one = ({ data, table, id }: Row): Rows => ({ data, table, ids: [id] }); diff --git a/npm/examples/src/ui/shell.ts b/npm/examples/src/ui/shell.ts deleted file mode 100644 index 4d350c15..00000000 --- a/npm/examples/src/ui/shell.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { Gpu } from 'latkit'; -import { h } from './dom.js'; -import { exportSection } from './export.js'; -import { Stage } from './stage.js'; - -declare global { - interface Window { - /** The active example's stage, exposed for the browser gate. */ - stage?: Stage; - } -} - -export interface Page { - readonly id: string; - readonly title: string; - load(): Promise<{ default: (stage: Stage) => Promise | void }>; -} - -/** The page layout every example shares: a header with the example picker, the main view, the - * controls panel, and the timeline. It shows the example named in the URL hash. */ -export class Shell { - readonly #picker = h('select', { 'aria-label': 'Example' }); - readonly #detail = h('p', { class: 'detail' }); - readonly #error = h('div', { role: 'alert', class: 'error', hidden: true }); - readonly #loading = h('p', { class: 'loading', role: 'status', hidden: true }, 'Loading…'); - readonly #regions = { - main: h('main', { id: 'example', 'aria-label': 'Example' }), - side: h('aside', { id: 'controls', 'aria-label': 'Controls' }), - timeline: h('footer', { 'aria-label': 'Playback' }), - }; - #stage?: Stage; - #showing = 0; - - constructor( - private readonly pages: readonly Page[], - private readonly gpu: Promise, - ) { - this.#picker.append(...pages.map(({ id, title }) => h('option', { value: id }, title))); - this.#picker.addEventListener('change', () => (location.hash = this.#picker.value)); - const toggle = h( - 'button', - { - type: 'button', - class: 'controls-toggle', - 'aria-expanded': 'true', - 'aria-controls': 'controls', - }, - 'Controls', - ); - toggle.addEventListener('click', () => { - const hidden = !this.#regions.side.hidden; - this.#regions.side.hidden = hidden; - toggle.setAttribute('aria-expanded', String(!hidden)); - }); - document.body.append( - h('a', { class: 'skip-link', href: '#example' }, 'Skip to example'), - h( - 'header', - {}, - h('a', { class: 'brand', href: `#${pages[0]!.id}` }, 'latkit'), - h('label', { class: 'example-picker' }, h('span', {}, 'Examples'), this.#picker), - this.#detail, - toggle, - ), - this.#error, - this.#loading, - this.#regions.main, - this.#regions.side, - this.#regions.timeline, - ); - document.querySelector('.skip-link')!.addEventListener('click', (event) => { - event.preventDefault(); - this.#regions.main.querySelector('canvas')?.focus(); - }); - addEventListener('hashchange', () => void this.#show()); - void this.#show(); - } - - async #show(): Promise { - const showing = ++this.#showing; - const page = this.pages.find((p) => `#${p.id}` === location.hash) ?? this.pages[0]!; - this.#stage?.close(); - this.#stage = window.stage = undefined; - this.#error.hidden = true; - this.#loading.hidden = false; - this.#regions.main.setAttribute('aria-busy', 'true'); - this.#picker.value = page.id; - document.title = `${page.title} · latkit`; - this.#regions.main.setAttribute('aria-label', page.title); - try { - const [gpu, module] = await Promise.all([this.gpu, page.load()]); - if (showing !== this.#showing) return; - const stage = new Stage(gpu, this.#regions, this.#detail, (error) => { - if (showing === this.#showing) this.#fail(error); - }); - this.#stage = window.stage = stage; - await module.default(stage); - if (showing === this.#showing && !stage.signal.aborted) { - for (const section of this.#regions.side.querySelectorAll('.performance')) { - this.#regions.side.append(section); - } - exportSection(stage); - } - } catch (error) { - if (showing === this.#showing) this.#fail(error); - } finally { - if (showing === this.#showing) { - this.#loading.hidden = true; - this.#regions.main.removeAttribute('aria-busy'); - } - } - } - - #fail(error: unknown): void { - this.#stage?.close(); - this.#stage = window.stage = undefined; - this.#error.hidden = false; - const unsupported = (error as { code?: string }).code === 'unsupported'; - this.#error.replaceChildren( - h( - 'p', - {}, - unsupported - ? 'WebGPU is unavailable. Open these examples in a browser with WebGPU enabled.' - : `Could not load this example. ${String(error)}`, - ), - ); - const retry = h('button', { type: 'button' }, 'Reload'); - retry.addEventListener('click', () => location.reload()); - this.#error.append(retry); - console.error(error); - } -} diff --git a/npm/examples/src/ui/stage.ts b/npm/examples/src/ui/stage.ts deleted file mode 100644 index d3b9b730..00000000 --- a/npm/examples/src/ui/stage.ts +++ /dev/null @@ -1,175 +0,0 @@ -import type { Canvas, Gpu, Rect, Scene, View } from 'latkit'; -import { Section } from './controls.js'; -import { h } from './dom.js'; -import { Legend } from './legend.js'; -import { Menu } from './menu.js'; -import { Meter } from './meter.js'; -import { Playback, type PlaybackOptions } from './playback.js'; - -/** The page regions an example fills: the main view, the side panel, and the timeline. */ -export interface Regions { - main: HTMLElement; - side: HTMLElement; - timeline: HTMLElement; -} - -/** Creates an example page's views, canvases, and controls. Everything made through it is - * destroyed when the page closes. */ -export class Stage { - readonly #abort = new AbortController(); - /** Aborted when the page closes. Pass it to any listener the page adds itself. */ - readonly signal = this.#abort.signal; - readonly #views = new Set(); - readonly #canvases: Canvas[] = []; - readonly #keys = new Map void>(); - - constructor( - readonly gpu: Gpu, - readonly regions: Regions, - private readonly line: HTMLElement, - readonly fail: (error: unknown) => void, - ) { - addEventListener( - 'keydown', - (event) => { - const action = this.#keys.get(event.key.toLowerCase()); - const typing = (event.target as Element | null)?.closest?.( - 'input, select, textarea, button, summary, [contenteditable]', - ); - if (!action || typing || event.ctrlKey || event.metaKey || event.altKey) return; - event.preventDefault(); - action(); - }, - { signal: this.signal }, - ); - } - - /** Every view the page made. */ - get views(): View[] { - return [...this.#views]; - } - - /** The page's canvases, in creation order. */ - get canvases(): readonly Canvas[] { - return this.#canvases; - } - - /** Creates a view in its own canvas, appended to `into` (the main region by default). */ - view(scene: Scene, options: { label: string; into?: HTMLElement; height?: number }): View { - return this.panels([scene], options)[0]!; - } - - /** - * Creates views that share one canvas, appended to `into` (the main region by default). Each - * view fills its `at` rectangle: x, y, width, and height as fractions of the canvas. By default - * the views sit side by side. On screens 700 px wide or less, each rectangle's axes swap, so the - * views stack. Input goes to the view under the pointer. - */ - panels( - scenes: readonly Scene[], - { - label, - into = this.regions.main, - height, - at = scenes.map((_, i): Rect => [i / scenes.length, 0, 1 / scenes.length, 1]), - }: { label: string; into?: HTMLElement; height?: number; at?: readonly Rect[] }, - ): View[] { - const element = h('canvas', { tabindex: 0, 'aria-label': label }); - if (height) element.style.height = `${height}px`; - into.append(element); - const views = scenes.map((scene) => this.gpu.view(scene)); - const narrow = matchMedia('(max-width: 700px)'); - const layout = () => - views.map((view, i) => { - const [x, y, width, height] = at[i]!; - return { - view, - at: narrow.matches && views.length > 1 ? ([y, x, height, width] as const) : at[i], - }; - }); - const canvas = this.gpu.canvas(element, { layout: layout() }); - this.#canvases.push(canvas); - if (views.length > 1) - narrow.addEventListener('change', () => (canvas.layout = layout()), { signal: this.signal }); - views.forEach((view) => { - // A failed fetch (`io`) only leaves that data undrawn and is reported in the header. Any - // other error stops the page. - view.on('error', (error) => - error.code === 'io' ? this.detail(error.message) : this.fail(error), - ); - this.#views.add(view); - }); - return views; - } - - /** Calls `step` every frame before the views draw, with the seconds since the previous frame, - * capped at 0.1. */ - frame(step: (dt: number) => void): void { - let last: number | undefined; - const stop = this.gpu.on('frame', (now) => { - const dt = last === undefined ? 0 : Math.min(0.1, (now - last) / 1000); - last = now; - try { - step(dt); - } catch (error) { - this.fail(error); - } - }); - this.signal.addEventListener('abort', stop); - } - - /** Runs `action` when `key` is pressed without modifiers, unless focus is in a form control. */ - key(key: string, action: () => void): void { - this.#keys.set(key.toLowerCase(), action); - } - - /** Adds a titled group of controls to the side panel. */ - section(title: string, open = true): Section { - const section = new Section(this, title, open); - this.regions.side.append(section.element); - return section; - } - - /** Adds the playback controls: live, paused, or playing a recorded span. */ - playback(options: PlaybackOptions): Playback { - return new Playback(this, options); - } - - /** Adds a performance readout for `view`, with frame rate, frame times, and the page's own - * measurements. */ - meter(view: View): Meter { - return new Meter(this, view); - } - - /** Creates a context menu over the main view, to open where a view requests one. */ - menu(): Menu { - return new Menu(this); - } - - /** Adds a colormap legend over the main view, at the bottom, or at the top (`'top'`) so it does - * not cover a plot's axes. */ - legend(place: 'foot' | 'top' = 'foot'): Legend { - return new Legend(this, place); - } - - /** Adds a line under the main view that credits the data source. */ - credit(): HTMLElement { - const line = h('p', { class: 'credit' }); - this.regions.main.append(line); - return line; - } - - /** Shows short dataset information in the shared header. */ - detail(text: string): void { - this.line.textContent = text; - } - - close(): void { - this.#abort.abort(); - for (const canvas of this.#canvases) canvas.destroy(); - for (const view of this.#views) view.destroy(); - const { main, side, timeline } = this.regions; - for (const region of [main, side, timeline]) region.replaceChildren(); - this.line.textContent = ''; - } -} diff --git a/npm/examples/src/ui/theme.ts b/npm/examples/src/ui/theme.ts deleted file mode 100644 index df5497c2..00000000 --- a/npm/examples/src/ui/theme.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Scene } from 'latkit'; - -/** Default canvas colors. Examples override only the colors they demonstrate. */ -export const theme = { - base: 'dark', - background: '#101214', - grid: '#ffffff1c', -} as const satisfies Scene['theme']; diff --git a/npm/schema.json b/npm/schema.json index c6cf8517..6acc7c27 100644 --- a/npm/schema.json +++ b/npm/schema.json @@ -51,7 +51,7 @@ "required": [ "space" ], - "description": "A view's content and behavior: a space, marks drawn in order, a layout, a theme, controls, an\nease duration, and an optional title.\n\nA scene is plain data. In JSON, fields are camelCase and each mark is\n`{ vertices | edges | faces | lines | areas: options }`.\n\n```\n# use latkit::{Colormap, Edges, Earth, Projection, Scene, Vertices, field};\nlet voltage = field(\"vm\").colormap(Colormap::BALANCE).domain(0.95..=1.05);\nlet buses = Vertices::of(\"grid\", \"Bus\").position(field(\"lon\"), field(\"lat\")).color(voltage);\nlet scene = Scene::new(Earth::default().projection(Projection::Globe))\n .draw(Edges::of(\"grid\", \"Branch\").ends(\"from\", \"to\"))\n .draw(buses.legend(\"Bus voltage · pu\"))\n .title(\"Voltage\");\n```" + "description": "A view's content and behavior: a space, marks drawn in order, a layout, a theme, controls, an\nease duration, and an optional title.\n\nA scene is plain data. In JSON, fields are camelCase, and the key naming each mark's kind holds\nits dataset and table: `{ \"vertices\": [\"grid\", \"Bus\"], \"radius\": 4 }`.\n\n```\n# use latkit::{Colormap, Edges, Earth, Projection, Scene, Vertices, field};\nlet voltage = field(\"vm\").colormap(Colormap::BALANCE).domain(0.95..=1.05);\nlet buses = Vertices::of(\"grid\", \"Bus\").position(field(\"lon\"), field(\"lat\")).color(voltage);\nlet scene = Scene::new(Earth::default().projection(Projection::Globe))\n .draw(Edges::of(\"grid\", \"Branch\").ends(\"from\", \"to\"))\n .draw(buses.legend(\"Bus voltage · pu\"))\n .title(\"Voltage\");\n```" }, "Space": { "anyOf": [ @@ -362,74 +362,24 @@ "description": "A flat sheet for diagrams, seen from straight above; the camera pans and zooms but does not\nturn. Positions are in units with y down. Lengths are in units too, a unit being a CSS pixel at\nzoom 0: sizes, padding, ports, strokes, text, and the theme's lengths. Sizes given with `px`,\nthe text halo, the focus glow, and the pick radius stay in CSS pixels. Zooming scales the whole\npicture. Only a diagram has ports, groups, nets, and orthogonal routes, and the scene's layout\nplaces its blocks." }, "Mark": { + "description": "One table a scene draws. The key naming the mark's kind holds its dataset and table, beside its options: `{ vertices: [data, table], radius: 4 }`.", "oneOf": [ { - "type": "object", - "properties": { - "vertices": { - "$ref": "#/$defs/Vertices" - } - }, - "required": [ - "vertices" - ], - "additionalProperties": false, - "description": "Rows drawn as markers." + "$ref": "#/$defs/Vertices" }, { - "type": "object", - "properties": { - "edges": { - "$ref": "#/$defs/Edges" - } - }, - "required": [ - "edges" - ], - "additionalProperties": false, - "description": "Rows drawn as strokes between two vertices." + "$ref": "#/$defs/Edges" }, { - "type": "object", - "properties": { - "faces": { - "$ref": "#/$defs/Faces" - } - }, - "required": [ - "faces" - ], - "additionalProperties": false, - "description": "Rows drawn as triangles between three vertices." + "$ref": "#/$defs/Faces" }, { - "type": "object", - "properties": { - "lines": { - "$ref": "#/$defs/Lines" - } - }, - "required": [ - "lines" - ], - "additionalProperties": false, - "description": "Rows drawn as lines along their paths, or on a plot through their samples." + "$ref": "#/$defs/Lines" }, { - "type": "object", - "properties": { - "areas": { - "$ref": "#/$defs/Areas" - } - }, - "required": [ - "areas" - ], - "additionalProperties": false, - "description": "Rows drawn as the areas their polygons cover." + "$ref": "#/$defs/Areas" } - ], - "description": "One table a scene draws. In JSON: `{ vertices | edges | faces | lines | areas: options }`." + ] }, "Vertices": { "allOf": [ @@ -438,8 +388,23 @@ }, { "type": "object", - "description": "The mark's JSON form: `of`, its own options, then the stroke options and the options\nevery mark has.", + "description": "The mark's JSON form: the key naming its kind, its own options, then the stroke options\nand the options every mark has.", "properties": { + "vertices": { + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "minItems": 2, + "maxItems": 2, + "description": "The dataset and table drawn, `[data, table]`.", + "tsType": "TableInput" + }, "position": { "type": [ "array", @@ -541,7 +506,10 @@ "type": "boolean", "description": "Whether each block or group casts a shadow on the sheet, styled by the theme's\n`diagram.shadow`. Default `false`. Diagrams only." } - } + }, + "required": [ + "vertices" + ] } ], "unevaluatedProperties": false, @@ -612,7 +580,7 @@ "minItems": 2, "maxItems": 2, "description": "The dataset and table the field is read from, joined by row id.", - "tsType": "[data: string, table: string]" + "tsType": "TableInput" }, "step": { "type": "boolean", @@ -1883,7 +1851,7 @@ "type": "object", "properties": { "text": { - "description": "The text field to show, or a `when` whose values are each that field or missing. A row\nwhose text is missing has no label. In a diagram the text reads only data, since the sheet\nsizes blocks around it.", + "description": "The text field to show, or a `when` whose values are each that field or missing. A row\nwhose text is missing or empty has no label. In a diagram the text reads only data, since the sheet\nsizes blocks around it.", "$ref": "#/$defs/Expr" }, "size": { @@ -1927,27 +1895,12 @@ ] } ], - "description": "Text for each row, placed beside, inside, or along the row's shape and following it every frame:\na text field, or options. A label shows wherever its row shows and its text fits the room the\nrow gives it, such as inside a block, whatever other labels it meets. A row whose text is\nmissing has no label, so a `when` choosing between the text field and missing labels only the\nrows it names. In a diagram a label is part of the drawing: it scales with the zoom, fades out\nwhen too small to read, and shows wherever it fits its vertex.\n\n```\n# use latkit::{field, when, zoom, Expr, Label, Vertices};\nlet named = when(zoom().ge(6.0)).then(field(\"name\")).otherwise(Expr::missing());\nlet buses = Vertices::of(\"grid\", \"Bus\").position(field(\"lon\"), field(\"lat\"));\nlet labeled = buses.label(Label::new(named));\n```" + "description": "Text for each row, placed beside, inside, or along the row's shape and following it every frame:\na text field, or options. A label shows wherever its row shows and its text fits the room the\nrow gives it, such as inside a block. Labels beside or along their rows never overlap: where they\nwould, the label of the row drawn on top shows, so a mark's `priority` decides, and a dense view\nlabels as many rows as fit. A row whose text is missing or empty has no label, so a `when` choosing\nbetween the text field and missing labels only the rows it names. In a diagram a label is part\nof the drawing: it scales with the zoom, fades out when too small to read, and shows wherever it\nfits its vertex.\n\n```\n# use latkit::{field, when, zoom, Expr, Label, Vertices};\nlet named = when(zoom().ge(6.0)).then(field(\"name\")).otherwise(Expr::missing());\nlet buses = Vertices::of(\"grid\", \"Bus\").position(field(\"lon\"), field(\"lat\"));\nlet labeled = buses.label(Label::new(named));\n```" }, "MarkOptions": { "type": "object", "description": "The options every mark has.", "properties": { - "of": { - "type": "array", - "prefixItems": [ - { - "type": "string" - }, - { - "type": "string" - } - ], - "minItems": 2, - "maxItems": 2, - "description": "The dataset and table drawn.", - "tsType": "[data: string, table: string]" - }, "color": { "anyOf": [ { @@ -1972,7 +1925,7 @@ "type": "null" } ], - "description": "Each row's priority, from its data alone (no view state). Higher-priority rows draw\non top, and are the last dropped when a table too large to draw whole is thinned to\nthe tiles in view." + "description": "Each row's priority, from its data alone (no view state). Higher-priority rows draw\non top, their labels win where labels would overlap, and they are the last dropped\nwhen a table too large to draw whole is thinned to the tiles in view." }, "name": { "type": [ @@ -2014,10 +1967,7 @@ ], "description": "A legend of the mark's colors at the view's bottom right, under this title: a ticked\ncolormap across its domain, a cell for each category, or a swatch beside each name\nof a map's `categories`. `color` must map through a colormap with a fixed domain or\nnamed categories; in a conditional, `otherwise` is searched before the cases." } - }, - "required": [ - "of" - ] + } }, "Edges": { "allOf": [ @@ -2029,8 +1979,23 @@ }, { "type": "object", - "description": "The mark's JSON form: `of`, its own options, then the stroke options and the options\nevery mark has.", + "description": "The mark's JSON form: the key naming its kind, its own options, then the stroke options\nand the options every mark has.", "properties": { + "edges": { + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "minItems": 2, + "maxItems": 2, + "description": "The dataset and table drawn, `[data, table]`.", + "tsType": "TableInput" + }, "ends": { "type": [ "array", @@ -2093,7 +2058,10 @@ "description": "Draws each row as a net on a plane or the earth, joining the vertex rows of `table` whose\n`reference` field names it: a star from their center to each member.", "tsType": "[table: string, reference: string]" } - } + }, + "required": [ + "edges" + ] } ], "unevaluatedProperties": false, @@ -2151,8 +2119,23 @@ }, { "type": "object", - "description": "The mark's JSON form: `of`, its own options, then the stroke options and the options\nevery mark has.", + "description": "The mark's JSON form: the key naming its kind, its own options, then the stroke options\nand the options every mark has.", "properties": { + "faces": { + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "minItems": 2, + "maxItems": 2, + "description": "The dataset and table drawn, `[data, table]`.", + "tsType": "TableInput" + }, "corners": { "type": [ "array", @@ -2166,7 +2149,10 @@ "description": "The reference fields naming each row's three vertices; required. Without a color, a face\nblends the colors of its corners.", "tsType": "[a: string, b: string, c: string]" } - } + }, + "required": [ + "faces" + ] } ], "unevaluatedProperties": false, @@ -2182,8 +2168,23 @@ }, { "type": "object", - "description": "The mark's JSON form: `of`, its own options, then the stroke options and the options\nevery mark has.", + "description": "The mark's JSON form: the key naming its kind, its own options, then the stroke options\nand the options every mark has.", "properties": { + "lines": { + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "minItems": 2, + "maxItems": 2, + "description": "The dataset and table drawn, `[data, table]`.", + "tsType": "TableInput" + }, "geometry": { "type": [ "string", @@ -2202,7 +2203,10 @@ "$ref": "#/$defs/Interpolation", "description": "How each row runs between its samples on a plot; `linear` by default." } - } + }, + "required": [ + "lines" + ] } ], "unevaluatedProperties": false, @@ -2235,8 +2239,23 @@ }, { "type": "object", - "description": "The mark's JSON form: `of`, its own options, then the stroke options and the options\nevery mark has.", + "description": "The mark's JSON form: the key naming its kind, its own options, then the stroke options\nand the options every mark has.", "properties": { + "areas": { + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "minItems": 2, + "maxItems": 2, + "description": "The dataset and table drawn, `[data, table]`.", + "tsType": "TableInput" + }, "geometry": { "type": [ "string", @@ -2244,7 +2263,10 @@ ], "description": "The polygon field each row fills, holding one polygon's rings or several polygons; the field\nnamed `geometry` by default." } - } + }, + "required": [ + "areas" + ] } ], "unevaluatedProperties": false, @@ -4612,7 +4634,7 @@ "type": "string" }, "path": { - "description": "The invalid part of the input, such as `marks[2].vertices.color`, on `invalid` failures.", + "description": "The invalid part of the input, such as `marks[2].color`, on `invalid` failures.", "type": "string" } }, diff --git a/npm/schema.mjs b/npm/schema.mjs index cf0d99ee..6a6bd22c 100644 --- a/npm/schema.mjs +++ b/npm/schema.mjs @@ -26,7 +26,7 @@ const OUTPUTS = [ ]; /** Hand-written types that a `tsType` names, keyed by the module that declares them. */ -const IMPORTS = { './scene.js': ['Color', 'ExprInput'] }; +const IMPORTS = { './data.js': ['TableInput'], './scene.js': ['Color', 'ExprInput'] }; /** Returns the sources of `inputs.ts` and `outputs.ts` for the schema at `file`, formatted with the * repository's Prettier config. */ diff --git a/npm/src/canvas.ts b/npm/src/canvas.ts index 8049b3e9..ba0488eb 100644 --- a/npm/src/canvas.ts +++ b/npm/src/canvas.ts @@ -1,102 +1,99 @@ -/** Canvases that show views in a `` element or an `OffscreenCanvas`. */ +/** Canvases, which views create: the views shown in one `` or `OffscreenCanvas`. */ import type * as bindings from '../pkg/latkit.js'; import { Handle, inner } from './internal.js'; -import { Shot, type ShotOptions } from './shot.js'; import { handleOf, type View } from './view.js'; /** A region `[x, y, width, height]` in fractions of a canvas or picture, from its top left. */ export type Rect = readonly [x: number, y: number, width: number, height: number]; -/** A view and the region of a canvas or picture it draws in. */ -export interface Panel { - /** The view drawn. */ - view: View; - /** Where the view draws; the whole canvas or picture by default. */ - at?: Rect; -} - -/** The rect covering a whole canvas or picture. */ -const WHOLE: Rect = [0, 0, 1, 1]; +/** @internal A `` element or an `OffscreenCanvas` that views draw in. */ +export type Target = HTMLCanvasElement | OffscreenCanvas; /** - * Shows views in a `` element or an `OffscreenCanvas`, each in its layout panel; created by - * `Gpu.canvas()`. A frame redraws only the views that need it, or every view after a resize or a - * layout change. Destroying it stops drawing; the views stay usable, and the element keeps its last - * frame. + * @internal Shows views in a `` element or an `OffscreenCanvas`, each in its region, later + * views over earlier ones. A `` element sizes itself to its CSS box at the device pixel + * ratio and passes pointer, wheel, and key input to the view under the pointer. A frame redraws + * only the views that need it, or every view after a resize or a change of views. It is destroyed + * with its last view, and the element keeps its last frame. */ export class Canvas extends Handle { - #target: HTMLCanvasElement | OffscreenCanvas; - #layout: readonly Panel[]; + #views: View[] = []; #released: () => void; private constructor( device: bindings.Gpu, - target: HTMLCanvasElement | OffscreenCanvas, - options: { layout?: readonly Panel[]; pixelRatio?: number }, + target: Target, + pixelRatio: number | undefined, released: () => void, ) { - const layout = options.layout ?? []; - super(device.canvas(target, ...encoded(layout), options.pixelRatio), 'Canvas'); - this.#target = target; - this.#layout = [...layout]; + super(device.canvas(target, ...encoded([]), pixelRatio), 'Canvas'); this.#released = released; } - /** @internal Creates a canvas of `device` in `target`; `released` runs once it is destroyed. */ + /** Creates a canvas of `device` in `target`; `released` runs once it is destroyed. */ static made( device: bindings.Gpu, - target: HTMLCanvasElement | OffscreenCanvas, - options: { layout?: readonly Panel[]; pixelRatio?: number }, + target: Target, + pixelRatio: number | undefined, released: () => void, ): Canvas { - return new Canvas(device, target, options, released); + return new Canvas(device, target, pixelRatio, released); } - /** The `` element or `OffscreenCanvas` it draws into. */ - get element(): HTMLCanvasElement | OffscreenCanvas { - return this.#target; + /** The views shown, in drawing order. */ + get views(): readonly View[] { + return this.#views; } - /** - * The views shown and their regions; later panels draw over earlier ones. Setting it replaces the - * shown views. A view shows in at most one panel of one canvas at a time. - */ - get layout(): readonly Panel[] { - return this.#layout; + /** The canvas's size in CSS pixels. */ + get cssSize(): [width: number, height: number] { + const [width = 1, height = 1] = inner(this).cssSize; + return [width, height]; } - set layout(layout: readonly Panel[]) { - inner(this).setLayout(...encoded(layout)); - this.#layout = [...layout]; + /** Shows `view` over the others. */ + add(view: View): void { + this.#views.push(view); + this.relayout(); } - /** - * A {@link Shot} of exactly what the canvas shows: its views in its layout, at the canvas's CSS - * size times `options.scale` in pixels. - */ - shot(options: ShotOptions = {}): Shot { - const scale = options.scale ?? 1; - const css = inner(this).cssSize; - const [width = 1, height = 1] = Array.from(css, (side) => - Math.max(1, Math.round(side * scale)), - ); - return new Shot([width, height], { ...options, layout: this.#layout }); + /** Stops showing `view`; with no view left, destroys the canvas. */ + remove(view: View): void { + this.#views = this.#views.filter((shown) => shown !== view); + if (this.#views.length) this.relayout(); + else this.destroy(); + } + + /** Tells the bindings where each view draws, after a view or its region changed. */ + relayout(): void { + inner(this).setLayout(...encoded(this.#views)); } - /** @internal Tells the GPU the canvas is gone. */ protected override freeing(): void { + this.#views = []; this.#released(); } } +/** A view drawn into a picture, in its own region or in `region`. */ +export type Placed = + | View + | { + /** The view drawn. */ + readonly view: View; + /** Where it draws in the picture, in place of its own region. */ + readonly region: Rect; + }; + /** - * @internal A layout as the bindings take it: each panel's view, and its rect's four fractions in - * one array, the whole target where `at` is unset. + * @internal Views as the bindings take them: each view's handle, and its region's four fractions + * in one array. */ -export function encoded(layout: readonly Panel[]): [bindings.Handle[], Float32Array] { +export function encoded(views: readonly Placed[]): [bindings.Handle[], Float32Array] { + const placed = views.map((v) => ('view' in v ? v : { view: v, region: v.region })); return [ - layout.map(({ view }) => handleOf(view)), - Float32Array.from(layout.flatMap(({ at }) => at ?? WHOLE)), + placed.map(({ view }) => handleOf(view)), + Float32Array.from(placed.flatMap(({ region }) => region)), ]; } diff --git a/npm/src/data.ts b/npm/src/data.ts index fbfd1e35..4b72e2c6 100644 --- a/npm/src/data.ts +++ b/npm/src/data.ts @@ -5,7 +5,35 @@ import type { EnvelopeRequest, Id, Query } from './inputs.js'; import { bytesOf, Handle, inner, wasm, waited, type ByteSource, type Wait } from './internal.js'; import { load } from './load.js'; import type { Description, Value } from './outputs.js'; -import { framed, type Patch } from './patch.js'; +import { framed, Patch, type Fields, type RowIds, type Run, type Values } from './patch.js'; + +/** + * A table of a dataset, which a mark draws or a field reads from: `grid.table('Bus')`. A view draws + * the datasets its scene's tables belong to. Its JSON form is `[data, table]`. + */ +export class Table { + /** The dataset the table belongs to. */ + readonly source: Source; + /** The table's name. */ + readonly name: string; + + /** Table `name` of `source`. */ + constructor(source: Source, name: string) { + this.source = source; + this.name = name; + } + + /** The JSON form, `[data, table]`, which `JSON.stringify` writes for the table. */ + toJSON(): [data: string, table: string] { + return [this.source.name, this.name]; + } +} + +/** + * A table that a mark draws or a field reads from: a dataset's {@link Table}, or its JSON form + * `[data, table]`, which names a dataset the view's `sources` give. + */ +export type TableInput = Table | readonly [data: string, table: string]; /** The rows a query returns, as columns in the forms a patch takes. */ export interface QueryResult { @@ -41,11 +69,16 @@ type Reads = bindings.Data | bindings.Snapshot; * and a {@link Snapshot}, which reads its own. */ export abstract class Dataset extends Handle { - /** The dataset's name, by which a mark's `data` option draws it. */ + /** The dataset's name: the first part of its tables' JSON form. */ get name(): string { return inner(this).name; } + /** The dataset's table `name`, for a mark to draw or a field to read from. */ + table(name: string): Table { + return new Table(this as Dataset as Data | Snapshot, name); + } + /** The tables and their row counts, each field and its kind, and the axis and its span. */ describe(): Description { return JSON.parse(inner(this).describe()) as Description; @@ -93,15 +126,15 @@ export abstract class Dataset extends Handle { } /** - * A dataset that changes in place, only by patches, each applied whole or not at all. Views draw - * its latest version from the frame after each patch. A {@link Snapshot} keeps one version and - * shares its pages with the dataset, so an undo history costs only the pages that differ. - * Destroying it leaves views drawing it and its snapshots usable. + * A dataset that changes in place, each change applied whole or not at all. Each method of + * {@link Patch} is also a method here that applies that one change at once; `apply` applies a patch + * of several. Views draw its latest version from the frame after each change. A {@link Snapshot} + * keeps one version and shares its pages with the dataset, so an undo history costs only the pages + * that differ. Destroying it leaves views drawing it and its snapshots usable. * * ```ts - * const grid = new Data('grid').apply(new Patch().replace('Bus', [1, 2], { kv: [230, 115] })); - * view.sources = [grid]; - * grid.apply(new Patch().set('Bus', [2], { kv: [69] })); // drawn next frame + * const grid = new Data('grid').set('Bus', [1, 2], { kv: [230, 115] }); + * grid.set('Bus', [2], { kv: [69] }); // drawn the next frame * ``` */ export class Data extends Dataset { @@ -147,6 +180,60 @@ export class Data extends Dataset { return this; } + /** Applies {@link Patch.axis}: names the axis that sampled fields are recorded over. */ + axis(name: string, unit: string): this { + return this.apply(new Patch().axis(name, unit)); + } + + /** + * Applies {@link Patch.replace}: replaces table `table` with rows `ids`, or `0` to `n - 1`, and + * `fields`. + */ + replace(table: string, fields: Fields): this; + replace(table: string, ids: RowIds, fields?: Fields): this; + replace(table: string, ids: RowIds | Fields, fields?: Fields): this { + // `Patch.replace` tells ids from fields itself. + return this.apply(new Patch().replace(table, ids as RowIds, fields)); + } + + /** + * Applies {@link Patch.set}: writes `fields` for rows `ids` of `table`, or `0` to `n - 1`, adding + * new rows. + */ + set(table: string, fields: Fields): this; + set(table: string, ids: RowIds, fields?: Fields): this; + set(table: string, ids: RowIds | Fields, fields?: Fields): this { + return this.apply(new Patch().set(table, ids as RowIds, fields)); + } + + /** Applies {@link Patch.remove}: removes the rows of `table` with `ids`. */ + remove(table: string, ids: RowIds): this { + return this.apply(new Patch().remove(table, ids)); + } + + /** Applies {@link Patch.append}: records frames of `table` at the axis coordinates `at`. */ + append( + table: string, + at: number | readonly number[], + fields: Readonly>, + options?: { + /** The rows the frames cover, every row by default; other rows keep their last values. */ + ids?: RowIds; + }, + ): this { + return this.apply(new Patch().append(table, at, fields, options)); + } + + /** Applies {@link Patch.forget}: drops every sampled field's frames before `before`. */ + forget(before: number): this { + return this.apply(new Patch().forget(before)); + } + + /** Applies {@link Patch.frames}: stores a run of a sampled field's entries, as a time tile holds. */ + frames(table: string, field: string, run: Run): this { + return this.apply(new Patch().frames(table, field, run)); + } + /** The current version as a {@link Snapshot}, which later patches leave unchanged. O(1). */ snapshot(): Snapshot { return Snapshot.made(inner(this).snapshot()); @@ -199,9 +286,14 @@ export class Tiles { this.name = name; this.url = url; } + + /** The tiles' table `name`, such as a vector tile layer, for a mark to draw. */ + table(name: string): Table { + return new Table(this, name); + } } -/** A dataset a view draws; views match it to the scene's marks by the dataset's name. */ +/** A dataset a view draws: the one a {@link Table} belongs to, or one of the view's `sources`. */ export type Source = Data | Snapshot | Tiles; /** @internal Converts `source` to a new bindings `Source` on every call. */ diff --git a/npm/src/gpu.ts b/npm/src/gpu.ts index 3846cc97..66d0a080 100644 --- a/npm/src/gpu.ts +++ b/npm/src/gpu.ts @@ -1,22 +1,26 @@ -/** The GPU device: it creates views and canvases, owns their shared memory, and runs frames. */ +/** The GPU device: it draws views on their canvases, owns their shared memory, and runs frames. */ import type * as bindings from '../pkg/latkit.js'; -import { Canvas, type Panel } from './canvas.js'; -import type { Source } from './data.js'; -import type { Budget, Scene } from './inputs.js'; +import { Canvas, type Target } from './canvas.js'; +import type { Budget } from './inputs.js'; import { Handle, inner, wasm, waited, type Wait } from './internal.js'; import { firstDevice, load } from './load.js'; import type { Work } from './outputs.js'; -import { View } from './view.js'; +import type { View } from './view.js'; /** - * A WebGPU device that views draw with. The views and canvases of one `Gpu` share its memory and - * draw together in each animation frame, so linked views always show the same moment. Destroying - * it destroys its canvases, views, and frame listeners; other GPUs and datasets stay usable. + * A WebGPU device that views draw with. The views of one `Gpu` share its memory and draw together + * in each animation frame, so linked views always show the same moment. Views use the first `Gpu` + * created unless given another, and `View.create` creates it when there is none. Destroying it + * destroys its views and frame listeners; other GPUs and datasets stay usable. */ export class Gpu extends Handle { + /** The GPU views use when given none, and the request for it while one is being created. */ + static #shared: Gpu | undefined; + static #creating: Promise | undefined; + #views = new Set(); - #canvases = new Set(); + #canvases = new Map(); #listeners = new Set<() => void>(); private constructor(device: bindings.Gpu) { @@ -24,48 +28,22 @@ export class Gpu extends Handle { } /** - * Requests a WebGPU device; the first call takes the one `load()` requested. Rejects with an - * `unsupported` error when the browser has no WebGPU device. + * Requests a WebGPU device; the first call takes the one `load()` requested. Create one before + * any view to set its `budget` first, or another for views that should not share memory with the + * rest. Rejects with an `unsupported` error when the browser has no WebGPU device. */ static async create(): Promise { await load(); - return new Gpu(await (firstDevice() ?? wasm().Gpu.create())); + const gpu = new Gpu(await (firstDevice() ?? wasm().Gpu.create())); + if (!Gpu.#shared || Gpu.#shared.lost) Gpu.#shared = gpu; + return gpu; } - /** - * Creates a view that draws `scene`. Show it in a {@link Canvas}, or draw it into pictures with a - * `Shot`, with or without a canvas. - */ - view( - scene: Scene, - options: { - /** The datasets the scene draws; see {@link View.sources}. */ - sources?: readonly Source[]; - } = {}, - ): View { - const view = View.made(device(this), scene, () => this.#views.delete(view)); - this.#views.add(view); - if (options.sources) view.sources = options.sources; - return view; - } - - /** - * Creates a canvas that shows views in `target`. A `` element sizes itself to its CSS box - * at the device pixel ratio, and passes pointer, wheel, and key input to the views in its panels. - * An `OffscreenCanvas`, as in a worker, keeps the size it is given and takes no input. - */ - canvas( - target: HTMLCanvasElement | OffscreenCanvas, - options: { - /** The views shown and their regions; see {@link Canvas.layout}. None by default. */ - layout?: readonly Panel[]; - /** Pixels per CSS pixel of an `OffscreenCanvas`, 1 by default. */ - pixelRatio?: number; - } = {}, - ): Canvas { - const canvas = Canvas.made(device(this), target, options, () => this.#canvases.delete(canvas)); - this.#canvases.add(canvas); - return canvas; + /** @internal The GPU views use when given none, created when there is none. */ + static shared(): Promise { + const shared = Gpu.#shared; + if (shared && !shared.lost) return Promise.resolve(shared); + return (Gpu.#creating ??= Gpu.create().finally(() => (Gpu.#creating = undefined))); } /** @@ -144,14 +122,37 @@ export class Gpu extends Handle { await waited(device(this).landed(), wait, 'waiting for work'); } - /** @internal Destroys the GPU's canvases, views, and frame listeners before its device. */ + /** + * @internal Keeps `view` until it or the GPU is destroyed, and shows it on `target`, over the + * views already there. Returns the canvas showing it. + */ + adopt(view: View, target: Target | null, pixelRatio?: number): Canvas | undefined { + this.#views.add(view); + if (!target) return undefined; + let canvas = this.#canvases.get(target); + if (!canvas) { + canvas = Canvas.made(device(this), target, pixelRatio, () => this.#canvases.delete(target)); + this.#canvases.set(target, canvas); + } + canvas.add(view); + return canvas; + } + + /** @internal Forgets `view`, once it is destroyed. */ + release(view: View): void { + this.#views.delete(view); + } + + /** @internal Destroys the GPU's views, canvases, and frame listeners before its device. */ protected override freeing(): void { - for (const canvas of this.#canvases) canvas.destroy(); + if (Gpu.#shared === this) Gpu.#shared = undefined; for (const view of this.#views) view.destroy(); + for (const canvas of this.#canvases.values()) canvas.destroy(); for (const stop of this.#listeners) stop(); } } -function device(owner: Gpu): bindings.Gpu { +/** @internal Returns `owner`'s bindings device. */ +export function device(owner: Gpu): bindings.Gpu { return inner(owner); } diff --git a/npm/src/index.ts b/npm/src/index.ts index c32f4892..15d08115 100644 --- a/npm/src/index.ts +++ b/npm/src/index.ts @@ -1,11 +1,13 @@ +/// /// /** * latkit: columnar data drawn with WebGPU. * * Scenes, expressions, and patches are plain data and need no WebAssembly. Datasets, views, * pictures, and the colormap functions need it, so `await load()` first. Every `Handle` also works - * with `using`; the reference above makes `Symbol.dispose` typecheck whatever a project's `lib` - * setting. + * with `using`. The references above make the browser types latkit names (canvases, `Blob`, + * `Response`, `WebAssembly.Module`) and `Symbol.dispose` typecheck whatever a project's `lib` + * setting, in Node.js too. */ export type * from './inputs.js'; export type * from './outputs.js'; @@ -21,10 +23,12 @@ export { Data, Dataset, Snapshot, + Table, Tiles, type Envelope, type QueryResult, type Source, + type TableInput, } from './data.js'; export { Patch, @@ -33,11 +37,12 @@ export { type Geometry, type Reference, type RowIds, + type Run, type Values, } from './patch.js'; export * from './scene.js'; export { colormapCss, colormaps } from './colormap.js'; export { Gpu } from './gpu.js'; export { View, type Insets, type Point, type Position, type ViewEvents } from './view.js'; -export { Canvas, type Panel, type Rect } from './canvas.js'; +export type { Placed, Rect } from './canvas.js'; export { Image, Shot, Video, type ImageFormat, type ShotOptions } from './shot.js'; diff --git a/npm/src/inputs.ts b/npm/src/inputs.ts index 5841a1c9..db7ada86 100644 --- a/npm/src/inputs.ts +++ b/npm/src/inputs.ts @@ -1,5 +1,6 @@ /** The types latkit reads, written by `node npm/build.mjs schema` from `npm/schema.json`. Do not edit. */ +import type { TableInput } from './data.js'; import type { Color, ExprInput } from './scene.js'; /** Defaults for areas. */ @@ -15,6 +16,8 @@ export interface AreaTheme { * add a `Lines` mark of the same table, which draws their rings. */ export interface Areas extends MarkOptions { + /** The dataset and table drawn, `[data, table]`. */ + areas: TableInput; /** * The polygon field each row fills, holding one polygon's rings or several polygons; the field * named `geometry` by default. @@ -478,6 +481,8 @@ export interface EdgeTheme { * field names it. */ export interface Edges extends MarkOptions, StrokeOptions { + /** The dataset and table drawn, `[data, table]`. */ + edges: TableInput; /** The reference fields naming each row's two vertices. */ ends?: [from: string, to: string] | null; /** A path field whose points each row passes through between its ends. */ @@ -550,6 +555,8 @@ export type ExprNode = * from above shows its exact colors. */ export interface Faces extends MarkOptions { + /** The dataset and table drawn, `[data, table]`. */ + faces: TableInput; /** * The reference fields naming each row's three vertices; required. Without a color, a face blends * the colors of its corners. @@ -565,7 +572,7 @@ export interface FieldNode { /** The field's name. */ field: string; /** The dataset and table the field is read from, joined by row id. */ - from?: [data: string, table: string] | null; + from?: TableInput | null; /** * For a sampled field, reads the last frame at or before the shown coordinate instead of * interpolating. Default `false`. @@ -687,18 +694,20 @@ export type Interpolation = /** * Text for each row, placed beside, inside, or along the row's shape and following it every frame: * a text field, or options. A label shows wherever its row shows and its text fits the room the - * row gives it, such as inside a block, whatever other labels it meets. A row whose text is - * missing has no label, so a `when` choosing between the text field and missing labels only the - * rows it names. In a diagram a label is part of the drawing: it scales with the zoom, fades out - * when too small to read, and shows wherever it fits its vertex. + * row gives it, such as inside a block. Labels beside or along their rows never overlap: where + * they would, the label of the row drawn on top shows, so a mark's `priority` decides, and a dense + * view labels as many rows as fit. A row whose text is missing or empty has no label, so a `when` + * choosing between the text field and missing labels only the rows it names. In a diagram a label + * is part of the drawing: it scales with the zoom, fades out when too small to read, and shows + * wherever it fits its vertex. */ export type Label = | ExprInput | { /** * The text field to show, or a `when` whose values are each that field or missing. A row whose - * text is missing has no label. In a diagram the text reads only data, since the sheet sizes - * blocks around it. + * text is missing or empty has no label. In a diagram the text reads only data, since the sheet + * sizes blocks around it. */ text: ExprInput; /** @@ -780,6 +789,8 @@ export interface LineTheme { * antimeridian. */ export interface Lines extends MarkOptions, StrokeOptions { + /** The dataset and table drawn, `[data, table]`. */ + lines: TableInput; /** * The path or polygon field each row runs along; the field named `geometry` by default. Not * allowed on a plot. @@ -794,33 +805,14 @@ export interface Lines extends MarkOptions, StrokeOptions { interpolation?: Interpolation; } -/** One table a scene draws. In JSON: `{ vertices | edges | faces | lines | areas: options }`. */ -export type Mark = - | { - /** Rows drawn as markers. */ - vertices: Vertices; - } - | { - /** Rows drawn as strokes between two vertices. */ - edges: Edges; - } - | { - /** Rows drawn as triangles between three vertices. */ - faces: Faces; - } - | { - /** Rows drawn as lines along their paths, or on a plot through their samples. */ - lines: Lines; - } - | { - /** Rows drawn as the areas their polygons cover. */ - areas: Areas; - }; +/** + * One table a scene draws. The key naming the mark's kind holds its dataset and table, beside its + * options: `{ vertices: [data, table], radius: 4 }`. + */ +export type Mark = Vertices | Edges | Faces | Lines | Areas; /** The options every mark has. */ export interface MarkOptions { - /** The dataset and table drawn. */ - of: [data: string, table: string]; /** * Each row's color: a CSS color, a value through a colormap, or a conditional choosing between * colors. Unset, rows take the theme's default for the mark; faces instead blend their corners' @@ -830,8 +822,9 @@ export interface MarkOptions { /** Whether each row is drawn and can be picked. Default `true`. */ visible?: ExprInput; /** - * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, and - * are the last dropped when a table too large to draw whole is thinned to the tiles in view. + * Each row's priority, from its data alone (no view state). Higher-priority rows draw on top, + * their labels win where labels would overlap, and they are the last dropped when a table too + * large to draw whole is thinned to the tiles in view. */ priority?: ExprInput | null; /** Distinguishes two marks of the same table across scenes, and names the mark in hits. */ @@ -1073,8 +1066,8 @@ export interface ScaleNode { * A view's content and behavior: a space, marks drawn in order, a layout, a theme, controls, an * ease duration, and an optional title. * - * A scene is plain data. In JSON, fields are camelCase and each mark is - * `{ vertices | edges | faces | lines | areas: options }`. + * A scene is plain data. In JSON, fields are camelCase, and the key naming each mark's kind holds + * its dataset and table: `{ "vertices": ["grid", "Bus"], "radius": 4 }`. */ export interface Scene { /** The coordinate space positions are in. */ @@ -1280,6 +1273,8 @@ export interface VertexTheme { /** Rows drawn as markers: points of a map, nodes of a graph, blocks of a diagram. */ export interface Vertices extends MarkOptions { + /** The dataset and table drawn, `[data, table]`. */ + vertices: TableInput; /** * Each row's x and y. Without a position, the point field that `geometry` names places rows, and * the scene's layout places rows that have neither. diff --git a/npm/src/internal.ts b/npm/src/internal.ts index 31577437..109bc775 100644 --- a/npm/src/internal.ts +++ b/npm/src/internal.ts @@ -23,7 +23,7 @@ export interface LatkitError extends Error { * - `destroyed`: the object was destroyed, so it can no longer be used. */ code: Failure['code'] | 'unloaded' | 'destroyed'; - /** The invalid part of the input, such as `marks[2].vertices.color`, on `invalid` errors. */ + /** The invalid part of the input, such as `marks[2].color`, on `invalid` errors. */ path?: string; } diff --git a/npm/src/outputs.ts b/npm/src/outputs.ts index 22bbb4b0..44a117d5 100644 --- a/npm/src/outputs.ts +++ b/npm/src/outputs.ts @@ -38,7 +38,7 @@ export interface Failure { code: 'invalid' | 'unsupported' | 'limit' | 'io' | 'device-lost' | 'cancelled' | 'timeout'; /** What failed, in words. */ message: string; - /** The invalid part of the input, such as `marks[2].vertices.color`, on `invalid` failures. */ + /** The invalid part of the input, such as `marks[2].color`, on `invalid` failures. */ path?: string; } diff --git a/npm/src/patch.ts b/npm/src/patch.ts index 2d902c0d..58d8b392 100644 --- a/npm/src/patch.ts +++ b/npm/src/patch.ts @@ -62,6 +62,21 @@ export function reference(table: string, ids: RowIds): Reference { /** A table's fields by name: values, or a {@link Reference}. */ export type Fields = Readonly>; +/** A run of a sampled field's entries at one level of detail, as {@link Patch.frames} stores. */ +export interface Run { + /** The level of detail, from 0 to 21; 0 by default. */ + level?: number; + /** The first entry's number: a safe nonnegative integer. */ + first: number; + /** Each frame's coordinate, or each bucket's first and last frames' coordinates. */ + at: Float64Array | readonly number[]; + /** + * A value per row, entry by entry; at a bucket level, every bucket's per-row minimums, then every + * bucket's per-row maximums. NaN marks a missing value. + */ + values: Float32Array | readonly number[]; +} + const MAGIC = [0x4c, 0x41, 0x54, 0x4b]; // LATK /** Header bytes: the magic and the frame's length. */ const HEADER = 12; @@ -240,17 +255,25 @@ export class Patch { }); } - /** Replaces table `table` with rows `ids` and `fields`, adding the table if it does not exist. */ - replace(table: string, ids: RowIds, fields: Fields = {}): this { - return this.#op(OP.replace, (w) => writeTable(w, table, ids, fields)); + /** + * Replaces table `table` with rows `ids` and `fields`, adding the table if it does not exist. + * Without ids, the rows are `0` to `n - 1`, `n` being the length of each field's values. + */ + replace(table: string, fields: Fields): this; + replace(table: string, ids: RowIds, fields?: Fields): this; + replace(table: string, ids: RowIds | Fields, fields?: Fields): this { + return this.#op(OP.replace, (w) => writeTable(w, table, ...rows(ids, fields))); } /** * Writes `fields` for rows `ids` of `table`. Rows with existing ids change, new ids add rows, and - * omitted fields keep their values. Adds the table if it does not exist. + * omitted fields keep their values. Adds the table if it does not exist. Without ids, the rows + * are `0` to `n - 1`, `n` being the length of each field's values. */ - set(table: string, ids: RowIds, fields: Fields = {}): this { - return this.#op(OP.set, (w) => writeTable(w, table, ids, fields)); + set(table: string, fields: Fields): this; + set(table: string, ids: RowIds, fields?: Fields): this; + set(table: string, ids: RowIds | Fields, fields?: Fields): this { + return this.#op(OP.set, (w) => writeTable(w, table, ...rows(ids, fields))); } /** Removes the rows of `table` with `ids`. */ @@ -319,23 +342,7 @@ export class Patch { * `k`, a bucket of `8^k` frames. None of the entries may be stored already. Entries past the * field's last frame extend it. */ - frames( - table: string, - field: string, - run: { - /** The level of detail, from 0 to 21; 0 by default. */ - level?: number; - /** The first entry's number: a safe nonnegative integer. */ - first: number; - /** Each frame's coordinate, or each bucket's first and last frames' coordinates. */ - at: Float64Array | readonly number[]; - /** - * A value per row, entry by entry; at a bucket level, every bucket's per-row minimums, then - * every bucket's per-row maximums. NaN marks a missing value. - */ - values: Float32Array | readonly number[]; - }, - ): this { + frames(table: string, field: string, run: Run): this { const { level = 0, first, at, values } = run; const path = `${table}.${field}`; if (!Number.isInteger(level) || level < 0 || level > LEVELS) @@ -493,6 +500,30 @@ function isReference(field: Values | Reference): field is Reference { return !ArrayBuffer.isView(field) && !Array.isArray(field) && 'reference' in field; } +/** + * The ids and fields of `replace` or `set`, given as `(ids, fields)`, or as `(fields)` whose rows + * are `0` to `n - 1`, `n` being the length of the first field's values. + */ +function rows(ids: RowIds | Fields, fields: Fields | undefined): [RowIds, Fields] { + if (fields !== undefined || isIds(ids)) return [ids as RowIds, fields ?? {}]; + const first = Object.values(ids)[0]; + return [{ count: first ? length(isReference(first) ? first.ids : first) : 0 }, ids]; +} + +/** Whether `x` is row ids rather than fields: a list, or `{ count }`, since no field is a number. */ +function isIds(x: RowIds | Fields): x is RowIds { + return ( + Array.isArray(x) || + ArrayBuffer.isView(x) || + typeof (x as { count?: unknown }).count === 'number' + ); +} + +/** How many values or ids `x` holds. */ +function length(x: Values | RowIds): number { + return 'length' in x ? x.length : x.count; +} + /** * Returns the frame length from the header, or `undefined` until `bytes` hold a whole header. * Throws for bytes that are not a frame, or a frame longer than `limit`. diff --git a/npm/src/scene.ts b/npm/src/scene.ts index 5ac56d27..c6a7ee30 100644 --- a/npm/src/scene.ts +++ b/npm/src/scene.ts @@ -3,6 +3,7 @@ * `inputs.ts`; nothing here needs WebAssembly. */ +import type { TableInput } from './data.js'; import type { Colormap, ColormapNode, @@ -49,9 +50,12 @@ export class Expr { return this.node; } - /** Reads this expression's fields from table `table` of dataset `data`, joined by row id. */ - from(data: string, table: string): Expr { - return new Expr(mapFields(this.node, (f) => ({ ...f, from: [data, table] }))); + /** + * Reads this expression's fields from `table`, such as `weather.table('Station')`, joined by row + * id. A view draws the table's dataset too. + */ + from(table: TableInput): Expr { + return new Expr(mapFields(this.node, (f) => ({ ...f, from: table }))); } /** diff --git a/npm/src/shot.ts b/npm/src/shot.ts index 78d6d87a..8016ffe8 100644 --- a/npm/src/shot.ts +++ b/npm/src/shot.ts @@ -1,8 +1,9 @@ /** Pictures of views: images to encode as files, and H.264 video written as frames are drawn. */ import type * as bindings from '../pkg/latkit.js'; -import { encoded, type Panel } from './canvas.js'; +import { encoded, type Placed } from './canvas.js'; import { Handle, inner, thrown, waited, wasm, type LatkitError, type Wait } from './internal.js'; +import { canvasOf, type View } from './view.js'; /** Options for a {@link Shot}. */ export interface ShotOptions { @@ -27,38 +28,58 @@ export interface ShotOptions { } /** - * A picture of views, `size` pixels in all, each view drawn into its layout panel. While a shot - * draws a view, a canvas showing that view keeps its last frame. An image draws each view's data as - * it was when the picture began. A canvas's `shot()` makes a shot of what the canvas shows. + * A picture of views, `size` pixels in all, each view drawn in its region. While a shot draws a + * view, its canvas keeps its last frame. An image draws each view's data as it was when the + * picture began. `Shot.of(view)` makes a shot of what a view's canvas shows. * * ```ts - * const png = await (await canvas.shot({ scale: 2 }).image()).encode('png'); + * const png = await (await Shot.of(view, { scale: 2 }).image()).encode('png'); * ``` */ export class Shot { /** The picture's size, `[width, height]` in pixels. */ readonly size: readonly [number, number]; - /** How the picture is taken, and in `layout`, the panel each view is drawn into. */ - readonly options: ShotOptions & { layout?: readonly Panel[] }; + /** How the picture is taken, and in `views`, the views drawn into it. */ + readonly options: ShotOptions & { views?: readonly Placed[] }; /** Creates a shot of `size`, `[width, height]` in pixels. */ constructor( size: readonly [number, number], options: ShotOptions & { - /** The views drawn, each in its panel of the picture. */ - layout?: readonly Panel[]; + /** + * The views drawn, later views over earlier ones: each in its own region, or, given as + * `{ view, region }`, in that region of the picture. + */ + views?: readonly Placed[]; } = {}, ) { this.size = size; this.options = options; } + /** + * A shot of exactly what the canvas showing `view` shows: its views in their regions, at the + * canvas's CSS size times `options.scale` in pixels. Throws an `invalid` error for a view on no + * canvas, which a shot must be given a size for. + */ + static of(view: View, options: ShotOptions = {}): Shot { + const canvas = canvasOf(view); + if (!canvas) + throw thrown({ + code: 'invalid', + message: 'the view is on no canvas: create a Shot of a size', + }); + const scale = options.scale ?? 1; + const [width, height] = canvas.cssSize.map((side) => Math.max(1, Math.round(side * scale))); + return new Shot([width!, height!], { ...options, views: canvas.views }); + } + /** * Draws the picture once every view is complete, as `capture` says, and resolves to the * {@link Image}. Rejects with the first error a view reports while drawing. */ async image(wait: Wait = {}): Promise { - const taken = wasm().image(this.#json(), ...encoded(this.options.layout ?? [])); + const taken = wasm().image(this.#json(), ...encoded(this.options.views ?? [])); return Image.made(await waited(taken, wait, 'taking a picture')); } @@ -81,8 +102,8 @@ export class Shot { options.signal?.throwIfAborted(); const writer = options.output.getWriter(); try { - const layout = encoded(this.options.layout ?? []); - const video = wasm().video(this.#json(), ...layout, options.fps, options.bitrate); + const views = encoded(this.options.views ?? []); + const video = wasm().video(this.#json(), ...views, options.fps, options.bitrate); return Video.made(video, writer, options.fps, options.signal); } catch (error) { writer.releaseLock(); @@ -90,7 +111,7 @@ export class Shot { } } - /** The shot as JSON for the bindings, with times in seconds; the layout crosses on its own. */ + /** The shot as JSON for the bindings, with times in seconds; the views cross on their own. */ #json(): string { const { scale = 1, transparent = false, capture = 'still', elapsed } = this.options; return JSON.stringify({ size: [...this.size], scale, transparent, capture, elapsed }); diff --git a/npm/src/view.ts b/npm/src/view.ts index b3ad98e1..b61c920b 100644 --- a/npm/src/view.ts +++ b/npm/src/view.ts @@ -1,16 +1,24 @@ /** - * Views: a scene drawn from the given sources. A view's state is a set of properties, and setting - * one emits no event. When the user changes that state, the view emits an event named after the - * property. Queries are methods, answered by the frames that draw the view. + * Views: a scene drawn on a canvas from its datasets. A view's state is a set of properties, and + * setting one emits no event. When the user changes that state, the view emits an event named after + * the property. Queries are methods, answered by the frames that draw the view. */ import type * as bindings from '../pkg/latkit.js'; -import { Snapshot, sourceOf, type Source } from './data.js'; +import type { Canvas, Rect } from './canvas.js'; +import { Snapshot, sourceOf, Table, type Source } from './data.js'; +import { device, Gpu } from './gpu.js'; import type { Camera, Fit, Row, Rows, Scene } from './inputs.js'; import { Handle, inner, thrown, waited, type LatkitError, type Wait } from './internal.js'; import type { Failure, Hit, Stats, ViewEvent, Waiting } from './outputs.js'; import { Patch } from './patch.js'; +/** The region covering a whole canvas or picture. */ +const WHOLE: Rect = [0, 0, 1, 1]; + +/** @internal Returns the canvas showing `view`, if any. */ +export let canvasOf: (view: View) => Canvas | undefined; + /** * The events a view emits, by type, mapped to each event's detail. Intents (`click`, `open`, * `menu`, `drag`, `connect`, `delete`) are emitted once each, in the order the user acted. Changes @@ -35,31 +43,98 @@ export type Point = [x: number, y: number]; export type Position = [x: number, y: number, elevation: number]; /** - * A scene drawn from its sources; created by `Gpu.view()`. A canvas shows it and passes it input, - * and a `Shot` draws it into pictures. Destroying it rejects its unanswered queries; a canvas - * showing it keeps its last frame. + * A scene drawn from its datasets, on a canvas and into pictures; created by {@link View.create}. + * The canvas passes it pointer, wheel, and key input, and a `Shot` draws it into pictures. + * Destroying it rejects its unanswered queries; its canvas keeps its last frame. + * + * ```ts + * const grid = new Data('grid').set('Bus', [1, 2], { x: [0, 30], y: [0, 20], kv: [230, 115] }); + * const view = await View.create(canvas, { + * space: 'plane', + * marks: [{ vertices: grid.table('Bus'), position: ['$x', '$y'], color: field('kv').colormap('viridis') }], + * }); + * ``` */ export class View extends Handle { + static { + canvasOf = (view) => view.#canvas; + } + #listeners = new Map void>>(); + #gpu: Gpu; + #canvas: Canvas | undefined; + #region: Rect; #scene: Scene; + /** The datasets of the tables the scene names. */ + #tables: readonly Source[]; #sources: readonly Source[] = []; - #released: () => void; - private constructor(device: bindings.Gpu, scene: Scene, released: () => void) { + private constructor(gpu: Gpu, scene: Scene, region: Rect) { // The view sends events once it exists, so the callback finds it through `told`. const told: { view?: View } = {}; const tell = (json: string) => { if (told.view) told.view.#tell(json); }; - super(device.view(JSON.stringify(scene), tell), 'View'); + const { json, tables } = encoded(scene); + super(device(gpu).view(json, tell), 'View'); told.view = this; + this.#gpu = gpu; + this.#region = region; this.#scene = scene; - this.#released = released; + this.#tables = tables; + } + + /** + * Creates a view that draws `scene` on `canvas`, in `options.region`, over any views already on + * it; with a `null` canvas, the view draws only into pictures. It draws the datasets its scene's + * tables belong to, and frames everything once it draws, unless given a camera. + */ + static async create( + canvas: HTMLCanvasElement | OffscreenCanvas | null, + scene: Scene, + options: { + /** Where the view draws on the canvas and in pictures; the whole of them by default. */ + region?: Rect; + /** Where the view looks first. Without one, it frames everything drawn. */ + camera?: Camera; + /** Datasets beside those of the scene's tables; see {@link View.sources}. */ + sources?: readonly Source[]; + /** The GPU that draws the view; the first `Gpu` created by default, created if need be. */ + gpu?: Gpu; + /** Pixels per CSS pixel of an `OffscreenCanvas`, 1 by default; the first view on it sets it. */ + pixelRatio?: number; + } = {}, + ): Promise { + const gpu = options.gpu ?? (await Gpu.shared()); + const view = new View(gpu, scene, options.region ?? WHOLE); + try { + view.sources = options.sources ?? []; + if (options.camera) view.camera = options.camera; + else view.fit(); + view.#canvas = gpu.adopt(view, canvas, options.pixelRatio); + } catch (error) { + view.destroy(); + throw error; + } + return view; + } + + /** The GPU that draws the view. */ + get gpu(): Gpu { + return this.#gpu; + } + + /** + * Where the view draws on its canvas and in pictures: `[x, y, width, height]` in fractions of + * them, from the top left. Views of one canvas draw in the order they were created. + */ + get region(): Rect { + return this.#region; } - /** @internal Creates a view of `scene` on `device`; `released` is called once it is destroyed. */ - static made(device: bindings.Gpu, scene: Scene, released: () => void): View { - return new View(device, scene, released); + set region(region: Rect) { + this.#region = region; + this.#canvas?.relayout(); } /** @@ -71,25 +146,49 @@ export class View extends Handle { } set scene(scene: Scene) { - view(this).setScene(JSON.stringify(scene)); + const { json, tables } = encoded(scene); + const changed = + tables.length !== this.#tables.length || tables.some((t, i) => t !== this.#tables[i]); + const drawn = changed ? this.#drawn(tables, this.#sources) : undefined; + view(this).setScene(json); this.#scene = scene; + this.#tables = tables; + if (drawn) view(this).setSources(drawn); } /** - * The datasets the view draws, matched to marks by dataset name, which must be unique. The view - * draws a `Data` at its latest version, from the frame after each patch; a `Snapshot` unchanged; - * and `Tiles` as the pieces it needs. A mark whose dataset no longer fits it draws nothing; it is - * listed in `failures`, and the next frame emits an `error` event. + * Datasets the view draws besides those of its scene's tables, and in their place where they + * share a name: a mark whose table is written `[data, table]` draws the source named `data`. The + * view draws a `Data` at its latest version, from the frame after each change; a `Snapshot` + * unchanged; and `Tiles` as the pieces it needs. A mark whose dataset no longer fits it draws + * nothing; it is listed in `failures`, and the next frame emits an `error` event. */ get sources(): readonly Source[] { return this.#sources; } set sources(sources: readonly Source[]) { - view(this).setSources(sources.map(sourceOf)); + view(this).setSources(this.#drawn(this.#tables, sources)); this.#sources = [...sources]; } + /** + * The datasets drawn, as the bindings take them: `sources`, then those of the scene's `tables` + * that no source replaces by name. Throws an `invalid` error for two datasets of one name. + */ + #drawn(tables: readonly Source[], sources: readonly Source[]): bindings.Source[] { + const named = new Set(sources.map((source) => source.name)); + const drawn = [...sources, ...tables.filter((table) => !named.has(table.name))]; + const twice = drawn.find((source, i) => drawn.findIndex((s) => s.name === source.name) !== i); + if (twice) + throw thrown({ + code: 'invalid', + message: `two datasets are named '${twice.name}'`, + path: 'sources', + }); + return drawn.map(sourceOf); + } + /** * The axis coordinate shown, such as a time. Marks read sampled fields at it, interpolating * between the frame at or before it and the next, unless an expression uses `step()`. @@ -302,11 +401,13 @@ export class View extends Handle { } } - /** @internal Drops the listeners and tells the GPU the view is gone. */ + /** @internal Drops the listeners, and takes the view off its canvas and its GPU. */ protected override freeing(): void { for (const listeners of this.#listeners.values()) listeners.clear(); this.#listeners.clear(); - this.#released(); + this.#canvas?.remove(this); + this.#canvas = undefined; + this.#gpu.release(this); } } @@ -314,6 +415,18 @@ function view(owner: View): bindings.View { return inner(owner); } +/** `scene` as JSON, and the datasets of the tables it names. */ +function encoded(scene: Scene): { json: string; tables: Source[] } { + const tables = new Set(); + const json = JSON.stringify(scene, function (this: Record, key, value: unknown) { + // `value` is what `toJSON` returned; the holder still has the table itself. + const held = this[key]; + if (held instanceof Table) tables.add(held.source); + return value; + }); + return { json, tables: [...tables] }; +} + /** @internal Returns a new bindings handle to the view, for a canvas to show or a shot to draw. */ export function handleOf(owner: View): bindings.Handle { return view(owner).handle(); diff --git a/npm/test/consumer/main.ts b/npm/test/consumer/main.ts index 7f89e581..d43b544c 100644 --- a/npm/test/consumer/main.ts +++ b/npm/test/consumer/main.ts @@ -1,4 +1,4 @@ -import { load, field, Data, Gpu, Patch, Shot, type Scene } from 'latkit'; +import { load, field, Data, Gpu, Patch, Shot, View, type Scene } from 'latkit'; import { quickstart } from './quickstart'; import wasmUrl from 'latkit/wasm?url'; import workerUrl from 'latkit/worker?url'; @@ -28,18 +28,17 @@ async function until(condition: () => boolean): Promise { } } +// The scene names its dataset by name, so each view draws the `grid` its sources give. const scene: Scene = { space: { plane: { grid: 'off' } }, theme: 'dark', ease: 0, marks: [ { - vertices: { - of: ['grid', 'Bus'], - position: ['$x', '$y'], - radius: field('radius'), - color: '#ff0000', - }, + vertices: ['grid', 'Bus'], + position: ['$x', '$y'], + radius: field('radius'), + color: '#ff0000', }, ], }; @@ -118,19 +117,17 @@ async function run(custom: boolean, compiled: boolean) { const secondGpu = await Gpu.create(); const deviceReady = performance.now(); const canvas = (id: string) => document.querySelector(`#${id}`)!; - const first = gpu.view(scene); - const second = gpu.view(scene); - const third = secondGpu.view(scene); - gpu.canvas(canvas('first'), { layout: [{ view: first }] }); - gpu.canvas(canvas('second'), { layout: [{ view: second }] }); - secondGpu.canvas(canvas('third'), { layout: [{ view: third }] }); - // A view that no canvas shows still draws in shots. - const headless = gpu.view(scene); + const sources = [decoded]; + const first = await View.create(canvas('first'), scene, { gpu, sources }); + const second = await View.create(canvas('second'), scene, { gpu, sources }); + const third = await View.create(canvas('third'), scene, { gpu: secondGpu, sources: [kept] }); + // A view on no canvas still draws in shots. + const headless = await View.create(null, scene, { gpu, sources }); + check(first.gpu === gpu && third.gpu === secondGpu, 'views draw with the GPU they are given'); for (const view of [first, second, third, headless]) { view.on('error', (error) => { throw error; }); - view.sources = view === third ? [kept] : [decoded]; } decoded.destroy(); // the views keep their own reference to the dataset kept.destroy(); @@ -138,8 +135,14 @@ async function run(custom: boolean, compiled: boolean) { [first, second, third].every((view) => view.stats.frames > 0 && view.stats.drawn === 1), ); const firstFrame = performance.now(); - const picture = await new Shot([64, 64], { layout: [{ view: headless }] }).image(); + const picture = await new Shot([64, 64], { views: [headless] }).image(); check(picture.width === 64 && picture.height === 64, 'a view without a canvas draws in a shot'); + const shown = Shot.of(first, { scale: 2 }); + const css = canvas('first').getBoundingClientRect(); + check( + shown.size.join() === [css.width * 2, css.height * 2].map(Math.round).join(), + "a view's shot is its canvas's size", + ); const png = await picture.encode('png'); const jpeg = await picture.encode('jpeg', { quality: 80 }); const webp = await picture.encode('webp'); @@ -169,7 +172,7 @@ async function run(custom: boolean, compiled: boolean) { chunks.push(bytes); }, }); - const video = new Shot([64, 64], { layout: [{ view: headless }] }).video({ fps: 30, output }); + const video = new Shot([64, 64], { views: [headless] }).video({ fps: 30, output }); await video.record((time) => time < 0.1); check(video.frames === 3 && !output.locked, 'video retains counters and releases its output'); check( @@ -177,7 +180,7 @@ async function run(custom: boolean, compiled: boolean) { 'video writes an MP4 header', ); const cancelledOutput = new WritableStream(); - const cancelled = new Shot([64, 64], { layout: [{ view: headless }] }).video({ + const cancelled = new Shot([64, 64], { views: [headless] }).video({ fps: 30, output: cancelledOutput, }); @@ -187,21 +190,10 @@ async function run(custom: boolean, compiled: boolean) { await cancelled.abort('consumer cancellation'); check((await settled)[0].status === 'rejected', 'cancellation interrupts the WASM frame'); check(!cancelledOutput.locked, 'cancelled video releases the writer'); - const restored = await new Shot([64, 64], { layout: [{ view: headless }] }).image(); + const restored = await new Shot([64, 64], { views: [headless] }).image(); restored.destroy(); } headless.destroy(); - check( - (() => { - try { - gpu.canvas(document.createElement('canvas'), { layout: [{ view: second }] }); - return false; - } catch { - return true; - } - })(), - 'a view shows in one canvas at a time', - ); const hits = await first.pick([128, 128], { radius: 8 }); check( hits.some((hit) => hit.row.table === 'Bus'), @@ -224,7 +216,7 @@ async function run(custom: boolean, compiled: boolean) { stop(); stop(); destroyed(() => second.stats); - destroyed(() => gpu.view(scene)); + destroyed(() => gpu.budget); for (const answer of await pending) { check(answer.status === 'rejected', 'destroying the view must cancel pending queries'); const error: unknown = answer.reason; @@ -263,7 +255,11 @@ async function run(custom: boolean, compiled: boolean) { document.body.append(exampleCanvas); const example = await quickstart(exampleCanvas); await until(() => example.view.stats.drawn === 2); - example.gpu.destroy(); + check( + example.view.gpu !== gpu, + 'a view given no GPU draws with a new one once the first is gone', + ); + example.view.gpu.destroy(); example.grid.destroy(); exampleCanvas.remove(); const transient = await Gpu.create(); diff --git a/npm/test/consumer/quickstart.ts b/npm/test/consumer/quickstart.ts index b353ed25..2408c244 100644 --- a/npm/test/consumer/quickstart.ts +++ b/npm/test/consumer/quickstart.ts @@ -1,26 +1,19 @@ -import { Data, Gpu, Patch, field, type Scene } from 'latkit'; +import { Data, View, field, load } from 'latkit'; /** Draws two buses on `canvas`, colored by their voltage. */ export async function quickstart(canvas: HTMLCanvasElement) { - const gpu = await Gpu.create(); - const grid = new Data('grid').apply( - new Patch().replace('Bus', [101, 102], { x: [0, 30], y: [0, 20], kv: [230, 115] }), - ); - const scene: Scene = { + await load(); + const grid = new Data('grid').set('Bus', [101, 102], { x: [0, 30], y: [0, 20], kv: [230, 115] }); + const view = await View.create(canvas, { space: 'plane', marks: [ { - vertices: { - of: ['grid', 'Bus'], - position: ['$x', '$y'], - radius: 6, - color: field('kv').colormap('viridis'), - }, + vertices: grid.table('Bus'), + position: ['$x', '$y'], + radius: 6, + color: field('kv').colormap('viridis'), }, ], - }; - const view = gpu.view(scene, { sources: [grid] }); - gpu.canvas(canvas, { layout: [{ view }] }); - view.fit(); - return { gpu, view, grid }; + }); + return { view, grid }; } diff --git a/npm/test/data.test.mjs b/npm/test/data.test.mjs index cfedf0d5..93b77eae 100644 --- a/npm/test/data.test.mjs +++ b/npm/test/data.test.mjs @@ -158,6 +158,27 @@ test('changes apply in place, a snapshot keeps a version, and a patch names its before.destroy(); }); +test('each change applies on its own, and tables write as their JSON form', () => { + const data = new Data('grid') + .axis('time', 's') + .replace('Bus', [1, 2, 3], { x: [1, 2, 3] }) + .set('Bus', [4], { x: [4] }) + .remove('Bus', [1]) + .append('Bus', [0, 1], { v: Float32Array.of(1, 2, 3, 4, 5, 6) }) + .forget(1); + assert.deepEqual(ids(data, 'Bus'), [2, 3, 4]); + assert.deepEqual(data.describe().span, [1, 1]); + assert.throws(() => data.set('Bus', [1], { x: ['a'] }), { code: 'invalid' }); + assert.equal(data.get('Bus', 2, 'x'), 2, 'a failed change changes nothing'); + const bus = data.table('Bus'); + assert.equal(bus.source, data); + assert.equal( + JSON.stringify({ vertices: bus, color: field('kv').from(data.table('Station')) }), + '{"vertices":["grid","Bus"],"color":{"field":"kv","from":["grid","Station"]}}', + ); + data.destroy(); +}); + test('consecutive appends write one change and copy each frame when called', () => { const frame = new Float32Array(2); const patch = new Patch().replace('Bus', [1, 2], { x: [1, 2] }); diff --git a/npm/test/scenes.js b/npm/test/scenes.js index ee1e6ee4..27e028a6 100644 --- a/npm/test/scenes.js +++ b/npm/test/scenes.js @@ -15,10 +15,10 @@ export default [ { "space": { "earth": { "projection": "globe", "daylight": 1782043200, "atmosphere": true } }, "marks": [ - { "areas": { "of": ["base", "water"], "color": "#1d3557" } }, - { "areas": { "of": ["city", "polygons"], "geometry": "geometry", "color": { "colormap": "tab10", "value": "$use" }, "label": "$name" } }, - { "lines": { "of": ["city", "polygons"], "width": 1, "color": "rgba(255, 255, 255, 0.25)" } }, - { "lines": { "of": ["base", "transportation"], "width": { "div": ["$lanes", 2] } } } + { "areas": ["base", "water"], "color": "#1d3557" }, + { "areas": ["city", "polygons"], "geometry": "geometry", "color": { "colormap": "tab10", "value": "$use" }, "label": "$name" }, + { "lines": ["city", "polygons"], "width": 1, "color": "rgba(255, 255, 255, 0.25)" }, + { "lines": ["base", "transportation"], "width": { "div": ["$lanes", 2] } } ], "theme": { "area": { "color": "#223344" }, "earth": { "atmosphere": "#5aa9ff88", "night": 0.35 } } }, @@ -27,39 +27,33 @@ export default [ "space": { "earth": { "projection": "globe" } }, "marks": [ { - "edges": { - "of": ["grid", "Branch"], - "ends": ["from", "to"], - "width": { "units": 2, "atLeast": 1 }, - "dash": { "gt": ["$loading", 0.9] }, - "flow": { "div": ["$p", 20] }, - "priority": { "abs": ["$p"] }, - "color": { "colormap": "amber", "value": "$loading", "domain": [0, 1] }, - "name": "branches" - } + "edges": ["grid", "Branch"], + "ends": ["from", "to"], + "width": { "units": 2, "atLeast": 1 }, + "dash": { "gt": ["$loading", 0.9] }, + "flow": { "div": ["$p", 20] }, + "priority": { "abs": ["$p"] }, + "color": { "colormap": "amber", "value": "$loading", "domain": [0, 1] }, + "name": "branches" }, { - "vertices": { - "of": ["grid", "Bus"], - "position": ["$lon", "$lat"], - "elevation": 0, - "radius": { "add": [{ "div": ["$kv", 100] }, 1] }, - "marker": "diamond", - "color": { - "when": [[{ "state": "selected" }, "#ffb000"]], - "otherwise": { "colormap": "balance", "value": { "field": "vm", "step": true }, "domain": [0.95, 1.05] } - }, - "visible": { "and": [{ "ge": ["$kv", 115] }, { "contains": ["$name", "north"] }] } - } + "vertices": ["grid", "Bus"], + "position": ["$lon", "$lat"], + "elevation": 0, + "radius": { "add": [{ "div": ["$kv", 100] }, 1] }, + "marker": "diamond", + "color": { + "when": [[{ "state": "selected" }, "#ffb000"]], + "otherwise": { "colormap": "balance", "value": { "field": "vm", "step": true }, "domain": [0.95, 1.05] } + }, + "visible": { "and": [{ "ge": ["$kv", 115] }, { "contains": ["$name", "north"] }] } }, { - "vertices": { - "of": ["grid", "Bus"], - "name": "load", - "size": [12, { "units": 4 }], - "marker": { "block": { "corner": 2 } }, - "color": { "colormap": "viridis", "value": { "field": "load", "from": ["weather", "Station"] } } - } + "vertices": ["grid", "Bus"], + "name": "load", + "size": [12, { "units": 4 }], + "marker": { "block": { "corner": 2 } }, + "color": { "colormap": "viridis", "value": { "field": "load", "from": ["weather", "Station"] } } } ], "theme": { @@ -74,46 +68,42 @@ export default [ "space": { "plane": { "grid": { "lines": { "pitch": 1 } } } }, "controls": { "turn": false, "pickRadius": 14 }, "marks": [ - { "faces": { "of": ["mesh", "Face"], "corners": ["a", "b", "c"], "pick": false } }, - { "edges": { "of": ["mesh", "Spring"], "ends": ["a", "b"], "color": "#ffffff14" } }, - { "vertices": { "of": ["mesh", "Vertex"], "position": ["$x", "$y"], "elevation": "$z", "select": false, "drag": true } } + { "faces": ["mesh", "Face"], "corners": ["a", "b", "c"], "pick": false }, + { "edges": ["mesh", "Spring"], "ends": ["a", "b"], "color": "#ffffff14" }, + { "vertices": ["mesh", "Vertex"], "position": ["$x", "$y"], "elevation": "$z", "select": false, "drag": true } ], "theme": { "shading": 0.7, "text": { "font": "monospace", "size": 12 }, "plot": { "playhead": "#ffe5d4" } } }, { "space": { "plot": { "x": { "label": "Time (s)" } } }, - "marks": [{ "lines": { "of": ["mesh", "Body"], "trace": "height", "color": "#66c7ff", "width": 2, "flow": 12 } }] + "marks": [{ "lines": ["mesh", "Body"], "trace": "height", "color": "#66c7ff", "width": 2, "flow": 12 }] }, { "space": { "plot": { "y": { "format": "engineering", "precision": 1, "ticks": [0, [0.5, "half"], 1] } } }, "controls": { "selectBox": true }, "marks": [ { - "lines": { - "of": ["mesh", "Body"], - "trace": "speed", - "interpolation": "stepAfter", - "label": { - "text": { "when": [[{ "ge": ["$rank", 2] }, "$name"]], "otherwise": null }, - "size": 11, - "maxWidth": 120, - "color": "white" - } + "lines": ["mesh", "Body"], + "trace": "speed", + "interpolation": "stepAfter", + "label": { + "text": { "when": [[{ "ge": ["$rank", 2] }, "$name"]], "otherwise": null }, + "size": 11, + "maxWidth": 120, + "color": "white" } }, { - "vertices": { - "of": ["grid", "Bus"], - "marker": { "gauge": { "fill": "$load", "shape": "rounded" } }, - "ring": "red", - "label": "$name", - "radius": { "mul": [{ "state": "zoom" }, 2] } - } + "vertices": ["grid", "Bus"], + "marker": { "gauge": { "fill": "$load", "shape": "rounded" } }, + "ring": "red", + "label": "$name", + "radius": { "mul": [{ "state": "zoom" }, 2] } }, - { "vertices": { "of": ["grid", "Plant"], "marker": { "pie": { "slices": ["$coal", "$gas"], "colors": ["#333333", "#88aaff"], "hole": 0.4 } } } }, - { "vertices": { "of": ["grid", "Icon"], "marker": { "icon": { "images": [{ "size": [1, 1], "rgba": [255, 0, 0, 255] }], "index": 0 } } } }, - { "vertices": { "of": ["grid", "Custom"], "marker": { "wgsl": { "source": "fn marker(f: MarkerFragment) -> MarkerColor { return MarkerColor(f.color, 0.0); }", "inputs": ["$load"] } } } }, - { "edges": { "of": ["grid", "Branch"], "ends": ["from", "to"] } } + { "vertices": ["grid", "Plant"], "marker": { "pie": { "slices": ["$coal", "$gas"], "colors": ["#333333", "#88aaff"], "hole": 0.4 } } }, + { "vertices": ["grid", "Icon"], "marker": { "icon": { "images": [{ "size": [1, 1], "rgba": [255, 0, 0, 255] }], "index": 0 } } }, + { "vertices": ["grid", "Custom"], "marker": { "wgsl": { "source": "fn marker(f: MarkerFragment) -> MarkerColor { return MarkerColor(f.color, 0.0); }", "inputs": ["$load"] } } }, + { "edges": ["grid", "Branch"], "ends": ["from", "to"] } ], "theme": { "edge": { "spacing": 5, "arrow": 8 }, "text": { "halo": 2 }, "flow": { "spacing": { "units": 40 }, "length": { "units": 12 } } } }, @@ -122,19 +112,17 @@ export default [ "layout": { "stress": { "gap": 50 } }, "controls": { "connect": true, "delete": true }, "marks": [ - { "vertices": { "of": ["plant", "Area"], "contains": ["Device", "area"], "label": "$name", "shadow": true } }, - { "edges": { "of": ["plant", "Wire"], "ends": ["from", "to"], "bends": "path", "arrows": true, "width": { "px": 1.5 } } }, - { "edges": { "of": ["plant", "Net"], "route": "orthogonal", "arrows": true } }, + { "vertices": ["plant", "Area"], "contains": ["Device", "area"], "label": "$name", "shadow": true }, + { "edges": ["plant", "Wire"], "ends": ["from", "to"], "bends": "path", "arrows": true, "width": { "px": 1.5 } }, + { "edges": ["plant", "Net"], "route": "orthogonal", "arrows": true }, { - "vertices": { - "of": ["plant", "Device"], - "size": [40, 24], - "marker": "block", - "ports": [ - ["input", "left"], - ["output", { "side": "right", "order": 0, "label": "out", "shape": "diamond", "output": true }] - ] - } + "vertices": ["plant", "Device"], + "size": [40, 24], + "marker": "block", + "ports": [ + ["input", "left"], + ["output", { "side": "right", "order": 0, "label": "out", "shape": "diamond", "output": true }] + ] } ], "theme": { "diagram": { "groupPadding": 16, "portSize": 9, "group": "#1b2530", "shadow": { "color": "#00000040", "lift": 4 } } } From 0e5d053d417c6e865b3281fb912aa0ff2e910ece Mon Sep 17 00:00:00 2001 From: Luke Lowery Date: Tue, 27 Oct 2026 06:40:56 -0500 Subject: [PATCH 12/14] major documetnation cleanup --- .changeset/api.md | 18 +- .changeset/camera.md | 13 +- .changeset/labels.md | 15 +- .changeset/sources.md | 94 ++-- .changeset/tiles.md | 13 +- .changeset/values.md | 18 +- CONTRIBUTING.md | 8 +- README.md | 16 +- RELEASING.md | 14 +- benchmark/README.md | 38 +- benchmark/run.ts | 4 +- crates/latkit-cli/README.md | 11 +- .../examples/branch-opening-orbit/README.md | 37 +- .../examples/branch-opening/README.md | 89 ++- .../latkit-cli/examples/bus-fault/README.md | 23 +- .../examples/wecc240-fault/README.md | 77 ++- crates/latkit-cli/src/args.rs | 6 +- crates/latkit-cli/src/main.rs | 4 +- crates/latkit-cli/src/render.rs | 2 +- crates/latkit-cli/src/sinks.rs | 10 +- crates/latkit-js/src/canvas.rs | 2 +- crates/latkit-js/src/gpu.rs | 2 +- crates/latkit-js/src/shot.rs | 28 +- crates/latkit-js/src/view.rs | 4 +- crates/latkit/README.md | 7 +- crates/latkit/shaders/compute/evaluate.wesl | 8 +- crates/latkit/shaders/compute/fields.wesl | 18 +- crates/latkit/shaders/compute/probe.wesl | 16 +- crates/latkit/shaders/compute/texts.wesl | 17 +- crates/latkit/shaders/draw/glyphs.wesl | 21 +- crates/latkit/shaders/draw/points.wesl | 6 +- crates/latkit/shaders/draw/strokes.wesl | 8 +- crates/latkit/shaders/lib/center.wesl | 6 +- crates/latkit/shaders/lib/color.wesl | 2 +- crates/latkit/shaders/lib/drawn.wesl | 2 +- crates/latkit/shaders/lib/ends.wesl | 4 +- crates/latkit/shaders/lib/focus.wesl | 2 +- crates/latkit/shaders/lib/label.wesl | 27 +- crates/latkit/shaders/lib/marker.wesl | 2 +- crates/latkit/shaders/lib/path.wesl | 17 +- crates/latkit/shaders/lib/read.wesl | 6 +- crates/latkit/shaders/lib/state.wesl | 2 +- crates/latkit/shaders/lib/triangles.wesl | 4 +- crates/latkit/shaders/lib/types.wesl | 27 +- crates/latkit/src/data/bytes.rs | 15 +- crates/latkit/src/data/column.rs | 55 +- crates/latkit/src/data/describe.rs | 37 +- crates/latkit/src/data/forms.rs | 36 +- crates/latkit/src/data/frames.rs | 32 +- crates/latkit/src/data/geometry.rs | 6 +- crates/latkit/src/data/ids.rs | 14 +- crates/latkit/src/data/json.rs | 71 +-- crates/latkit/src/data/name.rs | 15 +- crates/latkit/src/data/ordered.rs | 6 +- crates/latkit/src/data/patch.rs | 69 +-- crates/latkit/src/data/query.rs | 44 +- crates/latkit/src/data/samples.rs | 96 ++-- crates/latkit/src/data/snapshot.rs | 28 +- crates/latkit/src/data/source.rs | 18 +- crates/latkit/src/data/table.rs | 34 +- crates/latkit/src/data/values.rs | 62 ++- crates/latkit/src/derive/graph.rs | 9 +- crates/latkit/src/derive/lanes.rs | 4 +- crates/latkit/src/derive/matches.rs | 10 +- crates/latkit/src/derive/mod.rs | 50 +- crates/latkit/src/derive/order.rs | 35 +- crates/latkit/src/derive/places.rs | 6 +- crates/latkit/src/derive/pyramid.rs | 53 +- crates/latkit/src/derive/shapes.rs | 63 +-- crates/latkit/src/derive/sight.rs | 48 +- crates/latkit/src/derive/stars.rs | 13 +- crates/latkit/src/derive/tiled.rs | 69 +-- crates/latkit/src/error.rs | 12 +- crates/latkit/src/expr/color.rs | 4 +- crates/latkit/src/expr/colormap.rs | 12 +- crates/latkit/src/expr/compile.rs | 30 +- crates/latkit/src/expr/cpu.rs | 18 +- crates/latkit/src/expr/form.rs | 61 +- crates/latkit/src/expr/mod.rs | 44 +- crates/latkit/src/expr/ramp.rs | 6 +- crates/latkit/src/geo/earcut.rs | 10 +- crates/latkit/src/geo/mercator.rs | 4 +- crates/latkit/src/geo/mvt.rs | 4 +- crates/latkit/src/geo/pmtiles.rs | 4 +- crates/latkit/src/geo/tiles.rs | 4 +- crates/latkit/src/gpu/arena.rs | 55 +- crates/latkit/src/gpu/atlas.rs | 38 +- crates/latkit/src/gpu/dictionary.rs | 17 +- crates/latkit/src/gpu/grown.rs | 12 +- crates/latkit/src/gpu/icons.rs | 9 +- crates/latkit/src/gpu/mod.rs | 42 +- crates/latkit/src/gpu/packed.rs | 30 +- crates/latkit/src/gpu/pipelines.rs | 71 +-- crates/latkit/src/gpu/readback.rs | 7 +- crates/latkit/src/gpu/shaders.rs | 10 +- crates/latkit/src/gpu/work.rs | 22 +- crates/latkit/src/host/browser.rs | 7 +- crates/latkit/src/host/job.rs | 8 +- crates/latkit/src/host/mod.rs | 20 +- crates/latkit/src/host/native.rs | 6 +- .../src/host/native/media_foundation.rs | 4 +- crates/latkit/src/host/queue.rs | 2 +- crates/latkit/src/json.rs | 9 +- crates/latkit/src/layout/boxes.rs | 14 +- crates/latkit/src/layout/layered.rs | 16 +- crates/latkit/src/layout/levels.rs | 14 +- crates/latkit/src/layout/overlap.rs | 6 +- crates/latkit/src/layout/pack.rs | 2 +- crates/latkit/src/layout/route.rs | 28 +- crates/latkit/src/layout/stress.rs | 10 +- crates/latkit/src/order.rs | 2 +- crates/latkit/src/pending.rs | 4 +- crates/latkit/src/scene/areas.rs | 16 +- crates/latkit/src/scene/compile.rs | 5 +- crates/latkit/src/scene/controls.rs | 57 +- crates/latkit/src/scene/edges.rs | 36 +- crates/latkit/src/scene/faces.rs | 8 +- crates/latkit/src/scene/form.rs | 12 +- crates/latkit/src/scene/label.rs | 49 +- crates/latkit/src/scene/layout.rs | 16 +- crates/latkit/src/scene/lines.rs | 21 +- crates/latkit/src/scene/mark.rs | 161 +++--- crates/latkit/src/scene/marker.rs | 17 +- crates/latkit/src/scene/mod.rs | 38 +- crates/latkit/src/scene/port.rs | 4 +- crates/latkit/src/scene/space.rs | 60 +- crates/latkit/src/scene/theme.rs | 72 +-- crates/latkit/src/scene/vertices.rs | 68 +-- crates/latkit/src/sheet/groups.rs | 8 +- crates/latkit/src/sheet/mod.rs | 35 +- crates/latkit/src/sheet/wires.rs | 58 +- crates/latkit/src/shot.rs | 13 +- crates/latkit/src/tiles/cache.rs | 24 +- crates/latkit/src/tiles/frames.rs | 26 +- crates/latkit/src/tiles/mod.rs | 45 +- crates/latkit/src/tiles/sets.rs | 14 +- crates/latkit/src/tiles/tiling.rs | 31 +- crates/latkit/src/tiles/vector.rs | 82 +-- crates/latkit/src/video/mod.rs | 14 +- crates/latkit/src/video/mp4.rs | 4 +- crates/latkit/src/view/axes.rs | 26 +- crates/latkit/src/view/camera.rs | 93 ++-- crates/latkit/src/view/drawing.rs | 76 +-- crates/latkit/src/view/ends.rs | 12 +- crates/latkit/src/view/event.rs | 66 +-- crates/latkit/src/view/fit.rs | 27 +- crates/latkit/src/view/focus.rs | 10 +- crates/latkit/src/view/frame.rs | 107 ++-- crates/latkit/src/view/gestures.rs | 83 +-- crates/latkit/src/view/graph.rs | 2 +- crates/latkit/src/view/input.rs | 6 +- crates/latkit/src/view/kept.rs | 4 +- crates/latkit/src/view/labels.rs | 66 +-- crates/latkit/src/view/legends.rs | 16 +- crates/latkit/src/view/lists.rs | 4 +- crates/latkit/src/view/marks.rs | 50 +- crates/latkit/src/view/mod.rs | 184 +++--- crates/latkit/src/view/passes.rs | 48 +- crates/latkit/src/view/plan.rs | 70 +-- crates/latkit/src/view/probe.rs | 21 +- crates/latkit/src/view/program.rs | 19 +- crates/latkit/src/view/query.rs | 27 +- crates/latkit/src/view/resources.rs | 5 +- crates/latkit/src/view/rig.rs | 41 +- crates/latkit/src/view/sync.rs | 75 +-- crates/latkit/src/view/transition.rs | 22 +- crates/latkit/src/view/uniforms.rs | 4 +- crates/latkit/src/view/variants.rs | 43 +- crates/latkit/src/view/words.rs | 18 +- crates/latkit/src/web.rs | 2 +- crates/latkit/src/wire.rs | 6 +- crates/latkit/tests/areas.rs | 2 +- crates/latkit/tests/gestures.rs | 8 +- crates/latkit/tests/labels.rs | 9 +- crates/latkit/tests/picks.rs | 8 +- crates/latkit/tests/scale.rs | 8 +- docs/cli.md | 35 +- docs/examples/bloom.md | 2 +- docs/examples/clifford.md | 4 +- docs/examples/currents.md | 2 +- docs/examples/diagram.md | 6 +- docs/examples/frequency.md | 4 +- docs/examples/globe.md | 6 +- docs/examples/grid.md | 4 +- docs/examples/helios.md | 2 +- docs/examples/index.md | 15 +- docs/examples/opening.md | 32 -- docs/examples/terrain.md | 2 +- docs/examples/texas.md | 2 +- docs/examples/tides.md | 4 +- docs/examples/torus.md | 2 +- docs/gallery/clifford.ts | 2 +- docs/gallery/render.ts | 9 +- docs/guide.md | 250 ++++----- docs/index.md | 26 +- docs/performance.md | 58 +- docs/reference/api.md | 526 +++++++++--------- docs/reference/cli.json | 6 +- docs/reference/cli.md | 8 +- docs/reference/types.md | 517 +++++++++-------- npm/README.md | 8 +- npm/build.mjs | 6 +- npm/docs.mjs | 185 +++--- npm/examples/gate/gate.ts | 6 +- npm/examples/src/pages/bunny/index.ts | 2 +- npm/examples/src/pages/diagram.ts | 8 +- npm/examples/src/pages/grids/maps.ts | 11 +- npm/examples/src/pages/telemetry.ts | 9 +- npm/examples/src/shell/dom.ts | 6 +- npm/examples/src/shell/page.ts | 24 +- npm/examples/src/shell/shell.ts | 20 +- npm/examples/src/shell/timeline.ts | 4 +- npm/schema.json | 326 +++++------ npm/schema.mjs | 38 +- npm/src/canvas.ts | 22 +- npm/src/colormap.ts | 6 +- npm/src/data.ts | 111 ++-- npm/src/encoder.ts | 8 +- npm/src/gpu.ts | 55 +- npm/src/index.ts | 7 +- npm/src/inputs.ts | 466 ++++++++-------- npm/src/internal.ts | 64 +-- npm/src/jobs.ts | 8 +- npm/src/load.ts | 24 +- npm/src/outputs.ts | 56 +- npm/src/patch.ts | 84 +-- npm/src/scene.ts | 60 +- npm/src/shot.ts | 40 +- npm/src/view.ts | 124 +++-- npm/test/consumer/main.ts | 2 +- xtask/src/main.rs | 8 +- 231 files changed, 4068 insertions(+), 3940 deletions(-) delete mode 100644 docs/examples/opening.md diff --git a/.changeset/api.md b/.changeset/api.md index a7f56c44..91a3f14d 100644 --- a/.changeset/api.md +++ b/.changeset/api.md @@ -2,12 +2,14 @@ 'latkit': minor --- -A view takes one call, a mark names its table with its kind, and a dataset writes itself. +`View.create` opens a view in one call, a mark's kind key holds its table, and `Data` applies changes directly. -- Added: `View.create(canvas, scene, { region, camera, sources, gpu, pixelRatio })` shows a view on a `` or `OffscreenCanvas`, over the views already there, or with a `null` canvas draws only into pictures. Views share the first `Gpu` created, which `View.create` creates when there is none; `view.region` moves a view and `view.gpu` is its GPU. -- Added: `data.table(name)` and `tiles.table(name)` give a `Table`, which a mark draws and `field(name).from(table)` reads. A view draws the datasets its scene's tables belong to, so `sources` lists only the datasets named by `[data, table]`. -- Added: every `Patch` change is also a `Data` method that applies at once: `axis`, `replace`, `set`, `remove`, `append`, `forget`, `frames`. -- Added: `Shot.of(view, options)` pictures what a view's canvas shows; `new Shot(size, { views })` draws views in their regions, or `{ view, region }` in another. -- Changed: a mark's kind key holds its table, beside its options: `{ vertices: grid.table('Bus'), radius: 4 }` (in JSON, `{ "vertices": ["grid", "Bus"], … }`) replaces `{ vertices: { of: ['grid', 'Bus'], radius: 4 } }`. Errors name the option's path, as `marks[1].radius`. -- Changed: `replace` and `set` given fields alone write rows `0` to `n - 1`. -- Removed: `gpu.view`, `gpu.canvas`, `Canvas`, `canvas.layout`, `canvas.shot()`, the `Panel` type, and the shot's `layout` option. +- Added: `View.create(canvas, scene, { region, camera, sources, gpu, pixelRatio })` draws a view on a `` or `OffscreenCanvas`, over any views already there. With a `null` canvas, the view draws only into pictures. +- Added: views share the first `Gpu` created, and `View.create` creates one if there is none. `view.region` moves a view, and `view.gpu` is its GPU. +- Added: `data.table(name)` and `tiles.table(name)` return a `Table` for a mark to draw or `field(name).from(table)` to read. A view loads every dataset its scene's tables belong to, so `sources` lists only datasets named as `[data, table]`. +- Added: a `Data` method for every `Patch` change, applied immediately: `axis`, `replace`, `set`, `remove`, `append`, `forget`, `frames`. +- Added: `Shot.of(view, options)` captures what a view's canvas shows. `new Shot(size, { views })` draws views in their own regions, or in another region given as `{ view, region }`. +- Changed: a mark's kind key holds its table, next to its options: `{ vertices: grid.table('Bus'), radius: 4 }`, or `{ "vertices": ["grid", "Bus"], … }` in JSON. This replaces `{ vertices: { of: ['grid', 'Bus'], radius: 4 } }`. +- Changed: errors name the option's path, such as `marks[1].radius`. +- Changed: `replace` and `set` given only fields write rows `0` to `n - 1`. +- Removed: `gpu.view`, `gpu.canvas`, `Canvas` (with `canvas.show(views)` and its `panels`), and the `Panel` type. diff --git a/.changeset/camera.md b/.changeset/camera.md index 5890574c..70787ba4 100644 --- a/.changeset/camera.md +++ b/.changeset/camera.md @@ -2,10 +2,13 @@ 'latkit': minor --- -A flat map is one world, and every camera move keeps within the camera's reach. +A flat map shows one world, and every camera move stays within the camera's zoom and pan limits. -- Changed: a flat map's ground ends at the antimeridian, as it ends at Web Mercator's latitude limit. Its camera zooms out only until the whole world fits inside the insets, and pans only until the world's edges meet the view's. A globe's camera zooms out until the globe spans a third of the view's shorter side. -- Changed: setting the camera, flights, fits, orbits, and every gesture keep the camera within reach, which fits narrow to what they framed, and its pitch within 75°. When a resize, new insets, or a new space moves the camera back within reach, a `camera` event reports it. -- Changed: a flight on a flat map eases across the world instead of the short way over the antimeridian; on a globe, longitude still turns the short way. +- Changed: a flat map's ground ends at the antimeridian, as it ends at Web Mercator's latitude limit. +- Changed: on a flat map, the camera zooms out only until the whole world fits inside the insets, and pans only until the world's edges meet the view's edges. +- Changed: on a globe, the camera zooms out until the globe spans a third of the view's shorter side. +- Changed: setting the camera, flights, fits, orbits, and gestures keep the camera within its zoom and pan limits, and its pitch within 75°. After a fit, the limits narrow to the area the fit framed. +- Changed: a `camera` event fires when a resize, new insets, or a new space moves the camera back within its limits. +- Changed: on a flat map, a flight eases across the world instead of the short way over the antimeridian. On a globe, longitude still turns the short way. - Fixed: panning a flat map no longer wraps its center across the antimeridian, which moved every row a world away. -- Fixed: a scene with another kind of space drops the old space's fits and the camera motion under way. +- Fixed: a scene with another kind of space drops the old space's fits and any camera motion in progress. diff --git a/.changeset/labels.md b/.changeset/labels.md index f6c2f018..3c6d7db4 100644 --- a/.changeset/labels.md +++ b/.changeset/labels.md @@ -2,11 +2,14 @@ 'latkit': minor --- -Labels are placed from this frame alone: no state carried between frames, and no fades. Where labels would overlap, the label of the row drawn on top shows. +Labels are placed from each frame alone, with no fades. Where labels would overlap, the row drawn on top keeps its label. -- Added: a label's `text` takes a `when` whose values are its text field or `null`. A row whose text is missing has no label, so `when(zoom().ge(6)).then(field('name')).otherwise(null)` labels rows from zoom 6 on. -- Changed: a row whose text is empty has no label, as one whose text is missing. -- Changed: a label shows wherever its row shows, its text is set, and it fits the room its row gives. Labels beside or along their rows never overlap: each claims the screen it covers, and the row drawn on top wins, so a mark's `priority` decides. Only labels that show need texts, so a view of any size settles with as many texts as fit on the screen. Labels move with the camera in the frame it moves, keep showing while their mark gains texts, and never inherit another row's state as tiles change. -- Changed: when the text atlas is full of texts in view, it takes no more until the view changes, instead of evicting texts it still shows. -- Changed: `Waiting`'s `labels` is label texts being read back or not yet set. +- Added: a label's `text` takes a `when` that picks the text field or `null`. A row whose text is missing has no label, so `when(zoom().ge(6)).then(field('name')).otherwise(null)` labels rows from zoom 6 on. +- Changed: a row whose text is empty has no label, like a row whose text is missing. +- Changed: a label shows when its row is visible, its text is set, and the text fits, for example inside a block. +- Changed: labels beside or along their rows never overlap. Where they would, the row drawn on top wins, so a mark's `priority` decides. +- Changed: only labels that show need their texts, so a view of any size settles with as many texts as fit on screen. +- Changed: labels move with the camera in the frame it moves, keep showing while their mark gains texts, and never take another row's state when tiles change. +- Changed: when the text atlas is full of texts in view, it accepts no more until the view changes, instead of evicting texts it still shows. +- Changed: `'labels'` in `Waiting` means label texts are being read back or are not yet set. - Removed: a label's `priority` and `max`, and label fades. A mark's `priority` orders its drawing, its thinning, and its labels. diff --git a/.changeset/sources.md b/.changeset/sources.md index a3077469..e32ea90f 100644 --- a/.changeset/sources.md +++ b/.changeset/sources.md @@ -4,44 +4,74 @@ Views draw from sources, every wait can be cancelled, colors and lengths are typed, and large tables draw in pages that stay on the GPU. -- Added: `view.sources` and `gpu.view(scene, { sources })`. A source is a `Data` (drawn at its latest version from the frame after each patch), a `Snapshot` (one fixed version: `data.snapshot()`, `data.restore(snapshot)`), or `Tiles(name, url)` (time tiles or vector tiles, fetched once per GPU). `view.snapshot(name)` returns what a view currently draws of a source. -- Added: `load({ wasm, worker })` loads the WebAssembly; using the library before it finishes throws `unloaded`. Every class and builder is exported from `latkit`. -- Added: every wait takes `{ signal }` and times out after 30 seconds by default: picks, `screenOf`, `arrange`, pictures, encodings, videos, `gpu.done()`, and `gpu.landed()`. A timeout rejects with `timeout`; an abort or a destroyed view rejects with `cancelled` and drops the work in progress. -- Added: `gpu.canvas(element, { layout: [{ view, at }] })` and `canvas.layout`; `canvas.shot()` captures exactly what a canvas shows; `new Shot(size, { scale, layout, transparent, capture, elapsed })`, where `capture: 'now'` takes views as they are; `shot.video({ fps, output, bitrate, signal })`. -- Added: `px(n)` and `units(n)` give a length's unit. Lengths default to CSS pixels on planes, the earth, and plots, and to position units in diagrams. A stroke in pixels keeps its width as a diagram zooms. -- Added: the theme in groups: `text`, `vertex`, `edge`, `line`, `area`, `focus`, `flow`, `diagram`, `earth`, `plot`. A comet's `flow.spacing` and `flow.length` take `{ px }` or `{ units }`. `diagram.outline`, `corner`, `portSize`, and `bend` style blocks, groups, ports, and wire bends; `shadow: true` on blocks and groups casts a soft shadow `diagram.shadow.lift` below them. -- Added: `Gpu.resident` and `Gpu.uploading`, the bytes on the GPU and waiting to upload across all its views; `Work.packed`, `Work.resolved`, `Work.cleared`, and `Work.uploads`. -- Added: `Handle`, the base of every object that holds memory (`Data`, `Snapshot`, `Gpu`, `View`, `Canvas`, `Image`): `destroy()` and `using`. `Dataset` holds the reads `Data` and `Snapshot` share. +- Added: `view.sources`. A source is a `Data`, drawn at its latest version from the frame after each patch. It can also be a `Snapshot`, one fixed version (`data.snapshot()`, `data.restore(snapshot)`), or `Tiles(name, url)`, time tiles or vector tiles fetched once per GPU. +- Added: `view.snapshot(name)` returns what a view currently draws of a source. +- Added: `load({ wasm, worker })` loads the WebAssembly. Using the library before it finishes throws `unloaded`. +- Added: every class and builder is exported from `latkit`. +- Added: every wait takes `{ signal }` and times out after 30 seconds by default: picks, `screenOf`, `arrange`, pictures, encodings, videos, `gpu.done()`, and `gpu.landed()`. A timeout rejects with `timeout`. An abort or a destroyed view rejects with `cancelled` and drops the work in progress. +- Added: `new Shot(size, { scale, transparent, capture, elapsed })`, where `capture: 'now'` takes views as they are, and `shot.video({ fps, output, bitrate, signal })`. +- Added: `px(n)` and `units(n)` set a length's unit. Lengths default to position units in diagrams and CSS pixels elsewhere. A stroke in pixels keeps its width as a diagram zooms. +- Added: theme groups `text`, `vertex`, `edge`, `line`, `area`, `focus`, `flow`, `diagram`, `earth`, and `plot`. +- Added: a comet's `flow.spacing` and `flow.length` take `{ px }` or `{ units }`. +- Added: `diagram.outline`, `corner`, `portSize`, and `bend` style blocks, groups, ports, and wire bends. +- Added: `shadow: true` on blocks and groups casts a soft shadow `diagram.shadow.lift` below them. +- Added: `Gpu.resident` and `Gpu.uploading`, the bytes on the GPU and the bytes waiting to upload, across all its views. +- Added: `Work.packed`, `Work.resolved`, `Work.cleared`, and `Work.uploads`. +- Added: `Handle`, the base class of every object that holds memory (`Data`, `Snapshot`, `Gpu`, `View`, `Image`), with `destroy()` and `using`. +- Added: `Dataset`, the reads that `Data` and `Snapshot` share. - Added: `colormaps()` lists the catalog's colormaps with their names, labels, and kinds. -- Added: the package's types document every member, every value of an enumeration, every form of a union, and every field of an options object. -- Added: the CLI's `--camera` takes a camera as JSON, as `latkit query` takes a query, or a file of it. +- Added: the package's types document every member, enumeration value, union form, and options field. +- Added: the CLI's `--camera` takes a camera as JSON or as a file, like `latkit query` takes a query. - Changed: `view.scene`, `view.selection`, `view.screen(position)`, `view.position(point)`, `view.screenOf(row)`, `view.waiting`, and the `hovered` and `selection` events replace the methods and events removed below. -- Changed: every error the package throws is a `LatkitError` with a `code`. A failed fetch or an error status is `io`; a call on a destroyed object has the new code `destroyed`; a bad `Patch.stream` limit and a `Video` drawn out of turn are `invalid`. Errors from the video encoder and worker jobs keep their codes, and an abort during a fetch rejects as `cancelled` or `timeout`. -- Changed: latkit's bytes carry no version or flags. A frame's header is the magic `LATK` and the frame's length, 12 bytes; files written by earlier versions no longer open. +- Changed: every error the package throws is a `LatkitError` with a `code`. +- Changed: a failed fetch or an error status is `io`. A call on a destroyed object has the new code `destroyed`. A bad `Patch.stream` limit and a `Video` drawn out of turn are `invalid`. +- Changed: errors from the video encoder and worker jobs keep their codes. An abort during a fetch rejects as `cancelled` or `timeout`. +- Changed: latkit's bytes carry no version or flags. A frame's 12-byte header is the magic `LATK` and the frame's length. Files written by earlier versions no longer open. - Changed: `theme.missing` is the color a colormap gives a missing value. An option given a missing value (`null`) still takes its theme value. -- Changed: `new Patch(dataset)` names its dataset; `Patch.read(source)` reads a patch's bytes or JSON, and `Data.read(source, { name, base })` a dataset's bytes, JSON, or GeoJSON, from a `ByteSource`: bytes, a `Blob`, a `Response`, a byte stream or async iterable, or a URL, read within the wait's `signal`; `patch.append(table, at, fields, { ids })`; `patch.frames(table, field, { level, first, at, values })`; `data.envelope({ table, field, range, spans, ids })`; `field(name).from(data, table)` reads another dataset. -- Changed: a video's size rounds down to whole 4×2 blocks, in the browser and the CLI alike; a smaller size is `invalid`. -- Changed: `view.screen` and `view.position` given the wrong number of coordinates throw `invalid` instead of answering `null`. -- Changed: the `'earth'` space, `{ earth: { projection: 'mercator' | 'globe', graticule, daylight, atmosphere } }`. The ground color is the theme's `earth.ground`. -- Changed: a table too large to draw whole draws in pages that keep their GPU slots as the camera moves. Pages no longer drawn stay on the GPU while the budget allows, so coming back is free, and a write rebuilds only the pages it touched. Appending rows never thins what draws. Lanes, net stars, joins, and fitted domains follow the data, not the camera. -- Changed: edges whose ends lie far apart draw within the budget: their endpoint rows load individually, and only edges whose endpoints moved look them up again. -- Changed: a hit describes the frame it came from. A GPU's views draw one frame together, and `Budget.uploadPerFrame` bounds what they upload in it. -- Changed: editing a diagram rederives only what the edit reaches: the moved blocks, the groups around them, and the wires near them. Map tiles draw in the frame they finish loading. -- Changed: a plot measures the room its axis labels and titles need. A GPU picks its antialiasing once. Zooming at a fit's limit keeps the center. -- Removed: `view.setData`, `view.setTiles`, `view.removeData`, and `view.data(name)` (use `view.sources`); `view.show(scene)`, `view.select(rows)`, `view.point`, `view.ground`, and `view.locate`; the `hover` and `select` events; `Stats.waiting`, `Stats.uploading`, `Stats.resident`, and `Stats.budget`. -- Removed: `loadLatkit()` and its result object; `latkit/core`; `createWorker` (use `load({ worker })`); `{ deadline }` options (use `{ signal }`); `canvas.show(views)` and the canvas's `panels`; `Shot`'s builder methods, `shot.image(views)`, and `shot.video(gpu, fps, output)`. -- Removed: `Patch.to`, `Patch.fromBytes`, `Data.load`, `Data.tables`, `data.ids()`, `data.fields()`, `data.span` (use `describe()`), the positional `envelope`, and `expr.of(data, table)` (use `from`). -- Removed: the `'mercator'` and `'globe'` spaces (use `earth`); flat theme keys such as `vertexRadius`, `fontSize`, and `edgeSpacing`; reading a string as a color only when it begins with `#`; `Flow.units`; shadows on edges, lines, areas, and faces, and the theme's `shadow.light` and `shadow.softness`. -- Fixed: a view whose next version does not fit releases what it draws so that version can load; when nothing fits, it reports a `limit` error and stops. -- Fixed: on the earth, sizes in `units(n)` are degrees, as comet lengths already were, not mercator radians. -- Fixed: zoomed in close on the globe, paged tables and vector tiles keep drawing their finer tiles; past about zoom 12 they fell back to the coarsest. -- Fixed: a text the host cannot rasterize reports one error, not one every frame it shows, and a text with nothing to draw is no error. +- Changed: `new Patch(dataset)` names its dataset. +- Changed: `Patch.read(source)` reads a patch's bytes or JSON, and `Data.read(source, { name, base })` reads a dataset's bytes, JSON, or GeoJSON. Both read from a `ByteSource` (bytes, a `Blob`, a `Response`, a byte stream or async iterable, or a URL) within the wait's `signal`. +- Changed: the signatures `patch.append(table, at, fields, { ids })`, `patch.frames(table, field, { level, first, at, values })`, and `data.envelope({ table, field, range, spans, ids })`. +- Changed: a video's size rounds down to whole 4×2 blocks, in the browser and the CLI. A smaller size is `invalid`. +- Changed: `view.screen` and `view.position` given the wrong number of coordinates throw `invalid` instead of returning `null`. +- Changed: the `'earth'` space takes `{ earth: { projection: 'mercator' | 'globe', graticule, daylight, atmosphere } }`. Its ground color is the theme's `earth.ground`. +- Changed: a table too large to draw at once draws in pages that keep their place on the GPU as the camera moves. +- Changed: pages no longer drawn stay on the GPU while the budget allows, so coming back to them is free. +- Changed: a write rebuilds only the pages it touched, and appending rows never thins what draws. +- Changed: lanes, net stars, joins, and fitted domains follow the data, not the camera. +- Changed: edges with far-apart ends draw within the budget. Their end rows load one by one, and only edges whose ends moved look them up again. +- Changed: a hit describes the frame it came from. +- Changed: all views of a GPU draw one frame together, and `Budget.uploadPerFrame` caps what they upload in it. +- Changed: editing a diagram recomputes only the moved blocks, the groups around them, and the wires near them. +- Changed: map tiles draw in the frame they finish loading. +- Changed: a plot measures the space its axis labels and titles need. +- Changed: a GPU picks its antialiasing once. +- Changed: zooming at a fit's limit keeps the center. +- Removed: `view.setData`, `view.setTiles`, `view.removeData`, and `view.data(name)`. Use `view.sources`. +- Removed: `view.show(scene)`, `view.select(rows)`, `view.point`, `view.ground`, `view.locate`, and the `hover` and `select` events. +- Removed: `Stats.waiting`, `Stats.uploading`, `Stats.resident`, and `Stats.budget`. +- Removed: `loadLatkit()` and its result object, and `latkit/core`. +- Removed: `createWorker`. Use `load({ worker })`. +- Removed: `{ deadline }` options. Use `{ signal }`. +- Removed: `Shot`'s builder methods, `shot.image(views)`, and `shot.video(gpu, fps, output)`. +- Removed: `Patch.to`, `Patch.fromBytes`, `Data.load`, and `Data.tables`. +- Removed: `data.ids()`, `data.fields()`, and `data.span`. Use `describe()`. +- Removed: the positional `envelope`. +- Removed: `expr.of(data, table)`. Use `from`. +- Removed: the `'mercator'` and `'globe'` spaces. Use `earth`. +- Removed: flat theme keys such as `vertexRadius`, `fontSize`, and `edgeSpacing`. +- Removed: reading a string as a color only when it begins with `#`. +- Removed: `Flow.units`. +- Removed: shadows on edges, lines, areas, and faces, and the theme's `shadow.light` and `shadow.softness`. +- Fixed: when a view's next version does not fit, the view releases what it draws so the new version can load. If nothing fits, it reports a `limit` error and stops. +- Fixed: on the earth, sizes in `units(n)` are degrees, like comet lengths, not Mercator radians. +- Fixed: zoomed in close on the globe, paged tables and vector tiles keep drawing their finer tiles. Past about zoom 12 they fell back to the coarsest. +- Fixed: a text the host cannot rasterize reports one error, not one every frame it shows. A text with nothing to draw is not an error. - Fixed: a layout or shaping job that fails runs once until its inputs change, not every frame. - Fixed: at the GPU's texture-layer limit, a new icon fails with a `limit` error instead of overwriting the last one. -- Fixed: `view.screenOf` answers `null` for a hidden stroke or face, and for a face beyond the globe's horizon. -- Fixed: edges and faces no longer compile a bounds pipeline they never use before their first frame. +- Fixed: `view.screenOf` returns `null` for a hidden stroke or face, and for a face beyond the globe's horizon. +- Fixed: edges and faces no longer compile an unused bounds pipeline before their first frame. - Fixed: a `priority` that tests a field only with `contains()` now reads that field when ranking a paged table's rows. - Fixed: a JSON patch's errors name the failing change the same way wherever they are found, such as `[0].append.fields.x`. - Fixed: a tile index that is neither TileJSON nor PMTiles fails as `invalid`, and tile errors keep their code. -- Fixed: the CLI names an output it cannot write in its error, rather than blaming the frames file. +- Fixed: when the CLI cannot write an output, its error names that output, not the frames file. - Fixed: a transition that starts in the frame a mark's tables grow no longer writes its rows over other data on the GPU, which could draw rows in the wrong colors or places. diff --git a/.changeset/tiles.md b/.changeset/tiles.md index 2ba5fc27..a519199e 100644 --- a/.changeset/tiles.md +++ b/.changeset/tiles.md @@ -2,10 +2,13 @@ 'latkit': minor --- -A vector tile costs the rows it brings, and tiles split where they draw largest. +A vector tile costs only the rows it brings, and the tiles drawn largest split first. -- Changed: a vector tile landing appends its rows, so rows already drawn neither upload nor evaluate again. A tile that stops drawing keeps its rows and draws them again if it returns; once rows no tile draws outnumber the drawn rows by a page, they are removed at once. Until then `view.snapshot(name)` of a vector source includes them. -- Changed: a tile measures the area it covers on screen, so tiles toward the globe's edge and a tilted map's horizon stay coarse. When the tile budget binds, the tiles drawn largest split first, for vector tiles and paged tables alike. At most 128 vector tiles draw at once. +- Changed: an arriving vector tile appends its rows, so rows already drawn do not upload or evaluate again. +- Changed: a tile that stops drawing keeps its rows and draws them again if it comes back. Once rows that no tile draws outnumber the drawn rows by a page, they are all removed. Until then, `view.snapshot(name)` of a vector source includes them. +- Changed: tiles split by the area they cover on screen, so tiles near the globe's edge and a tilted map's horizon stay coarse. +- Changed: when the tile budget runs out, the tiles drawn largest split first, for vector tiles and paged tables alike. +- Changed: at most 128 vector tiles draw at once. - Changed: a vector table draws each tile's rows in the order of the mark's `priority`, highest on top. -- Fixed: tiles past the antimeridian of a flat map are no longer fetched as if near. -- Fixed: two tiles landing together whose field reads as different kinds no longer fail every update. +- Fixed: a flat map no longer fetches tiles past the antimeridian as if they were near. +- Fixed: two tiles arriving together with different kinds for the same field no longer fail every update. diff --git a/.changeset/values.md b/.changeset/values.md index e7d45522..bb559d21 100644 --- a/.changeset/values.md +++ b/.changeset/values.md @@ -2,10 +2,16 @@ 'latkit': minor --- -One way to write a value: `'$name'` reads a field, any other string is a CSS color, and `null` is missing, in every option and query. +One way to write a value in every option and query: `'$name'` reads a field, any other string is a CSS color, and `null` is missing. -- Changed: in a scene or a query, a string `'$name'` reads field `name` and any other string is a CSS color, whatever the option; `field(name)` writes `'$name'`. A color option that computes a number or a boolean is an error that says how to map it to colors. -- Changed: an operation is an object with one key naming it: `{ add: [a, b] }`, `{ not: [a] }`, `{ is: ['$name', 'Austin'] }`. `null` is a missing value. One `when` chooses between values and colors alike. -- Changed: `.colormap()` returns `Mapped`, as `.scale()` does. -- Renamed: the layered layout's `flow` to `direction`, so `flow` means only comets. -- Removed: `{ op, args }`, `{ color }`, and `{ missing: true }`; `Paint`, `PaintInput`, `Colormapped`, `Missing`, `color()`, and `missing()`; bare strings as field names. In Rust, `Paint` and `Expr` from a string or a `Name`. +- Changed: in a scene or a query, a string `'$name'` reads field `name` and any other string is a CSS color, whatever the option. `field(name)` writes `'$name'`. +- Changed: a color option that computes a number or a boolean is an error, and the error says how to map it to colors. +- Changed: an operation is an object with one key, its name: `{ add: [a, b] }`, `{ not: [a] }`, `{ is: ['$name', 'Austin'] }`. +- Changed: `null` is a missing value. +- Changed: one `when` chooses between values and colors alike. +- Changed: `.colormap()` returns `Mapped`, like `.scale()`. +- Changed: the layered layout's `flow` is renamed `direction`, so `flow` means only comets. +- Removed: `{ op, args }`, `{ color }`, and `{ missing: true }`. +- Removed: `Paint`, `PaintInput`, `Colormapped`, `Missing`, `color()`, and `missing()`. +- Removed: bare strings as field names. +- Removed: in Rust, converting a string or a `Name` into `Paint` or `Expr`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 583a49c8..70a64bf4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ the [`npm/js`](https://github.com/lukelowry/latkit/tree/npm/js) branch. ## Develop -Requires Rust (pinned in `rust-toolchain.toml`), Node.js 24, pnpm 10.30, and uv for the docs. +You need Rust (pinned in `rust-toolchain.toml`), Node.js 24, pnpm 10.30, and uv for the docs. ```sh pnpm install @@ -30,8 +30,8 @@ cargo xtask bench --quick # every benchmark scenario once, at its smallest s ## Documentation -The site builds with Python alone from checked-in files. These commands rebuild the site and -regenerate its generated parts: +The site builds with Python alone, from checked-in files. To rebuild the site and its generated +parts: ```sh uv run --project docs --group docs sphinx-build -W -b html docs target/docs # the site @@ -43,6 +43,6 @@ node docs/gallery/render.ts [name...] # the pictures, drawn by th node benchmark/docs.ts # the performance table, from a baseline ``` -Drawing the pictures also needs the npm package built (`cargo xtask wasm`). +Drawing the pictures also needs the npm package, built with `cargo xtask wasm`. [Releasing](RELEASING.md) covers versions and publishing. diff --git a/README.md b/README.md index 9ab82099..b171f3ac 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

  • GyU*R^T)}_7uK;$9iRj!>WIe`u za1}>b*o?2@ydCkkZLi@IjjXQsNV`BDvL`tgt9d+63-J<21pPzogo^kl9`n|UN^o)P)I*B`Y{wVQ$-*p1# zJNZK`{@1c4{QpJ&pVm>Zgu69gnt`i<+b(f-ycvCW3k|3NeU#RmDFTdRm&iqM%e(4= zs?M)23jY}atIROggL}s&z&}{qg|Eb!9#<`d6SVgC+N452VqdQ za5nfHzR4ZaKcnxjb)Gpt`77SjymkKL-Q9 z$Kw<4Xh89CtwnVG!F=t!IV8B;psAnR=P8%;*-FBcUGzpgG^O8L>p6#ui7snkr9FIM zZPnmI7k%Leuj%rCsHHq(!jM16f3wv0m*vgNS?MnkysyuFV*Qm4$y;Bj|M>r1oFY;m zb?I_?5g*$J|JtR2Gxp*uT%h=WVl0`neoxHTKcTs>f9D0Xz|4Bo8|8#;;xMR(==sV; z4F+~)Q;*B9`%BXq%}zGvuaU*{uZ4NhBn22$J*B?{J$4Gtch2T=9@X~-4{*xTUzHg*(cs|8X+Hk z&F>n?b#+d`?ZtXy^ssR?@D_gl62RY~xIc`3d5oPux_ii)4*aAh z?bq%lX0sk8pPR+$Z!(Nqpv0{ik+##tnX* zL?5_-dl!bAou~g~POrA}o+L-LG2;8hVFmp)ENK7 zfA-m@mALB*xu!*&VBo_a_U;z{^d4KqUqx}c2v+?Y!JJNN@r5JjD8;Yk-a53VF6^4< zg}wWrXWI^+wpW+DE`k`FkO;SaOmC2rzJLi4=K0RwYVa~P99*go>=`4m``WYg%e^q? zcNlz-&2f)DY_>)2yJkPiM3YCqVS_L6$0fp5^){ZhpP#|z*5YT|cAT%c z{`OiN7>fw)o436Ksc%JBK(wbPLzJjcXiomtsdVbvto# z{_t-OK2u4~I_+*tw%%DS7&t4>ej~n~%dfo~s*#qMf553W*Edfs|6hF|w|Jkl_N(VA zn#dFQHv+e}&#n0PHt{`rIKP&2Qo;Jr{Tl`5nOW~*4Pv0#eZ9KD>dy6Ics|b24c{Kj z7xTk{U(}9#e+urhnigGpuDO`Tn%p>~T#!ruu3X|MrMqx-Xp^3lzb?sH}T&YrmzKg#F7I&fh1LU{R5FK3+GT}1rva?Zs1 zL-#}PKVjc1I$I0twqC*+i_>-Gh|aXL8hCN7eeCOVURKsC&7-D~b9c)V4c_?k2)&3u zJ(N#x_A~pP$MZzO^{sz3{o;(-!5|V&~m=WL-buzuV@+VH#_Z7IdVG zYKlH;P(C(PE_+Us+|G_UBI!f$<9wZPeRc4|$>Q-7T@7~%-D~)$4Ez%(YvrFs^4gt~ zdNiZ#6W;$Wi@i8)4hS!6-PDXLj#m&Dzfc|?^5>FK#LchPuQyU3zDL-gww4P{<#DF3 zi&-+UKKYUN!<|N(f5~ADNpZobuW%0bk7=Iopqi#)xIXS3HMw{m`2%e@yPktsYN3Xk zmXPflnF-+s8Pu1fzI2c2csMgA82G$VSfJ4ia|B!Q$lL1hl5&h^+Ek`rAe?sEZ83g! z6-N{km%qgScZj7!{@p&WrT-{nPqP0$vGsjzb1D3A zA77}S4WEgxo`i#0alz2N6T9}9zwit#Vca_TzdZXtP{XXMA%D{U<8-C*(n_-D&`Vk9 zXJX=C&v0M~UcOV@`OM-lr~rG$$wb>N1ZS432@BFGGSYN!vcEk|YyGK5`)(Tid@eio z5$8Fl%3HhH7@xccM;q9K6CKs{NpT~7kX!8Li9-XwOveib1GSo&-GrCK(X9x)pp`^h1Dp@$RST?z6-iInWr z3ZCN^C!f(g;nELjY1p3cN9SL@VDm70Q}Zl+nY}-g{}z1ZtV3t(AEU12x9P00cud^w zM|GDvvGFdwxi8p{?;OSTlD5YK{oc;+)tO89Lvws{CVl25KR#>*Wvcl~rLm%wkkLZE9=H+3@)@w2rTQ?rYrnvhRAc#=d;3 zOJ8?wWH;00Is0_-9x$ZaL)z;HdY)S0Oql%lb$B581v-P+=pg6Jl;hG|*TdxtTlBwA z@`qN5abrAu^&fcCK6{eOolrZ~B-U_bIUK$W|LK-B)8eWd`vxBPdCK9wH*Xxj`^kI~ zzE#5W% zNcWkk$No8Mevhxd`-#`sWBb(pG%oA^iK1&TyNC3wJ}`$2Xy`T;2wz zHKir2mY>&)b-C{FUHQRY@4w~qHnQOjJQ2q|Ay-zwzhhm(Cw%VHoa*4na$0~^T#L>| zvzX&~a}{>q=u+nr?l>Fx8(z}ITtGy8ZWDbgqM9;5BTJ|cEuT-`FCTvkr(ydv`TvXM z&L$sZ_rWmq8(L!vXNBvU(WuNGa>$4WeAH*w;LpFG(*O4ir^Wh>uC%u7bmMAX-v!>i zHYYYnUoksdSCLn(f2QZl83Df)6XWP#*;m7THOl>5aEiZ0o}kxZ{X4U|`xMrVOX5tn zoV8{;A3h+Leu?YSd3>f)grW;%sAZCb!LPH;C6U&>%Y_mS^vkCUO(s; zNba*U*vl)wng6P4Tx@fo4-9;#Fi zj=))Y zv&6~-`yBk<%pEOzYz^Oe5djX!{|maPJFoDyY;^O(FtrlhSu8fN-<1aX?l~N8dM$hx z&V;jP=x*CA;qF6$N8|YTp6KRTi_#-L!Wi*XiQnEkfe%Ls4|GvqZT>)WV5|9caVfF6 z8J>iv5EEYau7KLM4Q*2V|GB}839OGT$L0|OXT9gm4B}heee{z)L_>XDnBJv2Y%D3p z#_MVP=XtA}ljQ%6;^CVxYrZvKPeC7wXx3Z&?G+E%zvIha!=XiL{UXkCuzhuFyAFeX zuIYIX;dDjijwAf1pji`m9tp06^vJmloYza?-msl)Ah#BJ#viS}tMy0wPTcdci~Hy$ z7vN4Z-=|Q8ZnIndl?QUews^Jqe+T)#wwWUwB-mEX(V6Ya(^Z1%i*EAx9_w!>2Yse@ z;(bTmp;cF-p?c=t)6D-JF+1H@UMS!mj{)kfZt%sP?q6&c3@(+&M=#{&2QAcEZ)sRK z^X2Js9UqH*)%z94f#F|h{`2Z=dJ%5&ik)Z3{}*GZG4If-m?uJe#py_)rXl|6}LD@SA3@?PL1Qo@J#kQ#ouxV+g8Tu z+IQvi@FnXkdi4`{9EYF4->ZCX!e+ko!k&oH3Sx8}pG+w)h0Yru3xg>UVV2z84gOz| z+fKHzKa1F&z0=sEjjeIj)_NoI=@YLDq*h1e4hC+Hb(S)U-#aJv~X7Ad@r5eNAX}_#3=f>J&JhMIWv3z;-UF}_K#f0oT5Bfc>>LGnmQd{ivF`%`Tl(N zE&Iz~m0CJmy+W++$J4|^c{ovMKaA0bY15J4w+s&)wf=+l_Y|zoycBo%7iY#tKO3hH z%GaX2vEN=>a3((SiO+lgjiv2(LOHjfoE6y~dX|$gZt<6R$0NC}id=n~HauRQN+~D# z*-!AfOmX^8RcCtEsdu`&7w`s|{{K?B-@xQG`uO@ZINTfu_Tr(rP3*v&0y(XJgBJ*$kKZkq0)x>hb zV*Gc3^R=18NAs8b(3-NwbjQ{oYAm^=m>k^7-fb^O%YpxaM(zobBk~7v=fBOr@8IWS zzsq?E7y5oi!4Y({WPo{q}RTlPmL90*8c~dbB|x2 zlvCSgR6l+4o%A?HvGlmWB)snyZMC}kj(yw6b!V(KMr5@%eKH39w5j*X>E}afhZPdz z3T#vPD&LRkHT7_Jez1|PDvPs){pi_zHZd+)Y_)wYfp_+hZ^it=NbGkU{`1eva@e5} zVz}Y|dYt`c>Kl)w@s$&6UB&qu&-)*n1AD*r#c)twsrf5;nuXWP-0)dT$cG`po=?q0e=de3RiCV1`KIrvLW=M-?c z{qpq(vAS>uJcUamXdP|e!YBKh>8RM+2s`EZS8B|T6V+Tli)+8%t_H11U*OgNcbF^? z+j4OmcIq_S`|_^?y=YQ!e&d&FW3iJn;BLt-=GD{tZbUJ$Y?)j##QcbSaf1IRSNHEJ z5e$5P)@%UGdI8hZhN~q@sn5jNV!m;&Airo#2h8AHa5K*_JQ#?=mOc1h+BxQW{QZk_ z&O{8L8`#^uf9ZWJfGJbW(DBJ*MU0^`M`61 zIvl67ycRv7vv7rif#o>t3qOl1{^O7KjJNgIU~lMq@UmLtd3pger zWagD=hx@l!cYpj3#)W(<1q>Oe-VEKx z&k-{oJ){R6Pu{AI&s1lxOSPPTijM>EwPq9O1@FvncBI{ijScbD2vd|5{>OHB95brK(8?-6VfSxr+5KFr(fJVG9G-_Q6|cD1hG-RTU+`qJ|2L0Z4P zE|ycSs0V8|i`k3%(A|C45Bm@2W6k*9$4=s?q8j@H4sI^?u-F`U)OlO)mA?%BTT4vK zp(V%CeRk7y*#AI%b^c>}3%_oi@O_6L%!7%W5vlbFa^qfJ^M0oJ&wb`r)Bri^npYVi zpW%;{yml3z4i*c2w-Wth6At#&{9jKvy`PqVQ>KLl55~j%gE-zO+{2zvy5$*@(_sDE z!&dH!XaC<|e_{T76E;jPK#ya`4_VEzXW=tVgMsfxvXMIQX;PY+J05PEX{$PzN}4C#u4XqkmI}YVg1X4RlLVGTF;;A)0bjh-fvq?O(*U`y|Wo>akSO+ z+`Q@<_J}A4RT*x-#YPc(@Ox_Ck(tD7j?e0wm;cLs4X3CdVC3?CW)0jCVTHZTskv{!EGuC`HDt;U~&$KKI-%qLU)Z6_~>W^KK*zq22 zB3I4P!x#;pJ{}JS0<+ne|JTk(gZPc66m>u#BM#H=oR}P7U1#vNQ)X28WXEuP+g#AS zU+H*ix*mh%os={+zju6t++JU;@sIQ9`K`H+eg0$~VHZ8JfxAxl-_Mcd7MQWCuRJK9 zRPxL{*m9HHQ(4{7MLqCObN2!^qNU;y zXw6NA+MAuuKYQNxG2jWF8hs6n|DDF)TD)5Svt(wnae~`${xkl6rL6jLH~jFkSuJoE z{&wpHy^Z|~;iFGy%Y`(Hf+^)1zINPuX8whykeCiPMjZ?LmmRnFa#r(3o)dnD&Rvyb zi~Ia72@c!Zd!H5yaoWmF&-A-`vWLA)KHIEcM|0T4&8EKBzknf4BDs?%D!=|&-kvK) zTha$g*r)ik)3*AXo@-hgTv~myY7{=@bM~HtaTDbP{<>#{J7Maum*>iRkX}iby{p#! zw=m4ANMkh@US*5kJk5$qe{P72$+sK$+sk_DA3Sb0J6}9c=M~dc9)|}a`HWGo z@I-!BNWK{Gm7EPL3yJ>%o%8_lj!)mwl-{ZpXk0lD!M~oq=b$A26@#(n>IJrRHYq?e zs$srXzDFCv!Ol7K6mhx+>aJ2N^x-DcP+?uMFKAU_Z`dS$ zghQ0&>!-y3TmE$Momu+2{P>AFD3N|pWSYbPvpRmaNIv}Iw7zd18kXANPqCIr{`)%m zfIyWJzMDp#59!}WUVE0R`jS2Vr?w7S`n>HF2~8^ai#n@`v}SZQg`FbNG>d zFD(7bUO${nA6!VY!GE@RuG$S~HL3V>HXIl)iX!(d^I8AEzglu-|Gsb`67H}=d{lSO z>kazxJ3182{$5s`b|~F;7TtfZ*b^H+Ws?tA;~r_~B=T6Ow2Vd~9;UL}5B+e;By5gH zo&H>1D314wvnbi+Gqu(4UD@u#8fppnm`XpxlUZ*Gr&tAhR)%xK-WAeK318r@cc~<>;J;{KiDBgH#KG@v+TK?t&Z&uX!!L^-(krdby;)#1lDJM zXtwO5S-0QZ-74;`$#EKJh5Qxg zCmJ12vH&h0FDI^j=AroNuXT7VO!+}QwhdotGRu8_LHPC!ED_&7tG7zxHCg#};R?P7 zQ_g#!{!4?89=|OHgY;j|_SXRU@0oJ~G`~UoEr)d+8cc5+i~qpO8t{9h8mE9g?`-`= z>EDw((Ldn-(~|m8Fg|)B+{QjUrilz>+qdiKt}r{cJ#WUI4cIuzNjeoA`pWy~{4?Bq z3XJ~BeGF~zKisDBHTB#_{kHV_7^Ce8|M=DaU*)q0z4yI6H19EbOzG9jmz{xt|HtK- zDemiL+wm!3Pej_+eOlc+x*#kWt*>7U9+y@l+<#y`PR!kYB|g;8DT?A&)|5o; zy^20q`W$}~|Mf#$>Dy2(B?o47|J)t9zYMAX7!D7&v-PTtaL@A`y-357 zU*~%5-NfdRVB$~mNJvYl>hm_urEkBoCvf%;xOF_gx$5TP{}w((-`ST!pX3dF;v2p6 z?&iDrf0?hvNakRmQ!H^b(pkrk_I{1Mn*#qTdrw#}08TX8j6)yB*|O+={fmdFDd+H! z^z}p7Xl?=Sz20xudx$>=hQ8~8pT(vdn~5sGW*czA+Kts~H)-;{@u;op(54~%kd5WR z@9jZ!xhekuwvrPj;9K#->8MT|peJ}s%_rd4IrD4ocvAyWLd@4^y^Z5f3IOEhzj@&56VLTebtwp0h{fG*x#^Sr{_ZX^w9 z3*27U#+tg(kHf_0cHi{`J4?a8w6#44`!tO^Akb|#z4r>fEv8eQkoWmp=uYCu@TM1> z=)GQE+~Ga#dsd&(*?!F9gNgCqdQahO|arV|;2gSq|Ka+n`oP+`G{oW7W^Egh3 zE97jhj{4pC*N*C6vpw6?^O@k^Tt1aEp81G}W~w5(8wfxBV2^i1m+RC_t?`Ksi|Emx zxks{osHe_1Q=1R#LaThE&azh@m%E$N>l4#RirB-VMevos#UL#qerh(@Wshr2oQ;!W}XAZ#A4=Z8M&xbmbatf0()RqvBaytxCd= zC*orEdyu@ApFjVVmOYBWb??=S%{?y@t472nU;oPk}=Ko<`dHeNwDBi_R zX&f_-y!3!igzAoma6CJ%8#o`Pn#rDSRTa8NLDPJ!^0E8*YJTRew$QPNYV|Q_jmJOZZH& ztzx95IsjHC>!rTtug_2Eh2K!e_0j9vt>)W^CyC+K_+r^YbjKXd0R73|mWKzDH}ZaT zkkK&ZC$-0wwX{|~TrpyJV4B=l=oJp=c}nt?N6Gbn*yZ~l#Z*-~v$*?V54`M$A7<6} zh5yUslo5DA^;p(^2i^>J&g+4?CYA4`i_7o$(@MN6>X+&h`}0c{I#>h!vLk$JidYdJ zq5CdgKE*LhILEY%-Q<+A>ZM8IuR#hmi5Pr+r*EKtIsUcIoa|b)<3oBgU32~{rD7-AKKd0*jp9A#riB9ob&W9_RSvxHET<15v@$X;cin?;{8?!4(&5EdjA2rq6i|*`a4ct4ue}gG=Vw!yu z0}=T8Z#c{0(KtQaZ1-Hwi0Mth zerXqAH@H+?uI$rYKa!sEZ&S5gJ+ISW?z@5}bAXOrMNZNGC?d`y!IHM^!vdL$;TtFP zV+zq+?N`k)xU2no2xAuWsk&mP@^=0@R<22C22kugxu?$kWH0E?%jKVm_A`$?i`ph2 za!;LK%{#2-uTcgBW-jFCJ|o3!zJXKLW$PiHJFY#hkV{=8?w6Ove@Dr?x%FGmSsAsh7v*sgVb;fq`QGLCXSNdP$Y46wY zT)wh~-6O)|R}JMD``ePAHkMn86sBR1WuKWa5_ay1>3xrxiDSoU+vU!$%{omq|I4q2 zz<{L}ou5je$JdN6zi=-!ZqgI)3f*sA11ETOOdTKxydJ0Dl8XPz>lYTO@vZk)SltLl zUAO-y{<3edD)fDJ3m%%WklP!6u70ma{}-Rb6X7;~HwW7<`dx4R4!)gN&+U`F+>VdK zse!doY1eh5&DfXgJ=GAo*s(0$zDk{j zOQxD478b$w@@9C&%VBu`@whn}IiW2LYW)Rxh)*r^-<|Yo>`w3&4x}xjR^e}Z*gqlv z*d*7jni%S-;3MpRx25`YuxHpI{@1a81O7WIJdle$;~eC3o?(mJmk91gRF_5RrT&2{ z^KrX$aN_a;zE&7-8i@a~d74o$9@g)Prat+VHd|4?#|sBvmYa_11LJDvHtR=P=j(>* zIiEd-Z_Mnj2AHpJ1s8X~$a!+Yk@&QJd)3CQ!_XeIfLr$DH~gR9yk4bu_1GHUv;Qjj zwXL4iV0g*a<*(>V74hd=z3S=i`_X93)+*)Hex;?dCMkmKxMV95WzD=gMKKDm_!C-*G% znMpVDNj2`?>ClPUwGCz_{ABTd&~iWU*+Jb^p?_|VSX`pYV|cgoPn!5V zzFH=yxILt%9z&n}T<%E9{@K-p8=-HFBrceeqI3Es|58=(t$ zQ)~U^gHB&TF;c* zX1VZ?gZ6OXHe6+a7+nJM;o8;Z@E^{c?L_}cY(}$<_hhSK@MXR6!N=ww7vibj z|2aiyFnTT9 z6uxH`71ut#QO%JCcX^7Bk8q}QA1##~o@Fqz_DD@=Z<|e#AMofu_;`l1a@7^}b7lJp z|Grc|yrQYJ5&u<&^$!HtJ3$WT>T>bzk@gi%y)S6Z=HI{k6^>prHyO_zbEVzO`3oK? zC#K2=&$3%j2l(dvPwa4fcdboZ=}`AH$Z9_0-^2H)&${VC+cJ5TU|9%?08 zK`?^v{*M12R0{qy%pddVQZT@(%%p>K_l|M=*ECTge< zx7n=LyX34?C)hBP?WzkcXmVmTx2+|aoqnA=`%*y|})Wr*|yG?pMoD6ysJ@x%ycpbiZJ^KS`{r>c2ncCYdao-GH>z2_MR!alJxLEQY1lh*T#na$R?Lke@% zd}h{4XPseJ)^+UaS+|deE8Wyzy=chC^tk2J>P`4_HNO4Q`8Lmaj{p3Er>0+OrT~sy z?kh*oFj7YI+ABEd9d@x_8DRAf3+3(I>P(-}Ew)8y`l0Z@ zy5GI?#{FOI)cSqs9(=roSXsE=3=+)Qcu|eEO#kbovnQVO#lfZ@zd~x_EJ7MUdRTUCz-48v*-5W*l*?!?BkJ*dcwcK@+xYnq<;UE zKGqnTK{?v}JiVA-^|s%df5{=Az=}dS&95vn-yt>@!rige@c;W}Y2i@2r}U-oXpMaH z{C;%>8)bl@mG{WE(VdfOM;~}e8*a)@?aj!(h5bLt0b=WZBzj9zHk@d!3*Z2b|LJ<) zKw>t$P5(YSQ4eB^^S|=f0K6vRRxwpc>{P_3#KjvplK3b0(6C{a-^KTCSpF5;45Q^f zHM3X8X43$~z&}kqd%Uo~CvhA}Ebi%z%h}gL>F6W4 z?B=Cr67uVjpMz2N^`rW^o2sk4gA1=>mj`py4#~m;JDYpI@@jPNUlcEiXunb?r`x3P z9QaYFrZ~WN=GDNx#_;JI=It`6vB%+@Y!S;|=ZCdJ`Nl{2Z}T^L6EvyNolfV{sU!GS z3YfPV?+n#Hb0*?1`WG4b&Jp$3_#0vf*0)-LC*mJJ7BYkIvs%|Pu3_6U=f$X4O0yPk z`ca|@%9GZW*CnwZ?uV=vqgUh>LxGj6lm%HSf&DE@b zmzl%g^`u{_OYqR6|KJ?2X^m`HEF*5oS7XB5@BdIk`ivYnL3H!)C)vAIvG734A7Ckr zz5lnlF&H{Lj6UkKrq%X&a&WTixWxn9ldn{`;`{|4{Db|wtLBA)op9JT9Ta%4Yvxg7q#7*1DsADX42=j^1b^!IbQ^j2aT#0q<%|NrQOzG^xB z0QTGaif`|LXQT9#tiTSbd)-ucdXE_{EjJz+ZAEG{r9ou=H zTKGA9YbicX!u_$&^oZB1b!Lbed;2L_SRl|u|GNcFbjyD5m2=DWKk@H=Bjs;7-!>=0p@ZeW#qbjcy`8#Ap9jdKx$2LsoITVxZ5nzHQyr!1s*g)|>EtQ^n-R5XP&WzrQYLv+J-^`oD0mQZlnJmGlON>o0ij z)ZXJ-eR^JYTC3Q}B3Bk`=UH%{>~JE?8h5aDu{mm782)(=fB#cF!SWu*^FqynL!9H%0|DQjE1#=0M||2)$_>lr_Rz!V8>1|n4zZq!!<_w`+GIY?l?3BJgZnH zwN7IEW){7ti?afSb8gMQb;PqjUB+qob9}k1 zU`hJd6x#e+_jI#i2Dp=3+@zr|hx}|m3?EWOO*U5y%fnj^n4O3y7uw^2eD-V^*j^Pk zXoU;Pv%lq5AE~Q1mGWI=qiAP&*-UIifJ>R!cfkPKO;P7szot8g)94B1qBW0BT%N2lq!$R69+ZLsJtfF=7Wh1fM z(oE<}KDXyGzvZrMty(*7~M?ApCi644-C|H-}N z-{YFs;1m6?r)Q7*iH|1r%&_?QR`tEQ?>P+0&gL;Y>G6xHa_w;Qx874e>f>|&;qO)O zo{q4$V`Dk8qCMp6nYQp_?;E-!KjuyRce_~;{?aMGvnKq1UZMekKXA+>{B5@1kAE8$ z)x>M~#%S8gkqr8<^2>_K>i>Q66y8ztnDwY3c73Tf57(c#3)i}MCb*otq1V*GKg55H zCFU|(t4+lJk&^Ua@tIJ5TrAGVvGM=wfqnE&Va@3`cuGlg>-OjK&&4(TJ2_jw;q*GIp?N%mQ$DE`h92No^ny>JaQWO> z>kody&wAR|h;7vW`|ZyYGbVUxq^<5`e+ScUz`zY=(8T)Dp17#Iu$LciR)_uBP~Tg= zA9`PX0(1H;mdj)Di%06N-Sk2^>XMi_n~Yu@lYfZmG<@pN-=TW}#SXsw?6U7iO<<0- zI(-IDZz)z5jldKBG>efAcjRB^%i?6);2TVjCGR|D>(l)E3Y=ipZ zzQrv*h===k#Yt{H6~@-5)SdD|;zMw;k(!d7iom1&{@fVfUZywqmlZzM#ZjL+XNdDG z75}wpG$DQLczlXD3 zruLpW9}Q1_%$Hq##{V;qa5v0rHI6l0=pko`p>z$b_ahF;pL)WMaPc!YvR<{^IfBo9 zxlC-$!#6sG^m?3&%@4qpsQ6F48F1;H8jHV-JnjygMb`cpXOP#z?c3Al`2P@d_^ZU` zIksAE@6>m%y~iUM@MBi9YI5BPI9OncXYh>e>Uzys{r|df#hUiP-vh8TQ7-kvAm`DS z(f>P&7aV8Y7kv3mcwlx8_J575*_YfYd|v?Hnt*p-k|TFT!a2s{Txacl3bUT0<+pqG zGl%zxCXe%}kz#QuzS~KCG1q(Mxs3-zq8+5cf7gloy84C_)Sqw6x{2|h8pDbNzDsE` zn|(hZka{(~CjWg8=MKY^D{^qoT5w~vyCq`ajtl8Nr{QcJnqNov5nYsjFVlY)ss*l_ z*RoH$x~Yly)iwF+xM!YpMgL%zn#*S-6NhKYi@SpA(#g(84Y#(Y`c8P)B)%4@BW~k) zSNt9NZd`kx)?F?w_=r8s^S$>G7gILMW1Ym_Bel60jffuz6@E<@XNq2S5A=M>t#2Ii-4LB{uSHHk-ZI9b59<))D%RgRGe^y^HSr zdRACiN?)|8d0oDJcAR}}EtdrFHT>^Kc$9z*My)gtdx?&7-t3e7AJVVV^Xo2bSRx9X znXm6WM4iR|b9JVZ;mS|>X|`o%JLJ?F{mcqZlK)r8$qmeY{2`|l!9h;AccB^|-p;@I z%%5lMp>@UMhmo!8{Y8CbnBOu6J_s8s@uzbO^xk377P;|+&tElE4&!?jX-V6Mi{S}A z6Lwsxt-h4k3q)0eCms-pJJM`x1-&fs+g%)Gm@VFL)-`Y^^(=GoALJf!*h)^!n4kS& zYFw|4=(D3IVh=Gm>$dnx%pY^;Ps7S&mHeFzPIT7)vagR~>R%kBf55-JaASQhScIp% zhTo6nnz~!*zn(L6pIAz~Z(kPr_hdEZ3w)dImqsl8`hO+edwkAy8wYTw94d`MO=LBv zl=C5BZp$1)lw>KMN_mz=PJ4)LHnde35^|V~Ic>rWg~(y-QF3}zq?0qR&8dj2Jn#F} zAJ40&?tAy|_x)bi_4$0R!}s^w1ZR7w7bdFd{SGUvvBLe{OB0KCY1{3uc}EfZ@9M6e zjpI^$@7fo7mnC3DKmKRW6KIVUG~UXV>K%Ned65~D6`og}93QBWt5&Qp^TJ3m%4dT$ z=D~YuZ+x>v{*#kI{na6)_7kw? z504KdenZ!tlP4~tSF3vuhq&&-ZizVM^IqbmmHf`N>l6h0J~)uwLujQew$3@k-#6gk zOU~;pQ4`@W^}kjBqQ`sD1mDoV!)piU*Y&kw&-GVnp}OAnDfdZKSF6jnhki^;%atCT zE+*~$hVknEFr`oozShq3d0@o4gU(y<%{Km9L2rATq#uqivuN&4aWqtn_k;Ga{^_(5 zKD{f=clYT*iM^DT@H$KG_PDc+@N9v#_Je=H_dsvdOUl?!$2Ch1{LQDEJj(wM;CGxd zwz&L}W*c=6UH4z_g%W%3l~TuoWjQaJHiZ z=AAu5LY#y=hC9;brBnHNo^{ZN4XT-Ey-r7p;~%@p75mE#`SDDD8z%otJ?B{?QF>9o z>4nCK?uL&U%Nn-HmfI-9ty%a*A%~KH;*OE;$N}l^ z!F)EJuUs2VFSejH|EG5t;pD(Jb~u)bBjiTAN{9`=vuLq;aRWHL?Vq{mMYyZ6+@X4J z{fCA8n$JZvRcFU_^Gm8h(MGSziK6~em%(c%@W>oCD4Y*>K67s1adn6v^k?GqGT$`^ zJ}8(U;m=BD1V+MS@w!c&e?VVzV$rxf%go!GX84Di<*2RhBR+<7v1Y#g#4a<1_0%0* zqbYwGES|sO3*iso?R@oV{@cOtPlmmJ{7Acep|{nO{R?R_I^`1>H}D#K`APk+obw-Y z%D6Rp(r5W-Yxi|ij}2~(de1Bs9U6SzQAfBpJsFpY#hxEoi~D){HS+;`%=EStqxS39 zvifT46o&e6nBIF3Mn%?24(!L-aTD|i@LH0bq9RN$^BSBe?RhG6 zVnh4X?3_MLPEZr7uP?$+cO?a?zdA$~e&t)*XggRLWzjU7&TYMMRW{RKS9OW*YT|I~XeOL`;shUSO`&(a!NZ8Ec=S*8On*CKjdL)0&g85zGnb;ziZ35rsoxFH-wyr` z-cN(+oyA-gYuE(udh?z8=FtCa^4B&x#s1HRAKjm!L;vL&0&upJ-ewBh<>KjEbLIDJ zbDzCm_=ER59mZd{@kM^p2);iz%PcwGYpvcgrz?D%z@#8@hg9L#H5!HRx$%+#+{m)xxnCw{wT=*b*`#Wnmp zY^^pNmz5KDy}bjb0^PV*9iELV4D#LY%bSX9{_MdrhYGV;SP{Vbn9 zd61vZ5Z_`h_#DDvDM|L$i8Sozj-o^AN$S=dr4S+4uT;oc21y!>y`Rz2cmPQ8o!&Ep__~_M zei+r)j4*q)8KLLd!u8BR{4CbrPjPmij|bn^lEkKcuZZh+)gtZn^{dXyz~pf8)M14) zY75}nE4ZMMb7pc=dq}>aUHRlN&(q&QzoQSbta^R9ik2Ja=$~ z4%zE^<=E%MIp=>StGDp++cZeMQ22L5O^hyCyq*3(&WB6G-&f3zU#f#yw z%4TKz!r9{Z&;DOvzuY|M*`N?Y;UL*7WJq&ZAs}wI8Y#^Q)rPdI50e z;&tx~nI@mxV0N{Vdo3leUm~~rllB(>b+*uxwBgVq^9cUg*_tN8+)nnV{NHeSA-sV@ zmBaZzTOGZkR_&jsKavag6AKBRJ#v{Y6FUo=sAa5Vmp?pjKN{C9lux);@IAPtabnLs zcq}|Qu%en8^a*n=;;?RUGpE(sTuwqKwQjGb z^*lTbV*N0+P?#3^o%PJ7DZ}-w3+3rKI8^+V%#fRWV1}@rc~svoc*`Dq&rjHY;8SdZ z54Mgs`zzjiPsaZV^yOtSY44goD6a0rvv6kIKHr1k%P;85TvCIwu4**J$FVfV40{`6 zpIqEl#kchr0b(~W6jgYH;KPv%7V{0NSrr?TJ}f5~XwSf?>G@@4aC^S`0N&Zk-sxlTY8f>T zIKKgZROsTlPyBzI{3mh~y?qCMVV7E~aUT1I+Sl;wX54GTrMdK`zYUwK-*Q1MsI|3! z0sei`M1FxkcC28N(Bwb?4B3+Dd5hnQdHOf=E;gA0 zw_@;9oc=ezUcJtHTwvJ9&(x@Q{-cu*;oz6E)%13mKg*f!buLgn;m^07El6-is*}1#EBLok z-M`RY@|#QT^?$A9L~XqnS}k1cb#G`d>Ai7$qe7;f4WGr@uS|ZorzAaNU-x_hvwo11 zj)OJ)KmCv#3@#@%Ob&cr3NEdnhwM$L>y$sBe#Y0|wALqRfQ{_9eiu8iQ}B63Nf*Wk zYCI#q@22)~9v@7n|Ew=2-?<3d>s(3sdnLGff6yOryM6wJFK!x3m*A!`YOfDRvnRg# za=86!4_{#C&}n8Z)%8Aw%Z*?1EU;%i|D?8iJf$Y&^KnV)TDW;FZcabrI&d!+_C98< zmuaPY@c)2!>A!q7^4Y&zsBgl*ihaZqtyAru>7ZTwmOzd*erZH#=f zB8&~-KYkXGOB1Y1ae_!u+k54g+ zFbBRZ!Bu{vF`PJ7mwvPUeAqYaYjrH~{J&zf+LPiDp2otzTQB)Le*RmY^BirAe@6=g;?V@_aW@qs1QEBTcxX*mk%QU7~8n~N}m!p5>I$<=^pY&&wFnJ#S zpLSl)u!DEq43Ur1%Ij~NE#U_zQ#})jP7J=2dmhd`W8DQ8&9)vFoByKIvdvn<>2PAlXqwXhUc~Xc z=gB$PxCdK*CFifl*_uCJ_o4i>tDG?sGjSqCZEbj4sMxMVtEiVaSSFyP~Tt3qm ze}&2S`F=b-G3TQ5Xm`_{Y_g&?jXM#?M9U?5>n-Do2WYC@hw;%~b@uPg-NAs}0r66j z=981`9wh(mZ!Pp$v(@rD7*=V3ITU*GG`ln|uhw(G{1V(NJ5D^q!J%+=)D*Sy`_*XJ wDKAPrVutvKtIxucDIsbkU(%Rmn?L=)}98xwBohl+>)1Nxi3sQ>@~ literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/0/11.latk b/npm/examples/public/history/0/0/11.latk new file mode 100644 index 0000000000000000000000000000000000000000..8b12d9fad8b2f050af9b1d5d23e7ec89d53ecf01 GIT binary patch literal 67650 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 literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/0/13.latk b/npm/examples/public/history/0/0/13.latk new file mode 100644 index 0000000000000000000000000000000000000000..38ca0acc678bd0073304d8fbbe719f50d1d0a78f GIT binary patch literal 67650 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�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 literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/0/14.latk b/npm/examples/public/history/0/0/14.latk new file mode 100644 index 0000000000000000000000000000000000000000..f4e9a9191715c0bbb0ca65ed912928ce81439c71 GIT binary patch literal 67650 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 literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/0/15.latk b/npm/examples/public/history/0/0/15.latk new file mode 100644 index 0000000000000000000000000000000000000000..e906eb1747b2591895f685bef45a5fdc969244bb GIT binary patch literal 67650 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 e@TiKMOL7ppkut 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 literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/0/17.latk b/npm/examples/public/history/0/0/17.latk new file mode 100644 index 0000000000000000000000000000000000000000..65acd61c8c1a8d50ca0fce5a8921c0f19a1f4f27 GIT binary patch literal 67650 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 C?Raj5f$~GykKDqc8iUHVqga*w%D!Mt=O&a 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 literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/0/19.latk b/npm/examples/public/history/0/0/19.latk new file mode 100644 index 0000000000000000000000000000000000000000..c1f97dae330c4676cc205b3cf7ad2b6b218e489d GIT binary patch literal 67650 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 BM&1Ab literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/0/2.latk b/npm/examples/public/history/0/0/2.latk new file mode 100644 index 0000000000000000000000000000000000000000..bc899b42241b1a698189ec1c8c186fa76815913c GIT binary patch literal 67650 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 EKYW=lW&i*H literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/0/20.latk b/npm/examples/public/history/0/0/20.latk new file mode 100644 index 0000000000000000000000000000000000000000..77b2d43ea248d9237da6866843a95917fb644a33 GIT binary patch literal 67650 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 literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/0/21.latk b/npm/examples/public/history/0/0/21.latk new file mode 100644 index 0000000000000000000000000000000000000000..9e2fa2d902356d06c884bd1af1a8f35f69b744b6 GIT binary patch literal 67650 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 D6OU)X literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/0/22.latk b/npm/examples/public/history/0/0/22.latk new file mode 100644 index 0000000000000000000000000000000000000000..98990f260afbbfa7bf73cebe1b16f671643a5bb1 GIT binary patch literal 33858 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 literal 0 HcmV?d00001 diff --git a/npm/examples/public/history/0/0/3.latk b/npm/examples/public/history/0/0/3.latk new file mode 100644 index 0000000000000000000000000000000000000000..50781c674bc0f93747fd6a0acaace46c5b13c339 GIT binary patch literal 67650 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 =+j>7!?osUx_{X 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⁡1v#zk=3peX8gz@KL-75Y# z{U`SI7x8{j${){fc;^Xcx9!=;f%x7owRjw0Ya$J{6MW%&_4(xe?YzTL>; zJ^nR5!Y*zQFB?7$pRL_%SA(@JJzba7!xq|qG3ZQ$I+%5sdkR;vX3Z|EHUHIljb8dW zraMmrn}cV}=05Iv@N=5^+_xz?&@_&|-`iPoSl8+wG_RMe*EG+YvQ}$r$U~3vx$E~JPWhnQ3RjM{hgM;ANugQ?hmv7 z?_rbLRPt}|pqa6aP={GhV>bUDooEU@JZm1d4-pTBiFb4Rxc)kEVz52gssGV+IvH~b zjjZ<|_5m}`n13Zd_!Dk(8Ad%)2gks9GWg3ucv&r{8IMn0mVe#a0)H#V<*oUSIrJ{L z`VIf@v0eYSu>^CR^YWi@Vq?XZ#(9?hr{6HxuM#6~OolU4&GCx;En1o!_*HBg5ry|! zpPN$k{7r*@?l~q!pLmWM@j;k4k>A+2&0o&dh>AC@S6+I&m2~!8W zgy-Q0cg~Wpz_)Ml!%wp0^z`pC@ntj)GFi^@X?xErg%N*CR2Nu5%W}VhDlstDXW7w` zZq9MD^^YE)7l;)T@sPXt; z`+)qv-xr?$Xn&uxrV$Nk2n}&p@&D5 z`PK0+9+E_)2@+Js7BD%IG`ov_K4jxet-->~?rHj=Y+5ZmMG5EQlJ<5OBQQj)tGltfX zii>}tk2BBs%d!zlR+UOeQe}%6m{)c@{)GO!jG4MB<{T^3)Ok{VxUS&6vyvL=!dR~e5N(>b% znpvw>_8X^MGbdHQHhrO%_}^dsVl~Y&P5#2hYS1Yw>A#7_pS=v18b!nDrPskV8+?xc z@xh+2%U9d*VV@~UQ|CD4tTIkK>St$Uci5My_!Ruu&hA=Tr-XOCcjO+mwvOUef6sHn z?^jyu!kKtlTXqpiA1uZ%<)a%z)QFbQ=GbE_U&^>ctcjrii8Y~4 zb?0ZbOJHaj8-*E5UK1;RvX16_%>BBF#a+bHTG#cV$tl_$kuP@^Lql*L``CkD?ZSIf f{z>l)T|ZR^_0 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