VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
imguiOverlay.cpp
Go to the documentation of this file.
1//
2// ImGui overlay for VisuTwin Canvas — implementation.
3//
4// Uses ImGui's Metal C++ API (IMGUI_IMPL_METAL_CPP) which provides
5// type-safe MTL::Device*, MTL::CommandBuffer* etc. signatures that
6// work directly with metal-cpp, no Objective-C++ needed.
7//
8//
9
10#include "imguiOverlay.h"
11
12#include <cmath>
13
14// metal-cpp headers must come BEFORE imgui_impl_metal.h so that
15// IMGUI_IMPL_METAL_CPP picks up the MTL:: types.
16#include <Metal/Metal.hpp>
17#include <QuartzCore/CAMetalDrawable.hpp>
18
19#define IMGUI_IMPL_METAL_CPP
20#include <imgui.h>
21#include <implot.h>
22#include <imgui_impl_sdl3.h>
23#include <imgui_impl_metal.h>
24
25#include <SDL3/SDL.h>
26
27#include "spdlog/spdlog.h"
28
30
31namespace visutwin::canvas
32{
33 // ── Lifecycle ────────────────────────────────────────────────────────
34
36 {
37 if (_initialized) {
38 shutdown();
39 }
40 }
41
43 : _device(other._device)
44 , _window(other._window)
45 , _initialized(other._initialized)
46 , _viewProjection(other._viewProjection)
47 , _windowW(other._windowW)
48 , _windowH(other._windowH)
49 {
50 other._initialized = false;
51 other._device = nullptr;
52 other._window = nullptr;
53 }
54
56 {
57 if (this != &other) {
58 if (_initialized) shutdown();
59 _device = other._device;
60 _window = other._window;
61 _initialized = other._initialized;
62 _viewProjection = other._viewProjection;
63 _windowW = other._windowW;
64 _windowH = other._windowH;
65 other._initialized = false;
66 other._device = nullptr;
67 other._window = nullptr;
68 }
69 return *this;
70 }
71
72 void ImGuiOverlay::init(MetalGraphicsDevice* device, SDL_Window* window)
73 {
74 if (_initialized) {
75 spdlog::warn("ImGuiOverlay::init called on already-initialized overlay");
76 return;
77 }
78
79 _device = device;
80 _window = window;
81
82 // Create ImGui context
83 IMGUI_CHECKVERSION();
84 ImGui::CreateContext();
85 ImPlot::CreateContext();
86
87 ImGuiIO& io = ImGui::GetIO();
88 io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
89 io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
90
91 // Initialize backends — void* API (C++ mode, no __OBJC__)
92 ImGui_ImplSDL3_InitForMetal(_window);
93 ImGui_ImplMetal_Init(_device->raw()); // MTL::Device* → void*
94
95 // Apply digital twin theme
97
98 _initialized = true;
99 spdlog::info("ImGuiOverlay initialized (ImGui {}, ImPlot)", IMGUI_VERSION);
100 }
101
102 bool ImGuiOverlay::processEvent(const SDL_Event& event)
103 {
104 if (!_initialized) return false;
105 return ImGui_ImplSDL3_ProcessEvent(&event);
106 }
107
109 {
110 if (!_initialized) return false;
111 return ImGui::GetIO().WantCaptureMouse;
112 }
113
115 {
116 if (!_initialized) return false;
117 return ImGui::GetIO().WantCaptureKeyboard;
118 }
119
121 {
122 if (!_initialized || !_device) return;
123
124 auto* drawable = _device->frameDrawable();
125 if (!drawable) return;
126
127 // Create a render pass descriptor targeting the drawable's texture.
128 // LoadAction::Load preserves the 3D scene rendered underneath.
129 auto* desc = MTL::RenderPassDescriptor::alloc()->init();
130 auto* ca = desc->colorAttachments()->object(0);
131 ca->setTexture(drawable->texture());
132 ca->setLoadAction(MTL::LoadActionLoad);
133 ca->setStoreAction(MTL::StoreActionStore);
134
135 ImGui_ImplMetal_NewFrame(desc); // void*
136 ImGui_ImplSDL3_NewFrame();
137 ImGui::NewFrame();
138
139 desc->release();
140 }
141
143 {
144 if (!_initialized) return;
145 ImGui::Render();
146 }
147
149 {
150 if (!_initialized || !_device) return;
151
152 auto* drawData = ImGui::GetDrawData();
153 if (!drawData) return;
154
155 auto* drawable = _device->frameDrawable();
156 if (!drawable) return;
157
158 // Create a fresh render pass descriptor for encoding.
159 auto* desc = MTL::RenderPassDescriptor::alloc()->init();
160 auto* ca = desc->colorAttachments()->object(0);
161 ca->setTexture(drawable->texture());
162 ca->setLoadAction(MTL::LoadActionLoad);
163 ca->setStoreAction(MTL::StoreActionStore);
164
165 auto* cmdBuf = _device->commandQueue()->commandBuffer();
166 if (!cmdBuf) {
167 desc->release();
168 return;
169 }
170
171 auto* encoder = cmdBuf->renderCommandEncoder(desc);
172 if (!encoder) {
173 desc->release();
174 return;
175 }
176
177 // Encode ImGui draw data — void* API bridges metal-cpp ↔ Obj-C
178 ImGui_ImplMetal_RenderDrawData(drawData, cmdBuf, encoder);
179
180 encoder->endEncoding();
181 cmdBuf->commit();
182
183 desc->release();
184 }
185
187 {
188 if (!_initialized) return;
189
190 ImGui_ImplMetal_Shutdown();
191 ImGui_ImplSDL3_Shutdown();
192 ImPlot::DestroyContext();
193 ImGui::DestroyContext();
194
195 _initialized = false;
196 _device = nullptr;
197 _window = nullptr;
198
199 spdlog::info("ImGuiOverlay shut down");
200 }
201
202 // ── 3D-anchored labels ──────────────────────────────────────────────
203
204 bool ImGuiOverlay::worldToScreen(const Vector3& worldPos, float& screenX, float& screenY) const
205 {
206 // Transform to clip space
207 Vector4 clip = _viewProjection * Vector4(worldPos.getX(), worldPos.getY(), worldPos.getZ(), 1.0f);
208
209 // Behind camera check
210 if (clip.getW() <= 0.0f) return false;
211
212 // NDC
213 const float ndcX = clip.getX() / clip.getW();
214 const float ndcY = clip.getY() / clip.getW();
215
216 // NDC to screen: X [-1,+1] → [0, windowW], Y [-1,+1] → [windowH, 0]
217 screenX = (ndcX * 0.5f + 0.5f) * static_cast<float>(_windowW);
218 screenY = (1.0f - (ndcY * 0.5f + 0.5f)) * static_cast<float>(_windowH);
219
220 return true;
221 }
222
223 void ImGuiOverlay::label3D(const Vector3& worldPos, const char* text, const Color& color)
224 {
225 float sx, sy;
226 if (!worldToScreen(worldPos, sx, sy)) return;
227
228 // Render as a foreground overlay text (no window chrome)
229 auto* drawList = ImGui::GetForegroundDrawList();
230 const ImU32 imColor = IM_COL32(
231 static_cast<int>(color.r * 255.0f),
232 static_cast<int>(color.g * 255.0f),
233 static_cast<int>(color.b * 255.0f),
234 static_cast<int>(color.a * 255.0f)
235 );
236 drawList->AddText(ImVec2(sx, sy), imColor, text);
237 }
238
239 void ImGuiOverlay::panelLabel3D(const Vector3& worldPos, const char* title, const char* body,
240 const Color& panelColor)
241 {
242 float sx, sy;
243 if (!worldToScreen(worldPos, sx, sy)) return;
244
245 // Create a small ImGui window at the projected screen position
246 ImGui::SetNextWindowPos(ImVec2(sx + 12.0f, sy - 8.0f), ImGuiCond_Always);
247 ImGui::SetNextWindowBgAlpha(panelColor.a);
248
249 const std::string windowId = std::string("##label3d_") + title;
250 ImGui::Begin(windowId.c_str(), nullptr,
251 ImGuiWindowFlags_NoTitleBar |
252 ImGuiWindowFlags_NoResize |
253 ImGuiWindowFlags_AlwaysAutoResize |
254 ImGuiWindowFlags_NoMove |
255 ImGuiWindowFlags_NoScrollbar |
256 ImGuiWindowFlags_NoSavedSettings |
257 ImGuiWindowFlags_NoFocusOnAppearing |
258 ImGuiWindowFlags_NoBringToFrontOnFocus |
259 ImGuiWindowFlags_NoNav);
260
261 ImGui::TextColored(ImVec4(0.22f, 0.74f, 0.97f, 1.0f), "%s", title);
262 if (body && body[0] != '\0') {
263 ImGui::TextColored(ImVec4(0.58f, 0.64f, 0.72f, 0.80f), "%s", body);
264 }
265
266 ImGui::End();
267 }
268
269 // ── Theme ────────────────────────────────────────────────────────────
270
272 {
273 ImGuiStyle& style = ImGui::GetStyle();
274
275 // Panel geometry
276 style.WindowRounding = 8.0f;
277 style.FrameRounding = 4.0f;
278 style.PopupRounding = 6.0f;
279 style.ScrollbarRounding = 4.0f;
280 style.GrabRounding = 3.0f;
281 style.TabRounding = 4.0f;
282 style.ChildRounding = 6.0f;
283
284 style.WindowPadding = ImVec2(12.0f, 10.0f);
285 style.FramePadding = ImVec2(8.0f, 4.0f);
286 style.ItemSpacing = ImVec2(8.0f, 6.0f);
287 style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
288 style.ScrollbarSize = 10.0f;
289 style.GrabMinSize = 8.0f;
290
291 style.WindowBorderSize = 1.0f;
292 style.FrameBorderSize = 0.0f;
293 style.PopupBorderSize = 1.0f;
294
295 // ── Digital twin dark glassmorphism palette ───────────────────
296 auto* colors = style.Colors;
297
298 // Window
299 colors[ImGuiCol_WindowBg] = ImVec4(0.059f, 0.090f, 0.165f, 0.88f);
300 colors[ImGuiCol_ChildBg] = ImVec4(0.047f, 0.071f, 0.133f, 0.60f);
301 colors[ImGuiCol_PopupBg] = ImVec4(0.059f, 0.090f, 0.165f, 0.94f);
302
303 // Borders
304 colors[ImGuiCol_Border] = ImVec4(0.278f, 0.333f, 0.412f, 0.40f);
305 colors[ImGuiCol_BorderShadow] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f);
306
307 // Text
308 colors[ImGuiCol_Text] = ImVec4(0.945f, 0.961f, 0.976f, 0.95f);
309 colors[ImGuiCol_TextDisabled] = ImVec4(0.580f, 0.639f, 0.722f, 0.50f);
310
311 // Headers
312 colors[ImGuiCol_Header] = ImVec4(0.118f, 0.161f, 0.231f, 0.80f);
313 colors[ImGuiCol_HeaderHovered] = ImVec4(0.220f, 0.741f, 0.973f, 0.30f);
314 colors[ImGuiCol_HeaderActive] = ImVec4(0.220f, 0.741f, 0.973f, 0.45f);
315
316 // Buttons
317 colors[ImGuiCol_Button] = ImVec4(0.118f, 0.161f, 0.231f, 0.80f);
318 colors[ImGuiCol_ButtonHovered] = ImVec4(0.220f, 0.741f, 0.973f, 0.40f);
319 colors[ImGuiCol_ButtonActive] = ImVec4(0.220f, 0.741f, 0.973f, 0.65f);
320
321 // Frame
322 colors[ImGuiCol_FrameBg] = ImVec4(0.078f, 0.110f, 0.180f, 0.80f);
323 colors[ImGuiCol_FrameBgHovered] = ImVec4(0.220f, 0.741f, 0.973f, 0.20f);
324 colors[ImGuiCol_FrameBgActive] = ImVec4(0.220f, 0.741f, 0.973f, 0.35f);
325
326 // Title bar
327 colors[ImGuiCol_TitleBg] = ImVec4(0.047f, 0.071f, 0.133f, 0.95f);
328 colors[ImGuiCol_TitleBgActive] = ImVec4(0.059f, 0.090f, 0.165f, 0.95f);
329 colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.047f, 0.071f, 0.133f, 0.70f);
330
331 // Tabs
332 colors[ImGuiCol_Tab] = ImVec4(0.078f, 0.110f, 0.180f, 0.80f);
333 colors[ImGuiCol_TabHovered] = ImVec4(0.220f, 0.741f, 0.973f, 0.40f);
334 colors[ImGuiCol_TabSelected] = ImVec4(0.220f, 0.741f, 0.973f, 0.25f);
335 colors[ImGuiCol_TabDimmed] = ImVec4(0.059f, 0.090f, 0.165f, 0.80f);
336 colors[ImGuiCol_TabDimmedSelected] = ImVec4(0.118f, 0.161f, 0.231f, 0.90f);
337
338 // Scrollbar
339 colors[ImGuiCol_ScrollbarBg] = ImVec4(0.047f, 0.071f, 0.133f, 0.40f);
340 colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.278f, 0.333f, 0.412f, 0.50f);
341 colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.220f, 0.741f, 0.973f, 0.40f);
342 colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.220f, 0.741f, 0.973f, 0.60f);
343
344 // Slider / grab
345 colors[ImGuiCol_SliderGrab] = ImVec4(0.220f, 0.741f, 0.973f, 0.70f);
346 colors[ImGuiCol_SliderGrabActive] = ImVec4(0.404f, 0.910f, 0.976f, 0.90f);
347 colors[ImGuiCol_CheckMark] = ImVec4(0.220f, 0.741f, 0.973f, 0.90f);
348
349 // Separator
350 colors[ImGuiCol_Separator] = ImVec4(0.278f, 0.333f, 0.412f, 0.40f);
351 colors[ImGuiCol_SeparatorHovered] = ImVec4(0.220f, 0.741f, 0.973f, 0.40f);
352 colors[ImGuiCol_SeparatorActive] = ImVec4(0.220f, 0.741f, 0.973f, 0.65f);
353
354 // Resize grip
355 colors[ImGuiCol_ResizeGrip] = ImVec4(0.220f, 0.741f, 0.973f, 0.15f);
356 colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.220f, 0.741f, 0.973f, 0.40f);
357 colors[ImGuiCol_ResizeGripActive] = ImVec4(0.220f, 0.741f, 0.973f, 0.65f);
358
359 // Plot
360 colors[ImGuiCol_PlotLines] = ImVec4(0.220f, 0.741f, 0.973f, 0.80f);
361 colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.404f, 0.910f, 0.976f, 1.00f);
362 colors[ImGuiCol_PlotHistogram] = ImVec4(0.220f, 0.741f, 0.973f, 0.70f);
363 colors[ImGuiCol_PlotHistogramHovered] = ImVec4(0.404f, 0.910f, 0.976f, 1.00f);
364
365 // Table
366 colors[ImGuiCol_TableHeaderBg] = ImVec4(0.078f, 0.110f, 0.180f, 0.90f);
367 colors[ImGuiCol_TableBorderStrong] = ImVec4(0.278f, 0.333f, 0.412f, 0.50f);
368 colors[ImGuiCol_TableBorderLight] = ImVec4(0.278f, 0.333f, 0.412f, 0.25f);
369 colors[ImGuiCol_TableRowBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f);
370 colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.0f, 1.0f, 1.0f, 0.02f);
371
372 // Misc
373 colors[ImGuiCol_TextSelectedBg] = ImVec4(0.220f, 0.741f, 0.973f, 0.25f);
374 colors[ImGuiCol_DragDropTarget] = ImVec4(0.220f, 0.741f, 0.973f, 0.70f);
375 colors[ImGuiCol_NavHighlight] = ImVec4(0.220f, 0.741f, 0.973f, 0.70f);
376 colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.50f);
377
378 // ── ImPlot theme ─────────────────────────────────────────────
379 ImPlot::StyleColorsAuto();
380 ImPlotStyle& plotStyle = ImPlot::GetStyle();
381 plotStyle.Colors[ImPlotCol_PlotBorder] = ImVec4(0.278f, 0.333f, 0.412f, 0.40f);
382 plotStyle.Colors[ImPlotCol_PlotBg] = ImVec4(0.047f, 0.071f, 0.133f, 0.60f);
383 plotStyle.Colors[ImPlotCol_LegendBg] = ImVec4(0.059f, 0.090f, 0.165f, 0.85f);
384 plotStyle.Colors[ImPlotCol_LegendBorder]= ImVec4(0.278f, 0.333f, 0.412f, 0.30f);
385 plotStyle.Colors[ImPlotCol_LegendText] = ImVec4(0.945f, 0.961f, 0.976f, 0.90f);
386
387 // Custom colormap: digital twin palette (cyan → teal → emerald → amber → red)
388 static const ImVec4 dtColors[] = {
389 ImVec4(0.220f, 0.741f, 0.973f, 1.0f), // cyan-400
390 ImVec4(0.173f, 0.824f, 0.773f, 1.0f), // teal-400
391 ImVec4(0.204f, 0.827f, 0.600f, 1.0f), // emerald-400
392 ImVec4(0.984f, 0.749f, 0.141f, 1.0f), // amber-400
393 ImVec4(0.973f, 0.443f, 0.443f, 1.0f), // red-400
394 ImVec4(0.659f, 0.533f, 0.973f, 1.0f), // violet-400
395 };
396 ImPlot::AddColormap("DigitalTwin", dtColors, 6);
397
398 spdlog::info("Digital twin theme applied");
399 }
400
401} // namespace visutwin::canvas
ImGuiOverlay & operator=(const ImGuiOverlay &)=delete
bool wantCaptureKeyboard() const
True if ImGui wants exclusive keyboard input (an ImGui text field is active).
bool processEvent(const SDL_Event &event)
void label3D(const Vector3 &worldPos, const char *text, const Color &color=Color(0.94f, 0.96f, 0.98f, 0.95f))
void shutdown()
Shut down ImGui and release all resources.
void panelLabel3D(const Vector3 &worldPos, const char *title, const char *body, const Color &panelColor=Color(0.06f, 0.09f, 0.16f, 0.88f))
Render a text label with a background panel anchored to a 3D world position.
bool wantCaptureMouse() const
True if ImGui wants exclusive mouse input (cursor is over an ImGui window).
static void applyDigitalTwinTheme()
Apply the digital twin dark glassmorphism theme.
void beginFrame()
Begin a new ImGui frame. Call once per frame before building any UI.
void init(MetalGraphicsDevice *device, SDL_Window *window)
RGBA color with floating-point components in [0, 1].
Definition color.h:18
3D vector for positions, directions, and normals with multi-backend SIMD acceleration.
Definition vector3.h:29
4D vector for homogeneous coordinates, color values, and SIMD operations.
Definition vector4.h:20
float getX() const
Definition vector4.h:85
float getY() const
Definition vector4.h:98