diff --git a/.changeset/added_configurable_stroke_styles.md b/.changeset/added_configurable_stroke_styles.md deleted file mode 100644 index 5c33b41..0000000 --- a/.changeset/added_configurable_stroke_styles.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -default: major ---- - -# Added configurable stroke caps and joins - -Vector strokes can now select their endpoint caps, segment joins, and miter limit: - -```rust -let stroke = Stroke::new(color, 4.0) - .with_cap(StrokeCap::Butt) - .with_join(StrokeJoin::Miter) - .with_miter_limit(4.0); -``` - -`Stroke::new` defaults to `StrokeCap::Round` and `StrokeJoin::Round`, preserving the GPU renderer's existing appearance. CPU and GPU rendering now use the same explicit style and miter threshold. This intentionally changes CPU output that previously inherited tiny-skia's implicit butt-cap and miter-join defaults. Stroke-based outline extraction also preserves the configured style and dash gaps. - -`Stroke` is now non-exhaustive. Replace direct struct construction with `Stroke::new(paint, width)` and the corresponding `with_*` methods, and add `..` to destructuring patterns. The live-preview plugin ABI is now v8, so rebuild plugins before loading them into an updated host. diff --git a/.changeset/added_ordered_audio_effect_components.md b/.changeset/added_ordered_audio_effect_components.md deleted file mode 100644 index d5a6d64..0000000 --- a/.changeset/added_ordered_audio_effect_components.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -default: major ---- - -# Added composable timeline trimming and audio envelopes - -Timeline components now support `trim`, while audio components support `fade_in`, `fade_out`, and `gain_envelope` directly in builder chains. Trim ranges use the immediate child's local clock, with negative endpoints counted from its end and open endpoints resolved to the exact child end. - -Temporal effects wrap in call order, so changing their order changes the result: - -```rust -source.fade_in(1.0).trim(0.5..); // starts halfway through the existing fade -source.trim(0.5..).fade_in(1.0); // starts a new fade from silence -``` - -`AudioFile` effects now finish the source builder immediately: place `path`, `gain`, and `duration` before the first effect, omit a trailing `.build()`, and keep `.fill()` outermost. Custom audio components must implement `render_audio_block` instead of `samples` or `mix_into`. diff --git a/.changeset/fixed_gpu_closed_stroke_artifacts.md b/.changeset/fixed_gpu_closed_stroke_artifacts.md deleted file mode 100644 index d0723c8..0000000 --- a/.changeset/fixed_gpu_closed_stroke_artifacts.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -default: patch ---- - -# Fixed GPU artifacts on explicitly closed strokes - -Closed vector paths whose final segment already returns to the subpath start no longer produce detached bands during GPU rasterization. Affected strokes preserve their closed joins while remaining within the same painted footprint as CPU rendering. diff --git a/.changeset/fixed_vector_paint_bounds.md b/.changeset/fixed_vector_paint_bounds.md deleted file mode 100644 index 4687f5d..0000000 --- a/.changeset/fixed_vector_paint_bounds.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -default: major ---- - -# Fixed clipped and distorted vector decorations - -Vector fragments and stroked paths now preserve their full painted extent during rasterization, preventing overflowing shadows, rounded borders, and path strokes from being stretched or clipped at component edges. The correction is applied consistently by both CPU and GPU renderers. - -Existing projects that contain affected decorations may produce different video pixels after upgrading, so regenerate and review reference renders or snapshots. diff --git a/.changeset/increased_timeline_time_precision.md b/.changeset/increased_timeline_time_precision.md deleted file mode 100644 index fd6898d..0000000 --- a/.changeset/increased_timeline_time_precision.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -default: major ---- - -# Increased timeline time precision - -Timeline and local times now use `f64` seconds, preserving distinct frame and audio-sample positions in long projects. `Phase`, drawing values, gains, and PCM samples remain `f32`. - -Unsuffixed numeric literals need no changes, but explicit second values and custom `Time` or `TimelineComponent` implementations must migrate their timeline-facing types from `f32` to `f64`. - -Live-preview plugins now use the v6 entry symbol and must be rebuilt. diff --git a/.changeset/made_component_cache_frequency_aware.md b/.changeset/made_component_cache_frequency_aware.md deleted file mode 100644 index ce02962..0000000 --- a/.changeset/made_component_cache_frequency_aware.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -default: patch ---- - -# Made component caching reuse-aware - -Component outputs are now admitted after their second observation instead of immediately. Cached CPU and GPU rasters are retained according to decayed reuse frequency, observed rerendering cost, and byte size, reducing memory spent on one-off results while preserving entries that avoid the most rendering work. diff --git a/.changeset/made_component_trait_objects_cloneable.md b/.changeset/made_component_trait_objects_cloneable.md deleted file mode 100644 index c49a58a..0000000 --- a/.changeset/made_component_trait_objects_cloneable.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -default: major ---- - -# Made component trait objects cloneable - -`Box`, `Box`, and `Box` now implement `Clone`, enabling function-form `#[component]` definitions to accept boxed effects and children and allowing custom wrappers to clone boxed descendants. The canonical `Box` form is cloneable too, so composable timeline effects can be passed through erased fields: - -```rust -#[component(timeline)] -fn ProcessedAudio( - source: Box, -) -> impl TimelineComponent { - Timeline::builder().child(source).build() -} - -let source: Box = AudioFile::builder() - .path("voice.wav") - .fade_in(0.2) - .fade_out(0.2) - .into(); - -let track = ProcessedAudio::builder().source(source).build(); -``` - -This changes the component implementation contract: custom raster, vector, and timeline component types must support cloning, normally with `#[derive(Clone)]`. Generic wrappers may also need an explicit `T: Clone` bound. - -Live-preview plugins now use the v9 entry symbol and must be rebuilt. diff --git a/.changeset/made_raster_residency_consumer_driven.md b/.changeset/made_raster_residency_consumer_driven.md deleted file mode 100644 index adf8608..0000000 --- a/.changeset/made_raster_residency_consumer_driven.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -default: major ---- - -# Moved CPU/GPU raster residency control to render callers - -Raster consumers now request CPU- or GPU-resident component outputs from the render context. Uploads and readbacks are therefore render-context responsibilities, and uploaded GPU representations of raster components participate in the main component cache's frequency-aware admission and eviction instead of living in an independent upload cache. - -Timeline components propagate the residency received by `frame` into the main component cache, so custom components must forward that request when rendering descendants. - -For example, a CPU consumer now requests its output representation directly: - -```diff -- let frame = timeline.frame(t, target, &mut ctx); -+ let frame = timeline.frame(t, target, RasterResidency::Cpu, &mut ctx); -``` - -Component implementations receive the same request and forward it when rendering descendants: - -```diff - fn render( - &self, - size: Vec2, - target: Resolution, -+ residency: RasterResidency, - ctx: &mut dyn RenderContext, - ) -> RasterImage { - // ... -- ctx.render(self.child.as_ref(), size, target) -+ ctx.render(self.child.as_ref(), size, target, residency) - } -``` - -The `RasterComponent`, `RenderContext`, `GpuRasterBackend`, `TimelineComponent`, `Timeline`, and `TimelineCollection` interfaces changed, so live-preview plugins now use the v5 entry symbol and must be rebuilt. - -The independent upload-cache counters and live-preview diagnostic headers were removed; GPU representation usage is now reported through the main component-cache and VRAM metrics. diff --git a/.changeset/removed_legacy_timeline_api.md b/.changeset/removed_legacy_timeline_api.md deleted file mode 100644 index e80fd1f..0000000 --- a/.changeset/removed_legacy_timeline_api.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -default: major ---- - -# Removed the legacy closure timeline API - -The old `tellur_core::timeline::{Timeline, timeline}` closure API, its `LegacyTimeline` adapter, and `export_legacy_timeline!` have been removed. Replace closure scenes with function-form `#[component(timeline)]` components, read time from an injected `#[clock] Clock`, and export the resulting `TimelineComponent` tree with `export_timeline!`: - -```rust -#[component(timeline)] -fn Main(#[clock] clock: Clock) -> impl TimelineComponent { - let scene = Scene::builder().time(clock.global()).build(); - Timeline::builder() - .child(scene.at(0.0..5.0)) - .build() -} - -export_timeline!( - root = Main::builder().build(), - title = "Main", -); -``` - -`export_timeline!` now takes the root component expression directly instead of -a zero-argument factory function. `title` is the human-readable label. A single -exported timeline uses the machine-facing lookup id `"main"` by default; add -`id = "..."` after `title` only when a different stable id is required. - -For offline rendering, pass the built tree through `resolve` or -`resolve_with_canvas`, then give the resulting `&ResolvedTimeline` to -`FfmpegEncoder::encode`. The former `encode_timeline` method has been folded -into `encode`, which muxes the resolved audio by default; select -`AudioExport::Omit` when the output should contain video only. diff --git a/.changeset/removed_redundant_child_composition_cache.md b/.changeset/removed_redundant_child_composition_cache.md deleted file mode 100644 index e4c4d49..0000000 --- a/.changeset/removed_redundant_child_composition_cache.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -default: patch ---- - -# Removed the redundant child-composition cache - -The cache only skipped the final blend after independently cached child images had already been rendered, while retaining both its inputs and output outside the main component cache. That limited reuse did not justify its memory and lifecycle cost, so child batches are now recomposited when needed. diff --git a/.changeset/removed_timeline_composite_cache.md b/.changeset/removed_timeline_composite_cache.md deleted file mode 100644 index 973bd33..0000000 --- a/.changeset/removed_timeline_composite_cache.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -default: patch ---- - -# Removed the timeline composite cache - -Real-project testing showed no meaningful rendering speedup from reusing completed timeline composites, while the global cache retained its input and output rasters outside the render context's lifecycle and memory accounting. Timeline frame batches are now recomposited when needed. diff --git a/.changeset/unified_positioning_and_added_stack.md b/.changeset/unified_positioning_and_added_stack.md deleted file mode 100644 index 6baafbe..0000000 --- a/.changeset/unified_positioning_and_added_stack.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -default: major ---- - -# Unified placement vocabulary and added Stack containers - -`Positioned` now snaps a child anchor to either an absolute `Vec2` point or an `Anchor` on its resolved parent box, with a shared `.offset(Vec2)` adjustment. `Alignment`, `Anchor::to`, and `Frame.align` have been removed; wrap a child with `.anchored(child_anchor).snap_to(parent_anchor)` for box-relative placement. Direct `Positioned` construction must use its new `child`, `anchor`, `target`, and `offset` representation. - -Vector and raster `Stack` containers now size themselves from one required `base` child, lay out `under` and `over` children tightly at that resolved size, preserve overlay paint-bounds overflow, and paint in `under → base → over` order. diff --git a/CHANGELOG.md b/CHANGELOG.md index 788258e..ce2e30c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,165 @@ All notable changes to tellur are documented in this file. Versions are lockstep across the workspace. +## 0.3.0 (2026-07-19) + +### Breaking Changes + +- **Added configurable stroke caps and joins** + + Vector strokes can now select their endpoint caps, segment joins, and miter limit: + + ```rust + let stroke = Stroke::new(color, 4.0) + .with_cap(StrokeCap::Butt) + .with_join(StrokeJoin::Miter) + .with_miter_limit(4.0); + ``` + + `Stroke::new` defaults to `StrokeCap::Round` and `StrokeJoin::Round`, preserving the GPU renderer's existing appearance. CPU and GPU rendering now use the same explicit style and miter threshold. This intentionally changes CPU output that previously inherited tiny-skia's implicit butt-cap and miter-join defaults. Stroke-based outline extraction also preserves the configured style and dash gaps. + + `Stroke` is now non-exhaustive. Replace direct struct construction with `Stroke::new(paint, width)` and the corresponding `with_*` methods, and add `..` to destructuring patterns. The live-preview plugin ABI is now v8, so rebuild plugins before loading them into an updated host. + +- **Added composable timeline trimming and audio envelopes** + + Timeline components now support `trim`, while audio components support `fade_in`, `fade_out`, and `gain_envelope` directly in builder chains. Trim ranges use the immediate child's local clock, with negative endpoints counted from its end and open endpoints resolved to the exact child end. + + Temporal effects wrap in call order, so changing their order changes the result: + + ```rust + source.fade_in(1.0).trim(0.5..); // starts halfway through the existing fade + source.trim(0.5..).fade_in(1.0); // starts a new fade from silence + ``` + + `AudioFile` effects now finish the source builder immediately: place `path`, `gain`, and `duration` before the first effect, omit a trailing `.build()`, and keep `.fill()` outermost. Custom audio components must implement `render_audio_block` instead of `samples` or `mix_into`. + +- **Fixed clipped and distorted vector decorations** + + Vector fragments and stroked paths now preserve their full painted extent during rasterization, preventing overflowing shadows, rounded borders, and path strokes from being stretched or clipped at component edges. The correction is applied consistently by both CPU and GPU renderers. + + Existing projects that contain affected decorations may produce different video pixels after upgrading, so regenerate and review reference renders or snapshots. + +- **Increased timeline time precision** + + Timeline and local times now use `f64` seconds, preserving distinct frame and audio-sample positions in long projects. `Phase`, drawing values, gains, and PCM samples remain `f32`. + + Unsuffixed numeric literals need no changes, but explicit second values and custom `Time` or `TimelineComponent` implementations must migrate their timeline-facing types from `f32` to `f64`. + + Live-preview plugins now use the v6 entry symbol and must be rebuilt. + +- **Made component trait objects cloneable** + + `Box`, `Box`, and `Box` now implement `Clone`, enabling function-form `#[component]` definitions to accept boxed effects and children and allowing custom wrappers to clone boxed descendants. The canonical `Box` form is cloneable too, so composable timeline effects can be passed through erased fields: + + ```rust + #[component(timeline)] + fn ProcessedAudio( + source: Box, + ) -> impl TimelineComponent { + Timeline::builder().child(source).build() + } + + let source: Box = AudioFile::builder() + .path("voice.wav") + .fade_in(0.2) + .fade_out(0.2) + .into(); + + let track = ProcessedAudio::builder().source(source).build(); + ``` + + This changes the component implementation contract: custom raster, vector, and timeline component types must support cloning, normally with `#[derive(Clone)]`. Generic wrappers may also need an explicit `T: Clone` bound. + + Live-preview plugins now use the v9 entry symbol and must be rebuilt. + +- **Moved CPU/GPU raster residency control to render callers** + + Raster consumers now request CPU- or GPU-resident component outputs from the render context. Uploads and readbacks are therefore render-context responsibilities, and uploaded GPU representations of raster components participate in the main component cache's frequency-aware admission and eviction instead of living in an independent upload cache. + + Timeline components propagate the residency received by `frame` into the main component cache, so custom components must forward that request when rendering descendants. + + For example, a CPU consumer now requests its output representation directly: + + ```diff + - let frame = timeline.frame(t, target, &mut ctx); + + let frame = timeline.frame(t, target, RasterResidency::Cpu, &mut ctx); + ``` + + Component implementations receive the same request and forward it when rendering descendants: + + ```diff + fn render( + &self, + size: Vec2, + target: Resolution, + + residency: RasterResidency, + ctx: &mut dyn RenderContext, + ) -> RasterImage { + // ... + - ctx.render(self.child.as_ref(), size, target) + + ctx.render(self.child.as_ref(), size, target, residency) + } + ``` + + The `RasterComponent`, `RenderContext`, `GpuRasterBackend`, `TimelineComponent`, `Timeline`, and `TimelineCollection` interfaces changed, so live-preview plugins now use the v5 entry symbol and must be rebuilt. + + The independent upload-cache counters and live-preview diagnostic headers were removed; GPU representation usage is now reported through the main component-cache and VRAM metrics. + +- **Removed the legacy closure timeline API** + + The old `tellur_core::timeline::{Timeline, timeline}` closure API, its `LegacyTimeline` adapter, and `export_legacy_timeline!` have been removed. Replace closure scenes with function-form `#[component(timeline)]` components, read time from an injected `#[clock] Clock`, and export the resulting `TimelineComponent` tree with `export_timeline!`: + + ```rust + #[component(timeline)] + fn Main(#[clock] clock: Clock) -> impl TimelineComponent { + let scene = Scene::builder().time(clock.global()).build(); + Timeline::builder() + .child(scene.at(0.0..5.0)) + .build() + } + + export_timeline!( + root = Main::builder().build(), + title = "Main", + ); + ``` + + `export_timeline!` now takes the root component expression directly instead of + a zero-argument factory function. `title` is the human-readable label. A single + exported timeline uses the machine-facing lookup id `"main"` by default; add + `id = "..."` after `title` only when a different stable id is required. + + For offline rendering, pass the built tree through `resolve` or + `resolve_with_canvas`, then give the resulting `&ResolvedTimeline` to + `FfmpegEncoder::encode`. The former `encode_timeline` method has been folded + into `encode`, which muxes the resolved audio by default; select + `AudioExport::Omit` when the output should contain video only. + +- **Unified placement vocabulary and added Stack containers** + + `Positioned` now snaps a child anchor to either an absolute `Vec2` point or an `Anchor` on its resolved parent box, with a shared `.offset(Vec2)` adjustment. `Alignment`, `Anchor::to`, and `Frame.align` have been removed; wrap a child with `.anchored(child_anchor).snap_to(parent_anchor)` for box-relative placement. Direct `Positioned` construction must use its new `child`, `anchor`, `target`, and `offset` representation. + + Vector and raster `Stack` containers now size themselves from one required `base` child, lay out `under` and `over` children tightly at that resolved size, preserve overlay paint-bounds overflow, and paint in `under → base → over` order. + +### Fixes + +- **Fixed GPU artifacts on explicitly closed strokes** + + Closed vector paths whose final segment already returns to the subpath start no longer produce detached bands during GPU rasterization. Affected strokes preserve their closed joins while remaining within the same painted footprint as CPU rendering. + +- **Made component caching reuse-aware** + + Component outputs are now admitted after their second observation instead of immediately. Cached CPU and GPU rasters are retained according to decayed reuse frequency, observed rerendering cost, and byte size, reducing memory spent on one-off results while preserving entries that avoid the most rendering work. + +- **Removed the redundant child-composition cache** + + The cache only skipped the final blend after independently cached child images had already been rendered, while retaining both its inputs and output outside the main component cache. That limited reuse did not justify its memory and lifecycle cost, so child batches are now recomposited when needed. + +- **Removed the timeline composite cache** + + Real-project testing showed no meaningful rendering speedup from reusing completed timeline composites, while the global cache retained its input and output rasters outside the render context's lifecycle and memory accounting. Timeline frame batches are now recomposited when needed. + + ## 0.2.1 (2026-07-10) ### Fixes diff --git a/Cargo.lock b/Cargo.lock index 3f83ed2..52c7b78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2030,7 +2030,7 @@ dependencies = [ [[package]] name = "tellur" -version = "0.2.1" +version = "0.3.0" dependencies = [ "cargo_metadata", "clap", @@ -2043,7 +2043,7 @@ dependencies = [ [[package]] name = "tellur-core" -version = "0.2.1" +version = "0.3.0" dependencies = [ "ab_glyph", "bon", @@ -2068,7 +2068,7 @@ dependencies = [ [[package]] name = "tellur-live" -version = "0.2.1" +version = "0.3.0" dependencies = [ "lru", "png", @@ -2079,7 +2079,7 @@ dependencies = [ [[package]] name = "tellur-macros" -version = "0.2.1" +version = "0.3.0" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2089,14 +2089,14 @@ dependencies = [ [[package]] name = "tellur-plugin" -version = "0.2.1" +version = "0.3.0" dependencies = [ "tellur-core", ] [[package]] name = "tellur-renderer" -version = "0.2.1" +version = "0.3.0" dependencies = [ "bytemuck", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 48cd5a2..71b6c36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ exclude = ["tools/release-notes"] resolver = "2" [workspace.package] -version = "0.2.1" +version = "0.3.0" edition = "2021" license = "MIT" readme = "README.md" diff --git a/tellur-core/Cargo.toml b/tellur-core/Cargo.toml index 25cb9ff..4fac099 100644 --- a/tellur-core/Cargo.toml +++ b/tellur-core/Cargo.toml @@ -40,7 +40,7 @@ rustybuzz = "0.20" # full Symphonia feature set so users can import common audio containers/codecs # such as WAV/AIFF, FLAC, Ogg/Vorbis, MP3, and MP4 AAC/ALAC. symphonia = { version = "0.5.4", default-features = false, features = ["all"] } -tellur-macros = { path = "../tellur-macros", version = "0.2.1" } +tellur-macros = { path = "../tellur-macros", version = "0.3.0" } thiserror = "2.0.18" # LaTeX math layout (RaTeX), gated behind the `latex` feature. `ratex-layout` diff --git a/tellur-live/Cargo.toml b/tellur-live/Cargo.toml index 6638239..9092f36 100644 --- a/tellur-live/Cargo.toml +++ b/tellur-live/Cargo.toml @@ -24,9 +24,9 @@ include = [ [dependencies] lru = "0.12.5" png = "0.18.1" -tellur-core = { path = "../tellur-core", version = "0.2.1" } -tellur-plugin = { path = "../tellur-plugin", version = "0.2.1" } -tellur-renderer = { path = "../tellur-renderer", version = "0.2.1" } +tellur-core = { path = "../tellur-core", version = "0.3.0" } +tellur-plugin = { path = "../tellur-plugin", version = "0.3.0" } +tellur-renderer = { path = "../tellur-renderer", version = "0.3.0" } [[example]] name = "demo_timeline_plugin" @@ -41,4 +41,4 @@ name = "timeline_showcase" crate-type = ["cdylib"] [dev-dependencies] -tellur-plugin = { path = "../tellur-plugin", version = "0.2.1" } +tellur-plugin = { path = "../tellur-plugin", version = "0.3.0" } diff --git a/tellur-plugin/Cargo.toml b/tellur-plugin/Cargo.toml index c35b8a6..39d404d 100644 --- a/tellur-plugin/Cargo.toml +++ b/tellur-plugin/Cargo.toml @@ -9,4 +9,4 @@ description = "Plugin ABI for loading tellur timelines as dynamic libraries" build = "build.rs" [dependencies] -tellur-core = { path = "../tellur-core", version = "0.2.1" } +tellur-core = { path = "../tellur-core", version = "0.3.0" } diff --git a/tellur-renderer/Cargo.toml b/tellur-renderer/Cargo.toml index 339e7db..dcdaab1 100644 --- a/tellur-renderer/Cargo.toml +++ b/tellur-renderer/Cargo.toml @@ -36,7 +36,7 @@ lru = "0.12" pollster = "0.3" rustc-hash = "1.1.0" sysinfo = { version = "0.32", default-features = false, features = ["system"] } -tellur-core = { path = "../tellur-core", version = "0.2.1" } +tellur-core = { path = "../tellur-core", version = "0.3.0" } thiserror = "2.0.18" tiny-skia = "0.12.0" vello = "0.2.1" diff --git a/tellur/Cargo.toml b/tellur/Cargo.toml index acd805f..f3f04c0 100644 --- a/tellur/Cargo.toml +++ b/tellur/Cargo.toml @@ -25,10 +25,10 @@ latex = ["tellur-core/latex", "tellur-renderer?/latex"] cli = ["dep:clap", "dep:cargo_metadata", "dep:toml_edit", "dep:tellur-live", "renderer"] [dependencies] -tellur-core = { path = "../tellur-core", version = "0.2.1" } -tellur-plugin = { path = "../tellur-plugin", version = "0.2.1" } -tellur-renderer = { path = "../tellur-renderer", version = "0.2.1", optional = true } -tellur-live = { path = "../tellur-live", version = "0.2.1", optional = true } +tellur-core = { path = "../tellur-core", version = "0.3.0" } +tellur-plugin = { path = "../tellur-plugin", version = "0.3.0" } +tellur-renderer = { path = "../tellur-renderer", version = "0.3.0", optional = true } +tellur-live = { path = "../tellur-live", version = "0.3.0", optional = true } clap = { version = "4", features = ["derive"], optional = true } cargo_metadata = { version = "0.18", optional = true } toml_edit = { version = "0.22", optional = true }