diff --git a/python/mujoco/experimental/dear_imgui/dear_imgui.cc b/python/mujoco/experimental/dear_imgui/dear_imgui.cc new file mode 100644 index 00000000..c8f41fb0 --- /dev/null +++ b/python/mujoco/experimental/dear_imgui/dear_imgui.cc @@ -0,0 +1,1233 @@ +// Copyright 2026 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#define NAMESPACE ImGui +#include "dear_imgui_macros.h" +#include +#include +#include +#include +#include +#include + +// NOLINTBEGIN(whitespace/line_length) + +namespace py = pybind11; +using ImString = const char*; +static constexpr const ImVec2 ImVec2_Zero = ImVec2(0.0f, 0.0f); +static constexpr const ImVec2 ImVec2_One = ImVec2(1.0f, 1.0f); +static constexpr const ImVec2 ImVec2_Min_Zero = ImVec2(-FLT_MIN, 0.0f); +static constexpr const ImVec4 ImVec4_Zero = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); +static constexpr const ImVec4 ImVec4_One = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + + +PYBIND11_MODULE(dear_imgui, m) { + // Types. + + py::class_(m, "Vec2") + .def(py::init<>()) + .def(py::init(), py::arg("_x"), py::arg("_y")) + .def_readwrite("x", &ImVec2::x) + .def_readwrite("y", &ImVec2::y); + + py::class_(m, "Vec4") + .def(py::init<>()) + .def(py::init(), py::arg("_x"), py::arg("_y"), py::arg("_z"), py::arg("_w")) + .def_readwrite("x", &ImVec4::x) + .def_readwrite("y", &ImVec4::y) + .def_readwrite("z", &ImVec4::z) + .def_readwrite("w", &ImVec4::w); + + py::class_(m, "Style") + .def_readwrite("FramePadding", &ImGuiStyle::FramePadding) + .def_readwrite("ItemSpacing", &ImGuiStyle::ItemSpacing) + .def_readwrite("WindowPadding", &ImGuiStyle::WindowPadding); + + m.def("GetStyle", &ImGui::GetStyle, py::return_value_policy::reference); + + py::class_(m, "IO") + .def_readonly("DisplaySize", &ImGuiIO::DisplaySize) + .def_readonly("DeltaTime", &ImGuiIO::DeltaTime) + .def_readonly("Framerate", &ImGuiIO::Framerate) + .def_readonly("WantCaptureMouse", &ImGuiIO::WantCaptureMouse) + .def_readonly("WantCaptureKeyboard", &ImGuiIO::WantCaptureKeyboard) + .def_readonly("KeyShift", &ImGuiIO::KeyShift) + .def_readonly("KeyCtrl", &ImGuiIO::KeyCtrl) + .def_readonly("KeyAlt", &ImGuiIO::KeyAlt) + .def_readonly("KeySuper", &ImGuiIO::KeySuper) + .def_readonly("MousePos", &ImGuiIO::MousePos) + .def_readonly("MouseWheel", &ImGuiIO::MouseWheel) + .def_readonly("MouseDelta", &ImGuiIO::MouseDelta); + + m.def("GetIO", &ImGui::GetIO, py::return_value_policy::reference); + + m.def("GetCurrentContext", []() { + return reinterpret_cast(ImGui::GetCurrentContext()); + }); + m.def("SetCurrentContext", [](uintptr_t ptr) { + ImGui::SetCurrentContext(reinterpret_cast(ptr)); + }); + + py::class_(m, "ListClipper") + .def(py::init<>()) + .def("Begin", &ImGuiListClipper::Begin, py::arg("items_count"), py::arg("items_height") = -1.0f) + .def("End", &ImGuiListClipper::End) + .def("Step", &ImGuiListClipper::Step) + .def("IncludeItemsByIndex", &ImGuiListClipper::IncludeItemsByIndex, py::arg("item_begin"), py::arg("item_end")) + .def("IncludeItemByIndex", &ImGuiListClipper::IncludeItemByIndex, py::arg("item_index")) + .def("SeekCursorForItem", &ImGuiListClipper::SeekCursorForItem, py::arg("item_index")) + .def_readonly("DisplayStart", &ImGuiListClipper::DisplayStart) + .def_readonly("DisplayEnd", &ImGuiListClipper::DisplayEnd); + + // Enumerations. + + py::enum_(m, "WindowFlags") + .value("None", ImGuiWindowFlags_None) + .value("NoTitleBar", ImGuiWindowFlags_NoTitleBar) + .value("NoResize", ImGuiWindowFlags_NoResize) + .value("NoMove", ImGuiWindowFlags_NoMove) + .value("NoScrollbar", ImGuiWindowFlags_NoScrollbar) + .value("NoScrollWithMouse", ImGuiWindowFlags_NoScrollWithMouse) + .value("NoCollapse", ImGuiWindowFlags_NoCollapse) + .value("AlwaysAutoResize", ImGuiWindowFlags_AlwaysAutoResize) + .value("NoBackground", ImGuiWindowFlags_NoBackground) + .value("NoSavedSettings", ImGuiWindowFlags_NoSavedSettings) + .value("NoMouseInputs", ImGuiWindowFlags_NoMouseInputs) + .value("MenuBar", ImGuiWindowFlags_MenuBar) + .value("HorizontalScrollbar", ImGuiWindowFlags_HorizontalScrollbar) + .value("NoFocusOnAppearing", ImGuiWindowFlags_NoFocusOnAppearing) + .value("NoBringToFrontOnFocus", ImGuiWindowFlags_NoBringToFrontOnFocus) + .value("AlwaysVerticalScrollbar", ImGuiWindowFlags_AlwaysVerticalScrollbar) + .value("AlwaysHorizontalScrollbar", ImGuiWindowFlags_AlwaysHorizontalScrollbar) + .value("NoNavInputs", ImGuiWindowFlags_NoNavInputs) + .value("NoNavFocus", ImGuiWindowFlags_NoNavFocus) + .value("UnsavedDocument", ImGuiWindowFlags_UnsavedDocument) + .value("NoDocking", ImGuiWindowFlags_NoDocking) + .value("NoNav", ImGuiWindowFlags_NoNav) + .value("NoDecoration", ImGuiWindowFlags_NoDecoration) + .value("NoInputs", ImGuiWindowFlags_NoInputs) + .value("ChildWindow", ImGuiWindowFlags_ChildWindow) + .value("Tooltip", ImGuiWindowFlags_Tooltip) + .value("Popup", ImGuiWindowFlags_Popup) + .value("Modal", ImGuiWindowFlags_Modal) + .value("ChildMenu", ImGuiWindowFlags_ChildMenu) + .value("DockNodeHost", ImGuiWindowFlags_DockNodeHost); + + py::enum_(m, "ChildFlags") + .value("None", ImGuiChildFlags_None) + .value("Borders", ImGuiChildFlags_Borders) + .value("AlwaysUseWindowPadding", ImGuiChildFlags_AlwaysUseWindowPadding) + .value("ResizeX", ImGuiChildFlags_ResizeX) + .value("ResizeY", ImGuiChildFlags_ResizeY) + .value("AutoResizeX", ImGuiChildFlags_AutoResizeX) + .value("AutoResizeY", ImGuiChildFlags_AutoResizeY) + .value("AlwaysAutoResize", ImGuiChildFlags_AlwaysAutoResize) + .value("FrameStyle", ImGuiChildFlags_FrameStyle) + .value("NavFlattened", ImGuiChildFlags_NavFlattened); + + py::enum_(m, "InputTextFlags") + .value("None", ImGuiInputTextFlags_None) + .value("CharsDecimal", ImGuiInputTextFlags_CharsDecimal) + .value("CharsHexadecimal", ImGuiInputTextFlags_CharsHexadecimal) + .value("CharsScientific", ImGuiInputTextFlags_CharsScientific) + .value("CharsUppercase", ImGuiInputTextFlags_CharsUppercase) + .value("CharsNoBlank", ImGuiInputTextFlags_CharsNoBlank) + .value("AllowTabInput", ImGuiInputTextFlags_AllowTabInput) + .value("EnterReturnsTrue", ImGuiInputTextFlags_EnterReturnsTrue) + .value("EscapeClearsAll", ImGuiInputTextFlags_EscapeClearsAll) + .value("CtrlEnterForNewLine", ImGuiInputTextFlags_CtrlEnterForNewLine) + .value("ReadOnly", ImGuiInputTextFlags_ReadOnly) + .value("Password", ImGuiInputTextFlags_Password) + .value("AlwaysOverwrite", ImGuiInputTextFlags_AlwaysOverwrite) + .value("AutoSelectAll", ImGuiInputTextFlags_AutoSelectAll) + .value("ParseEmptyRefVal", ImGuiInputTextFlags_ParseEmptyRefVal) + .value("DisplayEmptyRefVal", ImGuiInputTextFlags_DisplayEmptyRefVal) + .value("NoHorizontalScroll", ImGuiInputTextFlags_NoHorizontalScroll) + .value("NoUndoRedo", ImGuiInputTextFlags_NoUndoRedo) + .value("CallbackCompletion", ImGuiInputTextFlags_CallbackCompletion) + .value("CallbackHistory", ImGuiInputTextFlags_CallbackHistory) + .value("CallbackAlways", ImGuiInputTextFlags_CallbackAlways) + .value("CallbackCharFilter", ImGuiInputTextFlags_CallbackCharFilter) + .value("CallbackResize", ImGuiInputTextFlags_CallbackResize) + .value("CallbackEdit", ImGuiInputTextFlags_CallbackEdit); + + py::enum_(m, "TreeNodeFlags") + .value("None", ImGuiTreeNodeFlags_None) + .value("Selected", ImGuiTreeNodeFlags_Selected) + .value("Framed", ImGuiTreeNodeFlags_Framed) + .value("AllowOverlap", ImGuiTreeNodeFlags_AllowOverlap) + .value("NoTreePushOnOpen", ImGuiTreeNodeFlags_NoTreePushOnOpen) + .value("NoAutoOpenOnLog", ImGuiTreeNodeFlags_NoAutoOpenOnLog) + .value("DefaultOpen", ImGuiTreeNodeFlags_DefaultOpen) + .value("OpenOnDoubleClick", ImGuiTreeNodeFlags_OpenOnDoubleClick) + .value("OpenOnArrow", ImGuiTreeNodeFlags_OpenOnArrow) + .value("Leaf", ImGuiTreeNodeFlags_Leaf) + .value("Bullet", ImGuiTreeNodeFlags_Bullet) + .value("FramePadding", ImGuiTreeNodeFlags_FramePadding) + .value("SpanAvailWidth", ImGuiTreeNodeFlags_SpanAvailWidth) + .value("SpanFullWidth", ImGuiTreeNodeFlags_SpanFullWidth) + .value("SpanTextWidth", ImGuiTreeNodeFlags_SpanTextWidth) + .value("SpanAllColumns", ImGuiTreeNodeFlags_SpanAllColumns) + .value("NavLeftJumpsBackHere", ImGuiTreeNodeFlags_NavLeftJumpsBackHere) + .value("CollapsingHeader", ImGuiTreeNodeFlags_CollapsingHeader); + + py::enum_(m, "PopupFlags") + .value("None", ImGuiPopupFlags_None) + .value("MouseButtonLeft", ImGuiPopupFlags_MouseButtonLeft) + .value("MouseButtonRight", ImGuiPopupFlags_MouseButtonRight) + .value("MouseButtonMiddle", ImGuiPopupFlags_MouseButtonMiddle) + .value("MouseButtonMask", ImGuiPopupFlags_MouseButtonMask_) + .value("NoReopen", ImGuiPopupFlags_NoReopen) + .value("NoOpenOverExistingPopup", ImGuiPopupFlags_NoOpenOverExistingPopup) + .value("NoOpenOverItems", ImGuiPopupFlags_NoOpenOverItems) + .value("AnyPopupId", ImGuiPopupFlags_AnyPopupId) + .value("AnyPopupLevel", ImGuiPopupFlags_AnyPopupLevel) + .value("AnyPopup", ImGuiPopupFlags_AnyPopup); + + py::enum_(m, "SelectableFlags") + .value("None", ImGuiSelectableFlags_None) + .value("DontClosePopups", ImGuiSelectableFlags_DontClosePopups) + .value("SpanAllColumns", ImGuiSelectableFlags_SpanAllColumns) + .value("AllowDoubleClick", ImGuiSelectableFlags_AllowDoubleClick) + .value("Disabled", ImGuiSelectableFlags_Disabled) + .value("AllowOverlap", ImGuiSelectableFlags_AllowOverlap); + + py::enum_(m, "ComboFlags") + .value("None", ImGuiComboFlags_None) + .value("PopupAlignLeft", ImGuiComboFlags_PopupAlignLeft) + .value("HeightSmall", ImGuiComboFlags_HeightSmall) + .value("HeightRegular", ImGuiComboFlags_HeightRegular) + .value("HeightLarge", ImGuiComboFlags_HeightLarge) + .value("HeightLargest", ImGuiComboFlags_HeightLargest) + .value("NoArrowButton", ImGuiComboFlags_NoArrowButton) + .value("NoPreview", ImGuiComboFlags_NoPreview) + .value("WidthFitPreview", ImGuiComboFlags_WidthFitPreview) + .value("HeightMask", ImGuiComboFlags_HeightMask_); + + py::enum_(m, "TabBarFlags") + .value("None", ImGuiTabBarFlags_None) + .value("Reorderable", ImGuiTabBarFlags_Reorderable) + .value("AutoSelectNewTabs", ImGuiTabBarFlags_AutoSelectNewTabs) + .value("TabListPopupButton", ImGuiTabBarFlags_TabListPopupButton) + .value("NoCloseWithMiddleMouseButton", ImGuiTabBarFlags_NoCloseWithMiddleMouseButton) + .value("NoTabListScrollingButtons", ImGuiTabBarFlags_NoTabListScrollingButtons) + .value("NoTooltip", ImGuiTabBarFlags_NoTooltip) + .value("DrawSelectedOverline", ImGuiTabBarFlags_DrawSelectedOverline) + .value("FittingPolicyResizeDown", ImGuiTabBarFlags_FittingPolicyResizeDown) + .value("FittingPolicyScroll", ImGuiTabBarFlags_FittingPolicyScroll) + .value("FittingPolicyMask", ImGuiTabBarFlags_FittingPolicyMask_) + .value("FittingPolicyDefault", ImGuiTabBarFlags_FittingPolicyDefault_); + + py::enum_(m, "TabItemFlags") + .value("None", ImGuiTabItemFlags_None) + .value("UnsavedDocument", ImGuiTabItemFlags_UnsavedDocument) + .value("SetSelected", ImGuiTabItemFlags_SetSelected) + .value("NoCloseWithMiddleMouseButton", ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) + .value("NoPushId", ImGuiTabItemFlags_NoPushId) + .value("NoTooltip", ImGuiTabItemFlags_NoTooltip) + .value("NoReorder", ImGuiTabItemFlags_NoReorder) + .value("Leading", ImGuiTabItemFlags_Leading) + .value("Trailing", ImGuiTabItemFlags_Trailing) + .value("NoAssumedClosure", ImGuiTabItemFlags_NoAssumedClosure); + + py::enum_(m, "FocusedFlags") + .value("None", ImGuiFocusedFlags_None) + .value("ChildWindows", ImGuiFocusedFlags_ChildWindows) + .value("RootWindow", ImGuiFocusedFlags_RootWindow) + .value("AnyWindow", ImGuiFocusedFlags_AnyWindow) + .value("NoPopupHierarchy", ImGuiFocusedFlags_NoPopupHierarchy) + .value("DockHierarchy", ImGuiFocusedFlags_DockHierarchy) + .value("RootAndChildWindows", ImGuiFocusedFlags_RootAndChildWindows); + + py::enum_(m, "HoveredFlags") + .value("None", ImGuiHoveredFlags_None) + .value("ChildWindows", ImGuiHoveredFlags_ChildWindows) + .value("RootWindow", ImGuiHoveredFlags_RootWindow) + .value("AnyWindow", ImGuiHoveredFlags_AnyWindow) + .value("NoPopupHierarchy", ImGuiHoveredFlags_NoPopupHierarchy) + .value("DockHierarchy", ImGuiHoveredFlags_DockHierarchy) + .value("AllowWhenBlockedByPopup", ImGuiHoveredFlags_AllowWhenBlockedByPopup) + .value("AllowWhenBlockedByActiveItem", ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) + .value("AllowWhenOverlappedByItem", ImGuiHoveredFlags_AllowWhenOverlappedByItem) + .value("AllowWhenOverlappedByWindow", ImGuiHoveredFlags_AllowWhenOverlappedByWindow) + .value("AllowWhenDisabled", ImGuiHoveredFlags_AllowWhenDisabled) + .value("NoNavOverride", ImGuiHoveredFlags_NoNavOverride) + .value("AllowWhenOverlapped", ImGuiHoveredFlags_AllowWhenOverlapped) + .value("RectOnly", ImGuiHoveredFlags_RectOnly) + .value("RootAndChildWindows", ImGuiHoveredFlags_RootAndChildWindows) + .value("ForTooltip", ImGuiHoveredFlags_ForTooltip) + .value("Stationary", ImGuiHoveredFlags_Stationary) + .value("DelayNone", ImGuiHoveredFlags_DelayNone) + .value("DelayShort", ImGuiHoveredFlags_DelayShort) + .value("DelayNormal", ImGuiHoveredFlags_DelayNormal) + .value("NoSharedDelay", ImGuiHoveredFlags_NoSharedDelay); + + py::enum_(m, "DockNodeFlags") + .value("None", ImGuiDockNodeFlags_None) + .value("KeepAliveOnly", ImGuiDockNodeFlags_KeepAliveOnly) + .value("NoDockingOverCentralNode", ImGuiDockNodeFlags_NoDockingOverCentralNode) + .value("PassthruCentralNode", ImGuiDockNodeFlags_PassthruCentralNode) + .value("NoDockingSplit", ImGuiDockNodeFlags_NoDockingSplit) + .value("NoResize", ImGuiDockNodeFlags_NoResize) + .value("AutoHideTabBar", ImGuiDockNodeFlags_AutoHideTabBar) + .value("NoUndocking", ImGuiDockNodeFlags_NoUndocking); + + py::enum_(m, "DragDropFlags") + .value("None", ImGuiDragDropFlags_None) + .value("SourceNoPreviewTooltip", ImGuiDragDropFlags_SourceNoPreviewTooltip) + .value("SourceNoDisableHover", ImGuiDragDropFlags_SourceNoDisableHover) + .value("SourceNoHoldToOpenOthers", ImGuiDragDropFlags_SourceNoHoldToOpenOthers) + .value("SourceAllowNullID", ImGuiDragDropFlags_SourceAllowNullID) + .value("SourceExtern", ImGuiDragDropFlags_SourceExtern) + .value("PayloadAutoExpire", ImGuiDragDropFlags_PayloadAutoExpire) + .value("PayloadNoCrossContext", ImGuiDragDropFlags_PayloadNoCrossContext) + .value("PayloadNoCrossProcess", ImGuiDragDropFlags_PayloadNoCrossProcess) + .value("AcceptBeforeDelivery", ImGuiDragDropFlags_AcceptBeforeDelivery) + .value("AcceptNoDrawDefaultRect", ImGuiDragDropFlags_AcceptNoDrawDefaultRect) + .value("AcceptNoPreviewTooltip", ImGuiDragDropFlags_AcceptNoPreviewTooltip) + .value("AcceptPeekOnly", ImGuiDragDropFlags_AcceptPeekOnly); + + py::enum_(m, "DataType") + .value("S8", ImGuiDataType_S8) + .value("U8", ImGuiDataType_U8) + .value("S16", ImGuiDataType_S16) + .value("U16", ImGuiDataType_U16) + .value("S32", ImGuiDataType_S32) + .value("U32", ImGuiDataType_U32) + .value("S64", ImGuiDataType_S64) + .value("U64", ImGuiDataType_U64) + .value("Float", ImGuiDataType_Float) + .value("Double", ImGuiDataType_Double); + + py::enum_(m, "Dir") + .value("None", ImGuiDir_None) + .value("Left", ImGuiDir_Left) + .value("Right", ImGuiDir_Right) + .value("Up", ImGuiDir_Up) + .value("Down", ImGuiDir_Down); + + py::enum_(m, "SortDirection") + .value("None", ImGuiSortDirection_None) + .value("Ascending", ImGuiSortDirection_Ascending) + .value("Descending", ImGuiSortDirection_Descending); + + py::enum_(m, "InputFlags") + .value("None", ImGuiInputFlags_None) + .value("Repeat", ImGuiInputFlags_Repeat) + .value("RouteActive", ImGuiInputFlags_RouteActive) + .value("RouteFocused", ImGuiInputFlags_RouteFocused) + .value("RouteGlobal", ImGuiInputFlags_RouteGlobal) + .value("RouteAlways", ImGuiInputFlags_RouteAlways) + .value("RouteOverFocused", ImGuiInputFlags_RouteOverFocused) + .value("RouteOverActive", ImGuiInputFlags_RouteOverActive) + .value("RouteUnlessBgFocused", ImGuiInputFlags_RouteUnlessBgFocused) + .value("RouteFromRootWindow", ImGuiInputFlags_RouteFromRootWindow) + .value("Tooltip", ImGuiInputFlags_Tooltip); + + py::enum_(m, "ConfigFlags") + .value("None", ImGuiConfigFlags_None) + .value("NavEnableKeyboard", ImGuiConfigFlags_NavEnableKeyboard) + .value("NavEnableGamepad", ImGuiConfigFlags_NavEnableGamepad) + .value("NavEnableSetMousePos", ImGuiConfigFlags_NavEnableSetMousePos) + .value("NavNoCaptureKeyboard", ImGuiConfigFlags_NavNoCaptureKeyboard) + .value("NoMouse", ImGuiConfigFlags_NoMouse) + .value("NoMouseCursorChange", ImGuiConfigFlags_NoMouseCursorChange) + .value("NoKeyboard", ImGuiConfigFlags_NoKeyboard) + .value("DockingEnable", ImGuiConfigFlags_DockingEnable) + .value("ViewportsEnable", ImGuiConfigFlags_ViewportsEnable) + .value("DpiEnableScaleViewports", ImGuiConfigFlags_DpiEnableScaleViewports) + .value("DpiEnableScaleFonts", ImGuiConfigFlags_DpiEnableScaleFonts) + .value("IsSRGB", ImGuiConfigFlags_IsSRGB) + .value("IsTouchScreen", ImGuiConfigFlags_IsTouchScreen); + + py::enum_(m, "BackendFlags") + .value("None", ImGuiBackendFlags_None) + .value("HasGamepad", ImGuiBackendFlags_HasGamepad) + .value("HasMouseCursors", ImGuiBackendFlags_HasMouseCursors) + .value("HasSetMousePos", ImGuiBackendFlags_HasSetMousePos) + .value("RendererHasVtxOffset", ImGuiBackendFlags_RendererHasVtxOffset) + .value("PlatformHasViewports", ImGuiBackendFlags_PlatformHasViewports) + .value("HasMouseHoveredViewport", ImGuiBackendFlags_HasMouseHoveredViewport) + .value("RendererHasViewports", ImGuiBackendFlags_RendererHasViewports); + + py::enum_(m, "Col") + .value("Text", ImGuiCol_Text) + .value("TextDisabled", ImGuiCol_TextDisabled) + .value("WindowBg", ImGuiCol_WindowBg) + .value("ChildBg", ImGuiCol_ChildBg) + .value("PopupBg", ImGuiCol_PopupBg) + .value("Border", ImGuiCol_Border) + .value("BorderShadow", ImGuiCol_BorderShadow) + .value("FrameBg", ImGuiCol_FrameBg) + .value("FrameBgHovered", ImGuiCol_FrameBgHovered) + .value("FrameBgActive", ImGuiCol_FrameBgActive) + .value("TitleBg", ImGuiCol_TitleBg) + .value("TitleBgActive", ImGuiCol_TitleBgActive) + .value("TitleBgCollapsed", ImGuiCol_TitleBgCollapsed) + .value("MenuBarBg", ImGuiCol_MenuBarBg) + .value("ScrollbarBg", ImGuiCol_ScrollbarBg) + .value("ScrollbarGrab", ImGuiCol_ScrollbarGrab) + .value("ScrollbarGrabHovered", ImGuiCol_ScrollbarGrabHovered) + .value("ScrollbarGrabActive", ImGuiCol_ScrollbarGrabActive) + .value("CheckMark", ImGuiCol_CheckMark) + .value("SliderGrab", ImGuiCol_SliderGrab) + .value("SliderGrabActive", ImGuiCol_SliderGrabActive) + .value("Button", ImGuiCol_Button) + .value("ButtonHovered", ImGuiCol_ButtonHovered) + .value("ButtonActive", ImGuiCol_ButtonActive) + .value("Header", ImGuiCol_Header) + .value("HeaderHovered", ImGuiCol_HeaderHovered) + .value("HeaderActive", ImGuiCol_HeaderActive) + .value("Separator", ImGuiCol_Separator) + .value("SeparatorHovered", ImGuiCol_SeparatorHovered) + .value("SeparatorActive", ImGuiCol_SeparatorActive) + .value("ResizeGrip", ImGuiCol_ResizeGrip) + .value("ResizeGripHovered", ImGuiCol_ResizeGripHovered) + .value("ResizeGripActive", ImGuiCol_ResizeGripActive) + .value("TabHovered", ImGuiCol_TabHovered) + .value("Tab", ImGuiCol_Tab) + .value("TabSelected", ImGuiCol_TabSelected) + .value("TabSelectedOverline", ImGuiCol_TabSelectedOverline) + .value("TabDimmed", ImGuiCol_TabDimmed) + .value("TabDimmedSelected", ImGuiCol_TabDimmedSelected) + .value("TabDimmedSelectedOverline", ImGuiCol_TabDimmedSelectedOverline) + .value("DockingPreview", ImGuiCol_DockingPreview) + .value("DockingEmptyBg", ImGuiCol_DockingEmptyBg) + .value("PlotLines", ImGuiCol_PlotLines) + .value("PlotLinesHovered", ImGuiCol_PlotLinesHovered) + .value("PlotHistogram", ImGuiCol_PlotHistogram) + .value("PlotHistogramHovered", ImGuiCol_PlotHistogramHovered) + .value("TableHeaderBg", ImGuiCol_TableHeaderBg) + .value("TableBorderStrong", ImGuiCol_TableBorderStrong) + .value("TableBorderLight", ImGuiCol_TableBorderLight) + .value("TableRowBg", ImGuiCol_TableRowBg) + .value("TableRowBgAlt", ImGuiCol_TableRowBgAlt) + .value("TextLink", ImGuiCol_TextLink) + .value("TextSelectedBg", ImGuiCol_TextSelectedBg) + .value("DragDropTarget", ImGuiCol_DragDropTarget) + .value("NavHighlight", ImGuiCol_NavHighlight) + .value("NavWindowingHighlight", ImGuiCol_NavWindowingHighlight) + .value("NavWindowingDimBg", ImGuiCol_NavWindowingDimBg) + .value("ModalWindowDimBg", ImGuiCol_ModalWindowDimBg); + + py::enum_(m, "StyleVar") + .value("Alpha", ImGuiStyleVar_Alpha) + .value("DisabledAlpha", ImGuiStyleVar_DisabledAlpha) + .value("WindowPadding", ImGuiStyleVar_WindowPadding) + .value("WindowRounding", ImGuiStyleVar_WindowRounding) + .value("WindowBorderSize", ImGuiStyleVar_WindowBorderSize) + .value("WindowMinSize", ImGuiStyleVar_WindowMinSize) + .value("WindowTitleAlign", ImGuiStyleVar_WindowTitleAlign) + .value("ChildRounding", ImGuiStyleVar_ChildRounding) + .value("ChildBorderSize", ImGuiStyleVar_ChildBorderSize) + .value("PopupRounding", ImGuiStyleVar_PopupRounding) + .value("PopupBorderSize", ImGuiStyleVar_PopupBorderSize) + .value("FramePadding", ImGuiStyleVar_FramePadding) + .value("FrameRounding", ImGuiStyleVar_FrameRounding) + .value("FrameBorderSize", ImGuiStyleVar_FrameBorderSize) + .value("ItemSpacing", ImGuiStyleVar_ItemSpacing) + .value("ItemInnerSpacing", ImGuiStyleVar_ItemInnerSpacing) + .value("IndentSpacing", ImGuiStyleVar_IndentSpacing) + .value("CellPadding", ImGuiStyleVar_CellPadding) + .value("ScrollbarSize", ImGuiStyleVar_ScrollbarSize) + .value("ScrollbarRounding", ImGuiStyleVar_ScrollbarRounding) + .value("GrabMinSize", ImGuiStyleVar_GrabMinSize) + .value("GrabRounding", ImGuiStyleVar_GrabRounding) + .value("TabRounding", ImGuiStyleVar_TabRounding) + .value("TabBorderSize", ImGuiStyleVar_TabBorderSize) + .value("TabBarBorderSize", ImGuiStyleVar_TabBarBorderSize) + .value("TableAngledHeadersAngle", ImGuiStyleVar_TableAngledHeadersAngle) + .value("TableAngledHeadersTextAlign", ImGuiStyleVar_TableAngledHeadersTextAlign) + .value("ButtonTextAlign", ImGuiStyleVar_ButtonTextAlign) + .value("SelectableTextAlign", ImGuiStyleVar_SelectableTextAlign) + .value("SeparatorTextBorderSize", ImGuiStyleVar_SeparatorTextBorderSize) + .value("SeparatorTextAlign", ImGuiStyleVar_SeparatorTextAlign) + .value("SeparatorTextPadding", ImGuiStyleVar_SeparatorTextPadding) + .value("DockingSeparatorSize", ImGuiStyleVar_DockingSeparatorSize); + + py::enum_(m, "ButtonFlags") + .value("None", ImGuiButtonFlags_None) + .value("MouseButtonLeft", ImGuiButtonFlags_MouseButtonLeft) + .value("MouseButtonRight", ImGuiButtonFlags_MouseButtonRight) + .value("MouseButtonMiddle", ImGuiButtonFlags_MouseButtonMiddle) + .value("MouseButtonMask", ImGuiButtonFlags_MouseButtonMask_); + + py::enum_(m, "ColorEditFlags") + .value("None", ImGuiColorEditFlags_None) + .value("NoAlpha", ImGuiColorEditFlags_NoAlpha) + .value("NoPicker", ImGuiColorEditFlags_NoPicker) + .value("NoOptions", ImGuiColorEditFlags_NoOptions) + .value("NoSmallPreview", ImGuiColorEditFlags_NoSmallPreview) + .value("NoInputs", ImGuiColorEditFlags_NoInputs) + .value("NoTooltip", ImGuiColorEditFlags_NoTooltip) + .value("NoLabel", ImGuiColorEditFlags_NoLabel) + .value("NoSidePreview", ImGuiColorEditFlags_NoSidePreview) + .value("NoDragDrop", ImGuiColorEditFlags_NoDragDrop) + .value("NoBorder", ImGuiColorEditFlags_NoBorder) + .value("AlphaBar", ImGuiColorEditFlags_AlphaBar) + .value("AlphaPreview", ImGuiColorEditFlags_AlphaPreview) + .value("AlphaPreviewHalf", ImGuiColorEditFlags_AlphaPreviewHalf) + .value("HDR", ImGuiColorEditFlags_HDR) + .value("DisplayRGB", ImGuiColorEditFlags_DisplayRGB) + .value("DisplayHSV", ImGuiColorEditFlags_DisplayHSV) + .value("DisplayHex", ImGuiColorEditFlags_DisplayHex) + .value("Uint8", ImGuiColorEditFlags_Uint8) + .value("Float", ImGuiColorEditFlags_Float) + .value("PickerHueBar", ImGuiColorEditFlags_PickerHueBar) + .value("PickerHueWheel", ImGuiColorEditFlags_PickerHueWheel) + .value("InputRGB", ImGuiColorEditFlags_InputRGB) + .value("InputHSV", ImGuiColorEditFlags_InputHSV) + .value("DefaultOptions", ImGuiColorEditFlags_DefaultOptions_) + .value("DisplayMask", ImGuiColorEditFlags_DisplayMask_) + .value("DataTypeMask", ImGuiColorEditFlags_DataTypeMask_) + .value("PickerMask", ImGuiColorEditFlags_PickerMask_) + .value("InputMask", ImGuiColorEditFlags_InputMask_); + + py::enum_(m, "SliderFlags") + .value("None", ImGuiSliderFlags_None) + .value("AlwaysClamp", ImGuiSliderFlags_AlwaysClamp) + .value("Logarithmic", ImGuiSliderFlags_Logarithmic) + .value("NoRoundToFormat", ImGuiSliderFlags_NoRoundToFormat) + .value("NoInput", ImGuiSliderFlags_NoInput) + .value("WrapAround", ImGuiSliderFlags_WrapAround) + .value("InvalidMask", ImGuiSliderFlags_InvalidMask_); + + py::enum_(m, "MouseButton") + .value("Left", ImGuiMouseButton_Left) + .value("Right", ImGuiMouseButton_Right) + .value("Middle", ImGuiMouseButton_Middle); + + py::enum_(m, "MouseCursor") + .value("None", ImGuiMouseCursor_None) + .value("Arrow", ImGuiMouseCursor_Arrow) + .value("TextInput", ImGuiMouseCursor_TextInput) + .value("ResizeAll", ImGuiMouseCursor_ResizeAll) + .value("ResizeNS", ImGuiMouseCursor_ResizeNS) + .value("ResizeEW", ImGuiMouseCursor_ResizeEW) + .value("ResizeNESW", ImGuiMouseCursor_ResizeNESW) + .value("ResizeNWSE", ImGuiMouseCursor_ResizeNWSE) + .value("Hand", ImGuiMouseCursor_Hand) + .value("NotAllowed", ImGuiMouseCursor_NotAllowed); + + py::enum_(m, "MouseSource") + .value("Mouse", ImGuiMouseSource_Mouse) + .value("TouchScreen", ImGuiMouseSource_TouchScreen) + .value("Pen", ImGuiMouseSource_Pen); + + py::enum_(m, "Cond") + .value("None", ImGuiCond_None) + .value("Always", ImGuiCond_Always) + .value("Once", ImGuiCond_Once) + .value("FirstUseEver", ImGuiCond_FirstUseEver) + .value("Appearing", ImGuiCond_Appearing); + + py::enum_(m, "TableFlags") + .value("None", ImGuiTableFlags_None) + .value("Resizable", ImGuiTableFlags_Resizable) + .value("Reorderable", ImGuiTableFlags_Reorderable) + .value("Hideable", ImGuiTableFlags_Hideable) + .value("Sortable", ImGuiTableFlags_Sortable) + .value("NoSavedSettings", ImGuiTableFlags_NoSavedSettings) + .value("ContextMenuInBody", ImGuiTableFlags_ContextMenuInBody) + .value("RowBg", ImGuiTableFlags_RowBg) + .value("BordersInnerH", ImGuiTableFlags_BordersInnerH) + .value("BordersOuterH", ImGuiTableFlags_BordersOuterH) + .value("BordersInnerV", ImGuiTableFlags_BordersInnerV) + .value("BordersOuterV", ImGuiTableFlags_BordersOuterV) + .value("BordersH", ImGuiTableFlags_BordersH) + .value("BordersV", ImGuiTableFlags_BordersV) + .value("BordersInner", ImGuiTableFlags_BordersInner) + .value("BordersOuter", ImGuiTableFlags_BordersOuter) + .value("Borders", ImGuiTableFlags_Borders) + .value("NoBordersInBody", ImGuiTableFlags_NoBordersInBody) + .value("NoBordersInBodyUntilResize", ImGuiTableFlags_NoBordersInBodyUntilResize) + .value("SizingFixedFit", ImGuiTableFlags_SizingFixedFit) + .value("SizingFixedSame", ImGuiTableFlags_SizingFixedSame) + .value("SizingStretchProp", ImGuiTableFlags_SizingStretchProp) + .value("SizingStretchSame", ImGuiTableFlags_SizingStretchSame) + .value("NoHostExtendX", ImGuiTableFlags_NoHostExtendX) + .value("NoHostExtendY", ImGuiTableFlags_NoHostExtendY) + .value("NoKeepColumnsVisible", ImGuiTableFlags_NoKeepColumnsVisible) + .value("PreciseWidths", ImGuiTableFlags_PreciseWidths) + .value("NoClip", ImGuiTableFlags_NoClip) + .value("PadOuterX", ImGuiTableFlags_PadOuterX) + .value("NoPadOuterX", ImGuiTableFlags_NoPadOuterX) + .value("NoPadInnerX", ImGuiTableFlags_NoPadInnerX) + .value("ScrollX", ImGuiTableFlags_ScrollX) + .value("ScrollY", ImGuiTableFlags_ScrollY) + .value("SortMulti", ImGuiTableFlags_SortMulti) + .value("SortTristate", ImGuiTableFlags_SortTristate) + .value("HighlightHoveredColumn", ImGuiTableFlags_HighlightHoveredColumn) + .value("SizingMask", ImGuiTableFlags_SizingMask_); + + py::enum_(m, "TableColumnFlags") + .value("None", ImGuiTableColumnFlags_None) + .value("Disabled", ImGuiTableColumnFlags_Disabled) + .value("DefaultHide", ImGuiTableColumnFlags_DefaultHide) + .value("DefaultSort", ImGuiTableColumnFlags_DefaultSort) + .value("WidthStretch", ImGuiTableColumnFlags_WidthStretch) + .value("WidthFixed", ImGuiTableColumnFlags_WidthFixed) + .value("NoResize", ImGuiTableColumnFlags_NoResize) + .value("NoReorder", ImGuiTableColumnFlags_NoReorder) + .value("NoHide", ImGuiTableColumnFlags_NoHide) + .value("NoClip", ImGuiTableColumnFlags_NoClip) + .value("NoSort", ImGuiTableColumnFlags_NoSort) + .value("NoSortAscending", ImGuiTableColumnFlags_NoSortAscending) + .value("NoSortDescending", ImGuiTableColumnFlags_NoSortDescending) + .value("NoHeaderLabel", ImGuiTableColumnFlags_NoHeaderLabel) + .value("NoHeaderWidth", ImGuiTableColumnFlags_NoHeaderWidth) + .value("PreferSortAscending", ImGuiTableColumnFlags_PreferSortAscending) + .value("PreferSortDescending", ImGuiTableColumnFlags_PreferSortDescending) + .value("IndentEnable", ImGuiTableColumnFlags_IndentEnable) + .value("IndentDisable", ImGuiTableColumnFlags_IndentDisable) + .value("AngledHeader", ImGuiTableColumnFlags_AngledHeader) + .value("IsEnabled", ImGuiTableColumnFlags_IsEnabled) + .value("IsVisible", ImGuiTableColumnFlags_IsVisible) + .value("IsSorted", ImGuiTableColumnFlags_IsSorted) + .value("IsHovered", ImGuiTableColumnFlags_IsHovered) + .value("WidthMask", ImGuiTableColumnFlags_WidthMask_) + .value("IndentMask", ImGuiTableColumnFlags_IndentMask_) + .value("StatusMask", ImGuiTableColumnFlags_StatusMask_) + .value("NoDirectResize", ImGuiTableColumnFlags_NoDirectResize_); + + py::enum_(m, "TableRowFlags") + .value("None", ImGuiTableRowFlags_None) + .value("Headers", ImGuiTableRowFlags_Headers); + + py::enum_(m, "TableBgTarget") + .value("None", ImGuiTableBgTarget_None) + .value("RowBg0", ImGuiTableBgTarget_RowBg0) + .value("RowBg1", ImGuiTableBgTarget_RowBg1) + .value("CellBg", ImGuiTableBgTarget_CellBg); + + py::enum_(m, "Key") + .value("None", ImGuiKey_None) + .value("Tab", ImGuiKey_Tab) + .value("LeftArrow", ImGuiKey_LeftArrow) + .value("RightArrow", ImGuiKey_RightArrow) + .value("UpArrow", ImGuiKey_UpArrow) + .value("DownArrow", ImGuiKey_DownArrow) + .value("PageUp", ImGuiKey_PageUp) + .value("PageDown", ImGuiKey_PageDown) + .value("Home", ImGuiKey_Home) + .value("End", ImGuiKey_End) + .value("Insert", ImGuiKey_Insert) + .value("Delete", ImGuiKey_Delete) + .value("Backspace", ImGuiKey_Backspace) + .value("Space", ImGuiKey_Space) + .value("Enter", ImGuiKey_Enter) + .value("Escape", ImGuiKey_Escape) + .value("LeftCtrl", ImGuiKey_LeftCtrl) + .value("LeftShift", ImGuiKey_LeftShift) + .value("LeftAlt", ImGuiKey_LeftAlt) + .value("LeftSuper", ImGuiKey_LeftSuper) + .value("RightCtrl", ImGuiKey_RightCtrl) + .value("RightShift", ImGuiKey_RightShift) + .value("RightAlt", ImGuiKey_RightAlt) + .value("RightSuper", ImGuiKey_RightSuper) + .value("Menu", ImGuiKey_Menu) + // "N" prefix ensures "imgui.Key.N0" parses correctly in Python + // (numbers are not valid identifier prefixes). + .value("N0", ImGuiKey_0) + .value("N1", ImGuiKey_1) + .value("N2", ImGuiKey_2) + .value("N3", ImGuiKey_3) + .value("N4", ImGuiKey_4) + .value("N5", ImGuiKey_5) + .value("N6", ImGuiKey_6) + .value("N7", ImGuiKey_7) + .value("N8", ImGuiKey_8) + .value("N9", ImGuiKey_9) + .value("A", ImGuiKey_A) + .value("B", ImGuiKey_B) + .value("C", ImGuiKey_C) + .value("D", ImGuiKey_D) + .value("E", ImGuiKey_E) + .value("F", ImGuiKey_F) + .value("G", ImGuiKey_G) + .value("H", ImGuiKey_H) + .value("I", ImGuiKey_I) + .value("J", ImGuiKey_J) + .value("K", ImGuiKey_K) + .value("L", ImGuiKey_L) + .value("M", ImGuiKey_M) + .value("N", ImGuiKey_N) + .value("O", ImGuiKey_O) + .value("P", ImGuiKey_P) + .value("Q", ImGuiKey_Q) + .value("R", ImGuiKey_R) + .value("S", ImGuiKey_S) + .value("T", ImGuiKey_T) + .value("U", ImGuiKey_U) + .value("V", ImGuiKey_V) + .value("W", ImGuiKey_W) + .value("X", ImGuiKey_X) + .value("Y", ImGuiKey_Y) + .value("Z", ImGuiKey_Z) + .value("F1", ImGuiKey_F1) + .value("F2", ImGuiKey_F2) + .value("F3", ImGuiKey_F3) + .value("F4", ImGuiKey_F4) + .value("F5", ImGuiKey_F5) + .value("F6", ImGuiKey_F6) + .value("F7", ImGuiKey_F7) + .value("F8", ImGuiKey_F8) + .value("F9", ImGuiKey_F9) + .value("F10", ImGuiKey_F10) + .value("F11", ImGuiKey_F11) + .value("F12", ImGuiKey_F12) + .value("F13", ImGuiKey_F13) + .value("F14", ImGuiKey_F14) + .value("F15", ImGuiKey_F15) + .value("F16", ImGuiKey_F16) + .value("F17", ImGuiKey_F17) + .value("F18", ImGuiKey_F18) + .value("F19", ImGuiKey_F19) + .value("F20", ImGuiKey_F20) + .value("F21", ImGuiKey_F21) + .value("F22", ImGuiKey_F22) + .value("F23", ImGuiKey_F23) + .value("F24", ImGuiKey_F24) + .value("Apostrophe", ImGuiKey_Apostrophe) + .value("Comma", ImGuiKey_Comma) + .value("Minus", ImGuiKey_Minus) + .value("Period", ImGuiKey_Period) + .value("Slash", ImGuiKey_Slash) + .value("Semicolon", ImGuiKey_Semicolon) + .value("Equal", ImGuiKey_Equal) + .value("LeftBracket", ImGuiKey_LeftBracket) + .value("Backslash", ImGuiKey_Backslash) + .value("RightBracket", ImGuiKey_RightBracket) + .value("GraveAccent", ImGuiKey_GraveAccent) + .value("CapsLock", ImGuiKey_CapsLock) + .value("ScrollLock", ImGuiKey_ScrollLock) + .value("NumLock", ImGuiKey_NumLock) + .value("PrintScreen", ImGuiKey_PrintScreen) + .value("Pause", ImGuiKey_Pause) + .value("Keypad0", ImGuiKey_Keypad0) + .value("Keypad1", ImGuiKey_Keypad1) + .value("Keypad2", ImGuiKey_Keypad2) + .value("Keypad3", ImGuiKey_Keypad3) + .value("Keypad4", ImGuiKey_Keypad4) + .value("Keypad5", ImGuiKey_Keypad5) + .value("Keypad6", ImGuiKey_Keypad6) + .value("Keypad7", ImGuiKey_Keypad7) + .value("Keypad8", ImGuiKey_Keypad8) + .value("Keypad9", ImGuiKey_Keypad9) + .value("KeypadDecimal", ImGuiKey_KeypadDecimal) + .value("KeypadDivide", ImGuiKey_KeypadDivide) + .value("KeypadMultiply", ImGuiKey_KeypadMultiply) + .value("KeypadSubtract", ImGuiKey_KeypadSubtract) + .value("KeypadAdd", ImGuiKey_KeypadAdd) + .value("KeypadEnter", ImGuiKey_KeypadEnter) + .value("KeypadEqual", ImGuiKey_KeypadEqual) + .value("AppBack", ImGuiKey_AppBack) + .value("AppForward", ImGuiKey_AppForward) + .value("GamepadStart", ImGuiKey_GamepadStart) + .value("GamepadBack", ImGuiKey_GamepadBack) + .value("GamepadFaceLeft", ImGuiKey_GamepadFaceLeft) + .value("GamepadFaceRight", ImGuiKey_GamepadFaceRight) + .value("GamepadFaceUp", ImGuiKey_GamepadFaceUp) + .value("GamepadFaceDown", ImGuiKey_GamepadFaceDown) + .value("GamepadDpadLeft", ImGuiKey_GamepadDpadLeft) + .value("GamepadDpadRight", ImGuiKey_GamepadDpadRight) + .value("GamepadDpadUp", ImGuiKey_GamepadDpadUp) + .value("GamepadDpadDown", ImGuiKey_GamepadDpadDown) + .value("GamepadL1", ImGuiKey_GamepadL1) + .value("GamepadR1", ImGuiKey_GamepadR1) + .value("GamepadL2", ImGuiKey_GamepadL2) + .value("GamepadR2", ImGuiKey_GamepadR2) + .value("GamepadL3", ImGuiKey_GamepadL3) + .value("GamepadR3", ImGuiKey_GamepadR3) + .value("GamepadLStickLeft", ImGuiKey_GamepadLStickLeft) + .value("GamepadLStickRight", ImGuiKey_GamepadLStickRight) + .value("GamepadLStickUp", ImGuiKey_GamepadLStickUp) + .value("GamepadLStickDown", ImGuiKey_GamepadLStickDown) + .value("GamepadRStickLeft", ImGuiKey_GamepadRStickLeft) + .value("GamepadRStickRight", ImGuiKey_GamepadRStickRight) + .value("GamepadRStickUp", ImGuiKey_GamepadRStickUp) + .value("GamepadRStickDown", ImGuiKey_GamepadRStickDown) + .value("MouseLeft", ImGuiKey_MouseLeft) + .value("MouseRight", ImGuiKey_MouseRight) + .value("MouseMiddle", ImGuiKey_MouseMiddle) + .value("MouseX1", ImGuiKey_MouseX1) + .value("MouseX2", ImGuiKey_MouseX2) + .value("MouseWheelX", ImGuiKey_MouseWheelX) + .value("MouseWheelY", ImGuiKey_MouseWheelY) + .value("ReservedForModCtrl", ImGuiKey_ReservedForModCtrl) + .value("ReservedForModShift", ImGuiKey_ReservedForModShift) + .value("ReservedForModAlt", ImGuiKey_ReservedForModAlt) + .value("ReservedForModSuper", ImGuiKey_ReservedForModSuper) + .value("Ctrl", ImGuiMod_Ctrl) + .value("Shift", ImGuiMod_Shift) + .value("Alt", ImGuiMod_Alt) + .value("Super", ImGuiMod_Super) + .value("Mask", ImGuiMod_Mask_); + + // Functions. + // + // Most functions can be bound directly with no custom implementation. A few + // overloaded functions are bound with unique names to prevent ambiguities + // on the python side. Which function is given an alternative name is more + // or less arbitrary. + // + // Functions that take pointers require custom implementations. Pointers are + // usually used as either in/out parameters or lists. For in/out parameters, + // we return a tuple containing the return value of the function and the + // updated value stored in the pointer. For lists, we accept a std::vector + // of the corresponding type and pass the raw array into the ImGui functions. + // + // Any printf-like function (that took a format + va_list) instead simply + // takes a string argument, on the assumption that the formatting will be done + // in python. + // + // Note: this list of functions isn't guaranteed to be complete. We ignore all + // rendering specific functions as well as debug functions and anything else + // that we feel isn't worth the effort to support. + + DEF0_F(ShowDemoWindow, { + bool open = true; + ImGui::ShowDemoWindow(&open); + return open; + }); + + DEF3_F(Begin, (ImString, name, ), (bool*, p_open, = nullptr), (ImGuiWindowFlags, flags, = 0), { + const auto result = ImGui::Begin(name, p_open, flags); + return std::make_tuple(result, *p_open); + }); + DEF0(End); + DEF4(BeginChild, (ImString, str_id, ), (const ImVec2&, size, = ImVec2_Zero), (ImGuiChildFlags, child_flags, = 0), (ImGuiWindowFlags, window_flags, = 0)); + DEF4_AS(BeginChild, BeginChildId, (ImGuiID, id, ), (const ImVec2&, size, = ImVec2_Zero), (ImGuiChildFlags, child_flags, = 0), (ImGuiWindowFlags, window_flags, = 0)); + DEF0(EndChild); + DEF0(IsWindowAppearing); + DEF0(IsWindowCollapsed); + DEF1(IsWindowFocused, (ImGuiFocusedFlags, flags, = 0)); + DEF1(IsWindowHovered, (ImGuiHoveredFlags, flags, = 0)); + DEF0(GetWindowDpiScale); + DEF0(GetWindowPos); + DEF0(GetWindowSize); + DEF0(GetWindowWidth); + DEF0(GetWindowHeight); + DEF3(SetNextWindowPos, (const ImVec2&, pos, ), (ImGuiCond, cond, = 0), (const ImVec2&, pivot, = ImVec2_Zero)); + DEF2(SetNextWindowSize, (const ImVec2&, size, ), (ImGuiCond, cond, = 0)); + DEF1(SetNextWindowContentSize, (const ImVec2&, size, )); + DEF2(SetNextWindowCollapsed, (bool, collapsed, ), (ImGuiCond, cond, = 0)); + DEF0(SetNextWindowFocus); + DEF1(SetNextWindowScroll, (const ImVec2&, scroll, )); + DEF1(SetNextWindowBgAlpha, (float, alpha, )); + DEF1(SetNextWindowViewport, (ImGuiID, viewport_id, )); + DEF2(SetWindowPos, (const ImVec2&, pos, ), (ImGuiCond, cond, = 0)); + DEF2(SetWindowSize, (const ImVec2&, size, ), (ImGuiCond, cond, = 0)); + DEF2(SetWindowCollapsed, (bool, collapsed, ), (ImGuiCond, cond, = 0)); + DEF0(SetWindowFocus); + DEF1(SetWindowFontScale, (float, scale, )); + DEF3(SetWindowPos, (ImString, name, ), (const ImVec2&, pos, ), (ImGuiCond, cond, = 0)); + DEF3(SetWindowSize, (ImString, name, ), (const ImVec2&, size, ), (ImGuiCond, cond, = 0)); + DEF3(SetWindowCollapsed, (ImString, name, ), (bool, collapsed, ), (ImGuiCond, cond, = 0)); + DEF1(SetWindowFocus, (ImString, name, )); + DEF0(GetContentRegionAvail); + DEF0(GetContentRegionMax); + DEF0(GetWindowContentRegionMin); + DEF0(GetWindowContentRegionMax); + DEF0(GetScrollX); + DEF0(GetScrollY); + DEF1(SetScrollX, (float, scroll_x, )); + DEF1(SetScrollY, (float, scroll_y, )); + DEF0(GetScrollMaxX); + DEF0(GetScrollMaxY); + DEF1(SetScrollHereX, (float, center_x_ratio, = 0.5f)); + DEF1(SetScrollHereY, (float, center_y_ratio, = 0.5f)); + DEF2(SetScrollFromPosX, (float, local_x, ), (float, center_x_ratio, = 0.5f)); + DEF2(SetScrollFromPosY, (float, local_y, ), (float, center_y_ratio, = 0.5f)); + DEF0(PopFont); + DEF2(PushStyleColor, (ImGuiCol, idx, ), (ImU32, col, )); + DEF2(PushStyleColor, (ImGuiCol, idx, ), (const ImVec4&, col, )); + DEF1(PopStyleColor, (int, count, = 1)); + DEF2(PushStyleVar, (ImGuiStyleVar, idx, ), (float, val, )); + DEF2(PushStyleVar, (ImGuiStyleVar, idx, ), (const ImVec2&, val, )); + DEF1(PopStyleVar, (int, count, = 1)); + DEF1(PushTabStop, (bool, tab_stop, )); + DEF0(PopTabStop); + DEF1(PushButtonRepeat, (bool, repeat, )); + DEF0(PopButtonRepeat); + DEF1(PushItemWidth, (float, item_width, )); + DEF0(PopItemWidth); + DEF1(SetNextItemWidth, (float, item_width, )); + DEF0(CalcItemWidth); + DEF1(PushTextWrapPos, (float, wrap_local_pos_x, = 0.0f)); + DEF0(PopTextWrapPos); + DEF0(GetFontSize); + DEF0(GetFontTexUvWhitePixel); + DEF2(GetColorU32, (ImGuiCol, idx, ), (float, alpha_mul, = 1.0f)); + DEF1(GetColorU32, (const ImVec4&, col, )); + DEF2(GetColorU32, (ImU32, col, ), (float, alpha_mul, = 1.0f)); + DEF1(GetStyleColorVec4, (ImGuiCol, idx, )); + DEF0(GetCursorScreenPos); + DEF1(SetCursorScreenPos, (const ImVec2&, pos, )); + DEF0(GetCursorPos); + DEF0(GetCursorPosX); + DEF0(GetCursorPosY); + DEF1(SetCursorPos, (const ImVec2&, local_pos, )); + DEF1(SetCursorPosX, (float, local_x, )); + DEF1(SetCursorPosY, (float, local_y, )); + DEF0(GetCursorStartPos); + DEF0(Separator); + DEF2(SameLine, (float, offset_from_start_x, = 0.0f), (float, spacing, = -1.0f)); + DEF0(NewLine); + DEF0(Spacing); + DEF1(Dummy, (const ImVec2&, size, )); + DEF1(Indent, (float, indent_w, = 0.0f)); + DEF1(Unindent, (float, indent_w, = 0.0f)); + DEF0(BeginGroup); + DEF0(EndGroup); + DEF0(AlignTextToFramePadding); + DEF0(GetTextLineHeight); + DEF0(GetTextLineHeightWithSpacing); + DEF0(GetFrameHeight); + DEF0(GetFrameHeightWithSpacing); + DEF1(PushID, (ImString, str_id, )); + DEF2(PushID, (ImString, str_id_begin, ), (ImString, str_id_end, )); + DEF1(PushID, (int, int_id, )); + DEF0(PopID); + DEF1(GetID, (ImString, str_id, )); + DEF2(GetID, (ImString, str_id_begin, ), (ImString, str_id_end, )); + DEF1_F(TextUnformatted, (ImString, txt, ), { + ImGui::TextUnformatted(txt); + }); + DEF1_F(Text, (ImString, txt, ), { + return ImGui::Text("%s", txt); + }); + DEF2_F(TextColored, (const ImVec4&, col, ), (ImString, txt, ), { + return ImGui::TextColored(col, "%s", txt); + }); + DEF1_F(TextDisabled, (ImString, txt, ), { + return ImGui::TextDisabled("%s", txt); + }); + DEF1_F(TextWrapped, (ImString, txt, ), { + return ImGui::TextWrapped("%s", txt); + }); + DEF2_F(LabelText, (ImString, label, ), (ImString, txt, ), { + return ImGui::LabelText(label, "%s", txt); + }); + DEF1_F(BulletText, (ImString, txt, ), { + return ImGui::BulletText("%s", txt); + }); + DEF1(SeparatorText, (ImString, label, )); + DEF2(Button, (ImString, label, ), (const ImVec2&, size, = ImVec2_Zero)); + DEF1(SmallButton, (ImString, label, )); + DEF3(InvisibleButton, (ImString, str_id, ), (const ImVec2&, size, ), (ImGuiButtonFlags, flags, = 0)); + DEF2(ArrowButton, (ImString, str_id, ), (ImGuiDir, dir, )); + DEF2_F(Checkbox, (ImString, label, ), (bool*, v, ), { + auto result = ImGui::Checkbox(label, v); + return std::make_tuple(result, *v); + }); + DEF2(RadioButton, (ImString, label, ), (bool, active, )); + DEF3(ProgressBar, (float, fraction, ), (const ImVec2&, size_arg, = ImVec2_Min_Zero), (ImString, overlay, = nullptr)); + DEF0(Bullet); + DEF1(TextLink, (ImString, label, )); + DEF2(TextLinkOpenURL, (ImString, label, ), (ImString, url, = nullptr)); + DEF6_F(Image, (long, user_texture_id, ), (const ImVec2&, image_size, ), (const ImVec2&, uv0, = ImVec2_Zero), (const ImVec2&, uv1, = ImVec2_One), (const ImVec4&, tint_col, = ImVec4_One), (const ImVec4&, border_col, = ImVec4_Zero), { + return ImGui::Image(reinterpret_cast(user_texture_id), image_size, uv0, uv1, tint_col, border_col); + }); + + DEF7_F(ImageButton, (ImString, str_id, ), (long, user_texture_id, ), (const ImVec2&, image_size, ), (const ImVec2&, uv0, = ImVec2_Zero), (const ImVec2&, uv1, = ImVec2_One), (const ImVec4&, bg_col, = ImVec4_Zero), (const ImVec4&, tint_col, = ImVec4_One), { + return ImGui::ImageButton(str_id, reinterpret_cast(user_texture_id), image_size, uv0, uv1, bg_col, tint_col); + }); + DEF3(BeginCombo, (ImString, label, ), (ImString, preview_value, ), (ImGuiComboFlags, flags, = 0)); + DEF0(EndCombo); + m.def( + "Combo", + [](const char* label, int current_item, std::vector items, + int popup_max_height_in_items) { + std::vector items_ptr; + items_ptr.reserve(items.size()); + for (const auto& item : items) { + items_ptr.push_back(item.c_str()); + } + const auto result = + ImGui::Combo(label, ¤t_item, items_ptr.data(), + items_ptr.size(), popup_max_height_in_items); + return std::make_tuple(result, current_item); + }, + py::arg("label"), py::arg("current_item"), py::arg("items"), + py::arg("popup_max_height_in_items") = -1); + m.def( + "ComboStr", + [](const char* label, int current_item, + const char* items_separated_by_zeros, int popup_max_height_in_items) { + const auto result = + ImGui::Combo(label, ¤t_item, items_separated_by_zeros, + popup_max_height_in_items); + return std::make_tuple(result, current_item); + }, + py::arg("label"), py::arg("current_item"), + py::arg("items_separated_by_zeros"), + py::arg("popup_max_height_in_items") = -1); + DEF7_F(DragFloat, (ImString, label, ), (float*, v, ), (float, v_speed, = 1.0f), (float, v_min, = 0.0f), (float, v_max, = 0.0f), (ImString, format, = "%.3f"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::DragFloat(label, v, v_speed, v_min, v_max, format, flags); + return std::make_tuple(result, *v); + }); + DEF7_F(DragFloatN, (ImString, label, ), (std::vector, v, ), (float, v_speed, = 1.0f), (float, v_min, = 0.0f), (float, v_max, = 0.0f), (ImString, format, = "%.3f"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::DragScalarN(label, ImGuiDataType_Float, v.data(), v.size(), v_speed, &v_min, &v_max, format, flags); + return std::make_tuple(result, v); + }); + DEF7_F(DragInt, (ImString, label, ), (int*, v, ), (float, v_speed, = 1.0f), (int, v_min, = 0), (int, v_max, = 0), (ImString, format, = "%d"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::DragInt(label, v, v_speed, v_min, v_max, format, flags); + return std::make_tuple(result, *v); + }); + DEF7_F(DragIntN, (ImString, label, ), (std::vector, v, ), (float, v_speed, = 1.0f), (int, v_min, = 0), (int, v_max, = 0), (ImString, format, = "%d"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::DragScalarN(label, ImGuiDataType_S32, v.data(), v.size(), v_speed, &v_min, &v_max, format, flags); + return std::make_tuple(result, v); + }); + DEF6_F(SliderFloat, (ImString, label, ), (float*, v, ), (float, v_min, ), (float, v_max, ), (ImString, format, = "%.3f"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::SliderFloat(label, v, v_min, v_max, format, flags); + return std::make_tuple(result, *v); + }); + DEF6_F(SliderFloatN, (ImString, label, ), (std::vector, v, ), (float, v_min, ), (float, v_max, ), (ImString, format, = "%.3f"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::SliderScalarN(label, ImGuiDataType_Float, v.data(), v.size(), &v_min, &v_max, format, flags); + return std::make_tuple(result, v); + }); + DEF6_F(SliderAngle, (ImString, label, ), (float*, v_rad, ), (float, v_degrees_min, = -360.0f), (float, v_degrees_max, = +360.0f), (ImString, format, = "%.0f deg"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::SliderAngle(label, v_rad, v_degrees_min, v_degrees_max, format, flags); + return std::make_tuple(result, *v_rad); + }); + DEF6_F(SliderInt, (ImString, label, ), (int*, v, ), (int, v_min, ), (int, v_max, ), (ImString, format, = "%d"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::SliderInt(label, v, v_min, v_max, format, flags); + return std::make_tuple(result, *v); + }); + DEF6_F(SliderIntN, (ImString, label, ), (std::vector, v, ), (int, v_min, ), (int, v_max, ), (ImString, format, = "%d"), (ImGuiSliderFlags, flags, = 0), { + const auto result = ImGui::SliderScalarN(label, ImGuiDataType_S32, v.data(), v.size(), &v_min, &v_max, format, flags); + return std::make_tuple(result, v); + }); + DEF6_F(InputFloat, (ImString, label, ), (float*, v, ), (float, step, = 0.0f), (float, step_fast, = 0.0f), (ImString, format, = "%.3f"), (ImGuiInputTextFlags, flags, = 0), { + const auto result = ImGui::InputFloat(label, v, step, step_fast, format, flags); + return std::make_tuple(result, *v); + }); + DEF4_F(InputFloatN, (ImString, label, ), (std::vector, v, ), (ImString, format, = "%.3f"), (ImGuiInputTextFlags, flags, = 0), { + const auto result = ImGui::InputScalarN(label, ImGuiDataType_Float, v.data(), v.size(), NULL, NULL, format, flags); + return std::make_tuple(result, v); + }); + DEF5_F(InputInt, (ImString, label, ), (int*, v, ), (int, step, = 1), (int, step_fast, = 100), (ImGuiInputTextFlags, flags, = 0), { + const auto result = ImGui::InputInt(label, v, step, step_fast, flags); + return std::make_tuple(result, *v); + }); + DEF3_F(InputIntN, (ImString, label, ), (std::vector, v, ), (ImGuiInputTextFlags, flags, = 0), { + const auto result = ImGui::InputScalarN(label, ImGuiDataType_S32, v.data(), v.size(), NULL, NULL, "%d", flags); + return std::make_tuple(result, v); + }); + DEF6_F(InputDouble, (ImString, label, ), (double*, v, ), (double, step, = 0.0), (double, step_fast, = 0.0), (ImString, format, = "%.6f"), (ImGuiInputTextFlags, flags, = 0), { + const auto result = ImGui::InputDouble(label, v, step, step_fast, format, flags); + return std::make_tuple(result, *v); + }); + DEF3_F(InputText, (ImString, label, ), (std::string, text, ), (ImGuiInputTextFlags, flags, = 0), { + const auto result = ImGui::InputText(label, &text, flags); + return std::make_tuple(result, text); + }); + DEF4_F(InputTextMultiline, (ImString, label, ), (std::string, text, ), (const ImVec2&, size, = ImVec2_Zero), (ImGuiInputTextFlags, flags, = 0), { + const auto result = ImGui::InputTextMultiline(label, &text, size, flags); + return std::make_tuple(result, text); + }); + DEF4_F(InputTextWithHint, (ImString, label, ), (ImString, hint, ), (std::string, text, ), (ImGuiInputTextFlags, flags, = 0), { + const auto result = ImGui::InputTextWithHint(label, hint, &text, flags); + return std::make_tuple(result, text); + }); + DEF3_F(ColorEdit3, (ImString, label, ), (std::vector, col, ), (ImGuiColorEditFlags, flags, = 0), { + const auto result = ImGui::ColorEdit3(label, col.data(), flags); + return std::make_tuple(result, col); + }); + DEF3_F(ColorEdit4, (ImString, label, ), (std::vector, col, ), (ImGuiColorEditFlags, flags, = 0), { + const auto result = ImGui::ColorEdit4(label, col.data(), flags); + return std::make_tuple(result, col); + }); + DEF3_F(ColorPicker3, (ImString, label, ), (std::vector, col, ), (ImGuiColorEditFlags, flags, = 0), { + const auto result = ImGui::ColorPicker3(label, col.data(), flags); + return std::make_tuple(result, col); + }); + DEF4_F(ColorPicker4, (ImString, label, ), (std::vector, col, ), (ImGuiColorEditFlags, flags, = 0), (const float*, ref_col, = nullptr), { + const auto result = ImGui::ColorPicker4(label, col.data(), flags, ref_col); + return std::make_tuple(result, col); + }); + DEF4(ColorButton, (ImString, desc_id, ), (const ImVec4&, col, ), (ImGuiColorEditFlags, flags, = 0), (const ImVec2&, size, = ImVec2_Zero)); + DEF1(SetColorEditOptions, (ImGuiColorEditFlags, flags, )); + DEF1(TreeNode, (ImString, label, )); + DEF2_F(TreeNode, (ImString, str_id, ), (ImString, txt, ), { + return ImGui::TreeNode(str_id, "%s", txt); + }); + DEF2(TreeNodeEx, (ImString, label, ), (ImGuiTreeNodeFlags, flags, = 0)); + DEF3_F(TreeNodeEx, (ImString, str_id, ), (ImGuiTreeNodeFlags, flags, ), (ImString, txt, ), { + return ImGui::TreeNodeEx(str_id, flags, "%s", txt); + }); + DEF1(TreePush, (ImString, str_id, )); + DEF0(TreePop); + DEF0(GetTreeNodeToLabelSpacing); + DEF2(CollapsingHeader, (ImString, label, ), (ImGuiTreeNodeFlags, flags, = 0)); + DEF3_F(CollapsingHeader2, (ImString, label, ), (bool*, p_visible, ), (ImGuiTreeNodeFlags, flags, = 0), { + const auto result = ImGui::CollapsingHeader(label, p_visible, flags); + return std::make_tuple(result, *p_visible); + }); + DEF2(SetNextItemOpen, (bool, is_open, ), (ImGuiCond, cond, = 0)); + DEF4(Selectable, (ImString, label, ), (bool, selected, = false), (ImGuiSelectableFlags, flags, = 0), (const ImVec2&, size, = ImVec2_Zero)); + DEF4_F(Selectable2, (ImString, label, ), (bool*, p_selected, ), (ImGuiSelectableFlags, flags, = 0), (const ImVec2&, size, = ImVec2_Zero), { + const auto result = ImGui::Selectable(label, p_selected, flags, size); + return std::make_tuple(result, *p_selected); + }); + DEF2(BeginListBox, (ImString, label, ), (const ImVec2&, size, = ImVec2_Zero)); + DEF0(EndListBox); + DEF2(Value, (ImString, prefix, ), (bool, b, )); + DEF2(Value, (ImString, prefix, ), (int, v, )); + DEF2(Value, (ImString, prefix, ), (unsigned int, v, )); + DEF3(Value, (ImString, prefix, ), (float, v, ), (ImString, float_format, = nullptr)); + DEF0(BeginMenuBar); + DEF0(EndMenuBar); + DEF0(BeginMainMenuBar); + DEF0(EndMainMenuBar); + DEF2(BeginMenu, (ImString, label, ), (bool, enabled, = true)); + DEF0(EndMenu); + DEF4(MenuItem, (ImString, label, ), (ImString, shortcut, = nullptr), (bool, selected, = false), (bool, enabled, = true)); + DEF4_F(MenuItem, (ImString, label, ), (ImString, shortcut, ), (bool*, p_selected, ), (bool, enabled, = true), { + const auto result = ImGui::MenuItem(label, shortcut, p_selected, enabled); + return std::make_tuple(result, *p_selected); + }); + DEF0(BeginTooltip); + DEF0(EndTooltip); + DEF1_F(SetTooltip, (ImString, txt, ), { + return ImGui::SetTooltip("%s", txt); + }); + DEF0(BeginItemTooltip); + DEF1_F(SetItemTooltip, (ImString, txt, ), { + return ImGui::SetItemTooltip("%s", txt); + }); + DEF2(BeginPopup, (ImString, str_id, ), (ImGuiWindowFlags, flags, = 0)); + DEF3_F(BeginPopupModal, (ImString, name, ), (bool*, p_open, = nullptr), (ImGuiWindowFlags, flags, = 0), { + const auto result = ImGui::BeginPopupModal(name, p_open, flags); + return std::make_tuple(result, *p_open); + }); + DEF0(EndPopup); + DEF2(OpenPopup, (ImString, str_id, ), (ImGuiPopupFlags, popup_flags, = 0)); + DEF2(OpenPopup, (ImGuiID, id, ), (ImGuiPopupFlags, popup_flags, = 0)); + DEF2(OpenPopupOnItemClick, (ImString, str_id, = nullptr), (ImGuiPopupFlags, popup_flags, = 0)); + DEF0(CloseCurrentPopup); + DEF2(BeginPopupContextItem, (ImString, str_id, = nullptr), (ImGuiPopupFlags, popup_flags, = 0)); + DEF2(BeginPopupContextWindow, (ImString, str_id, = nullptr), (ImGuiPopupFlags, popup_flags, = 0)); + DEF2(BeginPopupContextVoid, (ImString, str_id, = nullptr), (ImGuiPopupFlags, popup_flags, = 0)); + DEF2(IsPopupOpen, (ImString, str_id, ), (ImGuiPopupFlags, flags, = 0)); + DEF5(BeginTable, (ImString, str_id, ), (int, columns, ), (ImGuiTableFlags, flags, = 0), (const ImVec2&, outer_size, = ImVec2_Zero), (float, inner_width, = 0.0f)); + DEF0(EndTable); + DEF2(TableNextRow, (ImGuiTableRowFlags, row_flags, = 0), (float, min_row_height, = 0.0f)); + DEF0(TableNextColumn); + DEF1(TableSetColumnIndex, (int, column_n, )); + DEF4(TableSetupColumn, (ImString, label, ), (ImGuiTableColumnFlags, flags, = 0), (float, init_width_or_weight, = 0.0f), (ImGuiID, user_id, = 0)); + DEF2(TableSetupScrollFreeze, (int, cols, ), (int, rows, )); + DEF1(TableHeader, (ImString, label, )); + DEF0(TableHeadersRow); + DEF0(TableAngledHeadersRow); + DEF0(TableGetColumnCount); + DEF0(TableGetColumnIndex); + DEF0(TableGetRowIndex); + DEF1(TableGetColumnName, (int, column_n, = -1)); + DEF1(TableGetColumnFlags, (int, column_n, = -1)); + DEF2(TableSetColumnEnabled, (int, column_n, ), (bool, v, )); + DEF0(TableGetHoveredColumn); + DEF3(TableSetBgColor, (ImGuiTableBgTarget, target, ), (ImU32, color, ), (int, column_n, = -1)); + DEF3(Columns, (int, count, = 1), (ImString, id, = nullptr), (bool, border, = true)); + DEF0(NextColumn); + DEF0(GetColumnIndex); + DEF1(GetColumnWidth, (int, column_index, = -1)); + DEF2(SetColumnWidth, (int, column_index, ), (float, width, )); + DEF1(GetColumnOffset, (int, column_index, = -1)); + DEF2(SetColumnOffset, (int, column_index, ), (float, offset_x, )); + DEF0(GetColumnsCount); + DEF2(BeginTabBar, (ImString, str_id, ), (ImGuiTabBarFlags, flags, = 0)); + DEF0(EndTabBar); + DEF3_F(BeginTabItem, (ImString, label, ), (bool*, p_open, ), (ImGuiTabItemFlags, flags, = 0), { + const auto result = ImGui::BeginTabItem(label, p_open, flags); + return std::make_tuple(result, p_open ? *p_open : true); + }); + // Convenience overload for non-closable tab items. + // This function returns a boolean so you can call it directly in an if condition. + DEF2_F(BeginTabItem, (ImString, label, ), (ImGuiTabItemFlags, flags, = 0), { + return ImGui::BeginTabItem(label, nullptr, flags); + }); + DEF0(EndTabItem); + DEF2(TabItemButton, (ImString, label, ), (ImGuiTabItemFlags, flags, = 0)); + DEF1(SetTabItemClosed, (ImString, tab_or_docked_window_label, )); + DEF2(SetNextWindowDockID, (ImGuiID, dock_id, ), (ImGuiCond, cond, = 0)); + DEF0(GetWindowDockID); + DEF0(IsWindowDocked); + DEF1(BeginDisabled, (bool, disabled, = true)); + DEF0(EndDisabled); + DEF3(PushClipRect, (const ImVec2&, clip_rect_min, ), (const ImVec2&, clip_rect_max, ), (bool, intersect_with_current_clip_rect, )); + DEF0(PopClipRect); + DEF0(SetItemDefaultFocus); + DEF1(SetKeyboardFocusHere, (int, offset, = 0)); + DEF0(SetNextItemAllowOverlap); + DEF1(IsItemHovered, (ImGuiHoveredFlags, flags, = 0)); + DEF0(IsItemActive); + DEF0(IsItemFocused); + DEF1(IsItemClicked, (ImGuiMouseButton, mouse_button, = 0)); + DEF0(IsItemVisible); + DEF0(IsItemEdited); + DEF0(IsItemActivated); + DEF0(IsItemDeactivated); + DEF0(IsItemDeactivatedAfterEdit); + DEF0(IsItemToggledOpen); + DEF0(IsAnyItemHovered); + DEF0(IsAnyItemActive); + DEF0(IsAnyItemFocused); + DEF0(GetItemID); + DEF0(GetItemRectMin); + DEF0(GetItemRectMax); + DEF0(GetItemRectSize); + DEF1(IsRectVisible, (const ImVec2&, size, )); + DEF2(IsRectVisible, (const ImVec2&, rect_min, ), (const ImVec2&, rect_max, )); + DEF0(GetTime); + DEF0(GetFrameCount); + DEF4(CalcTextSize, (ImString, text, ), (ImString, text_end, = nullptr), (bool, hide_text_after_double_hash, = false), (float, wrap_width, = -1.0f)); + DEF1(ColorConvertU32ToFloat4, (ImU32, in, )); + DEF1(ColorConvertFloat4ToU32, (const ImVec4&, in, )); + DEF6(ColorConvertRGBtoHSV, (float, r, ), (float, g, ), (float, b, ), (float&, out_h, ), (float&, out_s, ), (float&, out_v, )); + DEF6(ColorConvertHSVtoRGB, (float, h, ), (float, s, ), (float, v, ), (float&, out_r, ), (float&, out_g, ), (float&, out_b, )); + DEF1(IsKeyDown, (ImGuiKey, key, )); + DEF2(IsKeyPressed, (ImGuiKey, key, ), (bool, repeat, = true)); + DEF1(IsKeyReleased, (ImGuiKey, key, )); + DEF1(IsKeyChordPressed, (ImGuiKeyChord, key_chord, )); + DEF3(GetKeyPressedAmount, (ImGuiKey, key, ), (float, repeat_delay, ), (float, rate, )); + DEF1(GetKeyName, (ImGuiKey, key, )); + DEF1(SetNextFrameWantCaptureKeyboard, (bool, want_capture_keyboard, )); + DEF2(Shortcut, (ImGuiKeyChord, key_chord, ), (ImGuiInputFlags, flags, = 0)); + DEF2(SetNextItemShortcut, (ImGuiKeyChord, key_chord, ), (ImGuiInputFlags, flags, = 0)); + DEF1(IsMouseDown, (ImGuiMouseButton, button, )); + DEF2(IsMouseClicked, (ImGuiMouseButton, button, ), (bool, repeat, = false)); + DEF1(IsMouseReleased, (ImGuiMouseButton, button, )); + DEF1(IsMouseDoubleClicked, (ImGuiMouseButton, button, )); + DEF1(GetMouseClickedCount, (ImGuiMouseButton, button, )); + DEF3(IsMouseHoveringRect, (const ImVec2&, r_min, ), (const ImVec2&, r_max, ), (bool, clip, = true)); + DEF1(IsMousePosValid, (const ImVec2*, mouse_pos, = nullptr)); + DEF0(IsAnyMouseDown); + DEF0(GetMousePos); + DEF0(GetMousePosOnOpeningCurrentPopup); + DEF2(IsMouseDragging, (ImGuiMouseButton, button, ), (float, lock_threshold, = -1.0f)); + DEF2(GetMouseDragDelta, (ImGuiMouseButton, button, = 0), (float, lock_threshold, = -1.0f)); + DEF1(ResetMouseDragDelta, (ImGuiMouseButton, button, = 0)); + DEF0(GetMouseCursor); + DEF1(SetMouseCursor, (ImGuiMouseCursor, cursor_type, )); + DEF1(SetNextFrameWantCaptureMouse, (bool, want_capture_mouse, )); + DEF0(GetClipboardText); + DEF1(SetClipboardText, (ImString, text, )); +} + +// NOLINTEND(whitespace/line_length) diff --git a/python/mujoco/experimental/dear_imgui/dear_imgui_macros.h b/python/mujoco/experimental/dear_imgui/dear_imgui_macros.h new file mode 100644 index 00000000..159b81e5 --- /dev/null +++ b/python/mujoco/experimental/dear_imgui/dear_imgui_macros.h @@ -0,0 +1,326 @@ +// Copyright 2026 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MUJOCO_PYTHON_EXPERIMENTAL_DEAR_IMGUI_DEAR_IMGUI_MACROS_H_ +#define MUJOCO_PYTHON_EXPERIMENTAL_DEAR_IMGUI_DEAR_IMGUI_MACROS_H_ + +// WARNING: This file is intended for internal use by dear_imgui libraries ONLY! +// +// The macros defined here use short, generic names (DEF0, ARG_ID, etc.) and +// are NOT #undef'd. Including this header elsewhere may cause naming conflicts. +// +// Define NAMESPACE to be the ImGui library you're binding before including this +// header, e.g. #define NAMESPACE ImGui +// +// ============================================================================ +// Quick Reference +// ============================================================================ +// +// DEFn(Name, Args...) // Binds NAMESPACE::Name as Name in Python +// DEFn_AS(CppName, PyName, Args...) // Binds NAMESPACE::CppName as PyName +// DEFn_F(PyName, Args..., { CppBody }) // Custom implementation +// +// Where 'n' is the number of arguments (0-9). +// +// ============================================================================ +// Argument Format +// ============================================================================ +// +// Each argument is a tuple: (Type, name, DefaultValue) +// +// - No default value: (ImString, label, ) // trailing comma required +// - With default value: (int, flags, = 0) // include the '=' +// - Complex defaults: (const ImVec2&, size, = ImVec2_Zero) +// +// NOTE: Default values cannot contain commas. Use predefined constants like +// ImVec2_Zero, ImVec4_One, etc. +// +// ============================================================================ +// Examples (from dear_imgui.cc) +// ============================================================================ +// +// Simple binding - NAMESPACE::End() exposed as End(): +// DEF0(End); +// +// Binding with arguments: +// DEF4(BeginChild, +// (ImString, str_id, ), +// (const ImVec2&, size, = ImVec2_Zero), +// (ImGuiChildFlags, child_flags, = 0), +// (ImGuiWindowFlags, window_flags, = 0)); +// +// Overloaded function - NAMESPACE::BeginChild(ImGuiID) exposed as BeginChildId(): +// DEF4_AS(BeginChild, BeginChildId, +// (ImGuiID, id, ), +// (const ImVec2&, size, = ImVec2_Zero), +// (ImGuiChildFlags, child_flags, = 0), +// (ImGuiWindowFlags, window_flags, = 0)); +// +// Custom implementation using DEFn_F is needed when: +// +// 1. Variadic functions (e.g., Text, TextColored) +// +// C++ variadic functions (those with "...") cannot be bound directly +// because the type/count of arguments is unknown at compile time. Use a +// wrapper that calls the function with a fixed format. Also note that +// user-controlled format strings are a security risk (format string +// attacks). Always use "%s": +// +// DEF1_F(Text, (ImString, txt, ), { +// return NAMESPACE::Text("%s", txt); +// }); +// +// 2. Output pointer parameters (e.g., Checkbox, SliderFloat) +// +// Python doesn't have output pointers, so return modified values as a +// tuple: +// +// DEF2_F(Checkbox, (ImString, label, ), (bool*, v, ), { +// auto result = NAMESPACE::Checkbox(label, v); +// return std::make_tuple(result, *v); +// }); +// +// 3. Type conversions (e.g., Image, ImageButton) +// +// Some C++ types don't have Python equivalents. For example, ImTextureID +// is a void* (opaque pointer), which pybind11 can't automatically convert. +// Accept a Python-friendly type (like long) and cast it: +// +// DEF2_F(Image, (long, tex_id, ), (const ImVec2&, size, ), { +// return NAMESPACE::Image(reinterpret_cast(tex_id), size); +// }); + +// ============================================================================ +// Internal helper macros (not intended to be called directly by binding code) +// ============================================================================ + +// Extracts the type and name of an argument tuple. +// Example: ARG_DECL((float, alpha, = 1.0f)) -> float alpha +#define ARG_DECL_X(T_, N_, V_) T_ N_ +#define ARG_DECL(A_) ARG_DECL_X A_ + +// Extracts the identifier of an argument tuple. +// Example: ARG_ID((float, alpha, = 1.0f)) -> alpha +#define ARG_ID_X(T_, N_, V_) N_ +#define ARG_ID(A_) ARG_ID_X A_ + +// Extracts the name of an argument tuple as a quoted string literal. +// Example: ARG_NAME((float, alpha, = 1.0f)) -> "alpha" +#define ARG_NAME_X(T_, N_, V_) #N_ +#define ARG_NAME(A_) ARG_NAME_X A_ + +// Extracts the default value of an argument tuple. +// Example: ARG_DEFVAL((float, alpha, = 1.0f)) -> = 1.0f +#define ARG_DEFVAL_X(T_, N_, V_) V_ +#define ARG_DEFVAL(A_) ARG_DEFVAL_X A_ + +// ============================================================================ +// Public macros for binding code +// ============================================================================ + +// +#define DEF0_F(N, FN) \ + m.def(#N, []( \ + ) FN \ + ); + +#define DEF1_F(N, A1, FN) \ + m.def(#N, []( \ + ARG_DECL(A1) \ + ) FN, \ + py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1) \ + ); + +#define DEF2_F(N, A1, A2, FN) \ + m.def(#N, []( \ + ARG_DECL(A1), \ + ARG_DECL(A2) \ + ) FN, \ + py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \ + py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2) \ + ); + +#define DEF3_F(N, A1, A2, A3, FN) \ + m.def(#N, []( \ + ARG_DECL(A1), \ + ARG_DECL(A2), \ + ARG_DECL(A3) \ + ) FN, \ + py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \ + py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2), \ + py::arg(ARG_NAME(A3)) ARG_DEFVAL(A3) \ + ); + +#define DEF4_F(N, A1, A2, A3, A4, FN) \ + m.def(#N, []( \ + ARG_DECL(A1), \ + ARG_DECL(A2), \ + ARG_DECL(A3), \ + ARG_DECL(A4) \ + ) FN, \ + py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \ + py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2), \ + py::arg(ARG_NAME(A3)) ARG_DEFVAL(A3), \ + py::arg(ARG_NAME(A4)) ARG_DEFVAL(A4) \ + ); + +#define DEF5_F(N, A1, A2, A3, A4, A5, FN) \ + m.def(#N, []( \ + ARG_DECL(A1), \ + ARG_DECL(A2), \ + ARG_DECL(A3), \ + ARG_DECL(A4), \ + ARG_DECL(A5) \ + ) FN, \ + py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \ + py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2), \ + py::arg(ARG_NAME(A3)) ARG_DEFVAL(A3), \ + py::arg(ARG_NAME(A4)) ARG_DEFVAL(A4), \ + py::arg(ARG_NAME(A5)) ARG_DEFVAL(A5) \ + ); + +#define DEF6_F(N, A1, A2, A3, A4, A5, A6, FN) \ + m.def(#N, []( \ + ARG_DECL(A1), \ + ARG_DECL(A2), \ + ARG_DECL(A3), \ + ARG_DECL(A4), \ + ARG_DECL(A5), \ + ARG_DECL(A6) \ + ) FN, \ + py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \ + py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2), \ + py::arg(ARG_NAME(A3)) ARG_DEFVAL(A3), \ + py::arg(ARG_NAME(A4)) ARG_DEFVAL(A4), \ + py::arg(ARG_NAME(A5)) ARG_DEFVAL(A5), \ + py::arg(ARG_NAME(A6)) ARG_DEFVAL(A6) \ + ); + +#define DEF7_F(N, A1, A2, A3, A4, A5, A6, A7, FN) \ + m.def(#N, []( \ + ARG_DECL(A1), \ + ARG_DECL(A2), \ + ARG_DECL(A3), \ + ARG_DECL(A4), \ + ARG_DECL(A5), \ + ARG_DECL(A6), \ + ARG_DECL(A7) \ + ) FN, \ + py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \ + py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2), \ + py::arg(ARG_NAME(A3)) ARG_DEFVAL(A3), \ + py::arg(ARG_NAME(A4)) ARG_DEFVAL(A4), \ + py::arg(ARG_NAME(A5)) ARG_DEFVAL(A5), \ + py::arg(ARG_NAME(A6)) ARG_DEFVAL(A6), \ + py::arg(ARG_NAME(A7)) ARG_DEFVAL(A7) \ + ); + +#define DEF8_F(N, A1, A2, A3, A4, A5, A6, A7, A8, FN) \ + m.def(#N, []( \ + ARG_DECL(A1), \ + ARG_DECL(A2), \ + ARG_DECL(A3), \ + ARG_DECL(A4), \ + ARG_DECL(A5), \ + ARG_DECL(A6), \ + ARG_DECL(A7), \ + ARG_DECL(A8) \ + ) FN, \ + py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \ + py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2), \ + py::arg(ARG_NAME(A3)) ARG_DEFVAL(A3), \ + py::arg(ARG_NAME(A4)) ARG_DEFVAL(A4), \ + py::arg(ARG_NAME(A5)) ARG_DEFVAL(A5), \ + py::arg(ARG_NAME(A6)) ARG_DEFVAL(A6), \ + py::arg(ARG_NAME(A7)) ARG_DEFVAL(A7), \ + py::arg(ARG_NAME(A8)) ARG_DEFVAL(A8) \ + ); + +#define DEF9_F(N, A1, A2, A3, A4, A5, A6, A7, A8, A9, FN) \ + m.def(#N, []( \ + ARG_DECL(A1), \ + ARG_DECL(A2), \ + ARG_DECL(A3), \ + ARG_DECL(A4), \ + ARG_DECL(A5), \ + ARG_DECL(A6), \ + ARG_DECL(A7), \ + ARG_DECL(A8), \ + ARG_DECL(A9) \ + ) FN, \ + py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \ + py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2), \ + py::arg(ARG_NAME(A3)) ARG_DEFVAL(A3), \ + py::arg(ARG_NAME(A4)) ARG_DEFVAL(A4), \ + py::arg(ARG_NAME(A5)) ARG_DEFVAL(A5), \ + py::arg(ARG_NAME(A6)) ARG_DEFVAL(A6), \ + py::arg(ARG_NAME(A7)) ARG_DEFVAL(A7), \ + py::arg(ARG_NAME(A8)) ARG_DEFVAL(A8), \ + py::arg(ARG_NAME(A9)) ARG_DEFVAL(A9) \ + ); + +#define DEF10_F(N, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, FN) \ + m.def(#N, []( \ + ARG_DECL(A1), \ + ARG_DECL(A2), \ + ARG_DECL(A3), \ + ARG_DECL(A4), \ + ARG_DECL(A5), \ + ARG_DECL(A6), \ + ARG_DECL(A7), \ + ARG_DECL(A8), \ + ARG_DECL(A9), \ + ARG_DECL(A10) \ + ) FN, \ + py::arg(ARG_NAME(A1)) ARG_DEFVAL(A1), \ + py::arg(ARG_NAME(A2)) ARG_DEFVAL(A2), \ + py::arg(ARG_NAME(A3)) ARG_DEFVAL(A3), \ + py::arg(ARG_NAME(A4)) ARG_DEFVAL(A4), \ + py::arg(ARG_NAME(A5)) ARG_DEFVAL(A5), \ + py::arg(ARG_NAME(A6)) ARG_DEFVAL(A6), \ + py::arg(ARG_NAME(A7)) ARG_DEFVAL(A7), \ + py::arg(ARG_NAME(A8)) ARG_DEFVAL(A8), \ + py::arg(ARG_NAME(A9)) ARG_DEFVAL(A9), \ + py::arg(ARG_NAME(A10)) ARG_DEFVAL(A10) \ + ); + +// NOLINTBEGIN(whitespace/line_length) + +#define DEF0_AS(N, AS) DEF0_F(AS, { return NAMESPACE::N(); } ) +#define DEF1_AS(N, AS, A1) DEF1_F(AS, A1, { return NAMESPACE::N(ARG_ID(A1)); } ) +#define DEF2_AS(N, AS, A1, A2) DEF2_F(AS, A1, A2, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2)); } ) +#define DEF3_AS(N, AS, A1, A2, A3) DEF3_F(AS, A1, A2, A3, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3)); } ) +#define DEF4_AS(N, AS, A1, A2, A3, A4) DEF4_F(AS, A1, A2, A3, A4, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4)); } ) +#define DEF5_AS(N, AS, A1, A2, A3, A4, A5) DEF5_F(AS, A1, A2, A3, A4, A5, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5)); } ) +#define DEF6_AS(N, AS, A1, A2, A3, A4, A5, A6) DEF6_F(AS, A1, A2, A3, A4, A5, A6, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6)); } ) +#define DEF7_AS(N, AS, A1, A2, A3, A4, A5, A6, A7) DEF7_F(AS, A1, A2, A3, A4, A5, A6, A7, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6), ARG_ID(A7)); } ) +#define DEF8_AS(N, AS, A1, A2, A3, A4, A5, A6, A7, A8) DEF8_F(AS, A1, A2, A3, A4, A5, A6, A7, A8, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6), ARG_ID(A7), ARG_ID(A8)); } ) +#define DEF9_AS(N, AS, A1, A2, A3, A4, A5, A6, A7, A8, A9) DEF9_F(AS, A1, A2, A3, A4, A5, A6, A7, A8, A9, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6), ARG_ID(A7), ARG_ID(A8), ARG_ID(A9)); } ) +#define DEF10_AS(N, AS, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10) DEF10_F(AS, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6), ARG_ID(A7), ARG_ID(A8), ARG_ID(A9), ARG_ID(A10)); } ) + +#define DEF0(N) DEF0_F(N, { return NAMESPACE::N(); } ) +#define DEF1(N, A1) DEF1_F(N, A1, { return NAMESPACE::N(ARG_ID(A1)); } ) +#define DEF2(N, A1, A2) DEF2_F(N, A1, A2, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2)); } ) +#define DEF3(N, A1, A2, A3) DEF3_F(N, A1, A2, A3, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3)); } ) +#define DEF4(N, A1, A2, A3, A4) DEF4_F(N, A1, A2, A3, A4, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4)); } ) +#define DEF5(N, A1, A2, A3, A4, A5) DEF5_F(N, A1, A2, A3, A4, A5, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5)); } ) +#define DEF6(N, A1, A2, A3, A4, A5, A6) DEF6_F(N, A1, A2, A3, A4, A5, A6, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6)); } ) +#define DEF7(N, A1, A2, A3, A4, A5, A6, A7) DEF7_F(N, A1, A2, A3, A4, A5, A6, A7, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6), ARG_ID(A7)); } ) +#define DEF8(N, A1, A2, A3, A4, A5, A6, A7, A8) DEF8_F(N, A1, A2, A3, A4, A5, A6, A7, A8, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6), ARG_ID(A7), ARG_ID(A8)); } ) +#define DEF9(N, A1, A2, A3, A4, A5, A6, A7, A8, A9) DEF9_F(N, A1, A2, A3, A4, A5, A6, A7, A8, A9, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6), ARG_ID(A7), ARG_ID(A8), ARG_ID(A9)); } ) +#define DEF10(N, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10) DEF10_F(N, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, { return NAMESPACE::N(ARG_ID(A1), ARG_ID(A2), ARG_ID(A3), ARG_ID(A4), ARG_ID(A5), ARG_ID(A6), ARG_ID(A7), ARG_ID(A8), ARG_ID(A9), ARG_ID(A10)); } ) + +// NOLINTEND(whitespace/line_length) + +#endif // MUJOCO_PYTHON_EXPERIMENTAL_DEAR_IMGUI_DEAR_IMGUI_MACROS_H_ diff --git a/python/mujoco/experimental/implot/implot.cc b/python/mujoco/experimental/implot/implot.cc new file mode 100644 index 00000000..a174ea8c --- /dev/null +++ b/python/mujoco/experimental/implot/implot.cc @@ -0,0 +1,438 @@ +// Copyright 2026 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#define NAMESPACE ImPlot +#include "dear_imgui_macros.h" +#include +#include +#include +#include +#include + +// NOLINTBEGIN(whitespace/line_length) + +namespace py = pybind11; +using ImString = const char*; +static constexpr const ImVec2 ImVec2_Zero = ImVec2(0.0f, 0.0f); +static constexpr const ImVec2 ImVec2_One = ImVec2(1.0f, 1.0f); +static constexpr const ImVec2 ImVec2_NegOne_Zero = ImVec2(-1.0f, 0.0f); +static constexpr const ImVec4 ImVec4_Zero = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); +static constexpr const ImVec4 ImVec4_One = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); +static constexpr const ImPlotRect ImPlotRect_Default{}; +static constexpr const ImPlotRange ImPlotRange_Default{}; + +PYBIND11_MODULE(implot, m) { + // Import dear_imgui to make types like ImVec2 available. + py::module_::import("mujoco.experimental.dear_imgui.dear_imgui"); + + // Types. + py::class_(m, "Point") + .def(py::init<>()) + .def(py::init(), py::arg("_x"), py::arg("_y")) + .def_readwrite("x", &ImPlotPoint::x) + .def_readwrite("y", &ImPlotPoint::y); + + py::class_(m, "Range") + .def(py::init<>()) + .def(py::init(), py::arg("_min"), py::arg("_max")) + .def_readwrite("min", &ImPlotRange::Min) + .def_readwrite("max", &ImPlotRange::Max); + +// py::class_(m, "Rect") +// .def(py::init<>()) +// .def(py::init(), py::arg("_x"), py::arg("_y")) +// .def_readwrite("x", &ImPlotRect::X) +// .def_readwrite("y", &ImPlotRect::Y); + + // Enumerations. + + py::enum_(m, "Axis") + .value("X1", ImAxis_X1) + .value("X2", ImAxis_X2) + .value("X3", ImAxis_X3) + .value("Y1", ImAxis_Y1) + .value("Y2", ImAxis_Y2) + .value("Y3", ImAxis_Y3); + + py::enum_(m, "Flags") + .value("None", ImPlotFlags_None) + .value("NoTitle", ImPlotFlags_NoTitle) + .value("NoLegend", ImPlotFlags_NoLegend) + .value("NoMouseText", ImPlotFlags_NoMouseText) + .value("NoInputs", ImPlotFlags_NoInputs) + .value("NoMenus", ImPlotFlags_NoMenus) + .value("NoBoxSelect", ImPlotFlags_NoBoxSelect) + .value("NoFrame", ImPlotFlags_NoFrame) + .value("Equal", ImPlotFlags_Equal) + .value("Crosshairs", ImPlotFlags_Crosshairs) + .value("CanvasOnly", ImPlotFlags_CanvasOnly); + + py::enum_(m, "AxisFlags") + .value("None", ImPlotAxisFlags_None) + .value("NoLabel", ImPlotAxisFlags_NoLabel) + .value("NoGridLines", ImPlotAxisFlags_NoGridLines) + .value("NoTickMarks", ImPlotAxisFlags_NoTickMarks) + .value("NoTickLabels", ImPlotAxisFlags_NoTickLabels) + .value("NoInitialFit", ImPlotAxisFlags_NoInitialFit) + .value("NoMenus", ImPlotAxisFlags_NoMenus) + .value("NoSideSwitch", ImPlotAxisFlags_NoSideSwitch) + .value("NoHighlight", ImPlotAxisFlags_NoHighlight) + .value("Opposite", ImPlotAxisFlags_Opposite) + .value("Foreground", ImPlotAxisFlags_Foreground) + .value("Invert", ImPlotAxisFlags_Invert) + .value("AutoFit", ImPlotAxisFlags_AutoFit) + .value("RangeFit", ImPlotAxisFlags_RangeFit) + .value("PanStretch", ImPlotAxisFlags_PanStretch) + .value("LockMin", ImPlotAxisFlags_LockMin) + .value("LockMax", ImPlotAxisFlags_LockMax) + .value("Lock", ImPlotAxisFlags_Lock) + .value("NoDecorations", ImPlotAxisFlags_NoDecorations) + .value("AuxDefault", ImPlotAxisFlags_AuxDefault); + + py::enum_(m, "SubplotFlags") + .value("None", ImPlotSubplotFlags_None) + .value("NoTitle", ImPlotSubplotFlags_NoTitle) + .value("NoLegend", ImPlotSubplotFlags_NoLegend) + .value("NoMenus", ImPlotSubplotFlags_NoMenus) + .value("NoResize", ImPlotSubplotFlags_NoResize) + .value("NoAlign", ImPlotSubplotFlags_NoAlign) + .value("ShareItems", ImPlotSubplotFlags_ShareItems) + .value("LinkRows", ImPlotSubplotFlags_LinkRows) + .value("LinkCols", ImPlotSubplotFlags_LinkCols) + .value("LinkAllX", ImPlotSubplotFlags_LinkAllX) + .value("LinkAllY", ImPlotSubplotFlags_LinkAllY) + .value("ColMajor", ImPlotSubplotFlags_ColMajor); + + py::enum_(m, "LegendFlags") + .value("None", ImPlotLegendFlags_None) + .value("NoButtons", ImPlotLegendFlags_NoButtons) + .value("NoHighlightItem", ImPlotLegendFlags_NoHighlightItem) + .value("NoHighlightAxis", ImPlotLegendFlags_NoHighlightAxis) + .value("NoMenus", ImPlotLegendFlags_NoMenus) + .value("Outside", ImPlotLegendFlags_Outside) + .value("Horizontal", ImPlotLegendFlags_Horizontal) + .value("Sort", ImPlotLegendFlags_Sort) + .value("Reverse", ImPlotLegendFlags_Reverse); + + py::enum_(m, "MouseTextFlags") + .value("None", ImPlotMouseTextFlags_None) + .value("NoAuxAxes", ImPlotMouseTextFlags_NoAuxAxes) + .value("NoFormat", ImPlotMouseTextFlags_NoFormat) + .value("ShowAlways", ImPlotMouseTextFlags_ShowAlways); + + py::enum_(m, "DragToolFlags") + .value("None", ImPlotDragToolFlags_None) + .value("NoCursors", ImPlotDragToolFlags_NoCursors) + .value("NoFit", ImPlotDragToolFlags_NoFit) + .value("NoInputs", ImPlotDragToolFlags_NoInputs) + .value("Delayed", ImPlotDragToolFlags_Delayed); + + py::enum_(m, "ColormapScaleFlags") + .value("None", ImPlotColormapScaleFlags_None) + .value("NoLabel", ImPlotColormapScaleFlags_NoLabel) + .value("Opposite", ImPlotColormapScaleFlags_Opposite) + .value("Invert", ImPlotColormapScaleFlags_Invert); + + py::enum_(m, "ItemFlags") + .value("None", ImPlotItemFlags_None) + .value("NoLegend", ImPlotItemFlags_NoLegend) + .value("NoFit", ImPlotItemFlags_NoFit); + + py::enum_(m, "LineFlags") + .value("None", ImPlotLineFlags_None) + .value("Segments", ImPlotLineFlags_Segments) + .value("Loop", ImPlotLineFlags_Loop) + .value("SkipNaN", ImPlotLineFlags_SkipNaN) + .value("NoClip", ImPlotLineFlags_NoClip) + .value("Shaded", ImPlotLineFlags_Shaded); + + py::enum_(m, "ScatterFlags") + .value("None", ImPlotScatterFlags_None) + .value("NoClip", ImPlotScatterFlags_NoClip); + + py::enum_(m, "StairsFlags") + .value("None", ImPlotStairsFlags_None) + .value("PreStep", ImPlotStairsFlags_PreStep) + .value("Shaded", ImPlotStairsFlags_Shaded); + + py::enum_(m, "ShadedFlags") + .value("None", ImPlotShadedFlags_None); + + py::enum_(m, "BarsFlags") + .value("None", ImPlotBarsFlags_None) + .value("Horizontal", ImPlotBarsFlags_Horizontal); + + py::enum_(m, "BarGroupsFlags") + .value("None", ImPlotBarGroupsFlags_None) + .value("Horizontal", ImPlotBarGroupsFlags_Horizontal) + .value("Stacked", ImPlotBarGroupsFlags_Stacked); + + py::enum_(m, "ErrorBarsFlags") + .value("None", ImPlotErrorBarsFlags_None) + .value("Horizontal", ImPlotErrorBarsFlags_Horizontal); + + py::enum_(m, "StemsFlags") + .value("None", ImPlotStemsFlags_None) + .value("Horizontal", ImPlotStemsFlags_Horizontal); + + py::enum_(m, "InfLinesFlags") + .value("None", ImPlotInfLinesFlags_None) + .value("Horizontal", ImPlotInfLinesFlags_Horizontal); + + py::enum_(m, "PieChartFlags") + .value("None", ImPlotPieChartFlags_None) + .value("Normalize", ImPlotPieChartFlags_Normalize) + .value("IgnoreHidden", ImPlotPieChartFlags_IgnoreHidden) + .value("Exploding", ImPlotPieChartFlags_Exploding); + + py::enum_(m, "HeatmapFlags") + .value("None", ImPlotHeatmapFlags_None) + .value("ColMajor", ImPlotHeatmapFlags_ColMajor); + + py::enum_(m, "HistogramFlags") + .value("None", ImPlotHistogramFlags_None) + .value("Horizontal", ImPlotHistogramFlags_Horizontal) + .value("Cumulative", ImPlotHistogramFlags_Cumulative) + .value("Density", ImPlotHistogramFlags_Density) + .value("NoOutliers", ImPlotHistogramFlags_NoOutliers) + .value("ColMajor", ImPlotHistogramFlags_ColMajor); + + py::enum_(m, "DigitalFlags") + .value("ImPlotNone", ImPlotDigitalFlags_None); + + py::enum_(m, "ImageFlags") + .value("None", ImPlotImageFlags_None); + + py::enum_(m, "TextFlags") + .value("None", ImPlotTextFlags_None) + .value("Vertical", ImPlotTextFlags_Vertical); + + py::enum_(m, "DummyFlags") + .value("None", ImPlotDummyFlags_None); + + py::enum_(m, "Cond") + .value("None", ImPlotCond_None) + .value("Always", ImPlotCond_Always) + .value("Once", ImPlotCond_Once); + + py::enum_(m, "Col") + .value("Line", ImPlotCol_Line) + .value("Fill", ImPlotCol_Fill) + .value("MarkerOutline", ImPlotCol_MarkerOutline) + .value("MarkerFill", ImPlotCol_MarkerFill) + .value("ErrorBar", ImPlotCol_ErrorBar) + .value("FrameBg", ImPlotCol_FrameBg) + .value("PlotBg", ImPlotCol_PlotBg) + .value("PlotBorder", ImPlotCol_PlotBorder) + .value("LegendBg", ImPlotCol_LegendBg) + .value("LegendBorder", ImPlotCol_LegendBorder) + .value("LegendText", ImPlotCol_LegendText) + .value("TitleText", ImPlotCol_TitleText) + .value("InlayText", ImPlotCol_InlayText) + .value("AxisText", ImPlotCol_AxisText) + .value("AxisGrid", ImPlotCol_AxisGrid) + .value("AxisTick", ImPlotCol_AxisTick) + .value("AxisBg", ImPlotCol_AxisBg) + .value("AxisBgHovered", ImPlotCol_AxisBgHovered) + .value("AxisBgActive", ImPlotCol_AxisBgActive) + .value("Selection", ImPlotCol_Selection) + .value("Crosshairs", ImPlotCol_Crosshairs); + + py::enum_(m, "StyleVar") + .value("LineWeight", ImPlotStyleVar_LineWeight) + .value("Marker", ImPlotStyleVar_Marker) + .value("MarkerSize", ImPlotStyleVar_MarkerSize) + .value("MarkerWeight", ImPlotStyleVar_MarkerWeight) + .value("FillAlpha", ImPlotStyleVar_FillAlpha) + .value("ErrorBarSize", ImPlotStyleVar_ErrorBarSize) + .value("ErrorBarWeight", ImPlotStyleVar_ErrorBarWeight) + .value("DigitalBitHeight", ImPlotStyleVar_DigitalBitHeight) + .value("DigitalBitGap", ImPlotStyleVar_DigitalBitGap) + .value("PlotBorderSize", ImPlotStyleVar_PlotBorderSize) + .value("MinorAlpha", ImPlotStyleVar_MinorAlpha) + .value("MajorTickLen", ImPlotStyleVar_MajorTickLen) + .value("MinorTickLen", ImPlotStyleVar_MinorTickLen) + .value("MajorTickSize", ImPlotStyleVar_MajorTickSize) + .value("MinorTickSize", ImPlotStyleVar_MinorTickSize) + .value("MajorGridSize", ImPlotStyleVar_MajorGridSize) + .value("MinorGridSize", ImPlotStyleVar_MinorGridSize) + .value("PlotPadding", ImPlotStyleVar_PlotPadding) + .value("LabelPadding", ImPlotStyleVar_LabelPadding) + .value("LegendPadding", ImPlotStyleVar_LegendPadding) + .value("LegendInnerPadding", ImPlotStyleVar_LegendInnerPadding) + .value("LegendSpacing", ImPlotStyleVar_LegendSpacing) + .value("MousePosPadding", ImPlotStyleVar_MousePosPadding) + .value("AnnotationPadding", ImPlotStyleVar_AnnotationPadding) + .value("FitPadding", ImPlotStyleVar_FitPadding) + .value("PlotDefaultSize", ImPlotStyleVar_PlotDefaultSize) + .value("PlotMinSize", ImPlotStyleVar_PlotMinSize); + + py::enum_(m, "Scale") + .value("ImPlotScale_Linear", ImPlotScale_Linear) + .value("ImPlotScale_Time", ImPlotScale_Time) + .value("ImPlotScale_Log10", ImPlotScale_Log10) + .value("ImPlotScale_SymLog", ImPlotScale_SymLog); + + py::enum_(m, "Marker") + .value("None", ImPlotMarker_None) + .value("Circle", ImPlotMarker_Circle) + .value("Square", ImPlotMarker_Square) + .value("Diamond", ImPlotMarker_Diamond) + .value("Up", ImPlotMarker_Up) + .value("Down", ImPlotMarker_Down) + .value("Left", ImPlotMarker_Left) + .value("Right", ImPlotMarker_Right) + .value("Cross", ImPlotMarker_Cross) + .value("Plus", ImPlotMarker_Plus) + .value("Asterisk", ImPlotMarker_Asterisk); + + py::enum_(m, "Location") + .value("Center", ImPlotLocation_Center) + .value("North", ImPlotLocation_North) + .value("South", ImPlotLocation_South) + .value("West", ImPlotLocation_West) + .value("East", ImPlotLocation_East) + .value("NorthWest", ImPlotLocation_NorthWest) + .value("NorthEast", ImPlotLocation_NorthEast) + .value("SouthWest", ImPlotLocation_SouthWest) + .value("SouthEast", ImPlotLocation_SouthEast); + + // Functions. + + DEF3(BeginPlot, (ImString, title_id, ), (const ImVec2&, size, = ImVec2_NegOne_Zero), (ImPlotFlags, flags, = 0)); + DEF0(EndPlot); + DEF7(BeginSubplots, (ImString, title_id, ), (int, rows, ), (int, cols, ), (const ImVec2&, size, ), (ImPlotSubplotFlags, flags, = 0), (float*, row_ratios, = nullptr), (float*, col_ratios, = nullptr)); + DEF0(EndSubplots); + DEF3(SetupAxis, (ImAxis, axis, ), (ImString, label, = nullptr), (ImPlotAxisFlags, flags, = 0)); + DEF4(SetupAxisLimits, (ImAxis, axis, ), (double, v_min, ), (double, v_max, ), (ImPlotCond, cond, = ImPlotCond_Once)); + DEF4_F(SetupAxisTicks, (ImAxis, axis, ), (std::vector, values, ), (std::vector, labels, ), (bool, keep_default, = false), { + std::vector c_labels; + c_labels.reserve(labels.size()); + for (const auto& l : labels) { + c_labels.push_back(l.c_str()); + } + ImPlot::SetupAxisTicks(axis, values.data(), values.size(), c_labels.empty() ? nullptr : c_labels.data(), keep_default); + }); + DEF3(SetupAxisLinks, (ImAxis, axis, ), (double*, link_min, ), (double*, link_max, )); + DEF2(SetupAxisFormat, (ImAxis, axis, ), (ImString, fmt, )); + DEF2(SetupAxisScale, (ImAxis, axis, ), (ImPlotScale, scale, )); + DEF3(SetupAxisLimitsConstraints, (ImAxis, axis, ), (double, v_min, ), (double, v_max, )); + DEF3(SetupAxisZoomConstraints, (ImAxis, axis, ), (double, z_min, ), (double, z_max, )); + DEF4(SetupAxes, (ImString, x_label, ), (ImString, y_label, ), (ImPlotAxisFlags, x_flags, = 0), (ImPlotAxisFlags, y_flags, = 0)); + DEF5(SetupAxesLimits, (double, x_min, ), (double, x_max, ), (double, y_min, ), (double, y_max, ), (ImPlotCond, cond, = ImPlotCond_Once)); + DEF2(SetupLegend, (ImPlotLocation, location, ), (ImPlotLegendFlags, flags, = 0)); + DEF2(SetupMouseText, (ImPlotLocation, location, ), (ImPlotMouseTextFlags, flags, = 0)); + DEF0(SetupFinish); + DEF4(SetNextAxisLimits, (ImAxis, axis, ), (double, v_min, ), (double, v_max, ), (ImPlotCond, cond, = ImPlotCond_Once)); + DEF3(SetNextAxisLinks, (ImAxis, axis, ), (double*, link_min, ), (double*, link_max, )); + DEF1(SetNextAxisToFit, (ImAxis, axis, )); + DEF5(SetNextAxesLimits, (double, x_min, ), (double, x_max, ), (double, y_min, ), (double, y_max, ), (ImPlotCond, cond, = ImPlotCond_Once)); + DEF0_F(SetNextAxesToFit, { + return ImPlot::SetNextAxesToFit(); + }); + DEF6_F(PlotLine, (ImString, label_id, ), (std::vector, xs, ), (std::vector, ys, ), (ImPlotLineFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), { + return ImPlot::PlotLine(label_id, xs.data(), ys.data(), xs.size(), flags, offset, stride); + }); + DEF6_F(PlotScatter, (ImString, label_id, ), (std::vector, xs, ), (std::vector, ys, ), (ImPlotScatterFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), { + return ImPlot::PlotScatter(label_id, xs.data(), ys.data(), xs.size(), flags, offset, stride); + }); + DEF6_F(PlotStairs, (ImString, label_id, ), (std::vector, xs, ), (std::vector, ys, ), (ImPlotStairsFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), { + return ImPlot::PlotStairs(label_id, xs.data(), ys.data(), xs.size(), flags, offset, stride); + }); + DEF7_F(PlotShaded, (ImString, label_id, ), (std::vector, xs, ), (std::vector, ys, ), (double, yref, = 0), (ImPlotShadedFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), { + return ImPlot::PlotShaded(label_id, xs.data(), ys.data(), xs.size(), yref, flags, offset, stride); + }); + DEF7_F(PlotShaded, (ImString, label_id, ), (std::vector, xs, ), (std::vector, ys1, ), (std::vector, ys2, ), (ImPlotShadedFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), { + return ImPlot::PlotShaded(label_id, xs.data(), ys1.data(), ys2.data(), xs.size(), flags, offset, stride); + }); + DEF7_F(PlotBars, (ImString, label_id, ), (std::vector, xs, ), (std::vector, ys, ), (double, bar_size, ), (ImPlotBarsFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), { + return ImPlot::PlotBars(label_id, xs.data(), ys.data(), xs.size(), bar_size, flags, offset, stride); + }); + DEF7_F(PlotErrorBars, (ImString, label_id, ), (std::vector, xs, ), (std::vector, ys, ), (std::vector, err, ), (ImPlotErrorBarsFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), { + return ImPlot::PlotErrorBars(label_id, xs.data(), ys.data(), err.data(), xs.size(), flags, offset, stride); + }); + DEF8_F(PlotErrorBars, (ImString, label_id, ), (std::vector, xs, ), (std::vector, ys, ), (std::vector, neg, ), (std::vector, pos, ), (ImPlotErrorBarsFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), { + return ImPlot::PlotErrorBars(label_id, xs.data(), ys.data(), neg.data(), pos.data(), xs.size(), flags, offset, stride); + }); + DEF7_F(PlotStems, (ImString, label_id, ), (std::vector, xs, ), (std::vector, ys, ), (double, ref, = 0), (ImPlotStemsFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), { + return ImPlot::PlotStems(label_id, xs.data(), ys.data(), xs.size(), ref, flags, offset, stride); + }); + DEF6_F(PlotDigital, (ImString, label_id, ), (std::vector, xs, ), (std::vector, ys, ), (ImPlotDigitalFlags, flags, = 0), (int, offset, = 0), (int, stride, = sizeof(double)), { + return ImPlot::PlotDigital(label_id, xs.data(), ys.data(), xs.size(), flags, offset, stride); + }); + DEF8(PlotImage, (ImString, label_id, ), (ImTextureRef, tex_ref, ), (const ImPlotPoint&, bounds_min, ), (const ImPlotPoint&, bounds_max, ), (const ImVec2&, uv0, = ImVec2_Zero), (const ImVec2&, uv1, = ImVec2_One), (const ImVec4&, tint_col, = ImVec4_One), (ImPlotImageFlags, flags, = 0)); + DEF5(PlotText, (ImString, text, ), (double, x, ), (double, y, ), (const ImVec2&, pix_offset, = ImVec2_Zero), (ImPlotTextFlags, flags, = 0)); + DEF2(PlotDummy, (ImString, label_id, ), (ImPlotDummyFlags, flags, = 0)); + DEF9(DragPoint, (int, id, ), (double*, x, ), (double*, y, ), (const ImVec4&, col, ), (float, size, = 4), (ImPlotDragToolFlags, flags, = 0), (bool*, out_clicked, = nullptr), (bool*, out_hovered, = nullptr), (bool*, out_held, = nullptr)); + DEF8(DragLineX, (int, id, ), (double*, x, ), (const ImVec4&, col, ), (float, thickness, = 1), (ImPlotDragToolFlags, flags, = 0), (bool*, out_clicked, = nullptr), (bool*, out_hovered, = nullptr), (bool*, out_held, = nullptr)); + DEF8(DragLineY, (int, id, ), (double*, y, ), (const ImVec4&, col, ), (float, thickness, = 1), (ImPlotDragToolFlags, flags, = 0), (bool*, out_clicked, = nullptr), (bool*, out_hovered, = nullptr), (bool*, out_held, = nullptr)); + DEF10(DragRect, (int, id, ), (double*, x1, ), (double*, y1, ), (double*, x2, ), (double*, y2, ), (const ImVec4&, col, ), (ImPlotDragToolFlags, flags, = 0), (bool*, out_clicked, = nullptr), (bool*, out_hovered, = nullptr), (bool*, out_held, = nullptr)); + DEF6(Annotation, (double, x, ), (double, y, ), (const ImVec4&, col, ), (const ImVec2&, pix_offset, ), (bool, clamp, ), (bool, round, = false)); + DEF6_F(Annotation, (double, x, ), (double, y, ), (const ImVec4&, col, ), (const ImVec2&, pix_offset, ), (bool, clamp, ), (ImString, txt, ), { + return ImPlot::Annotation(x, y, col, pix_offset, clamp, "%s", txt); + }); + DEF3(TagX, (double, x, ), (const ImVec4&, col, ), (bool, round, = false)); + DEF3_F(TagX, (double, x, ), (const ImVec4&, col, ), (ImString, txt, ), { + return ImPlot::TagX(x, col, "%s", txt); + }); + DEF3(TagY, (double, y, ), (const ImVec4&, col, ), (bool, round, = false)); + DEF3_F(TagY, (double, y, ), (const ImVec4&, col, ), (ImString, txt, ), { + return ImPlot::TagY(y, col, "%s", txt); + }); + DEF1(SetAxis, (ImAxis, axis, )); + DEF2(SetAxes, (ImAxis, x_axis, ), (ImAxis, y_axis, )); + DEF3(PixelsToPlot, (const ImVec2&, pix, ), (ImAxis, x_axis, = IMPLOT_AUTO), (ImAxis, y_axis, = IMPLOT_AUTO)); + DEF4(PixelsToPlot, (float, x, ), (float, y, ), (ImAxis, x_axis, = IMPLOT_AUTO), (ImAxis, y_axis, = IMPLOT_AUTO)); + DEF3(PlotToPixels, (const ImPlotPoint&, plt, ), (ImAxis, x_axis, = IMPLOT_AUTO), (ImAxis, y_axis, = IMPLOT_AUTO)); + DEF4(PlotToPixels, (double, x, ), (double, y, ), (ImAxis, x_axis, = IMPLOT_AUTO), (ImAxis, y_axis, = IMPLOT_AUTO)); + DEF0(GetPlotPos); + DEF0(GetPlotSize); + DEF2(GetPlotMousePos, (ImAxis, x_axis, = IMPLOT_AUTO), (ImAxis, y_axis, = IMPLOT_AUTO)); + DEF2(GetPlotLimits, (ImAxis, x_axis, = IMPLOT_AUTO), (ImAxis, y_axis, = IMPLOT_AUTO)); + DEF0(IsPlotHovered); + DEF1(IsAxisHovered, (ImAxis, axis, )); + DEF0(IsSubplotsHovered); + DEF0(IsPlotSelected); + DEF2(GetPlotSelection, (ImAxis, x_axis, = IMPLOT_AUTO), (ImAxis, y_axis, = IMPLOT_AUTO)); + DEF0(CancelPlotSelection); + DEF2(HideNextItem, (bool, hidden, = true), (ImPlotCond, cond, = ImPlotCond_Once)); + DEF2(BeginAlignedPlots, (ImString, group_id, ), (bool, vertical, = true)); + DEF0(EndAlignedPlots); + DEF2(BeginLegendPopup, (ImString, label_id, ), (ImGuiMouseButton, mouse_button, = 1)); + DEF0(EndLegendPopup); + DEF1(IsLegendEntryHovered, (ImString, label_id, )); + DEF0(BeginDragDropTargetPlot); + DEF1(BeginDragDropTargetAxis, (ImAxis, axis, )); + DEF0(BeginDragDropTargetLegend); + DEF0(EndDragDropTarget); + DEF1(BeginDragDropSourcePlot, (ImGuiDragDropFlags, flags, = 0)); + DEF2(BeginDragDropSourceAxis, (ImAxis, axis, ), (ImGuiDragDropFlags, flags, = 0)); + DEF2(BeginDragDropSourceItem, (ImString, label_id, ), (ImGuiDragDropFlags, flags, = 0)); + DEF0(EndDragDropSource); + DEF2(PushStyleColor, (ImPlotCol, idx, ), (ImU32, col, )); + DEF2(PushStyleColor, (ImPlotCol, idx, ), (const ImVec4&, col, )); + DEF1(PopStyleColor, (int, count, = 1)); + DEF2(PushStyleVar, (ImPlotStyleVar, idx, ), (float, val, )); + DEF2(PushStyleVar, (ImPlotStyleVar, idx, ), (int, val, )); + DEF2(PushStyleVar, (ImPlotStyleVar, idx, ), (const ImVec2&, val, )); + DEF1(PopStyleVar, (int, count, = 1)); + DEF2(SetNextLineStyle, (const ImVec4&, col, = IMPLOT_AUTO_COL), (float, weight, = IMPLOT_AUTO)); + DEF2(SetNextFillStyle, (const ImVec4&, col, = IMPLOT_AUTO_COL), (float, alpha_mod, = IMPLOT_AUTO)); + DEF5(SetNextMarkerStyle, (ImPlotMarker, marker, = IMPLOT_AUTO), (float, size, = IMPLOT_AUTO), (const ImVec4&, fill, = IMPLOT_AUTO_COL), (float, weight, = IMPLOT_AUTO), (const ImVec4&, outline, = IMPLOT_AUTO_COL)); + DEF3(SetNextErrorBarStyle, (const ImVec4&, col, = IMPLOT_AUTO_COL), (float, size, = IMPLOT_AUTO), (float, weight, = IMPLOT_AUTO)); + DEF1(PushPlotClipRect, (float, expand, = 0)); + DEF0(PopPlotClipRect); +} + +// NOLINTEND(whitespace/line_length) diff --git a/python/mujoco/experimental/studio/native_viewer.cc b/python/mujoco/experimental/studio/native_viewer.cc new file mode 100644 index 00000000..e4379862 --- /dev/null +++ b/python/mujoco/experimental/studio/native_viewer.cc @@ -0,0 +1,189 @@ +// Copyright 2026 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include "third_party/mujoco/src/experimental/platform/hal/graphics_mode.h" +#include "third_party/mujoco/src/experimental/platform/hal/renderer.h" +#include "third_party/mujoco/src/experimental/platform/hal/window.h" +#include "structs.h" +#include +#include +#include +#include + +static bool IsCuda() { +#ifdef CUDA + return true; +#else + return false; +#endif +} + +static bool IsCrd() { + const char* display = getenv("DISPLAY"); + return display ? strcmp(display, ":20") == 0 : false; +} + +static std::vector LoadAsset(std::string_view path) { + std::string file_path = "assets/" + + std::string(path.substr(path.find(':') + 1)); + + std::ifstream file(file_path, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + return {}; + } + auto file_size = file.tellg(); + file.seekg(0, std::ios::beg); + std::vector buffer(file_size); + if (!file.read(reinterpret_cast(buffer.data()), file_size)) { + return {}; + } + return buffer; +} + +// Holds loaded resource data for the MuJoCo resource provider. +struct ResourceData { + std::vector bytes; +}; + +class Viewer { + public: + Viewer(const std::string& title, int width, int height, + std::string graphics_mode_str) { + // Register resource providers for font and filament assets. + mjpResourceProvider resource_provider; + mjp_defaultResourceProvider(&resource_provider); + + resource_provider.open = [](mjResource* resource) { + auto* data = new ResourceData(); + data->bytes = LoadAsset(resource->name); + resource->data = data; + return static_cast(data->bytes.size()); + }; + resource_provider.read = [](mjResource* resource, const void** buffer) { + auto* data = static_cast(resource->data); + *buffer = data->bytes.data(); + return static_cast(data->bytes.size()); + }; + resource_provider.close = [](mjResource* resource) { + delete static_cast(resource->data); + resource->data = nullptr; + }; + resource_provider.prefix = "font"; + mjp_registerResourceProvider(&resource_provider); + resource_provider.prefix = "filament"; + mjp_registerResourceProvider(&resource_provider); + + mujoco::platform::Window::Config config; + using GraphicsMode = mujoco::platform::GraphicsMode; + config.gfx_mode = mujoco::platform::GraphicsModeFromString( + graphics_mode_str, GraphicsMode::FilamentOpenGl); + window_ = std::make_unique("PyStudio " + title, + width, height, config); + ImPlot::CreateContext(); + + renderer_ = std::make_unique( + window_->GetNativeWindowHandle(), config.gfx_mode); + } + + void InitRenderer(const mujoco::python::MjModelWrapper& model) { + renderer_->Init(model.get()); + } + + bool NewFrame() { + const mujoco::platform::Window::Status status = window_->NewFrame(); + return status == mujoco::platform::Window::Status::kRunning; + } + + intptr_t UploadImage(intptr_t tex_id, const std::string img, int width, + int height, int bpp) { + return renderer_->UploadImage(tex_id, (const std::byte*)img.data(), width, + height, bpp); + } + + int RenderToTexture(const mujoco::python::MjModelWrapper& model, + mujoco::python::MjDataWrapper& data, + mujoco::python::MjvCameraWrapper& cam, int width, + int height, int tex_id) { + const int bytes_per_pixel = 3; + std::vector bytes(width * height * bytes_per_pixel); + renderer_->RenderToTexture(model.get(), data.get(), cam.get(), width, + height, bytes.data()); + return renderer_->UploadImage(tex_id, bytes.data(), width, height, + bytes_per_pixel); + } + + std::string GetDropFile() { + return window_->GetDropFile(); + } + + void Present(const mujoco::python::MjModelWrapper& model, + mujoco::python::MjDataWrapper& data, + mujoco::python::MjvPerturbWrapper& perturb, + mujoco::python::MjvCameraWrapper& camera, + mujoco::python::MjvOptionWrapper& vis_options, + const std::vector& render_flags) { + const float width = window_->GetWidth(); + const float height = window_->GetHeight(); + const float scale = window_->GetScale(); + + if (mujoco::platform::IsHeadless(window_->GetGraphicsMode())) { + pixels_.resize(width * height * 3); + } else { + pixels_.clear(); + } + + // Update render flags before rendering. + mjtByte* flags = renderer_->GetRenderFlags(); + for (size_t i = 0; i < mjNRNDFLAG && i < render_flags.size(); ++i) { + flags[i] = render_flags[i]; + } + + renderer_->Render(model.get(), data.get(), perturb.get(), camera.get(), + vis_options.get(), width * scale, height * scale, + pixels_); + + window_->EndFrame(); + window_->Present(pixels_); + } + + private: + std::unique_ptr window_; + std::unique_ptr renderer_; + std::vector pixels_; +}; + +PYBIND11_MODULE(native_viewer_cc, m) { + pybind11::class_(m, "Viewer") + .def(pybind11::init()) + .def("InitRenderer", &Viewer::InitRenderer) + .def("NewFrame", &Viewer::NewFrame) + .def("Present", &Viewer::Present) + .def("UploadImage", &Viewer::UploadImage) + .def("RenderToTexture", &Viewer::RenderToTexture) + .def("GetDropFile", &Viewer::GetDropFile); + m.def("IsCrd", &IsCrd); + m.def("IsCuda", &IsCuda); +} diff --git a/python/mujoco/experimental/studio/native_viewer.py b/python/mujoco/experimental/studio/native_viewer.py new file mode 100644 index 00000000..dd9afb37 --- /dev/null +++ b/python/mujoco/experimental/studio/native_viewer.py @@ -0,0 +1,171 @@ +# Copyright 2026 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Simulation-agnostic native viewer for MuJoCo models. + +This class is simulation-agnostic and as such it does not own the model or data. + +See the documentation for studio_app.py for more details on the architecture +separating the viewer and simulation. See the sample/ folder for examples of how +to use these classes. +""" + +import mujoco + +from mujoco.experimental.studio import native_viewer_cc as _viewer +from mujoco.experimental.studio import ux + + +class NativeViewer: + """Simulation-agnostic native viewer for MuJoCo models.""" + + def __init__( + self, + model: mujoco.MjModel, + camera: mujoco.MjvCamera | None = None, + vis_options: mujoco.MjvOption | None = None, + perturb: mujoco.MjvPerturb | None = None, + render_flags: ux.RenderFlags | None = None, + title: str = '', + width: int = 1200, + height: int = 800, + gfx: str = '', + ) -> None: + """Initializes the NativeViewer. + + The viewer creates and modifies its own camera, perturbation, and + visualization option objects unless they are provided. + + Args: + model: The MuJoCo model, used to initialize the renderer. + camera: Camera parameters. Internal object is created if None. + vis_options: Visualization options. Internal object is created if None. + perturb: Perturbation parameters. Internal object is created if None. + render_flags: Render flags. Internal object is created if None. + title: Title of the viewer window. + width: Initial width of the viewer window. + height: Initial height of the viewer window. + gfx: Graphics mode. + """ + self.camera = camera or mujoco.MjvCamera() + self.perturb = perturb or mujoco.MjvPerturb() + self.vis_options = vis_options or mujoco.MjvOption() + self._viewer = _viewer.Viewer(title, width, height, gfx) + self._viewer.InitRenderer(model) + # This class does not own the model but we need to know if the model being + # rendered has changed, so we store the unique python object id here so we + # can use it to detect model changes. + self._renderer_model_id = id(model) + self._is_running = True + if render_flags is not None: + self.render_flags = render_flags + else: + self.render_flags = ux.RenderFlags() + # Initted to match mujoco/src/engine/engine_vis_init.c + self.render_flags.flags = [1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1] + + def _sync_renderer(self, model: mujoco.MjModel) -> None: + """Re-initializes the renderer if the model object has changed.""" + if id(model) != self._renderer_model_id: + self._viewer.InitRenderer(model) + self._renderer_model_id = id(model) + + def is_running(self) -> bool: + """Poll for a new frame; returns ``False`` when the window is closed.""" + if not self._is_running: + return False + self._is_running = self._viewer.NewFrame() + return self._is_running + + def sync( + self, + model: mujoco.MjModel, + data: mujoco.MjData, + ) -> None: + """Render the scene and present it to the window. + + Args: + model: The MuJoCo model provided by the simulation. + data: The MuJoCo data provided by the simulation. + """ + self._sync_renderer(model) + self._viewer.Present( + model, + data, + self.perturb, + self.camera, + self.vis_options, + self.render_flags.flags, + ) + + def stop(self) -> None: + """Stop the viewer.""" + self._is_running = False + + def get_drop_file(self) -> str: + """Returns the path of the file dropped into the window, or empty string.""" + return self._viewer.GetDropFile() + + def upload_image( + self, tex_id: int, img: str | bytes, width: int, height: int, bpp: int + ) -> int: + """Uploads an image to the backend for GUI rendering. + + The ID can be used in subsequent calls to update the texture data. An empty + `img` argument will free the texture if it exists. A `tex_id` of 0 will + create a new texture. + + Args: + tex_id: The texture ID. + img: The image data as string or bytes. + width: Width of the image. + height: Height of the image. + bpp: Bytes per pixel. + + Returns: + The texture ID. + """ + return self._viewer.UploadImage(tex_id, img, width, height, bpp) + + def render_to_texture( + self, + model: mujoco.MjModel, + data: mujoco.MjData, + tex_id: int, + width: int, + height: int, + ) -> int: + """Renders the scene to a texture. + + This function renders the scene from the current camera view into a texture. + It handles buffer allocation internally. + + Args: + model: The MuJoCo model provided by the simulation. + data: The MuJoCo data provided by the simulation. + tex_id: The texture ID to render into (0 to create a new one). + width: Width of the texture. + height: Height of the texture. + + Returns: + The texture ID. + """ + self._sync_renderer(model) + return self._viewer.RenderToTexture( + model, + data, + self.camera, + width, + height, + tex_id, + ) diff --git a/python/mujoco/experimental/studio/parser.cc b/python/mujoco/experimental/studio/parser.cc new file mode 100644 index 00000000..c1e191ce --- /dev/null +++ b/python/mujoco/experimental/studio/parser.cc @@ -0,0 +1,46 @@ +// Copyright 2026 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include +#include "third_party/mujoco/src/experimental/platform/sim/model_holder.h" +#include "structs.h" +#include + +namespace mujoco::python { + +// Loads, parses, and compiles a MuJoCo model from the given file. Returns the +// python mjData object (which also contains the compiled mjModel). +py::object Parse(std::string_view filepath) { + auto holder = platform::ModelHolder::FromFile(filepath); + if (!holder->ok()) { + throw py::value_error( + std::string("Failed to load model from '") + + std::string(filepath) + "': " + std::string(holder->error())); + } + mjModel* model = holder->ReleaseModel(); + mjData* data = holder->ReleaseData(); + py::object py_model = py::cast(MjModelWrapper(model)); + py::object py_data = + py::cast(MjDataWrapper(py::cast(py_model), data)); + return py_data; +} + +} // namespace mujoco::python + +PYBIND11_MODULE(parser, m) { + m.def("parse", &mujoco::python::Parse, + pybind11::return_value_policy::take_ownership); +} diff --git a/python/mujoco/experimental/studio/renderer.cc b/python/mujoco/experimental/studio/renderer.cc new file mode 100644 index 00000000..b121e1e9 --- /dev/null +++ b/python/mujoco/experimental/studio/renderer.cc @@ -0,0 +1,78 @@ +// Copyright 2026 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "third_party/mujoco/src/experimental/platform/hal/renderer.h" + +#include +#include +#include +#include +#include + +#include +#include "third_party/mujoco/src/experimental/platform/hal/graphics_mode.h" +#include "structs.h" +#include +#include +#include +#include + +namespace mujoco::python { + +class Renderer { + public: + using RendererImpl = mujoco::platform::Renderer; + using GraphicsMode = mujoco::platform::GraphicsMode; + + Renderer(const std::string& graphics_mode_str) { + const GraphicsMode mode = mujoco::platform::GraphicsModeFromString( + graphics_mode_str, GraphicsMode::FilamentOpenGl); + impl_ = std::make_unique(nullptr, mode); + } + + void Init(const MjModelWrapper& model) { impl_->Init(model.get()); } + + pybind11::bytes Render(const MjModelWrapper& model, MjDataWrapper& data, + std::optional& perturb, + std::optional& camera, + std::optional& vis_option, int width, + int height) { + std::vector pixels(width * height * 3); + impl_->Render( + model.get(), data.get(), perturb ? perturb.value().get() : nullptr, + camera ? camera.value().get() : nullptr, + vis_option ? vis_option.value().get() : nullptr, width, height, pixels); + return pybind11::bytes((const char*)pixels.data(), pixels.size()); + } + + pybind11::memoryview GetRenderFlags() { + return pybind11::memoryview::from_buffer( + impl_->GetRenderFlags(), {static_cast(mjNRNDFLAG)}, + {sizeof(mjtByte)}); + } + + private: + std::unique_ptr impl_; +}; + +} // namespace mujoco::python + +PYBIND11_MODULE(renderer, m) { + pybind11::class_(m, "Renderer") + .def(pybind11::init()) + .def("Init", &mujoco::python::Renderer::Init) + .def("Render", &mujoco::python::Renderer::Render) + .def("get_render_flags", &mujoco::python::Renderer::GetRenderFlags, + pybind11::keep_alive<0, 1>()); +} diff --git a/python/mujoco/experimental/studio/sample/async.py b/python/mujoco/experimental/studio/sample/async.py new file mode 100644 index 00000000..33f6d255 --- /dev/null +++ b/python/mujoco/experimental/studio/sample/async.py @@ -0,0 +1,231 @@ +# Copyright 2026 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This script runs a simulation and viewer in separate processes communicating asynchronously. + +In this example, we will run the viewer in an independent process communicating +via multiprocessing queues. Controls are provided to simulate network transit +latency and adjust the communication rates. + +You must provide a mjcf model file via the first command-line argument. +""" + +import dataclasses +import multiprocessing +import os +import sys +import time + +from absl import app as absl_app +from absl import flags as absl_flags +import mujoco +from mujoco.experimental.studio import native_viewer as _viewer +from mujoco.experimental.studio import sim as _sim +from mujoco.experimental.studio import studio_app +from mujoco.experimental.studio import ux +import numpy as np + +from mujoco.experimental.dear_imgui import dear_imgui as imgui + +_GFX = absl_flags.DEFINE_string('gfx', '', 'Rendering graphics mode.') +_WIDTH = absl_flags.DEFINE_integer('width', 1200, 'Width of the output image.') +_HEIGHT = absl_flags.DEFINE_integer('height', 800, 'Height of the output image') + + +@dataclasses.dataclass +class SimToView: + """A message sent from the simulation process to the viewer process.""" + + model: mujoco.MjModel | None = None + data: mujoco.MjData | None = None + state: np.ndarray | None = None + state_sig: int = 0 + send_time: float = 0.0 + + +@dataclasses.dataclass +class ViewToSim: + """A message sent from the viewer process to the simulation process.""" + + state: np.ndarray | None = None + state_sig: int = 0 + reset: bool = False + send_rate: float = 60.0 + + +class Network: + """Simulated networking parameters.""" + + def __init__(self) -> None: + self.transit_buffer = [] + self.send_rate = 60.0 + self.network_delay = 0.2 + + def get_arrived(self, q: multiprocessing.Queue) -> SimToView | None: + now = time.time() + while not q.empty(): + self.transit_buffer.append(q.get()) + arrived = None + while ( + self.transit_buffer + and now >= self.transit_buffer[0].send_time + self.network_delay + ): + arrived = self.transit_buffer.pop(0) + return arrived + + +def view( + sim_to_view: multiprocessing.Queue, + view_to_sim: multiprocessing.Queue, +) -> None: + """Entry-point for process that renders the simulation.""" + # Block until the first message (containing the model) arrives. + msg = sim_to_view.get() + assert msg.model is not None, 'First message must contain the MuJoCo model.' + + title = os.path.basename(sys.argv[0]) + xfrc_sig = int(mujoco.mjtState.mjSTATE_XFRC_APPLIED) + xfrc_size = mujoco.mj_stateSize(msg.model, xfrc_sig) + xfrc_state = np.zeros(xfrc_size, np.float64) + + app = studio_app.StudioApp(msg.model, msg.data) + network = Network() + viewer = _viewer.NativeViewer( + app.model, + title=title, + width=_WIDTH.value, + height=_HEIGHT.value, + gfx=_GFX.value, + ) + + while viewer.is_running() and app.is_running(): + + # Determine which messages have arrived through the simulated network. + arrived = network.get_arrived(sim_to_view) + + # Update the camera and compute the perturbation. + app.handle_mouse_events(viewer.camera, viewer.vis_options, viewer.perturb) + + # Sync state from the backend if a new payload actually arrived. + if arrived is not None and arrived.state is not None: + mujoco.mj_setState(app.model, app.data, arrived.state, arrived.state_sig) + mujoco.mj_forward(app.model, app.data) + + # Always apply the perturbation forces from the viewer. + app.apply_perturb(viewer.perturb) + + # Transmit user interaction when we get a new state + if arrived is not None: + mujoco.mj_getState(app.model, app.data, xfrc_state, xfrc_sig) + view_to_sim.put( + ViewToSim( + send_rate=network.send_rate, state=xfrc_state, state_sig=xfrc_sig + ) + ) + + # Build the UI. + ux.setup_theme(app.theme) + if imgui.Begin( + 'Settings', + flags=int(imgui.WindowFlags.AlwaysAutoResize) + | int(imgui.WindowFlags.NoTitleBar) + | int(imgui.WindowFlags.NoCollapse), + ): + imgui.PushItemWidth(200.0) + _, network.network_delay = imgui.SliderFloat( + 'Network Latency (s)', network.network_delay, 0.0, 2.0 + ) + updated, network.send_rate = imgui.SliderFloat( + 'Send Rate (Hz)', network.send_rate, 1.0, 120.0 + ) + if updated: + view_to_sim.put(ViewToSim(send_rate=network.send_rate)) + + imgui.SetNextItemWidth(-1) + if imgui.Button('Reset Simulation'): + view_to_sim.put(ViewToSim(reset=True, send_rate=network.send_rate)) + + imgui.PopItemWidth() + imgui.End() + + viewer.sync(app.model, app.data) + + +def sim( + data: mujoco.MjData, + model: mujoco.MjModel, + sim_to_view: multiprocessing.Queue, + view_to_sim: multiprocessing.Queue, + view_process: multiprocessing.Process, +) -> None: + """Entry-point for process that runs the simulation.""" + + sim_to_view.put(SimToView(model=model, data=data)) + + step_control = _sim.StepControl() + integration_sig = int(mujoco.mjtState.mjSTATE_INTEGRATION) + integration_size = mujoco.mj_stateSize(model, integration_sig) + integration_state = np.empty(integration_size, np.float64) + + msg = ViewToSim() + last_send_time = time.time() + + while view_process.is_alive(): + while not view_to_sim.empty(): + msg = view_to_sim.get() + + if msg.reset: + mujoco.mj_resetData(model, data) + mujoco.mj_forward(model, data) + msg.reset = False + + # Apply perturbation forces received from the viewer process. + if msg.state is not None: + mujoco.mj_setState(model, data, msg.state, msg.state_sig) + + # Advance the simulation keeping up with real-time. + step_control.advance(model, data) + + # Send the simulation state paced by the requested send_rate. + now = time.time() + if now - last_send_time >= 1.0 / max(1.0, msg.send_rate): + mujoco.mj_getState(model, data, integration_state, integration_sig) + sim_to_view.put( + SimToView( + state=integration_state, + state_sig=integration_sig, + send_time=now, + ) + ) + last_send_time = now + + +def main(argv: list[str]) -> None: + app = studio_app.StudioApp.from_argv(argv) + + # Queues for communication between the simulation and viewer processes. + sim_to_view = multiprocessing.Queue() + view_to_sim = multiprocessing.Queue() + + # Start the viewer process. + view_process = multiprocessing.Process( + target=view, args=(sim_to_view, view_to_sim) + ) + view_process.start() + + # Start the simulation in the main process. + sim(app.data, app.model, sim_to_view, view_to_sim, view_process) + + +if __name__ == '__main__': + absl_app.run(main) diff --git a/python/mujoco/experimental/studio/sample/implot.py b/python/mujoco/experimental/studio/sample/implot.py new file mode 100644 index 00000000..a801fccb --- /dev/null +++ b/python/mujoco/experimental/studio/sample/implot.py @@ -0,0 +1,199 @@ +# Copyright 2026 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Example to run studio in the native viewer with responsive ImPlot UI. + +This script runs a Studio viewer in-process and adds an 'Inspect Body' window +using ImGui and ImPlot bindings to visualize selected body data. The example +demonstrates how responsive UI layout rules are easily implemented. + +Provide an MJCF model file via the first command-line argument to launch. +""" + +import math +import os +import sys + +from absl import app as absl_app +from absl import flags as absl_flags +import mujoco +from mujoco.experimental.studio import native_viewer as _viewer +from mujoco.experimental.studio import studio_app +import numpy as np + +from mujoco.experimental.dear_imgui import dear_imgui as imgui +from mujoco.experimental.implot import implot + +_GFX = absl_flags.DEFINE_string('gfx', '', 'Rendering graphics mode.') +_WIDTH = absl_flags.DEFINE_integer('width', 1200, 'Width of the output image.') +_HEIGHT = absl_flags.DEFINE_integer('height', 800, 'Height of the output image') + + +_N_HISTORY = 100 + +_PLOT_FLAGS = ( + implot.Flags.NoInputs.value # Disable pan/zoom mouse interaction. + | implot.Flags.NoMenus.value # Disable right-click context menu. + | implot.Flags.NoBoxSelect.value # Disable drag-to-select regions. +) + +_AXIS_FLAGS = ( + implot.AxisFlags.NoGridLines.value # Hide background grid lines. + | implot.AxisFlags.NoTickMarks.value # Hide small tick marks on the axis. +) + + +def _setup_plot_flags(plot_size: imgui.Vec2) -> int: + flags = _PLOT_FLAGS + if min(plot_size.x, plot_size.y) < 300: + flags |= implot.Flags.NoTitle.value + if min(plot_size.x, plot_size.y) < 200: + flags |= implot.Flags.NoLegend.value + return flags + + +def _setup_time_axis(plot_size: imgui.Vec2) -> None: + flags = _AXIS_FLAGS + if plot_size.x < 300: + flags |= implot.AxisFlags.NoTickLabels.value + implot.SetupAxis(implot.Axis.X1, '', flags) + implot.SetupAxisLimits(implot.Axis.X1, 0, _N_HISTORY) + + +def _setup_xpos_axis(centroid: list[np.ndarray], plot_size: imgui.Vec2) -> None: + flags = _AXIS_FLAGS + if plot_size.y < 300: + flags |= implot.AxisFlags.NoTickLabels.value + implot.SetupAxis(implot.Axis.Y1, '', flags) + min_y = min(c[1] for c in centroid) + max_y = max(c[1] for c in centroid) + margin = max((max_y - min_y) * 0.1, 0.05) + implot.SetupAxisLimits( + implot.Axis.Y1, + min_y - margin, + max_y + margin, + cond=implot.Cond.Always, + ) + + +def _setup_angle_axis(plot_size: imgui.Vec2) -> None: + flags = _AXIS_FLAGS + if plot_size.y < 300: + flags |= implot.AxisFlags.NoTickLabels.value + implot.SetupAxis(implot.Axis.Y1, '', flags) + implot.SetupAxisLimits(implot.Axis.Y1, -185.0, 185.0) + implot.SetupAxisTicks( + implot.Axis.Y1, + [-180.0, -90.0, 0.0, 90.0, 180.0], + ['-180', '-90', '0', '90', '180'], + ) + + +def main(argv: list[str]) -> None: + app = studio_app.StudioApp.from_argv(argv) + title = os.path.basename(sys.argv[0]) + + # Initialize the viewer. + viewer = _viewer.NativeViewer( + app.model, + title=title, + width=_WIDTH.value, + height=_HEIGHT.value, + gfx=_GFX.value, + ) + + # Variables for the custom UI. + centroid = [np.zeros(3) for _ in range(_N_HISTORY)] + euler = [np.zeros(3) for _ in range(_N_HISTORY)] + body_id = -1 + + # Main viewer loop. + while viewer.is_running(): + if not app.update(viewer.camera, viewer.vis_options, viewer.perturb): + break + + # Build standard Studio UI. + app.build_gui(viewer.camera, viewer.vis_options, viewer.render_flags) + + # Inspect the perturb.select body + if viewer.perturb.select > 0: + body_id = viewer.perturb.select + + # Display selected body information. + if body_id > 0: + body_name = mujoco.mj_id2name( + app.model, int(mujoco.mjtObj.mjOBJ_BODY), body_id + ) + + imgui.SetNextWindowSize(imgui.Vec2(1200, 600), imgui.Cond.FirstUseEver) + + # Note: The window title uses the special "###" markup to ensure the imgui + # ID for the window is constant for all body names. This is needed for + # the window to retain its state for all bodies. + window_title = f'Inspect Body {body_name or "(???)"!r} ({body_id})###Plot' + if imgui.Begin(window_title): + avail = imgui.GetContentRegionAvail() + wide = avail.x > avail.y + + # Add a small padding factor to prevent scrollbars. + plot_size = imgui.Vec2( + avail.x * 0.5 - 4 if wide else avail.x, + avail.y if wide else avail.y * 0.5 - 4, + ) + + plot_flags = _setup_plot_flags(plot_size) + if implot.BeginPlot('Centroid vs Time', plot_size, flags=plot_flags): + _setup_time_axis(plot_size) + _setup_xpos_axis(centroid, plot_size) + implot.PlotLine('x', range(_N_HISTORY), [c[0] for c in centroid]) + implot.PlotLine('y', range(_N_HISTORY), [c[1] for c in centroid]) + implot.PlotLine('z', range(_N_HISTORY), [c[2] for c in centroid]) + implot.EndPlot() + + if wide: + imgui.SameLine() + + if implot.BeginPlot('Euler Angle vs Time', plot_size, flags=plot_flags): + _setup_time_axis(plot_size) + _setup_angle_axis(plot_size) + implot.PlotLine('roll', range(_N_HISTORY), [e[0] for e in euler]) + implot.PlotLine('pitch', range(_N_HISTORY), [e[1] for e in euler]) + implot.PlotLine('yaw', range(_N_HISTORY), [e[2] for e in euler]) + implot.EndPlot() + imgui.End() + + # Update plot data + centroid.pop(0) + euler.pop(0) + if body_id > 0: + centroid.append(app.data.xpos[body_id].copy()) + # Convert quaternion to Euler angles via rotation matrix. + quat = app.data.xquat[body_id] + mat = np.zeros(9) + mujoco.mju_quat2Mat(mat, quat) + # mat is row-major 3x3: R[i,j] = mat[3*i + j]. + roll = math.atan2(mat[7], mat[8]) + pitch = math.atan2(-mat[6], math.sqrt(mat[7] ** 2 + mat[8] ** 2)) + yaw = math.atan2(mat[3], mat[0]) + euler.append(np.degrees(np.array([roll, pitch, yaw]))) + else: + centroid.append(np.zeros(3)) + euler.append(np.zeros(3)) + + viewer.sync(app.model, app.data) + + viewer.stop() + + +if __name__ == '__main__': + absl_app.run(main) diff --git a/python/mujoco/experimental/studio/sample/render.py b/python/mujoco/experimental/studio/sample/render.py new file mode 100644 index 00000000..c373537c --- /dev/null +++ b/python/mujoco/experimental/studio/sample/render.py @@ -0,0 +1,73 @@ +# Copyright 2026 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Render a MuJoCo model to an image.""" + +import os +import sys + +from absl import app +from absl import flags +import mujoco +from mujoco.experimental.studio import parser +from mujoco.experimental.studio import renderer +from PIL import Image + +_MODEL = flags.DEFINE_string('model', '', 'Model file to load.') +_OUTPUT = flags.DEFINE_string('output', '', 'Output file to save.') +_GFX = flags.DEFINE_string('gfx', '', 'Renderer to use.') +_WIDTH = flags.DEFINE_integer('width', 320, 'Width of the output image.') +_HEIGHT = flags.DEFINE_integer('height', 240, 'Height of the output image.') +_STEPS = flags.DEFINE_integer('steps', 1, 'Number of steps before render.') + + +def main(argv): + if len(argv) > 1: + raise app.UsageError('Too many command-line arguments.') + if not _MODEL.value: + raise ValueError('`model` flag is required.') + if not _OUTPUT.value: + raise ValueError('`output flag is required.') + + try: + data = parser.parse(_MODEL.value) + model = data.model + except Exception as ex: # pylint: disable=broad-except + print(f'Error loading model from `{_MODEL.value}`: {ex}') + sys.exit(-1) + + for _ in range(_STEPS.value): + mujoco.mj_step(model, data) + + try: + r = renderer.Renderer(_GFX.value) + r.Init(model) + pixels = r.Render( + model, data, None, None, None, _WIDTH.value, _HEIGHT.value + ) + except Exception as ex: # pylint: disable=broad-except + print(f'Error rendering model: {ex}') + sys.exit(-2) + + try: + img = Image.frombytes('RGB', (_WIDTH.value, _HEIGHT.value), pixels) + img.save(_OUTPUT.value, format=os.path.splitext(_OUTPUT.value)[1][1:]) + except Exception as ex: # pylint: disable=broad-except + print(f'Error saving image to `{_OUTPUT.value}`: {ex}') + sys.exit(-3) + + return 0 + + +if __name__ == '__main__': + app.run(main) diff --git a/python/mujoco/experimental/studio/sim.cc b/python/mujoco/experimental/studio/sim.cc new file mode 100644 index 00000000..ae75612b --- /dev/null +++ b/python/mujoco/experimental/studio/sim.cc @@ -0,0 +1,65 @@ +// Copyright 2026 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Python bindings for MuJoCo platform simulation components. + +#include +#include "third_party/mujoco/src/experimental/platform/sim/step_control.h" +#include "structs.h" +#include + +namespace py = pybind11; + +using StepControl = mujoco::platform::StepControl; + +PYBIND11_MODULE(sim, m) { + m.doc() = "MuJoCo platform simulation bindings for Link."; + + py::enum_(m, "StepStatus") + .value("OK", StepControl::Status::kOk) + .value("PAUSED", StepControl::Status::kPaused) + .value("VISCOUS_PAUSED", StepControl::Status::kViscousPaused) + .value("AUTO_RESET", StepControl::Status::kAutoReset) + .value("DIVERGED", StepControl::Status::kDiverged); + + py::enum_(m, "PauseState") + .value("UNPAUSED", StepControl::PauseState::kUnpaused) + .value("NORMAL_PAUSED", StepControl::PauseState::kNormalPaused) + .value("VISCOUS_PAUSED", StepControl::PauseState::kViscousPaused); + + py::class_(m, "StepControl") + .def(py::init<>()) + .def( + "advance", + [](StepControl& self, mujoco::python::MjModelWrapper& model, + mujoco::python::MjDataWrapper& data) { + return self.Advance(model.get(), data.get()); + }, + py::arg("model"), py::arg("data"), + "Step physics forward, respecting speed settings and refresh budget.") + .def("force_sync", &StepControl::ForceSync, + "Ensures the next Advance() will synchronize time and step once.") + .def("get_speed", &StepControl::GetSpeed, + "Returns the desired simulation speed as a percentage of real time.") + .def("get_speed_measured", &StepControl::GetSpeedMeasured, + "Returns the measured simulation speed.") + .def("set_speed", &StepControl::SetSpeed, py::arg("speed"), + "Sets the desired speed (clamped to [0.1%, 100%]).") + .def("set_pause_state", &StepControl::SetPauseState, py::arg("state"), + "Sets the pause state of the simulation.") + .def("get_pause_state", &StepControl::GetPauseState, + "Returns the current pause state.") + .def("request_single_step", &StepControl::RequestSingleStep, + "Request a single step if paused."); +} diff --git a/python/mujoco/experimental/studio/studio.py b/python/mujoco/experimental/studio/studio.py new file mode 100644 index 00000000..b21af916 --- /dev/null +++ b/python/mujoco/experimental/studio/studio.py @@ -0,0 +1,50 @@ +# Copyright 2026 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This script runs Studio from Python, visualized in a native viewer.""" + +from absl import app as absl_app +from absl import flags as absl_flags +from mujoco.experimental.studio import native_viewer +from mujoco.experimental.studio import studio_app + +_GFX = absl_flags.DEFINE_string('gfx', '', 'Rendering graphics mode.') +_WIDTH = absl_flags.DEFINE_integer('width', 1200, 'Width of the output image.') +_HEIGHT = absl_flags.DEFINE_integer('height', 800, 'Height of the output image') + + +def main(argv: list[str]) -> None: + app = studio_app.StudioApp.from_argv(argv) + + # Initialize the viewer. + viewer = native_viewer.NativeViewer( + app.model, + width=_WIDTH.value, + height=_HEIGHT.value, + gfx=_GFX.value, + ) + + # Main viewer loop. + while viewer.is_running(): + if not app.update_from_viewer(viewer): + break + + app.build_gui(viewer.camera, viewer.vis_options, viewer.render_flags) + + viewer.sync(app.model, app.data) + + viewer.stop() + + +if __name__ == '__main__': + absl_app.run(main) diff --git a/python/mujoco/experimental/studio/studio_app.py b/python/mujoco/experimental/studio/studio_app.py new file mode 100644 index 00000000..4b50ea25 --- /dev/null +++ b/python/mujoco/experimental/studio/studio_app.py @@ -0,0 +1,440 @@ +# Copyright 2026 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Viewer-agnostic Python implementation of Studio. + +Architecture: + StudioApp owns the simulation state (model, data) and the UI logic. + Viewer classes (e.g., NativeViewer) own the window (if required), renderer, + camera, and visualization options. The viewer never stores references to + model or data. Instead, the caller passes them each frame via + viewer.sync(model, data). This ensures the viewer always renders the current + model, even if StudioApp.load_model_from_file() swaps it. + +The class can be used to implement the full Studio application in Python. By +using the more granular member functions it can also build simple apps that only +use a subset of the Studio UI. This configuration is fully dynamic, there is +nothing to configure in advance, you can change your app by changing the +functions that get called each frame. This class is also viewer-agnostic and as +such does not own camera, vis_options or perturb objects (these are provided by +the viewer). + +See the sample/ folder for usage examples. +""" + +import os +import sys + +import mujoco +from mujoco.experimental.studio import parser +from mujoco.experimental.studio import sim +from mujoco.experimental.studio import studio_app_events as events +from mujoco.experimental.studio import ux +from mujoco.experimental.studio import viewer_protocol +import numpy as np + +from mujoco.experimental.dear_imgui import dear_imgui as imgui + + +def load_model_from_file( + model_path: str, +) -> tuple[mujoco.MjModel, mujoco.MjData] | None: + """Loads a model and data from a file path.""" + try: + data = parser.parse(model_path) + return data.model, data + except Exception as ex: # pylint: disable=broad-except + print(f'Error loading model from {model_path!r}: {ex}') + return None + + +class StudioApp: + """Viewer-agnostic Python implementation of Studio.""" + + @classmethod + def from_argv(cls, argv: list[str]) -> 'StudioApp': + """Constructs a StudioApp by parsing a model path from command-line args.""" + if len(argv) < 2: + model = mujoco.MjSpec().compile() + data = mujoco.MjData(model) + app = cls(model, data) + app.step_control.set_pause_state(sim.PauseState.NORMAL_PAUSED) + return app + + model_path = argv[1] + + res = load_model_from_file(model_path) + if res is None: + sys.exit(-1) + model, data = res + + app = cls(model, data) + app.model_path = model_path + return app + + def load_model_from_file( + self, model_path: str + ) -> tuple[mujoco.MjModel, mujoco.MjData] | None: + """Loads a new model from a file, replacing the current model and data.""" + res = load_model_from_file(model_path) + if res is None: + self.status = f'Error loading model from {model_path!r}' + return None + + model, data = res + self.model = model + self.data = data + self.model_path = model_path + self.step_control = sim.StepControl() + self.ux_state = ux.UxState() + self.status = f'Loaded: {os.path.basename(model_path)!r}' + + return model, data + + def __init__( + self, + model: mujoco.MjModel, + data: mujoco.MjData, + ): + """Initializes the Studio application.""" + self.model = model + self.data = data + self.model_path = '' + + self.step_control = sim.StepControl() + self.ux_state = ux.UxState() + self.theme = ux.GuiTheme.LIGHT + self.show_stats = False + self.show_solver = False + self.should_quit = False + self.status = 'Ready' + # TODO(matijak): This should be part of the viewer, also making a struct to + # pass it around with the camera would be convenient. + self._cam_speed = 0.001 + + def handle_vis_options_keyboard_events( + self, + vis_options: mujoco.MjvOption, + is_freecam_wasd: bool, + ) -> bool: + """Toggles visualization flags based on keyboard shortcuts. + + Args: + vis_options: The visualization options to modify. + is_freecam_wasd: If True, keys Q/E/A/D are reserved for camera movement + and will not toggle visualization flags. + + Returns: + True if a key was handled, False otherwise. + """ + if imgui.GetIO().WantCaptureKeyboard: + return False + + return events.handle_vis_options_keyboard_events( + vis_options, is_freecam_wasd + ) + + def handle_step_control_keyboard_events(self) -> bool: + """Handles keyboard shortcuts for simulation stepping control. + + Returns: + True if a key was handled, False otherwise. + """ + if imgui.GetIO().WantCaptureKeyboard: + return False + + return events.handle_step_control_keyboard_events( + self.model, self.data, self.step_control, self.ux_state + ) + + def handle_freecam_wasd_keyboard_events( + self, + camera: mujoco.MjvCamera, + ) -> bool: + """Handles keyboard shortcuts for free camera movement.""" + if imgui.GetIO().WantCaptureKeyboard: + return False + + handled, self._cam_speed = events.handle_freecam_wasd_keyboard_events( + self.model, self.data, camera, self._cam_speed + ) + return handled + + def handle_keyboard_events( + self, + camera: mujoco.MjvCamera, + vis_options: mujoco.MjvOption, + ) -> bool: + """Handle keyboard events according to Studio's bindings.""" + if imgui.GetIO().WantCaptureKeyboard: + return False + + is_freecam_wasd = self.ux_state.camera_index == ux.FREE_CAMERA_IDX + if events.handle_step_control_keyboard_events( + self.model, self.data, self.step_control, self.ux_state + ): + return True + + if events.handle_camera_select_keyboard_events( + self.model, camera, self.ux_state + ): + return True + + if events.handle_vis_options_keyboard_events(vis_options, is_freecam_wasd): + return True + + if is_freecam_wasd: + handled, self._cam_speed = events.handle_freecam_wasd_keyboard_events( + self.model, self.data, camera, self._cam_speed + ) + if handled: + return True + + return False + + def handle_camera_tracking_mouse_events( + self, + camera: mujoco.MjvCamera, + vis_options: mujoco.MjvOption, + ) -> None: + """Handles mouse events for camera tracking.""" + if imgui.GetIO().WantCaptureMouse: + return + + events.handle_camera_tracking_mouse_events( + self.model, self.data, camera, vis_options, self.ux_state + ) + + def handle_mouse_events( + self, + camera: mujoco.MjvCamera, + vis_options: mujoco.MjvOption, + perturb: mujoco.MjvPerturb, + ) -> None: + """Handles mouse events.""" + if imgui.GetIO().WantCaptureMouse: + return + + events.handle_mouse_events( + self.model, self.data, camera, vis_options, perturb, self.ux_state + ) + + def reset_physics(self) -> None: + """Reset the physics.""" + mujoco.mj_resetData(self.model, self.data) + mujoco.mj_forward(self.model, self.data) + + def apply_perturb(self, perturb: mujoco.MjvPerturb) -> None: + """Apply perturbation the model.""" + if self.step_control.get_pause_state() != sim.PauseState.NORMAL_PAUSED: + sig = mujoco.mjtState.mjSTATE_XFRC_APPLIED.value + size = mujoco.mj_stateSize(self.model, sig) + zero_state = np.zeros(size, np.float64) + mujoco.mj_setState(self.model, self.data, zero_state, sig) + mujoco.mjv_applyPerturbPose(self.model, self.data, perturb, 0) + mujoco.mjv_applyPerturbForce(self.model, self.data, perturb) + else: + mujoco.mjv_applyPerturbPose(self.model, self.data, perturb, 1) + + def update_physics(self, perturb: mujoco.MjvPerturb) -> None: + """Applies the purturbations and advances the physics.""" + self.apply_perturb(perturb) + + advance_status = self.step_control.advance(self.model, self.data) + if advance_status == sim.StepStatus.AUTO_RESET: + self.reset_physics() + + def reset_physics_gui(self) -> None: + """GUI to Reset the physics i.e., the reset button.""" + button_size = imgui.GetFrameHeight() + square_size = imgui.Vec2(button_size, button_size) + icon_reset_model = '\uf0e2' # FontAwesome "undo" icon. + if imgui.Button(icon_reset_model, square_size): + self.reset_physics() + imgui.SetItemTooltip('Reset') + + def is_running(self) -> bool: + """Returns True if the application should continue running (called by update()).""" + return not self.should_quit + + def update( + self, + camera: mujoco.MjvCamera, + vis_options: mujoco.MjvOption, + perturb: mujoco.MjvPerturb, + drop_file: str = '', + ) -> bool: + """Update the simulation and handle user input. + + Handles mouse input to compute perturbations or camera motion. + Handles keyboard input e.g., for keybindings or camera motion. + Applies the purturbations and advances the physics. + The argument objects are provided by the viewer. + + Args: + camera: The MuJoCo camera object. + vis_options: The MuJoCo visualization options. + perturb: The MuJoCo perturbation object. + drop_file: Path of a file dropped into the viewer window. If non-empty the + current model is replaced with the dropped file. + + Returns: + Whether the application should continue running, this is a + convenience to allow this function to be used in a while loop. + """ + if drop_file: + self.load_model_from_file(drop_file) + + self.handle_mouse_events(camera, vis_options, perturb) + self.handle_keyboard_events(camera, vis_options) + self.update_physics(perturb) + return self.is_running() + + def update_from_viewer(self, viewer: viewer_protocol.Viewer) -> bool: + """Convenience wrapper around update() that unpacks viewer attributes.""" + return self.update( + viewer.camera, + viewer.vis_options, + viewer.perturb, + drop_file=viewer.get_drop_file(), + ) + + def build_gui( + self, + camera: mujoco.MjvCamera, + vis_options: mujoco.MjvOption, + render_flags: ux.RenderFlags, + ) -> None: + """Emit full Studio UI.""" + ux.setup_theme(self.theme) + ux.configure_docking_layout() + + # -- Main menu bar -------------------------------------------------------- + if imgui.BeginMainMenuBar(): + if imgui.BeginMenu('File'): + if imgui.MenuItem('Quit'): + self.should_quit = True + imgui.EndMenu() + if imgui.BeginMenu('Simulation'): + imgui.EndMenu() + if imgui.BeginMenu('Charts'): + if imgui.MenuItem('Solver', '', self.show_solver): + self.show_solver = not self.show_solver + if imgui.MenuItem('Stats', '', self.show_stats): + self.show_stats = not self.show_stats + imgui.EndMenu() + if imgui.BeginMenu('Help'): + if imgui.MenuItem('Stats', '', self.show_stats): + self.show_stats = not self.show_stats + imgui.Separator() + version = f'Version {mujoco.mj_versionString()}' + imgui.MenuItem(version) + imgui.EndMenu() + imgui.EndMainMenuBar() + + # -- Tool Bar ------------------------------------------------------------- + if imgui.Begin('ToolBar'): + imgui.PushStyleVar(imgui.StyleVar.CellPadding, imgui.Vec2(0, 0)) + if imgui.BeginTable('##ToolBarTable', 2): + imgui.TableSetupColumn('', int(imgui.TableColumnFlags.WidthStretch)) + imgui.TableSetupColumn('', int(imgui.TableColumnFlags.WidthFixed)) + + imgui.TableNextColumn() + self.reset_physics_gui() + + imgui.SameLine() + ux.step_control_gui(self.model, self.step_control, self.ux_state) + + imgui.TableNextColumn() + ux.camera_selection_gui(self.model, self.data, camera, self.ux_state) + + imgui.SameLine() + ux.label_selection_gui(vis_options) + + imgui.SameLine() + ux.frame_selection_gui(vis_options) + + imgui.SameLine() + changed, self.theme = ux.theme_select_gui(self.theme) + if changed: + ux.setup_theme(self.theme) + + imgui.EndTable() + imgui.PopStyleVar() + imgui.End() + + # -- Left pane: Options --------------------------------------------------- + node_flags = int(imgui.TreeNodeFlags.SpanAvailWidth) | int( + imgui.TreeNodeFlags.Framed + ) + + imgui.Begin('Options') + if imgui.TreeNodeEx('Physics Settings', node_flags): + ux.physics_gui(self.model) + imgui.TreePop() + if imgui.TreeNodeEx('Rendering Settings', node_flags): + ux.rendering_gui(self.model, vis_options, render_flags) + imgui.TreePop() + if imgui.TreeNodeEx('Visibility Groups', node_flags): + ux.groups_gui(self.model, vis_options) + imgui.TreePop() + if imgui.TreeNodeEx('Visualization', node_flags): + ux.visualization_gui(self.model, vis_options, camera) + imgui.TreePop() + imgui.End() + + # -- Right pane: Inspector ------------------------------------------------ + imgui.Begin('Inspector') + if imgui.TreeNodeEx('Noise', node_flags): + ux.noise_gui(self.model, self.data, self.ux_state) + imgui.TreePop() + if imgui.TreeNodeEx('Joints', node_flags): + ux.joints_gui(self.model, self.data, vis_options) + imgui.TreePop() + if imgui.TreeNodeEx('Controls', node_flags): + ux.controls_gui(self.model, self.data, vis_options) + imgui.TreePop() + if imgui.TreeNodeEx( + 'Sensors', node_flags | int(imgui.TreeNodeFlags.DefaultOpen) + ): + ux.sensor_gui(self.model, self.data) + imgui.TreePop() + if imgui.TreeNodeEx('Watch', node_flags): + ux.watch_gui(self.model, self.data, self.ux_state) + imgui.TreePop() + if imgui.TreeNodeEx('State', node_flags): + ux.state_gui(self.model, self.data, self.ux_state) + imgui.TreePop() + imgui.End() + + # -- Floating windows ----------------------------------------------------- + if self.show_solver: + _, self.show_solver = imgui.Begin('Solver', self.show_solver) + ux.counts_gui(self.model, self.data) + ux.convergence_gui(self.model, self.data) + imgui.End() + + if self.show_stats: + _, self.show_stats = imgui.Begin('Stats', self.show_stats) + paused = self.step_control.get_pause_state() != sim.PauseState.UNPAUSED + ux.stats_gui(self.model, self.data, paused, 0.0) + imgui.End() + + # -- Status bar ----------------------------------------------------------- + imgui.PushStyleVar(imgui.StyleVar.CellPadding, imgui.Vec2(0, 0)) + imgui.PushStyleVar(imgui.StyleVar.FramePadding, imgui.Vec2(0, 0)) + imgui.PushStyleVar(imgui.StyleVar.WindowPadding, imgui.Vec2(0, 0)) + if imgui.Begin('StatusBar'): + imgui.Text(self.status) + imgui.End() + imgui.PopStyleVar(3) diff --git a/python/mujoco/experimental/studio/studio_app_events.py b/python/mujoco/experimental/studio/studio_app_events.py new file mode 100644 index 00000000..45397c0e --- /dev/null +++ b/python/mujoco/experimental/studio/studio_app_events.py @@ -0,0 +1,592 @@ +# Copyright 2026 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Temporary event handling functions for StudioApp.""" + +# TODO(matijak): These free functions implement the keyboard and mouse event +# handling for Studio. They are separated from the main StudioApp class to keep +# clarify the long-term API and avoid cluttering it with a large amount of +# temporary code. When studio/platform has a proper API for registering key +# bindings and mouse behaviour, the event handling functions will delegate to +# code shared with the C++ studio application. + +import mujoco +from mujoco.experimental.studio import sim +from mujoco.experimental.studio import ux +import numpy as np + +from mujoco.experimental.dear_imgui import dear_imgui as imgui + + +def handle_vis_options_keyboard_events( + vis_options: mujoco.MjvOption, + is_freecam_wasd: bool, +) -> bool: + """Toggles visualization flags based on keyboard shortcuts. + + Args: + vis_options: The visualization options to modify. + is_freecam_wasd: If True, keys Q/E/A/D are reserved for camera movement and + will not toggle visualization flags. + + Returns: + True if a key was handled, False otherwise. + """ + if imgui.GetIO().WantCaptureKeyboard: + return False + + pressed = imgui.IsKeyChordPressed + + # Frame and label cycling. + if pressed(imgui.Key.F6): + vis_options.frame = (vis_options.frame + 1) % mujoco.mjtFrame.mjNFRAME.value + elif pressed(imgui.Key.F7): + vis_options.label = (vis_options.label + 1) % mujoco.mjtLabel.mjNLABEL.value + + # Visualization flag toggles (single-key shortcuts). + elif pressed(imgui.Key.H): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONVEXHULL] ^= 1 + elif pressed(imgui.Key.X): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_TEXTURE] ^= 1 + elif pressed(imgui.Key.J): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_JOINT] ^= 1 + elif not is_freecam_wasd and pressed(imgui.Key.Q): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_CAMERA] ^= 1 + elif pressed(imgui.Key.U): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_ACTUATOR] ^= 1 + elif pressed(imgui.Key.Comma): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_ACTIVATION] ^= 1 + elif pressed(imgui.Key.Z): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_LIGHT] ^= 1 + elif pressed(imgui.Key.V): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_TENDON] ^= 1 + elif pressed(imgui.Key.Y): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_RANGEFINDER] ^= 1 + elif not is_freecam_wasd and pressed(imgui.Key.E): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONSTRAINT] ^= 1 + elif pressed(imgui.Key.I): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_INERTIA] ^= 1 + elif pressed(imgui.Key.Apostrophe): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_SCLINERTIA] ^= 1 + elif pressed(imgui.Key.B): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_PERTFORCE] ^= 1 + elif pressed(imgui.Key.O): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_PERTOBJ] ^= 1 + elif pressed(imgui.Key.C): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] ^= 1 + elif pressed(imgui.Key.N): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_ISLAND] ^= 1 + elif pressed(imgui.Key.F): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONTACTFORCE] ^= 1 + elif pressed(imgui.Key.P): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_CONTACTSPLIT] ^= 1 + elif pressed(imgui.Key.T): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_TRANSPARENT] ^= 1 + elif not is_freecam_wasd and pressed(imgui.Key.A): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_AUTOCONNECT] ^= 1 + elif pressed(imgui.Key.M): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_COM] ^= 1 + elif not is_freecam_wasd and pressed(imgui.Key.D): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_STATIC] ^= 1 + elif pressed(imgui.Key.Semicolon): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_SKIN] ^= 1 + elif pressed(imgui.Key.GraveAccent): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_BODYBVH] ^= 1 + elif pressed(imgui.Key.Backslash): + vis_options.flags[mujoco.mjtVisFlag.mjVIS_MESHBVH] ^= 1 + + # Site group toggles (Shift + 0-5). + elif pressed(int(imgui.Key.N0) | int(imgui.Key.Shift)): + vis_options.sitegroup[0] ^= 1 + elif pressed(int(imgui.Key.N1) | int(imgui.Key.Shift)): + vis_options.sitegroup[1] ^= 1 + elif pressed(int(imgui.Key.N2) | int(imgui.Key.Shift)): + vis_options.sitegroup[2] ^= 1 + elif pressed(int(imgui.Key.N3) | int(imgui.Key.Shift)): + vis_options.sitegroup[3] ^= 1 + elif pressed(int(imgui.Key.N4) | int(imgui.Key.Shift)): + vis_options.sitegroup[4] ^= 1 + elif pressed(int(imgui.Key.N5) | int(imgui.Key.Shift)): + vis_options.sitegroup[5] ^= 1 + + # Geom group toggles (0-5). + elif pressed(imgui.Key.N0): + vis_options.geomgroup[0] ^= 1 + elif pressed(imgui.Key.N1): + vis_options.geomgroup[1] ^= 1 + elif pressed(imgui.Key.N2): + vis_options.geomgroup[2] ^= 1 + elif pressed(imgui.Key.N3): + vis_options.geomgroup[3] ^= 1 + elif pressed(imgui.Key.N4): + vis_options.geomgroup[4] ^= 1 + elif pressed(imgui.Key.N5): + vis_options.geomgroup[5] ^= 1 + + else: + return False + + return True + + +def handle_step_control_keyboard_events( + model: mujoco.MjModel, + data: mujoco.MjData, + step_control: sim.StepControl, + ux_state: ux.UxState, +) -> bool: + """Handles keyboard shortcuts for simulation stepping control. + + Args: + model: The MuJoCo model. + data: The MuJoCo data. + step_control: The simulation step control object. + ux_state: The UX state object. + + Returns: + True if a key was handled, False otherwise. + """ + if imgui.GetIO().WantCaptureKeyboard: + return False + + pressed = imgui.IsKeyChordPressed + + if pressed(int(imgui.Key.Ctrl) | int(imgui.Key.Space)): + if step_control.get_pause_state() == sim.PauseState.VISCOUS_PAUSED: + step_control.set_pause_state(sim.PauseState.UNPAUSED) + else: + step_control.set_pause_state(sim.PauseState.VISCOUS_PAUSED) + return True + elif pressed(imgui.Key.Space): + pause = step_control.get_pause_state() + if pause in (sim.PauseState.VISCOUS_PAUSED, sim.PauseState.UNPAUSED): + step_control.set_pause_state(sim.PauseState.NORMAL_PAUSED) + else: + step_control.set_pause_state(sim.PauseState.UNPAUSED) + return True + elif pressed(imgui.Key.Backspace): + mujoco.mj_resetData(model, data) + mujoco.mj_forward(model, data) + return True + elif pressed(imgui.Key.Minus): + ux.set_speed_index(step_control, ux_state, ux_state.speed_index + 1) + return True + elif pressed(imgui.Key.Equal): + ux.set_speed_index(step_control, ux_state, ux_state.speed_index - 1) + return True + + return False + + +def handle_camera_select_keyboard_events( + model: mujoco.MjModel, + camera: mujoco.MjvCamera, + ux_state: ux.UxState, +) -> bool: + """Handles keyboard shortcuts for camera selection. + + Args: + model: The MuJoCo model. + camera: The MuJoCo camera object. + ux_state: The UX state object. + + Returns: + True if a key was handled, False otherwise. + """ + if imgui.GetIO().WantCaptureKeyboard: + return False + + pressed = imgui.IsKeyChordPressed + + if pressed(imgui.Key.Escape): + ux_state.camera_index = ux.set_camera(model, camera, ux.TUMBLE_CAMERA_IDX) + return True + elif pressed(imgui.Key.LeftBracket): + ux_state.camera_index = ux.set_camera( + model, camera, ux_state.camera_index - 1 + ) + return True + elif pressed(imgui.Key.RightBracket): + ux_state.camera_index = ux.set_camera( + model, camera, ux_state.camera_index + 1 + ) + return True + + return False + + +def handle_freecam_wasd_keyboard_events( + model: mujoco.MjModel, + data: mujoco.MjData, + camera: mujoco.MjvCamera, + cam_speed: float, +) -> tuple[bool, float]: + """Handles keyboard shortcuts for free camera movement. + + Args: + model: The MuJoCo model. + data: The MuJoCo data. + camera: The MuJoCo camera object. + cam_speed: The current camera speed. + + Returns: + A tuple of (handled, updated_cam_speed). + """ + if imgui.GetIO().WantCaptureKeyboard: + return False, cam_speed + + if ( + imgui.IsKeyDown(imgui.Key.W) + or imgui.IsKeyDown(imgui.Key.S) + or imgui.IsKeyDown(imgui.Key.A) + or imgui.IsKeyDown(imgui.Key.D) + or imgui.IsKeyDown(imgui.Key.Q) + or imgui.IsKeyDown(imgui.Key.E) + ): + moved = False + + # Move (dolly) forward/backward using W and S keys. + if imgui.IsKeyDown(imgui.Key.W): + ux.MoveCamera( + model, + data, + camera, + ux.CameraMotion.TRUCK_DOLLY, + 0, + cam_speed, + ) + moved = True + elif imgui.IsKeyDown(imgui.Key.S): + ux.MoveCamera( + model, + data, + camera, + ux.CameraMotion.TRUCK_DOLLY, + 0, + -cam_speed, + ) + moved = True + + # Strafe (truck) left/right using A and D keys. + if imgui.IsKeyDown(imgui.Key.A): + ux.MoveCamera( + model, + data, + camera, + ux.CameraMotion.TRUCK_DOLLY, + -cam_speed, + 0, + ) + moved = True + elif imgui.IsKeyDown(imgui.Key.D): + ux.MoveCamera( + model, + data, + camera, + ux.CameraMotion.TRUCK_DOLLY, + cam_speed, + 0, + ) + moved = True + + # Move (pedestal) up/down using Q and E keys. + if imgui.IsKeyDown(imgui.Key.Q): + ux.MoveCamera( + model, + data, + camera, + ux.CameraMotion.TRUCK_PEDESTAL, + 0, + cam_speed, + ) + moved = True + elif imgui.IsKeyDown(imgui.Key.E): + ux.MoveCamera( + model, + data, + camera, + ux.CameraMotion.TRUCK_PEDESTAL, + 0, + -cam_speed, + ) + moved = True + + if moved: + cam_speed += 0.001 + max_speed = 0.1 if imgui.GetIO().KeyShift else 0.01 + if cam_speed > max_speed: + cam_speed = max_speed + else: + cam_speed = 0.001 + + return True, cam_speed + + return False, cam_speed + + +def handle_keyboard_events( + model: mujoco.MjModel, + data: mujoco.MjData, + camera: mujoco.MjvCamera, + vis_options: mujoco.MjvOption, + step_control: sim.StepControl, + ux_state: ux.UxState, + cam_speed: float, +) -> tuple[bool, float]: + """Handle keyboard events according to Studio's bindings. + + Args: + model: The MuJoCo model. + data: The MuJoCo data. + camera: The MuJoCo camera object. + vis_options: The MuJoCo visualization options. + step_control: The simulation step control object. + ux_state: The UX state object. + cam_speed: The current camera speed. + + Returns: + A tuple of (handled, updated_cam_speed). + """ + if imgui.GetIO().WantCaptureKeyboard: + return False, cam_speed + + is_freecam_wasd = ux_state.camera_index == ux.FREE_CAMERA_IDX + if handle_step_control_keyboard_events(model, data, step_control, ux_state): + return True, cam_speed + + if handle_camera_select_keyboard_events(model, camera, ux_state): + return True, cam_speed + + if handle_vis_options_keyboard_events(vis_options, is_freecam_wasd): + return True, cam_speed + + if is_freecam_wasd: + handled, cam_speed = handle_freecam_wasd_keyboard_events( + model, data, camera, cam_speed + ) + if handled: + return True, cam_speed + + return False, cam_speed + + +def handle_camera_tracking_mouse_events( + model: mujoco.MjModel, + data: mujoco.MjData, + camera: mujoco.MjvCamera, + vis_options: mujoco.MjvOption, + ux_state: ux.UxState, +) -> None: + """Handles mouse events for camera tracking.""" + io = imgui.GetIO() + if imgui.GetIO().WantCaptureMouse: + return + + if io.DisplaySize.x <= 0 or io.DisplaySize.y <= 0: + return + + mouse_x = io.MousePos.x / io.DisplaySize.x + mouse_y = io.MousePos.y / io.DisplaySize.y + aspect_ratio = io.DisplaySize.x / io.DisplaySize.y + + # Right double click. + if imgui.IsMouseDoubleClicked(imgui.MouseButton.Right): + picked = ux.Pick( + model, + data, + camera, + mouse_x, + mouse_y, + aspect_ratio, + vis_options, + ) + if picked.body > 0 and io.KeyCtrl: + # Switch camera to tracking mode and track the selected body. + camera.type = int(mujoco.mjtCamera.mjCAMERA_TRACKING) + camera.trackbodyid = picked.body + camera.fixedcamid = -1 + ux_state.camera_index = ux.TRACKING_CAMERA_IDX + + +def handle_mouse_events( + model: mujoco.MjModel, + data: mujoco.MjData, + camera: mujoco.MjvCamera, + vis_options: mujoco.MjvOption, + perturb: mujoco.MjvPerturb, + ux_state: ux.UxState, +) -> None: + """Handles mouse events.""" + io = imgui.GetIO() + if io.WantCaptureMouse: + return + + if io.DisplaySize.x <= 0 or io.DisplaySize.y <= 0: + return + + mouse_x = io.MousePos.x / io.DisplaySize.x + mouse_y = io.MousePos.y / io.DisplaySize.y + mouse_dx = io.MouseDelta.x / io.DisplaySize.x + mouse_dy = io.MouseDelta.y / io.DisplaySize.y + mouse_scroll = io.MouseWheel / 50.0 + + is_mouse_moving = mouse_dx != 0.0 or mouse_dy != 0.0 + is_any_mouse_down = ( + imgui.IsMouseDown(imgui.MouseButton.Left) + or imgui.IsMouseDown(imgui.MouseButton.Right) + or imgui.IsMouseDown(imgui.MouseButton.Middle) + ) + is_mouse_dragging = is_mouse_moving and is_any_mouse_down + + # If no mouse buttons are down, end any active perturbations. + if not is_any_mouse_down: + perturb.active = 0 + + # Handle perturbation mouse actions. + if is_mouse_dragging and io.KeyCtrl: + if perturb.select > 0: + action = int(mujoco.mjtMouse.mjMOUSE_NONE) + if imgui.IsMouseDown(imgui.MouseButton.Left): + if io.KeyAlt: + action = int( + mujoco.mjtMouse.mjMOUSE_MOVE_H + if io.KeyShift + else mujoco.mjtMouse.mjMOUSE_MOVE_V + ) + else: + action = int( + mujoco.mjtMouse.mjMOUSE_ROTATE_H + if io.KeyShift + else mujoco.mjtMouse.mjMOUSE_ROTATE_V + ) + elif imgui.IsMouseDown(imgui.MouseButton.Right): + action = int( + mujoco.mjtMouse.mjMOUSE_MOVE_H + if io.KeyShift + else mujoco.mjtMouse.mjMOUSE_MOVE_V + ) + elif imgui.IsMouseDown(imgui.MouseButton.Middle): + action = int(mujoco.mjtMouse.mjMOUSE_ZOOM) + + active = int( + mujoco.mjtPertBit.mjPERT_TRANSLATE + if action + in ( + int(mujoco.mjtMouse.mjMOUSE_MOVE_V), + int(mujoco.mjtMouse.mjMOUSE_MOVE_H), + ) + else mujoco.mjtPertBit.mjPERT_ROTATE + ) + if active != perturb.active: + ux.InitPerturb(model, data, camera, perturb, active) + ux.MovePerturb( + model, + data, + camera, + perturb, + action, + mouse_dx, + mouse_dy, + ) + elif is_mouse_dragging: + if ux_state.camera_index == ux.FREE_CAMERA_IDX: + if imgui.IsMouseDown(imgui.MouseButton.Left): + ux.MoveCamera( + model, + data, + camera, + ux.CameraMotion.PAN_TILT, + mouse_dx, + mouse_dy, + ) + else: + if imgui.IsMouseDown(imgui.MouseButton.Left): + ux.MoveCamera( + model, + data, + camera, + ux.CameraMotion.ORBIT, + mouse_dx, + mouse_dy, + ) + elif imgui.IsMouseDown(imgui.MouseButton.Middle): + ux.MoveCamera( + model, + data, + camera, + ux.CameraMotion.ZOOM, + mouse_dx, + mouse_dy, + ) + + # Right mouse movement is relative to the horizontal and vertical planes. + if imgui.IsMouseDown(imgui.MouseButton.Right) and io.KeyShift: + ux.MoveCamera( + model, + data, + camera, + ux.CameraMotion.PLANAR_MOVE_H, + mouse_dx, + mouse_dy, + ) + elif imgui.IsMouseDown(imgui.MouseButton.Right): + ux.MoveCamera( + model, + data, + camera, + ux.CameraMotion.PLANAR_MOVE_V, + mouse_dx, + mouse_dy, + ) + + # Mouse scroll. + if mouse_scroll != 0.0 and ux_state.camera_index != ux.FREE_CAMERA_IDX: + ux.MoveCamera( + model, + data, + camera, + ux.CameraMotion.ZOOM, + 0, + -mouse_scroll, + ) + + aspect_ratio = io.DisplaySize.x / io.DisplaySize.y + + # Left double click. + if imgui.IsMouseDoubleClicked(imgui.MouseButton.Left): + picked = ux.Pick( + model, + data, + camera, + mouse_x, + mouse_y, + aspect_ratio, + vis_options, + ) + if picked.body >= 0: + perturb.select = picked.body + perturb.flexselect = picked.flex + perturb.skinselect = picked.skin + + # Compute the local position of the selected object in the world. + tmp = np.array(picked.point, dtype=np.float64) - data.xpos[picked.body] + xmat = np.array(data.xmat[picked.body], dtype=np.float64).reshape(3, 3) + perturb.localpos = xmat.T @ tmp + else: + perturb.select = 0 + perturb.flexselect = -1 + perturb.skinselect = -1 + + handle_camera_tracking_mouse_events( + model, data, camera, vis_options, ux_state + ) diff --git a/python/mujoco/experimental/studio/ux.cc b/python/mujoco/experimental/studio/ux.cc new file mode 100644 index 00000000..8827a70f --- /dev/null +++ b/python/mujoco/experimental/studio/ux.cc @@ -0,0 +1,398 @@ +// Copyright 2026 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Python bindings for MuJoCo platform UX components. + +#include +#include +#include +#include +#include + +#include +#include +#include "third_party/mujoco/src/experimental/platform/helpers.h" +#include "third_party/mujoco/src/experimental/platform/sim/step_control.h" +#include "third_party/mujoco/src/experimental/platform/ux/gui.h" +#include "third_party/mujoco/src/experimental/platform/ux/interaction.h" +#include "structs.h" +#include +#include + +namespace py = pybind11; + +struct UxState { + // Read/edited by step_control_gui + int speed_index = 0; + + // Read/edited by state_gui + std::vector state; + int state_sig = 0; + + // Read/edited by watch_gui + char watch_field_name[256] = {0}; + int watch_field_index = 0; + + // Read/edited by noise_gui + float noise_scale = 0.0f; + float noise_rate = 0.0f; + + // Read/edited by camera_selection_gui + int camera_index = mujoco::platform::kTumbleCameraIdx; +}; + +struct RenderFlags { + std::array flags = {0}; +}; + +PYBIND11_MODULE(ux, m) { + py::class_(m, "RenderFlags") + .def(py::init<>()) + .def_readwrite("flags", &RenderFlags::flags); + + m.doc() = "MuJoCo platform UX components."; + + py::enum_(m, "GuiTheme") + .value("LIGHT", mujoco::platform::GuiTheme::kLight) + .value("DARK", mujoco::platform::GuiTheme::kDark) + .value("CLASSIC", mujoco::platform::GuiTheme::kClassic); + + py::class_(m, "UxState") + .def(py::init<>()) + .def_readwrite("speed_index", &UxState::speed_index) + .def_readwrite("state", &UxState::state) + .def_readwrite("state_sig", &UxState::state_sig) + .def_readwrite("watch_field_index", &UxState::watch_field_index) + .def_readwrite("noise_scale", &UxState::noise_scale) + .def_readwrite("noise_rate", &UxState::noise_rate) + .def_readwrite("camera_index", &UxState::camera_index) + .def_property( + "watch_field_name", + [](const UxState& self) { + return std::string(self.watch_field_name); + }, + [](UxState& self, const std::string& val) { + std::snprintf(self.watch_field_name, sizeof(self.watch_field_name), + "%s", val.c_str()); + }); + + m.def( + "setup_theme", + [](mujoco::platform::GuiTheme theme) { + mujoco::platform::SetupTheme(theme); + }, + py::arg("theme"), "Set up Dear ImGui visual theme."); + + m.def( + "configure_docking_layout", + []() { + ImVec4 r = mujoco::platform::ConfigureDockingLayout(); + return std::make_tuple(r.x, r.y, r.z, r.w); + }, + "Configure the docking layout with Options (left) and Inspector (right) " + "panes. Returns (x, y, w, h) of the central workspace area."); + + m.def( + "step_control_gui", + [](const mujoco::python::MjModelWrapper& model, + mujoco::platform::StepControl* step_control, UxState& ux_state) { + mujoco::platform::StepControlGui(model.get(), step_control, + ux_state.speed_index); + }, + py::arg("model"), py::arg("step_control"), py::arg("ux_state"), + "Render the simulation stepping control GUI. Modifies " + "ux_state.speed_index."); + + m.def( + "theme_select_gui", + [](mujoco::platform::GuiTheme theme) { + bool changed = mujoco::platform::ThemeSelectGui(&theme); + return std::make_tuple(changed, theme); + }, + py::arg("theme"), + "Render the GUI theme selector. Returns (changed, theme)."); + + m.def( + "label_selection_gui", + [](mujoco::python::MjvOptionWrapper& vis_options) { + return mujoco::platform::LabelSelectionGui(vis_options.get()); + }, + py::arg("vis_options"), "Render the visualization label selection GUI."); + + m.def( + "frame_selection_gui", + [](mujoco::python::MjvOptionWrapper& vis_options) { + return mujoco::platform::FrameSelectionGui(vis_options.get()); + }, + py::arg("vis_options"), "Render the visualization frame selection GUI."); + + m.def( + "camera_selection_gui", + [](const mujoco::python::MjModelWrapper& model, + mujoco::python::MjDataWrapper& data, + mujoco::python::MjvCameraWrapper& camera, UxState& ux_state) { + bool changed = mujoco::platform::CameraSelectionGui( + model.get(), data.get(), *camera.get(), ux_state.camera_index); + return changed; + }, + py::arg("model"), py::arg("data"), py::arg("camera"), py::arg("ux_state"), + "Render the camera selection GUI. Modifies ux_state.camera_index. " + "Returns true if camera changed."); + + m.def( + "set_camera", + [](const mujoco::python::MjModelWrapper& model, + mujoco::python::MjvCameraWrapper& camera, int request_idx) { + return mujoco::platform::SetCamera(model.get(), camera.get(), request_idx); + }, + py::arg("model"), py::arg("camera"), py::arg("request_idx"), + "Set the camera index and update the camera object."); + + m.def( + "set_speed_index", + [](mujoco::platform::StepControl* step_control, UxState& ux_state, int idx) { + mujoco::platform::SetSpeedIndex(step_control, ux_state.speed_index, idx); + }, + py::arg("step_control"), py::arg("ux_state"), py::arg("idx"), + "Set the simulation speed index."); + + m.def( + "physics_gui", + [](mujoco::python::MjModelWrapper& model, float min_width) { + mujoco::platform::PhysicsGui(model.get(), min_width); + }, + py::arg("model"), py::arg("min_width") = 150.0f, + "Render the physics settings UI."); + + m.def( + "rendering_gui", + [](const mujoco::python::MjModelWrapper& model, + mujoco::python::MjvOptionWrapper& vis_options, + RenderFlags& render_flags) { + mjtByte flags[mjNRNDFLAG] = {0}; + for (int i = 0; i < mjNRNDFLAG; ++i) { + flags[i] = render_flags.flags[i]; + } + mujoco::platform::RenderingGui(model.get(), vis_options.get(), flags, + 150.0f); + for (int i = 0; i < mjNRNDFLAG; ++i) { + render_flags.flags[i] = flags[i]; + } + }, + py::arg("model"), py::arg("vis_options"), py::arg("render_flags"), + "Render the rendering settings UI. Modifies render_flags in place."); + + m.def( + "groups_gui", + [](const mujoco::python::MjModelWrapper& model, + mujoco::python::MjvOptionWrapper& vis_options, float min_width) { + mujoco::platform::GroupsGui(model.get(), vis_options.get(), min_width); + }, + py::arg("model"), py::arg("vis_options"), py::arg("min_width") = 150.0f, + "Render the visibility groups UI."); + + m.def( + "visualization_gui", + [](mujoco::python::MjModelWrapper& model, + mujoco::python::MjvOptionWrapper& vis_options, + mujoco::python::MjvCameraWrapper& camera, float min_width) { + mujoco::platform::VisualizationGui(model.get(), vis_options.get(), + camera.get(), min_width); + }, + py::arg("model"), py::arg("vis_options"), py::arg("camera"), + py::arg("min_width") = 150.0f, "Render the visualization settings UI."); + + m.def( + "controls_gui", + [](const mujoco::python::MjModelWrapper& model, + mujoco::python::MjDataWrapper& data, + mujoco::python::MjvOptionWrapper& vis_options) { + mujoco::platform::ControlsGui(model.get(), data.get(), + vis_options.get()); + }, + py::arg("model"), py::arg("data"), py::arg("vis_options"), + "Render the actuator controls UI."); + + m.def( + "joints_gui", + [](const mujoco::python::MjModelWrapper& model, + mujoco::python::MjDataWrapper& data, + mujoco::python::MjvOptionWrapper& vis_options) { + mujoco::platform::JointsGui(model.get(), data.get(), vis_options.get()); + }, + py::arg("model"), py::arg("data"), py::arg("vis_options"), + "Render the joints UI."); + + m.def( + "sensor_gui", + [](const mujoco::python::MjModelWrapper& model, + mujoco::python::MjDataWrapper& data) { + mujoco::platform::SensorGui(model.get(), data.get()); + }, + py::arg("model"), py::arg("data"), "Render the sensor data plot."); + + m.def( + "state_gui", + [](const mujoco::python::MjModelWrapper& model, + mujoco::python::MjDataWrapper& data, UxState& ux_state, + float min_width) { + mujoco::platform::StateGui(model.get(), data.get(), ux_state.state, + ux_state.state_sig, min_width); + }, + py::arg("model"), py::arg("data"), py::arg("ux_state"), + py::arg("min_width") = 150.0f, + "Render the state UI. Modifies ux_state.state and ux_state.state_sig."); + + m.def( + "watch_gui", + [](const mujoco::python::MjModelWrapper& model, + mujoco::python::MjDataWrapper& data, UxState& ux_state) { + mujoco::platform::WatchGui( + model.get(), data.get(), ux_state.watch_field_name, + sizeof(ux_state.watch_field_name), ux_state.watch_field_index); + }, + py::arg("model"), py::arg("data"), py::arg("ux_state"), + "Render the watch UI. Modifies ux_state.watch_field_name and " + "ux_state.watch_field_index."); + + m.def( + "noise_gui", + [](const mujoco::python::MjModelWrapper& model, + mujoco::python::MjDataWrapper& data, UxState& ux_state) { + mujoco::platform::NoiseGui(model.get(), data.get(), + ux_state.noise_scale, ux_state.noise_rate); + }, + py::arg("model"), py::arg("data"), py::arg("ux_state"), + "Render the noise UI. Modifies ux_state.noise_scale and " + "ux_state.noise_rate."); + + m.def( + "convergence_gui", + [](const mujoco::python::MjModelWrapper& model, + mujoco::python::MjDataWrapper& data) { + mujoco::platform::ConvergenceGui(model.get(), data.get()); + }, + py::arg("model"), py::arg("data"), + "Render the solver convergence chart."); + + m.def( + "counts_gui", + [](const mujoco::python::MjModelWrapper& model, + mujoco::python::MjDataWrapper& data) { + mujoco::platform::CountsGui(model.get(), data.get()); + }, + py::arg("model"), py::arg("data"), "Render the solver counts chart."); + + m.def( + "stats_gui", + [](const mujoco::python::MjModelWrapper& model, + mujoco::python::MjDataWrapper& data, bool paused, float fps) { + mujoco::platform::StatsGui(model.get(), data.get(), paused, fps); + }, + py::arg("model"), py::arg("data"), py::arg("paused"), py::arg("fps"), + "Render the simulation statistics UI."); + + m.attr("FREE_CAMERA_IDX") = mujoco::platform::kFreeCameraIdx; + m.attr("TUMBLE_CAMERA_IDX") = mujoco::platform::kTumbleCameraIdx; + m.attr("TRACKING_CAMERA_IDX") = mujoco::platform::kTrackingCameraIdx; + + py::enum_(m, "CameraMotion") + .value("ZOOM", mujoco::platform::CameraMotion::ZOOM) + .value("ORBIT", mujoco::platform::CameraMotion::ORBIT) + .value("TRUCK_PEDESTAL", mujoco::platform::CameraMotion::TRUCK_PEDESTAL) + .value("TRUCK_DOLLY", mujoco::platform::CameraMotion::TRUCK_DOLLY) + .value("PAN_TILT", mujoco::platform::CameraMotion::PAN_TILT) + .value("PLANAR_MOVE_H", mujoco::platform::CameraMotion::PLANAR_MOVE_H) + .value("PLANAR_MOVE_V", mujoco::platform::CameraMotion::PLANAR_MOVE_V) + .export_values(); + + m.def( + "MoveCamera", + [](const mujoco::python::MjModelWrapper& model, + const mujoco::python::MjDataWrapper& data, + mujoco::python::MjvCameraWrapper& cam, + mujoco::platform::CameraMotion motion, mjtNum dx, mjtNum dy) { + mujoco::platform::MoveCamera(model.get(), data.get(), cam.get(), motion, + dx, dy); + }, + py::arg("model"), py::arg("data"), py::arg("cam"), py::arg("motion"), + py::arg("dx"), py::arg("dy"), "Moves the given camera."); + + m.def( + "InitPerturb", + [](const mujoco::python::MjModelWrapper& model, + const mujoco::python::MjDataWrapper& data, + const mujoco::python::MjvCameraWrapper& cam, + mujoco::python::MjvPerturbWrapper& pert, int active) { + mujoco::platform::InitPerturb(model.get(), data.get(), cam.get(), + pert.get(), + static_cast(active)); + }, + py::arg("model"), py::arg("data"), py::arg("cam"), py::arg("pert"), + py::arg("active"), "Initializes mouse perturbation."); + + m.def( + "MovePerturb", + [](const mujoco::python::MjModelWrapper& model, + const mujoco::python::MjDataWrapper& data, + const mujoco::python::MjvCameraWrapper& cam, + mujoco::python::MjvPerturbWrapper& pert, int action, mjtNum reldx, + mjtNum reldy) { + mujoco::platform::MovePerturb(model.get(), data.get(), cam.get(), + pert.get(), static_cast(action), + reldx, reldy); + }, + py::arg("model"), py::arg("data"), py::arg("cam"), py::arg("pert"), + py::arg("action"), py::arg("reldx"), py::arg("reldy"), + "Moves mouse perturbation."); + + py::class_(m, "PickResult") + .def_readwrite("dist", &mujoco::platform::PickResult::dist) + .def_readwrite("body", &mujoco::platform::PickResult::body) + .def_readwrite("geom", &mujoco::platform::PickResult::geom) + .def_readwrite("flex", &mujoco::platform::PickResult::flex) + .def_readwrite("skin", &mujoco::platform::PickResult::skin) + .def_property( + "point", + [](const mujoco::platform::PickResult& res) { + return py::make_tuple(res.point[0], res.point[1], res.point[2]); + }, + [](mujoco::platform::PickResult& res, const py::tuple& t) { + res.point[0] = t[0].cast(); + res.point[1] = t[1].cast(); + res.point[2] = t[2].cast(); + }); + + m.def( + "Pick", + [](const mujoco::python::MjModelWrapper& model, + const mujoco::python::MjDataWrapper& data, + const mujoco::python::MjvCameraWrapper& cam, float x, float y, + float aspect_ratio, const mujoco::python::MjvOptionWrapper& opt) { + return mujoco::platform::Pick(model.get(), data.get(), cam.get(), x, y, + aspect_ratio, opt.get()); + }, + py::arg("model"), py::arg("data"), py::arg("cam"), py::arg("x"), + py::arg("y"), py::arg("aspect_ratio"), py::arg("opt"), + "Picks object under cursor."); + + m.def( + "camera_to_string", + [](const mujoco::python::MjDataWrapper& data, + const mujoco::python::MjvCameraWrapper& camera) { + return mujoco::platform::CameraToString(data.get(), camera.get()); + }, + py::arg("data"), py::arg("camera"), + "Returns an XML string representation of the camera."); +} diff --git a/python/mujoco/experimental/studio/viewer_protocol.py b/python/mujoco/experimental/studio/viewer_protocol.py new file mode 100644 index 00000000..7138eba8 --- /dev/null +++ b/python/mujoco/experimental/studio/viewer_protocol.py @@ -0,0 +1,43 @@ +# Copyright 2026 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Structural protocol defining the common viewer interface. + +StudioApp uses the protocol for convenience methods that accept any viewer. +""" + +from typing import Protocol + +import mujoco +from mujoco.experimental.studio import ux + + +class Viewer(Protocol): + """Structural interface for any viewer.""" + + camera: mujoco.MjvCamera + perturb: mujoco.MjvPerturb + vis_options: mujoco.MjvOption + render_flags: ux.RenderFlags + + def is_running(self) -> bool: + ... + + def sync(self, model: mujoco.MjModel, data: mujoco.MjData) -> None: + ... + + def stop(self) -> None: + ... + + def get_drop_file(self) -> str: + ...