5#include <glm/gtc/matrix_transform.hpp>
6#include <glm/gtc/type_ptr.hpp>
15constexpr float kPi = 3.14159265358979323846f;
16constexpr float kAxisHitRadius = 0.12f;
17constexpr float kPlaneHitSize = 0.25f;
18constexpr float kRingThickness = 0.08f;
20float snapValue(
float v,
float step) {
21 if (
step <= 0.f)
return v;
25glm::vec3 safeNormalize(
const glm::vec3 &
v,
const glm::vec3 &fallback) {
26 float len = glm::length(
v);
27 if (len < 1e-6f)
return fallback;
36 if (mode !=
"translate" && mode !=
"rotate" && mode !=
"scale" && mode !=
"bound") {
37 throw Exception(
"TransformGizmo::setMode: expected translate|rotate|scale|bound");
44 if (space !=
"local" && space !=
"world") {
45 throw Exception(
"TransformGizmo::setSpace: expected local|world");
52 if (size <= 0.f)
throw Exception(
"TransformGizmo::setSize: size must be > 0");
58 position_ = {
x,
y,
z};
63 rotation_ = {
x,
y,
z};
74 boundsMin_ = {std::min(minX, maxX), std::min(minY, maxY), std::min(minZ, maxZ)};
75 boundsMax_ = {std::max(minX, maxX), std::max(minY, maxY), std::max(minZ, maxZ)};
86 if (index < 0 || index > 15)
throw Exception(
"TransformGizmo::getMatrix: index 0..15");
87 glm::mat4
m = worldMatrix();
88 const float *
p = glm::value_ptr(
m);
92glm::mat4 TransformGizmo::localRotationMatrix()
const {
94 m = glm::rotate(
m, rotation_.z, glm::vec3(0.f, 0.f, 1.f));
95 m = glm::rotate(
m, rotation_.y, glm::vec3(0.f, 1.f, 0.f));
96 m = glm::rotate(
m, rotation_.x, glm::vec3(1.f, 0.f, 0.f));
100glm::mat4 TransformGizmo::worldMatrix()
const {
102 m = glm::translate(
m, position_);
103 m *= localRotationMatrix();
104 m = glm::scale(
m, scale_);
108glm::vec3 TransformGizmo::axisWorld(
int axis)
const {
109 glm::vec3 local(0.f);
111 if (space_ ==
"world" && mode_ !=
"bound")
return local;
112 glm::mat3 R(localRotationMatrix());
113 return safeNormalize(R * local, local);
116void TransformGizmo::colorForAxis(
const std::string &axis, glm::vec4 &out)
const {
117 bool active = (axis == activeAxis_ || axis == hoverAxis_);
118 float boost =
active ? 1.f : 0.75f;
120 out = {1.f * boost, 0.2f, 0.2f, 1.f};
121 else if (axis ==
"y")
122 out = {0.2f, 1.f * boost, 0.2f, 1.f};
123 else if (axis ==
"z")
124 out = {0.25f, 0.45f, 1.f * boost, 1.f};
125 else if (axis ==
"xy")
126 out = {1.f * boost, 1.f * boost, 0.2f, 0.85f};
127 else if (axis ==
"yz")
128 out = {0.2f, 1.f * boost, 1.f * boost, 0.85f};
129 else if (axis ==
"xz")
130 out = {1.f * boost, 0.2f, 1.f * boost, 0.85f};
131 else if (axis ==
"xyz")
132 out = {0.9f * boost, 0.9f * boost, 0.9f * boost, 1.f};
134 out = {0.8f * boost, 0.8f * boost, 0.2f, 1.f};
139 const glm::vec3 origin = position_;
141 auto addAxis = [&](
const char *axisName,
int ai,
const char *
kind) {
146 p.dir = axisWorld(ai);
148 p.radius = size_ * 0.04f;
149 colorForAxis(
p.axis,
p.color);
153 if (mode_ ==
"translate") {
154 addAxis(
"x", 0,
"axis");
155 addAxis(
"y", 1,
"axis");
156 addAxis(
"z", 2,
"axis");
158 for (
const char *plane : {
"xy",
"yz",
"xz"}) {
163 if (std::strcmp(plane,
"xy") == 0)
164 p.dir = glm::normalize(axisWorld(0) + axisWorld(1));
165 else if (std::strcmp(plane,
"yz") == 0)
166 p.dir = glm::normalize(axisWorld(1) + axisWorld(2));
168 p.dir = glm::normalize(axisWorld(0) + axisWorld(2));
169 p.length = size_ * kPlaneHitSize;
170 p.radius = size_ * kPlaneHitSize;
171 colorForAxis(
p.axis,
p.color);
178 c.dir = {0.f, 1.f, 0.f};
180 c.radius = size_ * 0.06f;
181 colorForAxis(
c.axis,
c.color);
183 }
else if (mode_ ==
"rotate") {
184 addAxis(
"x", 0,
"ring");
185 addAxis(
"y", 1,
"ring");
186 addAxis(
"z", 2,
"ring");
187 for (
auto &
p : parts_) {
191 }
else if (mode_ ==
"scale") {
192 addAxis(
"x", 0,
"axis");
193 addAxis(
"y", 1,
"axis");
194 addAxis(
"z", 2,
"axis");
199 u.dir = {0.f, 1.f, 0.f};
200 u.radius = size_ * 0.08f;
201 colorForAxis(
u.axis,
u.color);
204 const char *names[] = {
"bx",
"by",
"bz",
"bnx",
"bny",
"bnz"};
205 glm::vec3 centers[6] = {
206 {boundsMax_.x, 0.f, 0.f}, {0.f, boundsMax_.y, 0.f}, {0.f, 0.f, boundsMax_.z},
207 {boundsMin_.x, 0.f, 0.f}, {0.f, boundsMin_.y, 0.f}, {0.f, 0.f, boundsMin_.z},
209 glm::mat3 R(localRotationMatrix());
210 for (
int i = 0; i < 6; ++i) {
214 glm::vec3 local = centers[i];
217 p.origin = position_ + R * local;
218 p.dir = axisWorld(i % 3);
219 p.radius = size_ * 0.07f;
220 colorForAxis((i % 3 == 0) ?
"x" : (i % 3 == 1) ?
"y" :
"z",
p.color);
226 box.origin = position_;
227 box.dir = {0.f, 1.f, 0.f};
228 box.length = glm::length(boundsMax_ - boundsMin_) * 0.5f;
229 box.radius = box.length;
230 colorForAxis(
"xyz", box.color);
231 parts_.push_back(box);
235float TransformGizmo::hitAxis(
const glm::vec3 &ro,
const glm::vec3 &rd,
int axisIndex,
237 glm::vec3
a = axisWorld(axisIndex);
238 glm::vec3 p0 = position_;
239 glm::vec3 p1 = position_ +
a * size_;
241 glm::vec3
u = p1 - p0;
243 glm::vec3
w = p0 - ro;
244 float aa = glm::dot(
u,
u);
245 float bb = glm::dot(
v,
v);
246 float cc = glm::dot(
u,
v);
247 float dd = glm::dot(
u,
w);
248 float ee = glm::dot(
v,
w);
249 float denom = aa * bb - cc * cc;
250 float sn = 0.f, tn = 0.f;
251 if (std::fabs(denom) > 1e-8f) {
252 sn = (cc * ee - bb * dd) / denom;
253 tn = (aa * ee - cc * dd) / denom;
255 sn = std::clamp(sn, 0.f, 1.f);
256 if (tn < 0.f)
return -1.f;
257 glm::vec3 closestU = p0 +
u * sn;
258 glm::vec3 closestV = ro +
v * tn;
259 float dist = glm::length(closestU - closestV);
260 float thresh = size_ * kAxisHitRadius;
261 if (dist > thresh)
return -1.f;
266float TransformGizmo::hitPlane(
const glm::vec3 &ro,
const glm::vec3 &rd,
int planeMask,
270 if ((planeMask & 1) == 0)
n = axisWorld(0);
271 else if ((planeMask & 2) == 0)
n = axisWorld(1);
272 else n = axisWorld(2);
273 float denom = glm::dot(
n, rd);
274 if (std::fabs(denom) < 1e-6f)
return -1.f;
275 float t = glm::dot(position_ - ro,
n) / denom;
276 if (t < 0.f)
return -1.f;
277 glm::vec3 hit = ro + rd * t;
278 glm::vec3
d = hit - position_;
280 int a0 = (planeMask & 1) ? 0 : ((planeMask & 2) ? 1 : 2);
281 int a1 = (planeMask & 4) ? 2 : ((planeMask & 2) && a0 != 1 ? 1 : (a0 == 0 ? 1 : 0));
282 if (planeMask == 3) {
285 }
else if (planeMask == 6) {
288 }
else if (planeMask == 5) {
292 float u = glm::dot(
d, axisWorld(a0));
293 float v = glm::dot(
d, axisWorld(a1));
294 float lim = size_ * kPlaneHitSize;
295 float lo = size_ * 0.05f;
296 if (
u < lo || v < lo || u > lim ||
v > lim)
return -1.f;
301float TransformGizmo::hitRing(
const glm::vec3 &ro,
const glm::vec3 &rd,
int axisIndex,
303 glm::vec3
n = axisWorld(axisIndex);
304 float denom = glm::dot(
n, rd);
305 if (std::fabs(denom) < 1e-6f)
return -1.f;
306 float t = glm::dot(position_ - ro,
n) / denom;
307 if (t < 0.f)
return -1.f;
308 glm::vec3 hit = ro + rd * t;
309 float dist = glm::length(hit - position_);
310 float target = size_;
311 float thick = size_ * kRingThickness;
312 if (std::fabs(dist - target) > thick)
return -1.f;
314 return std::fabs(dist - target);
317float TransformGizmo::hitBoundHandle(
const glm::vec3 &ro,
const glm::vec3 &rd,
int handle,
319 if (handle < 0 || handle >=
static_cast<int>(parts_.size()))
return -1.f;
320 const Part &
p = parts_[handle];
321 if (
p.kind !=
"handle")
return -1.f;
323 glm::vec3 oc = ro -
p.origin;
324 float b = glm::dot(oc, rd);
325 float c = glm::dot(oc, oc) -
p.radius *
p.radius;
326 float disc =
b *
b -
c;
327 if (disc < 0.f)
return -1.f;
328 float t = -
b - std::sqrt(disc);
329 if (t < 0.f) t = -
b + std::sqrt(disc);
330 if (t < 0.f)
return -1.f;
336 glm::vec3 ro(ox, oy, oz);
337 glm::vec3 rd = safeNormalize(glm::vec3(dx, dy, dz), glm::vec3(0.f, 0.f, -1.f));
342 auto consider = [&](
const std::string &axis,
float score,
float t) {
343 if (score < 0.f)
return;
350 if (mode_ ==
"translate") {
352 for (
int i = 0; i < 3; ++i) {
353 float s = hitAxis(ro, rd, i, t);
354 consider(i == 0 ?
"x" : i == 1 ?
"y" :
"z",
s, t);
357 if (hitPlane(ro, rd, 3, tp) >= 0.f) consider(
"xy", 0.f, tp);
358 if (hitPlane(ro, rd, 6, tp) >= 0.f) consider(
"yz", 0.f, tp);
359 if (hitPlane(ro, rd, 5, tp) >= 0.f) consider(
"xz", 0.f, tp);
361 glm::vec3 oc = ro - position_;
362 float b = glm::dot(oc, rd);
363 float c = glm::dot(oc, oc) - (size_ * 0.06f) * (size_ * 0.06f);
364 float disc =
b *
b -
c;
366 float t = -
b - std::sqrt(disc);
367 if (t >= 0.f) consider(
"xyz", 0.f, t);
369 }
else if (mode_ ==
"rotate") {
371 for (
int i = 0; i < 3; ++i) {
372 float s = hitRing(ro, rd, i, t);
373 consider(i == 0 ?
"x" : i == 1 ?
"y" :
"z",
s, t);
375 }
else if (mode_ ==
"scale") {
377 for (
int i = 0; i < 3; ++i) {
378 float s = hitAxis(ro, rd, i, t);
379 consider(i == 0 ?
"x" : i == 1 ?
"y" :
"z",
s, t);
381 glm::vec3 oc = ro - position_;
382 float rad = size_ * 0.08f;
383 float b = glm::dot(oc, rd);
384 float c = glm::dot(oc, oc) - rad * rad;
385 float disc =
b *
b -
c;
387 float t = -
b - std::sqrt(disc);
388 if (t >= 0.f) consider(
"xyz", 0.f, t);
391 for (
int i = 0; i < static_cast<int>(parts_.size()); ++i) {
393 float s = hitBoundHandle(ro, rd, i, t);
394 if (
s >= 0.f) consider(parts_[i].axis,
s, t);
403glm::vec3 TransformGizmo::projectToDragPlane(
const glm::vec3 &ro,
const glm::vec3 &rd)
const {
404 float denom = glm::dot(dragPlaneNormal_, rd);
405 if (std::fabs(denom) < 1e-8f) {
407 return dragStartHit_;
409 float t = glm::dot(dragStartHit_ - ro, dragPlaneNormal_) / denom;
413void TransformGizmo::applySnapTranslate(glm::vec3 &
v)
const {
414 if (snapTranslate_.x > 0.f)
v.x = snapValue(
v.x, snapTranslate_.x);
415 if (snapTranslate_.y > 0.f)
v.y = snapValue(
v.y, snapTranslate_.y);
416 if (snapTranslate_.z > 0.f)
v.z = snapValue(
v.z, snapTranslate_.z);
419float TransformGizmo::applySnapRotate(
float radians)
const {
420 if (snapRotateDeg_ <= 0.f)
return radians;
421 float deg = radians * (180.f / kPi);
422 deg = snapValue(deg, snapRotateDeg_);
423 return deg * (kPi / 180.f);
426float TransformGizmo::applySnapScale(
float s)
const {
427 if (snapScale_ <= 0.f)
return s;
428 return snapValue(
s, snapScale_);
432 float dy,
float dz) {
433 if (axis.empty())
return false;
437 dragStartPos_ = position_;
438 dragStartRot_ = rotation_;
439 dragStartScale_ = scale_;
441 glm::vec3 ro(ox, oy, oz);
442 glm::vec3 rd = safeNormalize(glm::vec3(dx, dy, dz), glm::vec3(0.f, 0.f, -1.f));
444 if (mode_ ==
"translate" || mode_ ==
"scale" || mode_ ==
"bound") {
445 if (axis ==
"x" || axis ==
"bx" || axis ==
"bnx")
446 dragAxisDir_ = axisWorld(0);
447 else if (axis ==
"y" || axis ==
"by" || axis ==
"bny")
448 dragAxisDir_ = axisWorld(1);
449 else if (axis ==
"z" || axis ==
"bz" || axis ==
"bnz")
450 dragAxisDir_ = axisWorld(2);
451 else if (axis ==
"xy")
452 dragPlaneNormal_ = axisWorld(2);
453 else if (axis ==
"yz")
454 dragPlaneNormal_ = axisWorld(0);
455 else if (axis ==
"xz")
456 dragPlaneNormal_ = axisWorld(1);
458 dragPlaneNormal_ = safeNormalize(glm::cross(rd, glm::vec3(0.f, 1.f, 0.f)),
459 glm::vec3(0.f, 0.f, 1.f));
461 if (axis ==
"x" || axis ==
"y" || axis ==
"z" || axis ==
"bx" || axis ==
"by" ||
462 axis ==
"bz" || axis ==
"bnx" || axis ==
"bny" || axis ==
"bnz") {
464 glm::vec3 side = glm::cross(dragAxisDir_, rd);
465 if (glm::length(side) < 1e-5f) side = glm::cross(dragAxisDir_, glm::vec3(0.f, 1.f, 0.f));
466 dragPlaneNormal_ = safeNormalize(glm::cross(side, dragAxisDir_), glm::vec3(0.f, 1.f, 0.f));
469 dragPlaneNormal_ = -rd;
472 int ai = axis ==
"x" ? 0 : axis ==
"y" ? 1 : 2;
473 dragAxisDir_ = axisWorld(ai);
474 dragPlaneNormal_ = dragAxisDir_;
477 float denom = glm::dot(dragPlaneNormal_, rd);
478 if (std::fabs(denom) > 1e-8f) {
479 float t = glm::dot(position_ - ro, dragPlaneNormal_) / denom;
480 dragStartHit_ = ro + rd * t;
482 dragStartHit_ = position_;
489 if (!dragging_)
return false;
490 glm::vec3 ro(ox, oy, oz);
491 glm::vec3 rd = safeNormalize(glm::vec3(dx, dy, dz), glm::vec3(0.f, 0.f, -1.f));
492 glm::vec3 hit = projectToDragPlane(ro, rd);
493 glm::vec3 delta = hit - dragStartHit_;
495 if (mode_ ==
"translate") {
497 if (activeAxis_ ==
"x" || activeAxis_ ==
"y" || activeAxis_ ==
"z") {
498 float dist = glm::dot(delta, dragAxisDir_);
499 move = dragAxisDir_ * dist;
500 }
else if (activeAxis_ ==
"xy") {
501 move = axisWorld(0) * glm::dot(delta, axisWorld(0)) +
502 axisWorld(1) * glm::dot(delta, axisWorld(1));
503 }
else if (activeAxis_ ==
"yz") {
504 move = axisWorld(1) * glm::dot(delta, axisWorld(1)) +
505 axisWorld(2) * glm::dot(delta, axisWorld(2));
506 }
else if (activeAxis_ ==
"xz") {
507 move = axisWorld(0) * glm::dot(delta, axisWorld(0)) +
508 axisWorld(2) * glm::dot(delta, axisWorld(2));
509 }
else if (activeAxis_ ==
"xyz") {
512 position_ = dragStartPos_ + move;
513 applySnapTranslate(position_);
514 }
else if (mode_ ==
"rotate") {
515 glm::vec3 from = safeNormalize(dragStartHit_ - position_, dragAxisDir_);
516 glm::vec3 to = safeNormalize(hit - position_, from);
518 glm::vec3
c = glm::cross(from, to);
519 float sinA = glm::dot(
c, dragAxisDir_);
520 float cosA = glm::clamp(glm::dot(from, to), -1.f, 1.f);
521 float ang = std::atan2(sinA, cosA);
522 ang = applySnapRotate(ang);
523 rotation_ = dragStartRot_;
524 if (activeAxis_ ==
"x")
525 rotation_.x = dragStartRot_.x + ang;
526 else if (activeAxis_ ==
"y")
527 rotation_.y = dragStartRot_.y + ang;
529 rotation_.z = dragStartRot_.z + ang;
530 }
else if (mode_ ==
"scale") {
531 float dist = glm::dot(delta, dragAxisDir_);
532 float factor = 1.f + dist / std::max(size_, 1e-3f);
533 if (activeAxis_ ==
"xyz") {
534 float s = applySnapScale(std::max(0.01f, dragStartScale_.x * factor));
537 scale_ = dragStartScale_;
538 float s = applySnapScale(std::max(0.01f, (activeAxis_ ==
"x" ? dragStartScale_.x
539 : activeAxis_ ==
"y" ? dragStartScale_.y
540 : dragStartScale_.z) *
542 if (activeAxis_ ==
"x")
544 else if (activeAxis_ ==
"y")
550 float dist = glm::dot(delta, dragAxisDir_);
551 if (activeAxis_ ==
"bx")
552 boundsMax_.x = std::max(boundsMin_.x + 0.01f, boundsMax_.x + dist / std::max(scale_.x, 1e-3f));
553 else if (activeAxis_ ==
"by")
554 boundsMax_.y = std::max(boundsMin_.y + 0.01f, boundsMax_.y + dist / std::max(scale_.y, 1e-3f));
555 else if (activeAxis_ ==
"bz")
556 boundsMax_.z = std::max(boundsMin_.z + 0.01f, boundsMax_.z + dist / std::max(scale_.z, 1e-3f));
557 else if (activeAxis_ ==
"bnx")
558 boundsMin_.x = std::min(boundsMax_.x - 0.01f, boundsMin_.x + dist / std::max(scale_.x, 1e-3f));
559 else if (activeAxis_ ==
"bny")
560 boundsMin_.y = std::min(boundsMax_.y - 0.01f, boundsMin_.y + dist / std::max(scale_.y, 1e-3f));
561 else if (activeAxis_ ==
"bnz")
562 boundsMin_.z = std::min(boundsMax_.z - 0.01f, boundsMin_.z + dist / std::max(scale_.z, 1e-3f));
577bool TransformGizmo::validPart(
int index)
const {
578 return index >= 0 && index < static_cast<int>(parts_.size());
582 if (!validPart(index))
throw Exception(
"TransformGizmo::getPartKind: bad index");
583 return parts_[index].kind;
586 if (!validPart(index))
throw Exception(
"TransformGizmo::getPartAxis: bad index");
587 return parts_[index].axis;
590 if (!validPart(index))
throw Exception(
"TransformGizmo::getPartColorR: bad index");
591 return parts_[index].color.r;
594 if (!validPart(index))
throw Exception(
"TransformGizmo::getPartColorG: bad index");
595 return parts_[index].color.g;
598 if (!validPart(index))
throw Exception(
"TransformGizmo::getPartColorB: bad index");
599 return parts_[index].color.b;
602 if (!validPart(index))
throw Exception(
"TransformGizmo::getPartColorA: bad index");
603 return parts_[index].color.a;
606 if (!validPart(index))
throw Exception(
"TransformGizmo::getPartOriginX: bad index");
607 return parts_[index].origin.x;
610 if (!validPart(index))
throw Exception(
"TransformGizmo::getPartOriginY: bad index");
611 return parts_[index].origin.y;
614 if (!validPart(index))
throw Exception(
"TransformGizmo::getPartOriginZ: bad index");
615 return parts_[index].origin.z;
618 if (!validPart(index))
throw Exception(
"TransformGizmo::getPartDirX: bad index");
619 return parts_[index].dir.x;
622 if (!validPart(index))
throw Exception(
"TransformGizmo::getPartDirY: bad index");
623 return parts_[index].dir.y;
626 if (!validPart(index))
throw Exception(
"TransformGizmo::getPartDirZ: bad index");
627 return parts_[index].dir.z;
630 if (!validPart(index))
throw Exception(
"TransformGizmo::getPartLength: bad index");
631 return parts_[index].length;
634 if (!validPart(index))
throw Exception(
"TransformGizmo::getPartRadius: bad index");
635 return parts_[index].radius;