| DefineClass.ScaleGizmo = { | |
| __parents = { "XEditorGizmo" }, | |
| HasLocalCSSetting = false, | |
| HasSnapSetting = false, | |
| Title = "Scale gizmo (R)", | |
| Description = false, | |
| ActionSortKey = "3", | |
| ActionIcon = "CommonAssets/UI/Editor/Tools/ScaleGizmo.tga", | |
| ActionShortcut = "R", | |
| UndoOpName = "Scaled %d object(s)", | |
| side_mesh_a = pstr(""), | |
| side_mesh_b = pstr(""), | |
| side_mesh_c = pstr(""), | |
| b_over_a = false, | |
| b_over_b = false, | |
| b_over_c = false, | |
| scale = 100, | |
| thickness = 100, | |
| opacity = 255, | |
| sensitivity = 100, | |
| operation_started = false, | |
| initial_scales = false, | |
| text = false, | |
| scale_text = "", | |
| init_pos = false, | |
| init_mouse_pos = false, | |
| group_scale = false, | |
| } | |
| function ScaleGizmo:Done() | |
| self:DeleteText() | |
| end | |
| function ScaleGizmo:DeleteText() | |
| if self.text then | |
| self.text:delete() | |
| self.text = nil | |
| self.scale_text = "" | |
| end | |
| end | |
| function ScaleGizmo:CheckStartOperation(pt) | |
| return #editor.GetSel() > 0 and self:IntersectRay(camera.GetEye(), ScreenToGame(pt)) | |
| end | |
| function ScaleGizmo:StartOperation(pt) | |
| 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 | |
| self.init_pos = self:GetPos() | |
| self.init_mouse_pos = terminal.GetMousePos() | |
| self.initial_scales = {} | |
| for _, obj in ipairs(editor.GetSel()) do | |
| self.initial_scales[obj] = { scale = obj:GetScale(), offset = obj:GetVisualPos() - self.init_pos } | |
| end | |
| self.group_scale = terminal.IsKeyPressed(const.vkAlt) | |
| self.operation_started = true | |
| end | |
| function ScaleGizmo:PerformOperation(pt) | |
| local screenHeight = UIL.GetScreenSize():y() | |
| local mouseY = 4096.0 * (terminal.GetMousePos():y() - screenHeight / 2) / screenHeight | |
| local initY = 4096.0 * (self.init_mouse_pos:y() - screenHeight / 2) / screenHeight | |
| local scale | |
| if mouseY < initY then | |
| scale = 100 * (mouseY + 4096) / (initY + 4096) + 250 * (initY - mouseY) / (initY + 4096) | |
| else | |
| scale = 100 * (4096 - mouseY) / (4096 - initY) + 10 * (mouseY - initY) / (4096 - initY) | |
| end | |
| scale = 100 + MulDivRound(scale - 100, self.sensitivity, 100) | |
| self:SetScaleClamped(scale) | |
| for obj, data in pairs(self.initial_scales) do | |
| obj:SetScaleClamped(MulDivRound(data.scale, scale, 100)) | |
| if self.group_scale then | |
| XEditorSetPosAxisAngle(obj, self.init_pos + data.offset * scale / 100) | |
| end | |
| end | |
| local objs = table.keys(self.initial_scales) | |
| self.scale_text = #objs == 1 and | |
| string.format("%.2f", objs[1]:GetScale() / 100.0) or | |
| ((scale >= 100 and "+" or "-") .. string.format("%d%%", abs(scale - 100))) | |
| Msg("EditorCallback", "EditorCallbackScale", objs) | |
| end | |
| function ScaleGizmo:EndOperation() | |
| self:DeleteText() | |
| self:SetScale(100) | |
| self.init_pos = false | |
| self.init_mouse_pos = false | |
| self.initial_scales = false | |
| self.group_scale = false | |
| self.operation_started = false | |
| end | |
| function ScaleGizmo:RenderGizmo() | |
| local FloorPtA = MulDivRound(point(0, 4096, 0), self.scale * 25, 40960) | |
| local FloorPtB = MulDivRound(point(-3547, -2048, 0), self.scale * 25, 40960) | |
| local FloorPtC = MulDivRound(point(3547, -2048, 0), self.scale * 25, 40960) | |
| local UpperPt = MulDivRound(point(0, 0, 5900), self.scale * 25, 40960) | |
| local PyramidSize = FloorPtA:Dist(FloorPtB) | |
| self.side_mesh_a = self:RenderPlane(nil, UpperPt, FloorPtB, FloorPtC) | |
| self.side_mesh_b = self:RenderPlane(nil, FloorPtA, UpperPt, FloorPtC) | |
| self.side_mesh_c = self:RenderPlane(nil, FloorPtA, UpperPt, FloorPtB) | |
| if self.text then | |
| self.text:SetText(self.scale_text) | |
| end | |
| local vpstr = pstr("") | |
| vpstr = self:RenderCylinder(vpstr, PyramidSize, FloorPtA, 90, FloorPtB) | |
| vpstr = self:RenderCylinder(vpstr, PyramidSize, FloorPtB, 90, FloorPtC) | |
| vpstr = self:RenderCylinder(vpstr, PyramidSize, FloorPtC, 90, FloorPtA) | |
| vpstr = self:RenderCylinder(vpstr, PyramidSize, Cross(FloorPtA, axis_z), 35, FloorPtA) | |
| vpstr = self:RenderCylinder(vpstr, PyramidSize, Cross(FloorPtB, axis_z), 35, FloorPtB) | |
| vpstr = self:RenderCylinder(vpstr, PyramidSize, Cross(FloorPtC, axis_z), 35, FloorPtC) | |
| if self.b_over_a then vpstr = self:RenderPlane(vpstr, UpperPt, FloorPtB, FloorPtC) | |
| elseif self.b_over_b then vpstr = self:RenderPlane(vpstr, FloorPtA, UpperPt, FloorPtC) | |
| elseif self.b_over_c then vpstr = self:RenderPlane(vpstr, FloorPtA, UpperPt, FloorPtB) end | |
| return vpstr | |
| end | |
| function ScaleGizmo:ChangeScale() | |
| 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 ScaleGizmo:Render() | |
| local obj = not XEditorIsContextMenuOpen() and selo() | |
| if obj then | |
| self:SetPos(CenterOfMasses(editor.GetSel())) | |
| self:ChangeScale() | |
| self:SetMesh(self:RenderGizmo()) | |
| else self:SetMesh(pstr("")) end | |
| end | |
| function ScaleGizmo:CursorIntersection(mouse_pos) | |
| if self.b_over_a or self.b_over_b or self.b_over_c then | |
| local pos = self:GetVisualPos() | |
| local planeB = pos + axis_z | |
| local planeC = pos + axis_x | |
| local pt1 = camera.GetEye() | |
| local pt2 = ScreenToGame(mouse_pos) | |
| local intersection = IntersectRayPlane(pt1, pt2, pos, planeB, planeC) | |
| return ProjectPointOnLine(pos, pos + axis_z, intersection) | |
| end | |
| end | |
| function ScaleGizmo:IntersectRay(pt1, pt2) | |
| self.b_over_a = false | |
| self.b_over_b = false | |
| self.b_over_c = false | |
| local overA, lenA = IntersectRayMesh(self, pt1, pt2, self.side_mesh_a) | |
| local overB, lenB = IntersectRayMesh(self, pt1, pt2, self.side_mesh_b) | |
| local overC, lenC = IntersectRayMesh(self, pt1, pt2, self.side_mesh_c) | |
| if not (overA or overB or overC) then return end | |
| if lenA and lenB then | |
| if lenA < lenB then self.b_over_a = overA | |
| else self.b_over_b = overB end | |
| elseif lenA and lenC then | |
| if lenA < lenC then self.b_over_a = overA | |
| else self.b_over_c = overC end | |
| elseif lenB and lenC then | |
| if lenB < lenC then self.b_over_b = overB | |
| else self.b_over_c = overC end | |
| elseif lenA then self.b_over_a = overA | |
| elseif lenB then self.b_over_b = overB | |
| elseif lenC then self.b_over_c = overC end | |
| return self.b_over_a or self.b_over_b or self.b_over_c | |
| end | |
| function ScaleGizmo:RenderPlane(vpstr, ptA, ptB, ptC) | |
| vpstr = vpstr or pstr("") | |
| vpstr:AppendVertex(ptA, RGBA(255, 255, 0, MulDivRound(200, self.opacity, 255))) | |
| vpstr:AppendVertex(ptB) | |
| vpstr:AppendVertex(ptC) | |
| return vpstr | |
| end | |
| function ScaleGizmo:RenderCylinder(vpstr, height, axis, angle, offset) | |
| vpstr = vpstr or pstr("") | |
| local center = point(0, 0, 0) | |
| local radius = 0.10 * self.scale * self.thickness / 100 | |
| local color = RGBA(0, 192, 192, self.opacity) | |
| return AppendConeVertices(vpstr, center, point(0, 0, height), radius, radius, axis, angle, color, offset) | |
| end |