function ZO_TrianglePoints_SetPoint(points, index, p, isMirrored) -- Use of isMirrored assumes normalized points. Might add support for non-normalized points later if(index == 3 and isMirrored) then points[index] = { x = p.x, y = 1 - p.y } else points[index] = { x = p.x, y = p.y } end end local function Dot(x1, y1, x2, y2) return x1 * x2 + y1 * y2 end --[[ ZO_Triangle: A simple collection of 3 verts that allows point testing to determine if a user-supplied point is inside or outside the triangle and facilities to clamp a user-supplied point to the nearest edge. --]] ZO_Triangle = ZO_Object:Subclass() function ZO_Triangle:New(points, isMirrored) local triangle = ZO_Object.New(self) triangle:SetPoints(points) triangle.m_isMirrored = isMirrored return triangle end function ZO_Triangle:SetPoints(points) -- Points are defined in CCW order starting at lower left self.m_points = ZO_DeepTableCopy(points) end function ZO_Triangle:GetPoint(pointIndex) return self.m_points[pointIndex] end function ZO_Triangle:GetPreviousPoint(pointIndex) return pointIndex == 1 and self.m_points[3] or self.m_points[pointIndex - 1] end function ZO_Triangle:GetBarycentricCoordinates(x, y) local a = self:GetPoint(1) local b = self:GetPoint(2) local c = self:GetPoint(3) local v0 = { x = b.x - a.x, y = b.y - a.y } local v1 = { x = c.x - a.x, y = c.y - a.y } local v2 = { x = x - a.x, y = y - a.y } local denominator = v0.x * v1.y - v1.x * v0.y local factor = 1 / denominator --Calculate each value and clamp between 0 and 1 local right = zo_clamp((v2.x * v1.y - v1.x * v2.y) * factor, 0.0, 1.0) local top = zo_clamp((v0.x * v2.y - v2.x * v0.y) * factor, 0.0, 1.0) local left = zo_clamp(1 - right - top, 0.0, 1.0) return left, right, top end function ZO_Triangle:GetClosestPointOnTriangle(x, y) local minDist = nil local closestX, closestY for i=1, 3 do local currentPoint = self:GetPoint(i) local lastPoint = self:GetPreviousPoint(i) local edgeX, edgeY = currentPoint.x - lastPoint.x, currentPoint.y - lastPoint.y local edgeLength = Dot(edgeX, edgeY, edgeX, edgeY) local edgeNormalX, edgeNormalY = -edgeY, edgeX local differenceX, differenceY = x - lastPoint.x, y - lastPoint.y if Dot(differenceX, differenceY, edgeNormalX, edgeNormalY) > 0 then local edgeProjection = zo_clamp(Dot(differenceX, differenceY, edgeX, edgeY) / edgeLength, 0.0, 1.0) local pointOnEdgeX, pointOnEdgeY = lastPoint.x + (edgeX * edgeProjection), lastPoint.y + (edgeY * edgeProjection) local diffToEdgeX, diffToEdgeY = pointOnEdgeX - x, pointOnEdgeY - y local diffToEdgeLength = Dot(diffToEdgeX, diffToEdgeY, diffToEdgeX, diffToEdgeY) if not minDist or diffToEdgeLength < minDist then minDist = diffToEdgeLength closestX = pointOnEdgeX closestY = pointOnEdgeY end end end if minDist then --Outside return closestX, closestY, false end --Inside return x, y, true end function ZO_Triangle:ContainsPoint(x, y) if(self.m_isMirrored) then y = 1 - y end return self:GetClosestPointOnTriangle(x, y) end function ZO_Triangle:GetTriangleParams(x, y) local vTop = self:GetPoint(3) local vLeft = self:GetPoint(1) local vRight = self:GetPoint(2) local userX = x - vTop.x local userY = y - vTop.y local rX = vRight.x - vTop.x local rY = vRight.y - vTop.y local lX = vLeft.x - vTop.x local lY = vLeft.y - vTop.y local b = (userY * rX - userX * rY) / (lY * rX - lX * rY) local a = (userX - b * lX) / rX if(self.m_isMirrored) then a, b = b, a end if(a < 0) then a = zo_abs(a) b = b + a elseif(b < 0) then b = zo_abs(b) a = a + b end return a, b end function ZO_Triangle:PointFromParams(a, b) if(self.m_isMirrored) then a, b = b, a end local vTop = self:GetPoint(3) local xTop, yTop = vTop.x, vTop.y local vLeft = self:GetPoint(1) local xLeft, yLeft = vLeft.x, vLeft.y local vRight = self:GetPoint(2) local xRight, yRight = vRight.x, vRight.y local x = xTop + (xRight - xTop) * a + (xLeft - xTop) * b local y = yTop + (yRight - yTop) * a + (yLeft - yTop) * b if(self.m_isMirrored) then y = 1 - y end return x, y end --[[ ZO_TrianglePicker: Wraps a single triangle object and a gui control so that a thumb button can be positioned within the triangle --]] ZO_TrianglePicker = ZO_Object:Subclass() function ZO_TrianglePicker:New(...) local picker = ZO_Object.New(self) picker:Initialize(...) return picker end do local currentPickerId = 0 function ZO_TrianglePicker:Initialize(width, height, parent, control) self.width = width or 128 self.height = height or 128 self.enabled = true if(control) then self.control = control else currentPickerId = currentPickerId + 1 self.control = CreateControlFromVirtual("ZO_TrianglePickerControl", parent or GuiRoot, "ZO_TrianglePickerTemplate", currentPickerId) end self.control.owner = self self:UpdateTriangle() end end function ZO_TrianglePicker:UpdateTriangle() self.control:SetDimensions(self.width, self.height) local points = { { x = 0, y = self.height }, { x = self.width, y = self.height }, { x = self.width / 2, y = 0 }, } if(self.triangle) then self.triangle:SetPoints(points) else self.triangle = ZO_Triangle:New(points) end end function ZO_TrianglePicker:SetThumb(control) self.thumb = control end function ZO_TrianglePicker:SetEnabled(enabled) if(enabled ~= self.enabled) then self.enabled = enabled self:OnMouseUp() local thumb = self.thumb if(thumb) then if(enabled) then thumb:SetState(BSTATE_NORMAL, false) else thumb:SetState(BSTATE_DISABLED, true) end end end end function ZO_TrianglePicker:SetUpdateCallback(callback) self.updateCallback = callback end function ZO_TrianglePicker:GetControl() return self.control end local function SetThumbAnchor(thumb, anchorTo, x, y) if(thumb) then thumb:ClearAnchors() thumb:SetAnchor(CENTER, anchorTo, TOPLEFT, x, y) end end function ZO_TrianglePicker:GetThumbPosition() --In local control space return self.lastX, self.lastY end function ZO_TrianglePicker:SetThumbPosition(x, y) -- In local control space self.lastX, self.lastY = self.triangle:GetClosestPointOnTriangle(x, y) SetThumbAnchor(self.thumb, self.control, self.lastX, self.lastY) end local function GetControlSpaceCoordinates(control, x, y) local scale = control:GetScale() return (x - control:GetLeft()) / scale, (y - control:GetTop()) / scale end function ZO_TrianglePicker:OnUpdate() local x, y = GetControlSpaceCoordinates(self.control, GetUIMousePosition()) local closestPointX, closestPointY, isInside = self.triangle:GetClosestPointOnTriangle(x, y) self.isInside = isInside if(isInside or self.thumbMoving) then WINDOW_MANAGER:SetMouseCursor(MOUSE_CURSOR_UI_HAND) else WINDOW_MANAGER:SetMouseCursor(MOUSE_CURSOR_DO_NOT_CARE) end if(self.thumbMoving) then local lastX, lastY = self.lastX, self.lastY if(lastX ~= closestPointX or lastY ~= closestPointY) then SetThumbAnchor(self.thumb, self.control, closestPointX, closestPointY) if(self.updateCallback) then self.updateCallback(self, closestPointX, closestPointY) end end self.lastX, self.lastY = closestPointX, closestPointY end end function ZO_TrianglePicker:SetThumbMoving(moving) self.thumbMoving = moving end function ZO_TrianglePicker:SetUpdateHandlerEnabled(enableUpdates) if(enableUpdates) then if(not self.control:GetHandler("OnUpdate")) then self.onUpdateFunction = self.onUpdateFunction or function() self:OnUpdate() end self.control:SetHandler("OnUpdate", self.onUpdateFunction) end else self.control:SetHandler("OnUpdate", nil) end end function ZO_TrianglePicker:OnMouseDown() if(not self.enabled) then return end if(self.isInside) then self:SetThumbMoving(true) self:SetUpdateHandlerEnabled(true) PlaySound(SOUNDS.DEFAULT_CLICK) end end function ZO_TrianglePicker:OnMouseUp() if(not self.enabled) then WINDOW_MANAGER:SetMouseCursor(MOUSE_CURSOR_DO_NOT_CARE) PlaySound(SOUNDS.DEFAULT_CLICK) return end self:SetThumbMoving(false) if(not self.isInside) then WINDOW_MANAGER:SetMouseCursor(MOUSE_CURSOR_DO_NOT_CARE) end PlaySound(SOUNDS.DEFAULT_CLICK) -- Ok, the mouse wasn't over the actual triangle, but we may need to keep running the -- update handler because it's still over the control... self:SetUpdateHandlerEnabled(WINDOW_MANAGER:GetMouseOverControl() == self.control) end function ZO_TrianglePicker:OnMouseEnter() if(not self.enabled) then return end self:SetUpdateHandlerEnabled(true) end function ZO_TrianglePicker:OnMouseExit() if(not self.enabled) then return end if(not self.thumbMoving) then self:SetUpdateHandlerEnabled(false) WINDOW_MANAGER:SetMouseCursor(MOUSE_CURSOR_DO_NOT_CARE) end end function ZO_TrianglePicker:GetBarycentricCoordinates() return self.triangle:GetBarycentricCoordinates(self:GetThumbPosition()) end --[[ XML Handlers ]]-- function ZO_TrianglePicker_OnMouseDown(control) control.owner:OnMouseDown() end function ZO_TrianglePicker_OnMouseUp(control) control.owner:OnMouseUp() end function ZO_TrianglePicker_OnMouseEnter(control) control.owner:OnMouseEnter() end function ZO_TrianglePicker_OnMouseExit(control) control.owner:OnMouseExit() end