linkage-blaze 0.1.10

No-std 3D turtle graphics for animated jointed figures
Documentation

linkage-blaze

GitHub crates.io docs.rs

3D turtle graphics for animated jointed mechanisms and figures. Describe a figure with moves, turns, branches, links, joints, disks, and spheres, then animate parameters to bring it to life. Runs both no_std and std.

Project Links

What is Linkage Blaze?

Linkage Blaze is a Rust-based domain-specific language (DSL) for making animated jointed drawings. It works like 3D turtle graphics: move forward, turn, branch, draw links, place joints, and add simple shapes such as disks and spheres. Animate a few parameters, and the drawing moves.

The demos include robot arms, clocks, and motion-controlled skeletons. The workspace targets microcontrollers through Device Envoy's CYD (Cheap Yellow Display) APIs (ESP32 , RP, WASM).

The default crate configuration is no_std and allocation-free, so figures live in flash and animate on small microcontrollers. An opt-in alloc feature adds heap-based conveniences where an allocator is available.

Gallery

The live gallery is the main showcase: carlkcarlk.github.io/linkage-blaze/demos/

It shows preview images of each demo and links to the live, interactive WASM versions.

Articles

Usage

linkage-blaze can run on embedded systems:

  • It does not require the Rust standard library (no_std).
  • It does not use heap allocation.
  • ESP and RP applications can use the LinkageFixed access generated by linkage_file!.

It can also run, with more features, in std runs:

  • alloc enables owned parsing.
  • bvh enables host-side APIs for reading Biovision Hierarchy (BVH) motion-capture files.
[dependencies]
linkage-blaze = "<latest version>"

Replace <latest version> with the current release shown on crates.io.

Install the Biovision Hierarchy (BVH) converter with:

cargo install linkage-blaze --features bvh --bin bvh-to-lb

Platform Examples

  • Raspberry Pi Pico / RP - Pico 1 and Pico 2 examples, including Pico W variants.
  • ESP32 - Cheap Yellow Display examples across supported ESP32 families and boards.
  • Browser / WASM - Browser builds behind the live gallery.

The platform examples and browser adapter use the workspace version but are not separately published to crates.io. WASM applications provide their own cdylib and may depend on linkage-blaze with the alloc feature.

Quick Start

The core workflow is: construct one or more linkages, combine them, obtain a borrowed LinkageView, and evaluate that view for geometry or a final pose.

1. Construct a linkage from steps

LinkageFixed stores an allocation-free linkage in fixed-capacity arrays. Start at the origin, optionally define normalized parameters, and append movement and drawing steps with the fluent methods:

# use linkage_blaze::LinkageFixed;
const ARM: LinkageFixed<1, 1, 8> = LinkageFixed::start()
    .define_param("shoulder", 0.5)
    .yaw_param("shoulder", -90.0, 90.0)
    .forward(3.0)
    .mark("hand");

The const generic arguments are the parameter count, mark-slot count, and step capacity. Unused step capacity is allowed.

2. Combine linkages

LinkageFixed::combine appends another linkage without replaying its initial Start step:

# use linkage_blaze::LinkageFixed;
const BASE: LinkageFixed<0, 0, 2> = LinkageFixed::start().forward(2.0);
const TIP: LinkageFixed<0, 0, 2> = LinkageFixed::start().left(1.0);
const FIGURE: LinkageFixed<0, 0, 4> = BASE.combine(TIP.view());

The output type states the combined parameter, mark, and step capacities.

3. Evaluate the final pose

Call LinkageFixed::view to borrow a linkage, then pass one normalized value per parameter to LinkageView::final_pose:

# use linkage_blaze::{LinkageFixed, Vec3};
# fn main() -> Result<(), linkage_blaze::Error> {
const LINKAGE: LinkageFixed<1, 0, 4> = LinkageFixed::start()
    .define_param("reach", 0.5)
    .forward_param("reach", 1.0, 5.0);

let pose = LINKAGE.view().final_pose(&[0.5])?;
assert!(pose.position().is_close_to(&Vec3::from([3.0, 0.0, 0.0]), 1e-5));
# Ok(())
# }

4. Evaluate for rendering

LinkageView::draw_items_3d evaluates strokes and shapes as an iterator of render::Item3d values. A platform renderer can project and draw each item without allocating:

# use linkage_blaze::{LinkageFixed, render::Item3d};
# fn main() -> Result<(), linkage_blaze::Error> {
const LINKAGE: LinkageFixed<0, 0, 3> = LinkageFixed::start()
    .forward(2.0)
    .left(1.0);

let stroke_count = LINKAGE
    .view()
    .draw_items_3d(&[])?
    .filter(|item| matches!(item, Item3d::Stroke(_)))
    .count();
assert_eq!(stroke_count, 2);
# Ok(())
# }

See the complete ESP32, RP, and WASM examples for display integration.

5. Save and import a large linkage

Put a long fluent expression in a .lb.rs asset file. The file contains one linkage![...] expression with leading-dot methods:

# use linkage_blaze::{LinkageFixed, linkage};
# macro_rules! __linkage_blaze_start {
#     () => { LinkageFixed::<1, 1, 4>::start() };
# }
# let linkage: LinkageFixed<1, 1, 4> =
linkage![
    .define_param("reach", 0.5)
    .forward_param("reach", 1.0, 5.0)
    .mark("tip")
];
# assert_eq!(linkage.view().dof(), 1);

Import it with linkage_file!. The macro measures the asset at compile time and creates a module containing its exact fixed type, value, and borrowed view. Choose view() for a borrowed, allocation-free handle, fixed() when you need the compile-time owner, or buf() under alloc when the linkage must be growable:

use linkage_blaze::linkage_file;

linkage_file! {
    figure {
        file: "assets/figure.lb.rs",
    }
}

type FigureFixed = figure::Fixed;
type FigureView = figure::View;
const FIGURE: FigureFixed = figure::fixed();
const FIGURE_VIEW: FigureView = figure::view();
#[cfg(feature = "alloc")]
let figure_buf: figure::Buf = figure::buf();

The import is a text excerpt because rustdoc cannot provide the external asset file at the macro call site. The repository's linkage_file integration test compile-checks the complete external-file import path.

6. Edit a large linkage

Open the interactive Linkage Blaze editor to edit and preview .lb.rs assets. Open an existing file or paste an expression, adjust its generated parameter controls while watching the 3D preview, and use Save or Save As to write the edited .lb.rs file.

Editor output uses the same syntax as the allocator-backed parser, so it can be checked before use:

# #[cfg(feature = "alloc")]
# fn main() -> Result<(), String> {
use linkage_blaze::LinkageBuf;

let edited_source = r#"
linkage![
    .define_param("reach", 0.5)
    .forward_param("reach", 1.0, 5.0)
    .mark("tip")
]
"#;
let linkage = LinkageBuf::<1, 1>::from_lb_rs(edited_source)?;
assert_eq!(linkage.view().dof(), 1);
# Ok(())
# }
# #[cfg(not(feature = "alloc"))]
# fn main() {}

For motion-capture input, see the bvh module and its Biovision Hierarchy conversion APIs.

Policy on AI-assisted development and contributions

The use of AI tools is permitted for development and contributions to this repository. AI may be used as a productivity aid for drafting, exploration, and refactoring.

All code and documentation contributed to this repository must be reviewed, edited, and validated by a human contributor. AI tools are not a substitute for design judgment, testing, or responsibility for correctness.

AGENTS.md contains the general instructions and constraints given to AI tools used during development of this repository.

License

Licensed under either:

at your option.