Picking¶
This example demonstrates item-level marker hover and click selection.
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/picking (build and run), or rerun ./build/examples/c/features/picking |
| Python | Available; direct-engine adaptation | python3 -m examples.python.gallery.features.picking |
| 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¶
The marker grid uploads position, color, diameter_px, angle, and shape arrays, including several deliberately overlapping markers near the center. Move the cursor to query the frontmost marker item; hover scales the item, click toggles a persistent warning-color tint, and clicking the background clears selection. Current marker queries use marker-family bounds rather than exact SDF shape discard, which is enough to demonstrate interactive item picking.
Source¶
#!/usr/bin/env python3
"""Marker item picking with hover and click selection."""
from __future__ import annotations
import numpy as np
import datoviz as dvz
from examples.python.gallery import common as ex
GRID_COLS = 9
GRID_ROWS = 6
MARKER_COUNT = GRID_COLS * GRID_ROWS + 5
BASE_SIZE = 34.0
HOVER_SIZE = 50.0
QUERY_HOVER_ID = 1
QUERY_CLICK_ID = 2
def _marker_shape(index: int) -> int:
shapes = (
dvz.DVZ_MARKER_SHAPE_DISC,
dvz.DVZ_MARKER_SHAPE_SQUARE,
dvz.DVZ_MARKER_SHAPE_TRIANGLE,
dvz.DVZ_MARKER_SHAPE_DIAMOND,
dvz.DVZ_MARKER_SHAPE_CROSS,
dvz.DVZ_MARKER_SHAPE_RING,
)
return shapes[index % len(shapes)]
def _marker_palette(index: int):
palette = (ex.CYAN, ex.GREEN, ex.TEXT)
color = palette[index % len(palette)]
return color.r, color.g, color.b, 245
def _marker_data():
positions = np.zeros((MARKER_COUNT, 3), dtype=np.float32)
colors = np.zeros((MARKER_COUNT, 4), dtype=np.uint8)
diameters = np.zeros(MARKER_COUNT, dtype=np.float32)
angles = np.zeros(MARKER_COUNT, dtype=np.float32)
shapes = np.zeros(MARKER_COUNT, dtype=np.uint32)
for row in range(GRID_ROWS):
for col in range(GRID_COLS):
index = row * GRID_COLS + col
positions[index] = (
-0.88 + 1.76 * (col / (GRID_COLS - 1)),
-0.72 + 1.44 * (row / (GRID_ROWS - 1)),
0.0,
)
colors[index] = _marker_palette(index)
diameters[index] = BASE_SIZE
angles[index] = ((index % 12) / 12.0) * 2.0 * np.pi
shapes[index] = _marker_shape(index)
for i in range(5):
index = GRID_COLS * GRID_ROWS + i
positions[index] = (-0.05 + 0.025 * i, -0.02 + 0.020 * (i % 3), 0.0)
colors[index] = (ex.CYAN.r, ex.CYAN.g, ex.CYAN.b, 245)
diameters[index] = BASE_SIZE
angles[index] = 0.25 * np.pi * i
shapes[index] = _marker_shape(index + 2)
return positions, colors, diameters, angles.astype(np.float32), shapes
def _add_marker_grid(scene, panel):
marker = dvz.dvz_marker(scene, 0)
if not marker:
raise RuntimeError("dvz_marker() failed")
dvz.dvz_visual_set_query_capabilities(marker, dvz.DVZ_QUERY_CAPABILITY_ITEM)
style = dvz.dvz_marker_style()
style.aspect = dvz.DVZ_SHAPE_ASPECT_OUTLINE
style.edge_color = dvz.DvzColor(ex.TEXT.r, ex.TEXT.g, ex.TEXT.b, 210)
style.stroke_width_px = 2.0
if dvz.dvz_marker_set_style(marker, style) != 0:
raise RuntimeError("dvz_marker_set_style() failed")
positions, colors, diameters, angles, shapes = _marker_data()
if dvz.dvz_visual_set_data_many(
marker,
{
"position": positions,
"color": colors,
"diameter_px": diameters,
"angle": angles,
"shape": shapes,
},
) != 0:
raise RuntimeError("dvz_visual_set_data_many(marker) failed")
ex.add_visual(panel, marker)
return marker
def _handle_hover(state, query) -> None:
if not state["cursor_valid"]:
return
if ex.query_item_hit(query, dvz.DVZ_SCENE_VISUAL_FAMILY_MARKER, MARKER_COUNT):
latest = state["latest_hover_query"]
same_item = (
latest is not None
and latest.visual_id == query.visual_id
and latest.resolved_id == query.resolved_id
)
if not same_item:
if dvz.dvz_hover_apply_query(state["hover"], query) != 0:
raise RuntimeError("dvz_hover_apply_query() failed")
print(f"hover marker id={query.resolved_id}")
state["latest_hover_query"] = query
state["has_hover_query"] = True
else:
if state["has_hover_query"]:
dvz.dvz_hover_clear(state["hover"])
state["latest_hover_query"] = None
state["has_hover_query"] = False
def _handle_click(state, query) -> None:
if ex.query_item_hit(query, dvz.DVZ_SCENE_VISUAL_FAMILY_MARKER, MARKER_COUNT):
if dvz.dvz_selection_apply_query(state["selection"], query) != 0:
raise RuntimeError("dvz_selection_apply_query() failed")
print(f"toggle marker id={query.resolved_id}")
else:
dvz.dvz_selection_clear(state["selection"])
def main() -> None:
scene, figure, panel = ex.scene_panel()
_add_marker_grid(scene, panel)
selection = ex.create_item_selection(scene)
hover = ex.create_item_hover(scene, HOVER_SIZE / BASE_SIZE)
state = {
"selection": selection,
"hover": hover,
"cursor_valid": False,
"has_hover_query": False,
"latest_hover_query": None,
}
def configure_view(view) -> None:
ex.bind_panzoom(view, scene, panel, dvz.DVZ_DIM_MASK_XY)
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])
state["cursor_valid"] = panel_pos is not None
if panel_pos is None:
if state["has_hover_query"]:
dvz.dvz_hover_clear(hover)
state["has_hover_query"] = False
if event.type in (dvz.DVZ_POINTER_EVENT_PRESS, dvz.DVZ_POINTER_EVENT_CLICK):
dvz.dvz_selection_clear(selection)
return
if event.type == dvz.DVZ_POINTER_EVENT_MOVE:
ex.queue_panel_query(
panel,
panel_pos[0],
panel_pos[1],
QUERY_HOVER_ID,
dvz.DVZ_SCENE_TARGET_ITEM,
hit_policy=dvz.DVZ_QUERY_HIT_FRONTMOST,
)
elif event.button == dvz.DVZ_POINTER_BUTTON_LEFT:
ex.queue_panel_query(
panel,
panel_pos[0],
panel_pos[1],
QUERY_CLICK_ID,
dvz.DVZ_SCENE_TARGET_ITEM,
hit_policy=dvz.DVZ_QUERY_HIT_FRONTMOST,
)
def on_frame(_view, _frame_index: int, _elapsed: float) -> None:
for query in ex.poll_queries(scene):
if query.request_id == QUERY_CLICK_ID:
_handle_click(state, query)
elif query.request_id == QUERY_HOVER_ID:
_handle_hover(state, query)
ex.run_with_input_callbacks(
scene,
figure,
"Picking",
on_pointer=on_pointer,
on_frame=on_frame,
configure_view=configure_view,
)
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
*/
/* This example demonstrates item-level marker hover and click selection.
*
* What to look for: the marker grid uploads position, color, diameter_px, angle, and shape arrays,
* including several deliberately overlapping markers near the center. Move the cursor to query the
* frontmost marker item; hover scales the item, click toggles a persistent warning-color tint, and
* clicking the background clears selection. Current marker queries use marker-family bounds rather
* than exact SDF shape discard, which is enough to demonstrate interactive item picking.
*
* Scenario: features_picking
* Style: features, graphite_cyan, 1280x720 window target
*
* Build: just example-c features/picking
* Run: ./build/examples/c/features/picking --live
* Smoke: ./build/examples/c/features/picking --png
*/
#include <math.h>
#include <stdbool.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include "_alloc.h"
#include "datoviz/scene.h"
#include "example_style.h"
#include "runner/scenario_runner.h"
DvzScenarioSpec dvz_example_picking_scenario(void);
/*************************************************************************************************/
/* Constants */
/*************************************************************************************************/
#define WIDTH EXAMPLE_WINDOW_WIDTH
#define HEIGHT EXAMPLE_WINDOW_HEIGHT
#define GRID_COLS 9
#define GRID_ROWS 6
#define MARKER_COUNT (GRID_COLS * GRID_ROWS + 5)
#define BASE_SIZE 34.0f
#define HOVER_SIZE 50.0f
#define QUERY_HOVER_ID 1u
#define QUERY_CLICK_ID 2u
#define PI_F 3.14159265358979323846f
/*************************************************************************************************/
/* Structs */
/*************************************************************************************************/
typedef struct PickingState PickingState;
struct PickingState
{
DvzScene* scene;
DvzPanel* panel;
DvzSelection* selection;
DvzHover* hover;
DvzQueryResult latest_hover_query;
bool has_hover_query;
bool cursor_valid;
};
/*************************************************************************************************/
/* Helpers */
/*************************************************************************************************/
/**
* Return a deterministic mixed marker shape.
*
* @param index marker index
* @return marker shape enum value
*/
static uint32_t _marker_shape(uint32_t index)
{
switch (index % 6u)
{
case 1:
return DVZ_MARKER_SHAPE_SQUARE;
case 2:
return DVZ_MARKER_SHAPE_TRIANGLE;
case 3:
return DVZ_MARKER_SHAPE_DIAMOND;
case 4:
return DVZ_MARKER_SHAPE_CROSS;
case 5:
return DVZ_MARKER_SHAPE_RING;
default:
return DVZ_MARKER_SHAPE_DISC;
}
}
/**
* Return one deterministic marker color from the shared graphite-cyan palette.
*
* @param index marker index
* @return marker color
*/
static DvzColor _marker_palette_color(uint32_t index)
{
const ExampleStyleColorRole roles[] = {
EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY,
EXAMPLE_STYLE_COLOR_ACCENT_SECONDARY,
EXAMPLE_STYLE_COLOR_TEXT,
};
DvzColor color = example_graphite_cyan_color(roles[index % DVZ_ARRAY_COUNT(roles)]);
color.a = 245u;
return color;
}
/**
* Toggle retained selection for one queried marker.
*
* @param state marker-pick example state
* @param query marker item query result
*/
static void _toggle_marker_selection(PickingState* state, const DvzQueryResult* query)
{
if (state == NULL || query == NULL)
return;
if (
query->status != DVZ_QUERY_STATUS_HIT || !query->hit ||
query->visual_family != DVZ_SCENE_VISUAL_FAMILY_MARKER ||
query->resolved_target != DVZ_SCENE_TARGET_ITEM || query->resolved_id >= MARKER_COUNT)
return;
if (dvz_selection_apply_query(state->selection, query) != 0)
fprintf(stderr, "dvz_selection_apply_query() failed\n");
fprintf(stdout, "toggle marker id=%" PRIu64 "\n", query->resolved_id);
}
/*************************************************************************************************/
/* Callbacks */
/*************************************************************************************************/
/**
* Queue hover and click queries from panel-local pointer events.
*
* @param event portable pointer event
* @param user_data marker-pick example state
*/
static void _picking_pointer(const DvzScenarioPointerEvent* event, void* user_data)
{
PickingState* state = (PickingState*)user_data;
if (state == NULL || event == NULL)
return;
if (
event->type != DVZ_SCENARIO_POINTER_MOVE && event->type != DVZ_SCENARIO_POINTER_CLICK)
return;
double x = 0.0;
double y = 0.0;
double panel_pos[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_pos);
x = panel_pos[0];
y = panel_pos[1];
if (!state->cursor_valid)
{
if (state->has_hover_query)
{
dvz_hover_clear(state->hover);
state->has_hover_query = false;
}
if (event->type == DVZ_SCENARIO_POINTER_CLICK && event->button == DVZ_POINTER_BUTTON_LEFT)
dvz_selection_clear(state->selection);
return;
}
DvzQueryRequest request = dvz_query_request();
request.target = DVZ_SCENE_TARGET_ITEM;
request.hit_policy = DVZ_QUERY_HIT_FRONTMOST;
if (event->type == DVZ_SCENARIO_POINTER_MOVE)
{
request.request_id = QUERY_HOVER_ID;
if (dvz_panel_query_px(state->panel, x, y, &request) != 0)
fprintf(stderr, "dvz_panel_query_px() failed\n");
}
else if (event->type == DVZ_SCENARIO_POINTER_CLICK && event->button == DVZ_POINTER_BUTTON_LEFT)
{
request.request_id = QUERY_CLICK_ID;
if (dvz_panel_query_px(state->panel, x, y, &request) != 0)
fprintf(stderr, "dvz_panel_query_px(click) failed\n");
}
}
/**
* Consume marker query results and update hover styling.
*
* @param ctx scenario context
* @param user_data marker-pick example state
*/
static void _picking_post_frame(DvzScenarioContext* ctx, void* user_data)
{
(void)ctx;
PickingState* state = (PickingState*)user_data;
if (state == NULL)
return;
DvzQueryResult query = {0};
while (dvz_scene_poll_query(state->scene, &query))
{
if (query.request_id == QUERY_CLICK_ID)
{
if (
query.status == DVZ_QUERY_STATUS_HIT && query.hit &&
query.visual_family == DVZ_SCENE_VISUAL_FAMILY_MARKER &&
query.resolved_target == DVZ_SCENE_TARGET_ITEM && query.resolved_id < MARKER_COUNT)
{
_toggle_marker_selection(state, &query);
}
else
{
dvz_selection_clear(state->selection);
}
continue;
}
if (query.request_id != QUERY_HOVER_ID)
continue;
if (!state->cursor_valid)
continue;
if (
query.status == DVZ_QUERY_STATUS_HIT && query.hit &&
query.visual_family == DVZ_SCENE_VISUAL_FAMILY_MARKER &&
query.resolved_target == DVZ_SCENE_TARGET_ITEM && query.resolved_id < MARKER_COUNT)
{
bool same_item =
state->latest_hover_query.visual_id == query.visual_id &&
state->latest_hover_query.resolved_id == query.resolved_id;
bool hover_changed = !state->has_hover_query || !same_item;
if (hover_changed && dvz_hover_apply_query(state->hover, &query) != 0)
fprintf(stderr, "dvz_hover_apply_query() failed\n");
if (!same_item)
fprintf(stdout, "hover marker id=%" PRIu64 "\n", query.resolved_id);
state->has_hover_query = true;
state->latest_hover_query = query;
}
else
{
if (state->has_hover_query)
dvz_hover_clear(state->hover);
state->has_hover_query = false;
}
}
}
/**
* 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)
_picking_pointer(&event->content.pointer, user);
}
/*************************************************************************************************/
/* Scenario callbacks */
/*************************************************************************************************/
/**
* Initialize the retained marker picking 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;
PickingState* state = (PickingState*)dvz_calloc(1, sizeof(*state));
if (state == NULL)
return false;
if (out_user != NULL)
*out_user = state;
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;
example_graphite_cyan_set_panel_background(panel);
DvzVisual* visual = dvz_marker(ctx->scene, 0);
if (visual == NULL)
return false;
dvz_visual_set_query_capabilities(visual, DVZ_QUERY_CAPABILITY_ITEM);
DvzMarkerStyle style = dvz_marker_style();
style.aspect = DVZ_SHAPE_ASPECT_OUTLINE;
DvzColor grid = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_GRID);
style.edge_color = dvz_color_rgba(grid.r, grid.g, grid.b, 210);
style.stroke_width_px = 2.0f;
if (dvz_marker_set_style(visual, &style) != 0)
return false;
DvzSelection* selection = dvz_selection(
ctx->scene,
&(DvzSelectionDesc){
DVZ_STRUCT_INIT_FIELDS(DvzSelectionDesc),
.mode = DVZ_SELECT_TOGGLE,
.target = DVZ_SCENE_TARGET_ITEM,
});
if (selection == NULL)
return false;
DvzSelectionVisualStyle selection_style = dvz_selection_visual_style();
selection_style.selected_visual_flags = DVZ_ITEM_STATE_VISUAL_TINT;
selection_style.selected_tint = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_WARNING);
selection_style.selected_tint_mix = 1.0f;
selection_style.unselected_visual_flags = DVZ_ITEM_STATE_VISUAL_NONE;
if (dvz_selection_set_visual_style(selection, &selection_style) != 0)
return false;
DvzHover* hover = dvz_hover(
ctx->scene,
&(DvzHoverDesc){
DVZ_STRUCT_INIT_FIELDS(DvzHoverDesc),
.target = DVZ_SCENE_TARGET_ITEM,
.hit_policy = DVZ_QUERY_HIT_FRONTMOST,
});
if (hover == NULL)
return false;
DvzItemStateVisualStyle hover_style = dvz_item_state_visual_style();
hover_style.visual_flags = DVZ_ITEM_STATE_VISUAL_SCALE;
hover_style.scale = HOVER_SIZE / BASE_SIZE;
if (dvz_hover_set_visual_style(hover, &hover_style) != 0)
return false;
state->scene = ctx->scene;
state->panel = panel;
state->selection = selection;
state->hover = hover;
vec3 positions[MARKER_COUNT] = {0};
DvzColor colors[MARKER_COUNT] = {0};
float diameters[MARKER_COUNT] = {0};
float angles[MARKER_COUNT] = {0};
uint32_t shapes[MARKER_COUNT] = {0};
for (uint32_t row = 0; row < GRID_ROWS; row++)
{
for (uint32_t col = 0; col < GRID_COLS; col++)
{
uint32_t index = row * GRID_COLS + col;
positions[index][0] = -0.88f + 1.76f * ((float)col / (float)(GRID_COLS - 1));
positions[index][1] = -0.72f + 1.44f * ((float)row / (float)(GRID_ROWS - 1));
positions[index][2] = 0.0f;
colors[index] = _marker_palette_color(index);
diameters[index] = BASE_SIZE;
angles[index] = ((float)(index % 12u) / 12.0f) * 2.0f * PI_F;
shapes[index] = _marker_shape(index);
}
}
for (uint32_t i = 0; i < 5; i++)
{
uint32_t index = GRID_COLS * GRID_ROWS + i;
positions[index][0] = -0.05f + 0.025f * (float)i;
positions[index][1] = -0.02f + 0.020f * (float)(i % 3u);
positions[index][2] = 0.0f;
colors[index] = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY);
diameters[index] = BASE_SIZE;
angles[index] = 0.25f * PI_F * (float)i;
shapes[index] = _marker_shape(index + 2u);
}
DvzVisualDataUpdate updates[] = {
{.attr_name = "position", .data = positions, .item_count = MARKER_COUNT},
{.attr_name = "color", .data = colors, .item_count = MARKER_COUNT},
{.attr_name = "diameter_px", .data = diameters, .item_count = MARKER_COUNT},
{.attr_name = "angle", .data = angles, .item_count = MARKER_COUNT},
{.attr_name = "shape", .data = shapes, .item_count = MARKER_COUNT},
};
if (dvz_visual_set_data_many(visual, updates, 5) != 0)
return false;
if (dvz_panel_add_visual(panel, visual, NULL) != 0)
return false;
DvzPanzoom* panzoom = dvz_scenario_panzoom(ctx, panel, NULL, DVZ_DIM_MASK_XY);
return panzoom != NULL;
}
/**
* Destroy the retained marker picking feature scenario state.
*
* @param ctx scenario context
* @param user scenario state
*/
static void _scenario_destroy(DvzScenarioContext* ctx, void* user)
{
(void)ctx;
dvz_free(user);
}
/**
* Return the retained marker picking scenario specification.
*
* @return scenario specification
*/
DvzScenarioSpec dvz_example_picking_scenario(void)
{
return (DvzScenarioSpec){
.id = "features_picking",
.title = "Picking",
.width = WIDTH,
.height = HEIGHT,
.fps = 60.0,
.requirements = DVZ_SCENARIO_REQ_MARKER_VISUAL | DVZ_SCENARIO_REQ_QUERY_READBACK |
DVZ_SCENARIO_REQ_CONTROLLER | DVZ_SCENARIO_REQ_PANZOOM |
DVZ_SCENARIO_REQ_FRAME_CALLBACKS,
.init = _scenario_init,
.event = _scenario_event,
.post_frame = _picking_post_frame,
.destroy = _scenario_destroy,
};
}
/*************************************************************************************************/
/* Functions */
/*************************************************************************************************/
/**
* Run the retained marker picking and selection 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_picking_scenario();
return dvz_scenario_run_native_cli(&spec, argc, argv) == 0 ? 0 : 1;
}
#endif
Example details
- ID:
features_picking - Category:
feature - Lane:
features - Status:
supported - Source:
examples/c/features/picking.c - Approved adaptation starter:
yes - Python source:
examples/python/gallery/features/picking.py - Python adaptation: Available; direct-engine adaptation
- Browser support: Live in browser
- WebGPU live route:
examples/webgpu/live.html?id=features_picking - Browser capability tags:
marker,panzoom,query-readback,frame-callbacks - Validation:
smoke+interaction+screenshot
Data
| Field | Value |
|---|---|
kind |
synthetic |
