VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
transformGizmo.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3#include "transformGizmo.h"
4
5#include <algorithm>
6#include <cmath>
7#include <limits>
8
9#include <SDL3/SDL_mouse.h>
10
13#include "framework/engine.h"
15
16namespace visutwin::canvas
17{
18 namespace
19 {
20 constexpr float PICK_RADIUS_PX = 16.0f;
21
22 float clamp01(const float v)
23 {
24 return std::clamp(v, 0.0f, 1.0f);
25 }
26
27 float snapValue(const float value, const float increment)
28 {
29 if (increment <= 0.0f) {
30 return value;
31 }
32 return std::round(value / increment) * increment;
33 }
34
35 float pointSegmentDistanceSquared(
36 const float px, const float py,
37 const float ax, const float ay,
38 const float bx, const float by)
39 {
40 const float abx = bx - ax;
41 const float aby = by - ay;
42 const float apx = px - ax;
43 const float apy = py - ay;
44 const float abLenSq = abx * abx + aby * aby;
45 if (abLenSq <= 1e-6f) {
46 const float dx = px - ax;
47 const float dy = py - ay;
48 return dx * dx + dy * dy;
49 }
50
51 const float t = clamp01((apx * abx + apy * aby) / abLenSq);
52 const float cx = ax + abx * t;
53 const float cy = ay + aby * t;
54 const float dx = px - cx;
55 const float dy = py - cy;
56 return dx * dx + dy * dy;
57 }
58 }
59
61 : _engine(engine), _camera(camera)
62 {
63 // DEVIATION: this native port uses a lightweight gizmo entity hierarchy instead of
64 // Upstream Gizmo/Shape classes from extras/gizmo.
65 _root = new Entity();
66 _root->setEngine(engine);
67 if (_engine && _engine->root()) {
68 _engine->root()->addChild(_root);
69 }
70
71 _handleX.axis = Axis::X;
72 _handleX.baseColor = Color(1.0f, 0.0f, 0.0f, 1.0f);
73 _handleX.entity = createHandleEntity("cone", _handleX.baseColor);
74 _handleX.render = _handleX.entity ? _handleX.entity->findComponent<RenderComponent>() : nullptr;
75 _handleX.material = _handleX.render ? static_cast<StandardMaterial*>(_handleX.render->material()) : nullptr;
76
77 _handleY.axis = Axis::Y;
78 _handleY.baseColor = Color(0.0f, 1.0f, 0.0f, 1.0f);
79 _handleY.entity = createHandleEntity("cone", _handleY.baseColor);
80 _handleY.render = _handleY.entity ? _handleY.entity->findComponent<RenderComponent>() : nullptr;
81 _handleY.material = _handleY.render ? static_cast<StandardMaterial*>(_handleY.render->material()) : nullptr;
82
83 _handleZ.axis = Axis::Z;
84 _handleZ.baseColor = Color(0.0f, 0.35f, 1.0f, 1.0f);
85 _handleZ.entity = createHandleEntity("cone", _handleZ.baseColor);
86 _handleZ.render = _handleZ.entity ? _handleZ.entity->findComponent<RenderComponent>() : nullptr;
87 _handleZ.material = _handleZ.render ? static_cast<StandardMaterial*>(_handleZ.render->material()) : nullptr;
88
89 _handleCenter.axis = Axis::XYZ;
90 _handleCenter.baseColor = Color(0.92f, 0.92f, 0.92f, 1.0f);
91 _handleCenter.entity = createHandleEntity("sphere", _handleCenter.baseColor);
92 _handleCenter.render = _handleCenter.entity ? _handleCenter.entity->findComponent<RenderComponent>() : nullptr;
93 _handleCenter.material = _handleCenter.render ? static_cast<StandardMaterial*>(_handleCenter.render->material()) : nullptr;
94
95 _shaftX.axis = Axis::X;
96 _shaftX.baseColor = _handleX.baseColor;
97 _shaftX.entity = createHandleEntity("cylinder", _shaftX.baseColor);
98 _shaftX.render = _shaftX.entity ? _shaftX.entity->findComponent<RenderComponent>() : nullptr;
99 _shaftX.material = _shaftX.render ? static_cast<StandardMaterial*>(_shaftX.render->material()) : nullptr;
100
101 _shaftY.axis = Axis::Y;
102 _shaftY.baseColor = _handleY.baseColor;
103 _shaftY.entity = createHandleEntity("cylinder", _shaftY.baseColor);
104 _shaftY.render = _shaftY.entity ? _shaftY.entity->findComponent<RenderComponent>() : nullptr;
105 _shaftY.material = _shaftY.render ? static_cast<StandardMaterial*>(_shaftY.render->material()) : nullptr;
106
107 _shaftZ.axis = Axis::Z;
108 _shaftZ.baseColor = _handleZ.baseColor;
109 _shaftZ.entity = createHandleEntity("cylinder", _shaftZ.baseColor);
110 _shaftZ.render = _shaftZ.entity ? _shaftZ.entity->findComponent<RenderComponent>() : nullptr;
111 _shaftZ.material = _shaftZ.render ? static_cast<StandardMaterial*>(_shaftZ.render->material()) : nullptr;
112
114 }
115
116 Entity* TransformGizmo::createHandleEntity(const char* primitiveType, const Color& color)
117 {
118 if (!_engine || !_root) {
119 return nullptr;
120 }
121
122 auto* entity = new Entity();
123 entity->setEngine(_engine);
124
125 auto material = std::make_shared<StandardMaterial>();
126 material->setDiffuse(color);
127 material->setEmissive(color);
128 // Keep emissive modest to avoid tonemapping-driven white clipping.
129 material->setEmissiveIntensity(1.0f);
130 // Keep axis colors stable regardless of scene lights.
131 material->setUseLighting(false);
132 material->setUseSkybox(false);
133 // Ensure both StandardMaterial and base Material uniform paths see the same color.
134 material->setBaseColorFactor(color);
135 material->setEmissiveFactor(color);
136 _materials.push_back(material);
137
138 auto* render = static_cast<RenderComponent*>(entity->addComponent<RenderComponent>());
139 if (render) {
140 render->setType(primitiveType);
141 render->setMaterial(material.get());
142 // DEVIATION: render gizmo on world layer to avoid composition-specific UI sublayer behavior.
143 render->setLayers({LAYERID_WORLD});
144 }
145
146 _root->addChild(entity);
147 return entity;
148 }
149
151 {
152 _target = target;
153 _hoveredAxis = Axis::None;
154 _activeAxis = Axis::None;
155 _dragging = false;
156 update();
157 }
158
160 {
161 _mode = mode;
162
163 const char* axisType = "cone";
164 const char* centerType = "sphere";
165 switch (_mode) {
166 case Mode::Translate:
167 axisType = "cone";
168 centerType = "sphere";
169 break;
170 case Mode::Rotate:
171 // DEVIATION: upstream rotate gizmo uses arc rings. This implementation currently visualizes
172 // rotate handles as axis cylinders.
173 axisType = "cylinder";
174 centerType = "sphere";
175 break;
176 case Mode::Scale:
177 // DEVIATION: upstream scale gizmo uses box-line + plane handles. This implementation uses
178 // box endpoints and center box for scale interaction.
179 axisType = "box";
180 centerType = "box";
181 break;
182 }
183
184 if (_handleX.render) _handleX.render->setType(axisType);
185 if (_handleY.render) _handleY.render->setType(axisType);
186 if (_handleZ.render) _handleZ.render->setType(axisType);
187 if (_handleCenter.render) _handleCenter.render->setType(centerType);
188
189 updateHandleTransforms();
190 updateHandleColors();
191 }
192
193 bool TransformGizmo::handleEvent(const SDL_Event& event, const int windowWidth, const int windowHeight)
194 {
195 _windowWidth = std::max(1, windowWidth);
196 _windowHeight = std::max(1, windowHeight);
197
198 if (!_target) {
199 return false;
200 }
201
202 if (event.type == SDL_EVENT_MOUSE_MOTION) {
203 const float mx = event.motion.x;
204 const float my = event.motion.y;
205 if (_dragging) {
206 applyDrag(mx, my);
207 return true;
208 }
209
210 _hoveredAxis = pickAxis(mx, my);
211 updateHandleColors();
212 return _hoveredAxis != Axis::None;
213 }
214
215 if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT) {
216 const float mx = event.button.x;
217 const float my = event.button.y;
218 const Axis axis = pickAxis(mx, my);
219 if (axis == Axis::None) {
220 return false;
221 }
222 beginDrag(axis, mx, my);
223 return true;
224 }
225
226 if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_LEFT) {
227 if (_dragging) {
228 endDrag();
229 return true;
230 }
231 }
232
233 return false;
234 }
235
237 {
238 updateHandleTransforms();
239 updateHandleColors();
240 }
241
242 void TransformGizmo::updateHandleTransforms()
243 {
244 if (!_target || !_root) {
245 if (_root) {
246 _root->setEnabled(false);
247 }
248 return;
249 }
250
251 _root->setEnabled(true);
252
253 const Vector3 center = _target->position();
254 _root->setPosition(center);
255
256 const Vector3 ax = axisDirection(Axis::X);
257 const Vector3 ay = axisDirection(Axis::Y);
258 const Vector3 az = axisDirection(Axis::Z);
259
260 _handleX.worldPosition = center + ax * _gizmoSize;
261 _handleY.worldPosition = center + ay * _gizmoSize;
262 _handleZ.worldPosition = center + az * _gizmoSize;
263 _handleCenter.worldPosition = center;
264 // Cone height in local Y is 0.22 units. The cone primitive's origin sits at its
265 // geometric center, so the base is at -halfConeHeight and the tip at +halfConeHeight
266 // in local space. To flush the cone base against the shaft tip the cone center must
267 // be placed at (shaftLength + halfConeHeight) along the axis — which equals
268 // (_gizmoSize - halfConeHeight) since _gizmoSize == shaftLength + fullConeHeight.
269 constexpr float coneHeight = 0.22f;
270 constexpr float halfConeHeight = coneHeight * 0.5f;
271 const float shaftLength = std::max(0.05f, _gizmoSize - coneHeight);
272 const float shaftRadius = (_mode == Mode::Scale) ? 0.03f : 0.02f;
273 const bool showShafts = (_mode != Mode::Rotate);
274
275 if (_handleX.entity) {
276 _handleX.entity->setLocalEulerAngles(0.0f, 0.0f, -90.0f);
277 if (_mode == Mode::Rotate) {
278 _handleX.entity->setPosition(_handleX.worldPosition);
279 _handleX.entity->setLocalScale(0.06f, _gizmoSize * 0.55f, 0.06f);
280 } else if (_mode == Mode::Scale) {
281 _handleX.entity->setPosition(_handleX.worldPosition);
282 _handleX.entity->setLocalScale(0.16f, 0.16f, 0.16f);
283 } else {
284 // Place cone center at shaftLength + halfConeHeight so its base meets the shaft tip.
285 _handleX.entity->setPosition(center + ax * (shaftLength + halfConeHeight));
286 _handleX.entity->setLocalScale(0.08f, coneHeight, 0.08f);
287 }
288 }
289 if (_shaftX.entity) {
290 _shaftX.entity->setEnabled(showShafts);
291 _shaftX.entity->setPosition(center + ax * (shaftLength * 0.5f));
292 _shaftX.entity->setLocalEulerAngles(0.0f, 0.0f, -90.0f);
293 _shaftX.entity->setLocalScale(shaftRadius, shaftLength, shaftRadius);
294 }
295
296 if (_handleY.entity) {
297 _handleY.entity->setLocalEulerAngles(0.0f, 0.0f, 0.0f);
298 if (_mode == Mode::Rotate) {
299 _handleY.entity->setPosition(_handleY.worldPosition);
300 _handleY.entity->setLocalScale(0.06f, _gizmoSize * 0.55f, 0.06f);
301 } else if (_mode == Mode::Scale) {
302 _handleY.entity->setPosition(_handleY.worldPosition);
303 _handleY.entity->setLocalScale(0.16f, 0.16f, 0.16f);
304 } else {
305 _handleY.entity->setPosition(center + ay * (shaftLength + halfConeHeight));
306 _handleY.entity->setLocalScale(0.08f, coneHeight, 0.08f);
307 }
308 }
309 if (_shaftY.entity) {
310 _shaftY.entity->setEnabled(showShafts);
311 _shaftY.entity->setPosition(center + ay * (shaftLength * 0.5f));
312 _shaftY.entity->setLocalEulerAngles(0.0f, 0.0f, 0.0f);
313 _shaftY.entity->setLocalScale(shaftRadius, shaftLength, shaftRadius);
314 }
315
316 if (_handleZ.entity) {
317 _handleZ.entity->setLocalEulerAngles(90.0f, 0.0f, 0.0f);
318 if (_mode == Mode::Rotate) {
319 _handleZ.entity->setPosition(_handleZ.worldPosition);
320 _handleZ.entity->setLocalScale(0.06f, _gizmoSize * 0.55f, 0.06f);
321 } else if (_mode == Mode::Scale) {
322 _handleZ.entity->setPosition(_handleZ.worldPosition);
323 _handleZ.entity->setLocalScale(0.16f, 0.16f, 0.16f);
324 } else {
325 _handleZ.entity->setPosition(center + az * (shaftLength + halfConeHeight));
326 _handleZ.entity->setLocalScale(0.08f, coneHeight, 0.08f);
327 }
328 }
329 if (_shaftZ.entity) {
330 _shaftZ.entity->setEnabled(showShafts);
331 _shaftZ.entity->setPosition(center + az * (shaftLength * 0.5f));
332 _shaftZ.entity->setLocalEulerAngles(90.0f, 0.0f, 0.0f);
333 _shaftZ.entity->setLocalScale(shaftRadius, shaftLength, shaftRadius);
334 }
335
336 if (_handleCenter.entity) {
337 _handleCenter.entity->setPosition(_handleCenter.worldPosition);
338 if (_mode == Mode::Scale) {
339 _handleCenter.entity->setLocalScale(0.18f, 0.18f, 0.18f);
340 } else {
341 _handleCenter.entity->setLocalScale(0.12f, 0.12f, 0.12f);
342 }
343 }
344 }
345
346 void TransformGizmo::updateHandleColors()
347 {
348 auto applyColor = [&](Handle& handle) {
349 if (!handle.material) {
350 return;
351 }
352
353 Color color = handle.baseColor;
354 if (_activeAxis == handle.axis) {
355 color = Color(1.0f, 0.95f, 0.35f, 1.0f);
356 } else if (_hoveredAxis == handle.axis) {
357 color = Color(
358 std::min(handle.baseColor.r + 0.3f, 1.0f),
359 std::min(handle.baseColor.g + 0.3f, 1.0f),
360 std::min(handle.baseColor.b + 0.3f, 1.0f),
361 1.0f
362 );
363 }
364
365 handle.material->setDiffuse(color);
366 handle.material->setEmissive(color);
367 handle.material->setBaseColorFactor(color);
368 handle.material->setEmissiveFactor(color);
369 };
370
371 applyColor(_handleX);
372 applyColor(_handleY);
373 applyColor(_handleZ);
374 applyColor(_shaftX);
375 applyColor(_shaftY);
376 applyColor(_shaftZ);
377 applyColor(_handleCenter);
378 }
379
380 TransformGizmo::Axis TransformGizmo::pickAxis(const float mouseX, const float mouseY) const
381 {
382 if (!_target) {
383 return Axis::None;
384 }
385
386 float cx = 0.0f;
387 float cy = 0.0f;
388 if (!worldToScreen(_handleCenter.worldPosition, cx, cy)) {
389 return Axis::None;
390 }
391
392 const float centerDx = mouseX - cx;
393 const float centerDy = mouseY - cy;
394 if (centerDx * centerDx + centerDy * centerDy <= PICK_RADIUS_PX * PICK_RADIUS_PX) {
395 return Axis::XYZ;
396 }
397
398 const std::array<Axis, 3> axes = {Axis::X, Axis::Y, Axis::Z};
399 float bestDistance = std::numeric_limits<float>::max();
400 Axis bestAxis = Axis::None;
401
402 for (const Axis axis : axes) {
403 const Vector3 start = _target->position();
404 const Vector3 end = start + axisDirection(axis) * _gizmoSize;
405
406 float sx = 0.0f;
407 float sy = 0.0f;
408 float ex = 0.0f;
409 float ey = 0.0f;
410 if (!worldToScreen(start, sx, sy) || !worldToScreen(end, ex, ey)) {
411 continue;
412 }
413
414 const float d2 = pointSegmentDistanceSquared(mouseX, mouseY, sx, sy, ex, ey);
415 if (d2 < bestDistance) {
416 bestDistance = d2;
417 bestAxis = axis;
418 }
419 }
420
421 if (bestAxis == Axis::None || bestDistance > PICK_RADIUS_PX * PICK_RADIUS_PX) {
422 return Axis::None;
423 }
424
425 return bestAxis;
426 }
427
428 bool TransformGizmo::worldToScreen(const Vector3& world, float& outX, float& outY) const
429 {
430 if (!_camera || !_camera->camera() || !_camera->entity()) {
431 return false;
432 }
433
434 const Matrix4 view = _camera->entity()->worldTransform().inverse();
435 const Matrix4 proj = _camera->camera()->projectionMatrix();
436 const Vector3 viewPos = view.transformPoint(world);
437 if (viewPos.getZ() >= 0.0f) {
438 return false;
439 }
440
441 const Vector4 clip = proj * Vector4(viewPos.getX(), viewPos.getY(), viewPos.getZ(), 1.0f);
442 if (std::abs(clip.getW()) < 1e-6f) {
443 return false;
444 }
445
446 const float ndcX = clip.getX() / clip.getW();
447 const float ndcY = clip.getY() / clip.getW();
448 outX = (ndcX * 0.5f + 0.5f) * _windowWidth;
449 outY = (1.0f - (ndcY * 0.5f + 0.5f)) * _windowHeight;
450 return true;
451 }
452
453 Vector3 TransformGizmo::axisDirection(const Axis axis) const
454 {
455 switch (axis) {
456 case Axis::X:
457 return Vector3(1.0f, 0.0f, 0.0f);
458 case Axis::Y:
459 return Vector3(0.0f, 1.0f, 0.0f);
460 case Axis::Z:
461 return Vector3(0.0f, 0.0f, 1.0f);
462 case Axis::XYZ:
463 case Axis::None:
464 default:
465 return Vector3(0.0f, 0.0f, 0.0f);
466 }
467 }
468
469 Vector3 TransformGizmo::cameraRight() const
470 {
471 if (!_camera || !_camera->entity()) {
472 return Vector3(1.0f, 0.0f, 0.0f);
473 }
474 const auto& wt = _camera->entity()->worldTransform();
475 const Vector4 c0 = wt.getColumn(0);
476 return Vector3(c0.getX(), c0.getY(), c0.getZ()).normalized();
477 }
478
479 Vector3 TransformGizmo::cameraUp() const
480 {
481 if (!_camera || !_camera->entity()) {
482 return Vector3(0.0f, 1.0f, 0.0f);
483 }
484 const auto& wt = _camera->entity()->worldTransform();
485 const Vector4 c1 = wt.getColumn(1);
486 return Vector3(c1.getX(), c1.getY(), c1.getZ()).normalized();
487 }
488
489 Vector3 TransformGizmo::cameraForward() const
490 {
491 if (!_camera || !_camera->entity()) {
492 return Vector3(0.0f, 0.0f, -1.0f);
493 }
494 const auto& wt = _camera->entity()->worldTransform();
495 const Vector4 c2 = wt.getColumn(2);
496 return Vector3(c2.getX(), c2.getY(), c2.getZ()).normalized();
497 }
498
499 void TransformGizmo::beginDrag(const Axis axis, const float mouseX, const float mouseY)
500 {
501 if (!_target) {
502 return;
503 }
504
505 _dragging = true;
506 _activeAxis = axis;
507 _hoveredAxis = axis;
508
509 _dragStartMouseX = mouseX;
510 _dragStartMouseY = mouseY;
511
512 _targetStartPosition = _target->position();
513 _targetStartRotation = _target->rotation();
514 _targetStartScale = _target->localScale();
515
516 SDL_CaptureMouse(true);
517 updateHandleColors();
518 }
519
520 void TransformGizmo::applyDrag(const float mouseX, const float mouseY)
521 {
522 if (!_target || !_dragging || _activeAxis == Axis::None) {
523 return;
524 }
525
526 const float dx = mouseX - _dragStartMouseX;
527 const float dy = mouseY - _dragStartMouseY;
528
529 if (_mode == Mode::Translate) {
530 if (_activeAxis == Axis::XYZ) {
531 const float upp = unitsPerPixelAtTarget();
532 Vector3 delta = cameraRight() * (dx * upp) + cameraUp() * (-dy * upp);
533 Vector3 pos = _targetStartPosition + delta;
534 if (_snap) {
535 pos = Vector3(
536 snapValue(pos.getX(), _translateSnapIncrement),
537 snapValue(pos.getY(), _translateSnapIncrement),
538 snapValue(pos.getZ(), _translateSnapIncrement)
539 );
540 }
541 _target->setPosition(pos);
542 } else {
543 const Vector3 axis = axisDirection(_activeAxis);
544 float sx0 = 0.0f;
545 float sy0 = 0.0f;
546 float sx1 = 0.0f;
547 float sy1 = 0.0f;
548 const Vector3 p0 = _targetStartPosition;
549 const Vector3 p1 = p0 + axis * _gizmoSize;
550 if (!worldToScreen(p0, sx0, sy0) || !worldToScreen(p1, sx1, sy1)) {
551 return;
552 }
553
554 const float ax = sx1 - sx0;
555 const float ay = sy1 - sy0;
556 const float len = std::sqrt(ax * ax + ay * ay);
557 if (len < 1e-4f) {
558 return;
559 }
560
561 const float dirx = ax / len;
562 const float diry = ay / len;
563 const float projectedPixels = dx * dirx + dy * diry;
564 const float pixelsPerWorld = len / _gizmoSize;
565 float worldDelta = projectedPixels / std::max(pixelsPerWorld, 1e-4f);
566
567 float along = axis.dot(_targetStartPosition + axis * worldDelta);
568 if (_snap) {
569 along = snapValue(along, _translateSnapIncrement);
570 }
571 const Vector3 perp = _targetStartPosition - axis * axis.dot(_targetStartPosition);
572 const Vector3 pos = perp + axis * along;
573 _target->setPosition(pos);
574 }
575 } else if (_mode == Mode::Rotate) {
576 Vector3 axis = (_activeAxis == Axis::XYZ) ? cameraForward() : axisDirection(_activeAxis);
577 float angleDeg = dx * 0.35f;
578 if (_snap) {
579 angleDeg = snapValue(angleDeg, _rotateSnapIncrement);
580 }
581 const Quaternion q = Quaternion::fromAxisAngle(axis, angleDeg);
582 _target->setRotation(q * _targetStartRotation);
583 } else {
584 float factor = 1.0f + dx * 0.01f;
585 factor = std::max(factor, 0.05f);
586 if (_snap) {
587 factor = 1.0f + snapValue(factor - 1.0f, _scaleSnapIncrement);
588 }
589
590 Vector3 scale = _targetStartScale;
591 if (_activeAxis == Axis::X) {
592 scale = Vector3(std::max(0.05f, _targetStartScale.getX() * factor), scale.getY(), scale.getZ());
593 } else if (_activeAxis == Axis::Y) {
594 scale = Vector3(scale.getX(), std::max(0.05f, _targetStartScale.getY() * factor), scale.getZ());
595 } else if (_activeAxis == Axis::Z) {
596 scale = Vector3(scale.getX(), scale.getY(), std::max(0.05f, _targetStartScale.getZ() * factor));
597 } else {
598 scale = _targetStartScale * factor;
599 scale = Vector3(
600 std::max(0.05f, scale.getX()),
601 std::max(0.05f, scale.getY()),
602 std::max(0.05f, scale.getZ())
603 );
604 }
605 _target->setLocalScale(scale);
606 }
607
608 updateHandleTransforms();
609 }
610
611 void TransformGizmo::endDrag()
612 {
613 _dragging = false;
614 _activeAxis = Axis::None;
615 SDL_CaptureMouse(false);
616 updateHandleColors();
617 }
618
619 float TransformGizmo::unitsPerPixelAtTarget() const
620 {
621 if (!_camera || !_camera->camera() || !_camera->entity() || !_target) {
622 return 0.01f;
623 }
624
625 if (_camera->camera()->projection() == ProjectionType::Orthographic) {
626 return (_camera->camera()->orthoHeight() * 2.0f) / _windowHeight;
627 }
628
629 const float fovRad = _camera->camera()->fov() * DEG_TO_RAD;
630 const float distance = (_target->position() - _camera->entity()->position()).length();
631 const float worldHeight = 2.0f * std::tan(fovRad * 0.5f) * std::max(distance, 0.01f);
632 return worldHeight / _windowHeight;
633 }
634}
Central application orchestrator managing scenes, rendering, input, and resource loading.
Definition engine.h:38
ECS entity — a GraphNode that hosts components defining its behavior.
Definition entity.h:32
void setPosition(float x, float y, float z)
void setLocalEulerAngles(float x, float y, float z)
void setEnabled(bool value)
void setLocalScale(float x, float y, float z)
Full PBR material with metalness/roughness workflow and advanced surface features.
TransformGizmo(Engine *engine, CameraComponent *camera)
bool handleEvent(const SDL_Event &event, int windowWidth, int windowHeight)
constexpr int LAYERID_WORLD
Definition constants.h:15
constexpr float DEG_TO_RAD
Definition defines.h:49
RGBA color with floating-point components in [0, 1].
Definition color.h:18
static Quaternion fromAxisAngle(const Vector3 &axis, float angle)
3D vector for positions, directions, and normals with multi-backend SIMD acceleration.
Definition vector3.h:29
Vector3 normalized() const