Releases: posit-dev/hephaestus
Releases · posit-dev/hephaestus
Release list
Hephaestus v0.4.1
Fixed
- A frame wider or taller than 4096 px renders on the Hybrid backend. The device is asked for
max_texture_dimension_2dup to 16384 rather than wgpu's 8192 default, and the rasterizer's cap on intermediate layer textures is raised to match, so a clipped panel past 4096 px no longer fails the render; past a device's own limit — on either rasterizing backend — the frame is aBackendErrorrather than a wgpu panic. - A document holding a polar plot reads back. The decoder restores the projection before it attaches the axes, so a polar placement no longer panics against the not-yet-set Cartesian default; a placement that genuinely mismatches is reported as a
DocumentErrorrather than panicking.
Hephaestus v0.4.0
Changed
- The PNG writers take a
PngCompression, before their trailing dpi, the way the TIFF writers take aTiffCompression.Balancedmatches the previous output byte for byte;Fastencodes a dense frame in 8 ms rather than 146 ms, for about half again the bytes, which is what a host serializing a PNG per animation frame needs. Breaking:write_png/write_png_to/encode_pnggained the argument.
Hephaestus v0.3.0
Added
- Chrome is pickable.
SceneBuilder::push_pick_scope/pop_pick_scoperecord the tree a primitive is drawn in, andplot::picknames it: a hit on an axis tick label, gridline, legend key, strip or title reports its plot, region, part and ordinal throughPlotPath, and the scope stack is the path an event bubbles along. PickIndex::hits_atreturns every hit, topmost first, each carrying its scope chain — it costs the same as topmost-only.pick_atremains the one-number convenience.- Rectangle and lasso queries.
hits_in(bounds intersect),hits_within(bounds enclosed, and exact) andhits_in_path(bounds-centre inside a path) for brushing and marquee selection, plus*_intovariants that reuse a caller's buffer. Slot::from_nameandSlot::ALL— the reverse ofSlot::name, so a region name recovered from a hit round-trips back intoCompositionLayout::get.Plot::index_in_patchandAxis::id— a plot's position within its patch's attach list, and the handle an axis was attached under.(patch_id, index_in_patch)is the pairupdate_plot_atalready addresses plots by.RecordingScene::draw_opsandscope_at— the ops that draw something, and the pick-scope stack in effect at an op.- PDF export, behind the
pdffeature.hephaestus::pdf::write_pdf/write_pdf_to/encode_pdfoverPdfScene, which implementsSceneBuilderand notRenderer, soPlotComposition::renderfeeds it unchanged. Wheresvgaims at editable output this aims at a fixed one, so every glyph a plot draws is embedded — as a subset TrueType synthesized from the outlines actually used, which is a few kB rather than the megabyte face and covers CFF/OTF sources, variable-font instances andttcfcollections in one path. Text stays selectable and searchable through a/ToUnicodeCMap; markdown links become/Linkannotations; a filled-and-stroked mark is oneBoperator; gradients areShadingType 2/3patterns with a non-zero radial focal radius expressed natively; a mesh is a nativeShadingType 4Gouraud shading rather than one fill per triangle; a gradient or mesh whose alpha varies across it — a confidence band that fades, say — is carried by a luminosity soft mask rather than flattened to one opacity; a translucent or blended layer is a real transparency group; and color emoji render, both COLR paint graphs and bitmap strikes. Whatever PDF cannot express is reported byPdfScene::warnings. Adds onlyskrifaandflate2, both already in the tree, so--no-default-features --features document-read,pdfbuilds with no renderer on rustc 1.86; a bitmap color glyph additionally needspng. primitives::RibbonOptions::seam_bleedandprimitives::ribbon_band_mesh_with_bleed— control the overlap ribbon tessellation gives adjacent quads to hide the antialiased seam between independent fills. Both default to the existing values;0.0is for a consumer that paints the mesh as one object, where there is no seam and the overlap is distortion.- SVG export, behind the
svgfeature.hephaestus::svg::write_svg/write_svg_to/encode_svgoverSvgScene, which implementsSceneBuilderand notRenderer, soPlotComposition::renderfeeds it unchanged. Labels are<text>: markdown spans become<tspan>s,[text](url)an<a href>, underline and strikethroughtext-decoration-line, and a run is placed by one anchor plustextLength. A filled-and-stroked mark is one<path>, a wrapped label one<text>. The face request — family, size, weight, style,font-stretch, letter spacing, OpenType features and variable-font axes — is named on the root and inherited, with only the spans that differ naming their own;@importcovers families resolved throughfetch_google_font, andSvgConfig::embed_fontsinlines faces as@font-face. Whatever SVG cannot express is reported bySvgScene::warnings. Adds onlyskrifa, so--no-default-features --features document-read,svgbuilds with no renderer on rustc 1.86; embedding a raster image needspng. hephaestus::scene::TextSource— what a glyph run was shaped from, carried onGlyphRunandOwnedGlyphRun: the source substring, aFontSpec, the run's advance, its decorations, a link destination, and aTextGroupnaming the runs laid out together.Nonewhere a caller positioned glyphs itself.hephaestus::style_vocab::FontSpec, besideFontFamilyEntry,GenericFamilyKind,FontStyleKind,FontFeatureSettingandFontVariationSetting, which moved there fromtext. Re-exported fromcrate::text, so every existing path still resolves.TextRun::text/TextRun::font_spec— a shaped run keeps what it was shaped from.hephaestus::text::google_fonts::google_fetched_families— which families this process resolved through Google Fonts.- A plot can be on screen before the renderer exists.
PlotView.createtakes aplaceholder— an<img>already in the page, or a URL — and shows it until the first live frame lands. Measured in headless Chrome: plot pixels at 57 ms against 185 ms, with 0 of 378,000 pixels differing between a nativevello-hybridrender and the client's WebGL2 frame, no blank frame between them and zero layout shift. The placeholder becomes thesaveOnRightClickoverlay rather than a second element, which skips that overlay's eagertoDataURLand keeps the page'salt. Nothing touches the element until there is a frame to reveal, so a falseisSupported(), a failed wasm fetch and an unreadable document all leave the picture on screen;www/index.html?nowasm=1demonstrates it. - Every raster write entry point takes a trailing
Option<f64>dpi, so a file declares the resolution it was rendered at rather than leaving a viewer to assume 72 dpi: a PNGpHYschunk, the JFIF density fields, the TIFF resolution tags, and an EXIF block in the WebP container.Nonerecords nothing. hephaestus::document::read_document— the composition and the hints in one pass.read_hintsis only cheap in isolation; besideread_compositionit decodes the head twice.ReadContext::builtin()is a shared context beside it, sinceReadContext::newbuilds a fifteen-entry geom factory table per call.examples/document_placeholder.rs— reads a document, registers the faces the wasm client ships, rasterizes throughvello-hybridat a device-pixel box and writes a PNG with its dpi. Built withdocument-readalone, so the picture is made from what the reader rebuilds rather than from the live composition.crates/hephaestus-wasm/bench/— a dependency-free measurement harness driving the DevTools Protocol from Node: the pixel diff, the swap and first-paint checks, and a static server whoseapplication/wasmMIME and compression switches are the experiment.- An "Embedding: the producer's side" section in
crates/hephaestus-wasm/CLAUDE.md— what a host emits per render, the page shape, the three ways to get the wasm to the page and what each costs, and what is known and unknown about Positron's plot pane. - Images render in rich text.
draws wherever markdown reaches: aTextGeomorTextFitGeomrow, a plot or composition title, an axis title, a strip label, a legend title, a break label. Alt text is dropped — the location is the key. A registered name wins; otherwise the location is read as a filesystem path or anhttp(s)URL and cached back into the register, through the newImageRegistry::resolve(getstill answers for registered entries alone, andloaded_namesreports what a register read for itself). An inline image stands one em tall and takes its width from the pixel aspect ratio, centered on the font's ink band; a tag alone in its paragraph is a block image that fills the column, inherits the reservedimgselector, and at natural width reads its own pixels as pt. A location that resolves to nothing draws a one-em placeholder styled by the reservedbroken_imageselector.flatten_rich_rundrops images, so text on a curve keeps the advance and loses the picture. hephaestus::image::decode_image/read_image— decode by signature rather than by named format. A recognized format whose codec is not compiled in reportsio::ErrorKind::Unsupported, distinguishing it from bytes that are not an image.image-url(off by default) — read an image named by anhttp(s)URL. Synchronous fetch on first use, memoized for the process, no on-disk cache.- A composition-level image register.
PlotComposition::image_registry/set_image_registry/image_registry_mut/image_registry_ref. Composition chrome resolves against it and a plot's chrome and geoms against their own plot's, with no fallback between them. - Documents carry both registers. The
imgschunk includes names a register read from a location, not only those a caller registered, plus the composition's own register, which rides acompositionfield of its own. ImageGeom— raster images in a panel, with size, rotation, anchor, opacity, sampling mode and aspect-fit rule. Pixels live in aplot::ImageRegistryand the"image"channel carries the name, so a discrete scale maps a category onto an image. Each axis takes its extent from the channels supplied:"x2"/"y2"spans a data-space rect, absent anchors at"x"/"y"with"width"/"height"in pt, and the two mix freely; with neither size channel the image's own pixels are read as pt, with one the other follows the aspect."fit"is"stretch"(default),"contain"or"cover", and"anchor_x"/"anchor_y"distribute the slack or overflow. Band offsets default to0.0on all four edges, unlikeRectGeom; an image that should fill its band sets"x_band"and"x2_band"itself. Three limits: under a non-linear projection a data-space extent is the bounding box of its projected corners (...
Hephaestus v0.2.0
Added
- A second rasterizing backend, behind the off-by-default
vello-hybridfeature.backend::hybrid::{HybridScene, HybridRenderer}rasterize through Vello Hybrid's sparse strips — path processing and coverage on the CPU, a plain render pipeline on the GPU — independently ofvello: either, both, or neither. Picking returns only ids that were actually drawn, there is no draw-count ceiling, and the rasterizer is under half the wasm size of the compute-shader one. Parity covers fills, strokes, gradients, clip and blend layers, meshes, images and text. - A wasm build that needs no WebGPU, behind the off-by-default
webglfeature.backend::hybrid::HybridWebGlRendererruns the same sparse-strip rasterizer against a canvas's WebGL2 context and pulls in no wgpu at all;window::WebGlHostpresents it with the samerender/resize/dispatchsurfaceCanvasHostoffers. It cannot rasterize offscreen and does not implementRenderer, since the canvas is the only render target. - Live window presentation, behind the off-by-default
windowfeature (winit).window::run(config, app)opens an OS window and pumps an event loop; the app implementsWindowApp, drawing into aFrame— scene, physical size and dpi — and taking resize, cursor and mouse-button events. A resize renders at the new size rather than stretching the last frame. Picking is opt-in throughWindowConfig::picking, after whichEventCtx::pick_atanswers the id under any pixel. winit stays out of the public surface. Native only; seeexamples/window.rs. - Canvas presentation, behind the off-by-default
canvasfeature (wasm32 only, no winit).window::CanvasHostattaches to a<canvas>already on a page and sharesWindowApp,FrameandEventwith the desktop window, but the page keeps its own event loop and callsrender,resizeanddispatch. Picking answers from a frame or two earlier rather than parking the main thread. - Presentation runs on either rasterizing backend.
WindowConfig::backendnames it, and its variants exist only for the backends compiled in.windowandcanvasrequire a rasterizing backend rather thanvellospecifically. On the sparse-strip backend the hosts present straight into the swap chain, skipping the intermediate texture and its per-frame full-screen blit;HybridRenderer::set_target_formatis how a host names its surface's format. - The hitmap can refresh less often than the frame.
WindowConfig::pick_intervalcaps how often the pick pass runs andset_refresh_pickis the per-render control under it; frames in between reuse the previous hitmap, sopick_atmay describe a slightly older frame. The pick pass rasterizes the scene a second time — at 100k marks it costs 60 ms of a 145 ms frame — so throttling it during a resize drag or an animation recovers all of that. crates/hephaestus-wasm— a wasm render client, shipped as the npm packagehephaestus-wasm: a page points it at a canvas and a.hplotdocument and gets a plot that re-solves its layout on resize instead of stretching. WebGL2 is the default configuration and needs no WebGPU; the mutually exclusivewgpu-backendfeature swaps in the compute-shader path.PlotViewexposesrender,resize,setDark,isSupported,hasFontsanddocumentFormatVersion, plussaveOnRightClick— an overlaid, coincident<img>that gives the plot an ordinary image context menu (Save image as…, Copy image, drag-to-save) under a caller-supplied filename, with the render dpi written into the exported PNG../build.shassemblesdist/andverify-dist.mjschecks it the way a consumer loads it.- The wasm client registers fonts once per page. WOFF and WOFF2 decode behind the default-on
webfontsfeature,registerGoogleFontneeds no API key, and four static Roboto faces are fetched as a fallback when the page registers nothing of its own. CJK stays a bring-your-own case. - Plot documents — behind the off-by-default
document-readanddocument-writefeatures (documentenables both).document::write_compositioncaptures aPlotCompositionas a self-contained byte string anddocument::read_compositionrebuilds a live one, so a plot can be authored in one process and drawn in another. Nothing shaped, measured or solved is written: the consumer callsrenderat whatever size it has and the plot reflows rather than scaling a frozen image.WriteOptionscarries the render hints,embed_fonts, and alossyswitch for the itemsunsupported_itemsreports as unwritable;ReadContexttakes the host's own geom constructors and named formatters. Hand-rolled and dependency-free, so--no-default-features --features document-writeis a complete writer with no renderer at all. Seeexamples/document_save.rsandexamples/document_load.rs. document::read_hintsandDocumentHints— read a document's render hints (background,size,dpi) without rebuilding the composition. Decodes the head alone, so it is cheap enough to call first.document::FORMAT_VERSION_MAJOR/FORMAT_VERSION_MINOR— the document format version this build speaks. The reader requires an exact major match, so it is a hard compatibility boundary between writer and reader.- JPEG, TIFF and WebP writers — behind the new
jpeg,tiffandwebpfeatures, one pure-Rust encoder each. Every format offers the same three entry points as PNG:write_*to a path,write_*_tofor any writer,encode_*for the bytes. TIFF and WebP are lossless and carry alpha; TIFF takes aTiffCompression, andwrite_jpegtakes aquality(1–100) and a backgroundColorto composite onto. hephaestus::image— the home of every raster writer, including PNG.hephaestus::png::{write_png, encode_png, write_png_to}continue to resolve as aliases.scene::recording::RecordingScene::replay— issue every recorded op against anySceneBuilder, in order. What lets a backend defer draws until it knows the frame size, or rasterize a second scene from the same draws.RecordingScene,OpandOwnedGlyphRunarePartialEq. Equality is op-for-op, which makes a recording a test oracle over drawing rather than pixels.Fontcompares by face — the same bytes at the same index — not by blob identity.examples/backend_perf.rs— where a frame's time goes on each backend at a given mark count, reporting the fastest of ten runs.- Markdown in every chrome text slot. Legend titles, colorbar titles, break labels and legend text swatches read the
markdownflag, so a theme that turns rich text on gets it everywhere. Polar titles that stamp glyphs along an arc stay plain. RichTextRunanswers the metricsTextRundoes —baseline_offset,cap_height,ink_top_offsetandinked_height— so a caller can anchor either kind of run the same way. The inked band unions glyph ink with every block paint box.RichTextStyleSheet::iter,lenandis_empty— walk a sheet's named style deltas.AxisTheme::resolved_with_root— resolve anAxisThemeagainst a root text element, asTheme::resolved_axisalready did forPerAxis.Geom::kind— a stable wire name for the concrete geom type behind aBox<dyn Geom>. Every geom in the crate returnsSome; the default isNone, so a downstream geom opts in by overriding it and registering a matching constructor withReadContext::with_geom.GeomBuilder::from_partsis the inverse ofinto_parts.Plot::add_boxed_geomandPlot::geoms— the counterpart toremove_geom, and a borrowing iterator over every geom with its id in draw order. Alongside them, getters for settings that had only setters:aspect_ratio_ref,is_clipped,tracks_identity,title_ref,subtitle_ref,caption_refandshape_registry_ref.Scale::with_named_format/set_named_format, andScale::format_spec— a label formatter registered under a name, plusFormatSpec(Default,Named,Custom) describing which kind a scale carries. A name is what lets a scale's labels be reproduced in another process; an anonymouswith_formatclosure staysCustom.Scale::try_with_binsandtry_set_bins— fallible siblings ofwith_bins, returningBinEdgeError(TooFew,NotFinite,NotIncreasing) instead of panicking.try_set_binsleaves the existing ladder in place when it rejects.linetype::try_patternandlinetype::check_pattern— fallible siblings ofpatternandvalidate_pattern, returningPatternError(OddLength,Misaligned).text::register_font_families— register a font blob and get the family names back, so a host with no system fonts can pair a blob withset_generic_familyinstead of guessing from a filename.register_font_byteskeeps returning the face count.text::registered_families— the font families the context knows, so a caller can decide whether a fallback font is needed.text::font_faces_for_family,generic_family_namesandset_generic_family— read the font files backing a family, or a generic family's resolution order, out of the font context and reinstate them elsewhere.font_faces_for_familyreturns one entry per distinct file, since a collection holds a whole family in one.
Changed
Localeis a tag and nothing more:Locale::EN_US,Locale::from("ar-EG"),locale.tag(). Describing a locale correctly takes a CLDR-sized table, which belongs with whatever formats labels rather than with the renderer that draws them. The tag rides onTheme, reaches everyLabelFormatter, and is written into a plot document. It is carried verbatim, soar_EGandar-EGare distinct locales.- The default tick formatter ignores the locale. Numbers render with a
.decimal and temporal values as compactYYYY-MM-DD/HH:MM:SS, whatever locale is passed. An axis that should follow a locale supplies a closu...