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
- ID:
showcases_scalebar_measurement - Category:
showcase - Lane:
showcases - Status:
supported - Source:
examples/c/showcases/scalebar_measurement.c - Approved adaptation starter:
no - Python source:
examples/python/gallery/showcases/scalebar_measurement.py - Python adaptation: Available
- Browser support: Live in browser
- WebGPU live route:
examples/webgpu/live.html?id=showcases_scalebar_measurement - Browser capability tags:
scale-bar,text,panel-grid,mesh,panzoom - Validation:
smoke+screenshot
Tags
workflow, scale-bar, measurement, synthetic
Data
| Field | Value |
|---|---|
kind |
synthetic |
