|
DefineClass.RotateGizmo = { |
|
__parents = { "XEditorGizmo" }, |
|
|
|
HasLocalCSSetting = true, |
|
HasSnapSetting = false, |
|
|
|
Title = "Rotate gizmo (E)", |
|
Description = false, |
|
ActionSortKey = "2", |
|
ActionIcon = "CommonAssets/UI/Editor/Tools/RotateGizmo.tga", |
|
ActionShortcut = "E", |
|
UndoOpName = "Rotated %d object(s)", |
|
|
|
mesh_x = pstr(""), |
|
mesh_y = pstr(""), |
|
mesh_z = pstr(""), |
|
mesh_big = pstr(""), |
|
mesh_sphere = pstr(""), |
|
|
|
b_over_x = false, |
|
b_over_y = false, |
|
b_over_z = false, |
|
b_over_big = false, |
|
b_over_sphere = false, |
|
|
|
v_axis_x = axis_x, |
|
v_axis_y = axis_y, |
|
v_axis_z = axis_z, |
|
|
|
scale = 100, |
|
thickness = 100, |
|
opacity = 255, |
|
sensitivity = 100, |
|
|
|
operation_started = false, |
|
|
|
tangent_vector = false, |
|
tangent_offset = false, |
|
tangent_axis = false, |
|
tangent_angle = false, |
|
|
|
initial_orientations = false, |
|
init_intersect = false, |
|
init_pos = false, |
|
rotation_center = false, |
|
rotation_axis = false, |
|
rotation_angle = 0, |
|
rotation_snap = false, |
|
|
|
text = false, |
|
} |
|
|
|
function RotateGizmo:Done() |
|
self:DeleteText() |
|
end |
|
|
|
function RotateGizmo:DeleteText() |
|
if self.text then |
|
self.text:delete() |
|
self.text = nil |
|
end |
|
end |
|
|
|
function RotateGizmo:CheckStartOperation(pt) |
|
return #editor.GetSel() > 0 and self:IntersectRay(camera.GetEye(), ScreenToGame(pt)) |
|
end |
|
|
|
function RotateGizmo:StartOperation(pt) |
|
if not self.b_over_sphere then |
|
self.text = XTemplateSpawn("XFloatingText") |
|
self.text:SetTextStyle("GizmoText") |
|
self.text:AddDynamicPosModifier({id = "attached_ui", target = self:GetPos()}) |
|
self.text.TextColor = RGB(255, 255, 255) |
|
self.text.ShadowType = "outline" |
|
self.text.ShadowSize = 1 |
|
self.text.ShadowColor = RGB(64, 64, 64) |
|
self.text.Translate = false |
|
end |
|
|
|
|
|
|
|
local center = self:GetPos() |
|
local objs = editor.GetSel() |
|
self:CursorIntersection(pt) |
|
if not self.b_over_sphere and self.rotation_axis == axis_z and HasAlignedObjs(objs) then |
|
local snap = const.SlabSizeX |
|
center = point(center:x() / snap * snap, center:y() / snap * snap, center:z()) or center |
|
end |
|
|
|
self.init_intersect = self:CursorIntersection(pt) |
|
self.init_pos = self:GetPos() |
|
self.rotation_center = center |
|
self.initial_orientations = {} |
|
for i, obj in ipairs(objs) do |
|
self.initial_orientations[obj] = { axis = obj:GetVisualAxis(), angle = obj:GetVisualAngle(), offset = obj:GetVisualPos() - center } |
|
end |
|
self.operation_started = true |
|
end |
|
|
|
function RotateGizmo:PerformOperation(pt) |
|
local intersection = self:CursorIntersection(pt) |
|
if not intersection then return end |
|
|
|
self.rotation_angle = 0 |
|
|
|
local axis, angle |
|
local offset = MulDivRound(intersection - self.init_intersect, 9 * self.sensitivity, self.scale) |
|
if self.b_over_sphere then |
|
local normal = Normalize(self:GetVisualPos() - camera.GetEye()) |
|
local axisX = Normalize(Cross(normal, axis_z)) |
|
local axisY = Normalize(Cross(normal, axisX)) |
|
local angleX = Dot(offset, axisY) / 4096 |
|
local angleY = -Dot(offset, axisX) / 4096 |
|
axis, angle = ComposeRotation(axisX, angleX, axisY, angleY) |
|
else |
|
axis, angle = self.rotation_axis, Dot(offset, self.tangent_vector) / 4096 |
|
if XEditorSettings:GetGizmoRotateSnapping() then |
|
local new_angle = self:SnapAngle(angle) |
|
angle, self.rotation_snap = new_angle, new_angle ~= angle |
|
end |
|
self.rotation_angle = angle / 60.0 |
|
end |
|
|
|
local center = self.rotation_center |
|
for obj, data in pairs(self.initial_orientations) do |
|
local newPos = center + RotateAxis(data.offset, axis, angle) |
|
if not obj:IsValidZ() then |
|
newPos = newPos:SetInvalidZ() |
|
end |
|
XEditorSetPosAxisAngle(obj, newPos, ComposeRotation(data.axis, data.angle, axis, angle)) |
|
end |
|
Msg("EditorCallback", "EditorCallbackRotate", table.keys(self.initial_orientations)) |
|
end |
|
|
|
function RotateGizmo:EndOperation() |
|
self.tangent_vector = false |
|
self.tangent_offset = false |
|
self.tangent_axis = false |
|
self.tangent_angle = false |
|
self.rotation_axis = false |
|
self.initial_orientations = false |
|
self.init_intersect = false |
|
self.init_pos = false |
|
self.operation_started = false |
|
self:DeleteText() |
|
end |
|
|
|
function RotateGizmo:SnapAngle(angle) |
|
local snapAngle = 15 * 60 |
|
local snapAngleTollerance = 120 |
|
if abs(angle) > snapAngleTollerance then |
|
if abs(angle % snapAngle) < snapAngleTollerance or abs(angle % snapAngle) > (snapAngle - snapAngleTollerance) then |
|
angle = (angle + snapAngleTollerance) / snapAngle * snapAngle |
|
end |
|
end |
|
return angle |
|
end |
|
|
|
function RotateGizmo:Render() |
|
local obj = not XEditorIsContextMenuOpen() and selo() |
|
if obj then |
|
if self.local_cs then |
|
self.v_axis_x, self.v_axis_y, self.v_axis_z = GetAxisVectors(obj) |
|
else |
|
self.v_axis_x = axis_x |
|
self.v_axis_y = axis_y |
|
self.v_axis_z = axis_z |
|
self:SetOrientation(axis_z, 0) |
|
end |
|
self:SetPos(self.init_pos or CenterOfMasses(editor.GetSel())) |
|
self:CalculateScale() |
|
self:SetMesh(self:RenderGizmo()) |
|
else self:SetMesh(pstr("")) end |
|
end |
|
|
|
function RotateGizmo:RenderGizmo() |
|
local vpstr = pstr("") |
|
local center = point(0, 0, 0) |
|
local pos = selo() and selo():GetVisualPos() or GetTerrainCursor() |
|
local normal = pos - camera.GetEye() |
|
normal = Normalize(normal) |
|
local bigTorusAxis, bigTorusAngle = GetAxisAngle(axis_z, normal) |
|
bigTorusAxis = Normalize(camera.GetEye() - self:GetPos()) |
|
bigTorusAngle = bigTorusAngle / 60 |
|
|
|
self.mesh_big = self:RenderBigTorus(nil, bigTorusAxis) |
|
self.mesh_sphere = self:RenderCircle(nil, bigTorusAxis, bigTorusAngle) |
|
self.mesh_x = self:RenderTorusAndAxis(nil, self.v_axis_x, self.b_over_x, normal) |
|
self.mesh_y = self:RenderTorusAndAxis(nil, self.v_axis_y, self.b_over_y, normal) |
|
self.mesh_z = self:RenderTorusAndAxis(nil, self.v_axis_z, self.b_over_z, normal) |
|
|
|
if self.text then |
|
self.text:SetText((self.rotation_snap and "<color 255 235 64>" or "") .. string.format("%.2f°", self.rotation_angle)) |
|
end |
|
|
|
vpstr = self:RenderBigTorus(vpstr, bigTorusAxis, self.b_over_big, true) |
|
vpstr = self:RenderOutlineTorus(vpstr, bigTorusAxis) |
|
vpstr = self:RenderCircle(vpstr, bigTorusAxis, bigTorusAngle, self.b_over_sphere) |
|
vpstr = self:RenderTorusAndAxis(vpstr, self.v_axis_x, self.b_over_x, normal, RGBA(192, 0, 0, self.opacity), true) |
|
vpstr = self:RenderTorusAndAxis(vpstr, self.v_axis_y, self.b_over_y, normal, RGBA(0, 192, 0, self.opacity), true) |
|
vpstr = self:RenderTorusAndAxis(vpstr, self.v_axis_z, self.b_over_z, normal, RGBA(0, 0, 192, self.opacity), true) |
|
return self:RenderTangent(vpstr) |
|
end |
|
|
|
function RotateGizmo:CalculateScale() |
|
local eye = camera.GetEye() |
|
local dir = self:GetVisualPos() |
|
local ray = dir - eye |
|
local cameraDistanceSquared = ray:x() * ray:x() + ray:y() * ray:y() + ray:z() * ray:z() |
|
local cameraDistance = 0 |
|
if cameraDistanceSquared >= 0 then cameraDistance = sqrt(cameraDistanceSquared) end |
|
self.scale = cameraDistance / 20 * self.scale / 100 |
|
end |
|
|
|
function RotateGizmo:IntersectRay(pt1, pt2) |
|
self.b_over_z = IntersectRayMesh(self, pt1, pt2, self.mesh_z) |
|
self.b_over_x = IntersectRayMesh(self, pt1, pt2, self.mesh_x) |
|
self.b_over_y = IntersectRayMesh(self, pt1, pt2, self.mesh_y) |
|
self.b_over_big = IntersectRayMesh(self, pt1, pt2, self.mesh_big) |
|
|
|
self.b_over_sphere = false |
|
if self.b_over_z then |
|
self.b_over_x = false |
|
self.b_over_y = false |
|
return true |
|
elseif self.b_over_x then |
|
self.b_over_y = false |
|
self.b_over_z = false |
|
return true |
|
elseif self.b_over_y then |
|
self.b_over_z = false |
|
self.b_over_x = false |
|
return true |
|
elseif self.b_over_big then |
|
return true |
|
end |
|
|
|
self.b_over_sphere = IntersectRayMesh(self, pt1, pt2, self.mesh_sphere) |
|
return self.b_over_sphere |
|
end |
|
|
|
function RotateGizmo:CursorIntersection(mouse_pos) |
|
local pt1 = camera.GetEye() |
|
local pt2 = ScreenToGame(mouse_pos) |
|
local pos = self:GetVisualPos() |
|
local pt_intersection = self.b_over_x or self.b_over_y or self.b_over_z or self.b_over_big |
|
if pt_intersection then |
|
if not self.operation_started then |
|
self.rotation_axis = |
|
self.b_over_x and self.v_axis_x or |
|
self.b_over_y and self.v_axis_y or |
|
self.b_over_z and self.v_axis_z or |
|
self.b_over_big and (camera.GetEye() - pos) |
|
self.rotation_axis = Normalize(self.rotation_axis) |
|
self.tangent_offset = pt_intersection - pos |
|
self.tangent_vector = Cross(self.rotation_axis, Normalize(self.tangent_offset)) |
|
self.tangent_vector = Normalize(self.tangent_vector) |
|
self.tangent_axis, self.tangent_angle = GetAxisAngle(axis_z, self.tangent_vector) |
|
self.tangent_axis, self.tangent_angle = Normalize(self.tangent_axis), self.tangent_angle / 60 |
|
end |
|
|
|
local axis |
|
if self.b_over_x then |
|
axis = self.v_axis_x |
|
elseif self.b_over_y then |
|
axis = self.v_axis_y |
|
elseif self.b_over_z then |
|
axis = self.v_axis_z |
|
elseif self.b_over_big then |
|
axis = camera.GetEye() - pos |
|
end |
|
local camDir = Normalize(camera.GetEye() - pos) |
|
local camX = Normalize(Cross((camDir), axis_z)) |
|
local planeB = pos + camX |
|
local planeC = pos + Normalize(Cross((camDir), camX)) |
|
local ptA = pos + self.tangent_offset |
|
local ptB = ptA + self.tangent_vector |
|
local intersection = IntersectRayPlane(pt1, pt2, pos, planeB, planeC) |
|
return ProjectPointOnLine(ptA, ptB, intersection) |
|
elseif self.b_over_sphere then |
|
local axis = Normalize(camera.GetEye() - pos) |
|
local screenX = Cross(axis, axis_z) |
|
local screenY = Cross(axis, axis_x) |
|
local planeB = pos + screenX |
|
local planeC = pos + screenY |
|
return IntersectRayPlane(pt1, pt2, pos, planeB, planeC) |
|
end |
|
end |
|
|
|
function RotateGizmo:RenderTangent(vpstr) |
|
if self.tangent_vector then |
|
local radius = 0.1 * self.scale * self.thickness / 100 |
|
local length = 2.5 * self.scale |
|
local coneHeight = 0.50 * self.scale |
|
local coneRadius = 0.30 * self.scale * self.thickness / 100 |
|
local color = RGBA(255, 0, 255, self.opacity) |
|
vpstr = AppendConeVertices(vpstr, point(0, 0, -length), point(0, 0, length * 2), radius, radius, self.tangent_axis, self.tangent_angle, color, self.tangent_offset) |
|
vpstr = AppendConeVertices(vpstr, point(0, 0, -length), point(0, 0, -coneHeight), coneRadius, 0, self.tangent_axis, self.tangent_angle, color, self.tangent_offset) |
|
vpstr = AppendConeVertices(vpstr, point(0, 0, length), point(0, 0, coneHeight), coneRadius, 0, self.tangent_axis, self.tangent_angle, color, self.tangent_offset) |
|
end |
|
return vpstr |
|
end |
|
|
|
function RotateGizmo:RenderCircle(vpstr, axis, angle, selected) |
|
vpstr = vpstr or pstr("") |
|
local HSeg = 32 |
|
local center = point(0, 0, 0) |
|
local rad = Cross(axis, axis_z) |
|
local radius = 2.3 * self.scale |
|
local color = selected and RGBA(255, 255, 0, 70 * self.opacity / 255) or RGBA(0, 0, 0, 0) |
|
rad = Normalize(rad) |
|
rad = MulDivRound(rad, radius, 4096) |
|
for i = 1, HSeg do |
|
local pt = Rotate(rad, MulDivRound(360 * 60, i, HSeg)) |
|
pt = RotateAxis(pt, rad, angle * 60) |
|
local nextPt = Rotate(rad, MulDivRound(360 * 60, i + 1, HSeg)) |
|
nextPt = RotateAxis(nextPt, rad, angle * 60) |
|
vpstr:AppendVertex(center, color) |
|
vpstr:AppendVertex(pt) |
|
vpstr:AppendVertex(nextPt) |
|
end |
|
return vpstr |
|
end |
|
|
|
function RotateGizmo:RenderBigTorus(vpstr, axis, selected, visual) |
|
local radius1 = 3.5 * self.scale |
|
local radius2 = visual and 0.15 * self.scale * self.thickness / 100 or 0.15 * self.scale |
|
local color = selected and RGBA(255, 255, 0, self.opacity) or RGBA(0, 192, 192, self.opacity) |
|
return AppendTorusVertices(vpstr, radius1, radius2, axis, color) |
|
end |
|
|
|
function RotateGizmo:RenderTorusAndAxis(vpstr, axis, selected, normal, color, visual) |
|
local radius1 = 2.3 * self.scale |
|
local radius2 = visual and 0.15 * self.scale * self.thickness / 100 or 0.15 * self.scale |
|
color = selected and RGBA(255, 255, 0, self.opacity) or color |
|
|
|
local height = 1.5 * self.scale |
|
local radius = 0.05 * self.scale |
|
vpstr = AppendTorusVertices(vpstr, radius1, radius2, axis, color, normal) |
|
local axis, angle = GetAxisAngle(axis_z, axis) |
|
axis = Normalize(axis) |
|
angle = angle / 60 |
|
return AppendConeVertices(vpstr, nil, point(0, 0, height), radius, radius, axis, angle, color) |
|
end |
|
|
|
function RotateGizmo:RenderOutlineTorus(vpstr, axis) |
|
local radius1 = 2.3 * self.scale |
|
local radius2 = 0.15 * self.scale * self.thickness / 100 |
|
local color = RGBA(128, 128, 128, 192 * self.opacity / 255) |
|
return AppendTorusVertices(vpstr, radius1, radius2, axis, color) |
|
end |