starting refactor for sanity
This commit is contained in:
125
FlexLove.lua
125
FlexLove.lua
@@ -10,8 +10,6 @@ local Units = req("Units")
|
||||
local Context = req("Context")
|
||||
---@type StateManager
|
||||
local StateManager = req("StateManager")
|
||||
local ErrorCodes = req("ErrorCodes")
|
||||
local ErrorHandler = req("ErrorHandler")
|
||||
local Performance = req("Performance")
|
||||
local ImageRenderer = req("ImageRenderer")
|
||||
local ImageScaler = req("ImageScaler")
|
||||
@@ -26,79 +24,23 @@ local LayoutEngine = req("LayoutEngine")
|
||||
local Renderer = req("Renderer")
|
||||
local EventHandler = req("EventHandler")
|
||||
local ScrollManager = req("ScrollManager")
|
||||
local ImageDataReader = req("ImageDataReader")
|
||||
---@type ErrorHandler
|
||||
local ErrorHandler = req("ErrorHandler")
|
||||
---@type Element
|
||||
local Element = req("Element")
|
||||
|
||||
-- externals
|
||||
---@type Animation
|
||||
local Animation = req("Animation")
|
||||
---@type AnimationGroup
|
||||
local AnimationGroup = req("AnimationGroup")
|
||||
---@type Easing
|
||||
local Easing = req("Easing")
|
||||
---@type Color
|
||||
local Color = req("Color")
|
||||
---@type Theme
|
||||
local Theme = req("Theme")
|
||||
local enums = utils.enums
|
||||
|
||||
Element.defaultDependencies = {
|
||||
Context = Context,
|
||||
Theme = Theme,
|
||||
Color = Color,
|
||||
Units = Units,
|
||||
Blur = Blur,
|
||||
ImageRenderer = ImageRenderer,
|
||||
ImageScaler = ImageScaler,
|
||||
NinePatch = NinePatch,
|
||||
RoundedRect = RoundedRect,
|
||||
ImageCache = ImageCache,
|
||||
utils = utils,
|
||||
Grid = Grid,
|
||||
InputEvent = InputEvent,
|
||||
GestureRecognizer = GestureRecognizer,
|
||||
StateManager = StateManager,
|
||||
TextEditor = TextEditor,
|
||||
LayoutEngine = LayoutEngine,
|
||||
Renderer = Renderer,
|
||||
EventHandler = EventHandler,
|
||||
ScrollManager = ScrollManager,
|
||||
ErrorHandler = ErrorHandler,
|
||||
}
|
||||
|
||||
---@class FlexLove
|
||||
local flexlove = Context
|
||||
|
||||
-- Initialize ErrorHandler with ErrorCodes dependency
|
||||
ErrorHandler.init({ ErrorCodes = ErrorCodes })
|
||||
|
||||
-- Initialize modules that use ErrorHandler via DI
|
||||
local errorHandlerDeps = { ErrorHandler = ErrorHandler }
|
||||
if ImageRenderer.init then
|
||||
ImageRenderer.init(errorHandlerDeps)
|
||||
end
|
||||
if ImageScaler then
|
||||
local ImageScaler = req("ImageScaler")
|
||||
if ImageScaler.init then
|
||||
ImageScaler.init(errorHandlerDeps)
|
||||
end
|
||||
end
|
||||
if NinePatch.init then
|
||||
NinePatch.init(errorHandlerDeps)
|
||||
end
|
||||
local ImageDataReader = req("ImageDataReader")
|
||||
if ImageDataReader.init then
|
||||
ImageDataReader.init(errorHandlerDeps)
|
||||
end
|
||||
|
||||
-- Initialize modules with dependencies
|
||||
Units.init({ Context = Context, ErrorHandler = ErrorHandler })
|
||||
Color.init({ ErrorHandler = ErrorHandler })
|
||||
utils.init({ ErrorHandler = ErrorHandler })
|
||||
Animation.init({ ErrorHandler = ErrorHandler, Easing = Easing, Color = Color })
|
||||
AnimationGroup.init({ ErrorHandler = ErrorHandler })
|
||||
|
||||
-- Add version and metadata
|
||||
flexlove._VERSION = "0.3.0"
|
||||
flexlove._DESCRIPTION = "UI Library for LÖVE Framework based on flexbox"
|
||||
flexlove._URL = "https://github.com/mikefreno/FlexLove"
|
||||
@@ -148,16 +90,52 @@ flexlove._deferredCallbacks = {}
|
||||
function flexlove.init(config)
|
||||
config = config or {}
|
||||
|
||||
if config.errorLogFile then
|
||||
ErrorHandler.setLogTarget("file")
|
||||
ErrorHandler.setLogFile(config.errorLogFile)
|
||||
elseif config.enableErrorLogging == true then
|
||||
-- Use default log file if logging enabled but no path specified
|
||||
ErrorHandler.setLogTarget("file")
|
||||
ErrorHandler.setLogFile("flexlove-errors.log")
|
||||
end
|
||||
flexlove._ErrorHandler = ErrorHandler.init({
|
||||
includeStackTrace = config.includeStackTrace,
|
||||
logLevel = config.reportingLogLevel,
|
||||
logTarget = config.errorLogTarget,
|
||||
logFile = config.errorLogFile,
|
||||
maxLogSize = config.errorLogMaxSize,
|
||||
maxLogFiles = config.maxErrorLogFiles,
|
||||
enableRotation = config.errorLogRotateEnabled,
|
||||
})
|
||||
|
||||
ImageRenderer.init({ ErrorHandler = flexlove._ErrorHandler })
|
||||
|
||||
ImageScaler.init({ ErrorHandler = flexlove._ErrorHandler })
|
||||
|
||||
NinePatch.init({ ErrorHandler = flexlove._ErrorHandler })
|
||||
ImageDataReader.init({ ErrorHandler = flexlove._ErrorHandler })
|
||||
|
||||
Units.init({ Context = Context, ErrorHandler = flexlove._ErrorHandler })
|
||||
Color.init({ ErrorHandler = flexlove._ErrorHandler })
|
||||
utils.init({ ErrorHandler = flexlove._ErrorHandler })
|
||||
Animation.init({ ErrorHandler = flexlove._ErrorHandler, Color = Color })
|
||||
|
||||
flexlove._defaultDependencies = {
|
||||
Context = Context,
|
||||
Theme = Theme,
|
||||
Color = Color,
|
||||
Units = Units,
|
||||
Blur = Blur,
|
||||
ImageRenderer = ImageRenderer,
|
||||
ImageScaler = ImageScaler,
|
||||
NinePatch = NinePatch,
|
||||
RoundedRect = RoundedRect,
|
||||
ImageCache = ImageCache,
|
||||
utils = utils,
|
||||
Grid = Grid,
|
||||
InputEvent = InputEvent,
|
||||
GestureRecognizer = GestureRecognizer,
|
||||
StateManager = StateManager,
|
||||
TextEditor = TextEditor,
|
||||
LayoutEngine = LayoutEngine,
|
||||
Renderer = Renderer,
|
||||
EventHandler = EventHandler,
|
||||
ScrollManager = ScrollManager,
|
||||
ErrorHandler = flexlove._ErrorHandler,
|
||||
}
|
||||
|
||||
-- Configure performance monitoring (default: true)
|
||||
local enablePerfMonitoring = config.performanceMonitoring
|
||||
if enablePerfMonitoring == nil then
|
||||
enablePerfMonitoring = true
|
||||
@@ -974,7 +952,7 @@ function flexlove.new(props)
|
||||
|
||||
-- If not in immediate mode, use standard Element.new
|
||||
if not flexlove._immediateMode then
|
||||
return Element.new(props, Element.defaultDependencies)
|
||||
return Element.new(props, flexlove._defaultDependencies)
|
||||
end
|
||||
|
||||
-- Auto-begin frame if not manually started (convenience feature)
|
||||
@@ -999,8 +977,7 @@ function flexlove.new(props)
|
||||
props._scrollX = state._scrollX or 0
|
||||
props._scrollY = state._scrollY or 0
|
||||
|
||||
-- Create the element
|
||||
local element = Element.new(props, Element.defaultDependencies)
|
||||
local element = Element.new(props, flexlove._defaultDependencies)
|
||||
|
||||
-- Bind persistent state to element (ImmediateModeState)
|
||||
-- Restore event handler state
|
||||
@@ -1114,8 +1091,6 @@ function flexlove.getStateStats()
|
||||
end
|
||||
|
||||
flexlove.Animation = Animation
|
||||
flexlove.AnimationGroup = AnimationGroup
|
||||
flexlove.Easing = Easing
|
||||
flexlove.Color = Color
|
||||
flexlove.Theme = Theme
|
||||
flexlove.enums = enums
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,338 +0,0 @@
|
||||
--- AnimationGroup module for running multiple animations together
|
||||
---@class AnimationGroup
|
||||
local AnimationGroup = {}
|
||||
AnimationGroup.__index = AnimationGroup
|
||||
|
||||
-- ErrorHandler dependency (injected via initializeErrorHandler)
|
||||
local ErrorHandler = nil
|
||||
|
||||
---@class AnimationGroupProps
|
||||
---@field animations table Array of Animation instances
|
||||
---@field mode string? "parallel", "sequence", or "stagger" (default: "parallel")
|
||||
---@field stagger number? Stagger delay in seconds (for stagger mode, default: 0.1)
|
||||
---@field onComplete function? Called when all animations complete: (group)
|
||||
---@field onStart function? Called when group starts: (group)
|
||||
|
||||
--- Coordinate multiple animations to play together, in sequence, or staggered for complex choreographed effects
|
||||
--- Use this to synchronize related UI changes like simultaneous fades or sequential reveals
|
||||
---@param props AnimationGroupProps
|
||||
---@return AnimationGroup group
|
||||
function AnimationGroup.new(props)
|
||||
if type(props) ~= "table" then
|
||||
ErrorHandler.warn("AnimationGroup", "AnimationGroup.new() requires a table argument. Using default values.")
|
||||
props = {animations = {}}
|
||||
end
|
||||
|
||||
if type(props.animations) ~= "table" or #props.animations == 0 then
|
||||
ErrorHandler.warn("AnimationGroup", "AnimationGroup requires at least one animation. Creating empty group.")
|
||||
props.animations = {}
|
||||
end
|
||||
|
||||
local self = setmetatable({}, AnimationGroup)
|
||||
|
||||
self.animations = props.animations
|
||||
self.mode = props.mode or "parallel"
|
||||
self.stagger = props.stagger or 0.1
|
||||
self.onComplete = props.onComplete
|
||||
self.onStart = props.onStart
|
||||
|
||||
-- Validate mode
|
||||
if self.mode ~= "parallel" and self.mode ~= "sequence" and self.mode ~= "stagger" then
|
||||
ErrorHandler.warn("AnimationGroup", string.format("Invalid mode: %s. Using 'parallel'.", tostring(self.mode)))
|
||||
self.mode = "parallel"
|
||||
end
|
||||
|
||||
-- Internal state
|
||||
self._currentIndex = 1
|
||||
self._staggerElapsed = 0
|
||||
self._startedAnimations = {}
|
||||
self._hasStarted = false
|
||||
self._paused = false
|
||||
self._state = "ready" -- "ready", "playing", "completed", "cancelled"
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Update all animations in parallel
|
||||
---@param dt number Delta time
|
||||
---@param element table? Optional element reference for callbacks
|
||||
---@return boolean finished True if all animations complete
|
||||
function AnimationGroup:_updateParallel(dt, element)
|
||||
local allFinished = true
|
||||
|
||||
for i, anim in ipairs(self.animations) do
|
||||
-- Check if animation has isCompleted method or check state
|
||||
local isCompleted = false
|
||||
if type(anim.getState) == "function" then
|
||||
isCompleted = anim:getState() == "completed"
|
||||
elseif anim._state then
|
||||
isCompleted = anim._state == "completed"
|
||||
end
|
||||
|
||||
if not isCompleted then
|
||||
local finished = anim:update(dt, element)
|
||||
if not finished then
|
||||
allFinished = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return allFinished
|
||||
end
|
||||
|
||||
--- Update animations in sequence (one after another)
|
||||
---@param dt number Delta time
|
||||
---@param element table? Optional element reference for callbacks
|
||||
---@return boolean finished True if all animations complete
|
||||
function AnimationGroup:_updateSequence(dt, element)
|
||||
if self._currentIndex > #self.animations then
|
||||
return true
|
||||
end
|
||||
|
||||
local currentAnim = self.animations[self._currentIndex]
|
||||
local finished = currentAnim:update(dt, element)
|
||||
|
||||
if finished then
|
||||
self._currentIndex = self._currentIndex + 1
|
||||
if self._currentIndex > #self.animations then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--- Update animations with stagger delay
|
||||
---@param dt number Delta time
|
||||
---@param element table? Optional element reference for callbacks
|
||||
---@return boolean finished True if all animations complete
|
||||
function AnimationGroup:_updateStagger(dt, element)
|
||||
self._staggerElapsed = self._staggerElapsed + dt
|
||||
|
||||
-- Start animations based on stagger timing
|
||||
for i, anim in ipairs(self.animations) do
|
||||
local startTime = (i - 1) * self.stagger
|
||||
|
||||
if self._staggerElapsed >= startTime and not self._startedAnimations[i] then
|
||||
self._startedAnimations[i] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Update started animations
|
||||
local allFinished = true
|
||||
for i, anim in ipairs(self.animations) do
|
||||
if self._startedAnimations[i] then
|
||||
local isCompleted = false
|
||||
if type(anim.getState) == "function" then
|
||||
isCompleted = anim:getState() == "completed"
|
||||
elseif anim._state then
|
||||
isCompleted = anim._state == "completed"
|
||||
end
|
||||
|
||||
if not isCompleted then
|
||||
local finished = anim:update(dt, element)
|
||||
if not finished then
|
||||
allFinished = false
|
||||
end
|
||||
end
|
||||
else
|
||||
allFinished = false
|
||||
end
|
||||
end
|
||||
|
||||
return allFinished
|
||||
end
|
||||
|
||||
--- Advance all animations in the group according to their coordination mode
|
||||
--- Call this each frame to progress parallel, sequential, or staggered animations
|
||||
---@param dt number Delta time
|
||||
---@param element table? Optional element reference for callbacks
|
||||
---@return boolean finished True if group is complete
|
||||
function AnimationGroup:update(dt, element)
|
||||
-- Sanitize dt
|
||||
if type(dt) ~= "number" or dt < 0 or dt ~= dt or dt == math.huge then
|
||||
dt = 0
|
||||
end
|
||||
|
||||
if self._paused or self._state == "completed" or self._state == "cancelled" then
|
||||
return self._state == "completed"
|
||||
end
|
||||
|
||||
-- Call onStart on first update
|
||||
if not self._hasStarted then
|
||||
self._hasStarted = true
|
||||
self._state = "playing"
|
||||
if self.onStart and type(self.onStart) == "function" then
|
||||
local success, err = pcall(self.onStart, self)
|
||||
if not success then
|
||||
print(string.format("[AnimationGroup] onStart error: %s", tostring(err)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local finished = false
|
||||
|
||||
if self.mode == "parallel" then
|
||||
finished = self:_updateParallel(dt, element)
|
||||
elseif self.mode == "sequence" then
|
||||
finished = self:_updateSequence(dt, element)
|
||||
elseif self.mode == "stagger" then
|
||||
finished = self:_updateStagger(dt, element)
|
||||
end
|
||||
|
||||
if finished then
|
||||
self._state = "completed"
|
||||
if self.onComplete and type(self.onComplete) == "function" then
|
||||
local success, err = pcall(self.onComplete, self)
|
||||
if not success then
|
||||
print(string.format("[AnimationGroup] onComplete error: %s", tostring(err)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return finished
|
||||
end
|
||||
|
||||
--- Freeze the entire animation sequence in unison
|
||||
--- Use this to pause complex multi-part animations during game pauses
|
||||
function AnimationGroup:pause()
|
||||
self._paused = true
|
||||
for _, anim in ipairs(self.animations) do
|
||||
if type(anim.pause) == "function" then
|
||||
anim:pause()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Continue all paused animations simultaneously from their paused states
|
||||
--- Use this to unpause coordinated animation sequences
|
||||
function AnimationGroup:resume()
|
||||
self._paused = false
|
||||
for _, anim in ipairs(self.animations) do
|
||||
if type(anim.resume) == "function" then
|
||||
anim:resume()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Determine if the entire group is currently paused
|
||||
--- Use this to sync other game logic with animation group state
|
||||
---@return boolean paused
|
||||
function AnimationGroup:isPaused()
|
||||
return self._paused
|
||||
end
|
||||
|
||||
--- Flip all animations to play backwards together
|
||||
--- Use this to reverse complex transitions like panel opens/closes
|
||||
function AnimationGroup:reverse()
|
||||
for _, anim in ipairs(self.animations) do
|
||||
if type(anim.reverse) == "function" then
|
||||
anim:reverse()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Control the tempo of all animations simultaneously
|
||||
--- Use this for slow-motion effects or debugging without adjusting individual animations
|
||||
---@param speed number Speed multiplier
|
||||
function AnimationGroup:setSpeed(speed)
|
||||
for _, anim in ipairs(self.animations) do
|
||||
if type(anim.setSpeed) == "function" then
|
||||
anim:setSpeed(speed)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Abort all animations in the group immediately without completion
|
||||
--- Use this when UI is dismissed mid-animation or transitions are interrupted
|
||||
---@param element table? Optional element reference for callbacks
|
||||
function AnimationGroup:cancel(element)
|
||||
if self._state ~= "cancelled" and self._state ~= "completed" then
|
||||
self._state = "cancelled"
|
||||
for _, anim in ipairs(self.animations) do
|
||||
if type(anim.cancel) == "function" then
|
||||
anim:cancel(element)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Restart the entire group from the beginning for reuse
|
||||
--- Use this to replay animation sequences without recreating objects
|
||||
function AnimationGroup:reset()
|
||||
self._currentIndex = 1
|
||||
self._staggerElapsed = 0
|
||||
self._startedAnimations = {}
|
||||
self._hasStarted = false
|
||||
self._paused = false
|
||||
self._state = "ready"
|
||||
|
||||
for _, anim in ipairs(self.animations) do
|
||||
if type(anim.reset) == "function" then
|
||||
anim:reset()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Check the overall lifecycle state of the animation group
|
||||
--- Use this to conditionally trigger follow-up actions or cleanup
|
||||
---@return string state "ready", "playing", "completed", "cancelled"
|
||||
function AnimationGroup:getState()
|
||||
return self._state
|
||||
end
|
||||
|
||||
--- Calculate completion percentage across all animations in the group
|
||||
--- Use this for progress bars or to synchronize other effects with the group
|
||||
---@return number progress
|
||||
function AnimationGroup:getProgress()
|
||||
if #self.animations == 0 then
|
||||
return 1
|
||||
end
|
||||
|
||||
if self.mode == "sequence" then
|
||||
-- For sequence, progress is based on current animation index + current animation progress
|
||||
local completedAnims = self._currentIndex - 1
|
||||
local currentProgress = 0
|
||||
|
||||
if self._currentIndex <= #self.animations then
|
||||
local currentAnim = self.animations[self._currentIndex]
|
||||
if type(currentAnim.getProgress) == "function" then
|
||||
currentProgress = currentAnim:getProgress()
|
||||
end
|
||||
end
|
||||
|
||||
return (completedAnims + currentProgress) / #self.animations
|
||||
else
|
||||
-- For parallel and stagger, average progress of all animations
|
||||
local totalProgress = 0
|
||||
for _, anim in ipairs(self.animations) do
|
||||
if type(anim.getProgress) == "function" then
|
||||
totalProgress = totalProgress + anim:getProgress()
|
||||
else
|
||||
totalProgress = totalProgress + 1
|
||||
end
|
||||
end
|
||||
return totalProgress / #self.animations
|
||||
end
|
||||
end
|
||||
|
||||
--- Attach this group to an element for automatic updates and integration
|
||||
--- Use this for hands-off animation management within FlexLove's system
|
||||
---@param element Element The element to apply animations to
|
||||
function AnimationGroup:apply(element)
|
||||
if not element or type(element) ~= "table" then
|
||||
ErrorHandler.warn("AnimationGroup", "Cannot apply animation group to nil or non-table element. Group not applied.")
|
||||
return
|
||||
end
|
||||
element.animationGroup = self
|
||||
end
|
||||
|
||||
--- Initialize dependencies
|
||||
---@param deps table Dependencies: { ErrorHandler = ErrorHandler }
|
||||
function AnimationGroup.init(deps)
|
||||
if type(deps) == "table" then
|
||||
ErrorHandler = deps.ErrorHandler
|
||||
end
|
||||
end
|
||||
|
||||
return AnimationGroup
|
||||
@@ -1,41 +1,33 @@
|
||||
local ErrorHandler = nil
|
||||
|
||||
--- Standardized error message formatter (fallback for when ErrorHandler not available)
|
||||
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
||||
---@param message string
|
||||
---@return string
|
||||
local function formatError(module, message)
|
||||
return string.format("[FlexLove.%s] %s", module, message)
|
||||
end
|
||||
|
||||
-- Named colors (CSS3 color names)
|
||||
local NAMED_COLORS = {
|
||||
-- Basic colors
|
||||
black = {0, 0, 0, 1},
|
||||
white = {1, 1, 1, 1},
|
||||
red = {1, 0, 0, 1},
|
||||
green = {0, 0.502, 0, 1},
|
||||
blue = {0, 0, 1, 1},
|
||||
yellow = {1, 1, 0, 1},
|
||||
cyan = {0, 1, 1, 1},
|
||||
magenta = {1, 0, 1, 1},
|
||||
|
||||
black = { 0, 0, 0, 1 },
|
||||
white = { 1, 1, 1, 1 },
|
||||
red = { 1, 0, 0, 1 },
|
||||
green = { 0, 0.502, 0, 1 },
|
||||
blue = { 0, 0, 1, 1 },
|
||||
yellow = { 1, 1, 0, 1 },
|
||||
cyan = { 0, 1, 1, 1 },
|
||||
magenta = { 1, 0, 1, 1 },
|
||||
|
||||
-- Extended colors
|
||||
gray = {0.502, 0.502, 0.502, 1},
|
||||
grey = {0.502, 0.502, 0.502, 1},
|
||||
silver = {0.753, 0.753, 0.753, 1},
|
||||
maroon = {0.502, 0, 0, 1},
|
||||
olive = {0.502, 0.502, 0, 1},
|
||||
lime = {0, 1, 0, 1},
|
||||
aqua = {0, 1, 1, 1},
|
||||
teal = {0, 0.502, 0.502, 1},
|
||||
navy = {0, 0, 0.502, 1},
|
||||
fuchsia = {1, 0, 1, 1},
|
||||
purple = {0.502, 0, 0.502, 1},
|
||||
orange = {1, 0.647, 0, 1},
|
||||
pink = {1, 0.753, 0.796, 1},
|
||||
brown = {0.647, 0.165, 0.165, 1},
|
||||
transparent = {0, 0, 0, 0},
|
||||
gray = { 0.502, 0.502, 0.502, 1 },
|
||||
grey = { 0.502, 0.502, 0.502, 1 },
|
||||
silver = { 0.753, 0.753, 0.753, 1 },
|
||||
maroon = { 0.502, 0, 0, 1 },
|
||||
olive = { 0.502, 0.502, 0, 1 },
|
||||
lime = { 0, 1, 0, 1 },
|
||||
aqua = { 0, 1, 1, 1 },
|
||||
teal = { 0, 0.502, 0.502, 1 },
|
||||
navy = { 0, 0, 0.502, 1 },
|
||||
fuchsia = { 1, 0, 1, 1 },
|
||||
purple = { 0.502, 0, 0.502, 1 },
|
||||
orange = { 1, 0.647, 0, 1 },
|
||||
pink = { 1, 0.753, 0.796, 1 },
|
||||
brown = { 0.647, 0.165, 0.165, 1 },
|
||||
transparent = { 0, 0, 0, 0 },
|
||||
}
|
||||
|
||||
--- Utility class for color handling
|
||||
@@ -56,13 +48,13 @@ Color.__index = Color
|
||||
---@return Color color The new color instance
|
||||
function Color.new(r, g, b, a)
|
||||
local self = setmetatable({}, Color)
|
||||
|
||||
|
||||
-- Sanitize and clamp color components
|
||||
local _, sanitizedR = Color.validateColorChannel(r or 0, 1)
|
||||
local _, sanitizedG = Color.validateColorChannel(g or 0, 1)
|
||||
local _, sanitizedB = Color.validateColorChannel(b or 0, 1)
|
||||
local _, sanitizedA = Color.validateColorChannel(a or 1, 1)
|
||||
|
||||
|
||||
self.r = sanitizedR or 0
|
||||
self.g = sanitizedG or 0
|
||||
self.b = sanitizedB or 0
|
||||
@@ -91,12 +83,12 @@ function Color.fromHex(hexWithTag)
|
||||
ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
||||
input = tostring(hexWithTag),
|
||||
issue = "not a string",
|
||||
fallback = "white (#FFFFFF)"
|
||||
fallback = "white (#FFFFFF)",
|
||||
})
|
||||
end
|
||||
return Color.new(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
|
||||
local hex = hexWithTag:gsub("#", "")
|
||||
if #hex == 6 then
|
||||
local r = tonumber("0x" .. hex:sub(1, 2))
|
||||
@@ -107,7 +99,7 @@ function Color.fromHex(hexWithTag)
|
||||
ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
||||
input = hexWithTag,
|
||||
issue = "invalid hex digits",
|
||||
fallback = "white (#FFFFFF)"
|
||||
fallback = "white (#FFFFFF)",
|
||||
})
|
||||
end
|
||||
return Color.new(1, 1, 1, 1) -- Return white as fallback
|
||||
@@ -123,7 +115,7 @@ function Color.fromHex(hexWithTag)
|
||||
ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
||||
input = hexWithTag,
|
||||
issue = "invalid hex digits",
|
||||
fallback = "white (#FFFFFFFF)"
|
||||
fallback = "white (#FFFFFFFF)",
|
||||
})
|
||||
end
|
||||
return Color.new(1, 1, 1, 1) -- Return white as fallback
|
||||
@@ -135,7 +127,7 @@ function Color.fromHex(hexWithTag)
|
||||
input = hexWithTag,
|
||||
expected = "#RRGGBB or #RRGGBBAA",
|
||||
hexLength = #hex,
|
||||
fallback = "white (#FFFFFF)"
|
||||
fallback = "white (#FFFFFF)",
|
||||
})
|
||||
end
|
||||
return Color.new(1, 1, 1, 1) -- Return white as fallback
|
||||
@@ -150,30 +142,30 @@ end
|
||||
---@return number? clamped Clamped value in 0-1 range, nil if invalid
|
||||
function Color.validateColorChannel(value, max)
|
||||
max = max or 1
|
||||
|
||||
|
||||
if type(value) ~= "number" then
|
||||
return false, nil
|
||||
end
|
||||
|
||||
|
||||
-- Check for NaN
|
||||
if value ~= value then
|
||||
return false, nil
|
||||
end
|
||||
|
||||
|
||||
-- Check for Infinity
|
||||
if value == math.huge or value == -math.huge then
|
||||
return false, nil
|
||||
end
|
||||
|
||||
|
||||
-- Normalize to 0-1 range
|
||||
local normalized = value
|
||||
if max == 255 then
|
||||
normalized = value / 255
|
||||
end
|
||||
|
||||
|
||||
-- Clamp to valid range
|
||||
normalized = math.max(0, math.min(1, normalized))
|
||||
|
||||
|
||||
return true, normalized
|
||||
end
|
||||
|
||||
@@ -185,20 +177,20 @@ function Color.validateHexColor(hex)
|
||||
if type(hex) ~= "string" then
|
||||
return false, "Hex color must be a string"
|
||||
end
|
||||
|
||||
|
||||
-- Remove # prefix
|
||||
local cleanHex = hex:gsub("^#", "")
|
||||
|
||||
|
||||
-- Check length (3, 6, or 8 characters)
|
||||
if #cleanHex ~= 3 and #cleanHex ~= 6 and #cleanHex ~= 8 then
|
||||
return false, string.format("Invalid hex length: %d. Expected 3, 6, or 8 characters", #cleanHex)
|
||||
end
|
||||
|
||||
|
||||
-- Check for valid hex characters
|
||||
if not cleanHex:match("^[0-9A-Fa-f]+$") then
|
||||
return false, "Invalid hex characters. Use only 0-9, A-F"
|
||||
end
|
||||
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
@@ -213,12 +205,12 @@ end
|
||||
function Color.validateRGBColor(r, g, b, a, max)
|
||||
max = max or 1
|
||||
a = a or max
|
||||
|
||||
|
||||
local rValid = Color.validateColorChannel(r, max)
|
||||
local gValid = Color.validateColorChannel(g, max)
|
||||
local bValid = Color.validateColorChannel(b, max)
|
||||
local aValid = Color.validateColorChannel(a, max)
|
||||
|
||||
|
||||
if not rValid then
|
||||
return false, string.format("Invalid red channel: %s", tostring(r))
|
||||
end
|
||||
@@ -231,7 +223,7 @@ function Color.validateRGBColor(r, g, b, a, max)
|
||||
if not aValid then
|
||||
return false, string.format("Invalid alpha channel: %s", tostring(a))
|
||||
end
|
||||
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
@@ -243,12 +235,12 @@ function Color.validateNamedColor(name)
|
||||
if type(name) ~= "string" then
|
||||
return false, "Color name must be a string"
|
||||
end
|
||||
|
||||
|
||||
local lowerName = name:lower()
|
||||
if not NAMED_COLORS[lowerName] then
|
||||
return false, string.format("Unknown color name: '%s'", name)
|
||||
end
|
||||
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
@@ -257,7 +249,7 @@ end
|
||||
---@return string? format Format type ("hex", "named", "table"), nil if invalid
|
||||
function Color.isValidColorFormat(value)
|
||||
local valueType = type(value)
|
||||
|
||||
|
||||
-- Check for hex string
|
||||
if valueType == "string" then
|
||||
if value:match("^#?[0-9A-Fa-f]+$") then
|
||||
@@ -266,22 +258,22 @@ function Color.isValidColorFormat(value)
|
||||
return "hex"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Check for named color
|
||||
if NAMED_COLORS[value:lower()] then
|
||||
return "named"
|
||||
end
|
||||
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
-- Check for table format
|
||||
if valueType == "table" then
|
||||
-- Check for Color instance
|
||||
if getmetatable(value) == Color then
|
||||
return "table"
|
||||
end
|
||||
|
||||
|
||||
-- Check for array format {r, g, b, a}
|
||||
if value[1] and value[2] and value[3] then
|
||||
local valid = Color.validateRGBColor(value[1], value[2], value[3], value[4])
|
||||
@@ -289,7 +281,7 @@ function Color.isValidColorFormat(value)
|
||||
return "table"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Check for named format {r=, g=, b=, a=}
|
||||
if value.r and value.g and value.b then
|
||||
local valid = Color.validateRGBColor(value.r, value.g, value.b, value.a)
|
||||
@@ -297,10 +289,10 @@ function Color.isValidColorFormat(value)
|
||||
return "table"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -314,21 +306,21 @@ function Color.validateColor(value, options)
|
||||
options = options or {}
|
||||
local allowNamed = options.allowNamed ~= false
|
||||
local requireAlpha = options.requireAlpha or false
|
||||
|
||||
|
||||
if value == nil then
|
||||
return false, "Color value is nil"
|
||||
end
|
||||
|
||||
|
||||
local format = Color.isValidColorFormat(value)
|
||||
|
||||
|
||||
if not format then
|
||||
return false, string.format("Invalid color format: %s", tostring(value))
|
||||
end
|
||||
|
||||
|
||||
if format == "named" and not allowNamed then
|
||||
return false, "Named colors not allowed"
|
||||
end
|
||||
|
||||
|
||||
-- Additional validation for alpha requirement
|
||||
if requireAlpha and format == "hex" then
|
||||
local cleanHex = value:gsub("^#", "")
|
||||
@@ -336,7 +328,7 @@ function Color.validateColor(value, options)
|
||||
return false, "Alpha channel required (use 8-digit hex)"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
@@ -347,22 +339,22 @@ end
|
||||
---@return Color color Sanitized color instance (guaranteed non-nil)
|
||||
function Color.sanitizeColor(value, default)
|
||||
default = default or Color.new(0, 0, 0, 1)
|
||||
|
||||
|
||||
local format = Color.isValidColorFormat(value)
|
||||
|
||||
|
||||
if not format then
|
||||
return default
|
||||
end
|
||||
|
||||
|
||||
-- Handle hex format
|
||||
if format == "hex" then
|
||||
local cleanHex = value:gsub("^#", "")
|
||||
|
||||
|
||||
-- Expand 3-digit hex to 6-digit
|
||||
if #cleanHex == 3 then
|
||||
cleanHex = cleanHex:gsub("(.)", "%1%1")
|
||||
end
|
||||
|
||||
|
||||
-- Try to parse
|
||||
local success, result = pcall(Color.fromHex, "#" .. cleanHex)
|
||||
if success then
|
||||
@@ -371,7 +363,7 @@ function Color.sanitizeColor(value, default)
|
||||
return default
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Handle named format
|
||||
if format == "named" then
|
||||
local lowerName = value:lower()
|
||||
@@ -381,39 +373,39 @@ function Color.sanitizeColor(value, default)
|
||||
end
|
||||
return default
|
||||
end
|
||||
|
||||
|
||||
-- Handle table format
|
||||
if format == "table" then
|
||||
-- Color instance
|
||||
if getmetatable(value) == Color then
|
||||
return value
|
||||
end
|
||||
|
||||
|
||||
-- Array format
|
||||
if value[1] then
|
||||
local _, r = Color.validateColorChannel(value[1], 1)
|
||||
local _, g = Color.validateColorChannel(value[2], 1)
|
||||
local _, b = Color.validateColorChannel(value[3], 1)
|
||||
local _, a = Color.validateColorChannel(value[4] or 1, 1)
|
||||
|
||||
|
||||
if r and g and b and a then
|
||||
return Color.new(r, g, b, a)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Named format
|
||||
if value.r then
|
||||
local _, r = Color.validateColorChannel(value.r, 1)
|
||||
local _, g = Color.validateColorChannel(value.g, 1)
|
||||
local _, b = Color.validateColorChannel(value.b, 1)
|
||||
local _, a = Color.validateColorChannel(value.a or 1, 1)
|
||||
|
||||
|
||||
if r and g and b and a then
|
||||
return Color.new(r, g, b, a)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return default
|
||||
end
|
||||
|
||||
@@ -442,16 +434,16 @@ function Color.lerp(colorA, colorB, t)
|
||||
if type(t) ~= "number" or t ~= t or t == math.huge or t == -math.huge then
|
||||
t = 0
|
||||
end
|
||||
|
||||
|
||||
-- Clamp t to 0-1 range
|
||||
t = math.max(0, math.min(1, t))
|
||||
|
||||
|
||||
-- Linear interpolation for each channel
|
||||
local r = colorA.r * (1 - t) + colorB.r * t
|
||||
local g = colorA.g * (1 - t) + colorB.g * t
|
||||
local b = colorA.b * (1 - t) + colorB.b * t
|
||||
local a = colorA.a * (1 - t) + colorB.a * t
|
||||
|
||||
|
||||
return Color.new(r, g, b, a)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
--- Easing functions for animations
|
||||
--- Provides 30+ easing functions for smooth animation transitions
|
||||
---
|
||||
--- Easing function type
|
||||
---@alias EasingFunction fun(t: number): number
|
||||
---
|
||||
---@class Easing
|
||||
local Easing = {}
|
||||
|
||||
-- ============================================================================
|
||||
-- Linear
|
||||
-- ============================================================================
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.linear(t)
|
||||
return t
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Quadratic (Quad)
|
||||
-- ============================================================================
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInQuad(t)
|
||||
return t * t
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeOutQuad(t)
|
||||
return t * (2 - t)
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInOutQuad(t)
|
||||
return t < 0.5 and 2 * t * t or -1 + (4 - 2 * t) * t
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Cubic
|
||||
-- ============================================================================
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInCubic(t)
|
||||
return t * t * t
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeOutCubic(t)
|
||||
local t1 = t - 1
|
||||
return t1 * t1 * t1 + 1
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInOutCubic(t)
|
||||
return t < 0.5 and 4 * t * t * t or (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Quartic (Quart)
|
||||
-- ============================================================================
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInQuart(t)
|
||||
return t * t * t * t
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeOutQuart(t)
|
||||
local t1 = t - 1
|
||||
return 1 - t1 * t1 * t1 * t1
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInOutQuart(t)
|
||||
if t < 0.5 then
|
||||
return 8 * t * t * t * t
|
||||
else
|
||||
local t1 = t - 1
|
||||
return 1 - 8 * t1 * t1 * t1 * t1
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Quintic (Quint)
|
||||
-- ============================================================================
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInQuint(t)
|
||||
return t * t * t * t * t
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeOutQuint(t)
|
||||
local t1 = t - 1
|
||||
return 1 + t1 * t1 * t1 * t1 * t1
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInOutQuint(t)
|
||||
if t < 0.5 then
|
||||
return 16 * t * t * t * t * t
|
||||
else
|
||||
local t1 = t - 1
|
||||
return 1 + 16 * t1 * t1 * t1 * t1 * t1
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Exponential (Expo)
|
||||
-- ============================================================================
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInExpo(t)
|
||||
return t == 0 and 0 or math.pow(2, 10 * (t - 1))
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeOutExpo(t)
|
||||
return t == 1 and 1 or 1 - math.pow(2, -10 * t)
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInOutExpo(t)
|
||||
if t == 0 then return 0 end
|
||||
if t == 1 then return 1 end
|
||||
|
||||
if t < 0.5 then
|
||||
return 0.5 * math.pow(2, 20 * t - 10)
|
||||
else
|
||||
return 1 - 0.5 * math.pow(2, -20 * t + 10)
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Sine
|
||||
-- ============================================================================
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInSine(t)
|
||||
return 1 - math.cos(t * math.pi / 2)
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeOutSine(t)
|
||||
return math.sin(t * math.pi / 2)
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInOutSine(t)
|
||||
return -(math.cos(math.pi * t) - 1) / 2
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Circular (Circ)
|
||||
-- ============================================================================
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInCirc(t)
|
||||
return 1 - math.sqrt(1 - t * t)
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeOutCirc(t)
|
||||
local t1 = t - 1
|
||||
return math.sqrt(1 - t1 * t1)
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInOutCirc(t)
|
||||
if t < 0.5 then
|
||||
return (1 - math.sqrt(1 - 4 * t * t)) / 2
|
||||
else
|
||||
local t1 = -2 * t + 2
|
||||
return (math.sqrt(1 - t1 * t1) + 1) / 2
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Back (Overshoot)
|
||||
-- ============================================================================
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInBack(t)
|
||||
local c1 = 1.70158
|
||||
local c3 = c1 + 1
|
||||
return c3 * t * t * t - c1 * t * t
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeOutBack(t)
|
||||
local c1 = 1.70158
|
||||
local c3 = c1 + 1
|
||||
local t1 = t - 1
|
||||
return 1 + c3 * t1 * t1 * t1 + c1 * t1 * t1
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInOutBack(t)
|
||||
local c1 = 1.70158
|
||||
local c2 = c1 * 1.525
|
||||
|
||||
if t < 0.5 then
|
||||
return (2 * t * 2 * t * ((c2 + 1) * 2 * t - c2)) / 2
|
||||
else
|
||||
local t1 = 2 * t - 2
|
||||
return (t1 * t1 * ((c2 + 1) * t1 + c2) + 2) / 2
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Elastic (Spring)
|
||||
-- ============================================================================
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInElastic(t)
|
||||
if t == 0 then return 0 end
|
||||
if t == 1 then return 1 end
|
||||
|
||||
local c4 = (2 * math.pi) / 3
|
||||
return -math.pow(2, 10 * t - 10) * math.sin((t * 10 - 10.75) * c4)
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeOutElastic(t)
|
||||
if t == 0 then return 0 end
|
||||
if t == 1 then return 1 end
|
||||
|
||||
local c4 = (2 * math.pi) / 3
|
||||
return math.pow(2, -10 * t) * math.sin((t * 10 - 0.75) * c4) + 1
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInOutElastic(t)
|
||||
if t == 0 then return 0 end
|
||||
if t == 1 then return 1 end
|
||||
|
||||
local c5 = (2 * math.pi) / 4.5
|
||||
|
||||
if t < 0.5 then
|
||||
return -(math.pow(2, 20 * t - 10) * math.sin((20 * t - 11.125) * c5)) / 2
|
||||
else
|
||||
return (math.pow(2, -20 * t + 10) * math.sin((20 * t - 11.125) * c5)) / 2 + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Bounce
|
||||
-- ============================================================================
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeOutBounce(t)
|
||||
local n1 = 7.5625
|
||||
local d1 = 2.75
|
||||
|
||||
if t < 1 / d1 then
|
||||
return n1 * t * t
|
||||
elseif t < 2 / d1 then
|
||||
local t1 = t - 1.5 / d1
|
||||
return n1 * t1 * t1 + 0.75
|
||||
elseif t < 2.5 / d1 then
|
||||
local t1 = t - 2.25 / d1
|
||||
return n1 * t1 * t1 + 0.9375
|
||||
else
|
||||
local t1 = t - 2.625 / d1
|
||||
return n1 * t1 * t1 + 0.984375
|
||||
end
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInBounce(t)
|
||||
return 1 - Easing.easeOutBounce(1 - t)
|
||||
end
|
||||
|
||||
---@type EasingFunction
|
||||
function Easing.easeInOutBounce(t)
|
||||
if t < 0.5 then
|
||||
return (1 - Easing.easeOutBounce(1 - 2 * t)) / 2
|
||||
else
|
||||
return (1 + Easing.easeOutBounce(2 * t - 1)) / 2
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Configurable Easing Factories
|
||||
-- ============================================================================
|
||||
|
||||
--- Create a custom back easing function with configurable overshoot
|
||||
---@param overshoot number? Overshoot amount (default: 1.70158)
|
||||
---@return EasingFunction
|
||||
function Easing.back(overshoot)
|
||||
overshoot = overshoot or 1.70158
|
||||
local c3 = overshoot + 1
|
||||
|
||||
return function(t)
|
||||
return c3 * t * t * t - overshoot * t * t
|
||||
end
|
||||
end
|
||||
|
||||
--- Create a custom elastic easing function
|
||||
---@param amplitude number? Amplitude (default: 1)
|
||||
---@param period number? Period (default: 0.3)
|
||||
---@return EasingFunction
|
||||
function Easing.elastic(amplitude, period)
|
||||
amplitude = amplitude or 1
|
||||
period = period or 0.3
|
||||
|
||||
return function(t)
|
||||
if t == 0 then return 0 end
|
||||
if t == 1 then return 1 end
|
||||
|
||||
local s = period / 4
|
||||
local a = amplitude
|
||||
|
||||
if a < 1 then
|
||||
a = 1
|
||||
s = period / 4
|
||||
else
|
||||
s = period / (2 * math.pi) * math.asin(1 / a)
|
||||
end
|
||||
|
||||
return a * math.pow(2, -10 * t) * math.sin((t - s) * (2 * math.pi) / period) + 1
|
||||
end
|
||||
end
|
||||
|
||||
--- Get list of all available easing function names
|
||||
---@return string[] names Array of easing function names
|
||||
function Easing.list()
|
||||
return {
|
||||
-- Linear
|
||||
"linear",
|
||||
-- Quad
|
||||
"easeInQuad", "easeOutQuad", "easeInOutQuad",
|
||||
-- Cubic
|
||||
"easeInCubic", "easeOutCubic", "easeInOutCubic",
|
||||
-- Quart
|
||||
"easeInQuart", "easeOutQuart", "easeInOutQuart",
|
||||
-- Quint
|
||||
"easeInQuint", "easeOutQuint", "easeInOutQuint",
|
||||
-- Expo
|
||||
"easeInExpo", "easeOutExpo", "easeInOutExpo",
|
||||
-- Sine
|
||||
"easeInSine", "easeOutSine", "easeInOutSine",
|
||||
-- Circ
|
||||
"easeInCirc", "easeOutCirc", "easeInOutCirc",
|
||||
-- Back
|
||||
"easeInBack", "easeOutBack", "easeInOutBack",
|
||||
-- Elastic
|
||||
"easeInElastic", "easeOutElastic", "easeInOutElastic",
|
||||
-- Bounce
|
||||
"easeInBounce", "easeOutBounce", "easeInOutBounce",
|
||||
}
|
||||
end
|
||||
|
||||
--- Get an easing function by name
|
||||
---@param name string Easing function name
|
||||
---@return EasingFunction? easing The easing function, or nil if not found
|
||||
function Easing.get(name)
|
||||
return Easing[name]
|
||||
end
|
||||
|
||||
return Easing
|
||||
@@ -153,8 +153,6 @@
|
||||
local Element = {}
|
||||
Element.__index = Element
|
||||
|
||||
-- Note: Element.defaultDependencies is now defined in FlexLove.lua
|
||||
|
||||
---@param props ElementProps
|
||||
---@param deps table Required dependency table (provided by FlexLove)
|
||||
---@return Element
|
||||
@@ -3015,23 +3013,23 @@ function Element:setTransition(property, config)
|
||||
if not self.transitions then
|
||||
self.transitions = {}
|
||||
end
|
||||
|
||||
|
||||
if type(config) ~= "table" then
|
||||
self._deps.ErrorHandler.warn("Element", "setTransition() requires a config table. Using default config.")
|
||||
config = {}
|
||||
end
|
||||
|
||||
|
||||
-- Validate config
|
||||
if config.duration and (type(config.duration) ~= "number" or config.duration < 0) then
|
||||
self._deps.ErrorHandler.warn("Element", "transition duration must be a non-negative number. Using 0.3 seconds.")
|
||||
config.duration = 0.3
|
||||
end
|
||||
|
||||
|
||||
self.transitions[property] = {
|
||||
duration = config.duration or 0.3,
|
||||
easing = config.easing or "easeOutQuad",
|
||||
delay = config.delay or 0,
|
||||
onComplete = config.onComplete
|
||||
onComplete = config.onComplete,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -3044,7 +3042,7 @@ function Element:setTransitionGroup(groupName, config, properties)
|
||||
self._deps.ErrorHandler.warn("Element", "setTransitionGroup() requires a properties array. No transitions set.")
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
for _, prop in ipairs(properties) do
|
||||
self:setTransition(prop, config)
|
||||
end
|
||||
@@ -3056,7 +3054,7 @@ function Element:removeTransition(property)
|
||||
if not self.transitions then
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
if property == "all" then
|
||||
self.transitions = {}
|
||||
else
|
||||
@@ -3071,21 +3069,21 @@ function Element:setProperty(property, value)
|
||||
-- Check if transitions are enabled for this property
|
||||
local shouldTransition = false
|
||||
local transitionConfig = nil
|
||||
|
||||
|
||||
if self.transitions then
|
||||
transitionConfig = self.transitions[property] or self.transitions["all"]
|
||||
shouldTransition = transitionConfig ~= nil
|
||||
end
|
||||
|
||||
|
||||
-- Don't transition if value is the same
|
||||
if self[property] == value then
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
if shouldTransition and transitionConfig then
|
||||
-- Get current value
|
||||
local currentValue = self[property]
|
||||
|
||||
|
||||
-- Only transition if we have a valid current value
|
||||
if currentValue ~= nil then
|
||||
-- Create animation for the property change
|
||||
@@ -3095,24 +3093,24 @@ function Element:setProperty(property, value)
|
||||
start = { [property] = currentValue },
|
||||
final = { [property] = value },
|
||||
easing = transitionConfig.easing,
|
||||
onComplete = transitionConfig.onComplete
|
||||
onComplete = transitionConfig.onComplete,
|
||||
})
|
||||
|
||||
|
||||
-- Set Color module reference if needed
|
||||
if self._deps and self._deps.Color then
|
||||
anim:setColorModule(self._deps.Color)
|
||||
end
|
||||
|
||||
|
||||
-- Set Transform module reference if needed
|
||||
if self._deps and self._deps.Transform then
|
||||
anim:setTransformModule(self._deps.Transform)
|
||||
end
|
||||
|
||||
|
||||
-- Apply delay if configured
|
||||
if transitionConfig.delay and transitionConfig.delay > 0 then
|
||||
anim:delay(transitionConfig.delay)
|
||||
end
|
||||
|
||||
|
||||
-- Apply animation
|
||||
anim:apply(self)
|
||||
else
|
||||
|
||||
@@ -1,473 +0,0 @@
|
||||
---@class ErrorCodes
|
||||
local ErrorCodes = {}
|
||||
|
||||
-- Error code categories
|
||||
ErrorCodes.categories = {
|
||||
VAL = "Validation",
|
||||
LAY = "Layout",
|
||||
REN = "Render",
|
||||
THM = "Theme",
|
||||
EVT = "Event",
|
||||
RES = "Resource",
|
||||
SYS = "System",
|
||||
}
|
||||
|
||||
-- Error code definitions
|
||||
ErrorCodes.codes = {
|
||||
-- Validation Errors (VAL_001 - VAL_099)
|
||||
VAL_001 = {
|
||||
code = "FLEXLOVE_VAL_001",
|
||||
category = "VAL",
|
||||
description = "Invalid property type",
|
||||
suggestion = "Check the property type matches the expected type (e.g., number, string, table)",
|
||||
},
|
||||
VAL_002 = {
|
||||
code = "FLEXLOVE_VAL_002",
|
||||
category = "VAL",
|
||||
description = "Property value out of range",
|
||||
suggestion = "Ensure the value is within the allowed min/max range",
|
||||
},
|
||||
VAL_003 = {
|
||||
code = "FLEXLOVE_VAL_003",
|
||||
category = "VAL",
|
||||
description = "Required property missing",
|
||||
suggestion = "Provide the required property in your element definition",
|
||||
},
|
||||
VAL_004 = {
|
||||
code = "FLEXLOVE_VAL_004",
|
||||
category = "VAL",
|
||||
description = "Invalid color format",
|
||||
suggestion = "Use valid color format: {r, g, b, a} with values 0-1, hex string, or Color object",
|
||||
},
|
||||
VAL_005 = {
|
||||
code = "FLEXLOVE_VAL_005",
|
||||
category = "VAL",
|
||||
description = "Invalid unit format",
|
||||
suggestion = "Use valid unit format: number (px), '50%', '10vw', '5vh', etc.",
|
||||
},
|
||||
VAL_006 = {
|
||||
code = "FLEXLOVE_VAL_006",
|
||||
category = "VAL",
|
||||
description = "Invalid file path",
|
||||
suggestion = "Check that the file path is correct and the file exists",
|
||||
},
|
||||
VAL_007 = {
|
||||
code = "FLEXLOVE_VAL_007",
|
||||
category = "VAL",
|
||||
description = "Invalid enum value",
|
||||
suggestion = "Use one of the allowed enum values for this property",
|
||||
},
|
||||
VAL_008 = {
|
||||
code = "FLEXLOVE_VAL_008",
|
||||
category = "VAL",
|
||||
description = "Invalid text input",
|
||||
suggestion = "Ensure text meets validation requirements (length, pattern, allowed characters)",
|
||||
},
|
||||
|
||||
-- Layout Errors (LAY_001 - LAY_099)
|
||||
LAY_001 = {
|
||||
code = "FLEXLOVE_LAY_001",
|
||||
category = "LAY",
|
||||
description = "Invalid flex direction",
|
||||
suggestion = "Use 'horizontal' or 'vertical' for flexDirection",
|
||||
},
|
||||
LAY_002 = {
|
||||
code = "FLEXLOVE_LAY_002",
|
||||
category = "LAY",
|
||||
description = "Circular dependency detected",
|
||||
suggestion = "Remove circular references in element hierarchy or layout constraints",
|
||||
},
|
||||
LAY_003 = {
|
||||
code = "FLEXLOVE_LAY_003",
|
||||
category = "LAY",
|
||||
description = "Invalid dimensions (negative or NaN)",
|
||||
suggestion = "Ensure width and height are positive numbers",
|
||||
},
|
||||
LAY_004 = {
|
||||
code = "FLEXLOVE_LAY_004",
|
||||
category = "LAY",
|
||||
description = "Layout calculation overflow",
|
||||
suggestion = "Reduce complexity of layout or increase recursion limit",
|
||||
},
|
||||
LAY_005 = {
|
||||
code = "FLEXLOVE_LAY_005",
|
||||
category = "LAY",
|
||||
description = "Invalid alignment value",
|
||||
suggestion = "Use valid alignment values (flex-start, center, flex-end, etc.)",
|
||||
},
|
||||
LAY_006 = {
|
||||
code = "FLEXLOVE_LAY_006",
|
||||
category = "LAY",
|
||||
description = "Invalid positioning mode",
|
||||
suggestion = "Use 'absolute', 'relative', 'flex', or 'grid' for positioning",
|
||||
},
|
||||
LAY_007 = {
|
||||
code = "FLEXLOVE_LAY_007",
|
||||
category = "LAY",
|
||||
description = "Grid layout error",
|
||||
suggestion = "Check grid template columns/rows and item placement",
|
||||
},
|
||||
|
||||
-- Rendering Errors (REN_001 - REN_099)
|
||||
REN_001 = {
|
||||
code = "FLEXLOVE_REN_001",
|
||||
category = "REN",
|
||||
description = "Invalid render state",
|
||||
suggestion = "Ensure element is properly initialized before rendering",
|
||||
},
|
||||
REN_002 = {
|
||||
code = "FLEXLOVE_REN_002",
|
||||
category = "REN",
|
||||
description = "Texture loading failed",
|
||||
suggestion = "Check image path and format, ensure file exists",
|
||||
},
|
||||
REN_003 = {
|
||||
code = "FLEXLOVE_REN_003",
|
||||
category = "REN",
|
||||
description = "Font loading failed",
|
||||
suggestion = "Check font path and format, ensure file exists",
|
||||
},
|
||||
REN_004 = {
|
||||
code = "FLEXLOVE_REN_004",
|
||||
category = "REN",
|
||||
description = "Invalid color value",
|
||||
suggestion = "Color components must be numbers between 0 and 1",
|
||||
},
|
||||
REN_005 = {
|
||||
code = "FLEXLOVE_REN_005",
|
||||
category = "REN",
|
||||
description = "Clipping stack overflow",
|
||||
suggestion = "Reduce nesting depth or check for missing scissor pops",
|
||||
},
|
||||
REN_006 = {
|
||||
code = "FLEXLOVE_REN_006",
|
||||
category = "REN",
|
||||
description = "Shader compilation failed",
|
||||
suggestion = "Check shader code for syntax errors",
|
||||
},
|
||||
REN_007 = {
|
||||
code = "FLEXLOVE_REN_007",
|
||||
category = "REN",
|
||||
description = "Invalid nine-patch configuration",
|
||||
suggestion = "Check nine-patch slice values and image dimensions",
|
||||
},
|
||||
|
||||
-- Theme Errors (THM_001 - THM_099)
|
||||
THM_001 = {
|
||||
code = "FLEXLOVE_THM_001",
|
||||
category = "THM",
|
||||
description = "Theme file not found",
|
||||
suggestion = "Check theme file path and ensure file exists",
|
||||
},
|
||||
THM_002 = {
|
||||
code = "FLEXLOVE_THM_002",
|
||||
category = "THM",
|
||||
description = "Invalid theme structure",
|
||||
suggestion = "Theme must return a table with 'name' and component styles",
|
||||
},
|
||||
THM_003 = {
|
||||
code = "FLEXLOVE_THM_003",
|
||||
category = "THM",
|
||||
description = "Required theme property missing",
|
||||
suggestion = "Ensure theme has required properties (name, base styles, etc.)",
|
||||
},
|
||||
THM_004 = {
|
||||
code = "FLEXLOVE_THM_004",
|
||||
category = "THM",
|
||||
description = "Invalid component style",
|
||||
suggestion = "Component styles must be tables with valid properties",
|
||||
},
|
||||
THM_005 = {
|
||||
code = "FLEXLOVE_THM_005",
|
||||
category = "THM",
|
||||
description = "Theme loading failed",
|
||||
suggestion = "Check theme file for Lua syntax errors",
|
||||
},
|
||||
THM_006 = {
|
||||
code = "FLEXLOVE_THM_006",
|
||||
category = "THM",
|
||||
description = "Invalid theme color",
|
||||
suggestion = "Theme colors must be valid color values (hex, rgba, Color object)",
|
||||
},
|
||||
|
||||
-- Event Errors (EVT_001 - EVT_099)
|
||||
EVT_001 = {
|
||||
code = "FLEXLOVE_EVT_001",
|
||||
category = "EVT",
|
||||
description = "Invalid event type",
|
||||
suggestion = "Use valid event types (mousepressed, textinput, etc.)",
|
||||
},
|
||||
EVT_002 = {
|
||||
code = "FLEXLOVE_EVT_002",
|
||||
category = "EVT",
|
||||
description = "Event handler error",
|
||||
suggestion = "Check event handler function for errors",
|
||||
},
|
||||
EVT_003 = {
|
||||
code = "FLEXLOVE_EVT_003",
|
||||
category = "EVT",
|
||||
description = "Event propagation error",
|
||||
suggestion = "Check event bubbling/capturing logic",
|
||||
},
|
||||
EVT_004 = {
|
||||
code = "FLEXLOVE_EVT_004",
|
||||
category = "EVT",
|
||||
description = "Invalid event target",
|
||||
suggestion = "Ensure event target element exists and is valid",
|
||||
},
|
||||
EVT_005 = {
|
||||
code = "FLEXLOVE_EVT_005",
|
||||
category = "EVT",
|
||||
description = "Event handler not a function",
|
||||
suggestion = "Event handlers must be functions",
|
||||
},
|
||||
|
||||
-- Resource Errors (RES_001 - RES_099)
|
||||
RES_001 = {
|
||||
code = "FLEXLOVE_RES_001",
|
||||
category = "RES",
|
||||
description = "File not found",
|
||||
suggestion = "Check file path and ensure file exists in the filesystem",
|
||||
},
|
||||
RES_002 = {
|
||||
code = "FLEXLOVE_RES_002",
|
||||
category = "RES",
|
||||
description = "Permission denied",
|
||||
suggestion = "Check file permissions and access rights",
|
||||
},
|
||||
RES_003 = {
|
||||
code = "FLEXLOVE_RES_003",
|
||||
category = "RES",
|
||||
description = "Invalid file format",
|
||||
suggestion = "Ensure file format is supported (png, jpg, ttf, etc.)",
|
||||
},
|
||||
RES_004 = {
|
||||
code = "FLEXLOVE_RES_004",
|
||||
category = "RES",
|
||||
description = "Resource loading failed",
|
||||
suggestion = "Check file integrity and format compatibility",
|
||||
},
|
||||
RES_005 = {
|
||||
code = "FLEXLOVE_RES_005",
|
||||
category = "RES",
|
||||
description = "Image cache error",
|
||||
suggestion = "Clear image cache or check memory availability",
|
||||
},
|
||||
|
||||
-- System Errors (SYS_001 - SYS_099)
|
||||
SYS_001 = {
|
||||
code = "FLEXLOVE_SYS_001",
|
||||
category = "SYS",
|
||||
description = "Memory allocation failed",
|
||||
suggestion = "Reduce memory usage or check available memory",
|
||||
},
|
||||
SYS_002 = {
|
||||
code = "FLEXLOVE_SYS_002",
|
||||
category = "SYS",
|
||||
description = "Stack overflow",
|
||||
suggestion = "Reduce recursion depth or check for infinite loops",
|
||||
},
|
||||
SYS_003 = {
|
||||
code = "FLEXLOVE_SYS_003",
|
||||
category = "SYS",
|
||||
description = "Invalid state",
|
||||
suggestion = "Check initialization order and state management",
|
||||
},
|
||||
SYS_004 = {
|
||||
code = "FLEXLOVE_SYS_004",
|
||||
category = "SYS",
|
||||
description = "Module initialization failed",
|
||||
suggestion = "Check module dependencies and initialization order",
|
||||
},
|
||||
|
||||
-- Performance Warnings (PERF_001 - PERF_099)
|
||||
PERF_001 = {
|
||||
code = "FLEXLOVE_PERF_001",
|
||||
category = "PERF",
|
||||
description = "Performance threshold exceeded",
|
||||
suggestion = "Operation took longer than recommended. Monitor for patterns.",
|
||||
},
|
||||
PERF_002 = {
|
||||
code = "FLEXLOVE_PERF_002",
|
||||
category = "PERF",
|
||||
description = "Critical performance threshold exceeded",
|
||||
suggestion = "Operation is causing frame drops. Consider optimizing or reducing frequency.",
|
||||
},
|
||||
|
||||
-- Memory Warnings (MEM_001 - MEM_099)
|
||||
MEM_001 = {
|
||||
code = "FLEXLOVE_MEM_001",
|
||||
category = "MEM",
|
||||
description = "Memory leak detected",
|
||||
suggestion = "Table is growing consistently. Review cache eviction policies and ensure objects are properly released.",
|
||||
},
|
||||
|
||||
-- State Management Warnings (STATE_001 - STATE_099)
|
||||
STATE_001 = {
|
||||
code = "FLEXLOVE_STATE_001",
|
||||
category = "STATE",
|
||||
description = "CallSite counters accumulating",
|
||||
suggestion = "This indicates incrementFrame() may not be called properly. Check immediate mode frame management.",
|
||||
},
|
||||
}
|
||||
|
||||
--- Get error information by code
|
||||
--- @param code string Error code (e.g., "VAL_001" or "FLEXLOVE_VAL_001")
|
||||
--- @return table? errorInfo Error information or nil if not found
|
||||
function ErrorCodes.get(code)
|
||||
-- Handle both short and full format
|
||||
local shortCode = code:gsub("^FLEXLOVE_", "")
|
||||
return ErrorCodes.codes[shortCode]
|
||||
end
|
||||
|
||||
--- Get human-readable description for error code
|
||||
--- @param code string Error code
|
||||
--- @return string description Error description
|
||||
function ErrorCodes.describe(code)
|
||||
local info = ErrorCodes.get(code)
|
||||
if info then
|
||||
return info.description
|
||||
end
|
||||
return "Unknown error code: " .. code
|
||||
end
|
||||
|
||||
--- Get suggested fix for error code
|
||||
--- @param code string Error code
|
||||
--- @return string suggestion Suggested fix
|
||||
function ErrorCodes.getSuggestion(code)
|
||||
local info = ErrorCodes.get(code)
|
||||
if info then
|
||||
return info.suggestion
|
||||
end
|
||||
return "No suggestion available"
|
||||
end
|
||||
|
||||
--- Get category for error code
|
||||
--- @param code string Error code
|
||||
--- @return string category Error category name
|
||||
function ErrorCodes.getCategory(code)
|
||||
local info = ErrorCodes.get(code)
|
||||
if info then
|
||||
return ErrorCodes.categories[info.category] or info.category
|
||||
end
|
||||
return "Unknown"
|
||||
end
|
||||
|
||||
--- List all error codes in a category
|
||||
--- @param category string Category code (e.g., "VAL", "LAY")
|
||||
--- @return table codes List of error codes in category
|
||||
function ErrorCodes.listByCategory(category)
|
||||
local result = {}
|
||||
for code, info in pairs(ErrorCodes.codes) do
|
||||
if info.category == category then
|
||||
table.insert(result, {
|
||||
code = code,
|
||||
fullCode = info.code,
|
||||
description = info.description,
|
||||
suggestion = info.suggestion,
|
||||
})
|
||||
end
|
||||
end
|
||||
table.sort(result, function(a, b)
|
||||
return a.code < b.code
|
||||
end)
|
||||
return result
|
||||
end
|
||||
|
||||
--- Search error codes by keyword
|
||||
--- @param keyword string Keyword to search for
|
||||
--- @return table codes Matching error codes
|
||||
function ErrorCodes.search(keyword)
|
||||
keyword = keyword:lower()
|
||||
local result = {}
|
||||
for code, info in pairs(ErrorCodes.codes) do
|
||||
local searchText = (code .. " " .. info.description .. " " .. info.suggestion):lower()
|
||||
if searchText:find(keyword, 1, true) then
|
||||
table.insert(result, {
|
||||
code = code,
|
||||
fullCode = info.code,
|
||||
description = info.description,
|
||||
suggestion = info.suggestion,
|
||||
category = ErrorCodes.categories[info.category],
|
||||
})
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
--- Get all error codes
|
||||
--- @return table codes All error codes
|
||||
function ErrorCodes.listAll()
|
||||
local result = {}
|
||||
for code, info in pairs(ErrorCodes.codes) do
|
||||
table.insert(result, {
|
||||
code = code,
|
||||
fullCode = info.code,
|
||||
description = info.description,
|
||||
suggestion = info.suggestion,
|
||||
category = ErrorCodes.categories[info.category],
|
||||
})
|
||||
end
|
||||
table.sort(result, function(a, b)
|
||||
return a.code < b.code
|
||||
end)
|
||||
return result
|
||||
end
|
||||
|
||||
--- Format error message with code
|
||||
--- @param code string Error code
|
||||
--- @param message string Error message
|
||||
--- @return string formattedMessage Formatted error message with code
|
||||
function ErrorCodes.formatMessage(code, message)
|
||||
local info = ErrorCodes.get(code)
|
||||
if info then
|
||||
return string.format("[%s] %s", info.code, message)
|
||||
end
|
||||
return message
|
||||
end
|
||||
|
||||
--- Validate that all error codes are unique and properly formatted
|
||||
--- @return boolean, string? Returns true if valid, or false with error message
|
||||
function ErrorCodes.validate()
|
||||
local seen = {}
|
||||
local fullCodes = {}
|
||||
|
||||
for code, info in pairs(ErrorCodes.codes) do
|
||||
-- Check for duplicates
|
||||
if seen[code] then
|
||||
return false, "Duplicate error code: " .. code
|
||||
end
|
||||
seen[code] = true
|
||||
|
||||
if fullCodes[info.code] then
|
||||
return false, "Duplicate full error code: " .. info.code
|
||||
end
|
||||
fullCodes[info.code] = true
|
||||
|
||||
-- Check format
|
||||
if not code:match("^[A-Z]+_[0-9]+$") then
|
||||
return false, "Invalid code format: " .. code .. " (expected CATEGORY_NUMBER)"
|
||||
end
|
||||
|
||||
-- Check full code format
|
||||
local expectedFullCode = "FLEXLOVE_" .. code
|
||||
if info.code ~= expectedFullCode then
|
||||
return false, "Mismatched full code for " .. code .. ": expected " .. expectedFullCode .. ", got " .. info.code
|
||||
end
|
||||
|
||||
-- Check required fields
|
||||
if not info.description or info.description == "" then
|
||||
return false, "Missing description for " .. code
|
||||
end
|
||||
if not info.suggestion or info.suggestion == "" then
|
||||
return false, "Missing suggestion for " .. code
|
||||
end
|
||||
if not info.category or info.category == "" then
|
||||
return false, "Missing category for " .. code
|
||||
end
|
||||
end
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
return ErrorCodes
|
||||
@@ -1,7 +1,476 @@
|
||||
local ErrorHandler = {}
|
||||
local ErrorCodes = nil -- Will be injected via init
|
||||
---@class ErrorCodes
|
||||
---@field categories table
|
||||
---@field codes table
|
||||
local ErrorCodes = {
|
||||
categories = {
|
||||
VAL = "Validation",
|
||||
LAY = "Layout",
|
||||
REN = "Render",
|
||||
THM = "Theme",
|
||||
EVT = "Event",
|
||||
RES = "Resource",
|
||||
SYS = "System",
|
||||
},
|
||||
codes = {
|
||||
-- Validation Errors (VAL_001 - VAL_099)
|
||||
VAL_001 = {
|
||||
code = "FLEXLOVE_VAL_001",
|
||||
category = "VAL",
|
||||
description = "Invalid property type",
|
||||
suggestion = "Check the property type matches the expected type (e.g., number, string, table)",
|
||||
},
|
||||
VAL_002 = {
|
||||
code = "FLEXLOVE_VAL_002",
|
||||
category = "VAL",
|
||||
description = "Property value out of range",
|
||||
suggestion = "Ensure the value is within the allowed min/max range",
|
||||
},
|
||||
VAL_003 = {
|
||||
code = "FLEXLOVE_VAL_003",
|
||||
category = "VAL",
|
||||
description = "Required property missing",
|
||||
suggestion = "Provide the required property in your element definition",
|
||||
},
|
||||
VAL_004 = {
|
||||
code = "FLEXLOVE_VAL_004",
|
||||
category = "VAL",
|
||||
description = "Invalid color format",
|
||||
suggestion = "Use valid color format: {r, g, b, a} with values 0-1, hex string, or Color object",
|
||||
},
|
||||
VAL_005 = {
|
||||
code = "FLEXLOVE_VAL_005",
|
||||
category = "VAL",
|
||||
description = "Invalid unit format",
|
||||
suggestion = "Use valid unit format: number (px), '50%', '10vw', '5vh', etc.",
|
||||
},
|
||||
VAL_006 = {
|
||||
code = "FLEXLOVE_VAL_006",
|
||||
category = "VAL",
|
||||
description = "Invalid file path",
|
||||
suggestion = "Check that the file path is correct and the file exists",
|
||||
},
|
||||
VAL_007 = {
|
||||
code = "FLEXLOVE_VAL_007",
|
||||
category = "VAL",
|
||||
description = "Invalid enum value",
|
||||
suggestion = "Use one of the allowed enum values for this property",
|
||||
},
|
||||
VAL_008 = {
|
||||
code = "FLEXLOVE_VAL_008",
|
||||
category = "VAL",
|
||||
description = "Invalid text input",
|
||||
suggestion = "Ensure text meets validation requirements (length, pattern, allowed characters)",
|
||||
},
|
||||
|
||||
local LOG_LEVELS = {
|
||||
-- Layout Errors (LAY_001 - LAY_099)
|
||||
LAY_001 = {
|
||||
code = "FLEXLOVE_LAY_001",
|
||||
category = "LAY",
|
||||
description = "Invalid flex direction",
|
||||
suggestion = "Use 'horizontal' or 'vertical' for flexDirection",
|
||||
},
|
||||
LAY_002 = {
|
||||
code = "FLEXLOVE_LAY_002",
|
||||
category = "LAY",
|
||||
description = "Circular dependency detected",
|
||||
suggestion = "Remove circular references in element hierarchy or layout constraints",
|
||||
},
|
||||
LAY_003 = {
|
||||
code = "FLEXLOVE_LAY_003",
|
||||
category = "LAY",
|
||||
description = "Invalid dimensions (negative or NaN)",
|
||||
suggestion = "Ensure width and height are positive numbers",
|
||||
},
|
||||
LAY_004 = {
|
||||
code = "FLEXLOVE_LAY_004",
|
||||
category = "LAY",
|
||||
description = "Layout calculation overflow",
|
||||
suggestion = "Reduce complexity of layout or increase recursion limit",
|
||||
},
|
||||
LAY_005 = {
|
||||
code = "FLEXLOVE_LAY_005",
|
||||
category = "LAY",
|
||||
description = "Invalid alignment value",
|
||||
suggestion = "Use valid alignment values (flex-start, center, flex-end, etc.)",
|
||||
},
|
||||
LAY_006 = {
|
||||
code = "FLEXLOVE_LAY_006",
|
||||
category = "LAY",
|
||||
description = "Invalid positioning mode",
|
||||
suggestion = "Use 'absolute', 'relative', 'flex', or 'grid' for positioning",
|
||||
},
|
||||
LAY_007 = {
|
||||
code = "FLEXLOVE_LAY_007",
|
||||
category = "LAY",
|
||||
description = "Grid layout error",
|
||||
suggestion = "Check grid template columns/rows and item placement",
|
||||
},
|
||||
|
||||
-- Rendering Errors (REN_001 - REN_099)
|
||||
REN_001 = {
|
||||
code = "FLEXLOVE_REN_001",
|
||||
category = "REN",
|
||||
description = "Invalid render state",
|
||||
suggestion = "Ensure element is properly initialized before rendering",
|
||||
},
|
||||
REN_002 = {
|
||||
code = "FLEXLOVE_REN_002",
|
||||
category = "REN",
|
||||
description = "Texture loading failed",
|
||||
suggestion = "Check image path and format, ensure file exists",
|
||||
},
|
||||
REN_003 = {
|
||||
code = "FLEXLOVE_REN_003",
|
||||
category = "REN",
|
||||
description = "Font loading failed",
|
||||
suggestion = "Check font path and format, ensure file exists",
|
||||
},
|
||||
REN_004 = {
|
||||
code = "FLEXLOVE_REN_004",
|
||||
category = "REN",
|
||||
description = "Invalid color value",
|
||||
suggestion = "Color components must be numbers between 0 and 1",
|
||||
},
|
||||
REN_005 = {
|
||||
code = "FLEXLOVE_REN_005",
|
||||
category = "REN",
|
||||
description = "Clipping stack overflow",
|
||||
suggestion = "Reduce nesting depth or check for missing scissor pops",
|
||||
},
|
||||
REN_006 = {
|
||||
code = "FLEXLOVE_REN_006",
|
||||
category = "REN",
|
||||
description = "Shader compilation failed",
|
||||
suggestion = "Check shader code for syntax errors",
|
||||
},
|
||||
REN_007 = {
|
||||
code = "FLEXLOVE_REN_007",
|
||||
category = "REN",
|
||||
description = "Invalid nine-patch configuration",
|
||||
suggestion = "Check nine-patch slice values and image dimensions",
|
||||
},
|
||||
|
||||
-- Theme Errors (THM_001 - THM_099)
|
||||
THM_001 = {
|
||||
code = "FLEXLOVE_THM_001",
|
||||
category = "THM",
|
||||
description = "Theme file not found",
|
||||
suggestion = "Check theme file path and ensure file exists",
|
||||
},
|
||||
THM_002 = {
|
||||
code = "FLEXLOVE_THM_002",
|
||||
category = "THM",
|
||||
description = "Invalid theme structure",
|
||||
suggestion = "Theme must return a table with 'name' and component styles",
|
||||
},
|
||||
THM_003 = {
|
||||
code = "FLEXLOVE_THM_003",
|
||||
category = "THM",
|
||||
description = "Required theme property missing",
|
||||
suggestion = "Ensure theme has required properties (name, base styles, etc.)",
|
||||
},
|
||||
THM_004 = {
|
||||
code = "FLEXLOVE_THM_004",
|
||||
category = "THM",
|
||||
description = "Invalid component style",
|
||||
suggestion = "Component styles must be tables with valid properties",
|
||||
},
|
||||
THM_005 = {
|
||||
code = "FLEXLOVE_THM_005",
|
||||
category = "THM",
|
||||
description = "Theme loading failed",
|
||||
suggestion = "Check theme file for Lua syntax errors",
|
||||
},
|
||||
THM_006 = {
|
||||
code = "FLEXLOVE_THM_006",
|
||||
category = "THM",
|
||||
description = "Invalid theme color",
|
||||
suggestion = "Theme colors must be valid color values (hex, rgba, Color object)",
|
||||
},
|
||||
|
||||
-- Event Errors (EVT_001 - EVT_099)
|
||||
EVT_001 = {
|
||||
code = "FLEXLOVE_EVT_001",
|
||||
category = "EVT",
|
||||
description = "Invalid event type",
|
||||
suggestion = "Use valid event types (mousepressed, textinput, etc.)",
|
||||
},
|
||||
EVT_002 = {
|
||||
code = "FLEXLOVE_EVT_002",
|
||||
category = "EVT",
|
||||
description = "Event handler error",
|
||||
suggestion = "Check event handler function for errors",
|
||||
},
|
||||
EVT_003 = {
|
||||
code = "FLEXLOVE_EVT_003",
|
||||
category = "EVT",
|
||||
description = "Event propagation error",
|
||||
suggestion = "Check event bubbling/capturing logic",
|
||||
},
|
||||
EVT_004 = {
|
||||
code = "FLEXLOVE_EVT_004",
|
||||
category = "EVT",
|
||||
description = "Invalid event target",
|
||||
suggestion = "Ensure event target element exists and is valid",
|
||||
},
|
||||
EVT_005 = {
|
||||
code = "FLEXLOVE_EVT_005",
|
||||
category = "EVT",
|
||||
description = "Event handler not a function",
|
||||
suggestion = "Event handlers must be functions",
|
||||
},
|
||||
|
||||
-- Resource Errors (RES_001 - RES_099)
|
||||
RES_001 = {
|
||||
code = "FLEXLOVE_RES_001",
|
||||
category = "RES",
|
||||
description = "File not found",
|
||||
suggestion = "Check file path and ensure file exists in the filesystem",
|
||||
},
|
||||
RES_002 = {
|
||||
code = "FLEXLOVE_RES_002",
|
||||
category = "RES",
|
||||
description = "Permission denied",
|
||||
suggestion = "Check file permissions and access rights",
|
||||
},
|
||||
RES_003 = {
|
||||
code = "FLEXLOVE_RES_003",
|
||||
category = "RES",
|
||||
description = "Invalid file format",
|
||||
suggestion = "Ensure file format is supported (png, jpg, ttf, etc.)",
|
||||
},
|
||||
RES_004 = {
|
||||
code = "FLEXLOVE_RES_004",
|
||||
category = "RES",
|
||||
description = "Resource loading failed",
|
||||
suggestion = "Check file integrity and format compatibility",
|
||||
},
|
||||
RES_005 = {
|
||||
code = "FLEXLOVE_RES_005",
|
||||
category = "RES",
|
||||
description = "Image cache error",
|
||||
suggestion = "Clear image cache or check memory availability",
|
||||
},
|
||||
|
||||
-- System Errors (SYS_001 - SYS_099)
|
||||
SYS_001 = {
|
||||
code = "FLEXLOVE_SYS_001",
|
||||
category = "SYS",
|
||||
description = "Memory allocation failed",
|
||||
suggestion = "Reduce memory usage or check available memory",
|
||||
},
|
||||
SYS_002 = {
|
||||
code = "FLEXLOVE_SYS_002",
|
||||
category = "SYS",
|
||||
description = "Stack overflow",
|
||||
suggestion = "Reduce recursion depth or check for infinite loops",
|
||||
},
|
||||
SYS_003 = {
|
||||
code = "FLEXLOVE_SYS_003",
|
||||
category = "SYS",
|
||||
description = "Invalid state",
|
||||
suggestion = "Check initialization order and state management",
|
||||
},
|
||||
SYS_004 = {
|
||||
code = "FLEXLOVE_SYS_004",
|
||||
category = "SYS",
|
||||
description = "Module initialization failed",
|
||||
suggestion = "Check module dependencies and initialization order",
|
||||
},
|
||||
|
||||
-- Performance Warnings (PERF_001 - PERF_099)
|
||||
PERF_001 = {
|
||||
code = "FLEXLOVE_PERF_001",
|
||||
category = "PERF",
|
||||
description = "Performance threshold exceeded",
|
||||
suggestion = "Operation took longer than recommended. Monitor for patterns.",
|
||||
},
|
||||
PERF_002 = {
|
||||
code = "FLEXLOVE_PERF_002",
|
||||
category = "PERF",
|
||||
description = "Critical performance threshold exceeded",
|
||||
suggestion = "Operation is causing frame drops. Consider optimizing or reducing frequency.",
|
||||
},
|
||||
|
||||
-- Memory Warnings (MEM_001 - MEM_099)
|
||||
MEM_001 = {
|
||||
code = "FLEXLOVE_MEM_001",
|
||||
category = "MEM",
|
||||
description = "Memory leak detected",
|
||||
suggestion = "Table is growing consistently. Review cache eviction policies and ensure objects are properly released.",
|
||||
},
|
||||
|
||||
-- State Management Warnings (STATE_001 - STATE_099)
|
||||
STATE_001 = {
|
||||
code = "FLEXLOVE_STATE_001",
|
||||
category = "STATE",
|
||||
description = "CallSite counters accumulating",
|
||||
suggestion = "This indicates incrementFrame() may not be called properly. Check immediate mode frame management.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
--- Get error information by code
|
||||
--- @param code string Error code (e.g., "VAL_001" or "FLEXLOVE_VAL_001")
|
||||
--- @return table? errorInfo Error information or nil if not found
|
||||
function ErrorCodes.get(code)
|
||||
-- Handle both short and full format
|
||||
local shortCode = code:gsub("^FLEXLOVE_", "")
|
||||
return ErrorCodes.codes[shortCode]
|
||||
end
|
||||
|
||||
--- Get human-readable description for error code
|
||||
--- @param code string Error code
|
||||
--- @return string description Error description
|
||||
function ErrorCodes.describe(code)
|
||||
local info = ErrorCodes.get(code)
|
||||
if info then
|
||||
return info.description
|
||||
end
|
||||
return "Unknown error code: " .. code
|
||||
end
|
||||
|
||||
--- Get suggested fix for error code
|
||||
--- @param code string Error code
|
||||
--- @return string suggestion Suggested fix
|
||||
function ErrorCodes.getSuggestion(code)
|
||||
local info = ErrorCodes.get(code)
|
||||
if info then
|
||||
return info.suggestion
|
||||
end
|
||||
return "No suggestion available"
|
||||
end
|
||||
|
||||
--- Get category for error code
|
||||
--- @param code string Error code
|
||||
--- @return string category Error category name
|
||||
function ErrorCodes.getCategory(code)
|
||||
local info = ErrorCodes.get(code)
|
||||
if info then
|
||||
return ErrorCodes.categories[info.category] or info.category
|
||||
end
|
||||
return "Unknown"
|
||||
end
|
||||
|
||||
--- List all error codes in a category
|
||||
--- @param category string Category code (e.g., "VAL", "LAY")
|
||||
--- @return table codes List of error codes in category
|
||||
function ErrorCodes.listByCategory(category)
|
||||
local result = {}
|
||||
for code, info in pairs(ErrorCodes.codes) do
|
||||
if info.category == category then
|
||||
table.insert(result, {
|
||||
code = code,
|
||||
fullCode = info.code,
|
||||
description = info.description,
|
||||
suggestion = info.suggestion,
|
||||
})
|
||||
end
|
||||
end
|
||||
table.sort(result, function(a, b)
|
||||
return a.code < b.code
|
||||
end)
|
||||
return result
|
||||
end
|
||||
|
||||
--- Search error codes by keyword
|
||||
--- @param keyword string Keyword to search for
|
||||
--- @return table codes Matching error codes
|
||||
function ErrorCodes.search(keyword)
|
||||
keyword = keyword:lower()
|
||||
local result = {}
|
||||
for code, info in pairs(ErrorCodes.codes) do
|
||||
local searchText = (code .. " " .. info.description .. " " .. info.suggestion):lower()
|
||||
if searchText:find(keyword, 1, true) then
|
||||
table.insert(result, {
|
||||
code = code,
|
||||
fullCode = info.code,
|
||||
description = info.description,
|
||||
suggestion = info.suggestion,
|
||||
category = ErrorCodes.categories[info.category],
|
||||
})
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
--- Get all error codes
|
||||
--- @return table codes All error codes
|
||||
function ErrorCodes.listAll()
|
||||
local result = {}
|
||||
for code, info in pairs(ErrorCodes.codes) do
|
||||
table.insert(result, {
|
||||
code = code,
|
||||
fullCode = info.code,
|
||||
description = info.description,
|
||||
suggestion = info.suggestion,
|
||||
category = ErrorCodes.categories[info.category],
|
||||
})
|
||||
end
|
||||
table.sort(result, function(a, b)
|
||||
return a.code < b.code
|
||||
end)
|
||||
return result
|
||||
end
|
||||
|
||||
--- Format error message with code
|
||||
--- @param code string Error code
|
||||
--- @param message string Error message
|
||||
--- @return string formattedMessage Formatted error message with code
|
||||
function ErrorCodes.formatMessage(code, message)
|
||||
local info = ErrorCodes.get(code)
|
||||
if info then
|
||||
return string.format("[%s] %s", info.code, message)
|
||||
end
|
||||
return message
|
||||
end
|
||||
|
||||
--- Validate that all error codes are unique and properly formatted
|
||||
--- @return boolean, string? Returns true if valid, or false with error message
|
||||
function ErrorCodes.validate()
|
||||
local seen = {}
|
||||
local fullCodes = {}
|
||||
|
||||
for code, info in pairs(ErrorCodes.codes) do
|
||||
-- Check for duplicates
|
||||
if seen[code] then
|
||||
return false, "Duplicate error code: " .. code
|
||||
end
|
||||
seen[code] = true
|
||||
|
||||
if fullCodes[info.code] then
|
||||
return false, "Duplicate full error code: " .. info.code
|
||||
end
|
||||
fullCodes[info.code] = true
|
||||
|
||||
-- Check format
|
||||
if not code:match("^[A-Z]+_[0-9]+$") then
|
||||
return false, "Invalid code format: " .. code .. " (expected CATEGORY_NUMBER)"
|
||||
end
|
||||
|
||||
-- Check full code format
|
||||
local expectedFullCode = "FLEXLOVE_" .. code
|
||||
if info.code ~= expectedFullCode then
|
||||
return false, "Mismatched full code for " .. code .. ": expected " .. expectedFullCode .. ", got " .. info.code
|
||||
end
|
||||
|
||||
-- Check required fields
|
||||
if not info.description or info.description == "" then
|
||||
return false, "Missing description for " .. code
|
||||
end
|
||||
if not info.suggestion or info.suggestion == "" then
|
||||
return false, "Missing suggestion for " .. code
|
||||
end
|
||||
if not info.category or info.category == "" then
|
||||
return false, "Missing category for " .. code
|
||||
end
|
||||
end
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
---@enum LOG_LEVEL
|
||||
local LOG_LEVEL = {
|
||||
CRITICAL = 1,
|
||||
ERROR = 2,
|
||||
WARNING = 3,
|
||||
@@ -9,124 +478,64 @@ local LOG_LEVELS = {
|
||||
DEBUG = 5,
|
||||
}
|
||||
|
||||
local config = {
|
||||
debugMode = false,
|
||||
includeStackTrace = false,
|
||||
logLevel = LOG_LEVELS.WARNING, -- Default: log errors and warnings
|
||||
logTarget = "console", -- Options: "console", "file", "both", "none"
|
||||
logFormat = "human", -- Options: "human", "json"
|
||||
logFile = "flexlove-errors.log",
|
||||
maxLogSize = 10 * 1024 * 1024, -- 10MB default
|
||||
maxLogFiles = 5, -- Keep 5 rotated logs
|
||||
enableRotation = true,
|
||||
---@enum LOG_TARGET
|
||||
local LOG_TARGET = {
|
||||
CONSOLE = "console",
|
||||
FILE = "file",
|
||||
BOTH = "both",
|
||||
NONE = "none",
|
||||
}
|
||||
|
||||
-- Internal state
|
||||
local logFileHandle = nil
|
||||
local currentLogSize = 0
|
||||
---@class ErrorHandler
|
||||
---@field errorCodes ErrorCodes
|
||||
---@field includeStackTrace boolean -- Default: false
|
||||
---@field logLevel LOG_LEVEL --Default: LOG_LEVEL.WARNING
|
||||
---@field logTarget "console" | "file" | "both"
|
||||
---@field logFile string
|
||||
---@field maxLogSize number in bytes
|
||||
---@field maxLogFiles number files to rotate
|
||||
---@field enableRotation boolean see maxLogFiles
|
||||
---@field _currentLogSize number private
|
||||
---@field _logFileHandle file* private
|
||||
local ErrorHandler = {
|
||||
errorCodes = ErrorCodes,
|
||||
}
|
||||
ErrorHandler.__index = ErrorHandler
|
||||
|
||||
--- Initialize ErrorHandler with dependencies
|
||||
---@param deps table Dependencies table with ErrorCodes
|
||||
function ErrorHandler.init(deps)
|
||||
if deps and deps.ErrorCodes then
|
||||
ErrorCodes = deps.ErrorCodes
|
||||
else
|
||||
-- Try to require if not provided (backward compatibility)
|
||||
local success, module = pcall(require, "modules.ErrorCodes")
|
||||
if success then
|
||||
ErrorCodes = module
|
||||
else
|
||||
-- Create minimal stub if ErrorCodes not available
|
||||
ErrorCodes = {
|
||||
get = function() return nil end,
|
||||
describe = function(code) return code end,
|
||||
getSuggestion = function() return "" end,
|
||||
}
|
||||
end
|
||||
---@type ErrorHandler|nil
|
||||
local instance = nil
|
||||
|
||||
---@param config { includeStackTrace?: boolean, logLevel?: LOG_LEVEL, logTarget?: "console" | "file" | "both", logFile?: string, maxLogSize?: number, maxLogFiles?: number, enableRotation?: boolean }|nil
|
||||
---@return ErrorHandler
|
||||
function ErrorHandler.init(config)
|
||||
if instance == nil then
|
||||
local self = setmetatable({}, ErrorHandler)
|
||||
self.includeStackTrace = config and config.includeStackTrace or false
|
||||
self.logLevel = config and config.logLevel or LOG_LEVEL.WARNING
|
||||
self.logTarget = config and config.logTarget or LOG_TARGET.CONSOLE
|
||||
self.logFile = config and config.logFile or "flexlove-errors.log"
|
||||
self.maxLogSize = config and config.maxLogSize or 10 * 1024 * 1024
|
||||
self.maxLogFiles = config and config.maxLogFiles or 5
|
||||
self.enableRotation = config and config.enableRotation or true
|
||||
self._currentLogSize = 0
|
||||
self._logFileHandle = nil
|
||||
instance = self
|
||||
end
|
||||
return instance
|
||||
end
|
||||
|
||||
--- Set debug mode (enables stack traces and verbose output)
|
||||
---@param enabled boolean Enable debug mode
|
||||
function ErrorHandler.setDebugMode(enabled)
|
||||
config.debugMode = enabled
|
||||
config.includeStackTrace = enabled
|
||||
if enabled then
|
||||
config.logLevel = LOG_LEVELS.DEBUG
|
||||
end
|
||||
end
|
||||
|
||||
--- Set whether to include stack traces
|
||||
---@param enabled boolean Enable stack traces
|
||||
function ErrorHandler.setStackTrace(enabled)
|
||||
config.includeStackTrace = enabled
|
||||
end
|
||||
|
||||
--- Set log level (minimum level to log)
|
||||
---@param level string|number Log level ("CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG") or number
|
||||
function ErrorHandler.setLogLevel(level)
|
||||
if type(level) == "string" then
|
||||
config.logLevel = LOG_LEVELS[level:upper()] or LOG_LEVELS.WARNING
|
||||
elseif type(level) == "number" then
|
||||
config.logLevel = level
|
||||
end
|
||||
end
|
||||
|
||||
--- Set log target
|
||||
---@param target string "console", "file", "both", or "none"
|
||||
function ErrorHandler.setLogTarget(target)
|
||||
config.logTarget = target
|
||||
-- Note: File will be opened lazily on first write
|
||||
if target == "console" or target == "none" then
|
||||
-- Close log file if open
|
||||
if logFileHandle then
|
||||
logFileHandle:close()
|
||||
logFileHandle = nil
|
||||
currentLogSize = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Set log format
|
||||
---@param format string "human" or "json"
|
||||
function ErrorHandler.setLogFormat(format)
|
||||
config.logFormat = format
|
||||
end
|
||||
|
||||
--- Set log file path
|
||||
---@param path string Path to log file
|
||||
function ErrorHandler.setLogFile(path)
|
||||
-- Close existing log file
|
||||
if logFileHandle then
|
||||
logFileHandle:close()
|
||||
logFileHandle = nil
|
||||
end
|
||||
|
||||
config.logFile = path
|
||||
currentLogSize = 0
|
||||
|
||||
-- Note: File will be opened lazily on first write
|
||||
end
|
||||
|
||||
--- Enable/disable log rotation
|
||||
---@param enabled boolean|table Enable rotation or config table
|
||||
function ErrorHandler.enableLogRotation(enabled)
|
||||
if type(enabled) == "boolean" then
|
||||
config.enableRotation = enabled
|
||||
elseif type(enabled) == "table" then
|
||||
config.enableRotation = true
|
||||
if enabled.maxSize then
|
||||
config.maxLogSize = enabled.maxSize
|
||||
end
|
||||
if enabled.maxFiles then
|
||||
config.maxLogFiles = enabled.maxFiles
|
||||
end
|
||||
--- Get the singleton instance (lazily initializes if needed)
|
||||
---@return ErrorHandler
|
||||
function ErrorHandler.getInstance()
|
||||
if instance == nil then
|
||||
ErrorHandler.init()
|
||||
end
|
||||
return instance
|
||||
end
|
||||
|
||||
--- Get current timestamp with milliseconds
|
||||
---@return string Formatted timestamp
|
||||
local function getTimestamp()
|
||||
---@return string|osdate Formatted timestamp
|
||||
function ErrorHandler:_getTimestamp()
|
||||
local time = os.time()
|
||||
local date = os.date("%Y-%m-%d %H:%M:%S", time)
|
||||
-- Note: Lua doesn't have millisecond precision by default, so we approximate
|
||||
@@ -134,39 +543,39 @@ local function getTimestamp()
|
||||
end
|
||||
|
||||
--- Rotate log file if needed
|
||||
local function rotateLogIfNeeded()
|
||||
if not config.enableRotation then
|
||||
function ErrorHandler:_rotateLogIfNeeded()
|
||||
if not self.enableRotation then
|
||||
return
|
||||
end
|
||||
if currentLogSize < config.maxLogSize then
|
||||
if self._currentLogSize < self.maxLogSize then
|
||||
return
|
||||
end
|
||||
|
||||
-- Close current log
|
||||
if logFileHandle then
|
||||
logFileHandle:close()
|
||||
logFileHandle = nil
|
||||
if self._logFileHandle then
|
||||
self._logFileHandle:close()
|
||||
self._logFileHandle = nil
|
||||
end
|
||||
|
||||
-- Rotate existing logs
|
||||
for i = config.maxLogFiles - 1, 1, -1 do
|
||||
local oldName = config.logFile .. "." .. i
|
||||
local newName = config.logFile .. "." .. (i + 1)
|
||||
for i = self.maxLogFiles - 1, 1, -1 do
|
||||
local oldName = self.logFile .. "." .. i
|
||||
local newName = self.logFile .. "." .. (i + 1)
|
||||
os.rename(oldName, newName) -- Will fail silently if file doesn't exist
|
||||
end
|
||||
|
||||
-- Move current log to .1
|
||||
os.rename(config.logFile, config.logFile .. ".1")
|
||||
os.rename(self.logFile, self.logFile .. ".1")
|
||||
|
||||
-- Create new log file
|
||||
logFileHandle = io.open(config.logFile, "a")
|
||||
currentLogSize = 0
|
||||
self._logFileHandle = io.open(self.logFile, "a")
|
||||
self._currentLogSize = 0
|
||||
end
|
||||
|
||||
--- Escape string for JSON
|
||||
---@param str string String to escape
|
||||
---@return string Escaped string
|
||||
local function escapeJson(str)
|
||||
function ErrorHandler:_escapeJson(str)
|
||||
str = tostring(str)
|
||||
str = str:gsub("\\", "\\\\")
|
||||
str = str:gsub('"', '\\"')
|
||||
@@ -179,15 +588,15 @@ end
|
||||
--- Format details as JSON object
|
||||
---@param details table|nil Details object
|
||||
---@return string JSON string
|
||||
local function formatDetailsJson(details)
|
||||
function ErrorHandler:_formatDetailsJson(details)
|
||||
if not details or type(details) ~= "table" then
|
||||
return "{}"
|
||||
end
|
||||
|
||||
local parts = {}
|
||||
for key, value in pairs(details) do
|
||||
local jsonKey = escapeJson(tostring(key))
|
||||
local jsonValue = escapeJson(tostring(value))
|
||||
local jsonKey = self:_escapeJson(tostring(key))
|
||||
local jsonValue = self:_escapeJson(tostring(value))
|
||||
table.insert(parts, string.format('"%s":"%s"', jsonKey, jsonValue))
|
||||
end
|
||||
|
||||
@@ -197,7 +606,7 @@ end
|
||||
--- Format details object as readable key-value pairs
|
||||
---@param details table|nil Details object
|
||||
---@return string Formatted details
|
||||
local function formatDetails(details)
|
||||
function ErrorHandler:_formatDetails(details)
|
||||
if not details or type(details) ~= "table" then
|
||||
return ""
|
||||
end
|
||||
@@ -222,8 +631,8 @@ end
|
||||
--- Extract and format stack trace
|
||||
---@param level number Stack level to start from
|
||||
---@return string Formatted stack trace
|
||||
local function formatStackTrace(level)
|
||||
if not config.includeStackTrace then
|
||||
function ErrorHandler:_formatStackTrace(level)
|
||||
if not self.includeStackTrace then
|
||||
return ""
|
||||
end
|
||||
|
||||
@@ -263,7 +672,7 @@ end
|
||||
---@param detailsOrSuggestion table|string|nil Details or suggestion
|
||||
---@param suggestionOrNil string|nil Suggestion
|
||||
---@return string Formatted message
|
||||
local function formatMessage(module, level, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestionOrNil)
|
||||
function ErrorHandler:_formatMessage(module, level, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestionOrNil)
|
||||
local code = nil
|
||||
local message = codeOrMessage
|
||||
local details = nil
|
||||
@@ -307,7 +716,7 @@ local function formatMessage(module, level, codeOrMessage, messageOrDetails, det
|
||||
|
||||
-- Details section
|
||||
if details then
|
||||
table.insert(parts, formatDetails(details))
|
||||
table.insert(parts, self:_formatDetails(details))
|
||||
end
|
||||
|
||||
-- Suggestion section
|
||||
@@ -326,91 +735,65 @@ end
|
||||
---@param message string Message
|
||||
---@param details table|nil Details
|
||||
---@param suggestion string|nil Suggestion
|
||||
local function writeLog(level, levelNum, module, code, message, details, suggestion)
|
||||
function ErrorHandler:_writeLog(level, levelNum, module, code, message, details, suggestion)
|
||||
-- Check if we should log this level
|
||||
if levelNum > config.logLevel then
|
||||
if levelNum > self.logLevel then
|
||||
return
|
||||
end
|
||||
|
||||
local timestamp = getTimestamp()
|
||||
local timestamp = self:_getTimestamp()
|
||||
local logEntry
|
||||
|
||||
if config.logFormat == "json" then
|
||||
-- JSON format
|
||||
local jsonParts = {
|
||||
string.format('"timestamp":"%s"', escapeJson(timestamp)),
|
||||
string.format('"level":"%s"', level),
|
||||
string.format('"module":"%s"', escapeJson(module)),
|
||||
string.format('"message":"%s"', escapeJson(message)),
|
||||
}
|
||||
local jsonParts = {
|
||||
string.format('"timestamp":"%s"', self:_escapeJson(timestamp)),
|
||||
string.format('"level":"%s"', level),
|
||||
string.format('"module":"%s"', self:_escapeJson(module)),
|
||||
string.format('"message":"%s"', self:_escapeJson(message)),
|
||||
}
|
||||
|
||||
if code then
|
||||
table.insert(jsonParts, string.format('"code":"%s"', escapeJson(code)))
|
||||
end
|
||||
|
||||
if details then
|
||||
table.insert(jsonParts, string.format('"details":%s', formatDetailsJson(details)))
|
||||
end
|
||||
|
||||
if suggestion then
|
||||
table.insert(jsonParts, string.format('"suggestion":"%s"', escapeJson(suggestion)))
|
||||
end
|
||||
|
||||
logEntry = "{" .. table.concat(jsonParts, ",") .. "}\n"
|
||||
else
|
||||
-- Human-readable format
|
||||
local parts = {
|
||||
string.format("[%s] [%s] [%s]", timestamp, level, module),
|
||||
}
|
||||
|
||||
if code then
|
||||
table.insert(parts, string.format("[%s]", code))
|
||||
end
|
||||
|
||||
table.insert(parts, message)
|
||||
logEntry = table.concat(parts, " ") .. "\n"
|
||||
|
||||
if details then
|
||||
logEntry = logEntry .. formatDetails(details):gsub("^\n\n", "") .. "\n"
|
||||
end
|
||||
|
||||
if suggestion then
|
||||
logEntry = logEntry .. "Suggestion: " .. suggestion .. "\n"
|
||||
end
|
||||
|
||||
logEntry = logEntry .. "\n"
|
||||
if code then
|
||||
table.insert(jsonParts, string.format('"code":"%s"', self:_escapeJson(code)))
|
||||
end
|
||||
|
||||
-- Write to console
|
||||
if config.logTarget == "console" or config.logTarget == "both" then
|
||||
if details then
|
||||
table.insert(jsonParts, string.format('"details":%s', self:_formatDetailsJson(details)))
|
||||
end
|
||||
|
||||
if suggestion then
|
||||
table.insert(jsonParts, string.format('"suggestion":"%s"', self:_escapeJson(suggestion)))
|
||||
end
|
||||
|
||||
logEntry = "{" .. table.concat(jsonParts, ",") .. "}\n"
|
||||
|
||||
if self.logTarget == "console" or self.logTarget == "both" then
|
||||
io.write(logEntry)
|
||||
io.flush()
|
||||
end
|
||||
|
||||
-- Write to file
|
||||
if config.logTarget == "file" or config.logTarget == "both" then
|
||||
if self.logTarget == "file" or self.logTarget == "both" then
|
||||
-- Lazy file opening: open on first write
|
||||
if not logFileHandle then
|
||||
logFileHandle = io.open(config.logFile, "a")
|
||||
if logFileHandle then
|
||||
if not self._logFileHandle then
|
||||
self._logFileHandle = io.open(self.logFile, "a")
|
||||
if self._logFileHandle then
|
||||
-- Get current file size
|
||||
local currentPos = logFileHandle:seek("end")
|
||||
currentLogSize = currentPos or 0
|
||||
local currentPos = self._logFileHandle:seek("end")
|
||||
self._currentLogSize = currentPos or 0
|
||||
end
|
||||
end
|
||||
|
||||
if logFileHandle then
|
||||
rotateLogIfNeeded()
|
||||
if self._logFileHandle then
|
||||
self:_rotateLogIfNeeded()
|
||||
|
||||
-- Reopen if rotation closed it
|
||||
if not logFileHandle then
|
||||
logFileHandle = io.open(config.logFile, "a")
|
||||
if not self._logFileHandle then
|
||||
self._logFileHandle = io.open(self.logFile, "a")
|
||||
end
|
||||
|
||||
if logFileHandle then
|
||||
logFileHandle:write(logEntry)
|
||||
logFileHandle:flush()
|
||||
currentLogSize = currentLogSize + #logEntry
|
||||
if self._logFileHandle then
|
||||
self._logFileHandle:write(logEntry)
|
||||
self._logFileHandle:flush()
|
||||
self._currentLogSize = self._currentLogSize + #logEntry
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -422,8 +805,8 @@ end
|
||||
---@param messageOrDetails string|table|nil Message or details
|
||||
---@param detailsOrSuggestion table|string|nil Details or suggestion
|
||||
---@param suggestion string|nil Suggestion
|
||||
function ErrorHandler.error(module, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
|
||||
local formattedMessage = formatMessage(module, "Error", codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
|
||||
function ErrorHandler:error(module, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
|
||||
local formattedMessage = self:_formatMessage(module, "Error", codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
|
||||
|
||||
-- Parse arguments for logging
|
||||
local code = nil
|
||||
@@ -454,11 +837,10 @@ function ErrorHandler.error(module, codeOrMessage, messageOrDetails, detailsOrSu
|
||||
end
|
||||
|
||||
-- Log the error
|
||||
writeLog("ERROR", LOG_LEVELS.ERROR, module, code, message, details, logSuggestion)
|
||||
self:_writeLog("ERROR", LOG_LEVEL.ERROR, module, code, message, details, logSuggestion)
|
||||
|
||||
-- Add stack trace if enabled
|
||||
if config.includeStackTrace then
|
||||
formattedMessage = formattedMessage .. formatStackTrace(3)
|
||||
if self.includeStackTrace then
|
||||
formattedMessage = formattedMessage .. self:_formatStackTrace(3)
|
||||
end
|
||||
|
||||
error(formattedMessage, 2)
|
||||
@@ -470,7 +852,7 @@ end
|
||||
---@param messageOrDetails string|table|nil Message or details
|
||||
---@param detailsOrSuggestion table|string|nil Details or suggestion
|
||||
---@param suggestion string|nil Suggestion
|
||||
function ErrorHandler.warn(module, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
|
||||
function ErrorHandler:warn(module, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
|
||||
-- Parse arguments for logging
|
||||
local code = nil
|
||||
local message = codeOrMessage
|
||||
@@ -499,8 +881,8 @@ function ErrorHandler.warn(module, codeOrMessage, messageOrDetails, detailsOrSug
|
||||
end
|
||||
end
|
||||
|
||||
-- Log the warning (writeLog handles console output based on config.logTarget)
|
||||
writeLog("WARNING", LOG_LEVELS.WARNING, module, code, message, details, logSuggestion)
|
||||
-- Log the warning
|
||||
self:_writeLog("WARNING", LOG_LEVEL.WARNING, module, code, message, details, logSuggestion)
|
||||
end
|
||||
|
||||
--- Validate that a value is not nil
|
||||
@@ -508,9 +890,9 @@ end
|
||||
---@param value any The value to check
|
||||
---@param paramName string The parameter name
|
||||
---@return boolean True if valid
|
||||
function ErrorHandler.assertNotNil(module, value, paramName)
|
||||
function ErrorHandler:assertNotNil(module, value, paramName)
|
||||
if value == nil then
|
||||
ErrorHandler.error(module, "VAL_003", "Required parameter missing", {
|
||||
self:error(module, "VAL_003", "Required parameter missing", {
|
||||
parameter = paramName,
|
||||
})
|
||||
return false
|
||||
@@ -524,10 +906,10 @@ end
|
||||
---@param expectedType string The expected type name
|
||||
---@param paramName string The parameter name
|
||||
---@return boolean True if valid
|
||||
function ErrorHandler.assertType(module, value, expectedType, paramName)
|
||||
function ErrorHandler:assertType(module, value, expectedType, paramName)
|
||||
local actualType = type(value)
|
||||
if actualType ~= expectedType then
|
||||
ErrorHandler.error(module, "VAL_001", "Invalid property type", {
|
||||
self:error(module, "VAL_001", "Invalid property type", {
|
||||
property = paramName,
|
||||
expected = expectedType,
|
||||
got = actualType,
|
||||
@@ -544,9 +926,9 @@ end
|
||||
---@param max number Maximum value (inclusive)
|
||||
---@param paramName string The parameter name
|
||||
---@return boolean True if valid
|
||||
function ErrorHandler.assertRange(module, value, min, max, paramName)
|
||||
function ErrorHandler:assertRange(module, value, min, max, paramName)
|
||||
if value < min or value > max then
|
||||
ErrorHandler.error(module, "VAL_002", "Property value out of range", {
|
||||
self:error(module, "VAL_002", "Property value out of range", {
|
||||
property = paramName,
|
||||
min = tostring(min),
|
||||
max = tostring(max),
|
||||
@@ -561,16 +943,16 @@ end
|
||||
---@param module string The module name
|
||||
---@param oldName string The deprecated name
|
||||
---@param newName string The new name to use
|
||||
function ErrorHandler.warnDeprecated(module, oldName, newName)
|
||||
ErrorHandler.warn(module, string.format("'%s' is deprecated. Use '%s' instead", oldName, newName))
|
||||
function ErrorHandler:warnDeprecated(module, oldName, newName)
|
||||
self:warn(module, string.format("'%s' is deprecated. Use '%s' instead", oldName, newName))
|
||||
end
|
||||
|
||||
--- Warn about a common mistake
|
||||
---@param module string The module name
|
||||
---@param issue string Description of the issue
|
||||
---@param suggestion string Suggested fix
|
||||
function ErrorHandler.warnCommonMistake(module, issue, suggestion)
|
||||
ErrorHandler.warn(module, string.format("%s. Suggestion: %s", issue, suggestion))
|
||||
function ErrorHandler:warnCommonMistake(module, issue, suggestion)
|
||||
self:warn(module, string.format("%s. Suggestion: %s", issue, suggestion))
|
||||
end
|
||||
|
||||
return ErrorHandler
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
--- Transform module for 2D transformations (rotate, scale, translate, skew)
|
||||
---@class Transform
|
||||
---@field rotate number? Rotation in radians (default: 0)
|
||||
---@field scaleX number? X-axis scale (default: 1)
|
||||
---@field scaleY number? Y-axis scale (default: 1)
|
||||
---@field translateX number? X translation in pixels (default: 0)
|
||||
---@field translateY number? Y translation in pixels (default: 0)
|
||||
---@field skewX number? X-axis skew in radians (default: 0)
|
||||
---@field skewY number? Y-axis skew in radians (default: 0)
|
||||
---@field originX number? Transform origin X (0-1, default: 0.5)
|
||||
---@field originY number? Transform origin Y (0-1, default: 0.5)
|
||||
local Transform = {}
|
||||
Transform.__index = Transform
|
||||
|
||||
--- Create a new transform instance
|
||||
---@param props TransformProps?
|
||||
---@return Transform transform
|
||||
function Transform.new(props)
|
||||
props = props or {}
|
||||
|
||||
local self = setmetatable({}, Transform)
|
||||
|
||||
self.rotate = props.rotate or 0
|
||||
self.scaleX = props.scaleX or 1
|
||||
self.scaleY = props.scaleY or 1
|
||||
self.translateX = props.translateX or 0
|
||||
self.translateY = props.translateY or 0
|
||||
self.skewX = props.skewX or 0
|
||||
self.skewY = props.skewY or 0
|
||||
self.originX = props.originX or 0.5
|
||||
self.originY = props.originY or 0.5
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Apply transform to LÖVE graphics context
|
||||
---@param transform Transform Transform instance
|
||||
---@param x number Element x position
|
||||
---@param y number Element y position
|
||||
---@param width number Element width
|
||||
---@param height number Element height
|
||||
function Transform.apply(transform, x, y, width, height)
|
||||
if not transform then
|
||||
return
|
||||
end
|
||||
|
||||
-- Calculate transform origin
|
||||
local ox = x + width * transform.originX
|
||||
local oy = y + height * transform.originY
|
||||
|
||||
-- Apply transform in correct order: translate → rotate → scale → skew
|
||||
love.graphics.push()
|
||||
love.graphics.translate(ox, oy)
|
||||
|
||||
if transform.rotate ~= 0 then
|
||||
love.graphics.rotate(transform.rotate)
|
||||
end
|
||||
|
||||
if transform.scaleX ~= 1 or transform.scaleY ~= 1 then
|
||||
love.graphics.scale(transform.scaleX, transform.scaleY)
|
||||
end
|
||||
|
||||
if transform.skewX ~= 0 or transform.skewY ~= 0 then
|
||||
love.graphics.shear(transform.skewX, transform.skewY)
|
||||
end
|
||||
|
||||
love.graphics.translate(-ox, -oy)
|
||||
love.graphics.translate(transform.translateX, transform.translateY)
|
||||
end
|
||||
|
||||
--- Remove transform from LÖVE graphics context
|
||||
function Transform.unapply()
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
--- Interpolate between two transforms
|
||||
---@param from Transform Starting transform
|
||||
---@param to Transform Ending transform
|
||||
---@param t number Interpolation factor (0-1)
|
||||
---@return Transform interpolated
|
||||
function Transform.lerp(from, to, t)
|
||||
-- Sanitize inputs
|
||||
if type(from) ~= "table" then
|
||||
from = Transform.new()
|
||||
end
|
||||
if type(to) ~= "table" then
|
||||
to = Transform.new()
|
||||
end
|
||||
if type(t) ~= "number" or t ~= t then
|
||||
-- NaN or invalid type
|
||||
t = 0
|
||||
elseif t == math.huge then
|
||||
-- Positive infinity
|
||||
t = 1
|
||||
elseif t == -math.huge then
|
||||
-- Negative infinity
|
||||
t = 0
|
||||
else
|
||||
-- Clamp t to 0-1 range
|
||||
t = math.max(0, math.min(1, t))
|
||||
end
|
||||
|
||||
return Transform.new({
|
||||
rotate = (from.rotate or 0) * (1 - t) + (to.rotate or 0) * t,
|
||||
scaleX = (from.scaleX or 1) * (1 - t) + (to.scaleX or 1) * t,
|
||||
scaleY = (from.scaleY or 1) * (1 - t) + (to.scaleY or 1) * t,
|
||||
translateX = (from.translateX or 0) * (1 - t) + (to.translateX or 0) * t,
|
||||
translateY = (from.translateY or 0) * (1 - t) + (to.translateY or 0) * t,
|
||||
skewX = (from.skewX or 0) * (1 - t) + (to.skewX or 0) * t,
|
||||
skewY = (from.skewY or 0) * (1 - t) + (to.skewY or 0) * t,
|
||||
originX = (from.originX or 0.5) * (1 - t) + (to.originX or 0.5) * t,
|
||||
originY = (from.originY or 0.5) * (1 - t) + (to.originY or 0.5) * t,
|
||||
})
|
||||
end
|
||||
|
||||
--- Check if transform is identity (no transformation)
|
||||
---@param transform Transform
|
||||
---@return boolean isIdentity
|
||||
function Transform.isIdentity(transform)
|
||||
if not transform then
|
||||
return true
|
||||
end
|
||||
|
||||
return transform.rotate == 0
|
||||
and transform.scaleX == 1
|
||||
and transform.scaleY == 1
|
||||
and transform.translateX == 0
|
||||
and transform.translateY == 0
|
||||
and transform.skewX == 0
|
||||
and transform.skewY == 0
|
||||
end
|
||||
|
||||
--- Clone a transform
|
||||
---@param transform Transform
|
||||
---@return Transform clone
|
||||
function Transform.clone(transform)
|
||||
if not transform then
|
||||
return Transform.new()
|
||||
end
|
||||
|
||||
return Transform.new({
|
||||
rotate = transform.rotate,
|
||||
scaleX = transform.scaleX,
|
||||
scaleY = transform.scaleY,
|
||||
translateX = transform.translateX,
|
||||
translateY = transform.translateY,
|
||||
skewX = transform.skewX,
|
||||
skewY = transform.skewY,
|
||||
originX = transform.originX,
|
||||
originY = transform.originY,
|
||||
})
|
||||
end
|
||||
|
||||
return Transform
|
||||
@@ -7,6 +7,10 @@ require("testing.loveStub")
|
||||
|
||||
-- Import the Color module
|
||||
local Color = require("modules.Color")
|
||||
local ErrorHandler = require("modules.ErrorHandler")
|
||||
local ErrorCodes = require("modules.ErrorCodes")
|
||||
ErrorHandler.init({ ErrorCodes })
|
||||
Color.init({ ErrorHandler })
|
||||
|
||||
-- Test Suite for Color Validation
|
||||
TestColorValidation = {}
|
||||
|
||||
@@ -20,27 +20,47 @@ function TestEasing:testAllEasingFunctionsExist()
|
||||
-- Linear
|
||||
"linear",
|
||||
-- Quad
|
||||
"easeInQuad", "easeOutQuad", "easeInOutQuad",
|
||||
"easeInQuad",
|
||||
"easeOutQuad",
|
||||
"easeInOutQuad",
|
||||
-- Cubic
|
||||
"easeInCubic", "easeOutCubic", "easeInOutCubic",
|
||||
"easeInCubic",
|
||||
"easeOutCubic",
|
||||
"easeInOutCubic",
|
||||
-- Quart
|
||||
"easeInQuart", "easeOutQuart", "easeInOutQuart",
|
||||
"easeInQuart",
|
||||
"easeOutQuart",
|
||||
"easeInOutQuart",
|
||||
-- Quint
|
||||
"easeInQuint", "easeOutQuint", "easeInOutQuint",
|
||||
"easeInQuint",
|
||||
"easeOutQuint",
|
||||
"easeInOutQuint",
|
||||
-- Expo
|
||||
"easeInExpo", "easeOutExpo", "easeInOutExpo",
|
||||
"easeInExpo",
|
||||
"easeOutExpo",
|
||||
"easeInOutExpo",
|
||||
-- Sine
|
||||
"easeInSine", "easeOutSine", "easeInOutSine",
|
||||
"easeInSine",
|
||||
"easeOutSine",
|
||||
"easeInOutSine",
|
||||
-- Circ
|
||||
"easeInCirc", "easeOutCirc", "easeInOutCirc",
|
||||
"easeInCirc",
|
||||
"easeOutCirc",
|
||||
"easeInOutCirc",
|
||||
-- Back
|
||||
"easeInBack", "easeOutBack", "easeInOutBack",
|
||||
"easeInBack",
|
||||
"easeOutBack",
|
||||
"easeInOutBack",
|
||||
-- Elastic
|
||||
"easeInElastic", "easeOutElastic", "easeInOutElastic",
|
||||
"easeInElastic",
|
||||
"easeOutElastic",
|
||||
"easeInOutElastic",
|
||||
-- Bounce
|
||||
"easeInBounce", "easeOutBounce", "easeInOutBounce",
|
||||
"easeInBounce",
|
||||
"easeOutBounce",
|
||||
"easeInOutBounce",
|
||||
}
|
||||
|
||||
|
||||
for _, name in ipairs(easings) do
|
||||
luaunit.assertNotNil(Easing[name], "Easing function " .. name .. " should exist")
|
||||
luaunit.assertEquals(type(Easing[name]), "function", name .. " should be a function")
|
||||
@@ -239,7 +259,7 @@ function TestEasing:testList()
|
||||
local list = Easing.list()
|
||||
luaunit.assertEquals(type(list), "table")
|
||||
luaunit.assertEquals(#list, 31, "Should have exactly 31 easing functions")
|
||||
|
||||
|
||||
-- Check that linear is in the list
|
||||
local hasLinear = false
|
||||
for _, name in ipairs(list) do
|
||||
@@ -257,7 +277,7 @@ function TestEasing:testGet()
|
||||
luaunit.assertNotNil(linear)
|
||||
luaunit.assertEquals(type(linear), "function")
|
||||
luaunit.assertEquals(linear(0.5), 0.5)
|
||||
|
||||
|
||||
-- Test non-existent easing
|
||||
local nonExistent = Easing.get("nonExistentEasing")
|
||||
luaunit.assertNil(nonExistent)
|
||||
@@ -266,11 +286,18 @@ end
|
||||
-- Test that all InOut easings are symmetric around 0.5
|
||||
function TestEasing:testInOutSymmetry()
|
||||
local inOutEasings = {
|
||||
"easeInOutQuad", "easeInOutCubic", "easeInOutQuart", "easeInOutQuint",
|
||||
"easeInOutExpo", "easeInOutSine", "easeInOutCirc", "easeInOutBack",
|
||||
"easeInOutElastic", "easeInOutBounce"
|
||||
"easeInOutQuad",
|
||||
"easeInOutCubic",
|
||||
"easeInOutQuart",
|
||||
"easeInOutQuint",
|
||||
"easeInOutExpo",
|
||||
"easeInOutSine",
|
||||
"easeInOutCirc",
|
||||
"easeInOutBack",
|
||||
"easeInOutElastic",
|
||||
"easeInOutBounce",
|
||||
}
|
||||
|
||||
|
||||
for _, name in ipairs(inOutEasings) do
|
||||
local easing = Easing[name]
|
||||
-- At t=0.5, all InOut easings should be close to 0.5
|
||||
@@ -283,28 +310,50 @@ end
|
||||
function TestEasing:testBoundaryConditions()
|
||||
local easings = {
|
||||
"linear",
|
||||
"easeInQuad", "easeOutQuad", "easeInOutQuad",
|
||||
"easeInCubic", "easeOutCubic", "easeInOutCubic",
|
||||
"easeInQuart", "easeOutQuart", "easeInOutQuart",
|
||||
"easeInQuint", "easeOutQuint", "easeInOutQuint",
|
||||
"easeInExpo", "easeOutExpo", "easeInOutExpo",
|
||||
"easeInSine", "easeOutSine", "easeInOutSine",
|
||||
"easeInCirc", "easeOutCirc", "easeInOutCirc",
|
||||
"easeInBack", "easeOutBack", "easeInOutBack",
|
||||
"easeInElastic", "easeOutElastic", "easeInOutElastic",
|
||||
"easeInBounce", "easeOutBounce", "easeInOutBounce",
|
||||
"easeInQuad",
|
||||
"easeOutQuad",
|
||||
"easeInOutQuad",
|
||||
"easeInCubic",
|
||||
"easeOutCubic",
|
||||
"easeInOutCubic",
|
||||
"easeInQuart",
|
||||
"easeOutQuart",
|
||||
"easeInOutQuart",
|
||||
"easeInQuint",
|
||||
"easeOutQuint",
|
||||
"easeInOutQuint",
|
||||
"easeInExpo",
|
||||
"easeOutExpo",
|
||||
"easeInOutExpo",
|
||||
"easeInSine",
|
||||
"easeOutSine",
|
||||
"easeInOutSine",
|
||||
"easeInCirc",
|
||||
"easeOutCirc",
|
||||
"easeInOutCirc",
|
||||
"easeInBack",
|
||||
"easeOutBack",
|
||||
"easeInOutBack",
|
||||
"easeInElastic",
|
||||
"easeOutElastic",
|
||||
"easeInOutElastic",
|
||||
"easeInBounce",
|
||||
"easeOutBounce",
|
||||
"easeInOutBounce",
|
||||
}
|
||||
|
||||
|
||||
for _, name in ipairs(easings) do
|
||||
local easing = Easing[name]
|
||||
-- All easings should start at 0
|
||||
local start = easing(0)
|
||||
luaunit.assertAlmostEquals(start, 0, 0.01, name .. " should start at 0")
|
||||
|
||||
|
||||
-- All easings should end at 1
|
||||
local finish = easing(1)
|
||||
luaunit.assertAlmostEquals(finish, 1, 0.01, name .. " should end at 1")
|
||||
end
|
||||
end
|
||||
|
||||
os.exit(luaunit.LuaUnit.run())
|
||||
if not _G.RUNNING_ALL_TESTS then
|
||||
os.exit(luaunit.LuaUnit.run())
|
||||
end
|
||||
|
||||
@@ -524,7 +524,7 @@ function TestEventHandler:test_onEventDeferred()
|
||||
local MockFlexLove = {
|
||||
deferCallback = function(callback)
|
||||
table.insert(deferredCallbacks, callback)
|
||||
end
|
||||
end,
|
||||
}
|
||||
package.loaded["FlexLove"] = MockFlexLove
|
||||
|
||||
@@ -533,7 +533,7 @@ function TestEventHandler:test_onEventDeferred()
|
||||
onEventDeferred = true,
|
||||
onEvent = function(el, event)
|
||||
table.insert(eventsReceived, event)
|
||||
end
|
||||
end,
|
||||
})
|
||||
local element = createMockElement()
|
||||
handler:initialize(element)
|
||||
@@ -545,12 +545,14 @@ function TestEventHandler:test_onEventDeferred()
|
||||
|
||||
-- Press and release mouse button
|
||||
handler:processMouseEvents(50, 50, true, true)
|
||||
love.mouse.isDown = function() return false end
|
||||
love.mouse.isDown = function()
|
||||
return false
|
||||
end
|
||||
handler:processMouseEvents(50, 50, true, true)
|
||||
|
||||
-- Events should not be immediately executed
|
||||
luaunit.assertEquals(#eventsReceived, 0)
|
||||
|
||||
|
||||
-- Should have deferred callbacks queued
|
||||
luaunit.assertTrue(#deferredCallbacks > 0)
|
||||
|
||||
@@ -561,7 +563,7 @@ function TestEventHandler:test_onEventDeferred()
|
||||
|
||||
-- Now events should be received
|
||||
luaunit.assertTrue(#eventsReceived > 0)
|
||||
|
||||
|
||||
-- Check that we got a click event
|
||||
local hasClick = false
|
||||
for _, event in ipairs(eventsReceived) do
|
||||
@@ -583,7 +585,7 @@ function TestEventHandler:test_onEventDeferred_false()
|
||||
onEventDeferred = false,
|
||||
onEvent = function(el, event)
|
||||
table.insert(eventsReceived, event)
|
||||
end
|
||||
end,
|
||||
})
|
||||
local element = createMockElement()
|
||||
handler:initialize(element)
|
||||
@@ -595,12 +597,14 @@ function TestEventHandler:test_onEventDeferred_false()
|
||||
|
||||
-- Press and release mouse button
|
||||
handler:processMouseEvents(50, 50, true, true)
|
||||
love.mouse.isDown = function() return false end
|
||||
love.mouse.isDown = function()
|
||||
return false
|
||||
end
|
||||
handler:processMouseEvents(50, 50, true, true)
|
||||
|
||||
-- Events should be immediately executed
|
||||
luaunit.assertTrue(#eventsReceived > 0)
|
||||
|
||||
|
||||
-- Check that we got a click event
|
||||
local hasClick = false
|
||||
for _, event in ipairs(eventsReceived) do
|
||||
|
||||
@@ -642,19 +642,19 @@ end
|
||||
-- Test: deferCallback() queues callback
|
||||
function TestFlexLove:testDeferCallbackQueuesCallback()
|
||||
FlexLove.setMode("retained")
|
||||
|
||||
|
||||
local called = false
|
||||
FlexLove.deferCallback(function()
|
||||
called = true
|
||||
end)
|
||||
|
||||
|
||||
-- Callback should not be called immediately
|
||||
luaunit.assertFalse(called)
|
||||
|
||||
|
||||
-- Callback should be called after executeDeferredCallbacks
|
||||
FlexLove.draw()
|
||||
luaunit.assertFalse(called) -- Still not called
|
||||
|
||||
|
||||
FlexLove.executeDeferredCallbacks()
|
||||
luaunit.assertTrue(called) -- Now called
|
||||
end
|
||||
@@ -662,7 +662,7 @@ end
|
||||
-- Test: deferCallback() with multiple callbacks
|
||||
function TestFlexLove:testDeferCallbackMultiple()
|
||||
FlexLove.setMode("retained")
|
||||
|
||||
|
||||
local order = {}
|
||||
FlexLove.deferCallback(function()
|
||||
table.insert(order, 1)
|
||||
@@ -673,10 +673,10 @@ function TestFlexLove:testDeferCallbackMultiple()
|
||||
FlexLove.deferCallback(function()
|
||||
table.insert(order, 3)
|
||||
end)
|
||||
|
||||
|
||||
FlexLove.draw()
|
||||
FlexLove.executeDeferredCallbacks()
|
||||
|
||||
|
||||
luaunit.assertEquals(#order, 3)
|
||||
luaunit.assertEquals(order[1], 1)
|
||||
luaunit.assertEquals(order[2], 2)
|
||||
@@ -686,12 +686,12 @@ end
|
||||
-- Test: deferCallback() with non-function argument
|
||||
function TestFlexLove:testDeferCallbackInvalidArgument()
|
||||
FlexLove.setMode("retained")
|
||||
|
||||
|
||||
-- Should warn but not crash
|
||||
FlexLove.deferCallback("not a function")
|
||||
FlexLove.deferCallback(123)
|
||||
FlexLove.deferCallback(nil)
|
||||
|
||||
|
||||
FlexLove.draw()
|
||||
luaunit.assertTrue(true)
|
||||
end
|
||||
@@ -699,16 +699,16 @@ end
|
||||
-- Test: deferCallback() clears queue after execution
|
||||
function TestFlexLove:testDeferCallbackClearsQueue()
|
||||
FlexLove.setMode("retained")
|
||||
|
||||
|
||||
local callCount = 0
|
||||
FlexLove.deferCallback(function()
|
||||
callCount = callCount + 1
|
||||
end)
|
||||
|
||||
|
||||
FlexLove.draw()
|
||||
FlexLove.executeDeferredCallbacks() -- First execution
|
||||
luaunit.assertEquals(callCount, 1)
|
||||
|
||||
|
||||
FlexLove.draw()
|
||||
FlexLove.executeDeferredCallbacks() -- Second execution should not call again
|
||||
luaunit.assertEquals(callCount, 1)
|
||||
@@ -717,7 +717,7 @@ end
|
||||
-- Test: deferCallback() handles callback errors gracefully
|
||||
function TestFlexLove:testDeferCallbackWithError()
|
||||
FlexLove.setMode("retained")
|
||||
|
||||
|
||||
local called = false
|
||||
FlexLove.deferCallback(function()
|
||||
error("Intentional error")
|
||||
@@ -725,7 +725,7 @@ function TestFlexLove:testDeferCallbackWithError()
|
||||
FlexLove.deferCallback(function()
|
||||
called = true
|
||||
end)
|
||||
|
||||
|
||||
-- Should not crash, second callback should still execute
|
||||
FlexLove.draw()
|
||||
FlexLove.executeDeferredCallbacks()
|
||||
@@ -1329,4 +1329,6 @@ function TestFlexLoveUnhappyPaths:testImmediateModeFrameEdgeCases()
|
||||
luaunit.assertTrue(true)
|
||||
end
|
||||
|
||||
return TestFlexLove
|
||||
if not _G.RUNNING_ALL_TESTS then
|
||||
os.exit(luaunit.LuaUnit.run())
|
||||
end
|
||||
|
||||
@@ -29,13 +29,13 @@ function TestFontCache:testCacheHitOnRepeatedAccess()
|
||||
local stats1 = utils.getFontCacheStats()
|
||||
luaunit.assertEquals(stats1.misses, 1)
|
||||
luaunit.assertEquals(stats1.hits, 0)
|
||||
|
||||
|
||||
-- Second access should be a hit
|
||||
utils.FONT_CACHE.get(16, nil)
|
||||
local stats2 = utils.getFontCacheStats()
|
||||
luaunit.assertEquals(stats2.hits, 1)
|
||||
luaunit.assertEquals(stats2.misses, 1)
|
||||
|
||||
|
||||
-- Third access should also be a hit
|
||||
utils.FONT_CACHE.get(16, nil)
|
||||
local stats3 = utils.getFontCacheStats()
|
||||
@@ -46,17 +46,17 @@ end
|
||||
function TestFontCache:testCacheMissOnFirstAccess()
|
||||
utils.clearFontCache()
|
||||
utils.resetFontCacheStats()
|
||||
|
||||
|
||||
utils.FONT_CACHE.get(24, nil)
|
||||
local stats = utils.getFontCacheStats()
|
||||
|
||||
|
||||
luaunit.assertEquals(stats.misses, 1)
|
||||
luaunit.assertEquals(stats.hits, 0)
|
||||
end
|
||||
|
||||
function TestFontCache:testLRUEviction()
|
||||
utils.setFontCacheSize(3)
|
||||
|
||||
|
||||
-- Load 3 fonts (fills cache) with time steps to ensure different timestamps
|
||||
utils.FONT_CACHE.get(10, nil)
|
||||
love.timer.step(0.001)
|
||||
@@ -64,34 +64,34 @@ function TestFontCache:testLRUEviction()
|
||||
love.timer.step(0.001)
|
||||
utils.FONT_CACHE.get(14, nil)
|
||||
love.timer.step(0.001)
|
||||
|
||||
|
||||
local stats1 = utils.getFontCacheStats()
|
||||
luaunit.assertEquals(stats1.size, 3)
|
||||
luaunit.assertEquals(stats1.evictions, 0)
|
||||
|
||||
|
||||
-- Load 4th font (triggers eviction of font 10 - the oldest)
|
||||
utils.FONT_CACHE.get(16, nil)
|
||||
|
||||
|
||||
local stats2 = utils.getFontCacheStats()
|
||||
luaunit.assertEquals(stats2.size, 3)
|
||||
luaunit.assertEquals(stats2.evictions, 1)
|
||||
|
||||
|
||||
-- Access first font again - it should have been evicted (miss)
|
||||
local initialMisses = stats2.misses
|
||||
utils.FONT_CACHE.get(10, nil)
|
||||
|
||||
|
||||
local stats3 = utils.getFontCacheStats()
|
||||
luaunit.assertEquals(stats3.misses, initialMisses + 1) -- Should be a miss
|
||||
end
|
||||
|
||||
function TestFontCache:testCacheSizeLimitEnforced()
|
||||
utils.setFontCacheSize(5)
|
||||
|
||||
|
||||
-- Load 10 fonts
|
||||
for i = 1, 10 do
|
||||
utils.FONT_CACHE.get(10 + i, nil)
|
||||
end
|
||||
|
||||
|
||||
local stats = utils.getFontCacheStats()
|
||||
luaunit.assertEquals(stats.size, 5)
|
||||
luaunit.assertTrue(stats.evictions >= 5)
|
||||
@@ -102,7 +102,7 @@ function TestFontCache:testFontRounding()
|
||||
utils.FONT_CACHE.get(14.5, nil)
|
||||
local stats1 = utils.getFontCacheStats()
|
||||
luaunit.assertEquals(stats1.misses, 1)
|
||||
|
||||
|
||||
utils.FONT_CACHE.get(14.7, nil)
|
||||
local stats2 = utils.getFontCacheStats()
|
||||
luaunit.assertEquals(stats2.hits, 1) -- Should be a hit because both round to 15
|
||||
@@ -112,12 +112,12 @@ end
|
||||
function TestFontCache:testCacheClear()
|
||||
utils.FONT_CACHE.get(16, nil)
|
||||
utils.FONT_CACHE.get(18, nil)
|
||||
|
||||
|
||||
local stats1 = utils.getFontCacheStats()
|
||||
luaunit.assertEquals(stats1.size, 2)
|
||||
|
||||
|
||||
utils.clearFontCache()
|
||||
|
||||
|
||||
local stats2 = utils.getFontCacheStats()
|
||||
luaunit.assertEquals(stats2.size, 0)
|
||||
end
|
||||
@@ -126,7 +126,7 @@ function TestFontCache:testCacheKeyWithPath()
|
||||
-- Different cache keys for same size, different paths
|
||||
utils.FONT_CACHE.get(16, nil)
|
||||
utils.FONT_CACHE.get(16, "fonts/custom.ttf")
|
||||
|
||||
|
||||
local stats = utils.getFontCacheStats()
|
||||
luaunit.assertEquals(stats.misses, 2) -- Both should be misses
|
||||
luaunit.assertEquals(stats.size, 2)
|
||||
@@ -135,14 +135,14 @@ end
|
||||
function TestFontCache:testPreloadFont()
|
||||
utils.clearFontCache()
|
||||
utils.resetFontCacheStats()
|
||||
|
||||
|
||||
-- Preload multiple sizes
|
||||
utils.preloadFont(nil, {12, 14, 16, 18})
|
||||
|
||||
utils.preloadFont(nil, { 12, 14, 16, 18 })
|
||||
|
||||
local stats1 = utils.getFontCacheStats()
|
||||
luaunit.assertEquals(stats1.size, 4)
|
||||
luaunit.assertEquals(stats1.misses, 4) -- All preloads are misses
|
||||
|
||||
|
||||
-- Now access one - should be a hit
|
||||
utils.FONT_CACHE.get(16, nil)
|
||||
local stats2 = utils.getFontCacheStats()
|
||||
@@ -152,31 +152,31 @@ end
|
||||
function TestFontCache:testCacheHitRate()
|
||||
utils.clearFontCache()
|
||||
utils.resetFontCacheStats()
|
||||
|
||||
|
||||
-- 1 miss, 9 hits = 90% hit rate
|
||||
utils.FONT_CACHE.get(16, nil)
|
||||
for i = 1, 9 do
|
||||
utils.FONT_CACHE.get(16, nil)
|
||||
end
|
||||
|
||||
|
||||
local stats = utils.getFontCacheStats()
|
||||
luaunit.assertEquals(stats.hitRate, 0.9)
|
||||
end
|
||||
|
||||
function TestFontCache:testSetCacheSizeEvictsExcess()
|
||||
utils.setFontCacheSize(10)
|
||||
|
||||
|
||||
-- Load 10 fonts
|
||||
for i = 1, 10 do
|
||||
utils.FONT_CACHE.get(10 + i, nil)
|
||||
end
|
||||
|
||||
|
||||
local stats1 = utils.getFontCacheStats()
|
||||
luaunit.assertEquals(stats1.size, 10)
|
||||
|
||||
|
||||
-- Reduce cache size - should trigger evictions
|
||||
utils.setFontCacheSize(5)
|
||||
|
||||
|
||||
local stats2 = utils.getFontCacheStats()
|
||||
luaunit.assertEquals(stats2.size, 5)
|
||||
luaunit.assertTrue(stats2.evictions >= 5)
|
||||
@@ -186,14 +186,11 @@ function TestFontCache:testMinimalCacheSize()
|
||||
-- Minimum cache size is 1
|
||||
utils.setFontCacheSize(0)
|
||||
utils.FONT_CACHE.get(16, nil)
|
||||
|
||||
|
||||
local stats = utils.getFontCacheStats()
|
||||
luaunit.assertEquals(stats.size, 1)
|
||||
end
|
||||
|
||||
-- Run tests if executed directly
|
||||
if arg and arg[0]:find("font_cache_test%.lua$") then
|
||||
if not _G.RUNNING_ALL_TESTS then
|
||||
os.exit(luaunit.LuaUnit.run())
|
||||
end
|
||||
|
||||
return TestFontCache
|
||||
|
||||
@@ -21,11 +21,11 @@ function TestKeyframeAnimation:testCreateKeyframeAnimation()
|
||||
local anim = Animation.keyframes({
|
||||
duration = 2,
|
||||
keyframes = {
|
||||
{at = 0, values = {x = 0, opacity = 0}},
|
||||
{at = 1, values = {x = 100, opacity = 1}},
|
||||
}
|
||||
{ at = 0, values = { x = 0, opacity = 0 } },
|
||||
{ at = 1, values = { x = 100, opacity = 1 } },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
luaunit.assertNotNil(anim)
|
||||
luaunit.assertEquals(type(anim), "table")
|
||||
luaunit.assertEquals(anim.duration, 2)
|
||||
@@ -38,13 +38,13 @@ function TestKeyframeAnimation:testMultipleWaypoints()
|
||||
local anim = Animation.keyframes({
|
||||
duration = 3,
|
||||
keyframes = {
|
||||
{at = 0, values = {x = 0, opacity = 0}},
|
||||
{at = 0.25, values = {x = 50, opacity = 1}},
|
||||
{at = 0.75, values = {x = 150, opacity = 1}},
|
||||
{at = 1, values = {x = 200, opacity = 0}},
|
||||
}
|
||||
{ at = 0, values = { x = 0, opacity = 0 } },
|
||||
{ at = 0.25, values = { x = 50, opacity = 1 } },
|
||||
{ at = 0.75, values = { x = 150, opacity = 1 } },
|
||||
{ at = 1, values = { x = 200, opacity = 0 } },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
luaunit.assertEquals(#anim.keyframes, 4)
|
||||
luaunit.assertEquals(anim.keyframes[1].at, 0)
|
||||
luaunit.assertEquals(anim.keyframes[2].at, 0.25)
|
||||
@@ -57,12 +57,12 @@ function TestKeyframeAnimation:testKeyframeSorting()
|
||||
local anim = Animation.keyframes({
|
||||
duration = 1,
|
||||
keyframes = {
|
||||
{at = 1, values = {x = 100}},
|
||||
{at = 0, values = {x = 0}},
|
||||
{at = 0.5, values = {x = 50}},
|
||||
}
|
||||
{ at = 1, values = { x = 100 } },
|
||||
{ at = 0, values = { x = 0 } },
|
||||
{ at = 0.5, values = { x = 50 } },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
-- Should be sorted by 'at' position
|
||||
luaunit.assertEquals(anim.keyframes[1].at, 0)
|
||||
luaunit.assertEquals(anim.keyframes[2].at, 0.5)
|
||||
@@ -74,14 +74,14 @@ function TestKeyframeAnimation:testInterpolationAtStart()
|
||||
local anim = Animation.keyframes({
|
||||
duration = 1,
|
||||
keyframes = {
|
||||
{at = 0, values = {x = 0, opacity = 0}},
|
||||
{at = 1, values = {x = 100, opacity = 1}},
|
||||
}
|
||||
{ at = 0, values = { x = 0, opacity = 0 } },
|
||||
{ at = 1, values = { x = 100, opacity = 1 } },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
anim.elapsed = 0
|
||||
local result = anim:interpolate()
|
||||
|
||||
|
||||
luaunit.assertNotNil(result.x)
|
||||
luaunit.assertNotNil(result.opacity)
|
||||
luaunit.assertAlmostEquals(result.x, 0, 0.01)
|
||||
@@ -93,14 +93,14 @@ function TestKeyframeAnimation:testInterpolationAtEnd()
|
||||
local anim = Animation.keyframes({
|
||||
duration = 1,
|
||||
keyframes = {
|
||||
{at = 0, values = {x = 0, opacity = 0}},
|
||||
{at = 1, values = {x = 100, opacity = 1}},
|
||||
}
|
||||
{ at = 0, values = { x = 0, opacity = 0 } },
|
||||
{ at = 1, values = { x = 100, opacity = 1 } },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
anim.elapsed = 1
|
||||
local result = anim:interpolate()
|
||||
|
||||
|
||||
luaunit.assertAlmostEquals(result.x, 100, 0.01)
|
||||
luaunit.assertAlmostEquals(result.opacity, 1, 0.01)
|
||||
end
|
||||
@@ -110,14 +110,14 @@ function TestKeyframeAnimation:testInterpolationAtMidpoint()
|
||||
local anim = Animation.keyframes({
|
||||
duration = 1,
|
||||
keyframes = {
|
||||
{at = 0, values = {x = 0}},
|
||||
{at = 1, values = {x = 100}},
|
||||
}
|
||||
{ at = 0, values = { x = 0 } },
|
||||
{ at = 1, values = { x = 100 } },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
anim.elapsed = 0.5
|
||||
local result = anim:interpolate()
|
||||
|
||||
|
||||
luaunit.assertAlmostEquals(result.x, 50, 0.01)
|
||||
end
|
||||
|
||||
@@ -126,22 +126,22 @@ function TestKeyframeAnimation:testPerKeyframeEasing()
|
||||
local anim = Animation.keyframes({
|
||||
duration = 1,
|
||||
keyframes = {
|
||||
{at = 0, values = {x = 0}, easing = "easeInQuad"},
|
||||
{at = 0.5, values = {x = 50}, easing = "linear"},
|
||||
{at = 1, values = {x = 100}},
|
||||
}
|
||||
{ at = 0, values = { x = 0 }, easing = "easeInQuad" },
|
||||
{ at = 0.5, values = { x = 50 }, easing = "linear" },
|
||||
{ at = 1, values = { x = 100 } },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
-- At t=0.25 (middle of first segment with easeInQuad)
|
||||
anim.elapsed = 0.25
|
||||
anim._resultDirty = true -- Mark dirty to force recalculation
|
||||
anim._resultDirty = true -- Mark dirty to force recalculation
|
||||
local result1 = anim:interpolate()
|
||||
-- easeInQuad at 0.5 should give 0.25, so x = 0 + (50-0) * 0.25 = 12.5
|
||||
luaunit.assertTrue(result1.x < 25, "easeInQuad should slow start")
|
||||
|
||||
|
||||
-- At t=0.75 (middle of second segment with linear)
|
||||
anim.elapsed = 0.75
|
||||
anim._resultDirty = true -- Mark dirty to force recalculation
|
||||
anim._resultDirty = true -- Mark dirty to force recalculation
|
||||
local result2 = anim:interpolate()
|
||||
-- linear at 0.5 should give 0.5, so x = 50 + (100-50) * 0.5 = 75
|
||||
luaunit.assertAlmostEquals(result2.x, 75, 1)
|
||||
@@ -152,22 +152,22 @@ function TestKeyframeAnimation:testFindKeyframes()
|
||||
local anim = Animation.keyframes({
|
||||
duration = 1,
|
||||
keyframes = {
|
||||
{at = 0, values = {x = 0}},
|
||||
{at = 0.25, values = {x = 25}},
|
||||
{at = 0.75, values = {x = 75}},
|
||||
{at = 1, values = {x = 100}},
|
||||
}
|
||||
{ at = 0, values = { x = 0 } },
|
||||
{ at = 0.25, values = { x = 25 } },
|
||||
{ at = 0.75, values = { x = 75 } },
|
||||
{ at = 1, values = { x = 100 } },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
-- Test finding keyframes at different progress values
|
||||
local prev1, next1 = anim:findKeyframes(0.1)
|
||||
luaunit.assertEquals(prev1.at, 0)
|
||||
luaunit.assertEquals(next1.at, 0.25)
|
||||
|
||||
|
||||
local prev2, next2 = anim:findKeyframes(0.5)
|
||||
luaunit.assertEquals(prev2.at, 0.25)
|
||||
luaunit.assertEquals(next2.at, 0.75)
|
||||
|
||||
|
||||
local prev3, next3 = anim:findKeyframes(0.9)
|
||||
luaunit.assertEquals(prev3.at, 0.75)
|
||||
luaunit.assertEquals(next3.at, 1)
|
||||
@@ -178,18 +178,18 @@ function TestKeyframeAnimation:testKeyframeAnimationUpdate()
|
||||
local anim = Animation.keyframes({
|
||||
duration = 1,
|
||||
keyframes = {
|
||||
{at = 0, values = {opacity = 0}},
|
||||
{at = 1, values = {opacity = 1}},
|
||||
}
|
||||
{ at = 0, values = { opacity = 0 } },
|
||||
{ at = 1, values = { opacity = 1 } },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
-- Update halfway through
|
||||
anim:update(0.5)
|
||||
local result = anim:interpolate()
|
||||
|
||||
|
||||
luaunit.assertAlmostEquals(result.opacity, 0.5, 0.01)
|
||||
luaunit.assertFalse(anim:update(0)) -- Not complete yet
|
||||
|
||||
|
||||
-- Update to completion
|
||||
luaunit.assertTrue(anim:update(0.6)) -- Should complete
|
||||
luaunit.assertEquals(anim:getState(), "completed")
|
||||
@@ -200,23 +200,29 @@ function TestKeyframeAnimation:testKeyframeAnimationCallbacks()
|
||||
local startCalled = false
|
||||
local updateCalled = false
|
||||
local completeCalled = false
|
||||
|
||||
|
||||
local anim = Animation.keyframes({
|
||||
duration = 1,
|
||||
keyframes = {
|
||||
{at = 0, values = {x = 0}},
|
||||
{at = 1, values = {x = 100}},
|
||||
{ at = 0, values = { x = 0 } },
|
||||
{ at = 1, values = { x = 100 } },
|
||||
},
|
||||
onStart = function() startCalled = true end,
|
||||
onUpdate = function() updateCalled = true end,
|
||||
onComplete = function() completeCalled = true end,
|
||||
onStart = function()
|
||||
startCalled = true
|
||||
end,
|
||||
onUpdate = function()
|
||||
updateCalled = true
|
||||
end,
|
||||
onComplete = function()
|
||||
completeCalled = true
|
||||
end,
|
||||
})
|
||||
|
||||
|
||||
anim:update(0.5)
|
||||
luaunit.assertTrue(startCalled)
|
||||
luaunit.assertTrue(updateCalled)
|
||||
luaunit.assertFalse(completeCalled)
|
||||
|
||||
|
||||
anim:update(0.6)
|
||||
luaunit.assertTrue(completeCalled)
|
||||
end
|
||||
@@ -226,9 +232,9 @@ function TestKeyframeAnimation:testMissingKeyframes()
|
||||
-- Should create default keyframes with warning
|
||||
local anim = Animation.keyframes({
|
||||
duration = 1,
|
||||
keyframes = {}
|
||||
keyframes = {},
|
||||
})
|
||||
|
||||
|
||||
luaunit.assertNotNil(anim)
|
||||
luaunit.assertEquals(#anim.keyframes, 2) -- Should have default start and end
|
||||
end
|
||||
@@ -239,10 +245,10 @@ function TestKeyframeAnimation:testSingleKeyframe()
|
||||
local anim = Animation.keyframes({
|
||||
duration = 1,
|
||||
keyframes = {
|
||||
{at = 0.5, values = {x = 50}}
|
||||
}
|
||||
{ at = 0.5, values = { x = 50 } },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
luaunit.assertNotNil(anim)
|
||||
luaunit.assertTrue(#anim.keyframes >= 2) -- Should have at least 2 keyframes
|
||||
end
|
||||
@@ -252,11 +258,11 @@ function TestKeyframeAnimation:testKeyframesWithoutStart()
|
||||
local anim = Animation.keyframes({
|
||||
duration = 1,
|
||||
keyframes = {
|
||||
{at = 0.5, values = {x = 50}},
|
||||
{at = 1, values = {x = 100}},
|
||||
}
|
||||
{ at = 0.5, values = { x = 50 } },
|
||||
{ at = 1, values = { x = 100 } },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
-- Should auto-add keyframe at 0
|
||||
luaunit.assertEquals(anim.keyframes[1].at, 0)
|
||||
luaunit.assertEquals(anim.keyframes[1].values.x, 50) -- Should copy first keyframe values
|
||||
@@ -267,11 +273,11 @@ function TestKeyframeAnimation:testKeyframesWithoutEnd()
|
||||
local anim = Animation.keyframes({
|
||||
duration = 1,
|
||||
keyframes = {
|
||||
{at = 0, values = {x = 0}},
|
||||
{at = 0.5, values = {x = 50}},
|
||||
}
|
||||
{ at = 0, values = { x = 0 } },
|
||||
{ at = 0.5, values = { x = 50 } },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
-- Should auto-add keyframe at 1
|
||||
luaunit.assertEquals(anim.keyframes[#anim.keyframes].at, 1)
|
||||
luaunit.assertEquals(anim.keyframes[#anim.keyframes].values.x, 50) -- Should copy last keyframe values
|
||||
@@ -282,9 +288,9 @@ function TestKeyframeAnimation:testInvalidKeyframeProps()
|
||||
-- Should handle gracefully with warnings
|
||||
local anim = Animation.keyframes({
|
||||
duration = 0, -- Invalid
|
||||
keyframes = "not a table" -- Invalid
|
||||
keyframes = "not a table", -- Invalid
|
||||
})
|
||||
|
||||
|
||||
luaunit.assertNotNil(anim)
|
||||
luaunit.assertEquals(anim.duration, 1) -- Should use default
|
||||
end
|
||||
@@ -294,22 +300,22 @@ function TestKeyframeAnimation:testMultiPropertyKeyframes()
|
||||
local anim = Animation.keyframes({
|
||||
duration = 2,
|
||||
keyframes = {
|
||||
{at = 0, values = {x = 0, y = 0, opacity = 0, width = 50}},
|
||||
{at = 0.33, values = {x = 100, y = 50, opacity = 1, width = 100}},
|
||||
{at = 0.66, values = {x = 200, y = 100, opacity = 1, width = 150}},
|
||||
{at = 1, values = {x = 300, y = 150, opacity = 0, width = 200}},
|
||||
}
|
||||
{ at = 0, values = { x = 0, y = 0, opacity = 0, width = 50 } },
|
||||
{ at = 0.33, values = { x = 100, y = 50, opacity = 1, width = 100 } },
|
||||
{ at = 0.66, values = { x = 200, y = 100, opacity = 1, width = 150 } },
|
||||
{ at = 1, values = { x = 300, y = 150, opacity = 0, width = 200 } },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
-- Test interpolation at 0.5 (middle of second segment)
|
||||
anim.elapsed = 1.0 -- t = 0.5
|
||||
local result = anim:interpolate()
|
||||
|
||||
|
||||
luaunit.assertNotNil(result.x)
|
||||
luaunit.assertNotNil(result.y)
|
||||
luaunit.assertNotNil(result.opacity)
|
||||
luaunit.assertNotNil(result.width)
|
||||
|
||||
|
||||
-- Should be interpolating between keyframes at 0.33 and 0.66
|
||||
luaunit.assertTrue(result.x > 100 and result.x < 200)
|
||||
luaunit.assertTrue(result.y > 50 and result.y < 100)
|
||||
@@ -317,19 +323,21 @@ end
|
||||
|
||||
-- Test keyframe with easing function (not string)
|
||||
function TestKeyframeAnimation:testKeyframeWithEasingFunction()
|
||||
local customEasing = function(t) return t * t end
|
||||
|
||||
local customEasing = function(t)
|
||||
return t * t
|
||||
end
|
||||
|
||||
local anim = Animation.keyframes({
|
||||
duration = 1,
|
||||
keyframes = {
|
||||
{at = 0, values = {x = 0}, easing = customEasing},
|
||||
{at = 1, values = {x = 100}},
|
||||
}
|
||||
{ at = 0, values = { x = 0 }, easing = customEasing },
|
||||
{ at = 1, values = { x = 100 } },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
anim.elapsed = 0.5
|
||||
local result = anim:interpolate()
|
||||
|
||||
|
||||
-- At t=0.5, easing(0.5) = 0.25, so x = 0 + 100 * 0.25 = 25
|
||||
luaunit.assertAlmostEquals(result.x, 25, 1)
|
||||
end
|
||||
@@ -339,16 +347,18 @@ function TestKeyframeAnimation:testKeyframeCaching()
|
||||
local anim = Animation.keyframes({
|
||||
duration = 1,
|
||||
keyframes = {
|
||||
{at = 0, values = {x = 0}},
|
||||
{at = 1, values = {x = 100}},
|
||||
}
|
||||
{ at = 0, values = { x = 0 } },
|
||||
{ at = 1, values = { x = 100 } },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
anim.elapsed = 0.5
|
||||
local result1 = anim:interpolate()
|
||||
local result2 = anim:interpolate() -- Should return cached result
|
||||
|
||||
|
||||
luaunit.assertEquals(result1, result2) -- Should be same table
|
||||
end
|
||||
|
||||
os.exit(luaunit.LuaUnit.run())
|
||||
if not _G.RUNNING_ALL_TESTS then
|
||||
os.exit(luaunit.LuaUnit.run())
|
||||
end
|
||||
|
||||
@@ -11,10 +11,10 @@ TestTouchEvents = {}
|
||||
-- Test: InputEvent.fromTouch creates valid touch event
|
||||
function TestTouchEvents:testInputEvent_FromTouch()
|
||||
local InputEvent = package.loaded["modules.InputEvent"]
|
||||
|
||||
|
||||
local touchId = "touch1"
|
||||
local event = InputEvent.fromTouch(touchId, 100, 200, "began", 0.8)
|
||||
|
||||
|
||||
lu.assertEquals(event.type, "touchpress")
|
||||
lu.assertEquals(event.x, 100)
|
||||
lu.assertEquals(event.y, 200)
|
||||
@@ -27,9 +27,9 @@ end
|
||||
-- Test: Touch event with moved phase
|
||||
function TestTouchEvents:testInputEvent_FromTouch_Moved()
|
||||
local InputEvent = package.loaded["modules.InputEvent"]
|
||||
|
||||
|
||||
local event = InputEvent.fromTouch("touch1", 150, 250, "moved", 1.0)
|
||||
|
||||
|
||||
lu.assertEquals(event.type, "touchmove")
|
||||
lu.assertEquals(event.phase, "moved")
|
||||
end
|
||||
@@ -37,9 +37,9 @@ end
|
||||
-- Test: Touch event with ended phase
|
||||
function TestTouchEvents:testInputEvent_FromTouch_Ended()
|
||||
local InputEvent = package.loaded["modules.InputEvent"]
|
||||
|
||||
|
||||
local event = InputEvent.fromTouch("touch1", 150, 250, "ended", 1.0)
|
||||
|
||||
|
||||
lu.assertEquals(event.type, "touchrelease")
|
||||
lu.assertEquals(event.phase, "ended")
|
||||
end
|
||||
@@ -47,9 +47,9 @@ end
|
||||
-- Test: Touch event with cancelled phase
|
||||
function TestTouchEvents:testInputEvent_FromTouch_Cancelled()
|
||||
local InputEvent = package.loaded["modules.InputEvent"]
|
||||
|
||||
|
||||
local event = InputEvent.fromTouch("touch1", 150, 250, "cancelled", 1.0)
|
||||
|
||||
|
||||
lu.assertEquals(event.type, "touchcancel")
|
||||
lu.assertEquals(event.phase, "cancelled")
|
||||
end
|
||||
@@ -57,7 +57,7 @@ end
|
||||
-- Test: EventHandler tracks touch began
|
||||
function TestTouchEvents:testEventHandler_TouchBegan()
|
||||
FlexLove.beginFrame()
|
||||
|
||||
|
||||
local touchEvents = {}
|
||||
local element = FlexLove.new({
|
||||
width = 200,
|
||||
@@ -66,21 +66,25 @@ function TestTouchEvents:testEventHandler_TouchBegan()
|
||||
table.insert(touchEvents, event)
|
||||
end,
|
||||
})
|
||||
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
|
||||
-- Simulate touch began
|
||||
love.touch.getTouches = function() return {"touch1"} end
|
||||
love.touch.getTouches = function()
|
||||
return { "touch1" }
|
||||
end
|
||||
love.touch.getPosition = function(id)
|
||||
if id == "touch1" then return 100, 100 end
|
||||
if id == "touch1" then
|
||||
return 100, 100
|
||||
end
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
|
||||
-- Trigger touch event processing
|
||||
FlexLove.beginFrame()
|
||||
element._eventHandler:processTouchEvents()
|
||||
FlexLove.endFrame()
|
||||
|
||||
|
||||
-- Should have received a touchpress event
|
||||
lu.assertEquals(#touchEvents, 1)
|
||||
lu.assertEquals(touchEvents[1].type, "touchpress")
|
||||
@@ -90,7 +94,7 @@ end
|
||||
-- Test: EventHandler tracks touch moved
|
||||
function TestTouchEvents:testEventHandler_TouchMoved()
|
||||
FlexLove.beginFrame()
|
||||
|
||||
|
||||
local touchEvents = {}
|
||||
local element = FlexLove.new({
|
||||
width = 200,
|
||||
@@ -99,31 +103,37 @@ function TestTouchEvents:testEventHandler_TouchMoved()
|
||||
table.insert(touchEvents, event)
|
||||
end,
|
||||
})
|
||||
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
|
||||
-- Simulate touch began
|
||||
love.touch.getTouches = function() return {"touch1"} end
|
||||
love.touch.getTouches = function()
|
||||
return { "touch1" }
|
||||
end
|
||||
love.touch.getPosition = function(id)
|
||||
if id == "touch1" then return 100, 100 end
|
||||
if id == "touch1" then
|
||||
return 100, 100
|
||||
end
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
|
||||
-- First touch
|
||||
FlexLove.beginFrame()
|
||||
element._eventHandler:processTouchEvents()
|
||||
FlexLove.endFrame()
|
||||
|
||||
|
||||
-- Move touch
|
||||
love.touch.getPosition = function(id)
|
||||
if id == "touch1" then return 150, 150 end
|
||||
if id == "touch1" then
|
||||
return 150, 150
|
||||
end
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
|
||||
FlexLove.beginFrame()
|
||||
element._eventHandler:processTouchEvents()
|
||||
FlexLove.endFrame()
|
||||
|
||||
|
||||
-- Should have received touchpress and touchmove events
|
||||
lu.assertEquals(#touchEvents, 2)
|
||||
lu.assertEquals(touchEvents[1].type, "touchpress")
|
||||
@@ -135,7 +145,7 @@ end
|
||||
-- Test: EventHandler tracks touch ended
|
||||
function TestTouchEvents:testEventHandler_TouchEnded()
|
||||
FlexLove.beginFrame()
|
||||
|
||||
|
||||
local touchEvents = {}
|
||||
local element = FlexLove.new({
|
||||
width = 200,
|
||||
@@ -144,28 +154,34 @@ function TestTouchEvents:testEventHandler_TouchEnded()
|
||||
table.insert(touchEvents, event)
|
||||
end,
|
||||
})
|
||||
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
|
||||
-- Simulate touch began
|
||||
love.touch.getTouches = function() return {"touch1"} end
|
||||
love.touch.getTouches = function()
|
||||
return { "touch1" }
|
||||
end
|
||||
love.touch.getPosition = function(id)
|
||||
if id == "touch1" then return 100, 100 end
|
||||
if id == "touch1" then
|
||||
return 100, 100
|
||||
end
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
|
||||
-- First touch
|
||||
FlexLove.beginFrame()
|
||||
element._eventHandler:processTouchEvents()
|
||||
FlexLove.endFrame()
|
||||
|
||||
|
||||
-- End touch
|
||||
love.touch.getTouches = function() return {} end
|
||||
|
||||
love.touch.getTouches = function()
|
||||
return {}
|
||||
end
|
||||
|
||||
FlexLove.beginFrame()
|
||||
element._eventHandler:processTouchEvents()
|
||||
FlexLove.endFrame()
|
||||
|
||||
|
||||
-- Should have received touchpress and touchrelease events
|
||||
lu.assertEquals(#touchEvents, 2)
|
||||
lu.assertEquals(touchEvents[1].type, "touchpress")
|
||||
@@ -175,7 +191,7 @@ end
|
||||
-- Test: EventHandler tracks multiple simultaneous touches
|
||||
function TestTouchEvents:testEventHandler_MultiTouch()
|
||||
FlexLove.beginFrame()
|
||||
|
||||
|
||||
local touchEvents = {}
|
||||
local element = FlexLove.new({
|
||||
width = 200,
|
||||
@@ -184,26 +200,32 @@ function TestTouchEvents:testEventHandler_MultiTouch()
|
||||
table.insert(touchEvents, event)
|
||||
end,
|
||||
})
|
||||
|
||||
|
||||
FlexLove.endFrame()
|
||||
|
||||
|
||||
-- Simulate two touches
|
||||
love.touch.getTouches = function() return {"touch1", "touch2"} end
|
||||
love.touch.getTouches = function()
|
||||
return { "touch1", "touch2" }
|
||||
end
|
||||
love.touch.getPosition = function(id)
|
||||
if id == "touch1" then return 50, 50 end
|
||||
if id == "touch2" then return 150, 150 end
|
||||
if id == "touch1" then
|
||||
return 50, 50
|
||||
end
|
||||
if id == "touch2" then
|
||||
return 150, 150
|
||||
end
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
|
||||
FlexLove.beginFrame()
|
||||
element._eventHandler:processTouchEvents()
|
||||
FlexLove.endFrame()
|
||||
|
||||
|
||||
-- Should have received two touchpress events
|
||||
lu.assertEquals(#touchEvents, 2)
|
||||
lu.assertEquals(touchEvents[1].type, "touchpress")
|
||||
lu.assertEquals(touchEvents[2].type, "touchpress")
|
||||
|
||||
|
||||
-- Different touch IDs
|
||||
lu.assertNotEquals(touchEvents[1].touchId, touchEvents[2].touchId)
|
||||
end
|
||||
@@ -213,24 +235,26 @@ function TestTouchEvents:testGestureRecognizer_Tap()
|
||||
local GestureRecognizer = package.loaded["modules.GestureRecognizer"]
|
||||
local InputEvent = package.loaded["modules.InputEvent"]
|
||||
local utils = package.loaded["modules.utils"]
|
||||
|
||||
|
||||
local recognizer = GestureRecognizer.new({}, {
|
||||
InputEvent = InputEvent,
|
||||
utils = utils,
|
||||
})
|
||||
|
||||
|
||||
-- Simulate tap (press and quick release)
|
||||
local touchId = "touch1"
|
||||
local pressEvent = InputEvent.fromTouch(touchId, 100, 100, "began", 1.0)
|
||||
local releaseEvent = InputEvent.fromTouch(touchId, 102, 102, "ended", 1.0)
|
||||
|
||||
|
||||
recognizer:processTouchEvent(pressEvent)
|
||||
local gesture = recognizer:processTouchEvent(releaseEvent)
|
||||
|
||||
-- Note: The gesture detection returns from internal methods,
|
||||
|
||||
-- Note: The gesture detection returns from internal methods,
|
||||
-- needs to be captured from the event processing
|
||||
-- This is a basic structural test
|
||||
lu.assertNotNil(recognizer)
|
||||
end
|
||||
|
||||
os.exit(lu.LuaUnit.run())
|
||||
if not _G.RUNNING_ALL_TESTS then
|
||||
os.exit(lu.LuaUnit.run())
|
||||
end
|
||||
|
||||
@@ -19,29 +19,37 @@ local luaunit = require("testing.luaunit")
|
||||
local testFiles = {
|
||||
"testing/__tests__/animation_test.lua",
|
||||
"testing/__tests__/animation_properties_test.lua",
|
||||
"testing/__tests__/transform_test.lua",
|
||||
"testing/__tests__/blur_test.lua",
|
||||
"testing/__tests__/color_validation_test.lua",
|
||||
"testing/__tests__/critical_failures_test.lua",
|
||||
"testing/__tests__/easing_test.lua",
|
||||
"testing/__tests__/element_test.lua",
|
||||
"testing/__tests__/error_handler_test.lua",
|
||||
"testing/__tests__/event_handler_test.lua",
|
||||
"testing/__tests__/flexlove_test.lua",
|
||||
"testing/__tests__/font_cache_test.lua",
|
||||
"testing/__tests__/grid_test.lua",
|
||||
"testing/__tests__/image_cache_test.lua",
|
||||
"testing/__tests__/image_renderer_test.lua",
|
||||
"testing/__tests__/image_scaler_test.lua",
|
||||
"testing/__tests__/image_tiling_test.lua",
|
||||
"testing/__tests__/input_event_test.lua",
|
||||
"testing/__tests__/keyframe_animation_test.lua",
|
||||
"testing/__tests__/layout_edge_cases_test.lua",
|
||||
"testing/__tests__/layout_engine_test.lua",
|
||||
"testing/__tests__/ninepatch_parser_test.lua",
|
||||
"testing/__tests__/ninepatch_test.lua",
|
||||
"testing/__tests__/overflow_test.lua",
|
||||
"testing/__tests__/path_validation_test.lua",
|
||||
"testing/__tests__/performance_instrumentation_test.lua",
|
||||
"testing/__tests__/performance_warnings_test.lua",
|
||||
"testing/__tests__/renderer_test.lua",
|
||||
"testing/__tests__/roundedrect_test.lua",
|
||||
"testing/__tests__/sanitization_test.lua",
|
||||
"testing/__tests__/text_editor_test.lua",
|
||||
"testing/__tests__/theme_test.lua",
|
||||
"testing/__tests__/touch_events_test.lua",
|
||||
"testing/__tests__/transform_test.lua",
|
||||
"testing/__tests__/units_test.lua",
|
||||
"testing/__tests__/utils_test.lua",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user