VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
annotationManager.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3//
4//
5// DEVIATION: Rendering replaced with ImGui-based overlays. This file now
6// only handles annotation registration, screen-space projection, hover
7// detection, and click handling. All visual rendering is external.
8//
9#include "annotationManager.h"
10#include "annotation.h"
11
12#include <algorithm>
13#include <cmath>
14
15#include <SDL3/SDL.h>
16
17#include "spdlog/spdlog.h"
18
19#include "framework/engine.h"
20#include "framework/entity.h"
22#include "core/math/vector4.h"
23#include "scene/camera.h"
24
25namespace visutwin::canvas
26{
28 {
29 auto* eng = entity()->engine();
30 if (!eng) return;
31
32 // Camera is found lazily in update() since it may not exist at initialize time
33 findCameraEntity();
34
35 // Listen for annotation add/remove events on the engine
36 eng->on("annotation:add", [this](Annotation* annotation) {
37 registerAnnotation(annotation);
38 });
39 eng->on("annotation:remove", [this](Annotation* annotation) {
40 unregisterAnnotation(annotation);
41 });
42
43 spdlog::info("AnnotationManager initialized");
44 }
45
46 void AnnotationManager::registerAnnotation(Annotation* annotation)
47 {
48 // Avoid duplicates
49 auto it = std::find(_annotations.begin(), _annotations.end(), annotation);
50 if (it != _annotations.end()) return;
51
52 _annotations.push_back(annotation);
53 spdlog::info("Registered annotation: label='{}' title='{}'", annotation->label(), annotation->title());
54 }
55
56 void AnnotationManager::unregisterAnnotation(Annotation* annotation)
57 {
58 auto it = std::find(_annotations.begin(), _annotations.end(), annotation);
59 if (it == _annotations.end()) return;
60
61 if (_activeAnnotation == annotation) {
62 _activeAnnotation = nullptr;
63 }
64 if (_hoveredAnnotation == annotation) {
65 _hoveredAnnotation = nullptr;
66 }
67
68 _annotations.erase(it);
69 }
70
71 bool AnnotationManager::worldToScreen(const Vector3& worldPos, float& screenX, float& screenY) const
72 {
73 if (!_camera) return false;
74
75 auto* cameraComp = _camera->findComponent<CameraComponent>();
76 if (!cameraComp || !cameraComp->camera()) return false;
77
78 // Get view matrix from camera's world transform inverse
79 const Matrix4 viewMatrix = _camera->worldTransform().inverse();
80 const Matrix4& projMatrix = cameraComp->camera()->projectionMatrix();
81
82 // Transform to view space
83 Vector3 viewPos = viewMatrix.transformPoint(worldPos);
84
85 // Check if behind camera (view space Z is positive = behind camera in right-hand coords)
86 // In our column-major layout, after view transform, negative Z is in front
87 if (viewPos.getZ() >= 0.0f) {
88 return false;
89 }
90
91 // Transform to clip space
92 Vector4 clipPos = projMatrix * Vector4(viewPos.getX(), viewPos.getY(), viewPos.getZ(), 1.0f);
93
94 if (std::abs(clipPos.getW()) < 1e-6f) return false;
95
96 // NDC coordinates
97 float ndcX = clipPos.getX() / clipPos.getW();
98 float ndcY = clipPos.getY() / clipPos.getW();
99
100 // Get screen dimensions
101 int windowW = 0, windowH = 0;
102 auto* eng = entity()->engine();
103 if (eng && eng->sdlWindow()) {
104 SDL_GetWindowSize(eng->sdlWindow(), &windowW, &windowH);
105 }
106 if (windowW <= 0 || windowH <= 0) return false;
107
108 // Convert NDC to screen coordinates
109 // NDC X: -1 (left) to +1 (right) -> screen 0 to windowW
110 // NDC Y: -1 (bottom) to +1 (top) -> screen windowH to 0 (screen Y is top-down)
111 screenX = (ndcX * 0.5f + 0.5f) * static_cast<float>(windowW);
112 screenY = (1.0f - (ndcY * 0.5f + 0.5f)) * static_cast<float>(windowH);
113
114 return true;
115 }
116
117 void AnnotationManager::findCameraEntity()
118 {
119 if (_camera) return;
120
121 auto* eng = entity()->engine();
122 if (!eng) return;
123
124 // DEVIATION: upstream finds camera via findComponent; we search the scene graph
125 auto root = eng->root();
126 if (!root) return;
127
128 std::function<Entity*(Entity*)> search = [&](Entity* e) -> Entity* {
129 if (e->findComponent<CameraComponent>()) return e;
130 for (auto* child : e->children()) {
131 auto* ent = dynamic_cast<Entity*>(child);
132 if (!ent) continue;
133 auto* found = search(ent);
134 if (found) return found;
135 }
136 return nullptr;
137 };
138 _camera = search(root.get());
139 }
140
142 {
143 // Lazily find camera if not yet available
144 if (!_camera) {
145 findCameraEntity();
146 }
147 if (!_camera) return;
148
149 // Rebuild screen-space info for all annotations
150 _screenInfos.clear();
151 _screenInfos.reserve(_annotations.size());
152
153 for (auto* annotation : _annotations) {
154 if (!annotation->enabled()) continue;
155
157 info.annotation = annotation;
158
159 const Vector3 worldPos = annotation->entity()->position();
160 info.visible = worldToScreen(worldPos, info.screenX, info.screenY);
161
162 _screenInfos.push_back(info);
163 }
164 }
165
166 void AnnotationManager::updateHover(float mouseX, float mouseY)
167 {
168 float closestDist = _hotspotSize + 5.0f;
169 Annotation* closest = nullptr;
170
171 for (const auto& info : _screenInfos) {
172 if (!info.visible) continue;
173
174 float dx = mouseX - info.screenX;
175 float dy = mouseY - info.screenY;
176 float dist = std::sqrt(dx * dx + dy * dy);
177
178 if (dist < closestDist) {
179 closestDist = dist;
180 closest = info.annotation;
181 }
182 }
183
184 _hoveredAnnotation = closest;
185 }
186
187 void AnnotationManager::handleClick(float screenX, float screenY)
188 {
189 float closestDist = _hotspotSize + 5.0f;
190 Annotation* closestAnnotation = nullptr;
191
192 for (const auto& info : _screenInfos) {
193 if (!info.visible) continue;
194
195 float dx = screenX - info.screenX;
196 float dy = screenY - info.screenY;
197 float dist = std::sqrt(dx * dx + dy * dy);
198
199 if (dist < closestDist) {
200 closestDist = dist;
201 closestAnnotation = info.annotation;
202 }
203 }
204
205 if (closestAnnotation) {
206 if (_activeAnnotation == closestAnnotation) {
207 // Toggle off
208 _activeAnnotation = nullptr;
209 spdlog::info("Annotation hidden: '{}'", closestAnnotation->title());
210 } else {
211 _activeAnnotation = closestAnnotation;
212 spdlog::info("Annotation selected: '{}' -- {}", closestAnnotation->title(), closestAnnotation->text());
213 }
214 } else {
215 _activeAnnotation = nullptr;
216 }
217 }
218}
const std::string & text() const
Definition annotation.h:55
const std::string & label() const
Definition annotation.h:41
const std::string & title() const
Definition annotation.h:48
void handleClick(float screenX, float screenY)
Handle mouse click at screen coordinates — selects/deselects the nearest annotation.
void updateHover(float mouseX, float mouseY)
Update hover state based on mouse position (call each frame with current mouse pos).
std::shared_ptr< Entity > root()
Definition engine.h:92
Engine * engine() const
Definition entity.h:123
Entity * entity() const
Definition script.h:71
3D vector for positions, directions, and normals with multi-backend SIMD acceleration.
Definition vector3.h:29