9#include <SDL3/SDL_mouse.h>
20 constexpr float PICK_RADIUS_PX = 16.0f;
22 float clamp01(
const float v)
24 return std::clamp(v, 0.0f, 1.0f);
27 float snapValue(
const float value,
const float increment)
29 if (increment <= 0.0f) {
32 return std::round(value / increment) * increment;
35 float pointSegmentDistanceSquared(
36 const float px,
const float py,
37 const float ax,
const float ay,
38 const float bx,
const float by)
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;
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;
61 : _engine(engine), _camera(camera)
66 _root->setEngine(engine);
67 if (_engine && _engine->root()) {
68 _engine->root()->addChild(_root);
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;
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;
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;
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;
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;
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;
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;
116 Entity* TransformGizmo::createHandleEntity(
const char* primitiveType,
const Color& color)
118 if (!_engine || !_root) {
122 auto* entity =
new Entity();
123 entity->setEngine(_engine);
125 auto material = std::make_shared<StandardMaterial>();
126 material->setDiffuse(color);
127 material->setEmissive(color);
129 material->setEmissiveIntensity(1.0f);
131 material->setUseLighting(
false);
132 material->setUseSkybox(
false);
134 material->setBaseColorFactor(color);
135 material->setEmissiveFactor(color);
136 _materials.push_back(material);
138 auto* render =
static_cast<RenderComponent*
>(entity->addComponent<RenderComponent>());
140 render->setType(primitiveType);
141 render->setMaterial(material.get());
146 _root->addChild(entity);
163 const char* axisType =
"cone";
164 const char* centerType =
"sphere";
168 centerType =
"sphere";
173 axisType =
"cylinder";
174 centerType =
"sphere";
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);
189 updateHandleTransforms();
190 updateHandleColors();
195 _windowWidth = std::max(1, windowWidth);
196 _windowHeight = std::max(1, windowHeight);
202 if (event.type == SDL_EVENT_MOUSE_MOTION) {
203 const float mx =
event.motion.x;
204 const float my =
event.motion.y;
210 _hoveredAxis = pickAxis(mx, my);
211 updateHandleColors();
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);
222 beginDrag(axis, mx, my);
226 if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_LEFT) {
238 updateHandleTransforms();
239 updateHandleColors();
242 void TransformGizmo::updateHandleTransforms()
244 if (!_target || !_root) {
253 const Vector3 center = _target->
position();
256 const Vector3 ax = axisDirection(
Axis::X);
257 const Vector3 ay = axisDirection(
Axis::Y);
258 const Vector3 az = axisDirection(
Axis::Z);
260 _handleX.worldPosition = center + ax * _gizmoSize;
261 _handleY.worldPosition = center + ay * _gizmoSize;
262 _handleZ.worldPosition = center + az * _gizmoSize;
263 _handleCenter.worldPosition = center;
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;
275 if (_handleX.entity) {
278 _handleX.entity->
setPosition(_handleX.worldPosition);
279 _handleX.entity->
setLocalScale(0.06f, _gizmoSize * 0.55f, 0.06f);
281 _handleX.entity->setPosition(_handleX.worldPosition);
282 _handleX.entity->setLocalScale(0.16f, 0.16f, 0.16f);
285 _handleX.entity->setPosition(center + ax * (shaftLength + halfConeHeight));
286 _handleX.entity->setLocalScale(0.08f, coneHeight, 0.08f);
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);
296 if (_handleY.entity) {
297 _handleY.entity->setLocalEulerAngles(0.0f, 0.0f, 0.0f);
299 _handleY.entity->setPosition(_handleY.worldPosition);
300 _handleY.entity->setLocalScale(0.06f, _gizmoSize * 0.55f, 0.06f);
302 _handleY.entity->setPosition(_handleY.worldPosition);
303 _handleY.entity->setLocalScale(0.16f, 0.16f, 0.16f);
305 _handleY.entity->setPosition(center + ay * (shaftLength + halfConeHeight));
306 _handleY.entity->setLocalScale(0.08f, coneHeight, 0.08f);
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);
316 if (_handleZ.entity) {
317 _handleZ.entity->setLocalEulerAngles(90.0f, 0.0f, 0.0f);
319 _handleZ.entity->setPosition(_handleZ.worldPosition);
320 _handleZ.entity->setLocalScale(0.06f, _gizmoSize * 0.55f, 0.06f);
322 _handleZ.entity->setPosition(_handleZ.worldPosition);
323 _handleZ.entity->setLocalScale(0.16f, 0.16f, 0.16f);
325 _handleZ.entity->setPosition(center + az * (shaftLength + halfConeHeight));
326 _handleZ.entity->setLocalScale(0.08f, coneHeight, 0.08f);
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);
336 if (_handleCenter.entity) {
337 _handleCenter.entity->setPosition(_handleCenter.worldPosition);
339 _handleCenter.entity->setLocalScale(0.18f, 0.18f, 0.18f);
341 _handleCenter.entity->setLocalScale(0.12f, 0.12f, 0.12f);
346 void TransformGizmo::updateHandleColors()
348 auto applyColor = [&](Handle& handle) {
349 if (!handle.material) {
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) {
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),
365 handle.material->setDiffuse(color);
366 handle.material->setEmissive(color);
367 handle.material->setBaseColorFactor(color);
368 handle.material->setEmissiveFactor(color);
371 applyColor(_handleX);
372 applyColor(_handleY);
373 applyColor(_handleZ);
377 applyColor(_handleCenter);
388 if (!worldToScreen(_handleCenter.worldPosition, cx, cy)) {
392 const float centerDx = mouseX - cx;
393 const float centerDy = mouseY - cy;
394 if (centerDx * centerDx + centerDy * centerDy <= PICK_RADIUS_PX * PICK_RADIUS_PX) {
399 float bestDistance = std::numeric_limits<float>::max();
402 for (
const Axis axis : axes) {
403 const Vector3 start = _target->position();
404 const Vector3 end = start + axisDirection(axis) * _gizmoSize;
410 if (!worldToScreen(start, sx, sy) || !worldToScreen(end, ex, ey)) {
414 const float d2 = pointSegmentDistanceSquared(mouseX, mouseY, sx, sy, ex, ey);
415 if (d2 < bestDistance) {
421 if (bestAxis ==
Axis::None || bestDistance > PICK_RADIUS_PX * PICK_RADIUS_PX) {
428 bool TransformGizmo::worldToScreen(
const Vector3& world,
float& outX,
float& outY)
const
430 if (!_camera || !_camera->camera() || !_camera->entity()) {
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) {
441 const Vector4 clip = proj * Vector4(viewPos.getX(), viewPos.getY(), viewPos.getZ(), 1.0f);
442 if (std::abs(clip.getW()) < 1e-6f) {
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;
453 Vector3 TransformGizmo::axisDirection(
const Axis axis)
const
457 return Vector3(1.0f, 0.0f, 0.0f);
459 return Vector3(0.0f, 1.0f, 0.0f);
461 return Vector3(0.0f, 0.0f, 1.0f);
465 return Vector3(0.0f, 0.0f, 0.0f);
469 Vector3 TransformGizmo::cameraRight()
const
471 if (!_camera || !_camera->entity()) {
472 return Vector3(1.0f, 0.0f, 0.0f);
474 const auto& wt = _camera->entity()->worldTransform();
475 const Vector4 c0 = wt.getColumn(0);
476 return Vector3(c0.getX(), c0.getY(), c0.getZ()).
normalized();
479 Vector3 TransformGizmo::cameraUp()
const
481 if (!_camera || !_camera->entity()) {
482 return Vector3(0.0f, 1.0f, 0.0f);
484 const auto& wt = _camera->entity()->worldTransform();
485 const Vector4 c1 = wt.getColumn(1);
486 return Vector3(c1.getX(), c1.getY(), c1.getZ()).
normalized();
489 Vector3 TransformGizmo::cameraForward()
const
491 if (!_camera || !_camera->entity()) {
492 return Vector3(0.0f, 0.0f, -1.0f);
494 const auto& wt = _camera->entity()->worldTransform();
495 const Vector4 c2 = wt.getColumn(2);
496 return Vector3(c2.getX(), c2.getY(), c2.getZ()).
normalized();
499 void TransformGizmo::beginDrag(
const Axis axis,
const float mouseX,
const float mouseY)
509 _dragStartMouseX = mouseX;
510 _dragStartMouseY = mouseY;
512 _targetStartPosition = _target->position();
513 _targetStartRotation = _target->rotation();
514 _targetStartScale = _target->localScale();
516 SDL_CaptureMouse(
true);
517 updateHandleColors();
520 void TransformGizmo::applyDrag(
const float mouseX,
const float mouseY)
522 if (!_target || !_dragging || _activeAxis ==
Axis::None) {
526 const float dx = mouseX - _dragStartMouseX;
527 const float dy = mouseY - _dragStartMouseY;
531 const float upp = unitsPerPixelAtTarget();
532 Vector3 delta = cameraRight() * (dx * upp) + cameraUp() * (-dy * upp);
533 Vector3 pos = _targetStartPosition + delta;
536 snapValue(pos.getX(), _translateSnapIncrement),
537 snapValue(pos.getY(), _translateSnapIncrement),
538 snapValue(pos.getZ(), _translateSnapIncrement)
541 _target->setPosition(pos);
543 const Vector3 axis = axisDirection(_activeAxis);
548 const Vector3 p0 = _targetStartPosition;
549 const Vector3 p1 = p0 + axis * _gizmoSize;
550 if (!worldToScreen(p0, sx0, sy0) || !worldToScreen(p1, sx1, sy1)) {
554 const float ax = sx1 - sx0;
555 const float ay = sy1 - sy0;
556 const float len = std::sqrt(ax * ax + ay * ay);
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);
567 float along = axis.dot(_targetStartPosition + axis * worldDelta);
569 along = snapValue(along, _translateSnapIncrement);
571 const Vector3 perp = _targetStartPosition - axis * axis.dot(_targetStartPosition);
572 const Vector3 pos = perp + axis * along;
573 _target->setPosition(pos);
576 Vector3 axis = (_activeAxis ==
Axis::XYZ) ? cameraForward() : axisDirection(_activeAxis);
577 float angleDeg = dx * 0.35f;
579 angleDeg = snapValue(angleDeg, _rotateSnapIncrement);
582 _target->setRotation(q * _targetStartRotation);
584 float factor = 1.0f + dx * 0.01f;
585 factor = std::max(factor, 0.05f);
587 factor = 1.0f + snapValue(factor - 1.0f, _scaleSnapIncrement);
590 Vector3 scale = _targetStartScale;
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));
598 scale = _targetStartScale * factor;
600 std::max(0.05f, scale.getX()),
601 std::max(0.05f, scale.getY()),
602 std::max(0.05f, scale.getZ())
605 _target->setLocalScale(scale);
608 updateHandleTransforms();
611 void TransformGizmo::endDrag()
615 SDL_CaptureMouse(
false);
616 updateHandleColors();
619 float TransformGizmo::unitsPerPixelAtTarget()
const
621 if (!_camera || !_camera->camera() || !_camera->entity() || !_target) {
626 return (_camera->camera()->orthoHeight() * 2.0f) / _windowHeight;
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;
Central application orchestrator managing scenes, rendering, input, and resource loading.
ECS entity — a GraphNode that hosts components defining its behavior.
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.
constexpr int LAYERID_WORLD
constexpr float DEG_TO_RAD
RGBA color with floating-point components in [0, 1].
static Quaternion fromAxisAngle(const Vector3 &axis, float angle)
3D vector for positions, directions, and normals with multi-backend SIMD acceleration.
Vector3 normalized() const