A lit 3D cube in a 2D diagram

explainer

The brief. Put a lit 3D cube that turns once inside a 2D diagram, with labels that follow its top and front faces while the camera moves in.

Open in the playground Source on GitHub

What it uses

The code

examples/src/cube_3d.rs in oxframe-sdk, for oxframe 0.20.2. In the playground it compiles in the browser and you can change it.

//! A 3D node inside a 2D diagram: a lit cube turns in a `View3d`, and 2D
//! labels follow points on its faces.

use oxframe::prelude::*;

/// A label and a leader line pinned to a point on the cube.
fn pin(world: &Scene3d, cube: &Object3d, frame: View3d, text: &str, local: Vec3, offset: Vec2) {
    let (w, c, to_view) = (world.clone(), cube.clone(), frame.projector());
    let at = move || to_view(w.world_point(&c, local));
    let label = frame.add(Txt::new().text(text).caption().opacity(0.0));
    let line = frame.add(
        Line::new()
            .points([(0.0, 0.0), (0.0, 0.0)])
            .stroke(Muted)
            .line_width(2.0)
            .opacity(0.0),
    );
    let a = at.clone();
    label.x().bind(move || a().x + offset.x);
    let a = at.clone();
    label.y().bind(move || a().y + offset.y);
    line.points().bind(move || {
        let p = at();
        vec![p + offset * 0.8, p]
    });
    spawn(async move {
        all((label.fade_in(), line.fade_in())).await;
    });
}

#[oxframe::scene]
pub async fn diagram(view: View) {
    view.add(
        Txt::new()
            .text("A 3D node in a 2D diagram")
            .h1()
            .to_edge(Edge::Top),
    );
    let world = Scene3d::new();
    let cube = world.add(Mesh::cube().color(Accent).roughness(0.35));
    let frame = view.add(View3d::new(&world).size(760.0, 520.0).y(40.0));

    // The diagram around it is plain 2D.
    let code = view.add(
        Rect::new()
            .width(340.0)
            .height(90.0)
            .radius(Lg)
            .fill(Surface)
            .x(-740.0)
            .y(40.0),
    );
    code.add(Txt::new().text("Scene3d").body());
    let out = view.add(
        Rect::new()
            .width(340.0)
            .height(90.0)
            .radius(Lg)
            .fill(Surface)
            .x(740.0)
            .y(40.0),
    );
    out.add(Txt::new().text("MP4 · PNG · check").body());
    for (a, b) in [
        ((-560.0, 40.0), (-400.0, 40.0)),
        ((400.0, 40.0), (560.0, 40.0)),
    ] {
        view.add(
            Line::new()
                .points([a, b])
                .end_arrow(true)
                .stroke(Muted)
                .line_width(4.0),
        );
    }

    pin(
        &world,
        &cube,
        frame,
        "top",
        Vec3::new(0.0, 0.5, 0.0),
        Vec2::new(-120.0, -110.0),
    );
    pin(
        &world,
        &cube,
        frame,
        "front",
        Vec3::new(0.0, 0.0, 0.5),
        Vec2::new(150.0, 90.0),
    );

    // One full turn while the camera moves in.
    all((
        cube.rotation().to(Vec3::new(0.0, 360.0, 0.0), 6.0),
        world.camera().position().to(Vec3::new(2.2, 1.8, 3.0), 6.0),
    ))
    .await;
    cube.color().to(Success, 0.6).await;
    wait_for(0.5).await;
}

pub fn project() -> Project {
    Project::new("cube_3d").scene(diagram())
}

More examples