Download CommonLua/Editor/XEditor/RotateGizmo.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 12.4 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/Editor/XEditor/RotateGizmo.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/CommonLua/Editor/XEditor/RotateGizmo.lua
-
curl -L -o RotateGizmo.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/Editor/XEditor/RotateGizmo.lua
12.4 kB
| 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 | |
| -- with Slabs in the selection, rotate around a half-grid aligned point (if rotating around the Z axis) | |
| -- to ensure Slab positions are aligned after the rotation | |
| local center = self:GetPos() | |
| local objs = editor.GetSel() | |
| self:CursorIntersection(pt) -- initialize rotation_axis | |
| 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) -- 9 is the magic number that gives +/-45 degree range | |
| 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 -- don't snap at the 0 degree rotation, allowing for small adjustments | |
| 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 |