#ifdef ENABLE_EDITOR
#include "editor.h"
#include <cassert>
#include "imgui.h"
#include "passes/debug/debug_draw.h"
#include "passes/debug/debug_ray.h"
#include "scene/camera.h"
#include "scene/scene.h"
namespace editor {
constexpr u8 GIZMO_AXIS_X = 0;
constexpr u8 GIZMO_AXIS_Y = 1;
constexpr u8 GIZMO_AXIS_Z = 2;
constexpr u8 GIZMO_FREE_AXIS = 3; // center axis
constexpr u8 GIZMO_PLANE_YZ = 4; // normal = X, spans Y+Z
constexpr u8 GIZMO_PLANE_XZ = 5; // normal = Y, spans X+Z
constexpr u8 GIZMO_PLANE_XY = 6; // normal = Z, spans X+Y
constexpr u8 GIZMO_VIEW_AXIS = 7; // outer view ring (rotate)
constexpr f32 GIZMO_AXIS_HIT_PX = 10.0f;
constexpr f32 GIZMO_CENTER_RADIUS_PX = 10.0f;
constexpr f32 GIZMO_CENTER_HIT_PX = 12.0f;
constexpr f32 GIZMO_HANDLE_LEN_PX = 90.0f;
constexpr f32 GIZMO_PLANE_FRAC = 0.35f;
constexpr f32 GIZMO_RING_HIT_PX = 6.0f;
constexpr f32 GIZMO_RING_HIT_EDGE_PX = 4.0f; // edge-on rings project to a line through center
constexpr f32 GIZMO_OUTER_HIT_PX = 7.0f;
constexpr f32 GIZMO_RING_DEADZONE_PX = GIZMO_CENTER_HIT_PX + 6.0f;
constexpr f32 GIZMO_OUTER_RING_SCALE = 1.2f;
constexpr u32 GIZMO_RING_SEGS = 48;
inline vec3 gizmo_axis_dir(u8 axis) {
if (axis == 0) {
return vec3(1.0f, 0.0f, 0.0f);
}
if (axis == 1) {
return vec3(0.0f, 1.0f, 0.0f);
}
return vec3(0.0f, 0.0f, 1.0f);
}
inline vec4 gizmo_axis_color(u8 axis) {
if (axis == 0) {
return vec4(10.0f, 0.4f, 0.4f, 1.0f);
}
if (axis == 1) {
return vec4(0.4f, 10.0f, 0.4f, 1.0f);
}
return vec4(0.5f, 0.5f, 10.0f, 1.0f);
}
// planar handle -> spanned axes + normal axis
// map map for hit/draw/grab/scale.
struct GizmoPlaneMap {
u8 ia;
u8 ib;
u8 n_axis;
};
inline GizmoPlaneMap gizmo_plane_map(u8 plane_id) {
if (plane_id == GIZMO_PLANE_YZ) {
return {1, 2, GIZMO_AXIS_X};
}
if (plane_id == GIZMO_PLANE_XZ) {
return {0, 2, GIZMO_AXIS_Y};
}
return {0, 1, GIZMO_AXIS_Z}; // GIZMO_PLANE_XY (and invalid fallback)
}
inline u8 plane_normal_axis(u8 plane_id) {
return gizmo_plane_map(plane_id).n_axis;
}
// scale mask: 1 on axes affected by the handle, 0 elsewhere (0,0,0) = invalid
inline vec3 gizmo_scale_mask(u8 handle) {
if (handle == GIZMO_FREE_AXIS) {
return vec3(1.0f);
}
if (handle == GIZMO_AXIS_X) {
return vec3(1.0f, 0.0f, 0.0f);
}
if (handle == GIZMO_AXIS_Y) {
return vec3(0.0f, 1.0f, 0.0f);
}
if (handle == GIZMO_AXIS_Z) {
return vec3(0.0f, 0.0f, 1.0f);
}
if (handle == GIZMO_PLANE_YZ) {
return vec3(0.0f, 1.0f, 1.0f);
}
if (handle == GIZMO_PLANE_XZ) {
return vec3(1.0f, 0.0f, 1.0f);
}
if (handle == GIZMO_PLANE_XY) {
return vec3(1.0f, 1.0f, 0.0f);
}
return vec3(0.0f);
}
struct GizmoSelection {
vec3 origin;
mat4 base_mat;
};
inline GizmoSelection gizmo_selection(const Scene &scene, u32 node) {
const SceneGraph &sg = scene.scene_graph;
mat4 base = (sg.instance_count[node] > 0) ? scene.instance_world(node, 0) : sg.worlds[node];
vec4 h = transform_point(vec3(0.0f, 0.0f, 0.0f), base);
return {vec3(h.x, h.y, h.z), base};
}
inline bool project_to_view(vec3 p, const mat4 &view_proj, ImVec2 min, ImVec2 size, ImVec2 &out) {
vec4 c = transform_point(p, view_proj);
if (c.w <= 0.0f) { // behind camera
return false;
}
f32 nx = c.x / c.w;
f32 ny = c.y / c.w;
out = ImVec2(min.x + (nx * 0.5f + 0.5f) * size.x, min.y + (ny * 0.5f + 0.5f) * size.y);
return true;
}
inline f32 dist_point_seg(ImVec2 p, ImVec2 a, ImVec2 b) {
f32 dx = b.x - a.x;
f32 dy = b.y - a.y;
f32 len2 = dx * dx + dy * dy;
f32 t = (len2 > 1e-8f) ? ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2 : 0.0f;
if (t < 0.0f) {
t = 0.0f;
} else if (t > 1.0f) {
t = 1.0f;
}
f32 ex = a.x + dx * t - p.x;
f32 ey = a.y + dy * t - p.y;
return sqrtf(ex * ex + ey * ey);
}
// closest point on axis (origin + s*axis, |axis|==1) to ray (ray_o + t*ray_d).
inline vec3 closest_point_on_axis(vec3 origin, vec3 axis, vec3 ray_o, vec3 ray_d) {
vec3 w0 = origin - ray_o;
f32 b = axis.dot(ray_d);
f32 d = axis.dot(w0);
f32 f = ray_d.dot(w0);
f32 denom = 1.0f - b * b;
f32 s = (fabsf(denom) < 1e-6f) ? 0.0f : (b * f - d) / denom;
return origin + axis * s;
}
// signed distance along axis from origin of the closest point to the ray.
inline f32 axis_signed_distance(vec3 origin, vec3 axis, vec3 ray_o, vec3 ray_d) {
vec3 p = closest_point_on_axis(origin, axis, ray_o, ray_d);
return (p - origin).dot(axis);
}
// ray plane intersection (point + t*n, |n|==1). Falls back to `fallback` when the ray
// runs parallel to the plane or points away.
inline vec3 ray_plane_point(vec3 point, vec3 n, vec3 ray_o, vec3 ray_d, vec3 fallback) {
f32 denom = ray_d.dot(n);
if (fabsf(denom) < 1e-6f) {
return fallback;
}
f32 t = (point - ray_o).dot(n) / denom;
if (!(t > 0.0f) || !std::isfinite(t)) {
return fallback;
}
return ray_o + ray_d * t;
}
inline vec3 gizmo_quat_rotate(vec3 v, quat q) {
return vec3::from_vec4(rtm::quat_mul_vector3(v.to_vec4(), (rtm::quatf)q));
}
// orthonormal basis (u,v) spanning the plane perpendicular to axis
inline void gizmo_ring_basis(vec3 axis, vec3 &u, vec3 &v) {
vec3 helper = (fabsf(axis.z) < 0.9f) ? vec3(0.0f, 0.0f, 1.0f) : vec3(0.0f, 1.0f, 0.0f);
u = cross(helper, axis);
f32 l = u.length();
if (!(l > 1e-6f)) {
u = vec3(1.0f, 0.0f, 0.0f);
} else {
u = u / l;
}
v = cross(axis, u);
l = v.length();
if (l > 1e-6f) {
v = v / l;
}
}
inline f32 gizmo_wrap_pi(f32 a) {
while (a > PI) {
a -= 2.0f * PI;
}
while (a < -PI) {
a += 2.0f * PI;
}
return a;
}
inline f32 gizmo_angle_on_plane(vec3 p, vec3 origin, vec3 u, vec3 v) {
vec3 d = p - origin;
return atan2f(d.dot(v), d.dot(u));
}
// per-frame gizmo context, computed once per tool update.
struct GizmoFrame {
u32 node = 0;
SceneGraph *sg = nullptr;
const Camera *cam = nullptr;
vec3 origin = vec3(0.0f);
mat4 base_mat = identity();
vec3 axes[3] = {vec3(1.0f, 0.0f, 0.0f), vec3(0.0f, 1.0f, 0.0f), vec3(0.0f, 0.0f, 1.0f)};
f32 dist = 0.0f;
f32 len = 0.0f; // screen-constant handle length (world units)
ImVec2 mouse = ImVec2(0, 0);
bool hover_view = false;
ImVec2 o_px = ImVec2(0, 0);
bool o_visible = false;
DebugRay ray = {};
};
inline void gizmo_compute_axes(const Editor &e, const Scene &scene, u32 node, const mat4 &base_mat, vec3 *axes) {
if (e.gizmo_world) {
axes[0] = vec3(1.0f, 0.0f, 0.0f);
axes[1] = vec3(0.0f, 1.0f, 0.0f);
axes[2] = vec3(0.0f, 0.0f, 1.0f);
return;
}
(void)scene;
quat q = mat4_to_trs(base_mat).rotation;
axes[0] = gizmo_quat_rotate(vec3(1.0f, 0.0f, 0.0f), q).normalized();
axes[1] = gizmo_quat_rotate(vec3(0.0f, 1.0f, 0.0f), q).normalized();
axes[2] = gizmo_quat_rotate(vec3(0.0f, 0.0f, 1.0f), q).normalized();
for (int i = 0; i < 3; ++i) {
if (!debug_finite_vec3(axes[i]) || axes[i].length_squared() < 1e-8f) {
axes[i] = gizmo_axis_dir((u8)i);
}
}
}
// shared origin/base/axes/dist/len resolution for begin-frame + rebase
struct GizmoFrameData {
vec3 origin;
mat4 base_mat;
vec3 axes[3];
f32 dist = 0.0f;
f32 len = 0.0f;
};
inline bool gizmo_resolve_frame(const Editor &e, const Scene &scene, u32 node, const Camera &cam, GizmoFrameData &out) {
GizmoSelection sel = gizmo_selection(scene, node);
vec3 to_cam = cam.position - sel.origin;
f32 dist = to_cam.length();
if (!(dist > 0.0f) || !std::isfinite(dist)) {
return false;
}
// screen-constant handle size (see pixels_to_world).
f32 len = DebugDraw::pixels_to_world(dist, GIZMO_HANDLE_LEN_PX, cam.fov_y_deg, e.gizmo_view_size.y);
if (!(len > 0.0f) || !std::isfinite(len)) {
return false;
}
out.origin = sel.origin;
out.base_mat = sel.base_mat;
gizmo_compute_axes(e, scene, node, sel.base_mat, out.axes);
out.dist = dist;
out.len = len;
return true;
}
inline void gizmo_apply_frame_data(GizmoFrame &f, u32 node, const GizmoFrameData &d) {
f.node = node;
f.origin = d.origin;
f.base_mat = d.base_mat;
f.axes[0] = d.axes[0];
f.axes[1] = d.axes[1];
f.axes[2] = d.axes[2];
f.dist = d.dist;
f.len = d.len;
}
inline void gizmo_cancel_drag(Editor &e) {
e.gizmo_dragging = false;
e.gizmo_grab_node = -1;
e.gizmo_hover_axis = -1;
}
inline bool gizmo_begin_frame(Editor &e, Scene &scene, GizmoFrame &f) {
if (!e.show_cat_gizmo || e.selected_node < 0 || (u32)e.selected_node >= scene.scene_graph.node_count) {
gizmo_cancel_drag(e);
return false;
}
SceneGraph &sg = scene.scene_graph;
u32 node = (u32)e.selected_node;
if (!sg.is_node_alive(node)) {
gizmo_cancel_drag(e);
return false;
}
if (scene.active_camera >= scene.cameras.size()) {
gizmo_cancel_drag(e);
return false;
}
if (e.gizmo_view_size.x <= 0.0f || e.gizmo_view_size.y <= 0.0f) {
return false; // no viewport rect cached yet (first frame)
}
const Camera &cam = scene.cameras[scene.active_camera];
GizmoFrameData d;
if (!gizmo_resolve_frame(e, scene, node, cam, d)) {
gizmo_cancel_drag(e);
return false;
}
ImVec2 mouse = ImGui::GetMousePos();
bool hover_view = mouse.x >= e.gizmo_view_min.x && mouse.x <= e.gizmo_view_min.x + e.gizmo_view_size.x &&
mouse.y >= e.gizmo_view_min.y && mouse.y <= e.gizmo_view_min.y + e.gizmo_view_size.y;
ImVec2 o_px{};
bool o_visible = project_to_view(d.origin, cam.view_proj, e.gizmo_view_min, e.gizmo_view_size, o_px);
// mouse ray in world space (for grab + drag).
// NDC: up is y=-1.
// imgui pixels map straight through, no y-flip
f32 nx = ((mouse.x - e.gizmo_view_min.x) / e.gizmo_view_size.x) * 2.0f - 1.0f;
f32 ny = ((mouse.y - e.gizmo_view_min.y) / e.gizmo_view_size.y) * 2.0f - 1.0f;
DebugRay ray = debug_unproject(nx, ny, cam.position, cam.inv_view_proj);
f.sg = &sg;
f.cam = &cam;
gizmo_apply_frame_data(f, node, d);
f.mouse = mouse;
f.hover_view = hover_view;
f.o_px = o_px;
f.o_visible = o_visible;
f.ray = ray;
return true;
}
inline void gizmo_capture_common(Editor &e, SceneGraph &sg, u32 node, vec3 origin, const mat4 &base_mat, u8 handle) {
e.gizmo_dragging = true;
e.gizmo_grab_node = (int)node;
e.gizmo_drag_axis = handle;
e.gizmo_base_local = sg.translations[node];
e.gizmo_base_world = origin;
e.gizmo_base_world_mat = base_mat;
e.gizmo_base_local_mat = sg.locals[node];
e.gizmo_base_scale = sg.scales[node];
}
// FREE keeps the caller's plane (move/scale set it to the view plane first)
// AXIS refines per tool (rotate -> ring plane, scale -> grab distance)
// PLANAR always refreshes plane + distance (move ignores the distance)
inline void gizmo_derive_grab_refs(Editor &e, const GizmoFrame &f, const Camera &cam, vec3 origin, u8 handle) {
if (handle == GIZMO_FREE_AXIS) {
e.gizmo_grab_point = ray_plane_point(origin, e.gizmo_plane_n, f.ray.origin, f.ray.dir, origin);
} else if (handle == GIZMO_VIEW_AXIS) {
vec3 n = cam.forward().normalized();
e.gizmo_plane_n = n;
gizmo_ring_basis(n, e.gizmo_drag_u, e.gizmo_drag_v);
vec3 p = ray_plane_point(origin, n, f.ray.origin, f.ray.dir, origin);
e.gizmo_grab_point = p;
e.gizmo_grab_angle = gizmo_angle_on_plane(p, origin, e.gizmo_drag_u, e.gizmo_drag_v);
} else if (handle <= GIZMO_AXIS_Z) {
vec3 axis = f.axes[handle];
e.gizmo_grab_point = closest_point_on_axis(origin, axis, f.ray.origin, f.ray.dir);
// rotate uses plane perpendicular to axis
if (e.gizmo_tool == GIZMO_TOOL_ROTATE) {
e.gizmo_plane_n = axis;
gizmo_ring_basis(axis, e.gizmo_drag_u, e.gizmo_drag_v);
vec3 p = ray_plane_point(origin, axis, f.ray.origin, f.ray.dir, origin);
e.gizmo_grab_point = p;
e.gizmo_grab_angle = gizmo_angle_on_plane(p, origin, e.gizmo_drag_u, e.gizmo_drag_v);
} else if (e.gizmo_tool == GIZMO_TOOL_SCALE) {
e.gizmo_grab_dist = (e.gizmo_grab_point - origin).dot(axis);
}
} else {
// planar handles: grab on their plane
GizmoPlaneMap m = gizmo_plane_map(handle);
e.gizmo_plane_n = f.axes[m.n_axis];
e.gizmo_grab_point = ray_plane_point(origin, e.gizmo_plane_n, f.ray.origin, f.ray.dir, origin);
e.gizmo_grab_dist = (e.gizmo_grab_point - origin).length();
}
}
inline void gizmo_rebase_drag(Editor &e, Scene &scene, GizmoFrame &f) {
SceneGraph &sg = *f.sg;
bool alive = e.selected_node >= 0 && (u32)e.selected_node < sg.node_count && sg.is_node_alive((u32)e.selected_node);
if (!alive || !debug_finite_vec3(f.ray.dir) || !debug_finite_vec3(f.ray.origin)) {
e.gizmo_dragging = false;
e.gizmo_grab_node = -1;
return;
}
u32 node = (u32)e.selected_node;
const Camera &cam = *f.cam;
GizmoFrameData d;
if (!gizmo_resolve_frame(e, scene, node, cam, d)) {
e.gizmo_dragging = false;
e.gizmo_grab_node = -1;
return;
}
gizmo_apply_frame_data(f, node, d);
e.gizmo_grab_node = e.selected_node;
e.gizmo_base_local = sg.translations[node];
e.gizmo_base_world = d.origin;
e.gizmo_base_world_mat = d.base_mat;
e.gizmo_base_local_mat = sg.locals[node];
e.gizmo_base_scale = sg.scales[node];
gizmo_derive_grab_refs(e, f, cam, d.origin, e.gizmo_drag_axis);
}
inline void gizmo_write_translation(SceneGraph &sg, u32 node, vec3 base_local, vec3 base_world, vec3 new_world) {
int p = sg.parent[node];
mat4 parent_world = (p >= 0) ? sg.worlds[p] : identity();
vec3 local_delta = transform_direction(new_world - base_world, inverse(parent_world));
vec3 new_local = base_local + local_delta;
if (debug_finite_vec3(new_local)) {
sg.set_translation(node, new_local);
}
}
// world-space delta rotation about the node origin.
inline void gizmo_write_rotation(SceneGraph &sg, u32 node, const mat4 &base_world, vec3 axis_world, f32 delta) {
TRS base_trs = mat4_to_trs(base_world);
quat dq = quat::from_axis_angle(axis_world, delta); // normalizes axis internally
// quat qn = (dq * quat(base_trs.rotation)).normalized();
quat qn = (quat(base_trs.rotation) * dq).normalized();
TRS new_world_trs{base_trs.translation, qn, base_trs.scale};
mat4 new_world = trs_to_mat4(new_world_trs);
int p = sg.parent[node];
mat4 parent_world = (p >= 0) ? sg.worlds[p] : identity();
mat4 new_local = new_world * inverse(parent_world);
if (debug_finite_vec3(vec3(new_local.m[12], new_local.m[13], new_local.m[14]))) {
sg.set_local_matrix(node, new_local);
}
}
inline void gizmo_write_scale_local(SceneGraph &sg, u32 node, vec3 base_scale, u8 handle, f32 factor) {
if (!(factor > 0.0f) || !std::isfinite(factor)) {
return;
}
vec3 mask = gizmo_scale_mask(handle);
if (mask.length_squared() <= 0.0f) {
return;
}
vec3 s = {
mask.x > 0.5f ? base_scale.x * factor : base_scale.x,
mask.y > 0.5f ? base_scale.y * factor : base_scale.y,
mask.z > 0.5f ? base_scale.z * factor : base_scale.z
};
auto clamp_axis = [](f32 v) {
f32 a = fabsf(v);
if (!(a >= 1e-4f)) {
a = 1e-4f;
}
return (v < 0.0f ? -1.0f : 1.0f) * a;
};
s.x = clamp_axis(s.x);
s.y = clamp_axis(s.y);
s.z = clamp_axis(s.z);
if (debug_finite_vec3(s)) {
sg.set_scale(node, s);
}
}
// world-space scale about the node origin (handles rotated nodes).
inline void gizmo_write_scale_world(SceneGraph &sg, u32 node, const mat4 &base_world, u8 handle, f32 factor) {
if (!(factor > 0.0f) || !std::isfinite(factor)) {
return;
}
vec3 mask = gizmo_scale_mask(handle);
if (mask.length_squared() <= 0.0f) {
return;
}
vec3 f = vec3(1.0f) + mask * (factor - 1.0f);
mat4 s = scale(f);
mat4 new_world = base_world * s;
int p = sg.parent[node];
mat4 parent_world = (p >= 0) ? sg.worlds[p] : identity();
mat4 new_local = new_world * inverse(parent_world);
TRS t = mat4_to_trs(new_local);
if (t.scale.x < 1e-4f) {
t.scale.x = 1e-4f;
}
if (t.scale.y < 1e-4f) {
t.scale.y = 1e-4f;
}
if (t.scale.z < 1e-4f) {
t.scale.z = 1e-4f;
}
if (debug_finite_vec3(t.scale) && debug_finite_vec3(t.translation)) {
sg.set_local_matrix(node, trs_to_mat4(t));
}
}
inline bool gizmo_hit_center(ImVec2 mouse, ImVec2 o_px) {
f32 dx = mouse.x - o_px.x;
f32 dy = mouse.y - o_px.y;
return sqrtf(dx * dx + dy * dy) < GIZMO_CENTER_HIT_PX;
}
inline int gizmo_hit_axis(ImVec2 mouse, ImVec2 o_px, const GizmoFrame &f, const Editor &e) {
f32 best = GIZMO_AXIS_HIT_PX;
int hit = -1;
for (u8 a = 0; a < 3; ++a) {
ImVec2 t_px{};
if (!project_to_view(
f.origin + f.axes[a] * f.len, f.cam->view_proj, e.gizmo_view_min, e.gizmo_view_size, t_px
)) {
continue;
}
f32 d = dist_point_seg(mouse, o_px, t_px);
if (d < best) {
best = d;
hit = (int)a;
}
// fatten tip grab
f32 dx = mouse.x - t_px.x;
f32 dy = mouse.y - t_px.y;
if (sqrtf(dx * dx + dy * dy) < GIZMO_CENTER_HIT_PX && d < GIZMO_AXIS_HIT_PX + 4.0f) {
hit = (int)a;
best = d;
}
}
return hit;
}
inline bool gizmo_quad_contains(ImVec2 p, ImVec2 a, ImVec2 b, ImVec2 c, ImVec2 d) {
// convex quad a->b->c->d: same sign on all edge crosses
ImVec2 pts[4] = {a, b, c, d};
f32 sign = 0.0f;
for (int i = 0; i < 4; ++i) {
ImVec2 p0 = pts[i];
ImVec2 p1 = pts[(i + 1) & 3];
f32 cross = (p1.x - p0.x) * (p.y - p0.y) - (p1.y - p0.y) * (p.x - p0.x);
if (fabsf(cross) < 1e-6f) {
continue;
}
if (sign == 0.0f) {
sign = cross;
} else if ((sign > 0.0f) != (cross > 0.0f)) {
return false;
}
}
return sign != 0.0f;
}
// planar quad handle hit (move/scale): quad(origin, origin+a*q, origin+a*q+b*q, origin+b*q).
inline int gizmo_hit_planar(ImVec2 mouse, const GizmoFrame &f, const Editor &e) {
f32 q = f.len * GIZMO_PLANE_FRAC;
const u8 ids[3] = {GIZMO_PLANE_YZ, GIZMO_PLANE_XZ, GIZMO_PLANE_XY};
for (u8 id : ids) {
GizmoPlaneMap m = gizmo_plane_map(id);
ImVec2 p0 = f.o_px, p1{}, p2{}, p3{};
if (!project_to_view(f.origin + f.axes[m.ia] * q, f.cam->view_proj, e.gizmo_view_min, e.gizmo_view_size, p1)) {
continue;
}
if (!project_to_view(
f.origin + (f.axes[m.ia] + f.axes[m.ib]) * q, f.cam->view_proj, e.gizmo_view_min, e.gizmo_view_size, p2
)) {
continue;
}
if (!project_to_view(f.origin + f.axes[m.ib] * q, f.cam->view_proj, e.gizmo_view_min, e.gizmo_view_size, p3)) {
continue;
}
// reject edge-on (tiny area)
f32 area = fabsf((p1.x - p0.x) * (p3.y - p0.y) - (p1.y - p0.y) * (p3.x - p0.x));
if (area < 80.0f) {
continue;
}
if (gizmo_quad_contains(mouse, p0, p1, p2, p3)) {
return id;
}
}
return -1;
}
inline f32 gizmo_ring_screen_dist(ImVec2 mouse, const GizmoFrame &f, const Editor &e, vec3 axis, f32 radius) {
vec3 u, v;
gizmo_ring_basis(axis.normalized(), u, v);
ImVec2 prev{};
bool has_prev = false;
f32 best = 1e30f;
for (u32 i = 0; i <= GIZMO_RING_SEGS; ++i) {
f32 t = (f32)i / (f32)GIZMO_RING_SEGS * 2.0f * PI;
vec3 p = f.origin + (u * cosf(t) + v * sinf(t)) * radius;
ImVec2 px{};
if (!project_to_view(p, f.cam->view_proj, e.gizmo_view_min, e.gizmo_view_size, px)) {
has_prev = false;
continue;
}
if (has_prev) {
f32 d = dist_point_seg(mouse, prev, px);
if (d < best) {
best = d;
}
}
prev = px;
has_prev = true;
}
return best;
}
inline int gizmo_hit_rotate(ImVec2 mouse, ImVec2 o_px, const GizmoFrame &f, const Editor &e) {
vec3 view_dir = f.cam->forward().normalized();
f32 dx = mouse.x - o_px.x;
f32 dy = mouse.y - o_px.y;
f32 center_d = sqrtf(dx * dx + dy * dy);
// dead-zone around the origin: edge-on rings collapse to lines through the
// center, so clicks here almost never mean a ring grab.
if (center_d < GIZMO_RING_DEADZONE_PX) {
return -1;
}
// outer view ring first (topmost in Blender)
f32 outer_r = f.len * GIZMO_OUTER_RING_SCALE;
f32 outer_px = GIZMO_HANDLE_LEN_PX * GIZMO_OUTER_RING_SCALE;
if (fabsf(center_d - outer_px) < GIZMO_OUTER_HIT_PX) {
f32 d = gizmo_ring_screen_dist(mouse, f, e, view_dir, outer_r);
if (d < GIZMO_OUTER_HIT_PX) {
return GIZMO_VIEW_AXIS;
}
}
int hit = -1;
f32 best = 1e30f;
for (u8 a = 0; a < 3; ++a) {
// face-on rings read as circles (easier target), edge-on as thin lines.
f32 facing = fabsf(f.axes[a].normalized().dot(view_dir));
f32 tol = facing > 0.5f ? GIZMO_RING_HIT_PX : GIZMO_RING_HIT_EDGE_PX;
f32 d = gizmo_ring_screen_dist(mouse, f, e, f.axes[a], f.len);
if (d < tol && d < best) {
best = d;
hit = (int)a;
}
}
return hit;
}
inline void gizmo_draw_circle(DebugDraw &draw, vec3 origin, vec3 u, vec3 v, f32 radius, vec4 col, f32 w) {
vec3 prev = origin + u * radius;
for (u32 i = 1; i <= GIZMO_RING_SEGS; ++i) {
f32 t = (f32)i / (f32)GIZMO_RING_SEGS * 2.0f * PI;
vec3 p = origin + (u * cosf(t) + v * sinf(t)) * radius;
draw.line(prev, p, col, DEBUG_CAT_GIZMO, DEBUG_DEPTH_XRAY, w);
prev = p;
}
}
inline void gizmo_draw_center_ring(DebugDraw &draw, const GizmoFrame &f, const Editor &e, bool active) {
vec4 col = active ? vec4(5.0f, 5.0f, 5.0f, 1.0f) : vec4(3.0f, 3.0f, 3.0f, 1.0f);
f32 w = active ? 4.0f : 3.0f;
f32 r = DebugDraw::pixels_to_world(f.dist, GIZMO_CENTER_RADIUS_PX, f.cam->fov_y_deg, e.gizmo_view_size.y);
if (!(r > 0.0f) || !std::isfinite(r)) {
return;
}
gizmo_draw_circle(draw, f.origin, f.cam->right(), f.cam->up(), r, col, w);
}
inline void gizmo_draw_ring(DebugDraw &draw, const GizmoFrame &f, u8 axis_idx, vec3 axis, bool active) {
vec4 col = active ? vec4(5.0f, 5.0f, 5.0f, 1.0f) : gizmo_axis_color(axis_idx);
f32 w = active ? 4.0f : 3.0f;
vec3 u, v;
gizmo_ring_basis(axis.normalized(), u, v);
gizmo_draw_circle(draw, f.origin, u, v, f.len, col, w);
}
inline void gizmo_draw_view_ring(DebugDraw &draw, const GizmoFrame &f, const Camera &cam, bool active) {
vec4 col = active ? vec4(5.0f, 5.0f, 5.0f, 1.0f) : vec4(3.0f, 3.0f, 3.0f, 1.0f);
f32 w = active ? 4.0f : 3.0f;
vec3 n = cam.forward().normalized();
vec3 u, v;
gizmo_ring_basis(n, u, v);
gizmo_draw_circle(draw, f.origin, u, v, f.len * GIZMO_OUTER_RING_SCALE, col, w);
}
inline void gizmo_draw_planar_quad(DebugDraw &draw, const GizmoFrame &f, u8 plane_id, bool active) {
GizmoPlaneMap m = gizmo_plane_map(plane_id);
vec4 col = gizmo_axis_color(m.n_axis);
col.w = 1.0f;
if (active) {
col = vec4(5.0f, 5.0f, 5.0f, 1.0f);
} else {
col.x *= 0.55f;
col.y *= 0.55f;
col.z *= 0.55f;
}
f32 w = active ? 4.0f : 2.5f;
f32 q = f.len * GIZMO_PLANE_FRAC;
vec3 p0 = f.origin;
vec3 p1 = f.origin + f.axes[m.ia] * q;
vec3 p2 = f.origin + (f.axes[m.ia] + f.axes[m.ib]) * q;
vec3 p3 = f.origin + f.axes[m.ib] * q;
draw.line(p0, p1, col, DEBUG_CAT_GIZMO, DEBUG_DEPTH_XRAY, w);
draw.line(p1, p2, col, DEBUG_CAT_GIZMO, DEBUG_DEPTH_XRAY, w);
draw.line(p2, p3, col, DEBUG_CAT_GIZMO, DEBUG_DEPTH_XRAY, w);
draw.line(p3, p0, col, DEBUG_CAT_GIZMO, DEBUG_DEPTH_XRAY, w);
}
inline void gizmo_draw_scale_cube(DebugDraw &draw, vec3 center, f32 size, vec4 col, f32 w) {
mat4 world = scale(vec3(size)) * translate(center);
draw.box_mat(world, col, DEBUG_CAT_GIZMO, DEBUG_DEPTH_XRAY, w);
}
// shared hover path for move/scale: center -> axis -> planar
inline int gizmo_hover_move_scale(const GizmoFrame &f, const Editor &e) {
if (gizmo_hit_center(f.mouse, f.o_px)) {
return GIZMO_FREE_AXIS;
}
int hit = gizmo_hit_axis(f.mouse, f.o_px, f, e);
return hit >= 0 ? hit : gizmo_hit_planar(f.mouse, f, e);
}
// shared axis-line draw for move/scale; tip_fn draws the per-tool tip marker
template <typename TipFn>
inline void gizmo_draw_axes(DebugDraw &draw, const GizmoFrame &f, const Editor &e, int hover_axis, TipFn tip_fn) {
for (u8 a = 0; a < 3; ++a) {
bool active = (e.gizmo_dragging && a == e.gizmo_drag_axis) || (!e.gizmo_dragging && (int)a == hover_axis);
vec4 col = active ? vec4(5.0f, 5.0f, 5.0f, 1.0f) : gizmo_axis_color(a);
f32 w = active ? 4.0f : 3.0f;
vec3 tip = f.origin + f.axes[a] * f.len;
draw.line(f.origin, tip, col, DEBUG_CAT_GIZMO, DEBUG_DEPTH_XRAY, w);
tip_fn(draw, tip, f.len, col, w);
}
}
inline void gizmo_draw_planars(DebugDraw &draw, const GizmoFrame &f, const Editor &e, int hover_axis) {
for (u8 p = GIZMO_PLANE_YZ; p <= GIZMO_PLANE_XY; ++p) {
bool active = (e.gizmo_dragging && p == e.gizmo_drag_axis) || (!e.gizmo_dragging && (int)p == hover_axis);
gizmo_draw_planar_quad(draw, f, p, active);
}
}
inline bool gizmo_center_active(const Editor &e, int hover_axis) {
return (e.gizmo_dragging && e.gizmo_drag_axis == GIZMO_FREE_AXIS) ||
(!e.gizmo_dragging && hover_axis == GIZMO_FREE_AXIS);
}
bool gizmo_rebase_if_moved(Editor &e, Scene &scene, GizmoFrame &f, int tool) {
if (e.gizmo_dragging &&
(e.selected_node != e.gizmo_grab_node || e.gizmo_tool_grab != tool || e.gizmo_world_grab != e.gizmo_world)) {
gizmo_rebase_drag(e, scene, f);
e.gizmo_tool_grab = tool;
e.gizmo_world_grab = e.gizmo_world;
return true;
}
return false;
}
void gizmo_end_drag(Editor &e) {
e.gizmo_dragging = false;
e.gizmo_grab_node = -1;
e.gizmo_tool_grab = -1;
}
bool gizmo_can_hover(const GizmoFrame &f, const Editor &e) {
return f.o_visible && f.hover_view && !e.gizmo_dragging;
}
bool gizmo_can_grab(const GizmoFrame &f, int hover_axis) {
return f.hover_view && hover_axis >= 0 && debug_finite_vec3(f.ray.dir) && debug_finite_vec3(f.ray.origin);
}
void update_move_gizmo(Editor &e, Scene &scene, DebugDraw &draw) {
GizmoFrame f;
if (!gizmo_begin_frame(e, scene, f)) {
return;
}
SceneGraph &sg = *f.sg;
const Camera &cam = *f.cam;
int hover_axis = gizmo_can_hover(f, e) ? gizmo_hover_move_scale(f, e) : -1;
e.gizmo_hover_axis = hover_axis;
gizmo_rebase_if_moved(e, scene, f, GIZMO_TOOL_MOVE);
bool mouse_down = ImGui::IsMouseDown(0);
bool clicked = ImGui::IsMouseClicked(0);
if (!e.gizmo_dragging) {
if (clicked && gizmo_can_grab(f, hover_axis)) {
gizmo_capture_common(e, sg, f.node, f.origin, f.base_mat, (u8)hover_axis);
e.gizmo_tool_grab = GIZMO_TOOL_MOVE;
e.gizmo_world_grab = e.gizmo_world;
if (e.gizmo_drag_axis == GIZMO_FREE_AXIS) {
e.gizmo_plane_n = cam.forward().normalized();
}
gizmo_derive_grab_refs(e, f, cam, f.origin, e.gizmo_drag_axis);
}
} else if (!mouse_down) {
gizmo_end_drag(e);
} else {
vec3 cur = e.gizmo_grab_point;
if (e.gizmo_drag_axis == GIZMO_FREE_AXIS) {
cur = ray_plane_point(e.gizmo_base_world, e.gizmo_plane_n, f.ray.origin, f.ray.dir, cur);
} else if (e.gizmo_drag_axis <= GIZMO_AXIS_Z) {
// axis may have rotated since grab
// reconstructed from current frame axes (rebase keeps them fresh)
vec3 axis = f.axes[e.gizmo_drag_axis];
cur = closest_point_on_axis(e.gizmo_base_world, axis, f.ray.origin, f.ray.dir);
} else {
cur = ray_plane_point(e.gizmo_base_world, e.gizmo_plane_n, f.ray.origin, f.ray.dir, cur);
}
vec3 new_world = e.gizmo_base_world + (cur - e.gizmo_grab_point);
if (e.gizmo_snap && e.gizmo_snap_step > 0.0f) {
new_world.x = roundf(new_world.x / e.gizmo_snap_step) * e.gizmo_snap_step;
new_world.y = roundf(new_world.y / e.gizmo_snap_step) * e.gizmo_snap_step;
new_world.z = roundf(new_world.z / e.gizmo_snap_step) * e.gizmo_snap_step;
}
gizmo_write_translation(sg, f.node, e.gizmo_base_local, e.gizmo_base_world, new_world);
}
if (!f.o_visible) {
return;
}
gizmo_draw_axes(draw, f, e, hover_axis, [](DebugDraw &d, vec3 tip, f32 len, vec4 col, f32 w) {
d.cross(tip, len * 0.14f, col, DEBUG_CAT_GIZMO, DEBUG_DEPTH_XRAY, w);
});
gizmo_draw_planars(draw, f, e, hover_axis);
gizmo_draw_center_ring(draw, f, e, gizmo_center_active(e, hover_axis));
}
void update_rotate_gizmo(Editor &e, Scene &scene, DebugDraw &draw) {
GizmoFrame f;
if (!gizmo_begin_frame(e, scene, f)) {
return;
}
SceneGraph &sg = *f.sg;
const Camera &cam = *f.cam;
int hover_axis = gizmo_can_hover(f, e) ? gizmo_hit_rotate(f.mouse, f.o_px, f, e) : -1;
e.gizmo_hover_axis = hover_axis;
gizmo_rebase_if_moved(e, scene, f, GIZMO_TOOL_ROTATE);
bool mouse_down = ImGui::IsMouseDown(0);
bool clicked = ImGui::IsMouseClicked(0);
if (!e.gizmo_dragging) {
if (clicked && gizmo_can_grab(f, hover_axis)) {
gizmo_capture_common(e, sg, f.node, f.origin, f.base_mat, (u8)hover_axis);
e.gizmo_tool_grab = GIZMO_TOOL_ROTATE;
e.gizmo_world_grab = e.gizmo_world;
gizmo_derive_grab_refs(e, f, cam, f.origin, (u8)hover_axis);
}
} else if (!mouse_down) {
gizmo_end_drag(e);
} else {
vec3 p = ray_plane_point(e.gizmo_base_world, e.gizmo_plane_n, f.ray.origin, f.ray.dir, e.gizmo_grab_point);
f32 cur_ang = gizmo_angle_on_plane(p, e.gizmo_base_world, e.gizmo_drag_u, e.gizmo_drag_v);
f32 delta = gizmo_wrap_pi(cur_ang - e.gizmo_grab_angle);
if (e.gizmo_snap) {
f32 step = e.gizmo_snap_angle > 0.0f ? e.gizmo_snap_angle : 15.0f;
f32 deg = degrees(delta);
deg = roundf(deg / step) * step;
delta = radians(deg);
}
if (std::isfinite(delta) && fabsf(delta) > 1e-7f) {
gizmo_write_rotation(sg, f.node, e.gizmo_base_world_mat, e.gizmo_plane_n, delta);
}
}
if (!f.o_visible) {
return;
}
for (u8 a = 0; a < 3; ++a) {
bool active = (e.gizmo_dragging && a == e.gizmo_drag_axis) || (!e.gizmo_dragging && (int)a == hover_axis);
gizmo_draw_ring(draw, f, a, f.axes[a], active);
}
bool view_active = (e.gizmo_dragging && e.gizmo_drag_axis == GIZMO_VIEW_AXIS) ||
(!e.gizmo_dragging && hover_axis == GIZMO_VIEW_AXIS);
gizmo_draw_view_ring(draw, f, cam, view_active);
}
void update_scale_gizmo(Editor &e, Scene &scene, DebugDraw &draw) {
GizmoFrame f;
if (!gizmo_begin_frame(e, scene, f)) {
return;
}
SceneGraph &sg = *f.sg;
const Camera &cam = *f.cam;
int hover_axis = gizmo_can_hover(f, e) ? gizmo_hover_move_scale(f, e) : -1;
e.gizmo_hover_axis = hover_axis;
gizmo_rebase_if_moved(e, scene, f, GIZMO_TOOL_SCALE);
bool mouse_down = ImGui::IsMouseDown(0);
bool clicked = ImGui::IsMouseClicked(0);
if (!e.gizmo_dragging) {
if (clicked && gizmo_can_grab(f, hover_axis)) {
u8 handle = (u8)hover_axis;
gizmo_capture_common(e, sg, f.node, f.origin, f.base_mat, handle);
e.gizmo_tool_grab = GIZMO_TOOL_SCALE;
e.gizmo_world_grab = e.gizmo_world;
if (handle == GIZMO_FREE_AXIS) {
e.gizmo_plane_n = cam.forward().normalized();
gizmo_derive_grab_refs(e, f, cam, f.origin, handle);
e.gizmo_grab_dist = (e.gizmo_grab_point - f.origin).length();
} else {
gizmo_derive_grab_refs(e, f, cam, f.origin, handle);
}
if (!(e.gizmo_grab_dist > 1e-6f) || !std::isfinite(e.gizmo_grab_dist)) {
e.gizmo_grab_dist = f.len * 0.5f;
if (!(e.gizmo_grab_dist > 1e-6f)) {
e.gizmo_grab_dist = 1.0f;
}
}
}
} else if (!mouse_down) {
gizmo_end_drag(e);
} else {
f32 factor = 1.0f;
if (e.gizmo_drag_axis <= GIZMO_AXIS_Z) {
vec3 axis = f.axes[e.gizmo_drag_axis];
f32 cur_dist = axis_signed_distance(e.gizmo_base_world, axis, f.ray.origin, f.ray.dir);
if (fabsf(e.gizmo_grab_dist) > 1e-6f && std::isfinite(cur_dist)) {
factor = cur_dist / e.gizmo_grab_dist;
}
} else {
vec3 n = e.gizmo_plane_n;
vec3 p = ray_plane_point(e.gizmo_base_world, n, f.ray.origin, f.ray.dir, e.gizmo_grab_point);
f32 r = (p - e.gizmo_base_world).length();
if (e.gizmo_grab_dist > 1e-6f && std::isfinite(r)) {
factor = r / e.gizmo_grab_dist;
}
}
if (e.gizmo_snap && e.gizmo_snap_scale > 0.0f) {
factor = roundf(factor / e.gizmo_snap_scale) * e.gizmo_snap_scale;
}
if (factor < 1e-4f) {
factor = 1e-4f;
}
if (std::isfinite(factor)) {
if (e.gizmo_world) {
gizmo_write_scale_world(sg, f.node, e.gizmo_base_world_mat, e.gizmo_drag_axis, factor);
} else {
gizmo_write_scale_local(sg, f.node, e.gizmo_base_scale, e.gizmo_drag_axis, factor);
}
}
}
if (!f.o_visible) {
return;
}
gizmo_draw_axes(draw, f, e, hover_axis, [](DebugDraw &d, vec3 tip, f32 len, vec4 col, f32 w) {
gizmo_draw_scale_cube(d, tip, len * 0.11f, col, w);
});
gizmo_draw_planars(draw, f, e, hover_axis);
bool center_active = gizmo_center_active(e, hover_axis);
vec4 ccol = center_active ? vec4(5.0f, 5.0f, 5.0f, 1.0f) : vec4(3.0f, 3.0f, 3.0f, 1.0f);
gizmo_draw_scale_cube(draw, f.origin, f.len * 0.12f, ccol, center_active ? 4.0f : 3.0f);
}
void update_gizmo(Editor &e, Scene &scene, DebugDraw &draw) {
// goto tool that owns an in-flight drag, or just the curr tool
int tool = (int)e.gizmo_tool;
if (e.gizmo_dragging && e.gizmo_tool_grab >= 0) {
tool = e.gizmo_tool_grab;
}
switch (tool) {
case GIZMO_TOOL_ROTATE:
update_rotate_gizmo(e, scene, draw);
break;
case GIZMO_TOOL_SCALE:
update_scale_gizmo(e, scene, draw);
break;
case GIZMO_TOOL_MOVE:
update_move_gizmo(e, scene, draw);
break;
default:
assert(false && "invalid gizmo tool");
update_move_gizmo(e, scene, draw);
break;
}
}
} // namespace editor
#endif