Added axis movement.

This commit is contained in:
Justin Marshall
2026-05-17 23:14:29 -07:00
parent 76f642286e
commit 01b8ad9f7e
+601
View File
@@ -52,6 +52,12 @@ extern brush_t* Brush_CreateFaceExtrusion(brush_t* sourceBrush, face_t* sourceFa
extern void Select_ShiftTexture(float x, float y);
extern void Select_ScaleTexture(float x, float y);
extern void Select_RotateTexture(float amt, bool absolute);
extern void Select_Move(idVec3 delta, bool bSnap);
extern void Select_Ray(idVec3 origin, idVec3 dir, int flags);
extern void Select_Deselect(bool bDeselectFaces);
extern void Select_RotateAxis(int axis, float deg, bool bPaint, bool bMouse);
extern void Select_InitializeRotation();
extern void Select_FinalizeRotation();
/*
========================
@@ -1931,6 +1937,582 @@ static void CamWnd_FaceExtrudeDrawPreview(CCamWnd* cam) {
/*
========================
Camera selection axis gizmo
A selected object now gets a small world-axis handle in the camera view.
Hover an axis and left-drag to translate along that axis. Holding Shift while
starting the drag converts the same handle into yaw-only rotation around the Z
axis; no pitch/roll rotation handles are exposed here.
========================
*/
#define CAMWND_GIZMO_AXIS_NONE -1
#define CAMWND_GIZMO_AXIS_X 0
#define CAMWND_GIZMO_AXIS_Y 1
#define CAMWND_GIZMO_AXIS_Z 2
#define CAMWND_GIZMO_HIT_PIXELS 9.0f
#define CAMWND_GIZMO_DRAW_PIXELS 96.0f
#define CAMWND_GIZMO_ROTATE_AXIS 2
struct camWndGizmoState_t {
bool active;
bool rotating;
int hoverAxis;
int activeAxis;
CCamWnd* cam;
CPoint startPoint;
CPoint lastPoint;
idVec3 startCenter;
idVec3 axisDir;
float axisLength;
float startScalar;
float lastScalar;
float rotationAmount;
};
static camWndGizmoState_t s_camWndGizmoState;
static bool CamWnd_GizmoSelectionBounds(idVec3& mins, idVec3& maxs) {
if (selected_brushes.next == NULL || selected_brushes.next == &selected_brushes) {
return false;
}
mins[0] = mins[1] = mins[2] = 999999.0f;
maxs[0] = maxs[1] = maxs[2] = -999999.0f;
bool haveBounds = false;
for (brush_t* brush = selected_brushes.next; brush != &selected_brushes; brush = brush->next) {
if (brush == NULL) {
continue;
}
for (int i = 0; i < 3; i++) {
if (brush->mins[i] < mins[i]) {
mins[i] = brush->mins[i];
}
if (brush->maxs[i] > maxs[i]) {
maxs[i] = brush->maxs[i];
}
}
haveBounds = true;
}
return haveBounds;
}
static bool CamWnd_GizmoSelectionCenter(idVec3& center, idVec3* outMins = NULL, idVec3* outMaxs = NULL) {
idVec3 mins;
idVec3 maxs;
if (!CamWnd_GizmoSelectionBounds(mins, maxs)) {
return false;
}
center[0] = (mins[0] + maxs[0]) * 0.5f;
center[1] = (mins[1] + maxs[1]) * 0.5f;
center[2] = (mins[2] + maxs[2]) * 0.5f;
center += g_qeglobals.d_select_translate;
if (outMins) {
*outMins = mins;
}
if (outMaxs) {
*outMaxs = maxs;
}
return true;
}
static idVec3 CamWnd_GizmoAxisVector(int axis) {
idVec3 v;
v[0] = v[1] = v[2] = 0.0f;
if (axis >= 0 && axis < 3) {
v[axis] = 1.0f;
}
return v;
}
static float CamWnd_GizmoGridSize() {
float gridSize = (float)g_qeglobals.d_gridsize;
if (gridSize <= 0.0f) {
gridSize = 1.0f;
}
return gridSize;
}
static float CamWnd_GizmoSnapScalar(float scalar) {
if (g_PrefsDlg.m_bNoClamp) {
return scalar;
}
const float gridSize = CamWnd_GizmoGridSize();
return floor(scalar / gridSize + 0.5f) * gridSize;
}
static float CamWnd_GizmoWorldPerPixel(CCamWnd* cam, const idVec3& center) {
if (cam == NULL) {
return 1.0f;
}
camera_t& camera = cam->Camera();
idVec3 toCenter = center - camera.origin;
float depth = DotProduct(toCenter, camera.vpn);
if (depth < 8.0f) {
depth = toCenter.Length();
if (depth < 8.0f) {
depth = 8.0f;
}
}
float halfWidth = (float)camera.width * 0.5f;
if (halfWidth < 1.0f) {
halfWidth = 1.0f;
}
return depth / halfWidth;
}
static float CamWnd_GizmoAxisLength(CCamWnd* cam, const idVec3& center, const idVec3& mins, const idVec3& maxs) {
idVec3 size = maxs - mins;
float selectionSize = size.Length() * 0.55f;
float cameraSize = CamWnd_GizmoWorldPerPixel(cam, center) * CAMWND_GIZMO_DRAW_PIXELS;
float length = cameraSize;
if (length < selectionSize) {
length = selectionSize;
}
if (length < 32.0f) {
length = 32.0f;
}
return length;
}
static bool CamWnd_GizmoProjectPoint(CCamWnd* cam, const idVec3& point, CPoint& out) {
if (cam == NULL) {
return false;
}
camera_t& camera = cam->Camera();
idVec3 local = point - camera.origin;
float depth = DotProduct(local, camera.vpn);
if (depth <= 1.0f) {
return false;
}
float halfWidth = (float)camera.width * 0.5f;
if (halfWidth <= 0.0f) {
return false;
}
float r = DotProduct(local, camera.vright) / depth;
float u = DotProduct(local, camera.vup) / depth;
float sx = (float)camera.width * 0.5f + r * halfWidth;
float syBottom = (float)camera.height * 0.5f + u * halfWidth;
CRect rect;
cam->GetClientRect(rect);
out.x = (int)(sx + 0.5f);
out.y = rect.bottom - 1 - (int)(syBottom + 0.5f);
return true;
}
static float CamWnd_GizmoDistanceToSegment(const CPoint& point, const CPoint& a, const CPoint& b, float& tOut) {
float px = (float)point.x;
float py = (float)point.y;
float ax = (float)a.x;
float ay = (float)a.y;
float bx = (float)b.x;
float by = (float)b.y;
float dx = bx - ax;
float dy = by - ay;
float lenSqr = dx * dx + dy * dy;
if (lenSqr <= 0.0001f) {
tOut = 0.0f;
float ex = px - ax;
float ey = py - ay;
return sqrt(ex * ex + ey * ey);
}
float t = ((px - ax) * dx + (py - ay) * dy) / lenSqr;
if (t < 0.0f) {
t = 0.0f;
}
else if (t > 1.0f) {
t = 1.0f;
}
tOut = t;
float cx = ax + dx * t;
float cy = ay + dy * t;
float ex = px - cx;
float ey = py - cy;
return sqrt(ex * ex + ey * ey);
}
static int CamWnd_GizmoHitTest(CCamWnd* cam, const CPoint& point, float* bestDistance = NULL) {
idVec3 center;
idVec3 mins;
idVec3 maxs;
if (!CamWnd_GizmoSelectionCenter(center, &mins, &maxs)) {
if (bestDistance) {
*bestDistance = -1.0f;
}
return CAMWND_GIZMO_AXIS_NONE;
}
const float axisLength = CamWnd_GizmoAxisLength(cam, center, mins, maxs);
float best = CAMWND_GIZMO_HIT_PIXELS;
int bestAxis = CAMWND_GIZMO_AXIS_NONE;
CPoint start;
if (!CamWnd_GizmoProjectPoint(cam, center, start)) {
if (bestDistance) {
*bestDistance = -1.0f;
}
return CAMWND_GIZMO_AXIS_NONE;
}
for (int axis = 0; axis < 3; axis++) {
idVec3 axisDir = CamWnd_GizmoAxisVector(axis);
CPoint end;
if (!CamWnd_GizmoProjectPoint(cam, center + axisDir * axisLength, end)) {
continue;
}
float segmentT = 0.0f;
float dist = CamWnd_GizmoDistanceToSegment(point, start, end, segmentT);
if (dist <= best && segmentT >= 0.08f) {
best = dist;
bestAxis = axis;
}
}
if (bestDistance) {
*bestDistance = (bestAxis == CAMWND_GIZMO_AXIS_NONE) ? -1.0f : best;
}
return bestAxis;
}
static bool CamWnd_GizmoAxisScalarFromRay(CCamWnd* cam, const CPoint& point, const idVec3& lineOrigin, const idVec3& lineDir, float& scalar) {
idVec3 rayDir;
CamWnd_FaceExtrudeBuildRay(cam, point, rayDir);
const idVec3 rayOrigin = cam->Camera().origin;
const float b = DotProduct(rayDir, lineDir);
const float denom = 1.0f - b * b;
if (denom < 0.0001f) {
return false;
}
idVec3 w = rayOrigin - lineOrigin;
const float d = DotProduct(rayDir, w);
const float e = DotProduct(lineDir, w);
const float t = (e - b * d) / denom;
const float rayT = b * t - d;
if (rayT < 0.0f) {
return false;
}
scalar = t;
return true;
}
static bool CamWnd_GizmoScreenDeltaScalar(CCamWnd* cam, const CPoint& point, float& scalar) {
CPoint start;
CPoint end;
if (!CamWnd_GizmoProjectPoint(cam, s_camWndGizmoState.startCenter, start) ||
!CamWnd_GizmoProjectPoint(cam, s_camWndGizmoState.startCenter + s_camWndGizmoState.axisDir * s_camWndGizmoState.axisLength, end)) {
return false;
}
float dx = (float)(end.x - start.x);
float dy = (float)(end.y - start.y);
float len = sqrt(dx * dx + dy * dy);
if (len < 0.001f) {
return false;
}
dx /= len;
dy /= len;
float mouseDx = (float)(point.x - s_camWndGizmoState.startPoint.x);
float mouseDy = (float)(point.y - s_camWndGizmoState.startPoint.y);
scalar = (mouseDx * dx + mouseDy * dy) * (s_camWndGizmoState.axisLength / len);
return true;
}
static bool CamWnd_GizmoBegin(CCamWnd* cam, const CPoint& point, UINT nFlags) {
if (cam == NULL || !cam->GetSafeHwnd() || (nFlags & MK_CONTROL)) {
return false;
}
const int axis = CamWnd_GizmoHitTest(cam, point);
if (axis == CAMWND_GIZMO_AXIS_NONE) {
return false;
}
idVec3 center;
idVec3 mins;
idVec3 maxs;
if (!CamWnd_GizmoSelectionCenter(center, &mins, &maxs)) {
return false;
}
memset(&s_camWndGizmoState, 0, sizeof(s_camWndGizmoState));
s_camWndGizmoState.active = true;
s_camWndGizmoState.rotating = ((nFlags & MK_SHIFT) != 0) || ((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0);
s_camWndGizmoState.hoverAxis = axis;
s_camWndGizmoState.activeAxis = s_camWndGizmoState.rotating ? CAMWND_GIZMO_AXIS_Z : axis;
s_camWndGizmoState.cam = cam;
s_camWndGizmoState.startPoint = point;
s_camWndGizmoState.lastPoint = point;
s_camWndGizmoState.startCenter = center;
s_camWndGizmoState.axisDir = CamWnd_GizmoAxisVector(s_camWndGizmoState.rotating ? CAMWND_GIZMO_AXIS_Z : axis);
s_camWndGizmoState.axisLength = CamWnd_GizmoAxisLength(cam, center, mins, maxs);
s_camWndGizmoState.startScalar = 0.0f;
s_camWndGizmoState.lastScalar = 0.0f;
s_camWndGizmoState.rotationAmount = 0.0f;
CamWnd_GizmoAxisScalarFromRay(cam, point, center, s_camWndGizmoState.axisDir, s_camWndGizmoState.startScalar);
if (s_camWndGizmoState.rotating) {
Select_InitializeRotation();
}
cam->SetFocus();
cam->SetCapture();
SetCursor(LoadCursor(NULL, s_camWndGizmoState.rotating ? IDC_CROSS : IDC_SIZEALL));
Sys_Status(s_camWndGizmoState.rotating ? "Shift-drag gizmo: yaw rotate selected object.\n" : "Drag gizmo axis to move selected object.\n");
Sys_UpdateWindows(W_CAMERA | W_XY | W_Z);
return true;
}
static bool CamWnd_GizmoIsActive(CCamWnd* cam) {
return s_camWndGizmoState.active && s_camWndGizmoState.cam == cam;
}
static void CamWnd_GizmoEnd(CCamWnd* cam, bool commit) {
if (!CamWnd_GizmoIsActive(cam)) {
return;
}
if (::GetCapture() == cam->GetSafeHwnd()) {
::ReleaseCapture();
}
if (s_camWndGizmoState.rotating) {
Select_FinalizeRotation();
}
if (!commit) {
Sys_Status("camera gizmo drag ended.\n");
}
g_pParentWnd->SetStatusText(3, "");
const int lastHoverAxis = s_camWndGizmoState.hoverAxis;
memset(&s_camWndGizmoState, 0, sizeof(s_camWndGizmoState));
s_camWndGizmoState.hoverAxis = lastHoverAxis;
Sys_UpdateWindows(W_ALL);
}
static bool CamWnd_GizmoMouseMove(CCamWnd* cam, const CPoint& point, UINT nFlags) {
if (CamWnd_GizmoIsActive(cam)) {
if (s_camWndGizmoState.rotating) {
const int dx = point.x - s_camWndGizmoState.lastPoint.x;
const int dy = point.y - s_camWndGizmoState.lastPoint.y;
float deltaAngle = (float)dx * 0.5f;
if (idMath::Fabs(deltaAngle) < idMath::Fabs((float)dy) * 0.15f) {
deltaAngle = (float)-dy * 0.5f;
}
if (deltaAngle != 0.0f) {
Select_RotateAxis(CAMWND_GIZMO_ROTATE_AXIS, deltaAngle, false, false);
cam->MarkWorldDirty();
s_camWndGizmoState.rotationAmount += deltaAngle;
CString strStatus;
strStatus.Format("Yaw rotation: %.1f", s_camWndGizmoState.rotationAmount);
g_pParentWnd->SetStatusText(3, strStatus);
Sys_UpdateWindows(W_CAMERA | W_XY | W_Z);
}
}
else {
float currentScalar = 0.0f;
float moveScalar = 0.0f;
if (CamWnd_GizmoAxisScalarFromRay(cam, point, s_camWndGizmoState.startCenter, s_camWndGizmoState.axisDir, currentScalar)) {
moveScalar = currentScalar - s_camWndGizmoState.startScalar;
}
else if (!CamWnd_GizmoScreenDeltaScalar(cam, point, moveScalar)) {
moveScalar = 0.0f;
}
moveScalar = CamWnd_GizmoSnapScalar(moveScalar);
float deltaScalar = moveScalar - s_camWndGizmoState.lastScalar;
if (deltaScalar != 0.0f) {
idVec3 delta = s_camWndGizmoState.axisDir * deltaScalar;
Select_Move(delta, true);
cam->MarkWorldDirty();
s_camWndGizmoState.lastScalar = moveScalar;
CString strStatus;
strStatus.Format("Axis move: %.1f", moveScalar);
g_pParentWnd->SetStatusText(3, strStatus);
Sys_UpdateWindows(W_CAMERA | W_XY | W_Z);
}
}
s_camWndGizmoState.lastPoint = point;
SetCursor(LoadCursor(NULL, s_camWndGizmoState.rotating ? IDC_CROSS : IDC_SIZEALL));
return true;
}
if ((nFlags & (MK_LBUTTON | MK_RBUTTON | MK_MBUTTON)) == 0) {
int oldHover = s_camWndGizmoState.hoverAxis;
s_camWndGizmoState.hoverAxis = CamWnd_GizmoHitTest(cam, point);
if (s_camWndGizmoState.hoverAxis != CAMWND_GIZMO_AXIS_NONE) {
SetCursor(LoadCursor(NULL, (GetAsyncKeyState(VK_SHIFT) & 0x8000) ? IDC_CROSS : IDC_SIZEALL));
}
if (oldHover != s_camWndGizmoState.hoverAxis) {
Sys_UpdateWindows(W_CAMERA);
}
}
return false;
}
static bool CamWnd_GizmoSelectObjectClick(CCamWnd* cam, const CPoint& point, UINT nFlags) {
if (cam == NULL || !cam->GetSafeHwnd()) {
return false;
}
if (nFlags & (MK_CONTROL | MK_SHIFT)) {
return false;
}
if (GetAsyncKeyState(VK_MENU) & 0x8000) {
return false;
}
idVec3 dir;
CamWnd_FaceExtrudeBuildRay(cam, point, dir);
qertrace_t trace = Test_Ray(cam->Camera().origin, dir, 0);
if (trace.brush == NULL || trace.dist <= 0.0f || trace.dist >= HUGE_DISTANCE) {
return false;
}
if (CamWnd_MenuFilterBrush(cam, trace.brush) || Map_IsBrushFiltered(trace.brush)) {
return false;
}
if (trace.brush->owner != NULL && !Outliner_IsEntityVisible(trace.brush->owner)) {
return false;
}
// If the click hit something outside the current selection, make it the new
// selection and let the axis draw immediately. Clicks on an already-selected
// object still fall through to the original drag code for compatibility.
if (!trace.selected || selected_brushes.next == &selected_brushes) {
Select_Deselect(true);
Select_Ray(cam->Camera().origin, dir, 0);
s_camWndGizmoState.hoverAxis = CAMWND_GIZMO_AXIS_NONE;
Sys_UpdateWindows(W_ALL);
return true;
}
return false;
}
static void CamWnd_GizmoColorForAxis(int axis, bool active) {
if (active) {
glColor3f(1.0f, 1.0f, 0.0f);
return;
}
switch (axis) {
case CAMWND_GIZMO_AXIS_X:
glColor3f(1.0f, 0.1f, 0.1f);
break;
case CAMWND_GIZMO_AXIS_Y:
glColor3f(0.1f, 1.0f, 0.1f);
break;
case CAMWND_GIZMO_AXIS_Z:
glColor3f(0.25f, 0.45f, 1.0f);
break;
default:
glColor3f(1.0f, 1.0f, 1.0f);
break;
}
}
static idVec3 CamWnd_GizmoCross(const idVec3& a, const idVec3& b) {
idVec3 out;
out[0] = a[1] * b[2] - a[2] * b[1];
out[1] = a[2] * b[0] - a[0] * b[2];
out[2] = a[0] * b[1] - a[1] * b[0];
return out;
}
static void CamWnd_GizmoDrawAxisTip(const idVec3& axisEnd, const idVec3& axisDir, float tipSize) {
idVec3 side1 = CamWnd_GizmoCross(axisDir, idVec3(0.0f, 0.0f, 1.0f));
if (side1.LengthSqr() < 0.001f) {
side1 = CamWnd_GizmoCross(axisDir, idVec3(0.0f, 1.0f, 0.0f));
}
side1.Normalize();
idVec3 side2 = CamWnd_GizmoCross(axisDir, side1);
side2.Normalize();
const idVec3 base = axisEnd - axisDir * tipSize;
glBegin(GL_LINES);
glVertex3fv(axisEnd.ToFloatPtr());
glVertex3fv((base + side1 * (tipSize * 0.45f)).ToFloatPtr());
glVertex3fv(axisEnd.ToFloatPtr());
glVertex3fv((base - side1 * (tipSize * 0.45f)).ToFloatPtr());
glVertex3fv(axisEnd.ToFloatPtr());
glVertex3fv((base + side2 * (tipSize * 0.45f)).ToFloatPtr());
glVertex3fv(axisEnd.ToFloatPtr());
glVertex3fv((base - side2 * (tipSize * 0.45f)).ToFloatPtr());
glEnd();
}
static void CamWnd_GizmoDraw(CCamWnd* cam) {
idVec3 center;
idVec3 mins;
idVec3 maxs;
if (!CamWnd_GizmoSelectionCenter(center, &mins, &maxs)) {
return;
}
float axisLength = CamWnd_GizmoAxisLength(cam, center, mins, maxs);
float tipSize = axisLength * 0.12f;
if (tipSize < 8.0f) {
tipSize = 8.0f;
}
else if (tipSize > 32.0f) {
tipSize = 32.0f;
}
glPushAttrib(GL_CURRENT_BIT);
globalImages->BindNull();
glDisable(GL_LIGHTING);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glLineWidth(3.0f);
for (int axis = 0; axis < 3; axis++) {
idVec3 axisDir = CamWnd_GizmoAxisVector(axis);
idVec3 axisEnd = center + axisDir * axisLength;
bool active = (CamWnd_GizmoIsActive(cam) && s_camWndGizmoState.activeAxis == axis) ||
(!s_camWndGizmoState.active && s_camWndGizmoState.hoverAxis == axis);
CamWnd_GizmoColorForAxis(axis, active);
glBegin(GL_LINES);
glVertex3fv(center.ToFloatPtr());
glVertex3fv(axisEnd.ToFloatPtr());
glEnd();
CamWnd_GizmoDrawAxisTip(axisEnd, axisDir, tipSize);
}
glPointSize(6.0f);
glColor3f(1.0f, 1.0f, 1.0f);
glBegin(GL_POINTS);
glVertex3fv(center.ToFloatPtr());
glEnd();
glPopAttrib();
}
static COLORREF CamWnd_LerpColor(COLORREF a, COLORREF b, float t) {
const int ar = GetRValue(a);
const int ag = GetGValue(a);
@@ -2154,6 +2736,7 @@ INT_PTR WINAPI CamWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
CCamWnd* cam = DYNAMIC_DOWNCAST(CCamWnd, wnd);
if (cam) {
CamWnd_FaceExtrudeEnd(cam, false);
CamWnd_GizmoEnd(cam, false);
CameraNav_Stop(cam);
}
SendMessage(hWnd, WM_NCACTIVATE, FALSE, 0);
@@ -2274,6 +2857,7 @@ void CCamWnd::SetXYFriend(CXYWnd* pWnd) {
*/
void CCamWnd::OnDestroy() {
CamWnd_FaceExtrudeEnd(this, false);
CamWnd_GizmoEnd(this, false);
CamWnd_DestroyMenuBar(this);
CWnd::OnDestroy();
}
@@ -2306,6 +2890,11 @@ void CCamWnd::OnMouseMove(UINT nFlags, CPoint point) {
return;
}
if (CamWnd_GizmoMouseMove(this, point, nFlags)) {
m_ptLastCursor = point;
return;
}
if (GetCapture() == this && (GetAsyncKeyState(VK_MENU) & 0x8000)) {
// Alt-drag texture manipulation. The old condition excluded shift/control
// before checking them, so alt+shift and alt+control never reached the
@@ -2348,6 +2937,12 @@ void CCamWnd::OnLButtonDown(UINT nFlags, CPoint point) {
if ((nFlags & MK_CONTROL) && (nFlags & MK_SHIFT) && CamWnd_FaceExtrudeBegin(this, point)) {
return;
}
if (CamWnd_GizmoBegin(this, point, nFlags)) {
return;
}
if (CamWnd_GizmoSelectObjectClick(this, point, nFlags)) {
return;
}
OriginalMouseDown(nFlags, point);
}
@@ -2356,6 +2951,11 @@ void CCamWnd::OnLButtonDown(UINT nFlags, CPoint point) {
=======================================================================================================================
*/
void CCamWnd::OnLButtonUp(UINT nFlags, CPoint point) {
if (CamWnd_GizmoIsActive(this)) {
CamWnd_GizmoMouseMove(this, point, nFlags);
CamWnd_GizmoEnd(this, true);
return;
}
if (CamWnd_FaceExtrudeIsActive(this)) {
CamWnd_FaceExtrudeMouseMove(this, point);
CamWnd_FaceExtrudeEnd(this, true);
@@ -3256,6 +3856,7 @@ void CCamWnd::Cam_Draw() {
CamWnd_DrawMoveSelectedBrushSides(this);
CamWnd_FaceExtrudeDrawPreview(this);
CamWnd_GizmoDraw(this);
// edge / vertex flags
if (g_qeglobals.d_select_mode == sel_vertex) {