myspace / CommonLua /X /XCurveEditor.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
21.2 kB
local max_curve_points = 8
DefineClass.XCurveEditor = {
__parents = { "XControl", "XActionsHost" },
properties = {
{ category = "General", id = "ControlPoints", editor = "number", default = 4, min = 2, max = max_curve_points, },
{ category = "General", id = "MaxX", editor = "number", default = 1000, min = 1 },
{ category = "General", id = "MinX", editor = "number", default = 0, min = 1 },
{ category = "General", id = "MaxY", editor = "number", default = 1000, min = 1 },
{ category = "General", id = "MinY", editor = "number", default = 0 },
{ category = "General", id = "DisplayScaleX", editor = "number", default = 1000, min = 1, help = "Used for displaying numbers around the graph" },
{ category = "General", id = "DisplayScaleY", editor = "number", default = 1000, min = 1, help = "Used for displaying numbers around the graph"},
{ category = "General", id = "SnapX", editor = "number", default = 1, min = 1 },
{ category = "General", id = "SnapY", editor = "number", default = 1, min = 1 },
{ category = "General", id = "FixedX", editor = "bool", default = false },
{ category = "General", id = "PushPointsOnMove", editor = "bool", default = true, },
{ category = "General", id = "CurveColor", editor = "color", default = RGB(0,0,0), },
{ category = "General", id = "ControlPointMaxDist", editor = "number", default = 25, },
{ category = "General", id = "ControlPointColor", editor = "color", default = RGB(80, 80, 80), },
{ category = "General", id = "ControlPointCaptureColor", editor = "color", default = RGB(0,0,0), },
{ category = "General", id = "ControlPointHoverColor", editor = "color", default = RGB(130, 130, 130), },
{ category = "General", id = "GridUnitX", editor = "number", default = 100, },
{ category = "General", id = "GridUnitY", editor = "number", default = 100, },
{ category = "General", id = "GridColor", editor = "color", default = RGB(180, 180, 180), },
{ category = "General", id = "Smooth", editor = "bool", default = true, },
{ category = "General", id = "ReadOnly", editor = "bool", default = false, },
{ category = "General", id = "MinMaxRangeMode", editor = "bool", defalt = false, help = "Stores the /Min/ Value in point.z" },
},
OnCurveChanged = false,
point_handles = false,
capture_handle = false,
hover_handle = false,
points = false,
scale_texts = false,
font_id = false,
text_space_required = false,
MinMaxRangeMode = false,
}
-- debug helpers
for i = 1, max_curve_points do
local xname = "x" .. i
local yname = "y" .. i
table.insert(XCurveEditor.properties, {
category = "General", id = xname, editor = "number", min = 0, default = 0,
max = function(obj) return obj.MaxX end, slider = true, --read_only = true,
no_edit = function(obj) return i > obj.ControlPoints end,
})
table.insert(XCurveEditor.properties, {
category = "General", id = yname, editor = "number", min = 0, default = 0,
max = function(obj) return obj.MaxY end, slider = true, --read_only = true,
no_edit = function(obj) return i > obj.ControlPoints end,
})
XCurveEditor["Get" .. xname] = function(obj) return obj.points[i]:x() end
XCurveEditor["Get" .. yname] = function(obj) return obj.points[i]:y() end
XCurveEditor["Set" .. xname] = function(obj, value) obj:MovePoint(i, obj.points[i]:SetX(value)) end
XCurveEditor["Set" .. yname] = function(obj, value) obj:MovePoint(i, obj.points[i]:SetY(value)) end
end
function XCurveEditor:GeneratePointUIElements()
self.point_handles = {}
for idx, pt in pairs(self.points) do
local main_handle = XCurveEditorHandle:new({
point_idx = idx,
curve_editor = self,
})
table.insert(self.point_handles, main_handle)
if self.MinMaxRangeMode then
local handle = XCurveEditorMinHandle:new({
point_idx = idx,
curve_editor = self,
parent = main_handle,
})
table.insert(self.point_handles, handle)
handle = XCurveEditorMaxHandle:new({
point_idx = idx,
curve_editor = self,
parent = main_handle,
})
table.insert(self.point_handles, handle)
end
end
end
function XCurveEditor:GetRange()
return point(self.MaxX - self.MinX, self.MaxY - self.MinY, self.MaxY - self.MinY)
end
function XCurveEditor:GetRangeMin()
return point(self.MinX, self.MinY, self.MinY)
end
function XCurveEditor:GetRangeMax()
return point(self.MaxX, self.MaxY, self.MaxY)
end
function XCurveEditor:Init()
self.points = { }
local max_points = self.ControlPoints - 1
local range = self:GetRange()
assert(range:x() > 0 and range:y() > 0)
for i = 0, max_points do
local y = i * range:y() / max_points
table.insert(self.points, point(i * range:x() / max_points, y, y))
end
self:GeneratePointUIElements()
end
function XCurveEditor:GetControlPointSize()
return point(ScaleXY(self.scale, 10, 10))
end
function XCurveEditor:GetGraphBox()
local control_size = self:GetControlPointSize()
local topleft_padding = control_size / 2
local bottomright_padding = control_size / 2
local text_space_required = self.text_space_required
if text_space_required then
bottomright_padding = point(Max(text_space_required:x(), bottomright_padding:x()), Max(text_space_required:y(), bottomright_padding:y()))
end
return box(self.content_box:min() + topleft_padding, self.content_box:max() - bottomright_padding)
end
function XCurveEditor:TransformPoint(pos)
local draw_box = self:GetGraphBox()
local ranges = self:GetRange()
local base = point(draw_box:minx(), draw_box:maxy())
pos = pos - self:GetRangeMin()
pos = point(pos:x(), -pos:y())
return base + MulDivRoundPoint(draw_box:size(), pos, ranges)
end
local function FitScaleTexts(min_value, max_value, display_scale, size_getter, available_space, min_space_between)
local begin_text = FormatNumberProp(min_value, display_scale, 2)
local end_text = FormatNumberProp(max_value, display_scale, 2)
local text_list = {}
local secondary_axis_length = 0
local function subdivide(left_value, right_value, left_pos, right_pos)
left_pos = left_pos + min_space_between
right_pos = right_pos - min_space_between
local diff = right_pos - left_pos
if diff <= 10 then -- do not attempt to add text if we have less than 10 pixels
return
end
local target_value = (left_value + right_value) / 2
local text = FormatNumberProp(target_value, display_scale, 2)
local size, secondary_len = size_getter(text)
if size > diff then
return
end
secondary_axis_length = Max(secondary_axis_length, secondary_len)
table.insert(text_list, text)
local mid = left_pos + diff / 2
table.insert(text_list, mid - size / 2)
subdivide(left_value, target_value, left_pos, mid - size / 2)
subdivide(target_value, right_value, mid + size / 2, right_pos)
end
local begin_text_size, secondary_len1 = size_getter(begin_text)
local end_text_size, secondary_len2 = size_getter(end_text)
secondary_axis_length = Max(secondary_axis_length, Max(secondary_len1, secondary_len2))
table.insert(text_list, begin_text)
table.insert(text_list, 0)
subdivide(min_value, max_value, 0, available_space)
table.insert(text_list, end_text)
table.insert(text_list, available_space - end_text_size)
return text_list, secondary_axis_length
end
function XCurveEditor:Layout(x, y, width, height)
local ret = XWindow.Layout(self, x, y, width, height)
self:GenerateTexts()
return ret
end
function XCurveEditor:GenerateTexts()
self.font_id = TextStyles.GedDefault:GetFontIdHeightBaseline(self.scale:y())
self.text_space_required = point(0, 0)
local _, font_height = UIL.MeasureText("AQj", self.font_id)
local vertical_texts, min_width = FitScaleTexts(self.MinY, self.MaxY, self.DisplayScaleY, function(str)
local width, height = UIL.MeasureText(str, self.font_id)
return height, width
end, self.content_box:sizey() - font_height, 10)
local horizontal_texts = FitScaleTexts(0, self.MaxX, self.DisplayScaleX, function(str)
local width, height = UIL.MeasureText(str, self.font_id)
return width, height
end, self.content_box:sizex() - min_width, 10)
self.text_space_required = point(min_width, font_height)
local graph_box = self:GetGraphBox()
local content_box_min = self.content_box:min()
self.scale_texts = {}
for i = 2, #vertical_texts, 2 do
local start_pos = point(graph_box:maxx(), graph_box:maxy() - vertical_texts[i] - font_height) - content_box_min
vertical_texts[i] = sizebox(start_pos, point(UIL.MeasureText(vertical_texts[i - 1], self.font_id)))
end
for i = 2, #horizontal_texts, 2 do
local start_pos = point(graph_box:minx() + horizontal_texts[i], graph_box:maxy()) - content_box_min
horizontal_texts[i] = sizebox(start_pos, point(UIL.MeasureText(horizontal_texts[i - 1], self.font_id)))
end
table.iappend(vertical_texts, horizontal_texts)
self.scale_texts = vertical_texts
end
local function min_point(a, b)
return point(Min(a:x(), b:x()), Min(a:y(), b:y()), Min(a:z(), b:z()))
end
local function max_point(a, b)
return point(Max(a:x(), b:x()), Max(a:y(), b:y()), Max(a:z(), b:z()))
end
function XCurveEditor:MovePoint(index, pos)
local z = pos:z()
assert(z and type(z) == "number")
local points = self.points
assert(index >= 1 and index <= self.ControlPoints and index <= #points)
local old_pos = points[index]
local min_pos = self:GetRangeMin()
local max_pos = self:GetRangeMax()
if self.FixedX or index == 1 or index == #points then
min_pos = min_pos:SetX(old_pos:x())
max_pos = max_pos:SetX(old_pos:x())
end
if not self.PushPointsOnMove then
if index > 1 then
min_pos = max_point(min_pos, points[index - 1])
end
if index < #point then
max_pos = min_point(max_pos, points[index + 1])
end
end
pos = point((pos:x() + self.SnapX / 2) / self.SnapX * self.SnapX,
(pos:y() + self.SnapY / 2) / self.SnapY * self.SnapY,
(pos:z() + self.SnapY / 2) / self.SnapY * self.SnapY)
pos = min_point(max_point(pos, min_pos), max_pos)
points[index] = pos
if self.PushPointsOnMove and not self.FixedX then
for i = 1, index - 1 do
if points[i]:x() > pos:x() then
points[i] = points[i]:SetX(pos:x())
end
end
for i = index + 1, #points do
if points[i]:x() < pos:x() then
points[i] = points[i]:SetX(pos:x())
end
end
end
if self.OnCurveChanged and old_pos ~= pos then
self.OnCurveChanged(self)
self:Invalidate()
end
return pos
end
function XCurveEditor:OnMouseButtonDown(pt, button)
if button == "L" then
self.capture_handle = self.hover_handle
self:SetFocus()
self.desktop:SetMouseCapture(self)
self:OnMousePos(pt)
return "break"
end
end
function XCurveEditor:OnSetRollover(rollover)
XControl.OnSetRollover(self, rollover)
if not rollover then
self.hover_handle = false
self:Invalidate()
end
end
function XCurveEditor:OnMouseButtonUp(pt, button)
if button == "L" then
self.capture_handle = false
self:OnMousePos(pt)
self.desktop:SetMouseCapture()
self:Invalidate()
return "break"
end
end
function XCurveEditor:GetHoveredHandle(pt)
local handles = self.point_handles
local best_handle = -1
local best_dist = 999999
for i, handle in ipairs(handles) do
local dist = handle:HoverScore(self:TransformPoint(handle:GetPos()), pt)
if dist < best_dist then
best_handle = handle
best_dist = dist
end
end
local max_dist = ScaleXY(self.scale, self.ControlPointMaxDist)
if best_dist < max_dist * max_dist then
return best_handle
end
return false
end
function XCurveEditor:OnMousePos(pt)
local old_hover = self.hover_handle
self.hover_handle = self:GetHoveredHandle(pt)
if old_hover ~= self.hover_handle then
self:Invalidate()
end
if self.desktop:GetMouseCapture() ~= self then
self.capture_handle = false
return "break"
end
if not self.capture_handle then
return "break"
end
if self.ReadOnly then
return "break"
end
local content_box = self:GetGraphBox()
local pos = self:GetRangeMin() + MulDivRoundPoint(point(pt:x() - content_box:minx(), content_box:maxy() - pt:y()), self:GetRange(), content_box:size())
self.capture_handle:SetPos(pos)
return "break"
end
local function RoundUp(x, alignment)
if x % alignment == 0 then return x end
return (x / alignment) * alignment + alignment
end
function XCurveEditor:DrawGrid()
local range = self:GetRange()
local max_values = self:GetRangeMax()
local min_values = self:GetRangeMin()
if self.GridUnitX > 0 then
for x = RoundUp(min_values:x(), self.GridUnitX), max_values:x(), self.GridUnitX do
UIL.DrawLine(self:TransformPoint(point(x, min_values:y())), self:TransformPoint(point(x, max_values:y())), self.GridColor)
end
end
if self.GridUnitY > 0 then
for y = RoundUp(min_values:y(), self.GridUnitY), max_values:y(), self.GridUnitY do
UIL.DrawLine(self:TransformPoint(point(min_values:x(), y)), self:TransformPoint(point(max_values:x(), y)), self.GridColor)
end
end
end
function XCurveEditor:DrawControlPoints()
local graph_box = self:GetGraphBox()
local base = point(graph_box:minx(), graph_box:maxy())
local points = self.points
local size = self:GetControlPointSize() / 2
for idx = 1, #self.point_handles do
local handle = self.point_handles[idx]
local color = self.ControlPointColor
if handle:IsCaptured() then
color = self.ControlPointCaptureColor
elseif handle:IsHovered() then
color = self.ControlPointHoverColor
end
local pixel_pos = self:TransformPoint(handle:GetPos(true))
handle:Draw(graph_box, base, size, color, pixel_pos)
end
end
function XCurveEditor:DrawGraphBackground(graph_box, points)
end
function XCurveEditor:DrawScaleTexts()
--display current value whiel dragging
if self.capture_handle then
local pos = self.capture_handle:GetPos()
local graph_box = self:GetGraphBox()
local x_pos_text = FormatNumberProp(pos:x(), self.DisplayScaleX, 2)
local x_pos_text_size = point(UIL.MeasureText(x_pos_text, self.font_id))
local y_pos_text = FormatNumberProp(pos:y(), self.DisplayScaleY, 2)
local y_pos_text_size = point(UIL.MeasureText(y_pos_text, self.font_id))
local pixel_pos = self:TransformPoint(pos)
local draw_text_x = Min(Max(pixel_pos:x() - x_pos_text_size:x() / 2, 0), graph_box:maxx() - x_pos_text_size:x())
UIL.StretchText(x_pos_text, sizebox(point(draw_text_x, graph_box:maxy()), x_pos_text_size), self.font_id, self.CurveColor)
local draw_text_y = Min(Max(pixel_pos:y() - x_pos_text_size:y() / 2, 0), graph_box:maxy() - x_pos_text_size:y())
UIL.StretchText(y_pos_text, sizebox(point(graph_box:maxx(), draw_text_y), y_pos_text_size), self.font_id, self.CurveColor)
return
end
local content_box_min = self.content_box:min()
-- draw static texts. Cached and recalcualted only when needed as we might need to do more complex logic to decide out what to render and where
local texts = self.scale_texts
if not texts then
self:GenerateTexts()
texts = self.scale_texts
end
if texts then
local StretchText = UIL.StretchText
for i = 1, #texts, 2 do
StretchText(texts[i], Offset(texts[i + 1], content_box_min), self.font_id, self.CurveColor)
end
end
end
local function DrawCurveWithPoints(smooth, points, color)
if smooth then
local step = 3
local last_pt = points[1]
for i = 1, #points - 1 do
local left_pt_prev = points[i - 1] or points[i]
local left_pt = points[i]
local right_pt = points[i + 1]
local diff_x = right_pt:x() - left_pt:x()
local right_pt_next = points[i + 2] or points[i + 1]
for x = left_pt:x() + step, right_pt:x(), step do
--TODO: Use some other sample func
local pt = CatmullRomSpline(left_pt_prev, left_pt, right_pt, right_pt_next, MulDivRound(x - left_pt:x(), 1000, diff_x ), 1000)
pt = pt:SetX(x)
UIL.DrawLine(last_pt, pt, color)
last_pt = pt
end
end
else
local last_pt = points[1]
for i = 2, #points do
local pt = points[i]
UIL.DrawLine(last_pt, pt, color)
last_pt = pt
end
end
end
function XCurveEditor:DrawCurve()
assert(self.points[1]:z())
local graph = self:GetGraphBox()
local points = {}
for key, value in ipairs(self.points) do
points[key] = self:TransformPoint(value)
end
DrawCurveWithPoints(self.Smooth, points, self.CurveColor)
if self.MinMaxRangeMode then
points = {}
for key, value in ipairs(self.points) do
points[key] = self:TransformPoint(point(value:x(), value:z()))
end
DrawCurveWithPoints(self.Smooth, points, self.CurveColor)
end
UIL.DrawLine(point(graph:maxx(), graph:miny()), graph:max(), self.CurveColor)
UIL.DrawLine(point(graph:minx(), graph:maxy()), graph:max(), self.CurveColor)
end
function XCurveEditor:DrawContent()
local graph_box = self:GetGraphBox()
self:DrawGraphBackground(graph_box, self.points)
self:DrawGrid()
self:DrawCurve()
self:DrawControlPoints()
self:DrawScaleTexts()
end
function XCurveEditor:ValidatePoints()
for i = 1, #self.points do
local pt = self.points[i]
self.points[i] = max_point(min_point(pt, self:GetRangeMax()), self:GetRangeMin())
end
self.points[1] = self.points[1]:SetX(self.MinX)
self.points[#self.points] = self.points[#self.points]:SetX(self.MaxX)
end
------------ XCurveEditorHandle -------------
-- Spec of movable UI elements in the graph
DefineClass.XCurveEditorHandle = {
__parents = {"PropertyObject"},
type = false,
point_idx = false,
curve_editor = false,
box = false,
parent = false,
}
function XCurveEditorHandle:SetPos(pt)
local old_pt = self.curve_editor.points[self.point_idx]
local height = (old_pt:z() - old_pt:y()) / 2
local new_point = point(pt:x(), pt:y() - height, pt:y() + height)
self.curve_editor:MovePoint(self.point_idx, new_point)
return self:GetPos("refetch")
end
function XCurveEditorHandle:GetPos(refetch)
local pt = self.curve_editor.points[self.point_idx]
return point(pt:x(), (pt:y() + pt:z()) / 2)
end
function XCurveEditorHandle:GetBox()
local old_pt = self.curve_editor.points[self.point_idx]
if self.curve_editor.MinMaxRangeMode then
local height = (old_pt:z() - old_pt:y())
local height_in_pixels = MulDivRound(height, self.curve_editor:GetGraphBox():sizey(), self.curve_editor:GetRange():y())
local half_control_point_width = self.curve_editor:GetControlPointSize():x() / 4
local pixel_pos = self.curve_editor:TransformPoint(old_pt)
self.box = box(point(pixel_pos:x() - half_control_point_width, pixel_pos:y() - height_in_pixels),
point(pixel_pos:x() + half_control_point_width, pixel_pos:y()))
else
local half_control_point_width = self.curve_editor:GetControlPointSize():x() / 2
local pixel_pos = self.curve_editor:TransformPoint(old_pt)
self.box = box(point(pixel_pos:x() - half_control_point_width, pixel_pos:y() - half_control_point_width),
point(pixel_pos:x() + half_control_point_width, pixel_pos:y() + half_control_point_width))
end
return self.box
end
function XCurveEditorHandle:HoverScore(self_pt, mouse_pt)
if terminal.IsKeyPressed(const.vkShift) then
return 1000000
end
local is = self:GetBox():Intersect(box(mouse_pt - point(10, 10), mouse_pt + point(10, 10)))
if is ~= const.irOutside then
return 0
end
return mouse_pt:Dist2(self_pt)
end
function XCurveEditorHandle:Draw(graph_box, base, size, color, pixel_pos)
UIL.DrawSolidRect(self:GetBox(), color)
end
DefineClass.XCurveEditorMinHandle = {
__parents = {"XCurveEditorHandle"},
}
function XCurveEditorMinHandle:SetPos(pt)
local old_pt = self.curve_editor.points[self.point_idx]
local new_pt = point(pt:x(), Min(pt:y(), old_pt:z()), old_pt:z())
self.curve_editor:MovePoint(self.point_idx, new_pt)
return self:GetPos("refetch")
end
function XCurveEditorHandle:IsCaptured()
local capture = self.curve_editor.capture_handle
local current = self
while current do
if capture == current then return true end
current = current.parent
end
return false
end
function XCurveEditorHandle:IsHovered()
local capture = self.curve_editor.hover_handle
local current = self
while current do
if capture == current then return true end
current = current.parent
end
return false
end
function XCurveEditorMinHandle:GetPos(refetch)
local pt = self.curve_editor.points[self.point_idx]
return point(pt:x(), pt:y())
end
function XCurveEditorMinHandle:HoverScore(self_pt, mouse_pt)
return mouse_pt:Dist2(self_pt)
end
function XCurveEditorMinHandle:Draw(graph_box, base, size, color, pixel_pos)
UIL.DrawSolidRect(box(pixel_pos - size, pixel_pos + size), color)
end
DefineClass.XCurveEditorMaxHandle = {
__parents = {"XCurveEditorHandle"},
}
function XCurveEditorMaxHandle:SetPos(pt)
local old_pt = self.curve_editor.points[self.point_idx]
local new_pt = point(pt:x(), old_pt:y(), Max(pt:y(), old_pt:y()))
self.curve_editor:MovePoint(self.point_idx, new_pt)
return self:GetPos("refetch")
end
function XCurveEditorMaxHandle:GetPos(refetch)
local pt = self.curve_editor.points[self.point_idx]
return point(pt:x(), pt:z())
end
function XCurveEditorMaxHandle:HoverScore(self_pt, mouse_pt)
return mouse_pt:Dist2(self_pt)
end
function XCurveEditorMaxHandle:Draw(graph_box, base, size, color, pixel_pos)
UIL.DrawSolidRect(box(pixel_pos - size, pixel_pos + size), color)
end
DefineClass.TestPicker = {
__parents = {"PropertyObject"},
properties = {
{id = "test1", editor = "packedcurve", default = PackCurveParams(point(0, 127000), point(40000, 0), point(80000, 255000), point(255000, 0)),}
}
}