Skip to content

Scale Bar Measurement Workflow

This example compares scale bars across overview, detail, and 3D views.

Preview

Run And Adapt

Commands below assume a Datoviz source checkout and start at the repository root. Use your configured build environment; Python routes additionally require local bindings.

Route Availability Command or action
C Canonical native source just example-c showcases/scalebar_measurement (build and run), or rerun ./build/examples/c/showcases/scalebar_measurement
Python Available python3 -m examples.python.gallery.showcases.scalebar_measurement
Browser Live WebGPU route Open live example

Use this example as capability or integration evidence, not as a minimal copy-paste template. Start from the nearest supported, copy-safe example and add this feature after verifying the linked API reference.

What To Look For

The overview and detail panels use synthetic microscopy-like RGBA textures, zoom boxes, point markers, labels, and independent scale bars, while the 3D panel shows a small rotating specimen cloud with its own measurement cue. Compare the scale labels and view extents: the same visual language explains both image pixels and scene-space geometry.

This workflow is useful for scientific figures where readers must understand physical size while moving between context, zoomed detail, and a 3D measurement.

Source

#!/usr/bin/env python3
"""Scale-bar measurement workflow across image and 3D specimen panels."""

from __future__ import annotations

import ctypes

import numpy as np

import datoviz as dvz

from examples.python.gallery import common as ex


OVERVIEW_WIDTH = 224
OVERVIEW_HEIGHT = 160
DETAIL_WIDTH = 180
DETAIL_HEIGHT = 136
DETAIL_POINTS = 120
CLOUD_COUNT = 125
ROTATION_SPEED_RAD_PER_SEC = 0.35
TAU = 2.0 * np.pi


class ScalebarMeasurementState:
    def __init__(self) -> None:
        self.specimen_rotation = None
        self.specimen_animation = None

    def destroy_tracks(self) -> None:
        if self.specimen_rotation:
            dvz.dvz_track_destroy(self.specimen_rotation)
        self.specimen_rotation = None


def _u8(value):
    return np.clip(255.0 * value + 0.5, 0.0, 255.0).astype(np.uint8)


def _sample_field(x, y, detail: bool):
    value = 0.10 + 0.10 * np.sin(TAU * (2.2 * x + 0.7 * y))
    value += 0.06 * np.cos(TAU * (0.4 * x - 3.8 * y))
    for i in range(9):
        cx = np.mod(0.17 + 0.137 * i, 0.96)
        cy = np.mod(0.21 + 0.219 * i, 0.94)
        dx = x - cx
        dy = y - cy
        sigma = 0.030 + 0.004 * (i % 3) if detail else 0.044
        d2 = (dx * dx + 1.35 * dy * dy) / (2.0 * sigma * sigma)
        value += (0.33 + 0.05 * (i % 4)) * np.exp(-d2)
    return np.clip(value, 0.0, 1.0)


def _field_texture(width: int, height: int, *, detail: bool):
    x = np.linspace(0.0, 1.0, width, dtype=np.float32)
    y = np.linspace(0.0, 1.0, height, dtype=np.float32)
    u, v = np.meshgrid(x, y)
    sample = _sample_field(u, v, detail)
    ridge = 0.5 + 0.5 * np.sin(TAU * (7.0 * u + 2.4 * v))

    rgba = np.empty((height, width, 4), dtype=np.uint8)
    rgba[..., 0] = _u8(0.07 + 0.16 * sample + 0.05 * ridge)
    rgba[..., 1] = _u8(0.12 + 0.55 * sample + 0.06 * ridge)
    rgba[..., 2] = _u8(0.16 + 0.74 * sample)
    rgba[..., 3] = 255
    return rgba


def _set_domain(panel, xmin: float, xmax: float, ymin: float, ymax: float) -> None:
    if dvz.dvz_panel_set_domain(panel, dvz.DVZ_DIM_X, xmin, xmax) != 0:
        raise RuntimeError("dvz_panel_set_domain(X) failed")
    if dvz.dvz_panel_set_domain(panel, dvz.DVZ_DIM_Y, ymin, ymax) != 0:
        raise RuntimeError("dvz_panel_set_domain(Y) failed")


def _configure_panel(panel) -> None:
    dvz.dvz_panel_set_background_color(panel, ex.BG)


def _add_image(scene, panel, rgba, xmin: float, xmax: float, ymin: float, ymax: float) -> None:
    positions = np.array(
        [[xmin, ymin, 0.0], [xmin, ymax, 0.0], [xmax, ymin, 0.0], [xmax, ymax, 0.0]],
        dtype=np.float32,
    )
    texcoords = np.array(
        [[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]],
        dtype=np.float32,
    )
    image = dvz.dvz_image(scene, 0)
    if not image:
        raise RuntimeError("dvz_image() failed")
    if dvz.dvz_visual_set_data_many(image, {"position": positions, "texcoords": texcoords}) != 0:
        raise RuntimeError("dvz_visual_set_data_many(image) failed")
    field = dvz.dvz_sampled_field_from_array(scene, rgba)
    if dvz.dvz_visual_set_field(image, b"field", field) != 0:
        raise RuntimeError("dvz_visual_set_field(image) failed")
    if dvz.dvz_visual_set_depth_test(image, False) != 0:
        raise RuntimeError("dvz_visual_set_depth_test(image) failed")
    ex.add_visual(panel, image)


def _add_zoom_box(scene, panel) -> None:
    starts = np.array(
        [[3.10, 2.25, 0.0], [5.10, 2.25, 0.0], [5.10, 3.65, 0.0], [3.10, 3.65, 0.0]],
        dtype=np.float32,
    )
    ends = np.array(
        [[5.10, 2.25, 0.0], [5.10, 3.65, 0.0], [3.10, 3.65, 0.0], [3.10, 2.25, 0.0]],
        dtype=np.float32,
    )
    colors = np.tile(np.array([[ex.GREEN.r, ex.GREEN.g, ex.GREEN.b, 230]], dtype=np.uint8), (4, 1))
    widths = np.full(4, 2.2, dtype=np.float32)

    box = dvz.dvz_segment(scene, 0)
    if not box:
        raise RuntimeError("dvz_segment() failed")
    if dvz.dvz_visual_set_data_many(
        box,
        {
            "position_start": starts,
            "position_end": ends,
            "color": colors,
            "stroke_width_px": widths,
        },
    ) != 0:
        raise RuntimeError("dvz_visual_set_data_many(zoom box) failed")
    if dvz.dvz_segment_set_caps(box, dvz.DVZ_SEGMENT_CAP_SQUARE, dvz.DVZ_SEGMENT_CAP_SQUARE) != 0:
        raise RuntimeError("dvz_segment_set_caps(zoom box) failed")
    if dvz.dvz_visual_set_depth_test(box, False) != 0:
        raise RuntimeError("dvz_visual_set_depth_test(zoom box) failed")
    ex.add_visual(panel, box)


def _add_detail_points(scene, panel) -> None:
    t = np.linspace(0.0, 1.0, DETAIL_POINTS, dtype=np.float32)
    a = TAU * (9.0 * t + 0.07 * np.sin(21.0 * t))
    r = 0.10 + 0.78 * np.sqrt(t)

    positions = np.zeros((DETAIL_POINTS, 3), dtype=np.float32)
    positions[:, 0] = 4.10 + 0.54 * r * np.cos(a)
    positions[:, 1] = 2.95 + 0.34 * r * np.sin(a)

    pulse = np.sin(TAU * (3.0 * t + 0.11))
    colors = np.empty((DETAIL_POINTS, 4), dtype=np.uint8)
    colors[:, 0] = _u8(0.20 + 0.20 * t)
    colors[:, 1] = _u8(0.66 + 0.22 * t)
    colors[:, 2] = _u8(0.76 + 0.12 * pulse * pulse)
    colors[:, 3] = 210
    diameters = (4.4 + 5.4 * pulse * pulse).astype(np.float32)

    points = dvz.dvz_point(scene, 0)
    if not points:
        raise RuntimeError("dvz_point() failed")
    if dvz.dvz_visual_set_data_many(
        points,
        {
            "position": positions,
            "color": colors,
            "diameter_px": diameters,
        },
    ) != 0:
        raise RuntimeError("dvz_visual_set_data_many(detail points) failed")
    if dvz.dvz_visual_set_depth_test(points, False) != 0:
        raise RuntimeError("dvz_visual_set_depth_test(detail points) failed")
    ex.set_filled_point_style(points)
    ex.add_visual(panel, points)


def _add_panel_scalebar(panel, anchor, color, label_position) -> None:
    desc = dvz.dvz_scale_bar_desc()
    desc.dimension = dvz.DVZ_DIM_X
    desc.anchor = anchor
    desc.label_position = label_position
    desc.target_length_px = 132.0
    desc.min_length_px = 82.0
    desc.max_length_px = 190.0
    desc.offset_px[:] = (26.0, 24.0)
    desc.tick_length_px = 8.0
    desc.line_width_px = 2.0
    desc.unit = b"m"
    desc.data_to_unit = 0.001
    desc.line_color[:] = (color.r, color.g, color.b, 255)

    scalebar = dvz.dvz_scale_bar(panel, ctypes.byref(desc))
    if not scalebar:
        raise RuntimeError("dvz_scale_bar(panel) failed")

    style = dvz.dvz_text_style()
    style.size_px = 16.0
    style.renderer = dvz.DVZ_TEXT_RENDERER_MSDF_ATLAS
    style.color[:] = (color.r, color.g, color.b, 255)
    if dvz.dvz_scale_bar_set_label_style(scalebar, ctypes.byref(style)) != 0:
        raise RuntimeError("dvz_scale_bar_set_label_style(panel) failed")


def _add_world_scalebar(panel) -> None:
    desc = dvz.dvz_scale_bar_desc()
    desc.dimension = dvz.DVZ_DIM_X
    desc.reference_mode = dvz.DVZ_SCALEBAR_REFERENCE_VIEW_PLANE
    desc.reference_position[:] = (0.0, 0.0, 0.0)
    desc.anchor = dvz.DVZ_SCENE_ANCHOR_BOTTOM_RIGHT
    desc.label_position = dvz.DVZ_SCALEBAR_LABEL_ABOVE
    desc.target_length_px = 122.0
    desc.min_length_px = 78.0
    desc.max_length_px = 176.0
    desc.offset_px[:] = (24.0, 24.0)
    desc.tick_length_px = 8.0
    desc.line_width_px = 2.0
    desc.unit = b"m"
    desc.data_to_unit = 0.001
    desc.line_color[:] = (ex.TEXT.r, ex.TEXT.g, ex.TEXT.b, 255)

    scalebar = dvz.dvz_scale_bar(panel, ctypes.byref(desc))
    if not scalebar:
        raise RuntimeError("dvz_scale_bar(world) failed")

    style = dvz.dvz_text_style()
    style.size_px = 16.0
    style.renderer = dvz.DVZ_TEXT_RENDERER_MSDF_ATLAS
    style.color[:] = (ex.TEXT.r, ex.TEXT.g, ex.TEXT.b, 255)
    if dvz.dvz_scale_bar_set_label_style(scalebar, ctypes.byref(style)) != 0:
        raise RuntimeError("dvz_scale_bar_set_label_style(world) failed")


def _add_3d_cloud(scene, panel):
    camera = dvz.dvz_camera_desc()
    camera.view.eye[:] = (0.18, -0.10, 3.35)
    camera.view.target[:] = (0.0, 0.0, 0.0)
    camera.view.up[:] = (0.0, 1.0, 0.0)
    camera.projection.fov_y = 0.70
    camera.projection.near_clip = 0.1
    camera.projection.far_clip = 100.0
    if dvz.dvz_panel_set_camera_desc(panel, ctypes.byref(camera)) != 0:
        raise RuntimeError("dvz_panel_set_camera_desc() failed")

    positions = []
    colors = []
    diameters = []
    for z in range(5):
        for y in range(5):
            for x in range(5):
                fx = -1.0 + 0.5 * x
                fy = -1.0 + 0.5 * y
                fz = -1.0 + 0.5 * z
                d = np.sqrt(fx * fx + fy * fy + fz * fz)
                if d > 1.24 or len(positions) >= CLOUD_COUNT:
                    continue
                positions.append((fx, fy, fz))
                colors.append(
                    (
                        int(np.clip(255.0 * (0.22 + 0.15 * x / 4.0) + 0.5, 0, 255)),
                        int(np.clip(255.0 * (0.58 + 0.30 * y / 4.0) + 0.5, 0, 255)),
                        int(np.clip(255.0 * (0.84 + 0.12 * (1.0 - z / 4.0)) + 0.5, 0, 255)),
                        230,
                    )
                )
                diameters.append(11.0 + 8.0 * (1.24 - d))

    points = dvz.dvz_point(scene, 0)
    if not points:
        raise RuntimeError("dvz_point(3D cloud) failed")
    if dvz.dvz_visual_set_data_many(
        points,
        {
            "position": np.array(positions, dtype=np.float32),
            "color": np.array(colors, dtype=np.uint8),
            "diameter_px": np.array(diameters, dtype=np.float32),
        },
    ) != 0:
        raise RuntimeError("dvz_visual_set_data_many(3D cloud) failed")
    ex.set_filled_point_style(points)
    ex.add_visual(panel, points)
    return points


def _add_specimen_animation(scene, specimen_cloud, state: ScalebarMeasurementState) -> None:
    rotation_desc = dvz.dvz_track_rotation_desc()
    rotation_desc.axis[:] = (0.0, 1.0, 0.0)
    rotation_desc.speed_rad_per_sec = 1.0
    rotation = dvz.dvz_track_rotation(ctypes.byref(rotation_desc))
    if not rotation:
        raise RuntimeError("dvz_track_rotation() failed")
    state.specimen_rotation = rotation

    transform_desc = dvz.dvz_transform_motion_desc()
    transform_desc.rotation = rotation
    animation = dvz.dvz_anim_visual_transform(scene, specimen_cloud, ctypes.byref(transform_desc))
    if not animation:
        raise RuntimeError("dvz_anim_visual_transform() failed")
    state.specimen_animation = animation
    if dvz.dvz_anim_set_speed(animation, ROTATION_SPEED_RAD_PER_SEC) != 0:
        raise RuntimeError("dvz_anim_set_speed(specimen) failed")
    if dvz.dvz_anim_start(animation, 0.0) != 0:
        raise RuntimeError("dvz_anim_start(specimen) failed")


def _build_scene():
    scene = dvz.dvz_scene()
    if not scene:
        raise RuntimeError("dvz_scene() failed")
    figure = dvz.dvz_figure(scene, ex.WIDTH, ex.HEIGHT, 0)
    if not figure:
        raise RuntimeError("dvz_figure() failed")
    grid = dvz.dvz_figure_grid(figure, 2, 2)
    if not grid:
        raise RuntimeError("dvz_figure_grid() failed")

    margins = dvz.DvzPanelReserve(36.0, 30.0, 24.0, 30.0)
    if dvz.dvz_grid_set_margins(grid, ctypes.byref(margins)) != 0:
        raise RuntimeError("dvz_grid_set_margins() failed")
    if dvz.dvz_grid_set_gutter(grid, 28.0, 26.0) != 0:
        raise RuntimeError("dvz_grid_set_gutter() failed")

    overview = dvz.dvz_grid_panel_span(grid, 0, 0, 2, 1)
    detail = dvz.dvz_grid_panel(grid, 0, 1)
    specimen = dvz.dvz_grid_panel(grid, 1, 1)
    if not overview or not detail or not specimen:
        raise RuntimeError("dvz_grid_panel() failed")

    for panel in (overview, detail, specimen):
        _configure_panel(panel)

    _set_domain(overview, 0.0, 12.0, 0.0, 8.0)
    _set_domain(detail, 3.10, 5.10, 2.25, 3.65)

    overview_pixels = _field_texture(OVERVIEW_WIDTH, OVERVIEW_HEIGHT, detail=False)
    detail_pixels = _field_texture(DETAIL_WIDTH, DETAIL_HEIGHT, detail=True)
    _add_image(scene, overview, overview_pixels, 0.0, 12.0, 0.0, 8.0)
    _add_image(scene, detail, detail_pixels, 3.10, 5.10, 2.25, 3.65)
    _add_zoom_box(scene, overview)
    _add_detail_points(scene, detail)
    specimen_cloud = _add_3d_cloud(scene, specimen)

    _add_panel_scalebar(
        overview,
        dvz.DVZ_SCENE_ANCHOR_BOTTOM_LEFT,
        ex.CYAN,
        dvz.DVZ_SCALEBAR_LABEL_ABOVE,
    )
    _add_panel_scalebar(
        detail,
        dvz.DVZ_SCENE_ANCHOR_BOTTOM_RIGHT,
        ex.GREEN,
        dvz.DVZ_SCALEBAR_LABEL_ABOVE,
    )
    _add_world_scalebar(specimen)

    state = ScalebarMeasurementState()
    _add_specimen_animation(scene, specimen_cloud, state)
    return scene, figure, (overview, detail, specimen), state


def _configure_view(view, scene, panels, state: ScalebarMeasurementState) -> None:
    overview, detail, specimen = panels
    ex.bind_panzoom(view, scene, overview, dvz.DVZ_DIM_MASK_XY)
    ex.bind_panzoom(view, scene, detail, dvz.DVZ_DIM_MASK_XY)

    controller = dvz.dvz_arcball(scene, None)
    if not controller:
        raise RuntimeError("dvz_arcball() failed")
    if dvz.dvz_view_bind_controller(view, specimen, controller, dvz.DVZ_DIM_MASK_XYZ) != 0:
        raise RuntimeError("dvz_view_bind_controller(arcball) failed")
    arcball = dvz.dvz_controller_arcball(controller)
    if not arcball:
        raise RuntimeError("dvz_controller_arcball() failed")
    if dvz.dvz_arcball_set(arcball, (ctypes.c_float * 3)(0.56, -0.18, 0.30)) != 0:
        raise RuntimeError("dvz_arcball_set() failed")
    if state.specimen_animation and dvz.dvz_anim_set_interaction_policy(
        state.specimen_animation,
        controller,
        dvz.DVZ_ANIM_INTERACTION_PAUSE,
        0.0,
    ) != 0:
        raise RuntimeError("dvz_anim_set_interaction_policy() failed")


def _run(scene, figure, panels, state: ScalebarMeasurementState) -> None:
    app = dvz.dvz_app(scene)
    if not app:
        dvz.dvz_scene_destroy(scene)
        state.destroy_tracks()
        raise RuntimeError("dvz_app() failed")
    try:
        view = dvz.dvz_view_window(app, figure, ex.WIDTH, ex.HEIGHT, b"Scale Bar Measurement")
        if not view:
            raise RuntimeError("dvz_view_window() failed")
        _configure_view(view, scene, panels, state)
        ex.run_app(app, view)
    finally:
        dvz.dvz_app_destroy(app)
        dvz.dvz_scene_destroy(scene)
        state.destroy_tracks()


def main() -> None:
    scene, figure, panels, state = _build_scene()
    _run(scene, figure, panels, state)


if __name__ == "__main__":
    main()
/*
 * Copyright (c) 2021 Cyrille Rossant and contributors. All rights reserved.
 * Licensed under the MIT license. See LICENSE file in the project root for details.
 * SPDX-License-Identifier: MIT
 */

/* scalebar_measurement - This example compares scale bars across overview, detail, and 3D views.
 *
 * What to look for: the overview and detail panels use synthetic microscopy-like RGBA textures,
 * zoom boxes, point markers, labels, and independent scale bars, while the 3D panel shows a small
 * rotating specimen cloud with its own measurement cue. Compare the scale labels and view extents:
 * the same visual language explains both image pixels and scene-space geometry.
 *
 * This workflow is useful for scientific figures where readers must understand physical size while
 * moving between context, zoomed detail, and a 3D measurement.
 *
 * Scenario: showcases_scalebar_measurement
 * Style: showcase workflow, graphite_cyan, 1280x720 window target
 *
 * Build:  just example-c showcases/scalebar_measurement
 * Run:    ./build/examples/c/showcases/scalebar_measurement --live
 * Smoke:  ./build/examples/c/showcases/scalebar_measurement --png
 */



/*************************************************************************************************/
/*  Includes                                                                                     */
/*************************************************************************************************/

#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

#include "_alloc.h"
#include "_assertions.h"
#include "datoviz/scene.h"
#include "example_common.h"
#include "example_style.h"
#include "runner/scenario_runner.h"



/*************************************************************************************************/
/*  Forward declarations                                                                         */
/*************************************************************************************************/

DvzScenarioSpec dvz_showcase_scalebar_measurement_scenario(void);



/*************************************************************************************************/
/*  Constants                                                                                    */
/*************************************************************************************************/

#define WIDTH  EXAMPLE_WINDOW_WIDTH
#define HEIGHT EXAMPLE_WINDOW_HEIGHT
#define OVERVIEW_WIDTH   224u
#define OVERVIEW_HEIGHT  160u
#define DETAIL_WIDTH     180u
#define DETAIL_HEIGHT    136u
#define DETAIL_POINTS    120u
#define ZOOM_BOX_SEG     4u
#define CLOUD_COUNT      125u
#define ROTATION_SPEED_RAD_PER_SEC 0.35f

static const float TAU = 6.28318530718f;



/*************************************************************************************************/
/*  Structs                                                                                      */
/*************************************************************************************************/

typedef struct ScalebarMeasurementState
{
    DvzTrack* specimen_rotation;
    DvzAnimation* specimen_animation;
} ScalebarMeasurementState;



/*************************************************************************************************/
/*  Helpers                                                                                      */
/*************************************************************************************************/

/**
 * Convert a normalized float channel to an 8-bit channel.
 *
 * @param value normalized channel value
 * @return clamped 8-bit channel
 */
static uint8_t _u8(float value)
{
    if (value < 0.0f)
        value = 0.0f;
    if (value > 1.0f)
        value = 1.0f;
    return (uint8_t)(255.0f * value + 0.5f);
}



/**
 * Return a deterministic synthetic microscopy-like scalar sample.
 *
 * @param x normalized X coordinate
 * @param y normalized Y coordinate
 * @param detail whether to emphasize the zoomed field
 * @return normalized sample value
 */
static float _sample_field(float x, float y, bool detail)
{
    float value = 0.10f + 0.10f * sinf(TAU * (2.2f * x + 0.7f * y));
    value += 0.06f * cosf(TAU * (0.4f * x - 3.8f * y));

    for (uint32_t i = 0; i < 9u; i++)
    {
        const float fi = (float)i;
        const float cx = fmodf(0.17f + 0.137f * fi, 0.96f);
        const float cy = fmodf(0.21f + 0.219f * fi, 0.94f);
        const float dx = x - cx;
        const float dy = y - cy;
        const float sigma = detail ? 0.030f + 0.004f * (float)(i % 3u) : 0.044f;
        const float d2 = (dx * dx + 1.35f * dy * dy) / (2.0f * sigma * sigma);
        value += (0.33f + 0.05f * (float)(i % 4u)) * expf(-d2);
    }

    return fminf(fmaxf(value, 0.0f), 1.0f);
}



/**
 * Fill one RGBA field texture with the shared graphite/cyan release palette.
 *
 * @param pixels output RGBA8 texture
 * @param width texture width
 * @param height texture height
 * @param detail whether to use the zoomed-field variant
 */
static void _fill_field_texture(uint8_t* pixels, uint32_t width, uint32_t height, bool detail)
{
    ANN(pixels);

    for (uint32_t y = 0; y < height; y++)
    {
        for (uint32_t x = 0; x < width; x++)
        {
            const float u = width > 1u ? (float)x / (float)(width - 1u) : 0.0f;
            const float v = height > 1u ? (float)y / (float)(height - 1u) : 0.0f;
            const float sample = _sample_field(u, v, detail);
            const float ridge = 0.5f + 0.5f * sinf(TAU * (7.0f * u + 2.4f * v));
            const uint32_t k = 4u * (y * width + x);

            pixels[k + 0] = _u8(0.07f + 0.16f * sample + 0.05f * ridge);
            pixels[k + 1] = _u8(0.12f + 0.55f * sample + 0.06f * ridge);
            pixels[k + 2] = _u8(0.16f + 0.74f * sample);
            pixels[k + 3] = 255u;
        }
    }
}



/**
 * Copy one Datoviz color into a scale-bar descriptor array.
 *
 * @param out output RGBA8 array
 * @param color source color
 * @param alpha alpha channel override
 */
static void _copy_color(uint8_t out[4], DvzColor color, uint8_t alpha)
{
    ANN(out);
    out[0] = color.r;
    out[1] = color.g;
    out[2] = color.b;
    out[3] = alpha;
}



/**
 * Configure a panel with the shared feature-example background.
 *
 * @param panel target panel
 * @return true when panel layout was configured
 */
static bool _configure_panel(DvzPanel* panel)
{
    ANN(panel);
    example_graphite_cyan_set_panel_background(panel);
    return true;
}



/**
 * Set one panel data domain.
 *
 * @param panel target panel
 * @param xmin X minimum
 * @param xmax X maximum
 * @param ymin Y minimum
 * @param ymax Y maximum
 * @return true when both domain calls succeed
 */
static bool _set_domain(DvzPanel* panel, double xmin, double xmax, double ymin, double ymax)
{
    ANN(panel);
    int rc = dvz_panel_set_domain(panel, DVZ_DIM_X, xmin, xmax);
    if (rc != 0)
        return false;
    rc = dvz_panel_set_domain(panel, DVZ_DIM_Y, ymin, ymax);
    return rc == 0;
}



/**
 * Add an image visual whose corners are specified in panel data coordinates.
 *
 * @param scene scene owning the visual
 * @param panel panel receiving the visual
 * @param pixels RGBA8 texture pixels
 * @param width texture width
 * @param height texture height
 * @param xmin data-domain X minimum
 * @param xmax data-domain X maximum
 * @param ymin data-domain Y minimum
 * @param ymax data-domain Y maximum
 * @return true when the image was added
 */
static bool _add_image(
    DvzScene* scene, DvzPanel* panel, uint8_t* pixels, uint32_t width, uint32_t height,
    float xmin, float xmax, float ymin, float ymax)
{
    ANN(scene);
    ANN(panel);
    ANN(pixels);

    vec3 data_positions[4] = {
        {xmin, ymin, 0.0f},
        {xmin, ymax, 0.0f},
        {xmax, ymin, 0.0f},
        {xmax, ymax, 0.0f},
    };
    vec2 texcoords[4] = {
        {0.0f, 0.0f},
        {0.0f, 1.0f},
        {1.0f, 0.0f},
        {1.0f, 1.0f},
    };

    DvzVisual* image = dvz_image(scene, 0);
    if (image == NULL)
        return false;
    if (dvz_visual_set_data(image, "position", data_positions, 4) != 0)
        return false;
    if (dvz_visual_set_data(image, "texcoords", texcoords, 4) != 0)
        return false;
    if (!example_visual_set_rgba8_field(scene, image, "field", (const uint8_t*)pixels, width,
                                        height, NULL))
        return false;
    if (dvz_visual_set_depth_test(image, false) != 0)
        return false;
    return dvz_panel_add_visual(panel, image, NULL) == 0;
}



/**
 * Add a zoom-box overlay to the overview panel.
 *
 * @param scene scene owning the visual
 * @param panel panel receiving the visual
 * @return true when the overlay was added
 */
static bool _add_zoom_box(DvzScene* scene, DvzPanel* panel)
{
    ANN(scene);
    ANN(panel);

    vec3 starts[ZOOM_BOX_SEG] = {
        {3.10f, 2.25f, 0.0f},
        {5.10f, 2.25f, 0.0f},
        {5.10f, 3.65f, 0.0f},
        {3.10f, 3.65f, 0.0f},
    };
    vec3 ends[ZOOM_BOX_SEG] = {
        {5.10f, 2.25f, 0.0f},
        {5.10f, 3.65f, 0.0f},
        {3.10f, 3.65f, 0.0f},
        {3.10f, 2.25f, 0.0f},
    };
    DvzColor colors[ZOOM_BOX_SEG] = {{0}};
    float widths[ZOOM_BOX_SEG] = {0};
    DvzColor accent = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_SECONDARY);
    for (uint32_t i = 0; i < ZOOM_BOX_SEG; i++)
    {
        colors[i] = dvz_color_rgba(accent.r, accent.g, accent.b, 230);
        widths[i] = 2.2f;
    }

    DvzVisual* box = dvz_segment(scene, 0);
    if (box == NULL)
        return false;
    DvzVisualDataUpdate updates[] = {
        {.attr_name = "position_start", .data = starts, .item_count = ZOOM_BOX_SEG},
        {.attr_name = "position_end", .data = ends, .item_count = ZOOM_BOX_SEG},
        {.attr_name = "color", .data = colors, .item_count = ZOOM_BOX_SEG},
        {.attr_name = "stroke_width_px", .data = widths, .item_count = ZOOM_BOX_SEG},
    };
    if (dvz_visual_set_data_many(box, updates, 4) != 0)
        return false;
    if (dvz_segment_set_caps(box, DVZ_SEGMENT_CAP_SQUARE, DVZ_SEGMENT_CAP_SQUARE) != 0)
        return false;
    if (dvz_visual_set_depth_test(box, false) != 0)
        return false;
    return dvz_panel_add_visual(panel, box, NULL) == 0;
}



/**
 * Add deterministic highlighted objects to the detail panel.
 *
 * @param scene scene owning the visual
 * @param panel panel receiving the visual
 * @return true when the points were added
 */
static bool _add_detail_points(DvzScene* scene, DvzPanel* panel)
{
    ANN(scene);
    ANN(panel);

    vec3 data_positions[DETAIL_POINTS] = {{0}};
    DvzColor colors[DETAIL_POINTS] = {{0}};
    float diameters[DETAIL_POINTS] = {0};
    for (uint32_t i = 0; i < DETAIL_POINTS; i++)
    {
        const float t = (float)i / (float)(DETAIL_POINTS - 1u);
        const float a = TAU * (9.0f * t + 0.07f * sinf(21.0f * t));
        const float r = 0.10f + 0.78f * sqrtf(t);
        data_positions[i][0] = 4.10f + 0.54f * r * cosf(a);
        data_positions[i][1] = 2.95f + 0.34f * r * sinf(a);
        data_positions[i][2] = 0.0f;

        const float pulse = sinf(TAU * (3.0f * t + 0.11f));
        colors[i] = dvz_color_rgba(
            _u8(0.20f + 0.20f * t), _u8(0.66f + 0.22f * t),
            _u8(0.76f + 0.12f * pulse * pulse), 210);
        diameters[i] = 4.4f + 5.4f * pulse * pulse;
    }

    DvzVisual* points = dvz_point(scene, 0);
    if (points == NULL)
        return false;
    DvzVisualDataUpdate updates[] = {
        {.attr_name = "position", .data = data_positions, .item_count = DETAIL_POINTS},
        {.attr_name = "color", .data = colors, .item_count = DETAIL_POINTS},
        {.attr_name = "diameter_px", .data = diameters, .item_count = DETAIL_POINTS},
    };
    if (dvz_visual_set_data_many(points, updates, 3) != 0)
        return false;
    if (dvz_visual_set_depth_test(points, false) != 0)
        return false;
    return dvz_panel_add_visual(panel, points, NULL) == 0;
}



/**
 * Add a panel-domain scale bar with consistent release-example styling.
 *
 * @param panel panel receiving the annotation
 * @param anchor scale-bar anchor
 * @param unit base unit label used by the SI-prefix formatter
 * @param data_to_unit factor from panel data units to base units
 * @param color line and label color
 * @param label_position label position
 * @return true when the scale bar was added
 */
static bool _add_panel_scalebar(
    DvzPanel* panel, DvzSceneAnchor anchor, const char* unit, double data_to_unit, DvzColor color,
    DvzScaleBarLabelPosition label_position)
{
    ANN(panel);
    ANN(unit);

    DvzScaleBarDesc desc = {
        DVZ_STRUCT_INIT_FIELDS(DvzScaleBarDesc),
        .dimension = DVZ_DIM_X,
        .anchor = anchor,
        .label_position = label_position,
        .target_length_px = 132.0f,
        .min_length_px = 82.0f,
        .max_length_px = 190.0f,
        .offset_px = {26.0f, 24.0f},
        .tick_length_px = 8.0f,
        .line_width_px = 2.0f,
        .unit = unit,
        .data_to_unit = data_to_unit,
    };
    DvzTextStyle label_style = {
        DVZ_STRUCT_INIT_FIELDS(DvzTextStyle),
        .size_px = 16.0f,
        .renderer = DVZ_TEXT_RENDERER_MSDF_ATLAS,
    };
    _copy_color(desc.line_color, color, 255u);
    _copy_color(label_style.color, color, 255u);
    DvzScaleBar* scalebar = dvz_scale_bar(panel, &desc);
    return scalebar != NULL && dvz_scale_bar_set_label_style(scalebar, &label_style) == 0;
}



/**
 * Add a view-plane 3D scale bar to the specimen panel.
 *
 * @param panel panel receiving the annotation
 * @return true when the scale bar was added
 */
static bool _add_world_scalebar(DvzPanel* panel)
{
    ANN(panel);

    DvzColor color = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_TEXT);
    DvzScaleBarDesc desc = {
        DVZ_STRUCT_INIT_FIELDS(DvzScaleBarDesc),
        .dimension = DVZ_DIM_X,
        .reference_mode = DVZ_SCALEBAR_REFERENCE_VIEW_PLANE,
        .reference_position = {0.0, 0.0, 0.0},
        .anchor = DVZ_SCENE_ANCHOR_BOTTOM_RIGHT,
        .label_position = DVZ_SCALEBAR_LABEL_ABOVE,
        .target_length_px = 122.0f,
        .min_length_px = 78.0f,
        .max_length_px = 176.0f,
        .offset_px = {24.0f, 24.0f},
        .tick_length_px = 8.0f,
        .line_width_px = 2.0f,
        .unit = "m",
        .data_to_unit = 0.001,
    };
    DvzTextStyle label_style = {
        DVZ_STRUCT_INIT_FIELDS(DvzTextStyle),
        .size_px = 16.0f,
        .renderer = DVZ_TEXT_RENDERER_MSDF_ATLAS,
    };
    _copy_color(desc.line_color, color, 255u);
    _copy_color(label_style.color, color, 255u);
    DvzScaleBar* scalebar = dvz_scale_bar(panel, &desc);
    return scalebar != NULL && dvz_scale_bar_set_label_style(scalebar, &label_style) == 0;
}



/**
 * Add a compact 3D bead cloud for the world-referenced scale bar.
 *
 * @param scene scene owning the visual
 * @param panel panel receiving the visual
 * @param out optional created visual output
 * @return true when the visual and camera were configured
 */
static bool _add_3d_cloud(DvzScene* scene, DvzPanel* panel, DvzVisual** out)
{
    ANN(scene);
    ANN(panel);

    DvzCameraDesc camera_desc = dvz_camera_desc();
    camera_desc.view.eye[0] = 0.18f;
    camera_desc.view.eye[1] = -0.10f;
    camera_desc.view.eye[2] = 3.35f;
    camera_desc.view.up[1] = 1.0f;
    camera_desc.projection.fov_y = 0.70f;
    camera_desc.projection.near_clip = 0.1f;
    camera_desc.projection.far_clip = 100.0f;
    if (dvz_panel_set_camera_desc(panel, &camera_desc) != 0)
        return false;

    vec3 positions[CLOUD_COUNT] = {{0}};
    DvzColor colors[CLOUD_COUNT] = {{0}};
    float diameters[CLOUD_COUNT] = {0};
    uint32_t count = 0;
    for (uint32_t z = 0; z < 5u; z++)
    {
        for (uint32_t y = 0; y < 5u; y++)
        {
            for (uint32_t x = 0; x < 5u; x++)
            {
                const float fx = -1.0f + 0.5f * (float)x;
                const float fy = -1.0f + 0.5f * (float)y;
                const float fz = -1.0f + 0.5f * (float)z;
                const float d = sqrtf(fx * fx + fy * fy + fz * fz);
                if (d > 1.24f || count >= CLOUD_COUNT)
                    continue;

                positions[count][0] = fx;
                positions[count][1] = fy;
                positions[count][2] = fz;
                colors[count] = dvz_color_rgba(
                    _u8(0.22f + 0.15f * (float)x / 4.0f),
                    _u8(0.58f + 0.30f * (float)y / 4.0f),
                    _u8(0.84f + 0.12f * (1.0f - (float)z / 4.0f)), 230);
                diameters[count] = 11.0f + 8.0f * (1.24f - d);
                count++;
            }
        }
    }

    DvzVisual* points = dvz_point(scene, 0);
    if (points == NULL)
        return false;
    DvzVisualDataUpdate updates[] = {
        {.attr_name = "position", .data = positions, .item_count = count},
        {.attr_name = "color", .data = colors, .item_count = count},
        {.attr_name = "diameter_px", .data = diameters, .item_count = count},
    };
    if (dvz_visual_set_data_many(points, updates, 3) != 0)
        return false;
    if (dvz_panel_add_visual(panel, points, NULL) != 0)
        return false;
    if (out != NULL)
        *out = points;
    return true;
}



/*************************************************************************************************/
/*  Functions                                                                                    */
/*************************************************************************************************/

/**
 * Initialize the scale-aware measurement workflow scenario.
 *
 * @param ctx scenario context
 * @param out_user scenario state output
 * @return whether initialization succeeded
 */
static bool _scenario_init(DvzScenarioContext* ctx, void** out_user)
{
    if (ctx == NULL)
        return false;
    if (out_user != NULL)
        *out_user = NULL;

    bool ok = false;
    ScalebarMeasurementState* state =
        (ScalebarMeasurementState*)dvz_calloc(1, sizeof(*state));
    if (state == NULL)
        return false;
    if (out_user != NULL)
        *out_user = state;

    uint8_t overview_pixels[OVERVIEW_WIDTH * OVERVIEW_HEIGHT * 4u] = {0};
    uint8_t detail_pixels[DETAIL_WIDTH * DETAIL_HEIGHT * 4u] = {0};

    _fill_field_texture(overview_pixels, OVERVIEW_WIDTH, OVERVIEW_HEIGHT, false);
    _fill_field_texture(detail_pixels, DETAIL_WIDTH, DETAIL_HEIGHT, true);

    ctx->figure = dvz_figure(ctx->scene, ctx->width, ctx->height, 0);
    EXAMPLE_CHECK(ctx->figure != NULL, "dvz_figure() failed");

    DvzGrid* grid = dvz_figure_grid(ctx->figure, 2, 2);
    EXAMPLE_CHECK(grid != NULL, "dvz_figure_grid() failed");
    ok = dvz_grid_set_margins(
             grid, &(DvzPanelReserve){.left_px = 36.0f, .right_px = 30.0f, .top_px = 24.0f,
                                      .bottom_px = 30.0f}) == DVZ_OK;
    EXAMPLE_CHECK(ok, "dvz_grid_set_margins() failed");
    ok = dvz_grid_set_gutter(grid, 28.0f, 26.0f) == DVZ_OK;
    EXAMPLE_CHECK(ok, "dvz_grid_set_gutter() failed");

    DvzPanel* overview = dvz_grid_panel_span(grid, 0, 0, 2, 1);
    DvzPanel* detail = dvz_grid_panel(grid, 0, 1);
    DvzPanel* specimen = dvz_grid_panel(grid, 1, 1);
    EXAMPLE_CHECK(
        overview != NULL && detail != NULL && specimen != NULL, "dvz_grid_panel() failed");

    ok = _configure_panel(overview);
    EXAMPLE_CHECK(ok, "_configure_panel(overview) failed");
    ok = _configure_panel(detail);
    EXAMPLE_CHECK(ok, "_configure_panel(detail) failed");
    example_graphite_cyan_set_panel_background(specimen);

    ok = _set_domain(overview, 0.0, 12.0, 0.0, 8.0);
    EXAMPLE_CHECK(ok, "_set_domain(overview) failed");
    ok = _set_domain(detail, 3.10, 5.10, 2.25, 3.65);
    EXAMPLE_CHECK(ok, "_set_domain(detail) failed");

    ok = _add_image(
        ctx->scene, overview, overview_pixels, OVERVIEW_WIDTH, OVERVIEW_HEIGHT, 0.0f, 12.0f,
        0.0f, 8.0f);
    EXAMPLE_CHECK(ok, "_add_image(overview) failed");
    ok = _add_image(
        ctx->scene, detail, detail_pixels, DETAIL_WIDTH, DETAIL_HEIGHT, 3.10f, 5.10f, 2.25f,
        3.65f);
    EXAMPLE_CHECK(ok, "_add_image(detail) failed");
    ok = _add_zoom_box(ctx->scene, overview);
    EXAMPLE_CHECK(ok, "_add_zoom_box() failed");
    ok = _add_detail_points(ctx->scene, detail);
    EXAMPLE_CHECK(ok, "_add_detail_points() failed");
    DvzVisual* specimen_cloud = NULL;
    ok = _add_3d_cloud(ctx->scene, specimen, &specimen_cloud);
    EXAMPLE_CHECK(ok, "_add_3d_cloud() failed");

    DvzColor primary = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY);
    DvzColor secondary = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_SECONDARY);
    ok = _add_panel_scalebar(
        overview, DVZ_SCENE_ANCHOR_BOTTOM_LEFT, "m", 0.001, primary,
        DVZ_SCALEBAR_LABEL_ABOVE);
    EXAMPLE_CHECK(ok, "_add_panel_scalebar(overview) failed");
    ok = _add_panel_scalebar(
        detail, DVZ_SCENE_ANCHOR_BOTTOM_RIGHT, "m", 0.001, secondary,
        DVZ_SCALEBAR_LABEL_ABOVE);
    EXAMPLE_CHECK(ok, "_add_panel_scalebar(detail) failed");
    ok = _add_world_scalebar(specimen);
    EXAMPLE_CHECK(ok, "_add_world_scalebar() failed");

    DvzPanzoom* overview_panzoom =
        dvz_scenario_panzoom(ctx, overview, NULL, DVZ_DIM_MASK_XY);
    EXAMPLE_CHECK(overview_panzoom != NULL, "failed to bind overview panzoom controller");
    DvzPanzoom* detail_panzoom =
        dvz_scenario_panzoom(ctx, detail, NULL, DVZ_DIM_MASK_XY);
    EXAMPLE_CHECK(detail_panzoom != NULL, "failed to bind detail panzoom controller");

    DvzController* arcball_controller = dvz_arcball(ctx->scene, NULL);
    EXAMPLE_CHECK(arcball_controller != NULL, "dvz_arcball() failed");
    DvzArcball* arcball = dvz_controller_arcball(arcball_controller);
    EXAMPLE_CHECK(arcball != NULL, "failed to bind arcball controller");
    EXAMPLE_CHECK(
        dvz_scenario_bind_controller(ctx, specimen, arcball_controller, DVZ_DIM_MASK_XYZ) == 0,
        "dvz_scenario_bind_controller() failed");
    dvz_arcball_set(arcball, (vec3){+0.56f, -0.18f, +0.30f});

    DvzTrackRotationDesc rotation_desc = dvz_track_rotation_desc();
    rotation_desc.axis[1] = 1.0f;
    rotation_desc.speed_rad_per_sec = 1.0f;
    state->specimen_rotation = dvz_track_rotation(&rotation_desc);
    EXAMPLE_CHECK(state->specimen_rotation != NULL, "dvz_track_rotation(specimen) failed");
    DvzTransformMotionDesc transform_desc = dvz_transform_motion_desc();
    transform_desc.rotation = state->specimen_rotation;
    state->specimen_animation =
        dvz_anim_visual_transform(ctx->scene, specimen_cloud, &transform_desc);
    EXAMPLE_CHECK(
        state->specimen_animation != NULL, "dvz_anim_visual_transform(specimen) failed");
    dvz_anim_set_interaction_policy(
        state->specimen_animation, arcball_controller, DVZ_ANIM_INTERACTION_PAUSE, 0.0);
    dvz_anim_set_speed(state->specimen_animation, ROTATION_SPEED_RAD_PER_SEC);
    dvz_anim_start(state->specimen_animation, 0.0);

    ok = true;
cleanup:
    return ok;
}



/**
 * Destroy the scale-aware measurement workflow scenario state.
 *
 * @param ctx scenario context
 * @param user scenario state
 */
static void _scenario_destroy(DvzScenarioContext* ctx, void* user)
{
    (void)ctx;
    ScalebarMeasurementState* state = (ScalebarMeasurementState*)user;
    if (state == NULL)
        return;
    dvz_track_destroy(state->specimen_rotation);
    dvz_free(state);
}



DvzScenarioSpec dvz_showcase_scalebar_measurement_scenario(void)
{
    return (DvzScenarioSpec){
        .id = "showcases_scalebar_measurement",
        .title = "Scale Bar Measurement Workflow",
        .width = WIDTH,
        .height = HEIGHT,
        .fps = 60.0,
        .requirements = DVZ_SCENARIO_REQ_MESH_VISUAL | DVZ_SCENARIO_REQ_IMAGE_VISUAL |
                        DVZ_SCENARIO_REQ_TEXT_VISUAL | DVZ_SCENARIO_REQ_CONTROLLER |
                        DVZ_SCENARIO_REQ_PANZOOM | DVZ_SCENARIO_REQ_ARCBALL |
                        DVZ_SCENARIO_REQ_FRAME_CALLBACKS,
        .init = _scenario_init,
        .destroy = _scenario_destroy,
    };
}



/**
 * Run the scale-aware measurement workflow through the native scenario runner.
 *
 * @param argc command-line argument count
 * @param argv command-line argument vector
 * @return process exit code
 */
#ifndef DVZ_EXAMPLE_NO_MAIN
int main(int argc, char** argv)
{
    DvzScenarioSpec spec = dvz_showcase_scalebar_measurement_scenario();
    return dvz_scenario_run_native_cli(&spec, argc, argv) == 0 ? 0 : 1;
}
#endif
Example details

Tags

workflow, scale-bar, measurement, synthetic

Data

Field Value
kind synthetic