471 lines
14 KiB
ActionScript
471 lines
14 KiB
ActionScript
// Urho3D editor node transform gizmo handling
|
|
|
|
Node@ gizmoNode;
|
|
StaticModel@ gizmo;
|
|
|
|
const float axisMaxD = 0.1;
|
|
const float axisMaxT = 1.0;
|
|
const float rotSensitivity = 50.0;
|
|
|
|
EditMode lastGizmoMode;
|
|
|
|
// For undo
|
|
bool previousGizmoDrag;
|
|
bool needGizmoUndo;
|
|
Array<Transform> oldGizmoTransforms;
|
|
|
|
class GizmoAxis
|
|
{
|
|
Ray axisRay;
|
|
bool selected;
|
|
bool lastSelected;
|
|
float t;
|
|
float d;
|
|
float lastT;
|
|
float lastD;
|
|
|
|
GizmoAxis()
|
|
{
|
|
selected = false;
|
|
lastSelected = false;
|
|
t = 0.0;
|
|
d = 0.0;
|
|
lastT = 0.0;
|
|
lastD = 0.0;
|
|
}
|
|
|
|
void Update(Ray cameraRay, float scale, bool drag)
|
|
{
|
|
// Do not select when UI has modal element
|
|
if (ui.HasModalElement())
|
|
{
|
|
selected = false;
|
|
return;
|
|
}
|
|
|
|
Vector3 closest = cameraRay.ClosestPoint(axisRay);
|
|
Vector3 projected = axisRay.Project(closest);
|
|
d = axisRay.Distance(closest);
|
|
t = (projected - axisRay.origin).DotProduct(axisRay.direction);
|
|
|
|
// Determine the sign of d from a plane that goes through the camera position to the axis
|
|
Plane axisPlane(cameraNode.position, axisRay.origin, axisRay.origin + axisRay.direction);
|
|
if (axisPlane.Distance(closest) < 0.0)
|
|
d = -d;
|
|
|
|
// Update selected status only when not dragging
|
|
if (!drag)
|
|
{
|
|
selected = Abs(d) < axisMaxD * scale && t >= -axisMaxD * scale && t <= axisMaxT * scale;
|
|
lastT = t;
|
|
lastD = d;
|
|
}
|
|
}
|
|
|
|
void Moved()
|
|
{
|
|
lastT = t;
|
|
lastD = d;
|
|
}
|
|
}
|
|
|
|
GizmoAxis gizmoAxisX;
|
|
GizmoAxis gizmoAxisY;
|
|
GizmoAxis gizmoAxisZ;
|
|
|
|
void CreateGizmo()
|
|
{
|
|
gizmoNode = Node();
|
|
gizmo = gizmoNode.CreateComponent("StaticModel");
|
|
gizmo.model = cache.GetResource("Model", "Models/Editor/Axes.mdl");
|
|
gizmo.materials[0] = cache.GetResource("Material", "Materials/Editor/RedUnlit.xml");
|
|
gizmo.materials[1] = cache.GetResource("Material", "Materials/Editor/GreenUnlit.xml");
|
|
gizmo.materials[2] = cache.GetResource("Material", "Materials/Editor/BlueUnlit.xml");
|
|
gizmo.enabled = false;
|
|
gizmo.viewMask = 0x80000000; // Editor raycasts use viewmask 0x7fffffff
|
|
gizmo.occludee = false;
|
|
gizmoNode.name = "EditorGizmo";
|
|
|
|
gizmoAxisX.lastSelected = false;
|
|
gizmoAxisY.lastSelected = false;
|
|
gizmoAxisZ.lastSelected = false;
|
|
lastGizmoMode = EDIT_MOVE;
|
|
}
|
|
|
|
void HideGizmo()
|
|
{
|
|
if (gizmo !is null)
|
|
gizmo.enabled = false;
|
|
}
|
|
|
|
void ShowGizmo()
|
|
{
|
|
if (gizmo !is null)
|
|
{
|
|
gizmo.enabled = true;
|
|
|
|
// Because setting enabled = false detaches the gizmo from octree,
|
|
// and it is a manually added drawable, must readd to octree when showing
|
|
if (editorScene.octree !is null)
|
|
editorScene.octree.AddManualDrawable(gizmo);
|
|
}
|
|
}
|
|
|
|
void UpdateGizmo()
|
|
{
|
|
UseGizmo();
|
|
PositionGizmo();
|
|
ResizeGizmo();
|
|
}
|
|
|
|
void PositionGizmo()
|
|
{
|
|
if (gizmo is null)
|
|
return;
|
|
|
|
Vector3 center(0, 0, 0);
|
|
bool containsScene = false;
|
|
|
|
for (uint i = 0; i < editNodes.length; ++i)
|
|
{
|
|
// Scene's transform should not be edited, so hide gizmo if it is included
|
|
if (editNodes[i] is editorScene)
|
|
{
|
|
containsScene = true;
|
|
break;
|
|
}
|
|
center += editNodes[i].worldPosition;
|
|
}
|
|
|
|
if (editNodes.empty || containsScene)
|
|
{
|
|
HideGizmo();
|
|
return;
|
|
}
|
|
|
|
center /= editNodes.length;
|
|
gizmoNode.position = center;
|
|
|
|
if (axisMode == AXIS_WORLD || editNodes.length > 1)
|
|
gizmoNode.rotation = Quaternion();
|
|
else
|
|
gizmoNode.rotation = editNodes[0].worldRotation;
|
|
|
|
if (editMode != lastGizmoMode)
|
|
{
|
|
switch (editMode)
|
|
{
|
|
case EDIT_MOVE:
|
|
gizmo.model = cache.GetResource("Model", "Models/Editor/Axes.mdl");
|
|
break;
|
|
|
|
case EDIT_ROTATE:
|
|
gizmo.model = cache.GetResource("Model", "Models/Editor/RotateAxes.mdl");
|
|
break;
|
|
|
|
case EDIT_SCALE:
|
|
gizmo.model = cache.GetResource("Model", "Models/Editor/ScaleAxes.mdl");
|
|
break;
|
|
}
|
|
|
|
lastGizmoMode = editMode;
|
|
}
|
|
|
|
if ((editMode != EDIT_SELECT && !orbiting) && !gizmo.enabled)
|
|
ShowGizmo();
|
|
else if ((editMode == EDIT_SELECT || orbiting) && gizmo.enabled)
|
|
HideGizmo();
|
|
}
|
|
|
|
void ResizeGizmo()
|
|
{
|
|
if (gizmo is null || !gizmo.enabled)
|
|
return;
|
|
|
|
float scale = 0.1 / camera.zoom;
|
|
|
|
if (camera.orthographic)
|
|
scale *= camera.orthoSize;
|
|
else
|
|
scale *= (camera.view * gizmoNode.position).z;
|
|
|
|
gizmoNode.scale = Vector3(scale, scale, scale);
|
|
}
|
|
|
|
void CalculateGizmoAxes()
|
|
{
|
|
gizmoAxisX.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(1, 0, 0));
|
|
gizmoAxisY.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(0, 1, 0));
|
|
gizmoAxisZ.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(0, 0, 1));
|
|
}
|
|
|
|
void GizmoMoved()
|
|
{
|
|
gizmoAxisX.Moved();
|
|
gizmoAxisY.Moved();
|
|
gizmoAxisZ.Moved();
|
|
}
|
|
|
|
void UseGizmo()
|
|
{
|
|
if (gizmo is null || !gizmo.enabled || editMode == EDIT_SELECT)
|
|
{
|
|
StoreGizmoEditActions();
|
|
previousGizmoDrag = false;
|
|
return;
|
|
}
|
|
|
|
IntVector2 pos = ui.cursorPosition;
|
|
if (ui.GetElementAt(pos) !is null)
|
|
return;
|
|
Ray cameraRay = GetActiveViewportCameraRay();
|
|
float scale = gizmoNode.scale.x;
|
|
|
|
// Recalculate axes only when not left-dragging
|
|
bool drag = input.mouseButtonDown[MOUSEB_LEFT];
|
|
if (!drag)
|
|
CalculateGizmoAxes();
|
|
|
|
gizmoAxisX.Update(cameraRay, scale, drag);
|
|
gizmoAxisY.Update(cameraRay, scale, drag);
|
|
gizmoAxisZ.Update(cameraRay, scale, drag);
|
|
|
|
if (gizmoAxisX.selected != gizmoAxisX.lastSelected)
|
|
{
|
|
gizmo.materials[0] = cache.GetResource("Material", gizmoAxisX.selected ? "Materials/Editor/BrightRedUnlit.xml" :
|
|
"Materials/Editor/RedUnlit.xml");
|
|
gizmoAxisX.lastSelected = gizmoAxisX.selected;
|
|
}
|
|
if (gizmoAxisY.selected != gizmoAxisY.lastSelected)
|
|
{
|
|
gizmo.materials[1] = cache.GetResource("Material", gizmoAxisY.selected ? "Materials/Editor/BrightGreenUnlit.xml" :
|
|
"Materials/Editor/GreenUnlit.xml");
|
|
gizmoAxisY.lastSelected = gizmoAxisY.selected;
|
|
}
|
|
if (gizmoAxisZ.selected != gizmoAxisZ.lastSelected)
|
|
{
|
|
gizmo.materials[2] = cache.GetResource("Material", gizmoAxisZ.selected ? "Materials/Editor/BrightBlueUnlit.xml" :
|
|
"Materials/Editor/BlueUnlit.xml");
|
|
gizmoAxisZ.lastSelected = gizmoAxisZ.selected;
|
|
};
|
|
|
|
if (drag)
|
|
{
|
|
// Store initial transforms for undo when gizmo drag started
|
|
if (!previousGizmoDrag)
|
|
{
|
|
oldGizmoTransforms.Resize(editNodes.length);
|
|
for (uint i = 0; i < editNodes.length; ++i)
|
|
oldGizmoTransforms[i].Define(editNodes[i]);
|
|
}
|
|
|
|
bool moved = false;
|
|
|
|
if (editMode == EDIT_MOVE)
|
|
{
|
|
Vector3 adjust(0, 0, 0);
|
|
if (gizmoAxisX.selected)
|
|
adjust += Vector3(1, 0, 0) * (gizmoAxisX.t - gizmoAxisX.lastT);
|
|
if (gizmoAxisY.selected)
|
|
adjust += Vector3(0, 1, 0) * (gizmoAxisY.t - gizmoAxisY.lastT);
|
|
if (gizmoAxisZ.selected)
|
|
adjust += Vector3(0, 0, 1) * (gizmoAxisZ.t - gizmoAxisZ.lastT);
|
|
|
|
moved = MoveNodes(adjust);
|
|
}
|
|
else if (editMode == EDIT_ROTATE)
|
|
{
|
|
Vector3 adjust(0, 0, 0);
|
|
if (gizmoAxisX.selected)
|
|
adjust.x = (gizmoAxisX.d - gizmoAxisX.lastD) * rotSensitivity / scale;
|
|
if (gizmoAxisY.selected)
|
|
adjust.y = -(gizmoAxisY.d - gizmoAxisY.lastD) * rotSensitivity / scale;
|
|
if (gizmoAxisZ.selected)
|
|
adjust.z = (gizmoAxisZ.d - gizmoAxisZ.lastD) * rotSensitivity / scale;
|
|
|
|
moved = RotateNodes(adjust);
|
|
}
|
|
else if (editMode == EDIT_SCALE)
|
|
{
|
|
Vector3 adjust(0, 0, 0);
|
|
if (gizmoAxisX.selected)
|
|
adjust += Vector3(1, 0, 0) * (gizmoAxisX.t - gizmoAxisX.lastT);
|
|
if (gizmoAxisY.selected)
|
|
adjust += Vector3(0, 1, 0) * (gizmoAxisY.t - gizmoAxisY.lastT);
|
|
if (gizmoAxisZ.selected)
|
|
adjust += Vector3(0, 0, 1) * (gizmoAxisZ.t - gizmoAxisZ.lastT);
|
|
|
|
// Special handling for uniform scale: use the unmodified X-axis movement only
|
|
if (editMode == EDIT_SCALE && gizmoAxisX.selected && gizmoAxisY.selected && gizmoAxisZ.selected)
|
|
{
|
|
float x = gizmoAxisX.t - gizmoAxisX.lastT;
|
|
adjust = Vector3(x, x, x);
|
|
}
|
|
|
|
moved = ScaleNodes(adjust);
|
|
}
|
|
|
|
if (moved)
|
|
{
|
|
GizmoMoved();
|
|
UpdateNodeAttributes();
|
|
needGizmoUndo = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (previousGizmoDrag)
|
|
StoreGizmoEditActions();
|
|
}
|
|
|
|
previousGizmoDrag = drag;
|
|
}
|
|
|
|
bool IsGizmoSelected()
|
|
{
|
|
return gizmo !is null && gizmo.enabled && (gizmoAxisX.selected || gizmoAxisY.selected || gizmoAxisZ.selected);
|
|
}
|
|
|
|
bool MoveNodes(Vector3 adjust)
|
|
{
|
|
bool moved = false;
|
|
|
|
if (adjust.length > M_EPSILON)
|
|
{
|
|
for (uint i = 0; i < editNodes.length; ++i)
|
|
{
|
|
if (moveSnap)
|
|
{
|
|
float moveStepScaled = moveStep * snapScale;
|
|
adjust.x = Floor(adjust.x / moveStepScaled + 0.5) * moveStepScaled;
|
|
adjust.y = Floor(adjust.y / moveStepScaled + 0.5) * moveStepScaled;
|
|
adjust.z = Floor(adjust.z / moveStepScaled + 0.5) * moveStepScaled;
|
|
}
|
|
|
|
Node@ node = editNodes[i];
|
|
Vector3 nodeAdjust = adjust;
|
|
if (axisMode == AXIS_LOCAL && editNodes.length == 1)
|
|
nodeAdjust = node.worldRotation * nodeAdjust;
|
|
|
|
Vector3 worldPos = node.worldPosition;
|
|
Vector3 oldPos = node.position;
|
|
|
|
worldPos += nodeAdjust;
|
|
|
|
if (node.parent is null)
|
|
node.position = worldPos;
|
|
else
|
|
node.position = node.parent.WorldToLocal(worldPos);
|
|
|
|
if (node.position != oldPos)
|
|
moved = true;
|
|
}
|
|
}
|
|
|
|
return moved;
|
|
}
|
|
|
|
bool RotateNodes(Vector3 adjust)
|
|
{
|
|
bool moved = false;
|
|
|
|
if (rotateSnap)
|
|
{
|
|
float rotateStepScaled = rotateStep * snapScale;
|
|
adjust.x = Floor(adjust.x / rotateStepScaled + 0.5) * rotateStepScaled;
|
|
adjust.y = Floor(adjust.y / rotateStepScaled + 0.5) * rotateStepScaled;
|
|
adjust.z = Floor(adjust.z / rotateStepScaled + 0.5) * rotateStepScaled;
|
|
}
|
|
|
|
if (adjust.length > M_EPSILON)
|
|
{
|
|
moved = true;
|
|
|
|
for (uint i = 0; i < editNodes.length; ++i)
|
|
{
|
|
Node@ node = editNodes[i];
|
|
Quaternion rotQuat(adjust);
|
|
if (axisMode == AXIS_LOCAL && editNodes.length == 1)
|
|
node.rotation = node.rotation * rotQuat;
|
|
else
|
|
{
|
|
Vector3 offset = node.worldPosition - gizmoAxisX.axisRay.origin;
|
|
if (node.parent !is null && node.parent.worldRotation != Quaternion(1, 0, 0, 0))
|
|
rotQuat = node.parent.worldRotation.Inverse() * rotQuat * node.parent.worldRotation;
|
|
node.rotation = rotQuat * node.rotation;
|
|
Vector3 newPosition = gizmoAxisX.axisRay.origin + rotQuat * offset;
|
|
if (node.parent !is null)
|
|
newPosition = node.parent.WorldToLocal(newPosition);
|
|
node.position = newPosition;
|
|
}
|
|
}
|
|
}
|
|
|
|
return moved;
|
|
}
|
|
|
|
bool ScaleNodes(Vector3 adjust)
|
|
{
|
|
bool moved = false;
|
|
|
|
if (adjust.length > M_EPSILON)
|
|
{
|
|
for (uint i = 0; i < editNodes.length; ++i)
|
|
{
|
|
Node@ node = editNodes[i];
|
|
|
|
Vector3 scale = node.scale;
|
|
Vector3 oldScale = scale;
|
|
|
|
if (!scaleSnap)
|
|
scale += adjust;
|
|
else
|
|
{
|
|
float scaleStepScaled = scaleStep * snapScale;
|
|
if (adjust.x != 0)
|
|
{
|
|
scale.x += adjust.x * scaleStepScaled;
|
|
scale.x = Floor(scale.x / scaleStepScaled + 0.5) * scaleStepScaled;
|
|
}
|
|
if (adjust.y != 0)
|
|
{
|
|
scale.y += adjust.y * scaleStepScaled;
|
|
scale.y = Floor(scale.y / scaleStepScaled + 0.5) * scaleStepScaled;
|
|
}
|
|
if (adjust.z != 0)
|
|
{
|
|
scale.z += adjust.z * scaleStepScaled;
|
|
scale.z = Floor(scale.z / scaleStepScaled + 0.5) * scaleStepScaled;
|
|
}
|
|
}
|
|
|
|
if (scale != oldScale)
|
|
moved = true;
|
|
|
|
node.scale = scale;
|
|
}
|
|
}
|
|
|
|
return moved;
|
|
}
|
|
|
|
void StoreGizmoEditActions()
|
|
{
|
|
if (needGizmoUndo && editNodes.length > 0 && oldGizmoTransforms.length == editNodes.length)
|
|
{
|
|
EditActionGroup group;
|
|
|
|
for (uint i = 0; i < editNodes.length; ++i)
|
|
{
|
|
EditNodeTransformAction action;
|
|
action.Define(editNodes[i], oldGizmoTransforms[i]);
|
|
group.actions.Push(action);
|
|
}
|
|
|
|
SaveEditActionGroup(group);
|
|
SetSceneModified();
|
|
}
|
|
|
|
needGizmoUndo = false;
|
|
}
|