Skip to content

Image Probe

This example shows probing a scalar image at a marked data position.

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 features/image_probe (build and run), or rerun ./build/examples/c/features/image_probe
Python Available; direct-engine adaptation python3 -m examples.python.gallery.features.image_probe
Browser Live WebGPU route Open live example

This example is approved as a starting point for user code and coding agents. Keep the object lifetimes and data shapes intact while adapting the data and styling.

What To Look For

A synthetic microscopy-like float field is uploaded as an R32 sampled image and mapped through a scalar color scale. The image visual enables pixel-query capability, and segment plus point visuals draw a crosshair and dot at the probe location. Move the probe in the live preview and compare the in-scene value readout with bright and dim image regions; this is useful for inspecting raw scalar values behind a heat map or image without changing the displayed field on desktop and browser hosts.

Source

#!/usr/bin/env python3
"""Scalar image probing with a live marker and query readback."""

from __future__ import annotations

import ctypes
import math

import numpy as np

import datoviz as dvz

from examples.python.gallery import common as ex


FIELD_WIDTH = 256
FIELD_HEIGHT = 192
PROBE_X = 0.68
PROBE_Y = 0.56
PROBE_REQUEST_ID = 1
PROBE_RING_SEGMENTS = 28
PROBE_SEGMENTS = PROBE_RING_SEGMENTS + 4
TAU = 2.0 * math.pi


def _clamp01(value: float) -> float:
    return min(max(value, 0.0), 1.0)


def _sample_field(x: float, y: float) -> float:
    value = 0.11 + 0.05 * math.sin(TAU * (2.3 * x + 0.35 * y))
    value += 0.04 * math.cos(TAU * (0.55 * x - 3.6 * y))

    filament = math.sin(TAU * (x * 1.15 + 0.22 * math.sin(TAU * y)))
    value += 0.18 * math.exp(-18.0 * (filament - 0.18) * (filament - 0.18))

    centers = (
        (0.16, 0.22, 0.050),
        (0.31, 0.71, 0.042),
        (0.46, 0.38, 0.035),
        (0.58, 0.84, 0.040),
        (0.70, 0.56, 0.038),
        (0.78, 0.24, 0.046),
        (0.86, 0.69, 0.035),
        (0.24, 0.50, 0.030),
    )
    for i, (cx, cy, sigma) in enumerate(centers):
        dx = x - cx
        dy = y - cy
        d2 = (dx * dx + 1.4 * dy * dy) / (2.0 * sigma * sigma)
        value += (0.20 + 0.06 * (i % 3)) * math.exp(-d2)

    hot_dx = x - 0.69
    value += (
        0.30
        * math.exp(-(hot_dx * hot_dx) / (2.0 * 0.115 * 0.115))
        * (0.78 + 0.22 * math.cos(TAU * (y - 0.48)))
    )

    hot_dy = y - 0.57
    value += 0.50 * math.exp(
        -(hot_dx * hot_dx + hot_dy * hot_dy) / (2.0 * 0.022 * 0.022)
    )

    mirror_dy = y - 0.43
    value += 0.42 * math.exp(
        -(hot_dx * hot_dx + mirror_dy * mirror_dy) / (2.0 * 0.024 * 0.024)
    )
    return _clamp01(value)


def _probe_field() -> np.ndarray:
    x = np.linspace(0.0, 1.0, FIELD_WIDTH, dtype=np.float32)
    y = np.linspace(0.0, 1.0, FIELD_HEIGHT, dtype=np.float32)
    u, v = np.meshgrid(x, y)

    values = 0.11 + 0.05 * np.sin(TAU * (2.3 * u + 0.35 * v))
    values += 0.04 * np.cos(TAU * (0.55 * u - 3.6 * v))

    filament = np.sin(TAU * (u * 1.15 + 0.22 * np.sin(TAU * v)))
    values += 0.18 * np.exp(-18.0 * (filament - 0.18) * (filament - 0.18))

    centers = np.array(
        [
            [0.16, 0.22, 0.050],
            [0.31, 0.71, 0.042],
            [0.46, 0.38, 0.035],
            [0.58, 0.84, 0.040],
            [0.70, 0.56, 0.038],
            [0.78, 0.24, 0.046],
            [0.86, 0.69, 0.035],
            [0.24, 0.50, 0.030],
        ],
        dtype=np.float32,
    )
    for i, (cx, cy, sigma) in enumerate(centers):
        dx = u - cx
        dy = v - cy
        d2 = (dx * dx + 1.4 * dy * dy) / (2.0 * sigma * sigma)
        values += (0.20 + 0.06 * (i % 3)) * np.exp(-d2)

    hot_dx = u - 0.69
    values += (
        0.30
        * np.exp(-(hot_dx * hot_dx) / (2.0 * 0.115 * 0.115))
        * (0.78 + 0.22 * np.cos(TAU * (v - 0.48)))
    )

    hot_dy = v - 0.57
    values += 0.50 * np.exp(
        -(hot_dx * hot_dx + hot_dy * hot_dy) / (2.0 * 0.022 * 0.022)
    )

    mirror_dy = v - 0.43
    values += 0.42 * np.exp(
        -(hot_dx * hot_dx + mirror_dy * mirror_dy) / (2.0 * 0.024 * 0.024)
    )
    return np.clip(values, 0.0, 1.0).astype(np.float32)


def _set_probe_domain(panel) -> None:
    if dvz.dvz_panel_set_domain(panel, dvz.DVZ_DIM_X, 0.0, 1.0) != 0:
        raise RuntimeError("dvz_panel_set_domain(X) failed")
    if dvz.dvz_panel_set_domain(panel, dvz.DVZ_DIM_Y, 0.0, 1.0) != 0:
        raise RuntimeError("dvz_panel_set_domain(Y) failed")


def _add_probe_image(scene, panel, values: np.ndarray):
    image = dvz.dvz_image(scene, 0)
    if not image:
        raise RuntimeError("dvz_image() failed")

    positions = np.array(
        [
            [0.0, 0.0, 0.0],
            [0.0, 1.0, 0.0],
            [1.0, 0.0, 0.0],
            [1.0, 1.0, 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,
    )

    scale = ex.continuous_scale(scene, b"python_image_probe")
    field = dvz.dvz_sampled_field_from_array(scene, values)
    if dvz.dvz_visual_set_data_many(
        image,
        {
            "position": positions,
            "texcoords": texcoords,
        },
    ) != 0:
        raise RuntimeError("dvz_visual_set_data_many(image) failed")
    if dvz.dvz_visual_set_scale(image, b"color", scale) != 0:
        raise RuntimeError("dvz_visual_set_scale() failed")
    if dvz.dvz_visual_set_field(image, b"field", field) != 0:
        raise RuntimeError("dvz_visual_set_field() failed")
    if dvz.dvz_visual_set_depth_test(image, False) != 0:
        raise RuntimeError("dvz_visual_set_depth_test(image) failed")
    dvz.dvz_visual_set_query_capabilities(image, dvz.DVZ_QUERY_CAPABILITY_PIXEL)
    ex.add_visual(panel, image)
    return image


def _plot_size(panel) -> tuple[float, float]:
    rect = dvz.DvzRect()
    if (
        dvz.dvz_panel_plot_rect_px(panel, ctypes.byref(rect))
        and rect.width > 0.0
        and rect.height > 0.0
    ):
        return float(rect.width), float(rect.height)
    return float(ex.WIDTH), float(ex.HEIGHT)


def _probe_marker_arrays(panel, x: float, y: float):
    plot_width, plot_height = _plot_size(panel)
    starts = np.zeros((PROBE_SEGMENTS, 3), dtype=np.float32)
    ends = np.zeros((PROBE_SEGMENTS, 3), dtype=np.float32)
    colors = np.zeros((PROBE_SEGMENTS, 4), dtype=np.uint8)
    widths = np.zeros(PROBE_SEGMENTS, dtype=np.float32)

    gap_x = 6.0 / plot_width
    gap_y = 6.0 / plot_height
    arm_x = 20.0 / plot_width
    arm_y = 20.0 / plot_height

    starts[:4] = np.array(
        [
            [x - arm_x, y, 0.02],
            [x + gap_x, y, 0.02],
            [x, y - arm_y, 0.02],
            [x, y + gap_y, 0.02],
        ],
        dtype=np.float32,
    )
    ends[:4] = np.array(
        [
            [x - gap_x, y, 0.02],
            [x + arm_x, y, 0.02],
            [x, y - gap_y, 0.02],
            [x, y + arm_y, 0.02],
        ],
        dtype=np.float32,
    )

    cyan = np.array([ex.CYAN.r, ex.CYAN.g, ex.CYAN.b, 245], dtype=np.uint8)
    colors[:4] = cyan
    widths[:4] = 1.8

    rx = 12.0 / plot_width
    ry = 12.0 / plot_height
    for i in range(PROBE_RING_SEGMENTS):
        k = i + 4
        a0 = TAU * i / PROBE_RING_SEGMENTS
        a1 = TAU * (i + 1) / PROBE_RING_SEGMENTS
        starts[k] = (x + rx * math.cos(a0), y + ry * math.sin(a0), 0.02)
        ends[k] = (x + rx * math.cos(a1), y + ry * math.sin(a1), 0.02)
        colors[k] = (ex.CYAN.r, ex.CYAN.g, ex.CYAN.b, 225)
        widths[k] = 1.7

    return starts, ends, colors, widths


def _add_probe_marker(scene, panel):
    starts, ends, colors, widths = _probe_marker_arrays(panel, PROBE_X, PROBE_Y)
    segments = dvz.dvz_segment(scene, 0)
    if not segments:
        raise RuntimeError("dvz_segment() failed")
    if dvz.dvz_visual_set_data_many(
        segments,
        {
            "position_start": starts,
            "position_end": ends,
            "color": colors,
            "stroke_width_px": widths,
        },
    ) != 0:
        raise RuntimeError("dvz_visual_set_data_many(segments) failed")
    if dvz.dvz_segment_set_caps(
        segments, dvz.DVZ_SEGMENT_CAP_ROUND, dvz.DVZ_SEGMENT_CAP_ROUND
    ) != 0:
        raise RuntimeError("dvz_segment_set_caps() failed")
    if dvz.dvz_visual_set_depth_test(segments, False) != 0:
        raise RuntimeError("dvz_visual_set_depth_test(segments) failed")
    ex.add_visual(panel, segments)

    dot = dvz.dvz_point(scene, 0)
    if not dot:
        raise RuntimeError("dvz_point() failed")
    if dvz.dvz_visual_set_data_many(
        dot,
        {
            "position": np.array([[PROBE_X, PROBE_Y, 0.03]], dtype=np.float32),
            "color": ex.color_array(ex.CYAN),
            "diameter_px": np.array([6.0], dtype=np.float32),
        },
    ) != 0:
        raise RuntimeError("dvz_visual_set_data_many(dot) failed")
    ex.set_filled_point_style(dot)
    if dvz.dvz_visual_set_depth_test(dot, False) != 0:
        raise RuntimeError("dvz_visual_set_depth_test(dot) failed")
    ex.add_visual(panel, dot)
    return segments, dot


def _update_probe_marker(panel, segments, dot, x: float, y: float) -> None:
    starts, ends, _colors, _widths = _probe_marker_arrays(panel, x, y)
    if dvz.dvz_visual_set_data_many(
        segments,
        {
            "position_start": starts,
            "position_end": ends,
        },
    ) != 0:
        raise RuntimeError("dvz_visual_set_data_many(update segments) failed")
    if dvz.dvz_visual_set_data(
        dot, b"position", np.array([[x, y, 0.03]], dtype=np.float32)
    ) != 0:
        raise RuntimeError("dvz_visual_set_data(update dot) failed")


def _resolve_probe_value(panel, query) -> float | None:
    if query.status != dvz.DVZ_QUERY_STATUS_HIT or not query.hit:
        return None
    data = ex.panel_px_to_data(panel, query.panel_position[0], query.panel_position[1])
    if data is None:
        return None
    return _sample_field(data[0], data[1])


def main() -> None:
    scene, figure, panel = ex.scene_panel()
    _set_probe_domain(panel)

    values = _probe_field()
    _add_probe_image(scene, panel, values)
    probe_segments, probe_dot = _add_probe_marker(scene, panel)

    state = {
        "cursor": None,
        "last_hit": None,
        "last_value": None,
    }

    def set_probe_from_data(x: float, y: float) -> None:
        _update_probe_marker(panel, probe_segments, probe_dot, x, y)
        src = (ctypes.c_double * 2)(float(x), float(y))
        dst = (ctypes.c_double * 2)()
        ok = dvz.dvz_panel_transform_point(
            panel,
            dvz.DVZ_PANEL_COORD_DATA,
            dvz.DVZ_PANEL_COORD_PANEL_PX,
            src,
            dst,
        )
        if ok:
            state["cursor"] = (float(dst[0]), float(dst[1]))

    def on_pointer(event) -> None:
        if event.type not in (
            dvz.DVZ_POINTER_EVENT_MOVE,
            dvz.DVZ_POINTER_EVENT_PRESS,
            dvz.DVZ_POINTER_EVENT_CLICK,
        ):
            return
        panel_pos = ex.figure_to_panel_px(panel, event.pos[0], event.pos[1])
        if panel_pos is None:
            return
        data = ex.panel_px_to_data(panel, panel_pos[0], panel_pos[1])
        if data is None:
            return
        x = _clamp01(data[0])
        y = _clamp01(data[1])
        _update_probe_marker(panel, probe_segments, probe_dot, x, y)
        state["cursor"] = panel_pos

    def on_frame(_view, frame_index: int, _elapsed: float) -> None:
        if frame_index == 0 and state["cursor"] is None:
            set_probe_from_data(PROBE_X, PROBE_Y)
        cursor = state["cursor"]
        if cursor is not None:
            ex.queue_panel_query(
                panel,
                cursor[0],
                cursor[1],
                PROBE_REQUEST_ID,
                dvz.DVZ_SCENE_TARGET_PIXEL,
            )

        for query in ex.poll_queries(scene):
            if query.request_id != PROBE_REQUEST_ID:
                continue
            value = _resolve_probe_value(panel, query)
            hit = value is not None
            last_value = state["last_value"]
            changed = state["last_hit"] is None or state["last_hit"] != hit
            if hit and (last_value is None or abs(value - last_value) >= 1e-3):
                changed = True
            if changed:
                if hit:
                    print(f"probe value={value:.3f}")
                    state["last_value"] = value
                else:
                    print("probe miss")
                    state["last_value"] = None
                state["last_hit"] = hit

    ex.run_with_input_callbacks(
        scene,
        figure,
        "Image Probe",
        on_pointer=on_pointer,
        on_frame=on_frame,
    )


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
 */

/* image_probe - This example shows probing a scalar image at a marked data position.
 *
 * Scenario: features_image_probe
 * Style: features, graphite_cyan, 1280x720 window target
 *
 * Build:  just example-c features/image_probe
 * Run:    ./build/examples/c/features/image_probe --live
 * Smoke:  ./build/examples/c/features/image_probe --png
 *
 * What to look for: a synthetic microscopy-like float field is uploaded as an R32 sampled image and
 * mapped through a scalar color scale. The image visual enables pixel-query capability, and segment
 * plus point visuals draw a crosshair and dot at the probe location. Move the probe in the live
 * preview and compare the in-scene value readout with bright and dim image regions; this is useful
 * for inspecting raw scalar values behind a heat map or image without changing the displayed
 * field on desktop and browser hosts.
 */



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

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

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



DvzScenarioSpec dvz_example_image_probe_scenario(void);


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

#define WIDTH  EXAMPLE_WINDOW_WIDTH
#define HEIGHT EXAMPLE_WINDOW_HEIGHT
#define FIELD_WIDTH         256u
#define FIELD_HEIGHT        192u
#define PROBE_X             0.68f
#define PROBE_Y             0.56f
#define PROBE_REQUEST_ID    1u
#define PROBE_RING_SEGMENTS 28u
#define PROBE_SEGMENTS      (PROBE_RING_SEGMENTS + 4u)

static const float TAU = 6.28318530718f;



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

typedef struct ImageProbeState ImageProbeState;

struct ImageProbeState
{
    DvzScene* scene;
    DvzPanel* panel;
    DvzVisual* probe_segments;
    DvzVisual* probe_dot;
    DvzOverlayCard* readout;
    float* values;
    bool cursor_valid;
    double cursor_x;
    double cursor_y;
    double last_value;
    bool last_hit;
    bool has_last_result;
};



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

/**
 * Clamp a float to the unit interval.
 *
 * @param value input value
 * @return clamped value
 */
static float _clamp01(float value)
{
    if (value < 0.0f)
        return 0.0f;
    if (value > 1.0f)
        return 1.0f;
    return value;
}



/**
 * Return a deterministic synthetic microscopy-like scalar sample.
 *
 * @param x normalized X coordinate
 * @param y normalized Y coordinate
 * @return normalized sample value
 */
static float _sample_field(float x, float y)
{
    float value = 0.11f + 0.05f * sinf(TAU * (2.3f * x + 0.35f * y));
    value += 0.04f * cosf(TAU * (0.55f * x - 3.6f * y));

    const float filament = sinf(TAU * (x * 1.15f + 0.22f * sinf(TAU * y)));
    value += 0.18f * expf(-18.0f * (filament - 0.18f) * (filament - 0.18f));

    const float centers[8][3] = {
        {0.16f, 0.22f, 0.050f}, {0.31f, 0.71f, 0.042f}, {0.46f, 0.38f, 0.035f},
        {0.58f, 0.84f, 0.040f}, {0.70f, 0.56f, 0.038f}, {0.78f, 0.24f, 0.046f},
        {0.86f, 0.69f, 0.035f}, {0.24f, 0.50f, 0.030f},
    };
    for (uint32_t i = 0; i < 8u; i++)
    {
        const float dx = x - centers[i][0];
        const float dy = y - centers[i][1];
        const float sigma = centers[i][2];
        const float d2 = (dx * dx + 1.4f * dy * dy) / (2.0f * sigma * sigma);
        value += (0.20f + 0.06f * (float)(i % 3u)) * expf(-d2);
    }

    const float hot_dx = x - 0.69f;
    value +=
        0.30f * expf(-(hot_dx * hot_dx) / (2.0f * 0.115f * 0.115f)) *
        (0.78f + 0.22f * cosf(TAU * (y - 0.48f)));

    const float hot_dy = y - 0.57f;
    value += 0.50f * expf(-(hot_dx * hot_dx + hot_dy * hot_dy) / (2.0f * 0.022f * 0.022f));

    const float mirror_dy = y - 0.43f;
    value += 0.42f * expf(-(hot_dx * hot_dx + mirror_dy * mirror_dy) / (2.0f * 0.024f * 0.024f));

    return _clamp01(value);
}



/**
 * Fill the probe image scalar field.
 *
 * @param values output normalized scalar field
 */
static void _fill_probe_field(float values[FIELD_WIDTH * FIELD_HEIGHT])
{
    ANN(values);

    for (uint32_t y = 0; y < FIELD_HEIGHT; y++)
    {
        for (uint32_t x = 0; x < FIELD_WIDTH; x++)
        {
            const float u = FIELD_WIDTH > 1u ? (float)x / (float)(FIELD_WIDTH - 1u) : 0.0f;
            const float v = FIELD_HEIGHT > 1u ? (float)y / (float)(FIELD_HEIGHT - 1u) : 0.0f;
            values[y * FIELD_WIDTH + x] = _sample_field(u, v);
        }
    }
}



/**
 * Set a normalized data domain on the image panel.
 *
 * @param panel target panel
 * @return true when both domain calls succeed
 */
static bool _set_probe_domain(DvzPanel* panel)
{
    ANN(panel);
    int rc = dvz_panel_set_domain(panel, DVZ_DIM_X, 0.0, 1.0);
    if (rc != 0)
        return false;
    rc = dvz_panel_set_domain(panel, DVZ_DIM_Y, 0.0, 1.0);
    return rc == 0;
}



/**
 * Add the scalar image visual and enable pixel-query readback.
 *
 * @param scene scene owning the visual
 * @param panel panel receiving the visual
 * @param scale color scale bound to the scalar image
 * @param values scalar field values
 * @return true when the image was added
 */
static bool _add_probe_image(
    DvzScene* scene, DvzPanel* panel, DvzScale* scale, float values[FIELD_WIDTH * FIELD_HEIGHT])
{
    ANN(scene);
    ANN(panel);
    ANN(scale);
    ANN(values);

    vec3 data_positions[4] = {
        {0.0f, 0.0f, 0.0f},
        {0.0f, 1.0f, 0.0f},
        {1.0f, 0.0f, 0.0f},
        {1.0f, 1.0f, 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 (dvz_visual_set_scale(image, "color", scale) != 0)
        return false;

    DvzSampledField* field = dvz_sampled_field(
        scene, &(DvzSampledFieldDesc){DVZ_STRUCT_INIT_FIELDS(DvzSampledFieldDesc),
                   .dim = DVZ_FIELD_DIM_2D,
                   .format = DVZ_FIELD_FORMAT_R32_FLOAT,
                   .semantic = DVZ_FIELD_SEMANTIC_SCALAR,
                   .width = FIELD_WIDTH,
                   .height = FIELD_HEIGHT,
                   .depth = 1,
               });
    if (field == NULL)
        return false;
    if (dvz_sampled_field_set_data(
            field, &(DvzFieldDataView){DVZ_STRUCT_INIT_FIELDS(DvzFieldDataView),
                       .data = values,
                       .bytes_per_row = FIELD_WIDTH * sizeof(float),
                       .rows_per_image = FIELD_HEIGHT,
                   }) != DVZ_OK)
    {
        return false;
    }
    if (dvz_visual_set_field(image, "field", field) != DVZ_OK)
        return false;
    if (dvz_visual_set_depth_test(image, false) != 0)
        return false;
    dvz_visual_set_query_capabilities(image, DVZ_QUERY_CAPABILITY_PIXEL);
    return dvz_panel_add_visual(panel, image, NULL) == 0;
}



/**
 * Fill data-space crosshair and ring segments around a probe point.
 *
 * @param panel target panel
 * @param x normalized probe X coordinate
 * @param y normalized probe Y coordinate
 * @param starts output segment starts
 * @param ends output segment ends
 * @param colors output segment colors
 * @param widths output segment widths
 * @return true when marker geometry was filled
 */
static bool _fill_probe_marker(
    DvzPanel* panel, float x, float y, vec3 starts[PROBE_SEGMENTS], vec3 ends[PROBE_SEGMENTS],
    DvzColor colors[PROBE_SEGMENTS], float widths[PROBE_SEGMENTS])
{
    ANN(panel);
    ANN(starts);
    ANN(ends);
    ANN(colors);
    ANN(widths);

    DvzRect plot = {0};
    if (!dvz_panel_plot_rect_px(panel, &plot) || plot.width <= 0.0f || plot.height <= 0.0f)
        return false;

    const DvzColor cyan = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY);
    const float gap_x = 6.0f / plot.width;
    const float gap_y = 6.0f / plot.height;
    const float arm_x = 20.0f / plot.width;
    const float arm_y = 20.0f / plot.height;
    const vec3 cross_starts[4] = {
        {x - arm_x, y, 0.02f},
        {x + gap_x, y, 0.02f},
        {x, y - arm_y, 0.02f},
        {x, y + gap_y, 0.02f},
    };
    const vec3 cross_ends[4] = {
        {x - gap_x, y, 0.02f},
        {x + arm_x, y, 0.02f},
        {x, y - gap_y, 0.02f},
        {x, y + arm_y, 0.02f},
    };

    for (uint32_t i = 0; i < 4u; i++)
    {
        starts[i][0] = cross_starts[i][0];
        starts[i][1] = cross_starts[i][1];
        starts[i][2] = cross_starts[i][2];
        ends[i][0] = cross_ends[i][0];
        ends[i][1] = cross_ends[i][1];
        ends[i][2] = cross_ends[i][2];
        colors[i] = cyan;
        colors[i].a = 245u;
        widths[i] = 1.8f;
    }

    const float rx = 12.0f / plot.width;
    const float ry = 12.0f / plot.height;
    for (uint32_t i = 0; i < PROBE_RING_SEGMENTS; i++)
    {
        const uint32_t k = i + 4u;
        const float a0 = TAU * (float)i / (float)PROBE_RING_SEGMENTS;
        const float a1 = TAU * (float)(i + 1u) / (float)PROBE_RING_SEGMENTS;
        starts[k][0] = x + rx * cosf(a0);
        starts[k][1] = y + ry * sinf(a0);
        starts[k][2] = 0.02f;
        ends[k][0] = x + rx * cosf(a1);
        ends[k][1] = y + ry * sinf(a1);
        ends[k][2] = 0.02f;
        colors[k] = cyan;
        colors[k].a = 225u;
        widths[k] = 1.7f;
    }
    return true;
}



/**
 * Update the live probe marker to a normalized data position.
 *
 * @param state image probe example state
 * @param x normalized probe X coordinate
 * @param y normalized probe Y coordinate
 */
static void _update_probe_marker(ImageProbeState* state, float x, float y)
{
    if (state == NULL || state->panel == NULL || state->probe_segments == NULL ||
        state->probe_dot == NULL)
        return;

    DvzPanel* panel = state->panel;
    ANN(panel);

    vec3 starts[PROBE_SEGMENTS] = {{0}};
    vec3 ends[PROBE_SEGMENTS] = {{0}};
    DvzColor colors[PROBE_SEGMENTS] = {{0}};
    float widths[PROBE_SEGMENTS] = {0};
    if (!_fill_probe_marker(panel, x, y, starts, ends, colors, widths))
        return;

    DvzVisualDataUpdate segment_updates[] = {
        {.attr_name = "position_start", .data = starts, .item_count = PROBE_SEGMENTS},
        {.attr_name = "position_end", .data = ends, .item_count = PROBE_SEGMENTS},
    };
    if (dvz_visual_set_data_many(state->probe_segments, segment_updates, 2) != 0)
        return;

    vec3 dot_data[1] = {{x, y, 0.03f}};
    (void)dvz_visual_set_data(state->probe_dot, "position", dot_data, 1);
}



/**
 * Update the live probe marker from a panel-local cursor position.
 *
 * @param state image probe example state
 */
static void _update_probe_marker_from_cursor(ImageProbeState* state)
{
    if (state == NULL || !state->cursor_valid)
        return;

    double data[2] = {0};
    if (!dvz_panel_position_to_data(
            state->panel, DVZ_PANEL_COORD_PANEL_PX,
            (const double[2]){state->cursor_x, state->cursor_y}, data))
    {
        return;
    }
    _update_probe_marker(state, (float)data[0], (float)data[1]);
}



/**
 * Add the visible live probe marker.
 *
 * @param scene scene owning the visual
 * @param panel panel receiving the visual
 * @param out_segments output segment visual
 * @param out_dot output center dot visual
 * @return true when the marker was added
 */
static bool _add_probe_marker(
    DvzScene* scene, DvzPanel* panel, DvzVisual** out_segments, DvzVisual** out_dot)
{
    ANN(scene);
    ANN(panel);
    ANN(out_segments);
    ANN(out_dot);

    vec3 starts[PROBE_SEGMENTS] = {{0}};
    vec3 ends[PROBE_SEGMENTS] = {{0}};
    DvzColor colors[PROBE_SEGMENTS] = {{0}};
    float widths[PROBE_SEGMENTS] = {0};
    if (!_fill_probe_marker(panel, PROBE_X, PROBE_Y, starts, ends, colors, widths))
        return false;

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

    vec3 dot_data[1] = {{PROBE_X, PROBE_Y, 0.03f}};

    DvzVisual* dot = dvz_point(scene, 0);
    if (dot == NULL)
        return false;
    DvzColor dot_color[1] = {example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY)};
    dot_color[0].a = 245u;
    float dot_diameter[1] = {6.0f};
    DvzVisualDataUpdate dot_updates[] = {
        {.attr_name = "position", .data = dot_data, .item_count = 1},
        {.attr_name = "color", .data = dot_color, .item_count = 1},
        {.attr_name = "diameter_px", .data = dot_diameter, .item_count = 1},
    };
    if (dvz_visual_set_data_many(dot, dot_updates, 3) != 0)
        return false;
    DvzPointStyleDesc point_style = dvz_point_style_desc();
    point_style.aspect = DVZ_SHAPE_ASPECT_FILLED;
    point_style.stroke_width_px = 0.0f;
    if (dvz_point_set_style(dot, &point_style) != 0)
        return false;
    if (dvz_visual_set_depth_test(dot, false) != 0)
        return false;
    if (dvz_panel_add_visual(panel, dot, NULL) != 0)
        return false;

    *out_segments = marker;
    *out_dot = dot;
    return true;
}



/**
 * Create the shared scalar color scale.
 *
 * @param scene scene owning scale resources
 * @return created scale, or NULL on failure
 */
static DvzScale* _add_probe_scale(DvzScene* scene)
{
    ANN(scene);

    DvzScale* scale = dvz_scale(
        scene, &(DvzScaleDesc){DVZ_STRUCT_INIT_FIELDS(DvzScaleDesc),
                   .kind = DVZ_SCALE_CONTINUOUS,
                   .label = "intensity",
               });
    if (scale == NULL)
        return NULL;
    dvz_scale_set_format(
        scale, &(DvzFormatDesc){DVZ_STRUCT_INIT_FIELDS(DvzFormatDesc),
                   .precision = 2,
                   .trim_trailing_zeros = true});
    dvz_scale_set_domain(scale, 0.0, 1.0);
    dvz_scale_set_view_range(scale, 0.0, 1.0);

    DvzColormap* colormap = example_graphite_cyan_colormap(scene);
    if (colormap == NULL)
        return NULL;
    dvz_scale_set_colormap(scale, colormap);
    return scale;
}



/**
 * Resolve one scalar probe value from a query result.
 *
 * @param state image probe example state
 * @param query query result
 * @param out_value output scalar value
 * @return true when a scalar value was resolved
 */
static bool
_query_probe_value(ImageProbeState* state, const DvzQueryResult* query, double* out_value)
{
    if (state == NULL || query == NULL || out_value == NULL)
        return false;
    if (query->status != DVZ_QUERY_STATUS_HIT || !query->hit)
        return false;

    double data[2] = {0};
    if (!dvz_panel_position_to_data(
            state->panel, DVZ_PANEL_COORD_PANEL_PX, query->panel_position, data))
    {
        return false;
    }
    *out_value = (double)_sample_field((float)data[0], (float)data[1]);
    return true;
}



/**
 * Return whether a query result differs enough from the last printed value.
 *
 * @param state image query example state
 * @param query query result to compare
 * @return true when the result should be printed
 */
static bool _query_changed(ImageProbeState* state, const DvzQueryResult* query)
{
    if (state == NULL || query == NULL)
        return false;
    if (!state->has_last_result || state->last_hit != query->hit)
        return true;
    if (!query->hit)
        return false;

    double value = 0.0;
    if (!_query_probe_value(state, query, &value))
        return true;
    double delta = value - state->last_value;
    if (delta < 0.0)
        delta = -delta;
    return delta >= 1e-3;
}



/**
 * Remember the last printed query result.
 *
 * @param state image query example state
 * @param query query result to store
 */
static void _store_query_result(ImageProbeState* state, const DvzQueryResult* query)
{
    if (state == NULL || query == NULL)
        return;

    state->has_last_result = true;
    state->last_hit = query->hit;
    double value = 0.0;
    if (_query_probe_value(state, query, &value))
        state->last_value = value;
}



/**
 * Queue one pixel query at the latest probe position.
 *
 * @param state image query example state
 */
static void _queue_probe(ImageProbeState* state)
{
    if (state == NULL || !state->cursor_valid)
        return;

    DvzQueryRequest request = dvz_query_request();
    request.request_id = PROBE_REQUEST_ID;
    request.target = DVZ_SCENE_TARGET_PIXEL;

    const int rc = dvz_panel_query_px(state->panel, state->cursor_x, state->cursor_y, &request);
    if (rc != 0)
        dvz_fprintf(stderr, "dvz_panel_query_px() failed\n");
}



/**
 * Add the cross-platform scalar probe readout.
 *
 * @param panel panel owning the overlay card
 * @return created card, or NULL on failure
 */
static DvzOverlayCard* _add_probe_readout(DvzPanel* panel)
{
    ANN(panel);
    DvzOverlay* overlay = dvz_overlay(panel, 0);
    if (overlay == NULL)
        return NULL;

    DvzOverlayCardDesc desc = dvz_overlay_card_desc();
    desc.text = "Value: move the pointer over the image";
    desc.placement = DVZ_OVERLAY_CARD_PLACEMENT_TOP_RIGHT;
    desc.offset_px[0] = 24.0f;
    desc.offset_px[1] = 24.0f;
    DvzOverlayCard* card = dvz_overlay_card(overlay, &desc);
    if (card == NULL)
        return NULL;

    DvzOverlayCardStyle style = dvz_overlay_card_style();
    DvzColor background = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_PANEL_BG);
    background.a = 238u;
    style.background_color = background;
    style.text_color = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_TEXT);
    style.padding_px[0] = 16.0f;
    style.padding_px[1] = 10.0f;
    style.min_width_px = 350.0f;
    style.height_px = 46.0f;
    style.glyph_advance_px = 8.8f;
    style.text_size_px = 18.0f;
    style.text_renderer = DVZ_TEXT_RENDERER_MSDF_ATLAS;
    style.max_text_chars = 96u;
    return dvz_overlay_card_set_style(card, &style) == DVZ_OK ? card : NULL;
}



/*************************************************************************************************/
/*  Callbacks                                                                                    */
/*************************************************************************************************/

/**
 * Record the latest cursor position and move the live probe marker.
 *
 * @param event portable pointer event
 * @param user_data image probe example state
 */
static void _image_probe_pointer(const DvzScenarioPointerEvent* event, void* user_data)
{
    ImageProbeState* state = (ImageProbeState*)user_data;
    if (state == NULL || event == NULL)
        return;
    if (event->type != DVZ_SCENARIO_POINTER_MOVE && event->type != DVZ_SCENARIO_POINTER_CLICK)
        return;

    double data[2] = {0};
    double panel_px[2] = {0};
    state->cursor_valid =
        dvz_panel_transform_point(
            state->panel, DVZ_PANEL_COORD_FIGURE_PX, DVZ_PANEL_COORD_PANEL_PX,
            (const double[2]){event->x, event->y}, panel_px) &&
        dvz_panel_position_to_data(state->panel, DVZ_PANEL_COORD_PANEL_PX, panel_px, data);
    if (state->cursor_valid)
    {
        state->cursor_x = panel_px[0];
        state->cursor_y = panel_px[1];
    }
    _update_probe_marker_from_cursor(state);
}



/**
 * Poll image query results and queue the next probe.
 *
 * @param ctx scenario context
 * @param user_data image probe example state
 */
static void _image_probe_post_frame(DvzScenarioContext* ctx, void* user_data)
{
    (void)ctx;
    ImageProbeState* state = (ImageProbeState*)user_data;
    if (state == NULL)
        return;

    DvzQueryResult query = {0};
    while (dvz_scene_poll_query(state->scene, &query))
    {
        if (!_query_changed(state, &query))
            continue;

        double value = 0.0;
        if (_query_probe_value(state, &query, &value))
        {
            char readout[128] = {0};
            dvz_snprintf(
                readout, sizeof(readout), "Value: %.3f   Position: %.1f, %.1f px", value,
                query.panel_position[0], query.panel_position[1]);
            (void)dvz_overlay_card_set_text(state->readout, readout);
            dvz_fprintf(
                stdout, "probe value=%0.3f panel=(%0.1f,%0.1f)\n", value,
                query.panel_position[0], query.panel_position[1]);
        }
        else
        {
            char readout[96] = {0};
            dvz_snprintf(
                readout, sizeof(readout), "Value: outside image   Position: %.1f, %.1f px",
                query.panel_position[0], query.panel_position[1]);
            (void)dvz_overlay_card_set_text(state->readout, readout);
            dvz_fprintf(
                stdout, "probe miss panel=(%0.1f,%0.1f)\n", query.panel_position[0],
                query.panel_position[1]);
        }
        _store_query_result(state, &query);
    }

    _queue_probe(state);
}



/**
 * Handle portable scenario events.
 *
 * @param ctx scenario context
 * @param event portable event
 * @param user scenario state
 */
static void _scenario_event(DvzScenarioContext* ctx, const DvzScenarioEvent* event, void* user)
{
    (void)ctx;
    if (event == NULL)
        return;
    if (event->kind == DVZ_SCENARIO_EVENT_POINTER)
        _image_probe_pointer(&event->content.pointer, user);
}



/*************************************************************************************************/
/*  Scenario callbacks                                                                           */
/*************************************************************************************************/

/**
 * Initialize the scalar image probe feature scenario.
 *
 * @param ctx scenario context
 * @param out_user scenario state output
 * @return true on success
 */
static bool _scenario_init(DvzScenarioContext* ctx, void** out_user)
{
    if (ctx == NULL)
        return false;
    if (out_user != NULL)
        *out_user = NULL;

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

    state->values = (float*)dvz_calloc(FIELD_WIDTH * FIELD_HEIGHT, sizeof(*state->values));
    if (state->values == NULL)
        return false;
    _fill_probe_field(state->values);

    ctx->figure = dvz_figure(ctx->scene, ctx->width, ctx->height, 0);
    if (ctx->figure == NULL)
        return false;

    DvzPanel* panel = dvz_panel_full(ctx->figure);
    if (panel == NULL)
        return false;

    bool ok = _set_probe_domain(panel);
    if (!ok)
        return false;

    example_graphite_cyan_set_panel_background(panel);

    DvzScale* scale = _add_probe_scale(ctx->scene);
    if (scale == NULL)
        return false;

    ok = _add_probe_image(ctx->scene, panel, scale, state->values);
    if (!ok)
        return false;

    DvzVisual* probe_segments = NULL;
    DvzVisual* probe_dot = NULL;
    ok = _add_probe_marker(ctx->scene, panel, &probe_segments, &probe_dot);
    if (!ok)
        return false;

    double initial_probe_px[2] = {0};
    if (!dvz_panel_data_to_position(
            panel, DVZ_PANEL_COORD_PANEL_PX, (const double[2]){PROBE_X, PROBE_Y},
            initial_probe_px))
    {
        return false;
    }

    state->scene = ctx->scene;
    state->panel = panel;
    state->probe_segments = probe_segments;
    state->probe_dot = probe_dot;
    state->readout = _add_probe_readout(panel);
    if (state->readout == NULL)
        return false;
    state->cursor_valid = true;
    state->cursor_x = initial_probe_px[0];
    state->cursor_y = initial_probe_px[1];
    return true;
}



/**
 * Destroy the scalar image probe feature scenario state.
 *
 * @param ctx scenario context
 * @param user scenario state
 */
static void _scenario_destroy(DvzScenarioContext* ctx, void* user)
{
    (void)ctx;
    ImageProbeState* state = (ImageProbeState*)user;
    if (state == NULL)
        return;
    dvz_free(state->values);
    dvz_free(state);
}



/**
 * Return the scalar image probe scenario specification.
 *
 * @return scenario specification
 */
DvzScenarioSpec dvz_example_image_probe_scenario(void)
{
    return (DvzScenarioSpec){
        .id = "features_image_probe",
        .title = "Image Probe",
        .width = WIDTH,
        .height = HEIGHT,
        .fps = 60.0,
        .requirements = DVZ_SCENARIO_REQ_IMAGE_VISUAL | DVZ_SCENARIO_REQ_TEXT_VISUAL |
                        DVZ_SCENARIO_REQ_QUERY_READBACK | DVZ_SCENARIO_REQ_FRAME_CALLBACKS,
        .init = _scenario_init,
        .event = _scenario_event,
        .post_frame = _image_probe_post_frame,
        .destroy = _scenario_destroy,
    };
}



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

/**
 * Run the scalar image probe feature example 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_example_image_probe_scenario();
    return dvz_scenario_run_native_cli(&spec, argc, argv) == 0 ? 0 : 1;
}
#endif
Example details

Data

Field Value
kind synthetic